Improves rendering performance for chats.
This commit is contained in:
@@ -27,6 +27,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.ChevronRight
|
import androidx.compose.material.icons.filled.ChevronRight
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -45,6 +46,7 @@ import androidx.compose.ui.layout.onSizeChanged
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.platform.UriHandler
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
@@ -55,9 +57,12 @@ import com.google.accompanist.flowlayout.FlowRow
|
|||||||
import com.vitorpamplona.amethyst.NotificationCache
|
import com.vitorpamplona.amethyst.NotificationCache
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.EventInterface
|
||||||
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
||||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||||
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
||||||
@@ -67,6 +72,7 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
val ChatBubbleShapeMe = RoundedCornerShape(15.dp, 15.dp, 3.dp, 15.dp)
|
val ChatBubbleShapeMe = RoundedCornerShape(15.dp, 15.dp, 3.dp, 15.dp)
|
||||||
val ChatBubbleShapeThem = RoundedCornerShape(3.dp, 15.dp, 15.dp, 15.dp)
|
val ChatBubbleShapeThem = RoundedCornerShape(3.dp, 15.dp, 15.dp, 15.dp)
|
||||||
@@ -82,320 +88,416 @@ fun ChatroomMessageCompose(
|
|||||||
navController: NavController,
|
navController: NavController,
|
||||||
onWantsToReply: (Note) -> Unit
|
onWantsToReply: (Note) -> Unit
|
||||||
) {
|
) {
|
||||||
val noteState by baseNote.live().metadata.observeAsState()
|
|
||||||
val note = noteState?.note
|
|
||||||
|
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
val account = accountState?.account ?: return
|
val account = remember(accountState) { accountState?.account } ?: return
|
||||||
|
val loggedIn = remember(accountState) { accountState?.account?.userProfile() } ?: return
|
||||||
|
|
||||||
|
val noteState by baseNote.live().metadata.observeAsState()
|
||||||
|
val note = remember(noteState) { noteState?.note }
|
||||||
|
|
||||||
val noteReportsState by baseNote.live().reports.observeAsState()
|
val noteReportsState by baseNote.live().reports.observeAsState()
|
||||||
val noteForReports = noteReportsState?.note ?: return
|
val noteForReports = remember(noteReportsState) { noteReportsState?.note } ?: return
|
||||||
|
|
||||||
val accountUser = account.userProfile()
|
val noteEvent = note?.event
|
||||||
|
|
||||||
var popupExpanded by remember { mutableStateOf(false) }
|
var popupExpanded by remember { mutableStateOf(false) }
|
||||||
var showHiddenNote by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val context = LocalContext.current.applicationContext
|
if (noteEvent == null) {
|
||||||
val scope = rememberCoroutineScope()
|
BlankNote(Modifier.combinedClickable(
|
||||||
|
onClick = { },
|
||||||
|
onLongClick = { popupExpanded = true }
|
||||||
|
))
|
||||||
|
|
||||||
if (note?.event == null) {
|
note?.let {
|
||||||
BlankNote(Modifier)
|
NoteQuickActionMenu(it, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||||
} else if (!account.isAcceptable(noteForReports) && !showHiddenNote) {
|
|
||||||
if (!account.isHidden(noteForReports.author!!)) {
|
|
||||||
HiddenNote(
|
|
||||||
account.getRelevantReports(noteForReports),
|
|
||||||
account.userProfile(),
|
|
||||||
Modifier,
|
|
||||||
innerQuote,
|
|
||||||
navController,
|
|
||||||
onClick = { showHiddenNote = true }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var backgroundBubbleColor: Color
|
var showHiddenNote by remember { mutableStateOf(false) }
|
||||||
var alignment: Arrangement.Horizontal
|
var isAcceptableAndCanPreview by remember { mutableStateOf(Pair(true, true)) }
|
||||||
var shape: Shape
|
|
||||||
|
|
||||||
val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
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())
|
||||||
|
|
||||||
if (note.author == accountUser) {
|
val newIsAcceptable = account.isAcceptable(noteForReports)
|
||||||
backgroundBubbleColor = MaterialTheme.colors.primary.copy(alpha = 0.32f)
|
|
||||||
alignment = Arrangement.End
|
|
||||||
shape = ChatBubbleShapeMe
|
|
||||||
} else {
|
|
||||||
backgroundBubbleColor = MaterialTheme.colors.onSurface.copy(alpha = 0.12f)
|
|
||||||
alignment = Arrangement.Start
|
|
||||||
shape = ChatBubbleShapeThem
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentBackgroundColor != null) {
|
if (newIsAcceptable != isAcceptableAndCanPreview.first && newCanPreview != isAcceptableAndCanPreview.second) {
|
||||||
backgroundBubbleColor = backgroundBubbleColor.compositeOver(parentBackgroundColor)
|
isAcceptableAndCanPreview = Pair(newIsAcceptable, newCanPreview)
|
||||||
} else {
|
|
||||||
backgroundBubbleColor = backgroundBubbleColor.compositeOver(MaterialTheme.colors.background)
|
|
||||||
}
|
|
||||||
|
|
||||||
var isNew by remember { mutableStateOf<Boolean>(false) }
|
|
||||||
|
|
||||||
LaunchedEffect(key1 = routeForLastRead) {
|
|
||||||
routeForLastRead?.let {
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
val lastTime = NotificationCache.load(it)
|
|
||||||
|
|
||||||
val createdAt = note.createdAt()
|
|
||||||
if (createdAt != null) {
|
|
||||||
NotificationCache.markAsRead(it, createdAt)
|
|
||||||
isNew = createdAt > lastTime
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column() {
|
if (!isAcceptableAndCanPreview.first && !showHiddenNote) {
|
||||||
val modif = if (innerQuote) {
|
if (!account.isHidden(noteForReports.author!!)) {
|
||||||
Modifier.padding(top = 10.dp, end = 5.dp)
|
HiddenNote(
|
||||||
} else {
|
account.getRelevantReports(noteForReports),
|
||||||
Modifier
|
account.userProfile(),
|
||||||
.fillMaxWidth(1f)
|
Modifier,
|
||||||
.padding(
|
innerQuote,
|
||||||
start = 12.dp,
|
navController,
|
||||||
end = 12.dp,
|
onClick = { showHiddenNote = true }
|
||||||
top = 5.dp,
|
)
|
||||||
bottom = 5.dp
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
} else {
|
||||||
modifier = modif,
|
val backgroundBubbleColor: Color
|
||||||
horizontalArrangement = alignment
|
val alignment: Arrangement.Horizontal
|
||||||
) {
|
val shape: Shape
|
||||||
var availableBubbleSize by remember { mutableStateOf(IntSize.Zero) }
|
|
||||||
val modif2 = if (innerQuote) Modifier else Modifier.fillMaxWidth(0.85f)
|
if (note.author == loggedIn) {
|
||||||
|
backgroundBubbleColor = MaterialTheme.colors.primary.copy(alpha = 0.32f)
|
||||||
|
.compositeOver(parentBackgroundColor ?: MaterialTheme.colors.background)
|
||||||
|
|
||||||
|
alignment = Arrangement.End
|
||||||
|
shape = ChatBubbleShapeMe
|
||||||
|
} else {
|
||||||
|
backgroundBubbleColor = MaterialTheme.colors.onSurface.copy(alpha = 0.12f)
|
||||||
|
.compositeOver(parentBackgroundColor ?: MaterialTheme.colors.background)
|
||||||
|
|
||||||
|
alignment = Arrangement.Start
|
||||||
|
shape = ChatBubbleShapeThem
|
||||||
|
}
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = routeForLastRead) {
|
||||||
|
routeForLastRead?.let {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
val lastTime = NotificationCache.load(it)
|
||||||
|
|
||||||
|
val createdAt = note.createdAt()
|
||||||
|
if (createdAt != null) {
|
||||||
|
NotificationCache.markAsRead(it, createdAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column() {
|
||||||
|
val modif = remember {
|
||||||
|
if (innerQuote) {
|
||||||
|
Modifier.padding(top = 10.dp, end = 5.dp)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(1f)
|
||||||
|
.padding(
|
||||||
|
start = 12.dp,
|
||||||
|
end = 12.dp,
|
||||||
|
top = 5.dp,
|
||||||
|
bottom = 5.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = alignment,
|
modifier = modif,
|
||||||
modifier = modif2.onSizeChanged {
|
horizontalArrangement = alignment
|
||||||
availableBubbleSize = it
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
Surface(
|
var availableBubbleSize by remember { mutableStateOf(IntSize.Zero) }
|
||||||
color = backgroundBubbleColor,
|
val modif2 = if (innerQuote) Modifier else Modifier.fillMaxWidth(0.85f)
|
||||||
shape = shape,
|
|
||||||
modifier = Modifier
|
Row(
|
||||||
.combinedClickable(
|
horizontalArrangement = alignment,
|
||||||
onClick = { },
|
modifier = modif2.onSizeChanged {
|
||||||
onLongClick = { popupExpanded = true }
|
availableBubbleSize = it
|
||||||
)
|
}
|
||||||
) {
|
) {
|
||||||
var bubbleSize by remember { mutableStateOf(IntSize.Zero) }
|
Surface(
|
||||||
|
color = backgroundBubbleColor,
|
||||||
Column(
|
shape = shape,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 10.dp, end = 5.dp, bottom = 5.dp)
|
.combinedClickable(
|
||||||
.onSizeChanged {
|
onClick = { },
|
||||||
bubbleSize = it
|
onLongClick = { popupExpanded = true }
|
||||||
}
|
)
|
||||||
) {
|
) {
|
||||||
val authorState by note.author!!.live().metadata.observeAsState()
|
var bubbleSize by remember { mutableStateOf(IntSize.Zero) }
|
||||||
val author = authorState?.user!!
|
|
||||||
|
|
||||||
if (innerQuote || author != accountUser && note.event is ChannelMessageEvent) {
|
Column(
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = alignment,
|
|
||||||
modifier = Modifier.padding(top = 5.dp)
|
|
||||||
) {
|
|
||||||
RobohashAsyncImageProxy(
|
|
||||||
robot = author.pubkeyHex,
|
|
||||||
model = ResizeImage(author.profilePicture(), 25.dp),
|
|
||||||
contentDescription = stringResource(id = R.string.profile_image),
|
|
||||||
modifier = Modifier
|
|
||||||
.width(25.dp)
|
|
||||||
.height(25.dp)
|
|
||||||
.clip(shape = CircleShape)
|
|
||||||
.clickable(onClick = {
|
|
||||||
author.let {
|
|
||||||
navController.navigate("User/${it.pubkeyHex}")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
CreateClickableTextWithEmoji(
|
|
||||||
clickablePart = " ${author.toBestDisplayName()}",
|
|
||||||
suffix = "",
|
|
||||||
tags = author.info?.latestMetadata?.tags,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
overrideColor = MaterialTheme.colors.onBackground,
|
|
||||||
route = "User/${author.pubkeyHex}",
|
|
||||||
navController = navController
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val replyTo = note.replyTo
|
|
||||||
if (!innerQuote && !replyTo.isNullOrEmpty()) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
replyTo.toSet().mapIndexed { _, note ->
|
|
||||||
ChatroomMessageCompose(
|
|
||||||
note,
|
|
||||||
null,
|
|
||||||
innerQuote = true,
|
|
||||||
parentBackgroundColor = backgroundBubbleColor,
|
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
navController = navController,
|
|
||||||
onWantsToReply = onWantsToReply
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
val event = note.event
|
|
||||||
if (event is ChannelCreateEvent) {
|
|
||||||
val channelInfo = event.channelInfo()
|
|
||||||
val text = note.author?.toBestDisplayName()
|
|
||||||
.toString() + " ${stringResource(R.string.created)} " + (
|
|
||||||
channelInfo.name
|
|
||||||
?: ""
|
|
||||||
) + " ${stringResource(R.string.with_description_of)} '" + (
|
|
||||||
channelInfo.about
|
|
||||||
?: ""
|
|
||||||
) + "', ${stringResource(R.string.and_picture)} '" + (
|
|
||||||
channelInfo.picture
|
|
||||||
?: ""
|
|
||||||
) + "'"
|
|
||||||
|
|
||||||
CreateTextWithEmoji(
|
|
||||||
text = text,
|
|
||||||
tags = note.author?.info?.latestMetadata?.tags
|
|
||||||
)
|
|
||||||
} else if (event is ChannelMetadataEvent) {
|
|
||||||
val channelInfo = event.channelInfo()
|
|
||||||
val text = note.author?.toBestDisplayName()
|
|
||||||
.toString() + " ${stringResource(R.string.changed_chat_name_to)} '" + (
|
|
||||||
channelInfo.name
|
|
||||||
?: ""
|
|
||||||
) + "', ${stringResource(R.string.description_to)} '" + (
|
|
||||||
channelInfo.about
|
|
||||||
?: ""
|
|
||||||
) + "', ${stringResource(R.string.and_picture_to)} '" + (
|
|
||||||
channelInfo.picture
|
|
||||||
?: ""
|
|
||||||
) + "'"
|
|
||||||
|
|
||||||
CreateTextWithEmoji(
|
|
||||||
text = text,
|
|
||||||
tags = note.author?.info?.latestMetadata?.tags
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val eventContent = accountViewModel.decrypt(note)
|
|
||||||
|
|
||||||
val canPreview = note.author == accountUser ||
|
|
||||||
(note.author?.let { accountUser.isFollowingCached(it) } ?: true) ||
|
|
||||||
!noteForReports.hasAnyReports()
|
|
||||||
|
|
||||||
if (eventContent != null) {
|
|
||||||
TranslatableRichTextViewer(
|
|
||||||
eventContent,
|
|
||||||
canPreview,
|
|
||||||
Modifier.padding(top = 5.dp),
|
|
||||||
note.event?.tags(),
|
|
||||||
backgroundBubbleColor,
|
|
||||||
accountViewModel,
|
|
||||||
navController
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
TranslatableRichTextViewer(
|
|
||||||
stringResource(R.string.could_not_decrypt_the_message),
|
|
||||||
true,
|
|
||||||
Modifier.padding(top = 5.dp),
|
|
||||||
note.event?.tags(),
|
|
||||||
backgroundBubbleColor,
|
|
||||||
accountViewModel,
|
|
||||||
navController
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 5.dp)
|
.padding(start = 10.dp, end = 5.dp, bottom = 5.dp)
|
||||||
.then(
|
.onSizeChanged {
|
||||||
with(LocalDensity.current) {
|
bubbleSize = it
|
||||||
Modifier.widthIn(
|
}
|
||||||
bubbleSize.width.toDp(),
|
) {
|
||||||
availableBubbleSize.width.toDp()
|
if ((innerQuote || note.author != loggedIn) && noteEvent is ChannelMessageEvent) {
|
||||||
|
DrawAuthorInfo(
|
||||||
|
baseNote,
|
||||||
|
alignment,
|
||||||
|
navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val replyTo = note.replyTo
|
||||||
|
if (!innerQuote && !replyTo.isNullOrEmpty()) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
replyTo.toSet().mapIndexed { _, note ->
|
||||||
|
ChatroomMessageCompose(
|
||||||
|
note,
|
||||||
|
null,
|
||||||
|
innerQuote = true,
|
||||||
|
parentBackgroundColor = backgroundBubbleColor,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
navController = navController,
|
||||||
|
onWantsToReply = onWantsToReply
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
) {
|
|
||||||
Row() {
|
|
||||||
Text(
|
|
||||||
timeAgoShort(note.createdAt(), context),
|
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
|
||||||
fontSize = 12.sp
|
|
||||||
)
|
|
||||||
|
|
||||||
RelayBadges(note)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Row() {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
LikeReaction(baseNote, grayTint, accountViewModel)
|
when (noteEvent) {
|
||||||
Spacer(modifier = Modifier.width(5.dp))
|
is ChannelCreateEvent -> {
|
||||||
ZapReaction(baseNote, grayTint, accountViewModel)
|
RenderCreateChannelNote(note)
|
||||||
Spacer(modifier = Modifier.width(5.dp))
|
}
|
||||||
ReplyReaction(baseNote, grayTint, accountViewModel, showCounter = false) {
|
|
||||||
onWantsToReply(baseNote)
|
is ChannelMetadataEvent -> {
|
||||||
|
RenderChangeChannelMetadataNote(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
RenderRegularTextNote(
|
||||||
|
note,
|
||||||
|
loggedIn,
|
||||||
|
isAcceptableAndCanPreview.second,
|
||||||
|
backgroundBubbleColor,
|
||||||
|
accountViewModel,
|
||||||
|
navController
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 5.dp)
|
||||||
|
.then(
|
||||||
|
with(LocalDensity.current) {
|
||||||
|
Modifier.widthIn(
|
||||||
|
bubbleSize.width.toDp(),
|
||||||
|
availableBubbleSize.width.toDp()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
StatusRow(
|
||||||
|
baseNote,
|
||||||
|
accountViewModel,
|
||||||
|
onWantsToReply
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
|
NoteQuickActionMenu(
|
||||||
|
note,
|
||||||
|
popupExpanded,
|
||||||
|
{ popupExpanded = false },
|
||||||
|
accountViewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun StatusRow(
|
||||||
|
baseNote: Note,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
onWantsToReply: (Note) -> Unit
|
||||||
|
) {
|
||||||
|
val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
val time = remember { baseNote.createdAt() ?: 0 }
|
||||||
|
|
||||||
|
Row() {
|
||||||
|
ChatTimeAgo(time)
|
||||||
|
RelayBadges(baseNote)
|
||||||
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
Row() {
|
||||||
|
LikeReaction(baseNote, grayTint, accountViewModel)
|
||||||
|
Spacer(modifier = Modifier.width(5.dp))
|
||||||
|
ZapReaction(baseNote, grayTint, accountViewModel)
|
||||||
|
Spacer(modifier = Modifier.width(5.dp))
|
||||||
|
ReplyReaction(baseNote, grayTint, accountViewModel, showCounter = false) {
|
||||||
|
onWantsToReply(baseNote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatTimeAgo(time: Long) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
var timeStr by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = time) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
timeStr = timeAgoShort(time, context = context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
timeStr,
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
|
fontSize = 12.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderRegularTextNote(
|
||||||
|
note: Note,
|
||||||
|
loggedIn: User,
|
||||||
|
canPreview: Boolean,
|
||||||
|
backgroundBubbleColor: Color,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
|
val tags = remember { note.event?.tags() }
|
||||||
|
val eventContent = remember { accountViewModel.decrypt(note) }
|
||||||
|
val modifier = remember { Modifier.padding(top = 5.dp) }
|
||||||
|
|
||||||
|
if (eventContent != null) {
|
||||||
|
TranslatableRichTextViewer(
|
||||||
|
eventContent,
|
||||||
|
canPreview,
|
||||||
|
modifier,
|
||||||
|
tags,
|
||||||
|
backgroundBubbleColor,
|
||||||
|
accountViewModel,
|
||||||
|
navController
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
TranslatableRichTextViewer(
|
||||||
|
stringResource(R.string.could_not_decrypt_the_message),
|
||||||
|
true,
|
||||||
|
modifier,
|
||||||
|
tags,
|
||||||
|
backgroundBubbleColor,
|
||||||
|
accountViewModel,
|
||||||
|
navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderChangeChannelMetadataNote(
|
||||||
|
note: Note
|
||||||
|
) {
|
||||||
|
val noteEvent = note.event as? ChannelMetadataEvent ?: return
|
||||||
|
|
||||||
|
val channelInfo = noteEvent.channelInfo()
|
||||||
|
val text = note.author?.toBestDisplayName()
|
||||||
|
.toString() + " ${stringResource(R.string.changed_chat_name_to)} '" + (
|
||||||
|
channelInfo.name
|
||||||
|
?: ""
|
||||||
|
) + "', ${stringResource(R.string.description_to)} '" + (
|
||||||
|
channelInfo.about
|
||||||
|
?: ""
|
||||||
|
) + "', ${stringResource(R.string.and_picture_to)} '" + (
|
||||||
|
channelInfo.picture
|
||||||
|
?: ""
|
||||||
|
) + "'"
|
||||||
|
|
||||||
|
CreateTextWithEmoji(
|
||||||
|
text = text,
|
||||||
|
tags = note.author?.info?.latestMetadata?.tags
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderCreateChannelNote(note: Note) {
|
||||||
|
val noteEvent = note.event as? ChannelCreateEvent ?: return
|
||||||
|
val channelInfo = remember { noteEvent.channelInfo() }
|
||||||
|
|
||||||
|
val text = note.author?.toBestDisplayName()
|
||||||
|
.toString() + " ${stringResource(R.string.created)} " + (
|
||||||
|
channelInfo.name
|
||||||
|
?: ""
|
||||||
|
) + " ${stringResource(R.string.with_description_of)} '" + (
|
||||||
|
channelInfo.about
|
||||||
|
?: ""
|
||||||
|
) + "', ${stringResource(R.string.and_picture)} '" + (
|
||||||
|
channelInfo.picture
|
||||||
|
?: ""
|
||||||
|
) + "'"
|
||||||
|
|
||||||
|
CreateTextWithEmoji(
|
||||||
|
text = text,
|
||||||
|
tags = note.author?.info?.latestMetadata?.tags
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DrawAuthorInfo(
|
||||||
|
baseNote: Note,
|
||||||
|
alignment: Arrangement.Horizontal,
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
|
val userState by baseNote.author!!.live().metadata.observeAsState()
|
||||||
|
|
||||||
|
val pubkeyHex = remember { baseNote.author?.pubkeyHex } ?: return
|
||||||
|
val route = remember { "User/$pubkeyHex" }
|
||||||
|
val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() }
|
||||||
|
val userProfilePicture = remember(userState) { ResizeImage(userState?.user?.profilePicture(), 25.dp) }
|
||||||
|
val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags }
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = alignment,
|
||||||
|
modifier = Modifier.padding(top = 5.dp)
|
||||||
|
) {
|
||||||
|
RobohashAsyncImageProxy(
|
||||||
|
robot = pubkeyHex,
|
||||||
|
model = userProfilePicture,
|
||||||
|
contentDescription = stringResource(id = R.string.profile_image),
|
||||||
|
modifier = Modifier
|
||||||
|
.width(25.dp)
|
||||||
|
.height(25.dp)
|
||||||
|
.clip(shape = CircleShape)
|
||||||
|
.clickable(onClick = {
|
||||||
|
navController.navigate(route)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
CreateClickableTextWithEmoji(
|
||||||
|
clickablePart = " $userDisplayName",
|
||||||
|
suffix = "",
|
||||||
|
tags = userTags,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
overrideColor = MaterialTheme.colors.onBackground,
|
||||||
|
route = route,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RelayBadges(baseNote: Note) {
|
private fun RelayBadges(baseNote: Note) {
|
||||||
val noteRelaysState by baseNote.live().relays.observeAsState()
|
val noteRelaysState by baseNote.live().relays.observeAsState()
|
||||||
val noteRelays = noteRelaysState?.note?.relays ?: emptySet()
|
val noteRelays = remember(noteRelaysState) { noteRelaysState?.note?.relays ?: emptySet() }
|
||||||
|
val noteRelaysSimple = remember(noteRelaysState) { noteRelaysState?.note?.relays?.take(3) ?: emptySet() }
|
||||||
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val relaysToDisplay = if (expanded) noteRelays else noteRelays.take(3)
|
val relaysToDisplay by remember {
|
||||||
|
derivedStateOf {
|
||||||
val uri = LocalUriHandler.current
|
if (expanded) noteRelays else noteRelaysSimple
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
FlowRow(Modifier.padding(start = 10.dp)) {
|
FlowRow(Modifier.padding(start = 10.dp)) {
|
||||||
relaysToDisplay.forEach {
|
relaysToDisplay.forEach {
|
||||||
val url = it.removePrefix("wss://").removePrefix("ws://")
|
RenderRelay(it)
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.size(15.dp)
|
|
||||||
.padding(1.dp)
|
|
||||||
) {
|
|
||||||
RobohashFallbackAsyncImage(
|
|
||||||
robot = "https://$url/favicon.ico",
|
|
||||||
robotSize = 15.dp,
|
|
||||||
model = "https://$url/favicon.ico",
|
|
||||||
contentDescription = stringResource(id = R.string.relay_icon),
|
|
||||||
colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize(1f)
|
|
||||||
.clip(shape = CircleShape)
|
|
||||||
.background(MaterialTheme.colors.background)
|
|
||||||
.clickable(onClick = { uri.openUri("https://$url") })
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (noteRelays.size > 3 && !expanded) {
|
if (noteRelays.size > 3 && !expanded) {
|
||||||
@@ -413,3 +515,37 @@ private fun RelayBadges(baseNote: Note) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderRelay(dirtyUrl: String) {
|
||||||
|
val uri = LocalUriHandler.current
|
||||||
|
val website = remember {
|
||||||
|
val cleanUrl = dirtyUrl.removePrefix("wss://").removePrefix("ws://")
|
||||||
|
"https://$cleanUrl"
|
||||||
|
}
|
||||||
|
val iconUrl = remember {
|
||||||
|
val cleanUrl = dirtyUrl.removePrefix("wss://").removePrefix("ws://")
|
||||||
|
"https://$cleanUrl/favicon.ico"
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
remember {
|
||||||
|
Modifier
|
||||||
|
.size(15.dp)
|
||||||
|
.padding(1.dp)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
RobohashFallbackAsyncImage(
|
||||||
|
robot = iconUrl,
|
||||||
|
robotSize = 15.dp,
|
||||||
|
model = iconUrl,
|
||||||
|
contentDescription = stringResource(id = R.string.relay_icon),
|
||||||
|
colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(1f)
|
||||||
|
.clip(shape = CircleShape)
|
||||||
|
.background(MaterialTheme.colors.background)
|
||||||
|
.clickable(onClick = { uri.openUri(website) })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user