Merge branch 'develop' into feature/bma/dataObject

This commit is contained in:
Benoit Marty 2023-08-28 10:56:02 +02:00 committed by GitHub
commit 55255735d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
129 changed files with 2140 additions and 487 deletions

View file

@ -1,4 +1,4 @@
<!-- Please read [CONTRIBUTING.md](https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
<!-- Please read [CONTRIBUTING.md](https://github.com/vector-im/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
## Type of change
@ -17,13 +17,17 @@
## Screenshots / GIFs
<!-- Only if UI have been changed
<!--
We have screenshot tests in the project, so attaching screenshots to a PR is not mandatory, as far as there
is a Composable Preview covering the changes. In this case, the change will appear in the file diff.
Note that all the UI composables should be covered by a Composable Preview.
Providing a video of the change is still very useful for the reviewer and for the history of the project.
You can use a table like this to show screenshots comparison.
Uncomment this markdown table below and edit the last line `|||`:
|copy screenshot of before here|copy screenshot of after here|
-->
<!--
|Before|After|
|-|-|
|||
@ -47,11 +51,11 @@ Uncomment this markdown table below and edit the last line `|||`:
<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->
- [ ] 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

View file

@ -1,4 +1,4 @@
# Contributing to Element Android
# Contributing to Element X Android
<!--- TOC -->

1
changelog.d/1113.wip Normal file
View file

@ -0,0 +1 @@
[Polls] Improve UI and render ended state

1
changelog.d/1131.bugfix Normal file
View file

@ -0,0 +1 @@
Only display verification prompt after initial sync is done.

1
changelog.d/862.bugfix Normal file
View file

@ -0,0 +1 @@
Videos sent from the app were cropped in some cases.

View file

@ -5,6 +5,6 @@
<string name="screen_analytics_prompt_read_terms">"Du kannst alle unsere Nutzerbedingungen %1$s lesen."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
<string name="screen_analytics_prompt_settings">"Du kannst dies jederzeit deaktivieren"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben "<b>"keine"</b>" Informationen an Dritte weiter"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben deine Daten nicht an Dritte weiter"</string>
<string name="screen_analytics_prompt_title">"Hilf uns, %1$s zu verbessern"</string>
</resources>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_welcome_bullet_1">"Hovory, hlasování, vyhledávání a další budou přidány koncem tohoto roku."</string>
<string name="screen_welcome_bullet_2">"Historie zpráv šifrovaných místností nebude v této aktualizaci k dispozici."</string>
<string name="screen_welcome_bullet_3">"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."</string>
<string name="screen_welcome_button">"Jdeme na to!"</string>
<string name="screen_welcome_subtitle">"Zde je to, co potřebujete vědět:"</string>
<string name="screen_welcome_title">"Vítá vás %1$s!"</string>
</resources>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_welcome_bullet_1">"Anrufe, Standortfreigabe, Suche und mehr werden später in diesem Jahr hinzugefügt."</string>
<string name="screen_welcome_bullet_1">"Anrufe, Umfragen, Suche und mehr werden später in diesem Jahr hinzugefügt."</string>
<string name="screen_welcome_bullet_2">"Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein."</string>
<string name="screen_welcome_bullet_3">"Wir würden uns freuen, wenn du uns über die Einstellungsseite deine Meinung mitteilst."</string>
<string name="screen_welcome_button">"Los geht\'s!"</string>

View file

@ -9,7 +9,7 @@
<string name="screen_account_provider_signin_title">"Chystáte se přihlásit do %s"</string>
<string name="screen_account_provider_signup_subtitle">"Zde budou uloženy vaše konverzace - podobně jako u poskytovatele e-mailových služeb uchováváte své e-maily."</string>
<string name="screen_account_provider_signup_title">"Chystáte se vytvořit účet na %s"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org je otevřená síť pro bezpečnou, decentralizovanou komunikaci."</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org je velký bezplatný server ve veřejné síti Matrix pro bezpečnou decentralizovanou komunikaci, který provozuje nadace Matrix.org."</string>
<string name="screen_change_account_provider_other">"Jiný"</string>
<string name="screen_change_account_provider_subtitle">"Použijte jiného poskytovatele účtu, například vlastní soukromý server nebo pracovní účet."</string>
<string name="screen_change_account_provider_title">"Změnit poskytovatele účtu"</string>

View file

@ -9,7 +9,7 @@
<string name="screen_account_provider_signin_title">"Du bist dabei dich bei %s anzumelden"</string>
<string name="screen_account_provider_signup_subtitle">"Hier werden deine Konversationen stattfinden — genauso wie du einen E-Mail-Anbieter verwenden würdest, um deine E-Mails aufzubewahren."</string>
<string name="screen_account_provider_signup_title">"Du bist dabei ein Konto auf %s zu erstellen"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org ist ein offenes Netzwerk für sichere, dezentralisierte Kommunikation."</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"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."</string>
<string name="screen_change_account_provider_other">"Andere"</string>
<string name="screen_change_account_provider_subtitle">"Verwende einen anderen Kontoanbieter, z. B. deinen eigenen privaten Server oder ein Arbeitskonto."</string>
<string name="screen_change_account_provider_title">"Kontoanbieter ändern"</string>

View file

@ -9,7 +9,6 @@
<string name="screen_account_provider_signin_title">"Vous êtes sur le point de vous connecter à %s"</string>
<string name="screen_account_provider_signup_subtitle">"C\'est ici que vos conversations seront stockées - tout comme vous utiliseriez un fournisseur de messagerie pour conserver vos e-mails."</string>
<string name="screen_account_provider_signup_title">"Vous êtes sur le point de créer un compte sur %s"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org est un réseau ouvert pour des communications sécurisées et décentralisées."</string>
<string name="screen_change_account_provider_other">"Autre"</string>
<string name="screen_change_account_provider_subtitle">"Utilisez un autre fournisseur de compte, tel que votre propre serveur ou un compte professionnel."</string>
<string name="screen_change_account_provider_title">"Changer de fournisseur"</string>

View file

@ -9,7 +9,6 @@
<string name="screen_account_provider_signin_title">"Sunteți pe cale să vă conectați la %s"</string>
<string name="screen_account_provider_signup_subtitle">"Aici vor trăi conversațiile - la fel cum ați folosi un furnizor de e-mail pentru a vă păstra e-mailurile."</string>
<string name="screen_account_provider_signup_title">"Sunteți pe cale să creați un cont pe %s"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org este o rețea deschisă pentru o comunicare sigură și descentralizată."</string>
<string name="screen_change_account_provider_other">"Altul"</string>
<string name="screen_change_account_provider_subtitle">"Utilizați un alt furnizor de cont, cum ar fi propriul server privat sau un cont de serviciu."</string>
<string name="screen_change_account_provider_title">"Schimbați furnizorul contului"</string>

View file

@ -9,7 +9,6 @@
<string name="screen_account_provider_signin_title">"Вы собираетесь войти в %s"</string>
<string name="screen_account_provider_signup_subtitle">"Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."</string>
<string name="screen_account_provider_signup_title">"Вы собираетесь создать учетную запись на %s"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org — это открытая сеть для безопасной децентрализованной связи."</string>
<string name="screen_change_account_provider_other">"Другое"</string>
<string name="screen_change_account_provider_subtitle">"Используйте другого поставщика учетных записей, например, собственный частный сервер или рабочую учетную запись."</string>
<string name="screen_change_account_provider_title">"Сменить поставщика учетной записи"</string>

View file

@ -9,7 +9,6 @@
<string name="screen_account_provider_signin_title">"Chystáte sa prihlásiť do %s"</string>
<string name="screen_account_provider_signup_subtitle">"Tu budú žiť vaše konverzácie — podobne ako používate poskytovateľa e-mailových služieb na uchovávanie e-mailov."</string>
<string name="screen_account_provider_signup_title">"Chystáte sa vytvoriť účet na %s"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu."</string>
<string name="screen_change_account_provider_other">"Iný"</string>
<string name="screen_change_account_provider_subtitle">"Použite iného poskytovateľa účtu, ako napríklad vlastný súkromný server alebo pracovný účet."</string>
<string name="screen_change_account_provider_title">"Zmeniť poskytovateľa účtu"</string>

View file

@ -9,7 +9,7 @@
<string name="screen_account_provider_signin_title">"Youre about to sign in to %s"</string>
<string name="screen_account_provider_signup_subtitle">"This is where your conversations will live — just like you would use an email provider to keep your emails."</string>
<string name="screen_account_provider_signup_title">"Youre about to create an account on %s"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org is an open network for secure, decentralized communication."</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org is a large, free server on the public Matrix network for secure, decentralised communication, run by the Matrix.org Foundation."</string>
<string name="screen_change_account_provider_other">"Other"</string>
<string name="screen_change_account_provider_subtitle">"Use a different account provider, such as your own private server or a work account."</string>
<string name="screen_change_account_provider_title">"Change account provider"</string>

View file

@ -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 = {},
)
}

