ios: create new group with incognito membership (#3284)

* ios: create new group with incognito membership

* layout

* fix button

* update layout

* layout

* layout

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
spaced4ndy 2023-10-26 18:51:45 +04:00 committed by GitHub
parent 4a5fdd3e0e
commit 7102723c23
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 191 additions and 92 deletions

View file

@ -1017,9 +1017,9 @@ private func sendCommandOkResp(_ cmd: ChatCommand) async throws {
throw r
}
func apiNewGroup(_ p: GroupProfile) throws -> GroupInfo {
func apiNewGroup(incognito: Bool, groupProfile: GroupProfile) throws -> GroupInfo {
let userId = try currentUserId("apiNewGroup")
let r = chatSendCmdSync(.apiNewGroup(userId: userId, groupProfile: p))
let r = chatSendCmdSync(.apiNewGroup(userId: userId, incognito: incognito, groupProfile: groupProfile))
if case let .groupCreated(_, groupInfo) = r { return groupInfo }
throw r
}

View file

@ -37,6 +37,10 @@ struct ChatView: View {
@FocusState private var searchFocussed
// opening GroupMemberInfoView on member icon
@State private var selectedMember: GroupMember? = nil
// opening GroupLinkView on link button (incognito)
@State private var showGroupLinkSheet: Bool = false
@State private var groupLink: String?
@State private var groupLinkMemberRole: GroupMemberRole = .member
var body: some View {
if #available(iOS 16.0, *) {
@ -173,9 +177,16 @@ struct ChatView: View {
HStack {
if groupInfo.canAddMembers {
if (chat.chatInfo.incognito) {
Image(systemName: "person.crop.circle.badge.plus")
.foregroundColor(Color(uiColor: .tertiaryLabel))
.onTapGesture { AlertManager.shared.showAlert(cantInviteIncognitoAlert()) }
groupLinkButton()
.appSheet(isPresented: $showGroupLinkSheet) {
GroupLinkView(
groupId: groupInfo.groupId,
groupLink: $groupLink,
groupLinkMemberRole: $groupLinkMemberRole,
showTitle: true,
creatingGroup: false
)
}
} else {
addMembersButton()
.appSheet(isPresented: $showAddMembersSheet) {
@ -417,7 +428,26 @@ struct ChatView: View {
Image(systemName: "person.crop.circle.badge.plus")
}
}
private func groupLinkButton() -> some View {
Button {
if case let .group(gInfo) = chat.chatInfo {
Task {
do {
if let link = try apiGetGroupLink(gInfo.groupId) {
(groupLink, groupLinkMemberRole) = link
}
} catch let error {
logger.error("ChatView apiGetGroupLink: \(responseError(error))")
}
showGroupLinkSheet = true
}
}
} label: {
Image(systemName: "link.badge.plus")
}
}
private func loadChatItems(_ cInfo: ChatInfo, _ ci: ChatItem, _ proxy: ScrollViewProxy) {
if let firstItem = chatModel.reversedChatItems.last, firstItem.id == ci.id {
if loadingItems || firstPage { return }

View file

@ -225,9 +225,15 @@ struct GroupChatInfoView: View {
private func groupLinkButton() -> some View {
NavigationLink {
GroupLinkView(groupId: groupInfo.groupId, groupLink: $groupLink, groupLinkMemberRole: $groupLinkMemberRole)
.navigationBarTitle("Group link")
.navigationBarTitleDisplayMode(.large)
GroupLinkView(
groupId: groupInfo.groupId,
groupLink: $groupLink,
groupLinkMemberRole: $groupLinkMemberRole,
showTitle: false,
creatingGroup: false
)
.navigationBarTitle("Group link")
.navigationBarTitleDisplayMode(.large)
} label: {
if groupLink == nil {
Label("Create group link", systemImage: "link.badge.plus")

View file

@ -13,6 +13,9 @@ struct GroupLinkView: View {
var groupId: Int64
@Binding var groupLink: String?
@Binding var groupLinkMemberRole: GroupMemberRole
var showTitle: Bool = false
var creatingGroup: Bool = false
var linkCreatedCb: (() -> Void)? = nil
@State private var creatingLink = false
@State private var alert: GroupLinkAlert?
@ -29,10 +32,35 @@ struct GroupLinkView: View {
}
var body: some View {
if creatingGroup {
NavigationView {
groupLinkView()
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button ("Continue") { linkCreatedCb?() }
}
}
}
} else {
groupLinkView()
}
}
private func groupLinkView() -> some View {
List {
Text("You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.")
.listRowBackground(Color.clear)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
Group {
if showTitle {
Text("Group link")
.font(.largeTitle)
.bold()
.fixedSize(horizontal: false, vertical: true)
}
Text("You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.")
}
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
Section {
if let groupLink = groupLink {
Picker("Initial role", selection: $groupLinkMemberRole) {
@ -48,8 +76,10 @@ struct GroupLinkView: View {
Label("Share link", systemImage: "square.and.arrow.up")
}
Button(role: .destructive) { alert = .deleteLink } label: {
Label("Delete link", systemImage: "trash")
if !creatingGroup {
Button(role: .destructive) { alert = .deleteLink } label: {
Label("Delete link", systemImage: "trash")
}
}
} else {
Button(action: createGroupLink) {

View file

@ -12,6 +12,7 @@ import SimpleXChat
struct AddGroupView: View {
@EnvironmentObject var m: ChatModel
@Environment(\.dismiss) var dismiss: DismissAction
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
@State private var chat: Chat?
@State private var groupInfo: GroupInfo?
@State private var profile = GroupProfile(displayName: "", fullName: "")
@ -21,18 +22,35 @@ struct AddGroupView: View {
@State private var showTakePhoto = false
@State private var chosenImage: UIImage? = nil
@State private var showInvalidNameAlert = false
@State private var groupLink: String?
@State private var groupLinkMemberRole: GroupMemberRole = .member
var body: some View {
if let chat = chat, let groupInfo = groupInfo {
AddGroupMembersViewCommon(
chat: chat,
groupInfo: groupInfo,
creatingGroup: true,
showFooterCounter: false
) { _ in
dismiss()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
m.chatId = groupInfo.id
if !groupInfo.membership.memberIncognito {
AddGroupMembersViewCommon(
chat: chat,
groupInfo: groupInfo,
creatingGroup: true,
showFooterCounter: false
) { _ in
dismiss()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
m.chatId = groupInfo.id
}
}
} else {
GroupLinkView(
groupId: groupInfo.groupId,
groupLink: $groupLink,
groupLinkMemberRole: $groupLinkMemberRole,
showTitle: true,
creatingGroup: true
) {
dismiss()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
m.chatId = groupInfo.id
}
}
}
} else {
@ -41,77 +59,62 @@ struct AddGroupView: View {
}
func createGroupView() -> some View {
VStack(alignment: .leading) {
Text("Create secret group")
.font(.largeTitle)
.padding(.vertical, 4)
Text("The group is fully decentralized it is visible only to the members.")
.padding(.bottom, 4)
List {
Group {
Text("Create secret group")
.font(.largeTitle)
.bold()
.fixedSize(horizontal: false, vertical: true)
.padding(.bottom, 24)
.onTapGesture(perform: hideKeyboard)
HStack {
Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote)
Spacer().frame(width: 8)
Text("Your chat profile will be sent to group members").font(.footnote)
}
.padding(.bottom)
ZStack(alignment: .center) {
ZStack(alignment: .topTrailing) {
profileImageView(profile.image)
if profile.image != nil {
Button {
profile.image = nil
} label: {
Image(systemName: "multiply")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 12)
ZStack(alignment: .center) {
ZStack(alignment: .topTrailing) {
ProfileImage(imageStr: profile.image, color: Color(uiColor: .secondarySystemGroupedBackground))
.aspectRatio(1, contentMode: .fit)
.frame(maxWidth: 128, maxHeight: 128)
if profile.image != nil {
Button {
profile.image = nil
} label: {
Image(systemName: "multiply")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 12)
}
}
}
editImageButton { showChooseSource = true }
.buttonStyle(BorderlessButtonStyle()) // otherwise whole "list row" is clickable
}
editImageButton { showChooseSource = true }
.frame(maxWidth: .infinity, alignment: .center)
}
.frame(maxWidth: .infinity, alignment: .center)
.padding(.bottom, 4)
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
ZStack(alignment: .topLeading) {
let name = profile.displayName.trimmingCharacters(in: .whitespaces)
if name != mkValidName(name) {
Button {
showInvalidNameAlert = true
} label: {
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
}
} else {
Image(systemName: "exclamationmark.circle").foregroundColor(.clear)
Section {
groupNameTextField()
Button(action: createGroup) {
settingsRow("checkmark", color: .accentColor) { Text("Create group") }
}
textField("Enter group name…", text: $profile.displayName)
.focused($focusDisplayName)
.submitLabel(.go)
.onSubmit {
if canCreateProfile() { createGroup() }
}
.disabled(!canCreateProfile())
IncognitoToggle(incognitoEnabled: $incognitoDefault)
} footer: {
VStack(alignment: .leading, spacing: 4) {
sharedGroupProfileInfo(incognitoDefault)
Text("Fully decentralized visible only to members.")
}
.frame(maxWidth: .infinity, alignment: .leading)
.onTapGesture(perform: hideKeyboard)
}
.padding(.bottom)
Spacer()
Button {
createGroup()
} label: {
Text("Create")
Image(systemName: "greaterthan")
}
.disabled(!canCreateProfile())
.frame(maxWidth: .infinity, alignment: .trailing)
}
.onAppear() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
focusDisplayName = true
}
}
.padding()
.confirmationDialog("Group image", isPresented: $showChooseSource, titleVisibility: .visible) {
Button("Take picture") {
showTakePhoto = true
@ -141,20 +144,48 @@ struct AddGroupView: View {
profile.image = nil
}
}
.contentShape(Rectangle())
.onTapGesture { hideKeyboard() }
}
func groupNameTextField() -> some View {
ZStack(alignment: .leading) {
let name = profile.displayName.trimmingCharacters(in: .whitespaces)
if name != mkValidName(name) {
Button {
showInvalidNameAlert = true
} label: {
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
}
} else {
Image(systemName: "pencil").foregroundColor(.secondary)
}
textField("Enter group name…", text: $profile.displayName)
.focused($focusDisplayName)
.submitLabel(.continue)
.onSubmit {
if canCreateProfile() { createGroup() }
}
}
}
func textField(_ placeholder: LocalizedStringKey, text: Binding<String>) -> some View {
TextField(placeholder, text: text)
.padding(.leading, 32)
.padding(.leading, 36)
}
func sharedGroupProfileInfo(_ incognito: Bool) -> Text {
let name = ChatModel.shared.currentUser?.displayName ?? ""
return Text(
incognito
? "A new random profile will be shared."
: "Your profile **\(name)** will be shared."
)
}
func createGroup() {
hideKeyboard()
do {
profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces)
let gInfo = try apiNewGroup(profile)
let gInfo = try apiNewGroup(incognito: incognitoDefault, groupProfile: profile)
Task {
let groupMembers = await apiListMembers(gInfo.groupId)
await MainActor.run {

View file

@ -54,9 +54,11 @@ struct PasteToConnectView: View {
IncognitoToggle(incognitoEnabled: $incognitoDefault)
} footer: {
sharedProfileInfo(incognitoDefault)
+ Text(String("\n\n"))
+ Text("You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.")
VStack(alignment: .leading, spacing: 4) {
sharedProfileInfo(incognitoDefault)
Text("You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.")
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.alert(item: $alert) { a in planAndConnectAlert(a, dismiss: true) }

View file

@ -38,11 +38,11 @@ struct ScanToConnectView: View {
)
.padding(.top)
Group {
VStack(alignment: .leading, spacing: 4) {
sharedProfileInfo(incognitoDefault)
+ Text(String("\n\n"))
+ Text("If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.")
Text("If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.")
}
.frame(maxWidth: .infinity, alignment: .leading)
.font(.footnote)
.foregroundColor(.secondary)
.padding(.horizontal)

View file

@ -50,7 +50,7 @@ public enum ChatCommand {
case apiVerifyToken(token: DeviceToken, nonce: String, code: String)
case apiDeleteToken(token: DeviceToken)
case apiGetNtfMessage(nonce: String, encNtfInfo: String)
case apiNewGroup(userId: Int64, groupProfile: GroupProfile)
case apiNewGroup(userId: Int64, incognito: Bool, groupProfile: GroupProfile)
case apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole)
case apiJoinGroup(groupId: Int64)
case apiMemberRole(groupId: Int64, memberId: Int64, memberRole: GroupMemberRole)
@ -175,7 +175,7 @@ public enum ChatCommand {
case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)"
case let .apiDeleteToken(token): return "/_ntf delete \(token.cmdString)"
case let .apiGetNtfMessage(nonce, encNtfInfo): return "/_ntf message \(nonce) \(encNtfInfo)"
case let .apiNewGroup(userId, groupProfile): return "/_group \(userId) \(encodeJSON(groupProfile))"
case let .apiNewGroup(userId, incognito, groupProfile): return "/_group \(userId) incognito=\(onOff(incognito)) \(encodeJSON(groupProfile))"
case let .apiAddMember(groupId, contactId, memberRole): return "/_add #\(groupId) \(contactId) \(memberRole)"
case let .apiJoinGroup(groupId): return "/_join #\(groupId)"
case let .apiMemberRole(groupId, memberId, memberRole): return "/_member role #\(groupId) \(memberId) \(memberRole.rawValue)"