mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 20:29:53 +00:00
core, ui: errors for blocked files and contact addresses (#5510)
* core, ui: errors for blocked files and contact addresses * android * iOS: How it works, stub for blog post * android: blocked errors WIP * android: alert with button * update * fix encoding * nix * simplexmq
This commit is contained in:
parent
0d6b26c269
commit
eacae74fed
20 changed files with 216 additions and 140 deletions
|
@ -852,6 +852,18 @@ func apiConnect_(incognito: Bool, connReq: String) async -> ((ConnReqType, Pendi
|
|||
message: "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection."
|
||||
)
|
||||
return (nil, alert)
|
||||
case let .chatCmdError(_, .errorAgent(.SMP(_, .BLOCKED(info)))):
|
||||
let alert = Alert(
|
||||
title: Text("Connection blocked"),
|
||||
message: Text("Connection is blocked by server operator:\n\(info.reason.text)"),
|
||||
primaryButton: .default(Text("Ok")),
|
||||
secondaryButton: .default(Text("How it works")) {
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.open(contentModerationPostLink)
|
||||
}
|
||||
}
|
||||
)
|
||||
return (nil, alert)
|
||||
case .chatCmdError(_, .errorAgent(.SMP(_, .QUOTA))):
|
||||
let alert = mkAlert(
|
||||
title: "Undelivered messages",
|
||||
|
|
|
@ -118,16 +118,10 @@ struct CIFileView: View {
|
|||
}
|
||||
case let .rcvError(rcvFileError):
|
||||
logger.debug("CIFileView fileAction - in .rcvError")
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
showFileErrorAlert(rcvFileError)
|
||||
case let .rcvWarning(rcvFileError):
|
||||
logger.debug("CIFileView fileAction - in .rcvWarning")
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
showFileErrorAlert(rcvFileError, temporary: true)
|
||||
case .sndStored:
|
||||
logger.debug("CIFileView fileAction - in .sndStored")
|
||||
if file.fileProtocol == .local, let fileSource = getLoadedFileSource(file) {
|
||||
|
@ -140,16 +134,10 @@ struct CIFileView: View {
|
|||
}
|
||||
case let .sndError(sndFileError):
|
||||
logger.debug("CIFileView fileAction - in .sndError")
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
showFileErrorAlert(sndFileError)
|
||||
case let .sndWarning(sndFileError):
|
||||
logger.debug("CIFileView fileAction - in .sndWarning")
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
showFileErrorAlert(sndFileError, temporary: true)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
@ -268,6 +256,26 @@ func saveCryptoFile(_ fileSource: CryptoFile) {
|
|||
}
|
||||
}
|
||||
|
||||
func showFileErrorAlert(_ err: FileError, temporary: Bool = false) {
|
||||
let title: String = if temporary {
|
||||
NSLocalizedString("Temporary file error", comment: "file error alert title")
|
||||
} else {
|
||||
NSLocalizedString("File error", comment: "file error alert title")
|
||||
}
|
||||
if let btn = err.moreInfoButton {
|
||||
showAlert(title, message: err.errorInfo) {
|
||||
[
|
||||
okAlertAction,
|
||||
UIAlertAction(title: NSLocalizedString("How it works", comment: "alert button"), style: .default, handler: { _ in
|
||||
UIApplication.shared.open(contentModerationPostLink)
|
||||
})
|
||||
]
|
||||
}
|
||||
} else {
|
||||
showAlert(title, message: err.errorInfo)
|
||||
}
|
||||
}
|
||||
|
||||
struct CIFileView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let sentFile: ChatItem = ChatItem(
|
||||
|
|
|
@ -69,25 +69,13 @@ struct CIImageView: View {
|
|||
case .rcvComplete: () // ?
|
||||
case .rcvCancelled: () // TODO
|
||||
case let .rcvError(rcvFileError):
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
showFileErrorAlert(rcvFileError)
|
||||
case let .rcvWarning(rcvFileError):
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
showFileErrorAlert(rcvFileError, temporary: true)
|
||||
case let .sndError(sndFileError):
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
showFileErrorAlert(sndFileError)
|
||||
case let .sndWarning(sndFileError):
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
showFileErrorAlert(sndFileError, temporary: true)
|
||||
default: ()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -355,18 +355,12 @@ struct CIVideoView: View {
|
|||
case let .sndError(sndFileError):
|
||||
fileIcon("xmark", 10, 13)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
showFileErrorAlert(sndFileError)
|
||||
}
|
||||
case let .sndWarning(sndFileError):
|
||||
fileIcon("exclamationmark.triangle.fill", 10, 13)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
showFileErrorAlert(sndFileError, temporary: true)
|
||||
}
|
||||
case .rcvInvitation: fileIcon("arrow.down", 10, 13)
|
||||
case .rcvAccepted: fileIcon("ellipsis", 14, 11)
|
||||
|
@ -382,18 +376,12 @@ struct CIVideoView: View {
|
|||
case let .rcvError(rcvFileError):
|
||||
fileIcon("xmark", 10, 13)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
showFileErrorAlert(rcvFileError)
|
||||
}
|
||||
case let .rcvWarning(rcvFileError):
|
||||
fileIcon("exclamationmark.triangle.fill", 10, 13)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
showFileErrorAlert(rcvFileError, temporary: true)
|
||||
}
|
||||
case .invalid: fileIcon("questionmark", 10, 13)
|
||||
}
|
||||
|
|
|
@ -169,18 +169,12 @@ struct VoiceMessagePlayer: View {
|
|||
case let .sndError(sndFileError):
|
||||
fileStatusIcon("multiply", 14)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
showFileErrorAlert(sndFileError)
|
||||
}
|
||||
case let .sndWarning(sndFileError):
|
||||
fileStatusIcon("exclamationmark.triangle.fill", 16)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
showFileErrorAlert(sndFileError, temporary: true)
|
||||
}
|
||||
case .rcvInvitation: downloadButton(recordingFile, "play.fill")
|
||||
case .rcvAccepted: loadingIcon()
|
||||
|
@ -191,18 +185,12 @@ struct VoiceMessagePlayer: View {
|
|||
case let .rcvError(rcvFileError):
|
||||
fileStatusIcon("multiply", 14)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
showFileErrorAlert(rcvFileError)
|
||||
}
|
||||
case let .rcvWarning(rcvFileError):
|
||||
fileStatusIcon("exclamationmark.triangle.fill", 16)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
showFileErrorAlert(rcvFileError, temporary: true)
|
||||
}
|
||||
case .invalid: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
|
||||
}
|
||||
|
|
|
@ -2481,6 +2481,7 @@ public enum ProtocolErrorType: Decodable, Hashable {
|
|||
case CMD(cmdErr: ProtocolCommandError)
|
||||
indirect case PROXY(proxyErr: ProxyError)
|
||||
case AUTH
|
||||
case BLOCKED(blockInfo: BlockingInfo)
|
||||
case CRYPTO
|
||||
case QUOTA
|
||||
case STORE(storeErr: String)
|
||||
|
@ -2497,11 +2498,28 @@ public enum ProxyError: Decodable, Hashable {
|
|||
case NO_SESSION
|
||||
}
|
||||
|
||||
public struct BlockingInfo: Decodable, Equatable, Hashable {
|
||||
public var reason: BlockingReason
|
||||
}
|
||||
|
||||
public enum BlockingReason: String, Decodable {
|
||||
case spam
|
||||
case content
|
||||
|
||||
public var text: String {
|
||||
switch self {
|
||||
case .spam: NSLocalizedString("Spam", comment: "blocking reason")
|
||||
case .content: NSLocalizedString("Content violates conditions of use", comment: "blocking reason")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum XFTPErrorType: Decodable, Hashable {
|
||||
case BLOCK
|
||||
case SESSION
|
||||
case CMD(cmdErr: ProtocolCommandError)
|
||||
case AUTH
|
||||
case BLOCKED(blockInfo: BlockingInfo)
|
||||
case SIZE
|
||||
case QUOTA
|
||||
case DIGEST
|
||||
|
|
|
@ -15,6 +15,8 @@ public let CREATE_MEMBER_CONTACT_VERSION = 2
|
|||
// version to receive reports (MCReport)
|
||||
public let REPORTS_VERSION = 12
|
||||
|
||||
public let contentModerationPostLink = URL(string: "https://simplex.chat/blog/20250112-simplex-network-privacy-preserving-content-moderation.html")!
|
||||
|
||||
public struct User: Identifiable, Decodable, UserLike, NamedChat, Hashable {
|
||||
public var userId: Int64
|
||||
public var agentUserId: String
|
||||
|
@ -3024,7 +3026,7 @@ public enum SndError: Decodable, Hashable {
|
|||
case proxyRelay(proxyServer: String, srvError: SrvError)
|
||||
case other(sndError: String)
|
||||
|
||||
public var errorInfo: String {
|
||||
public var errorInfo: String {
|
||||
switch self {
|
||||
case .auth: NSLocalizedString("Wrong key or unknown connection - most likely this connection is deleted.", comment: "snd error text")
|
||||
case .quota: NSLocalizedString("Capacity exceeded - recipient did not receive previously sent messages.", comment: "snd error text")
|
||||
|
@ -3684,6 +3686,7 @@ public enum CIFileStatus: Decodable, Equatable, Hashable {
|
|||
|
||||
public enum FileError: Decodable, Equatable, Hashable {
|
||||
case auth
|
||||
case blocked(server: String, blockInfo: BlockingInfo)
|
||||
case noFile
|
||||
case relay(srvError: SrvError)
|
||||
case other(fileError: String)
|
||||
|
@ -3691,6 +3694,7 @@ public enum FileError: Decodable, Equatable, Hashable {
|
|||
var id: String {
|
||||
switch self {
|
||||
case .auth: return "auth"
|
||||
case let .blocked(srv, info): return "blocked \(srv) \(info)"
|
||||
case .noFile: return "noFile"
|
||||
case let .relay(srvError): return "relay \(srvError)"
|
||||
case let .other(fileError): return "other \(fileError)"
|
||||
|
@ -3700,11 +3704,19 @@ public enum FileError: Decodable, Equatable, Hashable {
|
|||
public var errorInfo: String {
|
||||
switch self {
|
||||
case .auth: NSLocalizedString("Wrong key or unknown file chunk address - most likely file is deleted.", comment: "file error text")
|
||||
case let .blocked(_, info): NSLocalizedString("File is blocked by server operator:\n\(info.reason.text).", comment: "file error text")
|
||||
case .noFile: NSLocalizedString("File not found - most likely file was deleted or cancelled.", comment: "file error text")
|
||||
case let .relay(srvError): String.localizedStringWithFormat(NSLocalizedString("File server error: %@", comment: "file error text"), srvError.errorInfo)
|
||||
case let .other(fileError): String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: "file error text"), fileError)
|
||||
}
|
||||
}
|
||||
|
||||
public var moreInfoButton: (label: LocalizedStringKey, link: URL)? {
|
||||
switch self {
|
||||
case .blocked: ("How it works", contentModerationPostLink)
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum MsgContent: Equatable, Hashable {
|
||||
|
|
|
@ -12,6 +12,7 @@ import chat.simplex.common.platform.*
|
|||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.call.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.chat.item.contentModerationPostLink
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.migration.MigrationToDeviceState
|
||||
|
@ -22,7 +23,6 @@ import dev.icerock.moko.resources.StringResource
|
|||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlin.collections.removeAll as remAll
|
||||
import kotlinx.datetime.*
|
||||
import kotlinx.datetime.TimeZone
|
||||
|
@ -3591,15 +3591,22 @@ sealed class CIFileStatus {
|
|||
@Serializable
|
||||
sealed class FileError {
|
||||
@Serializable @SerialName("auth") class Auth: FileError()
|
||||
@Serializable @SerialName("blocked") class Blocked(val server: String, val blockInfo: BlockingInfo): FileError()
|
||||
@Serializable @SerialName("noFile") class NoFile: FileError()
|
||||
@Serializable @SerialName("relay") class Relay(val srvError: SrvError): FileError()
|
||||
@Serializable @SerialName("other") class Other(val fileError: String): FileError()
|
||||
|
||||
val errorInfo: String get() = when (this) {
|
||||
is FileError.Auth -> generalGetString(MR.strings.file_error_auth)
|
||||
is FileError.NoFile -> generalGetString(MR.strings.file_error_no_file)
|
||||
is FileError.Relay -> generalGetString(MR.strings.file_error_relay).format(srvError.errorInfo)
|
||||
is FileError.Other -> generalGetString(MR.strings.ci_status_other_error).format(fileError)
|
||||
is Auth -> generalGetString(MR.strings.file_error_auth)
|
||||
is Blocked -> generalGetString(MR.strings.file_error_blocked).format(blockInfo.reason.text)
|
||||
is NoFile -> generalGetString(MR.strings.file_error_no_file)
|
||||
is Relay -> generalGetString(MR.strings.file_error_relay).format(srvError.errorInfo)
|
||||
is Other -> generalGetString(MR.strings.ci_status_other_error).format(fileError)
|
||||
}
|
||||
|
||||
val moreInfoButton: Pair<String, String>? get() = when(this) {
|
||||
is Blocked -> generalGetString(MR.strings.how_it_works) to contentModerationPostLink
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,10 +19,12 @@ import chat.simplex.common.model.ChatController.setNetCfg
|
|||
import chat.simplex.common.model.ChatModel.changingActiveUserMutex
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
|
||||
import chat.simplex.common.model.SMPErrorType.BLOCKED
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.call.*
|
||||
import chat.simplex.common.views.chat.item.showContentBlockedAlert
|
||||
import chat.simplex.common.views.chat.item.showQuotedItemDoesNotExistAlert
|
||||
import chat.simplex.common.views.chatlist.openGroupChat
|
||||
import chat.simplex.common.views.migration.MigrationFileLinkData
|
||||
|
@ -1411,6 +1413,15 @@ object ChatController {
|
|||
)
|
||||
return null
|
||||
}
|
||||
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent
|
||||
&& r.chatError.agentError is AgentErrorType.SMP
|
||||
&& r.chatError.agentError.smpErr is SMPErrorType.BLOCKED -> {
|
||||
showContentBlockedAlert(
|
||||
generalGetString(MR.strings.connection_error_blocked),
|
||||
generalGetString(MR.strings.connection_error_blocked_desc).format(r.chatError.agentError.smpErr.blockInfo.reason.text),
|
||||
)
|
||||
return null
|
||||
}
|
||||
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent
|
||||
&& r.chatError.agentError is AgentErrorType.SMP
|
||||
&& r.chatError.agentError.smpErr is SMPErrorType.QUOTA -> {
|
||||
|
@ -6756,6 +6767,7 @@ sealed class BrokerErrorType {
|
|||
@Serializable @SerialName("TIMEOUT") object TIMEOUT: BrokerErrorType()
|
||||
}
|
||||
|
||||
// ProtocolErrorType
|
||||
@Serializable
|
||||
sealed class SMPErrorType {
|
||||
val string: String get() = when (this) {
|
||||
|
@ -6764,9 +6776,10 @@ sealed class SMPErrorType {
|
|||
is CMD -> "CMD ${cmdErr.string}"
|
||||
is PROXY -> "PROXY ${proxyErr.string}"
|
||||
is AUTH -> "AUTH"
|
||||
is BLOCKED -> "BLOCKED ${json.encodeToString(blockInfo)}"
|
||||
is CRYPTO -> "CRYPTO"
|
||||
is QUOTA -> "QUOTA"
|
||||
is STORE -> "STORE ${storeErr}"
|
||||
is STORE -> "STORE $storeErr"
|
||||
is NO_MSG -> "NO_MSG"
|
||||
is LARGE_MSG -> "LARGE_MSG"
|
||||
is EXPIRED -> "EXPIRED"
|
||||
|
@ -6777,6 +6790,7 @@ sealed class SMPErrorType {
|
|||
@Serializable @SerialName("CMD") class CMD(val cmdErr: ProtocolCommandError): SMPErrorType()
|
||||
@Serializable @SerialName("PROXY") class PROXY(val proxyErr: ProxyError): SMPErrorType()
|
||||
@Serializable @SerialName("AUTH") class AUTH: SMPErrorType()
|
||||
@Serializable @SerialName("BLOCKED") class BLOCKED(val blockInfo: BlockingInfo): SMPErrorType()
|
||||
@Serializable @SerialName("CRYPTO") class CRYPTO: SMPErrorType()
|
||||
@Serializable @SerialName("QUOTA") class QUOTA: SMPErrorType()
|
||||
@Serializable @SerialName("STORE") class STORE(val storeErr: String): SMPErrorType()
|
||||
|
@ -6800,6 +6814,22 @@ sealed class ProxyError {
|
|||
@Serializable @SerialName("NO_SESSION") class NO_SESSION: ProxyError()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class BlockingInfo(
|
||||
val reason: BlockingReason
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class BlockingReason {
|
||||
@SerialName("spam") Spam,
|
||||
@SerialName("content") Content;
|
||||
|
||||
val text: String get() = when (this) {
|
||||
Spam -> generalGetString(MR.strings.blocking_reason_spam)
|
||||
Content -> generalGetString(MR.strings.blocking_reason_content)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class ProtocolCommandError {
|
||||
val string: String get() = when (this) {
|
||||
|
@ -6875,6 +6905,7 @@ sealed class XFTPErrorType {
|
|||
is SESSION -> "SESSION"
|
||||
is CMD -> "CMD ${cmdErr.string}"
|
||||
is AUTH -> "AUTH"
|
||||
is BLOCKED -> "BLOCKED ${json.encodeToString(blockInfo)}"
|
||||
is SIZE -> "SIZE"
|
||||
is QUOTA -> "QUOTA"
|
||||
is DIGEST -> "DIGEST"
|
||||
|
@ -6890,6 +6921,7 @@ sealed class XFTPErrorType {
|
|||
@Serializable @SerialName("SESSION") object SESSION: XFTPErrorType()
|
||||
@Serializable @SerialName("CMD") class CMD(val cmdErr: ProtocolCommandError): XFTPErrorType()
|
||||
@Serializable @SerialName("AUTH") object AUTH: XFTPErrorType()
|
||||
@Serializable @SerialName("BLOCKED") class BLOCKED(val blockInfo: BlockingInfo): XFTPErrorType()
|
||||
@Serializable @SerialName("SIZE") object SIZE: XFTPErrorType()
|
||||
@Serializable @SerialName("QUOTA") object QUOTA: XFTPErrorType()
|
||||
@Serializable @SerialName("DIGEST") object DIGEST: XFTPErrorType()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package chat.simplex.common.views.chat.item
|
||||
|
||||
import SectionItemView
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
|
@ -13,6 +14,8 @@ import androidx.compose.ui.draw.clip
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.*
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
@ -92,25 +95,13 @@ fun CIFileView(
|
|||
FileProtocol.LOCAL -> {}
|
||||
}
|
||||
file.fileStatus is CIFileStatus.RcvError ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.file_error),
|
||||
file.fileStatus.rcvFileError.errorInfo
|
||||
)
|
||||
showFileErrorAlert(file.fileStatus.rcvFileError)
|
||||
file.fileStatus is CIFileStatus.RcvWarning ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.temporary_file_error),
|
||||
file.fileStatus.rcvFileError.errorInfo
|
||||
)
|
||||
showFileErrorAlert(file.fileStatus.rcvFileError, temporary = true)
|
||||
file.fileStatus is CIFileStatus.SndError ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.file_error),
|
||||
file.fileStatus.sndFileError.errorInfo
|
||||
)
|
||||
showFileErrorAlert(file.fileStatus.sndFileError)
|
||||
file.fileStatus is CIFileStatus.SndWarning ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.temporary_file_error),
|
||||
file.fileStatus.sndFileError.errorInfo
|
||||
)
|
||||
showFileErrorAlert(file.fileStatus.sndFileError, temporary = true)
|
||||
file.forwardingAllowed() -> {
|
||||
withLongRunningApi(slow = 600_000) {
|
||||
var filePath = getLoadedFilePath(file)
|
||||
|
@ -235,6 +226,37 @@ fun CIFileView(
|
|||
|
||||
fun fileSizeValid(file: CIFile): Boolean = file.fileSize <= getMaxFileSize(file.fileProtocol)
|
||||
|
||||
fun showFileErrorAlert(err: FileError, temporary: Boolean = false) {
|
||||
val title: String = generalGetString(if (temporary) MR.strings.temporary_file_error else MR.strings.file_error)
|
||||
val btn = err.moreInfoButton
|
||||
if (btn != null) {
|
||||
showContentBlockedAlert(title, err.errorInfo)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(title, err.errorInfo)
|
||||
}
|
||||
}
|
||||
|
||||
val contentModerationPostLink = "https://simplex.chat/blog/20250112-simplex-network-privacy-preserving-content-moderation.html"
|
||||
|
||||
fun showContentBlockedAlert(title: String, message: String) {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(title, text = message, buttons = {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
Column {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
uriHandler.openUriCatching(contentModerationPostLink)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.how_it_works), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.ok), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
expect fun SaveOrOpenFileMenu(
|
||||
showMenu: MutableState<Boolean>,
|
||||
|
|
|
@ -238,25 +238,13 @@ fun CIImageView(
|
|||
FileProtocol.LOCAL -> {}
|
||||
}
|
||||
file.fileStatus is CIFileStatus.RcvError ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.file_error),
|
||||
file.fileStatus.rcvFileError.errorInfo
|
||||
)
|
||||
showFileErrorAlert(file.fileStatus.rcvFileError)
|
||||
file.fileStatus is CIFileStatus.RcvWarning ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.temporary_file_error),
|
||||
file.fileStatus.rcvFileError.errorInfo
|
||||
)
|
||||
showFileErrorAlert(file.fileStatus.rcvFileError, temporary = true)
|
||||
file.fileStatus is CIFileStatus.SndError ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.file_error),
|
||||
file.fileStatus.sndFileError.errorInfo
|
||||
)
|
||||
showFileErrorAlert(file.fileStatus.sndFileError)
|
||||
file.fileStatus is CIFileStatus.SndWarning ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.temporary_file_error),
|
||||
file.fileStatus.sndFileError.errorInfo
|
||||
)
|
||||
showFileErrorAlert(file.fileStatus.sndFileError, temporary = true)
|
||||
file.fileStatus is CIFileStatus.RcvTransfer -> {} // ?
|
||||
file.fileStatus is CIFileStatus.RcvComplete -> {} // ?
|
||||
file.fileStatus is CIFileStatus.RcvCancelled -> {} // TODO
|
||||
|
|
|
@ -499,10 +499,7 @@ private fun fileStatusIcon(file: CIFile?, smallView: Boolean) {
|
|||
painterResource(MR.images.ic_close),
|
||||
MR.strings.icon_descr_file,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.file_error),
|
||||
file.fileStatus.sndFileError.errorInfo
|
||||
)
|
||||
showFileErrorAlert(file.fileStatus.sndFileError)
|
||||
}
|
||||
)
|
||||
is CIFileStatus.SndWarning ->
|
||||
|
@ -510,10 +507,7 @@ private fun fileStatusIcon(file: CIFile?, smallView: Boolean) {
|
|||
painterResource(MR.images.ic_warning_filled),
|
||||
MR.strings.icon_descr_file,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.temporary_file_error),
|
||||
file.fileStatus.sndFileError.errorInfo
|
||||
)
|
||||
showFileErrorAlert(file.fileStatus.sndFileError, temporary = true)
|
||||
}
|
||||
)
|
||||
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_video_asked_to_receive)
|
||||
|
@ -532,10 +526,7 @@ private fun fileStatusIcon(file: CIFile?, smallView: Boolean) {
|
|||
painterResource(MR.images.ic_close),
|
||||
MR.strings.icon_descr_file,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.file_error),
|
||||
file.fileStatus.rcvFileError.errorInfo
|
||||
)
|
||||
showFileErrorAlert(file.fileStatus.rcvFileError)
|
||||
}
|
||||
)
|
||||
is CIFileStatus.RcvWarning ->
|
||||
|
@ -543,10 +534,7 @@ private fun fileStatusIcon(file: CIFile?, smallView: Boolean) {
|
|||
painterResource(MR.images.ic_warning_filled),
|
||||
MR.strings.icon_descr_file,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.temporary_file_error),
|
||||
file.fileStatus.rcvFileError.errorInfo
|
||||
)
|
||||
showFileErrorAlert(file.fileStatus.rcvFileError, temporary = true)
|
||||
}
|
||||
)
|
||||
is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file)
|
||||
|
|
|
@ -398,10 +398,7 @@ private fun VoiceMsgIndicator(
|
|||
sizeMultiplier,
|
||||
longClick,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.file_error),
|
||||
file.fileStatus.sndFileError.errorInfo
|
||||
)
|
||||
showFileErrorAlert(file.fileStatus.sndFileError)
|
||||
}
|
||||
)
|
||||
file != null && file.fileStatus is CIFileStatus.SndWarning ->
|
||||
|
@ -411,10 +408,7 @@ private fun VoiceMsgIndicator(
|
|||
sizeMultiplier,
|
||||
longClick,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.temporary_file_error),
|
||||
file.fileStatus.sndFileError.errorInfo
|
||||
)
|
||||
showFileErrorAlert(file.fileStatus.sndFileError, temporary = true)
|
||||
}
|
||||
)
|
||||
file?.fileStatus is CIFileStatus.RcvInvitation ->
|
||||
|
@ -430,10 +424,7 @@ private fun VoiceMsgIndicator(
|
|||
sizeMultiplier,
|
||||
longClick,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.file_error),
|
||||
file.fileStatus.rcvFileError.errorInfo
|
||||
)
|
||||
showFileErrorAlert(file.fileStatus.rcvFileError)
|
||||
}
|
||||
)
|
||||
file != null && file.fileStatus is CIFileStatus.RcvWarning ->
|
||||
|
@ -443,10 +434,7 @@ private fun VoiceMsgIndicator(
|
|||
sizeMultiplier,
|
||||
longClick,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.temporary_file_error),
|
||||
file.fileStatus.rcvFileError.errorInfo
|
||||
)
|
||||
showFileErrorAlert(file.fileStatus.rcvFileError, temporary = true)
|
||||
}
|
||||
)
|
||||
file != null && file.loaded && progress != null && duration != null ->
|
||||
|
|
|
@ -132,6 +132,8 @@
|
|||
<string name="for_chat_profile">For chat profile %s:</string>
|
||||
<string name="errors_in_servers_configuration">Errors in servers configuration.</string>
|
||||
<string name="error_accepting_operator_conditions">Error accepting conditions</string>
|
||||
<string name="blocking_reason_spam">Spam</string>
|
||||
<string name="blocking_reason_content">Content violates conditions of use</string>
|
||||
|
||||
<!-- API Error Responses - SimpleXAPI.kt -->
|
||||
<string name="connection_timeout">Connection timeout</string>
|
||||
|
@ -168,6 +170,8 @@
|
|||
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Please check that you used the correct link or ask your contact to send you another one.</string>
|
||||
<string name="connection_error_auth">Connection error (AUTH)</string>
|
||||
<string name="connection_error_auth_desc">Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection.</string>
|
||||
<string name="connection_error_blocked">Connection blocked</string>
|
||||
<string name="connection_error_blocked_desc">Connection is blocked by server operator:\n%1$s.</string>
|
||||
<string name="connection_error_quota">Undelivered messages</string>
|
||||
<string name="connection_error_quota_desc">The connection reached the limit of undelivered messages, your contact may be offline.</string>
|
||||
<string name="error_accepting_contact_request">Error accepting contact request</string>
|
||||
|
@ -323,6 +327,7 @@
|
|||
|
||||
<!-- CIFileStatus errors -->
|
||||
<string name="file_error_auth">Wrong key or unknown file chunk address - most likely file is deleted.</string>
|
||||
<string name="file_error_blocked">File is blocked by server operator:\n%1$s.</string>
|
||||
<string name="file_error_no_file">File not found - most likely file was deleted or cancelled.</string>
|
||||
<string name="file_error_relay">File server error: %1$s</string>
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
layout: layouts/article.html
|
||||
title: "SimpleX network: privacy preserving content moderation"
|
||||
date: 2024-12-18
|
||||
preview: How network operators prevent distribution of CSAM without compromising users privacy and security.
|
||||
# image: images/20241218-pub.jpg
|
||||
# imageWide: true
|
||||
draft: true
|
||||
permalink: "/blog/20250112-simplex-network-privacy-preserving-content-moderation.html"
|
||||
---
|
||||
|
||||
# SimpleX network: privacy preserving content moderation
|
||||
|
||||
**Will be published:** Jan 12, 2025
|
||||
|
||||
This blog post will cover our approach to removing CSAM that has:
|
||||
- NO user identification, thus preserving privacy of the users.
|
||||
- NO client- or server-side content scanning, thus preserving privacy and security of e2e encryption.
|
||||
|
||||
The current and future content restriction will only be applied based on the users' complaints, and only to the content that can be accessed by server operators via public channels.
|
||||
|
||||
Please read this document: https://github.com/simplex-chat/simplex-chat/blob/stable/docs/rfcs/2024-12-30-content-moderation.md
|
|
@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
|||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 9d9ec8cd0b171b2058c59c4e7292ccafa96b6e2b
|
||||
tag: 3d4e0b06c04a13555c55c2e0efde56f9f78e7ea1
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."9d9ec8cd0b171b2058c59c4e7292ccafa96b6e2b" = "0mvg9yrwb835vf2kz8k0ac4i7vzjpvbpcwg895n3kcfdkdcnxh14";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."3d4e0b06c04a13555c55c2e0efde56f9f78e7ea1" = "0l194fm6kxy54gkyz0lhvba3cxgjdg812qwpjki5kwfmhhliys6q";
|
||||
"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";
|
||||
|
|
|
@ -284,6 +284,7 @@ processAgentMsgSndFile _corrId aFileId msg = do
|
|||
agentFileError :: AgentErrorType -> FileError
|
||||
agentFileError = \case
|
||||
XFTP _ XFTP.AUTH -> FileErrAuth
|
||||
XFTP srv (XFTP.BLOCKED info) -> FileErrBlocked srv info
|
||||
FILE NO_FILE -> FileErrNoFile
|
||||
BROKER _ e -> brokerError FileErrRelay e
|
||||
e -> FileErrOther $ tshow e
|
||||
|
|
|
@ -52,7 +52,7 @@ import Simplex.Messaging.Crypto.File (CryptoFile (..))
|
|||
import qualified Simplex.Messaging.Crypto.File as CF
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, parseAll, sumTypeJSON)
|
||||
import Simplex.Messaging.Protocol (MsgBody)
|
||||
import Simplex.Messaging.Protocol (BlockingInfo, MsgBody)
|
||||
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>))
|
||||
#if defined(dbPostgres)
|
||||
import Database.PostgreSQL.Simple.FromField (FromField (..))
|
||||
|
@ -741,6 +741,7 @@ aciFileStatusJSON = \case
|
|||
|
||||
data FileError
|
||||
= FileErrAuth
|
||||
| FileErrBlocked {server :: String, blockInfo :: BlockingInfo}
|
||||
| FileErrNoFile
|
||||
| FileErrRelay {srvError :: SrvError}
|
||||
| FileErrOther {fileError :: Text}
|
||||
|
@ -749,14 +750,16 @@ data FileError
|
|||
instance StrEncoding FileError where
|
||||
strEncode = \case
|
||||
FileErrAuth -> "auth"
|
||||
FileErrBlocked srv info -> "blocked " <> strEncode (srv, info)
|
||||
FileErrNoFile -> "no_file"
|
||||
FileErrRelay srvErr -> "relay " <> strEncode srvErr
|
||||
FileErrOther e -> "other " <> encodeUtf8 e
|
||||
strP =
|
||||
A.takeWhile1 (/= ' ') >>= \case
|
||||
"auth" -> pure FileErrAuth
|
||||
"blocked" -> FileErrBlocked <$> _strP <*> _strP
|
||||
"no_file" -> pure FileErrNoFile
|
||||
"relay" -> FileErrRelay <$> (A.space *> strP)
|
||||
"relay" -> FileErrRelay <$> _strP
|
||||
"other" -> FileErrOther . safeDecodeUtf8 <$> (A.space *> A.takeByteString)
|
||||
s -> FileErrOther . safeDecodeUtf8 . (s <>) <$> A.takeByteString
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ import qualified Simplex.Messaging.Crypto.Ratchet as CR
|
|||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON)
|
||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, ProtocolServer (..), ProtocolTypeI, SProtocolType (..), UserProtocol)
|
||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, BlockingInfo (..), BlockingReason (..), ProtocolServer (..), ProtocolTypeI, SProtocolType (..), UserProtocol)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||
import Simplex.Messaging.Util (safeDecodeUtf8, tshow)
|
||||
|
@ -2223,6 +2223,12 @@ viewChatError isCmd logLevel testView = \case
|
|||
[ withConnEntity
|
||||
<> "error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection"
|
||||
]
|
||||
SMP _ (SMP.BLOCKED BlockingInfo {reason}) ->
|
||||
[withConnEntity <> "error: connection blocked by server operator: " <> reasonStr]
|
||||
where
|
||||
reasonStr = case reason of
|
||||
BRSpam -> "spam"
|
||||
BRContent -> "content violates conditions of use"
|
||||
BROKER _ NETWORK | not isCmd -> []
|
||||
BROKER _ TIMEOUT | not isCmd -> []
|
||||
AGENT A_DUPLICATE -> [withConnEntity <> "error: AGENT A_DUPLICATE" | logLevel == CLLDebug || isCmd]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue