diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Hex.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Hex.kt index e57047d2b..d9f70e24c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Hex.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Hex.kt @@ -41,6 +41,8 @@ fun decodePublicKey(key: String): ByteArray { Persona(privKey = key.bechToBytes()).pubKey } else if (key.startsWith("npub")) { key.bechToBytes() + } else if (key.startsWith("note")) { + key.bechToBytes() } else { //if (pattern.matcher(key).matches()) { //} else { Hex.decode(key) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 3481c0a6d..2b7f22068 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -45,11 +45,9 @@ import nostr.postr.events.TextNoteEvent @OptIn(ExperimentalComposeUiApi::class) @Composable -fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, account: Account) { +fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = null, account: Account) { val postViewModel: NewPostViewModel = viewModel() - postViewModel.load(account, baseReplyTo) - val context = LocalContext.current // initialize focus reference to be able to request focus programmatically @@ -59,6 +57,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, account: Account val scroolState = rememberScrollState() LaunchedEffect(Unit) { + postViewModel.load(account, baseReplyTo, quote) delay(100) focusRequester.requestFocus() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 6dcf25aae..bdf7ac785 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -31,7 +31,7 @@ class NewPostViewModel: ViewModel() { var userSuggestions by mutableStateOf>(emptyList()) var userSuggestionAnchor: TextRange? = null - fun load(account: Account, replyingTo: Note?) { + fun load(account: Account, replyingTo: Note?, quote: Note?) { originalNote = replyingTo replyingTo?.let { replyNote -> this.replyTos = (replyNote.replyTo ?: emptyList()).plus(replyNote) @@ -45,29 +45,60 @@ class NewPostViewModel: ViewModel() { } } + + quote?.let { + message = TextFieldValue(message.text + "\n\n@${it.idNote()}") + } + this.account = account } - fun addUserToMentionsIfNotInAndReturnIndex(user: User): Int { - val replyToSize = replyTos?.size ?: 0 + fun addUserToMentions(user: User) { + mentions = if (mentions?.contains(user) == true) mentions else mentions?.plus(user) ?: listOf(user) + } - var myMentions = mentions - if (myMentions == null) { - mentions = listOf(user) - return replyToSize + 0 // position of the user - } + fun addNoteToReplyTos(note: Note) { + note.author?.let { addUserToMentions(it) } + replyTos = if (replyTos?.contains(note) == true) replyTos else replyTos?.plus(note) ?: listOf(note) + } - val index = myMentions.indexOf(user) + fun tagIndex(user: User): Int { + // Postr Events assembles replies before mentions in the tag order + return (replyTos?.size ?: 0) + (mentions?.indexOf(user) ?: 0) + } - if (index >= 0) return replyToSize + index - - myMentions = myMentions.plus(user) - mentions = myMentions - return replyToSize + myMentions.indexOf(user) + fun tagIndex(note: Note): Int { + // Postr Events assembles replies before mentions in the tag order + return (replyTos?.indexOf(note) ?: 0) } fun sendPost() { - // Moves @npub to mentions + // adds all references to mentions and reply tos + message.text.split('\n').forEach { paragraph: String -> + paragraph.split(' ').forEach { word: String -> + try { + if (word.startsWith("@npub") && word.length >= 64) { + val keyB32 = word.substring(0, 64) + + val key = decodePublicKey(keyB32.removePrefix("@")) + val user = LocalCache.getOrCreateUser(key.toHexKey()) + + addUserToMentions(user) + } else if (word.startsWith("@note") && word.length >= 64) { + val keyB32 = word.substring(0, 64) + + val key = decodePublicKey(keyB32.removePrefix("@")) + val note = LocalCache.getOrCreateNote(key.toHexKey()) + + addNoteToReplyTos(note) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + // Tags the text in the correct order. val newMessage = message.text.split('\n').map { paragraph: String -> paragraph.split(' ').map { word: String -> try { @@ -78,15 +109,20 @@ class NewPostViewModel: ViewModel() { val key = decodePublicKey(keyB32.removePrefix("@")) val user = LocalCache.getOrCreateUser(key.toHexKey()) - val index = addUserToMentionsIfNotInAndReturnIndex(user) + "#[${tagIndex(user)}]$restOfWord" + } else if (word.startsWith("@note") && word.length >= 64) { + val keyB32 = word.substring(0, 64) + val restOfWord = word.substring(64) - val newWord = "#[${index}]" + val key = decodePublicKey(keyB32.removePrefix("@")) + val note = LocalCache.getOrCreateNote(key.toHexKey()) - newWord + restOfWord + "#[${tagIndex(note)}]$restOfWord" } else { word } } catch (e: Exception) { + e.printStackTrace() // if it can't parse the key, don't try to change. word } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 741f4cdf4..b4f01f147 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -109,12 +109,21 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) { mutableStateOf(null) } + var wantsToQuote by remember { + mutableStateOf(null) + } + if (wantsToReplyTo != null) - NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, account) + NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, null, account) + + if (wantsToQuote != null) + NewPostView({ wantsToQuote = null }, null, wantsToQuote, account) var wantsToZap by remember { mutableStateOf(false) } var wantsToChangeZapAmount by remember { mutableStateOf(false) } + var wantsToBoost by remember { mutableStateOf(false) } + Row( modifier = Modifier .padding(top = 8.dp) @@ -156,7 +165,7 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) { modifier = Modifier.then(Modifier.size(20.dp)), onClick = { if (account.isWriteable()) - accountViewModel.boost(baseNote) + wantsToBoost = true else scope.launch { Toast.makeText( @@ -167,6 +176,20 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) { } } ) { + if (wantsToBoost) { + BoostTypeChoicePopup( + baseNote, + accountViewModel, + onDismiss = { + wantsToBoost = false + }, + onQuote = { + wantsToBoost = false + wantsToQuote = baseNote + } + ) + } + if (boostedNote?.isBoostedBy(account.userProfile()) == true) { Icon( painter = painterResource(R.drawable.ic_retweeted), @@ -345,6 +368,52 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) { } } + + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun BoostTypeChoicePopup(baseNote: Note, accountViewModel: AccountViewModel, onDismiss: () -> Unit, onQuote: () -> Unit) { + val scope = rememberCoroutineScope() + + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account ?: return + + Popup( + alignment = Alignment.BottomCenter, + offset = IntOffset(0, -50), + onDismissRequest = { onDismiss() } + ) { + FlowRow() { + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = { + accountViewModel.boost(baseNote) + onDismiss() + }, + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults + .buttonColors( + backgroundColor = MaterialTheme.colors.primary + ) + ) { + Text("Boost", color = Color.White, textAlign = TextAlign.Center) + } + + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = onQuote, + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults + .buttonColors( + backgroundColor = MaterialTheme.colors.primary + ) + ) { + Text("Quote", color = Color.White, textAlign = TextAlign.Center) + } + } + } +} + @OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) @Composable private fun ZapAmountChoicePopup(baseNote: Note, accountViewModel: AccountViewModel, onDismiss: () -> Unit, onChangeAmount: () -> Unit) {