View file

@ -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
}
}

View file

@ -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,
)

View file

@ -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,
)
}
}

View file

@ -25,7 +25,7 @@ data class TimelineItemPollContent(
val answerItems: List<PollAnswerItem>,
val votes: Map<String, List<UserId>>,
val pollKind: PollKind,
val isDisclosed: Boolean,
val isEnded: Boolean,
) : TimelineItemEventContent {
override val type: String = "TimelineItemPollContent"
}

View file

@ -24,16 +24,16 @@ open class TimelineItemPollContentProvider : PreviewParameterProvider<TimelineIt
override val values: Sequence<TimelineItemPollContent>
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(),
)
}

View file

@ -5,19 +5,43 @@
<item quantity="few">"%1$d změny místnosti"</item>
<item quantity="other">"%1$d změn místnosti"</item>
</plurals>
<plurals name="screen_room_timeline_more_reactions">
<item quantity="one">"%1$d další"</item>
<item quantity="few">"%1$d další"</item>
<item quantity="other">"%1$d dalších"</item>
</plurals>
<string name="screen_room_attachment_source_camera">"Fotoaparát"</string>
<string name="screen_room_attachment_source_camera_photo">"Vyfotit"</string>
<string name="screen_room_attachment_source_camera_video">"Natočit video"</string>
<string name="screen_room_attachment_source_files">"Příloha"</string>
<string name="screen_room_attachment_source_gallery">"Knihovna fotografií a videí"</string>
<string name="screen_room_attachment_source_location">"Poloha"</string>
<string name="screen_room_attachment_source_poll">"Hlasování"</string>
<string name="screen_room_encrypted_history_banner">"Historie zpráv je momentálně v této místnosti nedostupná"</string>
<string name="screen_room_error_failed_retrieving_user_details">"Nepodařilo se načíst údaje o uživateli"</string>
<string name="screen_room_invite_again_alert_message">"Chtěli byste je pozvat zpět?"</string>
<string name="screen_room_invite_again_alert_title">"V tomto chatu jste sami"</string>
<string name="screen_room_message_copied">"Zpráva zkopírována"</string>
<string name="screen_room_no_permission_to_post">"Nemáte oprávnění zveřejňovat příspěvky v této místnosti"</string>
<string name="screen_room_notification_settings_allow_custom">"Povolit vlastní nastavení"</string>
<string name="screen_room_notification_settings_allow_custom_footnote">"Zapnutím této funkce přepíšete výchozí nastavení"</string>
<string name="screen_room_notification_settings_custom_settings_title">"Upozornit mě v tomto chatu na"</string>
<string name="screen_room_notification_settings_default_setting_footnote">"Můžete změnit ve vašem %1$s."</string>
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"globální nastavení"</string>
<string name="screen_room_notification_settings_default_setting_title">"Výchozí nastavení"</string>
<string name="screen_room_notification_settings_edit_remove_setting">"Odebrat vlastní nastavení"</string>
<string name="screen_room_notification_settings_error_loading_settings">"Při načítání nastavení oznámení došlo k chybě."</string>
<string name="screen_room_notification_settings_error_restoring_default">"Obnovení výchozího režimu se nezdařilo, zkuste to prosím znovu."</string>
<string name="screen_room_notification_settings_error_setting_mode">"Nastavení režimu se nezdařilo, zkuste to prosím znovu."</string>
<string name="screen_room_notification_settings_mode_all_messages">"Všechny zprávy"</string>
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Pouze zmínky a klíčová slova"</string>
<string name="screen_room_notification_settings_room_custom_settings_title">"V této místnosti mě upozornit na"</string>
<string name="screen_room_reactions_show_less">"Zobrazit méně"</string>
<string name="screen_room_reactions_show_more">"Zobrazit více"</string>
<string name="screen_room_retry_send_menu_send_again_action">"Odeslat znovu"</string>
<string name="screen_room_retry_send_menu_title">"Vaši zprávu se nepodařilo odeslat"</string>
<string name="screen_room_timeline_add_reaction">"Přidat emoji"</string>
<string name="screen_room_timeline_less_reactions">"Zobrazit méně"</string>
<string name="screen_room_error_failed_processing_media">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
<string name="screen_room_retry_send_menu_remove_action">"Odstranit"</string>
</resources>

View file

@ -10,16 +10,32 @@
<string name="screen_room_attachment_source_files">"Anhang"</string>
<string name="screen_room_attachment_source_gallery">"Foto- &amp; Video-Bibliothek"</string>
<string name="screen_room_attachment_source_location">"Standort"</string>
<string name="screen_room_attachment_source_poll">"Umfrage"</string>
<string name="screen_room_encrypted_history_banner">"Der Nachrichtenverlauf ist in diesem Raum derzeit nicht verfügbar"</string>
<string name="screen_room_error_failed_retrieving_user_details">"Benutzerdetails konnten nicht abgerufen werden"</string>
<string name="screen_room_invite_again_alert_message">"Möchtest du sie wieder einladen?"</string>
<string name="screen_room_invite_again_alert_title">"Du bist allein in diesem Chat"</string>
<string name="screen_room_message_copied">"Nachricht kopiert"</string>
<string name="screen_room_no_permission_to_post">"Du bist keine Berechtigung, um in diesem Raum zu posten"</string>
<string name="screen_room_notification_settings_allow_custom">"Benutzerdefinierte Einstellung zulassen"</string>
<string name="screen_room_notification_settings_allow_custom_footnote">"Das Aktivieren dieser Option wird die Standardeinstellungen überschreiben."</string>
<string name="screen_room_notification_settings_custom_settings_title">"Benachrichtige mich in diesem Chat für"</string>
<string name="screen_room_notification_settings_default_setting_footnote">"Du kannst es in deinem %1$s ändern."</string>
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"Globale Einstellungen"</string>
<string name="screen_room_notification_settings_default_setting_title">"Standardeinstellung"</string>
<string name="screen_room_notification_settings_edit_remove_setting">"Benutzerdefinierte Einstellung entfernen"</string>
<string name="screen_room_notification_settings_error_loading_settings">"Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten."</string>
<string name="screen_room_notification_settings_error_restoring_default">"Wiederherstellung des Standardmodus fehlgeschlagen. Bitte versuche es erneut."</string>
<string name="screen_room_notification_settings_error_setting_mode">"Fehler beim Einstellen des Modus. Bitte versuche es erneut."</string>
<string name="screen_room_notification_settings_mode_all_messages">"Alle Nachrichten"</string>
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Nur Erwähnungen und Schlüsselwörter"</string>
<string name="screen_room_notification_settings_room_custom_settings_title">"In diesem Raum, benachrichtige mich für"</string>
<string name="screen_room_reactions_show_less">"Weniger anzeigen"</string>
<string name="screen_room_reactions_show_more">"Mehr anzeigen"</string>
<string name="screen_room_retry_send_menu_send_again_action">"Erneut senden"</string>
<string name="screen_room_retry_send_menu_title">"Ihre Nachricht konnte nicht gesendet werden"</string>
<string name="screen_room_timeline_add_reaction">"Emoji hinzufügen"</string>
<string name="screen_room_timeline_less_reactions">"Weniger anzeigen"</string>
<string name="screen_room_error_failed_processing_media">"Fehler bei der Verarbeitung von Medien zum Hochladen, bitte versuche es erneut."</string>
<string name="screen_room_retry_send_menu_remove_action">"Entfernen"</string>
</resources>

