mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2024-09-29 16:30:49 +00:00
Custom reactions
This commit is contained in:
parent
a9e1ce5aec
commit
52bd6b4443
@ -49,6 +49,7 @@ private object PrefKeys {
|
||||
const val LANGUAGE_PREFS = "languagePreferences"
|
||||
const val TRANSLATE_TO = "translateTo"
|
||||
const val ZAP_AMOUNTS = "zapAmounts"
|
||||
const val REACTION_CHOICES = "reactionChoices"
|
||||
const val DEFAULT_ZAPTYPE = "defaultZapType"
|
||||
const val DEFAULT_FILE_SERVER = "defaultFileServer"
|
||||
const val DEFAULT_HOME_FOLLOW_LIST = "defaultHomeFollowList"
|
||||
@ -208,6 +209,7 @@ object LocalPreferences {
|
||||
putString(PrefKeys.LANGUAGE_PREFS, gson.toJson(account.languagePreferences))
|
||||
putString(PrefKeys.TRANSLATE_TO, account.translateTo)
|
||||
putString(PrefKeys.ZAP_AMOUNTS, gson.toJson(account.zapAmountChoices))
|
||||
putString(PrefKeys.REACTION_CHOICES, gson.toJson(account.reactionChoices))
|
||||
putString(PrefKeys.DEFAULT_ZAPTYPE, gson.toJson(account.defaultZapType))
|
||||
putString(PrefKeys.DEFAULT_FILE_SERVER, gson.toJson(account.defaultFileServer))
|
||||
putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, account.defaultHomeFollowList)
|
||||
@ -258,6 +260,11 @@ object LocalPreferences {
|
||||
object : TypeToken<List<Long>>() {}.type
|
||||
) ?: listOf(500L, 1000L, 5000L)
|
||||
|
||||
val reactionChoices = gson.fromJson<List<String>>(
|
||||
getString(PrefKeys.REACTION_CHOICES, "[]"),
|
||||
object : TypeToken<List<String>>() {}.type
|
||||
).ifEmpty { listOf("+") } ?: listOf("+")
|
||||
|
||||
val defaultZapType = gson.fromJson(
|
||||
getString(PrefKeys.DEFAULT_ZAPTYPE, "PUBLIC"),
|
||||
object : TypeToken<LnZapEvent.ZapType>() {}.type
|
||||
@ -314,28 +321,29 @@ object LocalPreferences {
|
||||
val warnAboutReports = getBoolean(PrefKeys.WARN_ABOUT_REPORTS, true)
|
||||
|
||||
val a = Account(
|
||||
Persona(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()),
|
||||
followingChannels,
|
||||
hiddenUsers,
|
||||
localRelays,
|
||||
dontTranslateFrom,
|
||||
languagePreferences,
|
||||
translateTo,
|
||||
zapAmountChoices,
|
||||
defaultZapType,
|
||||
defaultFileServer,
|
||||
defaultHomeFollowList,
|
||||
defaultStoriesFollowList,
|
||||
defaultNotificationFollowList,
|
||||
zapPaymentRequestServer,
|
||||
hideDeleteRequestDialog,
|
||||
hideBlockAlertDialog,
|
||||
latestContactList,
|
||||
proxy,
|
||||
proxyPort,
|
||||
showSensitiveContent,
|
||||
warnAboutReports,
|
||||
filterSpam
|
||||
loggedIn = Persona(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()),
|
||||
followingChannels = followingChannels,
|
||||
hiddenUsers = hiddenUsers,
|
||||
localRelays = localRelays,
|
||||
dontTranslateFrom = dontTranslateFrom,
|
||||
languagePreferences = languagePreferences,
|
||||
translateTo = translateTo,
|
||||
zapAmountChoices = zapAmountChoices,
|
||||
reactionChoices = reactionChoices,
|
||||
defaultZapType = defaultZapType,
|
||||
defaultFileServer = defaultFileServer,
|
||||
defaultHomeFollowList = defaultHomeFollowList,
|
||||
defaultStoriesFollowList = defaultStoriesFollowList,
|
||||
defaultNotificationFollowList = defaultNotificationFollowList,
|
||||
zapPaymentRequest = zapPaymentRequestServer,
|
||||
hideDeleteRequestDialog = hideDeleteRequestDialog,
|
||||
hideBlockAlertDialog = hideBlockAlertDialog,
|
||||
backupContactList = latestContactList,
|
||||
proxy = proxy,
|
||||
proxyPort = proxyPort,
|
||||
showSensitiveContent = showSensitiveContent,
|
||||
warnAboutPostsWithReports = warnAboutReports,
|
||||
filterSpamFromStrangers = filterSpam
|
||||
)
|
||||
|
||||
return a
|
||||
|
@ -56,6 +56,7 @@ class Account(
|
||||
var languagePreferences: Map<String, String> = mapOf(),
|
||||
var translateTo: String = Locale.getDefault().language,
|
||||
var zapAmountChoices: List<Long> = listOf(500L, 1000L, 5000L),
|
||||
var reactionChoices: List<String> = listOf("+"),
|
||||
var defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PRIVATE,
|
||||
var defaultFileServer: ServersAvailable = ServersAvailable.NOSTR_BUILD,
|
||||
var defaultHomeFollowList: String = KIND3_FOLLOWS,
|
||||
@ -143,8 +144,8 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
fun reactionTo(note: Note): List<Note> {
|
||||
return note.reactedBy(userProfile(), "+")
|
||||
fun reactionTo(note: Note, reaction: String): List<Note> {
|
||||
return note.reactedBy(userProfile(), reaction)
|
||||
}
|
||||
|
||||
fun hasBoosted(note: Note): Boolean {
|
||||
@ -155,20 +156,20 @@ class Account(
|
||||
return note.boostedBy(userProfile())
|
||||
}
|
||||
|
||||
fun hasReacted(note: Note): Boolean {
|
||||
return note.hasReacted(userProfile(), "+")
|
||||
fun hasReacted(note: Note, reaction: String): Boolean {
|
||||
return note.hasReacted(userProfile(), reaction)
|
||||
}
|
||||
|
||||
fun reactTo(note: Note) {
|
||||
fun reactTo(note: Note, reaction: String) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
if (hasReacted(note)) {
|
||||
if (hasReacted(note, reaction)) {
|
||||
// has already liked this note
|
||||
return
|
||||
}
|
||||
|
||||
note.event?.let {
|
||||
val event = ReactionEvent.createLike(it, loggedIn.privKey!!)
|
||||
val event = ReactionEvent.create(reaction, it, loggedIn.privKey!!)
|
||||
Client.send(event)
|
||||
LocalCache.consume(event)
|
||||
}
|
||||
@ -863,6 +864,12 @@ class Account(
|
||||
saveable.invalidateData()
|
||||
}
|
||||
|
||||
fun changeReactionTypes(newTypes: List<String>) {
|
||||
reactionChoices = newTypes
|
||||
live.invalidateData()
|
||||
saveable.invalidateData()
|
||||
}
|
||||
|
||||
fun changeZapPaymentRequest(newServer: Nip47URI?) {
|
||||
zapPaymentRequest = newServer
|
||||
live.invalidateData()
|
||||
|
@ -554,17 +554,8 @@ object LocalCache {
|
||||
|
||||
// Log.d("RE", "New Reaction ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
|
||||
|
||||
if (
|
||||
event.content == "" ||
|
||||
event.content == "+" ||
|
||||
event.content == "\u2764\uFE0F" || // red heart
|
||||
event.content == "\uD83E\uDD19" || // call me hand
|
||||
event.content == "\uD83D\uDC4D" // thumbs up
|
||||
) {
|
||||
// Counts the replies
|
||||
repliesTo.forEach {
|
||||
it.addReaction(note)
|
||||
}
|
||||
repliesTo.forEach {
|
||||
it.addReaction(note)
|
||||
}
|
||||
|
||||
refreshObservers(note)
|
||||
|
@ -43,7 +43,7 @@ open class Note(val idHex: String) {
|
||||
// These fields are updated every time an event related to this note is received.
|
||||
var replies = setOf<Note>()
|
||||
private set
|
||||
var reactions = setOf<Note>()
|
||||
var reactions = mapOf<String, Set<Note>>()
|
||||
private set
|
||||
var boosts = setOf<Note>()
|
||||
private set
|
||||
@ -134,8 +134,20 @@ open class Note(val idHex: String) {
|
||||
liveSet?.boosts?.invalidateData()
|
||||
}
|
||||
fun removeReaction(note: Note) {
|
||||
reactions = reactions - note
|
||||
liveSet?.reactions?.invalidateData()
|
||||
val reaction = note.event?.content() ?: "+"
|
||||
|
||||
if (reaction in reactions.keys && reactions[reaction]?.contains(note) == true) {
|
||||
reactions[reaction]?.let {
|
||||
val newList = it.minus(note)
|
||||
if (newList.isEmpty()) {
|
||||
reactions = reactions.minus(reaction)
|
||||
} else {
|
||||
reactions = reactions + Pair(reaction, newList)
|
||||
}
|
||||
|
||||
liveSet?.reactions?.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeReport(deleteNote: Note) {
|
||||
@ -203,8 +215,13 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
|
||||
fun addReaction(note: Note) {
|
||||
if (note !in reactions) {
|
||||
reactions = reactions + note
|
||||
val reaction = note.event?.content() ?: "+"
|
||||
|
||||
if (reaction !in reactions.keys) {
|
||||
reactions = reactions + Pair(reaction, setOf(note))
|
||||
liveSet?.reactions?.invalidateData()
|
||||
} else if (reactions[reaction]?.contains(note) == false) {
|
||||
reactions = reactions + Pair(reaction, (reactions[reaction] ?: emptySet()) + note)
|
||||
liveSet?.reactions?.invalidateData()
|
||||
}
|
||||
}
|
||||
@ -243,8 +260,10 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
}
|
||||
|
||||
fun isReactedBy(user: User): Boolean {
|
||||
return reactions.any { it.author?.pubkeyHex == user.pubkeyHex }
|
||||
fun isReactedBy(user: User): String? {
|
||||
return reactions.filter {
|
||||
it.value.any { it.author?.pubkeyHex == user.pubkeyHex }
|
||||
}.keys.firstOrNull()
|
||||
}
|
||||
|
||||
fun isBoostedBy(user: User): Boolean {
|
||||
@ -269,6 +288,10 @@ open class Note(val idHex: String) {
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
fun countReactions(): Int {
|
||||
return reactions.values.sumOf { it.size }
|
||||
}
|
||||
|
||||
fun zappedAmount(privKey: ByteArray?, walletServicePubkey: ByteArray?): BigDecimal {
|
||||
// Regular Zap Receipts
|
||||
val completedZaps = zaps.asSequence()
|
||||
@ -361,7 +384,11 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
|
||||
fun reactedBy(loggedIn: User, content: String): List<Note> {
|
||||
return reactions.filter { it.author == loggedIn && it.event?.content() == content }
|
||||
return reactions[content]?.filter { it.author == loggedIn && it.event?.content() == content } ?: emptyList()
|
||||
}
|
||||
|
||||
fun reactedBy(loggedIn: User): List<String> {
|
||||
return reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key }
|
||||
}
|
||||
|
||||
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
|
||||
|
@ -145,7 +145,7 @@ private fun Galeries(
|
||||
) {
|
||||
val zapEvents by remember { derivedStateOf { multiSetCard.zapEvents } }
|
||||
val boostEvents by remember { derivedStateOf { multiSetCard.boostEvents } }
|
||||
val likeEvents by remember { derivedStateOf { multiSetCard.likeEvents } }
|
||||
val likeEvents by remember { derivedStateOf { multiSetCard.likeEventsByType } }
|
||||
|
||||
val hasZapEvents by remember { derivedStateOf { multiSetCard.zapEvents.isNotEmpty() } }
|
||||
val hasBoostEvents by remember { derivedStateOf { multiSetCard.boostEvents.isNotEmpty() } }
|
||||
@ -160,38 +160,52 @@ private fun Galeries(
|
||||
}
|
||||
|
||||
if (hasLikeEvents) {
|
||||
RenderLikeGallery(likeEvents, backgroundColor, nav, accountViewModel)
|
||||
likeEvents.forEach {
|
||||
RenderLikeGallery(it.key, it.value, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderLikeGallery(
|
||||
reactionType: String,
|
||||
likeEvents: ImmutableList<Note>,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Row(remember { Modifier.fillMaxWidth() }) {
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.width(55.dp)
|
||||
.padding(end = 5.dp)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_liked),
|
||||
null,
|
||||
val isNotEmpty = remember(likeEvents) {
|
||||
likeEvents.isNotEmpty()
|
||||
}
|
||||
|
||||
if (isNotEmpty) {
|
||||
Row(remember { Modifier.fillMaxWidth() }) {
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.size(16.dp)
|
||||
.width(55.dp)
|
||||
.padding(end = 5.dp)
|
||||
}
|
||||
) {
|
||||
val modifier = remember {
|
||||
Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
},
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AuthorGallery(likeEvents, backgroundColor, nav, accountViewModel)
|
||||
when (reactionType) {
|
||||
"+" -> Icon(
|
||||
painter = painterResource(R.drawable.ic_liked),
|
||||
null,
|
||||
modifier = remember { modifier.size(18.dp) },
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
"-" -> Text(text = "\uD83D\uDC4E", modifier = modifier)
|
||||
else -> Text(text = reactionType, modifier = modifier)
|
||||
}
|
||||
}
|
||||
|
||||
AuthorGallery(likeEvents, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,6 +70,7 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -190,7 +191,7 @@ private fun ReactionDetailGallery(
|
||||
Column() {
|
||||
val zapEvents by remember(zapsState) { derivedStateOf { baseNote.zaps.mapNotNull { it.value?.let { zapEvent -> CombinedZap(it.key, zapEvent) } }.toImmutableList() } }
|
||||
val boostEvents by remember(boostsState) { derivedStateOf { baseNote.boosts.toImmutableList() } }
|
||||
val likeEvents by remember(reactionsState) { derivedStateOf { baseNote.reactions.toImmutableList() } }
|
||||
val likeEvents by remember(reactionsState) { derivedStateOf { baseNote.reactions.toImmutableMap() } }
|
||||
|
||||
val hasZapEvents by remember(zapsState) { derivedStateOf { baseNote.zaps.isNotEmpty() } }
|
||||
val hasBoostEvents by remember(boostsState) { derivedStateOf { baseNote.boosts.isNotEmpty() } }
|
||||
@ -215,12 +216,16 @@ private fun ReactionDetailGallery(
|
||||
}
|
||||
|
||||
if (hasLikeEvents) {
|
||||
RenderLikeGallery(
|
||||
likeEvents,
|
||||
backgroundColor,
|
||||
nav,
|
||||
accountViewModel
|
||||
)
|
||||
likeEvents.forEach {
|
||||
val reactions = remember(it.value) { it.value.toImmutableList() }
|
||||
RenderLikeGallery(
|
||||
it.key,
|
||||
reactions,
|
||||
backgroundColor,
|
||||
nav,
|
||||
accountViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -429,6 +434,7 @@ fun BoostText(baseNote: Note, grayTint: Color) {
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun LikeReaction(
|
||||
baseNote: Note,
|
||||
@ -444,29 +450,50 @@ fun LikeReaction(
|
||||
Modifier.size(iconSize)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
modifier = iconButtonModifier,
|
||||
onClick = {
|
||||
if (accountViewModel.isWriteable()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (accountViewModel.hasReactedTo(baseNote)) {
|
||||
accountViewModel.deleteReactionTo(baseNote)
|
||||
} else {
|
||||
accountViewModel.reactTo(baseNote)
|
||||
var wantsToChangeReactionSymbol by remember { mutableStateOf(false) }
|
||||
var wantsToReact by remember { mutableStateOf(false) }
|
||||
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = iconButtonModifier.combinedClickable(
|
||||
role = Role.Button,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false, radius = 24.dp),
|
||||
onClick = {
|
||||
likeClick(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
scope,
|
||||
context,
|
||||
onMultipleChoices = {
|
||||
wantsToReact = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_like_posts),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
)
|
||||
},
|
||||
onLongClick = {
|
||||
wantsToChangeReactionSymbol = true
|
||||
}
|
||||
}
|
||||
)
|
||||
) {
|
||||
LikeIcon(baseNote, heartSize, grayTint, accountViewModel.userProfile())
|
||||
|
||||
if (wantsToChangeReactionSymbol) {
|
||||
UpdateReactionTypeDialog({ wantsToChangeReactionSymbol = false }, accountViewModel = accountViewModel)
|
||||
}
|
||||
|
||||
if (wantsToReact) {
|
||||
ReactionChoicePopup(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
onDismiss = {
|
||||
wantsToReact = false
|
||||
},
|
||||
onChangeAmount = {
|
||||
wantsToReact = false
|
||||
wantsToChangeReactionSymbol = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LikeText(baseNote, grayTint)
|
||||
@ -476,15 +503,15 @@ fun LikeReaction(
|
||||
fun LikeIcon(baseNote: Note, iconSize: Dp = 20.dp, grayTint: Color, loggedIn: User) {
|
||||
val reactionsState by baseNote.live().reactions.observeAsState()
|
||||
|
||||
var wasReactedByLoggedIn by remember(reactionsState) {
|
||||
mutableStateOf(false)
|
||||
var reactionType by remember(baseNote) {
|
||||
mutableStateOf<String?>(null)
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = reactionsState) {
|
||||
launch(Dispatchers.Default) {
|
||||
val newWasReactedByLoggedIn = reactionsState?.note?.isReactedBy(loggedIn) == true
|
||||
if (wasReactedByLoggedIn != newWasReactedByLoggedIn) {
|
||||
wasReactedByLoggedIn = newWasReactedByLoggedIn
|
||||
val newReactionType = reactionsState?.note?.isReactedBy(loggedIn)
|
||||
if (reactionType != newReactionType) {
|
||||
reactionType = newReactionType
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -493,13 +520,19 @@ fun LikeIcon(baseNote: Note, iconSize: Dp = 20.dp, grayTint: Color, loggedIn: Us
|
||||
Modifier.size(iconSize)
|
||||
}
|
||||
|
||||
if (wasReactedByLoggedIn) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_liked),
|
||||
null,
|
||||
modifier = iconModifier,
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
if (reactionType != null) {
|
||||
when (reactionType) {
|
||||
"+" -> {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_liked),
|
||||
null,
|
||||
modifier = iconModifier,
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
"-" -> Text(text = "\uD83D\uDC4E")
|
||||
else -> Text(text = reactionType!!)
|
||||
}
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_like),
|
||||
@ -520,7 +553,7 @@ fun LikeText(baseNote: Note, grayTint: Color) {
|
||||
|
||||
LaunchedEffect(key1 = reactionsState) {
|
||||
launch(Dispatchers.Default) {
|
||||
val newReactionsCount = " " + showCount(reactionsState?.note?.reactions?.size)
|
||||
val newReactionsCount = " " + showCount(reactionsState?.note?.countReactions())
|
||||
if (reactionsCount != newReactionsCount) {
|
||||
reactionsCount = newReactionsCount
|
||||
}
|
||||
@ -534,6 +567,47 @@ fun LikeText(baseNote: Note, grayTint: Color) {
|
||||
)
|
||||
}
|
||||
|
||||
private fun likeClick(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
scope: CoroutineScope,
|
||||
context: Context,
|
||||
onMultipleChoices: () -> Unit
|
||||
) {
|
||||
if (accountViewModel.account.reactionChoices.isEmpty()) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.no_reaction_type_setup_long_press_to_change),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
} else if (!accountViewModel.isWriteable()) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
} else if (accountViewModel.account.reactionChoices.size == 1) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val reaction = accountViewModel.account.reactionChoices.first()
|
||||
if (accountViewModel.hasReactedTo(baseNote, reaction)) {
|
||||
accountViewModel.deleteReactionTo(baseNote, reaction)
|
||||
} else {
|
||||
accountViewModel.reactTo(baseNote, reaction)
|
||||
}
|
||||
}
|
||||
} else if (accountViewModel.account.reactionChoices.size > 1) {
|
||||
onMultipleChoices()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
fun ZapReaction(
|
||||
@ -863,6 +937,95 @@ private fun BoostTypeChoicePopup(baseNote: Note, accountViewModel: AccountViewMo
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ReactionChoicePopup(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
onDismiss: () -> Unit,
|
||||
onChangeAmount: () -> Unit
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account ?: return
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val toRemove = remember {
|
||||
baseNote.reactedBy(account.userProfile()).toSet()
|
||||
}
|
||||
|
||||
Popup(
|
||||
alignment = Alignment.BottomCenter,
|
||||
offset = IntOffset(0, -50),
|
||||
onDismissRequest = { onDismiss() }
|
||||
) {
|
||||
FlowRow(horizontalArrangement = Arrangement.Center) {
|
||||
account.reactionChoices.forEach { reactionType ->
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.reactToOrDelete(
|
||||
baseNote,
|
||||
reactionType
|
||||
)
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
)
|
||||
) {
|
||||
val thisModifier = remember(reactionType) {
|
||||
Modifier.combinedClickable(
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.reactToOrDelete(
|
||||
baseNote,
|
||||
reactionType
|
||||
)
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
onChangeAmount()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val removeSymbol = remember(reactionType) {
|
||||
if (reactionType in toRemove) {
|
||||
" ✖"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
when (reactionType) {
|
||||
"+" -> {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_liked),
|
||||
null,
|
||||
modifier = remember { thisModifier.size(16.dp) },
|
||||
tint = Color.White
|
||||
)
|
||||
Text(text = removeSymbol, color = Color.White, textAlign = TextAlign.Center, modifier = thisModifier)
|
||||
}
|
||||
"-" -> Text(text = "\uD83D\uDC4E$removeSymbol", color = Color.White, textAlign = TextAlign.Center, modifier = thisModifier)
|
||||
else -> Text(
|
||||
"$reactionType$removeSymbol",
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = thisModifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ZapAmountChoicePopup(
|
||||
|
@ -0,0 +1,231 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
class UpdateReactionTypeViewModel(val account: Account) : ViewModel() {
|
||||
var nextChoice by mutableStateOf(TextFieldValue(""))
|
||||
var reactionSet by mutableStateOf(listOf<String>())
|
||||
|
||||
fun load() {
|
||||
this.reactionSet = account.reactionChoices
|
||||
}
|
||||
|
||||
fun toListOfChoices(commaSeparatedAmounts: String): List<Long> {
|
||||
return commaSeparatedAmounts.split(",").map { it.trim().toLongOrNull() ?: 0 }
|
||||
}
|
||||
|
||||
fun addChoice() {
|
||||
val newValue = nextChoice.text.trim()
|
||||
reactionSet = reactionSet + newValue
|
||||
|
||||
nextChoice = TextFieldValue("")
|
||||
}
|
||||
|
||||
fun removeChoice(reaction: String) {
|
||||
reactionSet = reactionSet - reaction
|
||||
}
|
||||
|
||||
fun sendPost() {
|
||||
account.changeReactionTypes(reactionSet)
|
||||
nextChoice = TextFieldValue("")
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
nextChoice = TextFieldValue("")
|
||||
}
|
||||
|
||||
fun hasChanged(): Boolean {
|
||||
return reactionSet != account.reactionChoices
|
||||
}
|
||||
|
||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||
override fun <UpdateReactionTypeViewModel : ViewModel> create(modelClass: Class<UpdateReactionTypeViewModel>): UpdateReactionTypeViewModel {
|
||||
return UpdateReactionTypeViewModel(account) as UpdateReactionTypeViewModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun UpdateReactionTypeDialog(onClose: () -> Unit, nip47uri: String? = null, accountViewModel: AccountViewModel) {
|
||||
val postViewModel: UpdateReactionTypeViewModel = viewModel(
|
||||
key = accountViewModel.userProfile().pubkeyHex,
|
||||
factory = UpdateReactionTypeViewModel.Factory(accountViewModel.account)
|
||||
)
|
||||
|
||||
LaunchedEffect(accountViewModel) {
|
||||
postViewModel.load()
|
||||
}
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = { onClose() },
|
||||
properties = DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
dismissOnClickOutside = false,
|
||||
decorFitsSystemWindows = false
|
||||
)
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column(modifier = Modifier.padding(10.dp).imePadding()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CloseButton(onCancel = {
|
||||
postViewModel.cancel()
|
||||
onClose()
|
||||
})
|
||||
|
||||
SaveButton(
|
||||
onPost = {
|
||||
postViewModel.sendPost()
|
||||
onClose()
|
||||
},
|
||||
isActive = postViewModel.hasChanged()
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.animateContentSize()) {
|
||||
FlowRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
postViewModel.reactionSet.forEach { reactionType ->
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
),
|
||||
onClick = {
|
||||
postViewModel.removeChoice(reactionType)
|
||||
}
|
||||
) {
|
||||
when (reactionType) {
|
||||
"+" -> {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_liked),
|
||||
null,
|
||||
modifier = remember { Modifier.size(16.dp) },
|
||||
tint = Color.White
|
||||
)
|
||||
Text(text = " ✖", color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
"-" -> Text(text = "\uD83D\uDC4E ✖", color = Color.White, textAlign = TextAlign.Center)
|
||||
else -> Text(text = "$reactionType ✖", color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringResource(R.string.new_reaction_symbol)) },
|
||||
value = postViewModel.nextChoice,
|
||||
onValueChange = {
|
||||
postViewModel.nextChoice = it
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
keyboardType = KeyboardType.Text
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "\uD83D\uDCAF, \uD83C\uDF89, \uD83D\uDC4E",
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
},
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
.padding(end = 10.dp)
|
||||
.weight(1f)
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = { postViewModel.addChoice() },
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
)
|
||||
) {
|
||||
Text(text = stringResource(R.string.add), color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,8 @@ import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
|
||||
@Immutable
|
||||
abstract class Card() {
|
||||
@ -41,7 +43,12 @@ class ZapUserSetCard(val user: User, val zapEvents: ImmutableList<CombinedZap>)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
class MultiSetCard(val note: Note, val boostEvents: ImmutableList<Note>, val likeEvents: ImmutableList<Note>, val zapEvents: ImmutableList<CombinedZap>) : Card() {
|
||||
class MultiSetCard(
|
||||
val note: Note,
|
||||
val boostEvents: ImmutableList<Note>,
|
||||
val likeEvents: ImmutableList<Note>,
|
||||
val zapEvents: ImmutableList<CombinedZap>
|
||||
) : Card() {
|
||||
val maxCreatedAt = maxOf(
|
||||
zapEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0,
|
||||
likeEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0,
|
||||
@ -54,6 +61,10 @@ class MultiSetCard(val note: Note, val boostEvents: ImmutableList<Note>, val lik
|
||||
boostEvents.minOfOrNull { it.createdAt() ?: Long.MAX_VALUE } ?: Long.MAX_VALUE
|
||||
)
|
||||
|
||||
val likeEventsByType = likeEvents.groupBy { it.event?.content() ?: "+" }.mapValues {
|
||||
it.value.toImmutableList()
|
||||
}.toImmutableMap()
|
||||
|
||||
override fun createdAt(): Long {
|
||||
return maxCreatedAt
|
||||
}
|
||||
|
@ -39,16 +39,25 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
||||
return account.userProfile()
|
||||
}
|
||||
|
||||
fun reactTo(note: Note) {
|
||||
account.reactTo(note)
|
||||
fun reactTo(note: Note, reaction: String) {
|
||||
account.reactTo(note, reaction)
|
||||
}
|
||||
|
||||
fun hasReactedTo(baseNote: Note): Boolean {
|
||||
return account.hasReacted(baseNote)
|
||||
fun reactToOrDelete(note: Note, reaction: String) {
|
||||
val currentReactions = account.reactionTo(note, reaction)
|
||||
if (currentReactions.isNotEmpty()) {
|
||||
account.delete(currentReactions)
|
||||
} else {
|
||||
account.reactTo(note, reaction)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteReactionTo(note: Note) {
|
||||
account.delete(account.reactionTo(note))
|
||||
fun hasReactedTo(baseNote: Note, reaction: String): Boolean {
|
||||
return account.hasReacted(baseNote, reaction)
|
||||
}
|
||||
|
||||
fun deleteReactionTo(note: Note, reaction: String) {
|
||||
account.delete(account.reactionTo(note, reaction))
|
||||
}
|
||||
|
||||
fun hasBoosted(baseNote: Note): Boolean {
|
||||
|
@ -414,4 +414,7 @@
|
||||
<string name="recommended_apps">Recommends: </string>
|
||||
<string name="filter_spam_from_strangers">Filter spam from strangers</string>
|
||||
<string name="warn_when_posts_have_reports_from_your_follows">Warn when posts have reports from your follows</string>
|
||||
|
||||
<string name="new_reaction_symbol">New Reaction Symbol</string>
|
||||
<string name="no_reaction_type_setup_long_press_to_change">No reaction types selected. Long Press to change</string>
|
||||
</resources>
|
||||
|
Loading…
Reference in New Issue
Block a user