kopia lustrzana https://github.com/vitorpamplona/amethyst
3680 wiersze
117 KiB
Kotlin
3680 wiersze
117 KiB
Kotlin
package com.vitorpamplona.amethyst.ui.note
|
|
|
|
import android.graphics.Bitmap
|
|
import android.util.Log
|
|
import androidx.compose.animation.Crossfade
|
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
import androidx.compose.foundation.Image
|
|
import androidx.compose.foundation.background
|
|
import androidx.compose.foundation.border
|
|
import androidx.compose.foundation.clickable
|
|
import androidx.compose.foundation.combinedClickable
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
import androidx.compose.foundation.layout.Box
|
|
import androidx.compose.foundation.layout.Column
|
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
import androidx.compose.foundation.layout.FlowRow
|
|
import androidx.compose.foundation.layout.PaddingValues
|
|
import androidx.compose.foundation.layout.Row
|
|
import androidx.compose.foundation.layout.Spacer
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
import androidx.compose.foundation.layout.height
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.foundation.layout.size
|
|
import androidx.compose.foundation.layout.width
|
|
import androidx.compose.foundation.shape.CircleShape
|
|
import androidx.compose.foundation.shape.CutCornerShape
|
|
import androidx.compose.foundation.text.ClickableText
|
|
import androidx.compose.material.Button
|
|
import androidx.compose.material.ButtonDefaults
|
|
import androidx.compose.material.Divider
|
|
import androidx.compose.material.IconButton
|
|
import androidx.compose.material.LocalTextStyle
|
|
import androidx.compose.material.MaterialTheme
|
|
import androidx.compose.material.Text
|
|
import androidx.compose.material.darkColors
|
|
import androidx.compose.material.lightColors
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.Immutable
|
|
import androidx.compose.runtime.LaunchedEffect
|
|
import androidx.compose.runtime.MutableState
|
|
import androidx.compose.runtime.Stable
|
|
import androidx.compose.runtime.derivedStateOf
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.livedata.observeAsState
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.remember
|
|
import androidx.compose.runtime.rememberCoroutineScope
|
|
import androidx.compose.runtime.setValue
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.draw.clip
|
|
import androidx.compose.ui.graphics.Brush
|
|
import androidx.compose.ui.graphics.Color
|
|
import androidx.compose.ui.graphics.compositeOver
|
|
import androidx.compose.ui.graphics.luminance
|
|
import androidx.compose.ui.layout.ContentScale
|
|
import androidx.compose.ui.platform.LocalClipboardManager
|
|
import androidx.compose.ui.platform.LocalContext
|
|
import androidx.compose.ui.platform.LocalUriHandler
|
|
import androidx.compose.ui.res.painterResource
|
|
import androidx.compose.ui.res.stringResource
|
|
import androidx.compose.ui.text.AnnotatedString
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
import androidx.compose.ui.text.style.TextAlign
|
|
import androidx.compose.ui.text.style.TextOverflow
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.compose.ui.unit.sp
|
|
import androidx.core.graphics.drawable.toBitmap
|
|
import androidx.core.graphics.get
|
|
import androidx.lifecycle.distinctUntilChanged
|
|
import androidx.lifecycle.map
|
|
import coil.compose.AsyncImage
|
|
import coil.compose.AsyncImagePainter
|
|
import coil.request.SuccessResult
|
|
import com.vitorpamplona.amethyst.R
|
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
|
import com.vitorpamplona.amethyst.model.Channel
|
|
import com.vitorpamplona.amethyst.model.LocalCache
|
|
import com.vitorpamplona.amethyst.model.Note
|
|
import com.vitorpamplona.amethyst.model.User
|
|
import com.vitorpamplona.amethyst.model.UserMetadata
|
|
import com.vitorpamplona.amethyst.service.OnlineChecker
|
|
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
|
import com.vitorpamplona.amethyst.service.model.ATag
|
|
import com.vitorpamplona.amethyst.service.model.AppDefinitionEvent
|
|
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
|
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
|
|
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
|
import com.vitorpamplona.amethyst.service.model.BaseTextNoteEvent
|
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
|
import com.vitorpamplona.amethyst.service.model.ClassifiedsEvent
|
|
import com.vitorpamplona.amethyst.service.model.CommunityDefinitionEvent
|
|
import com.vitorpamplona.amethyst.service.model.CommunityPostApprovalEvent
|
|
import com.vitorpamplona.amethyst.service.model.EmojiPackEvent
|
|
import com.vitorpamplona.amethyst.service.model.EmojiPackSelectionEvent
|
|
import com.vitorpamplona.amethyst.service.model.EmojiUrl
|
|
import com.vitorpamplona.amethyst.service.model.FileHeaderEvent
|
|
import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent
|
|
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
|
import com.vitorpamplona.amethyst.service.model.HighlightEvent
|
|
import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent
|
|
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent
|
|
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_LIVE
|
|
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_PLANNED
|
|
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
|
import com.vitorpamplona.amethyst.service.model.Participant
|
|
import com.vitorpamplona.amethyst.service.model.PeopleListEvent
|
|
import com.vitorpamplona.amethyst.service.model.PinListEvent
|
|
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.RelaySetEvent
|
|
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
|
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
|
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
|
|
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
|
|
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
|
|
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
|
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
|
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
|
import com.vitorpamplona.amethyst.ui.components.LoadThumbAndThenVideoView
|
|
import com.vitorpamplona.amethyst.ui.components.MeasureSpaceWidth
|
|
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
|
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
|
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
|
import com.vitorpamplona.amethyst.ui.components.ShowMoreButton
|
|
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
|
import com.vitorpamplona.amethyst.ui.components.VideoView
|
|
import com.vitorpamplona.amethyst.ui.components.ZoomableContent
|
|
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
|
|
import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog
|
|
import com.vitorpamplona.amethyst.ui.components.ZoomableLocalImage
|
|
import com.vitorpamplona.amethyst.ui.components.ZoomableLocalVideo
|
|
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlImage
|
|
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlVideo
|
|
import com.vitorpamplona.amethyst.ui.components.figureOutMimeType
|
|
import com.vitorpamplona.amethyst.ui.components.imageExtensions
|
|
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
|
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.JoinCommunityButton
|
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LeaveCommunityButton
|
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
|
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
|
|
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
|
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
|
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
|
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
|
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
|
|
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
|
|
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
|
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
|
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
|
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
|
|
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
|
import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
|
|
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
|
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
|
|
import com.vitorpamplona.amethyst.ui.theme.Size30dp
|
|
import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
|
|
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
|
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
|
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
|
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
|
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
|
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
|
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
|
import com.vitorpamplona.amethyst.ui.theme.UserNameMaxRowHeight
|
|
import com.vitorpamplona.amethyst.ui.theme.UserNameRowHeight
|
|
import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifier
|
|
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
|
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
|
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
|
import com.vitorpamplona.amethyst.ui.theme.replyBackground
|
|
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
|
import com.vitorpamplona.amethyst.ui.theme.repostProfileBorder
|
|
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
|
import kotlinx.collections.immutable.ImmutableList
|
|
import kotlinx.collections.immutable.ImmutableSet
|
|
import kotlinx.collections.immutable.persistentListOf
|
|
import kotlinx.collections.immutable.persistentSetOf
|
|
import kotlinx.collections.immutable.toImmutableList
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.launch
|
|
import nostr.postr.toNpub
|
|
import java.io.File
|
|
import java.math.BigDecimal
|
|
import java.net.URL
|
|
import java.util.Locale
|
|
import kotlin.time.ExperimentalTime
|
|
import kotlin.time.measureTimedValue
|
|
|
|
@OptIn(ExperimentalFoundationApi::class)
|
|
@Composable
|
|
fun NoteCompose(
|
|
baseNote: Note,
|
|
routeForLastRead: String? = null,
|
|
modifier: Modifier = Modifier,
|
|
isBoostedNote: Boolean = false,
|
|
isQuotedNote: Boolean = false,
|
|
unPackReply: Boolean = true,
|
|
makeItShort: Boolean = false,
|
|
addMarginTop: Boolean = true,
|
|
showHidden: Boolean = false,
|
|
parentBackgroundColor: MutableState<Color>? = null,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val isBlank by baseNote.live().metadata.map {
|
|
it.note.event == null
|
|
}.distinctUntilChanged().observeAsState(baseNote.event == null)
|
|
|
|
Crossfade(targetState = isBlank) {
|
|
if (it) {
|
|
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
|
BlankNote(
|
|
remember {
|
|
modifier.combinedClickable(
|
|
onClick = { },
|
|
onLongClick = showPopup
|
|
)
|
|
},
|
|
isBoostedNote || isQuotedNote
|
|
)
|
|
}
|
|
} else {
|
|
CheckHiddenNoteCompose(
|
|
note = baseNote,
|
|
routeForLastRead = routeForLastRead,
|
|
modifier = modifier,
|
|
isBoostedNote = isBoostedNote,
|
|
isQuotedNote = isQuotedNote,
|
|
unPackReply = unPackReply,
|
|
makeItShort = makeItShort,
|
|
addMarginTop = addMarginTop,
|
|
showHidden = showHidden,
|
|
parentBackgroundColor = parentBackgroundColor,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun CheckHiddenNoteCompose(
|
|
note: Note,
|
|
routeForLastRead: String? = null,
|
|
modifier: Modifier = Modifier,
|
|
isBoostedNote: Boolean = false,
|
|
isQuotedNote: Boolean = false,
|
|
unPackReply: Boolean = true,
|
|
makeItShort: Boolean = false,
|
|
addMarginTop: Boolean = true,
|
|
showHidden: Boolean = false,
|
|
parentBackgroundColor: MutableState<Color>? = null,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
if (showHidden) {
|
|
// Ignores reports as well
|
|
val state by remember {
|
|
mutableStateOf(
|
|
NoteComposeReportState(
|
|
isAcceptable = true,
|
|
canPreview = true,
|
|
relevantReports = persistentSetOf()
|
|
)
|
|
)
|
|
}
|
|
|
|
RenderReportState(
|
|
state = state,
|
|
note = note,
|
|
routeForLastRead = routeForLastRead,
|
|
modifier = modifier,
|
|
isBoostedNote = isBoostedNote,
|
|
isQuotedNote = isQuotedNote,
|
|
unPackReply = unPackReply,
|
|
makeItShort = makeItShort,
|
|
addMarginTop = addMarginTop,
|
|
parentBackgroundColor = parentBackgroundColor,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
} else {
|
|
val isHidden by accountViewModel.account.liveHiddenUsers.map {
|
|
note.isHiddenFor(it)
|
|
}.observeAsState(accountViewModel.isNoteHidden(note))
|
|
|
|
Crossfade(targetState = isHidden) {
|
|
if (!it) {
|
|
LoadedNoteCompose(
|
|
note = note,
|
|
routeForLastRead = routeForLastRead,
|
|
modifier = modifier,
|
|
isBoostedNote = isBoostedNote,
|
|
isQuotedNote = isQuotedNote,
|
|
unPackReply = unPackReply,
|
|
makeItShort = makeItShort,
|
|
addMarginTop = addMarginTop,
|
|
parentBackgroundColor = parentBackgroundColor,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Immutable
|
|
data class NoteComposeReportState(
|
|
val isAcceptable: Boolean,
|
|
val canPreview: Boolean,
|
|
val relevantReports: ImmutableSet<Note>
|
|
)
|
|
|
|
@Composable
|
|
fun LoadedNoteCompose(
|
|
note: Note,
|
|
routeForLastRead: String? = null,
|
|
modifier: Modifier = Modifier,
|
|
isBoostedNote: Boolean = false,
|
|
isQuotedNote: Boolean = false,
|
|
unPackReply: Boolean = true,
|
|
makeItShort: Boolean = false,
|
|
addMarginTop: Boolean = true,
|
|
parentBackgroundColor: MutableState<Color>? = null,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
var state by remember {
|
|
mutableStateOf(
|
|
NoteComposeReportState(
|
|
isAcceptable = true,
|
|
canPreview = true,
|
|
relevantReports = persistentSetOf()
|
|
)
|
|
)
|
|
}
|
|
|
|
val scope = rememberCoroutineScope()
|
|
|
|
WatchForReports(note, accountViewModel) { newIsAcceptable, newCanPreview, newRelevantReports ->
|
|
if (newIsAcceptable != state.isAcceptable || newCanPreview != state.canPreview) {
|
|
val newState = NoteComposeReportState(newIsAcceptable, newCanPreview, newRelevantReports)
|
|
scope.launch(Dispatchers.Main) {
|
|
state = newState
|
|
}
|
|
}
|
|
}
|
|
|
|
Crossfade(targetState = state) {
|
|
RenderReportState(
|
|
it,
|
|
note,
|
|
routeForLastRead,
|
|
modifier,
|
|
isBoostedNote,
|
|
isQuotedNote,
|
|
unPackReply,
|
|
makeItShort,
|
|
addMarginTop,
|
|
parentBackgroundColor,
|
|
accountViewModel,
|
|
nav
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun RenderReportState(
|
|
state: NoteComposeReportState,
|
|
note: Note,
|
|
routeForLastRead: String? = null,
|
|
modifier: Modifier = Modifier,
|
|
isBoostedNote: Boolean = false,
|
|
isQuotedNote: Boolean = false,
|
|
unPackReply: Boolean = true,
|
|
makeItShort: Boolean = false,
|
|
addMarginTop: Boolean = true,
|
|
parentBackgroundColor: MutableState<Color>? = null,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
var showReportedNote by remember { mutableStateOf(false) }
|
|
|
|
Crossfade(targetState = !state.isAcceptable && !showReportedNote) { showHiddenNote ->
|
|
if (showHiddenNote) {
|
|
HiddenNote(
|
|
state.relevantReports,
|
|
accountViewModel,
|
|
modifier,
|
|
isBoostedNote,
|
|
nav,
|
|
onClick = { showReportedNote = true }
|
|
)
|
|
} else {
|
|
val canPreview = (!state.isAcceptable && showReportedNote) || state.canPreview
|
|
|
|
NormalNote(
|
|
note,
|
|
routeForLastRead,
|
|
modifier,
|
|
isBoostedNote,
|
|
isQuotedNote,
|
|
unPackReply,
|
|
makeItShort,
|
|
addMarginTop,
|
|
canPreview,
|
|
parentBackgroundColor,
|
|
accountViewModel,
|
|
nav
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun WatchForReports(
|
|
note: Note,
|
|
accountViewModel: AccountViewModel,
|
|
onChange: (Boolean, Boolean, ImmutableSet<Note>) -> Unit
|
|
) {
|
|
val userFollowsState by accountViewModel.userFollows.observeAsState()
|
|
val noteReportsState by note.live().reports.observeAsState()
|
|
|
|
LaunchedEffect(key1 = noteReportsState, key2 = userFollowsState) {
|
|
launch(Dispatchers.Default) {
|
|
accountViewModel.isNoteAcceptable(note, onChange)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun NormalNote(
|
|
baseNote: Note,
|
|
routeForLastRead: String? = null,
|
|
modifier: Modifier = Modifier,
|
|
isBoostedNote: Boolean = false,
|
|
isQuotedNote: Boolean = false,
|
|
unPackReply: Boolean = true,
|
|
makeItShort: Boolean = false,
|
|
addMarginTop: Boolean = true,
|
|
canPreview: Boolean = true,
|
|
parentBackgroundColor: MutableState<Color>? = null,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
when (baseNote.event) {
|
|
is ChannelCreateEvent, is ChannelMetadataEvent -> ChannelHeader(
|
|
channelNote = baseNote,
|
|
showVideo = !makeItShort,
|
|
showBottomDiviser = true,
|
|
sendToChannel = true,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
is CommunityDefinitionEvent -> (baseNote as? AddressableNote)?.let {
|
|
CommunityHeader(
|
|
baseNote = it,
|
|
showBottomDiviser = true,
|
|
sendToCommunity = true,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
}
|
|
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
|
|
is FileHeaderEvent -> FileHeaderDisplay(baseNote, accountViewModel)
|
|
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, accountViewModel)
|
|
else ->
|
|
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
|
CheckNewAndRenderNote(
|
|
baseNote,
|
|
routeForLastRead,
|
|
modifier,
|
|
isBoostedNote,
|
|
isQuotedNote,
|
|
unPackReply,
|
|
makeItShort,
|
|
addMarginTop,
|
|
canPreview,
|
|
parentBackgroundColor,
|
|
accountViewModel,
|
|
showPopup,
|
|
nav
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun CommunityHeader(
|
|
baseNote: AddressableNote,
|
|
showBottomDiviser: Boolean,
|
|
sendToCommunity: Boolean,
|
|
modifier: Modifier = StdPadding,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val expanded = remember { mutableStateOf(false) }
|
|
|
|
Column(
|
|
modifier = modifier.clickable {
|
|
if (sendToCommunity) {
|
|
routeFor(baseNote, accountViewModel.userProfile())?.let {
|
|
nav(it)
|
|
}
|
|
} else {
|
|
expanded.value = !expanded.value
|
|
}
|
|
}
|
|
) {
|
|
ShortCommunityHeader(baseNote, expanded, accountViewModel, nav)
|
|
|
|
if (expanded.value) {
|
|
LongCommunityHeader(baseNote, accountViewModel, nav)
|
|
}
|
|
}
|
|
|
|
if (showBottomDiviser) {
|
|
Divider(
|
|
thickness = 0.25.dp
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun LongCommunityHeader(baseNote: AddressableNote, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
|
val noteState by baseNote.live().metadata.observeAsState()
|
|
val noteEvent = remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return
|
|
|
|
Row(
|
|
Modifier
|
|
.fillMaxWidth()
|
|
.padding(top = 10.dp)
|
|
) {
|
|
val summary = remember(noteState) {
|
|
noteEvent.description()?.ifBlank { null }
|
|
}
|
|
|
|
Column(
|
|
Modifier
|
|
.weight(1f)
|
|
.padding(start = 10.dp)
|
|
) {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
val defaultBackground = MaterialTheme.colors.background
|
|
val background = remember {
|
|
mutableStateOf(defaultBackground)
|
|
}
|
|
|
|
TranslatableRichTextViewer(
|
|
content = summary ?: stringResource(id = R.string.community_no_descriptor),
|
|
canPreview = false,
|
|
tags = remember { ImmutableListOfLists(emptyList()) },
|
|
backgroundColor = background,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
}
|
|
|
|
val hashtags = remember(noteEvent) { noteEvent.hashtags().toImmutableList() }
|
|
DisplayUncitedHashtags(hashtags, summary ?: "", nav)
|
|
}
|
|
|
|
Column() {
|
|
Row() {
|
|
Spacer(DoubleHorzSpacer)
|
|
LongCommunityActionOptions(baseNote, accountViewModel, nav)
|
|
}
|
|
}
|
|
}
|
|
|
|
val rules = remember(noteState) {
|
|
noteEvent.rules()?.ifBlank { null }
|
|
}
|
|
|
|
rules?.let {
|
|
Spacer(DoubleVertSpacer)
|
|
Row(
|
|
Modifier
|
|
.fillMaxWidth()
|
|
.padding(start = 10.dp)
|
|
) {
|
|
val defaultBackground = MaterialTheme.colors.background
|
|
val background = remember {
|
|
mutableStateOf(defaultBackground)
|
|
}
|
|
val tags = remember(noteEvent) { noteEvent?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
|
|
|
|
TranslatableRichTextViewer(
|
|
content = it,
|
|
canPreview = false,
|
|
tags = tags,
|
|
backgroundColor = background,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
}
|
|
}
|
|
|
|
Spacer(DoubleVertSpacer)
|
|
|
|
Row(
|
|
Modifier
|
|
.fillMaxWidth()
|
|
.padding(start = 10.dp),
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Text(
|
|
text = stringResource(id = R.string.owner),
|
|
maxLines = 1,
|
|
overflow = TextOverflow.Ellipsis,
|
|
modifier = Modifier.width(75.dp)
|
|
)
|
|
Spacer(DoubleHorzSpacer)
|
|
NoteAuthorPicture(baseNote, nav, accountViewModel, Size25dp)
|
|
Spacer(DoubleHorzSpacer)
|
|
NoteUsernameDisplay(baseNote, remember { Modifier.weight(1f) })
|
|
TimeAgo(baseNote)
|
|
MoreOptionsButton(baseNote, accountViewModel)
|
|
}
|
|
|
|
var participantUsers by remember(baseNote) {
|
|
mutableStateOf<ImmutableList<Pair<Participant, User>>>(
|
|
persistentListOf()
|
|
)
|
|
}
|
|
|
|
LaunchedEffect(key1 = noteState) {
|
|
launch(Dispatchers.IO) {
|
|
val noteEvent = (noteState?.note?.event as? CommunityDefinitionEvent)
|
|
val newParticipantUsers = noteEvent?.moderators()?.mapNotNull { part ->
|
|
LocalCache.checkGetOrCreateUser(part.key)?.let { Pair(part, it) }
|
|
}?.toImmutableList()
|
|
|
|
if (newParticipantUsers != null && !equalImmutableLists(newParticipantUsers, participantUsers)) {
|
|
participantUsers = newParticipantUsers
|
|
}
|
|
}
|
|
}
|
|
|
|
participantUsers.forEach {
|
|
Row(
|
|
Modifier
|
|
.fillMaxWidth()
|
|
.padding(start = 10.dp, top = 10.dp)
|
|
.clickable {
|
|
nav("User/${it.second.pubkeyHex}")
|
|
},
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
it.first.role?.let { it1 ->
|
|
Text(
|
|
text = it1.capitalize(Locale.ROOT),
|
|
maxLines = 1,
|
|
overflow = TextOverflow.Ellipsis,
|
|
modifier = Modifier.width(75.dp)
|
|
)
|
|
}
|
|
Spacer(DoubleHorzSpacer)
|
|
ClickableUserPicture(it.second, Size25dp, accountViewModel)
|
|
Spacer(DoubleHorzSpacer)
|
|
UsernameDisplay(it.second, remember { Modifier.weight(1f) })
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun ShortCommunityHeader(baseNote: AddressableNote, expanded: MutableState<Boolean>, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
|
val noteState by baseNote.live().metadata.observeAsState()
|
|
val noteEvent = remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return
|
|
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
noteEvent.image()?.let {
|
|
RobohashAsyncImageProxy(
|
|
robot = baseNote.idHex,
|
|
model = it,
|
|
contentDescription = stringResource(R.string.profile_image),
|
|
contentScale = ContentScale.Crop,
|
|
modifier = Modifier
|
|
.padding(start = 10.dp)
|
|
.width(Size35dp)
|
|
.height(Size35dp)
|
|
.clip(shape = CircleShape)
|
|
)
|
|
}
|
|
|
|
Column(
|
|
modifier = Modifier
|
|
.padding(start = 10.dp)
|
|
.height(Size35dp)
|
|
.weight(1f),
|
|
verticalArrangement = Arrangement.Center
|
|
) {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Text(
|
|
text = remember(noteState) { noteEvent.dTag() },
|
|
fontWeight = FontWeight.Bold,
|
|
maxLines = 1,
|
|
overflow = TextOverflow.Ellipsis
|
|
)
|
|
}
|
|
|
|
val summary = remember(noteState) {
|
|
noteEvent.description()?.ifBlank { null }
|
|
}
|
|
|
|
if (summary != null && !expanded.value) {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Text(
|
|
text = summary,
|
|
color = MaterialTheme.colors.placeholderText,
|
|
maxLines = 1,
|
|
overflow = TextOverflow.Ellipsis,
|
|
fontSize = 12.sp
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
Row(
|
|
modifier = Modifier
|
|
.height(Size35dp)
|
|
.padding(start = 5.dp),
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
ShortCommunityActionOptions(baseNote, accountViewModel, nav)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun ShortCommunityActionOptions(
|
|
note: AddressableNote,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
Spacer(modifier = StdHorzSpacer)
|
|
LikeReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
|
|
Spacer(modifier = StdHorzSpacer)
|
|
ZapReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
|
|
|
|
WatchAddressableNoteFollows(note, accountViewModel) { isFollowing ->
|
|
if (!isFollowing) {
|
|
Spacer(modifier = StdHorzSpacer)
|
|
JoinCommunityButton(accountViewModel, note, nav)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun WatchAddressableNoteFollows(note: AddressableNote, accountViewModel: AccountViewModel, onFollowChanges: @Composable (Boolean) -> Unit) {
|
|
val showFollowingMark by accountViewModel.userFollows.map {
|
|
it.user.latestContactList?.isTaggedAddressableNote(note.idHex) ?: false
|
|
}.distinctUntilChanged().observeAsState(
|
|
accountViewModel.userProfile().latestContactList?.isTaggedAddressableNote(note.idHex) ?: false
|
|
)
|
|
|
|
onFollowChanges(showFollowingMark)
|
|
}
|
|
|
|
@Composable
|
|
private fun LongCommunityActionOptions(
|
|
note: AddressableNote,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
WatchAddressableNoteFollows(note, accountViewModel) { isFollowing ->
|
|
if (isFollowing) {
|
|
LeaveCommunityButton(accountViewModel, note, nav)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun CheckNewAndRenderNote(
|
|
baseNote: Note,
|
|
routeForLastRead: String? = null,
|
|
modifier: Modifier = Modifier,
|
|
isBoostedNote: Boolean = false,
|
|
isQuotedNote: Boolean = false,
|
|
unPackReply: Boolean = true,
|
|
makeItShort: Boolean = false,
|
|
addMarginTop: Boolean = true,
|
|
canPreview: Boolean = true,
|
|
parentBackgroundColor: MutableState<Color>? = null,
|
|
accountViewModel: AccountViewModel,
|
|
showPopup: () -> Unit,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val newItemColor = MaterialTheme.colors.newItemBackgroundColor
|
|
val defaultBackgroundColor = MaterialTheme.colors.background
|
|
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
|
|
|
|
LaunchedEffect(key1 = routeForLastRead, key2 = parentBackgroundColor?.value) {
|
|
launch(Dispatchers.IO) {
|
|
routeForLastRead?.let {
|
|
val lastTime = accountViewModel.account.loadLastRead(it)
|
|
|
|
val createdAt = baseNote.createdAt()
|
|
if (createdAt != null) {
|
|
accountViewModel.account.markAsRead(it, createdAt)
|
|
|
|
val isNew = createdAt > lastTime
|
|
|
|
val newBackgroundColor = if (isNew) {
|
|
if (parentBackgroundColor != null) {
|
|
newItemColor.compositeOver(parentBackgroundColor.value)
|
|
} else {
|
|
newItemColor.compositeOver(defaultBackgroundColor)
|
|
}
|
|
} else {
|
|
parentBackgroundColor?.value ?: defaultBackgroundColor
|
|
}
|
|
|
|
if (newBackgroundColor != backgroundColor.value) {
|
|
launch(Dispatchers.Main) {
|
|
backgroundColor.value = newBackgroundColor
|
|
}
|
|
}
|
|
}
|
|
} ?: run {
|
|
val newBackgroundColor = parentBackgroundColor?.value ?: defaultBackgroundColor
|
|
|
|
if (newBackgroundColor != backgroundColor.value) {
|
|
launch(Dispatchers.Main) {
|
|
backgroundColor.value = newBackgroundColor
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ClickableNote(
|
|
baseNote = baseNote,
|
|
backgroundColor = backgroundColor,
|
|
modifier = modifier,
|
|
accountViewModel = accountViewModel,
|
|
showPopup = showPopup,
|
|
nav = nav
|
|
) {
|
|
InnerNoteWithReactions(
|
|
baseNote = baseNote,
|
|
backgroundColor = backgroundColor,
|
|
isBoostedNote = isBoostedNote,
|
|
isQuotedNote = isQuotedNote,
|
|
addMarginTop = addMarginTop,
|
|
unPackReply = unPackReply,
|
|
makeItShort = makeItShort,
|
|
canPreview = canPreview,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
@OptIn(ExperimentalFoundationApi::class)
|
|
fun ClickableNote(
|
|
baseNote: Note,
|
|
modifier: Modifier,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
showPopup: () -> Unit,
|
|
nav: (String) -> Unit,
|
|
content: @Composable () -> Unit
|
|
) {
|
|
val scope = rememberCoroutineScope()
|
|
|
|
val updatedModifier = remember(baseNote, backgroundColor.value) {
|
|
modifier
|
|
.combinedClickable(
|
|
onClick = {
|
|
scope.launch {
|
|
val redirectToNote =
|
|
if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) {
|
|
baseNote.replyTo?.lastOrNull() ?: baseNote
|
|
} else {
|
|
baseNote
|
|
}
|
|
routeFor(redirectToNote, accountViewModel.userProfile())?.let {
|
|
nav(it)
|
|
}
|
|
}
|
|
},
|
|
onLongClick = showPopup
|
|
)
|
|
.background(backgroundColor.value)
|
|
}
|
|
|
|
Column(modifier = updatedModifier) {
|
|
content()
|
|
}
|
|
}
|
|
|
|
@OptIn(ExperimentalTime::class)
|
|
@Composable
|
|
fun InnerNoteWithReactions(
|
|
baseNote: Note,
|
|
backgroundColor: MutableState<Color>,
|
|
isBoostedNote: Boolean,
|
|
isQuotedNote: Boolean,
|
|
addMarginTop: Boolean,
|
|
unPackReply: Boolean,
|
|
makeItShort: Boolean,
|
|
canPreview: Boolean,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val notBoostedNorQuote = !isBoostedNote && !isQuotedNote
|
|
|
|
Row(
|
|
modifier = remember {
|
|
Modifier
|
|
.fillMaxWidth()
|
|
.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
|
|
// Don't add margin to the bottom because of the Divider down below
|
|
)
|
|
}
|
|
) {
|
|
if (notBoostedNorQuote) {
|
|
Column(WidthAuthorPictureModifier) {
|
|
val (value, elapsed) = measureTimedValue {
|
|
AuthorAndRelayInformation(baseNote, accountViewModel, nav)
|
|
}
|
|
Log.d("Rendering Metrics", "Author: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
|
}
|
|
Spacer(modifier = DoubleHorzSpacer)
|
|
}
|
|
|
|
Column(Modifier.fillMaxWidth()) {
|
|
val showSecondRow = baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent && !isBoostedNote && !isQuotedNote
|
|
val (value, elapsed) = measureTimedValue {
|
|
NoteBody(
|
|
baseNote = baseNote,
|
|
showAuthorPicture = isQuotedNote,
|
|
unPackReply = unPackReply,
|
|
makeItShort = makeItShort,
|
|
canPreview = canPreview,
|
|
showSecondRow = showSecondRow,
|
|
backgroundColor = backgroundColor,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
}
|
|
Log.d("Rendering Metrics", "TextBody: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
|
}
|
|
}
|
|
|
|
val isNotRepost = baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent
|
|
|
|
if (isNotRepost) {
|
|
if (makeItShort) {
|
|
if (isBoostedNote) {
|
|
} else {
|
|
Spacer(modifier = DoubleVertSpacer)
|
|
}
|
|
} else {
|
|
val (value, elapsed) = measureTimedValue {
|
|
ReactionsRow(
|
|
baseNote = baseNote,
|
|
showReactionDetail = notBoostedNorQuote,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
}
|
|
Log.d("Rendering Metrics", "Reaction: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
|
}
|
|
}
|
|
|
|
if (notBoostedNorQuote) {
|
|
Divider(
|
|
thickness = DividerThickness
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun NoteBody(
|
|
baseNote: Note,
|
|
showAuthorPicture: Boolean = false,
|
|
unPackReply: Boolean = true,
|
|
makeItShort: Boolean = false,
|
|
canPreview: Boolean = true,
|
|
showSecondRow: Boolean,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
FirstUserInfoRow(
|
|
baseNote = baseNote,
|
|
showAuthorPicture = showAuthorPicture,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
|
|
if (showSecondRow) {
|
|
SecondUserInfoRow(
|
|
baseNote,
|
|
accountViewModel,
|
|
nav
|
|
)
|
|
}
|
|
|
|
Spacer(modifier = HalfVertSpacer)
|
|
|
|
if (!makeItShort) {
|
|
ReplyRow(
|
|
baseNote,
|
|
unPackReply,
|
|
backgroundColor,
|
|
accountViewModel,
|
|
nav
|
|
)
|
|
}
|
|
|
|
RenderNoteRow(
|
|
baseNote,
|
|
backgroundColor,
|
|
makeItShort,
|
|
canPreview,
|
|
accountViewModel,
|
|
nav
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
private fun RenderNoteRow(
|
|
baseNote: Note,
|
|
backgroundColor: MutableState<Color>,
|
|
makeItShort: Boolean,
|
|
canPreview: Boolean,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val noteEvent = baseNote.event
|
|
when (noteEvent) {
|
|
is AppDefinitionEvent -> {
|
|
RenderAppDefinition(baseNote, accountViewModel, nav)
|
|
}
|
|
|
|
is ReactionEvent -> {
|
|
RenderReaction(baseNote, backgroundColor, accountViewModel, nav)
|
|
}
|
|
|
|
is RepostEvent -> {
|
|
RenderRepost(baseNote, backgroundColor, accountViewModel, nav)
|
|
}
|
|
|
|
is GenericRepostEvent -> {
|
|
RenderRepost(baseNote, backgroundColor, accountViewModel, nav)
|
|
}
|
|
|
|
is ReportEvent -> {
|
|
RenderReport(baseNote, backgroundColor, accountViewModel, nav)
|
|
}
|
|
|
|
is LongTextNoteEvent -> {
|
|
RenderLongFormContent(baseNote, accountViewModel, nav)
|
|
}
|
|
|
|
is BadgeAwardEvent -> {
|
|
RenderBadgeAward(baseNote, backgroundColor, accountViewModel, nav)
|
|
}
|
|
|
|
is PeopleListEvent -> {
|
|
DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav)
|
|
}
|
|
|
|
is RelaySetEvent -> {
|
|
DisplayRelaySet(baseNote, backgroundColor, accountViewModel, nav)
|
|
}
|
|
|
|
is AudioTrackEvent -> {
|
|
RenderAudioTrack(baseNote, accountViewModel, nav)
|
|
}
|
|
|
|
is PinListEvent -> {
|
|
RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav)
|
|
}
|
|
|
|
is EmojiPackEvent -> {
|
|
RenderEmojiPack(baseNote, true, backgroundColor, accountViewModel)
|
|
}
|
|
|
|
is LiveActivitiesEvent -> {
|
|
RenderLiveActivityEvent(baseNote, accountViewModel, nav)
|
|
}
|
|
|
|
is PrivateDmEvent -> {
|
|
RenderPrivateMessage(
|
|
baseNote,
|
|
makeItShort,
|
|
canPreview,
|
|
backgroundColor,
|
|
accountViewModel,
|
|
nav
|
|
)
|
|
}
|
|
|
|
is ClassifiedsEvent -> {
|
|
RenderClassifieds(
|
|
noteEvent,
|
|
baseNote,
|
|
accountViewModel
|
|
)
|
|
}
|
|
|
|
is HighlightEvent -> {
|
|
RenderHighlight(
|
|
baseNote,
|
|
makeItShort,
|
|
canPreview,
|
|
backgroundColor,
|
|
accountViewModel,
|
|
nav
|
|
)
|
|
}
|
|
|
|
is PollNoteEvent -> {
|
|
RenderPoll(
|
|
baseNote,
|
|
makeItShort,
|
|
canPreview,
|
|
backgroundColor,
|
|
accountViewModel,
|
|
nav
|
|
)
|
|
}
|
|
|
|
is CommunityPostApprovalEvent -> {
|
|
RenderPostApproval(
|
|
baseNote,
|
|
makeItShort,
|
|
canPreview,
|
|
backgroundColor,
|
|
accountViewModel,
|
|
nav
|
|
)
|
|
}
|
|
|
|
else -> {
|
|
RenderTextEvent(
|
|
baseNote,
|
|
makeItShort,
|
|
canPreview,
|
|
backgroundColor,
|
|
accountViewModel,
|
|
nav
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun routeFor(note: Note, loggedIn: User): String? {
|
|
val noteEvent = note.event
|
|
|
|
if (noteEvent is ChannelMessageEvent || noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) {
|
|
note.channelHex()?.let {
|
|
return "Channel/$it"
|
|
}
|
|
} else if (noteEvent is LiveActivitiesEvent || noteEvent is LiveActivitiesChatMessageEvent) {
|
|
note.channelHex()?.let {
|
|
return "Channel/$it"
|
|
}
|
|
} else if (noteEvent is PrivateDmEvent) {
|
|
return "Room/${noteEvent.talkingWith(loggedIn.pubkeyHex)}"
|
|
} else if (noteEvent is CommunityDefinitionEvent) {
|
|
return "Community/${note.idHex}"
|
|
} else {
|
|
return "Note/${note.idHex}"
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
fun routeFor(note: Channel): String {
|
|
return "Channel/${note.idHex}"
|
|
}
|
|
|
|
fun routeFor(user: User): String {
|
|
return "User/${user.pubkeyHex}"
|
|
}
|
|
|
|
fun authorRouteFor(note: Note): String {
|
|
return "User/${note.author?.pubkeyHex}"
|
|
}
|
|
|
|
@Composable
|
|
fun RenderTextEvent(
|
|
note: Note,
|
|
makeItShort: Boolean,
|
|
canPreview: Boolean,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val eventContent = remember(note.event) {
|
|
val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null }
|
|
val body = accountViewModel.decrypt(note)
|
|
|
|
if (!subject.isNullOrBlank() && body?.split("\n")?.get(0)?.contains(subject) == false) {
|
|
"## $subject\n$body"
|
|
} else {
|
|
body
|
|
}
|
|
}
|
|
|
|
if (eventContent != null) {
|
|
val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) }
|
|
|
|
if (makeItShort && isAuthorTheLoggedUser) {
|
|
Text(
|
|
text = eventContent,
|
|
color = MaterialTheme.colors.placeholderText,
|
|
maxLines = 2,
|
|
overflow = TextOverflow.Ellipsis
|
|
)
|
|
} else {
|
|
SensitivityWarning(
|
|
note = note,
|
|
accountViewModel = accountViewModel
|
|
) {
|
|
val modifier = remember(note) { Modifier.fillMaxWidth() }
|
|
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
|
|
|
|
TranslatableRichTextViewer(
|
|
content = eventContent,
|
|
canPreview = canPreview && !makeItShort,
|
|
modifier = modifier,
|
|
tags = tags,
|
|
backgroundColor = backgroundColor,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
}
|
|
|
|
val hashtags = remember(note.event) { note.event?.hashtags()?.toImmutableList() ?: persistentListOf() }
|
|
DisplayUncitedHashtags(hashtags, eventContent, nav)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun RenderPoll(
|
|
note: Note,
|
|
makeItShort: Boolean,
|
|
canPreview: Boolean,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val noteEvent = note.event as? PollNoteEvent ?: return
|
|
val eventContent = remember { noteEvent.content() }
|
|
|
|
if (makeItShort && accountViewModel.isLoggedUser(note.author)) {
|
|
Text(
|
|
text = eventContent,
|
|
color = MaterialTheme.colors.placeholderText,
|
|
maxLines = 2,
|
|
overflow = TextOverflow.Ellipsis
|
|
)
|
|
} else {
|
|
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
|
|
|
|
SensitivityWarning(
|
|
note = note,
|
|
accountViewModel = accountViewModel
|
|
) {
|
|
TranslatableRichTextViewer(
|
|
content = eventContent,
|
|
canPreview = canPreview && !makeItShort,
|
|
modifier = remember { Modifier.fillMaxWidth() },
|
|
tags = tags,
|
|
backgroundColor = backgroundColor,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
|
|
PollNote(
|
|
note,
|
|
canPreview = canPreview && !makeItShort,
|
|
backgroundColor,
|
|
accountViewModel,
|
|
nav
|
|
)
|
|
}
|
|
|
|
val hashtags = remember { noteEvent.hashtags().toImmutableList() }
|
|
DisplayUncitedHashtags(hashtags, eventContent, nav)
|
|
}
|
|
}
|
|
|
|
@OptIn(ExperimentalFoundationApi::class)
|
|
@Composable
|
|
fun RenderAppDefinition(
|
|
note: Note,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val noteEvent = note.event as? AppDefinitionEvent ?: return
|
|
|
|
var metadata by remember {
|
|
mutableStateOf<UserMetadata?>(null)
|
|
}
|
|
|
|
LaunchedEffect(key1 = noteEvent) {
|
|
launch(Dispatchers.Default) {
|
|
metadata = noteEvent.appMetaData()
|
|
}
|
|
}
|
|
|
|
metadata?.let {
|
|
Box {
|
|
val clipboardManager = LocalClipboardManager.current
|
|
val uri = LocalUriHandler.current
|
|
|
|
if (!it.banner.isNullOrBlank()) {
|
|
var zoomImageDialogOpen by remember { mutableStateOf(false) }
|
|
|
|
AsyncImage(
|
|
model = it.banner,
|
|
contentDescription = stringResource(id = R.string.profile_image),
|
|
contentScale = ContentScale.FillWidth,
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.height(125.dp)
|
|
.combinedClickable(
|
|
onClick = {},
|
|
onLongClick = {
|
|
clipboardManager.setText(AnnotatedString(it.banner!!))
|
|
}
|
|
)
|
|
)
|
|
|
|
if (zoomImageDialogOpen) {
|
|
ZoomableImageDialog(imageUrl = figureOutMimeType(it.banner!!), onDismiss = { zoomImageDialogOpen = false }, accountViewModel = accountViewModel)
|
|
}
|
|
} else {
|
|
Image(
|
|
painter = painterResource(R.drawable.profile_banner),
|
|
contentDescription = stringResource(id = R.string.profile_banner),
|
|
contentScale = ContentScale.FillWidth,
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.height(125.dp)
|
|
)
|
|
}
|
|
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(horizontal = 10.dp)
|
|
.padding(top = 75.dp)
|
|
) {
|
|
Row(
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
verticalAlignment = Alignment.Bottom
|
|
) {
|
|
var zoomImageDialogOpen by remember { mutableStateOf(false) }
|
|
|
|
Box(Modifier.size(100.dp)) {
|
|
it.picture?.let {
|
|
AsyncImage(
|
|
model = it,
|
|
contentDescription = null,
|
|
contentScale = ContentScale.FillWidth,
|
|
modifier = Modifier
|
|
.border(
|
|
3.dp,
|
|
MaterialTheme.colors.background,
|
|
CircleShape
|
|
)
|
|
.clip(shape = CircleShape)
|
|
.fillMaxSize()
|
|
.background(MaterialTheme.colors.background)
|
|
.combinedClickable(
|
|
onClick = { zoomImageDialogOpen = true },
|
|
onLongClick = {
|
|
clipboardManager.setText(AnnotatedString(it))
|
|
}
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
if (zoomImageDialogOpen) {
|
|
ZoomableImageDialog(imageUrl = figureOutMimeType(it.banner!!), onDismiss = { zoomImageDialogOpen = false }, accountViewModel = accountViewModel)
|
|
}
|
|
|
|
Spacer(Modifier.weight(1f))
|
|
|
|
Row(
|
|
modifier = Modifier
|
|
.height(Size35dp)
|
|
.padding(bottom = 3.dp)
|
|
) {
|
|
}
|
|
}
|
|
|
|
val name = remember(it) { it.anyName() }
|
|
name?.let {
|
|
Row(verticalAlignment = Alignment.Bottom, modifier = Modifier.padding(top = 7.dp)) {
|
|
CreateTextWithEmoji(
|
|
text = it,
|
|
tags = remember { (note.event?.tags() ?: emptyList()).toImmutableListOfLists() },
|
|
fontWeight = FontWeight.Bold,
|
|
fontSize = 25.sp
|
|
)
|
|
}
|
|
}
|
|
|
|
val website = remember(it) { it.website }
|
|
if (!website.isNullOrEmpty()) {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
LinkIcon(Size16Modifier, MaterialTheme.colors.placeholderText)
|
|
|
|
ClickableText(
|
|
text = AnnotatedString(website.removePrefix("https://")),
|
|
onClick = { website.let { runCatching { uri.openUri(it) } } },
|
|
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary),
|
|
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp)
|
|
)
|
|
}
|
|
}
|
|
|
|
it.about?.let {
|
|
Row(
|
|
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)
|
|
) {
|
|
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
|
|
val bgColor = MaterialTheme.colors.background
|
|
val backgroundColor = remember {
|
|
mutableStateOf(bgColor)
|
|
}
|
|
TranslatableRichTextViewer(
|
|
content = it,
|
|
canPreview = false,
|
|
tags = tags,
|
|
backgroundColor = backgroundColor,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun RenderHighlight(
|
|
note: Note,
|
|
makeItShort: Boolean,
|
|
canPreview: Boolean,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val quote = remember {
|
|
(note.event as? HighlightEvent)?.quote() ?: ""
|
|
}
|
|
val author = remember() {
|
|
(note.event as? HighlightEvent)?.author()
|
|
}
|
|
val url = remember() {
|
|
(note.event as? HighlightEvent)?.inUrl()
|
|
}
|
|
val postHex = remember() {
|
|
(note.event as? HighlightEvent)?.taggedAddresses()?.firstOrNull()
|
|
}
|
|
|
|
DisplayHighlight(
|
|
quote,
|
|
author,
|
|
url,
|
|
postHex,
|
|
makeItShort,
|
|
canPreview,
|
|
backgroundColor,
|
|
accountViewModel,
|
|
nav
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
private fun RenderPrivateMessage(
|
|
note: Note,
|
|
makeItShort: Boolean,
|
|
canPreview: Boolean,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val noteEvent = note.event as? PrivateDmEvent ?: return
|
|
|
|
val withMe = remember { noteEvent.with(accountViewModel.userProfile().pubkeyHex) }
|
|
|
|
if (withMe) {
|
|
val eventContent = remember { accountViewModel.decrypt(note) }
|
|
|
|
val hashtags = remember(note.event?.id()) { note.event?.hashtags()?.toImmutableList() ?: persistentListOf() }
|
|
val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() }
|
|
val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) }
|
|
|
|
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
|
|
|
|
if (eventContent != null) {
|
|
if (makeItShort && isAuthorTheLoggedUser) {
|
|
Text(
|
|
text = eventContent,
|
|
color = MaterialTheme.colors.placeholderText,
|
|
maxLines = 2,
|
|
overflow = TextOverflow.Ellipsis
|
|
)
|
|
} else {
|
|
SensitivityWarning(
|
|
note = note,
|
|
accountViewModel = accountViewModel
|
|
) {
|
|
TranslatableRichTextViewer(
|
|
content = eventContent,
|
|
canPreview = canPreview && !makeItShort,
|
|
modifier = modifier,
|
|
tags = tags,
|
|
backgroundColor = backgroundColor,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
}
|
|
|
|
DisplayUncitedHashtags(hashtags, eventContent, nav)
|
|
}
|
|
}
|
|
} else {
|
|
val recipient = noteEvent.recipientPubKeyBytes()?.toNpub() ?: "Someone"
|
|
|
|
TranslatableRichTextViewer(
|
|
stringResource(
|
|
id = R.string.private_conversation_notification,
|
|
"@${note.author?.pubkeyNpub()}",
|
|
"@$recipient"
|
|
),
|
|
canPreview = !makeItShort,
|
|
Modifier.fillMaxWidth(),
|
|
ImmutableListOfLists(),
|
|
backgroundColor,
|
|
accountViewModel,
|
|
nav
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun DisplayRelaySet(
|
|
baseNote: Note,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val noteEvent = baseNote.event as? RelaySetEvent ?: return
|
|
|
|
val relays by remember {
|
|
mutableStateOf<ImmutableList<String>>(
|
|
noteEvent.relays().toImmutableList()
|
|
)
|
|
}
|
|
|
|
var expanded by remember {
|
|
mutableStateOf(false)
|
|
}
|
|
|
|
val toMembersShow = if (expanded) {
|
|
relays
|
|
} else {
|
|
relays.take(3)
|
|
}
|
|
|
|
val relayListName by remember {
|
|
derivedStateOf {
|
|
"#${noteEvent.dTag()}"
|
|
}
|
|
}
|
|
|
|
val relayDescription by remember {
|
|
derivedStateOf {
|
|
noteEvent.description()
|
|
}
|
|
}
|
|
|
|
Text(
|
|
text = relayListName,
|
|
fontWeight = FontWeight.Bold,
|
|
maxLines = 1,
|
|
overflow = TextOverflow.Ellipsis,
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(5.dp),
|
|
textAlign = TextAlign.Center
|
|
)
|
|
|
|
relayDescription?.let {
|
|
Text(
|
|
text = it,
|
|
maxLines = 3,
|
|
overflow = TextOverflow.Ellipsis,
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(5.dp),
|
|
textAlign = TextAlign.Center,
|
|
color = Color.Gray
|
|
)
|
|
}
|
|
|
|
Box {
|
|
Column(modifier = Modifier.padding(top = 5.dp)) {
|
|
toMembersShow.forEach { relay ->
|
|
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = CenterVertically) {
|
|
Text(
|
|
relay.trim().removePrefix("wss://").removePrefix("ws://").removeSuffix("/"),
|
|
fontWeight = FontWeight.Bold,
|
|
maxLines = 1,
|
|
overflow = TextOverflow.Ellipsis,
|
|
modifier = Modifier
|
|
.padding(start = 10.dp, bottom = 5.dp)
|
|
.weight(1f)
|
|
)
|
|
|
|
Column(modifier = Modifier.padding(start = 10.dp)) {
|
|
RelayOptionsAction(relay, accountViewModel, nav)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (relays.size > 3 && !expanded) {
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
horizontalArrangement = Arrangement.Center,
|
|
modifier = Modifier
|
|
.align(Alignment.BottomCenter)
|
|
.fillMaxWidth()
|
|
.background(getGradient(backgroundColor))
|
|
) {
|
|
ShowMoreButton {
|
|
expanded = !expanded
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun RelayOptionsAction(
|
|
relay: String,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val userStateRelayInfo by accountViewModel.account.userProfile().live().relayInfo.observeAsState()
|
|
val isCurrentlyOnTheUsersList by remember(userStateRelayInfo) {
|
|
derivedStateOf {
|
|
userStateRelayInfo?.user?.latestContactList?.relays()?.none { it.key == relay } == true
|
|
}
|
|
}
|
|
|
|
var wantsToAddRelay by remember {
|
|
mutableStateOf("")
|
|
}
|
|
|
|
if (wantsToAddRelay.isNotEmpty()) {
|
|
NewRelayListView({ wantsToAddRelay = "" }, accountViewModel, wantsToAddRelay, nav = nav)
|
|
}
|
|
|
|
if (isCurrentlyOnTheUsersList) {
|
|
AddRelayButton { wantsToAddRelay = relay }
|
|
} else {
|
|
RemoveRelayButton { wantsToAddRelay = relay }
|
|
}
|
|
}
|
|
|
|
@OptIn(ExperimentalLayoutApi::class)
|
|
@Composable
|
|
fun DisplayPeopleList(
|
|
baseNote: Note,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val noteEvent = baseNote.event as? PeopleListEvent ?: return
|
|
|
|
var members by remember { mutableStateOf<List<User>>(listOf()) }
|
|
|
|
val account = accountViewModel.userProfile()
|
|
var expanded by remember {
|
|
mutableStateOf(false)
|
|
}
|
|
|
|
val toMembersShow = if (expanded) {
|
|
members
|
|
} else {
|
|
members.take(3)
|
|
}
|
|
|
|
val name by remember {
|
|
derivedStateOf {
|
|
"#${noteEvent.dTag()}"
|
|
}
|
|
}
|
|
|
|
Text(
|
|
text = name,
|
|
fontWeight = FontWeight.Bold,
|
|
maxLines = 1,
|
|
overflow = TextOverflow.Ellipsis,
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(5.dp),
|
|
textAlign = TextAlign.Center
|
|
)
|
|
|
|
LaunchedEffect(Unit) {
|
|
launch(Dispatchers.IO) {
|
|
members = noteEvent.bookmarkedPeople().mapNotNull { hex ->
|
|
LocalCache.checkGetOrCreateUser(hex)
|
|
}.sortedBy { account.isFollowing(it) }.reversed()
|
|
}
|
|
}
|
|
|
|
Box {
|
|
FlowRow(modifier = Modifier.padding(top = 5.dp)) {
|
|
toMembersShow.forEach { user ->
|
|
Row(modifier = Modifier.fillMaxWidth()) {
|
|
UserCompose(
|
|
user,
|
|
overallModifier = Modifier,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (members.size > 3 && !expanded) {
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
horizontalArrangement = Arrangement.Center,
|
|
modifier = Modifier
|
|
.align(Alignment.BottomCenter)
|
|
.fillMaxWidth()
|
|
.background(getGradient(backgroundColor))
|
|
) {
|
|
ShowMoreButton {
|
|
expanded = !expanded
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@OptIn(ExperimentalLayoutApi::class)
|
|
@Composable
|
|
private fun RenderBadgeAward(
|
|
note: Note,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
if (note.replyTo.isNullOrEmpty()) return
|
|
|
|
val noteEvent = note.event as? BadgeAwardEvent ?: return
|
|
var awardees by remember { mutableStateOf<List<User>>(listOf()) }
|
|
|
|
val account = accountViewModel.userProfile()
|
|
|
|
Text(text = stringResource(R.string.award_granted_to))
|
|
|
|
LaunchedEffect(key1 = note) {
|
|
launch(Dispatchers.IO) {
|
|
awardees = noteEvent.awardees().mapNotNull { hex ->
|
|
LocalCache.checkGetOrCreateUser(hex)
|
|
}.sortedBy { account.isFollowing(it) }.reversed()
|
|
}
|
|
}
|
|
|
|
FlowRow(modifier = Modifier.padding(top = 5.dp)) {
|
|
awardees.take(100).forEach { user ->
|
|
Row(
|
|
modifier = Modifier
|
|
.size(size = Size35dp)
|
|
.clickable {
|
|
nav("User/${user.pubkeyHex}")
|
|
},
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
ClickableUserPicture(
|
|
baseUser = user,
|
|
accountViewModel = accountViewModel,
|
|
size = Size35dp
|
|
)
|
|
}
|
|
}
|
|
|
|
if (awardees.size > 100) {
|
|
Text(" and ${awardees.size - 100} others", maxLines = 1)
|
|
}
|
|
}
|
|
|
|
note.replyTo?.firstOrNull()?.let {
|
|
NoteCompose(
|
|
it,
|
|
modifier = Modifier,
|
|
isBoostedNote = false,
|
|
isQuotedNote = true,
|
|
unPackReply = false,
|
|
parentBackgroundColor = backgroundColor,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun RenderReaction(
|
|
note: Note,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
note.replyTo?.lastOrNull()?.let {
|
|
NoteCompose(
|
|
it,
|
|
modifier = Modifier,
|
|
isBoostedNote = true,
|
|
unPackReply = false,
|
|
parentBackgroundColor = backgroundColor,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
}
|
|
|
|
// Reposts have trash in their contents.
|
|
val refactorReactionText =
|
|
if (note.event?.content() == "+") "❤" else note.event?.content() ?: ""
|
|
|
|
Text(
|
|
text = refactorReactionText,
|
|
maxLines = 1
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
fun RenderRepost(
|
|
note: Note,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val boostedNote = remember {
|
|
note.replyTo?.lastOrNull()
|
|
}
|
|
|
|
boostedNote?.let {
|
|
NoteCompose(
|
|
it,
|
|
modifier = Modifier,
|
|
isBoostedNote = true,
|
|
unPackReply = false,
|
|
parentBackgroundColor = backgroundColor,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun RenderPostApproval(
|
|
note: Note,
|
|
makeItShort: Boolean,
|
|
canPreview: Boolean,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
if (note.replyTo.isNullOrEmpty()) return
|
|
|
|
val noteEvent = note.event as? CommunityPostApprovalEvent ?: return
|
|
|
|
Column(Modifier.fillMaxWidth()) {
|
|
noteEvent.communities().forEach {
|
|
LoadAddressableNote(it) {
|
|
it?.let {
|
|
NoteCompose(
|
|
it,
|
|
parentBackgroundColor = backgroundColor,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
Text(
|
|
text = stringResource(id = R.string.community_approved_posts),
|
|
fontWeight = FontWeight.Bold,
|
|
maxLines = 1,
|
|
overflow = TextOverflow.Ellipsis,
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(5.dp),
|
|
textAlign = TextAlign.Center
|
|
)
|
|
|
|
note.replyTo?.forEach {
|
|
NoteCompose(
|
|
it,
|
|
modifier = MaterialTheme.colors.replyModifier,
|
|
unPackReply = false,
|
|
makeItShort = true,
|
|
isQuotedNote = true,
|
|
parentBackgroundColor = backgroundColor,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun LoadAddressableNote(aTag: ATag, content: @Composable (AddressableNote?) -> Unit) {
|
|
var note by remember(aTag) {
|
|
mutableStateOf<AddressableNote?>(LocalCache.getAddressableNoteIfExists(aTag.toTag()))
|
|
}
|
|
|
|
if (note == null) {
|
|
LaunchedEffect(key1 = aTag) {
|
|
launch(Dispatchers.IO) {
|
|
note = LocalCache.getOrCreateAddressableNote(aTag)
|
|
}
|
|
}
|
|
}
|
|
|
|
content(note)
|
|
}
|
|
|
|
@Composable
|
|
public fun RenderEmojiPack(
|
|
baseNote: Note,
|
|
actionable: Boolean,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
onClick: ((EmojiUrl) -> Unit)? = null
|
|
) {
|
|
val noteEvent by baseNote.live().metadata.map {
|
|
it.note.event
|
|
}.distinctUntilChanged().observeAsState(baseNote.event)
|
|
|
|
if (noteEvent == null || noteEvent !is EmojiPackEvent) return
|
|
|
|
(noteEvent as? EmojiPackEvent)?.let {
|
|
RenderEmojiPack(
|
|
noteEvent = it,
|
|
baseNote = baseNote,
|
|
actionable = actionable,
|
|
backgroundColor = backgroundColor,
|
|
accountViewModel = accountViewModel,
|
|
onClick = onClick
|
|
)
|
|
}
|
|
}
|
|
|
|
@OptIn(ExperimentalLayoutApi::class)
|
|
@Composable
|
|
public fun RenderEmojiPack(
|
|
noteEvent: EmojiPackEvent,
|
|
baseNote: Note,
|
|
actionable: Boolean,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
onClick: ((EmojiUrl) -> Unit)? = null
|
|
) {
|
|
var expanded by remember {
|
|
mutableStateOf(false)
|
|
}
|
|
|
|
val allEmojis = remember(noteEvent) {
|
|
noteEvent.taggedEmojis()
|
|
}
|
|
|
|
val emojisToShow = if (expanded) {
|
|
allEmojis
|
|
} else {
|
|
allEmojis.take(60)
|
|
}
|
|
|
|
Row(verticalAlignment = CenterVertically) {
|
|
Text(
|
|
text = remember(noteEvent) { "#${noteEvent.dTag()}" },
|
|
fontWeight = FontWeight.Bold,
|
|
maxLines = 1,
|
|
overflow = TextOverflow.Ellipsis,
|
|
modifier = Modifier
|
|
.weight(1F)
|
|
.padding(5.dp),
|
|
textAlign = TextAlign.Center
|
|
)
|
|
|
|
if (actionable) {
|
|
EmojiListOptions(accountViewModel, baseNote)
|
|
}
|
|
}
|
|
|
|
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.TopCenter) {
|
|
FlowRow(modifier = Modifier.padding(top = 5.dp)) {
|
|
emojisToShow.forEach { emoji ->
|
|
if (onClick != null) {
|
|
IconButton(onClick = { onClick(emoji) }, modifier = Size35Modifier) {
|
|
AsyncImage(
|
|
model = emoji.url,
|
|
contentDescription = null,
|
|
modifier = Size35Modifier
|
|
)
|
|
}
|
|
} else {
|
|
Box(
|
|
modifier = Size35Modifier,
|
|
contentAlignment = Alignment.Center
|
|
) {
|
|
AsyncImage(
|
|
model = emoji.url,
|
|
contentDescription = null,
|
|
modifier = Size35Modifier
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (allEmojis.size > 60 && !expanded) {
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
horizontalArrangement = Arrangement.Center,
|
|
modifier = Modifier
|
|
.align(Alignment.BottomCenter)
|
|
.fillMaxWidth()
|
|
.background(getGradient(backgroundColor))
|
|
) {
|
|
ShowMoreButton {
|
|
expanded = !expanded
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun EmojiListOptions(
|
|
accountViewModel: AccountViewModel,
|
|
emojiPackNote: Note
|
|
) {
|
|
LoadAddressableNote(
|
|
aTag = ATag(
|
|
EmojiPackSelectionEvent.kind,
|
|
accountViewModel.userProfile().pubkeyHex,
|
|
"",
|
|
null
|
|
)
|
|
) {
|
|
it?.let { usersEmojiList ->
|
|
val hasAddedThis by usersEmojiList.live().metadata.map {
|
|
usersEmojiList.event?.isTaggedAddressableNote(emojiPackNote.idHex)
|
|
}.distinctUntilChanged().observeAsState()
|
|
|
|
Crossfade(targetState = hasAddedThis) {
|
|
val scope = rememberCoroutineScope()
|
|
if (it != true) {
|
|
AddButton() {
|
|
scope.launch(Dispatchers.IO) {
|
|
accountViewModel.addEmojiPack(usersEmojiList, emojiPackNote)
|
|
}
|
|
}
|
|
} else {
|
|
RemoveButton {
|
|
scope.launch(Dispatchers.IO) {
|
|
accountViewModel.removeEmojiPack(usersEmojiList, emojiPackNote)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun RemoveButton(onClick: () -> Unit) {
|
|
Button(
|
|
modifier = Modifier.padding(start = 3.dp),
|
|
onClick = onClick,
|
|
shape = ButtonBorder,
|
|
colors = ButtonDefaults
|
|
.buttonColors(
|
|
backgroundColor = MaterialTheme.colors.primary
|
|
),
|
|
contentPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp)
|
|
) {
|
|
Text(text = stringResource(R.string.remove), color = Color.White)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun AddButton(text: Int = R.string.add, onClick: () -> Unit) {
|
|
Button(
|
|
modifier = Modifier.padding(start = 3.dp),
|
|
onClick = onClick,
|
|
shape = ButtonBorder,
|
|
colors = ButtonDefaults
|
|
.buttonColors(
|
|
backgroundColor = MaterialTheme.colors.primary
|
|
),
|
|
contentPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp)
|
|
) {
|
|
Text(text = stringResource(text), color = Color.White, textAlign = TextAlign.Center)
|
|
}
|
|
}
|
|
|
|
@OptIn(ExperimentalLayoutApi::class)
|
|
@Composable
|
|
fun RenderPinListEvent(
|
|
baseNote: Note,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val noteEvent = baseNote.event as? PinListEvent ?: return
|
|
|
|
val pins by remember { mutableStateOf(noteEvent.pins()) }
|
|
|
|
var expanded by remember {
|
|
mutableStateOf(false)
|
|
}
|
|
|
|
val pinsToShow = if (expanded) {
|
|
pins
|
|
} else {
|
|
pins.take(3)
|
|
}
|
|
|
|
Text(
|
|
text = "#${noteEvent.dTag()}",
|
|
fontWeight = FontWeight.Bold,
|
|
maxLines = 1,
|
|
overflow = TextOverflow.Ellipsis,
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(5.dp),
|
|
textAlign = TextAlign.Center
|
|
)
|
|
|
|
Box {
|
|
FlowRow(modifier = Modifier.padding(top = 5.dp)) {
|
|
pinsToShow.forEach { pin ->
|
|
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = CenterVertically) {
|
|
PinIcon(modifier = Size15Modifier, tint = MaterialTheme.colors.onBackground.copy(0.32f))
|
|
|
|
Spacer(modifier = Modifier.width(5.dp))
|
|
|
|
TranslatableRichTextViewer(
|
|
content = pin,
|
|
canPreview = true,
|
|
tags = remember { ImmutableListOfLists() },
|
|
backgroundColor = backgroundColor,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pins.size > 3 && !expanded) {
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
horizontalArrangement = Arrangement.Center,
|
|
modifier = Modifier
|
|
.align(Alignment.BottomCenter)
|
|
.fillMaxWidth()
|
|
.background(getGradient(backgroundColor))
|
|
) {
|
|
ShowMoreButton {
|
|
expanded = !expanded
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun getGradient(backgroundColor: MutableState<Color>): Brush {
|
|
return Brush.verticalGradient(
|
|
colors = listOf(
|
|
backgroundColor.value.copy(alpha = 0f),
|
|
backgroundColor.value
|
|
)
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
private fun RenderAudioTrack(
|
|
note: Note,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val noteEvent = note.event as? AudioTrackEvent ?: return
|
|
|
|
AudioTrackHeader(noteEvent, accountViewModel, nav)
|
|
}
|
|
|
|
@Composable
|
|
private fun RenderLongFormContent(
|
|
note: Note,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val noteEvent = note.event as? LongTextNoteEvent ?: return
|
|
|
|
LongFormHeader(noteEvent, note, accountViewModel)
|
|
}
|
|
|
|
@Composable
|
|
private fun RenderReport(
|
|
note: Note,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val noteEvent = note.event as? ReportEvent ?: return
|
|
|
|
val base = remember {
|
|
(noteEvent.reportedPost() + noteEvent.reportedAuthor())
|
|
}
|
|
|
|
val reportType = base.map {
|
|
when (it.reportType) {
|
|
ReportEvent.ReportType.EXPLICIT -> stringResource(R.string.explicit_content)
|
|
ReportEvent.ReportType.NUDITY -> stringResource(R.string.nudity)
|
|
ReportEvent.ReportType.PROFANITY -> stringResource(R.string.profanity_hateful_speech)
|
|
ReportEvent.ReportType.SPAM -> stringResource(R.string.spam)
|
|
ReportEvent.ReportType.IMPERSONATION -> stringResource(R.string.impersonation)
|
|
ReportEvent.ReportType.ILLEGAL -> stringResource(R.string.illegal_behavior)
|
|
}
|
|
}.toSet().joinToString(", ")
|
|
|
|
val content = remember {
|
|
reportType + (note.event?.content()?.ifBlank { null }?.let { ": $it" } ?: "")
|
|
}
|
|
|
|
TranslatableRichTextViewer(
|
|
content = content,
|
|
canPreview = true,
|
|
modifier = remember { Modifier },
|
|
tags = remember { ImmutableListOfLists() },
|
|
backgroundColor = backgroundColor,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
|
|
note.replyTo?.lastOrNull()?.let {
|
|
NoteCompose(
|
|
baseNote = it,
|
|
isQuotedNote = true,
|
|
modifier = Modifier
|
|
.padding(top = 5.dp)
|
|
.fillMaxWidth()
|
|
.clip(shape = QuoteBorder)
|
|
.border(
|
|
1.dp,
|
|
MaterialTheme.colors.subtleBorder,
|
|
QuoteBorder
|
|
),
|
|
unPackReply = false,
|
|
makeItShort = true,
|
|
parentBackgroundColor = backgroundColor,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun ReplyRow(
|
|
note: Note,
|
|
unPackReply: Boolean,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val noteEvent = note.event
|
|
|
|
val showReply by remember {
|
|
derivedStateOf {
|
|
noteEvent is TextNoteEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())
|
|
}
|
|
}
|
|
|
|
if (showReply) {
|
|
val replyingDirectlyTo = remember { note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.kind } }
|
|
if (replyingDirectlyTo != null && unPackReply) {
|
|
ReplyNoteComposition(replyingDirectlyTo, backgroundColor, accountViewModel, nav)
|
|
Spacer(modifier = StdVertSpacer)
|
|
} else {
|
|
// ReplyInformation(note.replyTo, noteEvent.mentions(), accountViewModel, nav)
|
|
}
|
|
} else {
|
|
val showChannelReply by remember {
|
|
derivedStateOf {
|
|
(noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())) ||
|
|
(noteEvent is LiveActivitiesChatMessageEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser()))
|
|
}
|
|
}
|
|
|
|
if (showChannelReply) {
|
|
val channelHex = note.channelHex()
|
|
channelHex?.let {
|
|
ChannelHeader(
|
|
channelHex = channelHex,
|
|
showVideo = false,
|
|
showBottomDiviser = false,
|
|
sendToChannel = true,
|
|
modifier = remember { Modifier.padding(vertical = 5.dp) },
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
|
|
val replies = remember { note.replyTo?.toImmutableList() }
|
|
val mentions = remember { (note.event as? BaseTextNoteEvent)?.mentions()?.toImmutableList() ?: persistentListOf() }
|
|
|
|
ReplyInformationChannel(replies, mentions, accountViewModel, nav)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun ReplyNoteComposition(
|
|
replyingDirectlyTo: Note,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val replyBackgroundColor = remember {
|
|
mutableStateOf(backgroundColor.value)
|
|
}
|
|
val defaultReplyBackground = MaterialTheme.colors.replyBackground
|
|
|
|
LaunchedEffect(key1 = backgroundColor.value, key2 = defaultReplyBackground) {
|
|
launch(Dispatchers.Default) {
|
|
val newReplyBackgroundColor =
|
|
defaultReplyBackground.compositeOver(backgroundColor.value)
|
|
if (replyBackgroundColor.value != newReplyBackgroundColor) {
|
|
replyBackgroundColor.value = newReplyBackgroundColor
|
|
}
|
|
}
|
|
}
|
|
|
|
NoteCompose(
|
|
baseNote = replyingDirectlyTo,
|
|
isQuotedNote = true,
|
|
modifier = MaterialTheme.colors.replyModifier,
|
|
unPackReply = false,
|
|
makeItShort = true,
|
|
parentBackgroundColor = replyBackgroundColor,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
fun SecondUserInfoRow(
|
|
note: Note,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val noteEvent = remember { note.event } ?: return
|
|
val noteAuthor = remember { note.author } ?: return
|
|
|
|
Row(verticalAlignment = CenterVertically, modifier = UserNameMaxRowHeight) {
|
|
ObserveDisplayNip05Status(noteAuthor, remember { Modifier.weight(1f) })
|
|
|
|
val baseReward = remember { noteEvent.getReward()?.let { Reward(it) } }
|
|
if (baseReward != null) {
|
|
DisplayReward(baseReward, note, accountViewModel, nav)
|
|
}
|
|
|
|
val pow = remember { noteEvent.getPoWRank() }
|
|
if (pow > 20) {
|
|
DisplayPoW(pow)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun FirstUserInfoRow(
|
|
baseNote: Note,
|
|
showAuthorPicture: Boolean,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
Row(verticalAlignment = CenterVertically, modifier = remember { UserNameRowHeight }) {
|
|
val isRepost by remember(baseNote) {
|
|
derivedStateOf {
|
|
baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent
|
|
}
|
|
}
|
|
|
|
val isCommunityPost by remember(baseNote) {
|
|
derivedStateOf {
|
|
baseNote.event?.isTaggedAddressableKind(CommunityDefinitionEvent.kind) == true
|
|
}
|
|
}
|
|
|
|
if (showAuthorPicture) {
|
|
NoteAuthorPicture(baseNote, nav, accountViewModel, Size25dp)
|
|
Spacer(HalfPadding)
|
|
NoteUsernameDisplay(baseNote, remember { Modifier.weight(1f) })
|
|
} else {
|
|
NoteUsernameDisplay(baseNote, remember { Modifier.weight(1f) })
|
|
}
|
|
|
|
if (isRepost) {
|
|
BoostedMark()
|
|
} else if (isCommunityPost) {
|
|
DisplayFollowingCommunityInPost(baseNote, accountViewModel, nav)
|
|
} else {
|
|
DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav)
|
|
}
|
|
|
|
TimeAgo(baseNote)
|
|
|
|
MoreOptionsButton(baseNote, accountViewModel)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun BoostedMark() {
|
|
Text(
|
|
stringResource(id = R.string.boosted),
|
|
fontWeight = FontWeight.Bold,
|
|
color = MaterialTheme.colors.placeholderText,
|
|
maxLines = 1,
|
|
modifier = StdStartPadding
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
fun MoreOptionsButton(
|
|
baseNote: Note,
|
|
accountViewModel: AccountViewModel
|
|
) {
|
|
val popupExpanded = remember { mutableStateOf(false) }
|
|
val enablePopup = remember {
|
|
{ popupExpanded.value = true }
|
|
}
|
|
|
|
IconButton(
|
|
modifier = Size24Modifier,
|
|
onClick = enablePopup
|
|
) {
|
|
VerticalDotsIcon()
|
|
|
|
NoteDropDownMenu(
|
|
baseNote,
|
|
popupExpanded,
|
|
accountViewModel
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun TimeAgo(note: Note) {
|
|
val time = remember(note) { note.createdAt() } ?: return
|
|
TimeAgo(time)
|
|
}
|
|
|
|
@Composable
|
|
fun TimeAgo(time: Long) {
|
|
val context = LocalContext.current
|
|
val timeStr by remember(time) { mutableStateOf(timeAgo(time, context = context)) }
|
|
|
|
Text(
|
|
text = timeStr,
|
|
color = MaterialTheme.colors.placeholderText,
|
|
maxLines = 1
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
private fun AuthorAndRelayInformation(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
|
// Draws the boosted picture outside the boosted card.
|
|
Box(modifier = Size55Modifier, contentAlignment = Alignment.BottomEnd) {
|
|
RenderAuthorImages(baseNote, nav, accountViewModel)
|
|
}
|
|
|
|
BadgeBox(baseNote, accountViewModel, nav)
|
|
}
|
|
|
|
@Composable
|
|
private fun BadgeBox(
|
|
baseNote: Note,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val isRepost by remember {
|
|
derivedStateOf {
|
|
baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent
|
|
}
|
|
}
|
|
|
|
if (isRepost) {
|
|
val baseReply by remember {
|
|
derivedStateOf {
|
|
baseNote.replyTo?.lastOrNull()
|
|
}
|
|
}
|
|
baseReply?.let {
|
|
RelayBadges(it, accountViewModel, nav)
|
|
}
|
|
} else {
|
|
RelayBadges(baseNote, accountViewModel, nav)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun RenderAuthorImages(
|
|
baseNote: Note,
|
|
nav: (String) -> Unit,
|
|
accountViewModel: AccountViewModel
|
|
) {
|
|
NoteAuthorPicture(baseNote, nav, accountViewModel, Size55dp)
|
|
|
|
val isRepost = baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent
|
|
|
|
if (isRepost) {
|
|
RepostNoteAuthorPicture(baseNote, accountViewModel, nav)
|
|
}
|
|
|
|
val isChannel = baseNote.event is ChannelMessageEvent && baseNote.channelHex() != null
|
|
|
|
if (isChannel) {
|
|
val baseChannelHex = remember { baseNote.channelHex() }
|
|
if (baseChannelHex != null) {
|
|
LoadChannel(baseChannelHex) { channel ->
|
|
ChannelNotePicture(channel)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun LoadChannel(baseChannelHex: String, content: @Composable (Channel) -> Unit) {
|
|
var channel by remember(baseChannelHex) {
|
|
mutableStateOf<Channel?>(LocalCache.getChannelIfExists(baseChannelHex))
|
|
}
|
|
|
|
if (channel == null) {
|
|
LaunchedEffect(key1 = baseChannelHex) {
|
|
launch(Dispatchers.IO) {
|
|
val newChannel = LocalCache.checkGetOrCreateChannel(baseChannelHex)
|
|
launch(Dispatchers.Main) {
|
|
channel = newChannel
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
channel?.let {
|
|
content(it)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun ChannelNotePicture(baseChannel: Channel) {
|
|
val model by baseChannel.live.map {
|
|
it.channel.profilePicture()
|
|
}.distinctUntilChanged().observeAsState()
|
|
|
|
val backgroundColor = MaterialTheme.colors.background
|
|
|
|
val modifier = remember {
|
|
Modifier
|
|
.width(30.dp)
|
|
.height(30.dp)
|
|
.clip(shape = CircleShape)
|
|
.background(backgroundColor)
|
|
.border(
|
|
2.dp,
|
|
backgroundColor,
|
|
CircleShape
|
|
)
|
|
}
|
|
|
|
Box(Size30Modifier) {
|
|
RobohashAsyncImageProxy(
|
|
robot = baseChannel.idHex,
|
|
model = model,
|
|
contentDescription = stringResource(R.string.group_picture),
|
|
modifier = modifier
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun RepostNoteAuthorPicture(
|
|
baseNote: Note,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val baseRepost by remember {
|
|
derivedStateOf {
|
|
baseNote.replyTo?.lastOrNull()
|
|
}
|
|
}
|
|
|
|
baseRepost?.let {
|
|
Box(Size30Modifier) {
|
|
NoteAuthorPicture(
|
|
baseNote = it,
|
|
nav = nav,
|
|
accountViewModel = accountViewModel,
|
|
size = Size30dp,
|
|
pictureModifier = MaterialTheme.colors.repostProfileBorder
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun DisplayHighlight(
|
|
highlight: String,
|
|
authorHex: String?,
|
|
url: String?,
|
|
postAddress: ATag?,
|
|
makeItShort: Boolean,
|
|
canPreview: Boolean,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val quote =
|
|
remember {
|
|
highlight
|
|
.split("\n")
|
|
.map { "> *${it.removeSuffix(" ")}*" }
|
|
.joinToString("\n")
|
|
}
|
|
|
|
TranslatableRichTextViewer(
|
|
quote,
|
|
canPreview = canPreview && !makeItShort,
|
|
remember { Modifier.fillMaxWidth() },
|
|
remember { ImmutableListOfLists<String>(emptyList()) },
|
|
backgroundColor,
|
|
accountViewModel,
|
|
nav
|
|
)
|
|
|
|
DisplayQuoteAuthor(authorHex ?: "", url, postAddress, accountViewModel, nav)
|
|
}
|
|
|
|
@Composable
|
|
private fun DisplayQuoteAuthor(
|
|
authorHex: String,
|
|
url: String?,
|
|
postAddress: ATag?,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
var userBase by remember { mutableStateOf<User?>(LocalCache.getUserIfExists(authorHex)) }
|
|
|
|
LaunchedEffect(Unit) {
|
|
if (userBase == null) {
|
|
launch(Dispatchers.IO) {
|
|
val newUserBase = LocalCache.checkGetOrCreateUser(authorHex)
|
|
launch(Dispatchers.Main) {
|
|
userBase = newUserBase
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
MeasureSpaceWidth {
|
|
Row(horizontalArrangement = Arrangement.spacedBy(it), verticalAlignment = Alignment.CenterVertically) {
|
|
userBase?.let { userBase ->
|
|
LoadAndDisplayUser(userBase, nav)
|
|
}
|
|
|
|
url?.let { url ->
|
|
LoadAndDisplayUrl(url)
|
|
}
|
|
|
|
postAddress?.let { address ->
|
|
LoadAndDisplayPost(address, accountViewModel, nav)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun LoadAndDisplayPost(postAddress: ATag, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
|
LoadAddressableNote(aTag = postAddress) {
|
|
it?.let { note ->
|
|
val noteEvent by note.live().metadata.map {
|
|
it.note.event
|
|
}.distinctUntilChanged().observeAsState(note.event)
|
|
|
|
val title = remember(noteEvent) {
|
|
(noteEvent as? LongTextNoteEvent)?.title()
|
|
}
|
|
|
|
title?.let {
|
|
Text(remember { "-" }, maxLines = 1)
|
|
ClickableText(
|
|
text = AnnotatedString(title),
|
|
onClick = {
|
|
routeFor(note, accountViewModel.userProfile())?.let {
|
|
nav(it)
|
|
}
|
|
},
|
|
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun LoadAndDisplayUrl(url: String) {
|
|
val validatedUrl = remember {
|
|
try {
|
|
URL(url)
|
|
} catch (e: Exception) {
|
|
Log.w("Note Compose", "Invalid URI: $url")
|
|
null
|
|
}
|
|
}
|
|
|
|
validatedUrl?.host?.let { host ->
|
|
Text(remember { "-" }, maxLines = 1)
|
|
ClickableUrl(urlText = host, url = url)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun LoadAndDisplayUser(
|
|
userBase: User,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val route = remember { "User/${userBase.pubkeyHex}" }
|
|
|
|
val userState by userBase.live().metadata.observeAsState()
|
|
val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() }
|
|
val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() }
|
|
|
|
if (userDisplayName != null) {
|
|
CreateClickableTextWithEmoji(
|
|
clickablePart = userDisplayName,
|
|
suffix = " ",
|
|
maxLines = 1,
|
|
route = route,
|
|
nav = nav,
|
|
tags = userTags
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun DisplayFollowingCommunityInPost(
|
|
baseNote: Note,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
Column(HalfStartPadding) {
|
|
Row(verticalAlignment = CenterVertically) {
|
|
DisplayCommunity(baseNote, nav)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun DisplayFollowingHashtagsInPost(
|
|
baseNote: Note,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val noteEvent = remember { baseNote.event } ?: return
|
|
|
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
|
var firstTag by remember { mutableStateOf<String?>(null) }
|
|
|
|
LaunchedEffect(key1 = accountState) {
|
|
launch(Dispatchers.Default) {
|
|
val followingTags = accountState?.account?.followingTagSet() ?: emptySet()
|
|
val newFirstTag = noteEvent.firstIsTaggedHashes(followingTags)
|
|
|
|
if (firstTag != newFirstTag) {
|
|
launch(Dispatchers.Main) {
|
|
firstTag = newFirstTag
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
firstTag?.let {
|
|
Column() {
|
|
Row(verticalAlignment = CenterVertically) {
|
|
DisplayTagList(it, nav)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun DisplayTagList(firstTag: String, nav: (String) -> Unit) {
|
|
val displayTag = remember(firstTag) { AnnotatedString(" #$firstTag") }
|
|
val route = remember(firstTag) { "Hashtag/$firstTag" }
|
|
|
|
ClickableText(
|
|
text = displayTag,
|
|
onClick = { nav(route) },
|
|
style = LocalTextStyle.current.copy(
|
|
color = MaterialTheme.colors.primary.copy(
|
|
alpha = 0.52f
|
|
)
|
|
),
|
|
maxLines = 1
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
private fun DisplayCommunity(note: Note, nav: (String) -> Unit) {
|
|
val communityTag = remember(note) {
|
|
note.event?.getTagOfAddressableKind(CommunityDefinitionEvent.kind)
|
|
} ?: return
|
|
|
|
val displayTag = remember(note) { AnnotatedString(getCommunityShortName(communityTag)) }
|
|
val route = remember(note) { "Community/${communityTag.toTag()}" }
|
|
|
|
ClickableText(
|
|
text = displayTag,
|
|
onClick = { nav(route) },
|
|
style = LocalTextStyle.current.copy(
|
|
color = MaterialTheme.colors.primary.copy(
|
|
alpha = 0.52f
|
|
)
|
|
),
|
|
maxLines = 1
|
|
)
|
|
}
|
|
|
|
private fun getCommunityShortName(communityTag: ATag): String {
|
|
val name = if (communityTag.dTag.length > 10) {
|
|
communityTag.dTag.take(10) + "..."
|
|
} else {
|
|
communityTag.dTag.take(10)
|
|
}
|
|
|
|
return "/n/$name"
|
|
}
|
|
|
|
@OptIn(ExperimentalLayoutApi::class)
|
|
@Composable
|
|
fun DisplayUncitedHashtags(
|
|
hashtags: ImmutableList<String>,
|
|
eventContent: String,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val hasHashtags = remember {
|
|
hashtags.isNotEmpty()
|
|
}
|
|
|
|
if (hasHashtags) {
|
|
FlowRow(
|
|
modifier = remember { Modifier.padding(top = 5.dp) }
|
|
) {
|
|
hashtags.forEach { hashtag ->
|
|
if (!eventContent.contains(hashtag, true)) {
|
|
ClickableText(
|
|
text = AnnotatedString("#$hashtag "),
|
|
onClick = { nav("Hashtag/$hashtag") },
|
|
style = LocalTextStyle.current.copy(
|
|
color = MaterialTheme.colors.primary.copy(
|
|
alpha = 0.52f
|
|
)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun DisplayPoW(
|
|
pow: Int
|
|
) {
|
|
val powStr = remember(pow) {
|
|
"PoW-$pow"
|
|
}
|
|
|
|
Text(
|
|
powStr,
|
|
color = MaterialTheme.colors.lessImportantLink,
|
|
fontSize = Font14SP,
|
|
fontWeight = FontWeight.Bold,
|
|
maxLines = 1
|
|
)
|
|
}
|
|
|
|
@Stable
|
|
data class Reward(val amount: BigDecimal)
|
|
|
|
@Composable
|
|
fun DisplayReward(
|
|
baseReward: Reward,
|
|
baseNote: Note,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
var popupExpanded by remember { mutableStateOf(false) }
|
|
|
|
Column() {
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
modifier = Modifier.clickable { popupExpanded = true }
|
|
) {
|
|
ClickableText(
|
|
text = AnnotatedString("#bounty"),
|
|
onClick = { nav("Hashtag/bounty") },
|
|
style = LocalTextStyle.current.copy(
|
|
color = MaterialTheme.colors.primary.copy(
|
|
alpha = 0.52f
|
|
)
|
|
)
|
|
)
|
|
|
|
RenderPledgeAmount(baseNote, baseReward, accountViewModel)
|
|
}
|
|
|
|
if (popupExpanded) {
|
|
AddBountyAmountDialog(baseNote, accountViewModel) {
|
|
popupExpanded = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun RenderPledgeAmount(
|
|
baseNote: Note,
|
|
baseReward: Reward,
|
|
accountViewModel: AccountViewModel
|
|
) {
|
|
val repliesState by baseNote.live().replies.observeAsState()
|
|
var reward by remember {
|
|
mutableStateOf<String>(
|
|
showAmount(baseReward.amount)
|
|
)
|
|
}
|
|
|
|
var hasPledge by remember {
|
|
mutableStateOf<Boolean>(
|
|
false
|
|
)
|
|
}
|
|
|
|
LaunchedEffect(key1 = repliesState) {
|
|
launch(Dispatchers.IO) {
|
|
repliesState?.note?.pledgedAmountByOthers()?.let {
|
|
val newRewardAmount = showAmount(baseReward.amount.add(it))
|
|
if (newRewardAmount != reward) {
|
|
reward = newRewardAmount
|
|
}
|
|
}
|
|
val newHasPledge = repliesState?.note?.hasPledgeBy(accountViewModel.userProfile()) == true
|
|
if (hasPledge != newHasPledge) {
|
|
launch(Dispatchers.Main) {
|
|
hasPledge = newHasPledge
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hasPledge) {
|
|
ZappedIcon(modifier = Size20Modifier)
|
|
} else {
|
|
ZapIcon(modifier = Size20Modifier, MaterialTheme.colors.placeholderText)
|
|
}
|
|
|
|
Text(
|
|
text = reward,
|
|
color = MaterialTheme.colors.placeholderText,
|
|
maxLines = 1
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
fun BadgeDisplay(baseNote: Note) {
|
|
val background = MaterialTheme.colors.background
|
|
val badgeData = baseNote.event as? BadgeDefinitionEvent ?: return
|
|
|
|
val image = remember { badgeData.thumb()?.ifBlank { null } ?: badgeData.image() }
|
|
val name = remember { badgeData.name() }
|
|
val description = remember { badgeData.description() }
|
|
|
|
var backgroundFromImage by remember { mutableStateOf(Pair(background, background)) }
|
|
var imageResult by remember { mutableStateOf<SuccessResult?>(null) }
|
|
|
|
LaunchedEffect(key1 = imageResult) {
|
|
launch(Dispatchers.IO) {
|
|
imageResult?.let {
|
|
val backgroundColor = it.drawable.toBitmap(200, 200).copy(Bitmap.Config.ARGB_8888, false).get(0, 199)
|
|
val colorFromImage = Color(backgroundColor)
|
|
val textBackground = if (colorFromImage.luminance() > 0.5) {
|
|
lightColors().onBackground
|
|
} else {
|
|
darkColors().onBackground
|
|
}
|
|
|
|
launch(Dispatchers.Main) {
|
|
backgroundFromImage = Pair(colorFromImage, textBackground)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Row(
|
|
modifier = Modifier
|
|
.padding(10.dp)
|
|
.clip(shape = CutCornerShape(20, 20, 20, 20))
|
|
.border(
|
|
5.dp,
|
|
MaterialTheme.colors.mediumImportanceLink,
|
|
CutCornerShape(20)
|
|
)
|
|
.background(backgroundFromImage.first)
|
|
) {
|
|
RenderBadge(
|
|
image,
|
|
name,
|
|
backgroundFromImage.second,
|
|
description
|
|
) {
|
|
if (imageResult == null) {
|
|
imageResult = it.result
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun RenderBadge(
|
|
image: String?,
|
|
name: String?,
|
|
backgroundFromImage: Color,
|
|
description: String?,
|
|
onSuccess: (AsyncImagePainter.State.Success) -> Unit
|
|
) {
|
|
Column {
|
|
image.let {
|
|
AsyncImage(
|
|
model = it,
|
|
contentDescription = stringResource(
|
|
R.string.badge_award_image_for,
|
|
name ?: ""
|
|
),
|
|
contentScale = ContentScale.FillWidth,
|
|
modifier = Modifier.fillMaxWidth(),
|
|
onSuccess = onSuccess
|
|
)
|
|
}
|
|
|
|
name?.let {
|
|
Text(
|
|
text = it,
|
|
style = MaterialTheme.typography.body1,
|
|
textAlign = TextAlign.Center,
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(start = 10.dp, end = 10.dp),
|
|
color = backgroundFromImage
|
|
)
|
|
}
|
|
|
|
description?.let {
|
|
Text(
|
|
text = it,
|
|
style = MaterialTheme.typography.caption,
|
|
textAlign = TextAlign.Center,
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
|
|
color = Color.Gray,
|
|
maxLines = 3,
|
|
overflow = TextOverflow.Ellipsis
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun FileHeaderDisplay(note: Note, accountViewModel: AccountViewModel) {
|
|
val event = (note.event as? FileHeaderEvent) ?: return
|
|
val fullUrl = event.url() ?: return
|
|
|
|
var content by remember { mutableStateOf<ZoomableContent?>(null) }
|
|
|
|
if (content == null) {
|
|
LaunchedEffect(key1 = event.id) {
|
|
launch(Dispatchers.IO) {
|
|
val blurHash = event.blurhash()
|
|
val hash = event.hash()
|
|
val dimensions = event.dimensions()
|
|
val description = event.content
|
|
val removedParamsFromUrl = fullUrl.split("?")[0].lowercase()
|
|
val isImage = imageExtensions.any { removedParamsFromUrl.endsWith(it) }
|
|
val uri = "nostr:" + note.toNEvent()
|
|
val newContent = if (isImage) {
|
|
ZoomableUrlImage(fullUrl, description, hash, blurHash, dimensions, uri)
|
|
} else {
|
|
ZoomableUrlVideo(fullUrl, description, hash, uri)
|
|
}
|
|
|
|
launch(Dispatchers.Main) {
|
|
content = newContent
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Crossfade(targetState = content) {
|
|
if (it != null) {
|
|
SensitivityWarning(note = note, accountViewModel = accountViewModel) {
|
|
ZoomableContentView(content = it, accountViewModel = accountViewModel)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun FileStorageHeaderDisplay(baseNote: Note, accountViewModel: AccountViewModel) {
|
|
val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return
|
|
|
|
var fileNote by remember { mutableStateOf<Note?>(null) }
|
|
|
|
if (fileNote == null) {
|
|
LaunchedEffect(key1 = eventHeader.id) {
|
|
launch(Dispatchers.IO) {
|
|
val newFileNote = eventHeader.dataEventId()?.let { LocalCache.checkGetOrCreateNote(it) }
|
|
launch(Dispatchers.Main) {
|
|
fileNote = newFileNote
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Crossfade(targetState = fileNote) {
|
|
if (it != null) {
|
|
RenderNIP95(it, eventHeader, baseNote, accountViewModel)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun RenderNIP95(
|
|
content: Note,
|
|
eventHeader: FileStorageHeaderEvent,
|
|
header: Note,
|
|
accountViewModel: AccountViewModel
|
|
) {
|
|
val appContext = LocalContext.current.applicationContext
|
|
|
|
val noteState by content.live().metadata.observeAsState()
|
|
val note = remember(noteState) { noteState?.note }
|
|
|
|
var content by remember { mutableStateOf<ZoomableContent?>(null) }
|
|
|
|
if (content == null) {
|
|
LaunchedEffect(key1 = eventHeader.id, key2 = noteState, key3 = note?.event) {
|
|
launch(Dispatchers.IO) {
|
|
val uri = "nostr:" + header.toNEvent()
|
|
val localDir =
|
|
note?.idHex?.let { File(File(appContext.externalCacheDir, "NIP95"), it) }
|
|
val blurHash = eventHeader.blurhash()
|
|
val dimensions = eventHeader.dimensions()
|
|
val description = eventHeader.content
|
|
val mimeType = eventHeader.mimeType()
|
|
|
|
val newContent = if (mimeType?.startsWith("image") == true) {
|
|
ZoomableLocalImage(
|
|
localFile = localDir,
|
|
mimeType = mimeType,
|
|
description = description,
|
|
blurhash = blurHash,
|
|
dim = dimensions,
|
|
isVerified = true,
|
|
uri = uri
|
|
)
|
|
} else {
|
|
ZoomableLocalVideo(
|
|
localFile = localDir,
|
|
mimeType = mimeType,
|
|
description = description,
|
|
dim = dimensions,
|
|
isVerified = true,
|
|
uri = uri
|
|
)
|
|
}
|
|
|
|
launch(Dispatchers.Main) {
|
|
content = newContent
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Crossfade(targetState = content) {
|
|
if (it != null) {
|
|
SensitivityWarning(note = header, accountViewModel = accountViewModel) {
|
|
ZoomableContentView(content = it, accountViewModel = accountViewModel)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun AudioTrackHeader(noteEvent: AudioTrackEvent, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
|
val media = remember { noteEvent.media() }
|
|
val cover = remember { noteEvent.cover() }
|
|
val subject = remember { noteEvent.subject() }
|
|
val content = remember { noteEvent.content() }
|
|
val participants = remember { noteEvent.participants() }
|
|
|
|
var participantUsers by remember { mutableStateOf<List<Pair<Participant, User>>>(emptyList()) }
|
|
|
|
LaunchedEffect(key1 = participants) {
|
|
launch(Dispatchers.IO) {
|
|
participantUsers = participants.mapNotNull { part -> LocalCache.checkGetOrCreateUser(part.key)?.let { Pair(part, it) } }
|
|
}
|
|
}
|
|
|
|
Row(modifier = Modifier.padding(top = 5.dp)) {
|
|
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
|
Row() {
|
|
subject?.let {
|
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)) {
|
|
Text(
|
|
text = it,
|
|
fontWeight = FontWeight.Bold,
|
|
maxLines = 3,
|
|
overflow = TextOverflow.Ellipsis,
|
|
modifier = Modifier.fillMaxWidth()
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
participantUsers.forEach {
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
modifier = Modifier
|
|
.padding(top = 5.dp, start = 10.dp, end = 10.dp)
|
|
.clickable {
|
|
nav("User/${it.second.pubkeyHex}")
|
|
}
|
|
) {
|
|
ClickableUserPicture(it.second, 25.dp, accountViewModel)
|
|
Spacer(Modifier.width(5.dp))
|
|
UsernameDisplay(it.second, Modifier.weight(1f))
|
|
Spacer(Modifier.width(5.dp))
|
|
it.first.role?.let {
|
|
Text(
|
|
text = it.capitalize(Locale.ROOT),
|
|
color = MaterialTheme.colors.placeholderText,
|
|
maxLines = 1
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
media?.let { media ->
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
modifier = Modifier.padding(10.dp)
|
|
) {
|
|
cover?.let { cover ->
|
|
LoadThumbAndThenVideoView(
|
|
videoUri = media,
|
|
description = noteEvent.subject(),
|
|
thumbUri = cover,
|
|
accountViewModel = accountViewModel
|
|
)
|
|
}
|
|
?: VideoView(
|
|
videoUri = media,
|
|
description = noteEvent.subject(),
|
|
accountViewModel = accountViewModel
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun RenderLiveActivityEvent(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
|
Row(modifier = Modifier.padding(top = 5.dp)) {
|
|
Column(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalAlignment = Alignment.CenterHorizontally
|
|
) {
|
|
RenderLiveActivityEventInner(baseNote = baseNote, accountViewModel, nav)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun RenderLiveActivityEventInner(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
|
val noteEvent = baseNote.event as? LiveActivitiesEvent ?: return
|
|
|
|
val eventUpdates by baseNote.live().metadata.observeAsState()
|
|
|
|
val media = remember(eventUpdates) { noteEvent.streaming() }
|
|
val cover = remember(eventUpdates) { noteEvent.image() }
|
|
val subject = remember(eventUpdates) { noteEvent.title() }
|
|
val content = remember(eventUpdates) { noteEvent.summary() }
|
|
val participants = remember(eventUpdates) { noteEvent.participants() }
|
|
val status = remember(eventUpdates) { noteEvent.status() }
|
|
val starts = remember(eventUpdates) { noteEvent.starts() }
|
|
|
|
var isOnline by remember { mutableStateOf(false) }
|
|
|
|
LaunchedEffect(key1 = media) {
|
|
launch(Dispatchers.IO) {
|
|
isOnline = OnlineChecker.isOnline(media)
|
|
}
|
|
}
|
|
|
|
Row(
|
|
verticalAlignment = CenterVertically,
|
|
modifier = Modifier
|
|
.padding(vertical = 5.dp)
|
|
.fillMaxWidth()
|
|
) {
|
|
subject?.let {
|
|
Text(
|
|
text = it,
|
|
fontWeight = FontWeight.Bold,
|
|
maxLines = 3,
|
|
overflow = TextOverflow.Ellipsis,
|
|
modifier = Modifier.weight(1f)
|
|
)
|
|
}
|
|
|
|
Spacer(modifier = StdHorzSpacer)
|
|
|
|
Crossfade(targetState = status) {
|
|
when (it) {
|
|
STATUS_LIVE -> {
|
|
if (isOnline) {
|
|
LiveFlag()
|
|
}
|
|
}
|
|
STATUS_PLANNED -> {
|
|
ScheduledFlag(starts)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var participantUsers by remember {
|
|
mutableStateOf<ImmutableList<Pair<Participant, User>>>(
|
|
persistentListOf()
|
|
)
|
|
}
|
|
|
|
LaunchedEffect(key1 = eventUpdates) {
|
|
launch(Dispatchers.IO) {
|
|
val newParticipantUsers = participants.mapNotNull { part ->
|
|
LocalCache.checkGetOrCreateUser(part.key)?.let { Pair(part, it) }
|
|
}.toImmutableList()
|
|
|
|
if (!equalImmutableLists(newParticipantUsers, participantUsers)) {
|
|
participantUsers = newParticipantUsers
|
|
}
|
|
}
|
|
}
|
|
|
|
participantUsers.forEach {
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
modifier = Modifier
|
|
.padding(top = 5.dp, start = 10.dp, end = 10.dp)
|
|
.clickable {
|
|
nav("User/${it.second.pubkeyHex}")
|
|
}
|
|
) {
|
|
ClickableUserPicture(it.second, 25.dp, accountViewModel)
|
|
Spacer(Modifier.width(5.dp))
|
|
UsernameDisplay(it.second, Modifier.weight(1f))
|
|
Spacer(Modifier.width(5.dp))
|
|
it.first.role?.let {
|
|
Text(
|
|
text = it.capitalize(Locale.ROOT),
|
|
color = MaterialTheme.colors.placeholderText,
|
|
maxLines = 1
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
media?.let { media ->
|
|
if (status == STATUS_LIVE) {
|
|
if (isOnline) {
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
modifier = Modifier.padding(10.dp)
|
|
) {
|
|
VideoView(
|
|
videoUri = media,
|
|
description = subject,
|
|
accountViewModel = accountViewModel
|
|
)
|
|
}
|
|
} else {
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
modifier = Modifier
|
|
.padding(10.dp)
|
|
.height(100.dp)
|
|
) {
|
|
Text(
|
|
text = stringResource(id = R.string.live_stream_is_offline),
|
|
color = MaterialTheme.colors.onBackground,
|
|
fontWeight = FontWeight.Bold
|
|
)
|
|
}
|
|
}
|
|
} else if (status == "ended") {
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
modifier = Modifier
|
|
.padding(10.dp)
|
|
.height(100.dp)
|
|
) {
|
|
Text(
|
|
text = stringResource(id = R.string.live_stream_has_ended),
|
|
color = MaterialTheme.colors.onBackground,
|
|
fontWeight = FontWeight.Bold
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, accountViewModel: AccountViewModel) {
|
|
val image = remember(noteEvent) { noteEvent.image() }
|
|
val title = remember(noteEvent) { noteEvent.title() }
|
|
val summary = remember(noteEvent) { noteEvent.summary() ?: noteEvent.content.take(200).ifBlank { null } }
|
|
|
|
Row(
|
|
modifier = Modifier
|
|
.clip(shape = QuoteBorder)
|
|
.border(
|
|
1.dp,
|
|
MaterialTheme.colors.subtleBorder,
|
|
QuoteBorder
|
|
)
|
|
) {
|
|
Column {
|
|
val settings = accountViewModel.account.settings
|
|
val isMobile = ConnectivityStatus.isOnMobileData.value
|
|
|
|
val automaticallyShowUrlPreview = when (settings.automaticallyShowUrlPreview) {
|
|
true -> !isMobile
|
|
false -> false
|
|
else -> true
|
|
}
|
|
if (automaticallyShowUrlPreview) {
|
|
image?.let {
|
|
AsyncImage(
|
|
model = it,
|
|
contentDescription = stringResource(
|
|
R.string.preview_card_image_for,
|
|
it
|
|
),
|
|
contentScale = ContentScale.FillWidth,
|
|
modifier = Modifier.fillMaxWidth()
|
|
)
|
|
} ?: CreateImageHeader(note, accountViewModel)
|
|
}
|
|
|
|
title?.let {
|
|
Text(
|
|
text = it,
|
|
style = MaterialTheme.typography.body1,
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(start = 10.dp, end = 10.dp, top = 10.dp)
|
|
)
|
|
}
|
|
|
|
summary?.let {
|
|
Text(
|
|
text = it,
|
|
style = MaterialTheme.typography.caption,
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
|
|
color = Color.Gray,
|
|
maxLines = 3,
|
|
overflow = TextOverflow.Ellipsis
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun RenderClassifieds(noteEvent: ClassifiedsEvent, note: Note, accountViewModel: AccountViewModel) {
|
|
val image = remember(noteEvent) { noteEvent.image() }
|
|
val title = remember(noteEvent) { noteEvent.title() }
|
|
val summary = remember(noteEvent) { noteEvent.summary() ?: noteEvent.content.take(200).ifBlank { null } }
|
|
val price = remember(noteEvent) { noteEvent.price() }
|
|
val location = remember(noteEvent) { noteEvent.location() }
|
|
|
|
Row(
|
|
modifier = Modifier
|
|
.clip(shape = QuoteBorder)
|
|
.border(
|
|
1.dp,
|
|
MaterialTheme.colors.subtleBorder,
|
|
QuoteBorder
|
|
)
|
|
) {
|
|
Column {
|
|
Row() {
|
|
image?.let {
|
|
AsyncImage(
|
|
model = it,
|
|
contentDescription = stringResource(
|
|
R.string.preview_card_image_for,
|
|
it
|
|
),
|
|
contentScale = ContentScale.FillWidth,
|
|
modifier = Modifier.fillMaxWidth()
|
|
)
|
|
} ?: CreateImageHeader(note, accountViewModel)
|
|
}
|
|
|
|
Row(Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
title?.let {
|
|
Text(
|
|
text = it,
|
|
style = MaterialTheme.typography.body1,
|
|
maxLines = 1,
|
|
modifier = Modifier.weight(1f)
|
|
)
|
|
}
|
|
|
|
price?.let {
|
|
val priceTag = remember(noteEvent) {
|
|
if (price.frequency != null && price.currency != null) {
|
|
"${price.amount} ${price.currency}/${price.frequency}"
|
|
} else if (price.currency != null) {
|
|
"${price.amount} ${price.currency}"
|
|
} else {
|
|
price.amount
|
|
}
|
|
}
|
|
|
|
Text(
|
|
text = priceTag,
|
|
maxLines = 1,
|
|
color = MaterialTheme.colors.primary,
|
|
fontWeight = FontWeight.Bold,
|
|
modifier = remember {
|
|
Modifier
|
|
.clip(SmallBorder)
|
|
.background(Color.Black)
|
|
.padding(start = 5.dp)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
if (summary != null || location != null) {
|
|
Row(Modifier.padding(start = 10.dp, end = 10.dp, top = 5.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
summary?.let {
|
|
Text(
|
|
text = it,
|
|
style = MaterialTheme.typography.caption,
|
|
modifier = Modifier
|
|
.weight(1f),
|
|
color = Color.Gray,
|
|
maxLines = 3,
|
|
overflow = TextOverflow.Ellipsis
|
|
)
|
|
}
|
|
|
|
location?.let {
|
|
Text(
|
|
text = it,
|
|
style = MaterialTheme.typography.caption,
|
|
color = Color.Gray,
|
|
maxLines = 1,
|
|
overflow = TextOverflow.Ellipsis,
|
|
modifier = Modifier.padding(start = 5.dp)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
Spacer(modifier = DoubleVertSpacer)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun CreateImageHeader(
|
|
note: Note,
|
|
accountViewModel: AccountViewModel
|
|
) {
|
|
val banner = remember(note.author?.info) { note.author?.info?.banner }
|
|
|
|
Box() {
|
|
banner?.let {
|
|
AsyncImage(
|
|
model = it,
|
|
contentDescription = stringResource(
|
|
R.string.preview_card_image_for,
|
|
it
|
|
),
|
|
contentScale = ContentScale.FillWidth,
|
|
modifier = Modifier.fillMaxWidth()
|
|
)
|
|
} ?: Image(
|
|
painter = painterResource(R.drawable.profile_banner),
|
|
contentDescription = stringResource(R.string.profile_banner),
|
|
contentScale = ContentScale.FillWidth,
|
|
modifier = remember {
|
|
Modifier
|
|
.fillMaxWidth()
|
|
.height(150.dp)
|
|
}
|
|
)
|
|
|
|
Box(
|
|
remember {
|
|
Modifier
|
|
.width(75.dp)
|
|
.height(75.dp)
|
|
.padding(10.dp)
|
|
.align(Alignment.BottomStart)
|
|
}
|
|
) {
|
|
NoteAuthorPicture(baseNote = note, accountViewModel = accountViewModel, size = Size55dp)
|
|
}
|
|
}
|
|
}
|