View file

@ -5,6 +5,11 @@
<item quantity="few">"%1$d изменения в комнате"</item>
<item quantity="many">"%1$d изменений в комнате"</item>
</plurals>
<plurals name="screen_room_timeline_more_reactions">
<item quantity="one">"И ещё %1$d"</item>
<item quantity="few">"И ещё %1$d"</item>
<item quantity="many">"И ещё %1$d"</item>
</plurals>
<string name="screen_room_attachment_source_camera">"Камера"</string>
<string name="screen_room_attachment_source_camera_photo">"Сделать фото"</string>
<string name="screen_room_attachment_source_camera_video">"Записать видео"</string>
@ -24,11 +29,13 @@
<string name="screen_room_notification_settings_default_setting_footnote">"Вы можете изменить его в своем %1$s."</string>
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"Основные Настройки"</string>
<string name="screen_room_notification_settings_default_setting_title">"Настройка по умолчанию"</string>
<string name="screen_room_notification_settings_edit_remove_setting">"Удалить пользовательскую настройку"</string>
<string name="screen_room_notification_settings_error_loading_settings">"Произошла ошибка при загрузке настроек уведомлений."</string>
<string name="screen_room_notification_settings_error_restoring_default">"Не удалось восстановить режим по умолчанию, попробуйте еще раз."</string>
<string name="screen_room_notification_settings_error_setting_mode">"Не удалось настроить режим, попробуйте еще раз."</string>
<string name="screen_room_notification_settings_mode_all_messages">"Все сообщения"</string>
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Только упоминания и ключевые слова"</string>
<string name="screen_room_notification_settings_room_custom_settings_title">"В этой комнате уведомить меня о"</string>
<string name="screen_room_reactions_show_less">"Показать меньше"</string>
<string name="screen_room_reactions_show_more">"Показать больше"</string>
<string name="screen_room_retry_send_menu_send_again_action">"Отправить снова"</string>

View file

@ -16,6 +16,7 @@
<string name="screen_room_attachment_source_files">"Príloha"</string>
<string name="screen_room_attachment_source_gallery">"Knižnica fotografií a videí"</string>
<string name="screen_room_attachment_source_location">"Poloha"</string>
<string name="screen_room_attachment_source_poll">"Anketa"</string>
<string name="screen_room_encrypted_history_banner">"História správ v tejto miestnosti nie je momentálne k dispozícii"</string>
<string name="screen_room_error_failed_retrieving_user_details">"Nepodarilo sa získať údaje o používateľovi"</string>
<string name="screen_room_invite_again_alert_message">"Chceli by ste ich pozvať späť?"</string>
@ -28,11 +29,13 @@
<string name="screen_room_notification_settings_default_setting_footnote">"Môžete to zmeniť vo svojich %1$s."</string>
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"všeobecných nastaveniach"</string>
<string name="screen_room_notification_settings_default_setting_title">"Predvolené nastavenie"</string>
<string name="screen_room_notification_settings_edit_remove_setting">"Odstrániť vlastné nastavenie"</string>
<string name="screen_room_notification_settings_error_loading_settings">"Pri načítavaní nastavení oznámení došlo k chybe."</string>
<string name="screen_room_notification_settings_error_restoring_default">"Nepodarilo sa obnoviť predvolený režim, skúste to prosím znova."</string>
<string name="screen_room_notification_settings_error_setting_mode">"Nepodarilo sa nastaviť režim, skúste to prosím znova."</string>
<string name="screen_room_notification_settings_mode_all_messages">"Všetky správy"</string>
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Iba zmienky a kľúčové slová"</string>
<string name="screen_room_notification_settings_room_custom_settings_title">"V tejto miestnosti ma upozorniť na"</string>
<string name="screen_room_reactions_show_less">"Zobraziť menej"</string>
<string name="screen_room_reactions_show_more">"Zobraziť viac"</string>
<string name="screen_room_retry_send_menu_send_again_action">"Odoslať znova"</string>

View file

@ -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)

View file

@ -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<PollAnswerItem>,
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 = { },
)
}

View file

@ -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,
)

View file

@ -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 = { }
)
}

View file

@ -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
)

View file

@ -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<PollAnswerItem>,
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<PollAnswerItem>,
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<PollAnswerItem>,
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 = { },
)
}

View file

@ -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<String?> = 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,

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_bug_report_attach_screenshot">"Připojit snímek obrazovky"</string>
<string name="screen_bug_report_contact_me">"V případě dalších dotazů se na mě můžete obrátit"</string>
<string name="screen_bug_report_contact_me">"V případě dalších dotazů se na mě můžete obrátit."</string>
<string name="screen_bug_report_contact_me_title">"Kontaktujte mě"</string>
<string name="screen_bug_report_edit_screenshot">"Upravit snímek obrazovky"</string>
<string name="screen_bug_report_editor_description">"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í."</string>

View file

@ -10,6 +10,6 @@
<string name="screen_bug_report_include_crash_logs">"Absturzprotokolle senden"</string>
<string name="screen_bug_report_include_logs">"Logs zulassen"</string>
<string name="screen_bug_report_include_screenshot">"Bildschirmfoto senden"</string>
<string name="screen_bug_report_logs_description">"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."</string>
<string name="screen_bug_report_logs_description">"Deiner Nachricht werden Protokolle beigefügt, um sicherzustellen, dass alles ordnungsgemäß funktioniert. Um deine Nachricht ohne Logs zu senden, deaktiviere diese Einstellung."</string>
<string name="screen_bug_report_rash_logs_alert_title">"%1$s ist bei der letzten Verwendung abgestürzt. Möchtest du uns einen Absturzbericht senden?"</string>
</resources>

View file

