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
This commit is contained in:
Evgeny Poberezkin 2022-02-01 17:34:06 +00:00 committed by GitHub
parent a8a7bb3c99
commit 711207743b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 635 additions and 346 deletions

View file

@ -16,9 +16,10 @@ final class ChatModel: ObservableObject {
@Published var chatPreviews: [Chat] = [] @Published var chatPreviews: [Chat] = []
@Published var chatItems: [ChatItem] = [] @Published var chatItems: [ChatItem] = []
@Published var terminalItems: [TerminalItem] = [] @Published var terminalItems: [TerminalItem] = []
@Published var userAddress: String?
} }
class User: Codable { class User: Decodable {
var userId: Int64 var userId: Int64
var userContactId: Int64 var userContactId: Int64
var localDisplayName: ContactName var localDisplayName: ContactName
@ -59,17 +60,20 @@ let sampleProfile = Profile(
enum ChatType: String { enum ChatType: String {
case direct = "@" case direct = "@"
case group = "#" case group = "#"
case contactRequest = "<@"
} }
enum ChatInfo: Identifiable, Codable { enum ChatInfo: Identifiable, Decodable {
case direct(contact: Contact) case direct(contact: Contact)
case group(groupInfo: GroupInfo) case group(groupInfo: GroupInfo)
case contactRequest(contactRequest: UserContactRequest)
var localDisplayName: String { var localDisplayName: String {
get { get {
switch self { switch self {
case let .direct(contact): return "@\(contact.localDisplayName)" case let .direct(contact): return "@\(contact.localDisplayName)"
case let .group(groupInfo): return "#\(groupInfo.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 { var id: String {
get { get {
switch self { switch self {
case let .direct(contact): return "@\(contact.contactId)" case let .direct(contact): return contact.id
case let .group(groupInfo): return "#\(groupInfo.groupId)" case let .group(groupInfo): return groupInfo.id
case let .contactRequest(contactRequest): return contactRequest.id
} }
} }
} }
@ -88,6 +93,7 @@ enum ChatInfo: Identifiable, Codable {
switch self { switch self {
case .direct: return .direct case .direct: return .direct
case .group: return .group case .group: return .group
case .contactRequest: return .contactRequest
} }
} }
} }
@ -97,6 +103,7 @@ enum ChatInfo: Identifiable, Codable {
switch self { switch self {
case let .direct(contact): return contact.contactId case let .direct(contact): return contact.contactId
case let .group(groupInfo): return groupInfo.groupId 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 sampleGroupChatInfo = ChatInfo.group(groupInfo: sampleGroupInfo)
let sampleContactRequestChatInfo = ChatInfo.contactRequest(contactRequest: sampleContactRequest)
class Chat: Decodable, Identifiable { class Chat: Decodable, Identifiable {
var chatInfo: ChatInfo var chatInfo: ChatInfo
var chatItems: [ChatItem] var chatItems: [ChatItem]
@ -118,22 +127,46 @@ class Chat: Decodable, Identifiable {
var id: String { get { chatInfo.id } } var id: String { get { chatInfo.id } }
} }
struct Contact: Identifiable, Codable { struct Contact: Identifiable, Decodable {
var contactId: Int64 var contactId: Int64
var localDisplayName: ContactName var localDisplayName: ContactName
var profile: Profile var profile: Profile
var activeConn: Connection
var viaGroup: Int64? var viaGroup: Int64?
var id: String { get { "@\(contactId)" } } var id: String { get { "@\(contactId)" } }
var connected: Bool { get { activeConn.connStatus == "ready" || activeConn.connStatus == "snd-ready" } }
} }
let sampleContact = Contact( let sampleContact = Contact(
contactId: 1, contactId: 1,
localDisplayName: "alice", 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 profile: sampleProfile
) )
struct GroupInfo: Identifiable, Codable { struct GroupInfo: Identifiable, Decodable {
var groupId: Int64 var groupId: Int64
var localDisplayName: GroupName var localDisplayName: GroupName
var groupProfile: GroupProfile var groupProfile: GroupProfile
@ -157,7 +190,7 @@ let sampleGroupProfile = GroupProfile(
fullName: "My Team" fullName: "My Team"
) )
struct GroupMember: Codable { struct GroupMember: Decodable {
} }

View file

@ -22,25 +22,40 @@ enum ChatCommand {
case connect(connReq: String) case connect(connReq: String)
case apiDeleteChat(type: ChatType, id: Int64) case apiDeleteChat(type: ChatType, id: Int64)
case apiUpdateProfile(profile: Profile) case apiUpdateProfile(profile: Profile)
case createMyAddress
case deleteMyAddress
case showMyAddress
case apiAcceptContact(contactReqId: Int64)
case apiRejectContact(contactReqId: Int64)
case string(String) case string(String)
var cmdString: String { var cmdString: String {
get { get {
switch self { switch self {
case .apiGetChats: case .apiGetChats:
return "/get chats" return "/_get chats"
case let .apiGetChat(type, id): 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): case let .apiSendMessage(type, id, mc):
return "/send msg \(type.rawValue)\(id) \(mc.cmdString)" return "/_send \(type.rawValue)\(id) \(mc.cmdString)"
case .addContact: case .addContact:
return "/c" return "/connect"
case let .connect(connReq): case let .connect(connReq):
return "/c \(connReq)" return "/connect \(connReq)"
case let .apiDeleteChat(type, id): case let .apiDeleteChat(type, id):
return "/_del \(type.rawValue)\(id)" return "/_delete \(type.rawValue)\(id)"
case let .apiUpdateProfile(profile): 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): case let .string(str):
return str return str
} }
@ -62,9 +77,15 @@ enum ChatResponse: Decodable, Error {
case contactDeleted(contact: Contact) case contactDeleted(contact: Contact)
case userProfileNoChange case userProfileNoChange
case userProfileUpdated(fromProfile: Profile, toProfile: Profile) case userProfileUpdated(fromProfile: Profile, toProfile: Profile)
// case newSentInvitation case userContactLink(connReqContact: String)
case userContactLinkCreated(connReqContact: String)
case userContactLinkDeleted
case contactConnected(contact: Contact) case contactConnected(contact: Contact)
case receivedContactRequest(contactRequest: UserContactRequest)
case acceptingContactRequest(contact: Contact)
case contactRequestRejected
case newChatItem(chatItem: AChatItem) case newChatItem(chatItem: AChatItem)
case chatCmdError(chatError: ChatError)
var responseType: String { var responseType: String {
get { get {
@ -78,8 +99,15 @@ enum ChatResponse: Decodable, Error {
case .contactDeleted: return "contactDeleted" case .contactDeleted: return "contactDeleted"
case .userProfileNoChange: return "userProfileNoChange" case .userProfileNoChange: return "userProfileNoChange"
case .userProfileUpdated: return "userProfileNoChange" case .userProfileUpdated: return "userProfileNoChange"
case .userContactLink: return "userContactLink"
case .userContactLinkCreated: return "userContactLinkCreated"
case .userContactLinkDeleted: return "userContactLinkDeleted"
case .contactConnected: return "contactConnected" case .contactConnected: return "contactConnected"
case .receivedContactRequest: return "receivedContactRequest"
case .acceptingContactRequest: return "acceptingContactRequest"
case .contactRequestRejected: return "contactRequestRejected"
case .newChatItem: return "newChatItem" 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 .apiChats(chats): return String(describing: chats)
case let .apiChat(chat): return String(describing: chat) case let .apiChat(chat): return String(describing: chat)
case let .invitation(connReqInvitation): return connReqInvitation case let .invitation(connReqInvitation): return connReqInvitation
case .sentConfirmation: return "sentConfirmation: no details" case .sentConfirmation: return noDetails
case .sentInvitation: return "sentInvitation: no details" case .sentInvitation: return noDetails
case let .contactDeleted(contact): return String(describing: contact) 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 .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 .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 .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 { 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) { func processReceivedMsg(_ chatModel: ChatModel, _ res: ChatResponse) {
DispatchQueue.main.async { DispatchQueue.main.async {
chatModel.terminalItems.append(.resp(Date.now, res)) chatModel.terminalItems.append(.resp(Date.now, res))
switch res { switch res {
case let .contactConnected(contact): case let .contactConnected(contact):
chatModel.chatPreviews.insert( if let chat = chatModel.chats[contact.id] {
Chat(chatInfo: .direct(contact: contact), chatItems: []), chat.chatInfo = ChatInfo.direct(contact: contact)
at: 0 } 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): case let .newChatItem(aChatItem):
let ci = aChatItem.chatInfo let ci = aChatItem.chatInfo
let chat = chatModel.chats[ci.id] ?? Chat(chatInfo: ci, chatItems: []) let chat = chatModel.chats[ci.id] ?? Chat(chatInfo: ci, chatItems: [])
chatModel.chats[ci.id] = chat chatModel.chats[ci.id] = chat
chat.chatItems.append(aChatItem.chatItem) 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: default:
print("unsupported response: ", res.responseType) print("unsupported response: ", res.responseType)
} }
@ -316,3 +400,13 @@ private func encodeCJSON<T: Encodable>(_ value: T) -> [CChar] {
let str = String(decoding: data, as: UTF8.self) let str = String(decoding: data, as: UTF8.self)
return str.cString(using: .utf8)! 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
}

View file

@ -4,79 +4,11 @@ var greeting = "Hello, playground"
let jsonEncoder = JSONEncoder() 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)!) //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)

View file

@ -3,7 +3,7 @@
version = "3.0"> version = "3.0">
<TimelineItems> <TimelineItems>
<LoggerValueHistoryTimelineItem <LoggerValueHistoryTimelineItem
documentLocation = "file:///Users/evgeny/opensource/simplex-chat/simplex-chat/apps/ios/Shared/MyPlayground.playground#CharacterRangeLen=88&amp;CharacterRangeLoc=3634&amp;EndingColumnNumber=0&amp;EndingLineNumber=80&amp;StartingColumnNumber=3&amp;StartingLineNumber=79&amp;Timestamp=665235849.610096" documentLocation = "file:///Users/evgeny/opensource/simplex-chat/simplex-chat/apps/ios/Shared/MyPlayground.playground#CharacterRangeLen=88&amp;CharacterRangeLoc=91&amp;EndingColumnNumber=0&amp;EndingLineNumber=7&amp;StartingColumnNumber=3&amp;StartingLineNumber=6&amp;Timestamp=665423482.97412"
selectedRepresentationIndex = "0" selectedRepresentationIndex = "0"
shouldTrackSuperviewWidth = "NO"> shouldTrackSuperviewWidth = "NO">
</LoggerValueHistoryTimelineItem> </LoggerValueHistoryTimelineItem>

View file

@ -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))
}
}

View file

@ -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)
}
}

View file

@ -20,6 +20,7 @@ struct TerminalView: View {
NavigationLink { NavigationLink {
ScrollView { ScrollView {
Text(item.details) Text(item.details)
.textSelection(.enabled)
} }
} label: { } label: {
Text(item.label) Text(item.label)

View file

@ -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)
}
}

View file

@ -38,6 +38,13 @@ struct ChatPreviewView: View {
.padding(.bottom, 4) .padding(.bottom, 4)
.padding(.top, 1) .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)
// }
} }
} }
} }

View file

@ -10,22 +10,22 @@ import SwiftUI
struct ChatView: View { struct ChatView: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
@State var inProgress: Bool = false @Binding var chatId: String?
var chatInfo: ChatInfo var chatInfo: ChatInfo
var width: CGFloat
@State private var inProgress: Bool = false
var body: some View { var body: some View {
VStack { VStack {
if let chat: Chat = chatModel.chats[chatInfo.id] { if let chat: Chat = chatModel.chats[chatInfo.id] {
VStack { ScrollView {
ScrollView { LazyVStack(spacing: 5) {
LazyVStack(spacing: 5) { ForEach(chat.chatItems) {
ForEach(chat.chatItems) { ChatItemView(chatItem: $0)
ChatItemView(chatItem: $0)
}
} }
} }
} }
} else { } else {
Text("unexpected: chat not found...") Text("unexpected: chat not found...")
} }
@ -33,8 +33,20 @@ struct ChatView: View {
SendMessageView(sendMessage: sendMessage, inProgress: inProgress) SendMessageView(sendMessage: sendMessage, inProgress: inProgress)
} }
.edgesIgnoringSafeArea(.all) .toolbar {
.navigationBarHidden(true) 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) { func sendMessage(_ msg: String) {
@ -51,6 +63,7 @@ struct ChatView: View {
struct ChatView_Previews: PreviewProvider { struct ChatView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
@State var chatId: String? = "@1"
let chatModel = ChatModel() let chatModel = ChatModel()
chatModel.chats = [ chatModel.chats = [
"@1": Chat( "@1": Chat(
@ -66,7 +79,7 @@ struct ChatView_Previews: PreviewProvider {
] ]
) )
] ]
return ChatView(chatInfo: sampleDirectChatInfo) return ChatView(chatId: $chatId, chatInfo: sampleDirectChatInfo, width: 300)
.environmentObject(chatModel) .environmentObject(chatModel)
} }
} }

View file

@ -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)
}
}

View file

@ -23,6 +23,7 @@ struct ChatItemView: View {
.padding(.horizontal, 12) .padding(.horizontal, 12)
.frame(minWidth: 200, maxWidth: 300, alignment: .leading) .frame(minWidth: 200, maxWidth: 300, alignment: .leading)
.foregroundColor(sent ? .white : .primary) .foregroundColor(sent ? .white : .primary)
.textSelection(.enabled)
Text(getDateFormatter().string(from: chatItem.meta.itemTs)) Text(getDateFormatter().string(from: chatItem.meta.itemTs))
.font(.subheadline) .font(.subheadline)
.foregroundColor(sent ? .white : .secondary) .foregroundColor(sent ? .white : .secondary)

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -30,8 +30,8 @@ struct AddContactView: View {
Button { shareInvitation = true } label: { Button { shareInvitation = true } label: {
Label("Share", systemImage: "square.and.arrow.up") Label("Share", systemImage: "square.and.arrow.up")
} }
.padding() .padding()
.shareSheet(isPresented: $shareInvitation, items: [connReqInvitation]) .shareSheet(isPresented: $shareInvitation, items: [connReqInvitation])
} }
} }
} }

View file

@ -20,6 +20,7 @@ struct QRCode: View {
.resizable() .resizable()
.interpolation(.none) .interpolation(.none)
.aspectRatio(1, contentMode: .fit) .aspectRatio(1, contentMode: .fit)
.textSelection(.enabled)
} }
} }
.onAppear { .onAppear {

View file

@ -9,6 +9,7 @@
import SwiftUI import SwiftUI
struct SettingsButton: View { struct SettingsButton: View {
@EnvironmentObject var chatModel: ChatModel
@State private var showSettings = false @State private var showSettings = false
var body: some View { var body: some View {
@ -17,6 +18,13 @@ struct SettingsButton: View {
} }
.sheet(isPresented: $showSettings, content: { .sheet(isPresented: $showSettings, content: {
SettingsView() SettingsView()
.onAppear {
do {
chatModel.userAddress = try apiGetUserAddress()
} catch {
print(error)
}
}
}) })
} }
} }

View file

@ -12,7 +12,7 @@ struct SettingsView: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
var body: some View { var body: some View {
SettingsProfile() UserProfile()
UserAddress() UserAddress()
} }
} }

View file

@ -9,13 +9,67 @@
import SwiftUI import SwiftUI
struct UserAddress: View { struct UserAddress: View {
@EnvironmentObject var chatModel: ChatModel
@State private var shareAddressLink = false
@State private var deleteAddressAlert = false
var body: some View { 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 { struct UserAddress_Previews: PreviewProvider {
static var previews: some View { 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)
} }
} }

View file

@ -1,5 +1,5 @@
// //
// SettingsProfile.swift // UserProfile.swift
// SimpleX // SimpleX
// //
// Created by Evgeny Poberezkin on 31/01/2022. // Created by Evgeny Poberezkin on 31/01/2022.
@ -8,7 +8,7 @@
import SwiftUI import SwiftUI
struct SettingsProfile: View { struct UserProfile: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
@State private var profile = Profile(displayName: "", fullName: "") @State private var profile = Profile(displayName: "", fullName: "")
@State private var editProfile: Bool = false @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 { static var previews: some View {
let chatModel = ChatModel() let chatModel = ChatModel()
chatModel.currentUser = sampleUser chatModel.currentUser = sampleUser
return SettingsProfile() return UserProfile()
.environmentObject(chatModel) .environmentObject(chatModel)
} }
} }

View file

@ -56,22 +56,22 @@
5CA059F0279559F40002BEB4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5CA059C5279559F40002BEB4 /* Assets.xcassets */; }; 5CA059F0279559F40002BEB4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5CA059C5279559F40002BEB4 /* Assets.xcassets */; };
5CA05A4C27974EB60002BEB4 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA05A4B27974EB60002BEB4 /* WelcomeView.swift */; }; 5CA05A4C27974EB60002BEB4 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA05A4B27974EB60002BEB4 /* WelcomeView.swift */; };
5CA05A4D27974EB60002BEB4 /* 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 */; }; 5CB924D427A853F100ACCCDD /* SettingsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D327A853F100ACCCDD /* SettingsButton.swift */; };
5CB924D527A853F100ACCCDD /* 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 */; }; 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D627A8563F00ACCCDD /* SettingsView.swift */; };
5CB924D827A8563F00ACCCDD /* 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 */; }; 5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E027A867BA00ACCCDD /* UserProfile.swift */; };
5CB924E227A867BA00ACCCDD /* SettingsProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E027A867BA00ACCCDD /* SettingsProfile.swift */; }; 5CB924E227A867BA00ACCCDD /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E027A867BA00ACCCDD /* UserProfile.swift */; };
5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E327A8683A00ACCCDD /* UserAddress.swift */; }; 5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E327A8683A00ACCCDD /* UserAddress.swift */; };
5CB924E527A8683A00ACCCDD /* 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 */; }; 5CC1C99227A6C7F5000D9FF6 /* QRCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC1C99127A6C7F5000D9FF6 /* QRCode.swift */; };
5CC1C99327A6C7F5000D9FF6 /* 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 */; }; 5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */; };
5CC1C99627A6CF7F000D9FF6 /* 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 */; }; 5CCD403127A5F1C600368C90 /* ChatListToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403027A5F1C600368C90 /* ChatListToolbar.swift */; };
5CCD403227A5F1C600368C90 /* ChatHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403027A5F1C600368C90 /* ChatHeaderView.swift */; }; 5CCD403227A5F1C600368C90 /* ChatListToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403027A5F1C600368C90 /* ChatListToolbar.swift */; };
5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; }; 5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; };
5CCD403527A5F6DF00368C90 /* 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 */; }; 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 = "<group>"; }; 5CA059E7279559F40002BEB4 /* Tests_macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_macOS.swift; sourceTree = "<group>"; };
5CA059E9279559F40002BEB4 /* Tests_macOSLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_macOSLaunchTests.swift; sourceTree = "<group>"; }; 5CA059E9279559F40002BEB4 /* Tests_macOSLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_macOSLaunchTests.swift; sourceTree = "<group>"; };
5CA05A4B27974EB60002BEB4 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = "<group>"; }; 5CA05A4B27974EB60002BEB4 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = "<group>"; };
5CA05A4E279752D00002BEB4 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = "<group>"; };
5CB924D327A853F100ACCCDD /* SettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsButton.swift; sourceTree = "<group>"; }; 5CB924D327A853F100ACCCDD /* SettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsButton.swift; sourceTree = "<group>"; };
5CB924D627A8563F00ACCCDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; }; 5CB924D627A8563F00ACCCDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
5CB924E027A867BA00ACCCDD /* SettingsProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsProfile.swift; sourceTree = "<group>"; }; 5CB924E027A867BA00ACCCDD /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = "<group>"; };
5CB924E327A8683A00ACCCDD /* UserAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddress.swift; sourceTree = "<group>"; }; 5CB924E327A8683A00ACCCDD /* UserAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddress.swift; sourceTree = "<group>"; };
5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListNavLink.swift; sourceTree = "<group>"; };
5CC1C99127A6C7F5000D9FF6 /* QRCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCode.swift; sourceTree = "<group>"; }; 5CC1C99127A6C7F5000D9FF6 /* QRCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCode.swift; sourceTree = "<group>"; };
5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = "<group>"; }; 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = "<group>"; };
5CCD403027A5F1C600368C90 /* ChatHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHeaderView.swift; sourceTree = "<group>"; }; 5CCD403027A5F1C600368C90 /* ChatListToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListToolbar.swift; sourceTree = "<group>"; };
5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = "<group>"; }; 5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = "<group>"; };
5CCD403627A5F9A200368C90 /* ConnectContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectContactView.swift; sourceTree = "<group>"; }; 5CCD403627A5F9A200368C90 /* ConnectContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectContactView.swift; sourceTree = "<group>"; };
5CCD403927A5F9BE00368C90 /* CreateGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateGroupView.swift; sourceTree = "<group>"; }; 5CCD403927A5F9BE00368C90 /* CreateGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateGroupView.swift; sourceTree = "<group>"; };
@ -196,10 +196,9 @@
children = ( children = (
5C5F4AC227A5E9AF00B51EF1 /* Helpers */, 5C5F4AC227A5E9AF00B51EF1 /* Helpers */,
5CA05A4B27974EB60002BEB4 /* WelcomeView.swift */, 5CA05A4B27974EB60002BEB4 /* WelcomeView.swift */,
5C2E260A27A30CFA00F70299 /* ChatListView.swift */, 5CB9250B27A942F300ACCCDD /* ChatList */,
5C063D2627A4564100AEC577 /* ChatPreviewView.swift */, 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */,
5C2E260E27A30FDC00F70299 /* ChatView.swift */, 5C2E260E27A30FDC00F70299 /* ChatView.swift */,
5C2E261127A30FEA00F70299 /* TerminalView.swift */,
); );
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
@ -208,9 +207,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */, 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */,
5CA05A4E279752D00002BEB4 /* MessageView.swift */,
5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */, 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */,
5CCD403027A5F1C600368C90 /* ChatHeaderView.swift */, 5CCD403027A5F1C600368C90 /* ChatListToolbar.swift */,
5CB924DF27A8678B00ACCCDD /* UserSettings */, 5CB924DF27A8678B00ACCCDD /* UserSettings */,
5CB924DD27A8622200ACCCDD /* NewChat */, 5CB924DD27A8622200ACCCDD /* NewChat */,
); );
@ -332,11 +330,21 @@
5CB924D327A853F100ACCCDD /* SettingsButton.swift */, 5CB924D327A853F100ACCCDD /* SettingsButton.swift */,
5CB924D627A8563F00ACCCDD /* SettingsView.swift */, 5CB924D627A8563F00ACCCDD /* SettingsView.swift */,
5CB924E327A8683A00ACCCDD /* UserAddress.swift */, 5CB924E327A8683A00ACCCDD /* UserAddress.swift */,
5CB924E027A867BA00ACCCDD /* SettingsProfile.swift */, 5CB924E027A867BA00ACCCDD /* UserProfile.swift */,
); );
path = UserSettings; path = UserSettings;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
5CB9250B27A942F300ACCCDD /* ChatList */ = {
isa = PBXGroup;
children = (
5C2E260A27A30CFA00F70299 /* ChatListView.swift */,
5C2E261127A30FEA00F70299 /* TerminalView.swift */,
5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */,
);
path = ChatList;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@ -506,7 +514,7 @@
files = ( files = (
5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */, 5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */,
5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */, 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */,
5CB924E127A867BA00ACCCDD /* SettingsProfile.swift in Sources */, 5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */,
5C764E80279C7276000C6508 /* dummy.m in Sources */, 5C764E80279C7276000C6508 /* dummy.m in Sources */,
5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */, 5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */,
5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */, 5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */,
@ -514,8 +522,8 @@
5C9FD96B27A56D4D0075386C /* JSON.swift in Sources */, 5C9FD96B27A56D4D0075386C /* JSON.swift in Sources */,
5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */, 5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */,
5CC1C99227A6C7F5000D9FF6 /* QRCode.swift in Sources */, 5CC1C99227A6C7F5000D9FF6 /* QRCode.swift in Sources */,
5CCD403127A5F1C600368C90 /* ChatHeaderView.swift in Sources */, 5CCD403127A5F1C600368C90 /* ChatListToolbar.swift in Sources */,
5CA05A4F279752D00002BEB4 /* MessageView.swift in Sources */, 5CB9250D27A9432000ACCCDD /* ChatListNavLink.swift in Sources */,
5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */, 5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */,
5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */, 5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */,
5CA05A4C27974EB60002BEB4 /* WelcomeView.swift in Sources */, 5CA05A4C27974EB60002BEB4 /* WelcomeView.swift in Sources */,
@ -538,7 +546,7 @@
files = ( files = (
5C6AD81427A834E300348BD7 /* NewChatButton.swift in Sources */, 5C6AD81427A834E300348BD7 /* NewChatButton.swift in Sources */,
5CB924D827A8563F00ACCCDD /* SettingsView.swift in Sources */, 5CB924D827A8563F00ACCCDD /* SettingsView.swift in Sources */,
5CB924E227A867BA00ACCCDD /* SettingsProfile.swift in Sources */, 5CB924E227A867BA00ACCCDD /* UserProfile.swift in Sources */,
5C764E81279C7276000C6508 /* dummy.m in Sources */, 5C764E81279C7276000C6508 /* dummy.m in Sources */,
5CB924E527A8683A00ACCCDD /* UserAddress.swift in Sources */, 5CB924E527A8683A00ACCCDD /* UserAddress.swift in Sources */,
5C063D2827A4564100AEC577 /* ChatPreviewView.swift in Sources */, 5C063D2827A4564100AEC577 /* ChatPreviewView.swift in Sources */,
@ -546,8 +554,8 @@
5C9FD96C27A56D4D0075386C /* JSON.swift in Sources */, 5C9FD96C27A56D4D0075386C /* JSON.swift in Sources */,
5C9FD96F27A5D6ED0075386C /* SendMessageView.swift in Sources */, 5C9FD96F27A5D6ED0075386C /* SendMessageView.swift in Sources */,
5CC1C99327A6C7F5000D9FF6 /* QRCode.swift in Sources */, 5CC1C99327A6C7F5000D9FF6 /* QRCode.swift in Sources */,
5CCD403227A5F1C600368C90 /* ChatHeaderView.swift in Sources */, 5CCD403227A5F1C600368C90 /* ChatListToolbar.swift in Sources */,
5CA05A50279752D00002BEB4 /* MessageView.swift in Sources */, 5CB9250E27A9432000ACCCDD /* ChatListNavLink.swift in Sources */,
5CA059EE279559F40002BEB4 /* ContentView.swift in Sources */, 5CA059EE279559F40002BEB4 /* ContentView.swift in Sources */,
5CCD403527A5F6DF00368C90 /* AddContactView.swift in Sources */, 5CCD403527A5F6DF00368C90 /* AddContactView.swift in Sources */,
5CA05A4D27974EB60002BEB4 /* WelcomeView.swift in Sources */, 5CA05A4D27974EB60002BEB4 /* WelcomeView.swift in Sources */,