2023-01-11 18:31:20 +00:00
|
|
|
package com.vitorpamplona.amethyst.ui.note
|
|
|
|
|
2023-03-09 14:34:25 +00:00
|
|
|
import android.content.Intent
|
2023-03-16 20:32:42 +00:00
|
|
|
import android.graphics.Bitmap
|
2023-03-28 21:11:38 +00:00
|
|
|
import android.util.Log
|
2023-02-22 15:26:05 +00:00
|
|
|
import androidx.compose.foundation.*
|
|
|
|
import androidx.compose.foundation.layout.*
|
2023-01-11 18:31:20 +00:00
|
|
|
import androidx.compose.foundation.shape.CircleShape
|
2023-03-05 23:34:11 +00:00
|
|
|
import androidx.compose.foundation.shape.CutCornerShape
|
2023-02-27 22:14:15 +00:00
|
|
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
2023-03-16 18:20:30 +00:00
|
|
|
import androidx.compose.foundation.text.ClickableText
|
2023-02-22 15:26:05 +00:00
|
|
|
import androidx.compose.material.*
|
2023-02-06 22:16:27 +00:00
|
|
|
import androidx.compose.material.icons.Icons
|
2023-03-23 14:49:01 +00:00
|
|
|
import androidx.compose.material.icons.filled.Bolt
|
2023-02-06 22:16:27 +00:00
|
|
|
import androidx.compose.material.icons.filled.ExpandMore
|
|
|
|
import androidx.compose.material.icons.filled.MoreVert
|
2023-02-22 15:26:05 +00:00
|
|
|
import androidx.compose.runtime.*
|
2023-01-11 18:31:20 +00:00
|
|
|
import androidx.compose.runtime.livedata.observeAsState
|
|
|
|
import androidx.compose.ui.Alignment
|
|
|
|
import androidx.compose.ui.Modifier
|
|
|
|
import androidx.compose.ui.draw.clip
|
2023-02-25 23:00:18 +00:00
|
|
|
import androidx.compose.ui.graphics.Color
|
2023-02-06 22:16:27 +00:00
|
|
|
import androidx.compose.ui.graphics.ColorFilter
|
|
|
|
import androidx.compose.ui.graphics.ColorMatrix
|
2023-02-25 23:00:18 +00:00
|
|
|
import androidx.compose.ui.graphics.compositeOver
|
2023-03-16 20:32:42 +00:00
|
|
|
import androidx.compose.ui.graphics.luminance
|
2023-03-02 00:18:07 +00:00
|
|
|
import androidx.compose.ui.layout.ContentScale
|
2023-01-13 15:20:54 +00:00
|
|
|
import androidx.compose.ui.platform.LocalClipboardManager
|
2023-01-24 19:59:21 +00:00
|
|
|
import androidx.compose.ui.platform.LocalContext
|
2023-02-06 22:16:27 +00:00
|
|
|
import androidx.compose.ui.platform.LocalUriHandler
|
2023-01-26 16:16:57 +00:00
|
|
|
import androidx.compose.ui.res.painterResource
|
2023-02-28 18:20:49 +00:00
|
|
|
import androidx.compose.ui.res.stringResource
|
2023-03-09 14:34:25 +00:00
|
|
|
import androidx.compose.ui.text.AnnotatedString
|
2023-01-11 18:31:20 +00:00
|
|
|
import androidx.compose.ui.text.font.FontWeight
|
2023-03-05 23:34:11 +00:00
|
|
|
import androidx.compose.ui.text.style.TextAlign
|
2023-03-02 00:18:07 +00:00
|
|
|
import androidx.compose.ui.text.style.TextOverflow
|
2023-01-26 16:16:57 +00:00
|
|
|
import androidx.compose.ui.unit.Dp
|
2023-01-11 18:31:20 +00:00
|
|
|
import androidx.compose.ui.unit.dp
|
2023-03-28 12:46:07 +00:00
|
|
|
import androidx.compose.ui.unit.sp
|
2023-03-09 14:34:25 +00:00
|
|
|
import androidx.core.content.ContextCompat
|
2023-03-16 20:32:42 +00:00
|
|
|
import androidx.core.graphics.drawable.toBitmap
|
|
|
|
import androidx.core.graphics.get
|
2023-01-12 17:47:31 +00:00
|
|
|
import androidx.navigation.NavController
|
2023-01-11 18:31:20 +00:00
|
|
|
import coil.compose.AsyncImage
|
2023-05-06 23:19:57 +00:00
|
|
|
import coil.compose.AsyncImagePainter
|
2023-02-06 22:16:27 +00:00
|
|
|
import com.google.accompanist.flowlayout.FlowRow
|
2023-01-27 01:09:56 +00:00
|
|
|
import com.vitorpamplona.amethyst.NotificationCache
|
2023-01-26 16:16:57 +00:00
|
|
|
import com.vitorpamplona.amethyst.R
|
2023-03-23 14:49:01 +00:00
|
|
|
import com.vitorpamplona.amethyst.model.Account
|
2023-05-05 01:57:19 +00:00
|
|
|
import com.vitorpamplona.amethyst.model.Channel
|
2023-03-08 22:18:25 +00:00
|
|
|
import com.vitorpamplona.amethyst.model.LocalCache
|
2023-01-11 18:31:20 +00:00
|
|
|
import com.vitorpamplona.amethyst.model.Note
|
2023-01-26 16:16:57 +00:00
|
|
|
import com.vitorpamplona.amethyst.model.User
|
2023-03-20 08:38:14 +00:00
|
|
|
import com.vitorpamplona.amethyst.service.model.*
|
2023-03-29 07:00:15 +00:00
|
|
|
import com.vitorpamplona.amethyst.ui.components.*
|
2023-01-11 18:31:20 +00:00
|
|
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
2023-03-03 21:00:47 +00:00
|
|
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
2023-03-14 17:41:39 +00:00
|
|
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
|
2023-03-23 14:49:01 +00:00
|
|
|
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
2023-03-08 22:07:56 +00:00
|
|
|
import com.vitorpamplona.amethyst.ui.theme.Following
|
2023-03-10 14:37:17 +00:00
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
|
import kotlinx.coroutines.withContext
|
2023-05-01 20:42:58 +00:00
|
|
|
import java.io.File
|
2023-03-23 14:49:01 +00:00
|
|
|
import java.math.BigDecimal
|
2023-04-28 22:40:12 +00:00
|
|
|
import java.net.URL
|
2023-03-28 21:11:38 +00:00
|
|
|
import kotlin.time.ExperimentalTime
|
|
|
|
import kotlin.time.measureTimedValue
|
2023-01-11 18:31:20 +00:00
|
|
|
|
2023-04-07 20:58:25 +00:00
|
|
|
@OptIn(ExperimentalTime::class)
|
2023-01-11 18:31:20 +00:00
|
|
|
@Composable
|
2023-01-27 01:09:56 +00:00
|
|
|
fun NoteCompose(
|
|
|
|
baseNote: Note,
|
|
|
|
routeForLastRead: String? = null,
|
|
|
|
modifier: Modifier = Modifier,
|
2023-02-20 23:09:57 +00:00
|
|
|
isBoostedNote: Boolean = false,
|
|
|
|
isQuotedNote: Boolean = false,
|
2023-02-27 22:14:15 +00:00
|
|
|
unPackReply: Boolean = true,
|
|
|
|
makeItShort: Boolean = false,
|
2023-03-09 23:55:57 +00:00
|
|
|
addMarginTop: Boolean = true,
|
2023-02-25 23:00:18 +00:00
|
|
|
parentBackgroundColor: Color? = null,
|
2023-01-27 01:09:56 +00:00
|
|
|
accountViewModel: AccountViewModel,
|
|
|
|
navController: NavController
|
2023-03-28 21:11:38 +00:00
|
|
|
) {
|
|
|
|
val (value, elapsed) = measureTimedValue {
|
|
|
|
NoteComposeInner(
|
|
|
|
baseNote,
|
|
|
|
routeForLastRead,
|
|
|
|
modifier,
|
|
|
|
isBoostedNote,
|
|
|
|
isQuotedNote,
|
|
|
|
unPackReply,
|
|
|
|
makeItShort,
|
|
|
|
addMarginTop,
|
|
|
|
parentBackgroundColor,
|
|
|
|
accountViewModel,
|
|
|
|
navController
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-04-18 12:46:12 +00:00
|
|
|
Log.d("Time", "Note Compose in $elapsed for ${baseNote.idHex} ${baseNote.event?.kind()} ${baseNote.event?.content()?.split("\n")?.get(0)?.take(100)}")
|
2023-03-28 21:11:38 +00:00
|
|
|
}
|
|
|
|
|
2023-05-06 23:19:57 +00:00
|
|
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalTime::class)
|
2023-03-28 21:11:38 +00:00
|
|
|
@Composable
|
|
|
|
fun NoteComposeInner(
|
|
|
|
baseNote: Note,
|
|
|
|
routeForLastRead: String? = null,
|
|
|
|
modifier: Modifier = Modifier,
|
|
|
|
isBoostedNote: Boolean = false,
|
|
|
|
isQuotedNote: Boolean = false,
|
|
|
|
unPackReply: Boolean = true,
|
|
|
|
makeItShort: Boolean = false,
|
|
|
|
addMarginTop: Boolean = true,
|
|
|
|
parentBackgroundColor: Color? = null,
|
|
|
|
accountViewModel: AccountViewModel,
|
|
|
|
navController: NavController
|
2023-01-27 01:09:56 +00:00
|
|
|
) {
|
2023-01-24 19:59:21 +00:00
|
|
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
2023-01-26 16:16:57 +00:00
|
|
|
val account = accountState?.account ?: return
|
2023-04-07 21:21:21 +00:00
|
|
|
val loggedIn = account.userProfile()
|
2023-01-24 19:59:21 +00:00
|
|
|
|
2023-02-19 16:22:01 +00:00
|
|
|
val noteState by baseNote.live().metadata.observeAsState()
|
2023-01-11 18:31:20 +00:00
|
|
|
val note = noteState?.note
|
|
|
|
|
2023-02-19 16:22:01 +00:00
|
|
|
val noteReportsState by baseNote.live().reports.observeAsState()
|
2023-01-30 01:06:48 +00:00
|
|
|
val noteForReports = noteReportsState?.note ?: return
|
|
|
|
|
2023-01-13 15:20:54 +00:00
|
|
|
var popupExpanded by remember { mutableStateOf(false) }
|
2023-02-04 15:41:43 +00:00
|
|
|
var showHiddenNote by remember { mutableStateOf(false) }
|
2023-01-13 15:20:54 +00:00
|
|
|
|
2023-04-07 20:58:25 +00:00
|
|
|
var isAcceptable by remember { mutableStateOf(true) }
|
|
|
|
var canPreview by remember { mutableStateOf(true) }
|
|
|
|
|
|
|
|
LaunchedEffect(key1 = noteReportsState) {
|
|
|
|
withContext(Dispatchers.IO) {
|
2023-04-07 21:21:21 +00:00
|
|
|
canPreview = note?.author === loggedIn ||
|
|
|
|
(note?.author?.let { loggedIn.isFollowingCached(it) } ?: true) ||
|
2023-04-07 20:58:25 +00:00
|
|
|
!noteForReports.hasAnyReports()
|
|
|
|
|
|
|
|
isAcceptable = account.isAcceptable(noteForReports)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-19 00:14:52 +00:00
|
|
|
val noteEvent = note?.event
|
2023-03-03 21:00:47 +00:00
|
|
|
val baseChannel = note?.channel()
|
2023-02-12 23:23:02 +00:00
|
|
|
|
2023-02-19 00:14:52 +00:00
|
|
|
if (noteEvent == null) {
|
2023-03-08 22:07:56 +00:00
|
|
|
BlankNote(
|
|
|
|
modifier.combinedClickable(
|
|
|
|
onClick = { },
|
|
|
|
onLongClick = { popupExpanded = true }
|
|
|
|
),
|
|
|
|
isBoostedNote
|
|
|
|
)
|
2023-04-07 20:58:25 +00:00
|
|
|
} else if (!isAcceptable && !showHiddenNote) {
|
2023-03-21 00:46:25 +00:00
|
|
|
if (!account.isHidden(noteForReports.author!!)) {
|
|
|
|
HiddenNote(
|
|
|
|
account.getRelevantReports(noteForReports),
|
2023-04-07 21:21:21 +00:00
|
|
|
loggedIn,
|
2023-03-21 00:46:25 +00:00
|
|
|
modifier,
|
|
|
|
isBoostedNote,
|
|
|
|
navController,
|
|
|
|
onClick = { showHiddenNote = true }
|
|
|
|
)
|
|
|
|
}
|
2023-03-03 21:00:47 +00:00
|
|
|
} else if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && baseChannel != null) {
|
|
|
|
ChannelHeader(baseChannel = baseChannel, account = account, navController = navController)
|
2023-03-05 23:34:11 +00:00
|
|
|
} else if (noteEvent is BadgeDefinitionEvent) {
|
|
|
|
BadgeDisplay(baseNote = note)
|
2023-04-21 21:01:42 +00:00
|
|
|
} else if (noteEvent is FileHeaderEvent) {
|
|
|
|
FileHeaderDisplay(note)
|
2023-04-26 18:22:49 +00:00
|
|
|
} else if (noteEvent is FileStorageHeaderEvent) {
|
|
|
|
FileStorageHeaderDisplay(note)
|
2023-01-11 18:31:20 +00:00
|
|
|
} else {
|
2023-05-05 14:01:37 +00:00
|
|
|
var isNew by remember { mutableStateOf<Boolean>(false) }
|
2023-01-27 01:09:56 +00:00
|
|
|
|
2023-02-18 18:06:53 +00:00
|
|
|
LaunchedEffect(key1 = routeForLastRead) {
|
2023-03-10 14:37:17 +00:00
|
|
|
withContext(Dispatchers.IO) {
|
|
|
|
routeForLastRead?.let {
|
2023-03-13 17:47:44 +00:00
|
|
|
val lastTime = NotificationCache.load(it)
|
2023-02-18 18:06:53 +00:00
|
|
|
|
2023-03-10 14:37:17 +00:00
|
|
|
val createdAt = note.createdAt()
|
2023-05-05 14:01:37 +00:00
|
|
|
if (createdAt != null) {
|
2023-03-13 17:47:44 +00:00
|
|
|
NotificationCache.markAsRead(it, createdAt)
|
2023-05-05 14:01:37 +00:00
|
|
|
isNew = createdAt > lastTime
|
2023-05-05 01:57:19 +00:00
|
|
|
}
|
|
|
|
}
|
2023-02-25 23:00:18 +00:00
|
|
|
}
|
2023-03-08 22:07:56 +00:00
|
|
|
}
|
|
|
|
|
2023-05-05 14:01:37 +00:00
|
|
|
val backgroundColor = if (isNew) {
|
|
|
|
val newColor = MaterialTheme.colors.primary.copy(0.12f)
|
|
|
|
if (parentBackgroundColor != null) {
|
|
|
|
newColor.compositeOver(parentBackgroundColor)
|
|
|
|
} else {
|
|
|
|
newColor.compositeOver(MaterialTheme.colors.background)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
parentBackgroundColor ?: MaterialTheme.colors.background
|
|
|
|
}
|
|
|
|
|
2023-03-08 22:07:56 +00:00
|
|
|
Column(
|
2023-05-06 23:19:57 +00:00
|
|
|
modifier = modifier
|
|
|
|
.combinedClickable(
|
|
|
|
onClick = {
|
|
|
|
routeFor(note, loggedIn)?.let { navController.navigate(it) }
|
|
|
|
},
|
|
|
|
onLongClick = { popupExpanded = true }
|
|
|
|
)
|
|
|
|
.background(backgroundColor)
|
2023-01-13 15:20:54 +00:00
|
|
|
) {
|
2023-01-12 17:47:31 +00:00
|
|
|
Row(
|
|
|
|
modifier = Modifier
|
|
|
|
.padding(
|
2023-02-20 23:09:57 +00:00
|
|
|
start = if (!isBoostedNote) 12.dp else 0.dp,
|
|
|
|
end = if (!isBoostedNote) 12.dp else 0.dp,
|
2023-05-05 01:57:19 +00:00
|
|
|
top = if (addMarginTop && !isBoostedNote) 10.dp else 0.dp
|
2023-03-08 22:07:56 +00:00
|
|
|
)
|
2023-01-12 17:47:31 +00:00
|
|
|
) {
|
2023-02-20 23:09:57 +00:00
|
|
|
if (!isBoostedNote && !isQuotedNote) {
|
2023-05-05 01:57:19 +00:00
|
|
|
DrawAuthorImages(baseNote, loggedIn, navController)
|
2023-01-11 18:31:20 +00:00
|
|
|
}
|
|
|
|
|
2023-02-20 23:09:57 +00:00
|
|
|
Column(
|
2023-05-06 15:49:53 +00:00
|
|
|
modifier = Modifier
|
|
|
|
.padding(start = if (!isBoostedNote && !isQuotedNote) 10.dp else 0.dp)
|
2023-02-20 23:09:57 +00:00
|
|
|
) {
|
2023-05-07 00:02:54 +00:00
|
|
|
FirstUserInfoRow(
|
|
|
|
baseNote = baseNote,
|
|
|
|
showAuthorPicture = isQuotedNote,
|
|
|
|
account = account,
|
|
|
|
accountViewModel = accountViewModel,
|
|
|
|
navController = navController
|
|
|
|
)
|
2023-03-16 18:20:30 +00:00
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
if (noteEvent !is RepostEvent && !makeItShort && !isQuotedNote) {
|
2023-05-07 00:02:54 +00:00
|
|
|
SecondUserInfoRow(
|
|
|
|
note,
|
|
|
|
account,
|
|
|
|
navController
|
|
|
|
)
|
2023-05-06 15:49:53 +00:00
|
|
|
}
|
2023-02-27 18:41:23 +00:00
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
Spacer(modifier = Modifier.height(2.dp))
|
2023-02-27 18:41:23 +00:00
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
if (!makeItShort) {
|
|
|
|
ReplyRow(
|
|
|
|
note,
|
|
|
|
unPackReply,
|
|
|
|
backgroundColor,
|
|
|
|
account,
|
|
|
|
accountViewModel,
|
|
|
|
navController
|
|
|
|
)
|
2023-01-11 18:31:20 +00:00
|
|
|
}
|
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
when (noteEvent) {
|
|
|
|
is ReactionEvent -> {
|
|
|
|
RenderReaction(note, backgroundColor, accountViewModel, navController)
|
|
|
|
}
|
2023-03-23 14:49:01 +00:00
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
is RepostEvent -> {
|
|
|
|
RenderRepost(note, backgroundColor, accountViewModel, navController)
|
|
|
|
}
|
2023-03-28 12:46:07 +00:00
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
is ReportEvent -> {
|
|
|
|
RenderReport(note)
|
2023-03-23 14:49:01 +00:00
|
|
|
}
|
2023-02-28 18:20:49 +00:00
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
is LongTextNoteEvent -> {
|
|
|
|
RenderLongFormContent(note, loggedIn, accountViewModel, navController)
|
|
|
|
}
|
2023-02-27 00:22:22 +00:00
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
is BadgeAwardEvent -> {
|
|
|
|
RenderBadgeAward(note, backgroundColor, accountViewModel, navController)
|
2023-02-27 22:14:15 +00:00
|
|
|
}
|
2023-05-06 15:49:53 +00:00
|
|
|
|
|
|
|
is PrivateDmEvent -> {
|
|
|
|
RenderPrivateMessage(note, makeItShort, canPreview, backgroundColor, accountViewModel, navController)
|
2023-01-19 22:58:35 +00:00
|
|
|
}
|
2023-01-11 18:31:20 +00:00
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
is HighlightEvent -> {
|
|
|
|
RenderHighlight(note, makeItShort, canPreview, backgroundColor, accountViewModel, navController)
|
2023-01-11 18:31:20 +00:00
|
|
|
}
|
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
is PollNoteEvent -> {
|
|
|
|
RenderPoll(
|
|
|
|
note,
|
|
|
|
makeItShort,
|
|
|
|
canPreview,
|
|
|
|
backgroundColor,
|
|
|
|
accountViewModel,
|
|
|
|
navController
|
|
|
|
)
|
|
|
|
}
|
2023-01-11 18:31:20 +00:00
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
else -> {
|
|
|
|
RenderTextEvent(
|
|
|
|
note,
|
|
|
|
makeItShort,
|
|
|
|
canPreview,
|
|
|
|
backgroundColor,
|
|
|
|
accountViewModel,
|
|
|
|
navController
|
2023-01-11 18:31:20 +00:00
|
|
|
)
|
|
|
|
}
|
2023-05-06 15:49:53 +00:00
|
|
|
}
|
2023-02-19 00:14:52 +00:00
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-03-02 00:18:07 +00:00
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
fun routeFor(note: Note, loggedIn: User): String? {
|
|
|
|
val noteEvent = note.event
|
2023-03-02 00:18:07 +00:00
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
if (noteEvent is ChannelMessageEvent || noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) {
|
|
|
|
note.channel()?.let {
|
|
|
|
return "Channel/${it.idHex}"
|
|
|
|
}
|
|
|
|
} else if (noteEvent is PrivateDmEvent) {
|
|
|
|
val replyAuthorBase =
|
|
|
|
(note.event as? PrivateDmEvent)
|
|
|
|
?.recipientPubKey()
|
|
|
|
?.let { LocalCache.getOrCreateUser(it) }
|
2023-03-05 23:34:11 +00:00
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
var userToComposeOn = note.author!!
|
2023-03-05 23:34:11 +00:00
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
if (replyAuthorBase != null) {
|
|
|
|
if (note.author == loggedIn) {
|
|
|
|
userToComposeOn = replyAuthorBase
|
|
|
|
}
|
|
|
|
}
|
2023-03-05 23:34:11 +00:00
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
return "Room/${userToComposeOn.pubkeyHex}"
|
|
|
|
} else {
|
|
|
|
return "Note/${note.idHex}"
|
|
|
|
}
|
2023-03-09 23:55:57 +00:00
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
return null
|
|
|
|
}
|
2023-03-09 23:55:57 +00:00
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
@Composable
|
|
|
|
private fun RenderTextEvent(
|
|
|
|
note: Note,
|
|
|
|
makeItShort: Boolean,
|
|
|
|
canPreview: Boolean,
|
|
|
|
backgroundColor: Color,
|
|
|
|
accountViewModel: AccountViewModel,
|
|
|
|
navController: NavController
|
|
|
|
) {
|
|
|
|
val noteEvent = note.event ?: return
|
2023-04-28 22:40:12 +00:00
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
val eventContent = accountViewModel.decrypt(note)
|
2023-04-28 22:40:12 +00:00
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
if (eventContent != null) {
|
|
|
|
if (makeItShort && accountViewModel.isLoggedUser(note.author)) {
|
|
|
|
Text(
|
|
|
|
text = eventContent,
|
|
|
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
|
|
|
maxLines = 2,
|
|
|
|
overflow = TextOverflow.Ellipsis
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
TranslatableRichTextViewer(
|
|
|
|
content = eventContent,
|
|
|
|
canPreview = canPreview && !makeItShort,
|
|
|
|
modifier = Modifier.fillMaxWidth(),
|
|
|
|
tags = noteEvent.tags(),
|
|
|
|
backgroundColor = backgroundColor,
|
|
|
|
accountViewModel = accountViewModel,
|
|
|
|
navController = navController
|
|
|
|
)
|
2023-01-31 02:36:06 +00:00
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, navController)
|
|
|
|
}
|
|
|
|
}
|
2023-01-11 18:31:20 +00:00
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
if (!makeItShort) {
|
|
|
|
ReactionsRow(note, accountViewModel, navController)
|
|
|
|
}
|
2023-01-13 15:20:54 +00:00
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
Divider(
|
|
|
|
modifier = Modifier.padding(top = 10.dp),
|
|
|
|
thickness = 0.25.dp
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
private fun RenderPoll(
|
|
|
|
note: Note,
|
|
|
|
makeItShort: Boolean,
|
|
|
|
canPreview: Boolean,
|
|
|
|
backgroundColor: Color,
|
|
|
|
accountViewModel: AccountViewModel,
|
|
|
|
navController: NavController
|
|
|
|
) {
|
|
|
|
val noteEvent = note.event as? PollNoteEvent ?: return
|
|
|
|
val eventContent = noteEvent.content()
|
|
|
|
|
|
|
|
if (makeItShort && accountViewModel.isLoggedUser(note.author)) {
|
|
|
|
Text(
|
|
|
|
text = eventContent,
|
|
|
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
|
|
|
maxLines = 2,
|
|
|
|
overflow = TextOverflow.Ellipsis
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
TranslatableRichTextViewer(
|
|
|
|
eventContent,
|
|
|
|
canPreview = canPreview && !makeItShort,
|
|
|
|
Modifier.fillMaxWidth(),
|
|
|
|
noteEvent.tags(),
|
|
|
|
backgroundColor,
|
|
|
|
accountViewModel,
|
|
|
|
navController
|
|
|
|
)
|
|
|
|
|
|
|
|
DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, navController)
|
|
|
|
|
|
|
|
PollNote(
|
|
|
|
note,
|
|
|
|
canPreview = canPreview && !makeItShort,
|
|
|
|
backgroundColor,
|
|
|
|
accountViewModel,
|
|
|
|
navController
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!makeItShort) {
|
|
|
|
ReactionsRow(note, accountViewModel, navController)
|
|
|
|
}
|
|
|
|
|
|
|
|
Divider(
|
|
|
|
modifier = Modifier.padding(top = 10.dp),
|
|
|
|
thickness = 0.25.dp
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
private fun RenderHighlight(
|
|
|
|
note: Note,
|
|
|
|
makeItShort: Boolean,
|
|
|
|
canPreview: Boolean,
|
|
|
|
backgroundColor: Color,
|
|
|
|
accountViewModel: AccountViewModel,
|
|
|
|
navController: NavController
|
|
|
|
) {
|
|
|
|
val noteEvent = note.event as? HighlightEvent ?: return
|
|
|
|
|
|
|
|
DisplayHighlight(
|
|
|
|
noteEvent.quote(),
|
|
|
|
noteEvent.author(),
|
|
|
|
noteEvent.inUrl(),
|
|
|
|
makeItShort,
|
|
|
|
canPreview,
|
|
|
|
backgroundColor,
|
|
|
|
accountViewModel,
|
|
|
|
navController
|
|
|
|
)
|
|
|
|
|
|
|
|
if (!makeItShort) {
|
|
|
|
ReactionsRow(note, accountViewModel, navController)
|
|
|
|
}
|
|
|
|
|
|
|
|
Divider(
|
|
|
|
modifier = Modifier.padding(top = 10.dp),
|
|
|
|
thickness = 0.25.dp
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
private fun RenderPrivateMessage(
|
|
|
|
note: Note,
|
|
|
|
makeItShort: Boolean,
|
|
|
|
canPreview: Boolean,
|
|
|
|
backgroundColor: Color,
|
|
|
|
accountViewModel: AccountViewModel,
|
|
|
|
navController: NavController
|
|
|
|
) {
|
|
|
|
val noteEvent = note.event as? PrivateDmEvent ?: return
|
|
|
|
val withMe = noteEvent.with(accountViewModel.userProfile().pubkeyHex)
|
|
|
|
|
|
|
|
if (withMe) {
|
|
|
|
val eventContent = accountViewModel.decrypt(note)
|
|
|
|
|
|
|
|
if (eventContent != null) {
|
|
|
|
if (makeItShort && accountViewModel.isLoggedUser(note.author)) {
|
|
|
|
Text(
|
|
|
|
text = eventContent,
|
|
|
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
|
|
|
maxLines = 2,
|
|
|
|
overflow = TextOverflow.Ellipsis
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
TranslatableRichTextViewer(
|
|
|
|
content = eventContent,
|
|
|
|
canPreview = canPreview && !makeItShort,
|
|
|
|
modifier = Modifier.fillMaxWidth(),
|
|
|
|
tags = noteEvent.tags(),
|
|
|
|
backgroundColor = backgroundColor,
|
|
|
|
accountViewModel = accountViewModel,
|
|
|
|
navController = navController
|
|
|
|
)
|
|
|
|
|
|
|
|
DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, navController)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
val recipient = noteEvent.recipientPubKey()?.let { LocalCache.checkGetOrCreateUser(it) }
|
|
|
|
|
|
|
|
TranslatableRichTextViewer(
|
|
|
|
stringResource(
|
|
|
|
id = R.string.private_conversation_notification,
|
|
|
|
"@${note.author?.pubkeyNpub()}",
|
|
|
|
"@${recipient?.pubkeyNpub()}"
|
|
|
|
),
|
|
|
|
canPreview = !makeItShort,
|
|
|
|
Modifier.fillMaxWidth(),
|
|
|
|
noteEvent.tags(),
|
|
|
|
backgroundColor,
|
|
|
|
accountViewModel,
|
|
|
|
navController
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!makeItShort) {
|
|
|
|
ReactionsRow(note, accountViewModel, navController)
|
|
|
|
}
|
|
|
|
|
|
|
|
Divider(
|
|
|
|
modifier = Modifier.padding(top = 10.dp),
|
|
|
|
thickness = 0.25.dp
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
private fun RenderBadgeAward(
|
|
|
|
note: Note,
|
|
|
|
backgroundColor: Color,
|
|
|
|
accountViewModel: AccountViewModel,
|
|
|
|
navController: NavController
|
|
|
|
) {
|
|
|
|
if (note.replyTo.isNullOrEmpty()) return
|
|
|
|
|
|
|
|
val noteEvent = note.event as? BadgeAwardEvent ?: return
|
2023-05-06 23:19:57 +00:00
|
|
|
var awardees by remember { mutableStateOf<List<User>>(listOf()) }
|
2023-05-06 15:49:53 +00:00
|
|
|
|
|
|
|
Text(text = stringResource(R.string.award_granted_to))
|
2023-05-07 00:02:54 +00:00
|
|
|
|
|
|
|
LaunchedEffect(key1 = note) {
|
|
|
|
withContext(Dispatchers.IO) {
|
|
|
|
awardees = noteEvent.awardees().mapNotNull { hex ->
|
|
|
|
LocalCache.checkGetOrCreateUser(hex)
|
2023-05-06 23:19:57 +00:00
|
|
|
}
|
|
|
|
}
|
2023-05-07 00:02:54 +00:00
|
|
|
}
|
2023-05-06 15:49:53 +00:00
|
|
|
|
2023-05-07 00:02:54 +00:00
|
|
|
FlowRow(modifier = Modifier.padding(top = 5.dp)) {
|
|
|
|
awardees.forEach { user ->
|
|
|
|
Row(modifier = Modifier.clickable {
|
|
|
|
navController.navigate("User/${user.pubkeyHex}")
|
|
|
|
},
|
|
|
|
verticalAlignment = Alignment.CenterVertically
|
|
|
|
) {
|
|
|
|
UserPicture(
|
|
|
|
baseUser = user,
|
|
|
|
baseUserAccount = accountViewModel.userProfile(),
|
|
|
|
size = 35.dp
|
|
|
|
)
|
2023-05-06 15:49:53 +00:00
|
|
|
}
|
2023-05-07 00:02:54 +00:00
|
|
|
}
|
|
|
|
}
|
2023-05-06 15:49:53 +00:00
|
|
|
|
2023-05-07 00:02:54 +00:00
|
|
|
note.replyTo?.firstOrNull()?.let {
|
|
|
|
NoteCompose(
|
|
|
|
it,
|
|
|
|
modifier = Modifier,
|
|
|
|
isBoostedNote = false,
|
|
|
|
isQuotedNote = true,
|
|
|
|
unPackReply = false,
|
|
|
|
parentBackgroundColor = backgroundColor,
|
|
|
|
accountViewModel = accountViewModel,
|
|
|
|
navController = navController
|
|
|
|
)
|
|
|
|
}
|
2023-05-06 15:49:53 +00:00
|
|
|
|
|
|
|
ReactionsRow(note, accountViewModel, navController)
|
|
|
|
|
|
|
|
Divider(
|
|
|
|
modifier = Modifier.padding(top = 10.dp),
|
|
|
|
thickness = 0.25.dp
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
private fun RenderReaction(
|
|
|
|
note: Note,
|
|
|
|
backgroundColor: Color,
|
|
|
|
accountViewModel: AccountViewModel,
|
|
|
|
navController: NavController
|
|
|
|
) {
|
|
|
|
note.replyTo?.lastOrNull()?.let {
|
|
|
|
NoteCompose(
|
|
|
|
it,
|
|
|
|
modifier = Modifier,
|
|
|
|
isBoostedNote = true,
|
|
|
|
unPackReply = false,
|
|
|
|
parentBackgroundColor = backgroundColor,
|
|
|
|
accountViewModel = accountViewModel,
|
|
|
|
navController = navController
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reposts have trash in their contents.
|
|
|
|
val refactorReactionText =
|
|
|
|
if (note.event?.content() == "+") "❤" else note.event?.content() ?: ""
|
|
|
|
|
|
|
|
Text(
|
|
|
|
text = refactorReactionText
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
private fun RenderRepost(
|
|
|
|
note: Note,
|
|
|
|
backgroundColor: Color,
|
|
|
|
accountViewModel: AccountViewModel,
|
|
|
|
navController: NavController
|
|
|
|
) {
|
|
|
|
note.replyTo?.lastOrNull()?.let {
|
|
|
|
NoteCompose(
|
|
|
|
it,
|
|
|
|
modifier = Modifier,
|
|
|
|
isBoostedNote = true,
|
|
|
|
unPackReply = false,
|
|
|
|
parentBackgroundColor = backgroundColor,
|
|
|
|
accountViewModel = accountViewModel,
|
|
|
|
navController = navController
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
private fun RenderLongFormContent(
|
|
|
|
note: Note,
|
|
|
|
loggedIn: User,
|
|
|
|
accountViewModel: AccountViewModel,
|
|
|
|
navController: NavController
|
|
|
|
) {
|
|
|
|
val noteEvent = note.event as? LongTextNoteEvent ?: return
|
|
|
|
|
|
|
|
LongFormHeader(noteEvent, note, loggedIn)
|
|
|
|
|
|
|
|
ReactionsRow(note, accountViewModel, navController)
|
|
|
|
|
|
|
|
Divider(
|
|
|
|
modifier = Modifier.padding(top = 10.dp),
|
|
|
|
thickness = 0.25.dp
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
private fun RenderReport(note: Note) {
|
|
|
|
val noteEvent = note.event as? ReportEvent ?: return
|
|
|
|
|
|
|
|
val reportType = (noteEvent.reportedPost() + noteEvent.reportedAuthor()).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(", ")
|
|
|
|
|
|
|
|
Text(
|
|
|
|
text = reportType
|
|
|
|
)
|
|
|
|
|
|
|
|
Divider(
|
|
|
|
modifier = Modifier.padding(top = 40.dp),
|
|
|
|
thickness = 0.25.dp
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
private fun ReplyRow(
|
|
|
|
note: Note,
|
|
|
|
unPackReply: Boolean,
|
|
|
|
backgroundColor: Color,
|
|
|
|
account: Account,
|
|
|
|
accountViewModel: AccountViewModel,
|
|
|
|
navController: NavController
|
|
|
|
) {
|
|
|
|
val noteEvent = note.event
|
|
|
|
|
2023-05-07 00:02:54 +00:00
|
|
|
if (noteEvent is TextNoteEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())) {
|
2023-05-06 15:49:53 +00:00
|
|
|
val replyingDirectlyTo = note.replyTo?.lastOrNull()
|
|
|
|
if (replyingDirectlyTo != null && unPackReply) {
|
|
|
|
NoteCompose(
|
|
|
|
baseNote = replyingDirectlyTo,
|
|
|
|
isQuotedNote = true,
|
|
|
|
modifier = Modifier
|
|
|
|
.padding(top = 5.dp)
|
|
|
|
.fillMaxWidth()
|
|
|
|
.clip(shape = RoundedCornerShape(15.dp))
|
|
|
|
.border(
|
|
|
|
1.dp,
|
|
|
|
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
|
|
|
|
RoundedCornerShape(15.dp)
|
|
|
|
),
|
|
|
|
unPackReply = false,
|
|
|
|
makeItShort = true,
|
|
|
|
parentBackgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.05f)
|
|
|
|
.compositeOver(backgroundColor),
|
|
|
|
accountViewModel = accountViewModel,
|
|
|
|
navController = navController
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
ReplyInformation(note.replyTo, noteEvent.mentions(), account, navController)
|
|
|
|
}
|
|
|
|
|
|
|
|
Spacer(modifier = Modifier.height(5.dp))
|
2023-05-07 00:02:54 +00:00
|
|
|
} else if (noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())) {
|
2023-05-06 15:49:53 +00:00
|
|
|
val sortedMentions = noteEvent.mentions()
|
|
|
|
.mapNotNull { LocalCache.checkGetOrCreateUser(it) }
|
|
|
|
.toSet()
|
|
|
|
.sortedBy { account.isFollowing(it) }
|
|
|
|
|
|
|
|
note.channel()?.let {
|
|
|
|
ReplyInformationChannel(note.replyTo, sortedMentions, it, navController)
|
|
|
|
}
|
|
|
|
|
|
|
|
Spacer(modifier = Modifier.height(5.dp))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
private fun SecondUserInfoRow(
|
|
|
|
note: Note,
|
|
|
|
account: Account,
|
|
|
|
navController: NavController
|
|
|
|
) {
|
|
|
|
val noteEvent = note.event ?: return
|
|
|
|
val noteAuthor = note.author ?: return
|
|
|
|
|
|
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
|
|
ObserveDisplayNip05Status(noteAuthor, Modifier.weight(1f))
|
|
|
|
|
|
|
|
val baseReward = noteEvent.getReward()
|
|
|
|
if (baseReward != null) {
|
|
|
|
DisplayReward(baseReward, note, account, navController)
|
|
|
|
}
|
|
|
|
|
|
|
|
val pow = noteEvent.getPoWRank()
|
|
|
|
if (pow > 20) {
|
|
|
|
DisplayPoW(pow)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
private fun FirstUserInfoRow(
|
|
|
|
baseNote: Note,
|
|
|
|
showAuthorPicture: Boolean,
|
|
|
|
account: Account,
|
|
|
|
accountViewModel: AccountViewModel,
|
|
|
|
navController: NavController
|
|
|
|
) {
|
|
|
|
var moreActionsExpanded by remember { mutableStateOf(false) }
|
2023-05-06 23:19:57 +00:00
|
|
|
val eventNote = baseNote.event ?: return
|
|
|
|
val time = baseNote.createdAt() ?: return
|
|
|
|
val loggedIn = account.userProfile()
|
2023-05-06 15:49:53 +00:00
|
|
|
|
|
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
|
|
if (showAuthorPicture) {
|
2023-05-06 23:19:57 +00:00
|
|
|
NoteAuthorPicture(baseNote, navController, loggedIn, 25.dp)
|
2023-05-06 15:49:53 +00:00
|
|
|
Spacer(Modifier.padding(horizontal = 5.dp))
|
|
|
|
NoteUsernameDisplay(baseNote, Modifier.weight(1f))
|
|
|
|
} else {
|
|
|
|
NoteUsernameDisplay(baseNote, Modifier.weight(1f))
|
|
|
|
}
|
|
|
|
|
2023-05-06 23:19:57 +00:00
|
|
|
if (eventNote is RepostEvent) {
|
2023-05-06 15:49:53 +00:00
|
|
|
Text(
|
|
|
|
" ${stringResource(id = R.string.boosted)}",
|
|
|
|
fontWeight = FontWeight.Bold,
|
|
|
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
|
|
|
)
|
|
|
|
} else {
|
2023-05-06 23:19:57 +00:00
|
|
|
DisplayFollowingHashtagsInPost(eventNote, account, navController)
|
2023-01-11 18:31:20 +00:00
|
|
|
}
|
2023-05-06 15:49:53 +00:00
|
|
|
|
2023-05-06 23:19:57 +00:00
|
|
|
TimeAgo(time)
|
2023-05-06 15:49:53 +00:00
|
|
|
|
|
|
|
IconButton(
|
|
|
|
modifier = Modifier.size(24.dp),
|
|
|
|
onClick = { moreActionsExpanded = true }
|
|
|
|
) {
|
|
|
|
Icon(
|
|
|
|
imageVector = Icons.Default.MoreVert,
|
|
|
|
null,
|
|
|
|
modifier = Modifier.size(15.dp),
|
|
|
|
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
|
|
|
)
|
|
|
|
|
|
|
|
NoteDropDownMenu(
|
|
|
|
baseNote,
|
|
|
|
moreActionsExpanded,
|
|
|
|
{ moreActionsExpanded = false },
|
|
|
|
accountViewModel
|
|
|
|
)
|
|
|
|
}
|
2023-01-11 18:31:20 +00:00
|
|
|
}
|
2023-01-13 15:20:54 +00:00
|
|
|
}
|
|
|
|
|
2023-05-06 23:19:57 +00:00
|
|
|
@Composable
|
|
|
|
fun TimeAgo(time: Long) {
|
|
|
|
val context = LocalContext.current
|
|
|
|
|
|
|
|
var timeStr by remember { mutableStateOf("") }
|
|
|
|
|
|
|
|
LaunchedEffect(key1 = time) {
|
|
|
|
withContext(Dispatchers.IO) {
|
|
|
|
timeStr = timeAgo(time, context = context)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Text(
|
|
|
|
timeStr,
|
|
|
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
|
|
|
maxLines = 1
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-05-05 01:57:19 +00:00
|
|
|
@Composable
|
|
|
|
private fun DrawAuthorImages(baseNote: Note, loggedIn: User, navController: NavController) {
|
|
|
|
val baseChannel = baseNote.channel()
|
|
|
|
|
|
|
|
Column(Modifier.width(55.dp)) {
|
|
|
|
// Draws the boosted picture outside the boosted card.
|
|
|
|
Box(modifier = Modifier.width(55.dp), contentAlignment = Alignment.BottomEnd) {
|
|
|
|
NoteAuthorPicture(baseNote, navController, loggedIn, 55.dp)
|
|
|
|
|
|
|
|
if (baseNote.event is RepostEvent) {
|
|
|
|
RepostNoteAuthorPicture(baseNote, navController, loggedIn)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (baseNote.event is ChannelMessageEvent && baseChannel != null) {
|
|
|
|
ChannelNotePicture(baseChannel)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (baseNote.event is RepostEvent) {
|
|
|
|
baseNote.replyTo?.lastOrNull()?.let {
|
|
|
|
RelayBadges(it)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
RelayBadges(baseNote)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
private fun ChannelNotePicture(baseChannel: Channel) {
|
|
|
|
val channelState by baseChannel.live.observeAsState()
|
|
|
|
val channel = channelState?.channel
|
|
|
|
|
|
|
|
if (channel != null) {
|
|
|
|
Box(
|
|
|
|
Modifier
|
|
|
|
.width(30.dp)
|
|
|
|
.height(30.dp)
|
|
|
|
) {
|
|
|
|
RobohashAsyncImageProxy(
|
|
|
|
robot = channel.idHex,
|
|
|
|
model = ResizeImage(channel.profilePicture(), 30.dp),
|
|
|
|
contentDescription = stringResource(R.string.group_picture),
|
|
|
|
modifier = Modifier
|
|
|
|
.width(30.dp)
|
|
|
|
.height(30.dp)
|
|
|
|
.clip(shape = CircleShape)
|
|
|
|
.background(MaterialTheme.colors.background)
|
|
|
|
.border(
|
|
|
|
2.dp,
|
|
|
|
MaterialTheme.colors.background,
|
|
|
|
CircleShape
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
private fun RepostNoteAuthorPicture(
|
|
|
|
baseNote: Note,
|
|
|
|
navController: NavController,
|
|
|
|
loggedIn: User
|
|
|
|
) {
|
|
|
|
baseNote.replyTo?.lastOrNull()?.let {
|
|
|
|
Box(
|
|
|
|
Modifier
|
|
|
|
.width(30.dp)
|
|
|
|
.height(30.dp)
|
|
|
|
) {
|
|
|
|
NoteAuthorPicture(
|
|
|
|
it,
|
|
|
|
navController,
|
|
|
|
loggedIn,
|
|
|
|
35.dp,
|
|
|
|
pictureModifier = Modifier.border(
|
|
|
|
2.dp,
|
|
|
|
MaterialTheme.colors.background,
|
|
|
|
CircleShape
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-28 22:40:12 +00:00
|
|
|
@Composable
|
|
|
|
fun DisplayHighlight(
|
|
|
|
highlight: String,
|
|
|
|
authorHex: String?,
|
|
|
|
url: String?,
|
|
|
|
makeItShort: Boolean,
|
|
|
|
canPreview: Boolean,
|
|
|
|
backgroundColor: Color,
|
|
|
|
accountViewModel: AccountViewModel,
|
|
|
|
navController: NavController
|
|
|
|
) {
|
|
|
|
val quote = highlight.split("\n").map { "> *${it.removeSuffix(" ")}*" }.joinToString("\n")
|
|
|
|
|
2023-05-06 15:49:53 +00:00
|
|
|
TranslatableRichTextViewer(
|
|
|
|
quote,
|
|
|
|
canPreview = canPreview && !makeItShort,
|
|
|
|
Modifier.fillMaxWidth(),
|
|
|
|
emptyList(),
|
|
|
|
backgroundColor,
|
|
|
|
accountViewModel,
|
|
|
|
navController
|
|
|
|
)
|
2023-04-28 22:40:12 +00:00
|
|
|
|
|
|
|
FlowRow() {
|
|
|
|
authorHex?.let { authorHex ->
|
|
|
|
val userBase = LocalCache.checkGetOrCreateUser(authorHex)
|
|
|
|
|
|
|
|
if (userBase != null) {
|
|
|
|
val userState by userBase.live().metadata.observeAsState()
|
|
|
|
val user = userState?.user
|
|
|
|
|
|
|
|
if (user != null) {
|
|
|
|
CreateClickableText(
|
|
|
|
user.toBestDisplayName(),
|
|
|
|
"",
|
|
|
|
"User/${user.pubkeyHex}",
|
|
|
|
navController
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
url?.let { url ->
|
|
|
|
val validatedUrl = try {
|
|
|
|
URL(url)
|
|
|
|
} catch (e: Exception) {
|
|
|
|
Log.w("Note Compose", "Invalid URI: $url")
|
|
|
|
null
|
|
|
|
}
|
|
|
|
|
|
|
|
validatedUrl?.host?.let { host ->
|
|
|
|
Text("on ")
|
|
|
|
ClickableUrl(urlText = host, url = url)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-23 14:49:01 +00:00
|
|
|
@Composable
|
|
|
|
fun DisplayFollowingHashtagsInPost(
|
|
|
|
noteEvent: EventInterface,
|
|
|
|
account: Account,
|
|
|
|
navController: NavController
|
|
|
|
) {
|
2023-04-07 21:38:02 +00:00
|
|
|
var firstTag by remember { mutableStateOf<String?>(null) }
|
|
|
|
|
2023-05-06 23:19:57 +00:00
|
|
|
LaunchedEffect(key1 = noteEvent.id()) {
|
2023-04-18 12:45:02 +00:00
|
|
|
withContext(Dispatchers.IO) {
|
|
|
|
firstTag = noteEvent.firstIsTaggedHashes(account.followingTagSet())
|
|
|
|
}
|
2023-04-07 21:38:02 +00:00
|
|
|
}
|
|
|
|
|
2023-03-23 14:49:01 +00:00
|
|
|
Column() {
|
|
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
2023-04-07 21:38:02 +00:00
|
|
|
firstTag?.let {
|
2023-03-23 14:49:01 +00:00
|
|
|
ClickableText(
|
|
|
|
text = AnnotatedString(" #$firstTag"),
|
|
|
|
onClick = { navController.navigate("Hashtag/$firstTag") },
|
|
|
|
style = LocalTextStyle.current.copy(
|
|
|
|
color = MaterialTheme.colors.primary.copy(
|
|
|
|
alpha = 0.52f
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
fun DisplayUncitedHashtags(
|
2023-04-07 20:58:25 +00:00
|
|
|
hashtags: List<String>,
|
2023-03-23 14:49:01 +00:00
|
|
|
eventContent: String,
|
|
|
|
navController: NavController
|
|
|
|
) {
|
|
|
|
if (hashtags.isNotEmpty()) {
|
2023-03-23 15:07:29 +00:00
|
|
|
FlowRow(
|
|
|
|
modifier = Modifier.padding(top = 5.dp)
|
|
|
|
) {
|
|
|
|
hashtags.forEach { hashtag ->
|
|
|
|
if (!eventContent.contains(hashtag, true)) {
|
2023-03-23 14:49:01 +00:00
|
|
|
ClickableText(
|
2023-03-23 15:07:02 +00:00
|
|
|
text = AnnotatedString("#$hashtag "),
|
|
|
|
onClick = { navController.navigate("Hashtag/$hashtag") },
|
2023-03-23 14:49:01 +00:00
|
|
|
style = LocalTextStyle.current.copy(
|
|
|
|
color = MaterialTheme.colors.primary.copy(
|
|
|
|
alpha = 0.52f
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-28 12:46:07 +00:00
|
|
|
@Composable
|
|
|
|
fun DisplayPoW(
|
|
|
|
pow: Int
|
|
|
|
) {
|
|
|
|
Text(
|
|
|
|
"PoW-$pow",
|
|
|
|
color = MaterialTheme.colors.primary.copy(
|
|
|
|
alpha = 0.52f
|
|
|
|
),
|
|
|
|
fontSize = 14.sp,
|
|
|
|
fontWeight = FontWeight.Bold
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-03-23 14:49:01 +00:00
|
|
|
@Composable
|
|
|
|
fun DisplayReward(
|
|
|
|
baseReward: BigDecimal,
|
|
|
|
baseNote: Note,
|
2023-03-23 16:24:46 +00:00
|
|
|
account: Account,
|
2023-03-23 14:49:01 +00:00
|
|
|
navController: NavController
|
|
|
|
) {
|
2023-03-23 16:24:46 +00:00
|
|
|
var popupExpanded by remember { mutableStateOf(false) }
|
|
|
|
|
2023-03-23 14:49:01 +00:00
|
|
|
Column() {
|
2023-03-23 16:24:46 +00:00
|
|
|
Row(
|
|
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
|
|
modifier = Modifier.clickable { popupExpanded = true }
|
|
|
|
) {
|
2023-03-23 14:49:01 +00:00
|
|
|
ClickableText(
|
|
|
|
text = AnnotatedString("#bounty"),
|
|
|
|
onClick = { navController.navigate("Hashtag/bounty") },
|
|
|
|
style = LocalTextStyle.current.copy(
|
|
|
|
color = MaterialTheme.colors.primary.copy(
|
|
|
|
alpha = 0.52f
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
val repliesState by baseNote.live().replies.observeAsState()
|
|
|
|
val replyNote = repliesState?.note
|
|
|
|
|
2023-03-23 16:24:46 +00:00
|
|
|
if (replyNote?.hasPledgeBy(account.userProfile()) == true) {
|
|
|
|
Icon(
|
|
|
|
imageVector = Icons.Default.Bolt,
|
|
|
|
contentDescription = stringResource(R.string.zaps),
|
|
|
|
modifier = Modifier.size(20.dp),
|
|
|
|
tint = BitcoinOrange
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
Icon(
|
|
|
|
imageVector = Icons.Default.Bolt,
|
|
|
|
contentDescription = stringResource(R.string.zaps),
|
|
|
|
modifier = Modifier.size(20.dp),
|
|
|
|
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-03-23 14:49:01 +00:00
|
|
|
var rewardAmount by remember {
|
|
|
|
mutableStateOf<BigDecimal?>(
|
|
|
|
baseReward
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
LaunchedEffect(key1 = repliesState) {
|
|
|
|
withContext(Dispatchers.IO) {
|
|
|
|
replyNote?.pledgedAmountByOthers()?.let {
|
|
|
|
rewardAmount = baseReward.add(it)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Text(
|
|
|
|
showAmount(rewardAmount),
|
|
|
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
|
|
|
)
|
|
|
|
}
|
2023-03-23 16:24:46 +00:00
|
|
|
|
|
|
|
if (popupExpanded) {
|
|
|
|
AddBountyAmountDialog(baseNote, account) {
|
|
|
|
popupExpanded = false
|
|
|
|
}
|
|
|
|
}
|
2023-03-23 14:49:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-05 23:34:11 +00:00
|
|
|
@Composable
|
|
|
|
fun BadgeDisplay(baseNote: Note) {
|
2023-03-16 20:32:42 +00:00
|
|
|
val background = MaterialTheme.colors.background
|
2023-03-05 23:34:11 +00:00
|
|
|
val badgeData = baseNote.event as? BadgeDefinitionEvent ?: return
|
2023-05-06 23:19:57 +00:00
|
|
|
|
|
|
|
val image = badgeData.image()
|
|
|
|
val name = badgeData.name()
|
|
|
|
val description = badgeData.description()
|
|
|
|
|
|
|
|
var backgroundFromImage by remember { mutableStateOf(Pair(background, background)) }
|
|
|
|
var imageResult by remember { mutableStateOf<AsyncImagePainter.State.Success?>(null) }
|
|
|
|
|
|
|
|
LaunchedEffect(key1 = imageResult) {
|
|
|
|
withContext(Dispatchers.IO) {
|
|
|
|
imageResult?.let {
|
|
|
|
val backgroundColor = it.result.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
|
|
|
|
backgroundFromImage = Pair(colorFromImage, textBackground)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-03-05 23:34:11 +00:00
|
|
|
|
|
|
|
Row(
|
|
|
|
modifier = Modifier
|
|
|
|
.padding(10.dp)
|
2023-03-16 20:32:42 +00:00
|
|
|
.clip(shape = CutCornerShape(20, 20, 20, 20))
|
2023-03-05 23:34:11 +00:00
|
|
|
.border(
|
|
|
|
5.dp,
|
|
|
|
MaterialTheme.colors.primary.copy(alpha = 0.32f),
|
|
|
|
CutCornerShape(20)
|
|
|
|
)
|
2023-05-06 23:19:57 +00:00
|
|
|
.background(backgroundFromImage.first)
|
2023-03-05 23:34:11 +00:00
|
|
|
) {
|
|
|
|
Column {
|
2023-05-06 23:19:57 +00:00
|
|
|
image.let {
|
2023-03-05 23:34:11 +00:00
|
|
|
AsyncImage(
|
|
|
|
model = it,
|
|
|
|
contentDescription = stringResource(
|
|
|
|
R.string.badge_award_image_for,
|
2023-05-06 23:19:57 +00:00
|
|
|
name ?: ""
|
2023-03-05 23:34:11 +00:00
|
|
|
),
|
|
|
|
contentScale = ContentScale.FillWidth,
|
2023-03-16 20:32:42 +00:00
|
|
|
modifier = Modifier.fillMaxWidth(),
|
|
|
|
onSuccess = {
|
2023-05-06 23:19:57 +00:00
|
|
|
imageResult = it
|
2023-03-16 20:32:42 +00:00
|
|
|
}
|
2023-03-05 23:34:11 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-05-06 23:19:57 +00:00
|
|
|
name?.let {
|
2023-03-05 23:34:11 +00:00
|
|
|
Text(
|
|
|
|
text = it,
|
|
|
|
style = MaterialTheme.typography.body1,
|
|
|
|
textAlign = TextAlign.Center,
|
|
|
|
modifier = Modifier
|
|
|
|
.fillMaxWidth()
|
2023-03-16 20:32:42 +00:00
|
|
|
.padding(start = 10.dp, end = 10.dp),
|
2023-05-06 23:19:57 +00:00
|
|
|
color = backgroundFromImage.second
|
2023-03-05 23:34:11 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-05-06 23:19:57 +00:00
|
|
|
description?.let {
|
2023-03-05 23:34:11 +00:00
|
|
|
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
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-21 21:01:42 +00:00
|
|
|
@Composable
|
|
|
|
fun FileHeaderDisplay(note: Note) {
|
|
|
|
val event = (note.event as? FileHeaderEvent) ?: return
|
|
|
|
val fullUrl = event.url() ?: return
|
2023-04-25 12:20:51 +00:00
|
|
|
|
|
|
|
var content by remember { mutableStateOf<ZoomableContent?>(null) }
|
|
|
|
|
|
|
|
LaunchedEffect(key1 = event.id) {
|
|
|
|
withContext(Dispatchers.IO) {
|
|
|
|
val blurHash = event.blurhash()
|
|
|
|
val hash = event.hash()
|
2023-05-02 14:02:45 +00:00
|
|
|
val dimensions = event.dimensions()
|
2023-04-25 12:20:51 +00:00
|
|
|
val description = event.content
|
|
|
|
val removedParamsFromUrl = fullUrl.split("?")[0].lowercase()
|
|
|
|
val isImage = imageExtensions.any { removedParamsFromUrl.endsWith(it) }
|
2023-04-27 19:40:28 +00:00
|
|
|
val uri = "nostr:" + note.toNEvent()
|
2023-04-25 12:20:51 +00:00
|
|
|
content = if (isImage) {
|
2023-05-02 14:02:45 +00:00
|
|
|
ZoomableUrlImage(fullUrl, description, hash, blurHash, dimensions, uri)
|
2023-04-25 12:20:51 +00:00
|
|
|
} else {
|
2023-04-27 19:40:28 +00:00
|
|
|
ZoomableUrlVideo(fullUrl, description, hash, uri)
|
2023-04-25 12:20:51 +00:00
|
|
|
}
|
2023-04-21 21:01:42 +00:00
|
|
|
}
|
|
|
|
}
|
2023-04-25 12:20:51 +00:00
|
|
|
|
|
|
|
content?.let {
|
|
|
|
ZoomableContentView(content = it, listOf(it))
|
2023-05-02 19:44:59 +00:00
|
|
|
}
|
2023-04-21 21:01:42 +00:00
|
|
|
}
|
|
|
|
|
2023-04-26 18:22:49 +00:00
|
|
|
@Composable
|
|
|
|
fun FileStorageHeaderDisplay(baseNote: Note) {
|
2023-05-01 20:42:58 +00:00
|
|
|
val appContext = LocalContext.current.applicationContext
|
2023-04-26 22:04:38 +00:00
|
|
|
val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return
|
|
|
|
|
|
|
|
val fileNote = eventHeader.dataEventId()?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
|
2023-04-26 18:22:49 +00:00
|
|
|
|
|
|
|
val noteState by fileNote.live().metadata.observeAsState()
|
|
|
|
val note = noteState?.note
|
|
|
|
|
|
|
|
val eventBytes = (note?.event as? FileStorageEvent)
|
|
|
|
|
|
|
|
var content by remember { mutableStateOf<ZoomableContent?>(null) }
|
|
|
|
|
2023-04-26 22:04:38 +00:00
|
|
|
LaunchedEffect(key1 = eventHeader.id, key2 = noteState) {
|
2023-04-26 18:22:49 +00:00
|
|
|
withContext(Dispatchers.IO) {
|
2023-04-27 19:40:28 +00:00
|
|
|
val uri = "nostr:" + baseNote.toNEvent()
|
2023-05-01 20:42:58 +00:00
|
|
|
val localDir = File(File(appContext.externalCacheDir, "NIP95"), fileNote.idHex)
|
2023-04-26 18:22:49 +00:00
|
|
|
val bytes = eventBytes?.decode()
|
|
|
|
val blurHash = eventHeader.blurhash()
|
2023-05-02 14:02:45 +00:00
|
|
|
val dimensions = eventHeader.dimensions()
|
2023-04-26 18:22:49 +00:00
|
|
|
val description = eventHeader.content
|
|
|
|
val mimeType = eventHeader.mimeType()
|
|
|
|
|
|
|
|
content = if (mimeType?.startsWith("image") == true) {
|
2023-05-02 14:02:45 +00:00
|
|
|
ZoomableLocalImage(
|
|
|
|
localFile = localDir,
|
|
|
|
mimeType = mimeType,
|
|
|
|
description = description,
|
|
|
|
blurhash = blurHash,
|
|
|
|
dim = dimensions,
|
|
|
|
isVerified = true,
|
|
|
|
uri = uri
|
|
|
|
)
|
2023-04-26 18:22:49 +00:00
|
|
|
} else {
|
|
|
|
if (bytes != null) {
|
2023-05-02 14:02:45 +00:00
|
|
|
ZoomableLocalVideo(
|
|
|
|
localFile = localDir,
|
|
|
|
mimeType = mimeType,
|
|
|
|
description = description,
|
|
|
|
dim = dimensions,
|
|
|
|
isVerified = true,
|
|
|
|
uri = uri
|
|
|
|
)
|
2023-04-26 18:22:49 +00:00
|
|
|
} else {
|
|
|
|
null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
content?.let {
|
|
|
|
ZoomableContentView(content = it, listOf(it))
|
2023-05-02 19:44:59 +00:00
|
|
|
}
|
2023-04-26 18:22:49 +00:00
|
|
|
}
|
|
|
|
|
2023-03-03 16:35:29 +00:00
|
|
|
@Composable
|
2023-03-23 12:58:18 +00:00
|
|
|
private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, loggedIn: User) {
|
2023-03-03 16:35:29 +00:00
|
|
|
Row(
|
|
|
|
modifier = Modifier
|
|
|
|
.clip(shape = RoundedCornerShape(15.dp))
|
|
|
|
.border(
|
|
|
|
1.dp,
|
|
|
|
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
|
|
|
|
RoundedCornerShape(15.dp)
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
Column {
|
|
|
|
noteEvent.image()?.let {
|
|
|
|
AsyncImage(
|
|
|
|
model = it,
|
|
|
|
contentDescription = stringResource(
|
|
|
|
R.string.preview_card_image_for,
|
|
|
|
it
|
|
|
|
),
|
|
|
|
contentScale = ContentScale.FillWidth,
|
|
|
|
modifier = Modifier.fillMaxWidth()
|
|
|
|
)
|
2023-03-23 12:58:18 +00:00
|
|
|
} ?: Box() {
|
|
|
|
note.author?.info?.banner?.let {
|
|
|
|
AsyncImage(
|
|
|
|
model = it,
|
|
|
|
contentDescription = stringResource(
|
|
|
|
R.string.preview_card_image_for,
|
|
|
|
it
|
|
|
|
),
|
|
|
|
contentScale = ContentScale.FillWidth,
|
|
|
|
modifier = Modifier.fillMaxWidth()
|
|
|
|
)
|
2023-03-23 13:04:44 +00:00
|
|
|
} ?: Image(
|
|
|
|
painter = painterResource(R.drawable.profile_banner),
|
|
|
|
contentDescription = stringResource(R.string.profile_banner),
|
|
|
|
contentScale = ContentScale.FillWidth,
|
|
|
|
modifier = Modifier
|
|
|
|
.fillMaxWidth()
|
|
|
|
.height(150.dp)
|
|
|
|
)
|
2023-03-23 12:58:18 +00:00
|
|
|
|
|
|
|
Box(
|
|
|
|
Modifier
|
|
|
|
.width(75.dp)
|
|
|
|
.height(75.dp)
|
|
|
|
.padding(10.dp)
|
|
|
|
.align(Alignment.BottomStart)
|
|
|
|
) {
|
|
|
|
NoteAuthorPicture(baseNote = note, baseUserAccount = loggedIn, size = 55.dp)
|
|
|
|
}
|
2023-03-03 16:35:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
noteEvent.title()?.let {
|
|
|
|
Text(
|
|
|
|
text = it,
|
|
|
|
style = MaterialTheme.typography.body1,
|
|
|
|
modifier = Modifier
|
|
|
|
.fillMaxWidth()
|
|
|
|
.padding(start = 10.dp, end = 10.dp, top = 10.dp)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
noteEvent.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
|
|
|
|
)
|
|
|
|
}
|
2023-03-23 12:58:18 +00:00
|
|
|
?: Text(
|
|
|
|
text = noteEvent.content.take(200),
|
|
|
|
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
|
|
|
|
)
|
2023-03-03 16:35:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-06 22:16:27 +00:00
|
|
|
@Composable
|
|
|
|
private fun RelayBadges(baseNote: Note) {
|
2023-02-19 16:22:01 +00:00
|
|
|
val noteRelaysState by baseNote.live().relays.observeAsState()
|
2023-04-20 21:19:34 +00:00
|
|
|
val noteRelays = noteRelaysState?.note ?: return
|
2023-02-06 22:16:27 +00:00
|
|
|
|
|
|
|
var expanded by remember { mutableStateOf(false) }
|
2023-04-20 21:19:34 +00:00
|
|
|
var showShowMore by remember { mutableStateOf(false) }
|
|
|
|
var lazyRelayList by remember { mutableStateOf(emptyList<String>()) }
|
2023-02-06 22:16:27 +00:00
|
|
|
|
2023-04-20 21:19:34 +00:00
|
|
|
LaunchedEffect(key1 = noteRelaysState, key2 = expanded) {
|
|
|
|
withContext(Dispatchers.IO) {
|
|
|
|
val relayList = noteRelays.relays.map {
|
|
|
|
it.removePrefix("wss://").removePrefix("ws://")
|
|
|
|
}
|
2023-02-06 22:16:27 +00:00
|
|
|
|
2023-04-20 21:19:34 +00:00
|
|
|
val relaysToDisplay = if (expanded) relayList else relayList.take(3)
|
|
|
|
val shouldListChange = lazyRelayList.size < 3 || lazyRelayList.size != relayList.size
|
|
|
|
|
|
|
|
if (shouldListChange) {
|
|
|
|
lazyRelayList = relaysToDisplay
|
|
|
|
}
|
|
|
|
|
|
|
|
val nextShowMore = relayList.size > 3 && !expanded
|
|
|
|
if (nextShowMore != showShowMore) {
|
|
|
|
// only triggers recomposition when actually different
|
|
|
|
showShowMore = nextShowMore
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-02-06 22:16:27 +00:00
|
|
|
|
2023-03-28 14:55:30 +00:00
|
|
|
Spacer(Modifier.height(10.dp))
|
|
|
|
|
2023-04-20 21:19:34 +00:00
|
|
|
VerticalRelayPanelWithFlow(lazyRelayList)
|
2023-03-28 14:55:30 +00:00
|
|
|
|
2023-04-20 21:19:34 +00:00
|
|
|
if (showShowMore) {
|
|
|
|
ShowMoreRelaysButton {
|
|
|
|
expanded = true
|
2023-02-06 22:16:27 +00:00
|
|
|
}
|
|
|
|
}
|
2023-04-20 21:19:34 +00:00
|
|
|
}
|
2023-02-06 22:16:27 +00:00
|
|
|
|
2023-04-20 21:19:34 +00:00
|
|
|
@Composable
|
|
|
|
@Stable
|
|
|
|
private fun VerticalRelayPanelWithFlow(
|
|
|
|
relays: List<String>
|
|
|
|
) {
|
|
|
|
// FlowRow Seems to be a lot faster than LazyVerticalGrid
|
|
|
|
FlowRow() {
|
|
|
|
relays.forEach { url ->
|
|
|
|
RelayIconCompose(url)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
@Stable
|
|
|
|
private fun RelayIconCompose(url: String) {
|
|
|
|
val uri = LocalUriHandler.current
|
|
|
|
|
|
|
|
Box(
|
|
|
|
Modifier
|
|
|
|
.padding(1.dp)
|
|
|
|
.size(15.dp)
|
|
|
|
) {
|
|
|
|
RobohashFallbackAsyncImage(
|
|
|
|
robot = "https://$url/favicon.ico",
|
|
|
|
robotSize = 15.dp,
|
|
|
|
model = "https://$url/favicon.ico",
|
|
|
|
contentDescription = stringResource(R.string.relay_icon),
|
|
|
|
colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }),
|
|
|
|
modifier = Modifier
|
|
|
|
.width(13.dp)
|
|
|
|
.height(13.dp)
|
|
|
|
.clip(shape = CircleShape)
|
|
|
|
.background(MaterialTheme.colors.background)
|
|
|
|
.clickable(onClick = { uri.openUri("https://$url") })
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
private fun ShowMoreRelaysButton(onClick: () -> Unit) {
|
|
|
|
Row(
|
|
|
|
Modifier
|
|
|
|
.fillMaxWidth()
|
|
|
|
.height(25.dp),
|
|
|
|
horizontalArrangement = Arrangement.Center,
|
|
|
|
verticalAlignment = Alignment.Top
|
|
|
|
) {
|
|
|
|
IconButton(
|
|
|
|
modifier = Modifier.then(Modifier.size(24.dp)),
|
|
|
|
onClick = onClick
|
2023-03-08 22:07:56 +00:00
|
|
|
) {
|
2023-04-20 21:19:34 +00:00
|
|
|
Icon(
|
|
|
|
imageVector = Icons.Default.ExpandMore,
|
|
|
|
null,
|
|
|
|
modifier = Modifier.size(15.dp),
|
|
|
|
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
|
|
|
)
|
2023-02-06 22:16:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-26 16:16:57 +00:00
|
|
|
@Composable
|
2023-02-01 01:12:24 +00:00
|
|
|
fun NoteAuthorPicture(
|
2023-05-06 15:49:53 +00:00
|
|
|
baseNote: Note,
|
2023-01-26 16:16:57 +00:00
|
|
|
navController: NavController,
|
|
|
|
userAccount: User,
|
|
|
|
size: Dp,
|
|
|
|
pictureModifier: Modifier = Modifier
|
|
|
|
) {
|
2023-05-06 15:49:53 +00:00
|
|
|
NoteAuthorPicture(baseNote, userAccount, size, pictureModifier) {
|
2023-02-01 01:12:24 +00:00
|
|
|
navController.navigate("User/${it.pubkeyHex}")
|
2023-01-26 16:16:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
2023-02-01 01:12:24 +00:00
|
|
|
fun NoteAuthorPicture(
|
|
|
|
baseNote: Note,
|
|
|
|
baseUserAccount: User,
|
2023-01-26 16:16:57 +00:00
|
|
|
size: Dp,
|
2023-03-13 17:47:44 +00:00
|
|
|
modifier: Modifier = Modifier,
|
2023-02-01 01:12:24 +00:00
|
|
|
onClick: ((User) -> Unit)? = null
|
2023-01-26 16:16:57 +00:00
|
|
|
) {
|
2023-02-19 16:22:01 +00:00
|
|
|
val noteState by baseNote.live().metadata.observeAsState()
|
2023-02-01 01:12:24 +00:00
|
|
|
val note = noteState?.note ?: return
|
|
|
|
|
|
|
|
val author = note.author
|
2023-01-28 01:05:22 +00:00
|
|
|
|
2023-01-26 16:16:57 +00:00
|
|
|
Box(
|
|
|
|
Modifier
|
|
|
|
.width(size)
|
2023-03-08 22:07:56 +00:00
|
|
|
.height(size)
|
|
|
|
) {
|
2023-02-01 01:12:24 +00:00
|
|
|
if (author == null) {
|
2023-03-11 05:52:17 +00:00
|
|
|
RobohashAsyncImage(
|
|
|
|
robot = "authornotfound",
|
2023-04-20 14:07:03 +00:00
|
|
|
robotSize = size,
|
2023-02-28 18:20:49 +00:00
|
|
|
contentDescription = stringResource(R.string.unknown_author),
|
2023-03-13 17:47:44 +00:00
|
|
|
modifier = modifier
|
2023-03-15 15:21:53 +00:00
|
|
|
.width(size)
|
|
|
|
.height(size)
|
2023-01-26 16:16:57 +00:00
|
|
|
.clip(shape = CircleShape)
|
|
|
|
.background(MaterialTheme.colors.background)
|
|
|
|
)
|
|
|
|
} else {
|
2023-03-13 17:47:44 +00:00
|
|
|
UserPicture(author, baseUserAccount, size, modifier, onClick)
|
2023-02-01 01:12:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-01-28 01:05:22 +00:00
|
|
|
|
2023-02-01 01:12:24 +00:00
|
|
|
@Composable
|
|
|
|
fun UserPicture(
|
|
|
|
user: User,
|
|
|
|
navController: NavController,
|
|
|
|
userAccount: User,
|
|
|
|
size: Dp,
|
|
|
|
pictureModifier: Modifier = Modifier
|
|
|
|
) {
|
|
|
|
UserPicture(user, userAccount, size, pictureModifier) {
|
|
|
|
navController.navigate("User/${it.pubkeyHex}")
|
|
|
|
}
|
|
|
|
}
|
2023-01-26 16:16:57 +00:00
|
|
|
|
2023-05-06 23:19:57 +00:00
|
|
|
@OptIn(ExperimentalFoundationApi::class)
|
2023-02-01 01:12:24 +00:00
|
|
|
@Composable
|
|
|
|
fun UserPicture(
|
|
|
|
baseUser: User,
|
|
|
|
baseUserAccount: User,
|
|
|
|
size: Dp,
|
2023-03-13 17:47:44 +00:00
|
|
|
modifier: Modifier = Modifier,
|
2023-03-01 18:29:58 +00:00
|
|
|
onClick: ((User) -> Unit)? = null,
|
|
|
|
onLongClick: ((User) -> Unit)? = null
|
2023-02-01 01:12:24 +00:00
|
|
|
) {
|
2023-02-19 16:22:01 +00:00
|
|
|
val userState by baseUser.live().metadata.observeAsState()
|
2023-02-01 01:12:24 +00:00
|
|
|
val user = userState?.user ?: return
|
|
|
|
|
2023-03-15 15:39:30 +00:00
|
|
|
val accountState by baseUserAccount.live().follows.observeAsState()
|
|
|
|
val accountUser = accountState?.user ?: return
|
|
|
|
|
|
|
|
val showFollowingMark = accountUser.isFollowingCached(user) || user == accountUser
|
|
|
|
|
2023-05-06 23:19:57 +00:00
|
|
|
Row(
|
|
|
|
modifier = Modifier
|
|
|
|
.run {
|
|
|
|
if (onClick != null && onLongClick != null) {
|
|
|
|
this.combinedClickable(
|
|
|
|
onClick = { onClick(user) },
|
|
|
|
onLongClick = { onLongClick(user) }
|
|
|
|
)
|
|
|
|
} else if (onClick != null) {
|
|
|
|
this.clickable(onClick = { onClick(user) })
|
|
|
|
} else {
|
|
|
|
this
|
|
|
|
}
|
|
|
|
}
|
|
|
|
) {
|
|
|
|
UserPicture(
|
|
|
|
userHex = user.pubkeyHex,
|
|
|
|
userPicture = user.profilePicture(),
|
|
|
|
showFollowingMark = showFollowingMark,
|
|
|
|
size = size,
|
|
|
|
modifier = modifier
|
|
|
|
)
|
|
|
|
}
|
2023-03-15 15:39:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
fun UserPicture(
|
|
|
|
userHex: String,
|
|
|
|
userPicture: String?,
|
|
|
|
showFollowingMark: Boolean,
|
|
|
|
size: Dp,
|
2023-05-06 23:19:57 +00:00
|
|
|
modifier: Modifier = Modifier
|
2023-03-15 15:39:30 +00:00
|
|
|
) {
|
2023-02-01 01:12:24 +00:00
|
|
|
Box(
|
|
|
|
Modifier
|
|
|
|
.width(size)
|
2023-03-08 22:07:56 +00:00
|
|
|
.height(size)
|
|
|
|
) {
|
2023-03-11 05:52:17 +00:00
|
|
|
RobohashAsyncImageProxy(
|
2023-03-15 15:39:30 +00:00
|
|
|
robot = userHex,
|
|
|
|
model = ResizeImage(userPicture, size),
|
2023-02-28 18:20:49 +00:00
|
|
|
contentDescription = stringResource(id = R.string.profile_image),
|
2023-03-13 17:47:44 +00:00
|
|
|
modifier = modifier
|
2023-03-15 15:21:53 +00:00
|
|
|
.width(size)
|
|
|
|
.height(size)
|
2023-02-01 01:12:24 +00:00
|
|
|
.clip(shape = CircleShape)
|
|
|
|
.background(MaterialTheme.colors.background)
|
|
|
|
)
|
|
|
|
|
2023-03-15 15:39:30 +00:00
|
|
|
if (showFollowingMark) {
|
2023-02-01 01:12:24 +00:00
|
|
|
Box(
|
|
|
|
Modifier
|
|
|
|
.width(size.div(3.5f))
|
|
|
|
.height(size.div(3.5f))
|
|
|
|
.align(Alignment.TopEnd),
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
) {
|
|
|
|
// Background for the transparent checkmark
|
2023-02-22 15:26:05 +00:00
|
|
|
Box(
|
2023-02-01 01:12:24 +00:00
|
|
|
Modifier
|
2023-02-22 15:26:05 +00:00
|
|
|
.clip(CircleShape)
|
|
|
|
.fillMaxSize(0.6f)
|
2023-02-01 01:12:24 +00:00
|
|
|
.align(Alignment.Center)
|
|
|
|
.background(MaterialTheme.colors.background)
|
|
|
|
)
|
|
|
|
|
|
|
|
Icon(
|
|
|
|
painter = painterResource(R.drawable.ic_verified),
|
2023-02-28 18:20:49 +00:00
|
|
|
stringResource(id = R.string.following),
|
2023-02-01 01:12:24 +00:00
|
|
|
modifier = Modifier.fillMaxSize(),
|
|
|
|
tint = Following
|
|
|
|
)
|
2023-01-26 16:16:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-07 22:02:54 +00:00
|
|
|
data class DropDownParams(
|
|
|
|
val isFollowingAuthor: Boolean,
|
|
|
|
val isPrivateBookmarkNote: Boolean,
|
|
|
|
val isPublicBookmarkNote: Boolean,
|
|
|
|
val isLoggedUser: Boolean
|
|
|
|
)
|
|
|
|
|
2023-01-13 15:20:54 +00:00
|
|
|
@Composable
|
|
|
|
fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, accountViewModel: AccountViewModel) {
|
|
|
|
val clipboardManager = LocalClipboardManager.current
|
2023-03-09 14:34:25 +00:00
|
|
|
val appContext = LocalContext.current.applicationContext
|
|
|
|
val actContext = LocalContext.current
|
2023-03-14 17:41:39 +00:00
|
|
|
var reportDialogShowing by remember { mutableStateOf(false) }
|
2023-01-13 15:20:54 +00:00
|
|
|
|
2023-04-07 22:02:54 +00:00
|
|
|
var state by remember {
|
|
|
|
mutableStateOf<DropDownParams>(
|
|
|
|
DropDownParams(false, false, false, false)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
LaunchedEffect(key1 = note) {
|
2023-04-18 12:45:34 +00:00
|
|
|
withContext(Dispatchers.IO) {
|
|
|
|
state = DropDownParams(
|
|
|
|
accountViewModel.isFollowing(note.author),
|
|
|
|
accountViewModel.isInPrivateBookmarks(note),
|
|
|
|
accountViewModel.isInPublicBookmarks(note),
|
|
|
|
accountViewModel.isLoggedUser(note.author)
|
|
|
|
)
|
|
|
|
}
|
2023-04-07 22:02:54 +00:00
|
|
|
}
|
|
|
|
|
2023-01-13 15:20:54 +00:00
|
|
|
DropdownMenu(
|
|
|
|
expanded = popupExpanded,
|
|
|
|
onDismissRequest = onDismiss
|
|
|
|
) {
|
2023-04-07 22:02:54 +00:00
|
|
|
if (!state.isFollowingAuthor) {
|
2023-03-03 06:53:56 +00:00
|
|
|
DropdownMenuItem(onClick = {
|
|
|
|
accountViewModel.follow(
|
|
|
|
note.author ?: return@DropdownMenuItem
|
|
|
|
); onDismiss()
|
|
|
|
}) {
|
|
|
|
Text(stringResource(R.string.follow))
|
|
|
|
}
|
|
|
|
Divider()
|
|
|
|
}
|
2023-03-09 14:34:25 +00:00
|
|
|
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(accountViewModel.decrypt(note) ?: "")); onDismiss() }) {
|
|
|
|
Text(stringResource(R.string.copy_text))
|
|
|
|
}
|
2023-04-24 21:58:35 +00:00
|
|
|
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString("nostr:${note.author?.pubkeyNpub()}")); onDismiss() }) {
|
2023-03-09 14:34:25 +00:00
|
|
|
Text(stringResource(R.string.copy_user_pubkey))
|
|
|
|
}
|
2023-04-24 21:58:35 +00:00
|
|
|
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString("nostr:" + note.toNEvent())); onDismiss() }) {
|
2023-03-09 14:34:25 +00:00
|
|
|
Text(stringResource(R.string.copy_note_id))
|
|
|
|
}
|
|
|
|
DropdownMenuItem(onClick = {
|
|
|
|
val sendIntent = Intent().apply {
|
|
|
|
action = Intent.ACTION_SEND
|
|
|
|
type = "text/plain"
|
|
|
|
putExtra(
|
|
|
|
Intent.EXTRA_TEXT,
|
|
|
|
externalLinkForNote(note)
|
|
|
|
)
|
|
|
|
putExtra(Intent.EXTRA_TITLE, actContext.getString(R.string.quick_action_share_browser_link))
|
|
|
|
}
|
|
|
|
|
|
|
|
val shareIntent = Intent.createChooser(sendIntent, appContext.getString(R.string.quick_action_share))
|
|
|
|
ContextCompat.startActivity(actContext, shareIntent, null)
|
|
|
|
onDismiss()
|
|
|
|
}) {
|
|
|
|
Text(stringResource(R.string.quick_action_share))
|
|
|
|
}
|
|
|
|
Divider()
|
2023-04-07 22:02:54 +00:00
|
|
|
if (state.isPrivateBookmarkNote) {
|
2023-03-20 22:16:07 +00:00
|
|
|
DropdownMenuItem(onClick = { accountViewModel.removePrivateBookmark(note); onDismiss() }) {
|
|
|
|
Text(stringResource(R.string.remove_from_private_bookmarks))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
DropdownMenuItem(onClick = { accountViewModel.addPrivateBookmark(note); onDismiss() }) {
|
|
|
|
Text(stringResource(R.string.add_to_private_bookmarks))
|
|
|
|
}
|
|
|
|
}
|
2023-04-07 22:02:54 +00:00
|
|
|
if (state.isPublicBookmarkNote) {
|
2023-03-20 22:16:07 +00:00
|
|
|
DropdownMenuItem(onClick = { accountViewModel.removePublicBookmark(note); onDismiss() }) {
|
|
|
|
Text(stringResource(R.string.remove_from_public_bookmarks))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
DropdownMenuItem(onClick = { accountViewModel.addPublicBookmark(note); onDismiss() }) {
|
|
|
|
Text(stringResource(R.string.add_to_public_bookmarks))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Divider()
|
2023-01-13 15:20:54 +00:00
|
|
|
DropdownMenuItem(onClick = { accountViewModel.broadcast(note); onDismiss() }) {
|
2023-02-28 18:20:49 +00:00
|
|
|
Text(stringResource(R.string.broadcast))
|
2023-01-13 15:20:54 +00:00
|
|
|
}
|
2023-03-15 17:12:21 +00:00
|
|
|
Divider()
|
2023-04-07 22:02:54 +00:00
|
|
|
if (state.isLoggedUser) {
|
2023-02-26 17:02:07 +00:00
|
|
|
DropdownMenuItem(onClick = { accountViewModel.delete(note); onDismiss() }) {
|
2023-03-11 04:00:35 +00:00
|
|
|
Text(stringResource(R.string.request_deletion))
|
2023-02-26 17:02:07 +00:00
|
|
|
}
|
2023-03-15 17:12:21 +00:00
|
|
|
} else {
|
2023-03-14 17:41:39 +00:00
|
|
|
DropdownMenuItem(onClick = { reportDialogShowing = true }) {
|
|
|
|
Text("Block / Report")
|
2023-02-22 21:17:56 +00:00
|
|
|
}
|
2023-01-30 01:06:48 +00:00
|
|
|
}
|
2023-01-13 15:20:54 +00:00
|
|
|
}
|
2023-03-14 17:41:39 +00:00
|
|
|
|
|
|
|
if (reportDialogShowing) {
|
|
|
|
ReportNoteDialog(note = note, accountViewModel = accountViewModel) {
|
|
|
|
reportDialogShowing = false
|
|
|
|
onDismiss()
|
|
|
|
}
|
|
|
|
}
|
2023-03-05 21:42:19 +00:00
|
|
|
}
|