@ -13,7 +13,12 @@
<string name="screen_room_details_edition_error_title">"Nelze aktualizovat místnost"</string>
<string name="screen_room_details_encryption_enabled_subtitle">"Zprávy jsou zabezpečeny zámky. Pouze vy a příjemci máte jedinečné klíče k jejich odemčení."</string>
<string name="screen_room_details_encryption_enabled_title">"Šifrování zpráv povoleno"</string>
<string name="screen_room_details_error_loading_notification_settings">"Při načítání nastavení oznámení došlo k chybě."</string>
<string name="screen_room_details_error_muting">"Ztišení této místnosti se nezdařilo, zkuste to prosím znovu."</string>
<string name="screen_room_details_error_unmuting">"Nepodařilo se zrušit ztišení této místnosti, zkuste to prosím znovu."</string>
<string name="screen_room_details_invite_people_title">"Pozvat lidi"</string>
<string name="screen_room_details_notification_mode_custom">"Vlastní"</string>
<string name="screen_room_details_notification_mode_default">"Výchozí"</string>
<string name="screen_room_details_notification_title">"Oznámení"</string>
<string name="screen_room_details_room_name_label">"Název místnosti"</string>
<string name="screen_room_details_share_room_title">"Sdílet místnost"</string>

View file

@ -1,18 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="screen_room_member_list_header_title">
<item quantity="one">"1 Person"</item>
<item quantity="one">"%1$d Person"</item>
<item quantity="other">"%1$d Personen"</item>
</plurals>
<string name="screen_room_details_add_topic_title">"Thema hinzufügen"</string>
<string name="screen_room_details_already_a_member">"Bereits Mitglied"</string>
<string name="screen_room_details_already_invited">"Bereits eingeladen"</string>
<string name="screen_room_details_edit_room_title">"Raum bearbeiten"</string>
<string name="screen_room_details_edition_error">"Wir konnten nicht alle Informationen für diesen Raum aktualisieren."</string>
<string name="screen_room_details_edition_error">"Es gab einen unbekannten Fehler und die Informationen konnten nicht geändert werden."</string>
<string name="screen_room_details_edition_error_title">"Raum konnte nicht aktualisiert werden"</string>
<string name="screen_room_details_encryption_enabled_subtitle">"Nachrichten sind mit Schlössern gesichert. Nur du und der Empfänger haben die eindeutigen Schlüssel, um sie zu entsperren."</string>
<string name="screen_room_details_encryption_enabled_title">"Nachrichtenverschlüsselung aktiviert"</string>
<string name="screen_room_details_error_loading_notification_settings">"Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten."</string>
<string name="screen_room_details_error_muting">"Das Stummschalten dieses Raums ist fehlgeschlagen. Bitte versuche es erneut."</string>
<string name="screen_room_details_error_unmuting">"Die Stummschaltung dieses Raums konnte nicht aufgehoben werden. Bitte versuchen Sie es erneut."</string>
<string name="screen_room_details_invite_people_title">"Personen einladen"</string>
<string name="screen_room_details_notification_mode_custom">"Benutzerdefiniert"</string>
<string name="screen_room_details_notification_mode_default">"Standard"</string>
<string name="screen_room_details_notification_title">"Benachrichtigungen"</string>
<string name="screen_room_details_room_name_label">"Raumname"</string>
<string name="screen_room_details_share_room_title">"Raum teilen"</string>
@ -20,10 +25,10 @@
<string name="screen_room_member_list_pending_header_title">"Ausstehend"</string>
<string name="screen_room_member_list_room_members_header_title">"Raummitglieder"</string>
<string name="screen_dm_details_block_alert_action">"Blockieren"</string>
<string name="screen_dm_details_block_alert_description">"Blockierte Benutzer können dir keine Nachrichten senden und alle Nachrichten von ihnen werden ausgeblendet. Du kannst diese Aktion jederzeit rückgängig machen."</string>
<string name="screen_dm_details_block_alert_description">"Blockierte Benutzer können dir keine Nachrichten senden und alle ihre Nachrichten werden ausgeblendet. Du kannst sie jederzeit entsperren."</string>
<string name="screen_dm_details_block_user">"Nutzer blockieren"</string>
<string name="screen_dm_details_unblock_alert_action">"Blockierung aufheben"</string>
<string name="screen_dm_details_unblock_alert_description">"Wenn du den Benutzer entsperrst, kannst du wieder alle Nachrichten von ihm sehen."</string>
<string name="screen_dm_details_unblock_alert_description">"Du wirst alle ihre Nachrichten wieder sehen."</string>
<string name="screen_dm_details_unblock_user">"Nutzer entblockieren"</string>
<string name="screen_room_details_leave_room_title">"Raum verlassen"</string>
<string name="screen_room_details_people_title">"Personen"</string>

View file

@ -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) }

View file

@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_roomlist_a11y_create_message">"Vytvořte novou konverzaci nebo místnost"</string>
<string name="screen_roomlist_empty_message">"Začněte tím, že někomu pošnete zprávu."</string>
<string name="screen_roomlist_empty_title">"Zatím žádné konverzace."</string>
<string name="screen_roomlist_main_space_title">"Všechny chaty"</string>
<string name="session_verification_banner_message">"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."</string>
<string name="session_verification_banner_title">"Přístup k historii zpráv"</string>
<string name="session_verification_banner_title">"Ověřte, že jste to vy"</string>
</resources>

View file

@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_roomlist_a11y_create_message">"Ein neues Gespräch oder einen neuen Raum erstellen"</string>
<string name="screen_roomlist_empty_message">"Beginnen, indem du jemandem eine Nachricht sendest."</string>
<string name="screen_roomlist_empty_title">"Noch keine Chats."</string>
<string name="screen_roomlist_main_space_title">"Alle Chats"</string>
<string name="session_verification_banner_message">"Es sieht so aus, als ob du ein neues Gerät verwendest. Verifiziere, dass du es bist, um auf deine verschlüsselten Nachrichten zuzugreifen."</string>
<string name="session_verification_banner_title">"Greife auf deine Nachrichten-Historie zu"</string>
<string name="session_verification_banner_title">"Verifiziere, dass du es bist"</string>
</resources>

View file

@ -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,

View file

@ -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"

View file

@ -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<ListOption> {
return values.map { ListOption(it) }.toImmutableList()
}

View file

@ -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<ListOption>,
onConfirmClicked: (List<Int>) -> 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<Int> = 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<ListOption>,
confirmButtonTitle: String,
onConfirmClicked: (List<Int>) -> Unit,
dismissButtonTitle: String,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
title: String? = null,
initialSelected: ImmutableList<Int> = 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),
)
}
}
}

View file

@ -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<ListOption>,
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<ListOption>,
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
)
}
}
}

View file

@ -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) },
)
}

View file

@ -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)

View file

@ -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<ListOption>,
onSelectionChanged: (List<Int>) -> Unit,
resultFormatter: (List<Int>) -> String?,
modifier: Modifier = Modifier,
supportingText: String? = null,
leadingContent: ListItemContent? = null,
selected: ImmutableList<Int> = 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<Int>(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<Int>(0, 2)
MultipleSelectionListItem(
headline = "Headline",
options = options,
onSelectionChanged = {},
supportingText = "Supporting text",
resultFormatter = { selected.size.toString() },
displayResultInTrailingContent = true,
selected = selected,
)
}
}
private fun formatResult(result: List<Int>, options: ImmutableList<ListOption>): String? {
return options.mapIndexedNotNull { index, value -> value.title.takeIf { result.contains(index) } }.joinToString(", ").takeIf { it.isNotEmpty() }
}

View file

@ -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,
)
}

View file

@ -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<ListOption>,
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,
)
}
}

View file

@ -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) },
)
}

View file

@ -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)

View file

@ -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() =

View file

@ -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)
}
}
}

View file

@ -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))
}

View file

@ -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,

View file

@ -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 })
}
}
}

View file

@ -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,

View file

@ -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,

View file

@ -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)
}

View file

@ -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

View file

