ios: connection UI (wip)

This commit is contained in:
spaced4ndy
2023-11-28 22:33:57 +04:00
parent 05a64c99a2
commit 5caee14eb2
4 changed files with 514 additions and 6 deletions

View File

@@ -16,6 +16,7 @@ struct ChatListView: View {
@State private var showAddChat = false
@State private var userPickerVisible = false
@State private var showConnectDesktop = false
@State private var showCreateGroupSheet = false
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
var body: some View {
@@ -62,11 +63,7 @@ struct ChatListView: View {
private var chatListView: some View {
VStack {
if chatModel.chats.count > 0 {
chatList.searchable(text: $searchText)
} else {
chatList
}
chatList.searchable(text: $searchText)
}
.onDisappear() { withAnimation { userPickerVisible = false } }
.refreshable {
@@ -88,6 +85,9 @@ struct ChatListView: View {
.offset(x: -8)
.listStyle(.plain)
.navigationBarTitleDisplayMode(.inline)
.sheet(isPresented: $showCreateGroupSheet) {
AddGroupView()
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
let user = chatModel.currentUser ?? User.sampleData
@@ -124,7 +124,12 @@ struct ChatListView: View {
}
ToolbarItem(placement: .navigationBarTrailing) {
switch chatModel.chatRunning {
case .some(true): NewChatButton(showAddChat: $showAddChat)
// case .some(true): NewChatButton(showAddChat: $showAddChat)
case .some(true):
HStack {
createGroupButton()
NewChatButton2()
}
case .some(false): chatStoppedIcon()
case .none: EmptyView()
}
@@ -132,6 +137,17 @@ struct ChatListView: View {
}
}
private func createGroupButton() -> some View {
Button {
showCreateGroupSheet = true
} label: {
Image(systemName: "person.2")
.resizable()
.scaledToFit()
.frame(width: 28, height: 28)
}
}
private func toggleFilterButton() -> some View {
Button {
showUnreadAndFavorites = !showUnreadAndFavorites

View File

@@ -0,0 +1,53 @@
//
// NewChatButton2.swift
// SimpleX (iOS)
//
// Created by spaced4ndy on 28.11.2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
enum NewChatSheet: Identifiable {
case newChat(link: String, connection: PendingContactConnection)
var id: String {
switch self {
case let .newChat(link, _): return "newChat \(link)"
}
}
}
struct NewChatButton2: View {
@State private var actionSheet: NewChatSheet?
var body: some View {
Button {
addContactAction()
} label: {
Image(systemName: "square.and.pencil")
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
}
.sheet(item: $actionSheet) { sheet in
switch sheet {
case let .newChat(link, pcc):
NewChatView(selection: .invite, connReqInvitation: link, contactConnection: pcc)
}
}
}
func addContactAction() {
Task {
if let (connReq, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) {
actionSheet = .newChat(link: connReq, connection: pcc)
}
}
}
}
//#Preview {
// NewChatButton2()
//}

View File

@@ -0,0 +1,431 @@
//
// NewChatView.swift
// SimpleX (iOS)
//
// Created by spaced4ndy on 28.11.2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
import CodeScanner
enum NewChatOption: Identifiable {
case invite
case connect
var id: Self { self }
}
struct NewChatView: View {
@EnvironmentObject var m: ChatModel
@State var selection: NewChatOption
@State var connReqInvitation: String
@State var contactConnection: PendingContactConnection?
@State private var creatingConnReq = false
var body: some View {
NavigationView {
VStack(alignment: .leading) {
Text("Start a New Chat")
.font(.largeTitle)
.bold()
.fixedSize(horizontal: false, vertical: true)
.padding(.vertical)
Picker("New chat", selection: $selection) {
Label("Invite", systemImage: "link")
.tag(NewChatOption.invite)
Label("Connect", systemImage: "qrcode")
.tag(NewChatOption.connect)
}
.pickerStyle(.segmented)
switch selection {
case .invite: InviteView(
contactConnection: $contactConnection,
connReqInvitation: connReqInvitation
)
case .connect: ConnectView()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.padding()
.background(Color(.systemGroupedBackground))
.onChange(of: selection) { sel in
if case .connect = sel,
connReqInvitation == "" && contactConnection == nil && !creatingConnReq {
createInvitation()
}
}
.onAppear { m.connReqInv = connReqInvitation }
.onDisappear { m.connReqInv = nil }
}
}
private func createInvitation() {
creatingConnReq = true
Task {
if let (connReq, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) {
await MainActor.run {
connReqInvitation = connReq
contactConnection = pcc
m.connReqInv = connReq
}
} else {
await MainActor.run {
creatingConnReq = false
}
}
}
}
}
struct InviteView: View {
@EnvironmentObject private var chatModel: ChatModel
@Binding var contactConnection: PendingContactConnection?
var connReqInvitation: String
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
var body: some View {
GeometryReader { geo in
ScrollView {
VStack(alignment: .leading, spacing: 8) {
if connReqInvitation != "" {
HStack {
Text("Share this unique invite link")
.textCase(.uppercase)
.font(.footnote)
.foregroundColor(.secondary)
Spacer()
Button {
copyLink()
} label: {
Text("Copy")
.font(.footnote)
}
}
.padding(.horizontal)
Text(simplexChatLink(connReqInvitation))
.lineLimit(2)
.font(.callout)
.padding(.horizontal)
.padding(.vertical, 8)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(uiColor: .systemBackground))
)
Text("Or show this code")
.textCase(.uppercase)
.font(.footnote)
.foregroundColor(.secondary)
.padding(.horizontal)
.padding(.top, 8)
VStack(alignment: .center) {
SimpleXLinkQRCode(uri: connReqInvitation)
.padding(.horizontal)
.padding(.vertical, 10)
.frame(width: geo.size.width * 0.8)
}
.frame(maxWidth: .infinity, alignment: .center)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(uiColor: .systemBackground))
)
VStack(alignment: .leading) {
IncognitoToggle(incognitoEnabled: $incognitoDefault)
Divider()
shareLinkButton2(connReqInvitation)
}
.padding(.horizontal)
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(uiColor: .systemBackground))
)
.padding(.top)
VStack(alignment: .center) {
oneTimeLinkLearnMoreButton2()
}
.frame(maxWidth: .infinity, alignment: .center)
.padding(.top)
} else {
Text("Creating link…")
.textCase(.uppercase)
.font(.footnote)
.foregroundColor(.secondary)
.padding(.horizontal)
VStack(alignment: .center) {
ProgressView()
.progressViewStyle(.circular)
.scaleEffect(2)
.frame(maxWidth: .infinity)
.padding(.horizontal)
.padding(.vertical, 8)
}
.frame(maxWidth: .infinity, alignment: .center)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(uiColor: .systemBackground))
)
}
}
}
.padding(.vertical)
.onAppear { chatModel.connReqInv = connReqInvitation }
.onChange(of: incognitoDefault) { incognito in
Task {
do {
if let contactConn = contactConnection,
let conn = try await apiSetConnectionIncognito(connId: contactConn.pccConnId, incognito: incognito) {
await MainActor.run {
contactConnection = conn
chatModel.updateContactConnection(conn)
}
}
} catch {
logger.error("apiSetConnectionIncognito error: \(responseError(error))")
}
}
}
}
}
private func copyLink() {
UIPasteboard.general.string = simplexChatLink(connReqInvitation)
}
}
struct ConnectView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@State private var connectionLink: String = ""
@State private var alert: PlanAndConnectAlert?
@State private var sheet: PlanAndConnectActionSheet?
@State private var showScanQRCodeSheet = false
@State private var scannedLink: String = ""
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 8) {
(
Text("Paste the URL your received from your contact.")
+ Text("\n\n")
+ Text("You'll be connected to begin a private conversation with them.")
)
.font(.footnote)
.foregroundColor(.secondary)
.padding(.horizontal)
HStack {
Text("Unique invite link")
.textCase(.uppercase)
.font(.footnote)
.foregroundColor(.secondary)
Spacer()
if (connectionLink != "") {
Button {
clearLink()
} label: {
Text("Clear")
.font(.footnote)
}
}
}
.padding(.horizontal)
.padding(.top)
pasteLinkView()
Text("Or scan QR code")
.textCase(.uppercase)
.font(.footnote)
.foregroundColor(.secondary)
.padding(.horizontal)
.padding(.top, 8)
VStack(alignment: .center) {
scanQRCodeButton()
}
.padding(.horizontal)
.padding(.vertical, 8)
.frame(maxWidth: .infinity, alignment: .center)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(uiColor: .systemBackground))
)
VStack(alignment: .center) {
oneTimeLinkLearnMoreButton2()
}
.frame(maxWidth: .infinity, alignment: .center)
.padding(.top)
}
}
.padding(.vertical)
.alert(item: $alert) { a in planAndConnectAlert(a, dismiss: true) }
.actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) }
.sheet(isPresented: $showScanQRCodeSheet) {
if #available(iOS 16.0, *) {
ScanConnectionCodeView(scannedLink: $scannedLink)
.presentationDetents([.fraction(0.8)])
} else {
ScanConnectionCodeView(scannedLink: $scannedLink)
}
}
.onChange(of: scannedLink) { link in
connect(link)
}
}
private func clearLink() {
connectionLink = ""
}
@ViewBuilder private func pasteLinkView() -> some View {
if connectionLink == "" {
VStack(alignment: .center) {
ZStack {
Text("\n")
.font(.callout)
.padding(.horizontal)
.padding(.vertical, 8)
.opacity(0)
Button {
if let link = UIPasteboard.general.string {
// TODO test pasted text is a link, alert if not
connectionLink = link.trimmingCharacters(in: .whitespaces)
connect(connectionLink)
}
} label: {
Text("Click Here to Paste Link")
.foregroundColor(.accentColor)
.padding(.horizontal)
.padding(.vertical, 8)
}
}
}
.frame(maxWidth: .infinity, alignment: .center)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(uiColor: .systemBackground))
)
} else {
VStack() {
ZStack {
Text("\n")
.font(.callout)
.padding(.horizontal)
.padding(.vertical, 8)
.opacity(0)
Text(connectionLink)
.lineLimit(2)
.font(.callout)
.padding(.horizontal)
.padding(.vertical, 8)
}
}
.frame(maxWidth: .infinity)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(uiColor: .systemBackground))
)
}
}
private func connect(_ link: String) {
planAndConnect(
link,
showAlert: { alert = $0 },
showActionSheet: { sheet = $0 },
dismiss: true,
incognito: nil
)
}
private func scanQRCodeButton() -> some View {
Button {
showScanQRCodeSheet = true
} label: {
settingsRow("qrcode") {
Text("Scan code")
}
}
}
}
struct ScanConnectionCodeView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@Binding var scannedLink: String
var body: some View {
VStack(alignment: .leading) {
Text("Scan QR code")
.font(.largeTitle)
.bold()
.fixedSize(horizontal: false, vertical: true)
.padding(.vertical)
CodeScannerView(codeTypes: [.qr], completion: processQRCode)
.aspectRatio(1, contentMode: .fit)
.cornerRadius(12)
.padding(.top)
Text("If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.")
.padding(.top)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.padding()
}
private func processQRCode(_ resp: Result<ScanResult, ScanError>) {
switch resp {
case let .success(r):
scannedLink = r.string
dismiss()
case let .failure(e):
logger.error("ConnectContactView.processQRCode QR code error: \(e.localizedDescription)")
// TODO alert
dismiss()
}
}
}
// TODO move IncognitoToggle here
// TODO move shareLinkButton here
func shareLinkButton2(_ connReqInvitation: String) -> some View {
Button {
showShareSheet(items: [simplexChatLink(connReqInvitation)])
} label: {
settingsRow("square.and.arrow.up") {
Text("Share link")
}
}
}
// TODO move oneTimeLinkLearnMoreButton here
func oneTimeLinkLearnMoreButton2() -> some View {
NavigationLink {
AddContactLearnMore()
.navigationTitle("One-time invitation link")
.navigationBarTitleDisplayMode(.large)
} label: {
settingsRow("info.circle") {
Text("Need Guidance?")
.underline()
}
}
}
// TODO move planAndConnect here
//#Preview {
// NewChatView()
//}

