Merge branch 'develop' into feature/bma/dataObject
This commit is contained in:
commit
55255735d7
129 changed files with 2140 additions and 487 deletions
|
|
@ -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
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Contributing to Element Android
|
||||
# Contributing to Element X Android
|
||||
|
||||
<!--- TOC -->
|
||||
|
||||
|
|
|
|||
1
changelog.d/1113.wip
Normal file
1
changelog.d/1113.wip
Normal file
|
|
@ -0,0 +1 @@
|
|||
[Polls] Improve UI and render ended state
|
||||
1
changelog.d/1131.bugfix
Normal file
1
changelog.d/1131.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
|||
Only display verification prompt after initial sync is done.
|
||||
1
changelog.d/862.bugfix
Normal file
1
changelog.d/862.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
|||
Videos sent from the app were cropped in some cases.
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<string name="screen_account_provider_signin_title">"You’re 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">"You’re 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>
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -10,16 +10,32 @@
|
|||
<string name="screen_room_attachment_source_files">"Anhang"</string>
|
||||
<string name="screen_room_attachment_source_gallery">"Foto- & 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = { },
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 = { }
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 = { },
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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() }
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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() =
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">"Datenschutzerklä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 & Personen"</string>
|
||||
<string name="emoji_picker_category_places">"Reisen & 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">"We’ve simplified Notifications Settings to make options easier to find.
|
||||
<string name="screen_notification_settings_configuration_mismatch_description">"We’ve simplified Notifications Settings to make options easier to find.
|
||||
|
||||
Some custom settings you’ve chosen in the past are not shown here, but they’re still active.
|
||||
Some custom settings you’ve chosen in the past are not shown here, but they’re still active.
|
||||
|
||||
If you proceed, some of your settings may change."</string>
|
||||
<string name="screen_notification_settings_direct_chats">"Direct chats"</string>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e7aba18d2003a96a34e6eecf4fc6d0ee139fa8073549033da04eff3fa20b4c93
|
||||
size 40454
|
||||
oid sha256:a7f1d73aa1698bb02c03773ca9c3ec8494089d03c26d52b3b6f1aaa40a081528
|
||||
size 49266
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0422cc4e2bae580a88dacd711570e0c5ba105d5baae103313e42a07991823083
|
||||
size 42103
|
||||
oid sha256:f2154c7d196058a0a47aa46658ad9d2ea3edf6b89e5973396980178a2399179a
|
||||
size 50726
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:64e67ea3130d0e70bdc8d0edc0bfa92763673c15b3235d4332f554a0e7a4840a
|
||||
size 45553
|
||||
oid sha256:b6344d9976849f1e56782a463fcca625943a7c6adf74c598c7a58245809b9db5
|
||||
size 53642
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7f2fd6d2fccd68abec9d829ccd673b72a0f1277644a1affb09faed7be8b8a416
|
||||
size 47021
|
||||
oid sha256:fd7e6fc09011429e0fe19bff73d3c48124b1f3bed74d3cb12c9f3f0405f53980
|
||||
size 55728
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f704348b48d03ce3e788e7e37298a008116c66f3ad0a074d164df4ebbb05d9d8
|
||||
size 48964
|
||||
oid sha256:15d14bf99af3cd0433870ebd6032d9bf4a45196e2ef1df7184cc55859a704dee
|
||||
size 49008
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f704348b48d03ce3e788e7e37298a008116c66f3ad0a074d164df4ebbb05d9d8
|
||||
size 48964
|
||||
oid sha256:6e47fc219bbd63b76d01a5e50c3e4c6b1b0a8b4ec40b08b13271d0b2673d8d5e
|
||||
size 50932
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:945d8f7b4226c451c8d33f51bf051c7f08a316c786259d49475b2e2cde407bd7
|
||||
size 46415
|
||||
oid sha256:cacb91ffca97f21bbc19b29525813f58fc8017a858aa28ccd4e620b70a8cd9ca
|
||||
size 46119
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:945d8f7b4226c451c8d33f51bf051c7f08a316c786259d49475b2e2cde407bd7
|
||||
size 46415
|
||||
oid sha256:3f1c14a23ee598ece6843a68d3ca0b1d1f725f53174bd493e27c6137de70c508
|
||||
size 48296
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f689d80043e7d5121d072b928c866a3bde0358b5d5df06e1d4f0ceeb9a11dfef
|
||||
size 56344
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fb9e4bbe341a84452206a7485a477d81725b535369b1dfad3cf430548dbb21e8
|
||||
size 46450
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8bfbbae8e27c4be7ea7fadeff2470773775cb476ef37b25c8f1bb8c35b5eddd9
|
||||
size 43082
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f704348b48d03ce3e788e7e37298a008116c66f3ad0a074d164df4ebbb05d9d8
|
||||
size 48964
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:945d8f7b4226c451c8d33f51bf051c7f08a316c786259d49475b2e2cde407bd7
|
||||
size 46415
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:74508ef7f77a9c8713c75586ae4d34a9daab2608dbbd2f20de3e4d4a9a9be7e9
|
||||
size 39225
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4ac9e0523fc99d472a1fe8f21719e64dbb7d1ec01059dd4183aa4a152f8ead55
|
||||
size 38673
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ed68d9ebe67d5dad938a3efcd8c2b680444c650e635bdc04cfd140ba694d9f1d
|
||||
size 38928
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1d5c9d2042dad75b48b61cdbae5b2425d7c935eb860df5d5c6fa3bcb327d13d1
|
||||
size 38842
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d4ab648f5651b7457635be3eabb914be1c976154d12a163f1c1bb2cd92168824
|
||||
size 38730
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fbb713d36f8b36ce6b55f38c10323e784a80f6187e6613ded91dc531b23a7cb7
|
||||
size 36444
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:31735c42ea83974595544a0f03636a2337210f23e60d4b7f8e41d46ba21d483f
|
||||
size 35920
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c4c5bc5f2086bd78a364e9996f0f41526d73d69857875594f5de7ea9998333d7
|
||||
size 23388
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:42848910a949ec938ccc7a3547b19d1c2c81193f9a93b1db21ca77e4d9ed9663
|
||||
size 21761
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue