Skip to content

Commit

Permalink
Merge pull request #512 from DimensionDev/feature/vote_support
Browse files Browse the repository at this point in the history
add vote support
  • Loading branch information
Tlaster authored Oct 15, 2024
2 parents 85976b7 + b5d700c commit 0a9a8c1
Show file tree
Hide file tree
Showing 10 changed files with 334 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -1,36 +1,43 @@
package dev.dimension.flare.ui.component.status

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.Card
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
Expand Down Expand Up @@ -93,6 +100,7 @@ import dev.dimension.flare.ui.model.onSuccess
import dev.dimension.flare.ui.screen.status.statusTranslatePresenter
import dev.dimension.flare.ui.theme.MediumAlpha
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList

@Composable
fun CommonStatusComponent(
Expand Down Expand Up @@ -772,39 +780,159 @@ private fun StatusPollComponent(
poll: UiPoll,
modifier: Modifier = Modifier,
) {
val selectedOptions =
remember {
mutableStateListOf(*poll.ownVotes.toTypedArray())
}
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
poll.options.forEach { option ->
Column {
Row {
Box {
Text(
text = option.humanizedPercentage,
poll.options.forEachIndexed { index, option ->
PollOption(
option = option,
modifier = Modifier.fillMaxWidth(),
canVote = poll.canVote,
multiple = poll.multiple,
selected = selectedOptions.contains(index),
onClick = {
if (poll.multiple) {
if (selectedOptions.contains(index)) {
selectedOptions.remove(index)
} else {
selectedOptions.add(index)
}
} else {
selectedOptions.clear()
selectedOptions.add(index)
}
},
)
}
if (poll.expired) {
Text(
text = stringResource(id = R.string.poll_expired),
modifier =
Modifier
.align(Alignment.End)
.alpha(MediumAlpha),
style = MaterialTheme.typography.bodySmall,
)
} else {
Text(
text =
stringResource(
id = R.string.poll_expired_at,
poll.expiresAt.localizedFullTime,
),
modifier =
Modifier
.align(Alignment.End)
.alpha(MediumAlpha),
style = MaterialTheme.typography.bodySmall,
)
}
if (poll.canVote) {
FilledTonalButton(
modifier = Modifier.fillMaxWidth(),
onClick = {
poll.onVote.invoke(selectedOptions.toImmutableList())
},
) {
Text(
text = stringResource(id = R.string.vote),
)
}
}
}
}

@Composable
private fun PollOption(
canVote: Boolean,
multiple: Boolean,
option: UiPoll.Option,
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(
modifier =
modifier
.height(IntrinsicSize.Min)
.border(
width = FlareDividerDefaults.thickness,
color = FlareDividerDefaults.color,
shape = MaterialTheme.shapes.small,
)
// .background(
// color = MaterialTheme.colorScheme.secondaryContainer,
// shape = MaterialTheme.shapes.small,
// )
.clip(
shape = MaterialTheme.shapes.small,
),
) {
Box(
modifier =
Modifier
.fillMaxHeight()
.fillMaxWidth(option.percentage)
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.small,
),
)
val mutableInteractionSource =
remember {
MutableInteractionSource()
}
ListComponent(
modifier =
Modifier.clickable(
onClick = onClick,
interactionSource = mutableInteractionSource,
indication = LocalIndication.current,
enabled = canVote,
),
headlineContent = {
Text(
text = option.title,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier =
Modifier
.padding(8.dp),
)
},
trailingContent = {
if (canVote || selected) {
if (multiple) {
Checkbox(
checked = selected,
onCheckedChange = {
onClick.invoke()
},
interactionSource = mutableInteractionSource,
enabled = canVote,
)
Text(
text = "100%",
modifier = Modifier.alpha(0f),
} else {
RadioButton(
selected = selected,
onClick = onClick,
interactionSource = mutableInteractionSource,
enabled = canVote,
)
}
Spacer(modifier = Modifier.width(8.dp))
Text(
text = option.title,
} else {
// keep the height consist
RadioButton(
selected = false,
onClick = {},
enabled = false,
modifier = Modifier.alpha(0f),
)
}
Spacer(modifier = Modifier.height(4.dp))
LinearProgressIndicator(
progress = { option.percentage },
modifier =
Modifier
.fillMaxWidth()
.clip(CircleShape),
)
}
}
Text(
text = poll.expiresAt.localizedFullTime,
},
)
}
}
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -664,5 +664,9 @@
<string name="misskey_achievement_bubble_game_double_exploding_head_description">Two of the biggest objects in the bubble game at the same time</string>
<string name="misskey_achievement_bubble_game_double_exploding_head_flavor">You can fill a lunch box like this 🤯 🤯 a bit.</string>

<string name="vote">Vote</string>
<string name="poll_expired">Poll expired</string>
<string name="poll_voted">Voted</string>
<string name="poll_expired_at">Expired at %1$s</string>

</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import dev.dimension.flare.data.network.mastodon.api.model.PostList
import dev.dimension.flare.data.network.mastodon.api.model.PostPoll
import dev.dimension.flare.data.network.mastodon.api.model.PostReport
import dev.dimension.flare.data.network.mastodon.api.model.PostStatus
import dev.dimension.flare.data.network.mastodon.api.model.PostVote
import dev.dimension.flare.data.network.mastodon.api.model.Visibility
import dev.dimension.flare.data.repository.LocalFilterRepository
import dev.dimension.flare.model.MicroBlogKey
Expand Down Expand Up @@ -1175,4 +1176,82 @@ class MastodonDataSource(
},
)
}

override fun vote(
statusKey: MicroBlogKey,
id: String,
options: List<Int>,
) {
coroutineScope.launch {
updateStatusUseCase<StatusContent.Mastodon>(
statusKey = statusKey,
accountKey = accountKey,
cacheDatabase = database,
update = {
it.copy(
data =
it.data.copy(
poll =
it.data.poll?.copy(
voted = true,
ownVotes = options,
options =
it.data.poll.options?.mapIndexed { index, option ->
if (options.contains(index)) {
option.copy(votesCount = option.votesCount?.plus(1))
} else {
option
}
} ?: emptyList(),
),
),
)
},
)

runCatching {
service.vote(id = id, data = PostVote(choices = options.map { it.toString() }))
}.onFailure {
updateStatusUseCase<StatusContent.Mastodon>(
statusKey = statusKey,
accountKey = accountKey,
cacheDatabase = database,
update = {
it.copy(
data =
it.data.copy(
poll =
it.data.poll?.copy(
voted = false,
ownVotes = null,
options =
it.data.poll.options?.mapIndexed { index, option ->
if (options.contains(index)) {
option.copy(votesCount = option.votesCount?.minus(1))
} else {
option
}
} ?: emptyList(),
),
),
)
},
)
}.onSuccess { result ->
updateStatusUseCase<StatusContent.Mastodon>(
statusKey = statusKey,
accountKey = accountKey,
cacheDatabase = database,
update = {
it.copy(
data =
it.data.copy(
poll = result,
),
)
},
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ sealed interface StatusEvent {
statusKey: MicroBlogKey,
bookmarked: Boolean,
)

fun vote(
statusKey: MicroBlogKey,
id: String,
options: List<Int>,
)
}

interface Misskey : StatusEvent {
Expand All @@ -28,6 +34,11 @@ sealed interface StatusEvent {
)

fun renote(statusKey: MicroBlogKey)

fun vote(
statusKey: MicroBlogKey,
options: List<Int>,
)
}

interface Bluesky : StatusEvent {
Expand Down
Loading

0 comments on commit 0a9a8c1

Please sign in to comment.