diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/pull_request_template.md
similarity index 64%
rename from .github/PULL_REQUEST_TEMPLATE/pull_request_template.md
rename to .github/pull_request_template.md
index 431c018fdd..7aabcb77aa 100644
--- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,4 +1,4 @@
-
+
## Type of change
@@ -17,13 +17,17 @@
## Screenshots / GIFs
-
-
-- [ ] Changes has been tested on an Android device or Android emulator with API 21
+- [ ] Changes have been tested on an Android device or Android emulator with API 23
- [ ] UI change has been tested on both light and dark themes
-- [ ] Accessibility has been taken into account. See https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#accessibility
+- [ ] Accessibility has been taken into account. See https://github.com/vector-im/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
- [ ] Pull request is based on the develop branch
-- [ ] Pull request includes a new file under ./changelog.d. See https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#changelog
+- [ ] Pull request includes a new file under ./changelog.d. See https://github.com/vector-im/element-x-android/blob/develop/CONTRIBUTING.md#changelog
- [ ] Pull request includes screenshots or videos if containing UI changes
- [ ] Pull request includes a [sign off](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html#sign-off)
- [ ] You've made a self review of your PR
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d83826f9f1..11e6aeba19 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,4 +1,4 @@
-# Contributing to Element Android
+# Contributing to Element X Android
diff --git a/changelog.d/1113.wip b/changelog.d/1113.wip
new file mode 100644
index 0000000000..f95dc6ba88
--- /dev/null
+++ b/changelog.d/1113.wip
@@ -0,0 +1 @@
+[Polls] Improve UI and render ended state
diff --git a/changelog.d/1131.bugfix b/changelog.d/1131.bugfix
new file mode 100644
index 0000000000..23649a19d0
--- /dev/null
+++ b/changelog.d/1131.bugfix
@@ -0,0 +1 @@
+Only display verification prompt after initial sync is done.
diff --git a/changelog.d/862.bugfix b/changelog.d/862.bugfix
new file mode 100644
index 0000000000..30715a76e3
--- /dev/null
+++ b/changelog.d/862.bugfix
@@ -0,0 +1 @@
+Videos sent from the app were cropped in some cases.
diff --git a/features/analytics/impl/src/main/res/values-de/translations.xml b/features/analytics/impl/src/main/res/values-de/translations.xml
index 44890927a4..7ef2ff2500 100644
--- a/features/analytics/impl/src/main/res/values-de/translations.xml
+++ b/features/analytics/impl/src/main/res/values-de/translations.xml
@@ -5,6 +5,6 @@
"Du kannst alle unsere Nutzerbedingungen %1$s lesen."
"hier"
"Du kannst dies jederzeit deaktivieren"
- "Wir geben ""keine"" Informationen an Dritte weiter"
+ "Wir geben deine Daten nicht an Dritte weiter"
"Hilf uns, %1$s zu verbessern"
diff --git a/features/ftue/impl/src/main/res/values-cs/translations.xml b/features/ftue/impl/src/main/res/values-cs/translations.xml
new file mode 100644
index 0000000000..38bbf4dacb
--- /dev/null
+++ b/features/ftue/impl/src/main/res/values-cs/translations.xml
@@ -0,0 +1,9 @@
+
+
+ "Hovory, hlasování, vyhledávání a další budou přidány koncem tohoto roku."
+ "Historie zpráv šifrovaných místností nebude v této aktualizaci k dispozici."
+ "Rádi bychom se od vás dozvěděli, co si o tom myslíte, dejte nám vědět prostřednictvím stránky s nastavením."
+ "Jdeme na to!"
+ "Zde je to, co potřebujete vědět:"
+ "Vítá vás %1$s!"
+
diff --git a/features/ftue/impl/src/main/res/values-de/translations.xml b/features/ftue/impl/src/main/res/values-de/translations.xml
index 0f2efc1c57..d0ebbc31e7 100644
--- a/features/ftue/impl/src/main/res/values-de/translations.xml
+++ b/features/ftue/impl/src/main/res/values-de/translations.xml
@@ -1,6 +1,6 @@
- "Anrufe, Standortfreigabe, Suche und mehr werden später in diesem Jahr hinzugefügt."
+ "Anrufe, Umfragen, Suche und mehr werden später in diesem Jahr hinzugefügt."
"Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein."
"Wir würden uns freuen, wenn du uns über die Einstellungsseite deine Meinung mitteilst."
"Los geht\'s!"
diff --git a/features/login/impl/src/main/res/values-cs/translations.xml b/features/login/impl/src/main/res/values-cs/translations.xml
index 02581401bf..5d697bf34a 100644
--- a/features/login/impl/src/main/res/values-cs/translations.xml
+++ b/features/login/impl/src/main/res/values-cs/translations.xml
@@ -9,7 +9,7 @@
"Chystáte se přihlásit do %s"
"Zde budou uloženy vaše konverzace - podobně jako u poskytovatele e-mailových služeb uchováváte své e-maily."
"Chystáte se vytvořit účet na %s"
- "Matrix.org je otevřená síť pro bezpečnou, decentralizovanou komunikaci."
+ "Matrix.org je velký bezplatný server ve veřejné síti Matrix pro bezpečnou decentralizovanou komunikaci, který provozuje nadace Matrix.org."
"Jiný"
"Použijte jiného poskytovatele účtu, například vlastní soukromý server nebo pracovní účet."
"Změnit poskytovatele účtu"
diff --git a/features/login/impl/src/main/res/values-de/translations.xml b/features/login/impl/src/main/res/values-de/translations.xml
index 099d00e83b..7464e17f8d 100644
--- a/features/login/impl/src/main/res/values-de/translations.xml
+++ b/features/login/impl/src/main/res/values-de/translations.xml
@@ -9,7 +9,7 @@
"Du bist dabei dich bei %s anzumelden"
"Hier werden deine Konversationen stattfinden — genauso wie du einen E-Mail-Anbieter verwenden würdest, um deine E-Mails aufzubewahren."
"Du bist dabei ein Konto auf %s zu erstellen"
- "Matrix.org ist ein offenes Netzwerk für sichere, dezentralisierte Kommunikation."
+ "Matrix.org ist ein großer, kostenloser Server im öffentlichen Matrix-Netzwerk für sichere, dezentrale Kommunikation, der von der Matrix.org Foundation betrieben wird."
"Andere"
"Verwende einen anderen Kontoanbieter, z. B. deinen eigenen privaten Server oder ein Arbeitskonto."
"Kontoanbieter ändern"
diff --git a/features/login/impl/src/main/res/values-fr/translations.xml b/features/login/impl/src/main/res/values-fr/translations.xml
index 77d171bdf6..74f23cc5c9 100644
--- a/features/login/impl/src/main/res/values-fr/translations.xml
+++ b/features/login/impl/src/main/res/values-fr/translations.xml
@@ -9,7 +9,6 @@
"Vous êtes sur le point de vous connecter à %s"
"C\'est ici que vos conversations seront stockées - tout comme vous utiliseriez un fournisseur de messagerie pour conserver vos e-mails."
"Vous êtes sur le point de créer un compte sur %s"
- "Matrix.org est un réseau ouvert pour des communications sécurisées et décentralisées."
"Autre"
"Utilisez un autre fournisseur de compte, tel que votre propre serveur ou un compte professionnel."
"Changer de fournisseur"
diff --git a/features/login/impl/src/main/res/values-ro/translations.xml b/features/login/impl/src/main/res/values-ro/translations.xml
index 46596f2b95..0c58c45d03 100644
--- a/features/login/impl/src/main/res/values-ro/translations.xml
+++ b/features/login/impl/src/main/res/values-ro/translations.xml
@@ -9,7 +9,6 @@
"Sunteți pe cale să vă conectați la %s"
"Aici vor trăi conversațiile - la fel cum ați folosi un furnizor de e-mail pentru a vă păstra e-mailurile."
"Sunteți pe cale să creați un cont pe %s"
- "Matrix.org este o rețea deschisă pentru o comunicare sigură și descentralizată."
"Altul"
"Utilizați un alt furnizor de cont, cum ar fi propriul server privat sau un cont de serviciu."
"Schimbați furnizorul contului"
diff --git a/features/login/impl/src/main/res/values-ru/translations.xml b/features/login/impl/src/main/res/values-ru/translations.xml
index aa8b459f59..242ec4bb2c 100644
--- a/features/login/impl/src/main/res/values-ru/translations.xml
+++ b/features/login/impl/src/main/res/values-ru/translations.xml
@@ -9,7 +9,6 @@
"Вы собираетесь войти в %s"
"Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."
"Вы собираетесь создать учетную запись на %s"
- "Matrix.org — это открытая сеть для безопасной децентрализованной связи."
"Другое"
"Используйте другого поставщика учетных записей, например, собственный частный сервер или рабочую учетную запись."
"Сменить поставщика учетной записи"
diff --git a/features/login/impl/src/main/res/values-sk/translations.xml b/features/login/impl/src/main/res/values-sk/translations.xml
index a48fed0fd5..14a5407055 100644
--- a/features/login/impl/src/main/res/values-sk/translations.xml
+++ b/features/login/impl/src/main/res/values-sk/translations.xml
@@ -9,7 +9,6 @@
"Chystáte sa prihlásiť do %s"
"Tu budú žiť vaše konverzácie — podobne ako používate poskytovateľa e-mailových služieb na uchovávanie e-mailov."
"Chystáte sa vytvoriť účet na %s"
- "Matrix.org je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu."
"Iný"
"Použite iného poskytovateľa účtu, ako napríklad vlastný súkromný server alebo pracovný účet."
"Zmeniť poskytovateľa účtu"
diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml
index 236de68212..e09f4fe693 100644
--- a/features/login/impl/src/main/res/values/localazy.xml
+++ b/features/login/impl/src/main/res/values/localazy.xml
@@ -9,7 +9,7 @@
"You’re about to sign in to %s"
"This is where your conversations will live — just like you would use an email provider to keep your emails."
"You’re about to create an account on %s"
- "Matrix.org is an open network for secure, decentralized communication."
+ "Matrix.org is a large, free server on the public Matrix network for secure, decentralised communication, run by the Matrix.org Foundation."
"Other"
"Use a different account provider, such as your own private server or a work account."
"Change account provider"
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt
index fbeb0af1d7..dd5a2df2e0 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt
@@ -16,6 +16,7 @@
package io.element.android.features.messages.impl.timeline.components
+import android.annotation.SuppressLint
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -68,9 +69,11 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemGrou
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
+import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.libraries.designsystem.components.EqualWidthColumn
import io.element.android.libraries.designsystem.components.avatar.Avatar
@@ -357,11 +360,15 @@ private fun MessageEventBubbleContent(
onMessageLongClick: () -> Unit,
inReplyToClick: () -> Unit,
onTimestampClicked: () -> Unit,
- modifier: Modifier = Modifier
+ @SuppressLint("ModifierParameter") bubbleModifier: Modifier = Modifier, // need to rename this modifier to distinguish it from the following ones
) {
- val isMediaItem = event.content is TimelineItemImageContent
- || event.content is TimelineItemVideoContent
- || event.content is TimelineItemLocationContent
+ val timestampPosition = when (event.content) {
+ is TimelineItemImageContent,
+ is TimelineItemVideoContent,
+ is TimelineItemLocationContent -> TimestampPosition.Overlay
+ is TimelineItemPollContent -> TimestampPosition.Below
+ else -> TimestampPosition.Default
+ }
val replyToDetails = event.inReplyTo as? InReplyTo.Ready
// Long clicks are not not automatically propagated from a `clickable`
@@ -384,96 +391,97 @@ private fun MessageEventBubbleContent(
@Composable
fun ContentAndTimestampView(
- overlayTimestamp: Boolean,
+ timestampPosition: TimestampPosition,
modifier: Modifier = Modifier,
contentModifier: Modifier = Modifier,
timestampModifier: Modifier = Modifier,
) {
- if (overlayTimestamp) {
- Box(modifier) {
- ContentView(modifier = contentModifier)
- TimelineEventTimestampView(
- event = event,
- onClick = onTimestampClicked,
- onLongClick = ::onTimestampLongClick,
- modifier = timestampModifier
- .padding(horizontal = 4.dp, vertical = 4.dp) // Outer padding
- .background(ElementTheme.colors.bgSubtleSecondary, RoundedCornerShape(10.0.dp))
- .align(Alignment.BottomEnd)
- .padding(horizontal = 4.dp, vertical = 2.dp) // Inner padding
- )
- }
- } else {
- Box(modifier) {
- ContentView(modifier = contentModifier)
- TimelineEventTimestampView(
- event = event,
- onClick = onTimestampClicked,
- onLongClick = ::onTimestampLongClick,
- modifier = timestampModifier
- .align(Alignment.BottomEnd)
- .padding(horizontal = 8.dp, vertical = 4.dp)
- )
- }
+ when (timestampPosition) {
+ TimestampPosition.Overlay ->
+ Box(modifier) {
+ ContentView(modifier = contentModifier)
+ TimelineEventTimestampView(
+ event = event,
+ onClick = onTimestampClicked,
+ onLongClick = ::onTimestampLongClick,
+ modifier = timestampModifier
+ .padding(horizontal = 4.dp, vertical = 4.dp) // Outer padding
+ .background(ElementTheme.colors.bgSubtleSecondary, RoundedCornerShape(10.0.dp))
+ .align(Alignment.BottomEnd)
+ .padding(horizontal = 4.dp, vertical = 2.dp) // Inner padding
+ )
+ }
+ TimestampPosition.Aligned ->
+ Box(modifier) {
+ ContentView(modifier = contentModifier)
+ TimelineEventTimestampView(
+ event = event,
+ onClick = onTimestampClicked,
+ onLongClick = ::onTimestampLongClick,
+ modifier = timestampModifier
+ .align(Alignment.BottomEnd)
+ .padding(horizontal = 8.dp, vertical = 4.dp)
+ )
+ }
+ TimestampPosition.Below ->
+ Column(modifier) {
+ ContentView(modifier = contentModifier)
+ TimelineEventTimestampView(
+ event = event,
+ onClick = onTimestampClicked,
+ onLongClick = ::onTimestampLongClick,
+ modifier = timestampModifier
+ .align(Alignment.End)
+ .padding(horizontal = 8.dp, vertical = 4.dp)
+ )
+ }
}
}
- /** Used only for media items, with no reply to metadata. It displays the contents with no paddings. */
- @Composable
- fun SimpleMediaItemLayout(modifier: Modifier = Modifier) {
- ContentAndTimestampView(overlayTimestamp = true, modifier = modifier)
- }
-
- /** Used for every other type of message, groups the different components in a Column with some space between them. */
+ /** Groups the different components in a Column with some space between them. */
@Composable
fun CommonLayout(
inReplyToDetails: InReplyTo.Ready?,
modifier: Modifier = Modifier
) {
+ var modifierWithPadding: Modifier = Modifier
+ var contentModifier: Modifier = Modifier
EqualWidthColumn(modifier = modifier, spacing = 8.dp) {
- if (inReplyToDetails != null) {
- val senderName = inReplyToDetails.senderDisplayName ?: inReplyToDetails.senderId.value
- val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyToDetails)
- val text = textForInReplyTo(inReplyToDetails)
- ReplyToContent(
- senderName = senderName,
- text = text,
- attachmentThumbnailInfo = attachmentThumbnailInfo,
- modifier = Modifier
- .padding(top = 8.dp, start = 8.dp, end = 8.dp)
- .clip(RoundedCornerShape(6.dp))
- .clickable(enabled = true, onClick = inReplyToClick),
- )
- }
- val modifierWithPadding = if (isMediaItem) {
- Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)
- } else {
- Modifier
- }
-
- val contentModifier = if (isMediaItem) {
- Modifier.clip(RoundedCornerShape(12.dp))
- } else {
- if (inReplyToDetails != null) {
- Modifier.padding(start = 12.dp, end = 12.dp, top = 0.dp, bottom = 8.dp)
- } else {
- Modifier.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp)
+ when {
+ inReplyToDetails != null -> {
+ val senderName = inReplyToDetails.senderDisplayName ?: inReplyToDetails.senderId.value
+ val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyToDetails)
+ val text = textForInReplyTo(inReplyToDetails)
+ ReplyToContent(
+ senderName = senderName,
+ text = text,
+ attachmentThumbnailInfo = attachmentThumbnailInfo,
+ modifier = Modifier
+ .padding(top = 8.dp, start = 8.dp, end = 8.dp)
+ .clip(RoundedCornerShape(6.dp))
+ .clickable(enabled = true, onClick = inReplyToClick),
+ )
+ if (timestampPosition == TimestampPosition.Overlay) {
+ modifierWithPadding = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)
+ contentModifier = Modifier.clip(RoundedCornerShape(12.dp))
+ } else {
+ contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 0.dp, bottom = 8.dp)
+ }
+ }
+ timestampPosition != TimestampPosition.Overlay -> {
+ contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp)
}
}
ContentAndTimestampView(
- overlayTimestamp = isMediaItem,
+ timestampPosition = timestampPosition,
contentModifier = contentModifier,
modifier = modifierWithPadding,
)
}
}
- if (isMediaItem && replyToDetails == null) {
- SimpleMediaItemLayout()
- } else {
- CommonLayout(inReplyToDetails = replyToDetails, modifier = modifier)
- }
+ CommonLayout(inReplyToDetails = replyToDetails, modifier = bubbleModifier)
}
@Composable
@@ -810,3 +818,23 @@ internal fun TimelineItemEventRowLongSenderNamePreview() = ElementPreviewLight {
onTimestampClicked = {},
)
}
+
+// Note: no need for light/dark variant for this preview, we only look at the timestamp position
+@Preview
+@Composable
+internal fun TimelineItemEventTimestampBelowPreview() = ElementPreviewLight {
+ TimelineItemEventRow(
+ event = aTimelineItemEvent(content = aTimelineItemPollContent()),
+ isHighlighted = false,
+ canReply = true,
+ onClick = {},
+ onLongClick = {},
+ onUserDataClick = {},
+ inReplyToClick = {},
+ onReactionClick = { _, _ -> },
+ onReactionLongClick = { _, _ -> },
+ onMoreReactionsClick = {},
+ onSwipeToReply = {},
+ onTimestampClicked = {},
+ )
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimestampPosition.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimestampPosition.kt
new file mode 100644
index 0000000000..7063e07635
--- /dev/null
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimestampPosition.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.messages.impl.timeline.components
+
+enum class TimestampPosition {
+ /**
+ * Timestamp should overlay the timeline event content (eg. image).
+ */
+ Overlay,
+
+ /**
+ * Timestamp should be aligned with the timeline event content if this is possible (eg. text).
+ */
+ Aligned,
+
+ /**
+ * Timestamp should always be rendered below the timeline event content (eg. poll).
+ */
+ Below;
+
+ companion object {
+ /**
+ * Default timestamp position for timeline event contents.
+ */
+ val Default: TimestampPosition = Aligned
+ }
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt
index db3503be37..3608593cde 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt
@@ -21,7 +21,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContentProvider
-import io.element.android.features.poll.api.ActivePollContentView
+import io.element.android.features.poll.api.PollContentView
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.matrix.api.poll.PollAnswer
@@ -33,10 +33,11 @@ fun TimelineItemPollView(
onAnswerSelected: (PollAnswer) -> Unit,
modifier: Modifier = Modifier,
) {
- ActivePollContentView(
+ PollContentView(
question = content.question,
answerItems = content.answerItems.toImmutableList(),
pollKind = content.pollKind,
+ isPollEnded = content.isEnded,
onAnswerSelected = onAnswerSelected,
modifier = modifier,
)
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt
index 7c61466337..4a21874e1c 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt
@@ -23,7 +23,7 @@ import io.element.android.features.poll.api.PollAnswerItem
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClient
-import io.element.android.libraries.matrix.api.poll.PollKind
+import io.element.android.libraries.matrix.api.poll.isDisclosed
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import javax.inject.Inject
@@ -36,18 +36,33 @@ class TimelineItemContentPollFactory @Inject constructor(
if (!featureFlagService.isFeatureEnabled(FeatureFlags.Polls)) return TimelineItemUnknownContent
// Todo Move this computation to the matrix rust sdk
- val showResults = content.kind == PollKind.Disclosed && matrixClient.sessionId in content.votes.flatMap { it.value }
- val pollVotesCount = content.votes.flatMap { it.value }.size
- val userVotes = content.votes.filter { matrixClient.sessionId in it.value }.keys
+ val totalVoteCount = content.votes.flatMap { it.value }.size
+ val myVotes = content.votes.filter { matrixClient.sessionId in it.value }.keys
+ val isEndedPoll = content.endTime != null
+ val winnerIds = if (!isEndedPoll) {
+ emptyList()
+ } else {
+ content.answers
+ .map { answer -> answer.id }
+ .groupBy { answerId -> content.votes[answerId]?.size ?: 0 } // Group by votes count
+ .maxByOrNull { (votes, _) -> votes } // Keep max voted answers
+ ?.takeIf { (votes, _) -> votes > 0 } // Ignore if no option has been voted
+ ?.value
+ .orEmpty()
+ }
val answerItems = content.answers.map { answer ->
- val votesCount = content.votes[answer.id]?.size ?: 0
- val progress = if (pollVotesCount > 0) votesCount.toFloat() / pollVotesCount.toFloat() else 0f
+ val answerVoteCount = content.votes[answer.id]?.size ?: 0
+ val isSelected = answer.id in myVotes
+ val isWinner = answer.id in winnerIds
+ val percentage = if (totalVoteCount > 0) answerVoteCount.toFloat() / totalVoteCount.toFloat() else 0f
PollAnswerItem(
answer = answer,
- isSelected = answer.id in userVotes,
- isDisclosed = showResults,
- votesCount = votesCount,
- progress = progress,
+ isSelected = isSelected,
+ isEnabled = !isEndedPoll,
+ isWinner = isWinner,
+ isDisclosed = content.kind.isDisclosed || isEndedPoll,
+ votesCount = answerVoteCount,
+ percentage = percentage,
)
}
@@ -56,7 +71,7 @@ class TimelineItemContentPollFactory @Inject constructor(
answerItems = answerItems,
votes = content.votes,
pollKind = content.kind,
- isDisclosed = showResults
+ isEnded = isEndedPoll,
)
}
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt
index b8a2fa8bca..3c0e0edfd4 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt
@@ -25,7 +25,7 @@ data class TimelineItemPollContent(
val answerItems: List,
val votes: Map>,
val pollKind: PollKind,
- val isDisclosed: Boolean,
+ val isEnded: Boolean,
) : TimelineItemEventContent {
override val type: String = "TimelineItemPollContent"
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt
index 665d507ead..49dfa58c8a 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt
@@ -24,16 +24,16 @@ open class TimelineItemPollContentProvider : PreviewParameterProvider
get() = sequenceOf(
aTimelineItemPollContent(),
- aTimelineItemPollContent().copy(isDisclosed = true),
+ aTimelineItemPollContent().copy(pollKind = PollKind.Undisclosed),
)
}
fun aTimelineItemPollContent(): TimelineItemPollContent {
return TimelineItemPollContent(
pollKind = PollKind.Disclosed,
- isDisclosed = false,
question = "What type of food should we have at the party?",
answerItems = aPollAnswerItemList(),
+ isEnded = false,
votes = emptyMap(),
)
}
diff --git a/features/messages/impl/src/main/res/values-cs/translations.xml b/features/messages/impl/src/main/res/values-cs/translations.xml
index 8da8200bd5..7ca02ef21c 100644
--- a/features/messages/impl/src/main/res/values-cs/translations.xml
+++ b/features/messages/impl/src/main/res/values-cs/translations.xml
@@ -5,19 +5,43 @@
- "%1$d změny místnosti"
- "%1$d změn místnosti"
+
+ - "%1$d další"
+ - "%1$d další"
+ - "%1$d dalších"
+
"Fotoaparát"
"Vyfotit"
"Natočit video"
"Příloha"
"Knihovna fotografií a videí"
"Poloha"
+ "Hlasování"
+ "Historie zpráv je momentálně v této místnosti nedostupná"
"Nepodařilo se načíst údaje o uživateli"
"Chtěli byste je pozvat zpět?"
"V tomto chatu jste sami"
"Zpráva zkopírována"
"Nemáte oprávnění zveřejňovat příspěvky v této místnosti"
+ "Povolit vlastní nastavení"
+ "Zapnutím této funkce přepíšete výchozí nastavení"
+ "Upozornit mě v tomto chatu na"
+ "Můžete změnit ve vašem %1$s."
+ "globální nastavení"
+ "Výchozí nastavení"
+ "Odebrat vlastní nastavení"
+ "Při načítání nastavení oznámení došlo k chybě."
+ "Obnovení výchozího režimu se nezdařilo, zkuste to prosím znovu."
+ "Nastavení režimu se nezdařilo, zkuste to prosím znovu."
+ "Všechny zprávy"
+ "Pouze zmínky a klíčová slova"
+ "V této místnosti mě upozornit na"
+ "Zobrazit méně"
+ "Zobrazit více"
"Odeslat znovu"
"Vaši zprávu se nepodařilo odeslat"
+ "Přidat emoji"
+ "Zobrazit méně"
"Nahrání média se nezdařilo, zkuste to prosím znovu."
"Odstranit"
diff --git a/features/messages/impl/src/main/res/values-de/translations.xml b/features/messages/impl/src/main/res/values-de/translations.xml
index 84c903844e..482065c887 100644
--- a/features/messages/impl/src/main/res/values-de/translations.xml
+++ b/features/messages/impl/src/main/res/values-de/translations.xml
@@ -10,16 +10,32 @@
"Anhang"
"Foto- & Video-Bibliothek"
"Standort"
+ "Umfrage"
"Der Nachrichtenverlauf ist in diesem Raum derzeit nicht verfügbar"
"Benutzerdetails konnten nicht abgerufen werden"
"Möchtest du sie wieder einladen?"
"Du bist allein in diesem Chat"
"Nachricht kopiert"
"Du bist keine Berechtigung, um in diesem Raum zu posten"
+ "Benutzerdefinierte Einstellung zulassen"
+ "Das Aktivieren dieser Option wird die Standardeinstellungen überschreiben."
+ "Benachrichtige mich in diesem Chat für"
+ "Du kannst es in deinem %1$s ändern."
+ "Globale Einstellungen"
+ "Standardeinstellung"
+ "Benutzerdefinierte Einstellung entfernen"
+ "Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten."
+ "Wiederherstellung des Standardmodus fehlgeschlagen. Bitte versuche es erneut."
+ "Fehler beim Einstellen des Modus. Bitte versuche es erneut."
+ "Alle Nachrichten"
+ "Nur Erwähnungen und Schlüsselwörter"
+ "In diesem Raum, benachrichtige mich für"
"Weniger anzeigen"
"Mehr anzeigen"
"Erneut senden"
"Ihre Nachricht konnte nicht gesendet werden"
+ "Emoji hinzufügen"
+ "Weniger anzeigen"
"Fehler bei der Verarbeitung von Medien zum Hochladen, bitte versuche es erneut."
"Entfernen"
diff --git a/features/messages/impl/src/main/res/values-ru/translations.xml b/features/messages/impl/src/main/res/values-ru/translations.xml
index c3ed526564..33147f8ea2 100644
--- a/features/messages/impl/src/main/res/values-ru/translations.xml
+++ b/features/messages/impl/src/main/res/values-ru/translations.xml
@@ -5,6 +5,11 @@
- "%1$d изменения в комнате"
- "%1$d изменений в комнате"
+
+ - "И ещё %1$d"
+ - "И ещё %1$d"
+ - "И ещё %1$d"
+
"Камера"
"Сделать фото"
"Записать видео"
@@ -24,11 +29,13 @@
"Вы можете изменить его в своем %1$s."
"Основные Настройки"
"Настройка по умолчанию"
+ "Удалить пользовательскую настройку"
"Произошла ошибка при загрузке настроек уведомлений."
"Не удалось восстановить режим по умолчанию, попробуйте еще раз."
"Не удалось настроить режим, попробуйте еще раз."
"Все сообщения"
"Только упоминания и ключевые слова"
+ "В этой комнате уведомить меня о"
"Показать меньше"
"Показать больше"
"Отправить снова"
diff --git a/features/messages/impl/src/main/res/values-sk/translations.xml b/features/messages/impl/src/main/res/values-sk/translations.xml
index 07e62e9a4d..03a32f847f 100644
--- a/features/messages/impl/src/main/res/values-sk/translations.xml
+++ b/features/messages/impl/src/main/res/values-sk/translations.xml
@@ -16,6 +16,7 @@
"Príloha"
"Knižnica fotografií a videí"
"Poloha"
+ "Anketa"
"História správ v tejto miestnosti nie je momentálne k dispozícii"
"Nepodarilo sa získať údaje o používateľovi"
"Chceli by ste ich pozvať späť?"
@@ -28,11 +29,13 @@
"Môžete to zmeniť vo svojich %1$s."
"všeobecných nastaveniach"
"Predvolené nastavenie"
+ "Odstrániť vlastné nastavenie"
"Pri načítavaní nastavení oznámení došlo k chybe."
"Nepodarilo sa obnoviť predvolený režim, skúste to prosím znova."
"Nepodarilo sa nastaviť režim, skúste to prosím znova."
"Všetky správy"
"Iba zmienky a kľúčové slová"
+ "V tejto miestnosti ma upozorniť na"
"Zobraziť menej"
"Zobraziť viac"
"Odoslať znova"
diff --git a/features/poll/api/build.gradle.kts b/features/poll/api/build.gradle.kts
index be198ba740..6d94fa1b2f 100644
--- a/features/poll/api/build.gradle.kts
+++ b/features/poll/api/build.gradle.kts
@@ -27,8 +27,6 @@ dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
- implementation(libs.androidx.constraintlayout)
- implementation(libs.androidx.constraintlayout.compose)
implementation(projects.libraries.matrix.api)
ksp(libs.showkase.processor)
diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/ActivePollContentView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/ActivePollContentView.kt
deleted file mode 100644
index 587c3306b1..0000000000
--- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/ActivePollContentView.kt
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (c) 2023 New Vector Ltd
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package io.element.android.features.poll.api
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.selection.selectableGroup
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.BarChart
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import io.element.android.libraries.designsystem.preview.DayNightPreviews
-import io.element.android.libraries.designsystem.preview.ElementPreview
-import io.element.android.libraries.designsystem.theme.components.Icon
-import io.element.android.libraries.designsystem.theme.components.Text
-import io.element.android.libraries.matrix.api.poll.PollAnswer
-import io.element.android.libraries.matrix.api.poll.PollKind
-import io.element.android.libraries.theme.ElementTheme
-import io.element.android.libraries.ui.strings.CommonStrings
-import kotlinx.collections.immutable.ImmutableList
-
-@Composable
-fun ActivePollContentView(
- question: String,
- answerItems: ImmutableList,
- pollKind: PollKind,
- onAnswerSelected: (PollAnswer) -> Unit,
- modifier: Modifier = Modifier,
-) {
- val showResults = answerItems.any { it.isSelected }
- Column(
- modifier = modifier
- .selectableGroup()
- .fillMaxWidth(),
- verticalArrangement = Arrangement.spacedBy(16.dp),
- ) {
- Row(
- horizontalArrangement = Arrangement.spacedBy(4.dp),
- ) {
- Icon(imageVector = Icons.Default.BarChart, contentDescription = null)
- Text(
- text = question,
- style = ElementTheme.typography.fontBodyLgMedium
- )
- }
-
- answerItems.forEach { answerItem ->
- PollAnswerView(
- answerItem = answerItem,
- onClick = { onAnswerSelected(answerItem.answer) }
- )
- }
-
- val votesCount = answerItems.sumOf { it.votesCount }
- when {
- pollKind == PollKind.Undisclosed -> {
- Text(
- modifier = Modifier
- .align(Alignment.Start)
- .padding(start = 32.dp),
- style = ElementTheme.typography.fontBodyXsRegular,
- color = ElementTheme.colors.textSecondary,
- text = stringResource(CommonStrings.common_poll_undisclosed_text),
- )
- }
- showResults -> {
- Text(
- modifier = Modifier.align(Alignment.End),
- style = ElementTheme.typography.fontBodyXsRegular,
- color = ElementTheme.colors.textSecondary,
- text = stringResource(CommonStrings.common_poll_total_votes, votesCount),
- )
- }
- }
- }
-}
-
-@DayNightPreviews
-@Composable
-internal fun ActivePollContentNoResultsPreview() = ElementPreview {
- ActivePollContentView(
- question = "What type of food should we have at the party?",
- answerItems = aPollAnswerItemList(isDisclosed = false),
- pollKind = PollKind.Undisclosed,
- onAnswerSelected = { },
- )
-}
-
-@DayNightPreviews
-@Composable
-internal fun ActivePollContentWithResultsPreview() = ElementPreview {
- ActivePollContentView(
- question = "What type of food should we have at the party?",
- answerItems = aPollAnswerItemList(),
- pollKind = PollKind.Disclosed,
- onAnswerSelected = { },
- )
-}
diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerItem.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerItem.kt
index 24db33ad1f..1955701c5b 100644
--- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerItem.kt
+++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerItem.kt
@@ -23,14 +23,18 @@ import io.element.android.libraries.matrix.api.poll.PollAnswer
*
* @property answer the poll answer.
* @property isSelected whether the user has selected this answer.
+ * @property isEnabled whether the answer can be voted.
+ * @property isWinner whether this is the winner answer in the poll.
* @property isDisclosed whether the votes for this answer should be disclosed.
* @property votesCount the number of votes for this answer.
- * @property progress the percentage of votes for this answer.
+ * @property percentage the percentage of votes for this answer.
*/
data class PollAnswerItem(
val answer: PollAnswer,
val isSelected: Boolean,
+ val isEnabled: Boolean,
+ val isWinner: Boolean,
val isDisclosed: Boolean,
val votesCount: Int,
- val progress: Float,
+ val percentage: Float,
)
diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt
index 26fa6fbb71..3e52cc5fc7 100644
--- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt
+++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt
@@ -16,110 +16,166 @@
package io.element.android.features.poll.api
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.selection.selectable
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.CheckCircle
+import androidx.compose.material.icons.filled.RadioButtonUnchecked
+import androidx.compose.material3.IconButtonDefaults
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import androidx.constraintlayout.compose.ConstraintLayout
-import androidx.constraintlayout.compose.Dimension
-import androidx.constraintlayout.compose.Visibility
-import io.element.android.libraries.designsystem.preview.DayNightPreviews
-import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.ElementThemedPreview
+import io.element.android.libraries.designsystem.theme.components.Icon
+import io.element.android.libraries.designsystem.theme.components.IconToggleButton
import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator
-import io.element.android.libraries.designsystem.theme.components.RadioButton
import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.designsystem.toEnabledColor
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonPlurals
-@Suppress("DestructuringDeclarationWithTooManyEntries") // This is necessary to declare the constraints ids
@Composable
fun PollAnswerView(
answerItem: PollAnswerItem,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
- ConstraintLayout(
+ Row(
modifier
- .wrapContentHeight()
.fillMaxWidth()
.selectable(
selected = answerItem.isSelected,
+ enabled = answerItem.isEnabled,
onClick = onClick,
role = Role.RadioButton,
)
) {
- val (radioButton, answerText, votesText, progressBar) = createRefs()
- RadioButton(
- modifier = Modifier.constrainAs(radioButton) {
- top.linkTo(answerText.top)
- bottom.linkTo(answerText.bottom)
- start.linkTo(parent.start)
- end.linkTo(answerText.start)
- },
- selected = answerItem.isSelected,
- onClick = null // null recommended for accessibility with screenreaders
- )
- Text(
- modifier = Modifier.constrainAs(answerText) {
- width = Dimension.fillToConstraints
- top.linkTo(parent.top)
- start.linkTo(radioButton.end, margin = 8.dp)
- end.linkTo(votesText.start)
- bottom.linkTo(progressBar.top)
- },
- text = answerItem.answer.text,
- )
- Text(
- modifier = Modifier.constrainAs(votesText) {
- start.linkTo(answerText.end)
- end.linkTo(parent.end)
- bottom.linkTo(answerText.bottom)
- visibility = if (answerItem.isDisclosed) Visibility.Visible else Visibility.Gone
- },
- text = pluralStringResource(
- id = CommonPlurals.common_poll_votes_count,
- count = answerItem.votesCount,
- answerItem.votesCount
+ IconToggleButton(
+ modifier = Modifier.size(22.dp),
+ checked = answerItem.isSelected,
+ enabled = answerItem.isEnabled,
+ colors = IconButtonDefaults.iconToggleButtonColors(
+ contentColor = ElementTheme.colors.iconSecondary,
+ checkedContentColor = ElementTheme.colors.iconPrimary,
+ disabledContentColor = ElementTheme.colors.iconDisabled,
),
- style = ElementTheme.typography.fontBodySmRegular,
- color = ElementTheme.colors.textSecondary,
- )
- LinearProgressIndicator(
- progress = answerItem.progress,
- modifier = Modifier
- .constrainAs(progressBar) {
- start.linkTo(answerText.start)
- end.linkTo(votesText.end)
- top.linkTo(answerText.bottom, margin = 10.dp)
- bottom.linkTo(parent.bottom)
- width = Dimension.fillToConstraints
- visibility = if (answerItem.isDisclosed) Visibility.Visible else Visibility.Gone
-
+ onCheckedChange = { onClick() },
+ ) {
+ Icon(
+ imageVector = if (answerItem.isSelected) {
+ Icons.Default.CheckCircle
+ } else {
+ Icons.Default.RadioButtonUnchecked
},
- strokeCap = StrokeCap.Round,
- )
+ contentDescription = null,
+ )
+ }
+ Spacer(modifier = Modifier.width(12.dp))
+ Column {
+ Row {
+ Text(
+ modifier = Modifier.weight(1f),
+ text = answerItem.answer.text,
+ style = if (answerItem.isWinner) ElementTheme.typography.fontBodyLgMedium else ElementTheme.typography.fontBodyLgRegular,
+ )
+ if (answerItem.isDisclosed) {
+ Text(
+ modifier = Modifier.align(Alignment.Bottom),
+ text = pluralStringResource(
+ id = CommonPlurals.common_poll_votes_count,
+ count = answerItem.votesCount,
+ answerItem.votesCount
+ ),
+ style = if (answerItem.isWinner) ElementTheme.typography.fontBodySmMedium else ElementTheme.typography.fontBodySmRegular,
+ color = if (answerItem.isWinner) ElementTheme.colors.textPrimary else ElementTheme.colors.textSecondary,
+ )
+ }
+ }
+ Spacer(modifier = Modifier.height(10.dp))
+ LinearProgressIndicator(
+ modifier = Modifier.fillMaxWidth(),
+ color = if (answerItem.isWinner) ElementTheme.colors.textSuccessPrimary else answerItem.isEnabled.toEnabledColor(),
+ progress = when {
+ answerItem.isDisclosed -> answerItem.percentage
+ answerItem.isSelected -> 1f
+ else -> 0f
+ },
+ strokeCap = StrokeCap.Round,
+ )
+ }
}
}
-@DayNightPreviews
+@Preview
@Composable
-internal fun PollAnswerViewNoResultsPreview() = ElementPreview {
+internal fun PollAnswerDisclosedNotSelectedPreview() = ElementThemedPreview {
PollAnswerView(
- answerItem = aPollAnswerItem(),
+ answerItem = aPollAnswerItem(isDisclosed = true, isSelected = false),
onClick = { },
)
}
-@DayNightPreviews
+@Preview
@Composable
-internal fun PollAnswerViewWithResultPreview() = ElementPreview {
+internal fun PollAnswerDisclosedSelectedPreview() = ElementThemedPreview {
PollAnswerView(
- answerItem = aPollAnswerItem(isDisclosed = true),
+ answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true),
+ onClick = { }
+ )
+}
+
+@Preview
+@Composable
+internal fun PollAnswerUndisclosedNotSelectedPreview() = ElementThemedPreview {
+ PollAnswerView(
+ answerItem = aPollAnswerItem(isDisclosed = false, isSelected = false),
+ onClick = { },
+ )
+}
+
+@Preview
+@Composable
+internal fun PollAnswerUndisclosedSelectedPreview() = ElementThemedPreview {
+ PollAnswerView(
+ answerItem = aPollAnswerItem(isDisclosed = false, isSelected = true),
+ onClick = { }
+ )
+}
+
+@Preview
+@Composable
+internal fun PollAnswerEndedWinnerNotSelectedPreview() = ElementThemedPreview {
+ PollAnswerView(
+ answerItem = aPollAnswerItem(isDisclosed = true, isSelected = false, isEnabled = false, isWinner = true),
+ onClick = { }
+ )
+}
+
+@Preview
+@Composable
+internal fun PollAnswerEndedWinnerSelectedPreview() = ElementThemedPreview {
+ PollAnswerView(
+ answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true, isEnabled = false, isWinner = true),
+ onClick = { }
+ )
+}
+
+@Preview
+@Composable
+internal fun PollAnswerEndedSelectedPreview() = ElementThemedPreview {
+ PollAnswerView(
+ answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true, isEnabled = false, isWinner = false),
onClick = { }
)
}
diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt
index 062d09fd88..e94b5adeeb 100644
--- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt
+++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt
@@ -19,27 +19,33 @@ package io.element.android.features.poll.api
import io.element.android.libraries.matrix.api.poll.PollAnswer
import kotlinx.collections.immutable.persistentListOf
-fun aPollAnswerItemList(isDisclosed: Boolean = true) = persistentListOf(
+fun aPollAnswerItemList(isEnded: Boolean = false, isDisclosed: Boolean = true) = persistentListOf(
aPollAnswerItem(
answer = PollAnswer("option_1", "Italian \uD83C\uDDEE\uD83C\uDDF9"),
isDisclosed = isDisclosed,
+ isEnabled = !isEnded,
+ isWinner = isEnded,
votesCount = 5,
- progress = 0.5f
+ percentage = 0.5f
),
aPollAnswerItem(
answer = PollAnswer("option_2", "Chinese \uD83C\uDDE8\uD83C\uDDF3"),
isDisclosed = isDisclosed,
+ isEnabled = !isEnded,
+ isWinner = false,
votesCount = 0,
- progress = 0f
+ percentage = 0f
),
aPollAnswerItem(
answer = PollAnswer("option_3", "Brazilian \uD83C\uDDE7\uD83C\uDDF7"),
isDisclosed = isDisclosed,
+ isEnabled = !isEnded,
+ isWinner = false,
isSelected = true,
votesCount = 1,
- progress = 0.1f
+ percentage = 0.1f
),
- aPollAnswerItem(isDisclosed = isDisclosed),
+ aPollAnswerItem(isDisclosed = isDisclosed, isEnabled = !isEnded),
)
fun aPollAnswerItem(
@@ -48,13 +54,17 @@ fun aPollAnswerItem(
"French \uD83C\uDDEB\uD83C\uDDF7 But make it a very very very long option then this should just keep expanding"
),
isSelected: Boolean = false,
+ isEnabled: Boolean = true,
+ isWinner: Boolean = false,
isDisclosed: Boolean = true,
votesCount: Int = 4,
- progress: Float = 0.4f,
+ percentage: Float = 0.4f,
) = PollAnswerItem(
answer = answer,
isSelected = isSelected,
+ isEnabled = isEnabled,
+ isWinner = isWinner,
isDisclosed = isDisclosed,
votesCount = votesCount,
- progress = progress
+ percentage = percentage
)
diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt
new file mode 100644
index 0000000000..419aa21204
--- /dev/null
+++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.poll.api
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Poll
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import io.element.android.libraries.designsystem.preview.DayNightPreviews
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.theme.components.Icon
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.matrix.api.poll.PollAnswer
+import io.element.android.libraries.matrix.api.poll.PollKind
+import io.element.android.libraries.theme.ElementTheme
+import io.element.android.libraries.ui.strings.CommonStrings
+import kotlinx.collections.immutable.ImmutableList
+
+@Composable
+fun PollContentView(
+ question: String,
+ answerItems: ImmutableList,
+ pollKind: PollKind,
+ isPollEnded: Boolean,
+ onAnswerSelected: (PollAnswer) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier
+ .selectableGroup()
+ .fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ PollTitle(title = question)
+
+ PollAnswers(answerItems = answerItems, onAnswerSelected = onAnswerSelected)
+
+ when {
+ isPollEnded || pollKind == PollKind.Disclosed -> DisclosedPollBottomNotice(answerItems)
+ pollKind == PollKind.Undisclosed -> UndisclosedPollBottomNotice()
+ }
+ }
+}
+
+@Composable
+internal fun PollTitle(
+ title: String,
+ modifier: Modifier = Modifier
+) {
+ Row(
+ modifier = modifier,
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ Icon(
+ modifier = Modifier.size(22.dp),
+ imageVector = Icons.Outlined.Poll,
+ contentDescription = null
+ )
+ Text(
+ text = title,
+ style = ElementTheme.typography.fontBodyLgMedium
+ )
+ }
+}
+
+@Composable
+internal fun PollAnswers(
+ answerItems: ImmutableList,
+ onAnswerSelected: (PollAnswer) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+
+ answerItems.forEach { answerItem ->
+ PollAnswerView(
+ modifier = modifier,
+ answerItem = answerItem,
+ onClick = { onAnswerSelected(answerItem.answer) }
+ )
+ }
+}
+
+@Composable
+internal fun ColumnScope.DisclosedPollBottomNotice(
+ answerItems: ImmutableList,
+ modifier: Modifier = Modifier
+) {
+ val votesCount = answerItems.sumOf { it.votesCount }
+ Text(
+ modifier = modifier.align(Alignment.End),
+ style = ElementTheme.typography.fontBodyXsRegular,
+ color = ElementTheme.colors.textSecondary,
+ text = stringResource(CommonStrings.common_poll_total_votes, votesCount),
+ )
+}
+
+@Composable
+fun ColumnScope.UndisclosedPollBottomNotice(modifier: Modifier = Modifier) {
+ Text(
+ modifier = modifier
+ .align(Alignment.Start)
+ .padding(start = 34.dp),
+ style = ElementTheme.typography.fontBodyXsRegular,
+ color = ElementTheme.colors.textSecondary,
+ text = stringResource(CommonStrings.common_poll_undisclosed_text),
+ )
+}
+
+@DayNightPreviews
+@Composable
+internal fun PollContentUndisclosedPreview() = ElementPreview {
+ PollContentView(
+ question = "What type of food should we have at the party?",
+ answerItems = aPollAnswerItemList(isDisclosed = false),
+ pollKind = PollKind.Undisclosed,
+ isPollEnded = false,
+ onAnswerSelected = { },
+ )
+}
+
+@DayNightPreviews
+@Composable
+internal fun PollContentDisclosedPreview() = ElementPreview {
+ PollContentView(
+ question = "What type of food should we have at the party?",
+ answerItems = aPollAnswerItemList(),
+ pollKind = PollKind.Disclosed,
+ isPollEnded = false,
+ onAnswerSelected = { },
+ )
+}
+
+@DayNightPreviews
+@Composable
+internal fun PollContentEndedPreview() = ElementPreview {
+ PollContentView(
+ question = "What type of food should we have at the party?",
+ answerItems = aPollAnswerItemList(isEnded = true),
+ pollKind = PollKind.Disclosed,
+ isPollEnded = false,
+ onAnswerSelected = { },
+ )
+}
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt
index b33940b21e..af82a3ab2a 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt
@@ -20,7 +20,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -34,7 +33,6 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.user.getCurrentUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
-import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -62,11 +60,8 @@ class PreferencesRootPresenter @Inject constructor(
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
val hasAnalyticsProviders = remember { analyticsService.getAvailableAnalyticsProviders().isNotEmpty() }
- // Session verification status (unknown, not verified, verified)
- val sessionVerifiedStatus by sessionVerificationService.sessionVerifiedStatus.collectAsState()
- val sessionIsNotVerified by remember {
- derivedStateOf { sessionVerifiedStatus == SessionVerifiedStatus.NotVerified }
- }
+ // We should display the 'complete verification' option if the current session can be verified
+ val showCompleteVerification by sessionVerificationService.canVerifySessionFlow.collectAsState(false)
val accountManagementUrl: MutableState = remember {
mutableStateOf(null)
@@ -82,7 +77,7 @@ class PreferencesRootPresenter @Inject constructor(
logoutState = logoutState,
myUser = matrixUser.value,
version = versionFormatter.get(),
- showCompleteVerification = sessionIsNotVerified,
+ showCompleteVerification = showCompleteVerification,
accountManagementUrl = accountManagementUrl.value,
showAnalyticsSettings = hasAnalyticsProviders,
showDeveloperSettings = showDeveloperSettings,
diff --git a/features/rageshake/impl/src/main/res/values-cs/translations.xml b/features/rageshake/impl/src/main/res/values-cs/translations.xml
index d95752e91c..bfea484240 100644
--- a/features/rageshake/impl/src/main/res/values-cs/translations.xml
+++ b/features/rageshake/impl/src/main/res/values-cs/translations.xml
@@ -1,7 +1,7 @@
"Připojit snímek obrazovky"
- "V případě dalších dotazů se na mě můžete obrátit"
+ "V případě dalších dotazů se na mě můžete obrátit."
"Kontaktujte mě"
"Upravit snímek obrazovky"
"Popište prosím chybu. Co jste udělali? Co jste očekávali, že se stane? Co se ve skutečnosti stalo? Uveďte co nejvíce podrobností."
diff --git a/features/rageshake/impl/src/main/res/values-de/translations.xml b/features/rageshake/impl/src/main/res/values-de/translations.xml
index b316d8b45e..51b331a86f 100644
--- a/features/rageshake/impl/src/main/res/values-de/translations.xml
+++ b/features/rageshake/impl/src/main/res/values-de/translations.xml
@@ -10,6 +10,6 @@
"Absturzprotokolle senden"
"Logs zulassen"
"Bildschirmfoto senden"
- "Um zu überprüfen, ob alles wie vorgesehen funktioniert, werden Protokolle mit deiner Nachricht gesendet. Diese werden privat sein. Um nur Ihre Nachricht zu senden, schalte diese Einstellung aus."
+ "Deiner Nachricht werden Protokolle beigefügt, um sicherzustellen, dass alles ordnungsgemäß funktioniert. Um deine Nachricht ohne Logs zu senden, deaktiviere diese Einstellung."
"%1$s ist bei der letzten Verwendung abgestürzt. Möchtest du uns einen Absturzbericht senden?"
diff --git a/features/roomdetails/impl/src/main/res/values-cs/translations.xml b/features/roomdetails/impl/src/main/res/values-cs/translations.xml
index 6e92891c70..90a9bb0190 100644
--- a/features/roomdetails/impl/src/main/res/values-cs/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-cs/translations.xml
@@ -13,7 +13,12 @@
"Nelze aktualizovat místnost"
"Zprávy jsou zabezpečeny zámky. Pouze vy a příjemci máte jedinečné klíče k jejich odemčení."
"Šifrování zpráv povoleno"
+ "Při načítání nastavení oznámení došlo k chybě."
+ "Ztišení této místnosti se nezdařilo, zkuste to prosím znovu."
+ "Nepodařilo se zrušit ztišení této místnosti, zkuste to prosím znovu."
"Pozvat lidi"
+ "Vlastní"
+ "Výchozí"
"Oznámení"
"Název místnosti"
"Sdílet místnost"
diff --git a/features/roomdetails/impl/src/main/res/values-de/translations.xml b/features/roomdetails/impl/src/main/res/values-de/translations.xml
index 992ca94b85..8ae23e8152 100644
--- a/features/roomdetails/impl/src/main/res/values-de/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-de/translations.xml
@@ -1,18 +1,23 @@
- - "1 Person"
+ - "%1$d Person"
- "%1$d Personen"
"Thema hinzufügen"
"Bereits Mitglied"
"Bereits eingeladen"
"Raum bearbeiten"
- "Wir konnten nicht alle Informationen für diesen Raum aktualisieren."
+ "Es gab einen unbekannten Fehler und die Informationen konnten nicht geändert werden."
"Raum konnte nicht aktualisiert werden"
"Nachrichten sind mit Schlössern gesichert. Nur du und der Empfänger haben die eindeutigen Schlüssel, um sie zu entsperren."
"Nachrichtenverschlüsselung aktiviert"
+ "Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten."
+ "Das Stummschalten dieses Raums ist fehlgeschlagen. Bitte versuche es erneut."
+ "Die Stummschaltung dieses Raums konnte nicht aufgehoben werden. Bitte versuchen Sie es erneut."
"Personen einladen"
+ "Benutzerdefiniert"
+ "Standard"
"Benachrichtigungen"
"Raumname"
"Raum teilen"
@@ -20,10 +25,10 @@
"Ausstehend"
"Raummitglieder"
"Blockieren"
- "Blockierte Benutzer können dir keine Nachrichten senden und alle Nachrichten von ihnen werden ausgeblendet. Du kannst diese Aktion jederzeit rückgängig machen."
+ "Blockierte Benutzer können dir keine Nachrichten senden und alle ihre Nachrichten werden ausgeblendet. Du kannst sie jederzeit entsperren."
"Nutzer blockieren"
"Blockierung aufheben"
- "Wenn du den Benutzer entsperrst, kannst du wieder alle Nachrichten von ihm sehen."
+ "Du wirst alle ihre Nachrichten wieder sehen."
"Nutzer entblockieren"
"Raum verlassen"
"Personen"
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
index dbef9c799b..e069634d49 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
@@ -39,7 +39,6 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.user.getCurrentUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
-import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -73,11 +72,11 @@ class RoomListPresenter @Inject constructor(
}
// Session verification status (unknown, not verified, verified)
- val sessionVerifiedStatus by sessionVerificationService.sessionVerifiedStatus.collectAsState()
+ val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false)
var verificationPromptDismissed by rememberSaveable { mutableStateOf(false) }
// We combine both values to only display the prompt if the session is not verified and it wasn't dismissed
val displayVerificationPrompt by remember {
- derivedStateOf { sessionVerifiedStatus == SessionVerifiedStatus.NotVerified && !verificationPromptDismissed }
+ derivedStateOf { canVerifySession && !verificationPromptDismissed }
}
var displaySearchResults by rememberSaveable { mutableStateOf(false) }
diff --git a/features/roomlist/impl/src/main/res/values-cs/translations.xml b/features/roomlist/impl/src/main/res/values-cs/translations.xml
index d355d2c70c..cdf719e00f 100644
--- a/features/roomlist/impl/src/main/res/values-cs/translations.xml
+++ b/features/roomlist/impl/src/main/res/values-cs/translations.xml
@@ -1,7 +1,9 @@
"Vytvořte novou konverzaci nebo místnost"
+ "Začněte tím, že někomu pošnete zprávu."
+ "Zatím žádné konverzace."
"Všechny chaty"
"Zdá se, že používáte nové zařízení. Ověřte přihlášení, abyste měli přístup k zašifrovaným zprávám."
- "Přístup k historii zpráv"
+ "Ověřte, že jste to vy"
diff --git a/features/roomlist/impl/src/main/res/values-de/translations.xml b/features/roomlist/impl/src/main/res/values-de/translations.xml
index 2ed1cd0263..49a400b138 100644
--- a/features/roomlist/impl/src/main/res/values-de/translations.xml
+++ b/features/roomlist/impl/src/main/res/values-de/translations.xml
@@ -1,7 +1,9 @@
"Ein neues Gespräch oder einen neuen Raum erstellen"
+ "Beginnen, indem du jemandem eine Nachricht sendest."
+ "Noch keine Chats."
"Alle Chats"
"Es sieht so aus, als ob du ein neues Gerät verwendest. Verifiziere, dass du es bist, um auf deine verschlüsselten Nachrichten zuzugreifen."
- "Greife auf deine Nachrichten-Historie zu"
+ "Verifiziere, dass du es bist"
diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt
index eaa3801e12..d8524d5834 100644
--- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt
+++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt
@@ -202,7 +202,7 @@ class RoomListPresenterTests {
fun `present - handle DismissRequestVerificationPrompt`() = runTest {
val roomListService = FakeRoomListService()
val matrixClient = FakeMatrixClient(
- roomListService = roomListService
+ roomListService = roomListService,
)
val presenter = createRoomListPresenter(
client = matrixClient,
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 43c2a25bdb..118e6324c5 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -65,7 +65,7 @@ android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref
android_desugar = "com.android.tools:desugar_jdk_libs:2.0.3"
kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
# https://firebase.google.com/docs/android/setup#available-libraries
-google_firebase_bom = "com.google.firebase:firebase-bom:32.2.2"
+google_firebase_bom = "com.google.firebase:firebase-bom:32.2.3"
# AndroidX
androidx_material = { module = "com.google.android.material:material", version.ref = "material" }
@@ -146,7 +146,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
timber = "com.jakewharton.timber:timber:5.0.1"
-matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.46"
+matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.47"
sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" }
sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
@@ -159,8 +159,8 @@ vanniktech_emoji = "com.vanniktech:emoji-google:0.16.0"
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
statemachine = "com.freeletics.flowredux:compose:1.2.0"
maplibre = "org.maplibre.gl:android-sdk:10.2.0"
-maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:2.0.0"
-maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.0"
+maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:2.0.1"
+maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.1"
# Analytics
posthog = "com.posthog.android:posthog:2.0.3"
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListOption.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListOption.kt
new file mode 100644
index 0000000000..cfc414862f
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListOption.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.designsystem.components.dialogs
+
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.toImmutableList
+
+/**
+ * Used to store the visual data for a list option.
+ */
+data class ListOption(
+ val title: String,
+ val subtitle: String? = null,
+)
+
+/** Creates an immutable list of [ListOption]s from the given [values], using them as titles. */
+fun listOptionOf(vararg values: String): ImmutableList {
+ return values.map { ListOption(it) }.toImmutableList()
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/MultipleSelectionDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/MultipleSelectionDialog.kt
new file mode 100644
index 0000000000..af0bb4e1d1
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/MultipleSelectionDialog.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.designsystem.components.dialogs
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.toMutableStateList
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.airbnb.android.showkase.annotation.ShowkaseComposable
+import io.element.android.libraries.designsystem.components.list.CheckboxListItem
+import io.element.android.libraries.designsystem.preview.DayNightPreviews
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewGroup
+import io.element.android.libraries.designsystem.theme.components.DialogPreview
+import io.element.android.libraries.designsystem.theme.components.ListSupportingText
+import io.element.android.libraries.designsystem.theme.components.SimpleAlertDialogContent
+import io.element.android.libraries.ui.strings.CommonStrings
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MultipleSelectionDialog(
+ options: ImmutableList,
+ onConfirmClicked: (List) -> Unit,
+ onDismissRequest: () -> Unit,
+ modifier: Modifier = Modifier,
+ confirmButtonTitle: String = stringResource(CommonStrings.action_confirm),
+ dismissButtonTitle: String = stringResource(CommonStrings.action_cancel),
+ title: String? = null,
+ subtitle: String? = null,
+ initialSelection: ImmutableList = persistentListOf(),
+) {
+ val decoratedSubtitle: @Composable (() -> Unit)? = subtitle?.let {
+ @Composable {
+ ListSupportingText(
+ text = it,
+ modifier = Modifier.padding(start = 8.dp)
+ )
+ }
+ }
+ AlertDialog(
+ modifier = modifier,
+ onDismissRequest = onDismissRequest,
+ ) {
+ MultipleSelectionDialogContent(
+ title = title,
+ subtitle = decoratedSubtitle,
+ options = options,
+ confirmButtonTitle = confirmButtonTitle,
+ onConfirmClicked = onConfirmClicked,
+ dismissButtonTitle = dismissButtonTitle,
+ onDismissRequest = onDismissRequest,
+ initialSelected = initialSelection,
+ )
+ }
+}
+
+@Composable
+internal fun MultipleSelectionDialogContent(
+ options: ImmutableList,
+ confirmButtonTitle: String,
+ onConfirmClicked: (List) -> Unit,
+ dismissButtonTitle: String,
+ onDismissRequest: () -> Unit,
+ modifier: Modifier = Modifier,
+ title: String? = null,
+ initialSelected: ImmutableList = persistentListOf(),
+ subtitle: @Composable (() -> Unit)? = null,
+) {
+ val selectedOptionIndexes = remember { initialSelected.toMutableStateList() }
+
+ fun isSelected(index: Int) = selectedOptionIndexes.any { it == index }
+
+ SimpleAlertDialogContent(
+ title = title,
+ subtitle = subtitle,
+ modifier = modifier,
+ submitText = confirmButtonTitle,
+ onSubmitClicked = {
+ onConfirmClicked(selectedOptionIndexes.toList())
+ },
+ cancelText = dismissButtonTitle,
+ onCancelClicked = onDismissRequest,
+ applyPaddingToContents = false,
+ ) {
+ LazyColumn {
+ itemsIndexed(options) { index, option ->
+ CheckboxListItem(
+ headline = option.title,
+ checked = isSelected(index),
+ onChange = {
+ if (isSelected(index)) {
+ selectedOptionIndexes.remove(index)
+ } else {
+ selectedOptionIndexes.add(index)
+ }
+ },
+ supportingText = option.subtitle,
+ compactLayout = true,
+ modifier = Modifier.padding(start = 8.dp)
+ )
+ }
+ }
+ }
+}
+
+@DayNightPreviews
+@ShowkaseComposable(group = PreviewGroup.Dialogs)
+@Composable
+internal fun MultipleSelectionDialogContentPreview() {
+ ElementPreview(showBackground = false) {
+ DialogPreview {
+ val options = persistentListOf(
+ ListOption("Option 1", "Supporting line text lorem ipsum dolor sit amet, consectetur."),
+ ListOption("Option 2"),
+ ListOption("Option 3"),
+ )
+ MultipleSelectionDialogContent(
+ title = "Dialog title",
+ options = options,
+ onConfirmClicked = {},
+ onDismissRequest = {},
+ confirmButtonTitle = "Save",
+ dismissButtonTitle = "Cancel",
+ initialSelected = persistentListOf(0),
+ )
+ }
+ }
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/SingleSelectionDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/SingleSelectionDialog.kt
new file mode 100644
index 0000000000..5b75318136
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/SingleSelectionDialog.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.designsystem.components.dialogs
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.airbnb.android.showkase.annotation.ShowkaseComposable
+import io.element.android.libraries.designsystem.components.list.RadioButtonListItem
+import io.element.android.libraries.designsystem.preview.DayNightPreviews
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewGroup
+import io.element.android.libraries.designsystem.theme.components.DialogPreview
+import io.element.android.libraries.designsystem.theme.components.ListSupportingText
+import io.element.android.libraries.designsystem.theme.components.SimpleAlertDialogContent
+import io.element.android.libraries.ui.strings.CommonStrings
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SingleSelectionDialog(
+ options: ImmutableList,
+ onOptionSelected: (Int) -> Unit,
+ onDismissRequest: () -> Unit,
+ modifier: Modifier = Modifier,
+ title: String? = null,
+ subtitle: String? = null,
+ dismissButtonTitle: String = stringResource(CommonStrings.action_cancel),
+ initialSelection: Int? = null,
+) {
+ val decoratedSubtitle: @Composable (() -> Unit)? = subtitle?.let {
+ @Composable {
+ ListSupportingText(
+ text = it,
+ modifier = Modifier.padding(start = 8.dp)
+ )
+ }
+ }
+ AlertDialog(
+ modifier = modifier,
+ onDismissRequest = onDismissRequest,
+ ) {
+ SingleSelectionDialogContent(
+ title = title,
+ subtitle = decoratedSubtitle,
+ options = options,
+ onOptionSelected = onOptionSelected,
+ dismissButtonTitle = dismissButtonTitle,
+ onDismissRequest = onDismissRequest,
+ initialSelection = initialSelection,
+ )
+ }
+}
+
+@Composable
+internal fun SingleSelectionDialogContent(
+ options: ImmutableList,
+ onOptionSelected: (Int) -> Unit,
+ onDismissRequest: () -> Unit,
+ dismissButtonTitle: String,
+ modifier: Modifier = Modifier,
+ title: String? = null,
+ initialSelection: Int? = null,
+ subtitle: @Composable (() -> Unit)? = null,
+) {
+ SimpleAlertDialogContent(
+ title = title,
+ subtitle = subtitle,
+ modifier = modifier,
+ cancelText = dismissButtonTitle,
+ onCancelClicked = onDismissRequest,
+ applyPaddingToContents = false,
+ ) {
+ LazyColumn {
+ itemsIndexed(options) { index, option ->
+ RadioButtonListItem(
+ headline = option.title,
+ supportingText = option.subtitle,
+ selected = index == initialSelection,
+ onSelected = { onOptionSelected(index) },
+ compactLayout = true,
+ modifier = Modifier.padding(start = 8.dp)
+ )
+ }
+ }
+ }
+}
+
+@DayNightPreviews
+@ShowkaseComposable(group = PreviewGroup.Dialogs)
+@Composable
+internal fun SingleSelectionDialogContentPreview() {
+ ElementPreview(showBackground = false) {
+ DialogPreview {
+ val options = persistentListOf(
+ ListOption("Option 1"),
+ ListOption("Option 2"),
+ ListOption("Option 3"),
+ )
+ SingleSelectionDialogContent(
+ title = "Dialog title",
+ options = options,
+ onOptionSelected = {},
+ onDismissRequest = {},
+ dismissButtonTitle = "Cancel",
+ initialSelection = 0
+ )
+ }
+ }
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/CheckboxListItem.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/CheckboxListItem.kt
new file mode 100644
index 0000000000..d2bc30262a
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/CheckboxListItem.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.designsystem.components.list
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import io.element.android.libraries.designsystem.theme.components.ListItem
+import io.element.android.libraries.designsystem.theme.components.ListItemStyle
+import io.element.android.libraries.designsystem.theme.components.Text
+
+@Composable
+fun CheckboxListItem(
+ headline: String,
+ checked: Boolean,
+ onChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+ supportingText: String? = null,
+ trailingContent: ListItemContent? = null,
+ enabled: Boolean = true,
+ style: ListItemStyle = ListItemStyle.Default,
+ compactLayout: Boolean = false,
+) {
+ ListItem(
+ modifier = modifier,
+ headlineContent = { Text(headline) },
+ supportingContent = supportingText?.let { @Composable { Text(it) } },
+ leadingContent = ListItemContent.Checkbox(checked, null, enabled, compact = compactLayout),
+ trailingContent = trailingContent,
+ style = style,
+ enabled = enabled,
+ onClick = { onChange(!checked) },
+ )
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/ListItemContent.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/ListItemContent.kt
new file mode 100644
index 0000000000..cac8b557ee
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/ListItemContent.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.designsystem.components.list
+
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import io.element.android.libraries.designsystem.theme.components.IconSource
+import io.element.android.libraries.designsystem.theme.components.ListItem
+import io.element.android.libraries.designsystem.theme.components.Checkbox as CheckboxComponent
+import io.element.android.libraries.designsystem.theme.components.Icon as IconComponent
+import io.element.android.libraries.designsystem.theme.components.RadioButton as RadioButtonComponent
+import io.element.android.libraries.designsystem.theme.components.Switch as SwitchComponent
+import io.element.android.libraries.designsystem.theme.components.Text as TextComponent
+
+/**
+ * This is a helper to set default leading and trailing content for [ListItem]s.
+ */
+sealed interface ListItemContent {
+ /**
+ * Default Switch content for [ListItem].
+ * @param checked The current state of the switch.
+ * @param onChange Callback when the switch is toggled: it should only be set to override the default click behaviour in the [ListItem].
+ * @param enabled Whether the switch is enabled or not.
+ */
+ data class Switch(
+ val checked: Boolean,
+ val onChange: ((Boolean) -> Unit)? = null,
+ val enabled: Boolean = true
+ ) : ListItemContent
+
+ /**
+ * Default Checkbox content for [ListItem].
+ * @param checked The current state of the checkbox.
+ * @param onChange Callback when the checkbox is toggled: it should only be set to override the default click behaviour in the [ListItem].
+ * @param enabled Whether the checkbox is enabled or not.
+ * @param compact Reduces the size of the component to make the wrapping [ListItem] smaller.
+ * This is especially useful when the [ListItem] is used inside a Dialog. `false` by default.
+ */
+ data class Checkbox(
+ val checked: Boolean,
+ val onChange: ((Boolean) -> Unit)? = null,
+ val enabled: Boolean = true,
+ val compact: Boolean = false
+ ) : ListItemContent
+
+ /**
+ * Default RadioButton content for [ListItem].
+ * @param selected The current state of the radio button.
+ * @param onClick Callback when the radio button is toggled: it should only be set to override the default click behaviour in the [ListItem].
+ * @param enabled Whether the radio button is enabled or not.
+ * @param compact Reduces the size of the component to make the wrapping [ListItem] smaller.
+ * This is especially useful when the [ListItem] is used inside a Dialog. `false` by default.
+ */
+ data class RadioButton(
+ val selected: Boolean,
+ val onClick: (() -> Unit)? = null,
+ val enabled: Boolean = true,
+ val compact: Boolean = false
+ ) : ListItemContent
+
+ /**
+ * Default Icon content for [ListItem]. Sets the Icon component to a predefined size.
+ * @param iconSource The icon to display, using [IconSource.getPainter].
+ */
+ data class Icon(val iconSource: IconSource) : ListItemContent
+
+ /**
+ * Default Text content for [ListItem]. Sets the Text component to a max size and clips overflow.
+ * @param text The text to display.
+ */
+ data class Text(val text: String) : ListItemContent
+
+ /** Displays any custom content. */
+ data class Custom(val content: @Composable () -> Unit) : ListItemContent
+
+ @Composable
+ fun View() {
+ when (this) {
+ is Switch -> SwitchComponent(
+ checked = checked,
+ onCheckedChange = onChange,
+ enabled = enabled
+ )
+ is Checkbox -> CheckboxComponent(
+ modifier = if (compact) Modifier.size(maxCompactSize) else Modifier,
+ checked = checked,
+ onCheckedChange = onChange,
+ enabled = enabled
+ )
+ is RadioButton -> RadioButtonComponent(
+ modifier = if (compact) Modifier.size(maxCompactSize) else Modifier,
+ selected = selected,
+ onClick = onClick,
+ enabled = enabled
+ )
+ is Icon -> IconComponent(
+ modifier = Modifier.size(maxCompactSize),
+ painter = iconSource.getPainter(),
+ contentDescription = iconSource.contentDescription
+ )
+ is Text -> TextComponent(modifier = Modifier.widthIn(max = 128.dp), text = text, maxLines = 1, overflow = TextOverflow.Ellipsis)
+ is Custom -> content()
+ }
+ }
+}
+
+private val maxCompactSize = DpSize(24.dp, 24.dp)
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/MultipleSelectionListItem.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/MultipleSelectionListItem.kt
new file mode 100644
index 0000000000..8a0bc1b4a9
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/MultipleSelectionListItem.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.designsystem.components.list
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.toMutableStateList
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import io.element.android.libraries.designsystem.components.dialogs.ListOption
+import io.element.android.libraries.designsystem.components.dialogs.MultipleSelectionDialog
+import io.element.android.libraries.designsystem.components.dialogs.listOptionOf
+import io.element.android.libraries.designsystem.preview.ElementThemedPreview
+import io.element.android.libraries.designsystem.preview.PreviewGroup
+import io.element.android.libraries.designsystem.theme.components.ListItem
+import io.element.android.libraries.designsystem.theme.components.Text
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toImmutableList
+
+@Composable
+fun MultipleSelectionListItem(
+ headline: String,
+ options: ImmutableList,
+ onSelectionChanged: (List) -> Unit,
+ resultFormatter: (List) -> String?,
+ modifier: Modifier = Modifier,
+ supportingText: String? = null,
+ leadingContent: ListItemContent? = null,
+ selected: ImmutableList = persistentListOf(),
+ displayResultInTrailingContent: Boolean = false,
+) {
+ val selectedIndexes = remember(selected) { selected.toMutableStateList() }
+ val selectedItemsText by remember { derivedStateOf { resultFormatter(selectedIndexes) } }
+
+ val decoratedSupportedText: @Composable (() -> Unit)? = when {
+ !selectedItemsText.isNullOrBlank() && !displayResultInTrailingContent -> {
+ @Composable {
+ Text(selectedItemsText!!)
+ }
+ }
+ supportingText != null -> {
+ @Composable {
+ Text(supportingText)
+ }
+ }
+ else -> null
+ }
+
+ val trailingContent: ListItemContent? = if (!selectedItemsText.isNullOrBlank() && displayResultInTrailingContent) {
+ ListItemContent.Text(selectedItemsText!!)
+ } else {
+ null
+ }
+
+ var displaySelectionDialog by rememberSaveable { mutableStateOf(false) }
+
+ ListItem(
+ modifier = modifier,
+ headlineContent = { Text(text = headline) },
+ supportingContent = decoratedSupportedText,
+ leadingContent = leadingContent,
+ trailingContent = trailingContent,
+ onClick = { displaySelectionDialog = true }
+ )
+
+ if (displaySelectionDialog) {
+ MultipleSelectionDialog(
+ title = headline,
+ options = options,
+ onConfirmClicked = { newSelectedIndexes ->
+ if (newSelectedIndexes != selectedIndexes.toList()) {
+ onSelectionChanged(newSelectedIndexes)
+ selectedIndexes.clear()
+ selectedIndexes.addAll(newSelectedIndexes)
+ }
+ displaySelectionDialog = false
+ },
+ onDismissRequest = { displaySelectionDialog = false },
+ initialSelection = selectedIndexes.toImmutableList(),
+ )
+ }
+}
+
+@Preview("Multiple selection List item - no selection", group = PreviewGroup.ListItems)
+@Composable
+internal fun MutipleSelectionListItemPreview() {
+ ElementThemedPreview {
+ val options = listOptionOf("Option 1", "Option 2", "Option 3")
+ MultipleSelectionListItem(
+ headline = "Headline",
+ options = options,
+ onSelectionChanged = {},
+ supportingText = "Supporting text",
+ resultFormatter = { result -> formatResult(result, options) },
+ )
+ }
+}
+
+@Preview("Multiple selection List item - selection in supporting text", group = PreviewGroup.ListItems)
+@Composable
+internal fun MutipleSelectionListItemSelectedPreview() {
+ ElementThemedPreview {
+ val options = listOptionOf("Option 1", "Option 2", "Option 3")
+ val selected = persistentListOf(0, 2)
+ MultipleSelectionListItem(
+ headline = "Headline",
+ options = options,
+ onSelectionChanged = {},
+ supportingText = "Supporting text",
+ resultFormatter = {
+ val selectedValues = formatResult(it, options)
+ "Selected: $selectedValues"
+ },
+ selected = selected,
+ )
+ }
+}
+
+@Preview("Multiple selection List item - selection in trailing content", group = PreviewGroup.ListItems)
+@Composable
+internal fun MutipleSelectionListItemSelectedTrailingContentPreview() {
+ ElementThemedPreview {
+ val options = listOptionOf("Option 1", "Option 2", "Option 3")
+ val selected = persistentListOf(0, 2)
+ MultipleSelectionListItem(
+ headline = "Headline",
+ options = options,
+ onSelectionChanged = {},
+ supportingText = "Supporting text",
+ resultFormatter = { selected.size.toString() },
+ displayResultInTrailingContent = true,
+ selected = selected,
+ )
+ }
+}
+
+private fun formatResult(result: List, options: ImmutableList): String? {
+ return options.mapIndexedNotNull { index, value -> value.title.takeIf { result.contains(index) } }.joinToString(", ").takeIf { it.isNotEmpty() }
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/RadioButtonListItem.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/RadioButtonListItem.kt
new file mode 100644
index 0000000000..fff038121e
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/RadioButtonListItem.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.designsystem.components.list
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import io.element.android.libraries.designsystem.theme.components.ListItem
+import io.element.android.libraries.designsystem.theme.components.ListItemStyle
+import io.element.android.libraries.designsystem.theme.components.Text
+
+@Composable
+fun RadioButtonListItem(
+ headline: String,
+ selected: Boolean,
+ onSelected: () -> Unit,
+ modifier: Modifier = Modifier,
+ supportingText: String? = null,
+ trailingContent: ListItemContent? = null,
+ style: ListItemStyle = ListItemStyle.Default,
+ enabled: Boolean = true,
+ compactLayout: Boolean = false,
+) {
+ ListItem(
+ modifier = modifier,
+ headlineContent = { Text(headline) },
+ supportingContent = supportingText?.let { @Composable { Text(it) } },
+ leadingContent = ListItemContent.RadioButton(selected, null, enabled, compact = compactLayout),
+ trailingContent = trailingContent,
+ style = style,
+ enabled = enabled,
+ onClick = onSelected,
+ )
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/SingleSelectionListItem.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/SingleSelectionListItem.kt
new file mode 100644
index 0000000000..01dff71991
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/SingleSelectionListItem.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.designsystem.components.list
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import io.element.android.libraries.designsystem.components.dialogs.ListOption
+import io.element.android.libraries.designsystem.components.dialogs.SingleSelectionDialog
+import io.element.android.libraries.designsystem.components.dialogs.listOptionOf
+import io.element.android.libraries.designsystem.preview.ElementThemedPreview
+import io.element.android.libraries.designsystem.preview.PreviewGroup
+import io.element.android.libraries.designsystem.theme.components.ListItem
+import io.element.android.libraries.designsystem.theme.components.Text
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlin.time.Duration.Companion.seconds
+
+@Composable
+fun SingleSelectionListItem(
+ headline: String,
+ options: ImmutableList,
+ onSelectionChanged: (Int) -> Unit,
+ modifier: Modifier = Modifier,
+ supportingText: String? = null,
+ leadingContent: ListItemContent? = null,
+ resultFormatter: (Int) -> String? = { options.getOrNull(it)?.title },
+ selected: Int? = null,
+ displayResultInTrailingContent: Boolean = false,
+) {
+ val coroutineScope = rememberCoroutineScope()
+
+ var selectedIndex by rememberSaveable(selected) { mutableStateOf(selected) }
+ val selectedItem by remember { derivedStateOf { selectedIndex?.let { resultFormatter(it) } } }
+ val decoratedSupportedText: @Composable (() -> Unit)? = if (!selectedItem.isNullOrBlank() && !displayResultInTrailingContent) {
+ @Composable {
+ Text(selectedItem!!)
+ }
+ } else {
+ supportingText?.let {
+ @Composable {
+ Text(it)
+ }
+ }
+ }
+ val trailingContent: ListItemContent? = if (!selectedItem.isNullOrBlank() && displayResultInTrailingContent) {
+ ListItemContent.Text(selectedItem!!)
+ } else {
+ null
+ }
+
+ var displaySelectionDialog by rememberSaveable { mutableStateOf(false) }
+
+ ListItem(
+ modifier = modifier,
+ headlineContent = { Text(text = headline) },
+ supportingContent = decoratedSupportedText,
+ leadingContent = leadingContent,
+ trailingContent = trailingContent,
+ onClick = { displaySelectionDialog = true }
+ )
+
+ if (displaySelectionDialog) {
+ SingleSelectionDialog(
+ title = headline,
+ options = options,
+ onOptionSelected = { index ->
+ if (index != selectedIndex) {
+ onSelectionChanged(index)
+ selectedIndex = index
+ }
+ // Delay hiding the dialog for a bit so the new state is displayed in it before being dismissed
+ coroutineScope.launch {
+ delay(0.5.seconds)
+ displaySelectionDialog = false
+ }
+ },
+ onDismissRequest = { displaySelectionDialog = false },
+ initialSelection = selectedIndex,
+ )
+ }
+}
+
+@Preview("Single selection List item - no selection", group = PreviewGroup.ListItems)
+@Composable
+internal fun SingleSelectionListItemPreview() {
+ ElementThemedPreview {
+ SingleSelectionListItem(
+ headline = "Headline",
+ options = listOptionOf("Option 1", "Option 2", "Option 3"),
+ onSelectionChanged = {},
+ )
+ }
+}
+
+@Preview("Single selection List item - no selection, supporting text", group = PreviewGroup.ListItems)
+@Composable
+internal fun SingleSelectionListItemUnselectedWithSupportingTextPreview() {
+ ElementThemedPreview {
+ SingleSelectionListItem(
+ headline = "Headline",
+ options = listOptionOf("Option 1", "Option 2", "Option 3"),
+ supportingText = "Supporting text",
+ onSelectionChanged = {},
+ )
+ }
+}
+
+@Preview("Single selection List item - selection in supporting text", group = PreviewGroup.ListItems)
+@Composable
+internal fun SingleSelectionListItemSelectedInSupportingTextPreview() {
+ ElementThemedPreview {
+ SingleSelectionListItem(
+ headline = "Headline",
+ options = listOptionOf("Option 1", "Option 2", "Option 3"),
+ supportingText = "Supporting text",
+ onSelectionChanged = {},
+ selected = 1,
+ )
+ }
+}
+
+@Preview("Single selection List item - selection in trailing content", group = PreviewGroup.ListItems)
+@Composable
+internal fun SingleSelectionListItemSelectedInTrailingContentPreview() {
+ ElementThemedPreview {
+ SingleSelectionListItem(
+ headline = "Headline",
+ options = listOptionOf("Option 1", "Option 2", "Option 3"),
+ supportingText = "Supporting text",
+ onSelectionChanged = {},
+ selected = 1,
+ displayResultInTrailingContent = true,
+ )
+ }
+}
+
+@Preview("Single selection List item - custom formatter", group = PreviewGroup.ListItems)
+@Composable
+internal fun SingleSelectionListItemCustomFormattertPreview() {
+ ElementThemedPreview {
+ SingleSelectionListItem(
+ headline = "Headline",
+ options = listOptionOf("Option 1", "Option 2", "Option 3"),
+ supportingText = "Supporting text",
+ onSelectionChanged = {},
+ resultFormatter = { "Selected index: $it"},
+ selected = 1,
+ displayResultInTrailingContent = true,
+ )
+ }
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/SwitchListItem.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/SwitchListItem.kt
new file mode 100644
index 0000000000..bbd7b11396
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/SwitchListItem.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.designsystem.components.list
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import io.element.android.libraries.designsystem.theme.components.ListItem
+import io.element.android.libraries.designsystem.theme.components.ListItemStyle
+import io.element.android.libraries.designsystem.theme.components.Text
+
+@Composable
+fun SwitchListItem(
+ headline: String,
+ value: Boolean,
+ onChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+ supportingText: String? = null,
+ leadingContent: ListItemContent? = null,
+ enabled: Boolean = true,
+ style: ListItemStyle = ListItemStyle.Default,
+) {
+ ListItem(
+ modifier = modifier,
+ headlineContent = { Text(headline) },
+ supportingContent = supportingText?.let { @Composable { Text(it) } },
+ leadingContent = leadingContent,
+ trailingContent = ListItemContent.Switch(value, null, enabled),
+ style = style,
+ enabled = enabled,
+ onClick = { onChange(!value) },
+ )
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt
index a3c7274c45..c9bb2d9dd5 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt
@@ -16,6 +16,7 @@
package io.element.android.libraries.designsystem.theme.components
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -55,11 +56,49 @@ internal fun SimpleAlertDialogContent(
onCancelClicked: () -> Unit,
modifier: Modifier = Modifier,
title: String? = null,
+ subtitle: @Composable (() -> Unit)? = null,
submitText: String? = null,
onSubmitClicked: () -> Unit = {},
thirdButtonText: String? = null,
onThirdButtonClicked: () -> Unit = {},
+ applyPaddingToContents: Boolean = true,
icon: @Composable (() -> Unit)? = null,
+) {
+ SimpleAlertDialogContent(
+ content = {
+ Text(
+ text = content,
+ style = ElementTheme.materialTypography.bodyMedium,
+ )
+ },
+ cancelText = cancelText,
+ onCancelClicked = onCancelClicked,
+ modifier = modifier,
+ title = title,
+ subtitle = subtitle,
+ submitText = submitText,
+ onSubmitClicked = onSubmitClicked,
+ thirdButtonText = thirdButtonText,
+ onThirdButtonClicked = onThirdButtonClicked,
+ icon = icon,
+ applyPaddingToContents = applyPaddingToContents,
+ )
+}
+
+@Composable
+internal fun SimpleAlertDialogContent(
+ cancelText: String,
+ onCancelClicked: () -> Unit,
+ modifier: Modifier = Modifier,
+ title: String? = null,
+ subtitle: @Composable (() -> Unit)? = null,
+ submitText: String? = null,
+ onSubmitClicked: () -> Unit = {},
+ thirdButtonText: String? = null,
+ onThirdButtonClicked: () -> Unit = {},
+ applyPaddingToContents: Boolean = true,
+ icon: @Composable (() -> Unit)? = null,
+ content: @Composable () -> Unit,
) {
AlertDialogContent(
buttons = {
@@ -99,12 +138,8 @@ internal fun SimpleAlertDialogContent(
)
}
},
- text = {
- Text(
- text = content,
- style = ElementTheme.materialTypography.bodyMedium,
- )
- },
+ subtitle = subtitle,
+ content = content,
shape = DialogContentDefaults.shape,
containerColor = DialogContentDefaults.containerColor,
iconContentColor = DialogContentDefaults.iconContentColor,
@@ -117,6 +152,7 @@ internal fun SimpleAlertDialogContent(
// TextButtons will not consume this provided content color value, and will used their
// own defined or default colors.
buttonContentColor = MaterialTheme.colorScheme.primary,
+ applyPaddingToContents = applyPaddingToContents,
)
}
@@ -128,7 +164,8 @@ internal fun AlertDialogContent(
buttons: @Composable () -> Unit,
icon: (@Composable () -> Unit)?,
title: (@Composable () -> Unit)?,
- text: @Composable (() -> Unit)?,
+ subtitle: @Composable (() -> Unit)?,
+ content: @Composable (() -> Unit)?,
shape: Shape,
containerColor: Color,
tonalElevation: Dp,
@@ -137,6 +174,7 @@ internal fun AlertDialogContent(
titleContentColor: Color,
textContentColor: Color,
modifier: Modifier = Modifier,
+ applyPaddingToContents: Boolean = true,
) {
Surface(
modifier = modifier,
@@ -145,12 +183,21 @@ internal fun AlertDialogContent(
tonalElevation = tonalElevation,
) {
Column(
- modifier = Modifier.padding(DialogContentDefaults.externalPadding)
+ modifier = Modifier.padding(
+ if (applyPaddingToContents) {
+ // We can just apply the same padding to the whole dialog contents
+ DialogContentDefaults.externalPadding
+ } else {
+ // We should only apply vertical padding in this case, every component will apply the horizontal content individually
+ DialogContentDefaults.externalVerticalPadding
+ }
+ )
) {
icon?.let {
CompositionLocalProvider(LocalContentColor provides iconContentColor) {
Box(
Modifier
+ .then(if (applyPaddingToContents) Modifier else Modifier.padding(DialogContentDefaults.externalHorizontalPadding))
.padding(DialogContentDefaults.iconPadding)
.align(Alignment.CenterHorizontally)
) {
@@ -165,6 +212,12 @@ internal fun AlertDialogContent(
Box(
// Align the title to the center when an icon is present.
Modifier
+ .then(
+ if (applyPaddingToContents)
+ Modifier
+ else
+ Modifier.padding(DialogContentDefaults.externalHorizontalPadding)
+ )
.padding(DialogContentDefaults.titlePadding)
.align(
if (icon == null) {
@@ -179,23 +232,28 @@ internal fun AlertDialogContent(
}
}
}
- text?.let {
+ subtitle?.invoke()
+ content?.let {
CompositionLocalProvider(LocalContentColor provides textContentColor) {
- val textStyle =
- MaterialTheme.typography.bodyMedium
+ val textStyle = MaterialTheme.typography.bodyMedium
ProvideTextStyle(textStyle) {
Box(
Modifier
.weight(weight = 1f, fill = false)
+ // We don't apply padding here if it wasn't applied to the root component, this allows us to have a full width content
.padding(DialogContentDefaults.textPadding)
.align(Alignment.Start)
) {
- text()
+ content()
}
}
}
}
- Box(modifier = Modifier.align(Alignment.End)) {
+ Box(
+ modifier = Modifier
+ .then(if (applyPaddingToContents) Modifier else Modifier.padding(DialogContentDefaults.externalHorizontalPadding))
+ .align(Alignment.End)
+ ) {
CompositionLocalProvider(LocalContentColor provides buttonContentColor) {
val textStyle =
MaterialTheme.typography.labelLarge
@@ -304,6 +362,7 @@ private fun AlertDialogFlowRow(
internal fun DialogPreview(content: @Composable () -> Unit) {
Box(
modifier = Modifier
+ .background(ElementTheme.materialColors.onSurfaceVariant)
.sizeIn(minWidth = DialogMinWidth, maxWidth = DialogMaxWidth)
.padding(20.dp),
propagateMinConstraints = true
@@ -313,8 +372,11 @@ internal fun DialogPreview(content: @Composable () -> Unit) {
}
internal object DialogContentDefaults {
+ private val externalPaddingDp = 24.dp
val shape = RoundedCornerShape(12.dp)
- val externalPadding = PaddingValues(all = 24.dp)
+ val externalPadding = PaddingValues(all = externalPaddingDp)
+ val externalHorizontalPadding = PaddingValues(horizontal = externalPaddingDp)
+ val externalVerticalPadding = PaddingValues(vertical = externalPaddingDp)
val titlePadding = PaddingValues(bottom = 16.dp)
val iconPadding = PaddingValues(bottom = 8.dp)
val textPadding = PaddingValues(bottom = 16.dp)
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Icon.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Icon.kt
index 24de433058..af63763113 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Icon.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Icon.kt
@@ -24,6 +24,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
@@ -123,6 +124,21 @@ fun Icon(
)
}
+@Composable
+fun Icon(
+ painter: Painter,
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ tint: Color = LocalContentColor.current,
+) {
+ androidx.compose.material3.Icon(
+ painter = painter,
+ contentDescription = contentDescription,
+ modifier = modifier,
+ tint = tint
+ )
+}
+
@Preview(group = PreviewGroup.Icons)
@Composable
internal fun IconImageVectorPreview() =
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconToggleButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconToggleButton.kt
new file mode 100644
index 0000000000..fd355d3860
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconToggleButton.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.designsystem.theme.components
+
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.CheckCircle
+import androidx.compose.material.icons.filled.RadioButtonUnchecked
+import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.material3.IconToggleButtonColors
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import io.element.android.libraries.designsystem.preview.ElementThemedPreview
+import io.element.android.libraries.designsystem.preview.PreviewGroup
+
+@Composable
+fun IconToggleButton(
+ checked: Boolean,
+ onCheckedChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ colors: IconToggleButtonColors = IconButtonDefaults.iconToggleButtonColors(),
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ content: @Composable () -> Unit
+) {
+ androidx.compose.material3.IconToggleButton(
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ modifier = modifier,
+ enabled = enabled,
+ colors = colors,
+ interactionSource = interactionSource,
+ content = content,
+ )
+}
+
+@Preview(group = PreviewGroup.Toggles)
+@Composable
+internal fun IconToggleButtonPreview() = ElementThemedPreview(vertical = false) { ContentToPreview() }
+
+@Composable
+private fun ContentToPreview() {
+ var checked by remember { mutableStateOf(false) }
+ Column {
+ Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
+ val icon: @Composable () -> Unit = {
+ Icon(
+ imageVector = if (checked) Icons.Default.CheckCircle else Icons.Default.RadioButtonUnchecked,
+ contentDescription = "IconToggleButton"
+ )
+ }
+ IconToggleButton(checked = checked, enabled = true, onCheckedChange = { checked = !checked }, content = icon)
+ IconToggleButton(checked = checked, enabled = false, onCheckedChange = { checked = !checked }, content = icon)
+ }
+ Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
+ val icon: @Composable () -> Unit = {
+ Icon(
+ imageVector = if (!checked) Icons.Default.CheckCircle else Icons.Default.RadioButtonUnchecked,
+ contentDescription = "IconToggleButton"
+ )
+ }
+ IconToggleButton(checked = !checked, enabled = true, onCheckedChange = { checked = !checked }, content = icon)
+ IconToggleButton(checked = !checked, enabled = false, onCheckedChange = { checked = !checked }, content = icon)
+ }
+ }
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt
index a7e240ce7f..321dc812a9 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt
@@ -19,6 +19,7 @@ package io.element.android.libraries.designsystem.theme.components
import androidx.compose.foundation.clickable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Share
+import androidx.compose.material3.ListItemColors
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
@@ -29,9 +30,11 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.theme.ElementTheme
@@ -55,35 +58,27 @@ fun ListItem(
headlineContent: @Composable () -> Unit,
modifier: Modifier = Modifier,
supportingContent: @Composable (() -> Unit)? = null,
- leadingContent: @Composable (() -> Unit)? = null,
- trailingContent: @Composable (() -> Unit)? = null,
+ leadingContent: ListItemContent? = null,
+ trailingContent: ListItemContent? = null,
style: ListItemStyle = ListItemStyle.Default,
enabled: Boolean = true,
onClick: (() -> Unit)? = null,
) {
- val headlineColor = if (enabled) when (style) {
- ListItemStyle.Destructive -> ElementTheme.colors.textCriticalPrimary
- else -> ElementTheme.colors.textPrimary
- } else {
- // We cannot apply a disabled color by default: https://issuetracker.google.com/issues/280480132
- ElementTheme.colors.textDisabled
- }
+ val colors = ListItemDefaults.colors(
+ containerColor = Color.Transparent,
+ headlineColor = style.headlineColor(),
+ leadingIconColor = style.leadingIconColor(),
+ trailingIconColor = style.trailingIconColor(),
+ supportingColor = style.supportingTextColor(),
+ disabledHeadlineColor = ListItemDefaultColors.headlineDisabled,
+ disabledLeadingIconColor = ListItemDefaultColors.iconDisabled,
+ disabledTrailingIconColor = ListItemDefaultColors.iconDisabled,
+ )
- val supportingContentColor = if (enabled) {
- ElementTheme.materialColors.onSurfaceVariant
- } else {
- // We cannot apply a disabled color by default: https://issuetracker.google.com/issues/280480132
- ElementTheme.colors.textDisabled
- }
-
- val leadingTrailingContentColor = if (enabled) when (style) {
- ListItemStyle.Primary -> ElementTheme.colors.iconPrimary
- ListItemStyle.Destructive -> ElementTheme.colors.iconCriticalPrimary
- else -> ElementTheme.colors.iconTertiary
- } else {
- // We cannot apply a disabled color by default: https://issuetracker.google.com/issues/280480132
- ElementTheme.colors.iconDisabled
- }
+ // We cannot just pass the disabled colors, they must be set manually: https://issuetracker.google.com/issues/280480132
+ val headlineColor = if (enabled) colors.headlineColor else colors.disabledHeadlineColor
+ val leadingContentColor = if (enabled) colors.leadingIconColor else colors.disabledLeadingIconColor
+ val trailingContentColor = if (enabled) colors.trailingIconColor else colors.disabledTrailingIconColor
val decoratedHeadlineContent: @Composable () -> Unit = {
CompositionLocalProvider(
@@ -97,7 +92,6 @@ fun ListItem(
{
CompositionLocalProvider(
LocalTextStyle provides ElementTheme.materialTypography.bodyMedium,
- LocalContentColor provides supportingContentColor,
) {
content()
}
@@ -106,31 +100,31 @@ fun ListItem(
val decoratedLeadingContent: (@Composable () -> Unit)? = leadingContent?.let { content ->
{
CompositionLocalProvider(
- LocalContentColor provides leadingTrailingContentColor,
+ LocalContentColor provides leadingContentColor,
) {
- content()
+ content.View()
}
}
}
val decoratedTrailingContent: (@Composable () -> Unit)? = trailingContent?.let { content ->
{
CompositionLocalProvider(
- LocalContentColor provides leadingTrailingContentColor,
LocalTextStyle provides ElementTheme.typography.fontBodyMdRegular,
+ LocalContentColor provides trailingContentColor,
) {
- content()
+ content.View()
}
}
}
androidx.compose.material3.ListItem(
headlineContent = decoratedHeadlineContent,
- modifier = modifier.clickable(enabled = enabled && onClick != null, onClick = onClick ?: {}),
+ modifier = if (onClick != null) Modifier.clickable(enabled = enabled, onClick = onClick).then(modifier) else modifier,
overlineContent = null,
supportingContent = decoratedSupportingContent,
leadingContent = decoratedLeadingContent,
trailingContent = decoratedTrailingContent,
- colors = ListItemDefaults.colors(), // These aren't really used since we need the workaround for the disabled state color
+ colors = colors,
tonalElevation = 0.dp,
shadowElevation = 0.dp,
)
@@ -143,6 +137,47 @@ sealed interface ListItemStyle {
data object Default : ListItemStyle
data object Primary: ListItemStyle
data object Destructive : ListItemStyle
+
+ @Composable fun headlineColor() = when (this) {
+ Default, Primary -> ListItemDefaultColors.headline
+ Destructive -> ElementTheme.colors.textCriticalPrimary
+ }
+
+ @Composable fun supportingTextColor() = when (this) {
+ Default, Primary -> ListItemDefaultColors.supportingText
+ // FIXME once we have a defined color for this value
+ Destructive -> ElementTheme.colors.textCriticalPrimary.copy(alpha = 0.8f)
+ }
+
+ @Composable fun leadingIconColor() = when (this) {
+ Default -> ListItemDefaultColors.icon
+ Primary -> ElementTheme.colors.iconPrimary
+ Destructive -> ElementTheme.colors.iconCriticalPrimary
+ }
+
+ @Composable fun trailingIconColor() = when (this) {
+ Default -> ListItemDefaultColors.icon
+ Primary -> ElementTheme.colors.iconPrimary
+ Destructive -> ElementTheme.colors.iconCriticalPrimary
+ }
+}
+
+object ListItemDefaultColors {
+ val headline: Color @Composable get() = ElementTheme.colors.textPrimary
+ val headlineDisabled: Color @Composable get() = ElementTheme.colors.textDisabled
+ val supportingText: Color @Composable get() = ElementTheme.materialColors.onSurfaceVariant
+ val icon: Color @Composable get() = ElementTheme.colors.iconTertiary
+ val iconDisabled: Color @Composable get() = ElementTheme.colors.iconDisabled
+
+ val colors: ListItemColors @Composable get() = ListItemDefaults.colors(
+ headlineColor = headline,
+ supportingColor = supportingText,
+ leadingIconColor = icon,
+ trailingIconColor = icon,
+ disabledHeadlineColor = headlineDisabled,
+ disabledLeadingIconColor = iconDisabled,
+ disabledTrailingIconColor = iconDisabled,
+ )
}
// region: Simple list item
@@ -335,8 +370,9 @@ private object PreviewItems {
@Composable
fun ThreeLinesListItemPreview(
modifier: Modifier = Modifier,
- leadingContent: @Composable (() -> Unit)? = null,
- trailingContent: @Composable (() -> Unit)? = null,
+ style: ListItemStyle = ListItemStyle.Default,
+ leadingContent: ListItemContent? = null,
+ trailingContent: ListItemContent? = null,
) {
ElementThemedPreview {
ListItem(
@@ -344,6 +380,7 @@ private object PreviewItems {
supportingContent = PreviewItems.text(),
leadingContent = leadingContent,
trailingContent = trailingContent,
+ style = style,
modifier = modifier,
)
}
@@ -352,8 +389,9 @@ private object PreviewItems {
@Composable
fun TwoLinesListItemPreview(
modifier: Modifier = Modifier,
- leadingContent: @Composable (() -> Unit)? = null,
- trailingContent: @Composable (() -> Unit)? = null,
+ style: ListItemStyle = ListItemStyle.Default,
+ leadingContent: ListItemContent? = null,
+ trailingContent: ListItemContent? = null,
) {
ElementThemedPreview {
ListItem(
@@ -361,6 +399,7 @@ private object PreviewItems {
supportingContent = PreviewItems.textSingleLine(),
leadingContent = leadingContent,
trailingContent = trailingContent,
+ style = style,
modifier = modifier,
)
}
@@ -369,9 +408,9 @@ private object PreviewItems {
@Composable
fun OneLineListItemPreview(
modifier: Modifier = Modifier,
- leadingContent: @Composable (() -> Unit)? = null,
- trailingContent: @Composable (() -> Unit)? = null,
style: ListItemStyle = ListItemStyle.Default,
+ leadingContent: ListItemContent? = null,
+ trailingContent: ListItemContent? = null,
enabled: Boolean = true,
) {
ElementThemedPreview {
@@ -402,25 +441,22 @@ private object PreviewItems {
}
@Composable
- fun checkbox() = @Composable {
+ fun checkbox(): ListItemContent {
var checked by remember { mutableStateOf(false) }
- Checkbox(checked = checked, onCheckedChange = { checked = !checked })
+ return ListItemContent.Checkbox(checked = checked, onChange = { checked = !checked })
}
@Composable
- fun radioButton() = @Composable {
+ fun radioButton(): ListItemContent {
var checked by remember { mutableStateOf(false) }
- RadioButton(selected = checked, onClick = { checked = !checked })
+ return ListItemContent.RadioButton(selected = checked, onClick = { checked = !checked })
}
@Composable
- fun switch() = @Composable {
+ fun switch() : ListItemContent {
var checked by remember { mutableStateOf(false) }
- Switch(checked = checked, onCheckedChange = { checked = !checked })
+ return ListItemContent.Switch(checked = checked, onChange = { checked = !checked })
}
- @Composable
- fun icon() = @Composable {
- Icon(imageVector = Icons.Outlined.Share, contentDescription = null)
- }
+ fun icon() = ListItemContent.Icon(iconSource = IconSource.Vector(Icons.Outlined.Share))
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSection.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSection.kt
index d465192ec1..ff58f1e2e0 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSection.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSection.kt
@@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
+import androidx.compose.material.icons.outlined.Share
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.runtime.Composable
@@ -32,6 +33,7 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.theme.ElementTheme
@@ -249,7 +251,7 @@ internal fun ListSupportingTextDefaultPaddingPreview() {
internal fun ListSupportingTextSmallPaddingPreview() {
ElementThemedPreview {
Column {
- ListItem(headlineContent = { Text("A title") }, leadingContent = { Icon(Icons.Default.Share, null) })
+ ListItem(headlineContent = { Text("A title") }, leadingContent = ListItemContent.Icon(IconSource.Vector(Icons.Outlined.Share)))
ListSupportingText(
text = "Supporting line text lorem ipsum dolor sit amet, consectetur. Read more",
contentPadding = ListSupportingTextDefaults.Padding.SmallLeadingContent,
@@ -263,7 +265,7 @@ internal fun ListSupportingTextSmallPaddingPreview() {
internal fun ListSupportingTextLargePaddingPreview() {
ElementThemedPreview {
Column {
- ListItem(headlineContent = { Text("A title") }, leadingContent = { Switch(checked = true, onCheckedChange = null) })
+ ListItem(headlineContent = { Text("A title") }, leadingContent = ListItemContent.Switch(checked = true, onChange = {}))
ListSupportingText(
text = "Supporting line text lorem ipsum dolor sit amet, consectetur. Read more",
contentPadding = ListSupportingTextDefaults.Padding.LargeLeadingContent,
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt
index a8b186a6b2..9f373d1b71 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt
@@ -23,7 +23,10 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.material3.RadioButtonColors
import androidx.compose.material3.RadioButtonDefaults
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -67,14 +70,15 @@ internal fun RadioButtonPreview() = ElementThemedPreview(vertical = false) { Con
@Composable
private fun ContentToPreview() {
+ var checked by remember { mutableStateOf(false) }
Column {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
- RadioButton(selected = false, onClick = {})
- RadioButton(selected = false, enabled = false, onClick = {})
+ RadioButton(selected = checked, enabled = true, onClick = { checked = !checked })
+ RadioButton(selected = checked, enabled = false, onClick = { checked = !checked })
}
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
- RadioButton(selected = true, onClick = {})
- RadioButton(selected = true, enabled = false, onClick = {})
+ RadioButton(selected = !checked, enabled = true, onClick = { checked = !checked })
+ RadioButton(selected = !checked, enabled = false, onClick = { checked = !checked })
}
}
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/DatePickerPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/DatePickerPreview.kt
index 45b5eb39be..e0435ee30e 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/DatePickerPreview.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/DatePickerPreview.kt
@@ -50,7 +50,8 @@ private fun ContentToPreview() {
buttons = { /*TODO*/ },
icon = { /*TODO*/ },
title = { /*TODO*/ },
- text = { DatePicker(state = state, showModeToggle = true) },
+ subtitle = null,
+ content = { DatePicker(state = state, showModeToggle = true) },
shape = AlertDialogDefaults.shape,
containerColor = AlertDialogDefaults.containerColor,
tonalElevation = AlertDialogDefaults.TonalElevation,
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/TimePickerPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/TimePickerPreview.kt
index 7aae42ed0e..d900dd6d8b 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/TimePickerPreview.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/TimePickerPreview.kt
@@ -39,7 +39,8 @@ internal fun TimePickerHorizontalPreview() {
buttons = { /*TODO*/ },
icon = { /*TODO*/ },
title = { /*TODO*/ },
- text = { TimePicker(state = rememberTimePickerState(), layoutType = TimePickerLayoutType.Horizontal) },
+ subtitle = null,
+ content = { TimePicker(state = rememberTimePickerState(), layoutType = TimePickerLayoutType.Horizontal) },
shape = AlertDialogDefaults.shape,
containerColor = AlertDialogDefaults.containerColor,
tonalElevation = AlertDialogDefaults.TonalElevation,
@@ -60,7 +61,8 @@ internal fun TimePickerVerticalPreviewLight() {
buttons = { /*TODO*/ },
icon = { /*TODO*/ },
title = { /*TODO*/ },
- text = { TimePicker(state = rememberTimePickerState(), layoutType = TimePickerLayoutType.Vertical) },
+ subtitle = null,
+ content = { TimePicker(state = rememberTimePickerState(), layoutType = TimePickerLayoutType.Vertical) },
shape = AlertDialogDefaults.shape,
containerColor = AlertDialogDefaults.containerColor,
tonalElevation = AlertDialogDefaults.TonalElevation,
@@ -85,7 +87,8 @@ internal fun TimePickerVerticalPreviewDark() {
buttons = { /*TODO*/ },
icon = { /*TODO*/ },
title = { /*TODO*/ },
- text = { TimePicker(state = pickerState, layoutType = TimePickerLayoutType.Vertical) },
+ subtitle = null,
+ content = { TimePicker(state = pickerState, layoutType = TimePickerLayoutType.Vertical) },
shape = AlertDialogDefaults.shape,
containerColor = AlertDialogDefaults.containerColor,
tonalElevation = AlertDialogDefaults.TonalElevation,
diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt
index 65fc191e53..a0a0525fb5 100644
--- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt
+++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt
@@ -95,7 +95,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
is StateContent -> {
stateContentFormatter.format(content, senderDisplayName, isOutgoing, RenderingMode.RoomList)
}
- is PollContent,
+ is PollContent, // TODO Polls: handle last message
is FailedToParseMessageLikeContent, is FailedToParseStateContent, is UnknownContent -> {
prefixIfNeeded(sp.getString(CommonStrings.common_unsupported_event), senderDisplayName, isDmRoom)
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/poll/PollKind.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/poll/PollKind.kt
index 85bb7c0256..b78f00bc86 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/poll/PollKind.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/poll/PollKind.kt
@@ -20,5 +20,8 @@ enum class PollKind {
Disclosed,
/** Results should be only revealed when the poll is ended. */
- Undisclosed
+ Undisclosed,
}
+
+val PollKind.isDisclosed: Boolean
+ get() = this == PollKind.Disclosed
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
index f2c51c357e..88d35c83d4 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
@@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo
+import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import kotlinx.coroutines.flow.StateFlow
@@ -141,6 +142,21 @@ interface MatrixRoom : Closeable {
assetType: AssetType? = null,
): Result
+ /**
+ * Create a poll in the room.
+ *
+ * @param question The question to ask.
+ * @param answers The list of answers.
+ * @param maxSelections The maximum number of answers that can be selected.
+ * @param pollKind The kind of poll to create.
+ */
+ suspend fun createPoll(
+ question: String,
+ answers: List,
+ maxSelections: Int,
+ pollKind: PollKind,
+ ): Result
+
override fun close() = destroy()
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt
index d94a4e6817..c463530050 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt
@@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.api.verification
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
interface SessionVerificationService {
@@ -37,6 +38,11 @@ interface SessionVerificationService {
*/
val sessionVerifiedStatus: StateFlow
+ /**
+ * Returns whether the current session needs to be verified and the SDK is ready to start the verification.
+ */
+ val canVerifySessionFlow: Flow
+
/**
* Request verification of the current session.
*/
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
index c68913b705..4b0afb0b4f 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
@@ -35,7 +35,6 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
import io.element.android.libraries.matrix.api.sync.SyncService
-import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@@ -93,8 +92,8 @@ class RustMatrixClient constructor(
private val innerRoomListService = syncService.roomListService()
private val sessionDispatcher = dispatchers.io.limitedParallelism(64)
private val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-${sessionId}")
- private val verificationService = RustSessionVerificationService()
private val rustSyncService = RustSyncService(syncService, sessionCoroutineScope)
+ private val verificationService = RustSessionVerificationService(rustSyncService)
private val pushersService = RustPushersService(
client = client,
dispatchers = dispatchers,
@@ -149,13 +148,11 @@ class RustMatrixClient constructor(
init {
client.setDelegate(clientDelegate)
- rustSyncService.syncState
- .onEach { syncState ->
- if (syncState == SyncState.Running) {
- onSlidingSyncUpdate()
- }
+ roomListService.state.onEach { state ->
+ if (state == RoomListService.State.Running) {
+ setupVerificationControllerIfNeeded()
}
- .launchIn(sessionCoroutineScope)
+ }.launchIn(sessionCoroutineScope)
}
override suspend fun getRoom(roomId: RoomId): MatrixRoom? = withContext(sessionDispatcher) {
@@ -338,8 +335,8 @@ class RustMatrixClient constructor(
}
}
- private fun onSlidingSyncUpdate() {
- if (!verificationService.isReady.value) {
+ private fun setupVerificationControllerIfNeeded() {
+ if (verificationService.verificationController == null) {
try {
verificationService.verificationController = client.getSessionVerificationController()
} catch (e: Throwable) {
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/poll/PollKind.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/poll/PollKind.kt
index bde49464ad..d0682edf2c 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/poll/PollKind.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/poll/PollKind.kt
@@ -23,3 +23,8 @@ fun RustPollKind.map(): PollKind = when (this) {
RustPollKind.DISCLOSED -> PollKind.Disclosed
RustPollKind.UNDISCLOSED -> PollKind.Undisclosed
}
+
+fun PollKind.toInner(): RustPollKind = when (this) {
+ PollKind.Disclosed -> RustPollKind.DISCLOSED
+ PollKind.Undisclosed -> RustPollKind.UNDISCLOSED
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
index 878aaa4473..ffe06379d3 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
@@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo
+import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MessageEventType
@@ -41,6 +42,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl
import io.element.android.libraries.matrix.impl.media.map
+import io.element.android.libraries.matrix.impl.poll.toInner
import io.element.android.libraries.matrix.impl.room.location.toInner
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
import io.element.android.libraries.matrix.impl.util.destroyAll
@@ -378,7 +380,24 @@ class RustMatrixRoom(
description = description,
zoomLevel = zoomLevel?.toUByte(),
assetType = assetType?.toInner(),
- txnId = genTransactionId()
+ txnId = genTransactionId(),
+ )
+ }
+ }
+
+ override suspend fun createPoll(
+ question: String,
+ answers: List,
+ maxSelections: Int,
+ pollKind: PollKind,
+ ): Result = withContext(roomDispatcher) {
+ runCatching {
+ innerRoom.createPoll(
+ question = question,
+ answers = answers,
+ maxSelections = maxSelections.toUByte(),
+ pollKind = pollKind.toInner(),
+ txnId = genTransactionId(),
)
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt
index dc65bb74a6..29797ed3c4 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt
@@ -17,20 +17,25 @@
package io.element.android.libraries.matrix.impl.verification
import io.element.android.libraries.core.data.tryOrNull
+import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
+import io.element.android.libraries.matrix.impl.sync.RustSyncService
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
import org.matrix.rustcomponents.sdk.SessionVerificationController
import org.matrix.rustcomponents.sdk.SessionVerificationControllerDelegate
import org.matrix.rustcomponents.sdk.SessionVerificationControllerInterface
import org.matrix.rustcomponents.sdk.SessionVerificationEmoji
import javax.inject.Inject
-class RustSessionVerificationService @Inject constructor() : SessionVerificationService, SessionVerificationControllerDelegate {
+class RustSessionVerificationService @Inject constructor(
+ private val syncService: RustSyncService,
+) : SessionVerificationService, SessionVerificationControllerDelegate {
var verificationController: SessionVerificationControllerInterface? = null
set(value) {
@@ -52,6 +57,10 @@ class RustSessionVerificationService @Inject constructor() : SessionVerification
private val _sessionVerifiedStatus = MutableStateFlow(SessionVerifiedStatus.Unknown)
override val sessionVerifiedStatus: StateFlow = _sessionVerifiedStatus.asStateFlow()
+ override val canVerifySessionFlow = combine(sessionVerifiedStatus, syncService.syncState) { verificationStatus, syncState ->
+ syncState == SyncState.Running && verificationStatus == SessionVerifiedStatus.NotVerified
+ }
+
override suspend fun requestVerification() = tryOrFail {
verificationController?.requestVerification()
}
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
index a660c56a99..88f705162e 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
@@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo
+import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MessageEventType
@@ -83,6 +84,7 @@ class FakeMatrixRoom(
private var forwardEventResult = Result.success(Unit)
private var reportContentResult = Result.success(Unit)
private var sendLocationResult = Result.success(Unit)
+ private var createPollResult = Result.success(Unit)
private var progressCallbackValues = emptyList>()
val editMessageCalls = mutableListOf()
@@ -104,6 +106,9 @@ class FakeMatrixRoom(
private val _sentLocations = mutableListOf()
val sentLocations: List = _sentLocations
+ private val _createPollInvocations = mutableListOf()
+ val createPollInvocations: List = _createPollInvocations
+
var invitedUserId: UserId? = null
private set
@@ -305,6 +310,16 @@ class FakeMatrixRoom(
return sendLocationResult
}
+ override suspend fun createPoll(
+ question: String,
+ answers: List,
+ maxSelections: Int,
+ pollKind: PollKind
+ ): Result = simulateLongTask {
+ _createPollInvocations.add(CreatePollInvocation(question, answers, maxSelections, pollKind))
+ return createPollResult
+ }
+
fun givenLeaveRoomError(throwable: Throwable?) {
this.leaveRoomError = throwable
}
@@ -397,6 +412,10 @@ class FakeMatrixRoom(
sendLocationResult = result
}
+ fun givenCreatePollResult(result: Result) {
+ createPollResult = result
+ }
+
fun givenProgressCallbackValues(values: List>) {
progressCallbackValues = values
}
@@ -409,3 +428,10 @@ data class SendLocationInvocation(
val zoomLevel: Int?,
val assetType: AssetType?,
)
+
+data class CreatePollInvocation(
+ val question: String,
+ val answers: List,
+ val maxSelections: Int,
+ val pollKind: PollKind,
+)
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt
index 2f7887c537..62b0b39adf 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt
@@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationS
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -27,13 +28,13 @@ class FakeSessionVerificationService : SessionVerificationService {
private val _isReady = MutableStateFlow(false)
private val _sessionVerifiedStatus = MutableStateFlow(SessionVerifiedStatus.Unknown)
private var _verificationFlowState = MutableStateFlow(VerificationFlowState.Initial)
+ private var _canVerifySessionFlow = MutableStateFlow(true)
private var emojiList = emptyList()
var shouldFail = false
- override val verificationFlowState: StateFlow
- get() = _verificationFlowState
-
+ override val verificationFlowState: StateFlow =_verificationFlowState
override val sessionVerifiedStatus: StateFlow = _sessionVerifiedStatus
+ override val canVerifySessionFlow: Flow = _canVerifySessionFlow
override val isReady: StateFlow = _isReady
@@ -77,6 +78,10 @@ class FakeSessionVerificationService : SessionVerificationService {
_verificationFlowState.value = state
}
+ fun givenCanVerifySession(canVerify: Boolean) {
+ _canVerifySessionFlow.value = canVerify
+ }
+
fun givenIsReady(value: Boolean) {
_isReady.value = value
}
diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/VideoCompressor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/VideoCompressor.kt
index e7e294cd7c..5587eedfa4 100644
--- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/VideoCompressor.kt
+++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/VideoCompressor.kt
@@ -20,6 +20,8 @@ import android.content.Context
import android.net.Uri
import com.otaliastudios.transcoder.Transcoder
import com.otaliastudios.transcoder.TranscoderListener
+import com.otaliastudios.transcoder.resize.AtMostResizer
+import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
import io.element.android.libraries.androidutils.file.createTmpFile
import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.di.ApplicationContext
@@ -35,6 +37,11 @@ class VideoCompressor @Inject constructor(
fun compress(uri: Uri) = callbackFlow {
val tmpFile = context.createTmpFile(extension = "mp4")
val future = Transcoder.into(tmpFile.path)
+ .setVideoTrackStrategy(
+ DefaultVideoStrategy.Builder()
+ .addResizer(AtMostResizer(1920, 1080))
+ .build()
+ )
.addDataSource(context, uri)
.setListener(object : TranscoderListener {
override fun onTranscodeProgress(progress: Double) {
diff --git a/libraries/push/impl/src/main/res/values-de/translations.xml b/libraries/push/impl/src/main/res/values-de/translations.xml
index a2ae094731..04c7521d9e 100644
--- a/libraries/push/impl/src/main/res/values-de/translations.xml
+++ b/libraries/push/impl/src/main/res/values-de/translations.xml
@@ -7,7 +7,7 @@
"** Senden fehlgeschlagen - bitte Raum öffnen"
"Beitreten"
"Ablehnen"
- "Hat dich eingeladen"
+ "Hat dich zum Chatten eingeladen"
"Neue Nachrichten"
"Reagierte mit %1$s"
"Als gelesen markieren"
diff --git a/libraries/textcomposer/src/main/res/values-cs/translations.xml b/libraries/textcomposer/src/main/res/values-cs/translations.xml
index 8e0524b69a..c37f419ec4 100644
--- a/libraries/textcomposer/src/main/res/values-cs/translations.xml
+++ b/libraries/textcomposer/src/main/res/values-cs/translations.xml
@@ -1,5 +1,6 @@
+ "Přidat přílohu"
"Přepnout seznam s odrážkami"
"Přepnout blok kódu"
"Zpráva…"
diff --git a/libraries/textcomposer/src/main/res/values-de/translations.xml b/libraries/textcomposer/src/main/res/values-de/translations.xml
index a28c784792..dea872d7c0 100644
--- a/libraries/textcomposer/src/main/res/values-de/translations.xml
+++ b/libraries/textcomposer/src/main/res/values-de/translations.xml
@@ -1,5 +1,6 @@
+ "Anhang hinzufügen"
"Aufzählungsliste ein-/ausschalten"
"Codeblock umschalten"
"Nachricht…"
diff --git a/libraries/ui-strings/src/main/res/values-cs/translations.xml b/libraries/ui-strings/src/main/res/values-cs/translations.xml
index 9d897db9d8..ee1b221ad7 100644
--- a/libraries/ui-strings/src/main/res/values-cs/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml
@@ -24,7 +24,7 @@
"Upravit"
"Povolit"
"Zapomněli jste heslo?"
- "Vpřed"
+ "Přeposlat"
"Pozvat"
"Pozvat přátele"
"Pozvat přátele do %1$s"
@@ -40,6 +40,7 @@
"Otevřít v aplikaci"
"Rychlá odpověď"
"Citovat"
+ "Reagovat"
"Odstranit"
"Odpovědět"
"Nahlásit chybu"
@@ -94,6 +95,9 @@
"Heslo"
"Lidé"
"Trvalý odkaz"
+ "Konečné hlasy: %1$s"
+ "Celkový počet hlasů: %1$s"
+ "Výsledky se zobrazí po skončení hlasování"
"Zásady ochrany osobních údajů"
"Reakce"
"Obnovování…"
@@ -140,7 +144,11 @@
"Cestování a místa"
"Symboly"
"Vytvoření trvalého odkazu se nezdařilo"
+ "%1$s nemohl načíst mapu. Zkuste to prosím později."
"Načítání zpráv se nezdařilo"
+ "%1$s nemá přístup k vaší poloze. Zkuste to prosím později."
+ "%1$s nemá oprávnění k přístupu k vaší poloze. Přístup můžete povolit v Nastavení."
+ "%1$s nemá oprávnění k přístupu k vaší poloze. Povolit přístup níže."
"Některé zprávy nebyly odeslány"
"Omlouváme se, došlo k chybě"
"🔐️ Připojte se ke mně na %1$s"
@@ -154,6 +162,11 @@
- "%1$d členové"
- "%1$d členů"
+
+ - "%d hlas"
+ - "%d hlasy"
+ - "%d hlasů"
+
"Zatřeste zařízením pro nahlášení chyby"
"Zdá se, že jste frustrovaně třásli telefonem. Chcete otevřít obrazovku pro nahlášení chyby?"
"Tato zpráva bude nahlášena správci vašeho domovského serveru. Nebude si moci přečíst žádné šifrované zprávy."
@@ -167,7 +180,35 @@
"Nahrání média se nezdařilo, zkuste to prosím znovu."
"Toto je jednorázový proces, děkujeme za čekání."
"Nastavení vašeho účtu"
+ "Další nastavení"
+ "Halsové a video hovory"
+ "Neshoda konfigurace"
+ "Zjednodušili jsme nastavení oznámení, abychom usnadnili hledání možností.
+
+Některá vlastní nastavení, která jste si vybrali v minulosti, se zde nezobrazují, ale jsou stále aktivní.
+
+Pokud budete pokračovat, některá nastavení se mohou změnit."
+ "Přímé zprávy"
+ "Vlastní nastavení pro chat"
+ "Při aktualizaci nastavení oznámení došlo k chybě."
+ "Všechny zprávy"
+ "Pouze zmínky a klíčová slova"
+ "V přímých zprávách mě upozornit na"
+ "Ve skupinových chatech mě upozornit na"
+ "Povolit oznámení na tomto zařízení"
+ "Konfigurace nebyla opravena, zkuste to prosím znovu."
+ "Skupinové chaty"
+ "Zmínky"
+ "Vše"
+ "Zmínky"
+ "Upozornit mě na"
+ "Upozornit mě na @room"
+ "Chcete-li dostávat oznámení, změňte prosím svůj %1$s."
+ "systémová nastavení"
+ "Systémová oznámení byla vypnuta"
+ "Oznámení"
"Zaškrtněte, pokud chcete skrýt všechny aktuální a budoucí zprávy od tohoto uživatele"
+ "Účet a zařízení"
"Sdílet polohu"
"Sdílet moji polohu"
"Otevřít v Mapách Apple"
diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml
index 0f5259e663..68223c8513 100644
--- a/libraries/ui-strings/src/main/res/values-de/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-de/translations.xml
@@ -8,7 +8,7 @@
"Zurück"
"Abbrechen"
"Foto auswählen"
- "Löschen"
+ "Zurücksetzen"
"Schließen"
"Verifizierung abschließen"
"Bestätigen"
@@ -37,9 +37,10 @@
"Nein"
"Nicht jetzt"
"OK"
- "Öffne mit"
+ "Öffnen mit"
"Schnellantwort"
"Zitieren"
+ "Reagieren"
"Entfernen"
"Antworten"
"Fehler melden"
@@ -56,7 +57,7 @@
"Starten"
"Chat starten"
"Verifizierung starten"
- "Tippe, um die Karte zu laden"
+ "Zum Karte laden tippen"
"Foto aufnehmen"
"Quelltext anzeigen"
"Ja"
@@ -80,25 +81,28 @@
"Nachricht weiterleiten"
"GIF"
"Bild"
- "Wir können die Matrix-ID dieses Benutzers nicht validieren. Die Einladung wurde möglicherweise nicht empfangen."
- "Raum verlassen"
+ "Diese Matrix-ID kann nicht gefunden werden, daher wird die Einladung möglicherweise nicht empfangen."
+ "Verlasse Raum"
"Link in Zwischenablage kopiert"
- "Wird geladen…"
+ "Lädt…"
"Nachricht"
"Nachrichtenlayout"
- "Nachricht wurde entfernt"
+ "Nachricht entfernt"
"Modern"
"Stummschalten"
"Keine Ergebnisse"
"Offline"
"Passwort"
"Personen"
- "Permalink"
+ "Dauerlink"
+ "Endgültige Stimmen: %1$s"
+ "Stimmen insgesamt: %1$s"
+ "Ergebnisse werden nach Ende der Umfrage angezeigt"
"Datenschutzerklärung"
"Reaktionen"
"Aktualisiere…"
"Auf %1$s antworten"
- "Melde einen Fehler"
+ "Einen Fehler melden"
"Bericht gesendet"
"Raumname"
"z.B. dein Projektname"
@@ -106,12 +110,12 @@
"Suchergebnisse"
"Sicherheit"
"Wählen deinen Server"
- "Senden…"
+ "Sendet…"
"Server wird nicht unterstützt"
"Server-URL"
"Einstellungen"
"Geteilter Standort"
- "Chat wird gestartet…"
+ "Starte Chat…"
"Sticker"
"Erfolg"
"Vorschläge"
@@ -120,7 +124,7 @@
"Thema"
"Worum geht es in diesem Raum?"
"Entschlüsselung nicht möglich"
- "Wir konnten Einladungen nicht erfolgreich an einen oder mehrere Benutzer senden."
+ "Einladungen konnten nicht an einen oder mehrere Benutzer gesendet werden."
"Einladung(en) können nicht gesendet werden"
"Stummschaltung aufheben"
"Nicht unterstütztes Ereignis"
@@ -128,7 +132,7 @@
"Verifizierung abgebrochen"
"Verifizierung abgeschlossen"
"Video"
- "Warten…"
+ "Warte…"
"Bestätigung"
"Warnung"
"Aktivitäten"
@@ -139,10 +143,12 @@
"Smileys & Personen"
"Reisen & Orte"
"Symbole"
- "Fehler beim Erstellen des Permalinks"
+ "Fehler beim Erstellen des Dauerlinks"
"%1$s konnte die Karte nicht laden. Bitte versuche es später erneut."
"Fehler beim Laden der Nachrichten"
"%1$s konnte nicht auf deinen Standort zugreifen. Bitte versuche es später erneut."
+ "%1$s hat keine Berechtigung, auf deinen Standort zuzugreifen. Du kannst den Zugriff in den Einstellungen aktivieren."
+ "%1$s hat keine Berechtigung, auf deinen Standort zuzugreifen. Aktiviere den Zugriff unten."
"Einige Nachrichten wurden nicht gesendet"
"Entschuldigung, ein Fehler ist aufgetreten."
"🔐️ Besuche mich auf %1$s"
@@ -155,9 +161,13 @@
- "%1$d Mitglied"
- "%1$d Mitglieder"
- "Rageshake zum Melden von Fehlern"
+
+ - "%d Stimme"
+ - "%d Stimmen"
+
+ "Schütteln zum Melden von Fehlern"
"Du scheinst frustriert das Telefon zu schütteln. Möchtest du den Fehlerberichtsbildschirm öffnen?"
- "Diese Nachricht wird an deinen Heimserver-Admin gemeldet werden. Er wird nicht in der Lage sein, verschlüsselte Nachrichten zu lesen."
+ "Diese Nachricht wird an deinen Heimserver-Admin gemeldet. Er wird nicht in der Lage sein, verschlüsselte Nachrichten zu lesen."
"Grund für die Meldung dieses Inhalts"
"Dies ist der Anfang von %1$s."
"Dies ist der Beginn dieser Konversation."
@@ -168,7 +178,35 @@
"Hochladen von Medien fehlgeschlagen, bitte versuchen Sie es erneut."
"Dies ist ein einmaliger Vorgang, danke fürs Warten."
"Dein Konto einrichten"
+ "Zusätzliche Einstellungen"
+ "Audio- und Videoanrufe"
+ "Konfigurationskonflikt"
+ "Wir haben die Benachrichtigungseinstellungen vereinfacht, damit Optionen leichter zu finden sind.
+
+Einige benutzerdefinierte Einstellungen, die du in der Vergangenheit ausgewählt hast, werden hier nicht angezeigt, sind aber immer noch aktiv.
+
+Wenn du fortfährst, ändern sich möglicherweise einige deine Einstellungen."
+ "Direkte Chats"
+ "Benutzerdefinierte Einstellung pro Chat"
+ "Beim Aktualisieren der Benachrichtigungseinstellung ist ein Fehler aufgetreten."
+ "Alle Nachrichten"
+ "Nur Erwähnungen und Schlüsselwörter"
+ "Bei direkten Chats, benachrichtigen mich für"
+ "Bei Gruppenchats, benachrichtigte mich für"
+ "Benachrichtigungen auf diesem Gerät aktivieren"
+ "Die Konfiguration wurde nicht korrigiert. Bitte versuche es erneut."
+ "Gruppenchats"
+ "Erwähnungen"
+ "Alle"
+ "Erwähnungen"
+ "Benachrichtige mich für"
+ "Benachrichtige mich bei @room"
+ "Um Benachrichtigungen zu erhalten, ändern bitte deine %1$s."
+ "Systemeinstellungen"
+ "Systembenachrichtigungen deaktiviert"
+ "Benachrichtigungen"
"Prüfe, ob du alle aktuellen und zukünftigen Nachrichten dieses Benutzers ausblenden möchtest"
+ "Konto und Geräte"
"Standort teilen"
"Meinen Standort teilen"
"In Apple Maps öffnen"
diff --git a/libraries/ui-strings/src/main/res/values-ru/translations.xml b/libraries/ui-strings/src/main/res/values-ru/translations.xml
index 77c2df4268..865eace823 100644
--- a/libraries/ui-strings/src/main/res/values-ru/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml
@@ -182,11 +182,21 @@
"Настройка учетной записи."
"Дополнительные параметры"
"Аудио и видео звонки"
+ "Несоответствие конфигурации"
+ "Мы упростили настройки уведомлений, чтобы упростить поиск опций.
+
+Некоторые пользовательские настройки, выбранные вами ранее, не отображаются в данном меню, но они все еще активны.
+
+Если вы продолжите, некоторые настройки могут быть изменены."
"Прямые чаты"
+ "Индивидуальные настройки для каждого чата"
"При обновлении настроек уведомления произошла ошибка."
+ "Все сообщения"
+ "Только упоминания и ключевые слова"
"Уведомлять меня в личных чатах"
"Уведомлять меня в групповых чатах"
"Включить уведомления на данном устройстве"
+ "Конфигурация не была исправлена, попробуйте еще раз."
"Групповые чаты"
"Упоминания"
"Все"
@@ -198,6 +208,7 @@
"Системные уведомления выключены"
"Уведомления"
"Отметьте, хотите ли вы скрыть все текущие и будущие сообщения от этого пользователя"
+ "Учетная запись и устройства"
"Поделиться местоположением"
"Поделиться моим местоположением"
"Открыть в Apple Maps"
diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml
index 14dd5626c8..a070f874e9 100644
--- a/libraries/ui-strings/src/main/res/values-sk/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml
@@ -182,10 +182,21 @@
"Nastavenie vášho účtu."
"Ďalšie nastavenia"
"Audio a video hovory"
+ "Nezhoda konfigurácie"
+ "Zjednodušili sme Nastavenia oznámení, aby ste ľahšie našli možnosti.
+
+Niektoré vlastné nastavenia, ktoré ste si nastavili v minulosti, sa tu nezobrazujú, ale sú stále aktívne.
+
+Ak budete pokračovať, niektoré z vašich nastavení sa môžu zmeniť."
"Priame konverzácie"
+ "Vlastné nastavenie pre konverzácie"
+ "Pri aktualizácii nastavenia oznámenia došlo k chybe."
+ "Všetky správy"
+ "Iba zmienky a kľúčové slová"
"Pri priamych rozhovoroch ma upozorniť na"
"Pri skupinových rozhovoroch ma upozorniť na"
"Povoliť oznámenia na tomto zariadení"
+ "Konfigurácia nebola opravená, skúste to prosím znova."
"Skupinové rozhovory"
"Zmienky"
"Všetky"
@@ -197,6 +208,7 @@
"Systémové oznámenia sú vypnuté"
"Oznámenia"
"Označte, či chcete skryť všetky aktuálne a budúce správy od tohto používateľa"
+ "Účet a zariadenia"
"Zdieľať polohu"
"Zdieľať moju polohu"
"Otvoriť v Apple Maps"
diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml
index e38c4fc6b8..db58894472 100644
--- a/libraries/ui-strings/src/main/res/values/localazy.xml
+++ b/libraries/ui-strings/src/main/res/values/localazy.xml
@@ -181,9 +181,9 @@
"Additional settings"
"Audio and video calls"
"Configuration mismatch"
- "We’ve simplified Notifications Settings to make options easier to find.
+ "We’ve simplified Notifications Settings to make options easier to find.
-Some custom settings you’ve chosen in the past are not shown here, but they’re still active.
+Some custom settings you’ve chosen in the past are not shown here, but they’re still active.
If you proceed, some of your settings may change."
"Direct chats"
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderViewDark_0_null_0,NEXUS_5,1.0,en].png
index ab3bc3ee7f..bc067f20c1 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderViewDark_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderViewDark_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e7aba18d2003a96a34e6eecf4fc6d0ee139fa8073549033da04eff3fa20b4c93
-size 40454
+oid sha256:a7f1d73aa1698bb02c03773ca9c3ec8494089d03c26d52b3b6f1aaa40a081528
+size 49266
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderViewLight_0_null_0,NEXUS_5,1.0,en].png
index 87a527b090..3b94b23ab9 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderViewLight_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderViewLight_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0422cc4e2bae580a88dacd711570e0c5ba105d5baae103313e42a07991823083
-size 42103
+oid sha256:f2154c7d196058a0a47aa46658ad9d2ea3edf6b89e5973396980178a2399179a
+size 50726
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewDark_0_null_1,NEXUS_5,1.0,en].png
index e36afba0a8..3b68e95447 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewDark_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewDark_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:64e67ea3130d0e70bdc8d0edc0bfa92763673c15b3235d4332f554a0e7a4840a
-size 45553
+oid sha256:b6344d9976849f1e56782a463fcca625943a7c6adf74c598c7a58245809b9db5
+size 53642
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewLight_0_null_1,NEXUS_5,1.0,en].png
index f589e41a6d..2e35a8461c 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewLight_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewLight_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7f2fd6d2fccd68abec9d829ccd673b72a0f1277644a1affb09faed7be8b8a416
-size 47021
+oid sha256:fd7e6fc09011429e0fe19bff73d3c48124b1f3bed74d3cb12c9f3f0405f53980
+size 55728
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_0,NEXUS_5,1.0,en].png
index 0134aee79e..9bd8087f0c 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f704348b48d03ce3e788e7e37298a008116c66f3ad0a074d164df4ebbb05d9d8
-size 48964
+oid sha256:15d14bf99af3cd0433870ebd6032d9bf4a45196e2ef1df7184cc55859a704dee
+size 49008
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_1,NEXUS_5,1.0,en].png
index 0134aee79e..170f3d997a 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f704348b48d03ce3e788e7e37298a008116c66f3ad0a074d164df4ebbb05d9d8
-size 48964
+oid sha256:6e47fc219bbd63b76d01a5e50c3e4c6b1b0a8b4ec40b08b13271d0b2673d8d5e
+size 50932
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_0,NEXUS_5,1.0,en].png
index f327dc9154..cb9d6334b0 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:945d8f7b4226c451c8d33f51bf051c7f08a316c786259d49475b2e2cde407bd7
-size 46415
+oid sha256:cacb91ffca97f21bbc19b29525813f58fc8017a858aa28ccd4e620b70a8cd9ca
+size 46119
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_1,NEXUS_5,1.0,en].png
index f327dc9154..ec2787d842 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:945d8f7b4226c451c8d33f51bf051c7f08a316c786259d49475b2e2cde407bd7
-size 46415
+oid sha256:3f1c14a23ee598ece6843a68d3ca0b1d1f725f53174bd493e27c6137de70c508
+size 48296
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventTimestampBelow_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventTimestampBelow_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..c1d740b8cb
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventTimestampBelow_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f689d80043e7d5121d072b928c866a3bde0358b5d5df06e1d4f0ceeb9a11dfef
+size 56344
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentNoResults-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentNoResults-D-0_0_null,NEXUS_5,1.0,en].png
deleted file mode 100644
index 57ddb69c7a..0000000000
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentNoResults-D-0_0_null,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:fb9e4bbe341a84452206a7485a477d81725b535369b1dfad3cf430548dbb21e8
-size 46450
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentNoResults-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentNoResults-N-0_1_null,NEXUS_5,1.0,en].png
deleted file mode 100644
index 7a31eae39a..0000000000
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentNoResults-N-0_1_null,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:8bfbbae8e27c4be7ea7fadeff2470773775cb476ef37b25c8f1bb8c35b5eddd9
-size 43082
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentWithResults-D-1_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentWithResults-D-1_1_null,NEXUS_5,1.0,en].png
deleted file mode 100644
index 0134aee79e..0000000000
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentWithResults-D-1_1_null,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:f704348b48d03ce3e788e7e37298a008116c66f3ad0a074d164df4ebbb05d9d8
-size 48964
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentWithResults-N-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentWithResults-N-1_2_null,NEXUS_5,1.0,en].png
deleted file mode 100644
index f327dc9154..0000000000
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentWithResults-N-1_2_null,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:945d8f7b4226c451c8d33f51bf051c7f08a316c786259d49475b2e2cde407bd7
-size 46415
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..a3e90a69fe
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:74508ef7f77a9c8713c75586ae4d34a9daab2608dbbd2f20de3e4d4a9a9be7e9
+size 39225
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..7be79c2135
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4ac9e0523fc99d472a1fe8f21719e64dbb7d1ec01059dd4183aa4a152f8ead55
+size 38673
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..7ee39de1a4
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ed68d9ebe67d5dad938a3efcd8c2b680444c650e635bdc04cfd140ba694d9f1d
+size 38928
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..b2d901dc0d
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1d5c9d2042dad75b48b61cdbae5b2425d7c935eb860df5d5c6fa3bcb327d13d1
+size 38842
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..4998418006
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d4ab648f5651b7457635be3eabb914be1c976154d12a163f1c1bb2cd92168824
+size 38730
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..a2cd64d048
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fbb713d36f8b36ce6b55f38c10323e784a80f6187e6613ded91dc531b23a7cb7
+size 36444
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..2d973414d1
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:31735c42ea83974595544a0f03636a2337210f23e60d4b7f8e41d46ba21d483f
+size 35920
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewNoResults-D-2_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewNoResults-D-2_2_null,NEXUS_5,1.0,en].png
deleted file mode 100644
index 1eef37d750..0000000000
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewNoResults-D-2_2_null,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:c4c5bc5f2086bd78a364e9996f0f41526d73d69857875594f5de7ea9998333d7
-size 23388
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewNoResults-N-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewNoResults-N-2_3_null,NEXUS_5,1.0,en].png
deleted file mode 100644
index 95c64c4548..0000000000
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewNoResults-N-2_3_null,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:42848910a949ec938ccc7a3547b19d1c2c81193f9a93b1db21ca77e4d9ed9663
-size 21761
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewWithResult-D-3_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewWithResult-D-3_3_null,NEXUS_5,1.0,en].png
deleted file mode 100644
index 1eef37d750..0000000000
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewWithResult-D-3_3_null,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:c4c5bc5f2086bd78a364e9996f0f41526d73d69857875594f5de7ea9998333d7
-size 23388
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewWithResult-N-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewWithResult-N-3_4_null,NEXUS_5,1.0,en].png
deleted file mode 100644
index 95c64c4548..0000000000
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewWithResult-N-3_4_null,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:42848910a949ec938ccc7a3547b19d1c2c81193f9a93b1db21ca77e4d9ed9663
-size 21761
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-D-1_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-D-1_1_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..9bd8087f0c
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-D-1_1_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:15d14bf99af3cd0433870ebd6032d9bf4a45196e2ef1df7184cc55859a704dee
+size 49008
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-N-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-N-1_2_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..cb9d6334b0
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-N-1_2_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cacb91ffca97f21bbc19b29525813f58fc8017a858aa28ccd4e620b70a8cd9ca
+size 46119
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-2_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-2_2_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..f148a727ae
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-2_2_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5a9ccbbc0a208ac398f07f0070a43d0ca5ab67ef322b274b641270a9c21a4a60
+size 48972
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-2_3_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..df2a5c30b9
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-2_3_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5736eb1d232b841d62f9d96070fc9b2993453652f5d7c448b6a819db9543615e
+size 45756
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-D-0_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..7319bf6694
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-D-0_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2264139761f7fe3977ece3c9a7d949ac51223dd209497b7b311987cba3e5a069
+size 47154
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-N-0_1_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..3a129abb0b
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-N-0_1_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f3ff35998ce8558b5a7af84e378bee039e36099084b357fffccf329a4983b035
+size 43551
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_ConfirmationDialog_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_ConfirmationDialog_0_null,NEXUS_5,1.0,en].png
index fec2a14e4d..4d9869c5eb 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_ConfirmationDialog_0_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_ConfirmationDialog_0_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:461788974e3bee520d26dbec39b2721c8c2cc0b8b907a7850e8761784abc2792
-size 23763
+oid sha256:1e13461ce6fe7e873fa2796520cf107e6e041559b43c8d911ba78848de34b398
+size 24585
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_ErrorDialog_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_ErrorDialog_0_null,NEXUS_5,1.0,en].png
index 40868aec6e..0146de16cc 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_ErrorDialog_0_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_ErrorDialog_0_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:449a17861383c164774ada4098e8bfd5cb364de211d25b83baeb930e0f40020d
-size 17446
+oid sha256:eebe29b4f5b0aa38316bb54e439f9ab541185251deffbae07ba14342b453c5cb
+size 17528
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_MultipleSelectionDialogContent_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_MultipleSelectionDialogContent_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..d9cb1ead47
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_MultipleSelectionDialogContent_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:76aadb1f17f02efa6b0dbbafec5e5a08a1bfdb08e540b494eeaa48825c7c130a
+size 30155
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_RetryDialog_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_RetryDialog_0_null,NEXUS_5,1.0,en].png
index 84b542ad82..915f6f8539 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_RetryDialog_0_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_RetryDialog_0_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fd3224a9292b9268874f355b681f28ae8a530341fdded66c85e36db9a14818a7
-size 23194
+oid sha256:a19f93d6d31a074b2e5c1d691505881e8424361f1d244366bc6ee5fbd5de15f6
+size 23225
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_SingleSelectionDialogContent_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_SingleSelectionDialogContent_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..98594b6108
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_SingleSelectionDialogContent_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:18f79874b2342ffe0e6cd759f381e452767c6c824069cc04441c49914b2860d8
+size 20757
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_MultipleSelectionDialogContent-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_MultipleSelectionDialogContent-N_1_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..f92655c313
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_MultipleSelectionDialogContent-N_1_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f540b6b0f761949348fbf65596103d482b0f3c29eb8c99750756dce1c40e674e
+size 28692
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_SingleSelectionDialogContent-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_SingleSelectionDialogContent-N_1_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..de25110b80
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_SingleSelectionDialogContent-N_1_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:aa6c275983acd5902a73fecc03a3050f1dbf70c272557bf8240c7bc5ec6c1a45
+size 19809
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_MultipleselectionListitem-noselection_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_MultipleselectionListitem-noselection_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..49e9fd7ed5
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_MultipleselectionListitem-noselection_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:eb1e859e011c8359935b0254472be653d878146a3735196f9edee324a204b1c1
+size 14192
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_MultipleselectionListitem-selectioninsupportingtext_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_MultipleselectionListitem-selectioninsupportingtext_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..dbdcc65b12
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_MultipleselectionListitem-selectioninsupportingtext_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:504265795b72916040c091b767ffd278fe273a83d4f4a1683c06d805b3e3a16d
+size 17818
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_MultipleselectionListitem-selectionintrailingcontent_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_MultipleselectionListitem-selectionintrailingcontent_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..ec20ff6d63
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_MultipleselectionListitem-selectionintrailingcontent_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3a0eb132bfaf1c90c6507c12b25150fb29e260ae3bd0a5760f5fb2e9483fda26
+size 14814
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_SingleselectionListitem-customformatter_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_SingleselectionListitem-customformatter_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..3d0c5e4a60
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_SingleselectionListitem-customformatter_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:624a00c3098b821482dcdb9529db12cfcf46dc82e136d8b255561125e932862b
+size 19467
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_SingleselectionListitem-noselection,supportingtext_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_SingleselectionListitem-noselection,supportingtext_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..49e9fd7ed5
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_SingleselectionListitem-noselection,supportingtext_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:eb1e859e011c8359935b0254472be653d878146a3735196f9edee324a204b1c1
+size 14192
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_SingleselectionListitem-noselection_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_SingleselectionListitem-noselection_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..229ae01f43
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_SingleselectionListitem-noselection_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d86d982afc4add4d3d12574814ec686ad90f20f5098b965560a7a5e413b7f1dd
+size 8441
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_SingleselectionListitem-selectioninsupportingtext_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_SingleselectionListitem-selectioninsupportingtext_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..26c7c0519f
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_SingleselectionListitem-selectioninsupportingtext_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c442cb40b7c6a26d6014005587cd618d213ccb003e8d107faf8d688f069395f3
+size 11844
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_SingleselectionListitem-selectionintrailingcontent_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_SingleselectionListitem-selectionintrailingcontent_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..41bc1be58d
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_SingleselectionListitem-selectionintrailingcontent_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a2ffe9b836ee6dc753f7e6faf10814d7a52611cd1ff9d11a539cc045fdafc1db
+size 17227
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Dialogs_ProgressDialog_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Dialogs_ProgressDialog_0_null,NEXUS_5,1.0,en].png
index e7b5185fbe..fd42ba21ef 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Dialogs_ProgressDialog_0_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Dialogs_ProgressDialog_0_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6695c2d908300730424b28de599a966a3a7ea44f056ac6de745c713915a6a0a5
-size 21745
+oid sha256:ee64fde90f43c95741d8f39e9bf4c770cf542dfaadaf611e98151e9419f1bf9d
+size 22069
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithonlymessageandokbutton_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithonlymessageandokbutton_0_null,NEXUS_5,1.0,en].png
index 14cb782be4..32d5b45a6b 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithonlymessageandokbutton_0_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithonlymessageandokbutton_0_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:033fc99c9948b786db535249490b123f9830b4c0c6b8d29d40188e917b8e1209
-size 51022
+oid sha256:a38a4a04c3527e9d1bb7efc3c4b91f6cfbd4eab93c84d316e68a4fc558e137cd
+size 51533
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithtitle,iconandokbutton_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithtitle,iconandokbutton_0_null,NEXUS_5,1.0,en].png
index e8f354cd2c..30ca4d7948 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithtitle,iconandokbutton_0_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithtitle,iconandokbutton_0_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:61c633685a0afc23e5f896bb5576658a7444b0a0fa7a1aefc4be9fae86fcd3e7
-size 57358
+oid sha256:0182810e251fe8a355d275bbe97a1f7c8f82577b7f70a769a852811f8e9ee715
+size 57958
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithtitleandokbutton_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithtitleandokbutton_0_null,NEXUS_5,1.0,en].png
index 450b1e3057..d090dc2fa8 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithtitleandokbutton_0_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithtitleandokbutton_0_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:92502759287057d36a3fe5532d48e911e24df080c8c1e8eab01c4e406bd95fc8
-size 55792
+oid sha256:9980d0479b405e3b3215628ae92227c02423b7839c2754bb4b230da549e82cbe
+size 56305
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Listsections_Listsupportingtext-largepadding_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Listsections_Listsupportingtext-largepadding_0_null,NEXUS_5,1.0,en].png
index ae1a8f9573..f7b0bd2921 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Listsections_Listsupportingtext-largepadding_0_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Listsections_Listsupportingtext-largepadding_0_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5ba37602f8c2519c1854776ea349fd53fb9fb6fd4b1d94a55d297b297854656b
-size 27584
+oid sha256:3b37e1793e658812995411e954e060e983e48e7d450af28e1577b2b8f6f8e30f
+size 27527
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Listsections_Listsupportingtext-smallpadding_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Listsections_Listsupportingtext-smallpadding_0_null,NEXUS_5,1.0,en].png
index 7b01286f2e..b6851470fc 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Listsections_Listsupportingtext-smallpadding_0_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Listsections_Listsupportingtext-smallpadding_0_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1d2a776e9724abd16b1692f0fdac43e39dedcd8c003128d1ea354f1f8df245ac
-size 26147
+oid sha256:acb1adfada4482915a2b42b86c9b73c5815fe992647e67b065f569b45ae69779
+size 26304
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Toggles_IconToggleButton_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Toggles_IconToggleButton_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..d27488dc29
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Toggles_IconToggleButton_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9f9b51e2b099380112683be07ce85679436fdaa7765cc72c200cbaca1e0b843e
+size 13557