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 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 {
}

View file

@ -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<T: Encodable>(_ 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
}

View file

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

View file

@ -3,7 +3,7 @@
version = "3.0">
<TimelineItems>
<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"
shouldTrackSuperviewWidth = "NO">
</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 {
ScrollView {
Text(item.details)
.textSelection(.enabled)
}
} 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(.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 {
@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)
}
}

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)
.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)

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: {
Label("Share", systemImage: "square.and.arrow.up")
}
.padding()
.shareSheet(isPresented: $shareInvitation, items: [connReqInvitation])
.padding()
.shareSheet(isPresented: $shareInvitation, items: [connReqInvitation])
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = "<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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
@ -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 = "<group>";
@ -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 = "<group>";
};
5CB9250B27A942F300ACCCDD /* ChatList */ = {
isa = PBXGroup;
children = (
5C2E260A27A30CFA00F70299 /* ChatListView.swift */,
5C2E261127A30FEA00F70299 /* TerminalView.swift */,
5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */,
);
path = ChatList;
sourceTree = "<group>";
};
/* 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 */,