- Restructures Chat Message Rendering
- Adds Sensitive Content protections for chat renderings. - Adjusts padding of the channel headers.
This commit is contained in:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user