- Restructures Chat Message Rendering

- Adds Sensitive Content protections for chat renderings.
- Adjusts padding of the channel headers.
This commit is contained in:
Vitor Pamplona
2023-06-20 16:53:36 -04:00
parent 6addce3d20
commit 6a44d283f9
7 changed files with 413 additions and 231 deletions
@@ -1,7 +1,6 @@
package com.vitorpamplona.amethyst.ui.note package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -34,7 +33,6 @@ 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
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -69,19 +67,21 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ChatBubbleShapeMe import com.vitorpamplona.amethyst.ui.theme.ChatBubbleShapeMe
import com.vitorpamplona.amethyst.ui.theme.ChatBubbleShapeThem import com.vitorpamplona.amethyst.ui.theme.ChatBubbleShapeThem
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
import com.vitorpamplona.amethyst.ui.theme.Size13dp import com.vitorpamplona.amethyst.ui.theme.Size13dp
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
import com.vitorpamplona.amethyst.ui.theme.Size16dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.subtleBorder import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@@ -94,243 +94,407 @@ fun ChatroomMessageCompose(
nav: (String) -> Unit, nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit onWantsToReply: (Note) -> Unit
) { ) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
val loggedIn = remember(accountState) { accountState?.account?.userProfile() } ?: return
val noteState by baseNote.live().metadata.observeAsState() val noteState by baseNote.live().metadata.observeAsState()
val note = remember(noteState) { noteState?.note } ?: return val noteEvent = remember(noteState) { noteState?.note?.event }
val noteReportsState by baseNote.live().reports.observeAsState()
val noteForReports = remember(noteReportsState) { noteReportsState?.note } ?: return
val noteEvent = remember(noteState) { note.event }
var popupExpanded by remember { mutableStateOf(false) }
if (noteEvent == null) { if (noteEvent == null) {
BlankNote( LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
Modifier.combinedClickable( BlankNote(
onClick = { }, remember {
onLongClick = { popupExpanded = true } Modifier.combinedClickable(
onClick = { },
onLongClick = showPopup
)
}
)
}
} else {
CheckHiddenChatMessage(
baseNote,
routeForLastRead,
innerQuote,
parentBackgroundColor,
accountViewModel,
nav,
onWantsToReply
)
}
}
@Composable
fun CheckHiddenChatMessage(
baseNote: Note,
routeForLastRead: String?,
innerQuote: Boolean = false,
parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val isHidden by remember(accountState) {
derivedStateOf {
val isSensitive = baseNote.event?.isSensitive() ?: false
accountState?.account?.isHidden(baseNote.author!!) == true || (isSensitive && accountState?.account?.showSensitiveContent == false)
}
}
if (!isHidden) {
LoadedChatMessageCompose(
baseNote,
routeForLastRead,
innerQuote,
parentBackgroundColor,
accountViewModel,
nav,
onWantsToReply
)
}
}
@Composable
fun LoadedChatMessageCompose(
baseNote: Note,
routeForLastRead: String?,
innerQuote: Boolean = false,
parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit
) {
var state by remember {
mutableStateOf(
NoteComposeReportState(
isAcceptable = true,
canPreview = true,
relevantReports = persistentSetOf()
) )
) )
}
note.let { WatchForReports(baseNote, accountViewModel) { newIsAcceptable, newCanPreview, newRelevantReports ->
NoteQuickActionMenu(it, popupExpanded, { popupExpanded = false }, accountViewModel) if (newIsAcceptable != state.isAcceptable || newCanPreview != state.canPreview) {
state = NoteComposeReportState(newIsAcceptable, newCanPreview, newRelevantReports.toImmutableSet())
} }
} else if (account.isHidden(noteForReports.author!!)) { }
// Does nothing
var showReportedNote by remember { mutableStateOf(false) }
val showHiddenNote by remember(state, showReportedNote) {
derivedStateOf {
!state.isAcceptable && !showReportedNote
}
}
if (showHiddenNote) {
HiddenNote(
state.relevantReports,
accountViewModel,
Modifier,
innerQuote,
nav,
onClick = { showReportedNote = true }
)
} else { } else {
var showHiddenNote by remember { mutableStateOf(false) } val canPreview by remember(state, showReportedNote) {
var isAcceptableAndCanPreview by remember { mutableStateOf(Pair(true, true)) } derivedStateOf {
(!state.isAcceptable && showReportedNote) || state.canPreview
LaunchedEffect(key1 = noteReportsState, key2 = accountState) {
launch(Dispatchers.Default) {
account.userProfile().let { loggedIn ->
val newCanPreview = note.author?.pubkeyHex == loggedIn.pubkeyHex ||
(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)
}
}
} }
} }
if (!isAcceptableAndCanPreview.first && !showHiddenNote) { NormalChatNote(
val reports = remember { baseNote,
account.getRelevantReports(noteForReports).toImmutableSet() routeForLastRead,
} innerQuote,
HiddenNote( canPreview,
reports, parentBackgroundColor,
accountViewModel, accountViewModel,
Modifier, nav,
innerQuote, onWantsToReply
nav, )
onClick = { showHiddenNote = true } }
) }
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NormalChatNote(
note: Note,
routeForLastRead: String?,
innerQuote: Boolean = false,
canPreview: Boolean = true,
parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit
) {
val drawAuthorInfo by remember {
derivedStateOf {
(innerQuote || !accountViewModel.isLoggedUser(note.author)) && note.event !is PrivateDmEvent
}
}
val loggedInColors = MaterialTheme.colors.mediumImportanceLink
val otherColors = MaterialTheme.colors.subtleBorder
val defaultBackground = MaterialTheme.colors.background
val backgroundBubbleColor = remember {
if (accountViewModel.isLoggedUser(note.author)) {
mutableStateOf(loggedInColors.compositeOver(parentBackgroundColor?.value ?: defaultBackground))
} else { } else {
val loggedInColors = MaterialTheme.colors.mediumImportanceLink mutableStateOf(otherColors.compositeOver(parentBackgroundColor?.value ?: defaultBackground))
val otherColors = MaterialTheme.colors.subtleBorder }
val defaultBackground = MaterialTheme.colors.background }
val alignment: Arrangement.Horizontal = remember {
if (accountViewModel.isLoggedUser(note.author)) {
Arrangement.End
} else {
Arrangement.Start
}
}
val shape: Shape = remember {
if (accountViewModel.isLoggedUser(note.author)) {
ChatBubbleShapeMe
} else {
ChatBubbleShapeThem
}
}
val backgroundBubbleColor = remember { LaunchedEffect(key1 = routeForLastRead) {
if (note.author == loggedIn) { routeForLastRead?.let {
mutableStateOf(loggedInColors.compositeOver(parentBackgroundColor?.value ?: defaultBackground)) val createdAt = note.createdAt()
} else { if (createdAt != null) {
mutableStateOf(otherColors.compositeOver(parentBackgroundColor?.value ?: defaultBackground)) accountViewModel.account.markAsRead(it, createdAt)
}
} }
val alignment: Arrangement.Horizontal = remember { }
if (note.author == loggedIn) { }
Arrangement.End
} else { Column() {
Arrangement.Start 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
)
} }
val shape: Shape = remember { }
if (note.author == loggedIn) {
ChatBubbleShapeMe Row(
} else { modifier = modif,
ChatBubbleShapeThem horizontalArrangement = alignment
} ) {
val availableBubbleSize = remember { mutableStateOf(IntSize.Zero) }
var popupExpanded by remember { mutableStateOf(false) }
val modif2 = remember {
if (innerQuote) Modifier else Modifier.fillMaxWidth(0.85f)
} }
val scope = rememberCoroutineScope() val clickableModifier = remember {
Modifier
LaunchedEffect(key1 = routeForLastRead) { .combinedClickable(
routeForLastRead?.let { onClick = {
scope.launch(Dispatchers.IO) { if (note.event is ChannelCreateEvent) {
val lastTime = accountViewModel.account.loadLastRead(it) nav("Channel/${note.idHex}")
val createdAt = note.createdAt()
if (createdAt != null) {
accountViewModel.account.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(
modifier = modif,
horizontalArrangement = alignment
) {
var availableBubbleSize by remember { mutableStateOf(IntSize.Zero) }
val modif2 = if (innerQuote) Modifier else Modifier.fillMaxWidth(0.85f)
Row(
horizontalArrangement = alignment,
modifier = modif2.onSizeChanged {
availableBubbleSize = it
}
) {
Surface(
color = backgroundBubbleColor.value,
shape = shape,
modifier = Modifier
.combinedClickable(
onClick = {
if (noteEvent is ChannelCreateEvent) {
nav("Channel/${note.idHex}")
}
},
onLongClick = { popupExpanded = true }
)
) {
var bubbleSize by remember { mutableStateOf(IntSize.Zero) }
Column(
modifier = Modifier
.padding(start = 10.dp, end = 5.dp, bottom = 5.dp)
.onSizeChanged {
bubbleSize = it
}
) {
if ((innerQuote || note.author != loggedIn) && noteEvent !is PrivateDmEvent) {
DrawAuthorInfo(
baseNote,
alignment,
nav
)
} else {
Spacer(modifier = Modifier.height(5.dp))
}
val replyTo = note.replyTo
if (!innerQuote && !replyTo.isNullOrEmpty()) {
Row(verticalAlignment = Alignment.CenterVertically) {
replyTo.lastOrNull()?.let { note ->
ChatroomMessageCompose(
note,
null,
innerQuote = true,
parentBackgroundColor = backgroundBubbleColor,
accountViewModel = accountViewModel,
nav = nav,
onWantsToReply = onWantsToReply
)
}
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
when (noteEvent) {
is ChannelCreateEvent -> {
RenderCreateChannelNote(note)
}
is ChannelMetadataEvent -> {
RenderChangeChannelMetadataNote(note)
}
else -> {
RenderRegularTextNote(
note,
isAcceptableAndCanPreview.second,
backgroundBubbleColor,
accountViewModel,
nav
)
}
}
}
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
)
}
} }
} },
} onLongClick = { popupExpanded = true }
)
}
NoteQuickActionMenu( Row(
horizontalArrangement = alignment,
modifier = modif2.onSizeChanged {
availableBubbleSize.value = it
}
) {
Surface(
color = backgroundBubbleColor.value,
shape = shape,
modifier = clickableModifier
) {
RenderBubble(
note, note,
popupExpanded, drawAuthorInfo,
{ popupExpanded = false }, alignment,
accountViewModel innerQuote,
backgroundBubbleColor,
onWantsToReply,
canPreview,
availableBubbleSize,
accountViewModel,
nav
) )
} }
} }
NoteQuickActionMenu(
note = note,
popupExpanded = popupExpanded,
onDismiss = { popupExpanded = false },
accountViewModel = accountViewModel
)
} }
} }
} }
@Composable
private fun RenderBubble(
baseNote: Note,
drawAuthorInfo: Boolean,
alignment: Arrangement.Horizontal,
innerQuote: Boolean,
backgroundBubbleColor: MutableState<Color>,
onWantsToReply: (Note) -> Unit,
canPreview: Boolean,
availableBubbleSize: MutableState<IntSize>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val bubbleSize = remember { mutableStateOf(IntSize.Zero) }
val bubbleModifier = remember {
Modifier
.padding(start = 10.dp, end = 5.dp, bottom = 5.dp)
.onSizeChanged {
bubbleSize.value = it
}
}
Column(modifier = bubbleModifier) {
if (drawAuthorInfo) {
DrawAuthorInfo(
baseNote,
alignment,
nav
)
} else {
Spacer(modifier = StdVertSpacer)
}
RenderReplyRow(
note = baseNote,
innerQuote = innerQuote,
backgroundBubbleColor = backgroundBubbleColor,
accountViewModel = accountViewModel,
nav = nav,
onWantsToReply = onWantsToReply
)
NoteRow(
note = baseNote,
canPreview = canPreview,
backgroundBubbleColor = backgroundBubbleColor,
accountViewModel = accountViewModel,
nav = nav
)
ConstrainedStatusRow(
bubbleSize = bubbleSize,
availableBubbleSize = availableBubbleSize
) {
StatusRow(
baseNote = baseNote,
accountViewModel = accountViewModel,
onWantsToReply = onWantsToReply
)
}
}
}
@Composable
private fun RenderReplyRow(
note: Note,
innerQuote: Boolean,
backgroundBubbleColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit
) {
val replyTo by remember {
derivedStateOf {
note.replyTo?.lastOrNull()
}
}
if (!innerQuote && replyTo != null) {
Row(verticalAlignment = Alignment.CenterVertically) {
replyTo?.let { note ->
ChatroomMessageCompose(
note,
null,
innerQuote = true,
parentBackgroundColor = backgroundBubbleColor,
accountViewModel = accountViewModel,
nav = nav,
onWantsToReply = onWantsToReply
)
}
}
}
}
@Composable
private fun NoteRow(
note: Note,
canPreview: Boolean,
backgroundBubbleColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
Row(verticalAlignment = Alignment.CenterVertically) {
when (remember(note) { note.event }) {
is ChannelCreateEvent -> {
RenderCreateChannelNote(note)
}
is ChannelMetadataEvent -> {
RenderChangeChannelMetadataNote(note)
}
else -> {
RenderRegularTextNote(
note,
canPreview,
backgroundBubbleColor,
accountViewModel,
nav
)
}
}
}
}
@Composable
private fun ConstrainedStatusRow(
bubbleSize: MutableState<IntSize>,
availableBubbleSize: MutableState<IntSize>,
content: @Composable () -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.padding(top = 5.dp)
.then(
with(LocalDensity.current) {
Modifier.widthIn(
bubbleSize.value.width.toDp(),
availableBubbleSize.value.width.toDp()
)
}
)
) {
content()
}
}
@Composable @Composable
private fun StatusRow( private fun StatusRow(
baseNote: Note, baseNote: Note,
@@ -343,15 +507,15 @@ private fun StatusRow(
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
ChatTimeAgo(time) ChatTimeAgo(time)
RelayBadges(baseNote) RelayBadges(baseNote)
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = DoubleHorzSpacer)
} }
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
LikeReaction(baseNote, grayTint, accountViewModel) LikeReaction(baseNote, grayTint, accountViewModel)
Spacer(modifier = Modifier.width(5.dp)) Spacer(modifier = StdHorzSpacer)
ZapReaction(baseNote, grayTint, accountViewModel) ZapReaction(baseNote, grayTint, accountViewModel)
Spacer(modifier = Modifier.width(5.dp)) Spacer(modifier = StdHorzSpacer)
ReplyReaction(baseNote, grayTint, accountViewModel, showCounter = false, iconSize = 16.dp) { ReplyReaction(baseNote, grayTint, accountViewModel, showCounter = false, iconSize = Size16dp) {
onWantsToReply(baseNote) onWantsToReply(baseNote)
} }
} }
@@ -333,7 +333,7 @@ fun LoadedNoteCompose(
} }
@Composable @Composable
private fun WatchForReports( fun WatchForReports(
note: Note, note: Note,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
onChange: (Boolean, Boolean, Set<Note>) -> Unit onChange: (Boolean, Boolean, Set<Note>) -> Unit
@@ -380,7 +380,7 @@ fun NormalNote(
val channelHex = remember { baseNote.channelHex() } val channelHex = remember { baseNote.channelHex() }
if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && channelHex != null) { if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && channelHex != null) {
ChannelHeader(channelHex = channelHex, showVideo = !makeItShort, accountViewModel = accountViewModel, nav = nav) ChannelHeader(channelHex = channelHex, showVideo = !makeItShort, showBottomDiviser = true, accountViewModel = accountViewModel, nav = nav)
} else if (noteEvent is BadgeDefinitionEvent) { } else if (noteEvent is BadgeDefinitionEvent) {
BadgeDisplay(baseNote = baseNote) BadgeDisplay(baseNote = baseNote)
} else if (noteEvent is FileHeaderEvent) { } else if (noteEvent is FileHeaderEvent) {
@@ -1668,7 +1668,8 @@ private fun ReplyRow(
ChannelHeader( ChannelHeader(
channelHex = channelHex, channelHex = channelHex,
showVideo = false, showVideo = false,
modifier = remember { Modifier.padding(vertical = 10.dp) }, showBottomDiviser = false,
modifier = remember { Modifier.padding(vertical = 5.dp) },
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav nav = nav
) )
@@ -1677,7 +1678,6 @@ private fun ReplyRow(
val mentions = remember { (note.event as? BaseTextNoteEvent)?.mentions()?.toImmutableList() ?: persistentListOf() } val mentions = remember { (note.event as? BaseTextNoteEvent)?.mentions()?.toImmutableList() ?: persistentListOf() }
ReplyInformationChannel(replies, mentions, accountViewModel, nav) ReplyInformationChannel(replies, mentions, accountViewModel, nav)
Spacer(modifier = Modifier.height(5.dp))
} }
} }
} }
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.LocalTextStyle import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
@@ -21,6 +22,7 @@ import com.vitorpamplona.amethyst.model.*
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@@ -135,11 +137,16 @@ fun ReplyInformationChannel(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
sortedMentions = mentions val newSortedMentions = mentions
.mapNotNull { LocalCache.checkGetOrCreateUser(it) } .mapNotNull { LocalCache.checkGetOrCreateUser(it) }
.toSet() .toSet()
.sortedBy { accountViewModel.account.isFollowing(it) } .sortedBy { accountViewModel.account.isFollowing(it) }
.toImmutableList() .toImmutableList()
.ifEmpty { null }
if (newSortedMentions != sortedMentions) {
sortedMentions = newSortedMentions
}
} }
} }
@@ -151,6 +158,7 @@ fun ReplyInformationChannel(
nav("User/${it.pubkeyHex}") nav("User/${it.pubkeyHex}")
} }
) )
Spacer(modifier = StdVertSpacer)
} }
} }
@@ -378,7 +378,7 @@ fun NoteMaster(
) { ) {
Column() { Column() {
if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && note.channelHex() != null) { if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && note.channelHex() != null) {
ChannelHeader(channelHex = note.channelHex()!!, showVideo = true, accountViewModel = accountViewModel, nav = nav) ChannelHeader(channelHex = note.channelHex()!!, showVideo = true, showBottomDiviser = false, accountViewModel = accountViewModel, nav = nav)
} else if (noteEvent is FileHeaderEvent) { } else if (noteEvent is FileHeaderEvent) {
FileHeaderDisplay(baseNote) FileHeaderDisplay(baseNote)
} else if (noteEvent is FileStorageHeaderEvent) { } else if (noteEvent is FileStorageHeaderEvent) {
@@ -209,6 +209,7 @@ fun ChannelScreen(
ChannelHeader( ChannelHeader(
baseChannel = channel, baseChannel = channel,
showVideo = true, showVideo = true,
showBottomDiviser = true,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav nav = nav
) )
@@ -480,6 +481,7 @@ fun MyTextField(
fun ChannelHeader( fun ChannelHeader(
channelHex: String, channelHex: String,
showVideo: Boolean, showVideo: Boolean,
showBottomDiviser: Boolean,
modifier: Modifier = StdPadding, modifier: Modifier = StdPadding,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
@@ -497,6 +499,7 @@ fun ChannelHeader(
ChannelHeader( ChannelHeader(
it, it,
showVideo, showVideo,
showBottomDiviser,
modifier, modifier,
accountViewModel, accountViewModel,
nav nav
@@ -508,6 +511,7 @@ fun ChannelHeader(
fun ChannelHeader( fun ChannelHeader(
baseChannel: Channel, baseChannel: Channel,
showVideo: Boolean, showVideo: Boolean,
showBottomDiviser: Boolean,
modifier: Modifier = StdPadding, modifier: Modifier = StdPadding,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
@@ -559,7 +563,9 @@ fun ChannelHeader(
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
channel.toBestDisplayName(), channel.toBestDisplayName(),
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
) )
} }
@@ -589,10 +595,11 @@ fun ChannelHeader(
} }
} }
Divider( if (showBottomDiviser) {
modifier = Modifier.padding(start = 12.dp, end = 12.dp), Divider(
thickness = 0.25.dp thickness = 0.25.dp
) )
}
} }
} }
@@ -185,8 +185,7 @@ private fun FeedLoaded(
LazyColumn( LazyColumn(
contentPadding = PaddingValues( contentPadding = PaddingValues(
top = 10.dp, top = 10.dp
bottom = 10.dp
), ),
state = listState state = listState
) { ) {
@@ -194,6 +193,7 @@ private fun FeedLoaded(
ChannelHeader( ChannelHeader(
channelHex = item.idHex, channelHex = item.idHex,
showVideo = false, showVideo = false,
showBottomDiviser = true,
modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp), modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav nav = nav
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.theme package com.vitorpamplona.amethyst.ui.theme
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@@ -23,10 +24,12 @@ val ChatBubbleShapeThem = RoundedCornerShape(3.dp, 15.dp, 15.dp, 15.dp)
val StdButtonSizeModifier = Modifier.size(20.dp) val StdButtonSizeModifier = Modifier.size(20.dp)
val StdHorzSpacer = Modifier.width(5.dp) val StdHorzSpacer = Modifier.width(5.dp)
val StdVertSpacer = Modifier.height(5.dp)
val DoubleHorzSpacer = Modifier.width(10.dp) val DoubleHorzSpacer = Modifier.width(10.dp)
val Size35dp = 35.dp val Size35dp = 35.dp
val Size13dp = 13.dp val Size13dp = 13.dp
val Size16dp = 16.dp
val StdPadding = Modifier.padding(10.dp) val StdPadding = Modifier.padding(10.dp)