Adds sensitive-content tags to NIP94 and NIP95 media
This commit is contained in:
@@ -474,6 +474,7 @@ class Account(
|
|||||||
dimensions = headerInfo.dim,
|
dimensions = headerInfo.dim,
|
||||||
blurhash = headerInfo.blurHash,
|
blurhash = headerInfo.blurHash,
|
||||||
description = headerInfo.description,
|
description = headerInfo.description,
|
||||||
|
sensitiveContent = headerInfo.sensitiveContent,
|
||||||
privateKey = loggedIn.privKey!!
|
privateKey = loggedIn.privKey!!
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -503,6 +504,7 @@ class Account(
|
|||||||
dimensions = headerInfo.dim,
|
dimensions = headerInfo.dim,
|
||||||
blurhash = headerInfo.blurHash,
|
blurhash = headerInfo.blurHash,
|
||||||
description = headerInfo.description,
|
description = headerInfo.description,
|
||||||
|
sensitiveContent = headerInfo.sensitiveContent,
|
||||||
privateKey = loggedIn.privKey!!
|
privateKey = loggedIn.privKey!!
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -16,15 +16,16 @@ class FileHeader(
|
|||||||
val size: Int,
|
val size: Int,
|
||||||
val dim: String?,
|
val dim: String?,
|
||||||
val blurHash: String?,
|
val blurHash: String?,
|
||||||
val description: String? = null
|
val description: String? = null,
|
||||||
|
val sensitiveContent: Boolean = false
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
suspend fun prepare(fileUrl: String, mimeType: String?, description: String?, onReady: (FileHeader) -> Unit, onError: () -> Unit) {
|
suspend fun prepare(fileUrl: String, mimeType: String?, description: String?, sensitiveContent: Boolean, onReady: (FileHeader) -> Unit, onError: () -> Unit) {
|
||||||
try {
|
try {
|
||||||
val imageData: ByteArray? = ImageDownloader().waitAndGetImage(fileUrl)
|
val imageData: ByteArray? = ImageDownloader().waitAndGetImage(fileUrl)
|
||||||
|
|
||||||
if (imageData != null) {
|
if (imageData != null) {
|
||||||
prepare(imageData, fileUrl, mimeType, description, onReady, onError)
|
prepare(imageData, fileUrl, mimeType, description, sensitiveContent, onReady, onError)
|
||||||
} else {
|
} else {
|
||||||
onError()
|
onError()
|
||||||
}
|
}
|
||||||
@@ -39,6 +40,7 @@ class FileHeader(
|
|||||||
fileUrl: String,
|
fileUrl: String,
|
||||||
mimeType: String?,
|
mimeType: String?,
|
||||||
description: String?,
|
description: String?,
|
||||||
|
sensitiveContent: Boolean,
|
||||||
onReady: (FileHeader) -> Unit,
|
onReady: (FileHeader) -> Unit,
|
||||||
onError: () -> Unit
|
onError: () -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -79,7 +81,7 @@ class FileHeader(
|
|||||||
Pair(null, null)
|
Pair(null, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
onReady(FileHeader(fileUrl, mimeType, hash, size, dim, blurHash, description))
|
onReady(FileHeader(fileUrl, mimeType, hash, size, dim, blurHash, description, sensitiveContent))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}")
|
Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}")
|
||||||
onError()
|
onError()
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class FileHeaderEvent(
|
|||||||
magnetURI: String? = null,
|
magnetURI: String? = null,
|
||||||
torrentInfoHash: String? = null,
|
torrentInfoHash: String? = null,
|
||||||
encryptionKey: AESGCM? = null,
|
encryptionKey: AESGCM? = null,
|
||||||
|
sensitiveContent: Boolean? = null,
|
||||||
privateKey: ByteArray,
|
privateKey: ByteArray,
|
||||||
createdAt: Long = Date().time / 1000
|
createdAt: Long = Date().time / 1000
|
||||||
): FileHeaderEvent {
|
): FileHeaderEvent {
|
||||||
@@ -62,7 +63,14 @@ class FileHeaderEvent(
|
|||||||
blurhash?.let { listOf(BLUR_HASH, it) },
|
blurhash?.let { listOf(BLUR_HASH, it) },
|
||||||
magnetURI?.let { listOf(MAGNET_URI, it) },
|
magnetURI?.let { listOf(MAGNET_URI, it) },
|
||||||
torrentInfoHash?.let { listOf(TORRENT_INFOHASH, it) },
|
torrentInfoHash?.let { listOf(TORRENT_INFOHASH, it) },
|
||||||
encryptionKey?.let { listOf(ENCRYPTION_KEY, it.key, it.nonce) }
|
encryptionKey?.let { listOf(ENCRYPTION_KEY, it.key, it.nonce) },
|
||||||
|
sensitiveContent?.let {
|
||||||
|
if (it) {
|
||||||
|
listOf("content-warning", "")
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val content = description ?: ""
|
val content = description ?: ""
|
||||||
|
|||||||
+9
-1
@@ -50,6 +50,7 @@ class FileStorageHeaderEvent(
|
|||||||
magnetURI: String? = null,
|
magnetURI: String? = null,
|
||||||
torrentInfoHash: String? = null,
|
torrentInfoHash: String? = null,
|
||||||
encryptionKey: AESGCM? = null,
|
encryptionKey: AESGCM? = null,
|
||||||
|
sensitiveContent: Boolean? = null,
|
||||||
privateKey: ByteArray,
|
privateKey: ByteArray,
|
||||||
createdAt: Long = Date().time / 1000
|
createdAt: Long = Date().time / 1000
|
||||||
): FileStorageHeaderEvent {
|
): FileStorageHeaderEvent {
|
||||||
@@ -62,7 +63,14 @@ class FileStorageHeaderEvent(
|
|||||||
blurhash?.let { listOf(BLUR_HASH, it) },
|
blurhash?.let { listOf(BLUR_HASH, it) },
|
||||||
magnetURI?.let { listOf(MAGNET_URI, it) },
|
magnetURI?.let { listOf(MAGNET_URI, it) },
|
||||||
torrentInfoHash?.let { listOf(TORRENT_INFOHASH, it) },
|
torrentInfoHash?.let { listOf(TORRENT_INFOHASH, it) },
|
||||||
encryptionKey?.let { listOf(ENCRYPTION_KEY, it.key, it.nonce) }
|
encryptionKey?.let { listOf(ENCRYPTION_KEY, it.key, it.nonce) },
|
||||||
|
sensitiveContent?.let {
|
||||||
|
if (it) {
|
||||||
|
listOf("content-warning", "")
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val content = description ?: ""
|
val content = description ?: ""
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ open class NewMediaModel : ViewModel() {
|
|||||||
|
|
||||||
var selectedServer by mutableStateOf<ServersAvailable?>(null)
|
var selectedServer by mutableStateOf<ServersAvailable?>(null)
|
||||||
var description by mutableStateOf("")
|
var description by mutableStateOf("")
|
||||||
|
var sensitiveContent by mutableStateOf(false)
|
||||||
|
|
||||||
// Images and Videos
|
// Images and Videos
|
||||||
var galleryUri by mutableStateOf<Uri?>(null)
|
var galleryUri by mutableStateOf<Uri?>(null)
|
||||||
@@ -77,7 +78,7 @@ open class NewMediaModel : ViewModel() {
|
|||||||
uploadingPercentage.value = 0.2f
|
uploadingPercentage.value = 0.2f
|
||||||
uploadingDescription.value = "Loading"
|
uploadingDescription.value = "Loading"
|
||||||
contentResolver.openInputStream(fileUri)?.use {
|
contentResolver.openInputStream(fileUri)?.use {
|
||||||
createNIP95Record(it.readBytes(), contentType, description)
|
createNIP95Record(it.readBytes(), contentType, description, sensitiveContent)
|
||||||
}
|
}
|
||||||
?: run {
|
?: run {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -97,7 +98,7 @@ open class NewMediaModel : ViewModel() {
|
|||||||
server = serverToUse,
|
server = serverToUse,
|
||||||
contentResolver = contentResolver,
|
contentResolver = contentResolver,
|
||||||
onSuccess = { imageUrl, mimeType ->
|
onSuccess = { imageUrl, mimeType ->
|
||||||
createNIP94Record(imageUrl, mimeType, description)
|
createNIP94Record(imageUrl, mimeType, description, sensitiveContent)
|
||||||
},
|
},
|
||||||
onError = {
|
onError = {
|
||||||
isUploadingImage = false
|
isUploadingImage = false
|
||||||
@@ -137,7 +138,7 @@ open class NewMediaModel : ViewModel() {
|
|||||||
return !isUploadingImage && galleryUri != null && selectedServer != null
|
return !isUploadingImage && galleryUri != null && selectedServer != null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createNIP94Record(imageUrl: String, mimeType: String?, description: String) {
|
fun createNIP94Record(imageUrl: String, mimeType: String?, description: String, sensitiveContent: Boolean) {
|
||||||
uploadingPercentage.value = 0.40f
|
uploadingPercentage.value = 0.40f
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
uploadingDescription.value = "Server Processing"
|
uploadingDescription.value = "Server Processing"
|
||||||
@@ -157,6 +158,7 @@ open class NewMediaModel : ViewModel() {
|
|||||||
imageUrl,
|
imageUrl,
|
||||||
mimeType,
|
mimeType,
|
||||||
description,
|
description,
|
||||||
|
sensitiveContent,
|
||||||
onReady = {
|
onReady = {
|
||||||
uploadingPercentage.value = 0.90f
|
uploadingPercentage.value = 0.90f
|
||||||
uploadingDescription.value = "Sending"
|
uploadingDescription.value = "Sending"
|
||||||
@@ -189,7 +191,7 @@ open class NewMediaModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createNIP95Record(bytes: ByteArray, mimeType: String?, description: String) {
|
fun createNIP95Record(bytes: ByteArray, mimeType: String?, description: String, sensitiveContent: Boolean) {
|
||||||
uploadingPercentage.value = 0.30f
|
uploadingPercentage.value = 0.30f
|
||||||
uploadingDescription.value = "Hashing"
|
uploadingDescription.value = "Hashing"
|
||||||
|
|
||||||
@@ -199,6 +201,7 @@ open class NewMediaModel : ViewModel() {
|
|||||||
"",
|
"",
|
||||||
mimeType,
|
mimeType,
|
||||||
description,
|
description,
|
||||||
|
sensitiveContent,
|
||||||
onReady = {
|
onReady = {
|
||||||
uploadingDescription.value = "Signing"
|
uploadingDescription.value = "Signing"
|
||||||
uploadingPercentage.value = 0.40f
|
uploadingPercentage.value = 0.40f
|
||||||
|
|||||||
@@ -203,6 +203,18 @@ fun ImageVideoPost(postViewModel: NewMediaModel, acc: Account) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
SettingSwitchItem(
|
||||||
|
checked = postViewModel.sensitiveContent,
|
||||||
|
onCheckedChange = { postViewModel.sensitiveContent = it },
|
||||||
|
title = R.string.add_sensitive_content_label,
|
||||||
|
description = R.string.add_sensitive_content_description
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (isNIP94Server(postViewModel.selectedServer) ||
|
if (isNIP94Server(postViewModel.selectedServer) ||
|
||||||
postViewModel.selectedServer == ServersAvailable.NIP95
|
postViewModel.selectedServer == ServersAvailable.NIP95
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.*
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.selection.toggleable
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
@@ -41,6 +42,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
@@ -52,11 +54,13 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextDirection
|
import androidx.compose.ui.text.style.TextDirection
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
@@ -257,8 +261,8 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
|||||||
ImageVideoDescription(
|
ImageVideoDescription(
|
||||||
url,
|
url,
|
||||||
account.defaultFileServer,
|
account.defaultFileServer,
|
||||||
onAdd = { description, server ->
|
onAdd = { description, server, sensitiveContent ->
|
||||||
postViewModel.upload(url, description, server, context)
|
postViewModel.upload(url, description, sensitiveContent, server, context)
|
||||||
account.changeDefaultFileServer(server)
|
account.changeDefaultFileServer(server)
|
||||||
},
|
},
|
||||||
onCancel = {
|
onCancel = {
|
||||||
@@ -830,7 +834,7 @@ enum class ServersAvailable {
|
|||||||
fun ImageVideoDescription(
|
fun ImageVideoDescription(
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
defaultServer: ServersAvailable,
|
defaultServer: ServersAvailable,
|
||||||
onAdd: (String, ServersAvailable) -> Unit,
|
onAdd: (String, ServersAvailable, Boolean) -> Unit,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
onError: (String) -> Unit
|
onError: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -859,6 +863,7 @@ fun ImageVideoDescription(
|
|||||||
|
|
||||||
var selectedServer by remember { mutableStateOf(defaultServer) }
|
var selectedServer by remember { mutableStateOf(defaultServer) }
|
||||||
var message by remember { mutableStateOf("") }
|
var message by remember { mutableStateOf("") }
|
||||||
|
var sensitiveContent by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -983,6 +988,18 @@ fun ImageVideoDescription(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
SettingSwitchItem(
|
||||||
|
checked = sensitiveContent,
|
||||||
|
onCheckedChange = { sensitiveContent = it },
|
||||||
|
title = R.string.add_sensitive_content_label,
|
||||||
|
description = R.string.add_sensitive_content_description
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (isNIP94Server(selectedServer) ||
|
if (isNIP94Server(selectedServer) ||
|
||||||
selectedServer == ServersAvailable.NIP95
|
selectedServer == ServersAvailable.NIP95
|
||||||
) {
|
) {
|
||||||
@@ -1017,7 +1034,7 @@ fun ImageVideoDescription(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 10.dp),
|
.padding(vertical = 10.dp),
|
||||||
onClick = {
|
onClick = {
|
||||||
onAdd(message, selectedServer)
|
onAdd(message, selectedServer, sensitiveContent)
|
||||||
},
|
},
|
||||||
shape = QuoteBorder,
|
shape = QuoteBorder,
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
@@ -1036,3 +1053,54 @@ data class ImmutableListOfLists<T>(val lists: List<List<T>> = emptyList())
|
|||||||
fun List<List<String>>.toImmutableListOfLists(): ImmutableListOfLists<String> {
|
fun List<List<String>>.toImmutableListOfLists(): ImmutableListOfLists<String> {
|
||||||
return ImmutableListOfLists(this)
|
return ImmutableListOfLists(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingSwitchItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
|
title: Int,
|
||||||
|
description: Int,
|
||||||
|
enabled: Boolean = true
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
.toggleable(
|
||||||
|
value = checked,
|
||||||
|
enabled = enabled,
|
||||||
|
role = Role.Switch,
|
||||||
|
onValueChange = onCheckedChange
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1.0f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(3.dp)
|
||||||
|
) {
|
||||||
|
val contentAlpha = if (enabled) ContentAlpha.high else ContentAlpha.disabled
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = title),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.alpha(contentAlpha)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = description),
|
||||||
|
style = MaterialTheme.typography.caption,
|
||||||
|
color = Color.Gray,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.alpha(contentAlpha)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Switch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = null,
|
||||||
|
enabled = enabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ open class NewPostViewModel() : ViewModel() {
|
|||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun upload(galleryUri: Uri, description: String, server: ServersAvailable, context: Context) {
|
fun upload(galleryUri: Uri, description: String, sensitiveContent: Boolean, server: ServersAvailable, context: Context) {
|
||||||
isUploadingImage = true
|
isUploadingImage = true
|
||||||
contentToAddUrl = null
|
contentToAddUrl = null
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ open class NewPostViewModel() : ViewModel() {
|
|||||||
onReady = { fileUri, contentType, size ->
|
onReady = { fileUri, contentType, size ->
|
||||||
if (server == ServersAvailable.NIP95) {
|
if (server == ServersAvailable.NIP95) {
|
||||||
contentResolver.openInputStream(fileUri)?.use {
|
contentResolver.openInputStream(fileUri)?.use {
|
||||||
createNIP95Record(it.readBytes(), contentType, description)
|
createNIP95Record(it.readBytes(), contentType, description, sensitiveContent)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ImageUploader.uploadImage(
|
ImageUploader.uploadImage(
|
||||||
@@ -205,7 +205,7 @@ open class NewPostViewModel() : ViewModel() {
|
|||||||
contentResolver = contentResolver,
|
contentResolver = contentResolver,
|
||||||
onSuccess = { imageUrl, mimeType ->
|
onSuccess = { imageUrl, mimeType ->
|
||||||
if (isNIP94Server(server)) {
|
if (isNIP94Server(server)) {
|
||||||
createNIP94Record(imageUrl, mimeType, description)
|
createNIP94Record(imageUrl, mimeType, description, sensitiveContent)
|
||||||
} else {
|
} else {
|
||||||
isUploadingImage = false
|
isUploadingImage = false
|
||||||
message = TextFieldValue(message.text + "\n\n" + imageUrl)
|
message = TextFieldValue(message.text + "\n\n" + imageUrl)
|
||||||
@@ -370,13 +370,14 @@ open class NewPostViewModel() : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createNIP94Record(imageUrl: String, mimeType: String?, description: String) {
|
fun createNIP94Record(imageUrl: String, mimeType: String?, description: String, sensitiveContent: Boolean) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
// Images don't seem to be ready immediately after upload
|
// Images don't seem to be ready immediately after upload
|
||||||
FileHeader.prepare(
|
FileHeader.prepare(
|
||||||
imageUrl,
|
imageUrl,
|
||||||
mimeType,
|
mimeType,
|
||||||
description,
|
description,
|
||||||
|
sensitiveContent,
|
||||||
onReady = {
|
onReady = {
|
||||||
val note = account?.sendHeader(it)
|
val note = account?.sendHeader(it)
|
||||||
|
|
||||||
@@ -400,13 +401,14 @@ open class NewPostViewModel() : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createNIP95Record(bytes: ByteArray, mimeType: String?, description: String) {
|
fun createNIP95Record(bytes: ByteArray, mimeType: String?, description: String, sensitiveContent: Boolean) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
FileHeader.prepare(
|
FileHeader.prepare(
|
||||||
bytes,
|
bytes,
|
||||||
"",
|
"",
|
||||||
mimeType,
|
mimeType,
|
||||||
description,
|
description,
|
||||||
|
sensitiveContent,
|
||||||
onReady = {
|
onReady = {
|
||||||
val nip95 = account?.createNip95(bytes, headerInfo = it)
|
val nip95 = account?.createNip95(bytes, headerInfo = it)
|
||||||
val note = nip95?.let { it1 -> account?.sendNip95(it1.first, it1.second) }
|
val note = nip95?.let { it1 -> account?.sendNip95(it1.first, it1.second) }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.components
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -75,12 +76,14 @@ fun SensitivityWarning(
|
|||||||
mutableStateOf(accountState?.account?.showSensitiveContent != true)
|
mutableStateOf(accountState?.account?.showSensitiveContent != true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showContentWarningNote) {
|
Crossfade(targetState = showContentWarningNote) {
|
||||||
ContentWarningNote() {
|
if (it) {
|
||||||
showContentWarningNote = false
|
ContentWarningNote() {
|
||||||
|
showContentWarningNote = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
content()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
|
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifier
|
import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifierWithPadding
|
import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifierWithPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.bitcoinColor
|
||||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||||
import com.vitorpamplona.amethyst.ui.theme.overPictureBackground
|
import com.vitorpamplona.amethyst.ui.theme.overPictureBackground
|
||||||
import com.vitorpamplona.amethyst.ui.theme.profile35dpModifier
|
import com.vitorpamplona.amethyst.ui.theme.profile35dpModifier
|
||||||
@@ -445,7 +446,7 @@ fun CrossfadeToDisplayAmount(authorComment: MutableState<ZapAmountCommentNotific
|
|||||||
Text(
|
Text(
|
||||||
text = it,
|
text = it,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colors.secondaryVariant,
|
color = MaterialTheme.colors.bitcoinColor,
|
||||||
fontSize = commentTextSize,
|
fontSize = commentTextSize,
|
||||||
modifier = bottomPadding1dp
|
modifier = bottomPadding1dp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -447,8 +447,8 @@ fun NormalNote(
|
|||||||
nav = nav
|
nav = nav
|
||||||
)
|
)
|
||||||
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
|
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
|
||||||
is FileHeaderEvent -> FileHeaderDisplay(baseNote)
|
is FileHeaderEvent -> FileHeaderDisplay(baseNote, accountViewModel)
|
||||||
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote)
|
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, accountViewModel)
|
||||||
else ->
|
else ->
|
||||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
||||||
CheckNewAndRenderNote(
|
CheckNewAndRenderNote(
|
||||||
@@ -2870,7 +2870,7 @@ private fun RenderBadge(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FileHeaderDisplay(note: Note) {
|
fun FileHeaderDisplay(note: Note, accountViewModel: AccountViewModel) {
|
||||||
val event = (note.event as? FileHeaderEvent) ?: return
|
val event = (note.event as? FileHeaderEvent) ?: return
|
||||||
val fullUrl = event.url() ?: return
|
val fullUrl = event.url() ?: return
|
||||||
|
|
||||||
@@ -2901,13 +2901,15 @@ fun FileHeaderDisplay(note: Note) {
|
|||||||
|
|
||||||
Crossfade(targetState = content) {
|
Crossfade(targetState = content) {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
ZoomableContentView(content = it)
|
SensitivityWarning(note = note, accountViewModel = accountViewModel) {
|
||||||
|
ZoomableContentView(content = it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FileStorageHeaderDisplay(baseNote: Note) {
|
fun FileStorageHeaderDisplay(baseNote: Note, accountViewModel: AccountViewModel) {
|
||||||
val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return
|
val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return
|
||||||
|
|
||||||
var fileNote by remember { mutableStateOf<Note?>(null) }
|
var fileNote by remember { mutableStateOf<Note?>(null) }
|
||||||
@@ -2925,20 +2927,21 @@ fun FileStorageHeaderDisplay(baseNote: Note) {
|
|||||||
|
|
||||||
Crossfade(targetState = fileNote) {
|
Crossfade(targetState = fileNote) {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
RenderNIP95(it, eventHeader, baseNote)
|
RenderNIP95(it, eventHeader, baseNote, accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RenderNIP95(
|
private fun RenderNIP95(
|
||||||
it: Note,
|
content: Note,
|
||||||
eventHeader: FileStorageHeaderEvent,
|
eventHeader: FileStorageHeaderEvent,
|
||||||
baseNote: Note
|
header: Note,
|
||||||
|
accountViewModel: AccountViewModel
|
||||||
) {
|
) {
|
||||||
val appContext = LocalContext.current.applicationContext
|
val appContext = LocalContext.current.applicationContext
|
||||||
|
|
||||||
val noteState by it.live().metadata.observeAsState()
|
val noteState by content.live().metadata.observeAsState()
|
||||||
val note = remember(noteState) { noteState?.note }
|
val note = remember(noteState) { noteState?.note }
|
||||||
|
|
||||||
var content by remember { mutableStateOf<ZoomableContent?>(null) }
|
var content by remember { mutableStateOf<ZoomableContent?>(null) }
|
||||||
@@ -2946,7 +2949,7 @@ private fun RenderNIP95(
|
|||||||
if (content == null) {
|
if (content == null) {
|
||||||
LaunchedEffect(key1 = eventHeader.id, key2 = noteState, key3 = note?.event) {
|
LaunchedEffect(key1 = eventHeader.id, key2 = noteState, key3 = note?.event) {
|
||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
val uri = "nostr:" + baseNote.toNEvent()
|
val uri = "nostr:" + header.toNEvent()
|
||||||
val localDir =
|
val localDir =
|
||||||
note?.idHex?.let { File(File(appContext.externalCacheDir, "NIP95"), it) }
|
note?.idHex?.let { File(File(appContext.externalCacheDir, "NIP95"), it) }
|
||||||
val blurHash = eventHeader.blurhash()
|
val blurHash = eventHeader.blurhash()
|
||||||
@@ -2984,7 +2987,9 @@ private fun RenderNIP95(
|
|||||||
|
|
||||||
Crossfade(targetState = content) {
|
Crossfade(targetState = content) {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
ZoomableContentView(content = it)
|
SensitivityWarning(note = header, accountViewModel = accountViewModel) {
|
||||||
|
ZoomableContentView(content = it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -399,9 +399,9 @@ fun NoteMaster(
|
|||||||
if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && note.channelHex() != null) {
|
if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && note.channelHex() != null) {
|
||||||
ChannelHeader(channelHex = note.channelHex()!!, showVideo = true, showBottomDiviser = false, accountViewModel = accountViewModel, nav = nav)
|
ChannelHeader(channelHex = note.channelHex()!!, showVideo = true, showBottomDiviser = false, accountViewModel = accountViewModel, nav = nav)
|
||||||
} else if (noteEvent is FileHeaderEvent) {
|
} else if (noteEvent is FileHeaderEvent) {
|
||||||
FileHeaderDisplay(baseNote)
|
FileHeaderDisplay(baseNote, accountViewModel)
|
||||||
} else if (noteEvent is FileStorageHeaderEvent) {
|
} else if (noteEvent is FileStorageHeaderEvent) {
|
||||||
FileStorageHeaderDisplay(baseNote)
|
FileStorageHeaderDisplay(baseNote, accountViewModel)
|
||||||
} else if (noteEvent is PeopleListEvent) {
|
} else if (noteEvent is PeopleListEvent) {
|
||||||
DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav)
|
DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav)
|
||||||
} else if (noteEvent is AudioTrackEvent) {
|
} else if (noteEvent is AudioTrackEvent) {
|
||||||
|
|||||||
@@ -421,7 +421,7 @@ fun EditFieldRow(
|
|||||||
accountViewModel.account.defaultFileServer
|
accountViewModel.account.defaultFileServer
|
||||||
}
|
}
|
||||||
|
|
||||||
channelScreenModel.upload(it, "", fileServer, context)
|
channelScreenModel.upload(it, "", false, fileServer, context)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = TextFieldDefaults.textFieldColors(
|
colors = TextFieldDefaults.textFieldColors(
|
||||||
|
|||||||
@@ -259,16 +259,16 @@ private fun RenderVideoOrPictureNote(
|
|||||||
Row(remember { Modifier.weight(1f) }, verticalAlignment = Alignment.CenterVertically) {
|
Row(remember { Modifier.weight(1f) }, verticalAlignment = Alignment.CenterVertically) {
|
||||||
val noteEvent = remember { note.event }
|
val noteEvent = remember { note.event }
|
||||||
if (noteEvent is FileHeaderEvent) {
|
if (noteEvent is FileHeaderEvent) {
|
||||||
FileHeaderDisplay(note)
|
FileHeaderDisplay(note, accountViewModel)
|
||||||
} else if (noteEvent is FileStorageHeaderEvent) {
|
} else if (noteEvent is FileStorageHeaderEvent) {
|
||||||
FileStorageHeaderDisplay(note)
|
FileStorageHeaderDisplay(note, accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(verticalAlignment = Alignment.Bottom, modifier = remember { Modifier.fillMaxSize(1f) }) {
|
Row(verticalAlignment = Alignment.Bottom, modifier = remember { Modifier.fillMaxSize(1f) }) {
|
||||||
Column(remember { Modifier.weight(1f) }) {
|
Column(remember { Modifier.weight(1f) }) {
|
||||||
RenderVideoOrPicture(note, nav, accountViewModel)
|
RenderAuthorInformation(note, nav, accountViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
@@ -287,7 +287,7 @@ private fun RenderVideoOrPictureNote(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RenderVideoOrPicture(
|
private fun RenderAuthorInformation(
|
||||||
note: Note,
|
note: Note,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
accountViewModel: AccountViewModel
|
accountViewModel: AccountViewModel
|
||||||
|
|||||||
@@ -32,16 +32,19 @@ private val DarkColorPalette = darkColors(
|
|||||||
primary = Purple200,
|
primary = Purple200,
|
||||||
primaryVariant = Purple700,
|
primaryVariant = Purple700,
|
||||||
secondary = Teal200,
|
secondary = Teal200,
|
||||||
secondaryVariant = Color(0xFFF7931A)
|
secondaryVariant = Purple200
|
||||||
)
|
)
|
||||||
|
|
||||||
private val LightColorPalette = lightColors(
|
private val LightColorPalette = lightColors(
|
||||||
primary = Purple500,
|
primary = Purple500,
|
||||||
primaryVariant = Purple700,
|
primaryVariant = Purple700,
|
||||||
secondary = Teal200,
|
secondary = Teal200,
|
||||||
secondaryVariant = Color(0xFFB66605)
|
secondaryVariant = Purple500
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val BitcoinDark = Color(0xFFF7931A)
|
||||||
|
private val BitcoinLight = Color(0xFFB66605)
|
||||||
|
|
||||||
private val DarkNewItemBackground = DarkColorPalette.primary.copy(0.12f)
|
private val DarkNewItemBackground = DarkColorPalette.primary.copy(0.12f)
|
||||||
private val LightNewItemBackground = LightColorPalette.primary.copy(0.12f)
|
private val LightNewItemBackground = LightColorPalette.primary.copy(0.12f)
|
||||||
|
|
||||||
@@ -279,6 +282,9 @@ val Colors.hashVerified: Color
|
|||||||
val Colors.overPictureBackground: Color
|
val Colors.overPictureBackground: Color
|
||||||
get() = if (isLight) LightOverPictureBackground else DarkOverPictureBackground
|
get() = if (isLight) LightOverPictureBackground else DarkOverPictureBackground
|
||||||
|
|
||||||
|
val Colors.bitcoinColor: Color
|
||||||
|
get() = if (isLight) BitcoinLight else BitcoinDark
|
||||||
|
|
||||||
val Colors.markdownStyle: RichTextStyle
|
val Colors.markdownStyle: RichTextStyle
|
||||||
get() = if (isLight) MarkDownStyleOnLight else MarkDownStyleOnDark
|
get() = if (isLight) MarkDownStyleOnLight else MarkDownStyleOnDark
|
||||||
|
|
||||||
|
|||||||
@@ -480,4 +480,7 @@
|
|||||||
|
|
||||||
<string name="groups_no_descriptor">This group does not have a description or rules. Talk to the owner to add one</string>
|
<string name="groups_no_descriptor">This group does not have a description or rules. Talk to the owner to add one</string>
|
||||||
<string name="community_no_descriptor">This community does not have a description. Talk to the owner to add one</string>
|
<string name="community_no_descriptor">This community does not have a description. Talk to the owner to add one</string>
|
||||||
|
|
||||||
|
<string name="add_sensitive_content_label">Sensitive Content</string>
|
||||||
|
<string name="add_sensitive_content_description">Adds sensitive content warning before showing this content</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user