diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index 2b2d747284..5707cf5d15 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -63,6 +63,7 @@ enum ChatCommand: ChatCmdProtocol { case apiPlanForwardChatItems(fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64]) case apiForwardChatItems(toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, sendAsGroup: Bool, fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64], ttl: Int?) case apiShareChatMsgContent(shareChatType: ChatType, shareChatId: Int64, toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, sendAsGroup: Bool) + case apiShareMyAddress(toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, sendAsGroup: Bool) case apiGetNtfToken case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) case apiVerifyToken(token: DeviceToken, nonce: String, code: String) @@ -270,6 +271,9 @@ enum ChatCommand: ChatCmdProtocol { case let .apiShareChatMsgContent(shareChatType, shareChatId, toChatType, toChatId, toScope, sendAsGroup): let asGroup = sendAsGroup ? "(as_group=on)" : "" return "/_share chat content \(ref(shareChatType, shareChatId, scope: nil)) \(ref(toChatType, toChatId, scope: toScope))\(asGroup)" + case let .apiShareMyAddress(toChatType, toChatId, toScope, sendAsGroup): + let asGroup = sendAsGroup ? "(as_group=on)" : "" + return "/_share address \(ref(toChatType, toChatId, scope: toScope))\(asGroup)" case .apiGetNtfToken: return "/_ntf get " case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)" case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)" @@ -469,6 +473,7 @@ enum ChatCommand: ChatCmdProtocol { case .apiPlanForwardChatItems: return "apiPlanForwardChatItems" case .apiForwardChatItems: return "apiForwardChatItems" case .apiShareChatMsgContent: return "apiShareChatMsgContent" + case .apiShareMyAddress: return "apiShareMyAddress" case .apiGetNtfToken: return "apiGetNtfToken" case .apiRegisterToken: return "apiRegisterToken" case .apiVerifyToken: return "apiVerifyToken" diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 642efc894f..8b8de7c4a8 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -509,6 +509,12 @@ func apiShareChatMsgContent(shareChatType: ChatType, shareChatId: Int64, toChatT throw r.unexpected } +func apiShareMyAddress(toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, sendAsGroup: Bool) async throws -> MsgContent { + let r: ChatResponse1 = try await chatSendCmd(.apiShareMyAddress(toChatType: toChatType, toChatId: toChatId, toScope: toScope, sendAsGroup: sendAsGroup)) + if case let .chatMsgContent(_, mc) = r { return mc } + throw r.unexpected +} + func apiForwardChatItems(toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, sendAsGroup: Bool = false, fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64], ttl: Int?) async -> [ChatItem]? { let cmd: ChatCommand = .apiForwardChatItems(toChatType: toChatType, toChatId: toChatId, toScope: toScope, sendAsGroup: sendAsGroup, fromChatType: fromChatType, fromChatId: fromChatId, fromScope: fromScope, itemIds: itemIds, ttl: ttl) return await processSendMessageCmd(toChatType: toChatType, cmd: cmd) diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index c12740d364..3d1e7ef477 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -112,7 +112,6 @@ struct ChatInfoView: View { @State private var sendReceiptsUserDefault = true @State private var progressIndicator = false @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false - @State private var showSecrets: Set = [] enum ChatInfoViewAlert: Identifiable { case clearChatAlert @@ -394,13 +393,7 @@ struct ChatInfoView: View { .padding(.bottom, 2) } contactSimplexNameView(contact) { contact = $0 } - if let descr = cInfo.shortDescr?.trimmingCharacters(in: .whitespacesAndNewlines), descr != "" { - let r = markdownText(descr, textStyle: .subheadline, showSecrets: showSecrets, backgroundColor: theme.colors.background) - msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets, centered: true, smallFont: true) - .multilineTextAlignment(.center) - .lineLimit(4) - .fixedSize(horizontal: false, vertical: true) - } + ProfileDescriptionView(shortDescr: cInfo.shortDescr, description: cInfo.profileDescription) } .frame(maxWidth: .infinity, alignment: .center) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index e8a5ee96e2..abda2dbcfd 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -141,11 +141,12 @@ func msgTextResultView( _ t: Text, showSecrets: Binding>? = nil, sendCommand: ((String) -> Void)? = nil, + openModal: ((Format) -> Void)? = nil, centered: Bool = false, smallFont: Bool = false ) -> some View { t.if(r.hasSecrets, transform: hiddenSecretsView) - .if(r.handleTaps) { $0.overlay(handleTextTaps(r.string, showSecrets: showSecrets, sendCommand: sendCommand, centered: centered, smallFont: smallFont)) } + .if(r.handleTaps) { $0.overlay(handleTextTaps(r.string, showSecrets: showSecrets, sendCommand: sendCommand, openModal: openModal, centered: centered, smallFont: smallFont)) } } // smallFont parameter is used to pad height, otherwise CTFrameGetLines fails to see them as lines - it's needed if font is not .body @@ -154,6 +155,7 @@ private func handleTextTaps( _ s: NSAttributedString, showSecrets: Binding>? = nil, sendCommand: ((String) -> Void)? = nil, + openModal: ((Format) -> Void)? = nil, centered: Bool, smallFont: Bool ) -> some View { @@ -229,6 +231,8 @@ private func handleTextTaps( } } else if let sendCommand, let cmd = attrs[commandAttrKey] as? String { sendCommand(cmd) + } else if let openModal, let fmt = attrs[modalAttrKey] as? Format { + openModal(fmt) } stop.pointee = true } @@ -264,9 +268,67 @@ private let secretAttrKey = NSAttributedString.Key("chat.simplex.app.secret") private let commandAttrKey = NSAttributedString.Key("chat.simplex.app.command") private let nameAttrKey = NSAttributedString.Key("chat.simplex.app.name") +private let modalAttrKey = NSAttributedString.Key("chat.simplex.app.modal") typealias MsgTextResult = (string: NSMutableAttributedString, hasSecrets: Bool, handleTaps: Bool) +// Reusable profile bio/description header: renders the teaser and opens the full +// description in a sheet when the "Read more" (Format.modal) link is tapped. +struct ProfileDescriptionView: View { + @EnvironmentObject var theme: AppTheme + let shortDescr: String? + let description: String? + @State private var showSecrets: Set = [] + @State private var modal: ModalText? = nil + + var body: some View { + if let r = markdownProfileDescription(shortDescr: shortDescr, description: description, showSecrets: showSecrets, backgroundColor: theme.colors.background) { + msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets, openModal: openModal, centered: true, smallFont: true) + .multilineTextAlignment(.center) + .lineLimit(4) + .fixedSize(horizontal: false, vertical: true) + .appSheet(item: $modal) { m in + FullProfileDescriptionView(description: m.text).environmentObject(theme) + } + } + } + + private func openModal(_ format: Format) { + if case let .modal(_, text) = format { modal = ModalText(text: text) } + } +} + +private struct ModalText: Identifiable { + let id = UUID() + let text: String +} + +private struct FullProfileDescriptionView: View { + @Environment(\.dismiss) private var dismiss + @EnvironmentObject var theme: AppTheme + let description: String + @State private var showSecrets: Set = [] + + var body: some View { + NavigationView { + ScrollView { + let r = markdownText(description, showSecrets: showSecrets, backgroundColor: theme.colors.background) + msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets) + .frame(maxWidth: .infinity, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + .padding() + } + .navigationTitle("Description") + .modifier(ThemedBackground()) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { dismiss() } label: { Image(systemName: "xmark") } + } + } + } + } +} + @inline(__always) func markdownText( _ s: String, @@ -292,6 +354,46 @@ func markdownText( ) } +// Renders a profile bio/description: the bio, a short single-line description, or a +// truncated teaser followed by a "Read more" link that opens the full description (Format.modal). +func markdownProfileDescription( + shortDescr: String?, + description: String?, + showSecrets: Set? = nil, + backgroundColor: Color +) -> MsgTextResult? { + func trimmed(_ s: String?) -> String? { + guard let t = s?.trimmingCharacters(in: .whitespacesAndNewlines), !t.isEmpty else { return nil } + return t + } + let short = trimmed(shortDescr) + let descr = trimmed(description) + guard let descr else { + return short.map { markdownText($0, textStyle: .subheadline, showSecrets: showSecrets, backgroundColor: backgroundColor) } + } + let firstLine = String(descr.prefix(while: { $0 != "\n" })) + let truncated = firstLine.count > 100 + let multiline = descr.count > firstLine.count + if short == nil && !truncated && !multiline { + return markdownText(descr, textStyle: .subheadline, showSecrets: showSecrets, backgroundColor: backgroundColor) + } + let teaser = short ?? (truncated ? String(firstLine.prefix(100)).trimmingCharacters(in: .whitespaces) + "…" : firstLine + "…") + let readMore = NSLocalizedString("Read more", comment: "profile description teaser") + var formatted = parseSimpleXMarkdown(teaser) ?? [FormattedText(text: teaser)] + formatted.append(FormattedText(text: " ")) + formatted.append(FormattedText(text: readMore, format: .modal(modalName: Format.modalDescription, text: descr))) + return messageText( + "\(teaser) \(readMore)", + formatted, + textStyle: .subheadline, + sender: nil, + mentions: nil, + userMemberId: nil, + showSecrets: showSecrets, + backgroundColor: UIColor(backgroundColor) + ) +} + func messageText( _ text: String, @@ -456,6 +558,12 @@ func messageText( attrs[linkAttrKey] = "tel:" + t.replacingOccurrences(of: " ", with: "") handleTaps = true } + case let .modal(modalName, text): + attrs = linkAttrs() + if !preview { + attrs[modalAttrKey] = Format.modal(modalName: modalName, text: text) + handleTaps = true + } case .unknown: () case .none: () } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 21586d9533..6d9043332d 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -1005,7 +1005,6 @@ struct ChatView: View { @EnvironmentObject var theme: AppTheme @AppStorage(DEFAULT_CHAT_ITEM_ROUNDNESS) private var roundness = defaultChatItemRoundness @Binding @ObservedObject var chat: Chat - @State private var showSecrets: Set = [] var body: some View { let v = VStack(spacing: 8) { @@ -1028,14 +1027,8 @@ struct ChatView: View { .frame(maxWidth: 260) } - if let shortDescr = chat.chatInfo.shortDescr { - let r = markdownText(shortDescr, textStyle: .subheadline, showSecrets: showSecrets, backgroundColor: theme.colors.background) - msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets, centered: true, smallFont: true) - .multilineTextAlignment(.center) - .lineLimit(4) - .fixedSize(horizontal: false, vertical: true) - .padding(.horizontal) - } + ProfileDescriptionView(shortDescr: chat.chatInfo.shortDescr, description: chat.chatInfo.profileDescription) + .padding(.horizontal) switch chat.chatInfo { case let .direct(contact): diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index 60b500ec41..cc8584b46d 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -25,6 +25,7 @@ struct UserAddressView: View { @State private var mailViewResult: Result? = nil @State private var alert: UserAddressAlert? @State private var progressIndicator = false + @State private var showShareViaChat = false private enum UserAddressAlert: Identifiable { case deleteAddress @@ -156,6 +157,7 @@ struct UserAddressView: View { upgradeAddressButton() } shareAddressButton(userAddress) + shareViaChatButton(userAddress) // if MFMailComposeViewController.canSendMail() { // shareViaEmailButton(userAddress) // } @@ -378,6 +380,32 @@ struct UserAddressView: View { } } + private func shareViaChatButton(_ userAddress: UserContactLink) -> some View { + Button { + if userAddress.shouldBeUpgraded { + showAlert( + NSLocalizedString("Upgrade address?", comment: "alert title"), + message: NSLocalizedString("The address will be short, and your profile will be shared via the address.", comment: "alert message"), + actions: {[ + UIAlertAction(title: NSLocalizedString("Upgrade", comment: "alert button"), style: .default) { _ in + addShortLink(progressIndicator: $progressIndicator, onComplete: { showShareViaChat = true }) + }, + cancelAlertAction + ]} + ) + } else { + showShareViaChat = true + } + } label: { + settingsRow("arrowshape.turn.up.forward", color: theme.colors.primary) { + Text("Share via chat").foregroundColor(theme.colors.primary) + } + } + .sheet(isPresented: $showShareViaChat) { + shareAddressPicker() + } + } + private func shareViaEmailButton(_ userAddress: UserContactLink) -> some View { Button { showMailView = true @@ -451,7 +479,60 @@ func upgradeAndShareAddressAlert(progressIndicator: Binding, shareAddress: ) } -private func addShortLink(progressIndicator: Binding, shareOnCompletion: Bool = false) { +@ViewBuilder +func shareAddressPicker(composeState: Binding? = nil) -> some View { + let v = ChatItemForwardingView( + title: "Share address", + isProhibited: { $0.prohibitedByPref(hasSimplexLink: true, isMediaOrFileAttachment: false, isVoice: false) }, + onSelectChat: { chat in shareMyAddress(chat, composeState: composeState) }, + includeLocal: false + ) + if #available(iOS 16.0, *) { + v.presentationDetents([.fraction(0.8)]) + } else { + v + } +} + +func shareMyAddress(_ destChat: Chat, composeState: Binding? = nil) { + let sendAsGroup = if let gInfo = destChat.chatInfo.groupInfo { gInfo.useRelays && gInfo.membership.memberRole >= .owner } else { false } + Task { + do { + let mc = try await apiShareMyAddress( + toChatType: destChat.chatInfo.chatType, toChatId: destChat.chatInfo.apiId, + toScope: destChat.chatInfo.groupChatScope(), sendAsGroup: sendAsGroup + ) + if case let .chat(_, chatLink, ownerSig) = mc { + await MainActor.run { + dismissAllSheets { + let cs = ComposeState(preview: .chatLinkPreview(chatLink: chatLink, ownerSig: ownerSig)) + if let composeState { + composeState.wrappedValue = cs + } else { + ChatModel.shared.draft = cs + ChatModel.shared.draftChatId = destChat.id + } + if destChat.id != ChatModel.shared.chatId { + ItemsModel.shared.loadOpenChat(destChat.id) + } + } + } + } else { + logger.error("shareMyAddress: unexpected MsgContent: \(String(describing: mc))") + await MainActor.run { + showAlert(NSLocalizedString("Error sharing address", comment: "alert title"), message: String(describing: mc)) + } + } + } catch { + logger.error("shareMyAddress error: \(error.localizedDescription)") + await MainActor.run { + showAlert(NSLocalizedString("Error sharing address", comment: "alert title"), message: error.localizedDescription) + } + } + } +} + +private func addShortLink(progressIndicator: Binding, shareOnCompletion: Bool = false, onComplete: (() -> Void)? = nil) { progressIndicator.wrappedValue = true Task { do { @@ -462,6 +543,7 @@ private func addShortLink(progressIndicator: Binding, shareOnCompletion: B if shareOnCompletion, let userAddress { userAddress.shareAddress(short: true) } + onComplete?() } } catch let error { logger.error("apiAddMyAddressShortLink: \(responseError(error))") diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index 2e609c3f7d..0ad9f9003e 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -16,6 +16,8 @@ struct UserProfile: View { @State private var profile = Profile(displayName: "", fullName: "") @State private var currentProfileHash: Int? @State private var shortDescr = "" + @State private var description = "" + @State private var editingDescription = false // Modals @State private var showChooseSource = false @State private var showImagePicker = false @@ -54,6 +56,11 @@ struct UserProfile: View { } } } + Button { + editingDescription = true + } label: { + Text(description.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "Add description" : "Edit description") + } } footer: { Text("Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.") } @@ -64,7 +71,8 @@ struct UserProfile: View { } .disabled( currentProfileHash == profile.hashValue && - (profile.shortDescr ?? "") == shortDescr.trimmingCharacters(in: .whitespaces) + (profile.shortDescr ?? "") == shortDescr.trimmingCharacters(in: .whitespaces) && + (profile.description ?? "") == description.trimmingCharacters(in: .whitespacesAndNewlines) ) Button(action: saveProfile) { Text("Save (and notify contacts)") @@ -125,6 +133,9 @@ struct UserProfile: View { } } .alert(item: $alert) { a in userProfileAlert(a, $profile.displayName) } + .sheet(isPresented: $editingDescription) { + ProfileDescriptionEditor(description: $description) + } } private func showFullName(_ user: User) -> Bool { @@ -138,7 +149,8 @@ struct UserProfile: View { private var canSaveProfile: Bool { ( currentProfileHash != profile.hashValue || - (chatModel.currentUser?.profile.shortDescr ?? "") != shortDescr.trimmingCharacters(in: .whitespaces) + (chatModel.currentUser?.profile.shortDescr ?? "") != shortDescr.trimmingCharacters(in: .whitespaces) || + (chatModel.currentUser?.profile.description ?? "") != description.trimmingCharacters(in: .whitespacesAndNewlines) ) && profile.displayName.trimmingCharacters(in: .whitespaces) != "" && validDisplayName(profile.displayName) && @@ -151,6 +163,8 @@ struct UserProfile: View { do { profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces) profile.shortDescr = shortDescr.trimmingCharacters(in: .whitespaces) + let d = description.trimmingCharacters(in: .whitespacesAndNewlines) + profile.description = d.isEmpty ? nil : d if let (newProfile, _) = try await apiUpdateProfile(profile: profile) { await MainActor.run { chatModel.updateCurrentUser(newProfile) @@ -170,6 +184,7 @@ struct UserProfile: View { profile = fromLocalProfile(user.profile) currentProfileHash = profile.hashValue shortDescr = profile.shortDescr ?? "" + description = profile.description ?? "" } } } @@ -235,3 +250,32 @@ func editImageButton(action: @escaping () -> Void) -> some View { .frame(width: 48) } } + +struct ProfileDescriptionEditor: View { + @Environment(\.dismiss) var dismiss + @EnvironmentObject var theme: AppTheme + @Binding var description: String + + var body: some View { + NavigationView { + ZStack(alignment: .topLeading) { + TextEditor(text: $description) + if description.isEmpty { + Text("Enter description (optional)") + .foregroundColor(theme.colors.secondary) + .padding(.top, 8) + .padding(.leading, 5) + .allowsHitTesting(false) + } + } + .padding() + .navigationTitle("Description") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Done") { dismiss() } + } + } + } + } +} diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 9cb1493fa4..91b2a85fa8 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -34,6 +34,7 @@ public struct User: Identifiable, Decodable, UserLike, NamedChat, Hashable { public var displayName: String { get { profile.displayName } } public var fullName: String { get { profile.fullName } } public var shortDescr: String? { profile.shortDescr } + public var profileDescription: String? { profile.description } public var image: String? { get { profile.image } } public var localAlias: String { get { "" } } @@ -116,6 +117,7 @@ public struct Profile: Codable, NamedChat, Hashable { displayName: String, fullName: String, shortDescr: String? = nil, + description: String? = nil, image: String? = nil, contactLink: String? = nil, preferences: Preferences? = nil, @@ -125,6 +127,7 @@ public struct Profile: Codable, NamedChat, Hashable { self.displayName = displayName self.fullName = fullName self.shortDescr = shortDescr + self.description = description self.image = image self.contactLink = contactLink self.preferences = preferences @@ -134,6 +137,7 @@ public struct Profile: Codable, NamedChat, Hashable { public var displayName: String public var fullName: String public var shortDescr: String? + public var description: String? public var image: String? public var contactLink: String? public var preferences: Preferences? @@ -143,6 +147,8 @@ public struct Profile: Codable, NamedChat, Hashable { public var localAlias: String { get { "" } } public var contactDomain: SimplexDomainClaim? + public var profileDescription: String? { description } + var profileViewName: String { (fullName == "" || displayName == fullName) ? displayName : "\(displayName) (\(fullName))" } @@ -159,6 +165,7 @@ public struct LocalProfile: Codable, NamedChat, Hashable { displayName: String, fullName: String, shortDescr: String? = nil, + description: String? = nil, image: String? = nil, contactLink: String? = nil, preferences: Preferences? = nil, @@ -172,6 +179,7 @@ public struct LocalProfile: Codable, NamedChat, Hashable { self.displayName = displayName self.fullName = fullName self.shortDescr = shortDescr + self.description = description self.image = image self.contactLink = contactLink self.preferences = preferences @@ -186,6 +194,7 @@ public struct LocalProfile: Codable, NamedChat, Hashable { public var displayName: String public var fullName: String public var shortDescr: String? + public var description: String? public var image: String? public var contactLink: String? public var preferences: Preferences? @@ -195,6 +204,8 @@ public struct LocalProfile: Codable, NamedChat, Hashable { public var contactDomain: SimplexDomainClaim? public var contactDomainVerified: Bool? + public var profileDescription: String? { description } + var profileViewName: String { localAlias == "" ? (fullName == "" || displayName == fullName) ? displayName : "\(displayName) (\(fullName))" @@ -285,6 +296,7 @@ public func toLocalProfile (_ profileId: Int64, _ profile: Profile, _ localAlias displayName: profile.displayName, fullName: profile.fullName, shortDescr: profile.shortDescr, + description: profile.description, image: profile.image, contactLink: profile.contactLink, preferences: profile.preferences, @@ -298,6 +310,7 @@ public func fromLocalProfile (_ profile: LocalProfile) -> Profile { displayName: profile.displayName, fullName: profile.fullName, shortDescr: profile.shortDescr, + description: profile.description, image: profile.image, contactLink: profile.contactLink, preferences: profile.preferences, @@ -323,11 +336,14 @@ public protocol NamedChat { var displayName: String { get } var fullName: String { get } var shortDescr: String? { get } + var profileDescription: String? { get } var image: String? { get } var localAlias: String { get } } extension NamedChat { + public var profileDescription: String? { nil } + public var chatViewName: String { localAlias == "" ? displayName + (fullName == "" || fullName == displayName ? "" : " / \(fullName)") @@ -1608,6 +1624,17 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { } } + public var profileDescription: String? { + switch self { + case let .direct(contact): contact.profile.description + case let .group(groupInfo, _): groupInfo.profileDescription + case .local: nil + case let .contactRequest(contactRequest): contactRequest.profile.description + case .contactConnection: nil + case .invalidJSON: nil + } + } + public var image: String? { get { switch self { @@ -2168,6 +2195,7 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { public var displayName: String { localAlias == "" ? profile.displayName : localAlias } public var fullName: String { get { profile.fullName } } public var shortDescr: String? { profile.shortDescr } + public var profileDescription: String? { profile.description } public var image: String? { get { profile.image } } public var contactLink: String? { get { profile.contactLink } } public var localAlias: String { profile.localAlias } @@ -2386,6 +2414,7 @@ public struct UserContactRequest: Decodable, NamedChat, Hashable { var ready: Bool { get { true } } public var displayName: String { get { profile.displayName } } public var shortDescr: String? { profile.shortDescr } + public var profileDescription: String? { profile.description } public var fullName: String { get { profile.fullName } } public var image: String? { get { profile.image } } public var localAlias: String { "" } @@ -2562,6 +2591,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { public var displayName: String { localAlias == "" ? groupProfile.displayName : localAlias } public var fullName: String { get { groupProfile.fullName } } public var shortDescr: String? { groupProfile.shortDescr } + public var profileDescription: String? { businessChat != nil ? groupProfile.description : nil } public var image: String? { get { groupProfile.image } } public var chatTags: [Int64] public var chatItemTTL: Int64? @@ -5203,11 +5233,7 @@ public enum MsgChatLink: Equatable, Hashable { NSLocalizedString("One-time link", comment: "chat link info line") } if signed { - s += " " + ( - self.isPublicGroup - ? NSLocalizedString("(from owner)", comment: "chat link info line") - : NSLocalizedString("(signed)", comment: "chat link info line") - ) + s += " " + NSLocalizedString("(from owner)", comment: "chat link info line") } return s } @@ -5310,10 +5336,14 @@ public enum Format: Decodable, Equatable, Hashable { case simplexName(nameInfo: SimplexNameInfo) case command(commandStr: String) case mention(memberName: String) + case modal(modalName: String, text: String) case email case phone case unknown + // client-only format that opens a modal when tapped, see openMarkdownModal + public static let modalDescription = "description" + public var isSimplexLink: Bool { get { switch (self) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index db5239743a..a755b8bc6e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1294,6 +1294,7 @@ data class User( override val displayName: String get() = profile.displayName override val fullName: String get() = profile.fullName override val shortDescr: String? get() = profile.shortDescr + override val profileDescription: String? get() = profile.description override val image: String? get() = profile.image override val localAlias: String = "" @@ -1366,6 +1367,7 @@ interface NamedChat { val displayName: String val fullName: String val shortDescr: String? + val profileDescription: String? get() = null val image: String? val localAlias: String val chatViewName: String @@ -1491,6 +1493,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val displayName get() = contact.displayName override val fullName get() = contact.fullName override val shortDescr get() = contact.profile.shortDescr + override val profileDescription get() = contact.profile.description override val image get() = contact.image override val localAlias: String get() = contact.localAlias override fun anyNameContains(searchAnyCase: String): Boolean = contact.anyNameContains(searchAnyCase) @@ -1519,6 +1522,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val displayName get() = groupInfo.displayName override val fullName get() = groupInfo.fullName override val shortDescr get() = groupInfo.groupProfile.shortDescr + override val profileDescription get() = groupInfo.profileDescription override val image get() = groupInfo.image override val localAlias get() = groupInfo.localAlias @@ -1573,6 +1577,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val displayName get() = contactRequest.displayName override val fullName get() = contactRequest.fullName override val shortDescr get() = contactRequest.profile.shortDescr + override val profileDescription get() = contactRequest.profile.description override val image get() = contactRequest.image override val localAlias get() = contactRequest.localAlias @@ -1863,6 +1868,7 @@ data class Contact( override val displayName get() = localAlias.ifEmpty { profile.displayName } override val fullName get() = profile.fullName override val shortDescr get() = profile.shortDescr + override val profileDescription get() = profile.description override val image get() = profile.image val contactLink: String? = profile.contactLink override val localAlias get() = profile.localAlias @@ -2032,6 +2038,7 @@ data class Profile( override val displayName: String, override val fullName: String, override val shortDescr: String?, + val description: String? = null, override val image: String? = null, override val localAlias : String = "", val contactLink: String? = null, @@ -2042,12 +2049,14 @@ data class Profile( val badge: BadgeProof? = null, val contactDomain: SimplexDomainClaim? = null ): NamedChat { + override val profileDescription: String? get() = description + val profileViewName: String get() { return if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)" } - fun toLocalProfile(profileId: Long): LocalProfile = LocalProfile(profileId, displayName, fullName, shortDescr, image, localAlias, contactLink, preferences, peerType, contactDomain = contactDomain) + fun toLocalProfile(profileId: Long): LocalProfile = LocalProfile(profileId, displayName, fullName, shortDescr, description, image, localAlias, contactLink, preferences, peerType, contactDomain = contactDomain) companion object { val sampleData = Profile( @@ -2064,6 +2073,7 @@ data class LocalProfile( override val displayName: String, override val fullName: String, override val shortDescr: String?, + val description: String? = null, override val image: String? = null, override val localAlias: String, val contactLink: String? = null, @@ -2073,9 +2083,11 @@ data class LocalProfile( val contactDomain: SimplexDomainClaim? = null, val contactDomainVerified: Boolean? = null ): NamedChat { + override val profileDescription: String? get() = description + val profileViewName: String = localAlias.ifEmpty { if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)" } - fun toProfile(): Profile = Profile(displayName, fullName, shortDescr, image, localAlias, contactLink, preferences, peerType, contactDomain = contactDomain) + fun toProfile(): Profile = Profile(displayName, fullName, shortDescr, description, image, localAlias, contactLink, preferences, peerType, contactDomain = contactDomain) companion object { val sampleData = LocalProfile( @@ -2225,6 +2237,7 @@ data class GroupInfo ( override val displayName get() = localAlias.ifEmpty { groupProfile.displayName } override val fullName get() = groupProfile.fullName override val shortDescr get() = groupProfile.shortDescr + override val profileDescription get() = if (businessChat != null) groupProfile.description else null override val image get() = groupProfile.image val isOwner: Boolean @@ -2554,6 +2567,7 @@ data class GroupMember ( } override val fullName: String get() = memberProfile.fullName override val shortDescr: String? get() = memberProfile.shortDescr + override val profileDescription: String? get() = memberProfile.description override val image: String? get() = memberProfile.image val contactLink: String? = memberProfile.contactLink val verified get() = memberVerifiedCode != null || activeConn?.connectionCode != null @@ -4812,7 +4826,7 @@ sealed class MsgChatLink { is Invitation -> generalGetString(MR.strings.chat_link_one_time) } if (signed) { - s += " " + if (isPublicGroup) generalGetString(MR.strings.chat_link_from_owner) else generalGetString(MR.strings.chat_link_signed) + s += " " + generalGetString(MR.strings.chat_link_from_owner) } return s } @@ -4859,6 +4873,11 @@ sealed class Format { @Serializable @SerialName("simplexName") class SimplexName(val nameInfo: SimplexNameInfo): Format() @Serializable @SerialName("command") class Command(val commandStr: String): Format() @Serializable @SerialName("mention") class Mention(val memberName: String): Format() + @Serializable @SerialName("modal") class Modal(val modalName: String, val text: String): Format() { + companion object { + const val Description = "description" + } + } @Serializable @SerialName("email") class Email: Format() @Serializable @SerialName("phone") class Phone: Format() @Serializable @SerialName("unknown") class Unknown: Format() @@ -4879,6 +4898,7 @@ sealed class Format { is Mention -> SpanStyle(fontWeight = FontWeight.Medium) is Email -> linkStyle is Phone -> linkStyle + is Modal -> linkStyle is Unknown -> SpanStyle() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index ec3e58ca54..4ebfec1faa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -1174,6 +1174,13 @@ object ChatController { return null } + suspend fun apiShareMyAddress(rh: Long?, toChatType: ChatType, toChatId: Long, toScope: GroupChatScope?, sendAsGroup: Boolean): MsgContent? { + val r = sendCmd(rh, CC.ApiShareMyAddress(toChatType, toChatId, toScope, sendAsGroup)) + if (r is API.Result && r.res is CR.ChatMsgContent) return r.res.msgContent + apiErrorAlert("apiShareMyAddress", generalGetString(MR.strings.error_sharing_address), r) + return null + } + suspend fun apiPlanForwardChatItems(rh: Long?, fromChatType: ChatType, fromChatId: Long, fromScope: GroupChatScope?, chatItemIds: List): CR.ForwardPlan? { val r = sendCmd(rh, CC.ApiPlanForwardChatItems(fromChatType, fromChatId, fromScope, chatItemIds)) if (r is API.Result && r.res is CR.ForwardPlan) return r.res @@ -3809,6 +3816,7 @@ sealed class CC { class ApiPlanForwardChatItems(val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val chatItemIds: List): CC() class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val toScope: GroupChatScope?, val sendAsGroup: Boolean, val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val itemIds: List, val ttl: Int?): CC() class ApiShareChatMsgContent(val shareChatType: ChatType, val shareChatId: Long, val toChatType: ChatType, val toChatId: Long, val toScope: GroupChatScope?, val sendAsGroup: Boolean): CC() + class ApiShareMyAddress(val toChatType: ChatType, val toChatId: Long, val toScope: GroupChatScope?, val sendAsGroup: Boolean): CC() class ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC() class ApiNewPublicGroup(val userId: Long, val incognito: Boolean, val relayIds: List, val groupProfile: GroupProfile): CC() class ApiGetGroupRelays(val groupId: Long): CC() @@ -4015,6 +4023,7 @@ sealed class CC { is ApiShareChatMsgContent -> { "/_share chat content ${chatRef(shareChatType, shareChatId, null)} ${chatRef(toChatType, toChatId, toScope)}${if (sendAsGroup) "(as_group=on)" else ""}" } + is ApiShareMyAddress -> "/_share address ${chatRef(toChatType, toChatId, toScope)}${if (sendAsGroup) "(as_group=on)" else ""}" is ApiPlanForwardChatItems -> { "/_forward plan ${chatRef(fromChatType, fromChatId, fromScope)} ${chatItemIds.joinToString(",")}" } @@ -4206,6 +4215,7 @@ sealed class CC { is ApiGetReactionMembers -> "apiGetReactionMembers" is ApiForwardChatItems -> "apiForwardChatItems" is ApiShareChatMsgContent -> "apiShareChatMsgContent" + is ApiShareMyAddress -> "apiShareMyAddress" is ApiPlanForwardChatItems -> "apiPlanForwardChatItems" is ApiNewGroup -> "apiNewGroup" is ApiNewPublicGroup -> "apiNewPublicGroup" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt index 49c2f95667..843bacecec 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt @@ -370,7 +370,7 @@ fun createProfileInProfiles(chatModel: ChatModel, displayName: String, shortDesc withBGApi { val rhId = chatModel.remoteHostId() val user = chatModel.controller.apiCreateActiveUser( - rhId, Profile(displayName.trim(), "", shortDescr.trim().ifEmpty { null }, image) + rhId, Profile(displayName.trim(), "", shortDescr.trim().ifEmpty { null }, image = image) ) ?: return@withBGApi chatModel.currentUser.value = user if (chatModel.users.isEmpty()) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index a5cce3c258..91b3270dea 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -775,19 +775,46 @@ fun ChatInfoDescription(c: NamedChat, displayName: String, copyNameToClipboard: modifier = Modifier.padding(top = DEFAULT_PADDING_HALF).combinedClickable(onClick = copyFullName, onLongClick = copyFullName).onRightClick(copyFullName) ) } - val descr = c.shortDescr?.trim() - if (descr != null && descr != "") { - MarkdownText( - descr, - parseToMarkdown(descr), - toggleSecrets = true, - style = MaterialTheme.typography.body2.copy(color = MaterialTheme.colors.onBackground, lineHeight = 21.sp, textAlign = TextAlign.Center), - maxLines = 4, - overflow = TextOverflow.Ellipsis, - uriHandler = LocalUriHandler.current, - modifier = Modifier.padding(top = DEFAULT_PADDING_HALF), - linkMode = chatModel.simplexLinkMode.value - ) + ProfileDescriptionText( + shortDescr = c.shortDescr, + description = c.profileDescription, + style = MaterialTheme.typography.body2.copy(color = MaterialTheme.colors.onBackground, lineHeight = 21.sp, textAlign = TextAlign.Center), + modifier = Modifier.padding(top = DEFAULT_PADDING_HALF) + ) +} + +@Composable +fun ProfileDescriptionText(shortDescr: String?, description: String?, style: TextStyle, modifier: Modifier = Modifier) { + val short = shortDescr?.trim()?.ifEmpty { null } + val descr = description?.trim()?.ifEmpty { null } + val uriHandler = LocalUriHandler.current + val linkMode = chatModel.simplexLinkMode.value + if (descr == null) { + if (short != null) { + MarkdownText( + short, parseToMarkdown(short), toggleSecrets = true, style = style, maxLines = 4, + overflow = TextOverflow.Ellipsis, uriHandler = uriHandler, modifier = modifier, linkMode = linkMode + ) + } + } else { + val firstLine = descr.lineSequence().first() + val truncated = firstLine.length > 100 + val multiline = descr.length > firstLine.length + if (short == null && !truncated && !multiline) { + MarkdownText( + descr, parseToMarkdown(descr), toggleSecrets = true, style = style, maxLines = 4, + overflow = TextOverflow.Ellipsis, uriHandler = uriHandler, modifier = modifier, linkMode = linkMode + ) + } else { + val teaser = short ?: (if (truncated) firstLine.take(100).trimEnd() + "…" else "$firstLine…") + val readMore = stringResource(MR.strings.whats_new_read_more) + val formatted = (parseToMarkdown(teaser) ?: FormattedText.plain(teaser)) + + FormattedText(" ") + FormattedText(readMore, Format.Modal(Format.Modal.Description, descr)) + MarkdownText( + "$teaser $readMore", formatted, toggleSecrets = true, style = style, maxLines = 4, + overflow = TextOverflow.Ellipsis, uriHandler = uriHandler, modifier = modifier, linkMode = linkMode + ) + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index c1603f2bdc..e762c5610d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -2328,20 +2328,12 @@ fun BoxScope.ChatItemsList( ) } - val descr = chatInfo.shortDescr?.trim() - if (descr != null && descr != "") { - MarkdownText( - descr, - parseToMarkdown(descr), - toggleSecrets = true, - style = MaterialTheme.typography.body2.copy(color = MaterialTheme.colors.onBackground, lineHeight = 21.sp, textAlign = TextAlign.Center), - maxLines = 4, - overflow = TextOverflow.Ellipsis, - uriHandler = LocalUriHandler.current, - modifier = Modifier.padding(top = DEFAULT_PADDING_HALF), - linkMode = linkMode - ) - } + ProfileDescriptionText( + shortDescr = chatInfo.shortDescr, + description = chatInfo.profileDescription, + style = MaterialTheme.typography.body2.copy(color = MaterialTheme.colors.onBackground, lineHeight = 21.sp, textAlign = TextAlign.Center), + modifier = Modifier.padding(top = DEFAULT_PADDING_HALF) + ) when (chatInfo) { is ChatInfo.Direct -> ContactSimplexNameView(chatInfo.contact, verifiable = false) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index fb00603166..4c84965b9e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -526,6 +526,7 @@ fun ComposeView( is SharedContent.Text -> emptyList() is SharedContent.Forward -> emptyList() is SharedContent.ChatLink -> emptyList() + is SharedContent.MyAddress -> emptyList() } // When sharing a file and pasting it in SimpleX itself, the file shouldn't be deleted before sending or before leaving the chat after sharing chatModel.filesToDelete.removeAll { file -> @@ -1543,6 +1544,22 @@ fun ComposeView( } } } + is SharedContent.MyAddress -> { + val cInfo = chat.chatInfo + val sendAsGroup = cInfo.sendAsGroup + withBGApi { + val mc = chatModel.controller.apiShareMyAddress( + chat.remoteHostId, + cInfo.chatType, cInfo.apiId, + cInfo.groupChatScope(), sendAsGroup + ) + if (mc is MsgContent.MCChat) { + composeState.value = composeState.value.copy( + preview = ComposePreview.ChatLinkPreview(mc.chatLink, mc.ownerSig) + ) + } + } + } null -> {} } chatModel.sharedContent.value = null diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt index c7c96cd731..9d952eb10f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt @@ -3,6 +3,7 @@ package chat.simplex.common.views.chat.item import SectionItemView import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.InlineTextContent import androidx.compose.material.MaterialTheme @@ -23,6 +24,7 @@ import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.CurrentColors +import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.chat.SelectionHighlightColor import chat.simplex.common.views.helpers.* import chat.simplex.res.* @@ -39,6 +41,29 @@ fun appendSender(b: AnnotatedString.Builder, sender: String?, senderBold: Boolea } } +private fun openMarkdownModal(modal: Format.Modal) { + when (modal.modalName) { + Format.Modal.Description -> showFullProfileDescription(modal.text) + } +} + +private fun showFullProfileDescription(description: String) { + ModalManager.end.showModalCloseable { _ -> + ColumnWithScrollBar { + AppBarTitle(generalGetString(MR.strings.profile_description__field)) + MarkdownText( + description, + parseToMarkdown(description), + toggleSecrets = true, + style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground, lineHeight = 22.sp), + uriHandler = LocalUriHandler.current, + linkMode = chatModel.simplexLinkMode.value, + modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING), + ) + } + } +} + private val noTyping: AnnotatedString = AnnotatedString(" ") private val typingIndicators: List = listOf( @@ -193,6 +218,7 @@ fun MarkdownText ( var hasLinks = false var hasSecrets = false var hasCommands = false + var hasModals = false val annotatedText = buildAnnotatedString { inlineContent?.first?.invoke(this) appendSender(this, sender, senderBold) @@ -302,6 +328,13 @@ fun MarkdownText ( withStyle(ftStyle) { append(ft.text) } } } + is Format.Modal -> { + hasModals = true + val ftStyle = Format.linkStyle + withAnnotation(tag = "MODAL", annotation = i.toString()) { + withStyle(ftStyle) { append(ft.text) } + } + } is Format.Unknown -> append(ft.text) } } @@ -315,7 +348,7 @@ fun MarkdownText ( else */if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) } } val clampedRange = selectionRange?.let { it.first .. minOf(it.last, selectableEnd) } - if ((hasLinks && uriHandler != null) || hasSecrets || (hasCommands && sendCommandMsg != null)) { + if ((hasLinks && uriHandler != null) || hasSecrets || (hasCommands && sendCommandMsg != null) || hasModals) { val icon = remember { mutableStateOf(PointerIcon.Text) } ClickableText(annotatedText, style = style, selectionRange = clampedRange, modifier = modifier.pointerHoverIcon(icon.value), maxLines = maxLines, overflow = overflow, onLongClick = { offset -> @@ -353,11 +386,16 @@ fun MarkdownText ( if (hasCommands && sendCommandMsg != null) { withAnnotation("COMMAND") { a -> sendCommandMsg("/${a.item}") } } + if (hasModals) { + withAnnotation("MODAL") { a -> + (a.item.toIntOrNull()?.let { formattedText.getOrNull(it)?.format } as? Format.Modal)?.let { openMarkdownModal(it) } + } + } }, onHover = { offset -> val hasAnnotation: (String) -> Boolean = { tag -> annotatedText.hasStringAnnotations(tag, start = offset, end = offset) } icon.value = - if (hasAnnotation("WEB_URL") || hasAnnotation("SIMPLEX_URL") || hasAnnotation("OTHER_URL") || hasAnnotation("SIMPLEX_NAME") || hasAnnotation("SECRET") || hasAnnotation("COMMAND")) { + if (hasAnnotation("WEB_URL") || hasAnnotation("SIMPLEX_URL") || hasAnnotation("OTHER_URL") || hasAnnotation("SIMPLEX_NAME") || hasAnnotation("SECRET") || hasAnnotation("COMMAND") || hasAnnotation("MODAL")) { PointerIcon.Hand } else { PointerIcon.Text @@ -367,6 +405,7 @@ fun MarkdownText ( annotatedText.hasStringAnnotations(tag = "WEB_URL", start = offset, end = offset) || annotatedText.hasStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset) || annotatedText.hasStringAnnotations(tag = "OTHER_URL", start = offset, end = offset) + || annotatedText.hasStringAnnotations(tag = "MODAL", start = offset, end = offset) }, onTextLayout = { onTextLayoutResult?.invoke(it) } ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt index 4e58501b19..382d126db7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt @@ -51,7 +51,7 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) { } } } - is SharedContent.ChatLink -> { + is SharedContent.ChatLink, is SharedContent.MyAddress -> { hasSimplexLink = true } null -> {} @@ -99,9 +99,10 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal } val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } } val navButton: @Composable RowScope.() -> Unit = { + val sharedContent = remember { chatModel.sharedContent }.value when { showSearch -> NavigationButtonBack(hideSearchOnBack) - (users.size > 1 || chatModel.remoteHosts.isNotEmpty()) && remember { chatModel.sharedContent }.value !is SharedContent.Forward && remember { chatModel.sharedContent }.value !is SharedContent.ChatLink -> { + (users.size > 1 || chatModel.remoteHosts.isNotEmpty()) && sharedContent !is SharedContent.Forward && sharedContent !is SharedContent.ChatLink && sharedContent !is SharedContent.MyAddress -> { val allRead = users .filter { u -> !u.user.activeUser && !u.user.hidden } .all { u -> u.unreadCount == 0 } @@ -127,7 +128,6 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal } } else -> NavigationButtonBack(onButtonClicked = { - val sharedContent = chatModel.sharedContent.value // Drop shared content chatModel.sharedContent.value = null if (sharedContent is SharedContent.Forward) { @@ -150,6 +150,7 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal is SharedContent.File -> stringResource(MR.strings.share_file) is SharedContent.Forward -> if (v.chatItems.size > 1) stringResource(MR.strings.forward_multiple) else stringResource(MR.strings.forward_message) is SharedContent.ChatLink -> stringResource(MR.strings.share_channel) + is SharedContent.MyAddress -> stringResource(MR.strings.share_address) null -> stringResource(MR.strings.share_message) }, color = MaterialTheme.colors.onBackground, @@ -196,7 +197,7 @@ private fun ShareList( val oneHandUI = remember { appPrefs.oneHandUI.state } val chats by remember(search) { derivedStateOf { - val sorted = chatModel.chats.value.toList().filter { it.chatInfo.ready && it.chatInfo.sendMsgEnabled && !(chatModel.sharedContent.value is SharedContent.ChatLink && it.chatInfo is ChatInfo.Local) }.sortedByDescending { it.chatInfo is ChatInfo.Local } + val sorted = chatModel.chats.value.toList().filter { it.chatInfo.ready && it.chatInfo.sendMsgEnabled && !((chatModel.sharedContent.value is SharedContent.ChatLink || chatModel.sharedContent.value is SharedContent.MyAddress) && it.chatInfo is ChatInfo.Local) }.sortedByDescending { it.chatInfo is ChatInfo.Local } filteredChats(mutableStateOf(false), mutableStateOf>(emptySet()), search, sorted) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt index cf3281f776..e1aa1f98dd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt @@ -16,6 +16,7 @@ sealed class SharedContent { data class File(val text: String, val uri: URI): SharedContent() data class Forward(val chatItems: List, val fromChatInfo: ChatInfo): SharedContent() data class ChatLink(val groupInfo: GroupInfo): SharedContent() + object MyAddress: SharedContent() } enum class AnimatedViewState { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt index 5394bf1040..87660c7e65 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt @@ -354,6 +354,14 @@ private fun UserAddressLayout( share(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value)) } } + ShareViaChatButton { + val shareViaChat = { + chatModel.sharedContent.value = SharedContent.MyAddress + chatModel.chatId.value = null + ModalManager.closeAllModalsEverywhere() + } + if (userAddress.shouldBeUpgraded) showAddShortLinkAlert { shareViaChat() } else shareViaChat() + } // ShareViaEmailButton { sendEmail(userAddress) } BusinessAddressToggle(addressSettingsState) { saveAddressSettings(addressSettingsState.value, savedAddressSettingsState) } AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAddressSettings) @@ -438,6 +446,17 @@ private fun AddShortLinkButton(text: String, onClick: () -> Unit) { ) } +@Composable +private fun ShareViaChatButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_forward), + stringResource(MR.strings.share_via_chat), + onClick, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, + ) +} + @Composable private fun CreateOneTimeLinkButton() { val closeAll = { ModalManager.start.closeModals() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt index 45cdee6108..f8bca55ab7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt @@ -24,6 +24,7 @@ import chat.simplex.common.views.onboarding.ReadableText import chat.simplex.common.platform.* import chat.simplex.common.views.* import chat.simplex.res.MR +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.net.URI @@ -39,10 +40,10 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) { var profile by remember { mutableStateOf(user.profile.toProfile()) } UserProfileLayout( profile = profile, - close, - saveProfile = { displayName, fullName, shortDescr, image -> + close = close, + saveProfile = { displayName, fullName, shortDescr, description, image -> withBGApi { - val updatedProfile = profile.copy(displayName = displayName.trim(), fullName = fullName.trim(), shortDescr = shortDescr.trim().ifEmpty { null }, image = image) + val updatedProfile = profile.copy(displayName = displayName.trim(), fullName = fullName.trim(), shortDescr = shortDescr.trim().ifEmpty { null }, description = description.trim().ifEmpty { null }, image = image) val updated = chatModel.controller.apiUpdateProfile(user.remoteHostId, updatedProfile) if (updated != null) { val (newProfile, _) = updated @@ -60,12 +61,13 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) { fun UserProfileLayout( profile: Profile, close: () -> Unit, - saveProfile: (String, String, String, String?) -> Unit, + saveProfile: (String, String, String, String, String?) -> Unit, ) { val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val displayName = remember { mutableStateOf(profile.displayName) } val fullName = remember { mutableStateOf(profile.fullName) } val shortDescr = remember { mutableStateOf(profile.shortDescr ?: "") } + val description = remember { mutableStateOf(profile.description ?: "") } val chosenImage = rememberSaveable { mutableStateOf(null) } val profileImage = rememberSaveable { mutableStateOf(profile.image) } val scope = rememberCoroutineScope() @@ -73,6 +75,8 @@ fun UserProfileLayout( val keyboardState by getKeyboardState() var savedKeyboardState by remember { mutableStateOf(keyboardState) } val focusRequester = remember { FocusRequester() } + val descrFocusRequester = remember { FocusRequester() } + var editingDescription by remember { mutableStateOf(false) } ModalBottomSheetLayout( scrimColor = Color.Black.copy(alpha = 0.12F), sheetContent = { @@ -90,15 +94,36 @@ fun UserProfileLayout( displayName.value.trim() == profile.displayName && fullName.value.trim() == profile.fullName && shortDescr.value.trim() == (profile.shortDescr ?: "") && + description.value.trim() == (profile.description ?: "") && profile.image == profileImage.value val closeWithAlert = { if (dataUnchanged || !canSaveProfile(displayName.value, shortDescr.value, profile)) { close() } else { - showUnsavedChangesAlert({ saveProfile(displayName.value, fullName.value, shortDescr.value, profileImage.value) }, close) + showUnsavedChangesAlert({ saveProfile(displayName.value, fullName.value, shortDescr.value, description.value, profileImage.value) }, close) } } - ModalView(close = closeWithAlert) { + LaunchedEffect(editingDescription) { + if (editingDescription) { + delay(200) + descrFocusRequester.requestFocus() + } + } + ModalView(close = if (editingDescription) ({ editingDescription = false }) else closeWithAlert) { + if (editingDescription) { + ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) { + AppBarTitle(stringResource(MR.strings.profile_description__field), withPadding = false) + TextEditor( + description, + Modifier.heightIn(min = 100.dp), + placeholder = stringResource(MR.strings.enter_description_optional), + contentPadding = PaddingValues(), + focusRequester = descrFocusRequester, + ) + SectionBottomSpacer() + } + return@ModalView + } ColumnWithScrollBar( Modifier .padding(horizontal = DEFAULT_PADDING), @@ -167,9 +192,16 @@ fun UserProfileLayout( } ProfileNameField(shortDescr) + Spacer(Modifier.height(DEFAULT_PADDING)) + Text( + stringResource(if (description.value.isBlank()) MR.strings.add_description else MR.strings.edit_description), + color = MaterialTheme.colors.primary, + modifier = Modifier.clickable { editingDescription = true } + ) + Spacer(Modifier.height(DEFAULT_PADDING)) val enabled = !dataUnchanged && canSaveProfile(displayName.value, shortDescr.value, profile) - val saveModifier: Modifier = Modifier.clickable(enabled) { saveProfile(displayName.value, fullName.value, shortDescr.value, profileImage.value) } + val saveModifier: Modifier = Modifier.clickable(enabled) { saveProfile(displayName.value, fullName.value, shortDescr.value, description.value, profileImage.value) } val saveColor: Color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary Text( stringResource(MR.strings.save_and_notify_contacts), @@ -248,7 +280,7 @@ fun PreviewUserProfileLayoutEditOff() { UserProfileLayout( profile = Profile.sampleData, close = {}, - saveProfile = { _, _, _, _ -> } + saveProfile = { _, _, _, _, _ -> } ) } } @@ -264,7 +296,7 @@ fun PreviewUserProfileLayoutEditOn() { UserProfileLayout( profile = Profile.sampleData, close = {}, - saveProfile = { _, _, _, _ -> } + saveProfile = { _, _, _, _, _ -> } ) } } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index ecf6efc8a9..76a6768f60 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -2745,7 +2745,6 @@ شارك القناة… شارك عبر الدردشة ⚠️ فشل التحقق من التوقيع: %s. - (موقّع) بلاغات المشترك يمكن للمشتركين إضافة ردود الفعل على الرسائل. يمكن للمشتركين الدردشة مع المُدراء. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 223c09726a..937bb012ed 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -574,6 +574,7 @@ Forward message… Forward messages… Share channel… + Share address… Cannot send message Selected chat preferences prohibit this message. Share via chat @@ -584,8 +585,8 @@ Contact address One-time link (from owner) - (signed) Error sharing channel + Error sharing address Link signature verified. ⚠️ Signature verification failed: %s. @@ -1248,6 +1249,10 @@ Full name: Bio: Bio too large + Description + Add description + Edit description + Enter description (optional) Your current profile Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. Edit image diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml index 11b2c53765..605d2134a7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml @@ -2744,7 +2744,6 @@ Sdílet pomocí chatu Zobrazit SimpleX ⚠️ Ověření podpisu selhalo: %s. - (podepsán) SimpleX SimpleX - %d nepřečteno ODBĚRATEL diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml index e58f178796..ce16f529ca 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -2782,7 +2782,6 @@ Kanal teilen… Per Chat teilen ⚠️ Signaturüberprüfung fehlgeschlagen: %s. - (signiert) Zum Öffnen tippen Die Verbindung hat das Limit für nicht zugestellte Nachrichten erreicht Kanal-Präferenzen diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml index bc3aa165a9..84d5654e3f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -2776,7 +2776,6 @@ Compartir canal… Compartir en chat ⚠️ Verificación de firma fallida: %s. - (firmado) Los suscriptores pueden chatear con los administradores. Para comunicarte Pulsa para abrir diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml index 6eb41ac5bf..e1bc779f63 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -2670,7 +2670,6 @@ Csatorna megosztása… Megosztás egy csevegésen keresztül ⚠️ Nem sikerült ellenőrizni az aláírást: %s. - (aláírva) Koppintson ide a megnyitáshoz Letiltás Engedélyezés diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml index db40539996..903cb81970 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml @@ -2718,7 +2718,6 @@ Bagikan via obrolan Ketuk untuk buka Tautan 1-kali - (ditandatangani) ⚠️ Verifikasi tanda tangan gagal: %s. anda adalah pelanggan Kirim tautannya via aplikasi pesan apa pun - aman. Minta untuk tempel ke SimpleX. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index 7cc2b79ec6..e7ea4a0216 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -2705,7 +2705,6 @@ Condividi canale… Condividi via chat ⚠️ Verifica della firma fallita: %s. - (firmato) Tocca per aprire Link una tantum Disattiva diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml index bc2c1adb97..f12b32ca5a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -2211,7 +2211,6 @@ 連絡先アドレス ワンタイムリンク (オーナーから) - (署名済み) チャンネルの共有エラー リンクの署名が検証されました。 ⚠️ 署名の検証に失敗しました:%s。 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml index ce35d9d590..b5c560f7dd 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -2770,7 +2770,6 @@ Ваш канал Ссылка группы (от владельца) - (с подписью) Ошибка при публикации канала Вы подписчик Новая одноразовая ссылка diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml index 3ab01c75eb..0cc69bc2b1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -2805,7 +2805,6 @@ Kısa SimpleX adresi SimpleX\'i göster ⚠️ İmza doğrulama başarısız oldu: %s. - (imzalı) SimpleX SimpleX - %d okunmamış SimpleX adı diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index 5409bc736c..89fcd449ed 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -2690,7 +2690,6 @@ 分享频道… 经聊天分享 ⚠️ 签名验证失败:%s。 - (已签名) 轻触打开 发送链接预览可能会将你的 IP 地址暴露给网站。你可以稍后在“隐私”设置中更改此设置。 连接达到了未送达消息的上限 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml index 11cb52d580..d49c63886d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml @@ -2292,7 +2292,6 @@ 聯絡地址 一次性連結 (來自擁有者) - (已簽署) 分享頻道時發生錯誤 連結簽章已驗證。 ⚠️ 簽章驗證失敗:%s。 diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index 5c5718df4a..a3af872ac3 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -638,8 +638,8 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName Right CRGroupLink {groupLink = GroupLink {connLinkContact = CCLink cr sl_}} -> let linkBefore_ = profileGroupLinkText fromGroup linkNow_ = profileGroupLinkText toGroup - profileGroupLinkText GroupInfo {groupProfile = gp} = - maybe Nothing (fmap (\(FormattedText _ t) -> t) . find ftHasLink) $ parseMaybeMarkdownList =<< description gp + profileGroupLinkText GroupInfo {groupProfile = GroupProfile {description = descr_}} = + maybe Nothing (fmap (\(FormattedText _ t) -> t) . find ftHasLink) $ parseMaybeMarkdownList =<< descr_ ftHasLink = \case FormattedText (Just SimplexLink {simplexUri = ACL SCMContact cLink}) _ -> case cLink of CLFull cr' -> sameConnReqContact cr' cr diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index d34f6d8ff8..79338930c8 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -2774,6 +2774,7 @@ Unknown: - displayName: string - fullName: string - shortDescr: string? +- description: string? - image: string? - contactLink: string? - preferences: [Preferences](#preferences)? @@ -3163,6 +3164,7 @@ count= - displayName: string - fullName: string - shortDescr: string? +- description: string? - image: string? - contactLink: string? - preferences: [Preferences](#preferences)? diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index b4e59b8da4..e2e30d43bc 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -3011,6 +3011,7 @@ export interface LocalProfile { displayName: string fullName: string shortDescr?: string + description?: string image?: string contactLink?: string preferences?: Preferences @@ -3413,6 +3414,7 @@ export interface Profile { displayName: string fullName: string shortDescr?: string + description?: string image?: string contactLink?: string preferences?: Preferences diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_types.py b/packages/simplex-chat-python/src/simplex_chat/types/_types.py index 21e1b8279c..96d3f4dea4 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_types.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_types.py @@ -2107,6 +2107,7 @@ class LocalProfile(TypedDict): displayName: str fullName: str shortDescr: NotRequired[str] + description: NotRequired[str] image: NotRequired[str] contactLink: NotRequired[str] preferences: NotRequired["Preferences"] @@ -2388,6 +2389,7 @@ class Profile(TypedDict): displayName: str fullName: str shortDescr: NotRequired[str] + description: NotRequired[str] image: NotRequired[str] contactLink: NotRequired[str] preferences: NotRequired["Preferences"] diff --git a/plans/2026-07-12-directory-business-bot-registration.md b/plans/2026-07-12-directory-business-bot-registration.md new file mode 100644 index 0000000000..6c57885ac8 --- /dev/null +++ b/plans/2026-07-12-directory-business-bot-registration.md @@ -0,0 +1,509 @@ +# Directory registration of businesses and service bots (via signed contact card) + +Status: draft plan for review. Grounded against the current tree (branch `ep/improve-names-2`). + +## 1. Goal + +Let a business or a service (chat bot) operator register their **contact `/a` address** in +the directory by **forwarding a signed contact card** to the directory bot — exactly the +UX we already have for channels (`/share chat #ch @'SimpleX Directory'`), but for a contact +address instead of a channel link. + +**Guiding principle: the flow is the channel registration flow verbatim — owner-signed card, admin +approval, re-approval on any profile change, the same periodic link-check loop — differing only in +the listing type (a contact `peerType`, not a group). Where a detail is unspecified here, the answer +is "whatever channels do."** + +Product decisions from the discussion, baked into this plan: + +- **The owner sends the card, and the signature is the authorization.** The `ownerSig` (signed + with the address key) is what proves the address owner authorized the listing — only the key + holder can produce it. We deliberately do NOT use a "directory connects and asks the owner to + confirm" double opt-in (it is a spam vector, like any mailing-list signup). "Submitter ≠ owner" + is handled not by letting non-owners submit, but by giving the owner's tooling a way to send (the + support-bot entry point that would cover the headless case is deferred — §B.4). +- **The directory does NOT connect to / join these addresses.** They are not groups. It verifies + the signature via a link-data *fetch* (not a connection) and records the address in a new table. +- **One table for both businesses and bots** — they are all contact `/a` addresses. The MVP types + each accepted registration by `peerType`: a **bot** requires `peerType == CPTBot`; a **business** + requires `peerType ∈ {CPTHuman, CPTBusiness}` (an unset `peerType` counts as `CPTHuman`); an + unrecognized `CPTUnknown` is rejected. The admin then manually verifies a business before + approving — as for channels. +- **Listing type = `ChatPeerType`.** Extend `ChatPeerType` (today `CPTHuman | CPTBot`, `Types.hs:710`) + with **`CPTBusiness`** and **`CPTUnknown Text`** (forward-compat, like `GTUnknown`), and make the + decoder **lenient** (unknown tag → `CPTUnknown`) so this version won't choke on future tags. + **Wire-compat caveat (verified):** `ChatPeerType` decodes strictly today + (`textDecode … _ -> Nothing`, `Profile` via `deriveJSON`), so a present-but-unknown `peerType` + makes an *already-deployed* app fail to parse the whole profile — it does **not** downgrade to + human. So a business must **not** publish `CPTBusiness` yet (old apps couldn't reach it); a + business's profile stays `CPTHuman` in practice, with `CPTBusiness` reserved for later. The MVP + types a **bot** from `peerType == CPTBot` and a **business** from `peerType ∈ {CPTHuman, + CPTBusiness}` (unset ≙ `CPTHuman`), rejecting `CPTUnknown`; it stores the resolved type + (`CPTBot`/`CPTBusiness`) on the listing. When the lenient version is broadly adopted, businesses + can publish `CPTBusiness` directly. +- **`peerType` and `businessAddress` are orthogonal, and the directory ignores `businessAddress`.** + `businessAddress` chooses the *conversation type* a connector gets (a business chat / group vs a + direct 1:1); it can be set by non-businesses, and a real business may run a plain direct-chat + address. The directory does **not** use it to classify — the type comes from the profile's + `peerType` (bot vs human/business, above), not from `businessAddress`. (App-side only, unchanged: + the connect-preview briefcase shows when **either** `businessAddress` or `peerType == CPTBusiness`; + bot cube from `peerType`, else person — in the MVP that briefcase comes from `businessAddress`. + Separate from directory classification.) +- **Description lives on the contact `Profile`** (new `description` field, parallel to + `GroupProfile.description`). In group-member profiles it is **redacted per the group's policy — + the same treatment `shortDescr` gets** (links/names stripped when the group prohibits them), not + removed wholesale. It is carried **full** in the direct contact view, the address link preview, + and the directory. See §G. +- **`peerType` + `description` are visible in the app independent of the directory** (that is why + owners will set them). `peerType` drives the type icon in the pre-connect alert + (`ConnectPlan.kt:698-713`) and a marker in the chat list / chat banner. The compact surfaces (the + alert, the shared-link card) are too small for the large `description`, so it appears via a + **"Read more"** affordance in the **chat banner** (`ChatView.kt` `ChatBannerView`) and the + **contact info page** (`ChatInfoView.kt:778`) that opens the full text in a sheet (iOS) / alert + (Kotlin). These are NOT the welcome/auto-reply message (`AddressSettings.autoReply`, transient + on-connect). Full details in §H. + +Deliverables: (a) an API + CLI to prepare and share the signed contact card; (b) the +`Profile.description` field; (c) directory handling that verifies and stores the address; (d) admin +approval, web listing, and search. (A support-bot entry point for headless businesses is out of +scope for now — §B.4.) + +## 2. End-to-end flow + +``` +Operator's client Directory bot +----------------- ------------- +/share address @'SimpleX Directory' + -> get own /a address (short link, + businessAddress flag, root key) + -> build MCChat { chatLink = + MCLContact {connLink, profile, business}, + ownerSig = sign(rootPrivKey, + chatBinding <> connLink) } ── card ──▶ DEChatLinkReceived (MCLContact, ownerSig) + -> APIConnectPlan (PLAN only, no connect) + fetches link data (opaque) + verifies sig + => CPContactAddress (CAPOk {ownerVerification}) + -> if OVVerified: + addContactReg (bot if CPTBot, + else business), status pending + notify admins with profile (admin verifies) +admins: /approve ... -> status active -> listingsUpdated + -> web listing.json + bot search include it +``` + +Nothing is connected or joined. The only network action on the directory side is a one-time, +opaque link-data fetch for signature verification (consistent with the established rule that +the directory may fetch link data, only name *resolution* leaks membership). + +## 3. What already exists (reuse map) + +All grounded in the current tree: + +- **Chat-link card type** — `MCLContact {connLink :: ShortLinkContact, profile :: Profile, business :: Bool}` + already exists (`src/Simplex/Chat/Protocol.hs:769`). `MCChat {text, chatLink, ownerSig}` and + `LinkOwnerSig {ownerId, chatBinding, ownerSig}` at `Protocol.hs:764,774`. +- **Owner-signature verification for contact addresses is already wired.** `connectPlan`'s + `CTShortContact CCTContact` path fetches `FixedLinkData {rootKey}` + `UserContactData {owners}` + and computes `ov = verifyLinkOwner rootKey owners l' sig_`, surfaced as + `CPContactAddress (CAPOk {contactSLinkData_, ownerVerification})` + (`src/Simplex/Chat/Library/Commands.hs:4287-4289,4518-4527`; `Controller.hs:1114-1121,1139-1142`). + For plain/business addresses `owners == []`, so `ownerId = Nothing` and verification uses the + link **root key** (`verifyLinkOwner` fallback). +- **The directory already receives any `MCChat` card as `DEChatLinkReceived`** — `Directory/Events.hs:108` + turns `(MCChat {chatLink, ownerSig}, Nothing)` into `DEChatLinkReceived`. Today `deChatLinkReceived` + only matches `MCLGroup` and otherwise replies "Only channels can be added to directory via link." + (`Directory/Service.hs:964-979`). We add an `MCLContact` case. +- **Card-sharing UI + API + signing** — `/share chat #g @to` → `SharePublicGroup` + (`Commands.hs:2437-2449`, parser `Commands.hs:5551`) → `APIShareChatMsgContent` + (`Commands.hs:1136-1170`) which builds the `MCChat` and signs with `mkLinkOwnerSig` + + `shareChatBinding` (binds the card to the recipient connection, anti-replay). +- **Address key + business flag storage** — `link_priv_sig_key` (the address root private key, + Ed25519) is stored in `user_contact_links` by `createUserContactLink` + (`src/Simplex/Chat/Store/Profiles.hs:429-439`); `businessAddress` lives in `AddressSettings` + (`Profiles.hs:497-502`) and is published as `ContactShortLinkData.business` + (`Commands.hs:4528-4533`, `Protocol.hs:1584-1592`). Note: `getUserAddress`/`UserContactLink` + do **not** currently read `link_priv_sig_key` back (`Profiles.hs:479-524`). +- **Directory store / listing / web infra** — `sx_directory_group_regs` table + (`Directory/Store/{SQLite,Postgres}/Migrations.hs`), `GroupReg`/`GroupRegStatus` + (`Directory/Store.hs:116-226`), `getAllListedGroups_` (`Store.hs:354-363`), `generateListing` + (`Directory/Listing.hs:148-170`), `DirectoryEntry`/`DirectoryEntryType = DETGroup` + (`Listing.hs:55-86`), website renderer `website/src/js/directory.jsc`. + +## 4. Work items + +### A. Protocol / types + +- `MCLContact` exists; no new protocol message for the card itself. +- **Extend `ChatPeerType`** (`Types.hs:710`, today `CPTHuman | CPTBot`) with `CPTBusiness` and + `CPTUnknown Text` (forward-compat, like `GTUnknown`). Update the `TextEncoding`/JSON instances + (`Types.hs:724-731`): encode `CPTBusiness` as `"business"` and `CPTUnknown t` back to `t` + (round-trips the original tag); make `textDecode` **lenient** — an unrecognized tag becomes + `CPTUnknown t` instead of `Nothing`, so this version never fails to parse a profile with a future + tag. **Verified constraint:** the *current* decoder is strict (`_ -> Nothing`) and `Profile` + is `deriveJSON`-parsed, so an already-deployed app fails the whole profile on an unknown `peerType`; + therefore `CPTBusiness` must not be published on profiles until the lenient version is broadly + adopted. **MVP:** the directory types a **bot** from `peerType == CPTBot` and a **business** from + `peerType ∈ {CPTHuman, CPTBusiness}` (unset ≙ `CPTHuman`; stored as `CPTBusiness`), and **rejects + `CPTUnknown`**. Businesses are then admin-verified — the admin is the gate, as for channels. +- **New optional `description :: Maybe Text` on `Profile`** (`Types.hs:693`), parallel to + `GroupProfile.description` (`Types.hs:867`). Additive/nullable — only businesses/bots set it. + It rides into the address link data automatically (`ContactShortLinkData` embeds the whole + `Profile`, `Protocol.hs:1584`), so the directory reads it from the fetched link data. It is + redacted per group policy in group-member profiles, on **both send and receive** (see §G). No + version bump is needed — `Profile` is `deriveJSON`-parsed and aeson ignores unknown keys, so old + apps just drop `description` (same as when `peerType`/`badge`/`contactDomain` were added). +- **Setting `peerType`/`description` (for tests + eventual UI).** Both are plain `Profile` fields, so + they ride through the existing profile-update path (`APIUpdateProfile` / the `/p` command); tests + drive them via `/_profile`. A small dedicated setter for the multi-line `description` is worth + adding for CLI ergonomics. The app-UI toggle to set `peerType = CPTBusiness` is deferred (per the + wire-compat caveat above). + +### B. Client: prepare + share the contact-address card + +1. **Signing key — from the agent, not the chat DB.** Sign the card with the address short-link key + via `getConnLinkPrivKey (aConnId addressConn)` (already in the agent, used at `Subscriber.hs:1649`; + `getUserAddressConnection` gives the connection). This is the authoritative key — the private half + of the short link's root key the directory verifies against — and it exists whenever the short link + does, **including right after an upgrade** (`setConnShortLink` provisions it). Do **not** read the + chat-DB `link_priv_sig_key` for signing: it is written only at `createUserContactLink` and never on + upgrade. *(Separate cleanup, off the signing path: persist `link_priv_sig_key` on upgrade too — + `setMyAddressData`/`setUserContactLinkShortLink` — reading it back via `getConnLinkPrivKey` so the + column stops being stale.)* +2. **Card-builder API — `APIShareMyAddress {toSendRef :: SendRef}`** (Controller) + handler in + `Commands.hs`, mirroring the group-share case (`APIShareChatMsgContent`, `Commands.hs:1136`): + - `getUserAddress` → `connLinkContact` (short link) + profile + `businessAddress`. + - `getUserAddressConnection` → conn; `getConnLinkPrivKey (aConnId conn)` → `rootPrivKey` + (`Nothing` ⇒ not upgraded → error; the UI pre-empts this via §B.5). + - hoist `shareChatBinding` to top-level; `binding <- shareChatBinding user toSendRef`. + - `ownerSig = LinkOwnerSig {ownerId = Nothing, chatBinding = B64UrlByteString cb, + ownerSig = C.sign' rootPrivKey (cb <> smpEncode connShortLink)}` (contact variant of + `mkLinkOwnerSig`, `ownerId = Nothing` so the directory verifies against the link root key). + - return `CRChatMsgContent user (MCChat {text, chatLink = MCLContact {connLink, profile, business}, ownerSig})`. + `SendRef` covers direct **and** group/channel targets. +3. **CLI command — `ShareMyAddress {toChatName}`**, parser `/share address @to` / `/share address #to` + (`Commands.hs:5551` neighborhood), handler mirroring `SharePublicGroup` (`Commands.hs:2437-2449`): + resolve `toChatName` → `SendRef` → `APIShareMyAddress` → `APISendMessages`. Shares to contacts and + groups/channels alike. +4. **Support-bot entry point — OUT OF SCOPE (deferred).** A headless business running + `apps/simplex-support-bot` (TypeScript, no app UI) will eventually need a way to trigger the + share — a bot admin/config command that calls `APIShareMyAddress` against the directory contact + once connected. Deferred; the core `APIShareMyAddress`/`/share address` path built here is exactly + what it will call. +5. **App UI — "Share via chat" (Phase 1; mirrors the channel share).** The receiving/rendering half + already exists from the channel work (`MsgChatLink.Contact`, `CIChatLinkHeader`, the compose + preview, and the `SharedContent → ShareListView → ComposeView` picker). New pieces: the entry + point, a `SharedContent.AddressLink` case, the `apiShareMyAddress` call, and the upgrade branch. + - **Entry point:** a **"Share via chat"** button (reuse the channel string) in the user's own + address screen (`UserAddressView.kt`), beside the existing OS-share "Share" button. Address + creation lands on this same screen (`createAddress` sets `userAddress`, `UserAddressView.kt:73-84` + — verified), so the button is visible immediately after creating an address. + - **Flow:** tap → if `userAddress.shouldBeUpgraded` (old full address) show an **upgrade alert** + ("To share your address in a chat it will be upgraded to a short link. All your contacts stay + connected."), buttons **[Upgrade & share]** / **[Cancel]** — on confirm: spinner → + `apiAddMyAddressShortLink`, **then** continue (two separate API calls, cleaner errors); no + "share old" option. Then set `SharedContent.AddressLink` → `ShareListView` (contacts + + groups/channels, with the simplex-link prohibition filtering) → pick destination → `ComposeView` + `LaunchedEffect` calls `apiShareMyAddress` → sets the existing `ChatLinkPreview` → optional + message text (same UX as the channel share) → **Send** → the recipient sees the existing + `CIChatLinkHeader` card and taps to connect. + - iOS mirrors this via the existing channel-share flow (`f49d98511`); Kotlin per + `plans/2026-04-17-kotlin-share-channel-link.md`. + +### C. Directory: verify + store (no connect) + +1. **`deChatLinkReceived` — add the `MCLContact` case** (`Directory/Service.hs:964`). + **No new verification code** — reuse the exact plan path channels use (resolved, Q1): + - `deChatLinkReceived ct (MCLContact {connLink, profile, business}) (Just ownerSig)`: + - `APIConnectPlan userId (contact link) PRMAll (Just ownerSig)` — **plan only**, no connect + (rename `PRMAllGroups` → `PRMAll` and make it work for contact links too, not just groups). + `connectPlan`'s contact-address path already computes `ov = verifyLinkOwner rootKey owners l' sig_` + (`Commands.hs:4288`), identical to the channel path at `Commands.hs:4346`; for a plain/business + address `owners == []`, so the card's `ownerId = Nothing` makes `verifyLinkOwner` verify against + the link **root key**. Expect `CPContactAddress (CAPOk {contactSLinkData_ = Just csld, ownerVerification})`. + Use the **fetched** `csld.profile` (`peerType`, `description`, name claim, …) as authoritative + — the card's copies are display-only / potentially stale; `csld.business` is not used for + typing. + - `OVVerified` → type from the fetched profile's `peerType`: **bot** if `CPTBot`, **business** + if `CPTHuman`/`CPTBusiness` (unset ≙ `CPTHuman`; stored as `CPTBusiness`) → `addContactReg` + status pending; **reject `CPTUnknown`** ("unsupported account type"). A business is then + admin-verified before approving (as for channels). `OVFailed reason` → "ownership verification + failed". `CAPKnown`/other → appropriate message. + - The fetch is intrinsic (the root public key isn't in the card, same as channels) and is an + opaque link-data read, not a connection — consistent with the established directory rule. + - Keep the existing `MCLGroup` and fall-through cases unchanged. +2. **New store: `sx_directory_contact_regs`.** Add a migration to + `Directory/Store/SQLite/Migrations.hs` and `Directory/Store/Postgres/Migrations.hs` (new named + migration appended to `schemaMigrations`). Proposed columns: + ``` + contact_reg_id PK autoincrement + user_contact_reg_id INTEGER -- per-submitter sequence (cf. user_group_reg_id) + submitter_contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE + conn_short_link TEXT NOT NULL -- the contact link; the LISTING IDENTITY (stable for + -- contacts). A link change ⇒ unlist + re-register. + -- Must be present in the fetched profile (contactLink). + display_name TEXT NOT NULL + full_name TEXT + short_descr TEXT + description TEXT -- long description (Profile.description) + image TEXT -- base64, optional + peer_type TEXT NOT NULL -- resolved listing type: "bot" or "business" (ChatPeerType) + simplex_name TEXT -- verified SimpleX name, optional (see Q5) + reg_status TEXT NOT NULL + promoted INTEGER NOT NULL DEFAULT 0 + created_at, updated_at TEXT + UNIQUE(conn_short_link); UNIQUE(submitter_contact_id, user_contact_reg_id) + ``` + Records store the profile inline (`display_name`, `description`, `image`, `peerType`, …) — + self-contained, no FK to a joined contact, since we never connect. Reuse **`GroupRegStatus`** + (resolved, Q3) for `reg_status` — same states as channels. New `Directory/Store.hs` data + + functions mirroring the `GroupReg` ones: `ContactReg`, `addContactReg`, `setContactRegStatus`, + `deleteContactReg`, `getContactRegBy{Id,Link}`, `getAllListedContacts`, and a search query. +3. **Registration lifecycle mirrors channels.** `proposed → pending approval → active`, plus + `suspended/removed`. On submission, notify admins with the profile + an approve command. The + directory **re-reads the address links in the same periodic loop as channels** (resolved, Q6 — + `deGroupLinkCheck`, `Service.hs:832`): re-fetch the link data, refresh the stored profile, and + a profile change triggers **re-approval** (hidden until re-approved), exactly like a channel + profile change (`reapprove`, `Service.hs:858`). The loop is the same as channels; a contact + address has a single key and no group membership, so the channel `checkValidOwner` owner-list + re-check has no analog and doesn't run — which also means the empty-`linkOwners` false-delist bug + can't arise, and `UNIQUE(conn_short_link)` (below) prevents the duplicate-row class that triggered + it. **Re-submission of an already-registered link (to research + propose):** mirror the channel + re-registration path (`deReregistration`) — upsert the existing reg and send it back to admin + review on any change, rather than erroring. +4. **Admin & user commands — same commands, extended with a chat type** (resolved, Q4). Reuse the + existing command constructors and syntax; carry a chat-type discriminator on the id token — `#` + for a group (existing), `@` for a contact address — e.g. `/approve @: `, + `/list @...`, `/suspend @...`, mirroring the group forms. The `@`/`#` prefix disambiguates the + overlapping id spaces (a `group_id` and a `contact_reg_id` both start at 1), so no parallel + command names are needed. Extend the command constructors with the chat type, extend + `Directory/Events.hs` `directoryCmdP` to parse the prefix, and branch on it in `Service.hs` + `deSuperUserCommand`/`deUserCommand`. +5. **Link identity + verified SimpleX names (resolved).** The contact **link is the listing + identity** (for contacts the link is expected to be stable). Two conditions: + - **Link present in the profile.** For listing, the fetched profile must declare this link — + `Profile.contactLink` present and equal to the registered link. If the link **changes** (or the + profile stops declaring it), the address is **unlisted** and must be re-registered — the link is + the anchor, not a mutable attribute. + - **Name↔link consistency, verified inline.** If the profile claims a SimpleX name + (`Profile.contactDomain`), resolve that name and confirm it points to **this** link, comparing + inline — the reverse direction of the existing by-name plan path (`Commands.hs:4272-4281`, + `contactDomain`/`nameResolvesTo`). A **name change** re-runs this check. On success, populate + `sx_directory_contact_regs.simplex_name` → flows to `DirectoryEntry.simplexName` and bot/web + search. Reuse `plans/2026-06-25-name-resolution.md` and the group-names work. + *(To research + propose: how the directory detects a link change on re-read, whether an address's + published profile actually carries `contactLink == its own link`, and the exact resolve-and-compare + call for the link → name-claim → link round-trip.)* + +### D. Listing + web + +1. **`DirectoryEntryType`** (`Listing.hs:55`): add `DETContact {peerType :: ChatPeerType}`. The + `taggedObjectJSON`/`dropPrefix "DET"` derivation already emits `{"type":"contact", ...}` for a new + constructor for free (single→multi constructor is transparent); `peerType` serializes as + `"business"`/`"bot"`/etc. +2. **`contactDirectoryEntry`** builder (analogue of `groupDirectoryEntry`, `Listing.hs:100`), from a + `ContactReg` row: `DirectoryEntry {entryType = DETContact peerType, displayName, simplexName, + groupLink = PublicLink Nothing (Just connShortLink), shortDescr` (from `Profile.shortDescr`)`, + welcomeMessage` (from the new `Profile.description`)`, imageFile, activeAt, createdAt}`. + `PublicLink` already models contact links (`Listing.hs:63-68`). Store the profile fields (incl. + `description`, `peerType`) on the reg row at registration so the entry is self-contained. +3. **`generateListing`** (`Listing.hs:148`): merge group entries + contact entries into the single + `DirectoryListing`. Feed the contact rows from `getAllListedContacts` (status active); build + `DirectoryEntry`s from both sources and serialize together. `listingsUpdated` triggers stay as-is, + plus fire on contact-reg status changes. +4. **Website `directory.jsc`**: branch `displayEntries` on `entryType.type` and, for contacts, on + `entryType.peerType`: + - business vs bot label/avatar from `peerType` (`business`/`bot`); non-group avatar fallback + instead of `/img/group.svg`; + - "Connect"/"Chat" affordance instead of the "N members/subscribers" line (`entryMemberCount` + already returns 0 for non-group — `directory.jsc:183-193`); + - join URI already works via `connShortLink` (`directory.jsc:331-348`). + Search/filter already reads generic fields (`displayName`, `shortDescr`, `welcomeMessage`, + `simplexName`), so text search works unchanged. + +### E. Bot search + +Include active contact regs in the bot's search results (`DCSearchGroup` path, +`Service.hs:1115`, backed by `searchListedGroups` in `Store.hs`) as **one unified result set** (not a +separate contact search); match on display name and SimpleX name. + +### F. Tests + +- **Client** (`tests/ChatTests/`): `/share address` produces an `MCChat`/`MCLContact` card with a + valid `ownerSig` (`ownerId = Nothing`); parser test for `/share address`. +- **Directory** (`tests/Bots/DirectoryTests.hs`, mirroring `testRegisterChannelViaCard` + `:2050` and `testDirectoryChannelName` `:2129`): register a business and a bot via card + (verified → pending → admin approve → listed), reject on bad/absent signature, search finds it, + and the generated `listing.json` contains a `"type":"contact"` entry with the right `peerType` + (`business`/`bot`). Wire under the names/SMP test harness as needed. +- **Profile description** (§G): a member's `description` is **redacted per the group's policy** in + the profile others receive in a group (send side) and when stored from an incoming member profile + (receive side) — links/names stripped when the group prohibits them, clean prose passing through; + a direct contact / address preview keeps it full. + +### G. `Profile.description` field + member-profile redaction (resolved) + +`description :: Maybe Text` is added to `Profile` (§A). In group-member profiles it is **redacted +per the group's policy — the same treatment `shortDescr` gets today** (not removed wholesale): +links and SimpleX names are stripped when the group prohibits them. + +1. **Send side** — in `redactedMemberProfile` (`Internal.hs:1266-1277`, which already redacts + `shortDescr`/`contactLink`/name-proof under the group's `SGFSimplexLinks`/`SGFDirectMessages`), + also redact `description` — with a **new inline-strip helper** (per G.3), not `shortDescr`'s + drop-whole `removeSimplexLink`. Adding `description` to `Profile` forces this output record to be + rebuilt here anyway. (Used on every member-profile-out path — `Internal.hs:1254,1262`, + `Subscriber.hs:851,3273`, `Commands.hs:4134`.) +2. **Receive side** — apply the same redaction when ingesting a member profile from the network, so + a peer can't inject a link/name-laden description. Chokepoints: `updateMemberProfile` + (`Store/Groups.hs:3407`) and member creation (`Store/Groups.hs:2510`, `1395`); prefer a single + helper mirroring the send-side redaction. +3. **Redaction granularity (RESOLVED).** **Inline-strip links and names** — drop the + `Uri`/`HyperLink`/`SimplexLink`/`SimplexName` (the `isLink` set, `Markdown.hs:184`) and `Mention` + spans via `parseMaybeMarkdownList`, re-concat the remaining `FormattedText`, keep the prose (empty + result ⇒ `Nothing`). **Exception:** if `hasObfuscatedSimplexLink` matches (a link that can't be + cleanly isolated as a token), drop the **whole** description. +4. **Kept full where wanted** — the address link data (`ContactShortLinkData` embeds the full, + unredacted profile), the direct contact profile view, and the directory listing all carry the + full `description`. Group redaction applies only to member-profile *delivery into a group*, a + separate code path. For the **directory** page, abuse is gated by **admin review** (Q7), not an + automatic filter. +5. **UI/UX** — add a multi-line "Description" field to the profile/address editor (app UI, follow-on + with §B.5). Because the field can carry into groups (redacted), an edit-time hint that links and + names won't show where a group prohibits them is worthwhile, mirroring `shortDescr`. + +### H. App visibility of `peerType` + `description` (why owners will set them) + +These are persistent profile identity shown to everyone who reaches the address — independent of the +directory. That is the reason to fill them in; the directory is a bonus channel. Existing surfaces +(multiplatform paths; iOS/Android mirror them): + +**`peerType` — type icon / badge** (small, already-present surfaces): +- Pre-connect "Open chat?" alert (`newchat/ConnectPlan.kt:698-713`) — type icon + verification; + briefcase when **either** the address `business` flag or `peerType == CPTBusiness`, bot cube from + `peerType`, else person (see §1). The alert holds no description (too small — `AlertManager.kt:289`). +- Chat list (`chatlist/ChatPreviewView.kt:188`, `isBot`) and the chat banner + (`chat/ChatView.kt:2227` `ChatBannerView`, which already has per-type captions — bot / business / + contact) — extend to a business marker from `peerType`. + +**`description` — shown via a "Read more" affordance, NOT inline** (the alert and the in-chat link +card `CIChatLinkHeader.kt` are too small — they carry only the short teaser). Rendered in **two +surfaces: the chat banner (`ChatBannerView`) and the contact info page (`ChatInfoView`, `:778`)**: +- Teaser text: if `shortDescr` is present → show `shortDescr`, then a clickable **"Read more"**; if + `shortDescr` is absent → show the first line of `description` truncated to 100 chars with ellipsis + (up to the first line break), then **"Read more"**. "Read more" appears only when a `description` + exists to reveal. +- **"Read more" is a general, extensible `Modal` markdown element.** Add an inline `Format` variant + `Modal {modalName :: Text}` to `Markdown.hs:51` (sibling to `Command`/`Mention`/`SimplexLink`) — + **no `showText`**: the app resolves both the tappable label and the modal content from the current + chat by `modalName` (e.g. `modalName = "description"` → renders "Read more", opens the contact's + `description`). `Format`'s existing `Unknown` fallback (`parseJSON … <|> pure (Unknown v)`, + `Markdown.hs:533`) makes it forward-compat — old apps decode it as `Unknown`. The **teaser is built + app-side** from the profile fields (d3), so the Haskell core just adds the variant + JSON so the + app's mirrored enum matches; each client renders the tap (iOS sheet / Android modal), reusing the + existing tappable-markdown mechanism (no iOS multiline-hit-test hack). +- This is NOT the welcome/auto-reply message (`AddressSettings.autoReply`, a transient on-connect + message), and NOT shown in the pre-connect alert or the shared-link card. + +**Profile editor** (`usersettings/UserProfileView.kt`) — add the multi-line description field and a +way to set the account type (`peerType`). Note: the editor exposes two separate "business" concepts +— `peerType` (identity) and the `businessAddress` conversation-type setting — which must use distinct +labels, since both otherwise read as "business." + +Note: before connecting, the only surface with room to read the full description is the directory web +page; in-app it is the banner/info "Read more" once the (prepared) chat is open. + +## 5. Files to touch (summary) + +- `src/Simplex/Chat/Types.hs` — extend `ChatPeerType` (`CPTBusiness`, `CPTUnknown`, lenient decode); + add `Profile.description`; JSON/TextEncoding derivations. +- `src/Simplex/Chat/Markdown.hs` — add the `Modal {modalName}` `Format` variant + JSON (§H). +- App views (Phase 1, §B.5/§H) — `UserAddressView.kt` ("Share via chat" button + upgrade branch), + `ChatInfoView.kt` + `ChatView.kt` `ChatBannerView` (description teaser + `Modal` "Read more"), the + Kotlin/Swift `Format` mirror (`Modal` case + tap → sheet/alert) (+ iOS equivalents). The `peerType` + badge/editor UI is deferred. +- `src/Simplex/Chat/Controller.hs` — `APIShareMyAddress`, `ShareMyAddress` command constructors. +- `src/Simplex/Chat/Library/Commands.hs` — handlers + parsers for the two new commands; reuse + `shareChatBinding`. +- `src/Simplex/Chat/Library/Internal.hs` — redact `description` per group policy in `redactedMemberProfile` (send side, §G). +- `src/Simplex/Chat/Store/Groups.hs` — redact `description` when ingesting a member profile (receive side, §G). +- `src/Simplex/Chat/Store/Profiles.hs` — persist `link_priv_sig_key` on short-link upgrade + (`setUserContactLinkShortLink`/`setMyAddressData`); card signing uses the agent's + `getConnLinkPrivKey`, not this column. +- `apps/simplex-directory-service/src/Directory/Service.hs` — `MCLContact` case in + `deChatLinkReceived`; contact-reg lifecycle + admin/user commands; listing trigger. +- `apps/simplex-directory-service/src/Directory/Store.hs` — `ContactReg` model + queries. +- `apps/simplex-directory-service/src/Directory/Store/{SQLite,Postgres}/Migrations.hs` — new table. +- `apps/simplex-directory-service/src/Directory/Events.hs` — extend `directoryCmdP` to parse the + `@`/`#` chat-type prefix and thread the chat type into the (shared) command constructors. +- `apps/simplex-directory-service/src/Directory/Listing.hs` — `DETContact`, `contactDirectoryEntry`, + merge in `generateListing`. +- `website/src/js/directory.jsc` (+ a contact/bot avatar asset) — non-group card rendering. +- `tests/Bots/DirectoryTests.hs`, `tests/ChatTests/*` — tests. + +## 6. Design decisions + +Resolved: + +- **Submission model (RESOLVED: identical to channels).** Submission is by the **link owner**, + **signed with the address key** — exactly the channel card flow, no extra requirement. The + `ownerSig` (only the key-holder can produce it) is the authorization. Admins then decide to list; + any profile change sends it back to admin review; the address is re-read on the same periodic loop + — all as for channels. The only "submitter ≠ owner" accommodation is giving the headless support + bot a way to send (§B.4). *(Earlier we explored open submission with the verified SimpleX name as + the authenticity signal, and an opt-in flag in the address link data; both dropped — the channel + model already answers authorization, and name-verification proves identity, not consent to list.)* +- **Description home (RESOLVED: `Profile.description`, redacted per group policy).** New profile + field. In group-member profiles it is redacted the same way `shortDescr` is (links/names stripped + under the group's policy, §G), not removed wholesale; carried full in the address link data / + direct view / directory. Directory abuse is gated by admin review (Q7). +- **Q1 — Verification (RESOLVED: reuse the plan).** Already verifiable via `APIConnectPlan` — the + same `verifyLinkOwner` path channels use, no new code. The intrinsic link-data fetch (to get the + root public key) is opaque and not a connection. No card/protocol extension. +- **Q4 — Command surface (RESOLVED: same commands, extended with chat type).** Reuse the existing + command constructors and syntax with a chat-type discriminator on the id token — `#` group + (existing), `@` contact (new) — e.g. `/approve @: `. The prefix disambiguates + the overlapping `group_id`/`contact_reg_id` spaces, so no parallel command names are needed. +- **Q5 — SimpleX names (RESOLVED: support now).** Verify name↔link consistency for addresses and + populate `simplex_name`; flows through to listing + search (see §C.5). + +- **Q2 — Entry type (RESOLVED: `ChatPeerType`, typed + admin-verified).** The listing type is a + `ChatPeerType`: **bot** from `peerType == CPTBot`; **business** from `peerType ∈ {CPTHuman, + CPTBusiness}` (unset ≙ `CPTHuman`; stored as `CPTBusiness`); `CPTUnknown` is rejected. Because + `CPTBusiness` can't be published on profiles yet (wire-compat, §A), a business's profile is + `CPTHuman` in practice; the admin verifies it. When profiles can carry `CPTBusiness`, it's read + directly. +- **Q3 — Reg status type (RESOLVED: reuse the group/channel type).** Use the same `GroupRegStatus` + the channel registrations use — no separate `ContactRegStatus`. The lifecycle mirrors channels. +- **Q6 — Updates (RESOLVED: re-read in the same loop as channels).** The directory re-reads the + registered address links in the same periodic link-check loop it runs for channels + (`deGroupLinkCheck`, `Service.hs:832`), re-fetching the address link data to pick up profile/link + changes and re-verify the name. Opaque fetch, no connection. +- **Q7 — Description screening (RESOLVED: two surfaces, two mechanisms).** *Directory page:* admin + approval is the gate — a profile change (incl. description) triggers re-approval, hiding the + address until re-approved, exactly like a channel profile change; no separate automatic content + filter on the directory description. *Group member profiles:* the group's own policy redacts the + description on delivery (links/names stripped like `shortDescr`, §G). The two are independent. + +## 7. Suggested sequencing + +**Phase 1 — UX prerequisites (self-contained; do these first — no registration work until they +land).** + +1. **`Profile.description` field** (§A) + member-profile redaction on send and receive (§G) + a test + that a member's description is redacted per group policy. +2. **Show the description in the app** — banner + contact-info "Read more" via the `Modal` markdown + element (§H). This is the "see how it looks" step; iterate on the UX here. +3. **`ChatPeerType` extension** (`CPTBusiness`, `CPTUnknown`, lenient decoder) (§A) — the type only, + **no UI** to set or display it yet. +4. **Share a contact link via chat** — core (`getUserAddressSignKey`, `APIShareAddress`, + `/share address`, §B.1–3) + the app share UI mirroring the channel share (§B.5) + a client test on + the signed `MCLContact` card. + +**Phase 2 — directory (only after Phase 1).** + +5. Directory store: migration + `ContactReg` model/queries (link-keyed). +6. `deChatLinkReceived` `MCLContact` case (verify + derive type + link-in-profile check + + `addContactReg`) + name↔link verification + admin approval + directory test through to "listed". +7. Listing merge (`DETContact` + `contactDirectoryEntry` + `generateListing`) + one unified + group+contact search + website rendering. + +**Deferred:** peerType setting/badge UI; the support-bot entry point (§B.4). diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 9bd94403bc..cd3e0cbb5e 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."399c5fe8c62efd3e0b47f1e2469542621362ef77" = "1zdksdzzgjch9n6da8p26cj57l6ajdisjs7fpb1hvn4frlvwfhns"; + "https://github.com/simplex-chat/simplexmq.git"."399c5fe8c62efd3e0b47f1e2469542621362ef77" = "1mva875i9fm6ak2laiim9xl3q1l2g5r0h74dvql4m2lwcf8s8qx8"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index f08e6fcdb8..3df43c9bcc 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -149,6 +149,7 @@ library Simplex.Chat.Store.Postgres.Migrations.M20260629_roster_catchup Simplex.Chat.Store.Postgres.Migrations.M20260707_file_digest Simplex.Chat.Store.Postgres.Migrations.M20260714_member_security_code + Simplex.Chat.Store.Postgres.Migrations.M20260715_profile_description else exposed-modules: Simplex.Chat.Archive @@ -315,6 +316,7 @@ library Simplex.Chat.Store.SQLite.Migrations.M20260629_roster_catchup Simplex.Chat.Store.SQLite.Migrations.M20260707_file_digest Simplex.Chat.Store.SQLite.Migrations.M20260714_member_security_code + Simplex.Chat.Store.SQLite.Migrations.M20260715_profile_description other-modules: Paths_simplex_chat hs-source-dirs: diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index b0ae5d46ef..6a80f51a1e 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -398,6 +398,7 @@ data ChatCommand | APIPlanForwardChatItems {fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId} | APIForwardChatItems {toChatRef :: ChatRef, sendAsGroup :: ShowGroupAsSender, fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId, ttl :: Maybe Int} | APIShareChatMsgContent {shareChatRef :: ChatRef, toSendRef :: SendRef} + | APIShareMyAddress {toSendRef :: SendRef} | APIUserRead UserId | UserRead | APIChatRead {chatRef :: ChatRef} @@ -563,6 +564,7 @@ data ChatCommand | ForwardGroupMessage {toChatName :: ChatName, fromGroupName :: GroupName, fromMemberName_ :: Maybe ContactName, forwardedMsg :: Text} | ForwardLocalMessage {toChatName :: ChatName, forwardedMsg :: Text} | SharePublicGroup {shareGroupName :: GroupName, toChatName :: ChatName} + | ShareMyAddress {toChatName :: ChatName} | SendMessage SendName Text | SendMemberContactMessage GroupName ContactName Text | AcceptMemberContact ContactName diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index 51de91e588..5bcd381fed 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -158,7 +158,7 @@ createActiveUser cc CoreChatOpts {chatRelay, headless} createBot_ userDisplayNam loop = do displayName <- T.pack <$> withPrompt "display name" getLine createUser loop False $ mkProfile displayName - mkProfile displayName = Profile {displayName, fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing, contactDomain = Nothing} + mkProfile displayName = Profile {displayName, fullName = "", shortDescr = Nothing, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing, contactDomain = Nothing} createUser onError clientService p = execChatCommand' (CreateActiveUser NewUser {profile = Just p, pastTimestamp = False, userChatRelay = BoolDef chatRelay, clientService = BoolDef clientService}) 0 `runReaderT` cc >>= \case Right (CRActiveUser user) -> pure user diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index e24c3fbad5..ed1a53b776 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -1193,30 +1193,23 @@ processChatCommand cxt nm = \case _ -> Nothing ownerSig <- pure signingKeys $>>= \GroupKeys {memberPrivKey} -> - mkLinkOwnerSig memberPrivKey groupLink memberId <$$> shareChatBinding user toSendRef + mkLinkOwnerSig memberPrivKey groupLink (Just memberId) <$$> shareChatBinding user toSendRef let text = safeDecodeUtf8 $ strEncode groupLink pure $ CRChatMsgContent user MCChat {text, chatLink = MCLGroup groupLink gp, ownerSig} - where - mkLinkOwnerSig :: ConnectionModeI m => C.PrivateKeyEd25519 -> ConnShortLink m -> MemberId -> (ChatBinding, ByteString) -> LinkOwnerSig - mkLinkOwnerSig privKey connLink MemberId {unMemberId} (cbTag, bindingData) = - let ownerId = Just $ B64UrlByteString unMemberId - cb = encodeChatBinding cbTag bindingData - ownerSig = C.sign' privKey $ cb <> smpEncode connLink - in LinkOwnerSig {ownerId, chatBinding = B64UrlByteString cb, ownerSig} - shareChatBinding :: User -> SendRef -> CM (Maybe (ChatBinding, ByteString)) - shareChatBinding u = \case - SRDirect contactId -> do - ct <- withFastStore $ \db -> getContact db cxt u contactId - forM (contactConn ct) $ \conn -> - (CBDirect,) <$> withAgent (`getConnectionRatchetAdHash` aConnId conn) - SRGroup toGroupId _ asGroup -> do - GroupInfo {groupProfile = GroupProfile {publicGroup}, membership = m} <- withFastStore $ \db -> getGroupInfo db cxt u toGroupId - pure $ mkBinding m <$> publicGroup - where - mkBinding GroupMember {memberId} PublicGroupProfile {publicGroupId = pgId} - | asGroup = (CBChannel, smpEncode pgId) - | otherwise = (CBGroup, smpEncode (pgId, memberId)) APIShareChatMsgContent _ _ -> throwCmdError "sharing is only supported for public groups" + APIShareMyAddress toSendRef -> withUser $ \user -> do + UserContactLink {connLinkContact = CCLink _ sl_, addressSettings} <- withFastStore (`getUserAddress` user) + case sl_ of + Nothing -> throwCmdError "your address has no short link to share" + Just connLink -> do + conn <- withFastStore $ \db -> getUserAddressConnection db cxt user + ownerSig <- + withAgent (`getConnLinkPrivKey` aConnId conn) $>>= \privKey -> + mkLinkOwnerSig privKey connLink Nothing <$$> shareChatBinding user toSendRef + let business = businessAddress addressSettings + profile = userProfileDirect user Nothing Nothing True + text = safeDecodeUtf8 $ strEncode connLink + pure $ CRChatMsgContent user MCChat {text, chatLink = MCLContact {connLink, profile, business}, ownerSig} APIUserRead userId -> withUserId userId $ \user -> withFastStore' (`setUserChatsRead` user) >> ok user UserRead -> withUser $ \User {userId} -> processChatCommand cxt nm $ APIUserRead userId APIChatRead chatRef@(ChatRef cType chatId scope_) -> withUser $ \_ -> case cType of @@ -2509,6 +2502,18 @@ processChatCommand cxt nm = \case CRChatMsgContent _ mc -> processChatCommand cxt nm $ APISendMessages sendRef False Nothing False [composedMessage Nothing mc] r -> pure r + ShareMyAddress toChatName -> withUser $ \user -> do + toChatRef <- getChatRef user toChatName + sendRef <- case toChatRef of + ChatRef CTDirect ctId _ -> pure $ SRDirect ctId + ChatRef CTGroup gId scope_ -> do + gInfo <- withFastStore $ \db -> getGroupInfo db cxt user gId + pure $ SRGroup gId scope_ (useRelays' gInfo) + _ -> throwCmdError "unsupported share target" + processChatCommand cxt nm (APIShareMyAddress sendRef) >>= \case + CRChatMsgContent _ mc -> + processChatCommand cxt nm $ APISendMessages sendRef False Nothing False [composedMessage Nothing mc] + r -> pure r SendMessage sendName msg -> withUser $ \user -> do let mc = MCText msg case sendName of @@ -4603,6 +4608,25 @@ processChatCommand cxt nm = \case serverShortLink = \case CSLInvitation _ srv lnkId linkKey -> CSLInvitation SLSServer srv lnkId linkKey CSLContact _ ct srv linkKey -> CSLContact SLSServer ct srv linkKey + mkLinkOwnerSig :: ConnectionModeI m => C.PrivateKeyEd25519 -> ConnShortLink m -> Maybe MemberId -> (ChatBinding, ByteString) -> LinkOwnerSig + mkLinkOwnerSig privKey connLink ownerMemberId (cbTag, bindingData) = + let ownerId = (\MemberId {unMemberId} -> B64UrlByteString unMemberId) <$> ownerMemberId + cb = encodeChatBinding cbTag bindingData + ownerSig = C.sign' privKey $ cb <> smpEncode connLink + in LinkOwnerSig {ownerId, chatBinding = B64UrlByteString cb, ownerSig} + shareChatBinding :: User -> SendRef -> CM (Maybe (ChatBinding, ByteString)) + shareChatBinding u = \case + SRDirect contactId -> do + ct <- withFastStore $ \db -> getContact db cxt u contactId + forM (contactConn ct) $ \conn -> + (CBDirect,) <$> withAgent (`getConnectionRatchetAdHash` aConnId conn) + SRGroup toGroupId _ asGroup -> do + GroupInfo {groupProfile = GroupProfile {publicGroup}, membership = m} <- withFastStore $ \db -> getGroupInfo db cxt u toGroupId + pure $ mkBinding m <$> publicGroup + where + mkBinding GroupMember {memberId} PublicGroupProfile {publicGroupId = pgId} + | asGroup = (CBChannel, smpEncode pgId) + | otherwise = (CBGroup, smpEncode (pgId, memberId)) verifyLinkOwner :: ConnectionModeI m => C.PublicKeyEd25519 -> [OwnerAuth] -> ConnShortLink m -> Maybe LinkOwnerSig -> Maybe OwnerVerification verifyLinkOwner rootKey owners connLink = fmap $ \LinkOwnerSig {ownerId, chatBinding = B64UrlByteString bindingBytes, ownerSig} -> @@ -5440,6 +5464,7 @@ chatCommandP = "/_forward plan " *> (APIPlanForwardChatItems <$> chatRefP <*> _strP), "/_forward " *> (APIForwardChatItems <$> chatRefP <*> (" as_group=" *> onOffP <|> pure False) <* A.space <*> chatRefP <*> _strP <*> sendMessageTTLP), "/_share chat content " *> (APIShareChatMsgContent <$> chatRefP <* A.space <*> sendRefP), + "/_share address " *> (APIShareMyAddress <$> sendRefP), "/_read user " *> (APIUserRead <$> A.decimal), "/read user" $> UserRead, "/_read chat " *> (APIChatRead <$> chatRefP), @@ -5637,6 +5662,7 @@ chatCommandP = ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <*> pure Nothing <* A.space <*> msgTextP, ForwardLocalMessage <$> chatNameP <* " <- * " <*> msgTextP, "/share chat #" *> (SharePublicGroup <$> displayNameP <* A.space <*> chatNameP), + "/share address " *> (ShareMyAddress <$> chatNameP), SendMessage <$> sendNameP <* A.space <*> msgTextP, "@#" *> (SendMemberContactMessage <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <* A.space <*> msgTextP), "/accept_member_contact @" *> (AcceptMemberContact <$> displayNameP), @@ -5834,7 +5860,7 @@ chatCommandP = newUserP relay = do (cName, shortDescr) <- profileNameDescr service <- (" service=" *> onOffP) <|> pure False - let profile = Just Profile {displayName = cName, fullName = "", shortDescr, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing, contactDomain = Nothing} + let profile = Just Profile {displayName = cName, fullName = "", shortDescr, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing, contactDomain = Nothing} pure NewUser {profile, pastTimestamp = False, userChatRelay = BoolDef relay, clientService = BoolDef service} newBotUserP = do files_ <- optional $ "files=" *> onOffP <* A.space @@ -5843,7 +5869,7 @@ chatCommandP = let preferences = case files_ of Just True -> Nothing _ -> Just (emptyChatPrefs :: Preferences) {files = Just FilesPreference {allow = FANo}} - profile = Just Profile {displayName = cName, fullName = "", shortDescr, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences, badge = Nothing, contactDomain = Nothing} + profile = Just Profile {displayName = cName, fullName = "", shortDescr, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences, badge = Nothing, contactDomain = Nothing} pure NewUser {profile, pastTimestamp = False, userChatRelay = BoolDef False, clientService = BoolDef service} jsonP :: J.FromJSON a => Parser a jsonP = J.eitherDecodeStrict' <$?> A.takeByteString diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 9c9e794ff8..e22c4a87f1 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -1117,8 +1117,8 @@ rejectRelayInvitationAsync user uclId cxt groupRelayInv invId reqChatVRange init agentAcceptContactAsync cmdId acId False invId msg PQSupportOff chatV subMode businessGroupProfile :: Profile -> GroupPreferences -> GroupProfile -businessGroupProfile Profile {displayName, fullName, shortDescr, image} groupPreferences = - GroupProfile {displayName, fullName, description = Nothing, shortDescr, image, publicGroup = Nothing, groupPreferences = Just groupPreferences, memberAdmission = Nothing} +businessGroupProfile Profile {displayName, fullName, shortDescr, description, image} groupPreferences = + GroupProfile {displayName, fullName, description, shortDescr, image, publicGroup = Nothing, groupPreferences = Just groupPreferences, memberAdmission = Nothing} introduceToModerators :: StoreCxt -> User -> GroupInfo -> GroupMember -> CM () introduceToModerators cxt user gInfo@GroupInfo {groupId} m@GroupMember {memberRole, memberId} = do @@ -1264,17 +1264,25 @@ memberInfo g m@GroupMember {memberId, memberRole, memberProfile, memberPubKey, a } redactedMemberProfile :: GroupInfo -> GroupMember -> Profile -> Profile -redactedMemberProfile g m Profile {displayName, fullName, shortDescr, image, contactLink = lnk, peerType, badge, contactDomain} = - Profile {displayName, fullName, shortDescr = removeSimplexLink =<< shortDescr, image, contactLink, preferences = Nothing, peerType, badge, contactDomain = redactedDomain} +redactedMemberProfile g m Profile {displayName, fullName, shortDescr, description, image, contactLink = lnk, peerType, badge, contactDomain} = + Profile {displayName, fullName, shortDescr = removeSimplexLink True =<< shortDescr, description = removeSimplexLink False =<< description, image, contactLink, preferences = Nothing, peerType, badge, contactDomain = redactedDomain} where contactLink = if allowSimplexLinks then lnk else Nothing redactedDomain = if allowDirect then (\d -> d {proof = Nothing} :: SimplexDomainClaim) <$> contactDomain else Nothing allowDirect = groupFeatureMemberAllowed SGFDirectMessages m g allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m g && allowDirect - removeSimplexLink s + removeSimplexLink dropOnLink s | allowSimplexLinks = Just s - | hasObfuscatedSimplexLink s = Nothing - | otherwise = maybe (Just s) (\fts -> if any ftIsSimplexLink fts then Nothing else Just s) $ parseMaybeMarkdownList s + | otherwise = case parseMaybeMarkdownList s of + Nothing -> dropObfuscated + Just fts + | not (any ftIsSimplexLink fts) -> dropObfuscated + | dropOnLink || T.null (T.strip kept) || hasObfuscatedSimplexLink kept -> Nothing + | otherwise -> Just kept + where + kept = T.concat $ map (\(FormattedText _ t) -> t) $ filter (not . ftIsSimplexLink) fts + where + dropObfuscated = if hasObfuscatedSimplexLink s then Nothing else Just s -- Roles carried by the roster; owners are on the link, not the roster. isRosterRole :: GroupMemberRole -> Bool @@ -3176,6 +3184,7 @@ simplexTeamContactProfile = { displayName = "Ask SimpleX Team", fullName = "", shortDescr = Just "Send questions about SimpleX Chat app and your suggestions", + description = Nothing, image = Just simplexChatImage, contactLink = Just $ CLFull adminContactReq, peerType = Nothing, @@ -3190,6 +3199,7 @@ simplexStatusContactProfile = { displayName = "SimpleX Status", fullName = "", shortDescr = Just "Automatic server status and app release updates", + description = Nothing, image = Just (ImageData "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAr6ADAAQAAAABAAAArwAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgArwCvAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQAC//aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Q/v4ooooAKKKKACiiigAoorE8R+ItF8J6Jc+IvEVwlrZ2iGSWWQ4CgVUISlJRirtmdatTo05VaslGMU223ZJLVtvokbdFfl3of/BRbS734rtpup2Ig8LSsIYrjnzkOcea3bafTqBX6cafqFjq1jFqemSrPbzqHjkQ5VlPIINetm2Q43LXD65T5eZXX+XquqPiuC/Efh/itYh5HiVUdGTjJWaflJJ6uEvsy2fqXKKKK8c+5Ciq17e2mnWkl/fyLDDCpd3c4VVHJJJr8c/2kf8Ago34q8M3mpTfByG3fT7CGSJZrlC3nStwJF5GFU8gd69LA5VicXTrVaMfdpxcpPokk397toj4LjvxKyLhGjRqZxValVkowhFc05O9m0tPdjfV7dN2kfq346+J3w9+GWlPrXxA1m00i1QZL3Uqxj8Mnn8K/Mj4tf8ABYD4DeEJ5dM+Gmn3niq4TIE0YEFtn/ffBI+imv51vHfxA8b/ABR1+bxT8RNUuNXvp3LtJcOWCk84VeigdgBXI18LXzupLSkrL72fzrxH9IXNsTKVPKKMaMOkpe/P8fdXpaXqfqvrf/BYH9p6+1w3+iafo1jZA8WrRPKSPeTcpz9BX1l8J/8Ags34PvxDp/xn8M3OmSnAe709hcQfUoSHA/A1/PtSE4/GuKGZ4mLvz39T4TL/ABe4swlZ1ljpTvvGaUo/dbT/ALdsf2rfCX9pT4HfHGzF18M/EdnqTYBaFXCzJn+9G2GH5V7nX8IOm6hqGkX8eraLcy2d3EcpPbuY5FPsykGv6gf+CWf7QPxB+OPwX1Ky+JF22pX3h69+yJdyf62WJlDrvPdlzjPevdwGae3l7OcbP8D+i/DTxm/1ixkcqx2H5K7TalF3jLlV2rPWLtqtWvM/T2iiivYP3c//0f7+KKKKACiiigAooooAK/Fv/goX8Qvi2fFcXgfWrRtP8NDEls0bZS7YfxORxlT0Xt1r9pK8u+L/AMI/Cfxp8F3HgvxbFujlGYpgB5kMg6Op9R+tfR8K5vQy3MYYnE01KK0843+0vNf8NZn5f4wcFZhxTwziMpy3FOjVeqSdo1Lf8u5u11GXk97Xuro/mBFyDX3t+yL+2Be/CW+h8B+OHafw7cyALIxJa0Ldx6p6jt1FfMvx/wDgR4w/Z+8YN4d8RoZrSbLWd4owk6D+TDuK8KF0K/pLFYHA51geWVp0pq6a/Brs1/wH2P8ALvJsz4h4D4h9tR5qGLoS5ZRls11jJbSjJferSi9mf1uafqFlqtlFqWmyrPBOoeORDlWU8gg069vrPTbSS/v5FhghUu7ucKqjqSa/CH9j79sm++EuoQ/D/wAeSNceHbmRVjlZstZk9x6p6jt2q3+15+2fffFS8n8AfD2V7bw9CxWWZThrwj+Se3evxB+G2Zf2n9TX8Lf2nTl/+S/u/PbU/v2P0nuGv9Vf7cf+9/D9Xv73tLd/+ffXn7afF7pqftbfth3nxUu5vAXgGR7fw/A5WWUHDXZX19E9B361+Z/xKm3eCL9R3UfzFbQul6Cn+I/A3ivxR8LPEXivSbVn07RoVkurg8Iu5gAue7HPSv1HOsrwmVcN4uhRSjBUp6vq3Fq7fVt/5I/gTNeI884x4kjmeYOVWtKSdop2hCPvWjFbQjFNv5ybbuz4Toqa0ge9uoLOIhWnkSNSxwAXIUEnsBnmv0+/aK/4Jg+O/gj8Hoviz4b1n/hJFt40l1G2ig2NDG4yZEIJ3KvfgHHNfxVTw9SpGUoK6W5+xZVw1mWZYfEYrA0XOFBKU2raJ31te72b0T0R+XRIAyegr+gr/glx+yZoHhjwBc/tKfFywiafUY2OmpeIGS3sVGWmIbgF+TkjhR71+YP7DX7Lt9+1H8ZLfR75WTw5pBS61ScDKsoIKwg+snf0Ffqd/wAFSv2o4Phf4Ltv2WvhmVtrjUbRBfvA2Ps1kOFhAHQyAc9ML9a9HL6UacHi6q0W3mz9Q8M8owuV4KvxpnEL0aN40Yv/AJeVXpp5LZPo7v7J+M/7U/jX4e/EL4/+JfFXwrsI9P0Ke5K26RKESTZw0oUcAOeQBX7J/wDBFU5+HPjYf9RWH/0SK/nqACgKOgr+hT/giouPh143b11SH/0SKWVzc8YpPrf8jHwexk8XxzSxVRJSn7WTSVknKMnoui7H7a0UUV9cf3Mf/9L+/iiiigAoorzX4wfGD4afAP4bav8AF74v6xbaD4d0K3e6vb26cJHHGgyevUnoAOSeBTjFyajFXYHpVFf55Xxt/wCDu34nj9vzS/G3wX0Qz/ArQ2ksLnSp1CXurQyMA15uPMTqBmJD2+914/uU/Y//AGxfgH+3P8ENL+P37OutxazoWpoNwHyzW02PmhmjPKSKeCD9RxXqY/JcXg4QqV4WUvw8n2ZnCrGTaTPqGiiivKNDy/4u/CLwd8afBtx4N8ZW4kilBMUoH7yGTs6HsR+tfzjftA/AXxl+z54yfw34jQzWkuXs7xF/dzR/0YdxX9OPiDxBofhPQ7vxN4mu4rDT7CF57m4ncJHFFGMszMcAAAZJNf53n/Bav/g5W1H4ufGjTvg5+xB5F14E8JX4l1HVriIE6xNE2GjhLDKQdRuGC55HHX9L8Os+x2ExP1eKcsO/iX8vmvPy6/ifg3jZ4NYDjDBPFUEqeYU17k/50vsT8n0lvF+V0fq0LhTUgnA4r4y/ZG/bJ+FX7YXw9HjDwBP5N/ahV1LTZeJrSUjoR3U/wsOK+sRdL/n/APXX9G0nCrBTpu6Z/mVmuSYvLcXUwOPpOnWg7SjJWaf9ap7NarQ+pf2dP2evGH7Q3i4aLogNvp1uQ15esMpEnoPVj2Ffrd+1V8GvDnw5/YU8X+APh/Z7IrewEjYGXlZGUs7nqSQM18C/sO/ti6b8F7o/Dnx6qpoN9LvS6RRvglbjL45ZT69vpX7wX1poHjjwxNYzbL3TdUt2jbaQySRSrg4PoQa/nnxXxGaTxLwmIjy4e3uW2lpu33Xbp87v+7Po58I8L4nhfFVMuqKeY1oTp1nJe9S5k0oxWtoPfmXxve1uVfwqKA0YHYiv6Ev+CZ37bVv490eP9mb4zXAn1GKJo9Murg5F3bgYMLk9XUcD+8tflR+1/wDsn+Nv2XfiNdadqFs8vh28md9Mv1GY3iJyEY9nXoQa+UrC/v8ASr+DVdJnktbq2dZYZomKvG6nIZSOhFfztQrVMJW1Xqu5+Z8PZ5mvBWeSc4NSg+WrTeinHqv1jL56ptP+s7xHZ/A//gnR8EfE/jTwra+RHqF5JdxWpbLTXcwwkSnrsGPwXNfyrfEDx54l+J/jXU/iB4wna51LVZ3nmdj3Y8KPQKOAPQV2vxX/AGhvjT8corC3+K2vz6vFpq7beNgERT3YqvBY92NeNVeOxirNRpq0Fsju8RePKWfTo4TLqPscFRXuU9F7z+KTSuvJK7srvqwr+ir/AIIuaVd2/wAH/FesSIRDd6uFjb+8Y41Dfka/BX4YfCzx78ZfGVr4C+G+nyajqV22Aqj5I17u7dFUdya/r+/ZV+Aenfs2fBLSPhbZyC4ntVaW7nAx5tzKd0jfTJwPYV1ZLQk63tbaI+w8AOHcXiM8ebcjVClGS5ujlJWUV3sm27baX3R9FUUUV9Uf2gf/0/7+KKKKACv4If8Ag8QT9vN9W8IsVk/4Z+WJedOL7f7Xyd32/HGNu3yc/LnPev73q84+Lnwj+G/x3+HGr/CT4uaRba74d123e1vbK6QPHJG4weD0I6gjkHkV6WUY9YLFQxDgpJdP8vMipDmi0f4W1frt/wAEhP8Agrt8af8AglD8b38V+Fo21zwPr7xp4i0B3KpcRoeJoTyEnjBO04+boeK+m/8AguZ/wQz+I3/BMD4kyfEn4Ww3fiD4Oa5KzWWolC76XKx4tbphwOuI3PDAc81/PdX7LCeFzHC3VpU5f18mjympU5eZ/t9fsk/tb/Av9tv4G6N+0F+z3rUWs6BrEQYFCPNt5cfPDMnVJEPDKf5V794h8Q6F4T0O78TeJ7uGw06wiae4uZ3EcUUaDLMzHAAA6k1/j9f8EiP+Cunxv/4JTfHAeKPCZfWfAuuyRx+IvD8jkRTxg486Lsk8YJ2n+Loa/V7/AILy/wDBxZd/t2eHl/Zc/Y6mu9I+Gl1DDNrWoSBoLvUpGAY2+OqQoeH/AL5GOlfneI4OxCxio0taT+12Xn59u53xxMeW73ND/g4M/wCDgzVP2yNV1H9jz9j3UZrD4ZWE7waxrEDlH110ONiEYItgQe/7z6V/I6AAMDgCgAKNo6Cv0j/4Jkf8Ex/j/wD8FOvj/Y/Cj4UWE9voFvNGdf18xk2um2pPzEt0MhGdiZyTX6FhsNhctwvLH3YR1bfXzfn/AEjhlKVSR77/AMEMf2Rf2v8A9qr9tPRrb9mNpdL0fSp438UaxKjNYW+nk/PHKOA7uoIjTrnniv7Lfj98CvG37PPjiXwj4uiLxNl7S7UYjuIuzD39R1Ffvt+wn+wd+z5/wTy+A+n/AAF/Z70pbKyt1V728cA3V/c4w0079WYnoOijgV7V8cPgb4G+Pngqfwb41twwYEwXCgebBJ2ZT/MdDXi5N4mTwmYWqRvhXpb7S/vL9V28z8c8YfBXC8XYL61hbQx9Ne7LpNfyT8v5ZfZfkfyXi5r9Lf2Jv24bn4S3UHwz+JkzT+HZ5AsNy5LNZlu3vHn8q+KPj38CPHf7PPjabwn4yt2ELMxtLsD91cRg8Mp6Z9R2rxAXAPANfuePyzL89y/2c7TpTV1JdOzT6Nf8Bn8C5FnGfcEZ79Yw96OJpPlnCS0a6xkusX/k4u9mf2IeK/B/w++Mngt9C8U2ltrWi6lEGCuA6OrDhlPY+hHNfztftw/8E4tN+AGlTfE34ba3HJo0koVdMvGC3CFv4Ym/5aAenBArvf2PP2+9R+CGmv4B+JSy6joEUbtaOp3TQOBkRj1Rjx7V8uftEftH+Nf2i/G7+KPEzmG0hyllZqT5cEef1Y9zX4LT8GMTisynhsY7UI6qot5J7Jefe+i87o/prxI8YuEM/wCF6WM+rc2ZSXKo6qVJrdykvih/Ktebsmnb4DkilicxyqVYdQRzXUaN4R1HVMSzjyIf7zDk/QV6dIlpJIJ5Y1Z16MRk1+qf7DX7Ed58ULmH4p/Fe2kt/D8Dq9paSDabwjncf+mf/oX0rKXg3lOR+0zDPMW6lCL92EVyufZN3vfyjbvdI/AeFsJnHFOPp5TktD97L4pP4YLrJu2iXnq3ok20es/8Erv2f/G/gf8AtD4ozj7Bo2pwiFIpY/3t2VOQ4J5VFzx659q/aKq9paWthax2VlGsUMShERBtVVHAAA6AVYr4LNcdTxWIdSjRjSpqyjGKslFber7t6tn+k3APB1LhjJaOUUqsqjjdylJ/FKTvJpfZV9orbzd2yiiivNPsj//U/v4ooooAKKKKAPO/iz8Jvh18c/h1q/wm+LGk2+ueHtdt3tb2yukDxyxuMEEHoR1B6g81/lm/8Fy/+CFfxG/4Jh/ENvid8J4bzxF8Htdmke1vliaRtHctxbXTAEBecRyHAbGDzX+q54j8R6B4Q0C88U+KbyHT9N0+F7i5ubhxHFFFGMszMcAADqa/zM/+Dhb/AIL06p+3f4rvP2Tf2Xr6S0+Eui3DR397GcHXriM8N7W6EfIP4jz6V9fwfPGLFctD+H9q+3/D9jmxKjy+9ufyq0UAY4or9ZPMP0v/AOCX3/BLf9oT/gqP8d4Phf8ACa0lsvDtjLG3iDxDJGTa6bbse56NKwB8uPOSfav9ZX9hD9hT4Df8E8v2fdK/Z7+AenLbWNkoe8vHUfab+6I+eeZhyWY9B0UcCv8AKC/4JUf8FV/j1/wSu+PCfEf4aSHUvC+rPHH4i0CViIL63U43D+7MgJKN+B4r/Wd/Yy/bM+BH7eHwH0j9oL9n7Vo9S0fU4182LI8+0nx88MydVdTxz16ivzbjZ43nipfwOlu/n59uh6GE5Labn1ZRRRXwB2Hi3x3+BPgj9oHwJceCPGcIIYFre4UfvYJezKf5jvX8vH7QvwB8d/s4eOZfB/jKEtDIS9neKP3VxFngqfX1Hav6gvj58e/An7PHgK48ceN7gLtBW2twf3txL2RR/M9hX8rX7Qn7Rnjz9o3x5L418ZyhUXKWlqh/dW8WeFUevqe5r988G4Zu3Ut/ueu/839z/wBu6fM/jj6UdPhlwo8y/wCFTS3Lb+H/ANPf/bPtf9unlQuAec077SPWueFznrTxc1+/eyP4udE/XX9g79h24+K8tv8AF74qQvD4fgkDWdo64N4V53H/AKZg/wDfX0r+ge0tLWwtY7KyjWKGJQiIgwqqOAAOwFfzc/sIft2XnwO1KH4ZfEeVp/Ct5L8k7Es9k7YHH/TMnkjt1r+kDTNT07WtOg1fSJ0ubW5QSRSxncjowyCCOoNfyr4q0s3jmreYfwtfZW+Hl/8Akv5r6/Kx/or9HSXDX+rqhkqtidPb81vac/d/3P5Lab/auXqKKK/Lz+gwooooA//V/v4ooooAKxfEniTQPB2gXnirxVew6dpunQvcXV1cOI4oYoxlndjgAADJJrar/PV/4Ozf+CiX7Xlr8Yrf9hCx0u98GfDaS0iv5L1GZT4iZs5HmKceTERgx9d3LcYr08py2eOxMaEXbu/L9SKk1CN2fIX/AAcD/wDBfrXv27vFF1+yx+ylqFzpnwl0id476+icxSa/MhwGOMEWykHYv8fU9hX8qoAAwOAKUAAYFfqj/wAEnf8AglH8cv8Agqp8ek+Hvw/R9M8I6NJFJ4k19lzHZW7k/ImeGmcAhF/E8V+xUKGFyzC2Xuwju/1fds8tuVSXmM/4JQ/8Epfjr/wVU+Pcfw5+HiPpXhPSXjl8ReIZEJhsoGP3E7PO4B2J+J4r7o/4Li/8EC/H3/BL/UYPjH8Hp7vxV8JNQMcL3sy7rnTLkgDbcFRjZI3KPwATg9q/0rP2MP2MPgL+wZ8BdI/Z5/Z60hNM0bS4x5kpANxeTn7887gAvI55JPToOK9y+J/ww8AfGfwBqvwu+KOlW+t6Brdu9re2V0gkilicYIIP6HqDXwVbjSu8YqlNfulpy9139e3Y7VhY8tnuf4VdfqD/AMErP+Cpvx1/4Jb/ALQNn8S/h7cS6j4VvpUj8QeH2kIt723zgsB0WVRyjetffn/BeH/ghJ4x/wCCZvjlvjP8EYbvXPg5rk7GKcqZJdGmc5FvOwH+rOcRyH0wea/nCr9ApVcNmOGuvehL+vk0cLUqcvM/24v2Mf20PgH+3l8CdK/aA/Z61iPVNI1FF86LI+0Wc+PnhnTqjqeOevUcV3nx/wD2gfh/+zp4CuPHHjq5CBQVtrZT+9uJeyIP5noBX+Ud/wAEL/25f2t/2NP2u7A/s7xPrPhzW5Yk8T6LOzCyls1PzTE9I5UXJRupPHIr+p39o79pXx/+0v8AEGbxv42l2RrlLO0QnyreLPCqPX1PUmvM4b8KauYZg5VJWwkdW/tP+6vPu+i8z8r8VvF3D8L4P6vhbTx017sekF/PL/21fafkjV/aF/aN8e/tHePZ/GvjOc+XuK2lopPlW8WeFUevqe9eFfasDmsL7UB1r9kv+Cen/BPuX4mPa/Gv41Wrw6HE4k0/T5FwbsjkO4PPl56D+L6V/QWbZjlnDmW+1q2hSgrRit2+kYrq/wDh2fw9kXDmdcZ526NK9SvUfNOctorrKT6JdF6JIh/Yq/4JyXXxq8MSfEn4wtPpukXkLLp1vH8s0hYcTHPRR1Ud6+KP2nP2bvHX7MXj+Twl4pUz2U+Xsb5QRHcRZ/Rh/Etf2D2trbWNtHZ2caxRRKEREGFVRwAAOgFeSfHL4G+Af2gvAVz4A8f2wmt5huimUDzYJB0dD2I/Wv5/yrxgx0c3niMcr4abtyL7C6OPdrr/ADeWlv604g+jdlFTh6ngsrfLjaauqj/5eS6xn2i/s2+Hz1v/ABi+d3r9O/2DP28r/wCBGpRfDT4lSvdeFL2UBJmYs9izcZX1j7kduor48/ah/Zr8bfsu/EWTwZ4pHn2c4MtheqMJcQ5IB9mHRhXzd9oAFf0Djsuy3iHLeSdqlGorpr8Gn0a/4DW6P5DyrMc74Mzz2tG9LE0XaUXs11jJdYv/ACaezP7pdK1bTNd02DWdGnS6tLlBJFLEwZHRuQQR1FaFfix/wSG1n47X3hPVLHXUL+BoT/oEtxneLjPzLD6pjr2B6d6/aev424nyP+yMyrZf7RT5Huvv17NdV0Z/pTwPxP8A6w5Lh82dGVJ1FrGXdaNp9YveL6oKKKK8A+sP/9b+/iiiigAr4E/4KI/8E4f2b/8AgpZ8DLr4M/H7SklljV5NJ1aJQLzTblhxLC/Uc43L0YcGvvuitKNadKaqU3aS2Ymk1Zn+Vt8Nf+DZH9vDxJ/wUEn/AGQfGti+m+DdMkF5eeNlTNjLpRb5Xgz964cfL5XVWyTx1/0lv2L/ANif9nv9gn4H6b8Bv2dNDh0jSrFF8+YKDcXs4GGmuJOskjHPJ6dBxX1lgZz3pa9bNc+xWPjGFV2iui6vu/60M6dKMNgooorxTU4T4m/DHwB8ZfAeqfDH4paRba7oGtQPbXtjeRiWGaJxghlII/wr/M//AOCw/wDwbq/En9kb9o7Ttc/ZhQ6h8KvGl4VgknkUyaJIxy0UmTueMDmNgCexr/SN/aA/aA+Hf7N3w6u/iL8RbtYYIFIggBHm3Ev8Mca9yfyA5NfyB/tTftZfEX9qv4gSeL/GEv2exgLJYWEZPlW8WeOO7H+Ju9fsXhRwnmOZYl4hNwwi+Jv7T/lj5930Xnofj3iv4nYThrCPD0bTxs17kekV/PPy7L7T8rn58fs1fs1/Df8AZg8Dp4U8CwB7qYK19fuAZrmQDkseyjsvQV9GfaWrAWcjvUnnt6mv62w+Cp0KapUo2itkfwFmOLxWPxNTGYyo51Zu8pN6t/1stktEftx/wTa/YHsfi6sHx2+L8aT6BFJnT7DcGFy6dWlAzhQf4T171/SBaWltY20dlZRrFDEoREQYVVHAAA6AV/Hv+xJ+3N4y/ZO8Wi0ui+oeE9QkX7dYk5KdjLFzw49Ohr+tj4c/Efwb8WPB1l498A30eoaZqEYkiljOevVWHZh0IPIr+TPGXLs6p5p9Zxz5sO9KbXwxX8rXSXd/a3Wmi/t76P8AmHD08l+qZZDkxUdaydueT/mT0vDsl8Oz1d33FFFFfjR/QB4x8dPgN8O/2hvA1x4F+Idms8MgJhmAxLbydnjbqCP1r8RPg3/wSV8Z/wDC9r7T/izMreDNIlEkM8TYfUVPKpgcoAPv+/Ar+iKivrsh43zbKMLWweCq2hUXXXlf80eza0/HdJnwPFHhpkHEGOw+YZlQ5qlJ7rTnXSM/5op6/hs2jD8NeGdA8HaHbeGvC9nFYWFmgjhghUIiKOwArcoor5Oc5Tk5Sd292fd06cacVCCtFaJLZLsgoooqSz//1/7+KKKKACiiigAooooAK8J/aK/aG+H37M/wzvPiX8QrgRwwDbb26kebczH7saDuSep7DmvdW3bTt69s1/Hj/wAFS9c/acu/2hbiw+Psf2fTYWf+w47bd9ha2zw0ZPWQj7+eQfav0Dw44PpcRZssLXqqFOK5pK9pSS6RXfu+i1PzvxN4zrcN5PLF4ei51JPli7XjFv7U327Lq9Dwr9qv9rn4lftZ+Pv+Ev8AG8i29na7ksNPiJ8m2iJ7Ak5Y/wATHrXy/wDacDJNYfn45PFftR/wTX/4Ju6j8aryz+OXxttpLXwtbSrJY2Mi7W1Bl53MD0hB/wC+vpX9jZpmGU8LZT7WolTo01aMVu30jFdW/wDNvqz+HcryTOeLs4dODdSvUd5Tlsl1lJ9Eui9Elsix/wAE8/8Agmpc/Hq3HxZ+OcFxY+F8f6Daj93Jen++eMiMdum76V88ft4fsM+LP2RvGH9p6MJtS8G6gxNnfMMmFj/yxmIAAYfwnuPev7DbGxs9Ms4tP0+JYIIFCRxoNqqq8AADoBXL+P8AwB4R+KHhG+8C+OrGPUNM1CMxTQyjIIPcehHUEdDX8x4PxqzWOdvH11fDS0dJbKPRp/zrdvrtta39V47wCyWeQRy7D6YqOqrPeUuqkv5Hsl9ndXd7/wACwuGHevvT9iL9u7x1+yP4n+wMDqXhPUJVN/YMTlOxlh/uuB+BqH9vD9hXxl+yD4v/ALS03zNT8HajIfsV8VyYSf8AljNjgMOx/iHvX59C6bHav6fjDKeJsqurVcPVX9ecZRfzTP5LdLOeE850vRxNJ/15SjJfJo/v3+GnxJ8HfF3wRp/xC8BXiX2l6lEJYZEPr1Vh2YdCDyDXd1/PD/wRa8KftJW8moeKfPNp8N7kMBBdKT9ouR/Hbgn5QP4m6Gv6Hq/iHjXh6lkmb1svoVlUjF6Nbq/2ZdOZdbfhsf6AcC8SVs9yahmWIoOlOS1T2dvtR68r3V/x3ZRRRXyh9eFFFFABRRRQB//Q/v4ooooAKKKKACiiigAr5u/aj/Zg+HX7VvwyuPh14+i2N/rLO8jA861mHR0Pp2YdCOK+kaK6sDjq+DxEMVhZuFSDumt00cmOwOHxuHnhcVBTpzVpJ7NM/nF/ZW/4I2eINL+MV9rH7Rk0Vz4d0G5H2GCA8anjlXfuiDjcvJJ46V/RfY2FlpdlFpumxJBbwII444wFVEUYAAHAAFW6K9/injHM+Ia8a+Y1L8qsorSK7tLu3q3+iSPn+E+C8q4dw86GW07czvJvWT7Jvstkv1bYUUUV8sfVnEfEb4c+Dvix4Mv/AAB49sY9Q0vUYjFNDIMjB7j0YdQRyDX4HeH/APgiNJB+0LKNe1vzvhzARcxBeLyUEn/R27ADu46jtmv6KKK+r4d42zjI6Vajl1ZxjUVmt7P+aN9pW0uv0R8lxJwNk2e1aFfMqCnKk7p7XX8srbxvrZ/qzn/CnhXw/wCCPDll4R8K2sdlp2nQrBbwRDCoiDAAFdBRRXy05ynJzm7t6tvqfVwhGEVCCsloktkgoooqSgooooAKKKKAP//R/v4ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Z"), contactLink = Just (either error CLFull $ strDecode "simplex:/contact/#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FShQuD-rPokbDvkyotKx5NwM8P3oUXHxA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA6fSx1k9zrOmF0BJpCaTarZvnZpMTAVQhd3RkDQ35KT0%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"), peerType = Just CPTBot, diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index ef0ff9e345..5b6b85acd1 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -2832,7 +2832,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = when contentChanged $ updateBusinessChatProfile gInfo case memberContactId of Nothing -> do - m' <- withStore $ \db -> updateMemberProfile db cxt user m p' + m' <- withStore $ \db -> updateMemberProfile db cxt user m p'' unless (muteEventInChannel gInfo m') $ do when contentChanged $ forM_ msgTs_ $ createProfileUpdatedItem m' toView $ CEvtGroupMemberUpdated user gInfo m m' @@ -2857,7 +2857,8 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = | otherwise = pure m where - contentChanged = not (sameProfileContent (redactedMemberProfile gInfo m (fromLocalProfile p)) (redactedMemberProfile gInfo m p')) + p'' = redactedMemberProfile gInfo m p' + contentChanged = not (sameProfileContent (redactedMemberProfile gInfo m (fromLocalProfile p)) p'') updateBusinessChatProfile g@GroupInfo {businessChat} = case businessChat of Just bc | isMainBusinessMember bc m -> do g' <- withStore $ \db -> updateGroupProfileFromMember db user g p' diff --git a/src/Simplex/Chat/ProfileGenerator.hs b/src/Simplex/Chat/ProfileGenerator.hs index 4d10945ab6..b460a73d3d 100644 --- a/src/Simplex/Chat/ProfileGenerator.hs +++ b/src/Simplex/Chat/ProfileGenerator.hs @@ -10,7 +10,7 @@ generateRandomProfile :: IO Profile generateRandomProfile = do adjective <- pick adjectives noun <- pickNoun adjective 2 - pure $ Profile {displayName = adjective <> noun, fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing, contactDomain = Nothing} + pure $ Profile {displayName = adjective <> noun, fullName = "", shortDescr = Nothing, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing, contactDomain = Nothing} where pick :: [a] -> IO a pick xs = (xs !!) <$> randomRIO (0, length xs - 1) diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 5395520393..80c928567e 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -112,7 +112,7 @@ getConnectionEntity db cxt user@User {userId, userContactId} agentConnId = do db [sql| SELECT - c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, + c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.welcome_shared_msg_id, c.request_shared_msg_id, c.contact_request_id, c.contact_group_member_id, c.contact_grp_inv_sent, c.grp_direct_inv_link, c.grp_direct_inv_from_group_id, c.grp_direct_inv_from_group_member_id, c.grp_direct_inv_from_member_conn_id, c.grp_direct_inv_started_connection, c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl, @@ -124,8 +124,8 @@ getConnectionEntity db cxt user@User {userId, userContactId} agentConnId = do |] (userId, contactId, CSActive) toContact' :: UTCTime -> Int64 -> Connection -> [ChatTagId] -> ContactRow' -> Contact - toContact' currentTs contactId conn chatTags ((profileId, localDisplayName, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow :. domainRow) = - let profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge currentTs badgeRow, preferences, localAlias} + toContact' currentTs contactId conn chatTags ((profileId, localDisplayName, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow :. domainRow) = + let profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge currentTs badgeRow, preferences, localAlias} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn activeConn = Just conn @@ -156,13 +156,13 @@ getConnectionEntity db cxt user@User {userId, userContactId} agentConnId = do mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} - pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, + pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at, -- from GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at diff --git a/src/Simplex/Chat/Store/ContactRequest.hs b/src/Simplex/Chat/Store/ContactRequest.hs index 3bd481d7db..75652349b5 100644 --- a/src/Simplex/Chat/Store/ContactRequest.hs +++ b/src/Simplex/Chat/Store/ContactRequest.hs @@ -73,7 +73,7 @@ createOrUpdateContactRequest isSimplexTeam invId cReqChatVRange@(VersionRange minV maxV) - profile@Profile {displayName, fullName, shortDescr, image, contactLink, badge, preferences} + profile@Profile {displayName, fullName, shortDescr, description, image, contactLink, badge, preferences} xContactId_ welcomeMsgId_ requestMsg_ @@ -112,7 +112,7 @@ createOrUpdateContactRequest [sql| SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, @@ -148,7 +148,7 @@ createOrUpdateContactRequest SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, @@ -168,8 +168,8 @@ createOrUpdateContactRequest liftIO $ DB.execute db - "INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" - ((displayName, fullName, shortDescr, image, contactLink, userId) :. ("" :: LocalAlias, preferences, currentTs, currentTs) :. badgeToRow badge badgeVerified) + "INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" + ((displayName, fullName, shortDescr, description, image, contactLink, userId) :. ("" :: LocalAlias, preferences, currentTs, currentTs) :. badgeToRow badge badgeVerified) profileId <- liftIO $ insertedRowId db liftIO $ DB.execute @@ -238,6 +238,7 @@ createOrUpdateContactRequest SET display_name = ?, full_name = ?, short_descr = ?, + description = ?, image = ?, contact_link = ?, updated_at = ?, @@ -257,7 +258,7 @@ createOrUpdateContactRequest AND contact_request_id = ? ) |] - ((displayName, fullName, shortDescr, image, contactLink, currentTs) :. badgeToRow badge badgeVerified :. (userId, contactRequestId)) + ((displayName, fullName, shortDescr, description, image, contactLink, currentTs) :. badgeToRow badge badgeVerified :. (userId, contactRequestId)) updateRequest currentTs = if displayName == oldDisplayName then diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 71e5c80a63..723b12d448 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -321,7 +321,7 @@ getContactByConnReqHash db cxt user@User {userId} cReqHash1 cReqHash2 = do [sql| SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, @@ -731,17 +731,17 @@ updateContactProfile_ db userId profileId profile badgeVerified = do updateContactProfile_' db userId profileId profile badgeVerified currentTs updateContactProfile_' :: DB.Connection -> UserId -> ProfileId -> Profile -> Maybe Bool -> UTCTime -> IO () -updateContactProfile_' db userId profileId Profile {displayName, fullName, shortDescr, image, contactLink, contactDomain, preferences, peerType, badge} badgeVerified updatedAt = +updateContactProfile_' db userId profileId Profile {displayName, fullName, shortDescr, description, image, contactLink, contactDomain, preferences, peerType, badge} badgeVerified updatedAt = DB.execute db [sql| UPDATE contact_profiles - SET display_name = ?, full_name = ?, short_descr = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ?, + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ?, badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, contact_domain = ?, contact_domain_proof = ? WHERE user_id = ? AND contact_profile_id = ? |] - ((displayName, fullName, shortDescr, image, contactLink, preferences, peerType, updatedAt) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain :. (userId, profileId)) + ((displayName, fullName, shortDescr, description, image, contactLink, preferences, peerType, updatedAt) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain :. (userId, profileId)) -- update only member profile fields (when member doesn't have associated contact - we can reset contactLink and prefs) updateMemberContactProfileReset_ :: DB.Connection -> UserId -> ProfileId -> Profile -> Maybe Bool -> IO () @@ -750,17 +750,17 @@ updateMemberContactProfileReset_ db userId profileId profile badgeVerified = do updateMemberContactProfileReset_' db userId profileId profile badgeVerified currentTs updateMemberContactProfileReset_' :: DB.Connection -> UserId -> ProfileId -> Profile -> Maybe Bool -> UTCTime -> IO () -updateMemberContactProfileReset_' db userId profileId Profile {displayName, fullName, shortDescr, image, contactDomain, badge} badgeVerified updatedAt = +updateMemberContactProfileReset_' db userId profileId Profile {displayName, fullName, shortDescr, description, image, contactDomain, badge} badgeVerified updatedAt = DB.execute db [sql| UPDATE contact_profiles - SET display_name = ?, full_name = ?, short_descr = ?, image = ?, contact_link = NULL, preferences = NULL, updated_at = ?, + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = NULL, preferences = NULL, updated_at = ?, badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, contact_domain = ?, contact_domain_proof = ? WHERE user_id = ? AND contact_profile_id = ? |] - ((displayName, fullName, shortDescr, image, updatedAt) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain :. (userId, profileId)) + ((displayName, fullName, shortDescr, description, image, updatedAt) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain :. (userId, profileId)) -- update only member profile fields (when member has associated contact - we keep contactLink and prefs) updateMemberContactProfile_ :: DB.Connection -> UserId -> ProfileId -> Profile -> Maybe Bool -> IO () @@ -769,17 +769,17 @@ updateMemberContactProfile_ db userId profileId profile badgeVerified = do updateMemberContactProfile_' db userId profileId profile badgeVerified currentTs updateMemberContactProfile_' :: DB.Connection -> UserId -> ProfileId -> Profile -> Maybe Bool -> UTCTime -> IO () -updateMemberContactProfile_' db userId profileId Profile {displayName, fullName, shortDescr, image, contactDomain, badge} badgeVerified updatedAt = +updateMemberContactProfile_' db userId profileId Profile {displayName, fullName, shortDescr, description, image, contactDomain, badge} badgeVerified updatedAt = DB.execute db [sql| UPDATE contact_profiles - SET display_name = ?, full_name = ?, short_descr = ?, image = ?, updated_at = ?, + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, updated_at = ?, badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, contact_domain = ?, contact_domain_proof = ? WHERE user_id = ? AND contact_profile_id = ? |] - ((displayName, fullName, shortDescr, image, updatedAt) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain :. (userId, profileId)) + ((displayName, fullName, shortDescr, description, image, updatedAt) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain :. (userId, profileId)) updateContactLDN_ :: DB.Connection -> User -> Int64 -> ContactName -> ContactName -> UTCTime -> IO () updateContactLDN_ db user@User {userId} contactId displayName newName updatedAt = do @@ -849,7 +849,7 @@ contactRequestQuery = SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, @@ -970,7 +970,7 @@ getContact_ db cxt user@User {userId} contactId deleted = do [sql| SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 402b3cac03..d607541bda 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -260,11 +260,11 @@ import Database.SQLite.Simple (Only (..), Query, (:.) (..)) import Database.SQLite.Simple.QQ (sql) #endif -type MaybeGroupMemberRow = (Maybe GroupMemberId, Maybe GroupId, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId) :. ((Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe LocalAlias, Maybe Preferences) :. BadgeRow :. ContactDomainRow) :. (Maybe UTCTime, Maybe UTCTime) :. (Maybe UTCTime, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact, Maybe Text, Maybe UTCTime) +type MaybeGroupMemberRow = (Maybe GroupMemberId, Maybe GroupId, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId) :. ((Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe LocalAlias, Maybe Preferences) :. BadgeRow :. ContactDomainRow) :. (Maybe UTCTime, Maybe UTCTime) :. (Maybe UTCTime, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact, Maybe Text, Maybe UTCTime) toMaybeGroupMember :: UTCTime -> Int64 -> MaybeGroupMemberRow -> Maybe GroupMember -toMaybeGroupMember now userContactId ((Just groupMemberId, Just groupId, Just indexInGroup, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId) :. ((Just profileId, Just displayName, Just fullName, shortDescr, image, contactLink, peerType, Just localAlias, contactPreferences) :. badgeRow :. domainRow) :. (Just createdAt, Just updatedAt) :. (supportChatTs, Just supportChatUnread, Just supportChatUnanswered, Just supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink, memberCode_, memberCodeVerifiedAt_)) = - Just $ toGroupMember now userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. ((profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, contactPreferences) :. badgeRow :. domainRow) :. (createdAt, updatedAt) :. (supportChatTs, supportChatUnread, supportChatUnanswered, supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink, memberCode_, memberCodeVerifiedAt_)) +toMaybeGroupMember now userContactId ((Just groupMemberId, Just groupId, Just indexInGroup, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId) :. ((Just profileId, Just displayName, Just fullName, shortDescr, description, image, contactLink, peerType, Just localAlias, contactPreferences) :. badgeRow :. domainRow) :. (Just createdAt, Just updatedAt) :. (supportChatTs, Just supportChatUnread, Just supportChatUnanswered, Just supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink, memberCode_, memberCodeVerifiedAt_)) = + Just $ toGroupMember now userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. ((profileId, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, contactPreferences) :. badgeRow :. domainRow) :. (createdAt, updatedAt) :. (supportChatTs, supportChatUnread, supportChatUnanswered, supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink, memberCode_, memberCodeVerifiedAt_)) toMaybeGroupMember _ _ _ = Nothing createGroupLink :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> ConnId -> CreatedLinkContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO GroupLink @@ -2073,7 +2073,7 @@ createJoiningMember User {userId, userContactId} GroupInfo {groupId, membership} cReqChatVRange - Profile {displayName, fullName, shortDescr, image, contactLink, badge, preferences} + Profile {displayName, fullName, shortDescr, description, image, contactLink, badge, preferences} cReqXContactId_ cReqMemberId_ welcomeMsgId_ @@ -2086,8 +2086,8 @@ createJoiningMember liftIO $ DB.execute db - "INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, user_id, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" - ((displayName, fullName, shortDescr, image, contactLink, userId, preferences, currentTs, currentTs) :. badgeToRow badge badgeVerified) + "INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, user_id, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" + ((displayName, fullName, shortDescr, description, image, contactLink, userId, preferences, currentTs, currentTs) :. badgeToRow badge badgeVerified) profileId <- liftIO $ insertedRowId db case cReqMemberId_ of Just memberId -> do @@ -2454,13 +2454,13 @@ createNewGroupMember db cxt user gInfo invitingMember memInfo@MemberInfo {profil createNewMember_ db user gInfo newMember badgeVerified currentTs createNewMemberProfile_ :: DB.Connection -> StoreCxt -> User -> Profile -> UTCTime -> ExceptT StoreError IO (Text, ProfileId, Maybe Bool) -createNewMemberProfile_ db cxt User {userId} Profile {displayName, fullName, shortDescr, image, contactLink, badge, preferences} createdAt = +createNewMemberProfile_ db cxt User {userId} Profile {displayName, fullName, shortDescr, description, image, contactLink, badge, preferences} createdAt = ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do badgeVerified <- verifyBadge_ (badgeKeys cxt) badge DB.execute db - "INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, user_id, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" - ((displayName, fullName, shortDescr, image, contactLink, userId, preferences, createdAt, createdAt) :. badgeToRow badge badgeVerified) + "INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, user_id, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" + ((displayName, fullName, shortDescr, description, image, contactLink, userId, preferences, createdAt, createdAt) :. badgeToRow badge badgeVerified) profileId <- insertedRowId db pure $ Right (ldn, profileId, badgeVerified) @@ -2798,10 +2798,10 @@ updateGroupPreferences db User {userId} g@GroupInfo {groupId, groupProfile = p} pure (g :: GroupInfo) {groupProfile = p {groupPreferences = Just ps}, fullGroupPreferences = mergeGroupPreferences $ Just ps} updateGroupProfileFromMember :: DB.Connection -> User -> GroupInfo -> Profile -> ExceptT StoreError IO GroupInfo -updateGroupProfileFromMember db user g@GroupInfo {groupId} Profile {displayName = n, fullName = fn, shortDescr = sd, image = img} = do +updateGroupProfileFromMember db user g@GroupInfo {groupId} Profile {displayName = n, fullName = fn, shortDescr = sd, description = descr, image = img} = do p <- getGroupProfile -- to avoid any race conditions with UI let g' = g {groupProfile = p} :: GroupInfo - p' = p {displayName = n, fullName = fn, shortDescr = sd, image = img} :: GroupProfile + p' = p {displayName = n, fullName = fn, shortDescr = sd, description = descr, image = img} :: GroupProfile updateGroupProfile db user g' p' where getGroupProfile = diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index b4f9a7ff86..1cf350a79b 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -714,7 +714,7 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe -- GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, - p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at @@ -1135,7 +1135,7 @@ getContactRequestChatPreviews_ db User {userId} pagination clq = do SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, @@ -3069,7 +3069,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do -- GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, - p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -3078,14 +3078,14 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do -- quoted GroupMember rm.group_member_id, rm.group_id, rm.index_in_group, rm.member_id, rm.peer_chat_min_version, rm.peer_chat_max_version, rm.member_role, rm.member_category, rm.member_status, rm.show_messages, rm.member_restriction, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, - rp.display_name, rp.full_name, rp.short_descr, rp.image, rp.contact_link, rp.chat_peer_type, rp.local_alias, rp.preferences, + rp.display_name, rp.full_name, rp.short_descr, rp.description, rp.image, rp.contact_link, rp.chat_peer_type, rp.local_alias, rp.preferences, rp.badge_proof, rp.badge_pres_header, rp.badge_expiry, rp.badge_type, rp.badge_verified, rp.badge_extra, rp.badge_master_key, rp.badge_signature, rp.badge_key_idx, rp.contact_domain, rp.contact_domain_proof, rp.contact_domain_verified, rm.created_at, rm.updated_at, rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, rm.member_pub_key, rm.relay_link, rm.member_security_code, rm.member_security_code_verified_at, -- deleted by GroupMember dbm.group_member_id, dbm.group_id, dbm.index_in_group, dbm.member_id, dbm.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category, dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, - dbp.display_name, dbp.full_name, dbp.short_descr, dbp.image, dbp.contact_link, dbp.chat_peer_type, dbp.local_alias, dbp.preferences, + dbp.display_name, dbp.full_name, dbp.short_descr, dbp.description, dbp.image, dbp.contact_link, dbp.chat_peer_type, dbp.local_alias, dbp.preferences, dbp.badge_proof, dbp.badge_pres_header, dbp.badge_expiry, dbp.badge_type, dbp.badge_verified, dbp.badge_extra, dbp.badge_master_key, dbp.badge_signature, dbp.badge_key_idx, dbp.contact_domain, dbp.contact_domain_proof, dbp.contact_domain_verified, dbm.created_at, dbm.updated_at, dbm.support_chat_ts, dbm.support_chat_items_unread, dbm.support_chat_items_member_attention, dbm.support_chat_items_mentions, dbm.support_chat_last_msg_from_member_ts, dbm.member_pub_key, dbm.relay_link, dbm.member_security_code, dbm.member_security_code_verified_at diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index 1f2314fbb0..95fa4e1095 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -42,6 +42,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260603_simplex_name import Simplex.Chat.Store.Postgres.Migrations.M20260629_roster_catchup import Simplex.Chat.Store.Postgres.Migrations.M20260707_file_digest import Simplex.Chat.Store.Postgres.Migrations.M20260714_member_security_code +import Simplex.Chat.Store.Postgres.Migrations.M20260715_profile_description import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] @@ -83,7 +84,8 @@ schemaMigrations = ("20260603_simplex_name", m20260603_simplex_name, Just down_m20260603_simplex_name), ("20260629_roster_catchup", m20260629_roster_catchup, Just down_m20260629_roster_catchup), ("20260707_file_digest", m20260707_file_digest, Just down_m20260707_file_digest), - ("20260714_member_security_code", m20260714_member_security_code, Just down_m20260714_member_security_code) + ("20260714_member_security_code", m20260714_member_security_code, Just down_m20260714_member_security_code), + ("20260715_profile_description", m20260715_profile_description, Just down_m20260715_profile_description) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260715_profile_description.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260715_profile_description.hs new file mode 100644 index 0000000000..d27636e826 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260715_profile_description.hs @@ -0,0 +1,21 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260715_profile_description where + +import Data.Text (Text) +import qualified Data.Text as T +import Text.RawString.QQ (r) + +m20260715_profile_description :: Text +m20260715_profile_description = + T.pack + [r| +ALTER TABLE contact_profiles ADD COLUMN description TEXT; +|] + +down_m20260715_profile_description :: Text +down_m20260715_profile_description = + T.pack + [r| +ALTER TABLE contact_profiles DROP COLUMN description; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index f33a734777..21e77fd4f6 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -543,7 +543,8 @@ CREATE TABLE test_chat_schema.contact_profiles ( badge_key_idx bigint, contact_domain text, contact_domain_proof text, - contact_domain_verified smallint + contact_domain_verified smallint, + description text ); @@ -988,7 +989,7 @@ CREATE TABLE test_chat_schema.groups ( public_member_count bigint, relay_request_retries bigint DEFAULT 0 NOT NULL, relay_request_delay bigint DEFAULT 0 NOT NULL, - relay_request_execute_at timestamp with time zone DEFAULT '1970-01-01 04:00:00+04'::timestamp with time zone NOT NULL, + relay_request_execute_at timestamp with time zone DEFAULT '1970-01-01 01:00:00+01'::timestamp with time zone NOT NULL, relay_inactive_at timestamp with time zone, relay_sent_web_domain text, roster_version bigint, diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 522f966214..19120bca27 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -133,7 +133,7 @@ import Database.SQLite.Simple.QQ (sql) #endif createUserRecordAt :: DB.Connection -> AgentUserId -> Bool -> Bool -> Profile -> Bool -> UTCTime -> ExceptT StoreError IO User -createUserRecordAt db (AgentUserId auId) userChatRelay clientService Profile {displayName, fullName, shortDescr, image, peerType, preferences = userPreferences} activeUser currentTs = +createUserRecordAt db (AgentUserId auId) userChatRelay clientService Profile {displayName, fullName, shortDescr, description, image, peerType, preferences = userPreferences} activeUser currentTs = checkConstraint SEDuplicateName . liftIO $ do when activeUser $ DB.execute_ db "UPDATE users SET active_user = 0" let showNtfs = True @@ -154,8 +154,8 @@ createUserRecordAt db (AgentUserId auId) userChatRelay clientService Profile {di (displayName, displayName, userId, currentTs, currentTs) DB.execute db - "INSERT INTO contact_profiles (display_name, full_name, short_descr, image, chat_peer_type, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" - (displayName, fullName, shortDescr, image, peerType, userId, userPreferences, currentTs, currentTs) + "INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, chat_peer_type, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)" + (displayName, fullName, shortDescr, description, image, peerType, userId, userPreferences, currentTs, currentTs) profileId <- insertedRowId db DB.execute db @@ -163,7 +163,7 @@ createUserRecordAt db (AgentUserId auId) userChatRelay clientService Profile {di (profileId, displayName, userId, BI True, currentTs, currentTs, currentTs) contactId <- insertedRowId db DB.execute db "UPDATE users SET contact_id = ? WHERE user_id = ?" (contactId, userId) - pure $ toUser currentTs $ (userId, auId, contactId, profileId, BI activeUser, order) :. (displayName, fullName, shortDescr, image, Nothing, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, Nothing, Nothing, Nothing, BI userChatRelay, BI clientService, Nothing) :. localBadgeToRow Nothing :. (Nothing, Nothing, Nothing) + pure $ toUser currentTs $ (userId, auId, contactId, profileId, BI activeUser, order) :. (displayName, fullName, shortDescr, description, image, Nothing, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, Nothing, Nothing, Nothing, BI userChatRelay, BI clientService, Nothing) :. localBadgeToRow Nothing :. (Nothing, Nothing, Nothing) -- TODO [mentions] getUsersInfo :: DB.Connection -> IO [UserInfo] @@ -353,22 +353,22 @@ updateUserProfile db user p' DB.execute db "UPDATE users SET user_member_profile_updated_at = ? WHERE user_id = ?" (currentTs, userId) pure $ Just currentTs | otherwise = pure userMemberProfileUpdatedAt - userMemberProfileChanged = newName /= displayName || fn' /= fullName || d' /= shortDescr || img' /= image - User {userId, userContactId, localDisplayName, profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, localBadge, localAlias}, userMemberProfileUpdatedAt} = user - Profile {displayName = newName, fullName = fn', shortDescr = d', image = img', preferences} = p' + userMemberProfileChanged = newName /= displayName || fn' /= fullName || d' /= shortDescr || desc' /= description || img' /= image + User {userId, userContactId, localDisplayName, profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, localBadge, localAlias}, userMemberProfileUpdatedAt} = user + Profile {displayName = newName, fullName = fn', shortDescr = d', description = desc', image = img', preferences} = p' fullPreferences = fullPreferences' preferences -- own profile field update; leaves the badge columns alone (the credential is owned by setUserBadge/addUserBadge) updateUserProfileFields_' :: DB.Connection -> UserId -> ProfileId -> Profile -> UTCTime -> IO () -updateUserProfileFields_' db userId profileId Profile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType} updatedAt = +updateUserProfileFields_' db userId profileId Profile {displayName, fullName, shortDescr, description, image, contactLink, preferences, peerType} updatedAt = DB.execute db [sql| UPDATE contact_profiles - SET display_name = ?, full_name = ?, short_descr = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ? + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ? WHERE user_id = ? AND contact_profile_id = ? |] - ((displayName, fullName, shortDescr, image, contactLink, preferences, peerType, updatedAt) :. (userId, profileId)) + ((displayName, fullName, shortDescr, description, image, contactLink, preferences, peerType, updatedAt) :. (userId, profileId)) -- store the user's own badge credential; touches only the badge columns. -- bumps user_member_profile_updated_at so groups receive the updated profile (with the badge) on the next message. @@ -417,14 +417,14 @@ getUserContactProfiles db User {userId} = <$> DB.query db [sql| - SELECT display_name, full_name, short_descr, image, contact_link, chat_peer_type, contact_domain, preferences + SELECT display_name, full_name, short_descr, description, image, contact_link, chat_peer_type, contact_domain, preferences FROM contact_profiles WHERE user_id = ? |] (Only userId) where - toContactProfile :: (ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe SimplexDomain, Maybe Preferences) -> Profile - toContactProfile (displayName, fullName, shortDescr, image, contactLink, peerType, domain_, preferences) = Profile {displayName, fullName, shortDescr, image, contactLink, contactDomain = mkDomainClaim <$> domain_, peerType, preferences, badge = Nothing} + toContactProfile :: (ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe SimplexDomain, Maybe Preferences) -> Profile + toContactProfile (displayName, fullName, shortDescr, description, image, contactLink, peerType, domain_, preferences) = Profile {displayName, fullName, shortDescr, description, image, contactLink, contactDomain = mkDomainClaim <$> domain_, peerType, preferences, badge = Nothing} createUserContactLink :: DB.Connection -> User -> ConnId -> CreatedLinkContact -> SubscriptionMode -> C.PrivateKeyEd25519 -> ExceptT StoreError IO () createUserContactLink db User {userId} agentConnId (CCLink cReq shortLink) subMode linkPrivSigKey = diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index a2091e3b80..998fbbcd1b 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -165,6 +165,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260603_simplex_name import Simplex.Chat.Store.SQLite.Migrations.M20260629_roster_catchup import Simplex.Chat.Store.SQLite.Migrations.M20260707_file_digest import Simplex.Chat.Store.SQLite.Migrations.M20260714_member_security_code +import Simplex.Chat.Store.SQLite.Migrations.M20260715_profile_description import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -329,7 +330,8 @@ schemaMigrations = ("20260603_simplex_name", m20260603_simplex_name, Just down_m20260603_simplex_name), ("20260629_roster_catchup", m20260629_roster_catchup, Just down_m20260629_roster_catchup), ("20260707_file_digest", m20260707_file_digest, Just down_m20260707_file_digest), - ("20260714_member_security_code", m20260714_member_security_code, Just down_m20260714_member_security_code) + ("20260714_member_security_code", m20260714_member_security_code, Just down_m20260714_member_security_code), + ("20260715_profile_description", m20260715_profile_description, Just down_m20260715_profile_description) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260715_profile_description.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260715_profile_description.hs new file mode 100644 index 0000000000..5966493ca4 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260715_profile_description.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260715_profile_description where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260715_profile_description :: Query +m20260715_profile_description = + [sql| +ALTER TABLE contact_profiles ADD COLUMN description TEXT; +|] + +down_m20260715_profile_description :: Query +down_m20260715_profile_description = + [sql| +ALTER TABLE contact_profiles DROP COLUMN description; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index de7142aec6..719fbf9914 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -121,7 +121,7 @@ Plan: Query: SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, @@ -156,13 +156,13 @@ Query: mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} - pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, + pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at, -- from GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at @@ -397,7 +397,7 @@ Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, @@ -484,6 +484,7 @@ Query: SET display_name = ?, full_name = ?, short_descr = ?, + description = ?, image = ?, contact_link = ?, updated_at = ?, @@ -720,7 +721,7 @@ Plan: Query: SELECT - c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, + c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.welcome_shared_msg_id, c.request_shared_msg_id, c.contact_request_id, c.contact_group_member_id, c.contact_grp_inv_sent, c.grp_direct_inv_link, c.grp_direct_inv_from_group_id, c.grp_direct_inv_from_group_member_id, c.grp_direct_inv_from_member_conn_id, c.grp_direct_inv_started_connection, c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl, @@ -1067,7 +1068,7 @@ Query: -- GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, - p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at @@ -1376,7 +1377,7 @@ Query: -- GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, - p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -1385,14 +1386,14 @@ Query: -- quoted GroupMember rm.group_member_id, rm.group_id, rm.index_in_group, rm.member_id, rm.peer_chat_min_version, rm.peer_chat_max_version, rm.member_role, rm.member_category, rm.member_status, rm.show_messages, rm.member_restriction, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, - rp.display_name, rp.full_name, rp.short_descr, rp.image, rp.contact_link, rp.chat_peer_type, rp.local_alias, rp.preferences, + rp.display_name, rp.full_name, rp.short_descr, rp.description, rp.image, rp.contact_link, rp.chat_peer_type, rp.local_alias, rp.preferences, rp.badge_proof, rp.badge_pres_header, rp.badge_expiry, rp.badge_type, rp.badge_verified, rp.badge_extra, rp.badge_master_key, rp.badge_signature, rp.badge_key_idx, rp.contact_domain, rp.contact_domain_proof, rp.contact_domain_verified, rm.created_at, rm.updated_at, rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, rm.member_pub_key, rm.relay_link, rm.member_security_code, rm.member_security_code_verified_at, -- deleted by GroupMember dbm.group_member_id, dbm.group_id, dbm.index_in_group, dbm.member_id, dbm.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category, dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, - dbp.display_name, dbp.full_name, dbp.short_descr, dbp.image, dbp.contact_link, dbp.chat_peer_type, dbp.local_alias, dbp.preferences, + dbp.display_name, dbp.full_name, dbp.short_descr, dbp.description, dbp.image, dbp.contact_link, dbp.chat_peer_type, dbp.local_alias, dbp.preferences, dbp.badge_proof, dbp.badge_pres_header, dbp.badge_expiry, dbp.badge_type, dbp.badge_verified, dbp.badge_extra, dbp.badge_master_key, dbp.badge_signature, dbp.badge_key_idx, dbp.contact_domain, dbp.contact_domain_proof, dbp.contact_domain_verified, dbm.created_at, dbm.updated_at, dbm.support_chat_ts, dbm.support_chat_items_unread, dbm.support_chat_items_member_attention, dbm.support_chat_items_mentions, dbm.support_chat_last_msg_from_member_ts, dbm.member_pub_key, dbm.relay_link, dbm.member_security_code, dbm.member_security_code_verified_at @@ -1442,7 +1443,7 @@ SEARCH ri USING COVERING INDEX idx_chat_items_direct_shared_msg_id (user_id=? AN Query: SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, @@ -2006,7 +2007,7 @@ Plan: Query: SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, @@ -2093,7 +2094,7 @@ Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, @@ -2123,7 +2124,7 @@ Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, @@ -2153,7 +2154,7 @@ Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, @@ -3687,7 +3688,7 @@ Plan: SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT cp.contact_profile_id, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, cp.preferences, -- , ct.user_preferences + SELECT cp.contact_profile_id, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, cp.preferences, -- , ct.user_preferences cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified FROM contact_profiles cp WHERE cp.user_id = ? AND cp.contact_profile_id = ? @@ -3740,7 +3741,7 @@ SEARCH f USING PRIMARY KEY (file_id=?) SEARCH d USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT display_name, full_name, short_descr, image, contact_link, chat_peer_type, contact_domain, preferences + SELECT display_name, full_name, short_descr, description, image, contact_link, chat_peer_type, contact_domain, preferences FROM contact_profiles WHERE user_id = ? @@ -4836,8 +4837,8 @@ Query: Plan: Query: - INSERT INTO contact_profiles (display_name, full_name, short_descr, image, user_id, incognito, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?) + INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, user_id, incognito, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?) Plan: SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) @@ -5154,7 +5155,7 @@ SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE contact_profiles - SET display_name = ?, full_name = ?, short_descr = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ? + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ? WHERE user_id = ? AND contact_profile_id = ? Plan: @@ -5162,7 +5163,7 @@ SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE contact_profiles - SET display_name = ?, full_name = ?, short_descr = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ?, + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ?, badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, contact_domain = ?, contact_domain_proof = ? WHERE user_id = ? AND contact_profile_id = ? @@ -5172,7 +5173,7 @@ SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE contact_profiles - SET display_name = ?, full_name = ?, short_descr = ?, image = ?, contact_link = NULL, preferences = NULL, updated_at = ?, + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = NULL, preferences = NULL, updated_at = ?, badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, contact_domain = ?, contact_domain_proof = ? WHERE user_id = ? AND contact_profile_id = ? @@ -5182,7 +5183,7 @@ SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE contact_profiles - SET display_name = ?, full_name = ?, short_descr = ?, image = ?, updated_at = ?, + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, updated_at = ?, badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, contact_domain = ?, contact_domain_proof = ? WHERE user_id = ? AND contact_profile_id = ? @@ -5553,7 +5554,7 @@ Query: -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, - pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, + pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at @@ -5591,7 +5592,7 @@ Query: -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, - pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, + pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at @@ -5622,7 +5623,7 @@ Query: -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, - pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, + pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at @@ -5642,7 +5643,7 @@ Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, @@ -5659,7 +5660,7 @@ Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, @@ -5675,7 +5676,7 @@ SEARCH p USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -5703,7 +5704,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -5724,7 +5725,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -5744,7 +5745,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -5764,7 +5765,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -5784,7 +5785,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -5804,7 +5805,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -5824,7 +5825,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -5844,7 +5845,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -5864,7 +5865,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -5884,7 +5885,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -5904,7 +5905,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -5924,7 +5925,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -5944,7 +5945,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -5964,7 +5965,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -6164,7 +6165,7 @@ Plan: SEARCH server_operators USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u @@ -6177,7 +6178,7 @@ SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u @@ -6191,7 +6192,7 @@ SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u @@ -6205,7 +6206,7 @@ SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u @@ -6220,7 +6221,7 @@ SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u @@ -6234,7 +6235,7 @@ SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u @@ -6248,7 +6249,7 @@ SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u @@ -6262,7 +6263,7 @@ SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u @@ -6276,7 +6277,7 @@ SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u @@ -6289,7 +6290,7 @@ SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u @@ -6950,19 +6951,19 @@ Plan: Query: INSERT INTO chat_item_messages (chat_item_id, message_id, created_at, updated_at) VALUES (?,?,?,?) Plan: -Query: INSERT INTO contact_profiles (display_name, full_name, short_descr, image, chat_peer_type, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?) +Query: INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, chat_peer_type, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?) Plan: SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) -Query: INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, chat_peer_type, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx, contact_domain, contact_domain_proof) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) +Query: INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, chat_peer_type, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx, contact_domain, contact_domain_proof) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) -Query: INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) +Query: INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) -Query: INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, user_id, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) +Query: INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, user_id, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 1f7f97ae3e..94eca2e532 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -31,7 +31,8 @@ CREATE TABLE contact_profiles( badge_key_idx INTEGER, contact_domain TEXT, contact_domain_proof TEXT, - contact_domain_verified INTEGER + contact_domain_verified INTEGER, + description TEXT ) STRICT; CREATE TABLE users( user_id INTEGER PRIMARY KEY, diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 15cb80c365..050cf7dc97 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -321,14 +321,14 @@ createConnection_ db userId connType entityId acId connStatus connChatVersion pe ent ct = if connType == ct then entityId else Nothing createIncognitoProfile_ :: DB.Connection -> UserId -> UTCTime -> Profile -> IO Int64 -createIncognitoProfile_ db userId createdAt Profile {displayName, fullName, shortDescr, image} = do +createIncognitoProfile_ db userId createdAt Profile {displayName, fullName, shortDescr, description, image} = do DB.execute db [sql| - INSERT INTO contact_profiles (display_name, full_name, short_descr, image, user_id, incognito, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?) + INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, user_id, incognito, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?) |] - (displayName, fullName, shortDescr, image, userId, Just (BI True), createdAt, createdAt) + (displayName, fullName, shortDescr, description, image, userId, Just (BI True), createdAt, createdAt) insertedRowId db updateConnSupportPQ :: DB.Connection -> Int64 -> PQSupport -> PQEncryption -> IO () @@ -415,13 +415,13 @@ createContact db cxt user profile = do void $ createContact_ db cxt user profile emptyChatPrefs Nothing "" currentTs createContact_ :: DB.Connection -> StoreCxt -> User -> Profile -> Preferences -> Maybe (ACreatedConnLink, Maybe SharedMsgId) -> LocalAlias -> UTCTime -> ExceptT StoreError IO ContactId -createContact_ db cxt User {userId} Profile {displayName, fullName, shortDescr, image, contactLink, contactDomain, peerType, badge, preferences} ctUserPreferences prepared localAlias currentTs = +createContact_ db cxt User {userId} Profile {displayName, fullName, shortDescr, description, image, contactLink, contactDomain, peerType, badge, preferences} ctUserPreferences prepared localAlias currentTs = ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do badgeVerified <- verifyBadge_ (badgeKeys cxt) badge DB.execute db - "INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, chat_peer_type, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx, contact_domain, contact_domain_proof) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" - ((displayName, fullName, shortDescr, image, contactLink, peerType) :. (userId, localAlias, preferences, currentTs, currentTs) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain) + "INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, chat_peer_type, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx, contact_domain, contact_domain_proof) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" + ((displayName, fullName, shortDescr, description, image, contactLink, peerType) :. (userId, localAlias, preferences, currentTs, currentTs) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain) profileId <- insertedRowId db DB.execute db @@ -488,15 +488,15 @@ type PreparedContactRow = (Maybe AConnectionRequestUri, Maybe AConnShortLink, Ma type GroupDirectInvitationRow = (Maybe ConnReqInvitation, Maybe GroupId, Maybe GroupMemberId, Maybe Int64, BoolInt) -type ContactRow' = (ProfileId, ContactName, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. PreparedContactRow :. (Maybe Int64, Maybe GroupMemberId, BoolInt) :. GroupDirectInvitationRow :. (Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64) :. BadgeRow :. ContactDomainRow +type ContactRow' = (ProfileId, ContactName, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. PreparedContactRow :. (Maybe Int64, Maybe GroupMemberId, BoolInt) :. GroupDirectInvitationRow :. (Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64) :. BadgeRow :. ContactDomainRow type ContactRow = Only ContactId :. ContactRow' type ContactDomainRow = (Maybe SimplexDomain, Maybe SimplexDomainProof, Maybe BoolInt) toContact :: UTCTime -> StoreCxt -> User -> [ChatTagId] -> ContactRow :. MaybeConnectionRow -> Contact -toContact now cxt user chatTags ((Only contactId :. (profileId, localDisplayName, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow :. domainRow) :. connRow) = - let profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge now badgeRow, preferences, localAlias} +toContact now cxt user chatTags ((Only contactId :. (profileId, localDisplayName, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow :. domainRow) :. connRow) = + let profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge now badgeRow, preferences, localAlias} activeConn = toMaybeConnection cxt connRow chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} incognito = maybe False connIncognito activeConn @@ -537,25 +537,25 @@ getProfileById db userId profileId = do DB.query db [sql| - SELECT cp.contact_profile_id, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, cp.preferences, -- , ct.user_preferences + SELECT cp.contact_profile_id, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, cp.preferences, -- , ct.user_preferences cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified FROM contact_profiles cp WHERE cp.user_id = ? AND cp.contact_profile_id = ? |] (userId, profileId) -type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Maybe GroupId, Maybe Int64) :. (Int64, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias) :. (Maybe XContactId, PQSupport, Maybe SharedMsgId, Maybe SharedMsgId, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat) :. BadgeRow :. ContactDomainRow +type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Maybe GroupId, Maybe Int64) :. (Int64, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias) :. (Maybe XContactId, PQSupport, Maybe SharedMsgId, Maybe SharedMsgId, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat) :. BadgeRow :. ContactDomainRow toContactRequest :: UTCTime -> ContactRequestRow -> UserContactRequest -toContactRequest now ((contactRequestId, localDisplayName, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_) :. (profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias) :. (xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, preferences, createdAt, updatedAt, minVer, maxVer) :. badgeRow :. domainRow) = do - let profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, preferences, localBadge = rowToBadge now badgeRow, localAlias} +toContactRequest now ((contactRequestId, localDisplayName, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_) :. (profileId, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias) :. (xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, preferences, createdAt, updatedAt, minVer, maxVer) :. badgeRow :. domainRow) = do + let profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, preferences, localBadge = rowToBadge now badgeRow, localAlias} cReqChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer in UserContactRequest {contactRequestId, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_, cReqChatVRange, localDisplayName, profileId, profile, xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, createdAt, updatedAt} userQuery :: Query userQuery = [sql| - SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u @@ -563,11 +563,11 @@ userQuery = JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id |] -toUser :: UTCTime -> (UserId, UserId, ContactId, ProfileId, BoolInt, Int64) :. (ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe Preferences) :. (BoolInt, BoolInt, BoolInt, BoolInt, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime, BoolInt, BoolInt, Maybe UIThemeEntityOverrides) :. BadgeRow :. ContactDomainRow -> User -toUser now ((userId, auId, userContactId, profileId, BI activeUser, activeOrder) :. (displayName, fullName, shortDescr, image, contactLink, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt, BI userChatRelay, BI clientService, uiThemes) :. badgeRow :. domainRow) = +toUser :: UTCTime -> (UserId, UserId, ContactId, ProfileId, BoolInt, Int64) :. (ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe Preferences) :. (BoolInt, BoolInt, BoolInt, BoolInt, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime, BoolInt, BoolInt, Maybe UIThemeEntityOverrides) :. BadgeRow :. ContactDomainRow -> User +toUser now ((userId, auId, userContactId, profileId, BI activeUser, activeOrder) :. (displayName, fullName, shortDescr, description, image, contactLink, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt, BI userChatRelay, BI clientService, uiThemes) :. badgeRow :. domainRow) = User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, activeOrder, fullPreferences, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, autoAcceptMemberContacts, viewPwdHash, userMemberProfileUpdatedAt, userChatRelay = BoolDef userChatRelay, clientService = BoolDef clientService, uiThemes} where - profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge now badgeRow, preferences = userPreferences, localAlias = ""} + profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge now badgeRow, preferences = userPreferences, localAlias = ""} fullPreferences = fullPreferences' userPreferences viewPwdHash = UserPwdHash <$> viewPwdHash_ <*> viewPwdSalt_ @@ -689,7 +689,7 @@ type PublicGroupAccessRow = (Maybe Text, Maybe SimplexDomain, Maybe BoolInt, May type GroupMemberRow = (GroupMemberId, GroupId, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId) :. ProfileRow :. (UTCTime, UTCTime) :. (Maybe UTCTime, Int64, Int64, Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact, Maybe Text, Maybe UTCTime) -type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, Maybe Preferences) :. BadgeRow :. ContactDomainRow +type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, Maybe Preferences) :. BadgeRow :. ContactDomainRow toGroupInfo :: UTCTime -> StoreCxt -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo toGroupInfo now cxt userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, rosterVersion, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri, groupDomainVerified) :. groupKeysRow :. userMemberRow) = @@ -763,7 +763,7 @@ groupMemberQuery = [sql| SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -781,8 +781,8 @@ toContactMember now cxt User {userContactId} (memberRow :. connRow) = (toGroupMember now userContactId memberRow) {activeConn = toMaybeConnection cxt connRow} rowToLocalProfile :: UTCTime -> ProfileRow -> LocalProfile -rowToLocalProfile now ((profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, preferences) :. badgeRow :. domainRow) = - LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge now badgeRow, localAlias, preferences} +rowToLocalProfile now ((profileId, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, preferences) :. badgeRow :. domainRow) = + LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge now badgeRow, localAlias, preferences} toBusinessChatInfo :: Maybe SimplexDomainClaim -> BusinessChatInfoRow -> Maybe BusinessChatInfo toBusinessChatInfo businessDomain (Just chatType, Just businessId, Just customerId) = Just BusinessChatInfo {chatType, businessId, customerId, businessDomain} @@ -808,7 +808,7 @@ groupInfoQueryFields = -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, - pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, + pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 5754f9bea8..dce1a2a9c9 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -694,6 +694,7 @@ data Profile = Profile { displayName :: ContactName, fullName :: Text, shortDescr :: Maybe Text, -- short description limited to 160 characters + description :: Maybe Text, -- long description (businesses/bots); redacted per group policy in member profiles image :: Maybe ImageData, contactLink :: Maybe ConnLinkContact, preferences :: Maybe Preferences, @@ -732,14 +733,14 @@ instance TextEncoding ChatPeerType where profileFromName :: ContactName -> Profile profileFromName displayName = - Profile {displayName, fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, preferences = Nothing, peerType = Nothing, badge = Nothing, contactDomain = Nothing} + Profile {displayName, fullName = "", shortDescr = Nothing, description = Nothing, image = Nothing, contactLink = Nothing, preferences = Nothing, peerType = Nothing, badge = Nothing, contactDomain = Nothing} -- check if profiles match ignoring preferences profilesMatch :: LocalProfile -> LocalProfile -> Bool profilesMatch - LocalProfile {displayName = n1, fullName = fn1, image = i1, shortDescr = d1} - LocalProfile {displayName = n2, fullName = fn2, image = i2, shortDescr = d2} = - n1 == n2 && fn1 == fn2 && i1 == i2 && d1 == d2 + LocalProfile {displayName = n1, fullName = fn1, image = i1, shortDescr = d1, description = desc1} + LocalProfile {displayName = n2, fullName = fn2, image = i2, shortDescr = d2, description = desc2} = + n1 == n2 && fn1 == fn2 && i1 == i2 && d1 == d2 && desc1 == desc2 -- equal for profile-update detection: badge proofs are re-generated for every presentation, -- so compare badges by disclosed info (not proof bytes) - a re-presentation of the same badge is a no-op @@ -778,6 +779,7 @@ data LocalProfile = LocalProfile displayName :: ContactName, fullName :: Text, shortDescr :: Maybe Text, + description :: Maybe Text, image :: Maybe ImageData, contactLink :: Maybe ConnLinkContact, preferences :: Maybe Preferences, @@ -793,15 +795,15 @@ localProfileId :: LocalProfile -> ProfileId localProfileId LocalProfile {profileId} = profileId toLocalProfile :: ProfileId -> Profile -> LocalAlias -> UTCTime -> Maybe Bool -> Maybe Bool -> LocalProfile -toLocalProfile profileId Profile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, badge, contactDomain} localAlias now badgeVerified contactDomainVerified = - LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, preferences, peerType, localBadge, localAlias, contactDomain, contactDomainVerified} +toLocalProfile profileId Profile {displayName, fullName, shortDescr, description, image, contactLink, preferences, peerType, badge, contactDomain} localAlias now badgeVerified contactDomainVerified = + LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, preferences, peerType, localBadge, localAlias, contactDomain, contactDomainVerified} where localBadge = (\b@(BadgeProof _ _ _ info) -> PeerBadge b (mkBadgeStatus now badgeVerified info)) <$> badge fromLocalProfile :: LocalProfile -> Profile -fromLocalProfile LocalProfile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, localBadge, contactDomain} = +fromLocalProfile LocalProfile {displayName, fullName, shortDescr, description, image, contactLink, preferences, peerType, localBadge, contactDomain} = -- the name proof is re-signed on each send - Profile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, badge = localBadge >>= wireBadge, contactDomain = (\d -> d {proof = Nothing} :: SimplexDomainClaim) <$> contactDomain} + Profile {displayName, fullName, shortDescr, description, image, contactLink, preferences, peerType, badge = localBadge >>= wireBadge, contactDomain = (\d -> d {proof = Nothing} :: SimplexDomainClaim) <$> contactDomain} where wireBadge :: LocalBadge -> Maybe BadgeProof wireBadge = \case diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index afe52d4234..c136a08c7d 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -1815,9 +1815,10 @@ viewContactBadge = maybe [] $ \lb -> in [plain (textEncode badgeType <> " badge - " <> st), plain expiry] viewContactInfo :: Contact -> Maybe ConnectionStats -> Maybe Profile -> [StyledString] -viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink, localBadge, contactDomain, contactDomainVerified}, activeConn, uiThemes, customData} stats incognitoProfile = +viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink, localBadge, contactDomain, contactDomainVerified, description}, activeConn, uiThemes, customData} stats incognitoProfile = ["contact ID: " <> sShow contactId] <> viewContactBadge localBadge + <> maybe [] ((bold' "description:" :) . map plain . T.lines) description <> maybe [] viewConnectionStats stats <> maybe [] (\l -> ["contact address: " <> plain (strEncode (simplexChatContact' l))]) contactLink <> simplexDomainLine NTContact contactDomain contactDomainVerified @@ -1856,11 +1857,12 @@ viewCustomData :: Maybe CustomData -> [StyledString] viewCustomData = maybe [] (\(CustomData v) -> ["custom data: " <> viewJSON (J.Object v)]) viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> [StyledString] -viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias, contactLink, localBadge}, activeConn} stats = +viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias, contactLink, localBadge, description}, activeConn} stats = [ "group ID: " <> sShow groupId, "member ID: " <> sShow groupMemberId ] <> viewContactBadge localBadge + <> maybe [] ((bold' "description:" :) . map plain . T.lines) description <> maybe ["member not connected"] viewConnectionStats stats <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact' l)]) contactLink <> ["alias: " <> plain localAlias | localAlias /= ""] @@ -1959,14 +1961,15 @@ viewSwitchPhase = \case SPCompleted -> "changed address" viewUserProfileUpdated :: Profile -> Profile -> UserProfileUpdateSummary -> [StyledString] -viewUserProfileUpdated Profile {displayName = n, fullName, shortDescr, image, contactLink, preferences} Profile {displayName = n', fullName = fullName', shortDescr = shortDescr', image = image', contactLink = contactLink', preferences = prefs'} summary = +viewUserProfileUpdated Profile {displayName = n, fullName, shortDescr, description, image, contactLink, preferences} Profile {displayName = n', fullName = fullName', shortDescr = shortDescr', description = description', image = image', contactLink = contactLink', preferences = prefs'} summary = profileUpdated <> viewPrefsUpdated preferences prefs' where UserProfileUpdateSummary {updateSuccesses = s, updateFailures = f} = summary profileUpdated - | n == n' && fullName == fullName' && shortDescr == shortDescr' && image == image' && contactLink == contactLink' = [] - | n == n' && fullName == fullName' && shortDescr == shortDescr' && image == image' = [if isNothing contactLink' then "contact address removed" else "new contact address set"] - | n == n' && fullName == fullName' && shortDescr == shortDescr' = [if isNothing image' then "profile image removed" else "profile image updated"] + | n == n' && fullName == fullName' && shortDescr == shortDescr' && description == description' && image == image' && contactLink == contactLink' = [] + | n == n' && fullName == fullName' && shortDescr == shortDescr' && description == description' && image == image' = [if isNothing contactLink' then "contact address removed" else "new contact address set"] + | n == n' && fullName == fullName' && shortDescr == shortDescr' && description == description' = [if isNothing image' then "profile image removed" else "profile image updated"] + | n == n' && fullName == fullName' && shortDescr == shortDescr' = ["user description " <> (if maybe True T.null description' then "removed" else "changed to " <> maybe "" plain description') <> notified] | n == n' && fullName == fullName' = ["user bio " <> (if maybe True T.null shortDescr' then "removed" else "changed to " <> maybe "" plain shortDescr') <> notified] | n == n' = ["user full name " <> (if T.null fullName' || fullName' == n' then "removed" else "changed to " <> plain fullName') <> notified] | otherwise = ["user profile is changed to " <> ttyFullName n' fullName' shortDescr' <> notified] @@ -2257,13 +2260,17 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case viewContactUpdated :: Contact -> Contact -> [StyledString] viewContactUpdated - Contact {localDisplayName = n, profile = LocalProfile {fullName, shortDescr, contactLink}} - Contact {localDisplayName = n', profile = LocalProfile {fullName = fullName', shortDescr = shortDescr', contactLink = contactLink'}} - | n == n' && fullName == fullName' && shortDescr == shortDescr' && contactLink == contactLink' = [] - | n == n' && fullName == fullName' && shortDescr == shortDescr' = + Contact {localDisplayName = n, profile = LocalProfile {fullName, shortDescr, description, contactLink}} + Contact {localDisplayName = n', profile = LocalProfile {fullName = fullName', shortDescr = shortDescr', description = description', contactLink = contactLink'}} + | n == n' && fullName == fullName' && shortDescr == shortDescr' && description == description' && contactLink == contactLink' = [] + | n == n' && fullName == fullName' && shortDescr == shortDescr' && description == description' = if isNothing contactLink' then [ttyContact n <> " removed contact address"] else [ttyContact n <> " set new contact address, use " <> highlight ("/info " <> n) <> " to view"] + | n == n' && fullName == fullName' && shortDescr == shortDescr' = + if maybe True T.null description' + then ["contact " <> ttyContact n <> " removed description"] + else ["contact " <> ttyContact n <> " updated description: " <> maybe "" plain description'] | n == n' && fullName == fullName' = if maybe True T.null shortDescr' then ["contact " <> ttyContact n <> " removed bio"] diff --git a/tests/Bots/BroadcastTests.hs b/tests/Bots/BroadcastTests.hs index bfe7b96c7d..9edbb3cb73 100644 --- a/tests/Bots/BroadcastTests.hs +++ b/tests/Bots/BroadcastTests.hs @@ -33,7 +33,7 @@ withBroadcastBot opts test = bot = simplexChatCore testCfg (mkChatOpts opts) $ broadcastBot opts broadcastBotProfile :: Profile -broadcastBotProfile = Profile {displayName = "broadcast_bot", fullName = "Broadcast Bot", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing, badge = Nothing, contactDomain = Nothing} +broadcastBotProfile = Profile {displayName = "broadcast_bot", fullName = "Broadcast Bot", shortDescr = Nothing, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing, badge = Nothing, contactDomain = Nothing} mkBotOpts :: TestParams -> [KnownContact] -> BroadcastBotOpts mkBotOpts ps publishers = diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index 566036026e..0d49e075b9 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -109,7 +109,7 @@ directoryNameTests = do it "should mark an inconsistent SimpleX name as not verified" testDirectoryChannelNameNotVerified directoryProfile :: Profile -directoryProfile = Profile {displayName = "SimpleX Directory", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing, badge = Nothing, contactDomain = Nothing} +directoryProfile = Profile {displayName = "SimpleX Directory", fullName = "", shortDescr = Nothing, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing, badge = Nothing, contactDomain = Nothing} mkDirectoryOpts :: TestParams -> [KnownContact] -> Maybe KnownGroup -> Maybe FilePath -> DirectoryOpts mkDirectoryOpts TestParams {tmpPath = ps} superUsers ownersGroup webFolder = diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 99cd5bf4dd..4468756498 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -26,9 +26,9 @@ import qualified Data.Map.Strict as M import Simplex.Chat.Badges (BadgeCredential, BadgeInfo (..), BadgePurchase (..), BadgeRequest (..), BadgeType (..), generateMasterKey, issueBadge, verifyPayment) import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatHooks (..), defaultChatHooks, mkStoreCxt) import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts (..)) -import Simplex.Chat.Protocol (currentChatVersion) +import Simplex.Chat.Protocol (LinkOwnerSig, MsgChatLink (..), MsgContent (..), currentChatVersion) import Simplex.Chat.Store.Shared (createContact) -import Simplex.Chat.Types (ConnStatus (..), Profile (..), GroupRejectionReason (..)) +import Simplex.Chat.Types (ConnStatus (..), Profile (..), GroupRejectionReason (..), profileFromName) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.BBS (BBSPublicKey, BBSSecretKey, bbsKeyGen) import Simplex.Chat.Types.Shared (GroupMemberRole (..)) @@ -38,7 +38,7 @@ import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Encoding.String (StrEncoding (..)) import Simplex.Messaging.Server.Env.STM hiding (subscriptions) import Simplex.Messaging.Transport -import Simplex.Messaging.Util (encodeJSON) +import Simplex.Messaging.Util (decodeJSON, encodeJSON) import System.Directory (copyFile, createDirectoryIfMissing) import Test.Hspec hiding (it) @@ -46,6 +46,8 @@ chatProfileTests :: SpecWith TestParams chatProfileTests = do describe "user profiles" $ do it "update user profile and notify contacts" testUpdateProfile + it "profile description round-trips and shows in contact info" testProfileDescriptionShown + it "member profile description is redacted for members without a direct contact" testMemberDescriptionRedacted it "update user profile with image" testUpdateProfileImage it "reject profile image that is too large" testSetProfileImageTooLarge it "set profile image from file" testSetProfileImageFromFile @@ -122,6 +124,7 @@ chatProfileTests = do it "should connect via one-time invitation" testShortLinkInvitation it "should plan and connect via one-time invitation" testPlanShortLinkInvitation it "should connect via contact address" testShortLinkContactAddress + it "should share contact address via chat" testShareAddressViaChat it "should join group" testShortLinkJoinGroup describe "short links with attached data" shortLinkTests describe "client services" $ do @@ -202,6 +205,69 @@ testUpdateProfile = bob <## "use @cat to send messages" ] +-- Profile.description survives the connect-time round-trip and is shown in the contact /i view. +testProfileDescriptionShown :: HasCallStack => TestParams -> IO () +testProfileDescriptionShown = + testChat2 aliceProfile bobWithDescr $ + \alice bob -> do + connectUsers alice bob + alice ##> "/i @bob" + alice <## "contact ID: 2" + alice <## "description:" + alice <## "check [this link](https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok) out" + alice <##. "receiving messages via" + alice <##. "sending messages via" + alice <## "you've shared main profile with this contact" + alice <## "connection not verified, use /code command to see security code" + alice <## "quantum resistant end-to-end encryption" + alice <##. "peer chat protocol version range" + where + bobWithDescr = bobProfile {description = Just "check [this link](https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok) out"} + +-- for a member without a direct contact, the description is redacted per the group's link/name policy +testMemberDescriptionRedacted :: HasCallStack => TestParams -> IO () +testMemberDescriptionRedacted = + testChat3 aliceProfile bobProfile cathWithDescr $ + \alice bob cath -> do + connectUsers alice bob + connectUsers alice cath + alice ##> "/g team" + alice <## "group #team is created" + alice <## "to add members use /a team or /create link #team" + -- prohibit direct messages (and thus simplex links) before members join + alice ##> "/set direct #team off" + alice <## "updated group preferences:" + alice <## "Direct messages: off" + addMember "team" alice bob GRAdmin + bob ##> "/j team" + concurrently_ + (alice <## "#team: bob joined the group") + (bob <## "#team: you joined the group") + -- cath joins and is introduced to bob with a redacted profile (link stripped) + addMember "team" alice cath GRAdmin + cath ##> "/j team" + concurrentlyN_ + [ alice <## "#team: cath joined the group", + do + cath <## "#team: you joined the group" + cath <## "#team: member bob (Bob) is connected", + do + bob <## "#team: alice added cath (Catherine) to the group (connecting...)" + bob <## "#team: new member cath is connected" + ] + -- bob has no direct contact to cath, so his stored member profile has the link stripped + bob ##> "/i #team cath" + bob <## "group ID: 1" + bob <##. "member ID:" + bob <## "description:" + bob <## "check out" + bob <##. "receiving messages via" + bob <##. "sending messages via" + bob <## "connection not verified, use /code command to see security code" + bob <##. "peer chat protocol version range" + where + cathWithDescr = cathProfile {description = Just "check [this link](https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok) out"} + -- the test issuer key under index 1 in the test config testBadgeKeys :: BBSPublicKey -> M.Map Int BBSPublicKey testBadgeKeys = M.singleton 1 @@ -546,7 +612,7 @@ testMultiWordProfileNames = aliceProfile' = baseProfile {displayName = "Alice Jones"} bobProfile' = baseProfile {displayName = "Bob James"} cathProfile' = baseProfile {displayName = "Cath Johnson"} - baseProfile = Profile {displayName = "", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = defaultPrefs, badge = Nothing, contactDomain = Nothing} + baseProfile = Profile {displayName = "", fullName = "", shortDescr = Nothing, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = defaultPrefs, badge = Nothing, contactDomain = Nothing} testUserContactLink :: HasCallStack => TestParams -> IO () testUserContactLink = @@ -3036,6 +3102,38 @@ testPlanShortLinkInvitation = slSimplexScheme :: String -> String slSimplexScheme sl = T.unpack $ T.replace "https://localhost/" "simplex:/" (T.pack sl) <> "?h=localhost" +testShareAddressViaChat :: HasCallStack => TestParams -> IO () +testShareAddressViaChat = + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do + alice ##> "/ad" + _ <- getContactLinks alice True + connectUsers alice bob + connectUsers bob cath + -- alice shares her signed address card to bob + alice ##> "/share address @bob" + alice <# "@bob contact address of @alice (signed):" + _ <- getTermLine alice -- link + _ <- getTermLine alice -- owner signature (testView) + bob <# "alice> contact address of @alice (signed):" + bLink <- getTermLine bob + bSig <- getTermLine bob + -- bob verifies alice's owner signature + bob ##> ("/_connect plan 1 " <> bLink <> " sig=" <> bSig) + bob <## "contact address: ok to connect" + bob <## "owner signature: verified" + _ <- getTermLine bob -- link data + -- bob replays alice's signed card to cath: the binding was alice->bob, so cath strips the signature + let sig = maybe (error "bad sig") id (decodeJSON (T.pack bSig) :: Maybe LinkOwnerSig) + cLink = either error id $ strDecode (B.pack bLink) + mc = MCChat (T.pack bLink) (MCLContact cLink (profileFromName "alice") False) (Just sig) + cm = "{\"msgContent\":" <> T.unpack (encodeJSON mc) <> "}" + bob ##> ("/_send @3 json [" <> cm <> "]") + bob <# "@cath contact address of @alice (signed):" + _ <- getTermLine bob -- link + _ <- getTermLine bob -- owner signature (bob's sent view) + cath <# "bob> contact address of @alice:" + void $ getTermLine cath -- link (signature stripped) + testShortLinkContactAddress :: HasCallStack => TestParams -> IO () testShortLinkContactAddress = testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index 1f3426b81a..df2bff8ab6 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -89,7 +89,7 @@ serviceProfile :: Profile serviceProfile = mkProfile "service_user" "Service user" Nothing mkProfile :: T.Text -> T.Text -> Maybe ImageData -> Profile -mkProfile displayName descr image = Profile {displayName, fullName = "", shortDescr = Just descr, image, contactLink = Nothing, peerType = Nothing, preferences = defaultPrefs, badge = Nothing, contactDomain = Nothing} +mkProfile displayName descr image = Profile {displayName, fullName = "", shortDescr = Just descr, description = Nothing, image, contactLink = Nothing, peerType = Nothing, preferences = defaultPrefs, badge = Nothing, contactDomain = Nothing} it :: HasCallStack => String -> (ps -> Expectation) -> SpecWith (Arg (ps -> Expectation)) it name test = diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 42ab8c2589..798ab3c4d7 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -108,7 +108,7 @@ testGroupPreferences :: Maybe GroupPreferences testGroupPreferences = Just GroupPreferences {timedMessages = Nothing, directMessages = Nothing, reactions = Just ReactionsGroupPreference {enable = FEOn}, voice = Just VoiceGroupPreference {enable = FEOn, role = Nothing}, files = Nothing, fullDelete = Nothing, simplexLinks = Nothing, history = Nothing, reports = Nothing, support = Nothing, sessions = Nothing, comments = Nothing, signMessages = Nothing, commands = Nothing} testProfile :: Profile -testProfile = Profile {displayName = "alice", fullName = "Alice", shortDescr = Nothing, image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), peerType = Nothing, contactLink = Nothing, preferences = testChatPreferences, badge = Nothing, contactDomain = Nothing} +testProfile = Profile {displayName = "alice", fullName = "Alice", shortDescr = Nothing, description = Nothing, image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), peerType = Nothing, contactLink = Nothing, preferences = testChatPreferences, badge = Nothing, contactDomain = Nothing} testGroupProfile :: GroupProfile testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", description = Nothing, shortDescr = Nothing, image = Nothing, publicGroup = Nothing, groupPreferences = testGroupPreferences, memberAdmission = Nothing} @@ -241,7 +241,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do #==# XInfo testProfile it "x.info with empty full name" $ "{\"v\":\"1\",\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"\",\"displayName\":\"alice\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" - #==# XInfo Profile {displayName = "alice", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = testChatPreferences, badge = Nothing, contactDomain = Nothing} + #==# XInfo Profile {displayName = "alice", fullName = "", shortDescr = Nothing, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = testChatPreferences, badge = Nothing, contactDomain = Nothing} it "x.contact with xContactId" $ "{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"contactReqId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" #==# XContact testProfile (Just $ XContactId "\1\2\3\4") Nothing Nothing