Adds sensitive-content tags to NIP94 and NIP95 media

This commit is contained in:
Vitor Pamplona
2023-07-09 12:09:21 -04:00
parent 5c78135f1a
commit b520a0faed
16 changed files with 168 additions and 45 deletions
@@ -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 ?: ""
@@ -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
+3
View File
@@ -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>