mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 00:16:59 +00:00
feat: editable Notify block — pick receivers for private posts
Phase 3 of the private-notes plan: the composer's Notify row gains an '+ Add' chip backed by the existing user-suggestion search, so users can p-tag people who aren't cited in the text — for any post, public or private. While the private toggle is ON the row is always visible, relabeled 'Visible to' (the p-tags ARE the audience of the wrap), and an empty list shows a 'only you will see this' hint for self-only notes. https://claude.ai/code/session_01B39MQmrT3dz137nfpXABvo
This commit is contained in:
+23
-3
@@ -52,20 +52,23 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
fun Notifying(
|
||||
baseMentions: ImmutableList<User>?,
|
||||
accountViewModel: AccountViewModel,
|
||||
label: String? = null,
|
||||
showWhenEmpty: Boolean = false,
|
||||
onAddUser: (() -> Unit)? = null,
|
||||
onClick: (User) -> Unit,
|
||||
) {
|
||||
val mentions = baseMentions?.toSet()
|
||||
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(5.dp)) {
|
||||
if (!mentions.isNullOrEmpty()) {
|
||||
if (!mentions.isNullOrEmpty() || showWhenEmpty) {
|
||||
Text(
|
||||
stringRes(R.string.reply_notify),
|
||||
label ?: stringRes(R.string.reply_notify),
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
modifier = Modifier.align(CenterVertically),
|
||||
)
|
||||
|
||||
mentions.forEachIndexed { _, user ->
|
||||
mentions?.forEachIndexed { _, user ->
|
||||
Button(
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
@@ -77,6 +80,23 @@ fun Notifying(
|
||||
DisplayUserNameWithDeleteMark(user, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
if (onAddUser != null) {
|
||||
Button(
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.mediumImportanceLink,
|
||||
),
|
||||
onClick = onAddUser,
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.notify_add_user),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+33
-1
@@ -84,6 +84,7 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview
|
||||
import com.vitorpamplona.amethyst.ui.components.OutlinedThinPaddingTextField
|
||||
import com.vitorpamplona.amethyst.ui.components.getActivity
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
@@ -128,6 +129,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.SuggestionListDefaultHeightPage
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -306,11 +308,41 @@ private fun NewPostScreenBody(
|
||||
}
|
||||
|
||||
Row {
|
||||
Notifying(postViewModel.pTags?.toImmutableList(), accountViewModel) {
|
||||
Notifying(
|
||||
baseMentions = postViewModel.pTags?.toImmutableList(),
|
||||
accountViewModel = accountViewModel,
|
||||
label = if (postViewModel.wantsPrivateNote) stringRes(R.string.private_note_visible_to) else null,
|
||||
showWhenEmpty = postViewModel.wantsPrivateNote,
|
||||
onAddUser = { postViewModel.wantsToAddNotifyUser = !postViewModel.wantsToAddNotifyUser },
|
||||
) {
|
||||
postViewModel.removeFromReplyList(it)
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.wantsToAddNotifyUser) {
|
||||
OutlinedThinPaddingTextField(
|
||||
state = postViewModel.notifyUserSearchText,
|
||||
onTextChanged = postViewModel::onNotifyUserSearchTextChanged,
|
||||
label = { Text(text = stringRes(R.string.notify_search_and_add_user)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(R.string.zap_split_search_and_add_user_placeholder),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
|
||||
if (postViewModel.wantsPrivateNote && postViewModel.pTags.isNullOrEmpty()) {
|
||||
Text(
|
||||
text = stringRes(R.string.private_note_no_receivers),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
}
|
||||
|
||||
// Only show text input if no voice message is being posted
|
||||
if (postViewModel.voiceMetadata == null && postViewModel.voiceRecording == null) {
|
||||
Row(
|
||||
|
||||
+27
@@ -167,6 +167,7 @@ enum class UserSuggestionAnchor {
|
||||
MAIN_MESSAGE,
|
||||
FORWARD_ZAPS,
|
||||
TO_USERS,
|
||||
NOTIFY,
|
||||
}
|
||||
|
||||
@Stable
|
||||
@@ -322,6 +323,26 @@ open class ShortNotePostViewModel :
|
||||
wantsPrivateNote = !wantsPrivateNote
|
||||
}
|
||||
|
||||
// Notify / Visible-to editor: lets the user p-tag people who aren't
|
||||
// cited in the message. For private notes the Notify list IS the
|
||||
// audience, so this is how receivers are picked.
|
||||
var wantsToAddNotifyUser by mutableStateOf(false)
|
||||
val notifyUserSearchText = TextFieldState()
|
||||
|
||||
fun onNotifyUserSearchTextChanged() {
|
||||
if (notifyUserSearchText.selection.collapsed) {
|
||||
val lastWord = notifyUserSearchText.text.toString()
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.NOTIFY
|
||||
userSuggestions?.processCurrentWord(lastWord)
|
||||
}
|
||||
}
|
||||
|
||||
fun addToReplyList(user: User) {
|
||||
if (pTags?.contains(user) != true) {
|
||||
pTags = (pTags ?: emptyList()).plus(user)
|
||||
}
|
||||
}
|
||||
|
||||
// A single ephemeral signer reused for the whole compose session so that media
|
||||
// uploads (Blossom/NIP-96 auth events) and the final anonymous post are all signed
|
||||
// by the same throwaway key, instead of leaking the real account's pubkey into the
|
||||
@@ -1286,6 +1307,8 @@ open class ShortNotePostViewModel :
|
||||
scheduledForSec = null
|
||||
wantsPrivateNote = false
|
||||
privateNoteLocked = false
|
||||
wantsToAddNotifyUser = false
|
||||
notifyUserSearchText.clearText()
|
||||
|
||||
forwardZapTo.value = SplitBuilder()
|
||||
forwardZapToEditting.clearText()
|
||||
@@ -1350,6 +1373,10 @@ open class ShortNotePostViewModel :
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||
forwardZapTo.value.addItem(item)
|
||||
forwardZapToEditting.clearText()
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.NOTIFY) {
|
||||
addToReplyList(item)
|
||||
notifyUserSearchText.clearText()
|
||||
wantsToAddNotifyUser = false
|
||||
}
|
||||
|
||||
userSuggestionsMainMessage = null
|
||||
|
||||
@@ -873,6 +873,10 @@
|
||||
<string name="private_note">Make private: gift-wrap the note to the notified users only</string>
|
||||
<string name="disable_private_note">Make public</string>
|
||||
<string name="private_note_locked">Replies to a private note always stay private</string>
|
||||
<string name="private_note_visible_to">Visible to</string>
|
||||
<string name="private_note_no_receivers">No receivers yet: only you will be able to see this note. Add people to share it with.</string>
|
||||
<string name="notify_add_user">+ Add</string>
|
||||
<string name="notify_search_and_add_user">Search and add a user to notify</string>
|
||||
<string name="bookmark_absence_indicator">is not a bookmark here</string>
|
||||
<string name="bookmark_remove_action_desc">Remove bookmark from list</string>
|
||||
<string name="bookmark_add_action_desc">Add bookmark to list</string>
|
||||
|
||||
Reference in New Issue
Block a user