Merge branch 'develop' into feature/bma/dataObject

This commit is contained in:
Benoit Marty 2023-08-28 10:56:02 +02:00 committed by GitHub
commit 1a6376e723
129 changed files with 2140 additions and 487 deletions

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.components.dialogs
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
/**
* Used to store the visual data for a list option.
*/
data class ListOption(
val title: String,
val subtitle: String? = null,
)
/** Creates an immutable list of [ListOption]s from the given [values], using them as titles. */
fun listOptionOf(vararg values: String): ImmutableList<ListOption> {
return values.map { ListOption(it) }.toImmutableList()
}

View file

@ -0,0 +1,151 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.components.dialogs
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.airbnb.android.showkase.annotation.ShowkaseComposable
import io.element.android.libraries.designsystem.components.list.CheckboxListItem
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.DialogPreview
import io.element.android.libraries.designsystem.theme.components.ListSupportingText
import io.element.android.libraries.designsystem.theme.components.SimpleAlertDialogContent
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MultipleSelectionDialog(
options: ImmutableList<ListOption>,
onConfirmClicked: (List<Int>) -> Unit,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
confirmButtonTitle: String = stringResource(CommonStrings.action_confirm),
dismissButtonTitle: String = stringResource(CommonStrings.action_cancel),
title: String? = null,
subtitle: String? = null,
initialSelection: ImmutableList<Int> = persistentListOf(),
) {
val decoratedSubtitle: @Composable (() -> Unit)? = subtitle?.let {
@Composable {
ListSupportingText(
text = it,
modifier = Modifier.padding(start = 8.dp)
)
}
}
AlertDialog(
modifier = modifier,
onDismissRequest = onDismissRequest,
) {
MultipleSelectionDialogContent(
title = title,
subtitle = decoratedSubtitle,
options = options,
confirmButtonTitle = confirmButtonTitle,
onConfirmClicked = onConfirmClicked,
dismissButtonTitle = dismissButtonTitle,
onDismissRequest = onDismissRequest,
initialSelected = initialSelection,
)
}
}
@Composable
internal fun MultipleSelectionDialogContent(
options: ImmutableList<ListOption>,
confirmButtonTitle: String,
onConfirmClicked: (List<Int>) -> Unit,
dismissButtonTitle: String,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
title: String? = null,
initialSelected: ImmutableList<Int> = persistentListOf(),
subtitle: @Composable (() -> Unit)? = null,
) {
val selectedOptionIndexes = remember { initialSelected.toMutableStateList() }
fun isSelected(index: Int) = selectedOptionIndexes.any { it == index }
SimpleAlertDialogContent(
title = title,
subtitle = subtitle,
modifier = modifier,
submitText = confirmButtonTitle,
onSubmitClicked = {
onConfirmClicked(selectedOptionIndexes.toList())
},
cancelText = dismissButtonTitle,
onCancelClicked = onDismissRequest,
applyPaddingToContents = false,
) {
LazyColumn {
itemsIndexed(options) { index, option ->
CheckboxListItem(
headline = option.title,
checked = isSelected(index),
onChange = {
if (isSelected(index)) {
selectedOptionIndexes.remove(index)
} else {
selectedOptionIndexes.add(index)
}
},
supportingText = option.subtitle,
compactLayout = true,
modifier = Modifier.padding(start = 8.dp)
)
}
}
}
}
@DayNightPreviews
@ShowkaseComposable(group = PreviewGroup.Dialogs)
@Composable
internal fun MultipleSelectionDialogContentPreview() {
ElementPreview(showBackground = false) {
DialogPreview {
val options = persistentListOf(
ListOption("Option 1", "Supporting line text lorem ipsum dolor sit amet, consectetur."),
ListOption("Option 2"),
ListOption("Option 3"),
)
MultipleSelectionDialogContent(
title = "Dialog title",
options = options,
onConfirmClicked = {},
onDismissRequest = {},
confirmButtonTitle = "Save",
dismissButtonTitle = "Cancel",
initialSelected = persistentListOf(0),
)
}
}
}

View file

