mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-25 09:05:02 +00:00
core: attach badge proofs to files over size limit (#7455)
* core: attach badge proofs to files over size limit * types * more types * implement file badge proofs * tests * move file limits to config, add tests * more tests, work correctly in "send as group" case * fix races in tests * group badge tests * query plans * add history support, fixes * simplify * refactor * refactor * type * restructure schema for proofs * rename, refactor * refactor * fix, refactor * refactor * ui * comments * alerts * update nix, ios library * updare sharing * update simplexmq * update text * improve messages * api types * postgres schema * fix * test * query plans --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
This commit is contained in:
co-authored by
Evgeny @ SimpleX Chat
parent
47d32b674a
commit
5ffbe733a8
+9
-1
@@ -4245,6 +4245,13 @@ enum class MREmojiChar(val value: String) {
|
||||
@SerialName("✅") Check("✅");
|
||||
}
|
||||
|
||||
// set by the core when the file is above the size the sender's badge allows; badgeStatus is null when no proof was sent
|
||||
@Serializable
|
||||
data class FileProhibited(
|
||||
val maxSize: Long,
|
||||
val badgeStatus: BadgeStatus? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CIFile(
|
||||
val fileId: Long,
|
||||
@@ -4253,7 +4260,8 @@ data class CIFile(
|
||||
val fileSource: CryptoFile? = null,
|
||||
val fileStatus: CIFileStatus,
|
||||
val fileProtocol: FileProtocol,
|
||||
val fileExpires: Instant? = null
|
||||
val fileExpires: Instant? = null,
|
||||
val fileProhibited: FileProhibited? = null
|
||||
) {
|
||||
val expired: Boolean = fileExpires != null && fileExpires < Clock.System.now()
|
||||
|
||||
|
||||
+8
-4
@@ -115,7 +115,8 @@ data class ComposeState(
|
||||
val useLinkPreviews: Boolean,
|
||||
val mentions: MentionedMembers = emptyMap(),
|
||||
// the max file size the user may attach, raised by their active badge unless the chat is incognito; kept in sync on chat switch
|
||||
val maxFileSize: Long = getMaxFileSize(FileProtocol.XFTP)
|
||||
val maxFileSize: Long = getMaxFileSize(FileProtocol.XFTP),
|
||||
val sendIncognito: Boolean = false
|
||||
) {
|
||||
constructor(editingItem: ChatItem, liveMessage: LiveMessage? = null, useLinkPreviews: Boolean): this(
|
||||
ComposeMessage(
|
||||
@@ -331,7 +332,7 @@ fun MutableState<ComposeState>.processPickedFile(uri: URI?, text: String?) {
|
||||
} else if (fileSize != null) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize))
|
||||
largeFileMessage(fileSize, value.sendIncognito, expiredBadgeReason(fileSize, chatModel.currentUser.value?.profile))
|
||||
)
|
||||
} else {
|
||||
showWrongUriAlert()
|
||||
@@ -360,7 +361,7 @@ suspend fun MutableState<ComposeState>.processPickedMedia(uris: List<URI>, text:
|
||||
bitmap = null
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize))
|
||||
largeFileMessage(fileSize ?: 0, value.sendIncognito, expiredBadgeReason(fileSize ?: 0, chatModel.currentUser.value?.profile))
|
||||
)
|
||||
null
|
||||
}
|
||||
@@ -1420,7 +1421,10 @@ fun ComposeView(
|
||||
// keep the attach size limit in sync with the chat: the user's active badge raises it, but not in incognito chats where no badge is presented
|
||||
LaunchedEffect(chat.chatInfo) {
|
||||
val incognito = if (chat.chatInfo.profileChangeProhibited) chat.chatInfo.incognito else chatModel.controller.appPrefs.incognito.get()
|
||||
composeState.value = composeState.value.copy(maxFileSize = getMaxFileSize(FileProtocol.XFTP, if (incognito) null else chatModel.currentUser.value?.profile))
|
||||
composeState.value = composeState.value.copy(
|
||||
maxFileSize = getMaxFileSize(FileProtocol.XFTP, if (incognito) null else chatModel.currentUser.value?.profile),
|
||||
sendIncognito = incognito
|
||||
)
|
||||
}
|
||||
if (appPlatform.isDesktop) {
|
||||
// the same ComposeView is reused when switching chats, so `chat` captured by onDispose would be the chat opened first, not the current one
|
||||
|
||||
+20
-11
@@ -38,7 +38,6 @@ fun CIFileView(
|
||||
showTimestamp: Boolean,
|
||||
showMenu: MutableState<Boolean>,
|
||||
smallView: Boolean = false,
|
||||
senderProfile: LocalProfile?,
|
||||
receiveFile: (Long) -> Unit
|
||||
) {
|
||||
val saveFileLauncher = rememberSaveFileLauncher(ciFile = file)
|
||||
@@ -77,13 +76,11 @@ fun CIFileView(
|
||||
if (file != null) {
|
||||
when {
|
||||
file.fileStatus is CIFileStatus.RcvInvitation || file.fileStatus is CIFileStatus.RcvAborted -> {
|
||||
if (fileSizeValid(file, senderProfile)) {
|
||||
receiveFile(file.fileId)
|
||||
val prohibited = file.fileProhibited
|
||||
if (prohibited != null) {
|
||||
showProhibitedFileAlert(file, prohibited)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol, senderProfile)))
|
||||
)
|
||||
receiveFile(file.fileId)
|
||||
}
|
||||
}
|
||||
file.fileStatus is CIFileStatus.RcvAccepted ->
|
||||
@@ -157,7 +154,7 @@ fun CIFileView(
|
||||
is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
||||
is CIFileStatus.SndWarning -> fileIcon(innerIcon = painterResource(MR.images.ic_warning_filled))
|
||||
is CIFileStatus.RcvInvitation ->
|
||||
if (!fileSizeValid(file, senderProfile))
|
||||
if (!fileSizeValid(file))
|
||||
fileIcon(innerIcon = painterResource(MR.images.ic_priority_high), color = WarningOrange)
|
||||
else if (file.expired)
|
||||
fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
||||
@@ -239,9 +236,21 @@ fun CIFileView(
|
||||
}
|
||||
}
|
||||
|
||||
// whether a received file is within the size we accept from its sender
|
||||
fun fileSizeValid(file: CIFile, senderProfile: LocalProfile?): Boolean =
|
||||
file.fileSize <= getMaxFileSize(file.fileProtocol, senderProfile)
|
||||
// the core decides whether a received file is above the size the sender's badge allows
|
||||
fun fileSizeValid(file: CIFile): Boolean = file.fileProhibited == null
|
||||
|
||||
fun showProhibitedFileAlert(file: CIFile, prohibited: FileProhibited) {
|
||||
val badgeIssue = when (prohibited.badgeStatus) {
|
||||
null, BadgeStatus.Active -> ""
|
||||
BadgeStatus.Expired, BadgeStatus.ExpiredOld -> generalGetString(MR.strings.badge_expired)
|
||||
BadgeStatus.Failed -> generalGetString(MR.strings.badge_verification_failed)
|
||||
BadgeStatus.UnknownKey -> generalGetString(MR.strings.badge_no_key)
|
||||
}
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
largeFileMessage(file.fileSize, badgeIssue = badgeIssue)
|
||||
)
|
||||
}
|
||||
|
||||
fun showFileErrorAlert(err: FileError, file: CIFile? = null, temporary: Boolean = false) {
|
||||
val fileExpires = file?.fileExpires
|
||||
|
||||
+4
-8
@@ -38,7 +38,6 @@ fun CIImageView(
|
||||
imageProvider: () -> ImageGalleryProvider,
|
||||
showMenu: MutableState<Boolean>,
|
||||
smallView: Boolean,
|
||||
senderProfile: LocalProfile?,
|
||||
receiveFile: (Long) -> Unit
|
||||
) {
|
||||
val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) }
|
||||
@@ -84,7 +83,7 @@ fun CIImageView(
|
||||
is CIFileStatus.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.SndWarning -> fileIcon(painterResource(MR.images.ic_warning_filled), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.RcvInvitation ->
|
||||
if (file.expired && fileSizeValid(file, senderProfile))
|
||||
if (file.expired && fileSizeValid(file))
|
||||
fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
else
|
||||
fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_asked_to_receive)
|
||||
@@ -220,13 +219,10 @@ fun CIImageView(
|
||||
if (file != null) {
|
||||
when {
|
||||
file.fileStatus is CIFileStatus.RcvInvitation || file.fileStatus is CIFileStatus.RcvAborted ->
|
||||
if (fileSizeValid(file, senderProfile)) {
|
||||
receiveFile(file.fileId)
|
||||
if (file.fileProhibited != null) {
|
||||
showProhibitedFileAlert(file, file.fileProhibited)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol, senderProfile)))
|
||||
)
|
||||
receiveFile(file.fileId)
|
||||
}
|
||||
file.fileStatus is CIFileStatus.RcvAccepted ->
|
||||
when (file.fileProtocol) {
|
||||
|
||||
+10
-14
@@ -35,7 +35,6 @@ fun CIVideoView(
|
||||
imageProvider: () -> ImageGalleryProvider,
|
||||
showMenu: MutableState<Boolean>,
|
||||
smallView: Boolean = false,
|
||||
senderProfile: LocalProfile?,
|
||||
receiveFile: (Long) -> Unit
|
||||
) {
|
||||
val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) }
|
||||
@@ -99,7 +98,7 @@ fun CIVideoView(
|
||||
if (file != null) {
|
||||
when (file.fileStatus) {
|
||||
CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted ->
|
||||
receiveFileIfValidSize(file, senderProfile, receiveFile)
|
||||
receiveFileIfValidSize(file, receiveFile)
|
||||
CIFileStatus.RcvAccepted ->
|
||||
when (file.fileProtocol) {
|
||||
FileProtocol.XFTP ->
|
||||
@@ -129,16 +128,16 @@ fun CIVideoView(
|
||||
DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/)
|
||||
}
|
||||
if (showDownloadButton(file?.fileStatus) && !blurred.value && file != null) {
|
||||
PlayButton(error = false, sizeMultiplier, { showMenu.value = true }) { receiveFileIfValidSize(file, senderProfile, receiveFile) }
|
||||
PlayButton(error = false, sizeMultiplier, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) }
|
||||
}
|
||||
}
|
||||
}
|
||||
// Do not show download icon when the view is blurred
|
||||
if (!smallView && (!showDownloadButton(file?.fileStatus) || !blurred.value)) {
|
||||
fileStatusIcon(file, false, senderProfile)
|
||||
fileStatusIcon(file, false)
|
||||
} else if (smallView && file?.showStatusIconInSmallView == true) {
|
||||
Box(Modifier.align(Alignment.Center)) {
|
||||
fileStatusIcon(file, true, senderProfile)
|
||||
fileStatusIcon(file, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -486,7 +485,7 @@ private fun progressCircle(progress: Long, total: Long) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun fileStatusIcon(file: CIFile?, smallView: Boolean, senderProfile: LocalProfile?) {
|
||||
private fun fileStatusIcon(file: CIFile?, smallView: Boolean) {
|
||||
if (file != null) {
|
||||
Box(
|
||||
Modifier
|
||||
@@ -526,7 +525,7 @@ private fun fileStatusIcon(file: CIFile?, smallView: Boolean, senderProfile: Loc
|
||||
}
|
||||
)
|
||||
is CIFileStatus.RcvInvitation ->
|
||||
if (file.expired && fileSizeValid(file, senderProfile))
|
||||
if (file.expired && fileSizeValid(file))
|
||||
fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
else
|
||||
fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_video_asked_to_receive)
|
||||
@@ -565,14 +564,11 @@ private fun fileStatusIcon(file: CIFile?, smallView: Boolean, senderProfile: Loc
|
||||
private fun showDownloadButton(status: CIFileStatus?): Boolean =
|
||||
status is CIFileStatus.RcvInvitation || status is CIFileStatus.RcvAborted
|
||||
|
||||
private fun receiveFileIfValidSize(file: CIFile, senderProfile: LocalProfile?, receiveFile: (Long) -> Unit) {
|
||||
if (fileSizeValid(file, senderProfile)) {
|
||||
receiveFile(file.fileId)
|
||||
private fun receiveFileIfValidSize(file: CIFile, receiveFile: (Long) -> Unit) {
|
||||
if (file.fileProhibited != null) {
|
||||
showProhibitedFileAlert(file, file.fileProhibited)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol, senderProfile)))
|
||||
)
|
||||
receiveFile(file.fileId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -449,7 +449,7 @@ fun ChatItemView(
|
||||
}
|
||||
if (cItem.file != null && (getLoadedFilePath(cItem.file) != null || (chatModel.connectedToRemote() && cachedRemoteReqs[cItem.file.fileSource] != false && cItem.file.loaded))) {
|
||||
SaveContentItemAction(cItem, saveFileLauncher, showMenu)
|
||||
} else if (cItem.file != null && cItem.file.fileStatus is CIFileStatus.RcvInvitation && fileSizeValid(cItem.file, ciSenderProfile(cItem, chat.chatInfo))) {
|
||||
} else if (cItem.file != null && cItem.file.fileStatus is CIFileStatus.RcvInvitation && fileSizeValid(cItem.file)) {
|
||||
ItemAction(stringResource(MR.strings.download_file), painterResource(MR.images.ic_arrow_downward), onClick = {
|
||||
withBGApi {
|
||||
Log.d(TAG, "ChatItemView downloadFileAction")
|
||||
|
||||
+3
-3
@@ -219,7 +219,7 @@ fun FramedItemView(
|
||||
|
||||
@Composable
|
||||
fun ciFileView(ci: ChatItem, text: String) {
|
||||
CIFileView(ci.file, ci.meta, chatTTL, showViaProxy, showTimestamp, showMenu, false, ciSenderProfile(ci, chatInfo), receiveFile)
|
||||
CIFileView(ci.file, ci.meta, chatTTL, showViaProxy, showTimestamp, showMenu, false, receiveFile)
|
||||
if (text != "" || ci.meta.isLive) {
|
||||
CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
@@ -364,7 +364,7 @@ fun FramedItemView(
|
||||
} else {
|
||||
when (val mc = ci.content.msgContent) {
|
||||
is MsgContent.MCImage -> {
|
||||
CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, false, ciSenderProfile(ci, chatInfo), receiveFile)
|
||||
CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, false, receiveFile)
|
||||
if (mc.text == "" && !ci.meta.isLive) {
|
||||
metaColor = Color.White
|
||||
} else {
|
||||
@@ -372,7 +372,7 @@ fun FramedItemView(
|
||||
}
|
||||
}
|
||||
is MsgContent.MCVideo -> {
|
||||
CIVideoView(image = mc.image, mc.duration, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, smallView = false, senderProfile = ciSenderProfile(ci, chatInfo), receiveFile = receiveFile)
|
||||
CIVideoView(image = mc.image, mc.duration, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, smallView = false, receiveFile = receiveFile)
|
||||
if (mc.text == "" && !ci.meta.isLive) {
|
||||
metaColor = Color.White
|
||||
} else {
|
||||
|
||||
+3
-3
@@ -324,13 +324,13 @@ fun ChatPreviewView(
|
||||
}
|
||||
}
|
||||
is MsgContent.MCImage -> SmallContentPreview {
|
||||
CIImageView(image = mc.image, file = ci.file, provider, remember { mutableStateOf(false) }, smallView = true, senderProfile = ciSenderProfile(ci, chat.chatInfo)) {
|
||||
CIImageView(image = mc.image, file = ci.file, provider, remember { mutableStateOf(false) }, smallView = true) {
|
||||
val user = chatModel.currentUser.value ?: return@CIImageView
|
||||
withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) }
|
||||
}
|
||||
}
|
||||
is MsgContent.MCVideo -> SmallContentPreview {
|
||||
CIVideoView(image = mc.image, mc.duration, file = ci.file, provider, remember { mutableStateOf(false) }, smallView = true, senderProfile = ciSenderProfile(ci, chat.chatInfo)) {
|
||||
CIVideoView(image = mc.image, mc.duration, file = ci.file, provider, remember { mutableStateOf(false) }, smallView = true) {
|
||||
val user = chatModel.currentUser.value ?: return@CIVideoView
|
||||
withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) }
|
||||
}
|
||||
@@ -342,7 +342,7 @@ fun ChatPreviewView(
|
||||
}
|
||||
}
|
||||
is MsgContent.MCFile -> SmallContentPreviewFile {
|
||||
CIFileView(ci.file, ci.meta, cInfo.timedMessagesTTL, showViaProxy = false, showTimestamp = true, showMenu = remember { mutableStateOf(false) }, smallView = true, senderProfile = ciSenderProfile(ci, chat.chatInfo)) {
|
||||
CIFileView(ci.file, ci.meta, cInfo.timedMessagesTTL, showViaProxy = false, showTimestamp = true, showMenu = remember { mutableStateOf(false) }, smallView = true) {
|
||||
val user = chatModel.currentUser.value ?: return@CIFileView
|
||||
withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) }
|
||||
}
|
||||
|
||||
+39
-12
@@ -18,6 +18,7 @@ import com.charleskorn.kaml.decodeFromStream
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.serialization.encodeToString
|
||||
import java.io.*
|
||||
import java.net.URI
|
||||
@@ -27,6 +28,8 @@ import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.math.*
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
private val singleThreadDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
|
||||
@@ -123,10 +126,13 @@ const val MAX_FILE_SIZE_SMP: Long = 8000000
|
||||
|
||||
const val MAX_FILE_SIZE_XFTP: Long = 1_073_741_824 // 1GB
|
||||
|
||||
// raised XFTP receive limits for files from a sender with a supporter badge (also investor) or a legend badge
|
||||
// raised XFTP limits for a user with a supporter badge (also investor) or a legend badge
|
||||
const val MAX_FILE_SIZE_XFTP_SUPPORTER: Long = 2_147_483_648 // 2GB
|
||||
const val MAX_FILE_SIZE_XFTP_LEGEND: Long = 5_368_709_120 // 5GB
|
||||
|
||||
// a badge raises the limit at send for this long after its expiry, shorter than the receiver's grace
|
||||
val BADGE_SND_GRACE_INTERVAL: Duration = 1.days
|
||||
|
||||
const val MAX_FILE_SIZE_LOCAL: Long = Long.MAX_VALUE
|
||||
|
||||
expect fun getAppFileUri(fileName: String): URI
|
||||
@@ -474,25 +480,46 @@ fun directoryFileCountAndSize(dir: String): Pair<Int, Long> { // count, size in
|
||||
return fileCount to bytes
|
||||
}
|
||||
|
||||
fun badgeMaxFileSize(badge: LocalBadge): Long =
|
||||
if (badge.badge.badgeType == BadgeType.Legend) MAX_FILE_SIZE_XFTP_LEGEND else MAX_FILE_SIZE_XFTP_SUPPORTER
|
||||
|
||||
// a badge raises the limit at send until one day past its expiry, as the core applies it
|
||||
fun badgeActiveForSend(badge: LocalBadge): Boolean =
|
||||
badge.status == BadgeStatus.Active && badge.badge.badgeExpiry + BADGE_SND_GRACE_INTERVAL >= Clock.System.now()
|
||||
|
||||
// in incognito chats and above the largest badge's limit no badge applies, so badgeIssue is not used
|
||||
fun largeFileMessage(fileSize: Long, incognito: Boolean = false, badgeIssue: String = ""): String =
|
||||
if (incognito) {
|
||||
String.format(generalGetString(MR.strings.large_file_incognito), formatBytes(MAX_FILE_SIZE_XFTP))
|
||||
} else if (fileSize > MAX_FILE_SIZE_XFTP_LEGEND) {
|
||||
String.format(generalGetString(MR.strings.max_file_size_with_badge), formatBytes(MAX_FILE_SIZE_XFTP_LEGEND), generalGetString(MR.strings.legend_badge))
|
||||
} else {
|
||||
val supporter = fileSize <= MAX_FILE_SIZE_XFTP_SUPPORTER
|
||||
val message = String.format(
|
||||
generalGetString(MR.strings.large_file_requires_badge),
|
||||
generalGetString(if (supporter) MR.strings.supporter_badge else MR.strings.legend_badge),
|
||||
formatBytes(if (supporter) MAX_FILE_SIZE_XFTP else MAX_FILE_SIZE_XFTP_SUPPORTER)
|
||||
)
|
||||
if (badgeIssue.isEmpty()) message else message + " " + badgeIssue
|
||||
}
|
||||
|
||||
// the badge lapsed, and while active it would have allowed this file
|
||||
fun expiredBadgeReason(fileSize: Long, senderProfile: LocalProfile?): String {
|
||||
val badge = senderProfile?.localBadge
|
||||
return if (badge != null && !badgeActiveForSend(badge) && badgeMaxFileSize(badge) >= fileSize) {
|
||||
generalGetString(MR.strings.your_badge_expired)
|
||||
} else ""
|
||||
}
|
||||
|
||||
fun getMaxFileSize(fileProtocol: FileProtocol, senderProfile: LocalProfile? = null): Long = when (fileProtocol) {
|
||||
FileProtocol.SMP -> MAX_FILE_SIZE_SMP
|
||||
FileProtocol.LOCAL -> MAX_FILE_SIZE_LOCAL
|
||||
// a sender's active badge raises the XFTP limit: legend to 5GB, any other (supporter/investor) to 2GB
|
||||
FileProtocol.XFTP -> {
|
||||
val badge = senderProfile?.localBadge
|
||||
if (badge == null || badge.status != BadgeStatus.Active) MAX_FILE_SIZE_XFTP
|
||||
else if (badge.badge.badgeType == BadgeType.Legend) MAX_FILE_SIZE_XFTP_LEGEND
|
||||
else MAX_FILE_SIZE_XFTP_SUPPORTER
|
||||
if (badge != null && badgeActiveForSend(badge)) badgeMaxFileSize(badge) else MAX_FILE_SIZE_XFTP
|
||||
}
|
||||
}
|
||||
|
||||
// the profile of whoever sent a received chat item - the group member, or the direct chat's contact
|
||||
fun ciSenderProfile(ci: ChatItem, chatInfo: ChatInfo): LocalProfile? = when (val dir = ci.chatDir) {
|
||||
is CIDirection.GroupRcv -> dir.groupMember.memberProfile
|
||||
is CIDirection.DirectRcv -> (chatInfo as? ChatInfo.Direct)?.contact?.profile
|
||||
else -> null
|
||||
}
|
||||
|
||||
expect suspend fun getBitmapFromVideo(uri: URI, timestamp: Long? = null, random: Boolean = true, withAlertOnException: Boolean = true): VideoPlayerInterface.PreviewAndDuration
|
||||
|
||||
// Whether the file really contains a video track. Reads container metadata only, without decoding a frame.
|
||||
|
||||
@@ -687,6 +687,15 @@
|
||||
<string name="icon_descr_file">File</string>
|
||||
<string name="large_file">Large file!</string>
|
||||
<string name="contact_sent_large_file">Your contact sent a file that is larger than currently supported maximum size (%1$s).</string>
|
||||
<string name="supporter_badge">supporter badge</string>
|
||||
<string name="legend_badge">legend badge</string>
|
||||
<string name="large_file_requires_badge">You need a %1$s to send files larger than %2$s.</string>
|
||||
<string name="max_file_size_with_badge">Maximum supported file size is %1$s, with a %2$s.</string>
|
||||
<string name="badge_expired">Contact\'s badge expired.</string>
|
||||
<string name="badge_verification_failed">Contact\'s badge verification failed.</string>
|
||||
<string name="badge_no_key">No key to verify contact\'s badge.</string>
|
||||
<string name="your_badge_expired">Your badge expired.</string>
|
||||
<string name="large_file_incognito">Files larger than %1$s cannot be sent in incognito chats.</string>
|
||||
<string name="maximum_supported_file_size">Currently maximum supported file size is %1$s.</string>
|
||||
<string name="waiting_for_file">Waiting for file</string>
|
||||
<string name="file_will_be_received_when_contact_completes_uploading">File will be received when your contact completes uploading it.</string>
|
||||
|
||||
Reference in New Issue
Block a user