mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-14 21:15:37 +00:00
core: change user for prepared contact or group (#5985)
This commit is contained in:
@@ -122,6 +122,8 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
case apiConnectPlan(userId: Int64, connLink: String)
|
||||
case apiPrepareContact(userId: Int64, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData)
|
||||
case apiPrepareGroup(userId: Int64, connLink: CreatedConnLink, groupShortLinkData: GroupShortLinkData)
|
||||
case apiChangePreparedContactUser(contactId: Int64, newUserId: Int64)
|
||||
case apiChangePreparedGroupUser(groupId: Int64, newUserId: Int64)
|
||||
case apiConnectPreparedContact(contactId: Int64, incognito: Bool, msg: MsgContent)
|
||||
case apiConnectPreparedGroup(groupId: Int64, incognito: Bool)
|
||||
case apiConnect(userId: Int64, incognito: Bool, connLink: CreatedConnLink)
|
||||
@@ -320,6 +322,8 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
case let .apiConnectPlan(userId, connLink): return "/_connect plan \(userId) \(connLink)"
|
||||
case let .apiPrepareContact(userId, connLink, contactShortLinkData): return "/_prepare contact \(userId) \(connLink.connFullLink) \(connLink.connShortLink ?? "") \(encodeJSON(contactShortLinkData))"
|
||||
case let .apiPrepareGroup(userId, connLink, groupShortLinkData): return "/_prepare group \(userId) \(connLink.connFullLink) \(connLink.connShortLink ?? "") \(encodeJSON(groupShortLinkData))"
|
||||
case let .apiChangePreparedContactUser(contactId, newUserId): return "/_set contact user @\(contactId) \(newUserId)"
|
||||
case let .apiChangePreparedGroupUser(groupId, newUserId): return "/_set group user #\(groupId) \(newUserId)"
|
||||
case let .apiConnectPreparedContact(contactId, incognito, mc): return "/_connect contact @\(contactId) incognito=\(onOff(incognito)) \(mc.cmdString)"
|
||||
case let .apiConnectPreparedGroup(groupId, incognito): return "/_connect group #\(groupId) incognito=\(onOff(incognito))"
|
||||
case let .apiConnect(userId, incognito, connLink): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connLink.connFullLink) \(connLink.connShortLink ?? "")"
|
||||
@@ -492,6 +496,8 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
case .apiConnectPlan: return "apiConnectPlan"
|
||||
case .apiPrepareContact: return "apiPrepareContact"
|
||||
case .apiPrepareGroup: return "apiPrepareGroup"
|
||||
case .apiChangePreparedContactUser: return "apiChangePreparedContactUser"
|
||||
case .apiChangePreparedGroupUser: return "apiChangePreparedGroupUser"
|
||||
case .apiConnectPreparedContact: return "apiConnectPreparedContact"
|
||||
case .apiConnectPreparedGroup: return "apiConnectPreparedGroup"
|
||||
case .apiConnect: return "apiConnect"
|
||||
@@ -743,6 +749,8 @@ enum ChatResponse1: Decodable, ChatAPIResult {
|
||||
case connectionPlan(user: UserRef, connLink: CreatedConnLink, connectionPlan: ConnectionPlan)
|
||||
case newPreparedContact(user: UserRef, contact: Contact)
|
||||
case newPreparedGroup(user: UserRef, groupInfo: GroupInfo)
|
||||
case contactUserChanged(user: UserRef, fromContact: Contact, newUser: UserRef, toContact: Contact)
|
||||
case groupUserChanged(user: UserRef, fromGroup: GroupInfo, newUser: UserRef, toGroup: GroupInfo)
|
||||
case sentConfirmation(user: UserRef, connection: PendingContactConnection)
|
||||
case sentInvitation(user: UserRef, connection: PendingContactConnection)
|
||||
case startedConnectionToContact(user: UserRef, contact: Contact)
|
||||
@@ -786,6 +794,8 @@ enum ChatResponse1: Decodable, ChatAPIResult {
|
||||
case .connectionPlan: "connectionPlan"
|
||||
case .newPreparedContact: "newPreparedContact"
|
||||
case .newPreparedGroup: "newPreparedContact"
|
||||
case .contactUserChanged: "contactUserChanged"
|
||||
case .groupUserChanged: "groupUserChanged"
|
||||
case .sentConfirmation: "sentConfirmation"
|
||||
case .sentInvitation: "sentInvitation"
|
||||
case .startedConnectionToContact: "startedConnectionToContact"
|
||||
@@ -865,6 +875,8 @@ enum ChatResponse1: Decodable, ChatAPIResult {
|
||||
case let .connectionPlan(u, connLink, connectionPlan): return withUser(u, "connLink: \(String(describing: connLink))\nconnectionPlan: \(String(describing: connectionPlan))")
|
||||
case let .newPreparedContact(u, contact): return withUser(u, String(describing: contact))
|
||||
case let .newPreparedGroup(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .contactUserChanged(u, fromContact, newUser, toContact): return withUser(u, "fromContact: \(String(describing: fromContact))\nnewUserId: \(String(describing: newUser.userId))\ntoContact: \(String(describing: toContact))")
|
||||
case let .groupUserChanged(u, fromGroup, newUser, toGroup): return withUser(u, "fromGroup: \(String(describing: fromGroup))\nnewUserId: \(String(describing: newUser.userId))\ntoGroup: \(String(describing: toGroup))")
|
||||
case let .sentConfirmation(u, connection): return withUser(u, String(describing: connection))
|
||||
case let .sentInvitation(u, connection): return withUser(u, String(describing: connection))
|
||||
case let .startedConnectionToContact(u, contact): return withUser(u, String(describing: contact))
|
||||
|
||||
@@ -564,8 +564,15 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func updateChats(_ newChats: [ChatData]) {
|
||||
chats = newChats.map { Chat($0) }
|
||||
func updateChats(_ newChats: [ChatData], keepingChatId: String? = nil) {
|
||||
if let keepingChatId,
|
||||
let chatToKeep = getChat(keepingChatId),
|
||||
let i = newChats.firstIndex(where: { $0.id == keepingChatId }) {
|
||||
let remainingNewChats = Array(newChats[..<i] + newChats[(i + 1)...])
|
||||
chats = [chatToKeep] + remainingNewChats.map { Chat($0) }
|
||||
} else {
|
||||
chats = newChats.map { Chat($0) }
|
||||
}
|
||||
NtfManager.shared.setNtfBadgeCount(totalUnreadCountForAllUsers())
|
||||
popChatCollector.clear()
|
||||
}
|
||||
|
||||
@@ -889,7 +889,6 @@ func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> P
|
||||
|
||||
func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> PendingContactConnection {
|
||||
let r: ChatResponse1 = try await chatSendCmd(.apiChangeConnectionUser(connId: connId, userId: userId))
|
||||
|
||||
if case let .connectionUserChanged(_, _, toConnection, _) = r {return toConnection}
|
||||
throw r.unexpected
|
||||
}
|
||||
@@ -1017,6 +1016,18 @@ func apiPrepareGroup(connLink: CreatedConnLink, groupShortLinkData: GroupShortLi
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
func apiChangePreparedContactUser(contactId: Int64, newUserId: Int64) async throws -> Contact {
|
||||
let r: ChatResponse1 = try await chatSendCmd(.apiChangePreparedContactUser(contactId: contactId, newUserId: newUserId))
|
||||
if case let .contactUserChanged(_, _, _, toContact) = r {return toContact}
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
func apiChangePreparedGroupUser(groupId: Int64, newUserId: Int64) async throws -> GroupInfo {
|
||||
let r: ChatResponse1 = try await chatSendCmd(.apiChangePreparedGroupUser(groupId: groupId, newUserId: newUserId))
|
||||
if case let .groupUserChanged(_, _, _, toGroup) = r {return toGroup}
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
func apiConnectPreparedContact(contactId: Int64, incognito: Bool, msg: MsgContent) async throws -> Contact {
|
||||
let r: ChatResponse1 = try await chatSendCmd(.apiConnectPreparedContact(contactId: contactId, incognito: incognito, msg: msg))
|
||||
if case let .startedConnectionToContact(_, contact) = r { return contact }
|
||||
@@ -1969,7 +1980,7 @@ private func changeActiveUser_(_ userId: Int64, viewPwd: String?) throws {
|
||||
try getUserChatData()
|
||||
}
|
||||
|
||||
func changeActiveUserAsync_(_ userId: Int64?, viewPwd: String?) async throws {
|
||||
func changeActiveUserAsync_(_ userId: Int64?, viewPwd: String?, keepingChatId: String? = nil) async throws {
|
||||
let currentUser = if let userId = userId {
|
||||
try await apiSetActiveUserAsync(userId, viewPwd: viewPwd)
|
||||
} else {
|
||||
@@ -1981,7 +1992,7 @@ func changeActiveUserAsync_(_ userId: Int64?, viewPwd: String?) async throws {
|
||||
m.currentUser = currentUser
|
||||
m.users = users
|
||||
}
|
||||
try await getUserChatDataAsync()
|
||||
try await getUserChatDataAsync(keepingChatId: keepingChatId)
|
||||
await MainActor.run {
|
||||
if let currentUser = currentUser, var (_, invitation) = ChatModel.shared.callInvitations.first(where: { _, inv in inv.user.userId == userId }) {
|
||||
invitation.user = currentUser
|
||||
@@ -2003,7 +2014,7 @@ func getUserChatData() throws {
|
||||
tm.updateChatTags(m.chats)
|
||||
}
|
||||
|
||||
private func getUserChatDataAsync() async throws {
|
||||
private func getUserChatDataAsync(keepingChatId: String?) async throws {
|
||||
let m = ChatModel.shared
|
||||
let tm = ChatTagsModel.shared
|
||||
if m.currentUser != nil {
|
||||
@@ -2014,7 +2025,7 @@ private func getUserChatDataAsync() async throws {
|
||||
await MainActor.run {
|
||||
m.userAddress = userAddress
|
||||
m.chatItemTTL = chatItemTTL
|
||||
m.updateChats(chats)
|
||||
m.updateChats(chats, keepingChatId: keepingChatId)
|
||||
tm.activeFilter = nil
|
||||
tm.userTags = tags
|
||||
tm.updateChatTags(m.chats)
|
||||
|
||||
@@ -350,6 +350,14 @@ struct ComposeView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
Divider()
|
||||
if (chat.chatInfo.contact?.nextConnectPrepared ?? false) || (chat.chatInfo.groupInfo?.nextConnectPrepared ?? false),
|
||||
let user = chatModel.currentUser {
|
||||
ContextProfilePickerView(
|
||||
chat: chat,
|
||||
selectedUser: user
|
||||
)
|
||||
}
|
||||
|
||||
if let contact = chat.chatInfo.contact,
|
||||
contact.nextAcceptContactRequest,
|
||||
let contactRequestId = contact.contactRequestId {
|
||||
@@ -692,8 +700,7 @@ struct ComposeView: View {
|
||||
private func sendConnectPreparedContact() async {
|
||||
do {
|
||||
let mc = checkLinkPreview()
|
||||
// TODO [short links] allow to choose incognito, different user profile (as "compose context")
|
||||
let contact = try await apiConnectPreparedContact(contactId: chat.chatInfo.apiId, incognito: false, msg: mc)
|
||||
let contact = try await apiConnectPreparedContact(contactId: chat.chatInfo.apiId, incognito: incognitoGroupDefault.get(), msg: mc)
|
||||
await MainActor.run {
|
||||
self.chatModel.updateContact(contact)
|
||||
clearState()
|
||||
@@ -706,8 +713,7 @@ struct ComposeView: View {
|
||||
|
||||
private func connectPreparedGroup() async {
|
||||
do {
|
||||
// TODO [short links] allow to choose incognito, different user profile (as "compose context")
|
||||
let groupInfo = try await apiConnectPreparedGroup(groupId: chat.chatInfo.apiId, incognito: false)
|
||||
let groupInfo = try await apiConnectPreparedGroup(groupId: chat.chatInfo.apiId, incognito: incognitoGroupDefault.get())
|
||||
await MainActor.run {
|
||||
self.chatModel.updateGroup(groupInfo)
|
||||
clearState()
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
//
|
||||
// ContextProfilePickerView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 13.06.2025.
|
||||
// Copyright © 2025 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
let USER_ROW_SIZE: CGFloat = 60
|
||||
let MAX_VISIBLE_USER_ROWS: CGFloat = 4.8
|
||||
|
||||
struct ContextProfilePickerView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@State var selectedUser: User
|
||||
@State private var users: [User] = []
|
||||
@State private var listExpanded = false
|
||||
@State private var expandedListReady = false
|
||||
@State private var showIncognitoSheet = false
|
||||
|
||||
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
|
||||
|
||||
var body: some View {
|
||||
viewBody()
|
||||
.onAppear {
|
||||
users = chatModel.users
|
||||
.map { $0.user }
|
||||
.filter { u in u.activeUser || !u.hidden }
|
||||
}
|
||||
.sheet(isPresented: $showIncognitoSheet) {
|
||||
IncognitoHelp()
|
||||
}
|
||||
}
|
||||
|
||||
private func viewBody() -> some View {
|
||||
Group {
|
||||
if !listExpanded {
|
||||
currentSelection()
|
||||
} else {
|
||||
profilePicker()
|
||||
}
|
||||
}
|
||||
.padding(.bottom, -8)
|
||||
}
|
||||
|
||||
private func currentSelection() -> some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Text("Share profile")
|
||||
.font(.callout)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, -4)
|
||||
.padding(.leading, 12)
|
||||
.padding(.trailing)
|
||||
|
||||
if incognitoDefault {
|
||||
incognitoOption()
|
||||
} else {
|
||||
profilerPickerUserOption(selectedUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func profilePicker() -> some View {
|
||||
ScrollViewReader { proxy in
|
||||
Group {
|
||||
if expandedListReady {
|
||||
let scroll = ScrollView {
|
||||
LazyVStack(spacing: 0) {
|
||||
let otherUsers = users
|
||||
.filter { u in u.userId != selectedUser.userId }
|
||||
.sorted(using: KeyPathComparator<User>(\.activeOrder))
|
||||
ForEach(otherUsers) { p in
|
||||
profilerPickerUserOption(p)
|
||||
.contentShape(Rectangle())
|
||||
Divider()
|
||||
.padding(.leading)
|
||||
.padding(.leading, 48)
|
||||
}
|
||||
|
||||
if incognitoDefault {
|
||||
profilerPickerUserOption(selectedUser)
|
||||
.contentShape(Rectangle())
|
||||
Divider()
|
||||
.padding(.leading)
|
||||
.padding(.leading, 48)
|
||||
|
||||
incognitoOption()
|
||||
.contentShape(Rectangle())
|
||||
.id("BOTTOM_ANCHOR")
|
||||
} else {
|
||||
incognitoOption()
|
||||
.contentShape(Rectangle())
|
||||
Divider()
|
||||
.padding(.leading)
|
||||
.padding(.leading, 48)
|
||||
|
||||
profilerPickerUserOption(selectedUser)
|
||||
.contentShape(Rectangle())
|
||||
.id("BOTTOM_ANCHOR")
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: USER_ROW_SIZE * min(MAX_VISIBLE_USER_ROWS, CGFloat(users.count + 1))) // + 1 for incognito
|
||||
.onAppear {
|
||||
DispatchQueue.main.async {
|
||||
withAnimation(nil) {
|
||||
proxy.scrollTo("BOTTOM_ANCHOR", anchor: .bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
expandedListReady = false
|
||||
}
|
||||
|
||||
if #available(iOS 16.0, *) {
|
||||
scroll.scrollDismissesKeyboard(.never)
|
||||
} else {
|
||||
scroll
|
||||
}
|
||||
} else {
|
||||
// Keep showing current selection to avoid flickering of scroll to bottom
|
||||
currentSelection()
|
||||
.onAppear {
|
||||
// Delay rendering of expanded profile list
|
||||
DispatchQueue.main.async {
|
||||
expandedListReady = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func profilerPickerUserOption(_ user: User) -> some View {
|
||||
Button {
|
||||
if selectedUser == user {
|
||||
if !incognitoDefault {
|
||||
listExpanded.toggle()
|
||||
} else {
|
||||
incognitoDefault = false
|
||||
listExpanded = false
|
||||
}
|
||||
} else if selectedUser != user {
|
||||
changeProfile(user)
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
ProfileImage(imageStr: user.image, size: 38)
|
||||
Text(user.chatViewName)
|
||||
.fontWeight(selectedUser == user && !incognitoDefault ? .medium : .regular)
|
||||
.foregroundColor(theme.colors.onBackground)
|
||||
.lineLimit(1)
|
||||
|
||||
Spacer()
|
||||
|
||||
if selectedUser == user && !incognitoDefault {
|
||||
if listExpanded {
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.opacity(0.7)
|
||||
} else {
|
||||
Image(systemName: "chevron.up")
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.opacity(0.7)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.leading, 12)
|
||||
.padding(.trailing)
|
||||
.frame(height: USER_ROW_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
private func changeProfile(_ newUser: User) {
|
||||
Task {
|
||||
do {
|
||||
if let contact = chat.chatInfo.contact {
|
||||
let updatedContact = try await apiChangePreparedContactUser(contactId: contact.contactId, newUserId: newUser.userId)
|
||||
await MainActor.run {
|
||||
selectedUser = newUser
|
||||
incognitoDefault = false
|
||||
listExpanded = false
|
||||
chatModel.updateContact(updatedContact)
|
||||
}
|
||||
} else if let groupInfo = chat.chatInfo.groupInfo {
|
||||
let updatedGroupInfo = try await apiChangePreparedGroupUser(groupId: groupInfo.groupId, newUserId: newUser.userId)
|
||||
await MainActor.run {
|
||||
selectedUser = newUser
|
||||
incognitoDefault = false
|
||||
listExpanded = false
|
||||
chatModel.updateGroup(updatedGroupInfo)
|
||||
}
|
||||
}
|
||||
do {
|
||||
try await changeActiveUserAsync_(newUser.userId, viewPwd: nil, keepingChatId: chat.id)
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
showAlert(
|
||||
NSLocalizedString("Error switching profile", comment: "alert title"),
|
||||
message: String.localizedStringWithFormat(NSLocalizedString("Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile.", comment: "alert message"), newUser.chatViewName)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch let error {
|
||||
await MainActor.run {
|
||||
if let currentUser = chatModel.currentUser {
|
||||
selectedUser = currentUser
|
||||
}
|
||||
showAlert(
|
||||
NSLocalizedString("Error changing chat profile", comment: "alert title"),
|
||||
message: responseError(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func incognitoOption() -> some View {
|
||||
Button {
|
||||
if incognitoDefault {
|
||||
listExpanded.toggle()
|
||||
} else {
|
||||
incognitoDefault = true
|
||||
listExpanded = false
|
||||
}
|
||||
} label : {
|
||||
HStack {
|
||||
incognitoProfileImage()
|
||||
Text("Incognito")
|
||||
.fontWeight(incognitoDefault ? .medium : .regular)
|
||||
.foregroundColor(theme.colors.onBackground)
|
||||
Image(systemName: "info.circle")
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.onTapGesture {
|
||||
showIncognitoSheet = true
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if incognitoDefault {
|
||||
if listExpanded {
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.opacity(0.7)
|
||||
} else {
|
||||
Image(systemName: "chevron.up")
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.opacity(0.7)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.leading, 12)
|
||||
.padding(.trailing)
|
||||
.frame(height: USER_ROW_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
private func incognitoProfileImage() -> some View {
|
||||
Image(systemName: "theatermasks.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 38)
|
||||
.foregroundColor(.indigo)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContextProfilePickerView(
|
||||
chat: Chat.sampleData,
|
||||
selectedUser: User.sampleData
|
||||
)
|
||||
}
|
||||
@@ -367,7 +367,6 @@ private struct ActiveProfilePicker: View {
|
||||
.onAppear {
|
||||
profiles = chatModel.users
|
||||
.map { $0.user }
|
||||
.sorted { u, _ in u.activeUser }
|
||||
}
|
||||
.onChange(of: incognitoEnabled) { incognito in
|
||||
if profileSwitchStatus != .switchingIncognito {
|
||||
@@ -425,7 +424,7 @@ private struct ActiveProfilePicker: View {
|
||||
chatModel.updateContactConnection(conn)
|
||||
}
|
||||
do {
|
||||
try await changeActiveUserAsync_(profile.userId, viewPwd: profile.hidden ? trimmedSearchTextOrPassword : nil )
|
||||
try await changeActiveUserAsync_(profile.userId, viewPwd: profile.hidden ? trimmedSearchTextOrPassword : nil)
|
||||
await MainActor.run {
|
||||
profileSwitchStatus = .idle
|
||||
dismiss()
|
||||
@@ -559,8 +558,10 @@ private struct ActiveProfilePicker: View {
|
||||
let activeProfile = filteredProfiles.first { u in u.activeUser }
|
||||
|
||||
if let selectedProfile = activeProfile {
|
||||
let otherProfiles = filteredProfiles.filter { u in u.userId != activeProfile?.userId }
|
||||
|
||||
let otherProfiles = filteredProfiles
|
||||
.filter { u in u.userId != activeProfile?.userId }
|
||||
.sorted(using: KeyPathComparator<User>(\.activeOrder, order: .reverse))
|
||||
|
||||
if incognitoFirst {
|
||||
incognitoOption
|
||||
profilerPickerUserOption(selectedProfile)
|
||||
|
||||
@@ -185,6 +185,7 @@
|
||||
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; };
|
||||
64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */; };
|
||||
64E5E3632DF71A4E00A4D530 /* ContextContactRequestActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E5E3622DF71A4E00A4D530 /* ContextContactRequestActionsView.swift */; };
|
||||
64E5E3672DFC16A900A4D530 /* ContextProfilePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E5E3662DFC16A900A4D530 /* ContextProfilePickerView.swift */; };
|
||||
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; };
|
||||
64EEB0F72C353F1C00972D62 /* ServersSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */; };
|
||||
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; };
|
||||
@@ -550,6 +551,7 @@
|
||||
64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactLearnMore.swift; sourceTree = "<group>"; };
|
||||
64DAE1502809D9F5000DA960 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; };
|
||||
64E5E3622DF71A4E00A4D530 /* ContextContactRequestActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextContactRequestActionsView.swift; sourceTree = "<group>"; };
|
||||
64E5E3662DFC16A900A4D530 /* ContextProfilePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextProfilePickerView.swift; sourceTree = "<group>"; };
|
||||
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = "<group>"; };
|
||||
64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersSummaryView.swift; sourceTree = "<group>"; };
|
||||
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; };
|
||||
@@ -1087,6 +1089,7 @@
|
||||
D72A9087294BD7A70047C86D /* NativeTextEditor.swift */,
|
||||
64A77A012DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift */,
|
||||
64E5E3622DF71A4E00A4D530 /* ContextContactRequestActionsView.swift */,
|
||||
64E5E3662DFC16A900A4D530 /* ContextProfilePickerView.swift */,
|
||||
);
|
||||
path = ComposeMessage;
|
||||
sourceTree = "<group>";
|
||||
@@ -1513,6 +1516,7 @@
|
||||
5CA7DFC329302AF000F7FDDE /* AppSheet.swift in Sources */,
|
||||
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */,
|
||||
5CC1C99227A6C7F5000D9FF6 /* QRCode.swift in Sources */,
|
||||
64E5E3672DFC16A900A4D530 /* ContextProfilePickerView.swift in Sources */,
|
||||
5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */,
|
||||
5CB9250D27A9432000ACCCDD /* ChatListNavLink.swift in Sources */,
|
||||
649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */,
|
||||
|
||||
@@ -1350,8 +1350,8 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact):
|
||||
// TODO [short links] this will have additional statuses for pending contact requests before they are accepted
|
||||
if contact.sendMsgToConnect { return nil }
|
||||
if contact.nextAcceptContactRequest { return ("can't send messages", nil) }
|
||||
if !contact.active { return ("contact deleted", nil) }
|
||||
if !contact.sndReady { return ("contact not ready", nil) }
|
||||
if contact.activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false { return ("not synchronized", nil) }
|
||||
|
||||
@@ -450,8 +450,8 @@ data ChatCommand
|
||||
| APIConnectPlan UserId AConnectionLink
|
||||
| APIPrepareContact UserId ACreatedConnLink ContactShortLinkData
|
||||
| APIPrepareGroup UserId ACreatedConnLink GroupShortLinkData
|
||||
| APIChangeContactUser ContactId UserId
|
||||
| APIChangeGroupUser GroupId UserId
|
||||
| APIChangePreparedContactUser ContactId UserId
|
||||
| APIChangePreparedGroupUser GroupId UserId
|
||||
| APIConnectPreparedContact {contactId :: ContactId, incognito :: IncognitoEnabled, msgContent_ :: Maybe MsgContent}
|
||||
| APIConnectPreparedGroup GroupId IncognitoEnabled
|
||||
| APIConnect UserId IncognitoEnabled (Maybe ACreatedConnLink) (Maybe MsgContent)
|
||||
@@ -685,6 +685,8 @@ data ChatResponse
|
||||
| CRConnectionPlan {user :: User, connLink :: ACreatedConnLink, connectionPlan :: ConnectionPlan}
|
||||
| CRNewPreparedContact {user :: User, contact :: Contact}
|
||||
| CRNewPreparedGroup {user :: User, groupInfo :: GroupInfo}
|
||||
| CRContactUserChanged {user :: User, fromContact :: Contact, newUser :: User, toContact :: Contact}
|
||||
| CRGroupUserChanged {user :: User, fromGroup :: GroupInfo, newUser :: User, toGroup :: GroupInfo}
|
||||
| CRSentConfirmation {user :: User, connection :: PendingContactConnection}
|
||||
| CRSentInvitation {user :: User, connection :: PendingContactConnection, customUserProfile :: Maybe Profile}
|
||||
| CRStartedConnectionToContact {user :: User, contact :: Contact}
|
||||
|
||||
@@ -1760,13 +1760,21 @@ processChatCommand' vr = \case
|
||||
let GroupShortLinkData {groupProfile} = groupSLinkData
|
||||
gInfo <- withStore $ \db -> createPreparedGroup db vr user groupProfile accLink
|
||||
pure $ CRNewPreparedGroup user gInfo
|
||||
-- TODO [short links] change prepared entity user
|
||||
-- TODO - UI would call these APIs before APIConnectPrepared... APIs
|
||||
-- TODO - UI to transition to new user keeping chat opened
|
||||
APIChangeContactUser _contactId _newUserId -> withUser $ \_user -> do
|
||||
ok_
|
||||
APIChangeGroupUser _groupId _newUserId -> withUser $ \_user -> do
|
||||
ok_
|
||||
APIChangePreparedContactUser contactId newUserId -> withUser $ \user -> do
|
||||
ct@Contact {connLinkToConnect} <- withFastStore $ \db -> getContact db vr user contactId
|
||||
when (isNothing connLinkToConnect) $ throwCmdError "contact doesn't have link to connect"
|
||||
when (isJust $ contactConn ct) $ throwCmdError "contact already has connection"
|
||||
newUser <- privateGetUser newUserId
|
||||
ct' <- withFastStore $ \db -> updatePreparedContactUser db vr user ct newUser
|
||||
pure $ CRContactUserChanged user ct newUser ct'
|
||||
APIChangePreparedGroupUser groupId newUserId -> withUser $ \user -> do
|
||||
(gInfo, hostMember) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user groupId <*> getHostMember db vr user groupId
|
||||
let GroupInfo {connLinkToConnect} = gInfo
|
||||
when (isNothing connLinkToConnect) $ throwCmdError "group doesn't have link to connect"
|
||||
when (isJust $ memberConn hostMember) $ throwCmdError "host member already has connection"
|
||||
newUser <- privateGetUser newUserId
|
||||
gInfo' <- withFastStore $ \db -> updatePreparedGroupUser db vr user gInfo hostMember newUser
|
||||
pure $ CRGroupUserChanged user gInfo newUser gInfo'
|
||||
APIConnectPreparedContact contactId incognito msgContent_ -> withUser $ \user -> do
|
||||
Contact {connLinkToConnect} <- withFastStore $ \db -> getContact db vr user contactId
|
||||
case connLinkToConnect of
|
||||
@@ -4424,8 +4432,8 @@ chatCommandP =
|
||||
"/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> strP),
|
||||
"/_prepare contact " *> (APIPrepareContact <$> A.decimal <* A.space <*> connLinkP <* A.space <*> jsonP),
|
||||
"/_prepare group " *> (APIPrepareGroup <$> A.decimal <* A.space <*> connLinkP <* A.space <*> jsonP),
|
||||
"/_set contact user @" *> (APIChangeContactUser <$> A.decimal <* A.space <*> A.decimal),
|
||||
"/_set group user #" *> (APIChangeGroupUser <$> A.decimal <* A.space <*> A.decimal),
|
||||
"/_set contact user @" *> (APIChangePreparedContactUser <$> A.decimal <* A.space <*> A.decimal),
|
||||
"/_set group user #" *> (APIChangePreparedGroupUser <$> A.decimal <* A.space <*> A.decimal),
|
||||
"/_connect contact @" *> (APIConnectPreparedContact <$> A.decimal <*> incognitoOnOffP <*> optional (A.space *> msgContentP)),
|
||||
"/_connect group #" *> (APIConnectPreparedGroup <$> A.decimal <*> incognitoOnOffP),
|
||||
"/_connect " *> (APIAddContact <$> A.decimal <*> shortOnOffP <*> incognitoOnOffP),
|
||||
|
||||
@@ -31,6 +31,7 @@ module Simplex.Chat.Store.Direct
|
||||
getConnReqContactXContactId,
|
||||
getContactByConnReqHash,
|
||||
createPreparedContact,
|
||||
updatePreparedContactUser,
|
||||
createDirectContact,
|
||||
deleteContactConnections,
|
||||
deleteContactFiles,
|
||||
@@ -281,6 +282,35 @@ createPreparedContact db user@User {userId} p@Profile {preferences} connLinkToCo
|
||||
customData = Nothing
|
||||
}
|
||||
|
||||
updatePreparedContactUser :: DB.Connection -> VersionRangeChat -> User -> Contact -> User -> ExceptT StoreError IO Contact
|
||||
updatePreparedContactUser
|
||||
db
|
||||
vr
|
||||
user
|
||||
Contact {contactId, localDisplayName = oldLDN, profile = LocalProfile {profileId, displayName}}
|
||||
newUser@User {userId = newUserId} = do
|
||||
ExceptT . withLocalDisplayName db newUserId displayName $ \newLDN -> runExceptT $ do
|
||||
liftIO $ do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE contacts
|
||||
SET user_id = ?, local_display_name = ?, updated_at = ?
|
||||
WHERE contact_id = ?
|
||||
|]
|
||||
(newUserId, newLDN, currentTs, contactId)
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE contact_profiles
|
||||
SET user_id = ?, updated_at = ?
|
||||
WHERE contact_profile_id = ?
|
||||
|]
|
||||
(newUserId, currentTs, profileId)
|
||||
safeDeleteLDN db user oldLDN
|
||||
getContact db vr newUser contactId
|
||||
|
||||
createDirectContact :: DB.Connection -> User -> Connection -> Profile -> ExceptT StoreError IO Contact
|
||||
createDirectContact db user@User {userId} conn@Connection {connId, localAlias} p@Profile {preferences} = do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
|
||||
@@ -36,6 +36,7 @@ module Simplex.Chat.Store.Groups
|
||||
createGroupInvitation,
|
||||
deleteContactCardKeepConn,
|
||||
createPreparedGroup,
|
||||
updatePreparedGroupUser,
|
||||
setGroupConnLinkStartedConnection,
|
||||
updatePreparedUserAndHostMembersInvited,
|
||||
updatePreparedUserAndHostMembersRejected,
|
||||
@@ -607,6 +608,70 @@ createPreparedGroup db vr user@User {userId, userContactId} groupProfile connLin
|
||||
)
|
||||
insertedRowId db
|
||||
|
||||
updatePreparedGroupUser :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupMember -> User -> ExceptT StoreError IO GroupInfo
|
||||
updatePreparedGroupUser db vr user gInfo@GroupInfo {groupId, membership} hostMember newUser@User {userId = newUserId} = do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
updateGroup gInfo currentTs
|
||||
liftIO $ updateMembership membership currentTs
|
||||
updateHostMember hostMember currentTs
|
||||
getGroupInfo db vr newUser groupId
|
||||
where
|
||||
updateGroup GroupInfo {localDisplayName = oldGroupLDN, groupProfile = GroupProfile {displayName = groupDisplayName}} currentTs =
|
||||
ExceptT . withLocalDisplayName db newUserId groupDisplayName $ \newGroupLDN -> runExceptT $ do
|
||||
liftIO $ do
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE groups
|
||||
SET user_id = ?, local_display_name = ?, updated_at = ?
|
||||
WHERE group_id = ?
|
||||
|]
|
||||
(newUserId, newGroupLDN, currentTs, groupId)
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE group_profiles
|
||||
SET user_id = ?, updated_at = ?
|
||||
WHERE group_profile_id IN (SELECT group_profile_id FROM groups WHERE group_id = ?)
|
||||
|]
|
||||
(newUserId, currentTs, groupId)
|
||||
safeDeleteLDN db user oldGroupLDN
|
||||
updateMembership GroupMember {groupMemberId = membershipId} currentTs =
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE group_members
|
||||
SET user_id = ?, local_display_name = ?, contact_id = ?, contact_profile_id = ?, updated_at = ?
|
||||
WHERE group_member_id = ?
|
||||
|]
|
||||
(newUserId, localDisplayName' newUser, contactId' newUser, localProfileId $ profile' newUser, currentTs, membershipId)
|
||||
updateHostMember
|
||||
GroupMember
|
||||
{ groupMemberId = hostId,
|
||||
localDisplayName = oldHostLDN,
|
||||
memberProfile = LocalProfile {profileId = hostProfileId, displayName = hostDisplayName}
|
||||
}
|
||||
currentTs =
|
||||
ExceptT . withLocalDisplayName db newUserId hostDisplayName $ \newHostLDN -> runExceptT $ do
|
||||
liftIO $ do
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE group_members
|
||||
SET user_id = ?, local_display_name = ?, updated_at = ?
|
||||
WHERE group_member_id = ?
|
||||
|]
|
||||
(newUserId, newHostLDN, currentTs, hostId)
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE contact_profiles
|
||||
SET user_id = ?, updated_at = ?
|
||||
WHERE contact_profile_id = ?
|
||||
|]
|
||||
(newUserId, currentTs, hostProfileId)
|
||||
safeDeleteLDN db user oldHostLDN
|
||||
|
||||
setGroupConnLinkStartedConnection :: DB.Connection -> GroupInfo -> Bool -> IO GroupInfo
|
||||
setGroupConnLinkStartedConnection db groupInfo@GroupInfo {groupId} connLinkStartedConnection = do
|
||||
currentTs <- getCurrentTime
|
||||
|
||||
@@ -86,6 +86,14 @@ SEARCH gp USING INTEGER PRIMARY KEY (rowid=?)
|
||||
SEARCH mu USING INDEX idx_group_members_contact_id (contact_id=?)
|
||||
SEARCH pu USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
UPDATE contact_profiles
|
||||
SET user_id = ?, updated_at = ?
|
||||
WHERE contact_profile_id = ?
|
||||
|
||||
Plan:
|
||||
SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
UPDATE contact_requests
|
||||
SET agent_invitation_id = ?, pq_support = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, local_display_name = ?, updated_at = ?
|
||||
@@ -136,6 +144,14 @@ Query:
|
||||
Plan:
|
||||
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
UPDATE group_members
|
||||
SET user_id = ?, local_display_name = ?, updated_at = ?
|
||||
WHERE group_member_id = ?
|
||||
|
||||
Plan:
|
||||
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
INSERT INTO contact_requests
|
||||
(user_contact_link_id, agent_invitation_id, peer_chat_min_version, peer_chat_max_version, contact_profile_id, local_display_name, user_id,
|
||||
@@ -273,6 +289,24 @@ Query:
|
||||
Plan:
|
||||
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
UPDATE group_profiles
|
||||
SET user_id = ?, updated_at = ?
|
||||
WHERE group_profile_id IN (SELECT group_profile_id FROM groups WHERE group_id = ?)
|
||||
|
||||
Plan:
|
||||
SEARCH group_profiles USING INTEGER PRIMARY KEY (rowid=?)
|
||||
LIST SUBQUERY 1
|
||||
SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
UPDATE groups
|
||||
SET user_id = ?, local_display_name = ?, updated_at = ?
|
||||
WHERE group_id = ?
|
||||
|
||||
Plan:
|
||||
SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
UPDATE xftp_file_descriptions
|
||||
SET file_descr_text = ?, file_descr_part_no = ?, file_descr_complete = ?
|
||||
@@ -660,6 +694,22 @@ Query:
|
||||
Plan:
|
||||
SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
UPDATE contact_profiles
|
||||
SET user_id = ?, updated_at = ?
|
||||
WHERE contact_profile_id = ?
|
||||
|
||||
Plan:
|
||||
SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
UPDATE contacts
|
||||
SET user_id = ?, local_display_name = ?, updated_at = ?
|
||||
WHERE contact_id = ?
|
||||
|
||||
Plan:
|
||||
SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
UPDATE group_members
|
||||
SET member_id = ?,
|
||||
@@ -1348,6 +1398,14 @@ Query:
|
||||
Plan:
|
||||
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
UPDATE group_members
|
||||
SET user_id = ?, local_display_name = ?, contact_id = ?, contact_profile_id = ?, updated_at = ?
|
||||
WHERE group_member_id = ?
|
||||
|
||||
Plan:
|
||||
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
UPDATE group_profiles
|
||||
SET display_name = ?, full_name = ?, description = ?, image = ?, preferences = ?, member_admission = ?, updated_at = ?
|
||||
|
||||
@@ -192,6 +192,8 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte
|
||||
CRConnectionPlan u connLink connectionPlan -> ttyUser u $ viewConnectionPlan cfg connLink connectionPlan
|
||||
CRNewPreparedContact u c -> ttyUser u [ttyContact' c <> ": contact is prepared"]
|
||||
CRNewPreparedGroup u g -> ttyUser u [ttyGroup' g <> ": group is prepared"]
|
||||
CRContactUserChanged u c nu c' -> ttyUser u $ viewContactUserChanged u c nu c'
|
||||
CRGroupUserChanged u g nu g' -> ttyUser u $ viewGroupUserChanged u g nu g'
|
||||
CRSentConfirmation u _ -> ttyUser u ["confirmation sent!"]
|
||||
CRSentInvitation u _ customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView
|
||||
CRStartedConnectionToContact u c -> ttyUser u [ttyContact' c <> ": connection started"]
|
||||
@@ -1831,6 +1833,28 @@ viewConnectionUserChanged User {localDisplayName = n} PendingContactConnection {
|
||||
where
|
||||
cReqStr = strEncode $ simplexChatInvitation cReq
|
||||
|
||||
viewContactUserChanged :: User -> Contact -> User -> Contact -> [StyledString]
|
||||
viewContactUserChanged
|
||||
User {localDisplayName = un}
|
||||
ct@Contact {localDisplayName = cn}
|
||||
User {localDisplayName = un'}
|
||||
Contact {localDisplayName = cn'}
|
||||
| cn' /= cn = [userChangedStr <> ", new local name: " <> ttyContact cn']
|
||||
| otherwise = [userChangedStr]
|
||||
where
|
||||
userChangedStr = "contact " <> ttyContact' ct <> " changed from user " <> plain un <> " to user " <> plain un'
|
||||
|
||||
viewGroupUserChanged :: User -> GroupInfo -> User -> GroupInfo -> [StyledString]
|
||||
viewGroupUserChanged
|
||||
User {localDisplayName = un}
|
||||
g@GroupInfo {localDisplayName = gn}
|
||||
User {localDisplayName = un'}
|
||||
GroupInfo {localDisplayName = gn'}
|
||||
| gn' /= gn = [userChangedStr <> ", new local name: " <> ttyGroup gn']
|
||||
| otherwise = [userChangedStr]
|
||||
where
|
||||
userChangedStr = "group " <> ttyGroup' g <> " changed from user " <> plain un <> " to user " <> plain un'
|
||||
|
||||
viewConnectionPlan :: ChatConfig -> ACreatedConnLink -> ConnectionPlan -> [StyledString]
|
||||
viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case
|
||||
CPInvitationLink ilp -> case ilp of
|
||||
|
||||
+288
-11
@@ -106,17 +106,21 @@ chatProfileTests = do
|
||||
it "should plan and connect via one-time invitation" testPlanShortLinkInvitation
|
||||
it "should connect via contact address" testShortLinkContactAddress
|
||||
it "should join group" testShortLinkJoinGroup
|
||||
describe "connection via prepared entity" $ do
|
||||
it "prepare contact using invitation short link data and connect" testShortLinkInvitationPrepareContact
|
||||
it "prepare contact using address short link data and connect" testShortLinkAddressPrepareContact
|
||||
it "prepare group using group short link data and connect" testShortLinkPrepareGroup
|
||||
it "prepare group using group short link data and connect, host rejects" testShortLinkPrepareGroupReject
|
||||
-- TODO [short links] enable tests - AGENT A_MESSAGE error
|
||||
xit "setting incognito for invitation should update short link data" testShortLinkInvitationSetIncognito
|
||||
xit "changing user for invitation should update short link data" testShortLinkInvitationChangeUser
|
||||
it "changing profile should update address short link data" testShortLinkAddressChangeProfile
|
||||
it "changing auto-reply message should update address short link data" testShortLinkAddressChangeAutoReply
|
||||
it "changing group profile should update short link data" testShortLinkGroupChangeProfile
|
||||
describe "short links with attached data" $ do
|
||||
it "prepare contact using invitation short link data and connect" testShortLinkInvitationPrepareContact
|
||||
it "prepare contact using address short link data and connect" testShortLinkAddressPrepareContact
|
||||
it "prepare group using group short link data and connect" testShortLinkPrepareGroup
|
||||
it "prepare group using group short link data and connect, host rejects" testShortLinkPrepareGroupReject
|
||||
it "change prepared contact user" testShortLinkChangePreparedContactUser
|
||||
it "change prepared contact user, new user has contact with the same name" testShortLinkChangePreparedContactUserDuplicate
|
||||
it "change prepared group user" testShortLinkChangePreparedGroupUser
|
||||
it "change prepared group user, new user has group with the same name" testShortLinkChangePreparedGroupUserDuplicate
|
||||
-- TODO [short links] enable tests - AGENT A_MESSAGE error
|
||||
xit "setting incognito for invitation should update short link data" testShortLinkInvitationSetIncognito
|
||||
xit "changing user for invitation should update short link data" testShortLinkInvitationChangeUser
|
||||
it "changing profile should update address short link data" testShortLinkAddressChangeProfile
|
||||
it "changing auto-reply message should update address short link data" testShortLinkAddressChangeAutoReply
|
||||
it "changing group profile should update short link data" testShortLinkGroupChangeProfile
|
||||
|
||||
testUpdateProfile :: HasCallStack => TestParams -> IO ()
|
||||
testUpdateProfile =
|
||||
@@ -2894,6 +2898,279 @@ testShortLinkPrepareGroupReject =
|
||||
where
|
||||
cfg = testCfg {chatHooks = defaultChatHooks {acceptMember = Just (\_ _ _ -> pure $ Left GRRBlockedName)}}
|
||||
|
||||
testShortLinkChangePreparedContactUser :: HasCallStack => TestParams -> IO ()
|
||||
testShortLinkChangePreparedContactUser =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
bob ##> "/create user robert"
|
||||
showActiveUser bob "robert"
|
||||
bob ##> "/user bob"
|
||||
showActiveUser bob "bob (Bob)"
|
||||
|
||||
alice ##> "/_connect 1 short=on"
|
||||
(shortLink, fullLink) <- getShortInvitation alice
|
||||
bob ##> ("/_connect plan 1 " <> shortLink)
|
||||
bob <## "invitation link: ok to connect"
|
||||
contactSLinkData <- getTermLine bob
|
||||
bob ##> ("/_prepare contact 1 " <> fullLink <> " " <> shortLink <> " " <> contactSLinkData)
|
||||
bob <## "alice: contact is prepared"
|
||||
|
||||
-- 2 ids are for "user contacts", 2 ids are for second user contact cards, so alice is id 5
|
||||
bob ##> "/_set contact user @5 2"
|
||||
bob <## "contact alice changed from user bob to user robert"
|
||||
|
||||
bob ##> "/user robert"
|
||||
showActiveUser bob "robert"
|
||||
|
||||
bob ##> "/_connect contact @5 text hello"
|
||||
bob
|
||||
<### [ "alice: connection started",
|
||||
WithTime "@alice hello"
|
||||
]
|
||||
alice <# "robert> hello"
|
||||
concurrently_
|
||||
(bob <## "alice (Alice): contact is connected")
|
||||
(alice <## "robert: contact is connected")
|
||||
|
||||
alice <##> bob
|
||||
|
||||
alice @@@ [("@robert", "hey")]
|
||||
alice `hasContactProfiles` ["alice", "robert"]
|
||||
bob #$> ("/_get chats 2 pcc=on", chats, [("@alice", "hey"), ("@SimpleX Chat team", ""), ("@SimpleX-Status", ""), ("*", "")])
|
||||
bob `hasContactProfiles` ["robert", "alice", "SimpleX Chat team", "SimpleX-Status"]
|
||||
bob ##> "/user bob"
|
||||
showActiveUser bob "bob (Bob)"
|
||||
bob @@@ []
|
||||
bob `hasContactProfiles` ["bob"]
|
||||
|
||||
testShortLinkChangePreparedContactUserDuplicate :: HasCallStack => TestParams -> IO ()
|
||||
testShortLinkChangePreparedContactUserDuplicate =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
bob ##> "/create user robert"
|
||||
showActiveUser bob "robert"
|
||||
|
||||
connectUsers alice bob
|
||||
alice <##> bob
|
||||
|
||||
bob ##> "/user bob"
|
||||
showActiveUser bob "bob (Bob)"
|
||||
|
||||
alice ##> "/_connect 1 short=on"
|
||||
(shortLink, fullLink) <- getShortInvitation alice
|
||||
bob ##> ("/_connect plan 1 " <> shortLink)
|
||||
bob <## "invitation link: ok to connect"
|
||||
contactSLinkData <- getTermLine bob
|
||||
bob ##> ("/_prepare contact 1 " <> fullLink <> " " <> shortLink <> " " <> contactSLinkData)
|
||||
bob <## "alice: contact is prepared"
|
||||
|
||||
-- 2 ids are for "user contacts"
|
||||
-- 2 ids are for second user contact cards
|
||||
-- 1 for second user's alice
|
||||
-- so this alice is id 6
|
||||
bob ##> "/_set contact user @6 2"
|
||||
bob <## "contact alice changed from user bob to user robert, new local name: alice_1"
|
||||
|
||||
bob ##> "/user robert"
|
||||
showActiveUser bob "robert"
|
||||
|
||||
bob ##> "/_connect contact @6 text hello"
|
||||
bob
|
||||
<### [ "alice_1: connection started",
|
||||
WithTime "@alice_1 hello"
|
||||
]
|
||||
alice <# "robert_1> hello"
|
||||
concurrently_
|
||||
(bob <## "alice_1 (Alice): contact is connected")
|
||||
(alice <## "robert_1: contact is connected")
|
||||
|
||||
alice #> "@robert_1 hi"
|
||||
bob <# "alice_1> hi"
|
||||
bob #> "@alice_1 hey"
|
||||
alice <# "robert_1> hey"
|
||||
|
||||
alice <##> bob
|
||||
|
||||
alice @@@ [("@robert", "hey"), ("@robert_1", "hey")]
|
||||
alice `hasContactProfiles` ["alice", "robert", "robert"]
|
||||
bob #$> ("/_get chats 2 pcc=on", chats, [("@alice", "hey"), ("@alice_1", "hey"), ("@SimpleX Chat team", ""), ("@SimpleX-Status", ""), ("*", "")])
|
||||
bob `hasContactProfiles` ["robert", "alice", "alice", "SimpleX Chat team", "SimpleX-Status"]
|
||||
bob ##> "/user bob"
|
||||
showActiveUser bob "bob (Bob)"
|
||||
bob @@@ []
|
||||
bob `hasContactProfiles` ["bob"]
|
||||
|
||||
testShortLinkChangePreparedGroupUser :: HasCallStack => TestParams -> IO ()
|
||||
testShortLinkChangePreparedGroupUser =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
createGroup2 "team" alice cath
|
||||
alice ##> "/create link #team short"
|
||||
(shortLink, fullLink) <- getShortGroupLink alice "team" GRMember True
|
||||
|
||||
bob ##> "/create user robert"
|
||||
showActiveUser bob "robert"
|
||||
bob ##> "/user bob"
|
||||
showActiveUser bob "bob (Bob)"
|
||||
|
||||
bob ##> ("/_connect plan 1 " <> shortLink)
|
||||
bob <## "group link: ok to connect"
|
||||
groupSLinkData <- getTermLine bob
|
||||
bob ##> ("/_prepare group 1 " <> fullLink <> " " <> shortLink <> " " <> groupSLinkData)
|
||||
bob <## "#team: group is prepared"
|
||||
|
||||
bob ##> "/_set group user #1 2"
|
||||
bob <## "group #team changed from user bob to user robert"
|
||||
|
||||
bob ##> "/user robert"
|
||||
showActiveUser bob "robert"
|
||||
|
||||
bob ##> "/_connect group #1"
|
||||
bob <## "#team: connection started"
|
||||
alice <## "robert: accepting request to join group #team..."
|
||||
concurrentlyN_
|
||||
[ alice <## "#team: robert joined the group",
|
||||
do
|
||||
bob <## "#team: joining the group..."
|
||||
bob <## "#team: you joined the group"
|
||||
bob <## "#team: member cath (Catherine) is connected",
|
||||
do
|
||||
cath <## "#team: alice added robert to the group (connecting...)"
|
||||
cath <## "#team: new member robert is connected"
|
||||
]
|
||||
|
||||
alice #> "#team 1"
|
||||
[bob, cath] *<# "#team alice> 1"
|
||||
bob #> "#team 2"
|
||||
[alice, cath] *<# "#team robert> 2"
|
||||
threadDelay 1000000
|
||||
cath #> "#team 3"
|
||||
[alice, bob] *<# "#team cath> 3"
|
||||
|
||||
alice @@@ [("#team", "3"), ("@cath","sent invitation to join group team as admin")]
|
||||
alice `hasContactProfiles` ["alice", "cath", "robert"]
|
||||
bob #$> ("/_get chats 2 pcc=on", chats, [("#team", "3"), ("@SimpleX Chat team", ""), ("@SimpleX-Status", ""), ("*", "")])
|
||||
bob `hasContactProfiles` ["robert", "alice", "cath", "SimpleX Chat team", "SimpleX-Status"]
|
||||
cath @@@ [("#team", "3"), ("@alice","received invitation to join group team as admin")]
|
||||
cath `hasContactProfiles` ["cath", "alice", "robert"]
|
||||
bob ##> "/user bob"
|
||||
showActiveUser bob "bob (Bob)"
|
||||
bob @@@ []
|
||||
bob `hasContactProfiles` ["bob"]
|
||||
|
||||
testShortLinkChangePreparedGroupUserDuplicate :: HasCallStack => TestParams -> IO ()
|
||||
testShortLinkChangePreparedGroupUserDuplicate =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
createGroup2 "team" alice cath
|
||||
alice ##> "/create link #team short"
|
||||
(shortLink, fullLink) <- getShortGroupLink alice "team" GRMember True
|
||||
|
||||
bob ##> "/create user robert"
|
||||
showActiveUser bob "robert"
|
||||
|
||||
bob ##> ("/_connect plan 2 " <> shortLink)
|
||||
bob <## "group link: ok to connect"
|
||||
groupSLinkData1 <- getTermLine bob
|
||||
bob ##> ("/_prepare group 2 " <> fullLink <> " " <> shortLink <> " " <> groupSLinkData1)
|
||||
bob <## "#team: group is prepared"
|
||||
|
||||
bob ##> "/user bob"
|
||||
showActiveUser bob "bob (Bob)"
|
||||
|
||||
bob ##> ("/_connect plan 1 " <> shortLink)
|
||||
bob <## "group link: ok to connect"
|
||||
groupSLinkData2 <- getTermLine bob
|
||||
bob ##> ("/_prepare group 1 " <> fullLink <> " " <> shortLink <> " " <> groupSLinkData2)
|
||||
bob <## "#team: group is prepared"
|
||||
|
||||
bob ##> "/_set group user #2 2"
|
||||
bob <## "group #team changed from user bob to user robert, new local name: #team_1"
|
||||
|
||||
bob ##> "/user robert"
|
||||
showActiveUser bob "robert"
|
||||
|
||||
bob ##> "/_connect group #2"
|
||||
bob <## "#team_1: connection started"
|
||||
alice <## "robert: accepting request to join group #team..."
|
||||
concurrentlyN_
|
||||
[ alice <## "#team: robert joined the group",
|
||||
do
|
||||
bob <## "#team_1: joining the group..."
|
||||
bob <## "#team_1: you joined the group"
|
||||
bob <## "#team_1: member cath (Catherine) is connected",
|
||||
do
|
||||
cath <## "#team: alice added robert to the group (connecting...)"
|
||||
cath <## "#team: new member robert is connected"
|
||||
]
|
||||
|
||||
alice #> "#team 1"
|
||||
bob <# "#team_1 alice> 1"
|
||||
cath <# "#team alice> 1"
|
||||
|
||||
bob #> "#team_1 2"
|
||||
[alice, cath] *<# "#team robert> 2"
|
||||
|
||||
cath #> "#team 3"
|
||||
alice <# "#team cath> 3"
|
||||
bob <# "#team_1 cath> 3"
|
||||
|
||||
-- also connect to the first prepared instance of group
|
||||
bob ##> "/_connect group #1"
|
||||
bob <## "#team: connection started"
|
||||
alice <## "robert_1: accepting request to join group #team..."
|
||||
concurrentlyN_
|
||||
[ alice <## "#team: robert_1 joined the group",
|
||||
bob
|
||||
<### [ "#team: joining the group...",
|
||||
"#team: you joined the group",
|
||||
"#team: member cath_1 (Catherine) is connected",
|
||||
"#team: member robert_2 is connected",
|
||||
WithTime "#team alice_1> 1 [>>]",
|
||||
WithTime "#team robert_2> 2 [>>]",
|
||||
WithTime "#team cath_1> 3 [>>]",
|
||||
-- for previously joined instance of group:
|
||||
"#team_1: alice added robert_1 to the group (connecting...)",
|
||||
"#team_1: new member robert_1 is connected"
|
||||
],
|
||||
do
|
||||
cath <## "#team: alice added robert_1 to the group (connecting...)"
|
||||
cath <## "#team: new member robert_1 is connected"
|
||||
]
|
||||
|
||||
alice #> "#team 4"
|
||||
bob
|
||||
<### [ WithTime "#team_1 alice> 4",
|
||||
WithTime "#team alice_1> 4"
|
||||
]
|
||||
cath <# "#team alice> 4"
|
||||
|
||||
bob #> "#team_1 5"
|
||||
[alice, cath] *<# "#team robert> 5"
|
||||
bob <# "#team robert_2> 5"
|
||||
|
||||
bob #> "#team 6"
|
||||
[alice, cath] *<# "#team robert_1> 6"
|
||||
bob <# "#team_1 robert_1> 6"
|
||||
|
||||
threadDelay 1000000
|
||||
cath #> "#team 7"
|
||||
alice <# "#team cath> 7"
|
||||
bob
|
||||
<### [ WithTime "#team_1 cath> 7",
|
||||
WithTime "#team cath_1> 7"
|
||||
]
|
||||
|
||||
alice @@@ [("#team", "7"), ("@cath","sent invitation to join group team as admin")]
|
||||
alice `hasContactProfiles` ["alice", "cath", "robert", "robert"]
|
||||
bob `hasContactProfiles` ["robert", "robert", "robert", "alice", "alice", "cath", "cath", "SimpleX Chat team", "SimpleX-Status"]
|
||||
cath @@@ [("#team", "7"), ("@alice","received invitation to join group team as admin")]
|
||||
cath `hasContactProfiles` ["cath", "alice", "robert", "robert"]
|
||||
bob ##> "/user bob"
|
||||
showActiveUser bob "bob (Bob)"
|
||||
bob @@@ []
|
||||
bob `hasContactProfiles` ["bob"]
|
||||
|
||||
testShortLinkInvitationSetIncognito :: HasCallStack => TestParams -> IO ()
|
||||
testShortLinkInvitationSetIncognito =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
|
||||
Reference in New Issue
Block a user