ios: groups ui wip (#809)

This commit is contained in:
JRoberts
2022-07-14 16:40:32 +04:00
committed by GitHub
parent 01eff43585
commit 414b174e32
11 changed files with 275 additions and 70 deletions
+5
View File
@@ -24,6 +24,7 @@ final class ChatModel: ObservableObject {
@Published var chatId: String?
@Published var chatItems: [ChatItem] = []
@Published var chatToTop: String?
// @Published var groups: Dictionary<ChatId, SimpleXChat.Group> = [:]
// items in the terminal view
@Published var terminalItems: [TerminalItem] = []
@Published var userAddress: String?
@@ -124,6 +125,10 @@ final class ChatModel: ObservableObject {
}
}
// func addGroup(_ group: SimpleXChat.Group) {
// groups[group.groupInfo.id] = group
// }
func addChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
// update previews
if let i = getChatIndex(cInfo.id) {
+6
View File
@@ -520,6 +520,12 @@ private func sendCommandOkResp(_ cmd: ChatCommand) async throws {
throw r
}
func apiNewGroup(_ gp: GroupProfile) throws -> GroupInfo {
let r = chatSendCmdSync(.newGroup(groupProfile: gp))
if case let .groupCreated(groupInfo) = r { return groupInfo }
throw r
}
func initializeChat(start: Bool) throws {
logger.debug("initializeChat")
do {
+31 -44
View File
@@ -16,6 +16,7 @@ struct ChatInfoView: View {
@Binding var showChatInfo: Bool
@State var alert: ChatInfoViewAlert? = nil
@State var deletingContact: Contact?
var contact: Contact
enum ChatInfoViewAlert: Identifiable {
case deleteContactAlert
@@ -35,53 +36,39 @@ struct ChatInfoView: View {
Text(chat.chatInfo.fullName).font(.title)
.padding(.bottom)
VStack {
if case let .direct(contact) = chat.chatInfo {
HStack {
serverImage()
Text(chat.serverInfo.networkStatus.statusString)
.foregroundColor(.primary)
}
Text(chat.serverInfo.networkStatus.statusExplanation)
.font(.subheadline)
.multilineTextAlignment(.center)
.padding(.horizontal, 64)
.padding(.vertical, 8)
HStack {
serverImage()
Text(chat.serverInfo.networkStatus.statusString)
.foregroundColor(.primary)
}
Text(chat.serverInfo.networkStatus.statusExplanation)
.font(.subheadline)
.multilineTextAlignment(.center)
.padding(.horizontal, 64)
.padding(.vertical, 8)
Spacer()
Button() {
alert = .clearChatAlert
} label: {
Label("Clear conversation", systemImage: "gobackward")
}
.tint(Color.orange)
Button(role: .destructive) {
deletingContact = contact
alert = .deleteContactAlert
} label: {
Label("Delete contact", systemImage: "trash")
}
.padding()
}
else if case .group = chat.chatInfo {
Spacer()
Button() {
alert = .clearChatAlert
} label: {
Label("Clear conversation", systemImage: "gobackward")
}
.tint(Color.orange)
.padding()
}
Spacer()
Button() {
alert = .clearChatAlert
} label: {
Label("Clear conversation", systemImage: "gobackward")
}
.alert(item: $alert) { alertItem in
switch(alertItem) {
case .deleteContactAlert: return deleteContactAlert(deletingContact!)
case .clearChatAlert: return clearChatAlert()
}
.tint(Color.orange)
Button(role: .destructive) {
deletingContact = contact
alert = .deleteContactAlert
} label: {
Label("Delete contact", systemImage: "trash")
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.padding()
}
.alert(item: $alert) { alertItem in
switch(alertItem) {
case .deleteContactAlert: return deleteContactAlert(deletingContact!)
case .clearChatAlert: return clearChatAlert()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
func serverImage() -> some View {
@@ -131,6 +118,6 @@ struct ChatInfoView: View {
struct ChatInfoView_Previews: PreviewProvider {
static var previews: some View {
@State var showChatInfo = true
return ChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), showChatInfo: $showChatInfo)
return ChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), showChatInfo: $showChatInfo, contact: Contact.sampleData)
}
}
+5 -1
View File
@@ -103,7 +103,11 @@ struct ChatView: View {
ChatInfoToolbar(chat: chat)
}
.sheet(isPresented: $showChatInfo) {
ChatInfoView(chat: chat, showChatInfo: $showChatInfo)
if case let .direct(contact) = chat.chatInfo {
ChatInfoView(chat: chat, showChatInfo: $showChatInfo, contact: contact)
} else if case .group = chat.chatInfo {
GroupChatInfoView(chat: chat, showChatInfo: $showChatInfo)
}
}
}
ToolbarItem(placement: .navigationBarTrailing) {
@@ -0,0 +1,106 @@
//
// GroupChatInfoView.swift
// SimpleX (iOS)
//
// Created by JRoberts on 14.07.2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct GroupChatInfoView: View {
@EnvironmentObject var chatModel: ChatModel
@ObservedObject var alertManager = AlertManager.shared
@ObservedObject var chat: Chat
@Binding var showChatInfo: Bool
@State var alert: ChatInfoViewAlert? = nil
@State var deletingContact: Contact?
enum ChatInfoViewAlert: Identifiable {
case deleteContactAlert
case clearChatAlert
var id: ChatInfoViewAlert { get { self } }
}
var body: some View {
VStack{
ChatInfoImage(chat: chat)
.frame(width: 192, height: 192)
.padding(.top, 48)
.padding()
Text(chat.chatInfo.localDisplayName).font(.largeTitle)
.padding(.bottom, 2)
Text(chat.chatInfo.fullName).font(.title)
.padding(.bottom)
Spacer()
Button() {
alert = .clearChatAlert
} label: {
Label("Clear conversation", systemImage: "gobackward")
}
.tint(Color.orange)
.padding()
}
.alert(item: $alert) { alertItem in
switch(alertItem) {
case .deleteContactAlert: return deleteContactAlert(deletingContact!)
case .clearChatAlert: return clearChatAlert()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
func serverImage() -> some View {
let status = chat.serverInfo.networkStatus
return Image(systemName: status.imageName)
.foregroundColor(status == .connected ? .green : .secondary)
}
private func deleteContactAlert(_ contact: Contact) -> Alert {
Alert(
title: Text("Delete contact?"),
message: Text("Contact and all messages will be deleted - this cannot be undone!"),
primaryButton: .destructive(Text("Delete")) {
Task {
do {
try await apiDeleteChat(type: .direct, id: contact.apiId)
DispatchQueue.main.async {
chatModel.removeChat(contact.id)
showChatInfo = false
}
} catch let error {
logger.error("ChatInfoView.deleteContactAlert apiDeleteChat error: \(error.localizedDescription)")
}
}
},
secondaryButton: .cancel()
)
}
// TODO reuse between this and ChatInfoView
private func clearChatAlert() -> Alert {
Alert(
title: Text("Clear conversation?"),
message: Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."),
primaryButton: .destructive(Text("Clear")) {
Task {
await clearChat(chat)
DispatchQueue.main.async {
showChatInfo = false
}
}
},
secondaryButton: .cancel()
)
}
}
struct GroupChatInfoView_Previews: PreviewProvider {
static var previews: some View {
@State var showChatInfo = true
return GroupChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), showChatInfo: $showChatInfo)
}
}
@@ -0,0 +1,104 @@
//
// AddGroupView.swift
// SimpleX (iOS)
//
// Created by JRoberts on 13.07.2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct AddGroupView: View {
@Binding var openedSheet: NewChatAction?
@EnvironmentObject var m: ChatModel
@State private var displayName: String = ""
@State private var fullName: String = ""
@FocusState private var focusDisplayName
@FocusState private var focusFullName
var body: some View {
VStack(alignment: .leading) {
Text("Create group")
.font(.largeTitle)
.padding(.bottom, 4)
ZStack(alignment: .topLeading) {
if !validDisplayName(displayName) {
Image(systemName: "exclamationmark.circle")
.foregroundColor(.red)
.padding(.top, 4)
}
textField("Display name", text: $displayName)
.focused($focusDisplayName)
.submitLabel(.next)
.onSubmit {
if canCreateProfile() { focusFullName = true }
else { focusDisplayName = true }
}
}
textField("Full name (optional)", text: $fullName)
.focused($focusFullName)
.submitLabel(.go)
.onSubmit {
if canCreateProfile() { createGroup() }
else { focusFullName = true }
}
Spacer()
Button {
createGroup()
} label: {
Text("Create")
Image(systemName: "greaterthan")
}
.disabled(!canCreateProfile())
.frame(maxWidth: .infinity, alignment: .trailing)
}
.onAppear() {
focusDisplayName = true
}
.padding()
}
func textField(_ placeholder: LocalizedStringKey, text: Binding<String>) -> some View {
TextField(placeholder, text: text)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.padding(.leading, 28)
.padding(.bottom)
}
func createGroup() {
hideKeyboard()
let groupProfile = GroupProfile(
displayName: displayName,
fullName: fullName
)
do {
let groupInfo = try apiNewGroup(groupProfile)
m.addChat(Chat(chatInfo: .group(groupInfo: groupInfo), chatItems: []))
openedSheet = nil
DispatchQueue.main.async {
m.chatId = groupInfo.id
}
} catch {
fatalError("Failed to create group: \(responseError(error))")
}
}
func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
func canCreateProfile() -> Bool {
displayName != "" && validDisplayName(displayName)
}
}
struct AddGroupView_Previews: PreviewProvider {
static var previews: some View {
@State var openedSheet: NewChatAction? = nil
return AddGroupView(openedSheet: $openedSheet)
}
}
@@ -1,21 +0,0 @@
//
// CreateGroupView.swift
// SimpleX
//
// Created by Evgeny Poberezkin on 29/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
struct CreateGroupView: View {
var body: some View {
EmptyView()
}
}
struct CreateGroupView_Previews: PreviewProvider {
static var previews: some View {
CreateGroupView()
}
}
@@ -13,6 +13,7 @@ enum NewChatAction: Identifiable {
case createLink
case pasteLink
case scanQRCode
case createGroup
var id: NewChatAction { get { self } }
}
@@ -30,12 +31,14 @@ struct NewChatButton: View {
Button("Create link / QR code") { addContactAction() }
Button("Paste received link") { actionSheet = .pasteLink }
Button("Scan QR code") { actionSheet = .scanQRCode }
// Button("Create group") { actionSheet = .createGroup }
}
.sheet(item: $actionSheet) { sheet in
switch sheet {
case .createLink: AddContactView(connReqInvitation: connReq)
case .pasteLink: PasteToConnectView(openedSheet: $actionSheet)
case .scanQRCode: ScanToConnectView(openedSheet: $actionSheet)
case .createGroup: AddGroupView(openedSheet: $actionSheet)
}
}
}
@@ -94,6 +94,7 @@ struct MakeConnection: View {
case .createLink: AddContactView(connReqInvitation: connReq)
case .pasteLink: PasteToConnectView(openedSheet: $actionSheet)
case .scanQRCode: ScanToConnectView(openedSheet: $actionSheet)
case .createGroup: EmptyView() // TODO refactor / show during onboarding?
}
}
.onChange(of: actionSheet) { _ in checkOnboarding() }
+8 -4
View File
@@ -83,7 +83,6 @@
5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; };
5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; };
5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; };
5CCD403A27A5F9BE00368C90 /* CreateGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403927A5F9BE00368C90 /* CreateGroupView.swift */; };
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; };
5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@@ -112,6 +111,8 @@
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
640F50E327CF991C001E05C2 /* SMPServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640F50E227CF991C001E05C2 /* SMPServers.swift */; };
6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; };
6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */; };
6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6454036E2822A9750090DDFF /* ComposeFileView.swift */; };
646BB38C283BEEB9001CE359 /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */; };
646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */; };
@@ -260,7 +261,6 @@
5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = "<group>"; };
5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanToConnectView.swift; sourceTree = "<group>"; };
5CCD403927A5F9BE00368C90 /* CreateGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateGroupView.swift; sourceTree = "<group>"; };
5CDCAD452818589900503DA2 /* SimpleX NSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX NSE.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
5CDCAD472818589900503DA2 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
5CDCAD492818589900503DA2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -285,6 +285,8 @@
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = "<group>"; };
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; };
640F50E227CF991C001E05C2 /* SMPServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServers.swift; sourceTree = "<group>"; };
6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = "<group>"; };
6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatInfoView.swift; sourceTree = "<group>"; };
6454036E2822A9750090DDFF /* ComposeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeFileView.swift; sourceTree = "<group>"; };
646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/System/Library/Frameworks/LocalAuthentication.framework; sourceTree = DEVELOPER_DIR; };
646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationUtils.swift; sourceTree = "<group>"; };
@@ -380,6 +382,7 @@
5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */,
5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */,
5CE4407127ADB1D0007B033A /* Emoji.swift */,
6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */,
);
path = Chat;
sourceTree = "<group>";
@@ -506,8 +509,8 @@
5CCD403327A5F6DF00368C90 /* AddContactView.swift */,
5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */,
3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */,
5CCD403927A5F9BE00368C90 /* CreateGroupView.swift */,
5CC1C99127A6C7F5000D9FF6 /* QRCode.swift */,
6442E0B9287F169300CEC0F9 /* AddGroupView.swift */,
);
path = NewChat;
sourceTree = "<group>";
@@ -832,6 +835,7 @@
5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */,
5C9A5BDB2871E05400A5B906 /* SetNotificationsMode.swift in Sources */,
5CB0BA8E2827126500B3292C /* OnboardingView.swift in Sources */,
6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */,
5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */,
5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */,
5CC1C99227A6C7F5000D9FF6 /* QRCode.swift in Sources */,
@@ -851,6 +855,7 @@
5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */,
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */,
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */,
6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */,
5C971E2127AEBF8300C8A3CE /* ChatInfoImage.swift in Sources */,
5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */,
6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */,
@@ -863,7 +868,6 @@
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */,
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */,
5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */,
5CCD403A27A5F9BE00368C90 /* CreateGroupView.swift in Sources */,
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */,
5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */,
5C971E1D27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */,
+6
View File
@@ -399,6 +399,12 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat {
}
public struct GroupProfile: Codable, NamedChat {
public init(displayName: String, fullName: String, image: String? = nil) {
self.displayName = displayName
self.fullName = fullName
self.image = image
}
public var displayName: String
public var fullName: String
public var image: String?