amethyst/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt

1751 wiersze
55 KiB
Kotlin
Czysty Zwykły widok Historia

2023-01-11 18:31:20 +00:00
package com.vitorpamplona.amethyst.ui.note
import android.content.Intent
import android.graphics.Bitmap
import android.util.Log
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
2023-01-11 18:31:20 +00:00
import androidx.compose.foundation.shape.CircleShape
2023-03-05 23:34:11 +00:00
import androidx.compose.foundation.shape.CutCornerShape
2023-02-27 22:14:15 +00:00
import androidx.compose.foundation.shape.RoundedCornerShape
2023-03-16 18:20:30 +00:00
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.*
2023-02-06 22:16:27 +00:00
import androidx.compose.material.icons.Icons
2023-03-23 14:49:01 +00:00
import androidx.compose.material.icons.filled.Bolt
2023-02-06 22:16:27 +00:00
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.runtime.*
2023-01-11 18:31:20 +00:00
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
2023-02-06 22:16:27 +00:00
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
2023-02-06 22:16:27 +00:00
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
2023-01-11 18:31:20 +00:00
import androidx.compose.ui.text.font.FontWeight
2023-03-05 23:34:11 +00:00
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
2023-01-11 18:31:20 +00:00
import androidx.compose.ui.unit.dp
2023-03-28 12:46:07 +00:00
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.core.graphics.get
2023-01-12 17:47:31 +00:00
import androidx.navigation.NavController
2023-01-11 18:31:20 +00:00
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
2023-02-06 22:16:27 +00:00
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R
2023-03-23 14:49:01 +00:00
import com.vitorpamplona.amethyst.model.Account
2023-05-05 01:57:19 +00:00
import com.vitorpamplona.amethyst.model.Channel
2023-03-08 22:18:25 +00:00
import com.vitorpamplona.amethyst.model.LocalCache
2023-01-11 18:31:20 +00:00
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.*
2023-03-29 07:00:15 +00:00
import com.vitorpamplona.amethyst.ui.components.*
2023-01-11 18:31:20 +00:00
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
2023-03-14 17:41:39 +00:00
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
2023-03-23 14:49:01 +00:00
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
2023-03-08 22:07:56 +00:00
import com.vitorpamplona.amethyst.ui.theme.Following
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
2023-03-23 14:49:01 +00:00
import java.math.BigDecimal
import java.net.URL
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
2023-01-11 18:31:20 +00:00
@OptIn(ExperimentalTime::class)
2023-01-11 18:31:20 +00:00
@Composable
fun NoteCompose(
baseNote: Note,
routeForLastRead: String? = null,
modifier: Modifier = Modifier,
2023-02-20 23:09:57 +00:00
isBoostedNote: Boolean = false,
isQuotedNote: Boolean = false,
2023-02-27 22:14:15 +00:00
unPackReply: Boolean = true,
makeItShort: Boolean = false,
addMarginTop: Boolean = true,
parentBackgroundColor: Color? = null,
accountViewModel: AccountViewModel,
navController: NavController
) {
val (value, elapsed) = measureTimedValue {
NoteComposeInner(
baseNote,
routeForLastRead,
modifier,
isBoostedNote,
isQuotedNote,
unPackReply,
makeItShort,
addMarginTop,
parentBackgroundColor,
accountViewModel,
navController
)
}
Log.d("Time", "Note Compose in $elapsed for ${baseNote.idHex} ${baseNote.event?.kind()} ${baseNote.event?.content()?.split("\n")?.get(0)?.take(100)}")
}
@OptIn(ExperimentalFoundationApi::class, ExperimentalTime::class)
@Composable
fun NoteComposeInner(
baseNote: Note,
routeForLastRead: String? = null,
modifier: Modifier = Modifier,
isBoostedNote: Boolean = false,
isQuotedNote: Boolean = false,
unPackReply: Boolean = true,
makeItShort: Boolean = false,
addMarginTop: Boolean = true,
parentBackgroundColor: Color? = null,
accountViewModel: AccountViewModel,
navController: NavController
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
2023-04-07 21:21:21 +00:00
val loggedIn = account.userProfile()
val noteState by baseNote.live().metadata.observeAsState()
2023-01-11 18:31:20 +00:00
val note = noteState?.note
val noteReportsState by baseNote.live().reports.observeAsState()
val noteForReports = noteReportsState?.note ?: return
var popupExpanded by remember { mutableStateOf(false) }
2023-02-04 15:41:43 +00:00
var showHiddenNote by remember { mutableStateOf(false) }
var isAcceptable by remember { mutableStateOf(true) }
var canPreview by remember { mutableStateOf(true) }
LaunchedEffect(key1 = noteReportsState) {
withContext(Dispatchers.IO) {
2023-04-07 21:21:21 +00:00
canPreview = note?.author === loggedIn ||
(note?.author?.let { loggedIn.isFollowingCached(it) } ?: true) ||
!noteForReports.hasAnyReports()
isAcceptable = account.isAcceptable(noteForReports)
}
}
val noteEvent = note?.event
val baseChannel = note?.channel()
2023-02-12 23:23:02 +00:00
if (noteEvent == null) {
2023-03-08 22:07:56 +00:00
BlankNote(
modifier.combinedClickable(
onClick = { },
onLongClick = { popupExpanded = true }
),
isBoostedNote
)
} else if (!isAcceptable && !showHiddenNote) {
if (!account.isHidden(noteForReports.author!!)) {
HiddenNote(
account.getRelevantReports(noteForReports),
2023-04-07 21:21:21 +00:00
loggedIn,
modifier,
isBoostedNote,
navController,
onClick = { showHiddenNote = true }
)
}
} else if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && baseChannel != null) {
ChannelHeader(baseChannel = baseChannel, account = account, navController = navController)
2023-03-05 23:34:11 +00:00
} else if (noteEvent is BadgeDefinitionEvent) {
BadgeDisplay(baseNote = note)
2023-04-21 21:01:42 +00:00
} else if (noteEvent is FileHeaderEvent) {
FileHeaderDisplay(note)
2023-04-26 18:22:49 +00:00
} else if (noteEvent is FileStorageHeaderEvent) {
FileStorageHeaderDisplay(note)
2023-01-11 18:31:20 +00:00
} else {
var isNew by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = routeForLastRead) {
withContext(Dispatchers.IO) {
routeForLastRead?.let {
2023-03-13 17:47:44 +00:00
val lastTime = NotificationCache.load(it)
val createdAt = note.createdAt()
if (createdAt != null) {
2023-03-13 17:47:44 +00:00
NotificationCache.markAsRead(it, createdAt)
isNew = createdAt > lastTime
2023-05-05 01:57:19 +00:00
}
}
}
2023-03-08 22:07:56 +00:00
}
val backgroundColor = if (isNew) {
val newColor = MaterialTheme.colors.primary.copy(0.12f)
if (parentBackgroundColor != null) {
newColor.compositeOver(parentBackgroundColor)
} else {
newColor.compositeOver(MaterialTheme.colors.background)
}
} else {
parentBackgroundColor ?: MaterialTheme.colors.background
}
2023-03-08 22:07:56 +00:00
Column(
modifier = modifier
.combinedClickable(
onClick = {
routeFor(note, loggedIn)?.let { navController.navigate(it) }
},
onLongClick = { popupExpanded = true }
)
.background(backgroundColor)
) {
2023-01-12 17:47:31 +00:00
Row(
modifier = Modifier
.padding(
2023-02-20 23:09:57 +00:00
start = if (!isBoostedNote) 12.dp else 0.dp,
end = if (!isBoostedNote) 12.dp else 0.dp,
2023-05-05 01:57:19 +00:00
top = if (addMarginTop && !isBoostedNote) 10.dp else 0.dp
2023-03-08 22:07:56 +00:00
)
2023-01-12 17:47:31 +00:00
) {
2023-02-20 23:09:57 +00:00
if (!isBoostedNote && !isQuotedNote) {
2023-05-05 01:57:19 +00:00
DrawAuthorImages(baseNote, loggedIn, navController)
2023-01-11 18:31:20 +00:00
}
2023-02-20 23:09:57 +00:00
Column(
2023-05-06 15:49:53 +00:00
modifier = Modifier
.padding(start = if (!isBoostedNote && !isQuotedNote) 10.dp else 0.dp)
2023-02-20 23:09:57 +00:00
) {
2023-05-07 00:02:54 +00:00
FirstUserInfoRow(
baseNote = baseNote,
showAuthorPicture = isQuotedNote,
account = account,
accountViewModel = accountViewModel,
navController = navController
)
2023-03-16 18:20:30 +00:00
2023-05-06 15:49:53 +00:00
if (noteEvent !is RepostEvent && !makeItShort && !isQuotedNote) {
2023-05-07 00:02:54 +00:00
SecondUserInfoRow(
note,
account,
navController
)
2023-05-06 15:49:53 +00:00
}
2023-05-06 15:49:53 +00:00
Spacer(modifier = Modifier.height(2.dp))
2023-05-06 15:49:53 +00:00
if (!makeItShort) {
ReplyRow(
note,
unPackReply,
backgroundColor,
account,
accountViewModel,
navController
)
2023-01-11 18:31:20 +00:00
}
2023-05-06 15:49:53 +00:00
when (noteEvent) {
is ReactionEvent -> {
RenderReaction(note, backgroundColor, accountViewModel, navController)
}
2023-03-23 14:49:01 +00:00
2023-05-06 15:49:53 +00:00
is RepostEvent -> {
RenderRepost(note, backgroundColor, accountViewModel, navController)
}
2023-03-28 12:46:07 +00:00
2023-05-06 15:49:53 +00:00
is ReportEvent -> {
RenderReport(note)
2023-03-23 14:49:01 +00:00
}
2023-05-06 15:49:53 +00:00
is LongTextNoteEvent -> {
RenderLongFormContent(note, loggedIn, accountViewModel, navController)
}
2023-02-27 00:22:22 +00:00
2023-05-06 15:49:53 +00:00
is BadgeAwardEvent -> {
RenderBadgeAward(note, backgroundColor, accountViewModel, navController)
2023-02-27 22:14:15 +00:00
}
2023-05-06 15:49:53 +00:00
is PrivateDmEvent -> {
RenderPrivateMessage(note, makeItShort, canPreview, backgroundColor, accountViewModel, navController)
}
2023-01-11 18:31:20 +00:00
2023-05-06 15:49:53 +00:00
is HighlightEvent -> {
RenderHighlight(note, makeItShort, canPreview, backgroundColor, accountViewModel, navController)
2023-01-11 18:31:20 +00:00
}
2023-05-06 15:49:53 +00:00
is PollNoteEvent -> {
RenderPoll(
note,
makeItShort,
canPreview,
backgroundColor,
accountViewModel,
navController
)
}
2023-01-11 18:31:20 +00:00
2023-05-06 15:49:53 +00:00
else -> {
RenderTextEvent(
note,
makeItShort,
canPreview,
backgroundColor,
accountViewModel,
navController
2023-01-11 18:31:20 +00:00
)
}
2023-05-06 15:49:53 +00:00
}
2023-05-06 15:49:53 +00:00
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
}
}
}
}
}
2023-05-06 15:49:53 +00:00
fun routeFor(note: Note, loggedIn: User): String? {
val noteEvent = note.event
2023-05-06 15:49:53 +00:00
if (noteEvent is ChannelMessageEvent || noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) {
note.channel()?.let {
return "Channel/${it.idHex}"
}
} else if (noteEvent is PrivateDmEvent) {
val replyAuthorBase =
(note.event as? PrivateDmEvent)
?.recipientPubKey()
?.let { LocalCache.getOrCreateUser(it) }
2023-03-05 23:34:11 +00:00
2023-05-06 15:49:53 +00:00
var userToComposeOn = note.author!!
2023-03-05 23:34:11 +00:00
2023-05-06 15:49:53 +00:00
if (replyAuthorBase != null) {
if (note.author == loggedIn) {
userToComposeOn = replyAuthorBase
}
}
2023-03-05 23:34:11 +00:00
2023-05-06 15:49:53 +00:00
return "Room/${userToComposeOn.pubkeyHex}"
} else {
return "Note/${note.idHex}"
}
2023-05-06 15:49:53 +00:00
return null
}
2023-05-06 15:49:53 +00:00
@Composable
private fun RenderTextEvent(
note: Note,
makeItShort: Boolean,
canPreview: Boolean,
backgroundColor: Color,
accountViewModel: AccountViewModel,
navController: NavController
) {
val noteEvent = note.event ?: return
2023-05-06 15:49:53 +00:00
val eventContent = accountViewModel.decrypt(note)
2023-05-06 15:49:53 +00:00
if (eventContent != null) {
if (makeItShort && accountViewModel.isLoggedUser(note.author)) {
Text(
text = eventContent,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
} else {
TranslatableRichTextViewer(
content = eventContent,
canPreview = canPreview && !makeItShort,
modifier = Modifier.fillMaxWidth(),
tags = noteEvent.tags(),
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
navController = navController
)
2023-05-06 15:49:53 +00:00
DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, navController)
}
}
2023-01-11 18:31:20 +00:00
2023-05-06 15:49:53 +00:00
if (!makeItShort) {
ReactionsRow(note, accountViewModel, navController)
}
2023-05-06 15:49:53 +00:00
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
)
}
@Composable
private fun RenderPoll(
note: Note,
makeItShort: Boolean,
canPreview: Boolean,
backgroundColor: Color,
accountViewModel: AccountViewModel,
navController: NavController
) {
val noteEvent = note.event as? PollNoteEvent ?: return
val eventContent = noteEvent.content()
if (makeItShort && accountViewModel.isLoggedUser(note.author)) {
Text(
text = eventContent,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
} else {
TranslatableRichTextViewer(
eventContent,
canPreview = canPreview && !makeItShort,
Modifier.fillMaxWidth(),
noteEvent.tags(),
backgroundColor,
accountViewModel,
navController
)
DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, navController)
PollNote(
note,
canPreview = canPreview && !makeItShort,
backgroundColor,
accountViewModel,
navController
)
}
if (!makeItShort) {
ReactionsRow(note, accountViewModel, navController)
}
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
)
}
@Composable
private fun RenderHighlight(
note: Note,
makeItShort: Boolean,
canPreview: Boolean,
backgroundColor: Color,
accountViewModel: AccountViewModel,
navController: NavController
) {
val noteEvent = note.event as? HighlightEvent ?: return
DisplayHighlight(
noteEvent.quote(),
noteEvent.author(),
noteEvent.inUrl(),
makeItShort,
canPreview,
backgroundColor,
accountViewModel,
navController
)
if (!makeItShort) {
ReactionsRow(note, accountViewModel, navController)
}
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
)
}
@Composable
private fun RenderPrivateMessage(
note: Note,
makeItShort: Boolean,
canPreview: Boolean,
backgroundColor: Color,
accountViewModel: AccountViewModel,
navController: NavController
) {
val noteEvent = note.event as? PrivateDmEvent ?: return
val withMe = noteEvent.with(accountViewModel.userProfile().pubkeyHex)
if (withMe) {
val eventContent = accountViewModel.decrypt(note)
if (eventContent != null) {
if (makeItShort && accountViewModel.isLoggedUser(note.author)) {
Text(
text = eventContent,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
} else {
TranslatableRichTextViewer(
content = eventContent,
canPreview = canPreview && !makeItShort,
modifier = Modifier.fillMaxWidth(),
tags = noteEvent.tags(),
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
navController = navController
)
DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, navController)
}
}
} else {
val recipient = noteEvent.recipientPubKey()?.let { LocalCache.checkGetOrCreateUser(it) }
TranslatableRichTextViewer(
stringResource(
id = R.string.private_conversation_notification,
"@${note.author?.pubkeyNpub()}",
"@${recipient?.pubkeyNpub()}"
),
canPreview = !makeItShort,
Modifier.fillMaxWidth(),
noteEvent.tags(),
backgroundColor,
accountViewModel,
navController
)
}
if (!makeItShort) {
ReactionsRow(note, accountViewModel, navController)
}
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
)
}
@Composable
private fun RenderBadgeAward(
note: Note,
backgroundColor: Color,
accountViewModel: AccountViewModel,
navController: NavController
) {
if (note.replyTo.isNullOrEmpty()) return
val noteEvent = note.event as? BadgeAwardEvent ?: return
var awardees by remember { mutableStateOf<List<User>>(listOf()) }
2023-05-06 15:49:53 +00:00
Text(text = stringResource(R.string.award_granted_to))
2023-05-07 00:02:54 +00:00
LaunchedEffect(key1 = note) {
withContext(Dispatchers.IO) {
awardees = noteEvent.awardees().mapNotNull { hex ->
LocalCache.checkGetOrCreateUser(hex)
}
}
2023-05-07 00:02:54 +00:00
}
2023-05-06 15:49:53 +00:00
2023-05-07 00:02:54 +00:00
FlowRow(modifier = Modifier.padding(top = 5.dp)) {
awardees.forEach { user ->
Row(modifier = Modifier.clickable {
navController.navigate("User/${user.pubkeyHex}")
},
verticalAlignment = Alignment.CenterVertically
) {
UserPicture(
baseUser = user,
baseUserAccount = accountViewModel.userProfile(),
size = 35.dp
)
2023-05-06 15:49:53 +00:00
}
2023-05-07 00:02:54 +00:00
}
}
2023-05-06 15:49:53 +00:00
2023-05-07 00:02:54 +00:00
note.replyTo?.firstOrNull()?.let {
NoteCompose(
it,
modifier = Modifier,
isBoostedNote = false,
isQuotedNote = true,
unPackReply = false,
parentBackgroundColor = backgroundColor,
accountViewModel = accountViewModel,
navController = navController
)
}
2023-05-06 15:49:53 +00:00
ReactionsRow(note, accountViewModel, navController)
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
)
}
@Composable
private fun RenderReaction(
note: Note,
backgroundColor: Color,
accountViewModel: AccountViewModel,
navController: NavController
) {
note.replyTo?.lastOrNull()?.let {
NoteCompose(
it,
modifier = Modifier,
isBoostedNote = true,
unPackReply = false,
parentBackgroundColor = backgroundColor,
accountViewModel = accountViewModel,
navController = navController
)
}
// Reposts have trash in their contents.
val refactorReactionText =
if (note.event?.content() == "+") "" else note.event?.content() ?: ""
Text(
text = refactorReactionText
)
}
@Composable
private fun RenderRepost(
note: Note,
backgroundColor: Color,
accountViewModel: AccountViewModel,
navController: NavController
) {
note.replyTo?.lastOrNull()?.let {
NoteCompose(
it,
modifier = Modifier,
isBoostedNote = true,
unPackReply = false,
parentBackgroundColor = backgroundColor,
accountViewModel = accountViewModel,
navController = navController
)
}
}
@Composable
private fun RenderLongFormContent(
note: Note,
loggedIn: User,
accountViewModel: AccountViewModel,
navController: NavController
) {
val noteEvent = note.event as? LongTextNoteEvent ?: return
LongFormHeader(noteEvent, note, loggedIn)
ReactionsRow(note, accountViewModel, navController)
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
)
}
@Composable
private fun RenderReport(note: Note) {
val noteEvent = note.event as? ReportEvent ?: return
val reportType = (noteEvent.reportedPost() + noteEvent.reportedAuthor()).map {
when (it.reportType) {
ReportEvent.ReportType.EXPLICIT -> stringResource(R.string.explicit_content)
ReportEvent.ReportType.NUDITY -> stringResource(R.string.nudity)
ReportEvent.ReportType.PROFANITY -> stringResource(R.string.profanity_hateful_speech)
ReportEvent.ReportType.SPAM -> stringResource(R.string.spam)
ReportEvent.ReportType.IMPERSONATION -> stringResource(R.string.impersonation)
ReportEvent.ReportType.ILLEGAL -> stringResource(R.string.illegal_behavior)
}
}.toSet().joinToString(", ")
Text(
text = reportType
)
Divider(
modifier = Modifier.padding(top = 40.dp),
thickness = 0.25.dp
)
}
@Composable
private fun ReplyRow(
note: Note,
unPackReply: Boolean,
backgroundColor: Color,
account: Account,
accountViewModel: AccountViewModel,
navController: NavController
) {
val noteEvent = note.event
2023-05-07 00:02:54 +00:00
if (noteEvent is TextNoteEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())) {
2023-05-06 15:49:53 +00:00
val replyingDirectlyTo = note.replyTo?.lastOrNull()
if (replyingDirectlyTo != null && unPackReply) {
NoteCompose(
baseNote = replyingDirectlyTo,
isQuotedNote = true,
modifier = Modifier
.padding(top = 5.dp)
.fillMaxWidth()
.clip(shape = RoundedCornerShape(15.dp))
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
RoundedCornerShape(15.dp)
),
unPackReply = false,
makeItShort = true,
parentBackgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.05f)
.compositeOver(backgroundColor),
accountViewModel = accountViewModel,
navController = navController
)
} else {
ReplyInformation(note.replyTo, noteEvent.mentions(), account, navController)
}
Spacer(modifier = Modifier.height(5.dp))
2023-05-07 00:02:54 +00:00
} else if (noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())) {
2023-05-06 15:49:53 +00:00
val sortedMentions = noteEvent.mentions()
.mapNotNull { LocalCache.checkGetOrCreateUser(it) }
.toSet()
.sortedBy { account.isFollowing(it) }
note.channel()?.let {
ReplyInformationChannel(note.replyTo, sortedMentions, it, navController)
}
Spacer(modifier = Modifier.height(5.dp))
}
}
@Composable
private fun SecondUserInfoRow(
note: Note,
account: Account,
navController: NavController
) {
val noteEvent = note.event ?: return
val noteAuthor = note.author ?: return
Row(verticalAlignment = Alignment.CenterVertically) {
ObserveDisplayNip05Status(noteAuthor, Modifier.weight(1f))
val baseReward = noteEvent.getReward()
if (baseReward != null) {
DisplayReward(baseReward, note, account, navController)
}
val pow = noteEvent.getPoWRank()
if (pow > 20) {
DisplayPoW(pow)
}
}
}
@Composable
private fun FirstUserInfoRow(
baseNote: Note,
showAuthorPicture: Boolean,
account: Account,
accountViewModel: AccountViewModel,
navController: NavController
) {
var moreActionsExpanded by remember { mutableStateOf(false) }
val eventNote = baseNote.event ?: return
val time = baseNote.createdAt() ?: return
val loggedIn = account.userProfile()
2023-05-06 15:49:53 +00:00
Row(verticalAlignment = Alignment.CenterVertically) {
if (showAuthorPicture) {
NoteAuthorPicture(baseNote, navController, loggedIn, 25.dp)
2023-05-06 15:49:53 +00:00
Spacer(Modifier.padding(horizontal = 5.dp))
NoteUsernameDisplay(baseNote, Modifier.weight(1f))
} else {
NoteUsernameDisplay(baseNote, Modifier.weight(1f))
}
if (eventNote is RepostEvent) {
2023-05-06 15:49:53 +00:00
Text(
" ${stringResource(id = R.string.boosted)}",
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
} else {
DisplayFollowingHashtagsInPost(eventNote, account, navController)
2023-01-11 18:31:20 +00:00
}
2023-05-06 15:49:53 +00:00
TimeAgo(time)
2023-05-06 15:49:53 +00:00
IconButton(
modifier = Modifier.size(24.dp),
onClick = { moreActionsExpanded = true }
) {
Icon(
imageVector = Icons.Default.MoreVert,
null,
modifier = Modifier.size(15.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
NoteDropDownMenu(
baseNote,
moreActionsExpanded,
{ moreActionsExpanded = false },
accountViewModel
)
}
2023-01-11 18:31:20 +00:00
}
}
@Composable
fun TimeAgo(time: Long) {
val context = LocalContext.current
var timeStr by remember { mutableStateOf("") }
LaunchedEffect(key1 = time) {
withContext(Dispatchers.IO) {
timeStr = timeAgo(time, context = context)
}
}
Text(
timeStr,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
maxLines = 1
)
}
2023-05-05 01:57:19 +00:00
@Composable
private fun DrawAuthorImages(baseNote: Note, loggedIn: User, navController: NavController) {
val baseChannel = baseNote.channel()
Column(Modifier.width(55.dp)) {
// Draws the boosted picture outside the boosted card.
Box(modifier = Modifier.width(55.dp), contentAlignment = Alignment.BottomEnd) {
NoteAuthorPicture(baseNote, navController, loggedIn, 55.dp)
if (baseNote.event is RepostEvent) {
RepostNoteAuthorPicture(baseNote, navController, loggedIn)
}
if (baseNote.event is ChannelMessageEvent && baseChannel != null) {
ChannelNotePicture(baseChannel)
}
}
if (baseNote.event is RepostEvent) {
baseNote.replyTo?.lastOrNull()?.let {
RelayBadges(it)
}
} else {
RelayBadges(baseNote)
}
}
}
@Composable
private fun ChannelNotePicture(baseChannel: Channel) {
val channelState by baseChannel.live.observeAsState()
val channel = channelState?.channel
if (channel != null) {
Box(
Modifier
.width(30.dp)
.height(30.dp)
) {
RobohashAsyncImageProxy(
robot = channel.idHex,
model = ResizeImage(channel.profilePicture(), 30.dp),
contentDescription = stringResource(R.string.group_picture),
modifier = Modifier
.width(30.dp)
.height(30.dp)
.clip(shape = CircleShape)
.background(MaterialTheme.colors.background)
.border(
2.dp,
MaterialTheme.colors.background,
CircleShape
)
)
}
}
}
@Composable
private fun RepostNoteAuthorPicture(
baseNote: Note,
navController: NavController,
loggedIn: User
) {
baseNote.replyTo?.lastOrNull()?.let {
Box(
Modifier
.width(30.dp)
.height(30.dp)
) {
NoteAuthorPicture(
it,
navController,
loggedIn,
35.dp,
pictureModifier = Modifier.border(
2.dp,
MaterialTheme.colors.background,
CircleShape
)
)
}
}
}
@Composable
fun DisplayHighlight(
highlight: String,
authorHex: String?,
url: String?,
makeItShort: Boolean,
canPreview: Boolean,
backgroundColor: Color,
accountViewModel: AccountViewModel,
navController: NavController
) {
val quote = highlight.split("\n").map { "> *${it.removeSuffix(" ")}*" }.joinToString("\n")
2023-05-06 15:49:53 +00:00
TranslatableRichTextViewer(
quote,
canPreview = canPreview && !makeItShort,
Modifier.fillMaxWidth(),
emptyList(),
backgroundColor,
accountViewModel,
navController
)
FlowRow() {
authorHex?.let { authorHex ->
val userBase = LocalCache.checkGetOrCreateUser(authorHex)
if (userBase != null) {
val userState by userBase.live().metadata.observeAsState()
val user = userState?.user
if (user != null) {
CreateClickableText(
user.toBestDisplayName(),
"",
"User/${user.pubkeyHex}",
navController
)
}
}
}
url?.let { url ->
val validatedUrl = try {
URL(url)
} catch (e: Exception) {
Log.w("Note Compose", "Invalid URI: $url")
null
}
validatedUrl?.host?.let { host ->
Text("on ")
ClickableUrl(urlText = host, url = url)
}
}
}
}
2023-03-23 14:49:01 +00:00
@Composable
fun DisplayFollowingHashtagsInPost(
noteEvent: EventInterface,
account: Account,
navController: NavController
) {
var firstTag by remember { mutableStateOf<String?>(null) }
LaunchedEffect(key1 = noteEvent.id()) {
withContext(Dispatchers.IO) {
firstTag = noteEvent.firstIsTaggedHashes(account.followingTagSet())
}
}
2023-03-23 14:49:01 +00:00
Column() {
Row(verticalAlignment = Alignment.CenterVertically) {
firstTag?.let {
2023-03-23 14:49:01 +00:00
ClickableText(
text = AnnotatedString(" #$firstTag"),
onClick = { navController.navigate("Hashtag/$firstTag") },
style = LocalTextStyle.current.copy(
color = MaterialTheme.colors.primary.copy(
alpha = 0.52f
)
)
)
}
}
}
}
@Composable
fun DisplayUncitedHashtags(
hashtags: List<String>,
2023-03-23 14:49:01 +00:00
eventContent: String,
navController: NavController
) {
if (hashtags.isNotEmpty()) {
FlowRow(
modifier = Modifier.padding(top = 5.dp)
) {
hashtags.forEach { hashtag ->
if (!eventContent.contains(hashtag, true)) {
2023-03-23 14:49:01 +00:00
ClickableText(
2023-03-23 15:07:02 +00:00
text = AnnotatedString("#$hashtag "),
onClick = { navController.navigate("Hashtag/$hashtag") },
2023-03-23 14:49:01 +00:00
style = LocalTextStyle.current.copy(
color = MaterialTheme.colors.primary.copy(
alpha = 0.52f
)
)
)
}
}
}
}
}
2023-03-28 12:46:07 +00:00
@Composable
fun DisplayPoW(
pow: Int
) {
Text(
"PoW-$pow",
color = MaterialTheme.colors.primary.copy(
alpha = 0.52f
),
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
}
2023-03-23 14:49:01 +00:00
@Composable
fun DisplayReward(
baseReward: BigDecimal,
baseNote: Note,
account: Account,
2023-03-23 14:49:01 +00:00
navController: NavController
) {
var popupExpanded by remember { mutableStateOf(false) }
2023-03-23 14:49:01 +00:00
Column() {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable { popupExpanded = true }
) {
2023-03-23 14:49:01 +00:00
ClickableText(
text = AnnotatedString("#bounty"),
onClick = { navController.navigate("Hashtag/bounty") },
style = LocalTextStyle.current.copy(
color = MaterialTheme.colors.primary.copy(
alpha = 0.52f
)
)
)
val repliesState by baseNote.live().replies.observeAsState()
val replyNote = repliesState?.note
if (replyNote?.hasPledgeBy(account.userProfile()) == true) {
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = stringResource(R.string.zaps),
modifier = Modifier.size(20.dp),
tint = BitcoinOrange
)
} else {
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = stringResource(R.string.zaps),
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
2023-03-23 14:49:01 +00:00
var rewardAmount by remember {
mutableStateOf<BigDecimal?>(
baseReward
)
}
LaunchedEffect(key1 = repliesState) {
withContext(Dispatchers.IO) {
replyNote?.pledgedAmountByOthers()?.let {
rewardAmount = baseReward.add(it)
}
}
}
Text(
showAmount(rewardAmount),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
if (popupExpanded) {
AddBountyAmountDialog(baseNote, account) {
popupExpanded = false
}
}
2023-03-23 14:49:01 +00:00
}
}
2023-03-05 23:34:11 +00:00
@Composable
fun BadgeDisplay(baseNote: Note) {
val background = MaterialTheme.colors.background
2023-03-05 23:34:11 +00:00
val badgeData = baseNote.event as? BadgeDefinitionEvent ?: return
val image = badgeData.image()
val name = badgeData.name()
val description = badgeData.description()
var backgroundFromImage by remember { mutableStateOf(Pair(background, background)) }
var imageResult by remember { mutableStateOf<AsyncImagePainter.State.Success?>(null) }
LaunchedEffect(key1 = imageResult) {
withContext(Dispatchers.IO) {
imageResult?.let {
val backgroundColor = it.result.drawable.toBitmap(200, 200).copy(Bitmap.Config.ARGB_8888, false).get(0, 199)
val colorFromImage = Color(backgroundColor)
val textBackground = if (colorFromImage.luminance() > 0.5) lightColors().onBackground else darkColors().onBackground
backgroundFromImage = Pair(colorFromImage, textBackground)
}
}
}
2023-03-05 23:34:11 +00:00
Row(
modifier = Modifier
.padding(10.dp)
.clip(shape = CutCornerShape(20, 20, 20, 20))
2023-03-05 23:34:11 +00:00
.border(
5.dp,
MaterialTheme.colors.primary.copy(alpha = 0.32f),
CutCornerShape(20)
)
.background(backgroundFromImage.first)
2023-03-05 23:34:11 +00:00
) {
Column {
image.let {
2023-03-05 23:34:11 +00:00
AsyncImage(
model = it,
contentDescription = stringResource(
R.string.badge_award_image_for,
name ?: ""
2023-03-05 23:34:11 +00:00
),
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth(),
onSuccess = {
imageResult = it
}
2023-03-05 23:34:11 +00:00
)
}
name?.let {
2023-03-05 23:34:11 +00:00
Text(
text = it,
style = MaterialTheme.typography.body1,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp),
color = backgroundFromImage.second
2023-03-05 23:34:11 +00:00
)
}
description?.let {
2023-03-05 23:34:11 +00:00
Text(
text = it,
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
color = Color.Gray,
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
2023-04-21 21:01:42 +00:00
@Composable
fun FileHeaderDisplay(note: Note) {
val event = (note.event as? FileHeaderEvent) ?: return
val fullUrl = event.url() ?: return
2023-04-25 12:20:51 +00:00
var content by remember { mutableStateOf<ZoomableContent?>(null) }
LaunchedEffect(key1 = event.id) {
withContext(Dispatchers.IO) {
val blurHash = event.blurhash()
val hash = event.hash()
2023-05-02 14:02:45 +00:00
val dimensions = event.dimensions()
2023-04-25 12:20:51 +00:00
val description = event.content
val removedParamsFromUrl = fullUrl.split("?")[0].lowercase()
val isImage = imageExtensions.any { removedParamsFromUrl.endsWith(it) }
val uri = "nostr:" + note.toNEvent()
2023-04-25 12:20:51 +00:00
content = if (isImage) {
2023-05-02 14:02:45 +00:00
ZoomableUrlImage(fullUrl, description, hash, blurHash, dimensions, uri)
2023-04-25 12:20:51 +00:00
} else {
ZoomableUrlVideo(fullUrl, description, hash, uri)
2023-04-25 12:20:51 +00:00
}
2023-04-21 21:01:42 +00:00
}
}
2023-04-25 12:20:51 +00:00
content?.let {
ZoomableContentView(content = it, listOf(it))
}
2023-04-21 21:01:42 +00:00
}
2023-04-26 18:22:49 +00:00
@Composable
fun FileStorageHeaderDisplay(baseNote: Note) {
val appContext = LocalContext.current.applicationContext
2023-04-26 22:04:38 +00:00
val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return
val fileNote = eventHeader.dataEventId()?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
2023-04-26 18:22:49 +00:00
val noteState by fileNote.live().metadata.observeAsState()
val note = noteState?.note
val eventBytes = (note?.event as? FileStorageEvent)
var content by remember { mutableStateOf<ZoomableContent?>(null) }
2023-04-26 22:04:38 +00:00
LaunchedEffect(key1 = eventHeader.id, key2 = noteState) {
2023-04-26 18:22:49 +00:00
withContext(Dispatchers.IO) {
val uri = "nostr:" + baseNote.toNEvent()
val localDir = File(File(appContext.externalCacheDir, "NIP95"), fileNote.idHex)
2023-04-26 18:22:49 +00:00
val bytes = eventBytes?.decode()
val blurHash = eventHeader.blurhash()
2023-05-02 14:02:45 +00:00
val dimensions = eventHeader.dimensions()
2023-04-26 18:22:49 +00:00
val description = eventHeader.content
val mimeType = eventHeader.mimeType()
content = if (mimeType?.startsWith("image") == true) {
2023-05-02 14:02:45 +00:00
ZoomableLocalImage(
localFile = localDir,
mimeType = mimeType,
description = description,
blurhash = blurHash,
dim = dimensions,
isVerified = true,
uri = uri
)
2023-04-26 18:22:49 +00:00
} else {
if (bytes != null) {
2023-05-02 14:02:45 +00:00
ZoomableLocalVideo(
localFile = localDir,
mimeType = mimeType,
description = description,
dim = dimensions,
isVerified = true,
uri = uri
)
2023-04-26 18:22:49 +00:00
} else {
null
}
}
}
}
content?.let {
ZoomableContentView(content = it, listOf(it))
}
2023-04-26 18:22:49 +00:00
}
@Composable
private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, loggedIn: User) {
Row(
modifier = Modifier
.clip(shape = RoundedCornerShape(15.dp))
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
RoundedCornerShape(15.dp)
)
) {
Column {
noteEvent.image()?.let {
AsyncImage(
model = it,
contentDescription = stringResource(
R.string.preview_card_image_for,
it
),
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
} ?: Box() {
note.author?.info?.banner?.let {
AsyncImage(
model = it,
contentDescription = stringResource(
R.string.preview_card_image_for,
it
),
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
} ?: Image(
painter = painterResource(R.drawable.profile_banner),
contentDescription = stringResource(R.string.profile_banner),
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
)
Box(
Modifier
.width(75.dp)
.height(75.dp)
.padding(10.dp)
.align(Alignment.BottomStart)
) {
NoteAuthorPicture(baseNote = note, baseUserAccount = loggedIn, size = 55.dp)
}
}
noteEvent.title()?.let {
Text(
text = it,
style = MaterialTheme.typography.body1,
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, top = 10.dp)
)
}
noteEvent.summary()?.let {
Text(
text = it,
style = MaterialTheme.typography.caption,
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
color = Color.Gray,
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
?: Text(
text = noteEvent.content.take(200),
style = MaterialTheme.typography.caption,
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
color = Color.Gray,
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
}
}
2023-02-06 22:16:27 +00:00
@Composable
private fun RelayBadges(baseNote: Note) {
val noteRelaysState by baseNote.live().relays.observeAsState()
2023-04-20 21:19:34 +00:00
val noteRelays = noteRelaysState?.note ?: return
2023-02-06 22:16:27 +00:00
var expanded by remember { mutableStateOf(false) }
2023-04-20 21:19:34 +00:00
var showShowMore by remember { mutableStateOf(false) }
var lazyRelayList by remember { mutableStateOf(emptyList<String>()) }
2023-02-06 22:16:27 +00:00
2023-04-20 21:19:34 +00:00
LaunchedEffect(key1 = noteRelaysState, key2 = expanded) {
withContext(Dispatchers.IO) {
val relayList = noteRelays.relays.map {
it.removePrefix("wss://").removePrefix("ws://")
}
2023-02-06 22:16:27 +00:00
2023-04-20 21:19:34 +00:00
val relaysToDisplay = if (expanded) relayList else relayList.take(3)
val shouldListChange = lazyRelayList.size < 3 || lazyRelayList.size != relayList.size
if (shouldListChange) {
lazyRelayList = relaysToDisplay
}
val nextShowMore = relayList.size > 3 && !expanded
if (nextShowMore != showShowMore) {
// only triggers recomposition when actually different
showShowMore = nextShowMore
}
}
}
2023-02-06 22:16:27 +00:00
Spacer(Modifier.height(10.dp))
2023-04-20 21:19:34 +00:00
VerticalRelayPanelWithFlow(lazyRelayList)
2023-04-20 21:19:34 +00:00
if (showShowMore) {
ShowMoreRelaysButton {
expanded = true
2023-02-06 22:16:27 +00:00
}
}
2023-04-20 21:19:34 +00:00
}
2023-02-06 22:16:27 +00:00
2023-04-20 21:19:34 +00:00
@Composable
@Stable
private fun VerticalRelayPanelWithFlow(
relays: List<String>
) {
// FlowRow Seems to be a lot faster than LazyVerticalGrid
FlowRow() {
relays.forEach { url ->
RelayIconCompose(url)
}
}
}
@Composable
@Stable
private fun RelayIconCompose(url: String) {
val uri = LocalUriHandler.current
Box(
Modifier
.padding(1.dp)
.size(15.dp)
) {
RobohashFallbackAsyncImage(
robot = "https://$url/favicon.ico",
robotSize = 15.dp,
model = "https://$url/favicon.ico",
contentDescription = stringResource(R.string.relay_icon),
colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }),
modifier = Modifier
.width(13.dp)
.height(13.dp)
.clip(shape = CircleShape)
.background(MaterialTheme.colors.background)
.clickable(onClick = { uri.openUri("https://$url") })
)
}
}
@Composable
private fun ShowMoreRelaysButton(onClick: () -> Unit) {
Row(
Modifier
.fillMaxWidth()
.height(25.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.Top
) {
IconButton(
modifier = Modifier.then(Modifier.size(24.dp)),
onClick = onClick
2023-03-08 22:07:56 +00:00
) {
2023-04-20 21:19:34 +00:00
Icon(
imageVector = Icons.Default.ExpandMore,
null,
modifier = Modifier.size(15.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
2023-02-06 22:16:27 +00:00
}
}
}
@Composable
fun NoteAuthorPicture(
2023-05-06 15:49:53 +00:00
baseNote: Note,
navController: NavController,
userAccount: User,
size: Dp,
pictureModifier: Modifier = Modifier
) {
2023-05-06 15:49:53 +00:00
NoteAuthorPicture(baseNote, userAccount, size, pictureModifier) {
navController.navigate("User/${it.pubkeyHex}")
}
}
@Composable
fun NoteAuthorPicture(
baseNote: Note,
baseUserAccount: User,
size: Dp,
2023-03-13 17:47:44 +00:00
modifier: Modifier = Modifier,
onClick: ((User) -> Unit)? = null
) {
val noteState by baseNote.live().metadata.observeAsState()
val note = noteState?.note ?: return
val author = note.author
Box(
Modifier
.width(size)
2023-03-08 22:07:56 +00:00
.height(size)
) {
if (author == null) {
RobohashAsyncImage(
robot = "authornotfound",
robotSize = size,
contentDescription = stringResource(R.string.unknown_author),
2023-03-13 17:47:44 +00:00
modifier = modifier
.width(size)
.height(size)
.clip(shape = CircleShape)
.background(MaterialTheme.colors.background)
)
} else {
2023-03-13 17:47:44 +00:00
UserPicture(author, baseUserAccount, size, modifier, onClick)
}
}
}
@Composable
fun UserPicture(
user: User,
navController: NavController,
userAccount: User,
size: Dp,
pictureModifier: Modifier = Modifier
) {
UserPicture(user, userAccount, size, pictureModifier) {
navController.navigate("User/${it.pubkeyHex}")
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun UserPicture(
baseUser: User,
baseUserAccount: User,
size: Dp,
2023-03-13 17:47:44 +00:00
modifier: Modifier = Modifier,
onClick: ((User) -> Unit)? = null,
onLongClick: ((User) -> Unit)? = null
) {
val userState by baseUser.live().metadata.observeAsState()
val user = userState?.user ?: return
val accountState by baseUserAccount.live().follows.observeAsState()
val accountUser = accountState?.user ?: return
val showFollowingMark = accountUser.isFollowingCached(user) || user == accountUser
Row(
modifier = Modifier
.run {
if (onClick != null && onLongClick != null) {
this.combinedClickable(
onClick = { onClick(user) },
onLongClick = { onLongClick(user) }
)
} else if (onClick != null) {
this.clickable(onClick = { onClick(user) })
} else {
this
}
}
) {
UserPicture(
userHex = user.pubkeyHex,
userPicture = user.profilePicture(),
showFollowingMark = showFollowingMark,
size = size,
modifier = modifier
)
}
}
@Composable
fun UserPicture(
userHex: String,
userPicture: String?,
showFollowingMark: Boolean,
size: Dp,
modifier: Modifier = Modifier
) {
Box(
Modifier
.width(size)
2023-03-08 22:07:56 +00:00
.height(size)
) {
RobohashAsyncImageProxy(
robot = userHex,
model = ResizeImage(userPicture, size),
contentDescription = stringResource(id = R.string.profile_image),
2023-03-13 17:47:44 +00:00
modifier = modifier
.width(size)
.height(size)
.clip(shape = CircleShape)
.background(MaterialTheme.colors.background)
)
if (showFollowingMark) {
Box(
Modifier
.width(size.div(3.5f))
.height(size.div(3.5f))
.align(Alignment.TopEnd),
contentAlignment = Alignment.Center
) {
// Background for the transparent checkmark
Box(
Modifier
.clip(CircleShape)
.fillMaxSize(0.6f)
.align(Alignment.Center)
.background(MaterialTheme.colors.background)
)
Icon(
painter = painterResource(R.drawable.ic_verified),
stringResource(id = R.string.following),
modifier = Modifier.fillMaxSize(),
tint = Following
)
}
}
}
}
data class DropDownParams(
val isFollowingAuthor: Boolean,
val isPrivateBookmarkNote: Boolean,
val isPublicBookmarkNote: Boolean,
val isLoggedUser: Boolean
)
@Composable
fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, accountViewModel: AccountViewModel) {
val clipboardManager = LocalClipboardManager.current
val appContext = LocalContext.current.applicationContext
val actContext = LocalContext.current
2023-03-14 17:41:39 +00:00
var reportDialogShowing by remember { mutableStateOf(false) }
var state by remember {
mutableStateOf<DropDownParams>(
DropDownParams(false, false, false, false)
)
}
LaunchedEffect(key1 = note) {
2023-04-18 12:45:34 +00:00
withContext(Dispatchers.IO) {
state = DropDownParams(
accountViewModel.isFollowing(note.author),
accountViewModel.isInPrivateBookmarks(note),
accountViewModel.isInPublicBookmarks(note),
accountViewModel.isLoggedUser(note.author)
)
}
}
DropdownMenu(
expanded = popupExpanded,
onDismissRequest = onDismiss
) {
if (!state.isFollowingAuthor) {
DropdownMenuItem(onClick = {
accountViewModel.follow(
note.author ?: return@DropdownMenuItem
); onDismiss()
}) {
Text(stringResource(R.string.follow))
}
Divider()
}
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(accountViewModel.decrypt(note) ?: "")); onDismiss() }) {
Text(stringResource(R.string.copy_text))
}
2023-04-24 21:58:35 +00:00
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString("nostr:${note.author?.pubkeyNpub()}")); onDismiss() }) {
Text(stringResource(R.string.copy_user_pubkey))
}
2023-04-24 21:58:35 +00:00
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString("nostr:" + note.toNEvent())); onDismiss() }) {
Text(stringResource(R.string.copy_note_id))
}
DropdownMenuItem(onClick = {
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(
Intent.EXTRA_TEXT,
externalLinkForNote(note)
)
putExtra(Intent.EXTRA_TITLE, actContext.getString(R.string.quick_action_share_browser_link))
}
val shareIntent = Intent.createChooser(sendIntent, appContext.getString(R.string.quick_action_share))
ContextCompat.startActivity(actContext, shareIntent, null)
onDismiss()
}) {
Text(stringResource(R.string.quick_action_share))
}
Divider()
if (state.isPrivateBookmarkNote) {
2023-03-20 22:16:07 +00:00
DropdownMenuItem(onClick = { accountViewModel.removePrivateBookmark(note); onDismiss() }) {
Text(stringResource(R.string.remove_from_private_bookmarks))
}
} else {
DropdownMenuItem(onClick = { accountViewModel.addPrivateBookmark(note); onDismiss() }) {
Text(stringResource(R.string.add_to_private_bookmarks))
}
}
if (state.isPublicBookmarkNote) {
2023-03-20 22:16:07 +00:00
DropdownMenuItem(onClick = { accountViewModel.removePublicBookmark(note); onDismiss() }) {
Text(stringResource(R.string.remove_from_public_bookmarks))
}
} else {
DropdownMenuItem(onClick = { accountViewModel.addPublicBookmark(note); onDismiss() }) {
Text(stringResource(R.string.add_to_public_bookmarks))
}
}
Divider()
DropdownMenuItem(onClick = { accountViewModel.broadcast(note); onDismiss() }) {
Text(stringResource(R.string.broadcast))
}
Divider()
if (state.isLoggedUser) {
DropdownMenuItem(onClick = { accountViewModel.delete(note); onDismiss() }) {
2023-03-11 04:00:35 +00:00
Text(stringResource(R.string.request_deletion))
}
} else {
2023-03-14 17:41:39 +00:00
DropdownMenuItem(onClick = { reportDialogShowing = true }) {
Text("Block / Report")
2023-02-22 21:17:56 +00:00
}
}
}
2023-03-14 17:41:39 +00:00
if (reportDialogShowing) {
ReportNoteDialog(note = note, accountViewModel = accountViewModel) {
reportDialogShowing = false
onDismiss()
}
}
2023-03-05 21:42:19 +00:00
}