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 package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
@@ -9,6 +10,7 @@ import fr.acinq.secp256k1.Hex
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@Stable
class Channel(val idHex: String) { class Channel(val idHex: String) {
var creator: User? = null var creator: User? = null
var info = ChannelCreateEvent.ChannelData(null, null, null) var info = ChannelCreateEvent.ChannelData(null, null, null)
@@ -33,6 +35,10 @@ class Channel(val idHex: String) {
notes.remove(note.idHex) notes.remove(note.idHex)
} }
fun removeNote(noteHex: String) {
notes.remove(noteHex)
}
fun updateChannelInfo(creator: User, channelInfo: ChannelCreateEvent.ChannelData, updatedAt: Long) { fun updateChannelInfo(creator: User, channelInfo: ChannelCreateEvent.ChannelData, updatedAt: Long) {
this.creator = creator this.creator = creator
this.info = channelInfo this.info = channelInfo
@@ -429,8 +429,10 @@ object LocalCache {
masterNote.removeReport(deleteNote) masterNote.removeReport(deleteNote)
} }
val channel = deleteNote.channel() deleteNote.channelHex()?.let {
channel?.removeNote(deleteNote) val channel = checkGetOrCreateChannel(it)
channel?.removeNote(deleteNote)
}
if (deleteNote.event is PrivateDmEvent) { if (deleteNote.event is PrivateDmEvent) {
val author = deleteNote.author val author = deleteNote.author
@@ -73,10 +73,6 @@ open class Note(val idHex: String) {
?: (event as? ChannelCreateEvent)?.id ?: (event as? ChannelCreateEvent)?.id
} }
fun channel(): Channel? {
return channelHex()?.let { LocalCache.checkGetOrCreateChannel(it) }
}
open fun address(): ATag? = null open fun address(): ATag? = null
open fun createdAt() = event?.createdAt() open fun createdAt() = event?.createdAt()
@@ -16,7 +16,12 @@ class ChannelMessageEvent(
sig: HexKey sig: HexKey
) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) { ) : 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) } override fun replyTos() = tags.filter { it.firstOrNull() == "e" && it.getOrNull(1) != channel() }.mapNotNull { it.getOrNull(1) }
companion object { companion object {
@@ -16,7 +16,7 @@ class ChannelMetadataEvent(
content: String, content: String,
sig: HexKey sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) { ) : 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() = fun channelInfo() =
try { try {
MetadataEvent.gson.fromJson(content, ChannelCreateEvent.ChannelData::class.java) MetadataEvent.gson.fromJson(content, ChannelCreateEvent.ChannelData::class.java)
@@ -23,14 +23,12 @@ class ContactListEvent(
// This function is only used by the user logged in // This function is only used by the user logged in
// But it is used all the time. // But it is used all the time.
val verifiedFollowKeySet: Set<HexKey> by lazy { val verifiedFollowKeySet: Set<HexKey> by lazy {
tags.filter { it[0] == "p" }.mapNotNull { tags.filter { it.size > 1 && it[0] == "p" }.mapNotNull {
it.getOrNull(1)?.let { unverifiedHex: String -> try {
try { decodePublicKey(it[1]).toHexKey()
decodePublicKey(unverifiedHex).toHexKey() } catch (e: Exception) {
} catch (e: Exception) { Log.w("ContactListEvent", "Can't parse tags as a follows: ${it[1]}", e)
Log.w("ContactListEvent", "Can't parse tags as a follows: ${it[1]}", e) null
null
}
} }
}.toSet() }.toSet()
} }
@@ -1,13 +1,12 @@
package com.vitorpamplona.amethyst.ui.actions package com.vitorpamplona.amethyst.ui.actions
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.parseDirtyWordForKey import com.vitorpamplona.amethyst.model.parseDirtyWordForKey
import com.vitorpamplona.amethyst.service.nip19.Nip19 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) { fun addUserToMentions(user: User) {
mentions = if (mentions?.contains(user) == true) mentions else mentions?.plus(user) ?: listOf(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 { fun tagIndex(user: User): Int {
// Postr Events assembles replies before mentions in the tag order // 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 { fun tagIndex(note: Note): Int {
// Postr Events assembles replies before mentions in the tag order // 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() { 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 canAddInvoice = account.userProfile().info?.lnAddress() != null
canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channel() == null canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null
contentToAddUrl = null contentToAddUrl = null
wantsForwardZapTo = false wantsForwardZapTo = false
@@ -114,7 +114,7 @@ open class NewPostViewModel : ViewModel() {
} }
fun sendPost() { fun sendPost() {
val tagger = NewMessageTagger(originalNote?.channel(), mentions, replyTos, message.text) val tagger = NewMessageTagger(originalNote?.channelHex(), mentions, replyTos, message.text)
tagger.run() tagger.run()
val zapReceiver = if (wantsForwardZapTo) { val zapReceiver = if (wantsForwardZapTo) {
@@ -129,8 +129,8 @@ open class NewPostViewModel : ViewModel() {
if (wantsPoll) { if (wantsPoll) {
account?.sendPoll(tagger.message, tagger.replyTos, tagger.mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt, zapReceiver, wantsToMarkAsSensitive) account?.sendPoll(tagger.message, tagger.replyTos, tagger.mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt, zapReceiver, wantsToMarkAsSensitive)
} else if (originalNote?.channel() != null) { } else if (originalNote?.channelHex() != null) {
account?.sendChannelMessage(tagger.message, tagger.channel!!.idHex, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive) account?.sendChannelMessage(tagger.message, tagger.channelHex!!, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive)
} else if (originalNote?.event is PrivateDmEvent) { } else if (originalNote?.event is PrivateDmEvent) {
account?.sendPrivateMessage(tagger.message, originalNote!!.author!!.pubkeyHex, originalNote!!, tagger.mentions, zapReceiver, wantsToMarkAsSensitive) account?.sendPrivateMessage(tagger.message, originalNote!!.author!!.pubkeyHex, originalNote!!, tagger.mentions, zapReceiver, wantsToMarkAsSensitive)
} else { } else {
@@ -14,6 +14,7 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -45,9 +46,9 @@ import com.vitorpamplona.amethyst.service.NIP30Parser
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.service.nip19.Nip19 import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.ui.note.LoadChannel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable @Composable
fun ClickableRoute( fun ClickableRoute(
@@ -77,40 +78,52 @@ private fun DisplayEvent(
var noteBase by remember(nip19) { mutableStateOf<Note?>(null) } var noteBase by remember(nip19) { mutableStateOf<Note?>(null) }
LaunchedEffect(key1 = nip19.hex) { LaunchedEffect(key1 = nip19.hex) {
withContext(Dispatchers.IO) { if (noteBase == null) {
noteBase = LocalCache.checkGetOrCreateNote(nip19.hex) launch(Dispatchers.IO) {
noteBase = LocalCache.checkGetOrCreateNote(nip19.hex)
}
} }
} }
noteBase?.let { noteBase?.let {
val noteState by it.live().metadata.observeAsState() val noteState by it.live().metadata.observeAsState()
val note = remember(noteState) { noteState?.note } ?: return 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) { if (note.event is ChannelCreateEvent) {
CreateClickableText( CreateClickableText(
clickablePart = "@${note.idDisplayNote()}", clickablePart = noteIdDisplayNote,
suffix = "${nip19.additionalChars} ", suffix = "${nip19.additionalChars} ",
route = "Channel/${nip19.hex}", route = "Channel/${nip19.hex}",
nav = nav nav = nav
) )
} else if (note.event is PrivateDmEvent) { } else if (note.event is PrivateDmEvent) {
CreateClickableText( CreateClickableText(
clickablePart = "@${note.idDisplayNote()}", clickablePart = noteIdDisplayNote,
suffix = "${nip19.additionalChars} ", suffix = "${nip19.additionalChars} ",
route = "Room/${note.author?.pubkeyHex}", route = "Room/${note.author?.pubkeyHex}",
nav = nav nav = nav
) )
} else if (channel != null) { } else if (channelHex != null) {
CreateClickableText( LoadChannel(baseChannelHex = channelHex) { baseChannel ->
clickablePart = channel.toBestDisplayName(), val channelState by baseChannel.live.observeAsState()
suffix = "${nip19.additionalChars} ", val channelDisplayName by remember(channelState) {
route = "Channel/${channel.idHex}", derivedStateOf {
nav = nav channelState?.channel?.toBestDisplayName() ?: noteIdDisplayNote
) }
}
CreateClickableText(
clickablePart = channelDisplayName,
suffix = "${nip19.additionalChars} ",
route = "Channel/${baseChannel.idHex}",
nav = nav
)
}
} else { } else {
CreateClickableText( CreateClickableText(
clickablePart = "@${note.idDisplayNote()}", clickablePart = noteIdDisplayNote,
suffix = "${nip19.additionalChars} ", suffix = "${nip19.additionalChars} ",
route = "Event/${nip19.hex}", route = "Event/${nip19.hex}",
nav = nav nav = nav
@@ -141,32 +154,42 @@ private fun DisplayNote(
noteBase?.let { noteBase?.let {
val noteState by it.live().metadata.observeAsState() val noteState by it.live().metadata.observeAsState()
val note = remember(noteState) { noteState?.note } ?: return 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) { if (note.event is ChannelCreateEvent) {
CreateClickableText( CreateClickableText(
clickablePart = "@${note.idDisplayNote()}", clickablePart = noteIdDisplayNote,
suffix = "${nip19.additionalChars} ", suffix = "${nip19.additionalChars} ",
route = "Channel/${nip19.hex}", route = "Channel/${nip19.hex}",
nav = nav nav = nav
) )
} else if (note.event is PrivateDmEvent) { } else if (note.event is PrivateDmEvent) {
CreateClickableText( CreateClickableText(
clickablePart = "@${note.idDisplayNote()}", clickablePart = noteIdDisplayNote,
suffix = "${nip19.additionalChars} ", suffix = "${nip19.additionalChars} ",
route = "Room/${note.author?.pubkeyHex}", route = "Room/${note.author?.pubkeyHex}",
nav = nav nav = nav
) )
} else if (channel != null) { } else if (channelHex != null) {
CreateClickableText( LoadChannel(baseChannelHex = channelHex) { baseChannel ->
clickablePart = channel.toBestDisplayName(), val channelState by baseChannel.live.observeAsState()
suffix = "${nip19.additionalChars} ", val channelDisplayName by remember(channelState) {
route = "Channel/${note.channel()?.idHex}", derivedStateOf {
nav = nav channelState?.channel?.toBestDisplayName() ?: noteIdDisplayNote
) }
}
CreateClickableText(
clickablePart = channelDisplayName,
suffix = "${nip19.additionalChars} ",
route = "Channel/${baseChannel.idHex}",
nav = nav
)
}
} else { } else {
CreateClickableText( CreateClickableText(
clickablePart = "@${note.idDisplayNote()}", clickablePart = noteIdDisplayNote,
suffix = "${nip19.additionalChars} ", suffix = "${nip19.additionalChars} ",
route = "Note/${nip19.hex}", route = "Note/${nip19.hex}",
nav = nav nav = nav
@@ -434,7 +457,9 @@ fun ClickableInLineIconRenderer(wordsInOrder: List<Renderable>, style: SpanStyle
AsyncImage( AsyncImage(
model = value.url, model = value.url,
contentDescription = null, contentDescription = null,
modifier = Modifier.fillMaxSize().padding(1.dp) modifier = Modifier
.fillMaxSize()
.padding(1.dp)
) )
} }
) )
@@ -498,7 +523,9 @@ fun InLineIconRenderer(
AsyncImage( AsyncImage(
model = value.url, model = value.url,
contentDescription = null, 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 = { composable(route.route, route.arguments, content = {
LoadRedirectScreen( LoadRedirectScreen(
eventId = it.arguments?.getString("id"), eventId = it.arguments?.getString("id"),
accountViewModel = accountViewModel,
navController = navController navController = navController
) )
}) })
@@ -18,6 +18,7 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -66,39 +67,59 @@ fun ChatroomCompose(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val channelHex by remember(noteState) {
derivedStateOf {
noteState?.note?.channelHex()
}
}
if (note?.event == null) { if (note?.event == null) {
BlankNote(Modifier) BlankNote(Modifier)
} else if (note.channel() != null) { } else if (channelHex != null) {
val authorState by note.author!!.live().metadata.observeAsState() LoadChannel(baseChannelHex = channelHex!!) { channel ->
val author = authorState?.user val authorState by note.author!!.live().metadata.observeAsState()
val authorName = remember(authorState) {
authorState?.user?.toBestDisplayName()
}
val channelState by note.channel()!!.live.observeAsState() val chanHex = remember { channel.idHex }
val channel = channelState?.channel
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) } var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = notificationCache, key2 = note) { LaunchedEffect(key1 = notificationCache, key2 = note) {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
note.createdAt()?.let { timestamp -> note.createdAt()?.let { timestamp ->
hasNewMessages = hasNewMessages =
timestamp > notificationCache.cache.load("Channel/${chan.idHex}") timestamp > notificationCache.cache.load("Channel/$chanHex")
} }
} }
} }
ChannelName( ChannelName(
channelIdHex = chan.idHex, channelIdHex = chanHex,
channelPicture = chan.profilePicture(), channelPicture = channelPicture,
channelTitle = { modifier -> channelTitle = { modifier ->
Text( Text(
text = buildAnnotatedString { text = buildAnnotatedString {
@@ -107,7 +128,7 @@ fun ChatroomCompose(
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
) { ) {
append(chan.info.name) append(channelName)
} }
withStyle( withStyle(
@@ -125,9 +146,9 @@ fun ChatroomCompose(
) )
}, },
channelLastTime = note.createdAt(), channelLastTime = note.createdAt(),
channelLastContent = "${author?.toBestDisplayName()}: " + description, channelLastContent = "$authorName: $description",
hasNewMessages = hasNewMessages, hasNewMessages = hasNewMessages,
onClick = { nav("Channel/${chan.idHex}") } onClick = { nav("Channel/$chanHex") }
) )
} }
} else { } else {
@@ -115,7 +115,7 @@ fun ChatroomMessageCompose(
var isAcceptableAndCanPreview by remember { mutableStateOf(Pair(true, true)) } var isAcceptableAndCanPreview by remember { mutableStateOf(Pair(true, true)) }
LaunchedEffect(key1 = noteReportsState, key2 = accountState) { LaunchedEffect(key1 = noteReportsState, key2 = accountState) {
withContext(Dispatchers.IO) { launch(Dispatchers.Default) {
account.userProfile().let { loggedIn -> account.userProfile().let { loggedIn ->
val newCanPreview = note.author?.pubkeyHex == loggedIn.pubkeyHex || val newCanPreview = note.author?.pubkeyHex == loggedIn.pubkeyHex ||
(note.author?.let { loggedIn.isFollowingCached(it) } ?: true) || (note.author?.let { loggedIn.isFollowingCached(it) } ?: true) ||
@@ -138,6 +138,8 @@ import java.io.File
import java.math.BigDecimal import java.math.BigDecimal
import java.net.URL import java.net.URL
import java.util.Locale import java.util.Locale
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@@ -154,10 +156,12 @@ fun NoteCompose(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
println("NormalNote START NoteCompose")
val noteState by baseNote.live().metadata.observeAsState() val noteState by baseNote.live().metadata.observeAsState()
val noteEvent = remember(noteState) { noteState?.note?.event } val noteEvent = remember(noteState) { noteState?.note?.event }
if (noteEvent == null) { if (noteEvent == null) {
println("NormalNote Rendering Blank")
var popupExpanded by remember { mutableStateOf(false) } var popupExpanded by remember { mutableStateOf(false) }
BlankNote( BlankNote(
@@ -186,6 +190,7 @@ fun NoteCompose(
nav nav
) )
} }
println("NormalNote END NoteCompose")
} }
@Composable @Composable
@@ -251,32 +256,24 @@ fun LoadedNoteCompose(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
val accountState by accountViewModel.accountLiveData.observeAsState() var state by remember {
val account = remember(accountState) { accountState?.account } ?: return mutableStateOf(
NoteComposeReportState(
isAcceptable = true,
canPreview = true,
relevantReports = emptySet()
)
)
}
val noteReportsState by note.live().reports.observeAsState() WatchForReports(note, accountViewModel) { newIsAcceptable, newCanPreview, newRelevantReports ->
val noteForReports = remember(noteReportsState) { noteReportsState?.note } ?: return if (newIsAcceptable != state.isAcceptable || newCanPreview != state.canPreview) {
state = NoteComposeReportState(newIsAcceptable, newCanPreview, newRelevantReports)
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)
}
}
} }
} }
var showReportedNote by remember { mutableStateOf(false) }
val showHiddenNote by remember(state, showReportedNote) { val showHiddenNote by remember(state, showReportedNote) {
derivedStateOf { derivedStateOf {
!state.isAcceptable && !showReportedNote !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 @Composable
fun NormalNote( fun NormalNote(
baseNote: Note, baseNote: Note,
@@ -346,6 +371,8 @@ fun NormalNote(
} else if (noteEvent is FileStorageHeaderEvent) { } else if (noteEvent is FileStorageHeaderEvent) {
FileStorageHeaderDisplay(baseNote) FileStorageHeaderDisplay(baseNote)
} else { } else {
println("NormalNote LoadedNoteCompose")
var isNew by remember { mutableStateOf<Boolean>(false) } var isNew by remember { mutableStateOf<Boolean>(false) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -368,6 +395,8 @@ fun NormalNote(
} }
} }
println("NormalNote LoadedNoteCompose Launched Effect")
val primaryColor = MaterialTheme.colors.newItemBackgroundColor val primaryColor = MaterialTheme.colors.newItemBackgroundColor
val defaultBackgroundColor = MaterialTheme.colors.background val defaultBackgroundColor = MaterialTheme.colors.background
@@ -383,6 +412,8 @@ fun NormalNote(
} }
} }
println("NormalNote LoadedNoteCompose Background Color")
val columnModifier = remember(backgroundColor) { val columnModifier = remember(backgroundColor) {
modifier modifier
.combinedClickable( .combinedClickable(
@@ -398,6 +429,8 @@ fun NormalNote(
.background(backgroundColor) .background(backgroundColor)
} }
println("NormalNote LoadedNoteCompose Modifier")
Column(modifier = columnModifier) { Column(modifier = columnModifier) {
Row( Row(
modifier = remember { modifier = remember {
@@ -409,9 +442,14 @@ fun NormalNote(
) )
} }
) { ) {
if (!isBoostedNote && !isQuotedNote) { println("NormalNote Before FirstUserInfo")
DrawAuthorImages(baseNote, accountViewModel, nav) val (value, elapsed) = measureTimedValue {
if (!isBoostedNote && !isQuotedNote) {
DrawAuthorImages(baseNote, accountViewModel, nav)
}
} }
println("AAA $elapsed DrawAuthorImages")
println("NormalNote FirstUserInfo")
Column( Column(
modifier = remember { modifier = remember {
@@ -433,6 +471,7 @@ fun NormalNote(
nav nav
) )
} }
println("NormalNote SecondUserInfo")
Spacer(modifier = Modifier.height(2.dp)) Spacer(modifier = Modifier.height(2.dp))
@@ -521,8 +560,8 @@ fun routeFor(note: Note, loggedIn: User): String? {
val noteEvent = note.event val noteEvent = note.event
if (noteEvent is ChannelMessageEvent || noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) { if (noteEvent is ChannelMessageEvent || noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) {
note.channel()?.let { note.channelHex()?.let {
return "Channel/${it.idHex}" return "Channel/$it"
} }
} else if (noteEvent is PrivateDmEvent) { } else if (noteEvent is PrivateDmEvent) {
return "Room/${noteEvent.talkingWith(loggedIn.pubkeyHex)}" return "Room/${noteEvent.talkingWith(loggedIn.pubkeyHex)}"
@@ -546,6 +585,7 @@ private fun RenderTextEvent(
if (eventContent != null) { if (eventContent != null) {
val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) } val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) }
if (makeItShort && isAuthorTheLoggedUser) { if (makeItShort && isAuthorTheLoggedUser) {
Text( Text(
text = eventContent, text = eventContent,
@@ -1232,7 +1272,7 @@ private fun ReplyRow(
Spacer(modifier = Modifier.height(5.dp)) Spacer(modifier = Modifier.height(5.dp))
} else if (noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())) { } else if (noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())) {
note.channel()?.let { note.channelHex()?.let {
ReplyInformationChannel(note.replyTo, noteEvent.mentions(), it, accountViewModel, nav) ReplyInformationChannel(note.replyTo, noteEvent.mentions(), it, accountViewModel, nav)
} }
@@ -1341,23 +1381,36 @@ fun TimeAgo(time: Long) {
) )
} }
@OptIn(ExperimentalTime::class)
@Composable @Composable
private fun DrawAuthorImages(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) { 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) } val modifier = remember { Modifier.width(55.dp) }
Column(modifier) { Column(modifier) {
// Draws the boosted picture outside the boosted card. // Draws the boosted picture outside the boosted card.
Box(modifier = modifier, contentAlignment = Alignment.BottomEnd) { Box(modifier = modifier, contentAlignment = Alignment.BottomEnd) {
NoteAuthorPicture(baseNote, nav, accountViewModel, 55.dp) val (value1, elapsed1) = measureTimedValue {
NoteAuthorPicture(baseNote, nav, accountViewModel, 55.dp)
if (baseNote.event is RepostEvent) {
RepostNoteAuthorPicture(baseNote, accountViewModel, nav)
} }
if (baseNote.event is ChannelMessageEvent && baseChannel != null) { println("AAA $elapsed1 NoteAuthorPicture")
ChannelNotePicture(baseChannel)
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) { 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 @Composable
private fun ChannelNotePicture(baseChannel: Channel) { private fun ChannelNotePicture(baseChannel: Channel) {
val channelState by baseChannel.live.observeAsState() val channelState by baseChannel.live.observeAsState()
val channel = channelState?.channel val channel = remember(channelState) { channelState?.channel } ?: return
val modifier = remember { val modifier = remember {
Modifier Modifier
@@ -1391,25 +1463,23 @@ private fun ChannelNotePicture(baseChannel: Channel) {
.height(30.dp) .height(30.dp)
} }
if (channel != null) { val model = remember(channelState) {
val model = remember(channelState) { ResizeImage(channel.profilePicture(), 30.dp)
ResizeImage(channel.profilePicture(), 30.dp) }
}
Box(boxModifier) { Box(boxModifier) {
RobohashAsyncImageProxy( RobohashAsyncImageProxy(
robot = channel.idHex, robot = channel.idHex,
model = model, model = model,
contentDescription = stringResource(R.string.group_picture), contentDescription = stringResource(R.string.group_picture),
modifier = modifier modifier = modifier
.background(MaterialTheme.colors.background) .background(MaterialTheme.colors.background)
.border( .border(
2.dp, 2.dp,
MaterialTheme.colors.background, MaterialTheme.colors.background,
CircleShape CircleShape
) )
) )
}
} }
} }
@@ -2153,8 +2223,10 @@ fun NoteAuthorPicture(
onClick: ((User) -> Unit)? = null onClick: ((User) -> Unit)? = null
) { ) {
val noteState by baseNote.live().metadata.observeAsState() val noteState by baseNote.live().metadata.observeAsState()
val author = remember(noteState) { val author by remember(noteState) {
noteState?.note?.author derivedStateOf {
noteState?.note?.author
}
} }
val boxModifier = remember { val boxModifier = remember {
@@ -2179,7 +2251,7 @@ fun NoteAuthorPicture(
modifier = nullModifier.background(MaterialTheme.colors.background) modifier = nullModifier.background(MaterialTheme.colors.background)
) )
} else { } else {
UserPicture(author, size, accountViewModel, modifier, onClick) UserPicture(author!!, size, accountViewModel, modifier, onClick)
} }
} }
} }
@@ -2253,15 +2325,12 @@ fun UserPicture(
accountViewModel: AccountViewModel accountViewModel: AccountViewModel
) { ) {
val myBoxModifier = remember { val myBoxModifier = remember {
Modifier Modifier.size(size)
.width(size)
.height(size)
} }
val myImageModifier = remember { val myImageModifier = remember {
modifier modifier
.width(size) .size(size)
.height(size)
.clip(shape = CircleShape) .clip(shape = CircleShape)
} }
@@ -2283,10 +2352,17 @@ fun UserPicture(
private fun ObserveAndDisplayFollowingMark(userHex: String, iconSize: Dp, accountViewModel: AccountViewModel) { private fun ObserveAndDisplayFollowingMark(userHex: String, iconSize: Dp, accountViewModel: AccountViewModel) {
val accountFollowsState by accountViewModel.account.userProfile().live().follows.observeAsState() val accountFollowsState by accountViewModel.account.userProfile().live().follows.observeAsState()
val showFollowingMark by remember(accountFollowsState) { var showFollowingMark by remember { mutableStateOf(false) }
derivedStateOf {
accountFollowsState?.user?.isFollowingCached(userHex) == true || LaunchedEffect(key1 = accountFollowsState) {
(userHex == accountViewModel.account.userProfile().pubkeyHex) 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 @Composable
fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, accountViewModel: AccountViewModel) { 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 showSelectTextDialog by remember(note) { mutableStateOf(false) }
var showDeleteAlertDialog by remember(note) { mutableStateOf(false) } var showDeleteAlertDialog by remember(note) { mutableStateOf(false) }
var showBlockAlertDialog by remember(note) { mutableStateOf(false) } var showBlockAlertDialog by remember(note) { mutableStateOf(false) }
var showReportDialog 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) { 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 isOwnNote = accountViewModel.isLoggedUser(note.author)
val isFollowingUser = !isOwnNote && accountViewModel.isFollowing(note.author) val isFollowingUser = !isOwnNote && accountViewModel.isFollowing(note.author)
@@ -29,36 +29,35 @@ fun ReplyInformation(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
var dupMentions by remember { mutableStateOf<List<User>?>(null) } var sortedMentions by remember { mutableStateOf<List<User>?>(null) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
launch(Dispatchers.IO) { 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) { if (sortedMentions != null) {
ReplyInformation(replyTo, dupMentions, accountViewModel) { ReplyInformation(replyTo, sortedMentions) {
nav("User/${it.pubkeyHex}") nav("User/${it.pubkeyHex}")
} }
} }
} }
@Composable @Composable
fun ReplyInformation( private fun ReplyInformation(
replyTo: List<Note>?, replyTo: List<Note>?,
dupMentions: List<User>?, sortedMentions: List<User>?,
accountViewModel: AccountViewModel,
prefix: String = "", prefix: String = "",
onUserTagClick: (User) -> Unit onUserTagClick: (User) -> Unit
) { ) {
val mentions = dupMentions?.toSet()?.sortedBy { !accountViewModel.account.userProfile().isFollowingCached(it) } var expanded by remember { mutableStateOf((sortedMentions?.size ?: 0) <= 2) }
var expanded by remember { mutableStateOf((mentions?.size ?: 0) <= 2) }
FlowRow() { FlowRow() {
if (mentions != null && mentions.isNotEmpty()) { if (sortedMentions != null && sortedMentions.isNotEmpty()) {
if (replyTo != null && replyTo.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( Text(
stringResource(R.string.replying_to), stringResource(R.string.replying_to),
@@ -98,7 +97,7 @@ fun ReplyInformation(
) )
ClickableText( ClickableText(
AnnotatedString("${mentions.size - 2}"), AnnotatedString("${sortedMentions.size - 2}"),
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(alpha = 0.52f), fontSize = 13.sp), style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(alpha = 0.52f), fontSize = 13.sp),
onClick = { expanded = true } onClick = { expanded = true }
) )
@@ -120,7 +119,7 @@ fun ReplyInformation(
fun ReplyInformationChannel( fun ReplyInformationChannel(
replyTo: List<Note>?, replyTo: List<Note>?,
mentions: List<String>, mentions: List<String>,
channel: Channel, channelHex: String,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
@@ -136,17 +135,19 @@ fun ReplyInformationChannel(
} }
if (sortedMentions != null) { if (sortedMentions != null) {
ReplyInformationChannel( LoadChannel(channelHex) { channel ->
replyTo, ReplyInformationChannel(
sortedMentions, replyTo,
channel, sortedMentions,
onUserTagClick = { channel,
nav("User/${it.pubkeyHex}") onUserTagClick = {
}, nav("User/${it.pubkeyHex}")
onChannelTagClick = { },
nav("Channel/${it.idHex}") onChannelTagClick = {
} nav("Channel/${it.idHex}")
) }
)
}
} }
} }
@@ -92,6 +92,7 @@ private fun FeedLoaded(
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return val account = accountState?.account ?: return
val notificationCacheState = NotificationCache.live.observeAsState() val notificationCacheState = NotificationCache.live.observeAsState()
val notificationCache = notificationCacheState.value ?: return val notificationCache = notificationCacheState.value ?: return
@@ -99,9 +100,9 @@ private fun FeedLoaded(
if (markAsRead.value) { if (markAsRead.value) {
for (note in state.feed.value) { for (note in state.feed.value) {
note.event?.let { note.event?.let {
val channel = note.channel() val channelHex = note.channelHex()
val route = if (channel != null) { val route = if (channelHex != null) {
"Channel/${channel.idHex}" "Channel/$channelHex"
} else { } else {
val replyAuthorBase = val replyAuthorBase =
(note.event as? PrivateDmEvent) (note.event as? PrivateDmEvent)
@@ -33,6 +33,8 @@ import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@@ -143,6 +145,7 @@ private fun WatchScrollToTop(
} }
} }
@OptIn(ExperimentalTime::class)
@Composable @Composable
private fun FeedLoaded( private fun FeedLoaded(
state: FeedState.Loaded, state: FeedState.Loaded,
@@ -163,14 +166,17 @@ private fun FeedLoaded(
state = listState state = listState
) { ) {
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item -> itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
NoteCompose( val (value, elapsed) = measureTimedValue {
item, NoteCompose(
routeForLastRead = routeForLastRead, item,
modifier = baseModifier, routeForLastRead = routeForLastRead,
isBoostedNote = false, modifier = baseModifier,
accountViewModel = accountViewModel, isBoostedNote = false,
nav = nav accountViewModel = accountViewModel,
) nav = nav
)
}
println("AAA NoteCompose $elapsed ${item.event?.content()}")
} }
} }
} }
@@ -229,7 +229,7 @@ fun ChannelScreen(
trailingIcon = { trailingIcon = {
PostButton( PostButton(
onPost = { 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() tagger.run()
account.sendChannelMessage(tagger.message, channel.idHex, tagger.replyTos, tagger.mentions, wantsToMarkAsSensitive = false) account.sendChannelMessage(tagger.message, channel.idHex, tagger.replyTos, tagger.mentions, wantsToMarkAsSensitive = false)
channelScreenModel.message = TextFieldValue("") channelScreenModel.message = TextFieldValue("")
@@ -21,7 +21,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
@Composable @Composable
fun LoadRedirectScreen(eventId: String?, accountViewModel: AccountViewModel, navController: NavController) { fun LoadRedirectScreen(eventId: String?, navController: NavController) {
if (eventId == null) return if (eventId == null) return
val baseNote = LocalCache.checkGetOrCreateNote(eventId) ?: return val baseNote = LocalCache.checkGetOrCreateNote(eventId) ?: return
@@ -31,7 +31,7 @@ fun LoadRedirectScreen(eventId: String?, accountViewModel: AccountViewModel, nav
LaunchedEffect(key1 = noteState) { LaunchedEffect(key1 = noteState) {
val event = note?.event val event = note?.event
val channel = note?.channel() val channelHex = note?.channelHex()
if (event == null) { if (event == null) {
// stay here, loading // stay here, loading
@@ -41,9 +41,9 @@ fun LoadRedirectScreen(eventId: String?, accountViewModel: AccountViewModel, nav
} else if (event is PrivateDmEvent) { } else if (event is PrivateDmEvent) {
navController.backQueue.removeLast() navController.backQueue.removeLast()
navController.navigate("Room/${note.author?.pubkeyHex}") navController.navigate("Room/${note.author?.pubkeyHex}")
} else if (channel != null) { } else if (channelHex != null) {
navController.backQueue.removeLast() navController.backQueue.removeLast()
navController.navigate("Channel/${channel.idHex}") navController.navigate("Channel/$channelHex")
} else { } else {
navController.backQueue.removeLast() navController.backQueue.removeLast()
navController.navigate("Note/${note.idHex}") navController.navigate("Note/${note.idHex}")