@ -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<Unit>
/**
* 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<String>,
maxSelections: Int,
pollKind: PollKind,
): Result<Unit>
override fun close() = destroy()
}

View file

@ -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<SessionVerifiedStatus>
/**
* Returns whether the current session needs to be verified and the SDK is ready to start the verification.
*/
val canVerifySessionFlow: Flow<Boolean>
/**
* Request verification of the current session.
*/

View file

@ -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) {

View file

@ -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
}

View file

@ -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<String>,
maxSelections: Int,
pollKind: PollKind,
): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.createPoll(
question = question,
answers = answers,
maxSelections = maxSelections.toUByte(),
pollKind = pollKind.toInner(),
txnId = genTransactionId(),
)
}
}

View file

@ -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>(SessionVerifiedStatus.Unknown)
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus.asStateFlow()
override val canVerifySessionFlow = combine(sessionVerifiedStatus, syncService.syncState) { verificationStatus, syncState ->
syncState == SyncState.Running && verificationStatus == SessionVerifiedStatus.NotVerified
}
override suspend fun requestVerification() = tryOrFail {
verificationController?.requestVerification()
}

View file

@ -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<Pair<Long, Long>>()
val editMessageCalls = mutableListOf<String>()
@ -104,6 +106,9 @@ class FakeMatrixRoom(
private val _sentLocations = mutableListOf<SendLocationInvocation>()
val sentLocations: List<SendLocationInvocation> = _sentLocations
private val _createPollInvocations = mutableListOf<CreatePollInvocation>()
val createPollInvocations: List<CreatePollInvocation> = _createPollInvocations
var invitedUserId: UserId? = null
private set
@ -305,6 +310,16 @@ class FakeMatrixRoom(
return sendLocationResult
}
override suspend fun createPoll(
question: String,
answers: List<String>,
maxSelections: Int,
pollKind: PollKind
): Result<Unit> = 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<Unit>) {
createPollResult = result
}
fun givenProgressCallbackValues(values: List<Pair<Long, Long>>) {
progressCallbackValues = values
}
@ -409,3 +428,10 @@ data class SendLocationInvocation(
val zoomLevel: Int?,
val assetType: AssetType?,
)
data class CreatePollInvocation(
val question: String,
val answers: List<String>,
val maxSelections: Int,
val pollKind: PollKind,
)

View file

@ -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>(SessionVerifiedStatus.Unknown)
private var _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
private var _canVerifySessionFlow = MutableStateFlow(true)
private var emojiList = emptyList<VerificationEmoji>()
var shouldFail = false
override val verificationFlowState: StateFlow<VerificationFlowState>
get() = _verificationFlowState
override val verificationFlowState: StateFlow<VerificationFlowState> =_verificationFlowState
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus
override val canVerifySessionFlow: Flow<Boolean> = _canVerifySessionFlow
override val isReady: StateFlow<Boolean> = _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
}

View file

