mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-23 17:13:10 +00:00
ios ui
This commit is contained in:
1 parent
73a5829af1
commit
cede310299
8 files changed
+287
-26
No files matched your search
@@ -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"
|
||||
|
||||
@@ -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: ()
|
||||
}
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user