mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-04 01:19:33 +00:00
Merge branch 'release/26.09.1'
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
Main changes in this version: important bug fixes and improvements.
|
||||||
|
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||||
+2
-2
@@ -52,9 +52,9 @@ sealed interface TimelineEvent {
|
|||||||
*/
|
*/
|
||||||
sealed interface TimelineItemPollEvent : TimelineItemEvent
|
sealed interface TimelineItemPollEvent : TimelineItemEvent
|
||||||
|
|
||||||
data class SelectPollAnswer(
|
data class SendPollResponse(
|
||||||
val pollStartId: EventId,
|
val pollStartId: EventId,
|
||||||
val answerId: String
|
val answerIds: List<String>
|
||||||
) : TimelineItemPollEvent
|
) : TimelineItemPollEvent
|
||||||
|
|
||||||
data class EndPoll(
|
data class EndPoll(
|
||||||
|
|||||||
+5
-3
@@ -201,13 +201,15 @@ class TimelinePresenter(
|
|||||||
newEventState.value = NewEventState.None
|
newEventState.value = NewEventState.None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is TimelineEvent.SelectPollAnswer -> sessionCoroutineScope.launch {
|
is TimelineEvent.SendPollResponse -> sessionCoroutineScope.launch {
|
||||||
timelineController.invokeOnCurrentTimeline {
|
timelineController.invokeOnCurrentTimeline {
|
||||||
sendPollResponseAction.execute(
|
sendPollResponseAction.execute(
|
||||||
timeline = this,
|
timeline = this,
|
||||||
pollStartId = event.pollStartId,
|
pollStartId = event.pollStartId,
|
||||||
answerId = event.answerId
|
answerIds = event.answerIds,
|
||||||
)
|
).onFailure { error ->
|
||||||
|
Timber.tag(tag).e(error, "Failed to send poll response")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is TimelineEvent.EndPoll -> sessionCoroutineScope.launch {
|
is TimelineEvent.EndPoll -> sessionCoroutineScope.launch {
|
||||||
|
|||||||
+5
-3
@@ -26,8 +26,8 @@ fun TimelineItemPollView(
|
|||||||
eventSink: (TimelineEvent.TimelineItemPollEvent) -> Unit,
|
eventSink: (TimelineEvent.TimelineItemPollEvent) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
fun onSelectAnswer(pollStartId: EventId, answerId: String) {
|
fun onSendPollResponse(pollStartId: EventId, answerIds: List<String>) {
|
||||||
eventSink(TimelineEvent.SelectPollAnswer(pollStartId, answerId))
|
eventSink(TimelineEvent.SendPollResponse(pollStartId, answerIds))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEndPoll(pollStartId: EventId) {
|
fun onEndPoll(pollStartId: EventId) {
|
||||||
@@ -43,10 +43,12 @@ fun TimelineItemPollView(
|
|||||||
question = content.question,
|
question = content.question,
|
||||||
answerItems = content.answerItems.toImmutableList(),
|
answerItems = content.answerItems.toImmutableList(),
|
||||||
pollKind = content.pollKind,
|
pollKind = content.pollKind,
|
||||||
|
isMultipleSelection = content.maxSelections > 1u,
|
||||||
|
maxSelections = content.maxSelections,
|
||||||
isPollEnded = content.isEnded,
|
isPollEnded = content.isEnded,
|
||||||
isPollEditable = content.isEditable,
|
isPollEditable = content.isEditable,
|
||||||
isMine = content.isMine,
|
isMine = content.isMine,
|
||||||
onSelectAnswer = ::onSelectAnswer,
|
onSendPollResponse = ::onSendPollResponse,
|
||||||
onEditPoll = ::onEditPoll,
|
onEditPoll = ::onEditPoll,
|
||||||
onEndPoll = ::onEndPoll,
|
onEndPoll = ::onEndPoll,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|||||||
+1
@@ -33,6 +33,7 @@ class TimelineItemContentPollFactory(
|
|||||||
question = pollContentState.question,
|
question = pollContentState.question,
|
||||||
answerItems = pollContentState.answerItems,
|
answerItems = pollContentState.answerItems,
|
||||||
pollKind = pollContentState.pollKind,
|
pollKind = pollContentState.pollKind,
|
||||||
|
maxSelections = pollContentState.maxSelections,
|
||||||
isEnded = pollContentState.isPollEnded,
|
isEnded = pollContentState.isPollEnded,
|
||||||
isEdited = content.isEdited
|
isEdited = content.isEdited
|
||||||
)
|
)
|
||||||
|
|||||||
+1
@@ -19,6 +19,7 @@ data class TimelineItemPollContent(
|
|||||||
val question: String,
|
val question: String,
|
||||||
val answerItems: List<PollAnswerItem>,
|
val answerItems: List<PollAnswerItem>,
|
||||||
val pollKind: PollKind,
|
val pollKind: PollKind,
|
||||||
|
val maxSelections: ULong,
|
||||||
val isEnded: Boolean,
|
val isEnded: Boolean,
|
||||||
override val isEdited: Boolean,
|
override val isEdited: Boolean,
|
||||||
) : TimelineItemEventContent,
|
) : TimelineItemEventContent,
|
||||||
|
|||||||
+2
@@ -32,12 +32,14 @@ fun aTimelineItemPollContent(
|
|||||||
isEditable: Boolean = false,
|
isEditable: Boolean = false,
|
||||||
isEnded: Boolean = false,
|
isEnded: Boolean = false,
|
||||||
isEdited: Boolean = false,
|
isEdited: Boolean = false,
|
||||||
|
maxSelections: ULong = 1u,
|
||||||
): TimelineItemPollContent {
|
): TimelineItemPollContent {
|
||||||
return TimelineItemPollContent(
|
return TimelineItemPollContent(
|
||||||
eventId = EventId("\$anEventId"),
|
eventId = EventId("\$anEventId"),
|
||||||
pollKind = PollKind.Disclosed,
|
pollKind = PollKind.Disclosed,
|
||||||
question = question,
|
question = question,
|
||||||
answerItems = answerItems,
|
answerItems = answerItems,
|
||||||
|
maxSelections = maxSelections,
|
||||||
isMine = isMine,
|
isMine = isMine,
|
||||||
isEditable = isEditable,
|
isEditable = isEditable,
|
||||||
isEnded = isEnded,
|
isEnded = isEnded,
|
||||||
|
|||||||
+2
-2
@@ -951,14 +951,14 @@ class TimelinePresenterTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - PollAnswerSelected event`() = runTest {
|
fun `present - SendPollResponse event`() = runTest {
|
||||||
val sendPollResponseAction = FakeSendPollResponseAction()
|
val sendPollResponseAction = FakeSendPollResponseAction()
|
||||||
val presenter = createTimelinePresenter(
|
val presenter = createTimelinePresenter(
|
||||||
sendPollResponseAction = sendPollResponseAction,
|
sendPollResponseAction = sendPollResponseAction,
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink.invoke(TimelineEvent.SelectPollAnswer(AN_EVENT_ID, "anAnswerId"))
|
initialState.eventSink.invoke(TimelineEvent.SendPollResponse(AN_EVENT_ID, listOf("anAnswerId")))
|
||||||
}
|
}
|
||||||
delay(1)
|
delay(1)
|
||||||
sendPollResponseAction.verifyExecutionCount(1)
|
sendPollResponseAction.verifyExecutionCount(1)
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@ class TimelineItemPollViewTest : RobolectricTest() {
|
|||||||
matcher = hasText(answer.text),
|
matcher = hasText(answer.text),
|
||||||
useUnmergedTree = true,
|
useUnmergedTree = true,
|
||||||
).performClick()
|
).performClick()
|
||||||
eventsRecorder.assertSingle(TimelineEvent.SelectPollAnswer(content.eventId!!, answer.id))
|
eventsRecorder.assertSingle(TimelineEvent.SendPollResponse(content.eventId!!, listOf(answer.id)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+19
@@ -229,6 +229,25 @@ class TimelineItemContentMessageFactoryTest : RobolectricTest() {
|
|||||||
assertThat((result as TimelineItemTextContent).formattedBody).isEqualTo(SpannedString("body"))
|
assertThat((result as TimelineItemTextContent).formattedBody).isEqualTo(SpannedString("body"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test create TextMessageType with a formatted body containing only an image falls back to the body`() = runTest {
|
||||||
|
val sut = createTimelineItemContentMessageFactory()
|
||||||
|
val result = sut.create(
|
||||||
|
content = createMessageContent(
|
||||||
|
type = TextMessageType(
|
||||||
|
body = "\uD83D\uDE1C",
|
||||||
|
formatted = FormattedBody(MessageFormat.HTML, "<img src=\"mxc://matrix.org/anImage\" alt=\"\uD83D\uDE1C\" />")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
senderId = A_USER_ID,
|
||||||
|
senderProfile = aProfileDetails(),
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
)
|
||||||
|
assertThat((result as TimelineItemTextContent).formattedBody).isEqualTo(SpannedString("\uD83D\uDE1C"))
|
||||||
|
assertThat(result.htmlDocument).isNull()
|
||||||
|
assertThat(result.plainText).isEqualTo("\uD83D\uDE1C")
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test create VideoMessageType`() = runTest {
|
fun `test create VideoMessageType`() = runTest {
|
||||||
val sut = createTimelineItemContentMessageFactory()
|
val sut = createTimelineItemContentMessageFactory()
|
||||||
|
|||||||
+3
-3
@@ -12,17 +12,17 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends the current user's answer to a poll, wrapping the timeline call so the analytics are captured in one place.
|
* Sends the current user's answer(s) to a poll, wrapping the timeline call so the analytics are captured in one place.
|
||||||
*/
|
*/
|
||||||
interface SendPollResponseAction {
|
interface SendPollResponseAction {
|
||||||
/**
|
/**
|
||||||
* @param timeline the timeline the poll lives in.
|
* @param timeline the timeline the poll lives in.
|
||||||
* @param pollStartId the poll start event being answered.
|
* @param pollStartId the poll start event being answered.
|
||||||
* @param answerId the id of the chosen answer.
|
* @param answerIds the ids of the chosen answers.
|
||||||
*/
|
*/
|
||||||
suspend fun execute(
|
suspend fun execute(
|
||||||
timeline: Timeline,
|
timeline: Timeline,
|
||||||
pollStartId: EventId,
|
pollStartId: EventId,
|
||||||
answerId: String
|
answerIds: List<String>,
|
||||||
): Result<Unit>
|
): Result<Unit>
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -18,6 +18,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||||||
* @property question the poll question.
|
* @property question the poll question.
|
||||||
* @property answerItems the list of answers.
|
* @property answerItems the list of answers.
|
||||||
* @property pollKind the kind of poll.
|
* @property pollKind the kind of poll.
|
||||||
|
* @property maxSelections the maximum number of selections allowed.
|
||||||
* @property isPollEditable whether the poll is editable.
|
* @property isPollEditable whether the poll is editable.
|
||||||
* @property isPollEnded whether the poll is ended.
|
* @property isPollEnded whether the poll is ended.
|
||||||
* @property isMine whether the poll has been created by me.
|
* @property isMine whether the poll has been created by me.
|
||||||
@@ -27,7 +28,10 @@ data class PollContentState(
|
|||||||
val question: String,
|
val question: String,
|
||||||
val answerItems: ImmutableList<PollAnswerItem>,
|
val answerItems: ImmutableList<PollAnswerItem>,
|
||||||
val pollKind: PollKind,
|
val pollKind: PollKind,
|
||||||
|
val maxSelections: ULong,
|
||||||
val isPollEditable: Boolean,
|
val isPollEditable: Boolean,
|
||||||
val isPollEnded: Boolean,
|
val isPollEnded: Boolean,
|
||||||
val isMine: Boolean,
|
val isMine: Boolean,
|
||||||
)
|
) {
|
||||||
|
val isMultipleSelection = maxSelections > 1u
|
||||||
|
}
|
||||||
|
|||||||
+2
@@ -84,6 +84,7 @@ fun aPollContentState(
|
|||||||
hasVotes: Boolean = true,
|
hasVotes: Boolean = true,
|
||||||
question: String = aPollQuestion(),
|
question: String = aPollQuestion(),
|
||||||
pollKind: PollKind = PollKind.Disclosed,
|
pollKind: PollKind = PollKind.Disclosed,
|
||||||
|
maxSelections: ULong = 1u,
|
||||||
answerItems: ImmutableList<PollAnswerItem> = aPollAnswerItemList(
|
answerItems: ImmutableList<PollAnswerItem> = aPollAnswerItemList(
|
||||||
isEnded = isEnded,
|
isEnded = isEnded,
|
||||||
showVotes = showVotes,
|
showVotes = showVotes,
|
||||||
@@ -94,6 +95,7 @@ fun aPollContentState(
|
|||||||
question = question,
|
question = question,
|
||||||
answerItems = answerItems,
|
answerItems = answerItems,
|
||||||
pollKind = pollKind,
|
pollKind = pollKind,
|
||||||
|
maxSelections = maxSelections,
|
||||||
isPollEditable = isMine && !isEnded && isPollEditable,
|
isPollEditable = isMine && !isEnded && isPollEditable,
|
||||||
isPollEnded = isEnded,
|
isPollEnded = isEnded,
|
||||||
isMine = isMine,
|
isMine = isMine,
|
||||||
|
|||||||
+69
-12
@@ -40,7 +40,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||||||
@Composable
|
@Composable
|
||||||
fun PollContentView(
|
fun PollContentView(
|
||||||
state: PollContentState,
|
state: PollContentState,
|
||||||
onSelectAnswer: (pollStartId: EventId, answerId: String) -> Unit,
|
onSendPollResponse: (pollStartId: EventId, answerIds: List<String>) -> Unit,
|
||||||
onEditPoll: (pollStartId: EventId) -> Unit,
|
onEditPoll: (pollStartId: EventId) -> Unit,
|
||||||
onEndPoll: (pollStartId: EventId) -> Unit,
|
onEndPoll: (pollStartId: EventId) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -50,11 +50,13 @@ fun PollContentView(
|
|||||||
question = state.question,
|
question = state.question,
|
||||||
answerItems = state.answerItems,
|
answerItems = state.answerItems,
|
||||||
pollKind = state.pollKind,
|
pollKind = state.pollKind,
|
||||||
|
isMultipleSelection = state.isMultipleSelection,
|
||||||
|
maxSelections = state.maxSelections,
|
||||||
isPollEditable = state.isPollEditable,
|
isPollEditable = state.isPollEditable,
|
||||||
isPollEnded = state.isPollEnded,
|
isPollEnded = state.isPollEnded,
|
||||||
isMine = state.isMine,
|
isMine = state.isMine,
|
||||||
onEditPoll = onEditPoll,
|
onEditPoll = onEditPoll,
|
||||||
onSelectAnswer = onSelectAnswer,
|
onSendPollResponse = onSendPollResponse,
|
||||||
onEndPoll = onEndPoll,
|
onEndPoll = onEndPoll,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
@@ -66,10 +68,12 @@ fun PollContentView(
|
|||||||
question: String,
|
question: String,
|
||||||
answerItems: ImmutableList<PollAnswerItem>,
|
answerItems: ImmutableList<PollAnswerItem>,
|
||||||
pollKind: PollKind,
|
pollKind: PollKind,
|
||||||
|
isMultipleSelection: Boolean,
|
||||||
|
maxSelections: ULong,
|
||||||
isPollEditable: Boolean,
|
isPollEditable: Boolean,
|
||||||
isPollEnded: Boolean,
|
isPollEnded: Boolean,
|
||||||
isMine: Boolean,
|
isMine: Boolean,
|
||||||
onSelectAnswer: (pollStartId: EventId, answerId: String) -> Unit,
|
onSendPollResponse: (pollStartId: EventId, answerIds: List<String>) -> Unit,
|
||||||
onEditPoll: (pollStartId: EventId) -> Unit,
|
onEditPoll: (pollStartId: EventId) -> Unit,
|
||||||
onEndPoll: (pollStartId: EventId) -> Unit,
|
onEndPoll: (pollStartId: EventId) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -77,7 +81,23 @@ fun PollContentView(
|
|||||||
val votesCount = remember(answerItems) { answerItems.sumOf { it.votesCount } }
|
val votesCount = remember(answerItems) { answerItems.sumOf { it.votesCount } }
|
||||||
|
|
||||||
fun onSelectAnswer(pollAnswer: PollAnswer) {
|
fun onSelectAnswer(pollAnswer: PollAnswer) {
|
||||||
eventId?.let { onSelectAnswer(it, pollAnswer.id) }
|
eventId?.let {
|
||||||
|
if (isMultipleSelection) {
|
||||||
|
val newSelections = answerItems.map { item ->
|
||||||
|
if (item.answer.id == pollAnswer.id) {
|
||||||
|
item.copy(isSelected = !item.isSelected)
|
||||||
|
} else {
|
||||||
|
item
|
||||||
|
}
|
||||||
|
}.filter { it.isSelected }.map { it.answer.id }
|
||||||
|
// Only send the response if the selection count is within the limit
|
||||||
|
if (newSelections.size <= maxSelections.toInt()) {
|
||||||
|
onSendPollResponse(it, newSelections)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onSendPollResponse(it, listOf(pollAnswer.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEditPoll() {
|
fun onEditPoll() {
|
||||||
@@ -107,7 +127,11 @@ fun PollContentView(
|
|||||||
) {
|
) {
|
||||||
PollTitleView(title = question, isPollEnded = isPollEnded)
|
PollTitleView(title = question, isPollEnded = isPollEnded)
|
||||||
|
|
||||||
PollAnswers(answerItems = answerItems, onSelectAnswer = ::onSelectAnswer)
|
PollAnswers(
|
||||||
|
answerItems = answerItems,
|
||||||
|
isMultipleSelection = isMultipleSelection,
|
||||||
|
onSelectAnswer = ::onSelectAnswer,
|
||||||
|
)
|
||||||
|
|
||||||
if (isPollEnded || pollKind == PollKind.Disclosed) {
|
if (isPollEnded || pollKind == PollKind.Disclosed) {
|
||||||
DisclosedPollBottomNotice(votesCount = votesCount)
|
DisclosedPollBottomNotice(votesCount = votesCount)
|
||||||
@@ -130,8 +154,10 @@ fun PollContentView(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun PollAnswers(
|
private fun PollAnswers(
|
||||||
answerItems: ImmutableList<PollAnswerItem>,
|
answerItems: ImmutableList<PollAnswerItem>,
|
||||||
|
isMultipleSelection: Boolean,
|
||||||
onSelectAnswer: (PollAnswer) -> Unit,
|
onSelectAnswer: (PollAnswer) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val role = if (isMultipleSelection) Role.Checkbox else Role.RadioButton
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.selectableGroup(),
|
modifier = Modifier.selectableGroup(),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
@@ -144,7 +170,7 @@ private fun PollAnswers(
|
|||||||
selected = it.isSelected,
|
selected = it.isSelected,
|
||||||
enabled = it.isEnabled,
|
enabled = it.isEnabled,
|
||||||
onClick = { onSelectAnswer(it.answer) },
|
onClick = { onSelectAnswer(it.answer) },
|
||||||
role = Role.RadioButton,
|
role = role,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -207,10 +233,12 @@ internal fun PollContentViewUndisclosedPreview() = ElementPreview {
|
|||||||
question = "What type of food should we have at the party?",
|
question = "What type of food should we have at the party?",
|
||||||
answerItems = aPollAnswerItemList(showVotes = false),
|
answerItems = aPollAnswerItemList(showVotes = false),
|
||||||
pollKind = PollKind.Undisclosed,
|
pollKind = PollKind.Undisclosed,
|
||||||
|
isMultipleSelection = false,
|
||||||
|
maxSelections = 1u,
|
||||||
isPollEnded = false,
|
isPollEnded = false,
|
||||||
isPollEditable = false,
|
isPollEditable = false,
|
||||||
isMine = false,
|
isMine = false,
|
||||||
onSelectAnswer = { _, _ -> },
|
onSendPollResponse = { _, _ -> },
|
||||||
onEditPoll = {},
|
onEditPoll = {},
|
||||||
onEndPoll = {},
|
onEndPoll = {},
|
||||||
)
|
)
|
||||||
@@ -224,10 +252,31 @@ internal fun PollContentViewDisclosedPreview() = ElementPreview {
|
|||||||
question = "What type of food should we have at the party?",
|
question = "What type of food should we have at the party?",
|
||||||
answerItems = aPollAnswerItemList(),
|
answerItems = aPollAnswerItemList(),
|
||||||
pollKind = PollKind.Disclosed,
|
pollKind = PollKind.Disclosed,
|
||||||
|
isMultipleSelection = false,
|
||||||
|
maxSelections = 1u,
|
||||||
isPollEnded = false,
|
isPollEnded = false,
|
||||||
isPollEditable = false,
|
isPollEditable = false,
|
||||||
isMine = false,
|
isMine = false,
|
||||||
onSelectAnswer = { _, _ -> },
|
onSendPollResponse = { _, _ -> },
|
||||||
|
onEditPoll = {},
|
||||||
|
onEndPoll = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun PollContentViewMultipleSelectionPreview() = ElementPreview {
|
||||||
|
PollContentView(
|
||||||
|
eventId = EventId("\$anEventId"),
|
||||||
|
question = "What type of food should we have at the party?",
|
||||||
|
answerItems = aPollAnswerItemList(),
|
||||||
|
pollKind = PollKind.Disclosed,
|
||||||
|
isMultipleSelection = true,
|
||||||
|
maxSelections = 3u,
|
||||||
|
isPollEnded = false,
|
||||||
|
isPollEditable = false,
|
||||||
|
isMine = false,
|
||||||
|
onSendPollResponse = { _, _ -> },
|
||||||
onEditPoll = {},
|
onEditPoll = {},
|
||||||
onEndPoll = {},
|
onEndPoll = {},
|
||||||
)
|
)
|
||||||
@@ -241,10 +290,12 @@ internal fun PollContentViewEndedPreview() = ElementPreview {
|
|||||||
question = "What type of food should we have at the party?",
|
question = "What type of food should we have at the party?",
|
||||||
answerItems = aPollAnswerItemList(isEnded = true),
|
answerItems = aPollAnswerItemList(isEnded = true),
|
||||||
pollKind = PollKind.Disclosed,
|
pollKind = PollKind.Disclosed,
|
||||||
|
isMultipleSelection = false,
|
||||||
|
maxSelections = 1u,
|
||||||
isPollEnded = true,
|
isPollEnded = true,
|
||||||
isPollEditable = false,
|
isPollEditable = false,
|
||||||
isMine = false,
|
isMine = false,
|
||||||
onSelectAnswer = { _, _ -> },
|
onSendPollResponse = { _, _ -> },
|
||||||
onEditPoll = {},
|
onEditPoll = {},
|
||||||
onEndPoll = {},
|
onEndPoll = {},
|
||||||
)
|
)
|
||||||
@@ -258,10 +309,12 @@ internal fun PollContentViewCreatorEditablePreview() = ElementPreview {
|
|||||||
question = "What type of food should we have at the party?",
|
question = "What type of food should we have at the party?",
|
||||||
answerItems = aPollAnswerItemList(hasVotes = false, isEnded = false),
|
answerItems = aPollAnswerItemList(hasVotes = false, isEnded = false),
|
||||||
pollKind = PollKind.Disclosed,
|
pollKind = PollKind.Disclosed,
|
||||||
|
isMultipleSelection = false,
|
||||||
|
maxSelections = 1u,
|
||||||
isPollEnded = false,
|
isPollEnded = false,
|
||||||
isPollEditable = true,
|
isPollEditable = true,
|
||||||
isMine = true,
|
isMine = true,
|
||||||
onSelectAnswer = { _, _ -> },
|
onSendPollResponse = { _, _ -> },
|
||||||
onEditPoll = {},
|
onEditPoll = {},
|
||||||
onEndPoll = {},
|
onEndPoll = {},
|
||||||
)
|
)
|
||||||
@@ -275,10 +328,12 @@ internal fun PollContentViewCreatorPreview() = ElementPreview {
|
|||||||
question = "What type of food should we have at the party?",
|
question = "What type of food should we have at the party?",
|
||||||
answerItems = aPollAnswerItemList(isEnded = false),
|
answerItems = aPollAnswerItemList(isEnded = false),
|
||||||
pollKind = PollKind.Disclosed,
|
pollKind = PollKind.Disclosed,
|
||||||
|
isMultipleSelection = false,
|
||||||
|
maxSelections = 1u,
|
||||||
isPollEnded = false,
|
isPollEnded = false,
|
||||||
isPollEditable = false,
|
isPollEditable = false,
|
||||||
isMine = true,
|
isMine = true,
|
||||||
onSelectAnswer = { _, _ -> },
|
onSendPollResponse = { _, _ -> },
|
||||||
onEditPoll = {},
|
onEditPoll = {},
|
||||||
onEndPoll = {},
|
onEndPoll = {},
|
||||||
)
|
)
|
||||||
@@ -292,10 +347,12 @@ internal fun PollContentViewCreatorEndedPreview() = ElementPreview {
|
|||||||
question = "What type of food should we have at the party?",
|
question = "What type of food should we have at the party?",
|
||||||
answerItems = aPollAnswerItemList(isEnded = true),
|
answerItems = aPollAnswerItemList(isEnded = true),
|
||||||
pollKind = PollKind.Disclosed,
|
pollKind = PollKind.Disclosed,
|
||||||
|
isMultipleSelection = false,
|
||||||
|
maxSelections = 1u,
|
||||||
isPollEnded = true,
|
isPollEnded = true,
|
||||||
isPollEditable = false,
|
isPollEditable = false,
|
||||||
isMine = true,
|
isMine = true,
|
||||||
onSelectAnswer = { _, _ -> },
|
onSendPollResponse = { _, _ -> },
|
||||||
onEditPoll = {},
|
onEditPoll = {},
|
||||||
onEndPoll = {},
|
onEndPoll = {},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<plurals name="a11y_polls_percent_of_total">
|
<plurals name="a11y_polls_percent_of_total">
|
||||||
<item quantity="other">"占总投票的 %1$d%"</item>
|
<item quantity="other">"占总投票的 %1$d%%"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="a11y_polls_will_remove_selection">"将移除之前的选择"</string>
|
<string name="a11y_polls_will_remove_selection">"将移除之前的选择"</string>
|
||||||
<string name="a11y_polls_winning_answer">"此为胜出的答案"</string>
|
<string name="a11y_polls_winning_answer">"此为胜出的答案"</string>
|
||||||
|
|||||||
+1
-1
@@ -12,5 +12,5 @@ internal object PollConstants {
|
|||||||
const val MIN_ANSWERS = 2
|
const val MIN_ANSWERS = 2
|
||||||
const val MAX_ANSWERS = 20
|
const val MAX_ANSWERS = 20
|
||||||
const val MAX_ANSWER_LENGTH = 240
|
const val MAX_ANSWER_LENGTH = 240
|
||||||
const val MAX_SELECTIONS = 1
|
const val DEFAULT_MAX_SELECTIONS = 1
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -20,10 +20,10 @@ import io.element.android.services.analytics.api.AnalyticsService
|
|||||||
class DefaultSendPollResponseAction(
|
class DefaultSendPollResponseAction(
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
) : SendPollResponseAction {
|
) : SendPollResponseAction {
|
||||||
override suspend fun execute(timeline: Timeline, pollStartId: EventId, answerId: String): Result<Unit> {
|
override suspend fun execute(timeline: Timeline, pollStartId: EventId, answerIds: List<String>): Result<Unit> {
|
||||||
return timeline.sendPollResponse(
|
return timeline.sendPollResponse(
|
||||||
pollStartId = pollStartId,
|
pollStartId = pollStartId,
|
||||||
answers = listOf(answerId),
|
answers = answerIds,
|
||||||
).onSuccess {
|
).onSuccess {
|
||||||
analyticsService.capture(PollVote())
|
analyticsService.capture(PollVote())
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -18,6 +18,7 @@ sealed interface CreatePollEvent {
|
|||||||
data object AddAnswer : CreatePollEvent
|
data object AddAnswer : CreatePollEvent
|
||||||
data class RemoveAnswer(val index: Int) : CreatePollEvent
|
data class RemoveAnswer(val index: Int) : CreatePollEvent
|
||||||
data class SetPollKind(val pollKind: PollKind) : CreatePollEvent
|
data class SetPollKind(val pollKind: PollKind) : CreatePollEvent
|
||||||
|
data class SetMaxSelections(val maxSelections: Int) : CreatePollEvent
|
||||||
data object NavBack : CreatePollEvent
|
data object NavBack : CreatePollEvent
|
||||||
data object ConfirmNavBack : CreatePollEvent
|
data object ConfirmNavBack : CreatePollEvent
|
||||||
data object HideConfirmation : CreatePollEvent
|
data object HideConfirmation : CreatePollEvent
|
||||||
|
|||||||
+7
-2
@@ -24,7 +24,6 @@ import im.vector.app.features.analytics.plan.Composer
|
|||||||
import im.vector.app.features.analytics.plan.PollCreation
|
import im.vector.app.features.analytics.plan.PollCreation
|
||||||
import io.element.android.features.messages.api.MessageComposerContext
|
import io.element.android.features.messages.api.MessageComposerContext
|
||||||
import io.element.android.features.poll.api.create.CreatePollMode
|
import io.element.android.features.poll.api.create.CreatePollMode
|
||||||
import io.element.android.features.poll.impl.PollConstants.MAX_SELECTIONS
|
|
||||||
import io.element.android.features.poll.impl.data.PollRepository
|
import io.element.android.features.poll.impl.data.PollRepository
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||||
@@ -81,6 +80,7 @@ class CreatePollPresenter(
|
|||||||
question = it.question,
|
question = it.question,
|
||||||
answers = it.answers.map(PollAnswer::text).toImmutableList(),
|
answers = it.answers.map(PollAnswer::text).toImmutableList(),
|
||||||
isDisclosed = it.kind.isDisclosed,
|
isDisclosed = it.kind.isDisclosed,
|
||||||
|
maxSelections = it.maxSelections.toInt(),
|
||||||
)
|
)
|
||||||
initialPoll = loadedPoll
|
initialPoll = loadedPoll
|
||||||
poll = loadedPoll
|
poll = loadedPoll
|
||||||
@@ -109,7 +109,7 @@ class CreatePollPresenter(
|
|||||||
question = poll.question,
|
question = poll.question,
|
||||||
answers = poll.answers,
|
answers = poll.answers,
|
||||||
pollKind = poll.pollKind,
|
pollKind = poll.pollKind,
|
||||||
maxSelections = MAX_SELECTIONS,
|
maxSelections = poll.maxSelections,
|
||||||
).onSuccess {
|
).onSuccess {
|
||||||
analyticsService.capturePollSaved(
|
analyticsService.capturePollSaved(
|
||||||
isUndisclosed = poll.pollKind == PollKind.Undisclosed,
|
isUndisclosed = poll.pollKind == PollKind.Undisclosed,
|
||||||
@@ -151,6 +151,9 @@ class CreatePollPresenter(
|
|||||||
is CreatePollEvent.SetPollKind -> {
|
is CreatePollEvent.SetPollKind -> {
|
||||||
poll = poll.copy(isDisclosed = event.pollKind.isDisclosed)
|
poll = poll.copy(isDisclosed = event.pollKind.isDisclosed)
|
||||||
}
|
}
|
||||||
|
is CreatePollEvent.SetMaxSelections -> {
|
||||||
|
poll = poll.copy(maxSelections = event.maxSelections)
|
||||||
|
}
|
||||||
is CreatePollEvent.SetQuestion -> {
|
is CreatePollEvent.SetQuestion -> {
|
||||||
poll = poll.copy(question = event.question)
|
poll = poll.copy(question = event.question)
|
||||||
}
|
}
|
||||||
@@ -182,6 +185,8 @@ class CreatePollPresenter(
|
|||||||
question = poll.question,
|
question = poll.question,
|
||||||
answers = immutableAnswers,
|
answers = immutableAnswers,
|
||||||
pollKind = poll.pollKind,
|
pollKind = poll.pollKind,
|
||||||
|
maxSelections = poll.maxSelections,
|
||||||
|
maxAllowedSelections = poll.answers.size,
|
||||||
showBackConfirmation = showBackConfirmation,
|
showBackConfirmation = showBackConfirmation,
|
||||||
showDeleteConfirmation = showDeleteConfirmation,
|
showDeleteConfirmation = showDeleteConfirmation,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
|
|||||||
+2
@@ -18,6 +18,8 @@ data class CreatePollState(
|
|||||||
val question: String,
|
val question: String,
|
||||||
val answers: ImmutableList<Answer>,
|
val answers: ImmutableList<Answer>,
|
||||||
val pollKind: PollKind,
|
val pollKind: PollKind,
|
||||||
|
val maxSelections: Int,
|
||||||
|
val maxAllowedSelections: Int,
|
||||||
val showBackConfirmation: Boolean,
|
val showBackConfirmation: Boolean,
|
||||||
val showDeleteConfirmation: Boolean,
|
val showDeleteConfirmation: Boolean,
|
||||||
val eventSink: (CreatePollEvent) -> Unit,
|
val eventSink: (CreatePollEvent) -> Unit,
|
||||||
|
|||||||
+19
-1
@@ -150,6 +150,21 @@ class CreatePollStatePreviewParam : PreviewParameterProvider<CreatePollState> {
|
|||||||
showDeleteConfirmation = true,
|
showDeleteConfirmation = true,
|
||||||
showBackConfirmation = false,
|
showBackConfirmation = false,
|
||||||
),
|
),
|
||||||
|
aCreatePollState(
|
||||||
|
mode = CreatePollState.Mode.New,
|
||||||
|
canCreate = true,
|
||||||
|
canAddAnswer = true,
|
||||||
|
question = "What type of food should we have?",
|
||||||
|
answers = listOf(
|
||||||
|
Answer("Italian \uD83C\uDDEE\uD83C\uDDF9", false),
|
||||||
|
Answer("Chinese \uD83C\uDDE8\uD83C\uDDF3", false),
|
||||||
|
Answer("Brazilian \uD83C\uDDE7\uD83C\uDDF7", true),
|
||||||
|
),
|
||||||
|
showBackConfirmation = false,
|
||||||
|
showDeleteConfirmation = false,
|
||||||
|
pollKind = PollKind.Disclosed,
|
||||||
|
maxSelections = 2,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +176,8 @@ private fun aCreatePollState(
|
|||||||
answers: List<Answer>,
|
answers: List<Answer>,
|
||||||
showBackConfirmation: Boolean,
|
showBackConfirmation: Boolean,
|
||||||
showDeleteConfirmation: Boolean,
|
showDeleteConfirmation: Boolean,
|
||||||
pollKind: PollKind
|
pollKind: PollKind,
|
||||||
|
maxSelections: Int = 1,
|
||||||
): CreatePollState {
|
): CreatePollState {
|
||||||
return CreatePollState(
|
return CreatePollState(
|
||||||
mode = mode,
|
mode = mode,
|
||||||
@@ -172,6 +188,8 @@ private fun aCreatePollState(
|
|||||||
showBackConfirmation = showBackConfirmation,
|
showBackConfirmation = showBackConfirmation,
|
||||||
showDeleteConfirmation = showDeleteConfirmation,
|
showDeleteConfirmation = showDeleteConfirmation,
|
||||||
pollKind = pollKind,
|
pollKind = pollKind,
|
||||||
|
maxSelections = maxSelections,
|
||||||
|
maxAllowedSelections = answers.size,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+62
@@ -11,11 +11,13 @@ package io.element.android.features.poll.impl.create
|
|||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
@@ -25,6 +27,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
@@ -32,6 +35,8 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.poll.impl.R
|
import io.element.android.features.poll.impl.R
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
@@ -173,6 +178,17 @@ fun CreatePollView(
|
|||||||
item {
|
item {
|
||||||
Column {
|
Column {
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
|
ListItem(
|
||||||
|
content = { Text(text = stringResource(id = R.string.screen_create_poll_max_selections_headline)) },
|
||||||
|
trailingContent = ListItemContent.Custom {
|
||||||
|
MaxSelectionsStepper(
|
||||||
|
current = state.maxSelections,
|
||||||
|
max = state.maxAllowedSelections,
|
||||||
|
onDecrement = { state.eventSink(CreatePollEvent.SetMaxSelections(state.maxSelections - 1)) },
|
||||||
|
onIncrement = { state.eventSink(CreatePollEvent.SetMaxSelections(state.maxSelections + 1)) },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
ListItem(
|
ListItem(
|
||||||
content = { Text(text = stringResource(id = R.string.screen_create_poll_anonymous_headline)) },
|
content = { Text(text = stringResource(id = R.string.screen_create_poll_anonymous_headline)) },
|
||||||
supportingContent = { Text(text = stringResource(id = R.string.screen_create_poll_anonymous_desc)) },
|
supportingContent = { Text(text = stringResource(id = R.string.screen_create_poll_anonymous_desc)) },
|
||||||
@@ -229,6 +245,52 @@ private fun CreatePollTopAppBar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MaxSelectionsStepper(
|
||||||
|
current: Int,
|
||||||
|
max: Int,
|
||||||
|
onDecrement: () -> Unit,
|
||||||
|
onIncrement: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = CompoundIcons.Minus(),
|
||||||
|
contentDescription = stringResource(id = CommonStrings.a11y_create_poll_votes_allowed_decrease),
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(enabled = current > 1) { onDecrement() }
|
||||||
|
.size(32.dp)
|
||||||
|
.padding(4.dp),
|
||||||
|
tint = if (current > 1) {
|
||||||
|
ElementTheme.colors.bgAccentRest
|
||||||
|
} else {
|
||||||
|
ElementTheme.colors.iconDisabled
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = current.toString(),
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp),
|
||||||
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
imageVector = CompoundIcons.Plus(),
|
||||||
|
contentDescription = stringResource(id = CommonStrings.a11y_create_poll_votes_allowed_increase),
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(enabled = current < max) { onIncrement() }
|
||||||
|
.size(32.dp)
|
||||||
|
.padding(4.dp),
|
||||||
|
tint = if (current < max) {
|
||||||
|
ElementTheme.colors.bgAccentRest
|
||||||
|
} else {
|
||||||
|
ElementTheme.colors.iconDisabled
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun CreatePollViewPreview(
|
internal fun CreatePollViewPreview(
|
||||||
|
|||||||
+7
-1
@@ -25,12 +25,14 @@ data class PollFormState(
|
|||||||
val question: String,
|
val question: String,
|
||||||
val answers: ImmutableList<String>,
|
val answers: ImmutableList<String>,
|
||||||
val isDisclosed: Boolean,
|
val isDisclosed: Boolean,
|
||||||
|
val maxSelections: Int,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
val Empty = PollFormState(
|
val Empty = PollFormState(
|
||||||
question = "",
|
question = "",
|
||||||
answers = MutableList(MIN_ANSWERS) { "" }.toImmutableList(),
|
answers = MutableList(MIN_ANSWERS) { "" }.toImmutableList(),
|
||||||
isDisclosed = true,
|
isDisclosed = true,
|
||||||
|
maxSelections = 1,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +69,9 @@ data class PollFormState(
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
return copy(answers = answers.filterIndexed { i, _ -> i != index }.toImmutableList())
|
val newAnswers = answers.filterIndexed { i, _ -> i != index }.toImmutableList()
|
||||||
|
val clampedMaxSelections = maxSelections.coerceIn(1, newAnswers.size)
|
||||||
|
return copy(answers = newAnswers, maxSelections = clampedMaxSelections)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -110,6 +114,7 @@ internal val pollFormStateSaver = mapSaver(
|
|||||||
"question" to it.question,
|
"question" to it.question,
|
||||||
"answers" to it.answers.toTypedArray(),
|
"answers" to it.answers.toTypedArray(),
|
||||||
"isDisclosed" to it.isDisclosed,
|
"isDisclosed" to it.isDisclosed,
|
||||||
|
"maxSelections" to it.maxSelections,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
restore = { saved ->
|
restore = { saved ->
|
||||||
@@ -117,6 +122,7 @@ internal val pollFormStateSaver = mapSaver(
|
|||||||
question = saved["question"] as String,
|
question = saved["question"] as String,
|
||||||
answers = (saved["answers"] as Array<*>).map { it as String }.toImmutableList(),
|
answers = (saved["answers"] as Array<*>).map { it as String }.toImmutableList(),
|
||||||
isDisclosed = saved["isDisclosed"] as Boolean,
|
isDisclosed = saved["isDisclosed"] as Boolean,
|
||||||
|
maxSelections = saved["maxSelections"] as Int,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||||||
|
|
||||||
sealed interface PollHistoryEvent {
|
sealed interface PollHistoryEvent {
|
||||||
data object LoadMore : PollHistoryEvent
|
data object LoadMore : PollHistoryEvent
|
||||||
data class SelectPollAnswer(val pollStartId: EventId, val answerId: String) : PollHistoryEvent
|
data class SendPollResponse(val pollStartId: EventId, val answerIds: List<String>) : PollHistoryEvent
|
||||||
data class EndPoll(val pollStartId: EventId) : PollHistoryEvent
|
data class EndPoll(val pollStartId: EventId) : PollHistoryEvent
|
||||||
data class SelectFilter(val filter: PollHistoryFilter) : PollHistoryEvent
|
data class SelectFilter(val filter: PollHistoryFilter) : PollHistoryEvent
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-3
@@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class PollHistoryPresenter(
|
class PollHistoryPresenter(
|
||||||
@@ -68,12 +69,14 @@ class PollHistoryPresenter(
|
|||||||
is PollHistoryEvent.LoadMore -> {
|
is PollHistoryEvent.LoadMore -> {
|
||||||
coroutineScope.loadMore(timeline)
|
coroutineScope.loadMore(timeline)
|
||||||
}
|
}
|
||||||
is PollHistoryEvent.SelectPollAnswer -> sessionCoroutineScope.launch {
|
is PollHistoryEvent.SendPollResponse -> sessionCoroutineScope.launch {
|
||||||
sendPollResponseAction.execute(
|
sendPollResponseAction.execute(
|
||||||
timeline = timeline,
|
timeline = timeline,
|
||||||
pollStartId = event.pollStartId,
|
pollStartId = event.pollStartId,
|
||||||
answerId = event.answerId
|
answerIds = event.answerIds
|
||||||
)
|
).onFailure { error ->
|
||||||
|
Timber.e(error, "Failed to send poll response")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is PollHistoryEvent.EndPoll -> sessionCoroutineScope.launch {
|
is PollHistoryEvent.EndPoll -> sessionCoroutineScope.launch {
|
||||||
endPollAction.execute(timeline = timeline, pollStartId = event.pollStartId)
|
endPollAction.execute(timeline = timeline, pollStartId = event.pollStartId)
|
||||||
|
|||||||
+7
-7
@@ -65,8 +65,8 @@ fun PollHistoryView(
|
|||||||
state.eventSink(PollHistoryEvent.LoadMore)
|
state.eventSink(PollHistoryEvent.LoadMore)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSelectAnswer(pollStartId: EventId, answerId: String) {
|
fun onSendPollResponse(pollStartId: EventId, answerIds: List<String>) {
|
||||||
state.eventSink(PollHistoryEvent.SelectPollAnswer(pollStartId, answerId))
|
state.eventSink(PollHistoryEvent.SendPollResponse(pollStartId, answerIds))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEndPoll(pollStartId: EventId) {
|
fun onEndPoll(pollStartId: EventId) {
|
||||||
@@ -114,7 +114,7 @@ fun PollHistoryView(
|
|||||||
pollHistoryItems = pollHistoryItems,
|
pollHistoryItems = pollHistoryItems,
|
||||||
hasMoreToLoad = state.hasMoreToLoad,
|
hasMoreToLoad = state.hasMoreToLoad,
|
||||||
isLoading = state.isLoading,
|
isLoading = state.isLoading,
|
||||||
onSelectAnswer = ::onSelectAnswer,
|
onSendPollResponse = ::onSendPollResponse,
|
||||||
onEditPoll = onEditPoll,
|
onEditPoll = onEditPoll,
|
||||||
onEndPoll = ::onEndPoll,
|
onEndPoll = ::onEndPoll,
|
||||||
onLoadMore = ::onLoadMore,
|
onLoadMore = ::onLoadMore,
|
||||||
@@ -151,7 +151,7 @@ private fun PollHistoryList(
|
|||||||
pollHistoryItems: ImmutableList<PollHistoryItem>,
|
pollHistoryItems: ImmutableList<PollHistoryItem>,
|
||||||
hasMoreToLoad: Boolean,
|
hasMoreToLoad: Boolean,
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
onSelectAnswer: (pollStartId: EventId, answerId: String) -> Unit,
|
onSendPollResponse: (pollStartId: EventId, answerIds: List<String>) -> Unit,
|
||||||
onEditPoll: (pollStartId: EventId) -> Unit,
|
onEditPoll: (pollStartId: EventId) -> Unit,
|
||||||
onEndPoll: (pollStartId: EventId) -> Unit,
|
onEndPoll: (pollStartId: EventId) -> Unit,
|
||||||
onLoadMore: () -> Unit,
|
onLoadMore: () -> Unit,
|
||||||
@@ -166,7 +166,7 @@ private fun PollHistoryList(
|
|||||||
items(pollHistoryItems) { pollHistoryItem ->
|
items(pollHistoryItems) { pollHistoryItem ->
|
||||||
PollHistoryItemRow(
|
PollHistoryItemRow(
|
||||||
pollHistoryItem = pollHistoryItem,
|
pollHistoryItem = pollHistoryItem,
|
||||||
onSelectAnswer = onSelectAnswer,
|
onSendPollResponse = onSendPollResponse,
|
||||||
onEditPoll = onEditPoll,
|
onEditPoll = onEditPoll,
|
||||||
onEndPoll = onEndPoll,
|
onEndPoll = onEndPoll,
|
||||||
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp)
|
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp)
|
||||||
@@ -222,7 +222,7 @@ private fun LoadMoreButton(isLoading: Boolean, onClick: () -> Unit) {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun PollHistoryItemRow(
|
private fun PollHistoryItemRow(
|
||||||
pollHistoryItem: PollHistoryItem,
|
pollHistoryItem: PollHistoryItem,
|
||||||
onSelectAnswer: (pollStartId: EventId, answerId: String) -> Unit,
|
onSendPollResponse: (pollStartId: EventId, answerIds: List<String>) -> Unit,
|
||||||
onEditPoll: (pollStartId: EventId) -> Unit,
|
onEditPoll: (pollStartId: EventId) -> Unit,
|
||||||
onEndPoll: (pollStartId: EventId) -> Unit,
|
onEndPoll: (pollStartId: EventId) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -244,7 +244,7 @@ private fun PollHistoryItemRow(
|
|||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
PollContentView(
|
PollContentView(
|
||||||
state = pollHistoryItem.state,
|
state = pollHistoryItem.state,
|
||||||
onSelectAnswer = onSelectAnswer,
|
onSendPollResponse = onSendPollResponse,
|
||||||
onEditPoll = onEditPoll,
|
onEditPoll = onEditPoll,
|
||||||
onEndPoll = onEndPoll,
|
onEndPoll = onEndPoll,
|
||||||
)
|
)
|
||||||
|
|||||||
+8
-1
@@ -43,15 +43,21 @@ class DefaultPollContentStateFactory(
|
|||||||
?.value
|
?.value
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
}
|
}
|
||||||
|
val currentSelectionCount = myVotes.size
|
||||||
|
val maxSelections = content.maxSelections.toInt()
|
||||||
|
val isSelectionLimitReached = currentSelectionCount >= maxSelections
|
||||||
|
|
||||||
val answerItems = content.answers.map { answer ->
|
val answerItems = content.answers.map { answer ->
|
||||||
val answerVoteCount = content.votes[answer.id]?.size ?: 0
|
val answerVoteCount = content.votes[answer.id]?.size ?: 0
|
||||||
val isSelected = answer.id in myVotes
|
val isSelected = answer.id in myVotes
|
||||||
val isWinner = answer.id in winnerIds
|
val isWinner = answer.id in winnerIds
|
||||||
val percentage = if (totalVoteCount > 0) answerVoteCount.toFloat() / totalVoteCount.toFloat() else 0f
|
val percentage = if (totalVoteCount > 0) answerVoteCount.toFloat() / totalVoteCount.toFloat() else 0f
|
||||||
|
// An answer is enabled if the poll hasn't ended and either the user can still select more answers or this answer is already selected
|
||||||
|
val isEnabled = !isPollEnded && (!isSelectionLimitReached || isSelected)
|
||||||
PollAnswerItem(
|
PollAnswerItem(
|
||||||
answer = answer,
|
answer = answer,
|
||||||
isSelected = isSelected,
|
isSelected = isSelected,
|
||||||
isEnabled = !isPollEnded,
|
isEnabled = isEnabled,
|
||||||
isWinner = isWinner,
|
isWinner = isWinner,
|
||||||
showVotes = content.kind.isDisclosed || isPollEnded,
|
showVotes = content.kind.isDisclosed || isPollEnded,
|
||||||
votesCount = answerVoteCount,
|
votesCount = answerVoteCount,
|
||||||
@@ -64,6 +70,7 @@ class DefaultPollContentStateFactory(
|
|||||||
question = content.question,
|
question = content.question,
|
||||||
answerItems = answerItems.toImmutableList(),
|
answerItems = answerItems.toImmutableList(),
|
||||||
pollKind = content.kind,
|
pollKind = content.kind,
|
||||||
|
maxSelections = content.maxSelections,
|
||||||
isPollEditable = isEditable,
|
isPollEditable = isEditable,
|
||||||
isPollEnded = isPollEnded,
|
isPollEnded = isPollEnded,
|
||||||
isMine = isOwn,
|
isMine = isOwn,
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_create_poll_max_selections_headline">"Votes allowed per person"</string>
|
||||||
|
</resources>
|
||||||
+2
@@ -525,6 +525,8 @@ class CreatePollPresenterTest {
|
|||||||
assertThat(question).isEmpty()
|
assertThat(question).isEmpty()
|
||||||
assertThat(answers).isEqualTo(listOf(Answer("", false), Answer("", false)))
|
assertThat(answers).isEqualTo(listOf(Answer("", false), Answer("", false)))
|
||||||
assertThat(pollKind).isEqualTo(PollKind.Disclosed)
|
assertThat(pollKind).isEqualTo(PollKind.Disclosed)
|
||||||
|
assertThat(maxSelections).isEqualTo(1)
|
||||||
|
assertThat(maxAllowedSelections).isEqualTo(2)
|
||||||
assertThat(showBackConfirmation).isFalse()
|
assertThat(showBackConfirmation).isFalse()
|
||||||
assertThat(showDeleteConfirmation).isFalse()
|
assertThat(showDeleteConfirmation).isFalse()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -24,6 +24,7 @@ class PollFormStateSaverTest {
|
|||||||
question = "question",
|
question = "question",
|
||||||
answers = persistentListOf("answer1", "answer2"),
|
answers = persistentListOf("answer1", "answer2"),
|
||||||
isDisclosed = true,
|
isDisclosed = true,
|
||||||
|
maxSelections = 2,
|
||||||
)
|
)
|
||||||
|
|
||||||
val saved = with(CanSaveScope) {
|
val saved = with(CanSaveScope) {
|
||||||
|
|||||||
+1
-1
@@ -117,7 +117,7 @@ class PollHistoryPresenterTest {
|
|||||||
state.eventSink(PollHistoryEvent.EndPoll(AN_EVENT_ID))
|
state.eventSink(PollHistoryEvent.EndPoll(AN_EVENT_ID))
|
||||||
runCurrent()
|
runCurrent()
|
||||||
endPollAction.verifyExecutionCount(1)
|
endPollAction.verifyExecutionCount(1)
|
||||||
state.eventSink(PollHistoryEvent.SelectPollAnswer(AN_EVENT_ID, "answer"))
|
state.eventSink(PollHistoryEvent.SendPollResponse(AN_EVENT_ID, listOf("answer")))
|
||||||
runCurrent()
|
runCurrent()
|
||||||
sendPollResponseAction.verifyExecutionCount(1)
|
sendPollResponseAction.verifyExecutionCount(1)
|
||||||
cancelAndConsumeRemainingEvents()
|
cancelAndConsumeRemainingEvents()
|
||||||
|
|||||||
+1
-1
@@ -133,7 +133,7 @@ class PollHistoryViewTest : RobolectricTest() {
|
|||||||
useUnmergedTree = true,
|
useUnmergedTree = true,
|
||||||
).performClick()
|
).performClick()
|
||||||
eventsRecorder.assertSingle(
|
eventsRecorder.assertSingle(
|
||||||
PollHistoryEvent.SelectPollAnswer(eventId, answer.id)
|
PollHistoryEvent.SendPollResponse(eventId, listOf(answer.id))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-6
@@ -59,10 +59,10 @@ class PollContentStateFactoryTest {
|
|||||||
)
|
)
|
||||||
val expectedState = aPollContentState(
|
val expectedState = aPollContentState(
|
||||||
answerItems = listOf(
|
answerItems = listOf(
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_1, votesCount = 3, percentage = 0.3f),
|
aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, votesCount = 3, percentage = 0.3f),
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, votesCount = 6, percentage = 0.6f),
|
aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, votesCount = 6, percentage = 0.6f),
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_3),
|
aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false),
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_4, votesCount = 1, percentage = 0.1f),
|
aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, votesCount = 1, percentage = 0.1f),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(state).isEqualTo(expectedState)
|
assertThat(state).isEqualTo(expectedState)
|
||||||
@@ -139,10 +139,10 @@ class PollContentStateFactoryTest {
|
|||||||
val expectedState = aPollContentState(
|
val expectedState = aPollContentState(
|
||||||
pollKind = PollKind.Undisclosed,
|
pollKind = PollKind.Undisclosed,
|
||||||
answerItems = listOf(
|
answerItems = listOf(
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_1, showVotes = false, votesCount = 3, percentage = 0.3f),
|
aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, showVotes = false, votesCount = 3, percentage = 0.3f),
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_2, showVotes = false, isSelected = true, votesCount = 6, percentage = 0.6f),
|
aPollAnswerItem(answer = A_POLL_ANSWER_2, showVotes = false, isSelected = true, votesCount = 6, percentage = 0.6f),
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_3, showVotes = false),
|
aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false, showVotes = false),
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_4, showVotes = false, votesCount = 1, percentage = 0.1f),
|
aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, showVotes = false, votesCount = 1, percentage = 0.1f),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assertThat(state).isEqualTo(expectedState)
|
assertThat(state).isEqualTo(expectedState)
|
||||||
@@ -241,6 +241,7 @@ class PollContentStateFactoryTest {
|
|||||||
question = question,
|
question = question,
|
||||||
answerItems = answerItems.toImmutableList(),
|
answerItems = answerItems.toImmutableList(),
|
||||||
pollKind = pollKind,
|
pollKind = pollKind,
|
||||||
|
maxSelections = 1u,
|
||||||
isPollEditable = isEditable,
|
isPollEditable = isEditable,
|
||||||
isPollEnded = isEnded,
|
isPollEnded = isEnded,
|
||||||
isMine = isMine,
|
isMine = isMine,
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ class FakeSendPollResponseAction : SendPollResponseAction {
|
|||||||
assert(executionCount == count)
|
assert(executionCount == count)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun execute(timeline: Timeline, pollStartId: EventId, answerId: String): Result<Unit> {
|
override suspend fun execute(timeline: Timeline, pollStartId: EventId, answerIds: List<String>): Result<Unit> {
|
||||||
executionCount++
|
executionCount++
|
||||||
return Result.success(Unit)
|
return Result.success(Unit)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -22,6 +22,7 @@ class FakePollContentStateFactory : PollContentStateFactory {
|
|||||||
question = content.question,
|
question = content.question,
|
||||||
answerItems = emptyList<PollAnswerItem>().toImmutableList(),
|
answerItems = emptyList<PollAnswerItem>().toImmutableList(),
|
||||||
pollKind = content.kind,
|
pollKind = content.kind,
|
||||||
|
maxSelections = content.maxSelections,
|
||||||
isPollEditable = isEditable,
|
isPollEditable = isEditable,
|
||||||
isPollEnded = content.endTime != null,
|
isPollEnded = content.endTime != null,
|
||||||
isMine = isOwn,
|
isMine = isOwn,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ accompanist = "0.37.3"
|
|||||||
|
|
||||||
# Test
|
# Test
|
||||||
test_core = "1.7.0"
|
test_core = "1.7.0"
|
||||||
roborazzi = "1.72.0"
|
roborazzi = "1.73.0"
|
||||||
|
|
||||||
# Jetbrain
|
# Jetbrain
|
||||||
datetime = "0.8.0"
|
datetime = "0.8.0"
|
||||||
|
|||||||
+3
-2
@@ -19,7 +19,8 @@ import org.jsoup.safety.Safelist
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the HTML string [FormattedBody.body] to a [Document] by parsing it.
|
* Converts the HTML string [FormattedBody.body] to a [Document] by parsing it.
|
||||||
* If the message is not formatted or the format is not [MessageFormat.HTML] we return `null`.
|
* If the message is not formatted, the format is not [MessageFormat.HTML], or nothing renderable is left once the
|
||||||
|
* unsupported tags have been removed, we return `null` so that the plain text body is used instead.
|
||||||
*
|
*
|
||||||
* This will also make sure mentions are prefixed with `@`.
|
* This will also make sure mentions are prefixed with `@`.
|
||||||
*
|
*
|
||||||
@@ -44,7 +45,7 @@ fun FormattedBody.toHtmlDocument(
|
|||||||
// Prepend `@` to mentions
|
// Prepend `@` to mentions
|
||||||
fixMentions(dom, permalinkParser)
|
fixMentions(dom, permalinkParser)
|
||||||
|
|
||||||
dom
|
dom.takeIf { it.text().isNotBlank() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+24
@@ -103,4 +103,28 @@ class ToHtmlDocumentTest : RobolectricTest() {
|
|||||||
})
|
})
|
||||||
assertThat(document?.text()).isEqualTo("Hey Alice!")
|
assertThat(document?.text()).isEqualTo("Hey Alice!")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toHtmlDocument - returns null if the body only contains unsupported tags`() {
|
||||||
|
val body = FormattedBody(
|
||||||
|
format = MessageFormat.HTML,
|
||||||
|
body = "<img data-mx-emoticon src=\"mxc://matrix.org/anImage\" height=\"32\" width=\"32\" alt=\"\uD83D\uDE1C\" />"
|
||||||
|
)
|
||||||
|
|
||||||
|
val document = body.toHtmlDocument(permalinkParser = FakePermalinkParser())
|
||||||
|
|
||||||
|
assertThat(document).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toHtmlDocument - returns a Document if some text survives next to unsupported tags`() {
|
||||||
|
val body = FormattedBody(
|
||||||
|
format = MessageFormat.HTML,
|
||||||
|
body = "Look at this <img src=\"mxc://matrix.org/anImage\" alt=\"cat\" />!"
|
||||||
|
)
|
||||||
|
|
||||||
|
val document = body.toHtmlDocument(permalinkParser = FakePermalinkParser())
|
||||||
|
|
||||||
|
assertThat(document?.text()).isEqualTo("Look at this !")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
@@ -149,4 +149,16 @@ class ToPlainTextTest : RobolectricTest() {
|
|||||||
)
|
)
|
||||||
assertThat(messageType.toPlainText(permalinkParser = FakePermalinkParser())).isEqualTo("This is the message content.")
|
assertThat(messageType.toPlainText(permalinkParser = FakePermalinkParser())).isEqualTo("This is the message content.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `TextMessageType toPlainText - returns the body if the formatted one only contains an image`() {
|
||||||
|
val messageType = TextMessageType(
|
||||||
|
body = "\uD83D\uDE1C",
|
||||||
|
formatted = FormattedBody(
|
||||||
|
format = MessageFormat.HTML,
|
||||||
|
body = "<img data-mx-emoticon src=\"mxc://matrix.org/anImage\" height=\"32\" width=\"32\" alt=\"\uD83D\uDE1C\" />"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertThat(messageType.toPlainText(permalinkParser = FakePermalinkParser())).isEqualTo("\uD83D\uDE1C")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ private const val versionMonth = 9
|
|||||||
* Release number in the month. Value must be in [0,99].
|
* Release number in the month. Value must be in [0,99].
|
||||||
* Do not update this value. it is updated by the release script.
|
* Do not update this value. it is updated by the release script.
|
||||||
*/
|
*/
|
||||||
private const val versionReleaseNumber = 0
|
private const val versionReleaseNumber = 1
|
||||||
|
|
||||||
object Versions {
|
object Versions {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ class KonsistPreviewTest {
|
|||||||
"PollContentViewCreatorPreview",
|
"PollContentViewCreatorPreview",
|
||||||
"PollContentViewDisclosedPreview",
|
"PollContentViewDisclosedPreview",
|
||||||
"PollContentViewEndedPreview",
|
"PollContentViewEndedPreview",
|
||||||
|
"PollContentViewMultipleSelectionPreview",
|
||||||
"PollContentViewUndisclosedPreview",
|
"PollContentViewUndisclosedPreview",
|
||||||
"ProgressDialogWithContentPreview",
|
"ProgressDialogWithContentPreview",
|
||||||
"ProgressDialogWithTextAndContentPreview",
|
"ProgressDialogWithTextAndContentPreview",
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -42,6 +42,31 @@ forbiddenTerms = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# A complete, VALID placeholder, anchored to consume the whole token:
|
||||||
|
# %% -> literal percent (allowed)
|
||||||
|
# %s %d %f ... -> simple conversion
|
||||||
|
# %1$s %2$d ... -> positional
|
||||||
|
# %.2f %1$.2f -> optional precision
|
||||||
|
_VALID_PLACEHOLDER = re.compile(
|
||||||
|
r'%(?:\d+\$)?(?:\.\d+)?[a-zA-Z]'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Grab each candidate token: a '%' plus the run of chars that could belong to a
|
||||||
|
# specifier (digits, '$', '.', and letters). This makes '%sd' and '%1d' surface
|
||||||
|
# as single tokens instead of being partially swallowed.
|
||||||
|
_PLACEHOLDER_MATCHES = re.compile(r'%%|%[0-9$.A-Za-z]*')
|
||||||
|
|
||||||
|
def find_invalid_placeholders(value):
|
||||||
|
"""Return the list of malformed placeholder tokens found in `value`."""
|
||||||
|
invalid = []
|
||||||
|
for token in _PLACEHOLDER_MATCHES.findall(value):
|
||||||
|
if token == '%%':
|
||||||
|
continue # escaped percent is fine
|
||||||
|
if not _VALID_PLACEHOLDER.fullmatch(token):
|
||||||
|
invalid.append(token)
|
||||||
|
return invalid
|
||||||
|
|
||||||
|
|
||||||
content = minidom.parse(file)
|
content = minidom.parse(file)
|
||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
@@ -60,6 +85,9 @@ for elem in content.getElementsByTagName('string'):
|
|||||||
matches = re.search(term, value)
|
matches = re.search(term, value)
|
||||||
if matches and name not in exceptions:
|
if matches and name not in exceptions:
|
||||||
errors.append('Forbidden term "' + term + '" in string: "' + name + '": ' + value)
|
errors.append('Forbidden term "' + term + '" in string: "' + name + '": ' + value)
|
||||||
|
invalid_placeholders = find_invalid_placeholders(value)
|
||||||
|
for placeholder in invalid_placeholders:
|
||||||
|
errors.append('Invalid placeholder "' + placeholder + '" in string: "' + name + '": ' + value)
|
||||||
|
|
||||||
### Plurals
|
### Plurals
|
||||||
for elem in content.getElementsByTagName('plurals'):
|
for elem in content.getElementsByTagName('plurals'):
|
||||||
@@ -78,6 +106,9 @@ for elem in content.getElementsByTagName('plurals'):
|
|||||||
matches = re.search(term, value)
|
matches = re.search(term, value)
|
||||||
if matches and name not in exceptions:
|
if matches and name not in exceptions:
|
||||||
errors.append('Forbidden term "' + term + '" in plural: "' + name + '": ' + value)
|
errors.append('Forbidden term "' + term + '" in plural: "' + name + '": ' + value)
|
||||||
|
invalid_placeholders = find_invalid_placeholders(value)
|
||||||
|
for placeholder in invalid_placeholders:
|
||||||
|
errors.append('Invalid placeholder "' + placeholder + '" in plural: "' + name + '": ' + value)
|
||||||
|
|
||||||
# If errors is not empty print the report
|
# If errors is not empty print the report
|
||||||
if errors:
|
if errors:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package: name='io.element.android.x' versionCode='202609000' versionName='26.09.0' platformBuildVersionName='17' platformBuildVersionCode='37' compileSdkVersion='37' compileSdkVersionCodename='17'
|
package: name='io.element.android.x' versionCode='202609010' versionName='26.09.1' platformBuildVersionName='17' platformBuildVersionCode='37' compileSdkVersion='37' compileSdkVersionCodename='17'
|
||||||
sdkVersion:'24'
|
sdkVersion:'24'
|
||||||
targetSdkVersion:'37'
|
targetSdkVersion:'37'
|
||||||
uses-permission: name='android.permission.POST_NOTIFICATIONS'
|
uses-permission: name='android.permission.POST_NOTIFICATIONS'
|
||||||
|
|||||||
Reference in New Issue
Block a user