From 711207743b51ec3486f40969530f36f5ff598787 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 1 Feb 2022 17:34:06 +0000 Subject: [PATCH] add support for user addresses (#246) * add support for user addresses * started processing contact requests * update command syntax * fix: make Profile Codable * accept/reject contact requests * update API, accept/reject contact requests --- apps/ios/Shared/Model/ChatModel.swift | 47 +++- apps/ios/Shared/Model/SimpleXAPI.swift | 124 +++++++++-- .../MyPlayground.playground/Contents.swift | 80 +------ .../timeline.xctimeline | 2 +- .../Views/ChatList/ChatListNavLink.swift | 207 ++++++++++++++++++ .../Shared/Views/ChatList/ChatListView.swift | 75 +++++++ .../Views/{ => ChatList}/TerminalView.swift | 1 + apps/ios/Shared/Views/ChatListView.swift | 119 ---------- apps/ios/Shared/Views/ChatPreviewView.swift | 7 + apps/ios/Shared/Views/ChatView.swift | 37 +++- .../Shared/Views/Helpers/ChatHeaderView.swift | 55 ----- .../Shared/Views/Helpers/ChatItemView.swift | 1 + .../Views/Helpers/ChatListNavLink.swift | 21 ++ .../Views/Helpers/ChatListToolbar.swift | 42 ++++ .../Shared/Views/Helpers/MessageView.swift | 34 --- .../Helpers/NewChat/AddContactView.swift | 4 +- .../Shared/Views/Helpers/NewChat/QRCode.swift | 1 + .../Helpers/UserSettings/SettingsButton.swift | 8 + .../Helpers/UserSettings/SettingsView.swift | 2 +- .../Helpers/UserSettings/UserAddress.swift | 58 ++++- ...ettingsProfile.swift => UserProfile.swift} | 8 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 48 ++-- 22 files changed, 635 insertions(+), 346 deletions(-) create mode 100644 apps/ios/Shared/Views/ChatList/ChatListNavLink.swift create mode 100644 apps/ios/Shared/Views/ChatList/ChatListView.swift rename apps/ios/Shared/Views/{ => ChatList}/TerminalView.swift (97%) delete mode 100644 apps/ios/Shared/Views/ChatListView.swift delete mode 100644 apps/ios/Shared/Views/Helpers/ChatHeaderView.swift create mode 100644 apps/ios/Shared/Views/Helpers/ChatListNavLink.swift create mode 100644 apps/ios/Shared/Views/Helpers/ChatListToolbar.swift delete mode 100644 apps/ios/Shared/Views/Helpers/MessageView.swift rename apps/ios/Shared/Views/Helpers/UserSettings/{SettingsProfile.swift => UserProfile.swift} (95%) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 5a866bc617..1d4ef420ed 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -16,9 +16,10 @@ final class ChatModel: ObservableObject { @Published var chatPreviews: [Chat] = [] @Published var chatItems: [ChatItem] = [] @Published var terminalItems: [TerminalItem] = [] + @Published var userAddress: String? } -class User: Codable { +class User: Decodable { var userId: Int64 var userContactId: Int64 var localDisplayName: ContactName @@ -59,17 +60,20 @@ let sampleProfile = Profile( enum ChatType: String { case direct = "@" case group = "#" + case contactRequest = "<@" } -enum ChatInfo: Identifiable, Codable { +enum ChatInfo: Identifiable, Decodable { case direct(contact: Contact) case group(groupInfo: GroupInfo) + case contactRequest(contactRequest: UserContactRequest) 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)" } } } @@ -77,8 +81,9 @@ enum ChatInfo: Identifiable, Codable { var id: String { get { switch self { - case let .direct(contact): return "@\(contact.contactId)" - case let .group(groupInfo): return "#\(groupInfo.groupId)" + case let .direct(contact): return contact.id + case let .group(groupInfo): return groupInfo.id + case let .contactRequest(contactRequest): return contactRequest.id } } } @@ -88,6 +93,7 @@ enum ChatInfo: Identifiable, Codable { switch self { case .direct: return .direct case .group: return .group + case .contactRequest: return .contactRequest } } } @@ -97,6 +103,7 @@ enum ChatInfo: Identifiable, Codable { switch self { case let .direct(contact): return contact.contactId case let .group(groupInfo): return groupInfo.groupId + case let .contactRequest(contactRequest): return contactRequest.contactRequestId } } } @@ -106,6 +113,8 @@ let sampleDirectChatInfo = ChatInfo.direct(contact: sampleContact) let sampleGroupChatInfo = ChatInfo.group(groupInfo: sampleGroupInfo) +let sampleContactRequestChatInfo = ChatInfo.contactRequest(contactRequest: sampleContactRequest) + class Chat: Decodable, Identifiable { var chatInfo: ChatInfo var chatItems: [ChatItem] @@ -118,22 +127,46 @@ class Chat: Decodable, Identifiable { var id: String { get { chatInfo.id } } } -struct Contact: Identifiable, Codable { +struct Contact: Identifiable, Decodable { var contactId: Int64 var localDisplayName: ContactName var profile: Profile + var activeConn: Connection var viaGroup: Int64? var id: String { get { "@\(contactId)" } } + + var connected: Bool { get { activeConn.connStatus == "ready" || activeConn.connStatus == "snd-ready" } } } let sampleContact = Contact( contactId: 1, localDisplayName: "alice", + profile: sampleProfile, + activeConn: sampleConnection +) + +struct Connection: Decodable { + var connStatus: String +} + +let sampleConnection = Connection(connStatus: "ready") + +struct UserContactRequest: Decodable { + var contactRequestId: Int64 + var localDisplayName: ContactName + var profile: Profile + + var id: String { get { "<@\(contactRequestId)" } } +} + +let sampleContactRequest = UserContactRequest( + contactRequestId: 1, + localDisplayName: "alice", profile: sampleProfile ) -struct GroupInfo: Identifiable, Codable { +struct GroupInfo: Identifiable, Decodable { var groupId: Int64 var localDisplayName: GroupName var groupProfile: GroupProfile @@ -157,7 +190,7 @@ let sampleGroupProfile = GroupProfile( fullName: "My Team" ) -struct GroupMember: Codable { +struct GroupMember: Decodable { } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index a91192a6f3..94aaac5506 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -22,25 +22,40 @@ enum ChatCommand { case connect(connReq: String) case apiDeleteChat(type: ChatType, id: Int64) case apiUpdateProfile(profile: Profile) + case createMyAddress + case deleteMyAddress + case showMyAddress + case apiAcceptContact(contactReqId: Int64) + case apiRejectContact(contactReqId: Int64) case string(String) var cmdString: String { get { switch self { case .apiGetChats: - return "/get chats" + return "/_get chats" case let .apiGetChat(type, id): - return "/get chat \(type.rawValue)\(id)" + return "/_get chat \(type.rawValue)\(id) count=500" case let .apiSendMessage(type, id, mc): - return "/send msg \(type.rawValue)\(id) \(mc.cmdString)" + return "/_send \(type.rawValue)\(id) \(mc.cmdString)" case .addContact: - return "/c" + return "/connect" case let .connect(connReq): - return "/c \(connReq)" + return "/connect \(connReq)" case let .apiDeleteChat(type, id): - return "/_del \(type.rawValue)\(id)" + return "/_delete \(type.rawValue)\(id)" case let .apiUpdateProfile(profile): - return "/p \(profile.displayName) \(profile.fullName)" + return "/profile \(profile.displayName) \(profile.fullName)" + 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 .string(str): return str } @@ -62,9 +77,15 @@ enum ChatResponse: Decodable, Error { case contactDeleted(contact: Contact) case userProfileNoChange case userProfileUpdated(fromProfile: Profile, toProfile: Profile) -// case newSentInvitation + case userContactLink(connReqContact: String) + case userContactLinkCreated(connReqContact: String) + case userContactLinkDeleted case contactConnected(contact: Contact) + case receivedContactRequest(contactRequest: UserContactRequest) + case acceptingContactRequest(contact: Contact) + case contactRequestRejected case newChatItem(chatItem: AChatItem) + case chatCmdError(chatError: ChatError) var responseType: String { get { @@ -78,8 +99,15 @@ enum ChatResponse: Decodable, Error { case .contactDeleted: return "contactDeleted" case .userProfileNoChange: return "userProfileNoChange" case .userProfileUpdated: return "userProfileNoChange" + case .userContactLink: return "userContactLink" + case .userContactLinkCreated: return "userContactLinkCreated" + case .userContactLinkDeleted: return "userContactLinkDeleted" case .contactConnected: return "contactConnected" + case .receivedContactRequest: return "receivedContactRequest" + case .acceptingContactRequest: return "acceptingContactRequest" + case .contactRequestRejected: return "contactRequestRejected" case .newChatItem: return "newChatItem" + case .chatCmdError: return "chatCmdError" } } } @@ -91,16 +119,25 @@ enum ChatResponse: Decodable, Error { case let .apiChats(chats): return String(describing: chats) case let .apiChat(chat): return String(describing: chat) case let .invitation(connReqInvitation): return connReqInvitation - case .sentConfirmation: return "sentConfirmation: no details" - case .sentInvitation: return "sentInvitation: no details" + case .sentConfirmation: return noDetails + case .sentInvitation: return noDetails case let .contactDeleted(contact): return String(describing: contact) - case .userProfileNoChange: return "userProfileNoChange: no details" + case .userProfileNoChange: return noDetails case let .userProfileUpdated(_, toProfile): return String(describing: toProfile) + 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 .receivedContactRequest(contactRequest): return String(describing: contactRequest) + case let .acceptingContactRequest(contact): return String(describing: contact) + case .contactRequestRejected: return noDetails case let .newChatItem(chatItem): return String(describing: chatItem) + case let .chatCmdError(chatError): return String(describing: chatError) } } } + + private var noDetails: String { get { "\(responseType): no details" } } } enum TerminalItem: Identifiable { @@ -219,20 +256,67 @@ func apiUpdateProfile(profile: Profile) throws -> Profile? { } } +func apiCreateUserAddress() throws -> String { + let r = try chatSendCmd(.createMyAddress) + if case let .userContactLinkCreated(connReq) = r { return connReq } + throw r +} + +func apiDeleteUserAddress() throws { + let r = try chatSendCmd(.deleteMyAddress) + if case .userContactLinkDeleted = r { return } + throw r +} + +func apiGetUserAddress() throws -> String? { + let r = try chatSendCmd(.showMyAddress) + switch r { + case let .userContactLink(connReq): + return connReq + case .chatCmdError(chatError: .errorStore(storeError: .userContactLinkNotFound)): + return nil + default: throw r + } +} + +func apiAcceptContactRequest(contactReqId: Int64) throws -> Contact { + let r = try chatSendCmd(.apiAcceptContact(contactReqId: contactReqId)) + if case let .acceptingContactRequest(contact) = r { return contact } + throw r +} + +func apiRejectContactRequest(contactReqId: Int64) throws { + let r = try chatSendCmd(.apiRejectContact(contactReqId: contactReqId)) + if case .contactRequestRejected = r { return } + throw r +} + func processReceivedMsg(_ chatModel: ChatModel, _ res: ChatResponse) { DispatchQueue.main.async { chatModel.terminalItems.append(.resp(Date.now, res)) switch res { case let .contactConnected(contact): - chatModel.chatPreviews.insert( - Chat(chatInfo: .direct(contact: contact), chatItems: []), - at: 0 - ) + if let chat = chatModel.chats[contact.id] { + chat.chatInfo = ChatInfo.direct(contact: contact) + } else { + let chat = Chat(chatInfo: ChatInfo.direct(contact: contact), chatItems: []) + chatModel.chats[contact.id] = chat + chatModel.chatPreviews.insert(chat, at: 0) + } + case let .receivedContactRequest(contactRequest): + let chat = Chat(chatInfo: ChatInfo.contactRequest(contactRequest: contactRequest), chatItems: []) + chatModel.chats[contactRequest.id] = chat + chatModel.chatPreviews.insert(chat, at: 0) case let .newChatItem(aChatItem): let ci = aChatItem.chatInfo let chat = chatModel.chats[ci.id] ?? Chat(chatInfo: ci, chatItems: []) chatModel.chats[ci.id] = chat chat.chatItems.append(aChatItem.chatItem) + if let cp = chatModel.chatPreviews.first(where: { $0.id == ci.id } ) { + cp.chatItems = [aChatItem.chatItem] + } else { + chatModel.chatPreviews.insert(Chat(chatInfo: ci, chatItems: [aChatItem.chatItem]), at: 0) + } default: print("unsupported response: ", res.responseType) } @@ -316,3 +400,13 @@ private func encodeCJSON(_ value: T) -> [CChar] { let str = String(decoding: data, as: UTF8.self) return str.cString(using: .utf8)! } + +enum ChatError: Decodable { + case errorStore(storeError: StoreError) + // TODO other error cases +} + +enum StoreError: Decodable { + case userContactLinkNotFound + // TODO other error cases +} diff --git a/apps/ios/Shared/MyPlayground.playground/Contents.swift b/apps/ios/Shared/MyPlayground.playground/Contents.swift index aa4740fee5..16f46301b3 100644 --- a/apps/ios/Shared/MyPlayground.playground/Contents.swift +++ b/apps/ios/Shared/MyPlayground.playground/Contents.swift @@ -4,79 +4,11 @@ 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 str = """ -//{"resp":{"apiDirectChat":{"chat":{"chatInfo":{"direct":{"contact":{"contactId":2,"localDisplayName":"ep","profile":{"displayName":"ep","fullName":"Evgeny"},"activeConn":{"connId":1,"agentConnId":"bUk2OXZlN3lfNXFaVWRWMQ==","connLevel":0,"connType":"contact","connStatus":"ready","entityId":2,"createdAt":"2022-01-29T11:21:18.669786Z"}}}},"chatItems":[{"chatDir":{"directSnd":{}},"meta":{"itemId":1,"itemTs":"2022-01-29T11:21:47.947865Z","itemText":"hello","localItemTs":"2022-01-29T11:21:47.947865Z","createdAt":"2022-01-29T11:21:47.947865Z"},"content":{"sndMsgContent":{"msgContent":{"type":"text","text":"hello"}}}},{"chatDir":{"directRcv":{}},"meta":{"itemId":2,"itemTs":"2022-01-29T11:22:08Z","itemText":"hi","localItemTs":"2022-01-29T11:22:08Z","createdAt":"2022-01-29T11:22:08.563959Z"},"content":{"rcvMsgContent":{"msgContent":{"type":"text","text":"hi"}}}}]}}}} -//""" - -let str = "\"2022-01-29T11:21:47Z\"" - -let data = str.data(using: .utf8)! - -let jsonDecoder = JSONDecoder() - -let df1 = DateFormatter() -df1.locale = Locale(identifier: "en_US_POSIX") -df1.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" -df1.timeZone = TimeZone(secondsFromGMT: 0) - -let df2 = DateFormatter() -df2.locale = Locale(identifier: "en_US_POSIX") -df2.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" -df2.timeZone = TimeZone(secondsFromGMT: 0) - -jsonDecoder.dateDecodingStrategy = .iso8601 // .custom { decoder in -// let container = try decoder.singleValueContainer() -// let string = try container.decode(String.self) -// if let date = df1.date(from: string) ?? df2.date(from: string) { -// return date -// } -// throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)") -//} - - -let r: Date = try! jsonDecoder.decode(Date.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)!) -"\(ChatType.direct)" + +var a = [1, 2, 3] + +a.removeAll(where: { $0 == 1} ) + +print(a) diff --git a/apps/ios/Shared/MyPlayground.playground/timeline.xctimeline b/apps/ios/Shared/MyPlayground.playground/timeline.xctimeline index 19563d37f5..a6df7c391e 100644 --- a/apps/ios/Shared/MyPlayground.playground/timeline.xctimeline +++ b/apps/ios/Shared/MyPlayground.playground/timeline.xctimeline @@ -3,7 +3,7 @@ version = "3.0"> diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift new file mode 100644 index 0000000000..7707cd04bf --- /dev/null +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -0,0 +1,207 @@ +// +// ChatListNavLink.swift +// SimpleX +// +// Created by Evgeny Poberezkin on 01/02/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct ChatListNavLink: View { + @EnvironmentObject var chatModel: ChatModel + + @Binding var chatId: String? + @State var chatPreview: Chat + var width: CGFloat + + @State private var showDeleteContactAlert = false + @State private var showDeleteGroupAlert = false + @State private var showContactRequestAlert = false + @State private var showContactRequestDialog = false + @State private var alertContact: Contact? + @State private var alertGroupInfo: GroupInfo? + @State private var alertContactRequest: UserContactRequest? + + var body: some View { + switch chatPreview.chatInfo { + case let .direct(contact): + contactNavLink(contact) + case let .group(groupInfo): + groupNavLink(groupInfo) + case let .contactRequest(cReq): + contactRequestNavLink(cReq) + } + } + + private func chatView() -> some View { + ChatView( + chatId: $chatId, + chatInfo: chatPreview.chatInfo, + width: width + ) + .onAppear { + do { + let ci = chatPreview.chatInfo + let chat = try apiGetChat(type: ci.chatType, id: ci.apiId) + chatModel.chats[ci.id] = chat + } catch { + print("apiGetChatItems", error) + } + } + } + + private func contactNavLink(_ contact: Contact) -> some View { + NavigationLink( + tag: chatPreview.chatInfo.id, + selection: $chatId, + destination: { chatView() }, + label: { ChatPreviewView(chatPreview: chatPreview) } + ) + .disabled(!contact.connected) + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button(role: .destructive) { + alertContact = contact + showDeleteContactAlert = true + } label: { + Label("Delete", systemImage: "trash") + } + } + .alert(isPresented: $showDeleteContactAlert) { + deleteContactAlert(alertContact!) + } + .frame(height: 80) + } + + private func groupNavLink(_ groupInfo: GroupInfo) -> some View { + NavigationLink( + tag: chatPreview.chatInfo.id, + selection: $chatId, + destination: { chatView() }, + label: { ChatPreviewView(chatPreview: chatPreview) } + ) + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button(role: .destructive) { + alertGroupInfo = groupInfo + showDeleteGroupAlert = true + } label: { + Label("Delete", systemImage: "trash") + } + } + .alert(isPresented: $showDeleteGroupAlert) { + deleteGroupAlert(alertGroupInfo!) + } + .frame(height: 80) + } + + private func contactRequestNavLink(_ contactRequest: UserContactRequest) -> some View { + ChatPreviewView(chatPreview: chatPreview) + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button { acceptContactRequest(contactRequest) } + label: { Label("Accept", systemImage: "checkmark") } + .tint(.blue) + Button(role: .destructive) { + alertContactRequest = contactRequest + showContactRequestAlert = true + } label: { + Label("Reject", systemImage: "multiply") + } + } + .alert(isPresented: $showContactRequestAlert) { + contactRequestAlert(alertContactRequest!) + } + .background(Color(uiColor: .systemBackground)) + .frame(width: width, height: 80) + .onTapGesture { showContactRequestDialog = true } + .confirmationDialog("Connection request", isPresented: $showContactRequestDialog, titleVisibility: .visible) { + Button("Accept contact") { acceptContactRequest(contactRequest) } + Button("Reject contact (sender NOT notified)") { rejectContactRequest(contactRequest) } + } + } + + private func deleteContactAlert(_ contact: Contact) -> Alert { + Alert( + title: Text("Delete contact?"), + message: Text("Contact and all messages will be deleted"), + primaryButton: .destructive(Text("Delete")) { + do { + try apiDeleteChat(type: .direct, id: contact.contactId) + chatModel.chats.removeValue(forKey: contact.id) + chatModel.chatPreviews.removeAll(where: { $0.id == contact.id }) + } catch let error { + print("Error: \(error)") + } + alertContact = nil + }, secondaryButton: .cancel() { + alertContact = nil + } + ) + } + + private func deleteGroupAlert(_ groupInfo: GroupInfo) -> Alert { + Alert( + title: Text("Delete group"), + message: Text("Group deletion is not supported") + ) + } + + private func contactRequestAlert(_ contactRequest: UserContactRequest) -> Alert { + Alert( + title: Text("Reject contact request"), + message: Text("The sender will NOT be notified"), + primaryButton: .destructive(Text("Reject")) { + rejectContactRequest(contactRequest) + alertContactRequest = nil + }, secondaryButton: .cancel { + alertContactRequest = nil + } + ) + } + + private func acceptContactRequest(_ contactRequest: UserContactRequest) { + do { + let contact = try apiAcceptContactRequest(contactReqId: contactRequest.contactRequestId) + chatModel.chats.removeValue(forKey: contactRequest.id) + let chat = Chat(chatInfo: ChatInfo.direct(contact: contact), chatItems: []) + chatModel.chats[contact.id] = chat + if let i = chatModel.chatPreviews.firstIndex(where: { $0.id == contactRequest.id }) { + chatModel.chatPreviews[i] = chat + } else { + chatModel.chatPreviews.insert(chat, at: 0) + } + } catch let error { + print("Error: \(error)") + } + } + + private func rejectContactRequest(_ contactRequest: UserContactRequest) { + do { + try apiRejectContactRequest(contactReqId: contactRequest.contactRequestId) + chatModel.chats.removeValue(forKey: contactRequest.id) + chatModel.chatPreviews.removeAll(where: { $0.id == contactRequest.id }) + } catch let error { + print("Error: \(error)") + } + } +} + +struct ChatListNavLink_Previews: PreviewProvider { + static var previews: some View { + @State var chatId: String? = "@1" + return Group { + ChatListNavLink(chatId: $chatId, chatPreview: Chat( + chatInfo: sampleDirectChatInfo, + chatItems: [chatItemSample(1, .directSnd, Date.now, "hello")] + ), width: 300) + ChatListNavLink(chatId: $chatId, chatPreview: Chat( + chatInfo: sampleDirectChatInfo, + chatItems: [chatItemSample(1, .directSnd, Date.now, "hello")] + ), width: 300) + ChatListNavLink(chatId: $chatId, chatPreview: Chat( + chatInfo: sampleContactRequestChatInfo, + chatItems: [] + ), width: 300) + } + .previewLayout(.fixed(width: 360, height: 80)) + } +} diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift new file mode 100644 index 0000000000..caeb4468cf --- /dev/null +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -0,0 +1,75 @@ +// +// 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 + @State private var chatId: String? + + var user: User + + var body: some View { + return VStack { +// if chatModel.chats.isEmpty { +// VStack { +// Text("Hello chat") +// Text("Active user: \(user.localDisplayName) (\(user.profile.fullName))") +// } +// } + + NavigationView { + GeometryReader { geometry in + List { + NavigationLink { + TerminalView() + } label: { + Text("Terminal") + } + + ForEach(chatModel.chatPreviews) { chatPreview in + ChatListNavLink( + chatId: $chatId, + chatPreview: chatPreview, + width: geometry.size.width + ) + } + } + .padding(0) + .offset(x: -8) + .listStyle(.plain) + .toolbar { ChatListToolbar(width: geometry.size.width) } + .navigationBarTitleDisplayMode(.inline) + } + } + } + } +} + +struct ChatListView_Previews: PreviewProvider { + static var previews: some View { + let chatModel = ChatModel() + chatModel.chatPreviews = [ + Chat( + chatInfo: sampleDirectChatInfo, + chatItems: [chatItemSample(1, .directSnd, Date.now, "hello")] + ), + Chat( + chatInfo: sampleGroupChatInfo, + chatItems: [chatItemSample(1, .directSnd, Date.now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")] + ), + Chat( + chatInfo: sampleContactRequestChatInfo, + chatItems: [] + ) + + ] + return ChatListView(user: sampleUser) + .environmentObject(chatModel) + } +} diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/ChatList/TerminalView.swift similarity index 97% rename from apps/ios/Shared/Views/TerminalView.swift rename to apps/ios/Shared/Views/ChatList/TerminalView.swift index a58f1ea9ab..55f9bbaeb7 100644 --- a/apps/ios/Shared/Views/TerminalView.swift +++ b/apps/ios/Shared/Views/ChatList/TerminalView.swift @@ -20,6 +20,7 @@ struct TerminalView: View { NavigationLink { ScrollView { Text(item.details) + .textSelection(.enabled) } } label: { Text(item.label) diff --git a/apps/ios/Shared/Views/ChatListView.swift b/apps/ios/Shared/Views/ChatListView.swift deleted file mode 100644 index 8f69dbb9e1..0000000000 --- a/apps/ios/Shared/Views/ChatListView.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// 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 - @State private var chatId: String? - @State private var chatsToBeDeleted: IndexSet? - @State private var showDeleteAlert = false - - var user: User - - var body: some View { - return VStack { -// if chatModel.chats.isEmpty { -// VStack { -// Text("Hello chat") -// Text("Active user: \(user.localDisplayName) (\(user.profile.fullName))") -// } -// } - - ChatHeaderView(chatId: $chatId) - - NavigationView { - List { - NavigationLink { - TerminalView() - } label: { - Text("Terminal") - } - - ForEach(chatModel.chatPreviews) { chatPreview in - NavigationLink( - tag: chatPreview.chatInfo.id, - selection: $chatId, - destination: { - ChatView(chatInfo: chatPreview.chatInfo) - .onAppear { - do { - let ci = chatPreview.chatInfo - let chat = try apiGetChat(type: ci.chatType, id: ci.apiId) - chatModel.chats[ci.id] = chat - } catch { - print("apiGetChatItems", error) - } - } - }, label: { - ChatPreviewView(chatPreview: chatPreview) - .alert(isPresented: $showDeleteAlert) { - deleteChatAlert((chatsToBeDeleted?.first)!) - } - } - ) - .frame(height: 80) - } - .onDelete { idx in - chatsToBeDeleted = idx - showDeleteAlert = true - } - } - .padding(0) - .offset(x: -8) - .listStyle(.plain) - .edgesIgnoringSafeArea(.top) - } - } - } - - func deleteChatAlert(_ ix: IndexSet.Element) -> Alert { - let ci = chatModel.chatPreviews[ix].chatInfo - switch ci { - case .direct: - return Alert( - title: Text("Delete contact?"), - message: Text("Contact and all messages will be deleted"), - primaryButton: .destructive(Text("Delete")) { - do { - try apiDeleteChat(type: ci.chatType, id: ci.apiId) - chatModel.chatPreviews.remove(at: ix) - } catch let error { - print("Error: \(error)") - } - chatsToBeDeleted = nil - }, secondaryButton: .cancel() { - chatsToBeDeleted = nil - } - ) - case .group: - return Alert( - title: Text("Delete group"), - message: Text("Group deletion is not supported") - ) - } - } -} - -struct ChatListView_Previews: PreviewProvider { - static var previews: some View { - let chatModel = ChatModel() - chatModel.chatPreviews = [ - Chat( - chatInfo: sampleDirectChatInfo, - chatItems: [chatItemSample(1, .directSnd, Date.now, "hello")] - ), - Chat( - chatInfo: sampleGroupChatInfo, - chatItems: [chatItemSample(1, .directSnd, Date.now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")] - ) - ] - return ChatListView(user: sampleUser) - .environmentObject(chatModel) - } -} diff --git a/apps/ios/Shared/Views/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatPreviewView.swift index eb86336f0a..f9a706623e 100644 --- a/apps/ios/Shared/Views/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatPreviewView.swift @@ -38,6 +38,13 @@ struct ChatPreviewView: View { .padding(.bottom, 4) .padding(.top, 1) } +// else if case let .direct(contact) = chatPreview.chatInfo, !contact.connected { +// Text("Connecting...") +// .frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading) +// .padding([.leading, .trailing], 8) +// .padding(.bottom, 4) +// .padding(.top, 1) +// } } } } diff --git a/apps/ios/Shared/Views/ChatView.swift b/apps/ios/Shared/Views/ChatView.swift index fc323fce73..38e6969361 100644 --- a/apps/ios/Shared/Views/ChatView.swift +++ b/apps/ios/Shared/Views/ChatView.swift @@ -10,22 +10,22 @@ import SwiftUI struct ChatView: View { @EnvironmentObject var chatModel: ChatModel - @State var inProgress: Bool = false - + @Binding var chatId: String? var chatInfo: ChatInfo + var width: CGFloat + @State private var inProgress: Bool = false + var body: some View { VStack { if let chat: Chat = chatModel.chats[chatInfo.id] { - VStack { - ScrollView { - LazyVStack(spacing: 5) { - ForEach(chat.chatItems) { - ChatItemView(chatItem: $0) - } + ScrollView { + LazyVStack(spacing: 5) { + ForEach(chat.chatItems) { + ChatItemView(chatItem: $0) } } } - } else { + } else { Text("unexpected: chat not found...") } @@ -33,8 +33,20 @@ struct ChatView: View { SendMessageView(sendMessage: sendMessage, inProgress: inProgress) } - .edgesIgnoringSafeArea(.all) - .navigationBarHidden(true) + .toolbar { + HStack { + Button { chatId = nil } label: { Image(systemName: "chevron.backward") } + Spacer() + Text(chatInfo.localDisplayName) + .font(.title3) + Spacer() + EmptyView() + } + .padding(.horizontal) + .frame(minWidth: width, maxWidth: .infinity, alignment: .center) + } + .navigationBarBackButtonHidden(true) + } func sendMessage(_ msg: String) { @@ -51,6 +63,7 @@ struct ChatView: View { struct ChatView_Previews: PreviewProvider { static var previews: some View { + @State var chatId: String? = "@1" let chatModel = ChatModel() chatModel.chats = [ "@1": Chat( @@ -66,7 +79,7 @@ struct ChatView_Previews: PreviewProvider { ] ) ] - return ChatView(chatInfo: sampleDirectChatInfo) + return ChatView(chatId: $chatId, chatInfo: sampleDirectChatInfo, width: 300) .environmentObject(chatModel) } } diff --git a/apps/ios/Shared/Views/Helpers/ChatHeaderView.swift b/apps/ios/Shared/Views/Helpers/ChatHeaderView.swift deleted file mode 100644 index a515a17008..0000000000 --- a/apps/ios/Shared/Views/Helpers/ChatHeaderView.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// ChatHeaderView.swift -// SimpleX -// -// Created by Evgeny Poberezkin on 29/01/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import SwiftUI - -struct ChatHeaderView: View { - @Binding var chatId: String? - @EnvironmentObject var chatModel: ChatModel - - var body: some View { - HStack { - if let cId = chatId { - Button { chatId = nil } label: { Image(systemName: "chevron.backward") } - Spacer() - Text(chatModel.chats[cId]?.chatInfo.localDisplayName ?? "") - .font(.title3) - Spacer() - EmptyView() - } else { - SettingsButton() - Spacer() - Text("Your chats") - .font(.title3) - Spacer() - NewChatButton() - } - } - .padding([.horizontal, .top]) - } -} - -struct ChatHeaderView_Previews: PreviewProvider { - static var previews: some View { - @State var chatId1: String? = "@1" - @State var chatId2: String? - let chatModel = ChatModel() - chatModel.chats = [ - "@1": Chat( - chatInfo: sampleDirectChatInfo, - chatItems: [chatItemSample(1, .directSnd, Date.now, "hello")] - ) - ] - return Group { - ChatHeaderView(chatId: $chatId1) - ChatHeaderView(chatId: $chatId2) - } - .previewLayout(.fixed(width: 300, height: 70)) - .environmentObject(chatModel) - } -} diff --git a/apps/ios/Shared/Views/Helpers/ChatItemView.swift b/apps/ios/Shared/Views/Helpers/ChatItemView.swift index 71e6144f71..039e64ae72 100644 --- a/apps/ios/Shared/Views/Helpers/ChatItemView.swift +++ b/apps/ios/Shared/Views/Helpers/ChatItemView.swift @@ -23,6 +23,7 @@ struct ChatItemView: View { .padding(.horizontal, 12) .frame(minWidth: 200, maxWidth: 300, alignment: .leading) .foregroundColor(sent ? .white : .primary) + .textSelection(.enabled) Text(getDateFormatter().string(from: chatItem.meta.itemTs)) .font(.subheadline) .foregroundColor(sent ? .white : .secondary) diff --git a/apps/ios/Shared/Views/Helpers/ChatListNavLink.swift b/apps/ios/Shared/Views/Helpers/ChatListNavLink.swift new file mode 100644 index 0000000000..0034622b08 --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/ChatListNavLink.swift @@ -0,0 +1,21 @@ +// +// ChatListNavLink.swift +// SimpleX +// +// Created by Evgeny Poberezkin on 01/02/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct ChatListNavLink: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +struct ChatListNavLink_Previews: PreviewProvider { + static var previews: some View { + ChatListNavLink() + } +} diff --git a/apps/ios/Shared/Views/Helpers/ChatListToolbar.swift b/apps/ios/Shared/Views/Helpers/ChatListToolbar.swift new file mode 100644 index 0000000000..779792e9fb --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/ChatListToolbar.swift @@ -0,0 +1,42 @@ +// +// ChatListToolbar.swift +// SimpleX +// +// Created by Evgeny Poberezkin on 29/01/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct ChatListToolbar: View { + @EnvironmentObject var chatModel: ChatModel + var width: CGFloat + + var body: some View { + HStack { + SettingsButton() + Spacer() + Text("Your chats") + .font(.title3) + Spacer() + NewChatButton() + } + .padding(.horizontal) + .frame(minWidth: width, maxWidth: .infinity, alignment: .center) + } +} + +struct ChatListToolbar_Previews: PreviewProvider { + static var previews: some View { + let chatModel = ChatModel() + chatModel.chats = [ + "@1": Chat( + chatInfo: sampleDirectChatInfo, + chatItems: [chatItemSample(1, .directSnd, Date.now, "hello")] + ) + ] + return ChatListToolbar(width: 300) + .previewLayout(.fixed(width: 300, height: 70)) + .environmentObject(chatModel) + } +} diff --git a/apps/ios/Shared/Views/Helpers/MessageView.swift b/apps/ios/Shared/Views/Helpers/MessageView.swift deleted file mode 100644 index 76ebbc3341..0000000000 --- a/apps/ios/Shared/Views/Helpers/MessageView.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// MessageView.swift -// SimpleX -// -// Created by Evgeny Poberezkin on 18/01/2022. -// - -import SwiftUI - -struct MessageView: View { - var message: String - var sent: Bool - let receivedColor: Color = Color(UIColor(red: 240/255, green: 240/255, blue: 240/255, alpha: 1.0)) - - var body: some View { - Text(message) - .padding(10) - .foregroundColor(sent ? Color.white : Color.black) - .background(sent ? Color.blue : receivedColor) - .cornerRadius(10) - .frame(minWidth: 100, - maxWidth: .infinity, - minHeight: 0, - maxHeight: .infinity, - alignment: .leading) - - } -} - -struct MessageView_Previews: PreviewProvider { - static var previews: some View { - MessageView(message: "> Send message: \"Hello world!\"\nSuccessful", sent: false) - } -} diff --git a/apps/ios/Shared/Views/Helpers/NewChat/AddContactView.swift b/apps/ios/Shared/Views/Helpers/NewChat/AddContactView.swift index fc23891112..0f0b1521c1 100644 --- a/apps/ios/Shared/Views/Helpers/NewChat/AddContactView.swift +++ b/apps/ios/Shared/Views/Helpers/NewChat/AddContactView.swift @@ -30,8 +30,8 @@ struct AddContactView: View { Button { shareInvitation = true } label: { Label("Share", systemImage: "square.and.arrow.up") } - .padding() - .shareSheet(isPresented: $shareInvitation, items: [connReqInvitation]) + .padding() + .shareSheet(isPresented: $shareInvitation, items: [connReqInvitation]) } } } diff --git a/apps/ios/Shared/Views/Helpers/NewChat/QRCode.swift b/apps/ios/Shared/Views/Helpers/NewChat/QRCode.swift index b92dc44fd3..4d9b7835b0 100644 --- a/apps/ios/Shared/Views/Helpers/NewChat/QRCode.swift +++ b/apps/ios/Shared/Views/Helpers/NewChat/QRCode.swift @@ -20,6 +20,7 @@ struct QRCode: View { .resizable() .interpolation(.none) .aspectRatio(1, contentMode: .fit) + .textSelection(.enabled) } } .onAppear { diff --git a/apps/ios/Shared/Views/Helpers/UserSettings/SettingsButton.swift b/apps/ios/Shared/Views/Helpers/UserSettings/SettingsButton.swift index 840751aeb9..702ebbdeaf 100644 --- a/apps/ios/Shared/Views/Helpers/UserSettings/SettingsButton.swift +++ b/apps/ios/Shared/Views/Helpers/UserSettings/SettingsButton.swift @@ -9,6 +9,7 @@ import SwiftUI struct SettingsButton: View { + @EnvironmentObject var chatModel: ChatModel @State private var showSettings = false var body: some View { @@ -17,6 +18,13 @@ struct SettingsButton: View { } .sheet(isPresented: $showSettings, content: { SettingsView() + .onAppear { + do { + chatModel.userAddress = try apiGetUserAddress() + } catch { + print(error) + } + } }) } } diff --git a/apps/ios/Shared/Views/Helpers/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/Helpers/UserSettings/SettingsView.swift index dd93ce90aa..0d26ad87ca 100644 --- a/apps/ios/Shared/Views/Helpers/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/Helpers/UserSettings/SettingsView.swift @@ -12,7 +12,7 @@ struct SettingsView: View { @EnvironmentObject var chatModel: ChatModel var body: some View { - SettingsProfile() + UserProfile() UserAddress() } } diff --git a/apps/ios/Shared/Views/Helpers/UserSettings/UserAddress.swift b/apps/ios/Shared/Views/Helpers/UserSettings/UserAddress.swift index 9cbb63fb9d..1cd497a4d8 100644 --- a/apps/ios/Shared/Views/Helpers/UserSettings/UserAddress.swift +++ b/apps/ios/Shared/Views/Helpers/UserSettings/UserAddress.swift @@ -9,13 +9,67 @@ import SwiftUI struct UserAddress: View { + @EnvironmentObject var chatModel: ChatModel + @State private var shareAddressLink = false + @State private var deleteAddressAlert = false + var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + VStack (alignment: .leading) { + Text("Your chat address") + .font(.title) + .padding(.bottom) + Text("Your can share your address as a link or as a QR code - anybody will be able to connect to you, and if you later delete it - you won't lose your contacts.") + .padding(.bottom) + if let userAdress = chatModel.userAddress { + QRCode(uri: userAdress) + HStack { + Button { shareAddressLink = true } label: { + Label("Share link", systemImage: "square.and.arrow.up") + } + .padding() + .shareSheet(isPresented: $shareAddressLink, items: [userAdress]) + + Button { deleteAddressAlert = true } label: { + Label("Delete address", systemImage: "trash") + } + .padding() + .alert(isPresented: $deleteAddressAlert) { + Alert( + title: Text("Delete address?"), + message: Text("All your contacts will remain connected"), + primaryButton: .destructive(Text("Delete")) { + do { + try apiDeleteUserAddress() + chatModel.userAddress = nil + } catch let error { + print("Error: \(error)") + } + }, secondaryButton: .cancel() + ) + } + .shareSheet(isPresented: $shareAddressLink, items: [userAdress]) + } + .frame(maxWidth: .infinity) + } else { + Button { + do { + chatModel.userAddress = try apiCreateUserAddress() + } catch let error { + print("Error: \(error)") + } + } label: { Label("Create address", systemImage: "qrcode") } + .frame(maxWidth: .infinity) + } + } + .padding() } } struct UserAddress_Previews: PreviewProvider { static var previews: some View { - UserAddress() + let chatModel = ChatModel() + chatModel.userAddress = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D" + return UserAddress() + .environmentObject(chatModel) } } diff --git a/apps/ios/Shared/Views/Helpers/UserSettings/SettingsProfile.swift b/apps/ios/Shared/Views/Helpers/UserSettings/UserProfile.swift similarity index 95% rename from apps/ios/Shared/Views/Helpers/UserSettings/SettingsProfile.swift rename to apps/ios/Shared/Views/Helpers/UserSettings/UserProfile.swift index fc3eb7d1c2..9f9bb574db 100644 --- a/apps/ios/Shared/Views/Helpers/UserSettings/SettingsProfile.swift +++ b/apps/ios/Shared/Views/Helpers/UserSettings/UserProfile.swift @@ -1,5 +1,5 @@ // -// SettingsProfile.swift +// UserProfile.swift // SimpleX // // Created by Evgeny Poberezkin on 31/01/2022. @@ -8,7 +8,7 @@ import SwiftUI -struct SettingsProfile: View { +struct UserProfile: View { @EnvironmentObject var chatModel: ChatModel @State private var profile = Profile(displayName: "", fullName: "") @State private var editProfile: Bool = false @@ -76,11 +76,11 @@ struct SettingsProfile: View { } } -struct SettingsProfile_Previews: PreviewProvider { +struct UserProfile_Previews: PreviewProvider { static var previews: some View { let chatModel = ChatModel() chatModel.currentUser = sampleUser - return SettingsProfile() + return UserProfile() .environmentObject(chatModel) } } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index a54706da7b..1cb858b560 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -56,22 +56,22 @@ 5CA059F0279559F40002BEB4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5CA059C5279559F40002BEB4 /* Assets.xcassets */; }; 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 */; }; 5CB924D427A853F100ACCCDD /* SettingsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D327A853F100ACCCDD /* SettingsButton.swift */; }; 5CB924D527A853F100ACCCDD /* SettingsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D327A853F100ACCCDD /* SettingsButton.swift */; }; 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D627A8563F00ACCCDD /* SettingsView.swift */; }; 5CB924D827A8563F00ACCCDD /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D627A8563F00ACCCDD /* SettingsView.swift */; }; - 5CB924E127A867BA00ACCCDD /* SettingsProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E027A867BA00ACCCDD /* SettingsProfile.swift */; }; - 5CB924E227A867BA00ACCCDD /* SettingsProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E027A867BA00ACCCDD /* SettingsProfile.swift */; }; + 5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E027A867BA00ACCCDD /* UserProfile.swift */; }; + 5CB924E227A867BA00ACCCDD /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E027A867BA00ACCCDD /* UserProfile.swift */; }; 5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E327A8683A00ACCCDD /* UserAddress.swift */; }; 5CB924E527A8683A00ACCCDD /* UserAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E327A8683A00ACCCDD /* UserAddress.swift */; }; + 5CB9250D27A9432000ACCCDD /* ChatListNavLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */; }; + 5CB9250E27A9432000ACCCDD /* ChatListNavLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */; }; 5CC1C99227A6C7F5000D9FF6 /* QRCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC1C99127A6C7F5000D9FF6 /* QRCode.swift */; }; 5CC1C99327A6C7F5000D9FF6 /* QRCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC1C99127A6C7F5000D9FF6 /* QRCode.swift */; }; 5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */; }; 5CC1C99627A6CF7F000D9FF6 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */; }; - 5CCD403127A5F1C600368C90 /* ChatHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403027A5F1C600368C90 /* ChatHeaderView.swift */; }; - 5CCD403227A5F1C600368C90 /* ChatHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403027A5F1C600368C90 /* ChatHeaderView.swift */; }; + 5CCD403127A5F1C600368C90 /* ChatListToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403027A5F1C600368C90 /* ChatListToolbar.swift */; }; + 5CCD403227A5F1C600368C90 /* ChatListToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403027A5F1C600368C90 /* ChatListToolbar.swift */; }; 5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; }; 5CCD403527A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; }; 5CCD403727A5F9A200368C90 /* ConnectContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ConnectContactView.swift */; }; @@ -131,14 +131,14 @@ 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 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; - 5CA05A4E279752D00002BEB4 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; 5CB924D327A853F100ACCCDD /* SettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsButton.swift; sourceTree = ""; }; 5CB924D627A8563F00ACCCDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; - 5CB924E027A867BA00ACCCDD /* SettingsProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsProfile.swift; sourceTree = ""; }; + 5CB924E027A867BA00ACCCDD /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; 5CB924E327A8683A00ACCCDD /* UserAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddress.swift; sourceTree = ""; }; + 5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListNavLink.swift; sourceTree = ""; }; 5CC1C99127A6C7F5000D9FF6 /* QRCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCode.swift; sourceTree = ""; }; 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = ""; }; - 5CCD403027A5F1C600368C90 /* ChatHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHeaderView.swift; sourceTree = ""; }; + 5CCD403027A5F1C600368C90 /* ChatListToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListToolbar.swift; sourceTree = ""; }; 5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = ""; }; 5CCD403627A5F9A200368C90 /* ConnectContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectContactView.swift; sourceTree = ""; }; 5CCD403927A5F9BE00368C90 /* CreateGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateGroupView.swift; sourceTree = ""; }; @@ -196,10 +196,9 @@ children = ( 5C5F4AC227A5E9AF00B51EF1 /* Helpers */, 5CA05A4B27974EB60002BEB4 /* WelcomeView.swift */, - 5C2E260A27A30CFA00F70299 /* ChatListView.swift */, + 5CB9250B27A942F300ACCCDD /* ChatList */, 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */, 5C2E260E27A30FDC00F70299 /* ChatView.swift */, - 5C2E261127A30FEA00F70299 /* TerminalView.swift */, ); path = Views; sourceTree = ""; @@ -208,9 +207,8 @@ isa = PBXGroup; children = ( 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */, - 5CA05A4E279752D00002BEB4 /* MessageView.swift */, 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */, - 5CCD403027A5F1C600368C90 /* ChatHeaderView.swift */, + 5CCD403027A5F1C600368C90 /* ChatListToolbar.swift */, 5CB924DF27A8678B00ACCCDD /* UserSettings */, 5CB924DD27A8622200ACCCDD /* NewChat */, ); @@ -332,11 +330,21 @@ 5CB924D327A853F100ACCCDD /* SettingsButton.swift */, 5CB924D627A8563F00ACCCDD /* SettingsView.swift */, 5CB924E327A8683A00ACCCDD /* UserAddress.swift */, - 5CB924E027A867BA00ACCCDD /* SettingsProfile.swift */, + 5CB924E027A867BA00ACCCDD /* UserProfile.swift */, ); path = UserSettings; sourceTree = ""; }; + 5CB9250B27A942F300ACCCDD /* ChatList */ = { + isa = PBXGroup; + children = ( + 5C2E260A27A30CFA00F70299 /* ChatListView.swift */, + 5C2E261127A30FEA00F70299 /* TerminalView.swift */, + 5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */, + ); + path = ChatList; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -506,7 +514,7 @@ files = ( 5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */, 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */, - 5CB924E127A867BA00ACCCDD /* SettingsProfile.swift in Sources */, + 5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */, 5C764E80279C7276000C6508 /* dummy.m in Sources */, 5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */, 5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */, @@ -514,8 +522,8 @@ 5C9FD96B27A56D4D0075386C /* JSON.swift in Sources */, 5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */, 5CC1C99227A6C7F5000D9FF6 /* QRCode.swift in Sources */, - 5CCD403127A5F1C600368C90 /* ChatHeaderView.swift in Sources */, - 5CA05A4F279752D00002BEB4 /* MessageView.swift in Sources */, + 5CCD403127A5F1C600368C90 /* ChatListToolbar.swift in Sources */, + 5CB9250D27A9432000ACCCDD /* ChatListNavLink.swift in Sources */, 5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */, 5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */, 5CA05A4C27974EB60002BEB4 /* WelcomeView.swift in Sources */, @@ -538,7 +546,7 @@ files = ( 5C6AD81427A834E300348BD7 /* NewChatButton.swift in Sources */, 5CB924D827A8563F00ACCCDD /* SettingsView.swift in Sources */, - 5CB924E227A867BA00ACCCDD /* SettingsProfile.swift in Sources */, + 5CB924E227A867BA00ACCCDD /* UserProfile.swift in Sources */, 5C764E81279C7276000C6508 /* dummy.m in Sources */, 5CB924E527A8683A00ACCCDD /* UserAddress.swift in Sources */, 5C063D2827A4564100AEC577 /* ChatPreviewView.swift in Sources */, @@ -546,8 +554,8 @@ 5C9FD96C27A56D4D0075386C /* JSON.swift in Sources */, 5C9FD96F27A5D6ED0075386C /* SendMessageView.swift in Sources */, 5CC1C99327A6C7F5000D9FF6 /* QRCode.swift in Sources */, - 5CCD403227A5F1C600368C90 /* ChatHeaderView.swift in Sources */, - 5CA05A50279752D00002BEB4 /* MessageView.swift in Sources */, + 5CCD403227A5F1C600368C90 /* ChatListToolbar.swift in Sources */, + 5CB9250E27A9432000ACCCDD /* ChatListNavLink.swift in Sources */, 5CA059EE279559F40002BEB4 /* ContentView.swift in Sources */, 5CCD403527A5F6DF00368C90 /* AddContactView.swift in Sources */, 5CA05A4D27974EB60002BEB4 /* WelcomeView.swift in Sources */,