diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index bf89e2c171..2cceaba497 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -46,62 +46,59 @@ import SwiftUI struct ContentView: View { - - var controller: controller + @EnvironmentObject var chatModel: ChatModel - init(controller: controller) { - self.controller = controller - } +// var chatStore: chat_store +// private let controller: chat_controller + +// init(chatStore: chat_store) { +// self.chatStore = chatStore +// } - @State private var logbuffer = [String]() - @State private var chatcmd: String = "" - @State private var chatlog: String = "" - @FocusState private var focused: Bool - - func addLine(line: String) { - print(line) - logbuffer.append(line) - if(logbuffer.count > 50) { _ = logbuffer.dropFirst() } - chatlog = logbuffer.joined(separator: "\n") - } +// @State private var logbuffer = [String]() +// @State private var chatcmd: String = "" +// @State private var chatlog: String = "" +// @FocusState private var focused: Bool +// +// func addLine(line: String) { +// print(line) +// logbuffer.append(line) +// if(logbuffer.count > 50) { _ = logbuffer.dropFirst() } +// chatlog = logbuffer.joined(separator: "\n") +// } var body: some View { - - DispatchQueue.global().async { - while(true) { - let msg = String.init(cString: chat_recv_msg(controller)) - - DispatchQueue.main.async { - addLine(line: msg) - } - } + if let user = chatModel.currentUser { + ChatListView(user: user) + .onAppear { chatSendCmd(chatModel, .apiGetChats) } + } else { + WelcomeView() } - - return VStack { - ScrollView { - VStack(alignment: .leading) { - HStack { Spacer() } - Text(chatlog) - .lineLimit(nil) - .font(.system(.body, design: .monospaced)) - } - .frame(maxWidth: .infinity) - } - TextField("Chat command", text: $chatcmd) - .focused($focused) - .onSubmit { - print(chatcmd) - var cCmd = chatcmd.cString(using: .utf8)! - print(String.init(cString: chat_send_cmd(controller, &cCmd))) - } - .textInputAutocapitalization(.never) - .disableAutocorrection(true) - .padding() - } - } - +// return VStack { +// ScrollView { +// VStack(alignment: .leading) { +// HStack { Spacer() } +// Text(chatlog) +// .lineLimit(nil) +// .font(.system(.body, design: .monospaced)) +// } +// .frame(maxWidth: .infinity) +// } +// +// TextField("Chat command", text: $chatcmd) +// .focused($focused) +// .onSubmit { +// print(chatcmd) +// var cCmd = chatcmd.cString(using: .utf8)! +// print(String.init(cString: chat_send_cmd(controller, &cCmd))) +// } +// .textInputAutocapitalization(.never) +// .disableAutocorrection(true) +// .padding() +// } + } } diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index df83f32b4a..766ac78cc2 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -8,11 +8,13 @@ import Foundation import Combine -import SwiftUI final class ChatModel: ObservableObject { @Published var currentUser: User? - @Published var channels: [ChatChannel] = [] + @Published var chats: Dictionary = [:] + @Published var chatPreviews: [ChatPreview] = [] + @Published var chatItems: [ChatItem] = [] + @Published var apiResponses: [APIResponse] = [] } struct User: Codable { @@ -32,22 +34,76 @@ struct Profile: Codable { var fullName: String } -enum ChatChannel { - case contact(ContactInfo, [ChatMessage]) - case group(GroupInfo, [ChatMessage]) +struct ChatPreview: Identifiable, Codable { + var chatInfo: ChatInfo + var lastChatItem: ChatItem? + + var id: String { + get { chatInfo.id } + } } -struct ContactInfo: Codable { +enum ChatInfo: Identifiable, Codable { + case direct(contact: Contact) +// case group() + + var displayName: String { + get { + switch self { + case let .direct(contact): return "@\(contact.localDisplayName)" +// case let .group(groupInfo, _): return "#\(groupInfo.localDisplayName)" + } + } + } + + var id: String { + get { + switch self { + case let .direct(contact): return "@\(contact.contactId)" +// case let .group(contact): return group.id + } + } + } + + var apiType: String { + get { + switch self { + case .direct(_): return "direct" +// case let .group(_): return "group" + } + } + } + + var apiId: Int64 { + get { + switch self { + case let .direct(contact): return contact.contactId +// case let .group(contact): return group.id + } + } + } +} + +class Chat: Codable { + var chatInfo: ChatInfo + var chatItems: [ChatItem] +} + +struct Contact: Identifiable, Codable { var contactId: Int64 var localDisplayName: ContactName var profile: Profile var viaGroup: Int64? + + var id: String { get { "@\(contactId)" } } } -struct GroupInfo: Codable { +struct GroupInfo: Identifiable, Codable { var groupId: Int64 var localDisplayName: GroupName var groupProfile: GroupProfile + + var id: String { get { "#\(groupId)" } } } struct GroupProfile: Codable { @@ -55,13 +111,76 @@ struct GroupProfile: Codable { var fullName: String } -struct ChatMessage { - var from: ContactInfo? - var ts: Date - var content: MsgContent +struct GroupMember: Codable { + } -enum MsgContent { - case text(String) - case unknown +struct ChatItem: Identifiable, Codable { + var chatDir: CIDirection + var meta: CIMeta + var content: CIContent + + var id: Int64 { get { meta.itemId } } } + +enum CIDirection: Codable { + case directSnd + case directRcv + case groupSnd + case groupRcv(GroupMember) +} + +struct CIMeta: Codable { + var itemId: Int64 + var itemTs: Date + var itemText: String + var createdAt: Date +} + +enum CIContent: Codable { + case sndMsgContent(msgContent: MsgContent) + case rcvMsgContent(msgContent: MsgContent) + // files etc. + + var text: String { + get { + switch self { + case let .sndMsgContent(mc): return mc.string + case let .rcvMsgContent(mc): return mc.string + } + } + } +} + +enum MsgContent: Codable { + case text(String) + case unknown(type: String, text: String, json: String) + case invalid(json: String) + + init(from: Decoder) throws { + self = .invalid(json: "") + } + + var string: String { + get { + switch self { + case let .text(str): return str + case .unknown: return "unknown" + case .invalid: return "invalid" + } + } + } +} + +//func parseMsgContent(_ mc: SomeMsgContent) -> MsgContent { +// if let type = mc["type"] as? String { +// let text_ = mc["text"] as? String +// switch type { +// case "text": +// if let text = text_ { return .text(text) } +// case let t: +// return .unknown(type: t, text: text_ ?? "unknown item", json: prettyJSON(mc) ?? "error") +// } +// } +// return .invalid(json: prettyJSON(mc) ?? "error") +//} diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift new file mode 100644 index 0000000000..b2dd350fb0 --- /dev/null +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -0,0 +1,220 @@ +// +// ChatAPI.swift +// SimpleX +// +// Created by Evgeny Poberezkin on 27/01/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import Foundation + +private var chatStore: chat_store? +private var chatController: chat_ctrl? +private let jsonDecoder = JSONDecoder() +private let jsonEncoder = JSONEncoder() + +enum ChatCommand { + case apiGetChats + case apiGetChatItems(type: String, id: Int64) + case string(String) + case help + + var cmdString: String { + get { + switch self { + case .apiGetChats: + return "/api/v1/chats" + case let .apiGetChatItems(type, id): + return "/api/v1/chat/items/\(type)/\(id)" + case let .string(str): + return str + case .help: return "/help" + } + } + } +} + +struct APIResponse: Identifiable { + var resp: ChatResponse + var id: Int64 +} + +struct APIResponseJSON: Decodable { + var resp: ChatResponse +} + +enum ChatResponse: Codable { + case response(type: String, json: String) + case apiChats(chats: [ChatPreview]) + case apiDirectChat(chat: Chat) // direct/ or group/, same as ChatPreview.id +// case chatHelp(String) +// case newSentInvitation + case contactConnected(contact: Contact) + + var responseType: String { + get { + switch self { + case let .response(type, _): return "* \(type)" + case .apiChats(_): return "apiChats" + case .apiDirectChat(_): return "apiDirectChat" + case .contactConnected(_): return "contactConnected" + } + } + } + + var details: String { + get { + switch self { + case let .response(_, json): return json + case let .apiChats(chats): return String(describing: chats) + case let .apiDirectChat(chat): return String(describing: chat) + case let .contactConnected(contact): return String(describing: contact) + } + } + } +} + +func chatGetUser() -> User? { + let store = getStore() + print("chatGetUser") + let r: UserResponse? = decodeCJSON(chat_get_user(store)) + let user = r?.user + if user != nil { initChatCtrl(store) } + print("user", user as Any) + return user +} + +func chatCreateUser(_ p: Profile) -> User? { + let store = getStore() + print("chatCreateUser") + var str = encodeCJSON(p) + chat_create_user(store, &str) + let user = chatGetUser() + if user != nil { initChatCtrl(store) } + print("user", user as Any) + return user +} + +func chatSendCmd(_ chatModel: ChatModel, _ cmd: ChatCommand) { + var c = cmd.cmdString.cString(using: .utf8)! + processAPIResponse(chatModel, + apiResponse( + chat_send_cmd(getChatCtrl(), &c)!)) +} + +func chatRecvMsg(_ chatModel: ChatModel) { + processAPIResponse(chatModel, + apiResponse( + chat_recv_msg(getChatCtrl())!)) +} + +private func processAPIResponse(_ chatModel: ChatModel, _ res: APIResponse?) { + if let r = res { + DispatchQueue.main.async { + chatModel.apiResponses.append(r) + switch r.resp { + case let .apiChats(chats): + chatModel.chatPreviews = chats + case let .apiDirectChat(chat): + chatModel.chats[chat.chatInfo.id] = chat + case let .contactConnected(contact): + chatModel.chatPreviews.insert( + ChatPreview(chatInfo: .direct(contact: contact)), + at: 0 + ) + default: return + +// case let .response(type, _): +// chatModel.chatItems.append(ChatItem( +// ts: Date.now, +// content: .text(type) +// )) + } + } + } +} + +private struct UserResponse: Decodable { + var user: User? + var error: String? +} + +private var respId: Int64 = 0 + +private func apiResponse(_ cjson: UnsafePointer) -> APIResponse? { + let s = String.init(cString: cjson) + print("apiResponse", s) + 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(APIResponseJSON.self, from: d) + return APIResponse(resp: r.resp, id: respId) + } catch { + print (error) + } + + 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) + } + respId += 1 + return APIResponse( + resp: ChatResponse.response(type: type ?? "invalid", json: json ?? s), + id: respId + ) +} + +func prettyJSON(_ obj: NSDictionary) -> String? { + if let d = try? JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted) { + return String(decoding: d, as: UTF8.self) + } + return nil +} + +private func getStore() -> chat_store { + if let store = chatStore { return store } + let dataDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.path + "/mobile_v1" + var cstr = dataDir.cString(using: .utf8)! + chatStore = chat_init_store(&cstr) + return chatStore! +} + +private func initChatCtrl(_ store: chat_store) { + if chatController == nil { + chatController = chat_start(store) + } +} + +private func getChatCtrl() -> chat_ctrl { + if let controller = chatController { return controller } + fatalError("Chat controller was not started!") +} + +private func decodeCJSON(_ cjson: UnsafePointer) -> T? { + let s = String.init(cString: cjson) + print("decodeCJSON", s) + let d = s.data(using: .utf8)! +// let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson)) +// let d = Data.init(bytesNoCopy: p, count: strlen(cjson), deallocator: .free) + return try? jsonDecoder.decode(T.self, from: d) +} + +private func getJSONObject(_ cjson: UnsafePointer) -> NSDictionary? { + let s = String.init(cString: cjson) + let d = s.data(using: .utf8)! + return try? JSONSerialization.jsonObject(with: d) as? NSDictionary +} + +private func encodeCJSON(_ value: T) -> [CChar] { + let data = try! jsonEncoder.encode(value) + let str = String(decoding: data, as: UTF8.self) + print("encodeCJSON", str) + return str.cString(using: .utf8)! +} diff --git a/apps/ios/Shared/MyPlayground.playground/Contents.swift b/apps/ios/Shared/MyPlayground.playground/Contents.swift new file mode 100644 index 0000000000..b81ba746c3 --- /dev/null +++ b/apps/ios/Shared/MyPlayground.playground/Contents.swift @@ -0,0 +1,54 @@ +import Foundation + +var greeting = "Hello, playground" + +let jsonEncoder = JSONEncoder() + +let ct = Contact( + contactId: 123, + localDisplayName: "ep", + profile: Profile(displayName: "ep", fullName: "") +) + +//let data = try! jsonEncoder.encode(ChatResponse.contactConnected(contact: ct)) + +//print(String(decoding: data, as: UTF8.self)) + +//var str = """ +//{"resp":{"apiChats":{"chats": +//[{"chatItem":null,"chatInfo":{"direct":{"contact":{"contactId":2,"profile": +//{"displayName":"simplex","fullName":""},"activeConn": +//{"connLevel":0,"entityId":2,"connType":"contact","connId":1 +//,"agentConnId":"QTRteFhTR1dWQnpQZHE3NQ==","createdAt":"2022-01-27T19:43:44.015562Z","connStatus":"ready"},"localDisplayName":"simplex"}}}}, +//{"chatItem":null,"chatInfo":{"direct":{"contact":{"contactId":3,"profile": +//{"displayName":"ep","fullName":"Evgeny"},"activeConn": +//{"connLevel":0,"entityId":3,"connType":"contact","connId":2 +//,"agentConnId":"cTdFNkprSHhZZmZhdWFQVg==","createdAt":"2022-01-27T19:47:08.891646Z","connStatus":"ready"},"localDisplayName":"ep"}}}}]}}} +//""" + +//var str = """ +//[{"chatItem":null,"chatInfo":{"direct":{"contact":{"contactId":2,"profile": +//{"displayName":"simplex","fullName":""},"activeConn": +//{"connLevel":0,"entityId":2,"connType":"contact","connId":1 +//,"agentConnId":"QTRteFhTR1dWQnpQZHE3NQ==","createdAt":"2022-01-27T19:43:44.015562Z","connStatus":"ready"},"localDisplayName":"simplex"}}}}, +//{"chatItem":null,"chatInfo":{"direct":{"contact":{"contactId":3,"profile": +//{"displayName":"ep","fullName":"Evgeny"},"activeConn": +//{"connLevel":0,"entityId":3,"connType":"contact","connId":2 +//,"agentConnId":"cTdFNkprSHhZZmZhdWFQVg==","createdAt":"2022-01-27T19:47:08.891646Z","connStatus":"ready"},"localDisplayName":"ep"}}}}] +//""" +// +//let data = str.data(using: .utf8)! + +let jsonDecoder = JSONDecoder() + +//let r: [ChatPreview] = try! jsonDecoder.decode([ChatPreview].self, from: data) +// +//print(r) + +struct Test: Decodable { + var name: String + var id: Int64 = 0 +} + +jsonDecoder.decode(Test.self, from: "{\"name\":\"hello\",\"id\":1}".data(using: .utf8)!) + diff --git a/apps/ios/Shared/MyPlayground.playground/contents.xcplayground b/apps/ios/Shared/MyPlayground.playground/contents.xcplayground new file mode 100644 index 0000000000..cf026f2286 --- /dev/null +++ b/apps/ios/Shared/MyPlayground.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/apps/ios/Shared/MyPlayground.playground/timeline.xctimeline b/apps/ios/Shared/MyPlayground.playground/timeline.xctimeline new file mode 100644 index 0000000000..62084a5f42 --- /dev/null +++ b/apps/ios/Shared/MyPlayground.playground/timeline.xctimeline @@ -0,0 +1,11 @@ + + + + + + + diff --git a/apps/ios/Shared/SimpleX (iOS)-Bridging-Header.h b/apps/ios/Shared/SimpleX (iOS)-Bridging-Header.h index 4b5b9d876b..e9f8948778 100644 --- a/apps/ios/Shared/SimpleX (iOS)-Bridging-Header.h +++ b/apps/ios/Shared/SimpleX (iOS)-Bridging-Header.h @@ -2,14 +2,14 @@ // Use this file to import your target's public headers that you would like to expose to Swift. // -extern void hs_init(int argc, char ** argv[]); +extern void hs_init(int argc, char **argv[]); typedef void* chat_store; -typedef void* controller; +typedef void* chat_ctrl; -extern chat_store chat_init_store(char * path); +extern chat_store chat_init_store(char *path); extern char *chat_get_user(chat_store store); extern char *chat_create_user(chat_store store, char *data); -extern controller chat_start(chat_store store); -extern char *chat_send_cmd(controller ctl, char *cmd); -extern char *chat_recv_msg(controller ctl); +extern chat_ctrl chat_start(chat_store store); +extern char *chat_send_cmd(chat_ctrl ctl, char *cmd); +extern char *chat_recv_msg(chat_ctrl ctl); diff --git a/apps/ios/Shared/SimpleX (macOS)-Bridging-Header.h b/apps/ios/Shared/SimpleX (macOS)-Bridging-Header.h index 4b5b9d876b..62f2ba6626 100644 --- a/apps/ios/Shared/SimpleX (macOS)-Bridging-Header.h +++ b/apps/ios/Shared/SimpleX (macOS)-Bridging-Header.h @@ -5,11 +5,11 @@ extern void hs_init(int argc, char ** argv[]); typedef void* chat_store; -typedef void* controller; +typedef void* chat_ctrl; extern chat_store chat_init_store(char * path); extern char *chat_get_user(chat_store store); extern char *chat_create_user(chat_store store, char *data); -extern controller chat_start(chat_store store); -extern char *chat_send_cmd(controller ctl, char *cmd); -extern char *chat_recv_msg(controller ctl); +extern chat_ctrl chat_start(chat_store store); +extern char *chat_send_cmd(chat_ctrl ctl, char *cmd); +extern char *chat_recv_msg(chat_ctrl ctl); diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 68fd67c2d8..5af995644a 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -9,26 +9,33 @@ import SwiftUI @main struct SimpleXApp: App { - private let controller: controller + @StateObject private var chatModel = ChatModel() +// let store: chat_store + init() { hs_init(0, nil) - - let dataDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.path + "/mobile_v1" - var cstr = dataDir.cString(using: .utf8)! - let store = chat_init_store(&cstr) - let user = String.init(cString: chat_get_user(store)) - print(user) - if user == "{}" { - var data = "{ \"displayName\": \"test\", \"fullName\": \"ios test\" }".cString(using: .utf8)! - chat_create_user(store, &data) - } - controller = chat_start(store) - var cmd = "/help".cString(using: .utf8)! - print(String.init(cString: chat_send_cmd(controller, &cmd))) +// let dataDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.path + "/mobile_v1" +// var cstr = dataDir.cString(using: .utf8)! +// store = chat_init_store(&cstr) +// let user = String.init(cString: chat_get_user(store)) +// print(user) +// if user != "{}" { +// chatModel.currentUser = parseJSON(user) +// var data = "{ \"displayName\": \"test\", \"fullName\": \"ios test\" }".cString(using: .utf8)! +// chat_create_user(store, &data) +// } +// controller = chat_start(store) +// var cmd = "/help".cString(using: .utf8)! +// print(String.init(cString: chat_send_cmd(controller, &cmd))) } + var body: some Scene { WindowGroup { - ContentView(controller: controller) + ContentView() + .environmentObject(chatModel) + .onAppear() { + chatModel.currentUser = chatGetUser() + } } } } diff --git a/apps/ios/Shared/Views/ChatListView.swift b/apps/ios/Shared/Views/ChatListView.swift new file mode 100644 index 0000000000..d77d68a06d --- /dev/null +++ b/apps/ios/Shared/Views/ChatListView.swift @@ -0,0 +1,56 @@ +// +// ChatListView.swift +// SimpleX +// +// Created by Evgeny Poberezkin on 27/01/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct ChatListView: View { + @EnvironmentObject var chatModel: ChatModel + var user: User + + var body: some View { + DispatchQueue.global().async { + while(true) { chatRecvMsg(chatModel) } + } + + return VStack { +// if chatModel.chats.isEmpty { + VStack { + Text("Hello chat") + Text("Active user: \(user.localDisplayName) (\(user.profile.fullName))") + } +// } + NavigationView { + List { + NavigationLink { + TerminalView() + } label: { + Text("Terminal") + } + + ForEach(chatModel.chatPreviews) { chatPreview in + NavigationLink { + ChatView(chatInfo: chatPreview.chatInfo) + .onAppear { + chatSendCmd(chatModel, .apiGetChatItems(type: "direct", id: chatPreview.chatInfo.apiId)) + } + } label: { + ChatPreviewView(chatPreview: chatPreview) + } + } + } + } + .navigationViewStyle(.stack) + } + } +} + +//struct ChatListView_Previews: PreviewProvider { +// static var previews: some View { +// ChatListView() +// } +//} diff --git a/apps/ios/Shared/Views/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatPreviewView.swift new file mode 100644 index 0000000000..4b05988ab2 --- /dev/null +++ b/apps/ios/Shared/Views/ChatPreviewView.swift @@ -0,0 +1,33 @@ +// +// ChatPreviewView.swift +// SimpleX +// +// Created by Evgeny Poberezkin on 28/01/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct ChatPreviewView: View { + var chatPreview: ChatPreview + + var body: some View { + Text(chatPreview.chatInfo.displayName) + } +} + +struct ChatPreviewView_Previews: PreviewProvider { + static var previews: some View { + ChatPreviewView(chatPreview: ChatPreview( + chatInfo: .direct(contact: Contact( + contactId: 123, + localDisplayName: "ep", + profile: Profile( + displayName: "ep", + fullName: "Ep" + ), + viaGroup: nil + )) + )) + } +} diff --git a/apps/ios/Shared/Views/ChatView.swift b/apps/ios/Shared/Views/ChatView.swift new file mode 100644 index 0000000000..eb3b89ee19 --- /dev/null +++ b/apps/ios/Shared/Views/ChatView.swift @@ -0,0 +1,37 @@ +// +// ChatView.swift +// SimpleX +// +// Created by Evgeny Poberezkin on 27/01/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct ChatView: View { + @EnvironmentObject var chatModel: ChatModel + var chatInfo: ChatInfo + var body: some View { + VStack { + if let chat: Chat = chatModel.chats[chatInfo.id] { + VStack { + ScrollView { + LazyVStack { + ForEach(chat.chatItems) { chatItem in + Text(chatItem.content.text) + } + } + } + } + } else { + Text("unexpected: chat not found...") + } + } + } +} + +//struct ChatView_Previews: PreviewProvider { +// static var previews: some View { +// ChatView() +// } +//} diff --git a/apps/ios/Shared/MessageView.swift b/apps/ios/Shared/Views/MessageView.swift similarity index 100% rename from apps/ios/Shared/MessageView.swift rename to apps/ios/Shared/Views/MessageView.swift diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/TerminalView.swift new file mode 100644 index 0000000000..11521fb680 --- /dev/null +++ b/apps/ios/Shared/Views/TerminalView.swift @@ -0,0 +1,73 @@ +// +// TerminalView.swift +// SimpleX +// +// Created by Evgeny Poberezkin on 27/01/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct TerminalView: View { + @EnvironmentObject var chatModel: ChatModel + @State var command: String = "" + @State var inProgress: Bool = false + + var body: some View { + VStack { + ScrollView { + LazyVStack { + ForEach(chatModel.apiResponses) { r in + NavigationLink { + ScrollView { + Text(r.resp.details) + } + } label: { + Text(r.resp.responseType) + .frame(width: 360, height: 30, alignment: .leading) + } + } + } + } + .navigationViewStyle(.stack) + + Spacer() + + HStack { + TextField("Message...", text: $command) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .frame(minHeight: 30) + Button(action: sendMessage) { + Text("Send") + }.disabled(command.isEmpty) + } + .frame(minHeight: 30) + .padding() + } + } + + func sendMessage() { + DispatchQueue.global().async { + let cmd: String = self.$command.wrappedValue + inProgress = true + command = "" + chatSendCmd(chatModel, ChatCommand.string(cmd)) + inProgress = false + } + } +} + +struct TerminalView_Previews: PreviewProvider { + static var previews: some View { + let chatModel = ChatModel() + chatModel.apiResponses = [ + APIResponse(resp: ChatResponse.response(type: "contactSubscribed", json: "{}"), id: 1), + APIResponse(resp: ChatResponse.response(type: "newChatItem", json: "{}"), id: 2) + ] + return NavigationView { + TerminalView() + .environmentObject(chatModel) + } + + } +} diff --git a/apps/ios/Shared/Views/UserView.swift b/apps/ios/Shared/Views/UserView.swift new file mode 100644 index 0000000000..61e3f5e7ed --- /dev/null +++ b/apps/ios/Shared/Views/UserView.swift @@ -0,0 +1,83 @@ +// +// UserView.swift +// SimpleX +// +// Created by Evgeny Poberezkin on 27/01/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct UserView: View { + @EnvironmentObject var chatModel: ChatModel + var user: User + +// private let controller: chat_ctrl +// +// var chatStore: chat_store + +// @State private var logbuffer = [String]() +// @State private var chatcmd: String = "" +// @State private var chatlog: String = "" +// @FocusState private var focused: Bool + +// func addLine(line: String) { +// print(line) +// logbuffer.append(line) +// if(logbuffer.count > 50) { _ = logbuffer.dropFirst() } +// chatlog = logbuffer.joined(separator: "\n") +// } + + var body: some View { + if chatModel.userChats.isEmpty { + VStack { + Text("Hello chat") + Text("Active user: \(user.localDisplayName) (\(user.profile.fullName))") + } + } else { + ChatList() + } + + + +// DispatchQueue.global().async { +// while(true) { +// let msg = String.init(cString: chat_recv_msg(controller)) +// +// DispatchQueue.main.async { +// addLine(line: msg) +// } +// } +// } + +// return VStack { +// ScrollView { +// VStack(alignment: .leading) { +// HStack { Spacer() } +// Text(chatlog) +// .lineLimit(nil) +// .font(.system(.body, design: .monospaced)) +// } +// .frame(maxWidth: .infinity) +// } +// +// TextField("Chat command", text: $chatcmd) +// .focused($focused) +// .onSubmit { +// print(chatcmd) +// var cCmd = chatcmd.cString(using: .utf8)! +//// print(String.init(cString: chat_send_cmd(controller, &cCmd))) +// } +// .textInputAutocapitalization(.never) +// .disableAutocorrection(true) +// .padding() +// } + } + +} + +//struct UserView_Previews: PreviewProvider { +// static var previews: some View { +// UserView() +// } +//} diff --git a/apps/ios/Shared/ProfileView.swift b/apps/ios/Shared/Views/WelcomeView.swift similarity index 59% rename from apps/ios/Shared/ProfileView.swift rename to apps/ios/Shared/Views/WelcomeView.swift index a27f1ec9e2..7db90b11b5 100644 --- a/apps/ios/Shared/ProfileView.swift +++ b/apps/ios/Shared/Views/WelcomeView.swift @@ -1,5 +1,5 @@ // -// ProfileView.swift +// WelcomeView.swift // SimpleX // // Created by Evgeny Poberezkin on 18/01/2022. @@ -7,9 +7,11 @@ import SwiftUI -struct ProfileView: View { +struct WelcomeView: View { + @EnvironmentObject var chatModel: ChatModel @State var displayName: String = "" @State var fullName: String = "" + var body: some View { VStack(alignment: .leading) { Text("Create profile") @@ -20,13 +22,23 @@ struct ProfileView: View { TextField("Display name", text: $displayName) .padding(.bottom) TextField("Full name (optional)", text: $fullName) + .padding(.bottom) + Button("Create") { + let profile = Profile( + displayName: displayName, + fullName: fullName + ) + if let user = chatCreateUser(profile) { + chatModel.currentUser = user + } + } } .padding() } } -struct ProfileView_Previews: PreviewProvider { +struct WelcomeView_Previews: PreviewProvider { static var previews: some View { - ProfileView() + WelcomeView() } } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 9ccab2ff65..085246786a 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; }; + 5C063D2827A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; }; 5C1AEB82279F4A6400247F08 /* libHSsimplex-chat-1.0.2-FYXW0143sf06WiRQx9DgCD.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C1AEB7D279F4A6400247F08 /* libHSsimplex-chat-1.0.2-FYXW0143sf06WiRQx9DgCD.a */; }; 5C1AEB83279F4A6400247F08 /* libHSsimplex-chat-1.0.2-FYXW0143sf06WiRQx9DgCD.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C1AEB7D279F4A6400247F08 /* libHSsimplex-chat-1.0.2-FYXW0143sf06WiRQx9DgCD.a */; }; 5C1AEB84279F4A6400247F08 /* libHSsimplex-chat-1.0.2-FYXW0143sf06WiRQx9DgCD-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C1AEB7E279F4A6400247F08 /* libHSsimplex-chat-1.0.2-FYXW0143sf06WiRQx9DgCD-ghc8.10.7.a */; }; @@ -17,6 +19,14 @@ 5C1AEB89279F4A6400247F08 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C1AEB80279F4A6400247F08 /* libgmp.a */; }; 5C1AEB8A279F4A6400247F08 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C1AEB81279F4A6400247F08 /* libgmpxx.a */; }; 5C1AEB8B279F4A6400247F08 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C1AEB81279F4A6400247F08 /* libgmpxx.a */; }; + 5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; }; + 5C2E260827A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; }; + 5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260A27A30CFA00F70299 /* ChatListView.swift */; }; + 5C2E260C27A30CFA00F70299 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260A27A30CFA00F70299 /* ChatListView.swift */; }; + 5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260E27A30FDC00F70299 /* ChatView.swift */; }; + 5C2E261027A30FDC00F70299 /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260E27A30FDC00F70299 /* ChatView.swift */; }; + 5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E261127A30FEA00F70299 /* TerminalView.swift */; }; + 5C2E261327A30FEA00F70299 /* TerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E261127A30FEA00F70299 /* TerminalView.swift */; }; 5C764E80279C7276000C6508 /* dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E7F279C7276000C6508 /* dummy.m */; }; 5C764E81279C7276000C6508 /* dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E7F279C7276000C6508 /* dummy.m */; }; 5C764E82279C748B000C6508 /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C764E7B279C71D4000C6508 /* libiconv.tbd */; }; @@ -35,8 +45,8 @@ 5CA059EE279559F40002BEB4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059C4279559F40002BEB4 /* ContentView.swift */; }; 5CA059EF279559F40002BEB4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5CA059C5279559F40002BEB4 /* Assets.xcassets */; }; 5CA059F0279559F40002BEB4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5CA059C5279559F40002BEB4 /* Assets.xcassets */; }; - 5CA05A4C27974EB60002BEB4 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA05A4B27974EB60002BEB4 /* ProfileView.swift */; }; - 5CA05A4D27974EB60002BEB4 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA05A4B27974EB60002BEB4 /* ProfileView.swift */; }; + 5CA05A4C27974EB60002BEB4 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA05A4B27974EB60002BEB4 /* WelcomeView.swift */; }; + 5CA05A4D27974EB60002BEB4 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA05A4B27974EB60002BEB4 /* WelcomeView.swift */; }; 5CA05A4F279752D00002BEB4 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA05A4E279752D00002BEB4 /* MessageView.swift */; }; 5CA05A50279752D00002BEB4 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA05A4E279752D00002BEB4 /* MessageView.swift */; }; /* End PBXBuildFile section */ @@ -59,11 +69,17 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreviewView.swift; sourceTree = ""; }; 5C1AEB7D279F4A6400247F08 /* libHSsimplex-chat-1.0.2-FYXW0143sf06WiRQx9DgCD.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.0.2-FYXW0143sf06WiRQx9DgCD.a"; sourceTree = ""; }; 5C1AEB7E279F4A6400247F08 /* libHSsimplex-chat-1.0.2-FYXW0143sf06WiRQx9DgCD-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.0.2-FYXW0143sf06WiRQx9DgCD-ghc8.10.7.a"; sourceTree = ""; }; 5C1AEB7F279F4A6400247F08 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; 5C1AEB80279F4A6400247F08 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 5C1AEB81279F4A6400247F08 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXAPI.swift; sourceTree = ""; }; + 5C2E260927A2C63500F70299 /* MyPlayground.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = MyPlayground.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 5C2E260A27A30CFA00F70299 /* ChatListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = ""; }; + 5C2E260E27A30FDC00F70299 /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = ""; }; + 5C2E261127A30FEA00F70299 /* TerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalView.swift; sourceTree = ""; }; 5C764E7B279C71D4000C6508 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.2.sdk/usr/lib/libiconv.tbd; sourceTree = DEVELOPER_DIR; }; 5C764E7C279C71DB000C6508 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.2.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; }; 5C764E7D279C7275000C6508 /* SimpleX (iOS)-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SimpleX (iOS)-Bridging-Header.h"; sourceTree = ""; }; @@ -81,7 +97,7 @@ 5CA059E3279559F40002BEB4 /* Tests macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests macOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 5CA059E7279559F40002BEB4 /* Tests_macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_macOS.swift; sourceTree = ""; }; 5CA059E9279559F40002BEB4 /* Tests_macOSLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_macOSLaunchTests.swift; sourceTree = ""; }; - 5CA05A4B27974EB60002BEB4 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; + 5CA05A4B27974EB60002BEB4 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; 5CA05A4E279752D00002BEB4 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -131,6 +147,19 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 5C2E260D27A30E2400F70299 /* Views */ = { + isa = PBXGroup; + children = ( + 5CA05A4B27974EB60002BEB4 /* WelcomeView.swift */, + 5C2E260A27A30CFA00F70299 /* ChatListView.swift */, + 5CA05A4E279752D00002BEB4 /* MessageView.swift */, + 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */, + 5C2E260E27A30FDC00F70299 /* ChatView.swift */, + 5C2E261127A30FEA00F70299 /* TerminalView.swift */, + ); + path = Views; + sourceTree = ""; + }; 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( @@ -156,6 +185,7 @@ isa = PBXGroup; children = ( 5C764E88279CBCB3000C6508 /* ChatModel.swift */, + 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */, ); path = Model; sourceTree = ""; @@ -178,10 +208,10 @@ children = ( 5C764E87279CBC8E000C6508 /* Model */, 5CA059C3279559F40002BEB4 /* SimpleXApp.swift */, + 5C2E260927A2C63500F70299 /* MyPlayground.playground */, 5C764E7F279C7276000C6508 /* dummy.m */, 5CA059C4279559F40002BEB4 /* ContentView.swift */, - 5CA05A4B27974EB60002BEB4 /* ProfileView.swift */, - 5CA05A4E279752D00002BEB4 /* MessageView.swift */, + 5C2E260D27A30E2400F70299 /* Views */, 5CA059C5279559F40002BEB4 /* Assets.xcassets */, 5C764E7D279C7275000C6508 /* SimpleX (iOS)-Bridging-Header.h */, 5C764E7E279C7275000C6508 /* SimpleX (macOS)-Bridging-Header.h */, @@ -387,11 +417,16 @@ buildActionMask = 2147483647; files = ( 5C764E80279C7276000C6508 /* dummy.m in Sources */, + 5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */, + 5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */, 5CA05A4F279752D00002BEB4 /* MessageView.swift in Sources */, 5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */, - 5CA05A4C27974EB60002BEB4 /* ProfileView.swift in Sources */, + 5CA05A4C27974EB60002BEB4 /* WelcomeView.swift in Sources */, + 5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */, + 5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */, 5CA059EB279559F40002BEB4 /* SimpleXApp.swift in Sources */, 5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */, + 5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -400,11 +435,16 @@ buildActionMask = 2147483647; files = ( 5C764E81279C7276000C6508 /* dummy.m in Sources */, + 5C063D2827A4564100AEC577 /* ChatPreviewView.swift in Sources */, + 5C2E261327A30FEA00F70299 /* TerminalView.swift in Sources */, 5CA05A50279752D00002BEB4 /* MessageView.swift in Sources */, 5CA059EE279559F40002BEB4 /* ContentView.swift in Sources */, - 5CA05A4D27974EB60002BEB4 /* ProfileView.swift in Sources */, + 5CA05A4D27974EB60002BEB4 /* WelcomeView.swift in Sources */, + 5C2E261027A30FDC00F70299 /* ChatView.swift in Sources */, + 5C2E260C27A30CFA00F70299 /* ChatListView.swift in Sources */, 5CA059EC279559F40002BEB4 /* SimpleXApp.swift in Sources */, 5C764E8A279CBCB3000C6508 /* ChatModel.swift in Sources */, + 5C2E260827A2941F00F70299 /* SimpleXAPI.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };