Moves channel checks to happen before a Channel object is loaded.

This commit is contained in:
Vitor Pamplona
2023-06-01 19:56:31 -04:00
parent 1a3685c5ab
commit 42428c5e0f
20 changed files with 338 additions and 397 deletions
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
@@ -9,6 +10,7 @@ import fr.acinq.secp256k1.Hex
import kotlinx.coroutines.Dispatchers
import java.util.concurrent.ConcurrentHashMap
@Stable
class Channel(val idHex: String) {
var creator: User? = null
var info = ChannelCreateEvent.ChannelData(null, null, null)
@@ -33,6 +35,10 @@ class Channel(val idHex: String) {
notes.remove(note.idHex)
}
fun removeNote(noteHex: String) {
notes.remove(noteHex)
}
fun updateChannelInfo(creator: User, channelInfo: ChannelCreateEvent.ChannelData, updatedAt: Long) {
this.creator = creator
this.info = channelInfo
@@ -429,8 +429,10 @@ object LocalCache {
masterNote.removeReport(deleteNote)
}
val channel = deleteNote.channel()
channel?.removeNote(deleteNote)
deleteNote.channelHex()?.let {
val channel = checkGetOrCreateChannel(it)
channel?.removeNote(deleteNote)
}
if (deleteNote.event is PrivateDmEvent) {
val author = deleteNote.author
@@ -73,10 +73,6 @@ open class Note(val idHex: String) {
?: (event as? ChannelCreateEvent)?.id
}
fun channel(): Channel? {
return channelHex()?.let { LocalCache.checkGetOrCreateChannel(it) }
}
open fun address(): ATag? = null
open fun createdAt() = event?.createdAt()
@@ -16,7 +16,12 @@ class ChannelMessageEvent(
sig: HexKey
) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun channel() = tags.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) ?: tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
fun channel() = tags.firstOrNull {
it.size > 3 && it[0] == "e" && it[3] == "root"
}?.get(1) ?: tags.firstOrNull {
it.size > 1 && it[0] == "e"
}?.get(1)
override fun replyTos() = tags.filter { it.firstOrNull() == "e" && it.getOrNull(1) != channel() }.mapNotNull { it.getOrNull(1) }
companion object {
@@ -16,7 +16,7 @@ class ChannelMetadataEvent(
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun channel() = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
fun channel() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
fun channelInfo() =
try {
MetadataEvent.gson.fromJson(content, ChannelCreateEvent.ChannelData::class.java)
@@ -23,14 +23,12 @@ class ContactListEvent(
// This function is only used by the user logged in
// But it is used all the time.
val verifiedFollowKeySet: Set<HexKey> by lazy {
tags.filter { it[0] == "p" }.mapNotNull {
it.getOrNull(1)?.let { unverifiedHex: String ->
try {
decodePublicKey(unverifiedHex).toHexKey()
} catch (e: Exception) {
Log.w("ContactListEvent", "Can't parse tags as a follows: ${it[1]}", e)
null
}
tags.filter { it.size > 1 && it[0] == "p" }.mapNotNull {
try {
decodePublicKey(it[1]).toHexKey()
} catch (e: Exception) {
Log.w("ContactListEvent", "Can't parse tags as a follows: ${it[1]}", e)
null
}
}.toSet()
}
@@ -1,13 +1,12 @@
package com.vitorpamplona.amethyst.ui.actions
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.parseDirtyWordForKey
import com.vitorpamplona.amethyst.service.nip19.Nip19
class NewMessageTagger(var channel: Channel?, var mentions: List<User>?, var replyTos: List<Note>?, var message: String) {
class NewMessageTagger(var channelHex: String?, var mentions: List<User>?, var replyTos: List<Note>?, var message: String) {
fun addUserToMentions(user: User) {
mentions = if (mentions?.contains(user) == true) mentions else mentions?.plus(user) ?: listOf(user)
@@ -20,12 +19,12 @@ class NewMessageTagger(var channel: Channel?, var mentions: List<User>?, var rep
fun tagIndex(user: User): Int {
// Postr Events assembles replies before mentions in the tag order
return (if (channel != null) 1 else 0) + (replyTos?.size ?: 0) + (mentions?.indexOf(user) ?: 0)
return (if (channelHex != null) 1 else 0) + (replyTos?.size ?: 0) + (mentions?.indexOf(user) ?: 0)
}
fun tagIndex(note: Note): Int {
// Postr Events assembles replies before mentions in the tag order
return (if (channel != null) 1 else 0) + (replyTos?.indexOf(note) ?: 0)
return (if (channelHex != null) 1 else 0) + (replyTos?.indexOf(note) ?: 0)
}
fun run() {
@@ -1,197 +0,0 @@
package com.vitorpamplona.amethyst.ui.actions
import android.widget.Toast
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.ui.components.*
import com.vitorpamplona.amethyst.ui.note.ReplyInformation
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
import kotlinx.coroutines.delay
@Composable
fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = null, accountViewModel: AccountViewModel) {
val pollViewModel: NewPostViewModel = viewModel()
val context = LocalContext.current
val scrollState = rememberScrollState()
LaunchedEffect(Unit) {
pollViewModel.load(accountViewModel.account, baseReplyTo, quote)
delay(100)
pollViewModel.imageUploadingError.collect { error ->
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
}
}
Dialog(
onDismissRequest = { onClose() },
properties = DialogProperties(
usePlatformDefaultWidth = false,
dismissOnClickOutside = false,
decorFitsSystemWindows = false
)
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
Column(
modifier = Modifier
.padding(start = 10.dp, end = 10.dp, top = 10.dp)
.imePadding()
.weight(1f)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = {
pollViewModel.cancel()
onClose()
})
PollButton(
onPost = {
pollViewModel.sendPost()
onClose()
},
isActive = pollViewModel.message.text.isNotBlank() &&
pollViewModel.pollOptions.values.all { it.isNotEmpty() } &&
pollViewModel.isValidRecipients.value &&
pollViewModel.isValidvalueMaximum.value &&
pollViewModel.isValidvalueMinimum.value &&
pollViewModel.isValidConsensusThreshold.value &&
pollViewModel.isValidClosedAt.value
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(scrollState)
) {
if (pollViewModel.replyTos != null && baseReplyTo?.event is TextNoteEvent) {
ReplyInformation(pollViewModel.replyTos, pollViewModel.mentions, accountViewModel, "") {
pollViewModel.removeFromReplyList(it)
}
}
Text(stringResource(R.string.poll_heading_required))
// NewPollRecipientsField(pollViewModel, account)
NewPollPrimaryDescription(pollViewModel)
pollViewModel.pollOptions.values.forEachIndexed { index, _ ->
NewPollOption(pollViewModel, index)
}
Button(
onClick = { pollViewModel.pollOptions[pollViewModel.pollOptions.size] = "" },
border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f)),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
) {
Image(
painterResource(id = android.R.drawable.ic_input_add),
contentDescription = "Add poll option button",
modifier = Modifier.size(18.dp)
)
}
Text(stringResource(R.string.poll_heading_optional))
NewPollVoteValueRange(pollViewModel)
NewPollConsensusThreshold(pollViewModel)
NewPollClosing(pollViewModel)
}
}
val userSuggestions = pollViewModel.userSuggestions
if (userSuggestions.isNotEmpty()) {
LazyColumn(
contentPadding = PaddingValues(
top = 10.dp
),
modifier = Modifier.heightIn(0.dp, 300.dp)
) {
itemsIndexed(
userSuggestions,
key = { _, item -> item.pubkeyHex }
) { _, item ->
UserLine(item, accountViewModel) {
pollViewModel.autocompleteWithUser(item)
}
}
}
}
Row(modifier = Modifier.fillMaxWidth()) {
/*UploadFromGallery(
isUploading = pollViewModel.isUploadingImage
) {
pollViewModel.upload(it, context)
}*/
}
}
}
}
}
}
@Composable
fun PollButton(modifier: Modifier = Modifier, onPost: () -> Unit = {}, isActive: Boolean) {
Button(
modifier = modifier,
onClick = {
if (isActive) {
onPost()
}
},
shape = RoundedCornerShape(20.dp),
colors = ButtonDefaults
.buttonColors(
backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray
)
) {
Text(text = stringResource(R.string.post_poll), color = Color.White)
}
}
/*@Preview
@Composable
fun NewPollViewPreview() {
NewPollView(onClose = {}, account = Account(loggedIn = Persona()))
}*/
@@ -102,7 +102,7 @@ open class NewPostViewModel : ViewModel() {
}
canAddInvoice = account.userProfile().info?.lnAddress() != null
canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channel() == null
canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null
contentToAddUrl = null
wantsForwardZapTo = false
@@ -114,7 +114,7 @@ open class NewPostViewModel : ViewModel() {
}
fun sendPost() {
val tagger = NewMessageTagger(originalNote?.channel(), mentions, replyTos, message.text)
val tagger = NewMessageTagger(originalNote?.channelHex(), mentions, replyTos, message.text)
tagger.run()
val zapReceiver = if (wantsForwardZapTo) {
@@ -129,8 +129,8 @@ open class NewPostViewModel : ViewModel() {
if (wantsPoll) {
account?.sendPoll(tagger.message, tagger.replyTos, tagger.mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt, zapReceiver, wantsToMarkAsSensitive)
} else if (originalNote?.channel() != null) {
account?.sendChannelMessage(tagger.message, tagger.channel!!.idHex, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive)
} else if (originalNote?.channelHex() != null) {
account?.sendChannelMessage(tagger.message, tagger.channelHex!!, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive)
} else if (originalNote?.event is PrivateDmEvent) {
account?.sendPrivateMessage(tagger.message, originalNote!!.author!!.pubkeyHex, originalNote!!, tagger.mentions, zapReceiver, wantsToMarkAsSensitive)
} else {
@@ -14,6 +14,7 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
@@ -45,9 +46,9 @@ import com.vitorpamplona.amethyst.service.NIP30Parser
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.ui.note.LoadChannel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable
fun ClickableRoute(
@@ -77,40 +78,52 @@ private fun DisplayEvent(
var noteBase by remember(nip19) { mutableStateOf<Note?>(null) }
LaunchedEffect(key1 = nip19.hex) {
withContext(Dispatchers.IO) {
noteBase = LocalCache.checkGetOrCreateNote(nip19.hex)
if (noteBase == null) {
launch(Dispatchers.IO) {
noteBase = LocalCache.checkGetOrCreateNote(nip19.hex)
}
}
}
noteBase?.let {
val noteState by it.live().metadata.observeAsState()
val note = remember(noteState) { noteState?.note } ?: return
val channel = remember(noteState) { note.channel() }
val channelHex = remember(noteState) { note.channelHex() }
val noteIdDisplayNote = remember(noteState) { "@${note.idDisplayNote()}" }
if (note.event is ChannelCreateEvent) {
CreateClickableText(
clickablePart = "@${note.idDisplayNote()}",
clickablePart = noteIdDisplayNote,
suffix = "${nip19.additionalChars} ",
route = "Channel/${nip19.hex}",
nav = nav
)
} else if (note.event is PrivateDmEvent) {
CreateClickableText(
clickablePart = "@${note.idDisplayNote()}",
clickablePart = noteIdDisplayNote,
suffix = "${nip19.additionalChars} ",
route = "Room/${note.author?.pubkeyHex}",
nav = nav
)
} else if (channel != null) {
CreateClickableText(
clickablePart = channel.toBestDisplayName(),
suffix = "${nip19.additionalChars} ",
route = "Channel/${channel.idHex}",
nav = nav
)
} else if (channelHex != null) {
LoadChannel(baseChannelHex = channelHex) { baseChannel ->
val channelState by baseChannel.live.observeAsState()
val channelDisplayName by remember(channelState) {
derivedStateOf {
channelState?.channel?.toBestDisplayName() ?: noteIdDisplayNote
}
}
CreateClickableText(
clickablePart = channelDisplayName,
suffix = "${nip19.additionalChars} ",
route = "Channel/${baseChannel.idHex}",
nav = nav
)
}
} else {
CreateClickableText(
clickablePart = "@${note.idDisplayNote()}",
clickablePart = noteIdDisplayNote,
suffix = "${nip19.additionalChars} ",
route = "Event/${nip19.hex}",
nav = nav
@@ -141,32 +154,42 @@ private fun DisplayNote(
noteBase?.let {
val noteState by it.live().metadata.observeAsState()
val note = remember(noteState) { noteState?.note } ?: return
val channel = note.channel()
val channelHex = note.channelHex()
val noteIdDisplayNote = remember(noteState) { "@${note.idDisplayNote()}" }
if (note.event is ChannelCreateEvent) {
CreateClickableText(
clickablePart = "@${note.idDisplayNote()}",
clickablePart = noteIdDisplayNote,
suffix = "${nip19.additionalChars} ",
route = "Channel/${nip19.hex}",
nav = nav
)
} else if (note.event is PrivateDmEvent) {
CreateClickableText(
clickablePart = "@${note.idDisplayNote()}",
clickablePart = noteIdDisplayNote,
suffix = "${nip19.additionalChars} ",
route = "Room/${note.author?.pubkeyHex}",
nav = nav
)
} else if (channel != null) {
CreateClickableText(
clickablePart = channel.toBestDisplayName(),
suffix = "${nip19.additionalChars} ",
route = "Channel/${note.channel()?.idHex}",
nav = nav
)
} else if (channelHex != null) {
LoadChannel(baseChannelHex = channelHex) { baseChannel ->
val channelState by baseChannel.live.observeAsState()
val channelDisplayName by remember(channelState) {
derivedStateOf {
channelState?.channel?.toBestDisplayName() ?: noteIdDisplayNote
}
}
CreateClickableText(
clickablePart = channelDisplayName,
suffix = "${nip19.additionalChars} ",
route = "Channel/${baseChannel.idHex}",
nav = nav
)
}
} else {
CreateClickableText(
clickablePart = "@${note.idDisplayNote()}",
clickablePart = noteIdDisplayNote,
suffix = "${nip19.additionalChars} ",
route = "Note/${nip19.hex}",
nav = nav
@@ -434,7 +457,9 @@ fun ClickableInLineIconRenderer(wordsInOrder: List<Renderable>, style: SpanStyle
AsyncImage(
model = value.url,
contentDescription = null,
modifier = Modifier.fillMaxSize().padding(1.dp)
modifier = Modifier
.fillMaxSize()
.padding(1.dp)
)
}
)
@@ -498,7 +523,9 @@ fun InLineIconRenderer(
AsyncImage(
model = value.url,
contentDescription = null,
modifier = Modifier.fillMaxSize().padding(horizontal = 1.dp)
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 1.dp)
)
}
)
@@ -228,7 +228,6 @@ fun AppNavigation(
composable(route.route, route.arguments, content = {
LoadRedirectScreen(
eventId = it.arguments?.getString("id"),
accountViewModel = accountViewModel,
navController = navController
)
})
@@ -18,6 +18,7 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
@@ -66,39 +67,59 @@ fun ChatroomCompose(
val scope = rememberCoroutineScope()
val channelHex by remember(noteState) {
derivedStateOf {
noteState?.note?.channelHex()
}
}
if (note?.event == null) {
BlankNote(Modifier)
} else if (note.channel() != null) {
val authorState by note.author!!.live().metadata.observeAsState()
val author = authorState?.user
} else if (channelHex != null) {
LoadChannel(baseChannelHex = channelHex!!) { channel ->
val authorState by note.author!!.live().metadata.observeAsState()
val authorName = remember(authorState) {
authorState?.user?.toBestDisplayName()
}
val channelState by note.channel()!!.live.observeAsState()
val channel = channelState?.channel
val chanHex = remember { channel.idHex }
val noteEvent = note.event
val channelState by channel.live.observeAsState()
val channelPicture by remember(channelState) {
derivedStateOf {
channel.profilePicture()
}
}
val channelName by remember(channelState) {
derivedStateOf {
channel.info.name
}
}
val noteEvent = note.event
val description = if (noteEvent is ChannelCreateEvent) {
stringResource(R.string.channel_created)
} else if (noteEvent is ChannelMetadataEvent) {
"${stringResource(R.string.channel_information_changed_to)} "
} else {
noteEvent?.content()
}
val description = if (noteEvent is ChannelCreateEvent) {
stringResource(R.string.channel_created)
} else if (noteEvent is ChannelMetadataEvent) {
"${stringResource(R.string.channel_information_changed_to)} "
} else {
noteEvent?.content()
}
channel?.let { chan ->
var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = notificationCache, key2 = note) {
scope.launch(Dispatchers.IO) {
note.createdAt()?.let { timestamp ->
hasNewMessages =
timestamp > notificationCache.cache.load("Channel/${chan.idHex}")
timestamp > notificationCache.cache.load("Channel/$chanHex")
}
}
}
ChannelName(
channelIdHex = chan.idHex,
channelPicture = chan.profilePicture(),
channelIdHex = chanHex,
channelPicture = channelPicture,
channelTitle = { modifier ->
Text(
text = buildAnnotatedString {
@@ -107,7 +128,7 @@ fun ChatroomCompose(
fontWeight = FontWeight.Bold
)
) {
append(chan.info.name)
append(channelName)
}
withStyle(
@@ -125,9 +146,9 @@ fun ChatroomCompose(
)
},
channelLastTime = note.createdAt(),
channelLastContent = "${author?.toBestDisplayName()}: " + description,
channelLastContent = "$authorName: $description",
hasNewMessages = hasNewMessages,
onClick = { nav("Channel/${chan.idHex}") }
onClick = { nav("Channel/$chanHex") }
)
}
} else {
@@ -115,7 +115,7 @@ fun ChatroomMessageCompose(
var isAcceptableAndCanPreview by remember { mutableStateOf(Pair(true, true)) }
LaunchedEffect(key1 = noteReportsState, key2 = accountState) {
withContext(Dispatchers.IO) {
launch(Dispatchers.Default) {
account.userProfile().let { loggedIn ->
val newCanPreview = note.author?.pubkeyHex == loggedIn.pubkeyHex ||
(note.author?.let { loggedIn.isFollowingCached(it) } ?: true) ||
@@ -138,6 +138,8 @@ 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
@@ -154,10 +156,12 @@ fun NoteCompose(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
println("NormalNote START NoteCompose")
val noteState by baseNote.live().metadata.observeAsState()
val noteEvent = remember(noteState) { noteState?.note?.event }
if (noteEvent == null) {
println("NormalNote Rendering Blank")
var popupExpanded by remember { mutableStateOf(false) }
BlankNote(
@@ -186,6 +190,7 @@ fun NoteCompose(
nav
)
}
println("NormalNote END NoteCompose")
}
@Composable
@@ -251,32 +256,24 @@ fun LoadedNoteCompose(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
var state by remember {
mutableStateOf(
NoteComposeReportState(
isAcceptable = true,
canPreview = true,
relevantReports = emptySet()
)
)
}
val noteReportsState by note.live().reports.observeAsState()
val noteForReports = remember(noteReportsState) { noteReportsState?.note } ?: return
var showReportedNote by remember { mutableStateOf(false) }
var state by remember { mutableStateOf(NoteComposeReportState(true, true, emptySet())) }
LaunchedEffect(key1 = noteReportsState, key2 = accountState) {
withContext(Dispatchers.IO) {
account.userProfile().let { loggedIn ->
val newCanPreview = note.author?.pubkeyHex == loggedIn.pubkeyHex ||
(note.author?.let { loggedIn.isFollowingCached(it) } ?: true) ||
!(noteForReports.hasAnyReports())
val newIsAcceptable = account.isAcceptable(noteForReports)
val newRelevantReports = account.getRelevantReports(noteForReports)
if (newIsAcceptable != state.isAcceptable || newCanPreview != state.canPreview) {
state = NoteComposeReportState(newIsAcceptable, newCanPreview, newRelevantReports)
}
}
WatchForReports(note, accountViewModel) { newIsAcceptable, newCanPreview, newRelevantReports ->
if (newIsAcceptable != state.isAcceptable || newCanPreview != state.canPreview) {
state = NoteComposeReportState(newIsAcceptable, newCanPreview, newRelevantReports)
}
}
var showReportedNote by remember { mutableStateOf(false) }
val showHiddenNote by remember(state, showReportedNote) {
derivedStateOf {
!state.isAcceptable && !showReportedNote
@@ -316,7 +313,35 @@ fun LoadedNoteCompose(
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun WatchForReports(
note: Note,
accountViewModel: AccountViewModel,
onChange: (Boolean, Boolean, Set<Note>) -> Unit
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
val noteReportsState by note.live().reports.observeAsState()
val noteForReports = remember(noteReportsState) { noteReportsState?.note } ?: return
LaunchedEffect(key1 = noteReportsState, key2 = accountState) {
launch(Dispatchers.Default) {
account.userProfile().let { loggedIn ->
val newCanPreview = note.author?.pubkeyHex == loggedIn.pubkeyHex ||
(note.author?.let { loggedIn.isFollowingCached(it) } ?: true) ||
!(noteForReports.hasAnyReports())
val newIsAcceptable = account.isAcceptable(noteForReports)
val newRelevantReports = account.getRelevantReports(noteForReports)
onChange(newIsAcceptable, newCanPreview, newRelevantReports)
}
}
}
}
@OptIn(ExperimentalFoundationApi::class, ExperimentalTime::class)
@Composable
fun NormalNote(
baseNote: Note,
@@ -346,6 +371,8 @@ fun NormalNote(
} else if (noteEvent is FileStorageHeaderEvent) {
FileStorageHeaderDisplay(baseNote)
} else {
println("NormalNote LoadedNoteCompose")
var isNew by remember { mutableStateOf<Boolean>(false) }
val scope = rememberCoroutineScope()
@@ -368,6 +395,8 @@ fun NormalNote(
}
}
println("NormalNote LoadedNoteCompose Launched Effect")
val primaryColor = MaterialTheme.colors.newItemBackgroundColor
val defaultBackgroundColor = MaterialTheme.colors.background
@@ -383,6 +412,8 @@ fun NormalNote(
}
}
println("NormalNote LoadedNoteCompose Background Color")
val columnModifier = remember(backgroundColor) {
modifier
.combinedClickable(
@@ -398,6 +429,8 @@ fun NormalNote(
.background(backgroundColor)
}
println("NormalNote LoadedNoteCompose Modifier")
Column(modifier = columnModifier) {
Row(
modifier = remember {
@@ -409,9 +442,14 @@ fun NormalNote(
)
}
) {
if (!isBoostedNote && !isQuotedNote) {
DrawAuthorImages(baseNote, accountViewModel, nav)
println("NormalNote Before FirstUserInfo")
val (value, elapsed) = measureTimedValue {
if (!isBoostedNote && !isQuotedNote) {
DrawAuthorImages(baseNote, accountViewModel, nav)
}
}
println("AAA $elapsed DrawAuthorImages")
println("NormalNote FirstUserInfo")
Column(
modifier = remember {
@@ -433,6 +471,7 @@ fun NormalNote(
nav
)
}
println("NormalNote SecondUserInfo")
Spacer(modifier = Modifier.height(2.dp))
@@ -521,8 +560,8 @@ fun routeFor(note: Note, loggedIn: User): String? {
val noteEvent = note.event
if (noteEvent is ChannelMessageEvent || noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) {
note.channel()?.let {
return "Channel/${it.idHex}"
note.channelHex()?.let {
return "Channel/$it"
}
} else if (noteEvent is PrivateDmEvent) {
return "Room/${noteEvent.talkingWith(loggedIn.pubkeyHex)}"
@@ -546,6 +585,7 @@ private fun RenderTextEvent(
if (eventContent != null) {
val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) }
if (makeItShort && isAuthorTheLoggedUser) {
Text(
text = eventContent,
@@ -1232,7 +1272,7 @@ private fun ReplyRow(
Spacer(modifier = Modifier.height(5.dp))
} else if (noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())) {
note.channel()?.let {
note.channelHex()?.let {
ReplyInformationChannel(note.replyTo, noteEvent.mentions(), it, accountViewModel, nav)
}
@@ -1341,23 +1381,36 @@ fun TimeAgo(time: Long) {
)
}
@OptIn(ExperimentalTime::class)
@Composable
private fun DrawAuthorImages(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val baseChannel = remember { baseNote.channel() }
val baseChannelHex = remember { baseNote.channelHex() }
val modifier = remember { Modifier.width(55.dp) }
Column(modifier) {
// Draws the boosted picture outside the boosted card.
Box(modifier = modifier, contentAlignment = Alignment.BottomEnd) {
NoteAuthorPicture(baseNote, nav, accountViewModel, 55.dp)
if (baseNote.event is RepostEvent) {
RepostNoteAuthorPicture(baseNote, accountViewModel, nav)
val (value1, elapsed1) = measureTimedValue {
NoteAuthorPicture(baseNote, nav, accountViewModel, 55.dp)
}
if (baseNote.event is ChannelMessageEvent && baseChannel != null) {
ChannelNotePicture(baseChannel)
println("AAA $elapsed1 NoteAuthorPicture")
val (value2, elapsed2) = measureTimedValue {
if (baseNote.event is RepostEvent) {
RepostNoteAuthorPicture(baseNote, accountViewModel, nav)
}
}
println("AAA $elapsed2 RepostNoteAuthorPicture")
val (value3, elapsed3) = measureTimedValue {
if (baseNote.event is ChannelMessageEvent && baseChannelHex != null) {
LoadChannel(baseChannelHex) { channel ->
ChannelNotePicture(channel)
}
}
}
println("AAA $elapsed3 ChannelNotePicture")
}
if (baseNote.event is RepostEvent) {
@@ -1373,10 +1426,29 @@ private fun DrawAuthorImages(baseNote: Note, accountViewModel: AccountViewModel,
}
}
@Composable
fun LoadChannel(baseChannelHex: String, content: @Composable (Channel) -> Unit) {
var channel by remember(baseChannelHex) {
mutableStateOf<Channel?>(null)
}
LaunchedEffect(key1 = baseChannelHex) {
if (channel == null) {
launch(Dispatchers.IO) {
channel = LocalCache.checkGetOrCreateChannel(baseChannelHex)
}
}
}
channel?.let {
content(it)
}
}
@Composable
private fun ChannelNotePicture(baseChannel: Channel) {
val channelState by baseChannel.live.observeAsState()
val channel = channelState?.channel
val channel = remember(channelState) { channelState?.channel } ?: return
val modifier = remember {
Modifier
@@ -1391,25 +1463,23 @@ private fun ChannelNotePicture(baseChannel: Channel) {
.height(30.dp)
}
if (channel != null) {
val model = remember(channelState) {
ResizeImage(channel.profilePicture(), 30.dp)
}
val model = remember(channelState) {
ResizeImage(channel.profilePicture(), 30.dp)
}
Box(boxModifier) {
RobohashAsyncImageProxy(
robot = channel.idHex,
model = model,
contentDescription = stringResource(R.string.group_picture),
modifier = modifier
.background(MaterialTheme.colors.background)
.border(
2.dp,
MaterialTheme.colors.background,
CircleShape
)
)
}
Box(boxModifier) {
RobohashAsyncImageProxy(
robot = channel.idHex,
model = model,
contentDescription = stringResource(R.string.group_picture),
modifier = modifier
.background(MaterialTheme.colors.background)
.border(
2.dp,
MaterialTheme.colors.background,
CircleShape
)
)
}
}
@@ -2153,8 +2223,10 @@ fun NoteAuthorPicture(
onClick: ((User) -> Unit)? = null
) {
val noteState by baseNote.live().metadata.observeAsState()
val author = remember(noteState) {
noteState?.note?.author
val author by remember(noteState) {
derivedStateOf {
noteState?.note?.author
}
}
val boxModifier = remember {
@@ -2179,7 +2251,7 @@ fun NoteAuthorPicture(
modifier = nullModifier.background(MaterialTheme.colors.background)
)
} else {
UserPicture(author, size, accountViewModel, modifier, onClick)
UserPicture(author!!, size, accountViewModel, modifier, onClick)
}
}
}
@@ -2253,15 +2325,12 @@ fun UserPicture(
accountViewModel: AccountViewModel
) {
val myBoxModifier = remember {
Modifier
.width(size)
.height(size)
Modifier.size(size)
}
val myImageModifier = remember {
modifier
.width(size)
.height(size)
.size(size)
.clip(shape = CircleShape)
}
@@ -2283,10 +2352,17 @@ fun UserPicture(
private fun ObserveAndDisplayFollowingMark(userHex: String, iconSize: Dp, accountViewModel: AccountViewModel) {
val accountFollowsState by accountViewModel.account.userProfile().live().follows.observeAsState()
val showFollowingMark by remember(accountFollowsState) {
derivedStateOf {
accountFollowsState?.user?.isFollowingCached(userHex) == true ||
(userHex == accountViewModel.account.userProfile().pubkeyHex)
var showFollowingMark by remember { mutableStateOf(false) }
LaunchedEffect(key1 = accountFollowsState) {
launch(Dispatchers.Default) {
val newShowFollowingMark =
accountFollowsState?.user?.isFollowingCached(userHex) == true ||
(userHex == accountViewModel.account.userProfile().pubkeyHex)
if (newShowFollowingMark != showFollowingMark) {
showFollowingMark = newShowFollowingMark
}
}
}
@@ -111,33 +111,34 @@ private fun VerticalDivider(color: Color) =
@Composable
fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, accountViewModel: AccountViewModel) {
val context = LocalContext.current
val primaryLight = lightenColor(MaterialTheme.colors.primary, 0.1f)
val cardShape = RoundedCornerShape(5.dp)
val clipboardManager = LocalClipboardManager.current
val scope = rememberCoroutineScope()
var showSelectTextDialog by remember(note) { mutableStateOf(false) }
var showDeleteAlertDialog by remember(note) { mutableStateOf(false) }
var showBlockAlertDialog by remember(note) { mutableStateOf(false) }
var showReportDialog by remember(note) { mutableStateOf(false) }
val backgroundColor = if (MaterialTheme.colors.isLight) {
MaterialTheme.colors.primary
} else {
MaterialTheme.colors.primary.copy(alpha = 0.32f).compositeOver(MaterialTheme.colors.background)
}
val showToast = { stringResource: Int ->
scope.launch {
Toast.makeText(
context,
context.getString(stringResource),
Toast.LENGTH_SHORT
).show()
}
}
if (popupExpanded) {
val context = LocalContext.current
val primaryLight = lightenColor(MaterialTheme.colors.primary, 0.1f)
val cardShape = RoundedCornerShape(5.dp)
val clipboardManager = LocalClipboardManager.current
val scope = rememberCoroutineScope()
val backgroundColor = if (MaterialTheme.colors.isLight) {
MaterialTheme.colors.primary
} else {
MaterialTheme.colors.primary.copy(alpha = 0.32f).compositeOver(MaterialTheme.colors.background)
}
val showToast = { stringResource: Int ->
scope.launch {
Toast.makeText(
context,
context.getString(stringResource),
Toast.LENGTH_SHORT
).show()
}
}
val isOwnNote = accountViewModel.isLoggedUser(note.author)
val isFollowingUser = !isOwnNote && accountViewModel.isFollowing(note.author)
@@ -29,36 +29,35 @@ fun ReplyInformation(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var dupMentions by remember { mutableStateOf<List<User>?>(null) }
var sortedMentions by remember { mutableStateOf<List<User>?>(null) }
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
dupMentions = mentions.mapNotNull { LocalCache.checkGetOrCreateUser(it) }
sortedMentions = mentions.mapNotNull { LocalCache.checkGetOrCreateUser(it) }
?.toSet()?.sortedBy { !accountViewModel.account.userProfile().isFollowingCached(it) }
}
}
if (dupMentions != null) {
ReplyInformation(replyTo, dupMentions, accountViewModel) {
if (sortedMentions != null) {
ReplyInformation(replyTo, sortedMentions) {
nav("User/${it.pubkeyHex}")
}
}
}
@Composable
fun ReplyInformation(
private fun ReplyInformation(
replyTo: List<Note>?,
dupMentions: List<User>?,
accountViewModel: AccountViewModel,
sortedMentions: List<User>?,
prefix: String = "",
onUserTagClick: (User) -> Unit
) {
val mentions = dupMentions?.toSet()?.sortedBy { !accountViewModel.account.userProfile().isFollowingCached(it) }
var expanded by remember { mutableStateOf((mentions?.size ?: 0) <= 2) }
var expanded by remember { mutableStateOf((sortedMentions?.size ?: 0) <= 2) }
FlowRow() {
if (mentions != null && mentions.isNotEmpty()) {
if (sortedMentions != null && sortedMentions.isNotEmpty()) {
if (replyTo != null && replyTo.isNotEmpty()) {
val repliesToDisplay = if (expanded) mentions else mentions.take(2)
val repliesToDisplay = if (expanded) sortedMentions else sortedMentions.take(2)
Text(
stringResource(R.string.replying_to),
@@ -98,7 +97,7 @@ fun ReplyInformation(
)
ClickableText(
AnnotatedString("${mentions.size - 2}"),
AnnotatedString("${sortedMentions.size - 2}"),
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(alpha = 0.52f), fontSize = 13.sp),
onClick = { expanded = true }
)
@@ -120,7 +119,7 @@ fun ReplyInformation(
fun ReplyInformationChannel(
replyTo: List<Note>?,
mentions: List<String>,
channel: Channel,
channelHex: String,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
@@ -136,17 +135,19 @@ fun ReplyInformationChannel(
}
if (sortedMentions != null) {
ReplyInformationChannel(
replyTo,
sortedMentions,
channel,
onUserTagClick = {
nav("User/${it.pubkeyHex}")
},
onChannelTagClick = {
nav("Channel/${it.idHex}")
}
)
LoadChannel(channelHex) { channel ->
ReplyInformationChannel(
replyTo,
sortedMentions,
channel,
onUserTagClick = {
nav("User/${it.pubkeyHex}")
},
onChannelTagClick = {
nav("Channel/${it.idHex}")
}
)
}
}
}
@@ -92,6 +92,7 @@ private fun FeedLoaded(
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val notificationCacheState = NotificationCache.live.observeAsState()
val notificationCache = notificationCacheState.value ?: return
@@ -99,9 +100,9 @@ private fun FeedLoaded(
if (markAsRead.value) {
for (note in state.feed.value) {
note.event?.let {
val channel = note.channel()
val route = if (channel != null) {
"Channel/${channel.idHex}"
val channelHex = note.channelHex()
val route = if (channelHex != null) {
"Channel/$channelHex"
} else {
val replyAuthorBase =
(note.event as? PrivateDmEvent)
@@ -33,6 +33,8 @@ import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
@OptIn(ExperimentalMaterialApi::class)
@Composable
@@ -143,6 +145,7 @@ private fun WatchScrollToTop(
}
}
@OptIn(ExperimentalTime::class)
@Composable
private fun FeedLoaded(
state: FeedState.Loaded,
@@ -163,14 +166,17 @@ private fun FeedLoaded(
state = listState
) {
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
NoteCompose(
item,
routeForLastRead = routeForLastRead,
modifier = baseModifier,
isBoostedNote = false,
accountViewModel = accountViewModel,
nav = nav
)
val (value, elapsed) = measureTimedValue {
NoteCompose(
item,
routeForLastRead = routeForLastRead,
modifier = baseModifier,
isBoostedNote = false,
accountViewModel = accountViewModel,
nav = nav
)
}
println("AAA NoteCompose $elapsed ${item.event?.content()}")
}
}
}
@@ -229,7 +229,7 @@ fun ChannelScreen(
trailingIcon = {
PostButton(
onPost = {
val tagger = NewMessageTagger(channel, listOfNotNull(replyTo.value?.author), listOfNotNull(replyTo.value), channelScreenModel.message.text)
val tagger = NewMessageTagger(channel.idHex, listOfNotNull(replyTo.value?.author), listOfNotNull(replyTo.value), channelScreenModel.message.text)
tagger.run()
account.sendChannelMessage(tagger.message, channel.idHex, tagger.replyTos, tagger.mentions, wantsToMarkAsSensitive = false)
channelScreenModel.message = TextFieldValue("")
@@ -21,7 +21,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
@Composable
fun LoadRedirectScreen(eventId: String?, accountViewModel: AccountViewModel, navController: NavController) {
fun LoadRedirectScreen(eventId: String?, navController: NavController) {
if (eventId == null) return
val baseNote = LocalCache.checkGetOrCreateNote(eventId) ?: return
@@ -31,7 +31,7 @@ fun LoadRedirectScreen(eventId: String?, accountViewModel: AccountViewModel, nav
LaunchedEffect(key1 = noteState) {
val event = note?.event
val channel = note?.channel()
val channelHex = note?.channelHex()
if (event == null) {
// stay here, loading
@@ -41,9 +41,9 @@ fun LoadRedirectScreen(eventId: String?, accountViewModel: AccountViewModel, nav
} else if (event is PrivateDmEvent) {
navController.backQueue.removeLast()
navController.navigate("Room/${note.author?.pubkeyHex}")
} else if (channel != null) {
} else if (channelHex != null) {
navController.backQueue.removeLast()
navController.navigate("Channel/${channel.idHex}")
navController.navigate("Channel/$channelHex")
} else {
navController.backQueue.removeLast()
navController.navigate("Note/${note.idHex}")