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

850 wiersze
34 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 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
import androidx.compose.material.*
2023-02-06 22:16:27 +00:00
import androidx.compose.material.icons.Icons
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.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
import androidx.core.content.ContextCompat
2023-01-12 17:47:31 +00:00
import androidx.navigation.NavController
2023-01-11 18:31:20 +00:00
import coil.compose.AsyncImage
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-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
2023-03-05 23:34:11 +00:00
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
2023-01-11 18:31:20 +00:00
import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent
2023-01-11 18:31:20 +00:00
import com.vitorpamplona.amethyst.service.model.RepostEvent
2023-03-08 22:07:56 +00:00
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
2023-02-27 16:28:54 +00:00
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
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-08 22:07:56 +00:00
import com.vitorpamplona.amethyst.ui.theme.Following
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
2023-01-11 18:31:20 +00:00
2023-02-27 22:14:15 +00:00
@OptIn(ExperimentalFoundationApi::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 accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
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) }
val context = LocalContext.current.applicationContext
2023-02-12 23:23:02 +00:00
var moreActionsExpanded by remember { mutableStateOf(false) }
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
)
2023-02-04 15:41:43 +00:00
} else if (!account.isAcceptable(noteForReports) && !showHiddenNote) {
HiddenNote(
account.getRelevantReports(noteForReports),
account.userProfile(),
modifier,
2023-02-20 23:09:57 +00:00
isBoostedNote,
2023-02-04 15:41:43 +00:00
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-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-03-13 17:47:44 +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)
}
2023-03-08 22:07:56 +00:00
} else {
parentBackgroundColor ?: MaterialTheme.colors.background
2023-03-08 22:07:56 +00:00
}
Column(
modifier = modifier
.combinedClickable(
onClick = {
if (noteEvent is ChannelMessageEvent) {
baseChannel?.let {
navController.navigate("Channel/${it.idHex}")
}
} else if (noteEvent is PrivateDmEvent) {
navController.navigate("Room/${note.author?.pubkeyHex}") {
2023-03-08 22:07:56 +00:00
launchSingleTop = true
}
2023-03-08 22:07:56 +00:00
} else {
navController.navigate("Note/${note.idHex}") {
launchSingleTop = true
}
2023-03-08 22:07:56 +00:00
}
},
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,
top = if (addMarginTop) 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-02-06 22:16:27 +00:00
Column(Modifier.width(55.dp)) {
2023-03-08 22:07:56 +00:00
// Draws the boosted picture outside the boosted card.
Box(
modifier = Modifier
.width(55.dp)
.padding(0.dp)
) {
2023-02-06 22:16:27 +00:00
NoteAuthorPicture(note, navController, account.userProfile(), 55.dp)
if (noteEvent is RepostEvent) {
2023-02-06 22:16:27 +00:00
note.replyTo?.lastOrNull()?.let {
Box(
Modifier
.width(30.dp)
.height(30.dp)
2023-03-08 22:07:56 +00:00
.align(Alignment.BottomEnd)
) {
NoteAuthorPicture(
it,
navController,
account.userProfile(),
35.dp,
2023-02-06 22:16:27 +00:00
pictureModifier = Modifier.border(2.dp, MaterialTheme.colors.background, CircleShape)
)
}
}
}
2023-02-06 22:16:27 +00:00
// boosted picture
if (noteEvent is ChannelMessageEvent && baseChannel != null) {
2023-02-06 22:16:27 +00:00
val channelState by baseChannel.live.observeAsState()
val channel = channelState?.channel
if (channel != null) {
Box(
Modifier
.width(30.dp)
.height(30.dp)
2023-03-08 22:07:56 +00:00
.align(Alignment.BottomEnd)
) {
RobohashAsyncImageProxy(
robot = channel.idHex,
2023-02-15 17:31:15 +00:00
model = ResizeImage(channel.profilePicture(), 30.dp),
contentDescription = stringResource(R.string.group_picture),
2023-02-06 22:16:27 +00:00
modifier = Modifier
.width(30.dp)
.height(30.dp)
.clip(shape = CircleShape)
.background(MaterialTheme.colors.background)
.border(
2.dp,
MaterialTheme.colors.background,
CircleShape
)
)
}
}
}
}
2023-02-06 22:16:27 +00:00
if (noteEvent is RepostEvent) {
2023-02-06 22:16:27 +00:00
note.replyTo?.lastOrNull()?.let {
RelayBadges(it)
}
} else {
RelayBadges(baseNote)
}
2023-01-11 18:31:20 +00:00
}
}
2023-02-20 23:09:57 +00:00
Column(
modifier = Modifier.padding(start = if (!isBoostedNote && !isQuotedNote) 10.dp else 0.dp)
) {
2023-01-11 18:31:20 +00:00
Row(verticalAlignment = Alignment.CenterVertically) {
2023-02-20 23:09:57 +00:00
if (isQuotedNote) {
NoteAuthorPicture(note, navController, account.userProfile(), 25.dp)
Spacer(Modifier.padding(horizontal = 5.dp))
NoteUsernameDisplay(note, Modifier.weight(1f))
} else {
NoteUsernameDisplay(note, Modifier.weight(1f))
2023-02-20 23:09:57 +00:00
}
if (noteEvent is RepostEvent) {
2023-01-11 18:31:20 +00:00
Text(
" ${stringResource(id = R.string.boosted)}",
2023-01-11 18:31:20 +00:00
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
Text(
timeAgo(note.createdAt(), context = context),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
maxLines = 1
)
IconButton(
modifier = Modifier.then(Modifier.size(24.dp)),
onClick = { moreActionsExpanded = true }
) {
Icon(
imageVector = Icons.Default.MoreVert,
null,
modifier = Modifier.size(15.dp),
2023-03-08 22:07:56 +00:00
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
NoteDropDownMenu(baseNote, moreActionsExpanded, { moreActionsExpanded = false }, accountViewModel)
}
2023-01-11 18:31:20 +00:00
}
2023-02-27 22:14:15 +00:00
if (note.author != null && !makeItShort) {
2023-02-27 00:22:22 +00:00
ObserveDisplayNip05Status(note.author!!)
2023-02-27 22:14:15 +00:00
}
2023-02-27 22:14:15 +00:00
Spacer(modifier = Modifier.height(3.dp))
2023-02-27 00:22:22 +00:00
if (noteEvent is TextNoteEvent && (note.replyTo != null || noteEvent.mentions().isNotEmpty())) {
val sortedMentions = noteEvent.mentions()
.map { LocalCache.getOrCreateUser(it) }
.toSet()
.sortedBy { account.userProfile().isFollowing(it) }
2023-02-27 22:14:15 +00:00
val replyingDirectlyTo = note.replyTo?.lastOrNull()
if (replyingDirectlyTo != null && unPackReply) {
NoteCompose(
baseNote = replyingDirectlyTo,
isQuotedNote = true,
modifier = Modifier
.padding(0.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, sortedMentions, account, navController)
2023-02-27 22:14:15 +00:00
}
} else if (noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.mentions() != null)) {
val sortedMentions = noteEvent.mentions()
.map { LocalCache.getOrCreateUser(it) }
.toSet()
.sortedBy { account.userProfile().isFollowing(it) }
2023-02-26 21:56:12 +00:00
note.channel()?.let {
2023-02-26 21:56:12 +00:00
ReplyInformationChannel(note.replyTo, sortedMentions, it, navController)
}
2023-01-11 18:31:20 +00:00
}
if (noteEvent is ReactionEvent || noteEvent is RepostEvent) {
note.replyTo?.lastOrNull()?.let {
2023-01-11 18:31:20 +00:00
NoteCompose(
it,
modifier = Modifier,
2023-02-20 23:09:57 +00:00
isBoostedNote = true,
2023-02-27 22:14:15 +00:00
unPackReply = false,
parentBackgroundColor = backgroundColor,
2023-01-12 17:47:31 +00:00
accountViewModel = accountViewModel,
navController = navController
2023-01-11 18:31:20 +00:00
)
}
// Reposts have trash in their contents.
if (noteEvent is ReactionEvent) {
2023-01-11 18:31:20 +00:00
val refactorReactionText =
2023-03-13 17:47:44 +00:00
if (noteEvent.content == "+") "" else noteEvent.content
2023-01-11 18:31:20 +00:00
Text(
text = refactorReactionText
)
}
} else if (noteEvent is ReportEvent) {
val reportType = (noteEvent.reportedPost() + noteEvent.reportedAuthor()).map {
2023-03-01 15:07:59 +00:00
when (it.reportType) {
ReportEvent.ReportType.EXPLICIT -> stringResource(R.string.explicit_content)
2023-03-01 07:16:59 +00:00
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)
}
2023-02-28 20:24:23 +00:00
}.toSet().joinToString(", ")
Text(
text = reportType
)
Divider(
2023-02-28 20:24:23 +00:00
modifier = Modifier.padding(top = 40.dp),
thickness = 0.25.dp
)
} else if (noteEvent is LongTextNoteEvent) {
LongFormHeader(noteEvent)
ReactionsRow(note, accountViewModel)
2023-03-05 23:34:11 +00:00
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
)
} else if (noteEvent is BadgeAwardEvent && !note.replyTo.isNullOrEmpty()) {
Text(text = stringResource(R.string.award_granted_to))
FlowRow(modifier = Modifier.padding(top = 5.dp)) {
noteEvent.awardees()
.map { LocalCache.getOrCreateUser(it) }
.forEach {
2023-03-08 22:07:56 +00:00
UserPicture(
user = it,
navController = navController,
userAccount = account.userProfile(),
size = 35.dp
)
}
2023-03-05 23:34:11 +00:00
}
note.replyTo?.firstOrNull()?.let {
NoteCompose(
it,
modifier = Modifier,
isBoostedNote = false,
isQuotedNote = true,
unPackReply = false,
parentBackgroundColor = backgroundColor,
accountViewModel = accountViewModel,
navController = navController
)
}
ReactionsRow(note, accountViewModel)
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
)
} else if (noteEvent is PrivateDmEvent &&
noteEvent.recipientPubKey() != account.userProfile().pubkeyHex &&
note.author != account.userProfile()
) {
val recepient = noteEvent.recipientPubKey()?.let { LocalCache.checkGetOrCreateUser(it) }
TranslateableRichTextViewer(
stringResource(
id = R.string.private_conversation_notification,
"@${note.author?.pubkeyNpub()}",
"@${recepient?.pubkeyNpub()}"
),
canPreview = !makeItShort,
Modifier.fillMaxWidth(),
noteEvent.tags(),
backgroundColor,
accountViewModel,
navController
)
if (!makeItShort) {
ReactionsRow(note, accountViewModel)
}
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
)
2023-01-11 18:31:20 +00:00
} else {
val eventContent = accountViewModel.decrypt(note)
2023-03-08 22:07:56 +00:00
val canPreview = note.author == account.userProfile() ||
(note.author?.let { account.userProfile().isFollowing(it) } ?: true) ||
!noteForReports.hasAnyReports()
if (eventContent != null) {
TranslateableRichTextViewer(
eventContent,
2023-02-27 22:14:15 +00:00
canPreview = canPreview && !makeItShort,
Modifier.fillMaxWidth(),
2023-03-05 21:42:19 +00:00
noteEvent.tags(),
backgroundColor,
accountViewModel,
navController
)
}
2023-03-08 22:07:56 +00:00
if (!makeItShort) {
2023-02-27 22:14:15 +00:00
ReactionsRow(note, accountViewModel)
2023-03-08 22:07:56 +00:00
}
2023-01-11 18:31:20 +00:00
Divider(
2023-01-12 17:47:31 +00:00
modifier = Modifier.padding(top = 10.dp),
2023-01-11 18:31:20 +00:00
thickness = 0.25.dp
)
}
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
2023-01-11 18:31:20 +00:00
}
}
}
}
}
2023-03-05 23:34:11 +00:00
@Composable
fun BadgeDisplay(baseNote: Note) {
val badgeData = baseNote.event as? BadgeDefinitionEvent ?: return
Row(
modifier = Modifier
.padding(10.dp)
.clip(shape = CutCornerShape(20, 20, 0, 0))
.border(
5.dp,
MaterialTheme.colors.primary.copy(alpha = 0.32f),
CutCornerShape(20)
)
) {
Column {
badgeData.image()?.let {
AsyncImage(
model = it,
contentDescription = stringResource(
R.string.badge_award_image_for,
it
),
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
}
badgeData.name()?.let {
Text(
text = it,
style = MaterialTheme.typography.body1,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, top = 10.dp)
)
}
badgeData.description()?.let {
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
)
}
}
}
}
@Composable
private fun LongFormHeader(noteEvent: LongTextNoteEvent) {
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()
)
}
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
)
}
}
}
}
2023-02-06 22:16:27 +00:00
@Composable
private fun RelayBadges(baseNote: Note) {
val noteRelaysState by baseNote.live().relays.observeAsState()
2023-02-06 22:16:27 +00:00
val noteRelays = noteRelaysState?.note?.relays ?: emptySet()
var expanded by remember { mutableStateOf(false) }
val relaysToDisplay = if (expanded) noteRelays else noteRelays.take(3)
val uri = LocalUriHandler.current
FlowRow(Modifier.padding(top = 10.dp, start = 5.dp, end = 4.dp)) {
relaysToDisplay.forEach {
val url = it.removePrefix("wss://").removePrefix("ws://")
2023-02-12 23:23:02 +00:00
Box(
Modifier
.size(15.dp)
2023-03-08 22:07:56 +00:00
.padding(1.dp)
) {
RobohashFallbackAsyncImage(
robot = "https://$url/favicon.ico",
2023-03-08 22:07:56 +00:00
model = "https://$url/favicon.ico",
contentDescription = stringResource(R.string.relay_icon),
2023-02-06 22:16:27 +00:00
colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }),
modifier = Modifier
.fillMaxSize(1f)
.clip(shape = CircleShape)
.background(MaterialTheme.colors.background)
2023-02-12 23:23:02 +00:00
.clickable(onClick = { uri.openUri("https://" + url) })
2023-02-06 22:16:27 +00:00
)
}
}
}
if (noteRelays.size > 3 && !expanded) {
2023-02-12 23:23:02 +00:00
Row(
Modifier
.fillMaxWidth()
2023-03-08 22:07:56 +00:00
.height(25.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.Top
) {
2023-02-06 22:16:27 +00:00
IconButton(
modifier = Modifier.then(Modifier.size(24.dp)),
onClick = { expanded = true }
) {
Icon(
imageVector = Icons.Default.ExpandMore,
null,
modifier = Modifier.size(15.dp),
2023-03-08 22:07:56 +00:00
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
2023-02-06 22:16:27 +00:00
)
}
}
}
}
@Composable
fun NoteAuthorPicture(
note: Note,
navController: NavController,
userAccount: User,
size: Dp,
pictureModifier: Modifier = Modifier
) {
NoteAuthorPicture(note, 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",
contentDescription = stringResource(R.string.unknown_author),
2023-03-13 17:47:44 +00:00
modifier = modifier
.fillMaxSize(1f)
.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
Box(
Modifier
.width(size)
2023-03-08 22:07:56 +00:00
.height(size)
) {
RobohashAsyncImageProxy(
robot = user.pubkeyHex,
2023-02-15 17:31:15 +00:00
model = ResizeImage(user.profilePicture(), size),
contentDescription = stringResource(id = R.string.profile_image),
2023-03-13 17:47:44 +00:00
modifier = modifier
.fillMaxSize(1f)
.clip(shape = CircleShape)
.background(MaterialTheme.colors.background)
.run {
2023-03-08 22:07:56 +00:00
if (onClick != null && onLongClick != null) {
this.combinedClickable(onClick = { onClick(user) }, onLongClick = { onLongClick(user) })
} else if (onClick != null) {
this.clickable(onClick = { onClick(user) })
} else {
this
2023-03-08 22:07:56 +00:00
}
}
)
val accountState by baseUserAccount.live().follows.observeAsState()
val accountUser = accountState?.user ?: return
if (accountUser.isFollowing(user) || user == accountUser) {
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
)
}
}
}
}
@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) }
DropdownMenu(
expanded = popupExpanded,
onDismissRequest = onDismiss
) {
2023-03-08 22:07:56 +00:00
if (note.author != accountViewModel.accountLiveData.value?.account?.userProfile() && !accountViewModel.accountLiveData.value?.account?.userProfile()!!.isFollowing(note.author!!)) {
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-03-13 17:47:44 +00:00
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString("@${note.author?.pubkeyNpub()}")); onDismiss() }) {
Text(stringResource(R.string.copy_user_pubkey))
}
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(note.idNote())); 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()
DropdownMenuItem(onClick = { accountViewModel.broadcast(note); onDismiss() }) {
Text(stringResource(R.string.broadcast))
}
if (note.author == accountViewModel.accountLiveData.value?.account?.userProfile()) {
Divider()
DropdownMenuItem(onClick = { accountViewModel.delete(note); onDismiss() }) {
2023-03-11 04:00:35 +00:00
Text(stringResource(R.string.request_deletion))
}
}
2023-02-22 23:10:29 +00:00
if (note.author != accountViewModel.accountLiveData.value?.account?.userProfile()) {
2023-02-22 21:17:56 +00:00
Divider()
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
}