mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2024-09-29 08:20:51 +00:00
Trying to improve rendering by allowing recompositions to perform layout faster.
This commit is contained in:
parent
8fcd05a6bb
commit
b1de36f423
@ -1,6 +1,7 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
@ -30,24 +31,31 @@ fun RobohashAsyncImage(
|
||||
colorFilter: ColorFilter? = null,
|
||||
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality
|
||||
) {
|
||||
with(LocalDensity.current) {
|
||||
AsyncImage(
|
||||
model = Robohash.imageRequest(
|
||||
LocalContext.current,
|
||||
robot,
|
||||
Size(robotSize.roundToPx(), robotSize.roundToPx())
|
||||
),
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
transform = transform,
|
||||
onState = onState,
|
||||
alignment = alignment,
|
||||
contentScale = contentScale,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
filterQuality = filterQuality
|
||||
val context = LocalContext.current
|
||||
val size = with(LocalDensity.current) {
|
||||
robotSize.roundToPx()
|
||||
}
|
||||
|
||||
val imageRequest = remember(size, robot) {
|
||||
Robohash.imageRequest(
|
||||
context,
|
||||
robot,
|
||||
Size(size, size)
|
||||
)
|
||||
}
|
||||
|
||||
AsyncImage(
|
||||
model = imageRequest,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
transform = transform,
|
||||
onState = onState,
|
||||
alignment = alignment,
|
||||
contentScale = contentScale,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
filterQuality = filterQuality
|
||||
)
|
||||
}
|
||||
|
||||
var imageErrors = setOf<String>()
|
||||
@ -120,7 +128,7 @@ fun RobohashAsyncImageProxy(
|
||||
colorFilter: ColorFilter? = null,
|
||||
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality
|
||||
) {
|
||||
val proxy = model.proxyUrl()
|
||||
val proxy = remember(model) { model.proxyUrl() }
|
||||
if (proxy == null) {
|
||||
RobohashAsyncImage(
|
||||
robot = robot,
|
||||
|
@ -6,6 +6,7 @@ import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
@ -34,18 +35,18 @@ import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MessageSetCompose(messageSetCard: MessageSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) {
|
||||
fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) {
|
||||
val noteState by messageSetCard.note.live().metadata.observeAsState()
|
||||
val note = noteState?.note
|
||||
val note = remember(noteState) { noteState?.note }
|
||||
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
if (note == null) {
|
||||
BlankNote(Modifier, isInnerNote)
|
||||
BlankNote(Modifier)
|
||||
} else {
|
||||
var isNew by remember { mutableStateOf<Boolean>(false) }
|
||||
var isNew by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(key1 = messageSetCard.createdAt()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
@ -66,44 +67,40 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, isInnerNote: Boolean = fal
|
||||
MaterialTheme.colors.background
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.background(backgroundColor).combinedClickable(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
routeFor(
|
||||
note,
|
||||
accountViewModel.userProfile()
|
||||
)?.let { navController.navigate(it) }
|
||||
}
|
||||
},
|
||||
onLongClick = { popupExpanded = true }
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = if (!isInnerNote) 12.dp else 0.dp,
|
||||
end = if (!isInnerNote) 12.dp else 0.dp,
|
||||
top = 10.dp
|
||||
val columnModifier = remember {
|
||||
Modifier
|
||||
.background(backgroundColor)
|
||||
.padding(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = 10.dp
|
||||
)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
routeFor(
|
||||
note,
|
||||
accountViewModel.userProfile()
|
||||
)?.let { navController.navigate(it) }
|
||||
}
|
||||
},
|
||||
onLongClick = { popupExpanded = true }
|
||||
)
|
||||
.fillMaxWidth()
|
||||
}
|
||||
|
||||
Column(columnModifier) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Box(modifier = remember { Modifier.width(55.dp).padding(top = 5.dp, end = 5.dp) }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_dm),
|
||||
null,
|
||||
modifier = remember { Modifier.size(16.dp).align(Alignment.TopEnd) },
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
) {
|
||||
// Draws the like picture outside the boosted card.
|
||||
if (!isInnerNote) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(55.dp)
|
||||
.padding(top = 5.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_dm),
|
||||
null,
|
||||
modifier = Modifier.size(16.dp).align(Alignment.TopEnd),
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.padding(start = if (!isInnerNote) 10.dp else 0.dp)) {
|
||||
Column(modifier = remember { Modifier.padding(start = 10.dp) }) {
|
||||
NoteCompose(
|
||||
baseNote = messageSetCard.note,
|
||||
routeForLastRead = null,
|
||||
|
@ -19,6 +19,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@ -40,6 +41,7 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
|
||||
@ -52,19 +54,19 @@ import kotlinx.coroutines.launch
|
||||
@Composable
|
||||
fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) {
|
||||
val noteState by multiSetCard.note.live().metadata.observeAsState()
|
||||
val note = noteState?.note
|
||||
val note = remember(noteState) { noteState?.note }
|
||||
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account ?: return
|
||||
val account = remember(accountState) { accountState?.account }
|
||||
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
if (note == null) {
|
||||
if (note == null || account == null) {
|
||||
BlankNote(Modifier, false)
|
||||
} else {
|
||||
var isNew by remember { mutableStateOf<Boolean>(false) }
|
||||
var isNew by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(key1 = multiSetCard.createdAt()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
@ -84,112 +86,157 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
|
||||
MaterialTheme.colors.background
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
val columnModifier = remember {
|
||||
Modifier
|
||||
.background(backgroundColor)
|
||||
.padding(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = 10.dp
|
||||
)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
routeFor(note, account.userProfile())?.let { navController.navigate(it) }
|
||||
routeFor(
|
||||
note,
|
||||
account.userProfile()
|
||||
)?.let { navController.navigate(it) }
|
||||
}
|
||||
},
|
||||
onLongClick = { popupExpanded = true }
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = 10.dp
|
||||
)
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
if (multiSetCard.zapEvents.isNotEmpty()) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
// Draws the like picture outside the boosted card.
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(55.dp)
|
||||
.padding(0.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = "Zaps",
|
||||
tint = BitcoinOrange,
|
||||
modifier = Modifier
|
||||
.size(25.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
)
|
||||
}
|
||||
.fillMaxWidth()
|
||||
}
|
||||
|
||||
AuthorGalleryZaps(multiSetCard.zapEvents, navController, account, accountViewModel)
|
||||
}
|
||||
}
|
||||
val zapEvents = remember { multiSetCard.zapEvents }
|
||||
val boostEvents = remember { multiSetCard.boostEvents }
|
||||
val likeEvents = remember { multiSetCard.likeEvents }
|
||||
|
||||
if (multiSetCard.boostEvents.isNotEmpty()) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(55.dp)
|
||||
.padding(end = 4.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_retweeted),
|
||||
null,
|
||||
modifier = Modifier
|
||||
.size(18.dp)
|
||||
.align(Alignment.TopEnd),
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
Column(modifier = columnModifier) {
|
||||
if (zapEvents.isNotEmpty()) {
|
||||
RenderZapGallery(zapEvents, navController, account, accountViewModel)
|
||||
}
|
||||
|
||||
AuthorGallery(multiSetCard.boostEvents, navController, account, accountViewModel)
|
||||
}
|
||||
}
|
||||
if (boostEvents.isNotEmpty()) {
|
||||
RenderBoostGallery(boostEvents, navController, account, accountViewModel)
|
||||
}
|
||||
|
||||
if (multiSetCard.likeEvents.isNotEmpty()) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(55.dp)
|
||||
.padding(end = 5.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_liked),
|
||||
null,
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.align(Alignment.TopEnd),
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
if (likeEvents.isNotEmpty()) {
|
||||
RenderLikeGallery(likeEvents, navController, account, accountViewModel)
|
||||
}
|
||||
|
||||
AuthorGallery(multiSetCard.likeEvents, navController, account, accountViewModel)
|
||||
}
|
||||
}
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Spacer(modifier = Modifier.width(65.dp))
|
||||
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Spacer(modifier = Modifier.width(65.dp))
|
||||
NoteCompose(
|
||||
baseNote = multiSetCard.note,
|
||||
routeForLastRead = null,
|
||||
modifier = Modifier.padding(top = 5.dp),
|
||||
isBoostedNote = true,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
navController = navController
|
||||
)
|
||||
|
||||
NoteCompose(
|
||||
baseNote = multiSetCard.note,
|
||||
routeForLastRead = null,
|
||||
modifier = Modifier.padding(top = 5.dp),
|
||||
isBoostedNote = true,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
navController = navController
|
||||
)
|
||||
|
||||
NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||
}
|
||||
}
|
||||
NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderLikeGallery(
|
||||
likeEvents: List<Note>,
|
||||
navController: NavController,
|
||||
account: Account,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.width(55.dp)
|
||||
.padding(end = 5.dp)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_liked),
|
||||
null,
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.size(16.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
},
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
|
||||
AuthorGallery(likeEvents, navController, account, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderZapGallery(
|
||||
zapEvents: Map<Note, Note>,
|
||||
navController: NavController,
|
||||
account: Account,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.width(55.dp)
|
||||
.padding(0.dp)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = "Zaps",
|
||||
tint = BitcoinOrange,
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.size(25.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AuthorGalleryZaps(zapEvents, navController, account, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderBoostGallery(
|
||||
boostEvents: List<Note>,
|
||||
navController: NavController,
|
||||
account: Account,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.width(55.dp)
|
||||
.padding(end = 4.dp)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_retweeted),
|
||||
null,
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.size(18.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
},
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
|
||||
AuthorGallery(boostEvents, navController, account, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AuthorGalleryZaps(
|
||||
authorNotes: Map<Note, Note>,
|
||||
@ -197,13 +244,16 @@ fun AuthorGalleryZaps(
|
||||
account: Account,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val accountState by account.userProfile().live().follows.observeAsState()
|
||||
val accountUser = accountState?.user ?: return
|
||||
val accountState = account.userProfile().live().follows.observeAsState()
|
||||
|
||||
val listToRender = remember {
|
||||
authorNotes.keys.take(50)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
FlowRow() {
|
||||
authorNotes.forEach {
|
||||
AuthorPictureAndComment(it.key, navController, accountUser, accountViewModel)
|
||||
listToRender.forEach {
|
||||
AuthorPictureAndComment(it, navController, accountState, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -213,7 +263,7 @@ fun AuthorGalleryZaps(
|
||||
private fun AuthorPictureAndComment(
|
||||
zapRequest: Note,
|
||||
navController: NavController,
|
||||
accountUser: User,
|
||||
accountUser: State<UserState?>,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val author = zapRequest.author ?: return
|
||||
@ -245,19 +295,25 @@ private fun AuthorPictureAndComment(
|
||||
author: User,
|
||||
comment: String?,
|
||||
navController: NavController,
|
||||
accountUser: User,
|
||||
accountUser: State<UserState?>,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val modifier = if (!comment.isNullOrBlank()) {
|
||||
Modifier.fillMaxWidth()
|
||||
} else {
|
||||
Modifier
|
||||
val modifier = remember(comment) {
|
||||
if (!comment.isNullOrBlank()) {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
navController.navigate("User/${author.pubkeyHex}")
|
||||
}
|
||||
} else {
|
||||
Modifier.clickable {
|
||||
navController.navigate("User/${author.pubkeyHex}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier.clickable {
|
||||
navController.navigate("User/${author.pubkeyHex}")
|
||||
},
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
FastNoteAuthorPicture(
|
||||
@ -288,20 +344,22 @@ fun AuthorGallery(
|
||||
account: Account,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val accountState by account.userProfile().live().follows.observeAsState()
|
||||
val accountUser = accountState?.user ?: return
|
||||
val accountState = account.userProfile().live().follows.observeAsState()
|
||||
val listToRender = remember {
|
||||
Pair(
|
||||
authorNotes.take(50).mapNotNull { it.author },
|
||||
authorNotes.size
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
FlowRow() {
|
||||
authorNotes.take(50).forEach {
|
||||
val author = it.author
|
||||
if (author != null) {
|
||||
AuthorPictureAndComment(author, null, navController, accountUser, accountViewModel)
|
||||
}
|
||||
listToRender.first.forEach { author ->
|
||||
AuthorPictureAndComment(author, null, navController, accountState, accountViewModel)
|
||||
}
|
||||
|
||||
if (authorNotes.size > 50) {
|
||||
Text(" and ${authorNotes.size - 50} others")
|
||||
if (listToRender.second > 50) {
|
||||
Text(" and ${listToRender.second - 50} others")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -310,18 +368,26 @@ fun AuthorGallery(
|
||||
@Composable
|
||||
fun FastNoteAuthorPicture(
|
||||
author: User,
|
||||
userAccount: User,
|
||||
userAccount: State<UserState?>,
|
||||
size: Dp,
|
||||
pictureModifier: Modifier = Modifier
|
||||
) {
|
||||
val userState by author.live().metadata.observeAsState()
|
||||
val user = userState?.user ?: return
|
||||
val profilePicture = remember(userState) {
|
||||
userState?.user?.profilePicture()
|
||||
}
|
||||
|
||||
val showFollowingMark = userAccount.isFollowingCached(user) || user === userAccount
|
||||
val authorPubKey = remember {
|
||||
author.pubkeyHex
|
||||
}
|
||||
|
||||
val showFollowingMark = remember(userAccount.value) {
|
||||
userAccount.value?.user?.isFollowingCached(author) == true || (author === userAccount.value?.user)
|
||||
}
|
||||
|
||||
UserPicture(
|
||||
userHex = user.pubkeyHex,
|
||||
userPicture = user.profilePicture(),
|
||||
userHex = authorPubKey,
|
||||
userPicture = profilePicture,
|
||||
showFollowingMark = showFollowingMark,
|
||||
size = size,
|
||||
modifier = pictureModifier
|
||||
|
@ -96,6 +96,7 @@ import com.vitorpamplona.amethyst.service.model.PollNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ReportedKey
|
||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
||||
@ -178,208 +179,226 @@ fun NoteComposeInner(
|
||||
navController: NavController
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account ?: return
|
||||
val loggedIn = account.userProfile()
|
||||
val account = remember(accountState) { accountState?.account } ?: return
|
||||
val loggedIn = remember(accountState) { accountState?.account?.userProfile() } ?: return
|
||||
|
||||
val noteState by baseNote.live().metadata.observeAsState()
|
||||
val note = noteState?.note
|
||||
val note = remember(noteState) { noteState?.note }
|
||||
|
||||
val noteReportsState by baseNote.live().reports.observeAsState()
|
||||
val noteForReports = noteReportsState?.note ?: return
|
||||
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
var showHiddenNote by remember { mutableStateOf(false) }
|
||||
|
||||
var isAcceptableAndCanPreview by remember { mutableStateOf(Pair(true, true)) }
|
||||
|
||||
LaunchedEffect(key1 = noteReportsState) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val newCanPreview = note?.author === loggedIn ||
|
||||
(note?.author?.let { loggedIn.isFollowingCached(it) } ?: true) ||
|
||||
!noteForReports.hasAnyReports()
|
||||
|
||||
val newIsAcceptable = account.isAcceptable(noteForReports)
|
||||
|
||||
if (newIsAcceptable != isAcceptableAndCanPreview.first && newCanPreview != isAcceptableAndCanPreview.second) {
|
||||
isAcceptableAndCanPreview = Pair(newIsAcceptable, newCanPreview)
|
||||
}
|
||||
}
|
||||
}
|
||||
val noteForReports = remember(noteReportsState) { noteReportsState?.note } ?: return
|
||||
|
||||
val noteEvent = note?.event
|
||||
val baseChannel = note?.channel()
|
||||
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
if (noteEvent == null) {
|
||||
BlankNote(
|
||||
modifier.combinedClickable(
|
||||
onClick = { },
|
||||
onLongClick = { popupExpanded = true }
|
||||
),
|
||||
isBoostedNote
|
||||
)
|
||||
} else if (!isAcceptableAndCanPreview.first && !showHiddenNote) {
|
||||
if (!account.isHidden(noteForReports.author!!)) {
|
||||
HiddenNote(
|
||||
account.getRelevantReports(noteForReports),
|
||||
loggedIn,
|
||||
modifier,
|
||||
isBoostedNote,
|
||||
navController,
|
||||
onClick = { showHiddenNote = true }
|
||||
)
|
||||
}
|
||||
} else if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && baseChannel != null) {
|
||||
ChannelHeader(baseChannel = baseChannel, account = account, navController = navController)
|
||||
} else if (noteEvent is BadgeDefinitionEvent) {
|
||||
BadgeDisplay(baseNote = note)
|
||||
} else if (noteEvent is FileHeaderEvent) {
|
||||
FileHeaderDisplay(note)
|
||||
} else if (noteEvent is FileStorageHeaderEvent) {
|
||||
FileStorageHeaderDisplay(note)
|
||||
} else {
|
||||
var isNew by remember { mutableStateOf<Boolean>(false) }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(key1 = routeForLastRead) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
routeForLastRead?.let {
|
||||
val lastTime = NotificationCache.load(it)
|
||||
|
||||
val createdAt = note.createdAt()
|
||||
if (createdAt != null) {
|
||||
NotificationCache.markAsRead(it, createdAt)
|
||||
|
||||
val newIsNew = createdAt > lastTime
|
||||
if (newIsNew != isNew) {
|
||||
isNew = newIsNew
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
routeFor(note, loggedIn)?.let { navController.navigate(it) }
|
||||
}
|
||||
},
|
||||
remember {
|
||||
modifier.combinedClickable(
|
||||
onClick = { },
|
||||
onLongClick = { popupExpanded = true }
|
||||
)
|
||||
.background(backgroundColor)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = if (!isBoostedNote) 12.dp else 0.dp,
|
||||
end = if (!isBoostedNote) 12.dp else 0.dp,
|
||||
top = if (addMarginTop && !isBoostedNote) 10.dp else 0.dp
|
||||
)
|
||||
) {
|
||||
if (!isBoostedNote && !isQuotedNote) {
|
||||
DrawAuthorImages(baseNote, loggedIn, navController)
|
||||
},
|
||||
isBoostedNote
|
||||
)
|
||||
|
||||
note?.let {
|
||||
NoteQuickActionMenu(it, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||
}
|
||||
} else {
|
||||
var showHiddenNote by remember { mutableStateOf(false) }
|
||||
var isAcceptableAndCanPreview by remember { mutableStateOf(Pair(true, true)) }
|
||||
|
||||
LaunchedEffect(key1 = noteReportsState, key2 = accountState) {
|
||||
withContext(Dispatchers.IO) {
|
||||
account.userProfile().let { loggedIn ->
|
||||
val newCanPreview = note.author === loggedIn ||
|
||||
(note.author?.let { loggedIn.isFollowingCached(it) } ?: true) ||
|
||||
!(noteForReports.hasAnyReports())
|
||||
|
||||
val newIsAcceptable = account.isAcceptable(noteForReports)
|
||||
|
||||
if (newIsAcceptable != isAcceptableAndCanPreview.first && newCanPreview != isAcceptableAndCanPreview.second) {
|
||||
isAcceptableAndCanPreview = Pair(newIsAcceptable, newCanPreview)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = if (!isBoostedNote && !isQuotedNote) 10.dp else 0.dp)
|
||||
) {
|
||||
FirstUserInfoRow(
|
||||
baseNote = baseNote,
|
||||
showAuthorPicture = isQuotedNote,
|
||||
account = account,
|
||||
accountViewModel = accountViewModel,
|
||||
navController = navController
|
||||
if (!isAcceptableAndCanPreview.first && !showHiddenNote) {
|
||||
if (!account.isHidden(noteForReports.author!!)) {
|
||||
HiddenNote(
|
||||
account.getRelevantReports(noteForReports),
|
||||
account.userProfile(),
|
||||
modifier,
|
||||
isBoostedNote,
|
||||
navController,
|
||||
onClick = { showHiddenNote = true }
|
||||
)
|
||||
}
|
||||
} else if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && baseChannel != null) {
|
||||
ChannelHeader(baseChannel = baseChannel, account = account, navController = navController)
|
||||
} else if (noteEvent is BadgeDefinitionEvent) {
|
||||
BadgeDisplay(baseNote = note)
|
||||
} else if (noteEvent is FileHeaderEvent) {
|
||||
FileHeaderDisplay(note)
|
||||
} else if (noteEvent is FileStorageHeaderEvent) {
|
||||
FileStorageHeaderDisplay(note)
|
||||
} else {
|
||||
var isNew by remember { mutableStateOf<Boolean>(false) }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(key1 = routeForLastRead) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
routeForLastRead?.let {
|
||||
val lastTime = NotificationCache.load(it)
|
||||
|
||||
val createdAt = note.createdAt()
|
||||
if (createdAt != null) {
|
||||
NotificationCache.markAsRead(it, createdAt)
|
||||
|
||||
val newIsNew = createdAt > lastTime
|
||||
if (newIsNew != isNew) {
|
||||
isNew = newIsNew
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
val columnModifier = remember(backgroundColor) {
|
||||
modifier
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
routeFor(note, loggedIn)?.let {
|
||||
navController.navigate(it)
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongClick = { popupExpanded = true }
|
||||
)
|
||||
.background(backgroundColor)
|
||||
}
|
||||
|
||||
if (noteEvent !is RepostEvent && !makeItShort && !isQuotedNote) {
|
||||
SecondUserInfoRow(
|
||||
note,
|
||||
account,
|
||||
navController
|
||||
)
|
||||
Column(modifier = columnModifier) {
|
||||
Row(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.padding(
|
||||
start = if (!isBoostedNote) 12.dp else 0.dp,
|
||||
end = if (!isBoostedNote) 12.dp else 0.dp,
|
||||
top = if (addMarginTop && !isBoostedNote) 10.dp else 0.dp
|
||||
)
|
||||
}
|
||||
) {
|
||||
if (!isBoostedNote && !isQuotedNote) {
|
||||
DrawAuthorImages(baseNote, loggedIn, navController)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
if (!makeItShort) {
|
||||
ReplyRow(
|
||||
note,
|
||||
unPackReply,
|
||||
backgroundColor,
|
||||
account,
|
||||
accountViewModel,
|
||||
navController
|
||||
Column(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.padding(start = if (!isBoostedNote && !isQuotedNote) 10.dp else 0.dp)
|
||||
}
|
||||
) {
|
||||
FirstUserInfoRow(
|
||||
baseNote = baseNote,
|
||||
showAuthorPicture = isQuotedNote,
|
||||
account = account,
|
||||
accountViewModel = accountViewModel,
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
|
||||
when (noteEvent) {
|
||||
is ReactionEvent -> {
|
||||
RenderReaction(note, backgroundColor, accountViewModel, navController)
|
||||
}
|
||||
|
||||
is RepostEvent -> {
|
||||
RenderRepost(note, backgroundColor, accountViewModel, navController)
|
||||
}
|
||||
|
||||
is ReportEvent -> {
|
||||
RenderReport(note)
|
||||
}
|
||||
|
||||
is LongTextNoteEvent -> {
|
||||
RenderLongFormContent(note, loggedIn, accountViewModel, navController)
|
||||
}
|
||||
|
||||
is BadgeAwardEvent -> {
|
||||
RenderBadgeAward(note, backgroundColor, accountViewModel, navController)
|
||||
}
|
||||
|
||||
is PrivateDmEvent -> {
|
||||
RenderPrivateMessage(note, makeItShort, isAcceptableAndCanPreview.second, backgroundColor, accountViewModel, navController)
|
||||
}
|
||||
|
||||
is HighlightEvent -> {
|
||||
RenderHighlight(note, makeItShort, isAcceptableAndCanPreview.second, backgroundColor, accountViewModel, navController)
|
||||
}
|
||||
|
||||
is PollNoteEvent -> {
|
||||
RenderPoll(
|
||||
if (noteEvent !is RepostEvent && !makeItShort && !isQuotedNote) {
|
||||
SecondUserInfoRow(
|
||||
note,
|
||||
makeItShort,
|
||||
isAcceptableAndCanPreview.second,
|
||||
account,
|
||||
navController
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
if (!makeItShort) {
|
||||
ReplyRow(
|
||||
note,
|
||||
unPackReply,
|
||||
backgroundColor,
|
||||
account,
|
||||
accountViewModel,
|
||||
navController
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
RenderTextEvent(
|
||||
note,
|
||||
makeItShort,
|
||||
isAcceptableAndCanPreview.second,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
navController
|
||||
)
|
||||
}
|
||||
}
|
||||
when (noteEvent) {
|
||||
is ReactionEvent -> {
|
||||
RenderReaction(note, backgroundColor, accountViewModel, navController)
|
||||
}
|
||||
|
||||
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||
is RepostEvent -> {
|
||||
RenderRepost(note, backgroundColor, accountViewModel, navController)
|
||||
}
|
||||
|
||||
is ReportEvent -> {
|
||||
RenderReport(note)
|
||||
}
|
||||
|
||||
is LongTextNoteEvent -> {
|
||||
RenderLongFormContent(note, loggedIn, accountViewModel, navController)
|
||||
}
|
||||
|
||||
is BadgeAwardEvent -> {
|
||||
RenderBadgeAward(note, backgroundColor, accountViewModel, navController)
|
||||
}
|
||||
|
||||
is PrivateDmEvent -> {
|
||||
RenderPrivateMessage(note, makeItShort, isAcceptableAndCanPreview.second, backgroundColor, accountViewModel, navController)
|
||||
}
|
||||
|
||||
is HighlightEvent -> {
|
||||
RenderHighlight(note, makeItShort, isAcceptableAndCanPreview.second, backgroundColor, accountViewModel, navController)
|
||||
}
|
||||
|
||||
is PollNoteEvent -> {
|
||||
RenderPoll(
|
||||
note,
|
||||
makeItShort,
|
||||
isAcceptableAndCanPreview.second,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
navController
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
RenderTextEvent(
|
||||
note,
|
||||
makeItShort,
|
||||
isAcceptableAndCanPreview.second,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
navController
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -424,13 +443,14 @@ private fun RenderTextEvent(
|
||||
accountViewModel: AccountViewModel,
|
||||
navController: NavController
|
||||
) {
|
||||
val noteEvent = note.event ?: return
|
||||
|
||||
val tags = remember { note.event?.tags() }
|
||||
val hashtags = remember { note.event?.hashtags() ?: emptyList() }
|
||||
val eventContent = remember { accountViewModel.decrypt(note) }
|
||||
val modifier = remember { Modifier.fillMaxWidth() }
|
||||
val isAuthorTheLoggedUser = remember { accountViewModel.isLoggedUser(note.author) }
|
||||
|
||||
if (eventContent != null) {
|
||||
if (makeItShort && accountViewModel.isLoggedUser(note.author)) {
|
||||
if (makeItShort && isAuthorTheLoggedUser) {
|
||||
Text(
|
||||
text = eventContent,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
@ -442,13 +462,13 @@ private fun RenderTextEvent(
|
||||
content = eventContent,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
modifier = modifier,
|
||||
tags = noteEvent.tags(),
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
navController = navController
|
||||
)
|
||||
|
||||
DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, navController)
|
||||
DisplayUncitedHashtags(hashtags, eventContent, navController)
|
||||
}
|
||||
}
|
||||
|
||||
@ -637,9 +657,11 @@ private fun RenderBadgeAward(
|
||||
FlowRow(modifier = Modifier.padding(top = 5.dp)) {
|
||||
awardees.take(100).forEach { user ->
|
||||
Row(
|
||||
modifier = Modifier.size(size = 35.dp).clickable {
|
||||
navController.navigate("User/${user.pubkeyHex}")
|
||||
},
|
||||
modifier = Modifier
|
||||
.size(size = 35.dp)
|
||||
.clickable {
|
||||
navController.navigate("User/${user.pubkeyHex}")
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
UserPicture(
|
||||
@ -711,7 +733,11 @@ private fun RenderRepost(
|
||||
accountViewModel: AccountViewModel,
|
||||
navController: NavController
|
||||
) {
|
||||
note.replyTo?.lastOrNull()?.let {
|
||||
val boostedNote = remember {
|
||||
note.replyTo?.lastOrNull()
|
||||
}
|
||||
|
||||
boostedNote?.let {
|
||||
NoteCompose(
|
||||
it,
|
||||
modifier = Modifier,
|
||||
@ -745,9 +771,16 @@ private fun RenderLongFormContent(
|
||||
|
||||
@Composable
|
||||
private fun RenderReport(note: Note) {
|
||||
val noteEvent = note.event as? ReportEvent ?: return
|
||||
val base = remember {
|
||||
val noteEvent = note.event as? ReportEvent
|
||||
if (noteEvent == null) {
|
||||
emptyList<ReportedKey>()
|
||||
} else {
|
||||
(noteEvent.reportedPost() + noteEvent.reportedAuthor())
|
||||
}
|
||||
}
|
||||
|
||||
val reportType = (noteEvent.reportedPost() + noteEvent.reportedAuthor()).map {
|
||||
val reportType = base.map {
|
||||
when (it.reportType) {
|
||||
ReportEvent.ReportType.EXPLICIT -> stringResource(R.string.explicit_content)
|
||||
ReportEvent.ReportType.NUDITY -> stringResource(R.string.nudity)
|
||||
@ -780,7 +813,9 @@ private fun ReplyRow(
|
||||
val noteEvent = note.event
|
||||
|
||||
if (noteEvent is TextNoteEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())) {
|
||||
val replyingDirectlyTo = note.replyTo?.lastOrNull()
|
||||
val replyingDirectlyTo = remember {
|
||||
note.replyTo?.lastOrNull()
|
||||
}
|
||||
if (replyingDirectlyTo != null && unPackReply) {
|
||||
NoteCompose(
|
||||
baseNote = replyingDirectlyTo,
|
||||
@ -821,18 +856,18 @@ private fun SecondUserInfoRow(
|
||||
account: Account,
|
||||
navController: NavController
|
||||
) {
|
||||
val noteEvent = note.event ?: return
|
||||
val noteAuthor = note.author ?: return
|
||||
val noteEvent = remember { note.event } ?: return
|
||||
val noteAuthor = remember { note.author } ?: return
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
ObserveDisplayNip05Status(noteAuthor, Modifier.weight(1f))
|
||||
|
||||
val baseReward = noteEvent.getReward()
|
||||
val baseReward = remember { noteEvent.getReward() }
|
||||
if (baseReward != null) {
|
||||
DisplayReward(baseReward, note, account, navController)
|
||||
}
|
||||
|
||||
val pow = noteEvent.getPoWRank()
|
||||
val pow = remember { noteEvent.getPoWRank() }
|
||||
if (pow > 20) {
|
||||
DisplayPoW(pow)
|
||||
}
|
||||
@ -848,14 +883,17 @@ private fun FirstUserInfoRow(
|
||||
navController: NavController
|
||||
) {
|
||||
var moreActionsExpanded by remember { mutableStateOf(false) }
|
||||
val eventNote = baseNote.event ?: return
|
||||
val time = baseNote.createdAt() ?: return
|
||||
val loggedIn = account.userProfile()
|
||||
val eventNote = remember { baseNote.event } ?: return
|
||||
val time = remember { baseNote.createdAt() } ?: return
|
||||
val loggedIn = remember { account.userProfile() }
|
||||
val padding = remember {
|
||||
Modifier.padding(horizontal = 5.dp)
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (showAuthorPicture) {
|
||||
NoteAuthorPicture(baseNote, navController, loggedIn, 25.dp)
|
||||
Spacer(Modifier.padding(horizontal = 5.dp))
|
||||
Spacer(padding)
|
||||
NoteUsernameDisplay(baseNote, Modifier.weight(1f))
|
||||
} else {
|
||||
NoteUsernameDisplay(baseNote, Modifier.weight(1f))
|
||||
@ -916,10 +954,11 @@ fun TimeAgo(time: Long) {
|
||||
@Composable
|
||||
private fun DrawAuthorImages(baseNote: Note, loggedIn: User, navController: NavController) {
|
||||
val baseChannel = remember { baseNote.channel() }
|
||||
val modifier = remember { Modifier.width(55.dp) }
|
||||
|
||||
Column(Modifier.width(55.dp)) {
|
||||
Column(modifier) {
|
||||
// Draws the boosted picture outside the boosted card.
|
||||
Box(modifier = Modifier.width(55.dp), contentAlignment = Alignment.BottomEnd) {
|
||||
Box(modifier = modifier, contentAlignment = Alignment.BottomEnd) {
|
||||
NoteAuthorPicture(baseNote, navController, loggedIn, 55.dp)
|
||||
|
||||
if (baseNote.event is RepostEvent) {
|
||||
@ -932,7 +971,10 @@ private fun DrawAuthorImages(baseNote: Note, loggedIn: User, navController: NavC
|
||||
}
|
||||
|
||||
if (baseNote.event is RepostEvent) {
|
||||
baseNote.replyTo?.lastOrNull()?.let {
|
||||
val baseReply = remember {
|
||||
baseNote.replyTo?.lastOrNull()
|
||||
}
|
||||
baseReply?.let {
|
||||
RelayBadges(it)
|
||||
}
|
||||
} else {
|
||||
@ -946,20 +988,30 @@ private fun ChannelNotePicture(baseChannel: Channel) {
|
||||
val channelState by baseChannel.live.observeAsState()
|
||||
val channel = channelState?.channel
|
||||
|
||||
val modifier = remember {
|
||||
Modifier
|
||||
.width(30.dp)
|
||||
.height(30.dp)
|
||||
.clip(shape = CircleShape)
|
||||
}
|
||||
|
||||
val boxModifier = remember {
|
||||
Modifier
|
||||
.width(30.dp)
|
||||
.height(30.dp)
|
||||
}
|
||||
|
||||
if (channel != null) {
|
||||
Box(
|
||||
Modifier
|
||||
.width(30.dp)
|
||||
.height(30.dp)
|
||||
) {
|
||||
val model = remember(channelState) {
|
||||
ResizeImage(channel.profilePicture(), 30.dp)
|
||||
}
|
||||
|
||||
Box(boxModifier) {
|
||||
RobohashAsyncImageProxy(
|
||||
robot = channel.idHex,
|
||||
model = ResizeImage(channel.profilePicture(), 30.dp),
|
||||
model = model,
|
||||
contentDescription = stringResource(R.string.group_picture),
|
||||
modifier = Modifier
|
||||
.width(30.dp)
|
||||
.height(30.dp)
|
||||
.clip(shape = CircleShape)
|
||||
modifier = modifier
|
||||
.background(MaterialTheme.colors.background)
|
||||
.border(
|
||||
2.dp,
|
||||
@ -977,12 +1029,16 @@ private fun RepostNoteAuthorPicture(
|
||||
navController: NavController,
|
||||
loggedIn: User
|
||||
) {
|
||||
baseNote.replyTo?.lastOrNull()?.let {
|
||||
Box(
|
||||
Modifier
|
||||
.width(30.dp)
|
||||
.height(30.dp)
|
||||
) {
|
||||
val baseRepost = remember { baseNote.replyTo?.lastOrNull() }
|
||||
|
||||
val modifier = remember {
|
||||
Modifier
|
||||
.width(30.dp)
|
||||
.height(30.dp)
|
||||
}
|
||||
|
||||
baseRepost?.let {
|
||||
Box(modifier) {
|
||||
NoteAuthorPicture(
|
||||
it,
|
||||
navController,
|
||||
@ -1041,13 +1097,14 @@ fun DisplayHighlight(
|
||||
authorHex?.let { authorHex ->
|
||||
userBase?.let { userBase ->
|
||||
val userState by userBase.live().metadata.observeAsState()
|
||||
val user = userState?.user
|
||||
val route = remember { "User/${userBase.pubkeyHex}" }
|
||||
val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() }
|
||||
|
||||
if (user != null) {
|
||||
if (userDisplayName != null) {
|
||||
CreateClickableText(
|
||||
user.toBestDisplayName(),
|
||||
userDisplayName,
|
||||
"",
|
||||
"User/${user.pubkeyHex}",
|
||||
route,
|
||||
navController
|
||||
)
|
||||
}
|
||||
@ -1055,11 +1112,13 @@ fun DisplayHighlight(
|
||||
}
|
||||
|
||||
url?.let { url ->
|
||||
val validatedUrl = try {
|
||||
URL(url)
|
||||
} catch (e: Exception) {
|
||||
Log.w("Note Compose", "Invalid URI: $url")
|
||||
null
|
||||
val validatedUrl = remember {
|
||||
try {
|
||||
URL(url)
|
||||
} catch (e: Exception) {
|
||||
Log.w("Note Compose", "Invalid URI: $url")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
validatedUrl?.host?.let { host ->
|
||||
@ -1524,44 +1583,59 @@ private fun VerticalRelayPanelWithFlow(
|
||||
private fun RelayIconCompose(url: String) {
|
||||
val uri = LocalUriHandler.current
|
||||
|
||||
Box(
|
||||
val model = remember(url) { "https://$url/favicon.ico" }
|
||||
|
||||
val boxModifier = remember {
|
||||
Modifier
|
||||
.padding(1.dp)
|
||||
.size(15.dp)
|
||||
) {
|
||||
}
|
||||
val iconModifier = remember(url) {
|
||||
Modifier
|
||||
.width(13.dp)
|
||||
.height(13.dp)
|
||||
.clip(shape = CircleShape)
|
||||
.clickable(onClick = { uri.openUri("https://$url") })
|
||||
}
|
||||
val colorFilter = remember {
|
||||
ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) })
|
||||
}
|
||||
|
||||
Box(boxModifier) {
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = "https://$url/favicon.ico",
|
||||
robot = model,
|
||||
robotSize = 15.dp,
|
||||
model = "https://$url/favicon.ico",
|
||||
model = model,
|
||||
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") })
|
||||
colorFilter = colorFilter,
|
||||
modifier = iconModifier.background(MaterialTheme.colors.background)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowMoreRelaysButton(onClick: () -> Unit) {
|
||||
Row(
|
||||
val boxModifier = remember {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(25.dp),
|
||||
.height(25.dp)
|
||||
}
|
||||
val iconButtonModifier = remember { Modifier.size(24.dp) }
|
||||
val iconModifier = remember { Modifier.size(15.dp) }
|
||||
|
||||
Row(
|
||||
boxModifier,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
IconButton(
|
||||
modifier = Modifier.then(Modifier.size(24.dp)),
|
||||
modifier = iconButtonModifier,
|
||||
onClick = onClick
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ExpandMore,
|
||||
null,
|
||||
modifier = Modifier.size(15.dp),
|
||||
modifier = iconModifier,
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
}
|
||||
@ -1590,25 +1664,30 @@ fun NoteAuthorPicture(
|
||||
onClick: ((User) -> Unit)? = null
|
||||
) {
|
||||
val noteState by baseNote.live().metadata.observeAsState()
|
||||
val note = noteState?.note ?: return
|
||||
val author = remember(noteState) {
|
||||
noteState?.note?.author
|
||||
}
|
||||
|
||||
val author = note.author
|
||||
|
||||
Box(
|
||||
val boxModifier = remember {
|
||||
Modifier
|
||||
.width(size)
|
||||
.height(size)
|
||||
) {
|
||||
}
|
||||
|
||||
val nullModifier = remember {
|
||||
modifier
|
||||
.width(size)
|
||||
.height(size)
|
||||
.clip(shape = CircleShape)
|
||||
}
|
||||
|
||||
Box(boxModifier) {
|
||||
if (author == null) {
|
||||
RobohashAsyncImage(
|
||||
robot = "authornotfound",
|
||||
robotSize = size,
|
||||
contentDescription = stringResource(R.string.unknown_author),
|
||||
modifier = modifier
|
||||
.width(size)
|
||||
.height(size)
|
||||
.clip(shape = CircleShape)
|
||||
.background(MaterialTheme.colors.background)
|
||||
modifier = nullModifier.background(MaterialTheme.colors.background)
|
||||
)
|
||||
} else {
|
||||
UserPicture(author, baseUserAccount, size, modifier, onClick)
|
||||
@ -1640,31 +1719,38 @@ fun UserPicture(
|
||||
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
|
||||
val userPubkey = remember {
|
||||
baseUser.pubkeyHex
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
) {
|
||||
val userProfile = remember(userState) {
|
||||
userState?.user?.profilePicture()
|
||||
}
|
||||
|
||||
val showFollowingMark = remember(accountState) {
|
||||
accountState?.user?.isFollowingCached(baseUser) == true || baseUser === accountState?.user
|
||||
}
|
||||
|
||||
// BaseUser is the same reference as accountState.user
|
||||
val myModifier = remember {
|
||||
if (onClick != null && onLongClick != null) {
|
||||
Modifier.combinedClickable(
|
||||
onClick = { onClick(baseUser) },
|
||||
onLongClick = { onLongClick(baseUser) }
|
||||
)
|
||||
} else if (onClick != null) {
|
||||
Modifier.clickable(onClick = { onClick(baseUser) })
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
}
|
||||
|
||||
Row(modifier = myModifier) {
|
||||
UserPicture(
|
||||
userHex = user.pubkeyHex,
|
||||
userPicture = user.profilePicture(),
|
||||
userHex = userPubkey,
|
||||
userPicture = userProfile,
|
||||
showFollowingMark = showFollowingMark,
|
||||
size = size,
|
||||
modifier = modifier
|
||||
@ -1680,43 +1766,59 @@ fun UserPicture(
|
||||
size: Dp,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
val myBoxModifier = remember {
|
||||
Modifier
|
||||
.width(size)
|
||||
.height(size)
|
||||
) {
|
||||
}
|
||||
|
||||
val myImageModifier = remember {
|
||||
modifier
|
||||
.width(size)
|
||||
.height(size)
|
||||
.clip(shape = CircleShape)
|
||||
}
|
||||
|
||||
val myResizeImage = remember(userPicture) {
|
||||
ResizeImage(userPicture, size)
|
||||
}
|
||||
|
||||
Box(myBoxModifier) {
|
||||
RobohashAsyncImageProxy(
|
||||
robot = userHex,
|
||||
model = ResizeImage(userPicture, size),
|
||||
model = myResizeImage,
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
modifier = modifier
|
||||
.width(size)
|
||||
.height(size)
|
||||
.clip(shape = CircleShape)
|
||||
.background(MaterialTheme.colors.background)
|
||||
modifier = myImageModifier.background(MaterialTheme.colors.background)
|
||||
)
|
||||
|
||||
if (showFollowingMark) {
|
||||
Box(
|
||||
val myIconBoxModifier = remember {
|
||||
Modifier
|
||||
.width(size.div(3.5f))
|
||||
.height(size.div(3.5f))
|
||||
.align(Alignment.TopEnd),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// Background for the transparent checkmark
|
||||
.align(Alignment.TopEnd)
|
||||
}
|
||||
|
||||
val myIconBackgroundModifier = remember {
|
||||
Modifier
|
||||
.clip(CircleShape)
|
||||
.fillMaxSize(0.6f)
|
||||
.align(Alignment.Center)
|
||||
}
|
||||
|
||||
val myIconModifier = remember {
|
||||
Modifier.fillMaxSize()
|
||||
}
|
||||
|
||||
Box(myIconBoxModifier, contentAlignment = Alignment.Center) {
|
||||
Box(
|
||||
Modifier
|
||||
.clip(CircleShape)
|
||||
.fillMaxSize(0.6f)
|
||||
.align(Alignment.Center)
|
||||
.background(MaterialTheme.colors.background)
|
||||
myIconBackgroundModifier.background(MaterialTheme.colors.background)
|
||||
)
|
||||
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_verified),
|
||||
stringResource(id = R.string.following),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = myIconModifier,
|
||||
tint = Following
|
||||
)
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ import kotlin.math.roundToInt
|
||||
@Composable
|
||||
fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel, navController: NavController) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account ?: return
|
||||
val account = remember(accountState) { accountState?.account } ?: return
|
||||
|
||||
val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
|
||||
@ -123,13 +123,21 @@ fun ReplyReaction(
|
||||
onPress: () -> Unit
|
||||
) {
|
||||
val repliesState by baseNote.live().replies.observeAsState()
|
||||
val replies = repliesState?.note?.replies ?: emptySet()
|
||||
val replies = remember(repliesState) { repliesState?.note?.replies } ?: emptySet()
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val iconButtonModifier = remember {
|
||||
Modifier.size(iconSize)
|
||||
}
|
||||
|
||||
val iconModifier = remember {
|
||||
Modifier.size(iconSize)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier.size(iconSize),
|
||||
modifier = iconButtonModifier,
|
||||
onClick = {
|
||||
if (accountViewModel.isWriteable()) {
|
||||
onPress()
|
||||
@ -147,7 +155,7 @@ fun ReplyReaction(
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_comment),
|
||||
null,
|
||||
modifier = Modifier.size(iconSize),
|
||||
modifier = iconModifier,
|
||||
tint = grayTint
|
||||
)
|
||||
}
|
||||
@ -162,7 +170,7 @@ fun ReplyReaction(
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun BoostReaction(
|
||||
fun BoostReaction(
|
||||
baseNote: Note,
|
||||
grayTint: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
@ -170,17 +178,32 @@ public fun BoostReaction(
|
||||
onQuotePress: () -> Unit
|
||||
) {
|
||||
val boostsState by baseNote.live().boosts.observeAsState()
|
||||
val boostedNote = boostsState?.note
|
||||
val boostedNote = remember(boostsState) { boostsState?.note } ?: return
|
||||
|
||||
val hasBoosted = remember(boostsState) { accountViewModel.hasBoosted(baseNote) }
|
||||
val wasBoostedByLoggedIn = remember(boostsState) { boostedNote.isBoostedBy(accountViewModel.userProfile()) }
|
||||
val isWriteable = remember { accountViewModel.isWriteable() }
|
||||
|
||||
val boostCount = remember(boostsState) { showCount(boostedNote.boosts.size) }
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var wantsToBoost by remember { mutableStateOf(false) }
|
||||
|
||||
val iconButtonModifier = remember {
|
||||
Modifier.size(iconSize)
|
||||
}
|
||||
|
||||
val iconModifier = remember {
|
||||
Modifier.size(iconSize)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier.then(Modifier.size(iconSize)),
|
||||
modifier = iconButtonModifier,
|
||||
onClick = {
|
||||
if (accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.hasBoosted(baseNote)) {
|
||||
if (isWriteable) {
|
||||
if (hasBoosted) {
|
||||
accountViewModel.deleteBoostsTo(baseNote)
|
||||
} else {
|
||||
wantsToBoost = true
|
||||
@ -210,25 +233,16 @@ public fun BoostReaction(
|
||||
)
|
||||
}
|
||||
|
||||
if (boostedNote?.isBoostedBy(accountViewModel.userProfile()) == true) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_retweeted),
|
||||
null,
|
||||
modifier = Modifier.size(iconSize),
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_retweet),
|
||||
null,
|
||||
modifier = Modifier.size(iconSize),
|
||||
tint = grayTint
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_retweeted),
|
||||
null,
|
||||
modifier = iconModifier,
|
||||
tint = if (wasBoostedByLoggedIn) Color.Unspecified else grayTint
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
" ${showCount(boostedNote?.boosts?.size)}",
|
||||
" $boostCount",
|
||||
fontSize = 14.sp,
|
||||
color = grayTint
|
||||
)
|
||||
@ -243,16 +257,30 @@ fun LikeReaction(
|
||||
heartSize: Dp = 16.dp
|
||||
) {
|
||||
val reactionsState by baseNote.live().reactions.observeAsState()
|
||||
val reactedNote = reactionsState?.note ?: return
|
||||
val reactedNote = remember(reactionsState) { reactionsState?.note } ?: return
|
||||
|
||||
val hasReacted = remember(reactionsState) { accountViewModel.hasReactedTo(baseNote) }
|
||||
val wasReactedByLoggedIn = remember(reactionsState) { reactedNote.isReactedBy(accountViewModel.userProfile()) }
|
||||
val isWriteable = remember { accountViewModel.isWriteable() }
|
||||
|
||||
val reactionCount = remember(reactionsState) { showCount(reactedNote.reactions.size) }
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val iconButtonModifier = remember {
|
||||
Modifier.size(iconSize)
|
||||
}
|
||||
|
||||
val iconModifier = remember {
|
||||
Modifier.size(heartSize)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier.then(Modifier.size(iconSize)),
|
||||
modifier = iconButtonModifier,
|
||||
onClick = {
|
||||
if (accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.hasReactedTo(baseNote)) {
|
||||
if (isWriteable) {
|
||||
if (hasReacted) {
|
||||
accountViewModel.deleteReactionTo(baseNote)
|
||||
} else {
|
||||
accountViewModel.reactTo(baseNote)
|
||||
@ -268,25 +296,25 @@ fun LikeReaction(
|
||||
}
|
||||
}
|
||||
) {
|
||||
if (reactedNote.isReactedBy(accountViewModel.userProfile())) {
|
||||
if (wasReactedByLoggedIn) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_liked),
|
||||
null,
|
||||
modifier = Modifier.size(heartSize),
|
||||
modifier = iconModifier,
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_like),
|
||||
null,
|
||||
modifier = Modifier.size(heartSize),
|
||||
modifier = iconModifier,
|
||||
tint = grayTint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
" ${showCount(reactedNote.reactions.size)}",
|
||||
" $reactionCount",
|
||||
fontSize = 14.sp,
|
||||
color = grayTint
|
||||
)
|
||||
@ -472,6 +500,19 @@ public fun ViewCountReaction(
|
||||
numberSize: Dp = 24.dp
|
||||
) {
|
||||
val uri = LocalUriHandler.current
|
||||
val context = LocalContext.current
|
||||
|
||||
val iconButtonModifier = remember {
|
||||
Modifier.size(barChartSize)
|
||||
}
|
||||
|
||||
val iconModifier = remember {
|
||||
Modifier.height(numberSize)
|
||||
}
|
||||
|
||||
val colorFilter = remember {
|
||||
ColorFilter.tint(grayTint)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier.size(iconSize),
|
||||
@ -480,21 +521,25 @@ public fun ViewCountReaction(
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.BarChart,
|
||||
null,
|
||||
modifier = Modifier.size(barChartSize),
|
||||
modifier = iconButtonModifier,
|
||||
tint = grayTint
|
||||
)
|
||||
}
|
||||
|
||||
Row() {
|
||||
val request = remember {
|
||||
ImageRequest.Builder(context)
|
||||
.data("https://counter.amethyst.social/$idHex.svg?label=+&color=00000000")
|
||||
.diskCachePolicy(CachePolicy.DISABLED)
|
||||
.memoryCachePolicy(CachePolicy.ENABLED)
|
||||
.build()
|
||||
}
|
||||
|
||||
Row {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data("https://counter.amethyst.social/$idHex.svg?label=+&color=00000000")
|
||||
.diskCachePolicy(CachePolicy.DISABLED)
|
||||
.memoryCachePolicy(CachePolicy.ENABLED)
|
||||
.build(),
|
||||
model = request,
|
||||
contentDescription = stringResource(R.string.view_count),
|
||||
modifier = Modifier.height(numberSize),
|
||||
colorFilter = ColorFilter.tint(grayTint)
|
||||
modifier = iconModifier,
|
||||
colorFilter = colorFilter
|
||||
)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user