This commit is contained in:
Evgeny @ SimpleX Chat committed 2026-07-15 16:26:13 +00:00
1 parent 73a5829af1
commit cede310299
8 files changed
+287 -26

No files matched your search

+5
View File
@@ -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"
+6
View File
@@ -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)
@@ -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<Int> = []
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)
}
@@ -141,11 +141,12 @@ func msgTextResultView(
_ t: Text,
showSecrets: Binding<Set<Int>>? = 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<Set<Int>>? = 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<Int> = []
@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<Int> = []
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<Int>? = 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: ()
}
+2 -9
View File
@@ -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<Int> = []
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):
@@ -25,6 +25,7 @@ struct UserAddressView: View {
@State private var mailViewResult: Result<MFMailComposeResult, Error>? = 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<Bool>, shareAddress:
)
}
private func addShortLink(progressIndicator: Binding<Bool>, shareOnCompletion: Bool = false) {
@ViewBuilder
func shareAddressPicker(composeState: Binding<ComposeState>? = 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<ComposeState>? = 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<Bool>, shareOnCompletion: Bool = false, onComplete: (() -> Void)? = nil) {
progressIndicator.wrappedValue = true
Task {
do {
@@ -462,6 +543,7 @@ private func addShortLink(progressIndicator: Binding<Bool>, shareOnCompletion: B
if shareOnCompletion, let userAddress {
userAddress.shareAddress(short: true)
}
onComplete?()
}
} catch let error {
logger.error("apiAddMyAddressShortLink: \(responseError(error))")
@@ -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() }
}
}
}
}
}
+35 -5
View File
@@ -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) {