Files
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

193 lines
8.1 KiB
Swift

//
// CIImageView.swift
// SimpleX
//
// Created by JRoberts on 12/04/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
// Spec: spec/client/chat-view.md#CIImageView
struct CIImageView: View {
@EnvironmentObject var m: ChatModel
let chatItem: ChatItem
let senderProfile: LocalProfile?
var scrollToItem: ((ChatItem.ID) -> Void)? = nil
var preview: UIImage?
let maxWidth: CGFloat
var imgWidth: CGFloat?
var smallView: Bool = false
@Binding var showFullScreenImage: Bool
@State private var blurred: Bool = UserDefaults.standard.integer(forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) > 0
var body: some View {
let file = chatItem.file
VStack(alignment: .center, spacing: 6) {
if let uiImage = getLoadedImage(file) {
Group { if smallView { smallViewImageView(uiImage) } else { imageView(uiImage) } }
.fullScreenCover(isPresented: $showFullScreenImage) {
FullScreenMediaView(chatItem: chatItem, scrollToItem: scrollToItem, image: uiImage, showView: $showFullScreenImage)
}
.if(!smallView) { view in
view.modifier(PrivacyBlur(blurred: $blurred))
}
.if(!blurred) { v in
v.simultaneousGesture(TapGesture().onEnded { showFullScreenImage = true })
}
.onChange(of: m.activeCallViewIsCollapsed) { _ in
showFullScreenImage = false
}
} else if let preview {
Group {
if smallView {
smallViewImageView(preview)
} else {
imageView(preview).modifier(PrivacyBlur(blurred: $blurred))
}
}
.simultaneousGesture(TapGesture().onEnded {
if let file = file {
switch file.fileStatus {
case .rcvInvitation, .rcvAborted:
if fileSizeValid(file, senderProfile) {
Task {
if let user = m.currentUser {
await receiveFile(user: user, fileId: file.fileId)
}
}
} else {
let prettyMaxFileSize = ByteCountFormatter.string(fromByteCount: getMaxFileSize(file.fileProtocol, senderProfile), countStyle: .binary)
AlertManager.shared.showAlertMsg(
title: "Large file!",
message: "Your contact sent a file that is larger than currently supported maximum size (\(prettyMaxFileSize))."
)
}
case .rcvAccepted:
switch file.fileProtocol {
case .xftp:
AlertManager.shared.showAlertMsg(
title: "Waiting for image",
message: "Image will be received when your contact completes uploading it."
)
case .smp:
AlertManager.shared.showAlertMsg(
title: "Waiting for image",
message: "Image will be received when your contact is online, please wait or check later!"
)
case .local: ()
}
case .rcvTransfer: () // ?
case .rcvComplete: () // ?
case .rcvCancelled: () // TODO
case let .rcvError(rcvFileError):
showFileErrorAlert(rcvFileError)
case let .rcvWarning(rcvFileError):
showFileErrorAlert(rcvFileError, temporary: true)
case let .sndError(sndFileError):
showFileErrorAlert(sndFileError)
case let .sndWarning(sndFileError):
showFileErrorAlert(sndFileError, temporary: true)
default: ()
}
}
})
}
}
.onDisappear {
showFullScreenImage = false
}
}
private func imageView(_ img: UIImage) -> some View {
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth
return ZStack(alignment: .topTrailing) {
if img.imageData == nil {
Image(uiImage: img)
.resizable()
.scaledToFill()
.frame(width: w, height: w * heightRatio(img.size))
.clipped()
} else {
SwiftyGif(image: img, contentMode: .scaleAspectFill)
.frame(width: w, height: w * heightRatio(img.size))
.clipped()
}
if !blurred || !showDownloadButton(chatItem.file?.fileStatus) {
loadingIndicator()
}
}
}
private func smallViewImageView(_ img: UIImage) -> some View {
ZStack(alignment: .topTrailing) {
if img.imageData == nil {
Image(uiImage: img)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: maxWidth, height: maxWidth)
} else {
SwiftyGif(image: img, contentMode: .scaleAspectFill)
.frame(width: maxWidth, height: maxWidth)
}
if chatItem.file?.showStatusIconInSmallView == true {
loadingIndicator()
}
}
}
@ViewBuilder private func loadingIndicator() -> some View {
if let file = chatItem.file {
switch file.fileStatus {
case .sndStored:
switch file.fileProtocol {
case .xftp: progressView()
case .smp: EmptyView()
case .local: EmptyView()
}
case .sndTransfer: progressView()
case .sndComplete: fileIcon("checkmark", 10, 13)
case .sndCancelled: fileIcon("xmark", 10, 13)
case .sndError: fileIcon("xmark", 10, 13)
case .sndWarning: fileIcon("exclamationmark.triangle.fill", 10, 13)
case .rcvInvitation: fileIcon("arrow.down", 10, 13)
case .rcvAccepted: fileIcon("ellipsis", 14, 11)
case .rcvTransfer: progressView()
case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11)
case .rcvComplete: EmptyView()
case .rcvCancelled: fileIcon("xmark", 10, 13)
case .rcvError: fileIcon("xmark", 10, 13)
case .rcvWarning: fileIcon("exclamationmark.triangle.fill", 10, 13)
case .invalid: fileIcon("questionmark", 10, 13)
}
}
}
private func fileIcon(_ icon: String, _ size: CGFloat, _ padding: CGFloat) -> some View {
Image(systemName: icon)
.resizable()
.invertedForegroundStyle()
.aspectRatio(contentMode: .fit)
.frame(width: size, height: size)
.padding(padding)
}
private func progressView() -> some View {
ProgressView()
.progressViewStyle(.circular)
.frame(width: 20, height: 20)
.tint(.white)
.padding(8)
}
private func showDownloadButton(_ fileStatus: CIFileStatus?) -> Bool {
switch fileStatus {
case .rcvInvitation: true
case .rcvAborted: true
default: false
}
}
}