@ -0,0 +1,131 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.components.dialogs
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.airbnb.android.showkase.annotation.ShowkaseComposable
import io.element.android.libraries.designsystem.components.list.RadioButtonListItem
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.DialogPreview
import io.element.android.libraries.designsystem.theme.components.ListSupportingText
import io.element.android.libraries.designsystem.theme.components.SimpleAlertDialogContent
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SingleSelectionDialog(
options: ImmutableList<ListOption>,
onOptionSelected: (Int) -> Unit,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
title: String? = null,
subtitle: String? = null,
dismissButtonTitle: String = stringResource(CommonStrings.action_cancel),
initialSelection: Int? = null,
) {
val decoratedSubtitle: @Composable (() -> Unit)? = subtitle?.let {
@Composable {
ListSupportingText(
text = it,
modifier = Modifier.padding(start = 8.dp)
)
}
}
AlertDialog(
modifier = modifier,
onDismissRequest = onDismissRequest,
) {
SingleSelectionDialogContent(
title = title,
subtitle = decoratedSubtitle,
options = options,
onOptionSelected = onOptionSelected,
dismissButtonTitle = dismissButtonTitle,
onDismissRequest = onDismissRequest,
initialSelection = initialSelection,
)
}
}
@Composable
internal fun SingleSelectionDialogContent(
options: ImmutableList<ListOption>,
onOptionSelected: (Int) -> Unit,
onDismissRequest: () -> Unit,
dismissButtonTitle: String,
modifier: Modifier = Modifier,
title: String? = null,
initialSelection: Int? = null,
subtitle: @Composable (() -> Unit)? = null,
) {
SimpleAlertDialogContent(
title = title,
subtitle = subtitle,
modifier = modifier,
cancelText = dismissButtonTitle,
onCancelClicked = onDismissRequest,
applyPaddingToContents = false,
) {
LazyColumn {
itemsIndexed(options) { index, option ->
RadioButtonListItem(
headline = option.title,
supportingText = option.subtitle,
selected = index == initialSelection,
onSelected = { onOptionSelected(index) },
compactLayout = true,
modifier = Modifier.padding(start = 8.dp)
)
}
}
}
}
@DayNightPreviews
@ShowkaseComposable(group = PreviewGroup.Dialogs)
@Composable
internal fun SingleSelectionDialogContentPreview() {
ElementPreview(showBackground = false) {
DialogPreview {
val options = persistentListOf(
ListOption("Option 1"),
ListOption("Option 2"),
ListOption("Option 3"),
)
SingleSelectionDialogContent(
title = "Dialog title",
options = options,
onOptionSelected = {},
onDismissRequest = {},
dismissButtonTitle = "Cancel",
initialSelection = 0
)
}
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.components.list
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun CheckboxListItem(
headline: String,
checked: Boolean,
onChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
supportingText: String? = null,
trailingContent: ListItemContent? = null,
enabled: Boolean = true,
style: ListItemStyle = ListItemStyle.Default,
compactLayout: Boolean = false,
) {
ListItem(
modifier = modifier,
headlineContent = { Text(headline) },
supportingContent = supportingText?.let { @Composable { Text(it) } },
leadingContent = ListItemContent.Checkbox(checked, null, enabled, compact = compactLayout),
trailingContent = trailingContent,
style = style,
enabled = enabled,
onClick = { onChange(!checked) },
)
}

View file

@ -0,0 +1,126 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.components.list
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Checkbox as CheckboxComponent
import io.element.android.libraries.designsystem.theme.components.Icon as IconComponent
import io.element.android.libraries.designsystem.theme.components.RadioButton as RadioButtonComponent
import io.element.android.libraries.designsystem.theme.components.Switch as SwitchComponent
import io.element.android.libraries.designsystem.theme.components.Text as TextComponent
/**
* This is a helper to set default leading and trailing content for [ListItem]s.
*/
sealed interface ListItemContent {
/**
* Default Switch content for [ListItem].
* @param checked The current state of the switch.
* @param onChange Callback when the switch is toggled: it should only be set to override the default click behaviour in the [ListItem].
* @param enabled Whether the switch is enabled or not.
*/
data class Switch(
val checked: Boolean,
val onChange: ((Boolean) -> Unit)? = null,
val enabled: Boolean = true
) : ListItemContent
/**
* Default Checkbox content for [ListItem].
* @param checked The current state of the checkbox.
* @param onChange Callback when the checkbox is toggled: it should only be set to override the default click behaviour in the [ListItem].
* @param enabled Whether the checkbox is enabled or not.
* @param compact Reduces the size of the component to make the wrapping [ListItem] smaller.
* This is especially useful when the [ListItem] is used inside a Dialog. `false` by default.
*/
data class Checkbox(
val checked: Boolean,
val onChange: ((Boolean) -> Unit)? = null,
val enabled: Boolean = true,
val compact: Boolean = false
) : ListItemContent
/**
* Default RadioButton content for [ListItem].
* @param selected The current state of the radio button.
* @param onClick Callback when the radio button is toggled: it should only be set to override the default click behaviour in the [ListItem].
* @param enabled Whether the radio button is enabled or not.
* @param compact Reduces the size of the component to make the wrapping [ListItem] smaller.
* This is especially useful when the [ListItem] is used inside a Dialog. `false` by default.
*/
data class RadioButton(
val selected: Boolean,
val onClick: (() -> Unit)? = null,
val enabled: Boolean = true,
val compact: Boolean = false
) : ListItemContent
/**
* Default Icon content for [ListItem]. Sets the Icon component to a predefined size.
* @param iconSource The icon to display, using [IconSource.getPainter].
*/
data class Icon(val iconSource: IconSource) : ListItemContent
/**
* Default Text content for [ListItem]. Sets the Text component to a max size and clips overflow.
* @param text The text to display.
*/
data class Text(val text: String) : ListItemContent
/** Displays any custom content. */
data class Custom(val content: @Composable () -> Unit) : ListItemContent
@Composable
fun View() {
when (this) {
is Switch -> SwitchComponent(
checked = checked,
onCheckedChange = onChange,
enabled = enabled
)
is Checkbox -> CheckboxComponent(
modifier = if (compact) Modifier.size(maxCompactSize) else Modifier,
checked = checked,
onCheckedChange = onChange,
enabled = enabled
)
is RadioButton -> RadioButtonComponent(
modifier = if (compact) Modifier.size(maxCompactSize) else Modifier,
selected = selected,
onClick = onClick,
enabled = enabled
)
is Icon -> IconComponent(
modifier = Modifier.size(maxCompactSize),
painter = iconSource.getPainter(),
contentDescription = iconSource.contentDescription
)
is Text -> TextComponent(modifier = Modifier.widthIn(max = 128.dp), text = text, maxLines = 1, overflow = TextOverflow.Ellipsis)
is Custom -> content()
}
}
}
private val maxCompactSize = DpSize(24.dp, 24.dp)

View file

@ -0,0 +1,159 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.components.list
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.designsystem.components.dialogs.ListOption
import io.element.android.libraries.designsystem.components.dialogs.MultipleSelectionDialog
import io.element.android.libraries.designsystem.components.dialogs.listOptionOf
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@Composable
fun MultipleSelectionListItem(
headline: String,
options: ImmutableList<ListOption>,
onSelectionChanged: (List<Int>) -> Unit,
resultFormatter: (List<Int>) -> String?,
modifier: Modifier = Modifier,
supportingText: String? = null,
leadingContent: ListItemContent? = null,
selected: ImmutableList<Int> = persistentListOf(),
displayResultInTrailingContent: Boolean = false,
) {
val selectedIndexes = remember(selected) { selected.toMutableStateList() }
val selectedItemsText by remember { derivedStateOf { resultFormatter(selectedIndexes) } }
val decoratedSupportedText: @Composable (() -> Unit)? = when {
!selectedItemsText.isNullOrBlank() && !displayResultInTrailingContent -> {
@Composable {
Text(selectedItemsText!!)
}
}
supportingText != null -> {
@Composable {
Text(supportingText)
}
}
else -> null
}
val trailingContent: ListItemContent? = if (!selectedItemsText.isNullOrBlank() && displayResultInTrailingContent) {
ListItemContent.Text(selectedItemsText!!)
} else {
null
}
var displaySelectionDialog by rememberSaveable { mutableStateOf(false) }
ListItem(
modifier = modifier,
headlineContent = { Text(text = headline) },
supportingContent = decoratedSupportedText,
leadingContent = leadingContent,
trailingContent = trailingContent,
onClick = { displaySelectionDialog = true }
)
if (displaySelectionDialog) {
MultipleSelectionDialog(
title = headline,
options = options,
onConfirmClicked = { newSelectedIndexes ->
if (newSelectedIndexes != selectedIndexes.toList()) {
onSelectionChanged(newSelectedIndexes)
selectedIndexes.clear()
selectedIndexes.addAll(newSelectedIndexes)
}
displaySelectionDialog = false
},
onDismissRequest = { displaySelectionDialog = false },
initialSelection = selectedIndexes.toImmutableList(),
)
}
}
@Preview("Multiple selection List item - no selection", group = PreviewGroup.ListItems)
@Composable
internal fun MutipleSelectionListItemPreview() {
ElementThemedPreview {
val options = listOptionOf("Option 1", "Option 2", "Option 3")
MultipleSelectionListItem(
headline = "Headline",
options = options,
onSelectionChanged = {},
supportingText = "Supporting text",
resultFormatter = { result -> formatResult(result, options) },
)
}
}
@Preview("Multiple selection List item - selection in supporting text", group = PreviewGroup.ListItems)
@Composable
internal fun MutipleSelectionListItemSelectedPreview() {
ElementThemedPreview {
val options = listOptionOf("Option 1", "Option 2", "Option 3")
val selected = persistentListOf<Int>(0, 2)
MultipleSelectionListItem(
headline = "Headline",
options = options,
onSelectionChanged = {},
supportingText = "Supporting text",
resultFormatter = {
val selectedValues = formatResult(it, options)
"Selected: $selectedValues"
},
selected = selected,
)
}
}
@Preview("Multiple selection List item - selection in trailing content", group = PreviewGroup.ListItems)
@Composable
internal fun MutipleSelectionListItemSelectedTrailingContentPreview() {
ElementThemedPreview {
val options = listOptionOf("Option 1", "Option 2", "Option 3")
val selected = persistentListOf<Int>(0, 2)
MultipleSelectionListItem(
headline = "Headline",
options = options,
onSelectionChanged = {},
supportingText = "Supporting text",
resultFormatter = { selected.size.toString() },
displayResultInTrailingContent = true,
selected = selected,
)
}
}
private fun formatResult(result: List<Int>, options: ImmutableList<ListOption>): String? {
return options.mapIndexedNotNull { index, value -> value.title.takeIf { result.contains(index) } }.joinToString(", ").takeIf { it.isNotEmpty() }
}

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.components.list
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun RadioButtonListItem(
headline: String,
selected: Boolean,
onSelected: () -> Unit,
modifier: Modifier = Modifier,
supportingText: String? = null,
trailingContent: ListItemContent? = null,
style: ListItemStyle = ListItemStyle.Default,
enabled: Boolean = true,
compactLayout: Boolean = false,
) {
ListItem(
modifier = modifier,
headlineContent = { Text(headline) },
supportingContent = supportingText?.let { @Composable { Text(it) } },
leadingContent = ListItemContent.RadioButton(selected, null, enabled, compact = compactLayout),
trailingContent = trailingContent,
style = style,
enabled = enabled,
onClick = onSelected,
)
}

View file

@ -0,0 +1,174 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.components.list
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.designsystem.components.dialogs.ListOption
import io.element.android.libraries.designsystem.components.dialogs.SingleSelectionDialog
import io.element.android.libraries.designsystem.components.dialogs.listOptionOf
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds
@Composable
fun SingleSelectionListItem(
headline: String,
options: ImmutableList<ListOption>,
onSelectionChanged: (Int) -> Unit,
modifier: Modifier = Modifier,
supportingText: String? = null,
leadingContent: ListItemContent? = null,
resultFormatter: (Int) -> String? = { options.getOrNull(it)?.title },
selected: Int? = null,
displayResultInTrailingContent: Boolean = false,
) {
val coroutineScope = rememberCoroutineScope()
var selectedIndex by rememberSaveable(selected) { mutableStateOf(selected) }
val selectedItem by remember { derivedStateOf { selectedIndex?.let { resultFormatter(it) } } }
val decoratedSupportedText: @Composable (() -> Unit)? = if (!selectedItem.isNullOrBlank() && !displayResultInTrailingContent) {
@Composable {
Text(selectedItem!!)
}
} else {
supportingText?.let {
@Composable {
Text(it)
}
}
}
val trailingContent: ListItemContent? = if (!selectedItem.isNullOrBlank() && displayResultInTrailingContent) {
ListItemContent.Text(selectedItem!!)
} else {
null
}
var displaySelectionDialog by rememberSaveable { mutableStateOf(false) }
ListItem(
modifier = modifier,
headlineContent = { Text(text = headline) },
supportingContent = decoratedSupportedText,
leadingContent = leadingContent,
trailingContent = trailingContent,
onClick = { displaySelectionDialog = true }
)
if (displaySelectionDialog) {
SingleSelectionDialog(
title = headline,
options = options,
onOptionSelected = { index ->
if (index != selectedIndex) {
onSelectionChanged(index)
selectedIndex = index
}
// Delay hiding the dialog for a bit so the new state is displayed in it before being dismissed
coroutineScope.launch {
delay(0.5.seconds)
displaySelectionDialog = false
}
},
onDismissRequest = { displaySelectionDialog = false },
initialSelection = selectedIndex,
)
}
}
@Preview("Single selection List item - no selection", group = PreviewGroup.ListItems)
@Composable
internal fun SingleSelectionListItemPreview() {
ElementThemedPreview {
SingleSelectionListItem(
headline = "Headline",
options = listOptionOf("Option 1", "Option 2", "Option 3"),
onSelectionChanged = {},
)
}
}
@Preview("Single selection List item - no selection, supporting text", group = PreviewGroup.ListItems)
@Composable
internal fun SingleSelectionListItemUnselectedWithSupportingTextPreview() {
ElementThemedPreview {
SingleSelectionListItem(
headline = "Headline",
options = listOptionOf("Option 1", "Option 2", "Option 3"),
supportingText = "Supporting text",
onSelectionChanged = {},
)
}
}
@Preview("Single selection List item - selection in supporting text", group = PreviewGroup.ListItems)
@Composable
internal fun SingleSelectionListItemSelectedInSupportingTextPreview() {
ElementThemedPreview {
SingleSelectionListItem(
headline = "Headline",
options = listOptionOf("Option 1", "Option 2", "Option 3"),
supportingText = "Supporting text",
onSelectionChanged = {},
selected = 1,
)
}
}
@Preview("Single selection List item - selection in trailing content", group = PreviewGroup.ListItems)
@Composable
internal fun SingleSelectionListItemSelectedInTrailingContentPreview() {
ElementThemedPreview {
SingleSelectionListItem(
headline = "Headline",
options = listOptionOf("Option 1", "Option 2", "Option 3"),
supportingText = "Supporting text",
onSelectionChanged = {},
selected = 1,
displayResultInTrailingContent = true,
)
}
}
@Preview("Single selection List item - custom formatter", group = PreviewGroup.ListItems)
@Composable
internal fun SingleSelectionListItemCustomFormattertPreview() {
ElementThemedPreview {
SingleSelectionListItem(
headline = "Headline",
options = listOptionOf("Option 1", "Option 2", "Option 3"),
supportingText = "Supporting text",
onSelectionChanged = {},
resultFormatter = { "Selected index: $it"},
selected = 1,
displayResultInTrailingContent = true,
)
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.components.list
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun SwitchListItem(
headline: String,
value: Boolean,
onChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
supportingText: String? = null,
leadingContent: ListItemContent? = null,
enabled: Boolean = true,
style: ListItemStyle = ListItemStyle.Default,
) {
ListItem(
modifier = modifier,
headlineContent = { Text(headline) },
supportingContent = supportingText?.let { @Composable { Text(it) } },
leadingContent = leadingContent,
trailingContent = ListItemContent.Switch(value, null, enabled),
style = style,
enabled = enabled,
onClick = { onChange(!value) },
)
}

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.designsystem.theme.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -55,11 +56,49 @@ internal fun SimpleAlertDialogContent(
onCancelClicked: () -> Unit,
modifier: Modifier = Modifier,
title: String? = null,
subtitle: @Composable (() -> Unit)? = null,
submitText: String? = null,
onSubmitClicked: () -> Unit = {},
thirdButtonText: String? = null,
onThirdButtonClicked: () -> Unit = {},
applyPaddingToContents: Boolean = true,
icon: @Composable (() -> Unit)? = null,
) {
SimpleAlertDialogContent(
content = {
Text(
text = content,
style = ElementTheme.materialTypography.bodyMedium,
)
},
cancelText = cancelText,
onCancelClicked = onCancelClicked,
modifier = modifier,
title = title,
subtitle = subtitle,
submitText = submitText,
onSubmitClicked = onSubmitClicked,
thirdButtonText = thirdButtonText,
onThirdButtonClicked = onThirdButtonClicked,
icon = icon,
applyPaddingToContents = applyPaddingToContents,
)
}
@Composable
internal fun SimpleAlertDialogContent(
cancelText: String,
onCancelClicked: () -> Unit,
modifier: Modifier = Modifier,
title: String? = null,
subtitle: @Composable (() -> Unit)? = null,
submitText: String? = null,
onSubmitClicked: () -> Unit = {},
thirdButtonText: String? = null,
onThirdButtonClicked: () -> Unit = {},
applyPaddingToContents: Boolean = true,
icon: @Composable (() -> Unit)? = null,
content: @Composable () -> Unit,
) {
AlertDialogContent(
buttons = {
@ -99,12 +138,8 @@ internal fun SimpleAlertDialogContent(
)
}
},
text = {
Text(
text = content,
style = ElementTheme.materialTypography.bodyMedium,
)
},
subtitle = subtitle,
content = content,
shape = DialogContentDefaults.shape,
containerColor = DialogContentDefaults.containerColor,
iconContentColor = DialogContentDefaults.iconContentColor,
@ -117,6 +152,7 @@ internal fun SimpleAlertDialogContent(
// TextButtons will not consume this provided content color value, and will used their
// own defined or default colors.
buttonContentColor = MaterialTheme.colorScheme.primary,
applyPaddingToContents = applyPaddingToContents,
)
}
@ -128,7 +164,8 @@ internal fun AlertDialogContent(
buttons: @Composable () -> Unit,
icon: (@Composable () -> Unit)?,
title: (@Composable () -> Unit)?,
text: @Composable (() -> Unit)?,
subtitle: @Composable (() -> Unit)?,
content: @Composable (() -> Unit)?,
shape: Shape,
containerColor: Color,
tonalElevation: Dp,
@ -137,6 +174,7 @@ internal fun AlertDialogContent(
titleContentColor: Color,
textContentColor: Color,
modifier: Modifier = Modifier,
applyPaddingToContents: Boolean = true,
) {
Surface(
modifier = modifier,
@ -145,12 +183,21 @@ internal fun AlertDialogContent(
tonalElevation = tonalElevation,
) {
Column(
modifier = Modifier.padding(DialogContentDefaults.externalPadding)
modifier = Modifier.padding(
if (applyPaddingToContents) {
// We can just apply the same padding to the whole dialog contents
DialogContentDefaults.externalPadding
} else {
// We should only apply vertical padding in this case, every component will apply the horizontal content individually
DialogContentDefaults.externalVerticalPadding
}
)
) {
icon?.let {
CompositionLocalProvider(LocalContentColor provides iconContentColor) {
Box(
Modifier
.then(if (applyPaddingToContents) Modifier else Modifier.padding(DialogContentDefaults.externalHorizontalPadding))
.padding(DialogContentDefaults.iconPadding)
.align(Alignment.CenterHorizontally)
) {
@ -165,6 +212,12 @@ internal fun AlertDialogContent(
Box(
// Align the title to the center when an icon is present.
Modifier
.then(
if (applyPaddingToContents)
Modifier
else
Modifier.padding(DialogContentDefaults.externalHorizontalPadding)
)
.padding(DialogContentDefaults.titlePadding)
.align(
if (icon == null) {
@ -179,23 +232,28 @@ internal fun AlertDialogContent(
}
}
}
text?.let {
subtitle?.invoke()
content?.let {
CompositionLocalProvider(LocalContentColor provides textContentColor) {
val textStyle =
MaterialTheme.typography.bodyMedium
val textStyle = MaterialTheme.typography.bodyMedium
ProvideTextStyle(textStyle) {
Box(
Modifier
.weight(weight = 1f, fill = false)
// We don't apply padding here if it wasn't applied to the root component, this allows us to have a full width content
.padding(DialogContentDefaults.textPadding)
.align(Alignment.Start)
) {
text()
content()
}
}
}
}
Box(modifier = Modifier.align(Alignment.End)) {
Box(
modifier = Modifier
.then(if (applyPaddingToContents) Modifier else Modifier.padding(DialogContentDefaults.externalHorizontalPadding))
.align(Alignment.End)
) {
CompositionLocalProvider(LocalContentColor provides buttonContentColor) {
val textStyle =
MaterialTheme.typography.labelLarge
@ -304,6 +362,7 @@ private fun AlertDialogFlowRow(
internal fun DialogPreview(content: @Composable () -> Unit) {
Box(
modifier = Modifier
.background(ElementTheme.materialColors.onSurfaceVariant)
.sizeIn(minWidth = DialogMinWidth, maxWidth = DialogMaxWidth)
.padding(20.dp),
propagateMinConstraints = true
@ -313,8 +372,11 @@ internal fun DialogPreview(content: @Composable () -> Unit) {
}
internal object DialogContentDefaults {
private val externalPaddingDp = 24.dp
val shape = RoundedCornerShape(12.dp)
val externalPadding = PaddingValues(all = 24.dp)
val externalPadding = PaddingValues(all = externalPaddingDp)
val externalHorizontalPadding = PaddingValues(horizontal = externalPaddingDp)
val externalVerticalPadding = PaddingValues(vertical = externalPaddingDp)
val titlePadding = PaddingValues(bottom = 16.dp)
val iconPadding = PaddingValues(bottom = 8.dp)
val textPadding = PaddingValues(bottom = 16.dp)

View file

@ -24,6 +24,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
@ -123,6 +124,21 @@ fun Icon(
)
}
@Composable
fun Icon(
painter: Painter,
contentDescription: String?,
modifier: Modifier = Modifier,
tint: Color = LocalContentColor.current,
) {
androidx.compose.material3.Icon(
painter = painter,
contentDescription = contentDescription,
modifier = modifier,
tint = tint
)
}
@Preview(group = PreviewGroup.Icons)
@Composable
internal fun IconImageVectorPreview() =

View file

@ -0,0 +1,89 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.theme.components
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.RadioButtonUnchecked
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.IconToggleButtonColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
@Composable
fun IconToggleButton(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
colors: IconToggleButtonColors = IconButtonDefaults.iconToggleButtonColors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable () -> Unit
) {
androidx.compose.material3.IconToggleButton(
checked = checked,
onCheckedChange = onCheckedChange,
modifier = modifier,
enabled = enabled,
colors = colors,
interactionSource = interactionSource,
content = content,
)
}
@Preview(group = PreviewGroup.Toggles)
@Composable
internal fun IconToggleButtonPreview() = ElementThemedPreview(vertical = false) { ContentToPreview() }
@Composable
private fun ContentToPreview() {
var checked by remember { mutableStateOf(false) }
Column {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
val icon: @Composable () -> Unit = {
Icon(
imageVector = if (checked) Icons.Default.CheckCircle else Icons.Default.RadioButtonUnchecked,
contentDescription = "IconToggleButton"
)
}
IconToggleButton(checked = checked, enabled = true, onCheckedChange = { checked = !checked }, content = icon)
IconToggleButton(checked = checked, enabled = false, onCheckedChange = { checked = !checked }, content = icon)
}
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
val icon: @Composable () -> Unit = {
Icon(
imageVector = if (!checked) Icons.Default.CheckCircle else Icons.Default.RadioButtonUnchecked,
contentDescription = "IconToggleButton"
)
}
IconToggleButton(checked = !checked, enabled = true, onCheckedChange = { checked = !checked }, content = icon)
IconToggleButton(checked = !checked, enabled = false, onCheckedChange = { checked = !checked }, content = icon)
}
}
}

View file

@ -19,6 +19,7 @@ package io.element.android.libraries.designsystem.theme.components
import androidx.compose.foundation.clickable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Share
import androidx.compose.material3.ListItemColors
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
@ -29,9 +30,11 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.theme.ElementTheme
@ -55,35 +58,27 @@ fun ListItem(
headlineContent: @Composable () -> Unit,
modifier: Modifier = Modifier,
supportingContent: @Composable (() -> Unit)? = null,
leadingContent: @Composable (() -> Unit)? = null,
trailingContent: @Composable (() -> Unit)? = null,
leadingContent: ListItemContent? = null,
trailingContent: ListItemContent? = null,
style: ListItemStyle = ListItemStyle.Default,
enabled: Boolean = true,
onClick: (() -> Unit)? = null,
) {
val headlineColor = if (enabled) when (style) {
ListItemStyle.Destructive -> ElementTheme.colors.textCriticalPrimary
else -> ElementTheme.colors.textPrimary
} else {
// We cannot apply a disabled color by default: https://issuetracker.google.com/issues/280480132
ElementTheme.colors.textDisabled
}
val colors = ListItemDefaults.colors(
containerColor = Color.Transparent,
headlineColor = style.headlineColor(),
leadingIconColor = style.leadingIconColor(),
trailingIconColor = style.trailingIconColor(),
supportingColor = style.supportingTextColor(),
disabledHeadlineColor = ListItemDefaultColors.headlineDisabled,
disabledLeadingIconColor = ListItemDefaultColors.iconDisabled,
disabledTrailingIconColor = ListItemDefaultColors.iconDisabled,
)
val supportingContentColor = if (enabled) {
ElementTheme.materialColors.onSurfaceVariant
} else {
// We cannot apply a disabled color by default: https://issuetracker.google.com/issues/280480132
ElementTheme.colors.textDisabled
}
val leadingTrailingContentColor = if (enabled) when (style) {
ListItemStyle.Primary -> ElementTheme.colors.iconPrimary
ListItemStyle.Destructive -> ElementTheme.colors.iconCriticalPrimary
else -> ElementTheme.colors.iconTertiary
} else {
// We cannot apply a disabled color by default: https://issuetracker.google.com/issues/280480132
ElementTheme.colors.iconDisabled
}
// We cannot just pass the disabled colors, they must be set manually: https://issuetracker.google.com/issues/280480132
val headlineColor = if (enabled) colors.headlineColor else colors.disabledHeadlineColor
val leadingContentColor = if (enabled) colors.leadingIconColor else colors.disabledLeadingIconColor
val trailingContentColor = if (enabled) colors.trailingIconColor else colors.disabledTrailingIconColor
val decoratedHeadlineContent: @Composable () -> Unit = {
CompositionLocalProvider(
@ -97,7 +92,6 @@ fun ListItem(
{
CompositionLocalProvider(
LocalTextStyle provides ElementTheme.materialTypography.bodyMedium,
LocalContentColor provides supportingContentColor,
) {
content()
}
@ -106,31 +100,31 @@ fun ListItem(
val decoratedLeadingContent: (@Composable () -> Unit)? = leadingContent?.let { content ->
{
CompositionLocalProvider(
LocalContentColor provides leadingTrailingContentColor,
LocalContentColor provides leadingContentColor,
) {
content()
content.View()
}
}
}
val decoratedTrailingContent: (@Composable () -> Unit)? = trailingContent?.let { content ->
{
CompositionLocalProvider(
LocalContentColor provides leadingTrailingContentColor,
LocalTextStyle provides ElementTheme.typography.fontBodyMdRegular,
LocalContentColor provides trailingContentColor,
) {
content()
content.View()
}
}
}
androidx.compose.material3.ListItem(
headlineContent = decoratedHeadlineContent,
modifier = modifier.clickable(enabled = enabled && onClick != null, onClick = onClick ?: {}),
modifier = if (onClick != null) Modifier.clickable(enabled = enabled, onClick = onClick).then(modifier) else modifier,
overlineContent = null,
supportingContent = decoratedSupportingContent,
leadingContent = decoratedLeadingContent,
trailingContent = decoratedTrailingContent,
colors = ListItemDefaults.colors(), // These aren't really used since we need the workaround for the disabled state color
colors = colors,
tonalElevation = 0.dp,
shadowElevation = 0.dp,
)
@ -143,6 +137,47 @@ sealed interface ListItemStyle {
data object Default : ListItemStyle
data object Primary: ListItemStyle
data object Destructive : ListItemStyle
@Composable fun headlineColor() = when (this) {
Default, Primary -> ListItemDefaultColors.headline
Destructive -> ElementTheme.colors.textCriticalPrimary
}
@Composable fun supportingTextColor() = when (this) {
Default, Primary -> ListItemDefaultColors.supportingText
// FIXME once we have a defined color for this value
Destructive -> ElementTheme.colors.textCriticalPrimary.copy(alpha = 0.8f)
}
@Composable fun leadingIconColor() = when (this) {
Default -> ListItemDefaultColors.icon
Primary -> ElementTheme.colors.iconPrimary
Destructive -> ElementTheme.colors.iconCriticalPrimary
}
@Composable fun trailingIconColor() = when (this) {
Default -> ListItemDefaultColors.icon
Primary -> ElementTheme.colors.iconPrimary
Destructive -> ElementTheme.colors.iconCriticalPrimary
}
}
object ListItemDefaultColors {
val headline: Color @Composable get() = ElementTheme.colors.textPrimary
val headlineDisabled: Color @Composable get() = ElementTheme.colors.textDisabled
val supportingText: Color @Composable get() = ElementTheme.materialColors.onSurfaceVariant
val icon: Color @Composable get() = ElementTheme.colors.iconTertiary
val iconDisabled: Color @Composable get() = ElementTheme.colors.iconDisabled
val colors: ListItemColors @Composable get() = ListItemDefaults.colors(
headlineColor = headline,
supportingColor = supportingText,
leadingIconColor = icon,
trailingIconColor = icon,
disabledHeadlineColor = headlineDisabled,
disabledLeadingIconColor = iconDisabled,
disabledTrailingIconColor = iconDisabled,
)
}
// region: Simple list item
@ -335,8 +370,9 @@ private object PreviewItems {
@Composable
fun ThreeLinesListItemPreview(
modifier: Modifier = Modifier,
leadingContent: @Composable (() -> Unit)? = null,
trailingContent: @Composable (() -> Unit)? = null,
style: ListItemStyle = ListItemStyle.Default,
leadingContent: ListItemContent? = null,
trailingContent: ListItemContent? = null,
) {
ElementThemedPreview {
ListItem(
@ -344,6 +380,7 @@ private object PreviewItems {
supportingContent = PreviewItems.text(),
leadingContent = leadingContent,
trailingContent = trailingContent,
style = style,
modifier = modifier,
)
}
@ -352,8 +389,9 @@ private object PreviewItems {
@Composable
fun TwoLinesListItemPreview(
modifier: Modifier = Modifier,
leadingContent: @Composable (() -> Unit)? = null,
trailingContent: @Composable (() -> Unit)? = null,
style: ListItemStyle = ListItemStyle.Default,
leadingContent: ListItemContent? = null,
trailingContent: ListItemContent? = null,
) {
ElementThemedPreview {
ListItem(
@ -361,6 +399,7 @@ private object PreviewItems {
supportingContent = PreviewItems.textSingleLine(),
leadingContent = leadingContent,
trailingContent = trailingContent,
style = style,
modifier = modifier,
)
}
@ -369,9 +408,9 @@ private object PreviewItems {
@Composable
fun OneLineListItemPreview(
modifier: Modifier = Modifier,
leadingContent: @Composable (() -> Unit)? = null,
trailingContent: @Composable (() -> Unit)? = null,
style: ListItemStyle = ListItemStyle.Default,
leadingContent: ListItemContent? = null,
trailingContent: ListItemContent? = null,
enabled: Boolean = true,
) {
ElementThemedPreview {
@ -402,25 +441,22 @@ private object PreviewItems {
}
@Composable
fun checkbox() = @Composable {
fun checkbox(): ListItemContent {
var checked by remember { mutableStateOf(false) }
Checkbox(checked = checked, onCheckedChange = { checked = !checked })
return ListItemContent.Checkbox(checked = checked, onChange = { checked = !checked })
}
@Composable
fun radioButton() = @Composable {
fun radioButton(): ListItemContent {
var checked by remember { mutableStateOf(false) }
RadioButton(selected = checked, onClick = { checked = !checked })
return ListItemContent.RadioButton(selected = checked, onClick = { checked = !checked })
}
@Composable
fun switch() = @Composable {
fun switch() : ListItemContent {
var checked by remember { mutableStateOf(false) }
Switch(checked = checked, onCheckedChange = { checked = !checked })
return ListItemContent.Switch(checked = checked, onChange = { checked = !checked })
}
@Composable
fun icon() = @Composable {
Icon(imageVector = Icons.Outlined.Share, contentDescription = null)
}
fun icon() = ListItemContent.Icon(iconSource = IconSource.Vector(Icons.Outlined.Share))
}

View file

@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.outlined.Share
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.runtime.Composable
@ -32,6 +33,7 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.theme.ElementTheme
@ -249,7 +251,7 @@ internal fun ListSupportingTextDefaultPaddingPreview() {
internal fun ListSupportingTextSmallPaddingPreview() {
ElementThemedPreview {
Column {
ListItem(headlineContent = { Text("A title") }, leadingContent = { Icon(Icons.Default.Share, null) })
ListItem(headlineContent = { Text("A title") }, leadingContent = ListItemContent.Icon(IconSource.Vector(Icons.Outlined.Share)))
ListSupportingText(
text = "Supporting line text lorem ipsum dolor sit amet, consectetur. Read more",
contentPadding = ListSupportingTextDefaults.Padding.SmallLeadingContent,
@ -263,7 +265,7 @@ internal fun ListSupportingTextSmallPaddingPreview() {
internal fun ListSupportingTextLargePaddingPreview() {
ElementThemedPreview {
Column {
ListItem(headlineContent = { Text("A title") }, leadingContent = { Switch(checked = true, onCheckedChange = null) })
ListItem(headlineContent = { Text("A title") }, leadingContent = ListItemContent.Switch(checked = true, onChange = {}))
ListSupportingText(
text = "Supporting line text lorem ipsum dolor sit amet, consectetur. Read more",
contentPadding = ListSupportingTextDefaults.Padding.LargeLeadingContent,

View file

@ -23,7 +23,10 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.material3.RadioButtonColors
import androidx.compose.material3.RadioButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -67,14 +70,15 @@ internal fun RadioButtonPreview() = ElementThemedPreview(vertical = false) { Con
@Composable
private fun ContentToPreview() {
var checked by remember { mutableStateOf(false) }
Column {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
RadioButton(selected = false, onClick = {})
RadioButton(selected = false, enabled = false, onClick = {})
RadioButton(selected = checked, enabled = true, onClick = { checked = !checked })
RadioButton(selected = checked, enabled = false, onClick = { checked = !checked })
}
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
RadioButton(selected = true, onClick = {})
RadioButton(selected = true, enabled = false, onClick = {})
RadioButton(selected = !checked, enabled = true, onClick = { checked = !checked })
RadioButton(selected = !checked, enabled = false, onClick = { checked = !checked })
}
}
}

View file

@ -50,7 +50,8 @@ private fun ContentToPreview() {
buttons = { /*TODO*/ },
icon = { /*TODO*/ },
title = { /*TODO*/ },
text = { DatePicker(state = state, showModeToggle = true) },
subtitle = null,
content = { DatePicker(state = state, showModeToggle = true) },
shape = AlertDialogDefaults.shape,
containerColor = AlertDialogDefaults.containerColor,
tonalElevation = AlertDialogDefaults.TonalElevation,

View file

@ -39,7 +39,8 @@ internal fun TimePickerHorizontalPreview() {
buttons = { /*TODO*/ },
icon = { /*TODO*/ },
title = { /*TODO*/ },
text = { TimePicker(state = rememberTimePickerState(), layoutType = TimePickerLayoutType.Horizontal) },
subtitle = null,
content = { TimePicker(state = rememberTimePickerState(), layoutType = TimePickerLayoutType.Horizontal) },
shape = AlertDialogDefaults.shape,
containerColor = AlertDialogDefaults.containerColor,
tonalElevation = AlertDialogDefaults.TonalElevation,
@ -60,7 +61,8 @@ internal fun TimePickerVerticalPreviewLight() {
buttons = { /*TODO*/ },
icon = { /*TODO*/ },
title = { /*TODO*/ },
text = { TimePicker(state = rememberTimePickerState(), layoutType = TimePickerLayoutType.Vertical) },
subtitle = null,
content = { TimePicker(state = rememberTimePickerState(), layoutType = TimePickerLayoutType.Vertical) },
shape = AlertDialogDefaults.shape,
containerColor = AlertDialogDefaults.containerColor,
tonalElevation = AlertDialogDefaults.TonalElevation,
@ -85,7 +87,8 @@ internal fun TimePickerVerticalPreviewDark() {
buttons = { /*TODO*/ },
icon = { /*TODO*/ },
title = { /*TODO*/ },
text = { TimePicker(state = pickerState, layoutType = TimePickerLayoutType.Vertical) },
subtitle = null,
content = { TimePicker(state = pickerState, layoutType = TimePickerLayoutType.Vertical) },
shape = AlertDialogDefaults.shape,
containerColor = AlertDialogDefaults.containerColor,
tonalElevation = AlertDialogDefaults.TonalElevation,

View file

@ -95,7 +95,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
is StateContent -> {
stateContentFormatter.format(content, senderDisplayName, isOutgoing, RenderingMode.RoomList)
}
is PollContent,
is PollContent, // TODO Polls: handle last message
is FailedToParseMessageLikeContent, is FailedToParseStateContent, is UnknownContent -> {
prefixIfNeeded(sp.getString(CommonStrings.common_unsupported_event), senderDisplayName, isDmRoom)
}

View file

@ -20,5 +20,8 @@ enum class PollKind {
Disclosed,
/** Results should be only revealed when the poll is ended. */
Undisclosed
Undisclosed,
}
val PollKind.isDisclosed: Boolean
get() = this == PollKind.Disclosed

View file

@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import kotlinx.coroutines.flow.StateFlow
@ -141,6 +142,21 @@ interface MatrixRoom : Closeable {
assetType: AssetType? = null,
): Result<Unit>
/**
* Create a poll in the room.
*
* @param question The question to ask.
* @param answers The list of answers.
* @param maxSelections The maximum number of answers that can be selected.
* @param pollKind The kind of poll to create.
*/
suspend fun createPoll(
question: String,
answers: List<String>,
maxSelections: Int,
pollKind: PollKind,
): Result<Unit>
override fun close() = destroy()
}

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.api.verification
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
interface SessionVerificationService {
@ -37,6 +38,11 @@ interface SessionVerificationService {
*/
val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus>
/**
* Returns whether the current session needs to be verified and the SDK is ready to start the verification.
*/
val canVerifySessionFlow: Flow<Boolean>
/**
* Request verification of the current session.
*/

View file

@ -35,7 +35,6 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@ -93,8 +92,8 @@ class RustMatrixClient constructor(
private val innerRoomListService = syncService.roomListService()
private val sessionDispatcher = dispatchers.io.limitedParallelism(64)
private val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-${sessionId}")
private val verificationService = RustSessionVerificationService()
private val rustSyncService = RustSyncService(syncService, sessionCoroutineScope)
private val verificationService = RustSessionVerificationService(rustSyncService)
private val pushersService = RustPushersService(
client = client,
dispatchers = dispatchers,
@ -149,13 +148,11 @@ class RustMatrixClient constructor(
init {
client.setDelegate(clientDelegate)
rustSyncService.syncState
.onEach { syncState ->
if (syncState == SyncState.Running) {
onSlidingSyncUpdate()
}
roomListService.state.onEach { state ->
if (state == RoomListService.State.Running) {
setupVerificationControllerIfNeeded()
}
.launchIn(sessionCoroutineScope)
}.launchIn(sessionCoroutineScope)
}
override suspend fun getRoom(roomId: RoomId): MatrixRoom? = withContext(sessionDispatcher) {
@ -338,8 +335,8 @@ class RustMatrixClient constructor(
}
}
private fun onSlidingSyncUpdate() {
if (!verificationService.isReady.value) {
private fun setupVerificationControllerIfNeeded() {
if (verificationService.verificationController == null) {
try {
verificationService.verificationController = client.getSessionVerificationController()
} catch (e: Throwable) {

View file

@ -23,3 +23,8 @@ fun RustPollKind.map(): PollKind = when (this) {
RustPollKind.DISCLOSED -> PollKind.Disclosed
RustPollKind.UNDISCLOSED -> PollKind.Undisclosed
}
fun PollKind.toInner(): RustPollKind = when (this) {
PollKind.Disclosed -> RustPollKind.DISCLOSED
PollKind.Undisclosed -> RustPollKind.UNDISCLOSED
}

View file

@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MessageEventType
@ -41,6 +42,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl
import io.element.android.libraries.matrix.impl.media.map
import io.element.android.libraries.matrix.impl.poll.toInner
import io.element.android.libraries.matrix.impl.room.location.toInner
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
import io.element.android.libraries.matrix.impl.util.destroyAll
@ -378,7 +380,24 @@ class RustMatrixRoom(
description = description,
zoomLevel = zoomLevel?.toUByte(),
assetType = assetType?.toInner(),
txnId = genTransactionId()
txnId = genTransactionId(),
)
}
}
override suspend fun createPoll(
question: String,
answers: List<String>,
maxSelections: Int,
pollKind: PollKind,
): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.createPoll(
question = question,
answers = answers,
maxSelections = maxSelections.toUByte(),
pollKind = pollKind.toInner(),
txnId = genTransactionId(),
)
}
}

View file

@ -17,20 +17,25 @@
package io.element.android.libraries.matrix.impl.verification
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.impl.sync.RustSyncService
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import org.matrix.rustcomponents.sdk.SessionVerificationController
import org.matrix.rustcomponents.sdk.SessionVerificationControllerDelegate
import org.matrix.rustcomponents.sdk.SessionVerificationControllerInterface
import org.matrix.rustcomponents.sdk.SessionVerificationEmoji
import javax.inject.Inject
class RustSessionVerificationService @Inject constructor() : SessionVerificationService, SessionVerificationControllerDelegate {
class RustSessionVerificationService @Inject constructor(
private val syncService: RustSyncService,
) : SessionVerificationService, SessionVerificationControllerDelegate {
var verificationController: SessionVerificationControllerInterface? = null
set(value) {
@ -52,6 +57,10 @@ class RustSessionVerificationService @Inject constructor() : SessionVerification
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus.asStateFlow()
override val canVerifySessionFlow = combine(sessionVerifiedStatus, syncService.syncState) { verificationStatus, syncState ->
syncState == SyncState.Running && verificationStatus == SessionVerifiedStatus.NotVerified
}
override suspend fun requestVerification() = tryOrFail {
verificationController?.requestVerification()
}

View file

@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MessageEventType
@ -83,6 +84,7 @@ class FakeMatrixRoom(
private var forwardEventResult = Result.success(Unit)
private var reportContentResult = Result.success(Unit)
private var sendLocationResult = Result.success(Unit)
private var createPollResult = Result.success(Unit)
private var progressCallbackValues = emptyList<Pair<Long, Long>>()
val editMessageCalls = mutableListOf<String>()
@ -104,6 +106,9 @@ class FakeMatrixRoom(
private val _sentLocations = mutableListOf<SendLocationInvocation>()
val sentLocations: List<SendLocationInvocation> = _sentLocations
private val _createPollInvocations = mutableListOf<CreatePollInvocation>()
val createPollInvocations: List<CreatePollInvocation> = _createPollInvocations
var invitedUserId: UserId? = null
private set
@ -305,6 +310,16 @@ class FakeMatrixRoom(
return sendLocationResult
}
override suspend fun createPoll(
question: String,
answers: List<String>,
maxSelections: Int,
pollKind: PollKind
): Result<Unit> = simulateLongTask {
_createPollInvocations.add(CreatePollInvocation(question, answers, maxSelections, pollKind))
return createPollResult
}
fun givenLeaveRoomError(throwable: Throwable?) {
this.leaveRoomError = throwable
}
@ -397,6 +412,10 @@ class FakeMatrixRoom(
sendLocationResult = result
}
fun givenCreatePollResult(result: Result<Unit>) {
createPollResult = result
}
fun givenProgressCallbackValues(values: List<Pair<Long, Long>>) {
progressCallbackValues = values
}
@ -409,3 +428,10 @@ data class SendLocationInvocation(
val zoomLevel: Int?,
val assetType: AssetType?,
)
data class CreatePollInvocation(
val question: String,
val answers: List<String>,
val maxSelections: Int,
val pollKind: PollKind,
)

View file

@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationS
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -27,13 +28,13 @@ class FakeSessionVerificationService : SessionVerificationService {
private val _isReady = MutableStateFlow(false)
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
private var _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
private var _canVerifySessionFlow = MutableStateFlow(true)
private var emojiList = emptyList<VerificationEmoji>()
var shouldFail = false
override val verificationFlowState: StateFlow<VerificationFlowState>
get() = _verificationFlowState
override val verificationFlowState: StateFlow<VerificationFlowState> =_verificationFlowState
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus
override val canVerifySessionFlow: Flow<Boolean> = _canVerifySessionFlow
override val isReady: StateFlow<Boolean> = _isReady
@ -77,6 +78,10 @@ class FakeSessionVerificationService : SessionVerificationService {
_verificationFlowState.value = state
}
fun givenCanVerifySession(canVerify: Boolean) {
_canVerifySessionFlow.value = canVerify
}
fun givenIsReady(value: Boolean) {
_isReady.value = value
}

View file

@ -20,6 +20,8 @@ import android.content.Context
import android.net.Uri
import com.otaliastudios.transcoder.Transcoder
import com.otaliastudios.transcoder.TranscoderListener
import com.otaliastudios.transcoder.resize.AtMostResizer
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
import io.element.android.libraries.androidutils.file.createTmpFile
import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.di.ApplicationContext
@ -35,6 +37,11 @@ class VideoCompressor @Inject constructor(
fun compress(uri: Uri) = callbackFlow {
val tmpFile = context.createTmpFile(extension = "mp4")
val future = Transcoder.into(tmpFile.path)
.setVideoTrackStrategy(
DefaultVideoStrategy.Builder()
.addResizer(AtMostResizer(1920, 1080))
.build()
)
.addDataSource(context, uri)
.setListener(object : TranscoderListener {
override fun onTranscodeProgress(progress: Double) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -182,10 +182,21 @@
<string name="screen_migration_title">"Nastavenie vášho účtu."</string>
<string name="screen_notification_settings_additional_settings_section_title">"Ďalšie nastavenia"</string>
<string name="screen_notification_settings_calls_label">"Audio a video hovory"</string>
<string name="screen_notification_settings_configuration_mismatch">"Nezhoda konfigurácie"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Zjednodušili sme Nastavenia oznámení, aby ste ľahšie našli možnosti.
Niektoré vlastné nastavenia, ktoré ste si nastavili v minulosti, sa tu nezobrazujú, ale sú stále aktívne.
Ak budete pokračovať, niektoré z vašich nastavení sa môžu zmeniť."</string>
<string name="screen_notification_settings_direct_chats">"Priame konverzácie"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Vlastné nastavenie pre konverzácie"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Pri aktualizácii nastavenia oznámenia došlo k chybe."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Všetky správy"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Iba zmienky a kľúčové slová"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"Pri priamych rozhovoroch ma upozorniť na"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Pri skupinových rozhovoroch ma upozorniť na"</string>
<string name="screen_notification_settings_enable_notifications">"Povoliť oznámenia na tomto zariadení"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Konfigurácia nebola opravená, skúste to prosím znova."</string>
<string name="screen_notification_settings_group_chats">"Skupinové rozhovory"</string>
<string name="screen_notification_settings_mentions_section_title">"Zmienky"</string>
<string name="screen_notification_settings_mode_all">"Všetky"</string>
@ -197,6 +208,7 @@
<string name="screen_notification_settings_system_notifications_turned_off">"Systémové oznámenia sú vypnuté"</string>
<string name="screen_notification_settings_title">"Oznámenia"</string>
<string name="screen_report_content_block_user_hint">"Označte, či chcete skryť všetky aktuálne a budúce správy od tohto používateľa"</string>
<string name="screen_settings_oidc_account">"Účet a zariadenia"</string>
<string name="screen_share_location_title">"Zdieľať polohu"</string>
<string name="screen_share_my_location_action">"Zdieľať moju polohu"</string>
<string name="screen_share_open_apple_maps">"Otvoriť v Apple Maps"</string>

View file

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