@ -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) {

View file

@ -7,7 +7,7 @@
<string name="notification_inline_reply_failed">"** Senden fehlgeschlagen - bitte Raum öffnen"</string>
<string name="notification_invitation_action_join">"Beitreten"</string>
<string name="notification_invitation_action_reject">"Ablehnen"</string>
<string name="notification_invite_body">"Hat dich eingeladen"</string>
<string name="notification_invite_body">"Hat dich zum Chatten eingeladen"</string>
<string name="notification_new_messages">"Neue Nachrichten"</string>
<string name="notification_reaction_body">"Reagierte mit %1$s"</string>
<string name="notification_room_action_mark_as_read">"Als gelesen markieren"</string>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="rich_text_editor_a11y_add_attachment">"Přidat přílohu"</string>
<string name="rich_text_editor_bullet_list">"Přepnout seznam s odrážkami"</string>
<string name="rich_text_editor_code_block">"Přepnout blok kódu"</string>
<string name="rich_text_editor_composer_placeholder">"Zpráva…"</string>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="rich_text_editor_a11y_add_attachment">"Anhang hinzufügen"</string>
<string name="rich_text_editor_bullet_list">"Aufzählungsliste ein-/ausschalten"</string>
<string name="rich_text_editor_code_block">"Codeblock umschalten"</string>
<string name="rich_text_editor_composer_placeholder">"Nachricht…"</string>

View file

@ -24,7 +24,7 @@
<string name="action_edit">"Upravit"</string>
<string name="action_enable">"Povolit"</string>
<string name="action_forgot_password">"Zapomněli jste heslo?"</string>
<string name="action_forward">"Vpřed"</string>
<string name="action_forward">"Přeposlat"</string>
<string name="action_invite">"Pozvat"</string>
<string name="action_invite_friends">"Pozvat přátele"</string>
<string name="action_invite_friends_to_app">"Pozvat přátele do %1$s"</string>
@ -40,6 +40,7 @@
<string name="action_open_with">"Otevřít v aplikaci"</string>
<string name="action_quick_reply">"Rychlá odpověď"</string>
<string name="action_quote">"Citovat"</string>
<string name="action_react">"Reagovat"</string>
<string name="action_remove">"Odstranit"</string>
<string name="action_reply">"Odpovědět"</string>
<string name="action_report_bug">"Nahlásit chybu"</string>
@ -94,6 +95,9 @@
<string name="common_password">"Heslo"</string>
<string name="common_people">"Lidé"</string>
<string name="common_permalink">"Trvalý odkaz"</string>
<string name="common_poll_final_votes">"Konečné hlasy: %1$s"</string>
<string name="common_poll_total_votes">"Celkový počet hlasů: %1$s"</string>
<string name="common_poll_undisclosed_text">"Výsledky se zobrazí po skončení hlasování"</string>
<string name="common_privacy_policy">"Zásady ochrany osobních údajů"</string>
<string name="common_reactions">"Reakce"</string>
<string name="common_refreshing">"Obnovování…"</string>
@ -140,7 +144,11 @@
<string name="emoji_picker_category_places">"Cestování a místa"</string>
<string name="emoji_picker_category_symbols">"Symboly"</string>
<string name="error_failed_creating_the_permalink">"Vytvoření trvalého odkazu se nezdařilo"</string>
<string name="error_failed_loading_map">"%1$s nemohl načíst mapu. Zkuste to prosím později."</string>
<string name="error_failed_loading_messages">"Načítání zpráv se nezdařilo"</string>
<string name="error_failed_locating_user">"%1$s nemá přístup k vaší poloze. Zkuste to prosím později."</string>
<string name="error_missing_location_auth_android">"%1$s nemá oprávnění k přístupu k vaší poloze. Přístup můžete povolit v Nastavení."</string>
<string name="error_missing_location_rationale_android">"%1$s nemá oprávnění k přístupu k vaší poloze. Povolit přístup níže."</string>
<string name="error_some_messages_have_not_been_sent">"Některé zprávy nebyly odeslány"</string>
<string name="error_unknown">"Omlouváme se, došlo k chybě"</string>
<string name="invite_friends_rich_title">"🔐️ Připojte se ke mně na %1$s"</string>
@ -154,6 +162,11 @@
<item quantity="few">"%1$d členové"</item>
<item quantity="other">"%1$d členů"</item>
</plurals>
<plurals name="common_poll_votes_count">
<item quantity="one">"%d hlas"</item>
<item quantity="few">"%d hlasy"</item>
<item quantity="other">"%d hlasů"</item>
</plurals>
<string name="preference_rageshake">"Zatřeste zařízením pro nahlášení chyby"</string>
<string name="rageshake_dialog_content">"Zdá se, že jste frustrovaně třásli telefonem. Chcete otevřít obrazovku pro nahlášení chyby?"</string>
<string name="report_content_explanation">"Tato zpráva bude nahlášena správci vašeho domovského serveru. Nebude si moci přečíst žádné šifrované zprávy."</string>
@ -167,7 +180,35 @@
<string name="screen_media_upload_preview_error_failed_sending">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
<string name="screen_migration_message">"Toto je jednorázový proces, děkujeme za čekání."</string>
<string name="screen_migration_title">"Nastavení vašeho účtu"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Další nastavení"</string>
<string name="screen_notification_settings_calls_label">"Halsové a video hovory"</string>
<string name="screen_notification_settings_configuration_mismatch">"Neshoda konfigurace"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"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."</string>
<string name="screen_notification_settings_direct_chats">"Přímé zprávy"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Vlastní nastavení pro chat"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Při aktualizaci nastavení oznámení došlo k chybě."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Všechny zprávy"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Pouze zmínky a klíčová slova"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"V přímých zprávách mě upozornit na"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Ve skupinových chatech mě upozornit na"</string>
<string name="screen_notification_settings_enable_notifications">"Povolit oznámení na tomto zařízení"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Konfigurace nebyla opravena, zkuste to prosím znovu."</string>
<string name="screen_notification_settings_group_chats">"Skupinové chaty"</string>
<string name="screen_notification_settings_mentions_section_title">"Zmínky"</string>
<string name="screen_notification_settings_mode_all">"Vše"</string>
<string name="screen_notification_settings_mode_mentions">"Zmínky"</string>
<string name="screen_notification_settings_notification_section_title">"Upozornit mě na"</string>
<string name="screen_notification_settings_room_mention_label">"Upozornit mě na @room"</string>
<string name="screen_notification_settings_system_notifications_action_required">"Chcete-li dostávat oznámení, změňte prosím svůj %1$s."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"systémová nastavení"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Systémová oznámení byla vypnuta"</string>
<string name="screen_notification_settings_title">"Oznámení"</string>
<string name="screen_report_content_block_user_hint">"Zaškrtněte, pokud chcete skrýt všechny aktuální a budoucí zprávy od tohoto uživatele"</string>
<string name="screen_settings_oidc_account">"Účet a zařízení"</string>
<string name="screen_share_location_title">"Sdílet polohu"</string>
<string name="screen_share_my_location_action">"Sdílet moji polohu"</string>
<string name="screen_share_open_apple_maps">"Otevřít v Mapách Apple"</string>

View file

@ -8,7 +8,7 @@
<string name="action_back">"Zurück"</string>
<string name="action_cancel">"Abbrechen"</string>
<string name="action_choose_photo">"Foto auswählen"</string>
<string name="action_clear">"Löschen"</string>
<string name="action_clear">"Zurücksetzen"</string>
<string name="action_close">"Schließen"</string>
<string name="action_complete_verification">"Verifizierung abschließen"</string>
<string name="action_confirm">"Bestätigen"</string>
@ -37,9 +37,10 @@
<string name="action_no">"Nein"</string>
<string name="action_not_now">"Nicht jetzt"</string>
<string name="action_ok">"OK"</string>
<string name="action_open_with">"Öffne mit"</string>
<string name="action_open_with">"Öffnen mit"</string>
<string name="action_quick_reply">"Schnellantwort"</string>
<string name="action_quote">"Zitieren"</string>
<string name="action_react">"Reagieren"</string>
<string name="action_remove">"Entfernen"</string>
<string name="action_reply">"Antworten"</string>
<string name="action_report_bug">"Fehler melden"</string>
@ -56,7 +57,7 @@
<string name="action_start">"Starten"</string>
<string name="action_start_chat">"Chat starten"</string>
<string name="action_start_verification">"Verifizierung starten"</string>
<string name="action_static_map_load">"Tippe, um die Karte zu laden"</string>
<string name="action_static_map_load">"Zum Karte laden tippen"</string>
<string name="action_take_photo">"Foto aufnehmen"</string>
<string name="action_view_source">"Quelltext anzeigen"</string>
<string name="action_yes">"Ja"</string>
@ -80,25 +81,28 @@
<string name="common_forward_message">"Nachricht weiterleiten"</string>
<string name="common_gif">"GIF"</string>
<string name="common_image">"Bild"</string>
<string name="common_invite_unknown_profile">"Wir können die Matrix-ID dieses Benutzers nicht validieren. Die Einladung wurde möglicherweise nicht empfangen."</string>
<string name="common_leaving_room">"Raum verlassen"</string>
<string name="common_invite_unknown_profile">"Diese Matrix-ID kann nicht gefunden werden, daher wird die Einladung möglicherweise nicht empfangen."</string>
<string name="common_leaving_room">"Verlasse Raum"</string>
<string name="common_link_copied_to_clipboard">"Link in Zwischenablage kopiert"</string>
<string name="common_loading">"Wird geladen…"</string>
<string name="common_loading">"Lädt…"</string>
<string name="common_message">"Nachricht"</string>
<string name="common_message_layout">"Nachrichtenlayout"</string>
<string name="common_message_removed">"Nachricht wurde entfernt"</string>
<string name="common_message_removed">"Nachricht entfernt"</string>
<string name="common_modern">"Modern"</string>
<string name="common_mute">"Stummschalten"</string>
<string name="common_no_results">"Keine Ergebnisse"</string>
<string name="common_offline">"Offline"</string>
<string name="common_password">"Passwort"</string>
<string name="common_people">"Personen"</string>
<string name="common_permalink">"Permalink"</string>
<string name="common_permalink">"Dauerlink"</string>
<string name="common_poll_final_votes">"Endgültige Stimmen: %1$s"</string>
<string name="common_poll_total_votes">"Stimmen insgesamt: %1$s"</string>
<string name="common_poll_undisclosed_text">"Ergebnisse werden nach Ende der Umfrage angezeigt"</string>
<string name="common_privacy_policy">"Datenschutz­erklärung"</string>
<string name="common_reactions">"Reaktionen"</string>
<string name="common_refreshing">"Aktualisiere…"</string>
<string name="common_replying_to">"Auf %1$s antworten"</string>
<string name="common_report_a_bug">"Melde einen Fehler"</string>
<string name="common_report_a_bug">"Einen Fehler melden"</string>
<string name="common_report_submitted">"Bericht gesendet"</string>
<string name="common_room_name">"Raumname"</string>
<string name="common_room_name_placeholder">"z.B. dein Projektname"</string>
@ -106,12 +110,12 @@
<string name="common_search_results">"Suchergebnisse"</string>
<string name="common_security">"Sicherheit"</string>
<string name="common_select_your_server">"Wählen deinen Server"</string>
<string name="common_sending">"Senden…"</string>
<string name="common_sending">"Sendet…"</string>
<string name="common_server_not_supported">"Server wird nicht unterstützt"</string>
<string name="common_server_url">"Server-URL"</string>
<string name="common_settings">"Einstellungen"</string>
<string name="common_shared_location">"Geteilter Standort"</string>
<string name="common_starting_chat">"Chat wird gestartet…"</string>
<string name="common_starting_chat">"Starte Chat…"</string>
<string name="common_sticker">"Sticker"</string>
<string name="common_success">"Erfolg"</string>
<string name="common_suggestions">"Vorschläge"</string>
@ -120,7 +124,7 @@
<string name="common_topic">"Thema"</string>
<string name="common_topic_placeholder">"Worum geht es in diesem Raum?"</string>
<string name="common_unable_to_decrypt">"Entschlüsselung nicht möglich"</string>
<string name="common_unable_to_invite_message">"Wir konnten Einladungen nicht erfolgreich an einen oder mehrere Benutzer senden."</string>
<string name="common_unable_to_invite_message">"Einladungen konnten nicht an einen oder mehrere Benutzer gesendet werden."</string>
<string name="common_unable_to_invite_title">"Einladung(en) können nicht gesendet werden"</string>
<string name="common_unmute">"Stummschaltung aufheben"</string>
<string name="common_unsupported_event">"Nicht unterstütztes Ereignis"</string>
@ -128,7 +132,7 @@
<string name="common_verification_cancelled">"Verifizierung abgebrochen"</string>
<string name="common_verification_complete">"Verifizierung abgeschlossen"</string>
<string name="common_video">"Video"</string>
<string name="common_waiting">"Warten…"</string>
<string name="common_waiting">"Warte…"</string>
<string name="dialog_title_confirmation">"Bestätigung"</string>
<string name="dialog_title_warning">"Warnung"</string>
<string name="emoji_picker_category_activity">"Aktivitäten"</string>
@ -139,10 +143,12 @@
<string name="emoji_picker_category_people">"Smileys &amp; Personen"</string>
<string name="emoji_picker_category_places">"Reisen &amp; Orte"</string>
<string name="emoji_picker_category_symbols">"Symbole"</string>
<string name="error_failed_creating_the_permalink">"Fehler beim Erstellen des Permalinks"</string>
<string name="error_failed_creating_the_permalink">"Fehler beim Erstellen des Dauerlinks"</string>
<string name="error_failed_loading_map">"%1$s konnte die Karte nicht laden. Bitte versuche es später erneut."</string>
<string name="error_failed_loading_messages">"Fehler beim Laden der Nachrichten"</string>
<string name="error_failed_locating_user">"%1$s konnte nicht auf deinen Standort zugreifen. Bitte versuche es später erneut."</string>
<string name="error_missing_location_auth_android">"%1$s hat keine Berechtigung, auf deinen Standort zuzugreifen. Du kannst den Zugriff in den Einstellungen aktivieren."</string>
<string name="error_missing_location_rationale_android">"%1$s hat keine Berechtigung, auf deinen Standort zuzugreifen. Aktiviere den Zugriff unten."</string>
<string name="error_some_messages_have_not_been_sent">"Einige Nachrichten wurden nicht gesendet"</string>
<string name="error_unknown">"Entschuldigung, ein Fehler ist aufgetreten."</string>
<string name="invite_friends_rich_title">"🔐️ Besuche mich auf %1$s"</string>
@ -155,9 +161,13 @@
<item quantity="one">"%1$d Mitglied"</item>
<item quantity="other">"%1$d Mitglieder"</item>
</plurals>
<string name="preference_rageshake">"Rageshake zum Melden von Fehlern"</string>
<plurals name="common_poll_votes_count">
<item quantity="one">"%d Stimme"</item>
<item quantity="other">"%d Stimmen"</item>
</plurals>
<string name="preference_rageshake">"Schütteln zum Melden von Fehlern"</string>
<string name="rageshake_dialog_content">"Du scheinst frustriert das Telefon zu schütteln. Möchtest du den Fehlerberichtsbildschirm öffnen?"</string>
<string name="report_content_explanation">"Diese Nachricht wird an deinen Heimserver-Admin gemeldet werden. Er wird nicht in der Lage sein, verschlüsselte Nachrichten zu lesen."</string>
<string name="report_content_explanation">"Diese Nachricht wird an deinen Heimserver-Admin gemeldet. Er wird nicht in der Lage sein, verschlüsselte Nachrichten zu lesen."</string>
<string name="report_content_hint">"Grund für die Meldung dieses Inhalts"</string>
<string name="room_timeline_beginning_of_room">"Dies ist der Anfang von %1$s."</string>
<string name="room_timeline_beginning_of_room_no_name">"Dies ist der Beginn dieser Konversation."</string>
@ -168,7 +178,35 @@
<string name="screen_media_upload_preview_error_failed_sending">"Hochladen von Medien fehlgeschlagen, bitte versuchen Sie es erneut."</string>
<string name="screen_migration_message">"Dies ist ein einmaliger Vorgang, danke fürs Warten."</string>
<string name="screen_migration_title">"Dein Konto einrichten"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Zusätzliche Einstellungen"</string>
<string name="screen_notification_settings_calls_label">"Audio- und Videoanrufe"</string>
<string name="screen_notification_settings_configuration_mismatch">"Konfigurationskonflikt"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"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."</string>
<string name="screen_notification_settings_direct_chats">"Direkte Chats"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Benutzerdefinierte Einstellung pro Chat"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Beim Aktualisieren der Benachrichtigungseinstellung ist ein Fehler aufgetreten."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Alle Nachrichten"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Nur Erwähnungen und Schlüsselwörter"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"Bei direkten Chats, benachrichtigen mich für"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Bei Gruppenchats, benachrichtigte mich für"</string>
<string name="screen_notification_settings_enable_notifications">"Benachrichtigungen auf diesem Gerät aktivieren"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Die Konfiguration wurde nicht korrigiert. Bitte versuche es erneut."</string>
<string name="screen_notification_settings_group_chats">"Gruppenchats"</string>
<string name="screen_notification_settings_mentions_section_title">"Erwähnungen"</string>
<string name="screen_notification_settings_mode_all">"Alle"</string>
<string name="screen_notification_settings_mode_mentions">"Erwähnungen"</string>
<string name="screen_notification_settings_notification_section_title">"Benachrichtige mich für"</string>
<string name="screen_notification_settings_room_mention_label">"Benachrichtige mich bei @room"</string>
<string name="screen_notification_settings_system_notifications_action_required">"Um Benachrichtigungen zu erhalten, ändern bitte deine %1$s."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"Systemeinstellungen"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Systembenachrichtigungen deaktiviert"</string>
<string name="screen_notification_settings_title">"Benachrichtigungen"</string>
<string name="screen_report_content_block_user_hint">"Prüfe, ob du alle aktuellen und zukünftigen Nachrichten dieses Benutzers ausblenden möchtest"</string>
<string name="screen_settings_oidc_account">"Konto und Geräte"</string>
<string name="screen_share_location_title">"Standort teilen"</string>
<string name="screen_share_my_location_action">"Meinen Standort teilen"</string>
<string name="screen_share_open_apple_maps">"In Apple Maps öffnen"</string>

View file

@ -182,11 +182,21 @@
<string name="screen_migration_title">"Настройка учетной записи."</string>
<string name="screen_notification_settings_additional_settings_section_title">"Дополнительные параметры"</string>
<string name="screen_notification_settings_calls_label">"Аудио и видео звонки"</string>
<string name="screen_notification_settings_configuration_mismatch">"Несоответствие конфигурации"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Мы упростили настройки уведомлений, чтобы упростить поиск опций.
Некоторые пользовательские настройки, выбранные вами ранее, не отображаются в данном меню, но они все еще активны.
Если вы продолжите, некоторые настройки могут быть изменены."</string>
<string name="screen_notification_settings_direct_chats">"Прямые чаты"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Индивидуальные настройки для каждого чата"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"При обновлении настроек уведомления произошла ошибка."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Все сообщения"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Только упоминания и ключевые слова"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"Уведомлять меня в личных чатах"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Уведомлять меня в групповых чатах"</string>
<string name="screen_notification_settings_enable_notifications">"Включить уведомления на данном устройстве"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Конфигурация не была исправлена, попробуйте еще раз."</string>
<string name="screen_notification_settings_group_chats">"Групповые чаты"</string>
<string name="screen_notification_settings_mentions_section_title">"Упоминания"</string>
<string name="screen_notification_settings_mode_all">"Все"</string>
@ -198,6 +208,7 @@
<string name="screen_notification_settings_system_notifications_turned_off">"Системные уведомления выключены"</string>
<string name="screen_notification_settings_title">"Уведомления"</string>
<string name="screen_report_content_block_user_hint">"Отметьте, хотите ли вы скрыть все текущие и будущие сообщения от этого пользователя"</string>
<string name="screen_settings_oidc_account">"Учетная запись и устройства"</string>
<string name="screen_share_location_title">"Поделиться местоположением"</string>
<string name="screen_share_my_location_action">"Поделиться моим местоположением"</string>
<string name="screen_share_open_apple_maps">"Открыть в Apple Maps"</string>

View file

@ -182,10 +182,21 @@
<string name="screen_migration_title">"Nastavenie vášho účtu."</string>
<string name="screen_notification_settings_additional_settings_section_title">"Ďalšie nastavenia"</string>
<string name="screen_notification_settings_calls_label">"Audio a video hovory"</string>
<string name="screen_notification_settings_configuration_mismatch">"Nezhoda konfigurácie"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"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ť."</string>
<string name="screen_notification_settings_direct_chats">"Priame konverzácie"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Vlastné nastavenie pre konverzácie"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Pri aktualizácii nastavenia oznámenia došlo k chybe."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Všetky správy"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Iba zmienky a kľúčové slová"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"Pri priamych rozhovoroch ma upozorniť na"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Pri skupinových rozhovoroch ma upozorniť na"</string>
<string name="screen_notification_settings_enable_notifications">"Povoliť oznámenia na tomto zariadení"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Konfigurácia nebola opravená, skúste to prosím znova."</string>
<string name="screen_notification_settings_group_chats">"Skupinové rozhovory"</string>
<string name="screen_notification_settings_mentions_section_title">"Zmienky"</string>
<string name="screen_notification_settings_mode_all">"Všetky"</string>
@ -197,6 +208,7 @@
<string name="screen_notification_settings_system_notifications_turned_off">"Systémové oznámenia sú vypnuté"</string>
<string name="screen_notification_settings_title">"Oznámenia"</string>
<string name="screen_report_content_block_user_hint">"Označte, či chcete skryť všetky aktuálne a budúce správy od tohto používateľa"</string>
<string name="screen_settings_oidc_account">"Účet a zariadenia"</string>
<string name="screen_share_location_title">"Zdieľať polohu"</string>
<string name="screen_share_my_location_action">"Zdieľať moju polohu"</string>
<string name="screen_share_open_apple_maps">"Otvoriť v Apple Maps"</string>

View file

@ -181,9 +181,9 @@
<string name="screen_notification_settings_additional_settings_section_title">"Additional settings"</string>
<string name="screen_notification_settings_calls_label">"Audio and video calls"</string>
<string name="screen_notification_settings_configuration_mismatch">"Configuration mismatch"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Weve simplified Notifications Settings to make options easier to find.
<string name="screen_notification_settings_configuration_mismatch_description">"Weve simplified Notifications Settings to make options easier to find.
Some custom settings youve chosen in the past are not shown here, but theyre still active.
Some custom settings youve chosen in the past are not shown here, but theyre still active.
If you proceed, some of your settings may change."</string>
<string name="screen_notification_settings_direct_chats">"Direct chats"</string>

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e7aba18d2003a96a34e6eecf4fc6d0ee139fa8073549033da04eff3fa20b4c93
size 40454
oid sha256:a7f1d73aa1698bb02c03773ca9c3ec8494089d03c26d52b3b6f1aaa40a081528
size 49266

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0422cc4e2bae580a88dacd711570e0c5ba105d5baae103313e42a07991823083
size 42103
oid sha256:f2154c7d196058a0a47aa46658ad9d2ea3edf6b89e5973396980178a2399179a
size 50726

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:64e67ea3130d0e70bdc8d0edc0bfa92763673c15b3235d4332f554a0e7a4840a
size 45553
oid sha256:b6344d9976849f1e56782a463fcca625943a7c6adf74c598c7a58245809b9db5
size 53642

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7f2fd6d2fccd68abec9d829ccd673b72a0f1277644a1affb09faed7be8b8a416
size 47021
oid sha256:fd7e6fc09011429e0fe19bff73d3c48124b1f3bed74d3cb12c9f3f0405f53980
size 55728

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f704348b48d03ce3e788e7e37298a008116c66f3ad0a074d164df4ebbb05d9d8
size 48964
oid sha256:15d14bf99af3cd0433870ebd6032d9bf4a45196e2ef1df7184cc55859a704dee
size 49008

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f704348b48d03ce3e788e7e37298a008116c66f3ad0a074d164df4ebbb05d9d8
size 48964
oid sha256:6e47fc219bbd63b76d01a5e50c3e4c6b1b0a8b4ec40b08b13271d0b2673d8d5e
size 50932

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:945d8f7b4226c451c8d33f51bf051c7f08a316c786259d49475b2e2cde407bd7
size 46415
oid sha256:cacb91ffca97f21bbc19b29525813f58fc8017a858aa28ccd4e620b70a8cd9ca
size 46119

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:945d8f7b4226c451c8d33f51bf051c7f08a316c786259d49475b2e2cde407bd7
size 46415
oid sha256:3f1c14a23ee598ece6843a68d3ca0b1d1f725f53174bd493e27c6137de70c508
size 48296

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f689d80043e7d5121d072b928c866a3bde0358b5d5df06e1d4f0ceeb9a11dfef
size 56344

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fb9e4bbe341a84452206a7485a477d81725b535369b1dfad3cf430548dbb21e8
size 46450

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8bfbbae8e27c4be7ea7fadeff2470773775cb476ef37b25c8f1bb8c35b5eddd9
size 43082

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f704348b48d03ce3e788e7e37298a008116c66f3ad0a074d164df4ebbb05d9d8
size 48964

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:945d8f7b4226c451c8d33f51bf051c7f08a316c786259d49475b2e2cde407bd7
size 46415

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:74508ef7f77a9c8713c75586ae4d34a9daab2608dbbd2f20de3e4d4a9a9be7e9
size 39225

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4ac9e0523fc99d472a1fe8f21719e64dbb7d1ec01059dd4183aa4a152f8ead55
size 38673

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ed68d9ebe67d5dad938a3efcd8c2b680444c650e635bdc04cfd140ba694d9f1d
size 38928

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1d5c9d2042dad75b48b61cdbae5b2425d7c935eb860df5d5c6fa3bcb327d13d1
size 38842

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d4ab648f5651b7457635be3eabb914be1c976154d12a163f1c1bb2cd92168824
size 38730

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fbb713d36f8b36ce6b55f38c10323e784a80f6187e6613ded91dc531b23a7cb7
size 36444

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:31735c42ea83974595544a0f03636a2337210f23e60d4b7f8e41d46ba21d483f
size 35920

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c4c5bc5f2086bd78a364e9996f0f41526d73d69857875594f5de7ea9998333d7
size 23388

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:42848910a949ec938ccc7a3547b19d1c2c81193f9a93b1db21ca77e4d9ed9663
size 21761

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c4c5bc5f2086bd78a364e9996f0f41526d73d69857875594f5de7ea9998333d7
size 23388

Some files were not shown because too many files have changed in this diff Show more