Files
simplex-chat/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift
T
Evgeny ad23da63d0 core: supporter badges using anonymous BBS credentials (#7040)
* core: supporter badges using anonymous BBS credentials

* badges in profiles

* badge in profiles

* process badges

* update simplexmq

* update simplexmq

* change types

* fix migration

* migration

* update simplexmq

* fix bot API, schema

* fix postgresql build

* refactor

* postgresql schema

* correctly set badges in all cases

* badges ffi

* plan, bot types

* FFI

* FFI: export badge symbols

* add extra field

* refactor badge types to GADT

* configurable badge key

* add badge to profile, test

* ui: badge images

* generate badge key and sign badge

* badge sign in CLI

* fix commands, ui

* rename badges

* Binary

* image size, migration

* update badge images, add public key

* send badges in more cases

* update UI, tests

* bot types, schema

* postgres schema

* tone down badges

* revert formula

* refactor badges

* smaller badges

* badge position

* better badge position

* simpler

* position

* move position

* update simplexmq

* show badge after name

* badge layout

* fix badge

* debug badge height

* shift badge

* fix badge in member name

* bigger badge

* badge layout

* differentiate badge colors

* more avatars for the user's profiles

* refactor

* remove color filter

* alerts

* multiple keys, old expired

* use new BBS api

* update badge keys, bot api

* presentation header

* simplify

* parser

* update iOS images

* update public keys

* query plans

* update simplexmq

* refactor badge types

* simplexmq

* bot api types

* update simplexmq - commoncrypto flag

* update simplexmq

* pass commoncrypto flag to simplexmq in nix iOS build

* ios ui

* update core library, fixes

* badge layout

* badge size

* badge gap

* remove extensions

* simplify

* share badge in more events, reverify badge if verification failed

* larger files with badges

* allow sending larger files

* simpler

* update simplexmq

* better decoder for badge keys

* update simplexmq

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
Co-authored-by: shum <github.shum@liber.li>
2026-06-15 22:25:08 +01:00

333 lines
12 KiB
Swift

//
// ContactListNavLink.swift
// SimpleX (iOS)
//
// Created by Diogo Cunha on 01/08/2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct ContactListNavLink: View {
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
var showDeletedChatIcon: Bool
@State private var alert: SomeAlert? = nil
@State private var actionSheet: SomeActionSheet? = nil
@State private var sheet: SomeSheet<AnyView>? = nil
@State private var showConnectContactViaAddressDialog = false
@State private var showContactRequestDialog = false
var body: some View {
Group {
switch (chat.chatInfo) {
case let .direct(contact):
if contact.nextAcceptContactRequest {
contactWithRequestNavLink(contact)
} else if contact.isContactCard {
contactCardNavLink(contact)
} else if contact.chatDeleted {
deletedChatNavLink(contact)
} else if contact.active {
recentContactNavLink(contact)
}
case let .contactRequest(contactRequest):
contactRequestNavLink(contactRequest)
default:
EmptyView()
}
}
.alert(item: $alert) { $0.alert }
.actionSheet(item: $actionSheet) { $0.actionSheet }
.sheet(item: $sheet) {
if #available(iOS 16.0, *) {
$0.content
.presentationDetents([.fraction(0.4)])
} else {
$0.content
}
}
}
func recentContactNavLink(_ contact: Contact) -> some View {
Button {
dismissAllSheets(animated: true) {
ItemsModel.shared.loadOpenChat(contact.id)
}
} label: {
contactPreview(contact, titleColor: contact.sendMsgToConnect ? theme.colors.primary : theme.colors.onBackground)
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button {
deleteContactDialog(
chat,
contact,
dismissToChatList: false,
showAlert: { alert = $0 },
showActionSheet: { actionSheet = $0 },
showSheetContent: { sheet = $0 }
)
} label: {
Label("Delete", systemImage: "trash")
}
.tint(.red)
}
}
func contactWithRequestNavLink(_ contact: Contact) -> some View {
Button {
dismissAllSheets(animated: true) {
ItemsModel.shared.loadOpenChat(contact.id)
}
} label: {
contactRequestPreview(color: contact.groupDirectInv?.memberRemoved == true ? theme.colors.secondary : theme.colors.primary)
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
if let contactRequestId = contact.contactRequestId {
Button {
Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequestId) }
} label: {
Label("Accept", systemImage: "checkmark")
}
.tint(theme.colors.primary)
if !ChatModel.shared.addressShortLinkDataSet {
Button {
Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequestId) }
} label: {
Label("Accept incognito", systemImage: "theatermasks")
}
.tint(.indigo)
}
Button {
alert = SomeAlert(alert: rejectContactRequestAlert(contactRequestId), id: "rejectContactRequestAlert")
} label: {
Label("Reject", systemImage: "multiply")
}
.tint(.red)
} else if let groupDirectInv = contact.groupDirectInv, !groupDirectInv.memberRemoved {
Button {
acceptMemberContactRequest(contact)
} label: {
Label("Accept", systemImage: "checkmark")
}
.tint(theme.colors.primary)
Button {
showRejectMemberContactRequestAlert(contact)
} label: {
Label("Reject", systemImage: "multiply")
}
.tint(.red)
} else {
Button {
deleteContactDialog(
chat,
contact,
dismissToChatList: false,
showAlert: { alert = $0 },
showActionSheet: { actionSheet = $0 },
showSheetContent: { sheet = $0 }
)
} label: {
Label("Delete", systemImage: "trash")
}
.tint(.red)
}
}
}
func deletedChatNavLink(_ contact: Contact) -> some View {
Button {
Task {
await MainActor.run {
dismissAllSheets(animated: true) {
ItemsModel.shared.loadOpenChat(contact.id)
}
}
}
} label: {
contactPreview(contact, titleColor: theme.colors.onBackground)
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button {
deleteContactDialog(
chat,
contact,
dismissToChatList: false,
showAlert: { alert = $0 },
showActionSheet: { actionSheet = $0 },
showSheetContent: { sheet = $0 }
)
} label: {
Label("Delete", systemImage: "trash")
}
.tint(.red)
}
}
func contactPreview(_ contact: Contact, titleColor: Color) -> some View {
HStack{
ProfileImage(imageStr: contact.image, size: 30)
previewTitle(contact, titleColor: titleColor)
Spacer()
HStack {
if showDeletedChatIcon && contact.chatDeleted {
Image(systemName: "archivebox")
.resizable()
.scaledToFit()
.frame(width: 18, height: 18)
.foregroundColor(.secondary.opacity(0.65))
} else if chat.chatInfo.chatSettings?.favorite ?? false {
Image(systemName: "star.fill")
.resizable()
.scaledToFill()
.frame(width: 18, height: 18)
.foregroundColor(.secondary.opacity(0.65))
}
if contact.contactConnIncognito {
Image(systemName: "theatermasks")
.resizable()
.scaledToFit()
.frame(width: 22, height: 22)
.foregroundColor(.secondary)
}
}
}
}
private func previewTitle(_ contact: Contact, titleColor: Color) -> some View {
let t = Text(chat.chatInfo.chatViewName).foregroundColor(titleColor)
return NameWithBadge(
contact.verified == true ? verifiedIcon + t : t,
chat.chatInfo.nameBadge
)
.lineLimit(1)
}
private var verifiedIcon: Text {
(Text(Image(systemName: "checkmark.shield")) + textSpace)
.foregroundColor(.secondary)
.baselineOffset(1)
.kerning(-2)
}
func contactCardNavLink(_ contact: Contact) -> some View {
Button {
showConnectContactViaAddressDialog = true
} label: {
contactCardPreview(contact)
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button {
deleteContactDialog(
chat,
contact,
dismissToChatList: false,
showAlert: { alert = $0 },
showActionSheet: { actionSheet = $0 },
showSheetContent: { sheet = $0 }
)
} label: {
Label("Delete", systemImage: "trash")
}
.tint(.red)
}
.confirmationDialog("Connect with \(contact.chatViewName)", isPresented: $showConnectContactViaAddressDialog, titleVisibility: .visible) {
if !contact.profileChangeProhibited {
Button("Use current profile") { connectContactViaAddress_(contact, false) }
Button("Use new incognito profile") { connectContactViaAddress_(contact, true) }
} else if !contact.contactConnIncognito {
Button("Use current profile") { connectContactViaAddress_(contact, false) }
} else {
Button("Use incognito profile") { connectContactViaAddress_(contact, true) }
}
}
}
private func connectContactViaAddress_(_ contact: Contact, _ incognito: Bool) {
Task {
let ok = await connectContactViaAddress(contact.contactId, incognito, showAlert: { alert = SomeAlert(alert: $0, id: "ContactListNavLink connectContactViaAddress") })
if ok {
ItemsModel.shared.loadOpenChat(contact.id) {
dismissAllSheets(animated: true) {
AlertManager.shared.showAlert(connReqSentAlert(.contact))
}
}
}
}
}
func contactCardPreview(_ contact: Contact) -> some View {
HStack{
ProfileImage(imageStr: contact.image, size: 30)
Text(chat.chatInfo.chatViewName)
.foregroundColor(.accentColor)
.lineLimit(1)
Spacer()
Image(systemName: "envelope")
.resizable()
.scaledToFill()
.frame(width: 14, height: 14)
.foregroundColor(.accentColor)
}
}
func contactRequestNavLink(_ contactRequest: UserContactRequest) -> some View {
Button {
showContactRequestDialog = true
} label: {
contactRequestPreview(color: theme.colors.primary)
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button {
Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) }
} label: { Label("Accept", systemImage: "checkmark") }
.tint(theme.colors.primary)
if !ChatModel.shared.addressShortLinkDataSet {
Button {
Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) }
} label: {
Label("Accept incognito", systemImage: "theatermasks")
}
.tint(.indigo)
}
Button {
alert = SomeAlert(alert: rejectContactRequestAlert(contactRequest.apiId), id: "rejectContactRequestAlert")
} label: {
Label("Reject", systemImage: "multiply")
}
.tint(.red)
}
.confirmationDialog("Accept connection request?", isPresented: $showContactRequestDialog, titleVisibility: .visible) {
Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) } }
if !ChatModel.shared.addressShortLinkDataSet {
Button("Accept incognito") { Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) } }
}
Button("Reject (sender NOT notified)", role: .destructive) { Task { await rejectContactRequest(contactRequest.apiId) } }
}
}
func contactRequestPreview(color: Color) -> some View {
HStack{
ProfileImage(imageStr: chat.chatInfo.image, size: 30)
NameWithBadge(Text(chat.chatInfo.chatViewName).foregroundColor(color), chat.chatInfo.nameBadge)
.lineLimit(1)
Spacer()
Image(systemName: "checkmark")
.resizable()
.scaledToFill()
.frame(width: 14, height: 14)
.foregroundColor(color)
}
}
}