iOS: chats and messages layout (#241)

* iOS: chats and messages layout

* model update for updated API

* improve chat list view

* chat view layouts

* delete contacts

* larger headers, clean up, move message reception loop to ContentView

* settings: user profile
This commit is contained in:
Evgeny Poberezkin 2022-01-31 21:28:07 +00:00 committed by GitHub
parent 6d5b5ab44f
commit 53040dbe1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 643 additions and 221 deletions

View File

@ -5,73 +5,25 @@
// Created by Evgeny Poberezkin on 17/01/2022.
//
//import SwiftUI
//struct ContentView: View {
// @State var messages: [String] = ["Start session:"]
// @State var text: String = ""
//
// func sendMessage() {
// }
//
// var body: some View {
// VStack {
// ScrollView {
// LazyVStack {
// ForEach(messages, id: \.self) { msg in
// MessageView(message: msg, sent: false)
// }
// }
// .padding(10)
// }
// .frame(minWidth: 0,
// maxWidth: .infinity,
// minHeight: 0,
// maxHeight: .infinity,
// alignment: .topLeading)
// HStack {
// TextField("Message...", text: $text)
// .textFieldStyle(RoundedBorderTextFieldStyle())
// .frame(minHeight: CGFloat(30))
// Button(action: sendMessage) {
// Text("Send")
// }.disabled(text.isEmpty)
// }
// .frame(minHeight: CGFloat(30))
// .padding()
// }
// }
//}
import SwiftUI
struct ContentView: View {
@EnvironmentObject var chatModel: ChatModel
// var chatStore: chat_store
// private let controller: chat_controller
// init(chatStore: chat_store) {
// self.chatStore = chatStore
// }
// @State private var logbuffer = [String]()
// @State private var chatcmd: String = ""
// @State private var chatlog: String = ""
// @FocusState private var focused: Bool
//
// func addLine(line: String) {
// print(line)
// logbuffer.append(line)
// if(logbuffer.count > 50) { _ = logbuffer.dropFirst() }
// chatlog = logbuffer.joined(separator: "\n")
// }
var body: some View {
if let user = chatModel.currentUser {
ChatListView(user: user)
.onAppear {
DispatchQueue.global().async {
while(true) {
do {
try processReceivedMsg(chatModel, chatRecvMsg())
} catch {
print("error receiving message: ", error)
}
}
}
do {
let chats = try apiGetChats()
chatModel.chatPreviews = chats
@ -82,29 +34,6 @@ struct ContentView: View {
} else {
WelcomeView()
}
// return VStack {
// ScrollView {
// VStack(alignment: .leading) {
// HStack { Spacer() }
// Text(chatlog)
// .lineLimit(nil)
// .font(.system(.body, design: .monospaced))
// }
// .frame(maxWidth: .infinity)
// }
//
// TextField("Chat command", text: $chatcmd)
// .focused($focused)
// .onSubmit {
// print(chatcmd)
// var cCmd = chatcmd.cString(using: .utf8)!
// print(String.init(cString: chat_send_cmd(controller, &cCmd)))
// }
// .textInputAutocapitalization(.never)
// .disableAutocorrection(true)
// .padding()
// }
}
}

View File

@ -8,21 +8,30 @@
import Foundation
import Combine
import SwiftUI
final class ChatModel: ObservableObject {
@Published var currentUser: User?
@Published var chats: Dictionary<String, Chat> = [:]
@Published var chatPreviews: [ChatPreview] = []
@Published var chatPreviews: [Chat] = []
@Published var chatItems: [ChatItem] = []
@Published var terminalItems: [TerminalItem] = []
}
struct User: Codable {
class User: Codable {
var userId: Int64
var userContactId: Int64
var localDisplayName: ContactName
var profile: Profile
var activeUser: Bool
internal init(userId: Int64, userContactId: Int64, localDisplayName: ContactName, profile: Profile, activeUser: Bool) {
self.userId = userId
self.userContactId = userContactId
self.localDisplayName = localDisplayName
self.profile = profile
self.activeUser = activeUser
}
}
let sampleUser = User(
@ -47,15 +56,6 @@ let sampleProfile = Profile(
fullName: "Alice"
)
struct ChatPreview: Identifiable, Decodable {
var chatInfo: ChatInfo
var lastChatItem: ChatItem?
var id: String {
get { chatInfo.id }
}
}
enum ChatType: String {
case direct = "@"
case group = "#"
@ -106,7 +106,7 @@ let sampleDirectChatInfo = ChatInfo.direct(contact: sampleContact)
let sampleGroupChatInfo = ChatInfo.group(groupInfo: sampleGroupInfo)
class Chat: Decodable {
class Chat: Decodable, Identifiable {
var chatInfo: ChatInfo
var chatItems: [ChatItem]
@ -114,6 +114,8 @@ class Chat: Decodable {
self.chatInfo = chatInfo
self.chatItems = chatItems
}
var id: String { get { chatInfo.id } }
}
struct Contact: Identifiable, Codable {
@ -172,11 +174,30 @@ struct ChatItem: Identifiable, Decodable {
var id: Int64 { get { meta.itemId } }
}
func chatItemSample(_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String) -> ChatItem {
ChatItem(
chatDir: dir,
meta: ciMetaSample(id, ts, text),
content: .sndMsgContent(msgContent: .text(text))
)
}
enum CIDirection: Decodable {
case directSnd
case directRcv
case groupSnd
case groupRcv(GroupMember)
var sent: Bool {
get {
switch self {
case .directSnd: return true
case .directRcv: return false
case .groupSnd: return true
case .groupRcv: return false
}
}
}
}
struct CIMeta: Decodable {
@ -186,6 +207,15 @@ struct CIMeta: Decodable {
var createdAt: Date
}
func ciMetaSample(_ id: Int64, _ ts: Date, _ text: String) -> CIMeta {
CIMeta(
itemId: id,
itemTs: ts,
itemText: text,
createdAt: ts
)
}
enum CIContent: Decodable {
case sndMsgContent(msgContent: MsgContent)
case rcvMsgContent(msgContent: MsgContent)

View File

@ -20,6 +20,8 @@ enum ChatCommand {
case apiSendMessage(type: ChatType, id: Int64, msg: MsgContent)
case addContact
case connect(connReq: String)
case apiDeleteChat(type: ChatType, id: Int64)
case apiUpdateProfile(profile: Profile)
case string(String)
var cmdString: String {
@ -35,6 +37,10 @@ enum ChatCommand {
return "/c"
case let .connect(connReq):
return "/c \(connReq)"
case let .apiDeleteChat(type, id):
return "/_del \(type.rawValue)\(id)"
case let .apiUpdateProfile(profile):
return "/p \(profile.displayName) \(profile.fullName)"
case let .string(str):
return str
}
@ -48,11 +54,14 @@ struct APIResponse: Decodable {
enum ChatResponse: Decodable, Error {
case response(type: String, json: String)
case apiChats(chats: [ChatPreview])
case apiChats(chats: [Chat])
case apiChat(chat: Chat)
case invitation(connReqInvitation: String)
case sentConfirmation
case sentInvitation
case contactDeleted(contact: Contact)
case userProfileNoChange
case userProfileUpdated(fromProfile: Profile, toProfile: Profile)
// case newSentInvitation
case contactConnected(contact: Contact)
case newChatItem(chatItem: AChatItem)
@ -66,6 +75,9 @@ enum ChatResponse: Decodable, Error {
case .invitation: return "invitation"
case .sentConfirmation: return "sentConfirmation"
case .sentInvitation: return "sentInvitation"
case .contactDeleted: return "contactDeleted"
case .userProfileNoChange: return "userProfileNoChange"
case .userProfileUpdated: return "userProfileNoChange"
case .contactConnected: return "contactConnected"
case .newChatItem: return "newChatItem"
}
@ -81,6 +93,9 @@ enum ChatResponse: Decodable, Error {
case let .invitation(connReqInvitation): return connReqInvitation
case .sentConfirmation: return "sentConfirmation: no details"
case .sentInvitation: return "sentInvitation: no details"
case let .contactDeleted(contact): return String(describing: contact)
case .userProfileNoChange: return "userProfileNoChange: no details"
case let .userProfileUpdated(_, toProfile): return String(describing: toProfile)
case let .contactConnected(contact): return String(describing: contact)
case let .newChatItem(chatItem): return String(describing: chatItem)
}
@ -156,7 +171,7 @@ func chatRecvMsg() throws -> ChatResponse {
chatResponse(chat_recv_msg(getChatCtrl())!)
}
func apiGetChats() throws -> [ChatPreview] {
func apiGetChats() throws -> [Chat] {
let r = try chatSendCmd(.apiGetChats)
if case let .apiChats(chats) = r { return chats }
throw r
@ -189,13 +204,28 @@ func apiConnect(connReq: String) throws {
}
}
func apiDeleteChat(type: ChatType, id: Int64) throws {
let r = try chatSendCmd(.apiDeleteChat(type: type, id: id))
if case .contactDeleted = r { return }
throw r
}
func apiUpdateProfile(profile: Profile) throws -> Profile? {
let r = try chatSendCmd(.apiUpdateProfile(profile: profile))
switch r {
case .userProfileNoChange: return nil
case let .userProfileUpdated(_, toProfile): return toProfile
default: 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(
ChatPreview(chatInfo: .direct(contact: contact)),
Chat(chatInfo: .direct(contact: contact), chatItems: []),
at: 0
)
case let .newChatItem(aChatItem):
@ -204,7 +234,7 @@ func processReceivedMsg(_ chatModel: ChatModel, _ res: ChatResponse) {
chatModel.chats[ci.id] = chat
chat.chatItems.append(aChatItem.chatItem)
default:
print("unsupported response: ", res)
print("unsupported response: ", res.responseType)
}
}
}
@ -216,7 +246,6 @@ private struct UserResponse: Decodable {
private func chatResponse(_ cjson: UnsafePointer<CChar>) -> ChatResponse {
let s = String.init(cString: cjson)
print("chatResponse", s)
let d = s.data(using: .utf8)!
// TODO is there a way to do it without copying the data? e.g:
// let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson))
@ -270,7 +299,6 @@ private func getChatCtrl() -> chat_ctrl {
private func decodeCJSON<T: Decodable>(_ cjson: UnsafePointer<CChar>) -> T? {
let s = String.init(cString: cjson)
print("decodeCJSON", s)
let d = s.data(using: .utf8)!
// let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson))
// let d = Data.init(bytesNoCopy: p, count: strlen(cjson), deallocator: .free)
@ -286,6 +314,5 @@ private func getJSONObject(_ cjson: UnsafePointer<CChar>) -> NSDictionary? {
private func encodeCJSON<T: Encodable>(_ value: T) -> [CChar] {
let data = try! jsonEncoder.encode(value)
let str = String(decoding: data, as: UTF8.self)
print("encodeCJSON", str)
return str.cString(using: .utf8)!
}

View File

@ -10,19 +10,13 @@ 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 {
DispatchQueue.global().async {
while(true) {
do {
try processReceivedMsg(chatModel, chatRecvMsg())
} catch {
print("error receiving message: ", error)
}
}
}
return VStack {
// if chatModel.chats.isEmpty {
// VStack {
@ -31,8 +25,8 @@ struct ChatListView: View {
// }
// }
ChatHeaderView()
ChatHeaderView(chatId: $chatId)
NavigationView {
List {
NavigationLink {
@ -40,35 +34,86 @@ struct ChatListView: View {
} label: {
Text("Terminal")
}
ForEach(chatModel.chatPreviews) { chatPreview in
NavigationLink {
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)
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)
}
}, 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)
}
.navigationViewStyle(.stack)
}
}
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 = []
// return ChatListView(user: sampleUser)
// .environmentObject(chatModel)
// }
//}
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

@ -9,19 +9,55 @@
import SwiftUI
struct ChatPreviewView: View {
var chatPreview: ChatPreview
var chatPreview: Chat
var body: some View {
Text(chatPreview.chatInfo.localDisplayName)
let ci = chatPreview.chatItems.last
return VStack(spacing: 4) {
HStack(alignment: .top) {
Text(chatPreview.chatInfo.localDisplayName)
.font(.title3)
.fontWeight(.bold)
.padding(.leading, 8)
.padding(.top, 4)
.frame(maxHeight: .infinity, alignment: .topLeading)
Spacer()
if let ci = ci {
Text(getDateFormatter().string(from: ci.meta.itemTs))
.font(.subheadline)
.padding(.trailing, 8)
.padding(.top, 4)
.frame(minWidth: 60, alignment: .trailing)
.foregroundColor(.secondary)
}
}
if let ci = ci {
Text(ci.content.text)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading)
.padding([.leading, .trailing], 8)
.padding(.bottom, 4)
.padding(.top, 1)
}
}
}
}
struct ChatPreviewView_Previews: PreviewProvider {
static var previews: some View {
Group{
ChatPreviewView(chatPreview: ChatPreview(chatInfo: sampleDirectChatInfo))
ChatPreviewView(chatPreview: ChatPreview(chatInfo: sampleGroupChatInfo))
ChatPreviewView(chatPreview: Chat(
chatInfo: sampleDirectChatInfo,
chatItems: []
))
ChatPreviewView(chatPreview: Chat(
chatInfo: sampleDirectChatInfo,
chatItems: [chatItemSample(1, .directSnd, Date.now, "hello")]
))
ChatPreviewView(chatPreview: 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.")]
))
}
.previewLayout(.fixed(width: 300, height: 70))
.previewLayout(.fixed(width: 360, height: 80))
}
}

View File

@ -18,9 +18,9 @@ struct ChatView: View {
if let chat: Chat = chatModel.chats[chatInfo.id] {
VStack {
ScrollView {
LazyVStack {
ForEach(chat.chatItems) { chatItem in
Text(chatItem.content.text)
LazyVStack(spacing: 5) {
ForEach(chat.chatItems) {
ChatItemView(chatItem: $0)
}
}
}
@ -28,11 +28,13 @@ struct ChatView: View {
} else {
Text("unexpected: chat not found...")
}
Spacer()
Spacer(minLength: 0)
SendMessageView(sendMessage: sendMessage, inProgress: inProgress)
}
.edgesIgnoringSafeArea(.all)
.navigationBarHidden(true)
}
func sendMessage(_ msg: String) {
@ -53,7 +55,15 @@ struct ChatView_Previews: PreviewProvider {
chatModel.chats = [
"@1": Chat(
chatInfo: sampleDirectChatInfo,
chatItems: []
chatItems: [
chatItemSample(1, .directSnd, Date.now, "hello"),
chatItemSample(2, .directRcv, Date.now, "hi"),
chatItemSample(3, .directRcv, Date.now, "hi there"),
chatItemSample(4, .directRcv, Date.now, "hello again"),
chatItemSample(5, .directSnd, Date.now, "hi there!!!"),
chatItemSample(6, .directSnd, Date.now, "how are you?"),
chatItemSample(7, .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 ChatView(chatInfo: sampleDirectChatInfo)

View File

@ -9,79 +9,47 @@
import SwiftUI
struct ChatHeaderView: View {
@State private var showAddChat = false
@State private var addContact = false
@State private var addContactAlert = false
@State private var addContactError: Error?
@State private var connReqInvitation: String = ""
@State private var connectContact = false
@State private var connectAlert = false
@State private var connectError: Error?
@State private var createGroup = false
@Binding var chatId: String?
@EnvironmentObject var chatModel: ChatModel
var body: some View {
HStack {
Button("Edit", action: {})
Spacer()
Text("Your chats")
Spacer()
Button { showAddChat = true } label: {
Image(systemName: "square.and.pencil")
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()
}
.confirmationDialog("Start new chat", isPresented: $showAddChat, titleVisibility: .visible) {
Button("Add contact") { addContactAction() }
Button("Scan QR code") { connectContact = true }
Button("Create group") { createGroup = true }
}
.sheet(isPresented: $addContact, content: {
AddContactView(connReqInvitation: connReqInvitation)
})
.alert(isPresented: $addContactAlert) {
connectionError(addContactError)
}
.sheet(isPresented: $connectContact, content: {
connectContactSheet()
})
.alert(isPresented: $connectAlert) {
connectionError(connectError)
}
.sheet(isPresented: $createGroup, content: { CreateGroupView() })
}
.padding(.horizontal)
.padding(.top)
}
func addContactAction() {
do {
connReqInvitation = try apiAddContact()
addContact = true
} catch {
addContactAlert = true
addContactError = error
print(error)
}
}
func connectContactSheet() -> some View {
ConnectContactView(completed: { err in
connectContact = false
if err != nil {
connectAlert = true
connectError = err
}
})
}
func connectionError(_ error: Error?) -> Alert {
Alert(
title: Text("Connection error"),
message: Text(error?.localizedDescription ?? "")
)
.padding([.horizontal, .top])
}
}
struct ChatHeaderView_Previews: PreviewProvider {
static var previews: some View {
ChatHeaderView()
@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

@ -0,0 +1,63 @@
//
// ChatItemView.swift
// SimpleX
//
// Created by Evgeny Poberezkin on 30/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
private var dateFormatter: DateFormatter?
struct ChatItemView: View {
var chatItem: ChatItem
var body: some View {
let sent = chatItem.chatDir.sent
return VStack {
Group {
Text(chatItem.content.text)
.padding(.top, 8)
.padding(.horizontal, 12)
.frame(minWidth: 200, maxWidth: 300, alignment: .leading)
.foregroundColor(sent ? .white : .primary)
Text(getDateFormatter().string(from: chatItem.meta.itemTs))
.font(.subheadline)
.foregroundColor(sent ? .white : .secondary)
.padding(.bottom, 8)
.padding(.horizontal, 12)
.frame(minWidth: 200, maxWidth: 300, alignment: .trailing)
}
}
.background(sent ? .blue : Color(uiColor: .tertiarySystemGroupedBackground))
.cornerRadius(10)
.padding(.horizontal)
.frame(
minWidth: 200,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: sent ? .trailing : .leading
)
}
}
func getDateFormatter() -> DateFormatter {
if let df = dateFormatter { return df }
let df = DateFormatter()
df.dateFormat = "HH:mm"
dateFormatter = df
return df
}
struct ChatItemView_Previews: PreviewProvider {
static var previews: some View {
Group{
ChatItemView(chatItem: chatItemSample(1, .directSnd, Date.now, "hello"))
ChatItemView(chatItem: chatItemSample(2, .directRcv, Date.now, "hello there too"))
}
.previewLayout(.fixed(width: 300, height: 70))
}
}

View File

@ -0,0 +1,80 @@
//
// NewChatButton.swift
// SimpleX
//
// Created by Evgeny Poberezkin on 31/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
struct NewChatButton: View {
@State private var showAddChat = false
@State private var addContact = false
@State private var addContactAlert = false
@State private var addContactError: Error?
@State private var connReqInvitation: String = ""
@State private var connectContact = false
@State private var connectAlert = false
@State private var connectError: Error?
@State private var createGroup = false
var body: some View {
Button { showAddChat = true } label: {
Image(systemName: "square.and.pencil")
}
.confirmationDialog("Start new chat", isPresented: $showAddChat, titleVisibility: .visible) {
Button("Add contact") { addContactAction() }
Button("Scan QR code") { connectContact = true }
Button("Create group") { createGroup = true }
.disabled(true)
}
.sheet(isPresented: $addContact, content: {
AddContactView(connReqInvitation: connReqInvitation)
})
.alert(isPresented: $addContactAlert) {
connectionError(addContactError)
}
.sheet(isPresented: $connectContact, content: {
connectContactSheet()
})
.alert(isPresented: $connectAlert) {
connectionError(connectError)
}
.sheet(isPresented: $createGroup, content: { CreateGroupView() })
}
func addContactAction() {
do {
connReqInvitation = try apiAddContact()
addContact = true
} catch {
addContactAlert = true
addContactError = error
print(error)
}
}
func connectContactSheet() -> some View {
ConnectContactView(completed: { err in
connectContact = false
if err != nil {
connectAlert = true
connectError = err
}
})
}
func connectionError(_ error: Error?) -> Alert {
Alert(
title: Text("Connection error"),
message: Text(error?.localizedDescription ?? "")
)
}
}
struct NewChatButton_Previews: PreviewProvider {
static var previews: some View {
NewChatButton()
}
}

View File

@ -16,11 +16,10 @@ struct SendMessageView: View {
var body: some View {
HStack {
TextField("Message...", text: $command)
.textFieldStyle(RoundedBorderTextFieldStyle())
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.frame(minHeight: 30)
.onSubmit(submit)
.textFieldStyle(.roundedBorder)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.onSubmit(submit)
if (inProgress) {
ProgressView()
@ -31,7 +30,7 @@ struct SendMessageView: View {
}
}
.frame(minHeight: 30)
.padding()
.padding(12)
}
func submit() {

View File

@ -0,0 +1,21 @@
//
// ProfileHeader.swift
// SimpleX
//
// Created by Evgeny Poberezkin on 31/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
struct ProfileHeader: View {
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
}
}
struct ProfileHeader_Previews: PreviewProvider {
static var previews: some View {
ProfileHeader()
}
}

View File

@ -0,0 +1,28 @@
//
// SettingsButton.swift
// SimpleX
//
// Created by Evgeny Poberezkin on 31/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
struct SettingsButton: View {
@State private var showSettings = false
var body: some View {
Button { showSettings = true } label: {
Image(systemName: "gearshape")
}
.sheet(isPresented: $showSettings, content: {
SettingsView()
})
}
}
struct SettingsButton_Previews: PreviewProvider {
static var previews: some View {
SettingsButton()
}
}

View File

@ -0,0 +1,86 @@
//
// SettingsProfile.swift
// SimpleX
//
// Created by Evgeny Poberezkin on 31/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
struct SettingsProfile: View {
@EnvironmentObject var chatModel: ChatModel
@State private var profile = Profile(displayName: "", fullName: "")
@State private var editProfile: Bool = false
var body: some View {
let user: User = chatModel.currentUser!
return VStack(alignment: .leading) {
Text("Your chat profile")
.font(.title)
.padding(.bottom)
Text("Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile.")
.padding(.bottom)
if editProfile {
VStack(alignment: .leading) {
TextField("Display name", text: $profile.displayName)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.padding(.bottom)
TextField("Full name (optional)", text: $profile.fullName)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.padding(.bottom)
HStack(spacing: 20) {
Button("Cancel") { editProfile = false }
Button("Save (and notify contacts)") { saveProfile(user) }
}
}
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
} else {
VStack(alignment: .leading) {
HStack {
Text("Display name:")
Text(user.profile.displayName)
.fontWeight(.bold)
}
.padding(.bottom)
HStack {
Text("Full name:")
Text(user.profile.fullName)
.fontWeight(.bold)
}
.padding(.bottom)
Button("Edit") {
profile = user.profile
editProfile = true
}
}
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
}
}
.padding()
}
func saveProfile(_ user: User) {
do {
if let newProfile = try apiUpdateProfile(profile: profile) {
user.profile = newProfile
profile = newProfile
}
} catch {
print(error)
}
editProfile = false
}
}
struct SettingsProfile_Previews: PreviewProvider {
static var previews: some View {
let chatModel = ChatModel()
chatModel.currentUser = sampleUser
return SettingsProfile()
.environmentObject(chatModel)
}
}

View File

@ -0,0 +1,27 @@
//
// SettingsView.swift
// SimpleX
//
// Created by Evgeny Poberezkin on 31/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
struct SettingsView: View {
@EnvironmentObject var chatModel: ChatModel
var body: some View {
SettingsProfile()
UserAddress()
}
}
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
let chatModel = ChatModel()
chatModel.currentUser = sampleUser
return SettingsView()
.environmentObject(chatModel)
}
}

View File

@ -0,0 +1,21 @@
//
// UserAddress.swift
// SimpleX
//
// Created by Evgeny Poberezkin on 31/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
struct UserAddress: View {
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
}
}
struct UserAddress_Previews: PreviewProvider {
static var previews: some View {
UserAddress()
}
}

View File

@ -9,6 +9,8 @@
/* Begin PBXBuildFile section */
5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; };
5C063D2827A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; };
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; };
5C1A4C1F27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; };
5C1AEB86279F4A6400247F08 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C1AEB7F279F4A6400247F08 /* libffi.a */; };
5C1AEB87279F4A6400247F08 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C1AEB7F279F4A6400247F08 /* libffi.a */; };
5C1AEB88279F4A6400247F08 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C1AEB80279F4A6400247F08 /* libgmp.a */; };
@ -27,6 +29,8 @@
5C44B6A127A5FF22001C3154 /* libHSsimplex-chat-1.0.2-5okwuQXOXC78H7u8DMgS6w.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C44B69E27A5FF22001C3154 /* libHSsimplex-chat-1.0.2-5okwuQXOXC78H7u8DMgS6w.a */; };
5C44B6A227A5FF22001C3154 /* libHSsimplex-chat-1.0.2-5okwuQXOXC78H7u8DMgS6w-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C44B69F27A5FF22001C3154 /* libHSsimplex-chat-1.0.2-5okwuQXOXC78H7u8DMgS6w-ghc8.10.7.a */; };
5C44B6A327A5FF22001C3154 /* libHSsimplex-chat-1.0.2-5okwuQXOXC78H7u8DMgS6w-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C44B69F27A5FF22001C3154 /* libHSsimplex-chat-1.0.2-5okwuQXOXC78H7u8DMgS6w-ghc8.10.7.a */; };
5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; };
5C6AD81427A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; };
5C764E80279C7276000C6508 /* dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E7F279C7276000C6508 /* dummy.m */; };
5C764E81279C7276000C6508 /* dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E7F279C7276000C6508 /* dummy.m */; };
5C764E82279C748B000C6508 /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C764E7B279C71D4000C6508 /* libiconv.tbd */; };
@ -54,6 +58,14 @@
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 */; };
5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E327A8683A00ACCCDD /* UserAddress.swift */; };
5CB924E527A8683A00ACCCDD /* UserAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E327A8683A00ACCCDD /* UserAddress.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 */; };
@ -87,6 +99,7 @@
/* Begin PBXFileReference section */
5C063D2627A4564100AEC577 /* ChatPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreviewView.swift; sourceTree = "<group>"; };
5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemView.swift; sourceTree = "<group>"; };
5C1AEB7F279F4A6400247F08 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C1AEB80279F4A6400247F08 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C1AEB81279F4A6400247F08 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
@ -97,6 +110,7 @@
5C2E261127A30FEA00F70299 /* TerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalView.swift; sourceTree = "<group>"; };
5C44B69E27A5FF22001C3154 /* libHSsimplex-chat-1.0.2-5okwuQXOXC78H7u8DMgS6w.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.0.2-5okwuQXOXC78H7u8DMgS6w.a"; sourceTree = "<group>"; };
5C44B69F27A5FF22001C3154 /* libHSsimplex-chat-1.0.2-5okwuQXOXC78H7u8DMgS6w-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.0.2-5okwuQXOXC78H7u8DMgS6w-ghc8.10.7.a"; sourceTree = "<group>"; };
5C6AD81227A834E300348BD7 /* NewChatButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatButton.swift; sourceTree = "<group>"; };
5C764E7B279C71D4000C6508 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.2.sdk/usr/lib/libiconv.tbd; sourceTree = DEVELOPER_DIR; };
5C764E7C279C71DB000C6508 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.2.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; };
5C764E7D279C7275000C6508 /* SimpleX (iOS)-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SimpleX (iOS)-Bridging-Header.h"; sourceTree = "<group>"; };
@ -118,6 +132,10 @@
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>"; };
5CB924E327A8683A00ACCCDD /* UserAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddress.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>"; };
@ -191,12 +209,10 @@
children = (
5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */,
5CA05A4E279752D00002BEB4 /* MessageView.swift */,
5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */,
5CCD403027A5F1C600368C90 /* ChatHeaderView.swift */,
5CCD403327A5F6DF00368C90 /* AddContactView.swift */,
5CCD403627A5F9A200368C90 /* ConnectContactView.swift */,
5CCD403927A5F9BE00368C90 /* CreateGroupView.swift */,
5CC1C99127A6C7F5000D9FF6 /* QRCode.swift */,
5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */,
5CB924DF27A8678B00ACCCDD /* UserSettings */,
5CB924DD27A8622200ACCCDD /* NewChat */,
);
path = Helpers;
sourceTree = "<group>";
@ -297,6 +313,30 @@
path = "Tests macOS";
sourceTree = "<group>";
};
5CB924DD27A8622200ACCCDD /* NewChat */ = {
isa = PBXGroup;
children = (
5C6AD81227A834E300348BD7 /* NewChatButton.swift */,
5CCD403327A5F6DF00368C90 /* AddContactView.swift */,
5CCD403627A5F9A200368C90 /* ConnectContactView.swift */,
5CCD403927A5F9BE00368C90 /* CreateGroupView.swift */,
5CC1C99127A6C7F5000D9FF6 /* QRCode.swift */,
5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */,
);
path = NewChat;
sourceTree = "<group>";
};
5CB924DF27A8678B00ACCCDD /* UserSettings */ = {
isa = PBXGroup;
children = (
5CB924D327A853F100ACCCDD /* SettingsButton.swift */,
5CB924D627A8563F00ACCCDD /* SettingsView.swift */,
5CB924E327A8683A00ACCCDD /* UserAddress.swift */,
5CB924E027A867BA00ACCCDD /* SettingsProfile.swift */,
);
path = UserSettings;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -464,7 +504,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */,
5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */,
5CB924E127A867BA00ACCCDD /* SettingsProfile.swift in Sources */,
5C764E80279C7276000C6508 /* dummy.m in Sources */,
5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */,
5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */,
5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */,
5C9FD96B27A56D4D0075386C /* JSON.swift in Sources */,
@ -483,6 +527,8 @@
5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */,
5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */,
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */,
5CB924D427A853F100ACCCDD /* SettingsButton.swift in Sources */,
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -490,7 +536,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5C6AD81427A834E300348BD7 /* NewChatButton.swift in Sources */,
5CB924D827A8563F00ACCCDD /* SettingsView.swift in Sources */,
5CB924E227A867BA00ACCCDD /* SettingsProfile.swift in Sources */,
5C764E81279C7276000C6508 /* dummy.m in Sources */,
5CB924E527A8683A00ACCCDD /* UserAddress.swift in Sources */,
5C063D2827A4564100AEC577 /* ChatPreviewView.swift in Sources */,
5C2E261327A30FEA00F70299 /* TerminalView.swift in Sources */,
5C9FD96C27A56D4D0075386C /* JSON.swift in Sources */,
@ -509,6 +559,8 @@
5C764E8A279CBCB3000C6508 /* ChatModel.swift in Sources */,
5CC1C99627A6CF7F000D9FF6 /* ShareSheet.swift in Sources */,
5C2E260827A2941F00F70299 /* SimpleXAPI.swift in Sources */,
5CB924D527A853F100ACCCDD /* SettingsButton.swift in Sources */,
5C1A4C1F27A715B700EAD5AD /* ChatItemView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};