Visual support for Kind1 forks.

This commit is contained in:
Vitor Pamplona 2024-02-26 15:17:17 -05:00
parent 782926fbea
commit 3c36f52baf
15 changed files with 226 additions and 32 deletions

View File

@ -1325,6 +1325,7 @@ class Account(
replyingTo: String?,
root: String?,
directMentions: Set<HexKey>,
forkedFrom: Event?,
relayList: List<Relay>? = null,
geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null,
@ -1349,6 +1350,7 @@ class Account(
directMentions = directMentions,
geohash = geohash,
nip94attachments = nip94attachments,
forkedFrom = forkedFrom,
signer = signer,
) {
Client.send(it, relayList = relayList)

View File

@ -186,6 +186,7 @@ fun NewPostView(
onClose: () -> Unit,
baseReplyTo: Note? = null,
quote: Note? = null,
fork: Note? = null,
enableMessageInterface: Boolean = false,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
@ -201,7 +202,7 @@ fun NewPostView(
var relayList = remember { accountViewModel.account.activeWriteRelays().toImmutableList() }
LaunchedEffect(Unit) {
postViewModel.load(accountViewModel, baseReplyTo, quote)
postViewModel.load(accountViewModel, baseReplyTo, quote, fork)
launch(Dispatchers.IO) {
postViewModel.imageUploadingError.collect { error ->

View File

@ -55,6 +55,7 @@ import com.vitorpamplona.quartz.events.BaseTextNoteEvent
import com.vitorpamplona.quartz.events.ChatMessageEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
@ -85,10 +86,10 @@ open class NewPostViewModel() : ViewModel() {
var requiresNIP24: Boolean = false
var originalNote: Note? = null
var forkedFromNote: Note? = null
var pTags by mutableStateOf<List<User>?>(null)
var eTags by mutableStateOf<List<Note>?>(null)
var imetaTags = mutableStateListOf<Array<String>>()
var nip94attachments by mutableStateOf<List<FileHeaderEvent>>(emptyList())
var nip95attachments by
@ -166,6 +167,7 @@ open class NewPostViewModel() : ViewModel() {
accountViewModel: AccountViewModel,
replyingTo: Note?,
quote: Note?,
fork: Note?,
) {
this.accountViewModel = accountViewModel
this.account = accountViewModel.account
@ -214,6 +216,37 @@ open class NewPostViewModel() : ViewModel() {
zapRaiserAmount = null
forwardZapTo = Split()
forwardZapToEditting = TextFieldValue("")
fork?.let {
message = TextFieldValue(it.event?.content() ?: "")
urlPreview = findUrlInMessage()
it.event?.isSensitive()?.let {
if (it) wantsToMarkAsSensitive = true
}
it.event?.zapraiserAmount()?.let {
zapRaiserAmount = it
}
it.event?.zapSplitSetup()?.let {
val totalWeight = it.sumOf { if (it.isLnAddress) 0.0 else it.weight }
it.forEach {
if (!it.isLnAddress) {
forwardZapTo.addItem(LocalCache.getOrCreateUser(it.lnAddressOrPubKeyHex), (it.weight / totalWeight).toFloat())
}
}
}
it.author?.let {
if (this.pTags?.contains(it) != true) {
this.pTags = listOf(it) + (this.pTags ?: emptyList())
}
}
forkedFromNote = it
}
}
fun sendPost(relayList: List<Relay>? = null) {
@ -404,9 +437,16 @@ open class NewPostViewModel() : ViewModel() {
val replyId = originalNote?.idHex
val replyToSet =
if (forkedFromNote != null) {
(listOfNotNull(forkedFromNote) + (tagger.eTags ?: emptyList())).ifEmpty { null }
} else {
tagger.eTags
}
account?.sendPost(
message = tagger.message,
replyTo = tagger.eTags,
replyTo = replyToSet,
mentions = tagger.pTags,
tags = null,
zapReceiver = zapReceiver,
@ -415,6 +455,7 @@ open class NewPostViewModel() : ViewModel() {
replyingTo = replyId,
root = rootId,
directMentions = tagger.directMentions,
forkedFrom = forkedFromNote?.event as? Event,
relayList = relayList,
geohash = geoHash,
nip94attachments = usedAttachments,
@ -504,7 +545,6 @@ open class NewPostViewModel() : ViewModel() {
urlPreview = null
isUploadingImage = false
pTags = null
imetaTags.clear()
wantsDirectMessage = false

View File

@ -65,7 +65,7 @@ fun ChannelFabColumn(
if (wantsToSendNewMessage) {
NewPostView(
{ wantsToSendNewMessage = false },
onClose = { wantsToSendNewMessage = false },
enableMessageInterface = true,
accountViewModel = accountViewModel,
nav = nav,

View File

@ -294,6 +294,7 @@ fun CreateClickableText(
maxLines: Int = Int.MAX_VALUE,
overrideColor: Color? = null,
fontWeight: FontWeight? = null,
fontSize: TextUnit = TextUnit.Unspecified,
route: String,
nav: (String) -> Unit,
) {
@ -304,12 +305,14 @@ fun CreateClickableText(
remember(clickablePart, suffix) {
val clickablePartStyle =
SpanStyle(
fontSize = fontSize,
color = overrideColor ?: primaryColor,
fontWeight = fontWeight,
)
val nonClickablePartStyle =
SpanStyle(
fontSize = fontSize,
color = overrideColor ?: onBackgroundColor,
fontWeight = fontWeight,
)
@ -562,6 +565,7 @@ fun CreateClickableTextWithEmoji(
maxLines: Int = Int.MAX_VALUE,
overrideColor: Color? = null,
fontWeight: FontWeight = FontWeight.Normal,
fontSize: TextUnit = TextUnit.Unspecified,
route: String,
nav: (String) -> Unit,
tags: ImmutableListOfLists<String>?,
@ -570,11 +574,12 @@ fun CreateClickableTextWithEmoji(
text = clickablePart,
tags = tags,
onRegularText = {
CreateClickableText(it, null, maxLines, overrideColor, fontWeight, route, nav)
CreateClickableText(it, null, maxLines, overrideColor, fontWeight, fontSize, route, nav)
},
onEmojiText = {
val clickablePartStyle =
SpanStyle(
fontSize = fontSize,
color = overrideColor ?: MaterialTheme.colorScheme.primary,
fontWeight = fontWeight,
)

View File

@ -32,6 +32,15 @@ class SplitItem<T>(val key: T) {
class Split<T>() {
var items: List<SplitItem<T>> by mutableStateOf(emptyList())
fun addItem(
key: T,
percentage: Float,
) {
val newItem = SplitItem(key)
newItem.percentage = percentage
this.items = items.plus(newItem)
}
fun addItem(key: T): Int {
val wasEqualSplit = isEqualSplit()
val newItem = SplitItem(key)

View File

@ -184,6 +184,7 @@ class AddBountyAmountViewModel : ViewModel() {
replyingTo = null,
root = null,
directMentions = setOf(),
forkedFrom = null,
)
nextAmount = TextFieldValue("")

View File

@ -80,6 +80,7 @@ import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@ -178,6 +179,7 @@ import com.vitorpamplona.amethyst.ui.theme.grayText
import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import com.vitorpamplona.amethyst.ui.theme.nip05
import com.vitorpamplona.amethyst.ui.theme.normalNoteModifier
import com.vitorpamplona.amethyst.ui.theme.normalWithTopMarginNoteModifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@ -2512,28 +2514,32 @@ fun SecondUserInfoRow(
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val noteEvent = remember { note.event } ?: return
val noteAuthor = remember { note.author } ?: return
val noteEvent = note.event ?: return
val noteAuthor = note.author ?: return
Row(
verticalAlignment = CenterVertically,
modifier = UserNameMaxRowHeight,
) {
ObserveDisplayNip05Status(noteAuthor, remember { Modifier.weight(1f) }, accountViewModel, nav)
if (noteEvent is BaseTextNoteEvent && noteEvent.isAFork()) {
ShowForkInformation(noteEvent, remember(noteEvent) { Modifier.weight(1f) }, accountViewModel, nav)
} else {
ObserveDisplayNip05Status(noteAuthor, remember(noteEvent) { Modifier.weight(1f) }, accountViewModel, nav)
}
val geo = remember { noteEvent.getGeoHash() }
val geo = remember(noteEvent) { noteEvent.getGeoHash() }
if (geo != null) {
Spacer(StdHorzSpacer)
DisplayLocation(geo, nav)
}
val baseReward = remember { noteEvent.getReward()?.let { Reward(it) } }
val baseReward = remember(noteEvent) { noteEvent.getReward()?.let { Reward(it) } }
if (baseReward != null) {
Spacer(StdHorzSpacer)
DisplayReward(baseReward, note, accountViewModel, nav)
}
val pow = remember { noteEvent.getPoWRank() }
val pow = remember(noteEvent) { noteEvent.getPoWRank() }
if (pow > 20) {
Spacer(StdHorzSpacer)
DisplayPoW(pow)
@ -2541,6 +2547,76 @@ fun SecondUserInfoRow(
}
}
@Composable
private fun ShowForkInformation(
noteEvent: BaseTextNoteEvent,
modifier: Modifier,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val forkedAddress = remember(noteEvent) { noteEvent.forkFromAddress() }
val forkedEvent = remember(noteEvent) { noteEvent.forkFromVersion() }
if (forkedAddress != null) {
LoadAddressableNote(aTag = forkedAddress, accountViewModel = accountViewModel) { addressableNote ->
if (addressableNote != null) {
ForkInformationRowLightColor(addressableNote, modifier, accountViewModel, nav)
}
}
} else if (forkedEvent != null) {
LoadNote(forkedEvent, accountViewModel = accountViewModel) { event ->
if (event != null) {
ForkInformationRowLightColor(event, modifier, accountViewModel, nav)
}
}
}
}
@Composable
fun ForkInformationRowLightColor(
originalVersion: Note,
modifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val noteState by originalVersion.live().metadata.observeAsState()
val note = noteState?.note ?: return
val author = note.author ?: return
val route = remember(note) { routeFor(note, accountViewModel.userProfile()) }
if (route != null) {
Row(modifier) {
ClickableText(
text =
buildAnnotatedString {
append(stringResource(id = R.string.forked_from))
append(" ")
},
onClick = { nav(route) },
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.nip05, fontSize = Font14SP),
maxLines = 1,
overflow = TextOverflow.Visible,
)
val userState by author.live().metadata.observeAsState()
val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() }
val userTags =
remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() }
if (userDisplayName != null) {
CreateClickableTextWithEmoji(
clickablePart = userDisplayName,
maxLines = 1,
route = route,
overrideColor = MaterialTheme.colorScheme.nip05,
fontSize = Font14SP,
nav = nav,
tags = userTags,
)
}
}
}
}
@Composable
fun LoadStatuses(
user: User,
@ -3928,7 +4004,6 @@ private fun WikiNoteHeader(
nav: (String) -> Unit,
) {
val title = remember(noteEvent) { noteEvent.title() }
val forkedAddress = remember(noteEvent) { noteEvent.forkFromAddress() }
val summary =
remember(noteEvent) {
noteEvent.summary()?.ifBlank { null } ?: noteEvent.content.take(200).ifBlank { null }

View File

@ -127,6 +127,7 @@ import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.placeholderTextColorFilter
import com.vitorpamplona.quartz.encoders.Nip30CustomEmoji
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentListOf
@ -499,6 +500,7 @@ private fun BoostWithDialog(
nav: (String) -> Unit,
) {
var wantsToQuote by remember { mutableStateOf<Note?>(null) }
var wantsToFork by remember { mutableStateOf<Note?>(null) }
if (wantsToQuote != null) {
NewPostView(
@ -510,7 +512,34 @@ private fun BoostWithDialog(
)
}
BoostReaction(baseNote, grayTint, accountViewModel) { wantsToQuote = baseNote }
if (wantsToFork != null) {
val replyTo =
remember(wantsToFork) {
val forkEvent = wantsToFork?.event
if (forkEvent is BaseTextNoteEvent) {
val hex = forkEvent.replyingTo()
wantsToFork?.replyTo?.filter { it.event?.id() == hex }?.firstOrNull()
} else {
null
}
}
NewPostView(
onClose = { wantsToFork = null },
baseReplyTo = replyTo,
fork = wantsToFork,
accountViewModel = accountViewModel,
nav = nav,
)
}
BoostReaction(
baseNote,
grayTint,
accountViewModel,
onQuotePress = { wantsToQuote = baseNote },
onForkPress = { wantsToFork = baseNote },
)
}
@Composable
@ -650,6 +679,7 @@ fun BoostReaction(
iconSizeModifier: Modifier = Size20Modifier,
iconSize: Dp = Size20dp,
onQuotePress: () -> Unit,
onForkPress: () -> Unit,
) {
var wantsToBoost by remember { mutableStateOf(false) }
@ -671,7 +701,13 @@ fun BoostReaction(
wantsToBoost = false
onQuotePress()
},
onRepost = { accountViewModel.boost(baseNote) },
onRepost = {
accountViewModel.boost(baseNote)
},
onFork = {
wantsToBoost = false
onForkPress()
},
)
}
}
@ -1149,6 +1185,7 @@ private fun BoostTypeChoicePopup(
onDismiss: () -> Unit,
onQuote: () -> Unit,
onRepost: () -> Unit,
onFork: () -> Unit,
) {
val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() }
@ -1189,6 +1226,18 @@ private fun BoostTypeChoicePopup(
) {
Text(stringResource(R.string.quote), color = Color.White, textAlign = TextAlign.Center)
}
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = onFork,
shape = ButtonBorder,
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
),
) {
Text(stringResource(R.string.fork), color = Color.White, textAlign = TextAlign.Center)
}
}
}
}

View File

@ -83,7 +83,6 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.InlineCarrousel
@ -913,7 +912,7 @@ private fun RenderWikiHeaderForThread(
forkedAddress?.let {
LoadAddressableNote(aTag = it, accountViewModel = accountViewModel) { originalVersion ->
if (originalVersion != null) {
ShowForkInformation(originalVersion, Modifier.fillMaxWidth(), accountViewModel, nav)
ForkInformationRow(originalVersion, Modifier.fillMaxWidth(), accountViewModel, nav)
}
}
}
@ -934,8 +933,8 @@ private fun RenderWikiHeaderForThread(
}
@Composable
fun ShowForkInformation(
originalVersion: AddressableNote,
fun ForkInformationRow(
originalVersion: Note,
modifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,

View File

@ -476,9 +476,12 @@ fun ReactionsColumn(
accountViewModel = accountViewModel,
iconSizeModifier = Size40Modifier,
iconSize = Size40dp,
) {
wantsToQuote = baseNote
}
onQuotePress = {
wantsToQuote = baseNote
},
onForkPress = {
},
)
LikeReaction(
baseNote = baseNote,
grayTint = MaterialTheme.colorScheme.onBackground,

View File

@ -47,6 +47,7 @@
<string name="boost">Boost</string>
<string name="boosted">boosted</string>
<string name="quote">Quote</string>
<string name="fork">Fork</string>
<string name="new_amount_in_sats">New Amount in Sats</string>
<string name="add">Add</string>
<string name="replying_to">"replying to "</string>
@ -777,6 +778,7 @@
<string name="max_limit">Max Limit</string>
<string name="restricted_writes">Restricted Writes</string>
<string name="forked_from">Forked from</string>
<string name="forked_tag">FORK</string>
<string name="git_repository">Git Repository: %1$s</string>
<string name="git_web_address">Web:</string>
<string name="git_clone_address">Clone:</string>

View File

@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.encoders.Nip19Bech32.nip19regex
@ -42,6 +43,18 @@ open class BaseTextNoteEvent(
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun mentions() = taggedUsers()
fun isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" }
fun forkFromAddress() =
tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "fork" }?.let {
val aTagValue = it[1]
val relay = it.getOrNull(2)
ATag.parse(aTagValue, relay)
}
fun forkFromVersion() = tags.firstOrNull { it.size > 3 && it[0] == "e" && it[3] == "fork" }?.get(1)
open fun replyTos(): List<HexKey> {
val oldStylePositional = tags.filter { it.size > 1 && it.size <= 3 && it[0] == "e" }.map { it[1] }
val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "reply" }?.get(1)

View File

@ -57,6 +57,7 @@ class TextNoteEvent(
directMentions: Set<HexKey> = emptySet(),
geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null,
forkedFrom: Event? = null,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (TextNoteEvent) -> Unit,
@ -69,6 +70,7 @@ class TextNoteEvent(
root = root,
replyingTo = replyingTo,
directMentions = directMentions,
forkedFrom = forkedFrom?.id,
),
)
}
@ -93,6 +95,7 @@ class TextNoteEvent(
root = root,
replyingTo = replyingTo,
directMentions = directMentions,
forkedFrom = (forkedFrom as? AddressableEvent)?.address()?.toTag(),
),
)
}
@ -136,6 +139,7 @@ class TextNoteEvent(
root: String?,
replyingTo: String?,
directMentions: Set<HexKey>,
forkedFrom: String?,
) = sortedWith { o1, o2 ->
when {
o1 == o2 -> 0
@ -150,6 +154,7 @@ class TextNoteEvent(
when (it) {
root -> arrayOf(tagName, it, "", "root")
replyingTo -> arrayOf(tagName, it, "", "reply")
forkedFrom -> arrayOf(tagName, it, "", "fork")
in directMentions -> arrayOf(tagName, it, "", "mention")
else -> arrayOf(tagName, it)
}

View File

@ -41,16 +41,6 @@ class WikiNoteEvent(
fun topics() = hashtags()
fun forkFromAddress() =
tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "fork" }?.let {
val aTagValue = it[1]
val relay = it.getOrNull(2)
ATag.parse(aTagValue, relay)
}
fun forkFromVersion() = tags.firstOrNull { it.size > 3 && it[0] == "e" && it[3] == "fork" }?.get(1)
fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1)
fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1)