View File

@@ -178,6 +178,8 @@
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; };
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; };
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
64AEA4ED2B15D2A400334292 /* NewChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AEA4EC2B15D2A400334292 /* NewChatView.swift */; };
64AEA4EF2B15FEE100334292 /* NewChatButton2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AEA4EE2B15FEE100334292 /* NewChatButton2.swift */; };
64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; };
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; };
64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
@@ -462,6 +464,8 @@
649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = "<group>"; };
64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = "<group>"; };
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; };
64AEA4EC2B15D2A400334292 /* NewChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatView.swift; sourceTree = "<group>"; };
64AEA4EE2B15FEE100334292 /* NewChatButton2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatButton2.swift; sourceTree = "<group>"; };
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = "<group>"; };
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = "<group>"; };
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
@@ -736,6 +740,8 @@
5CB2085028DB64CA00D024EC /* CreateLinkView.swift */,
5CB2085228DB7CAF00D024EC /* ConnectViaLinkView.swift */,
64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */,
64AEA4EC2B15D2A400334292 /* NewChatView.swift */,
64AEA4EE2B15FEE100334292 /* NewChatButton2.swift */,
);
path = NewChat;
sourceTree = "<group>";
@@ -1115,12 +1121,14 @@
5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */,
644EFFE0292CFD7F00525D5B /* CIVoiceView.swift in Sources */,
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */,
64AEA4EF2B15FEE100334292 /* NewChatButton2.swift in Sources */,
5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */,
5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */,
5CBD285A295711D700EC2CF4 /* ImageUtils.swift in Sources */,
6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */,
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */,
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */,
64AEA4ED2B15D2A400334292 /* NewChatView.swift in Sources */,
5C029EAA283942EA004A9677 /* CallController.swift in Sources */,
5CBE6C142944CC12002D9531 /* ScanCodeView.swift in Sources */,
5CC036E029C488D500C0EF20 /* HiddenProfileView.swift in Sources */,