Integrate Element Call with widget API (#1581)
* Integrate Element Call with widget API. - Add `appconfig` module and extract constants that can be overridden in forks there. - Add an Element Call feature flag, disabled by default. - Refactor the whole `ElementCallActivity`, move most logic out of it. - Integrate with the Rust Widget Driver API (note the Rust SDK version used in this PR lacks some needed changes to make the calls actually work). - Handle calls differently based on `CallType`. - Add UI to create/join a call. --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
a814c4a95a
commit
46f78ef700
102 changed files with 2202 additions and 166 deletions
|
|
@ -21,6 +21,7 @@ import android.content.ContextWrapper
|
|||
import com.bumble.appyx.core.node.Node
|
||||
import io.element.android.libraries.di.DaggerComponentOwner
|
||||
|
||||
inline fun <reified T : Any> Node.optionalBindings() = optionalBindings(T::class.java)
|
||||
inline fun <reified T : Any> Node.bindings() = bindings(T::class.java)
|
||||
inline fun <reified T : Any> Context.bindings() = bindings(T::class.java)
|
||||
|
||||
|
|
@ -36,7 +37,7 @@ fun <T : Any> Context.bindings(klass: Class<T>): T {
|
|||
?: error("Unable to find bindings for ${klass.name}")
|
||||
}
|
||||
|
||||
fun <T : Any> Node.bindings(klass: Class<T>): T {
|
||||
fun <T : Any> Node.optionalBindings(klass: Class<T>): T? {
|
||||
// search dagger components in node hierarchy
|
||||
return generateSequence(this, Node::parent)
|
||||
.filterIsInstance<DaggerComponentOwner>()
|
||||
|
|
@ -44,5 +45,8 @@ fun <T : Any> Node.bindings(klass: Class<T>): T {
|
|||
.flatMap { if (it is Collection<*>) it else listOf(it) }
|
||||
.filterIsInstance(klass)
|
||||
.firstOrNull()
|
||||
?: error("Unable to find bindings for ${klass.name}")
|
||||
}
|
||||
|
||||
fun <T : Any> Node.bindings(klass: Class<T>): T {
|
||||
return optionalBindings(klass) ?: error("Unable to find bindings for ${klass.name}")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ 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.TextFieldListItem
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
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
|
||||
|
|
@ -45,6 +45,7 @@ fun ListDialog(
|
|||
subtitle: String? = null,
|
||||
cancelText: String = stringResource(CommonStrings.action_cancel),
|
||||
submitText: String = stringResource(CommonStrings.action_ok),
|
||||
enabled: Boolean = true,
|
||||
listItems: LazyListScope.() -> Unit,
|
||||
) {
|
||||
val decoratedSubtitle: @Composable (() -> Unit)? = subtitle?.let {
|
||||
|
|
@ -66,6 +67,7 @@ fun ListDialog(
|
|||
submitText = submitText,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onSubmitClicked = onSubmit,
|
||||
enabled = enabled,
|
||||
listItems = listItems,
|
||||
)
|
||||
}
|
||||
|
|
@ -80,6 +82,7 @@ private fun ListDialogContent(
|
|||
submitText: String,
|
||||
modifier: Modifier = Modifier,
|
||||
title: String? = null,
|
||||
enabled: Boolean = true,
|
||||
subtitle: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
SimpleAlertDialogContent(
|
||||
|
|
@ -90,6 +93,7 @@ private fun ListDialogContent(
|
|||
submitText = submitText,
|
||||
onCancelClicked = onDismissRequest,
|
||||
onSubmitClicked = onSubmitClicked,
|
||||
enabled = enabled,
|
||||
applyPaddingToContents = false,
|
||||
) {
|
||||
LazyColumn(
|
||||
|
|
|
|||
|
|
@ -16,10 +16,13 @@
|
|||
|
||||
package io.element.android.libraries.designsystem.components.list
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||
|
|
@ -29,24 +32,68 @@ import io.element.android.libraries.theme.ElementTheme
|
|||
|
||||
@Composable
|
||||
fun TextFieldListItem(
|
||||
placeholder: String,
|
||||
placeholder: String?,
|
||||
text: String,
|
||||
onTextChanged: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
error: String? = null,
|
||||
maxLines: Int = 1,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
) {
|
||||
val textFieldStyle = ElementTheme.materialTypography.bodyLarge
|
||||
|
||||
OutlinedTextField(
|
||||
value = text,
|
||||
onValueChange = onTextChanged,
|
||||
placeholder = { Text(placeholder) },
|
||||
onValueChange = { onTextChanged(it) },
|
||||
placeholder = placeholder?.let { @Composable { Text(it) } },
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
disabledBorderColor = Color.Transparent,
|
||||
errorBorderColor = Color.Transparent,
|
||||
focusedBorderColor = Color.Transparent,
|
||||
unfocusedBorderColor = Color.Transparent,
|
||||
),
|
||||
isError = error != null,
|
||||
supportingText = error?.let { @Composable { Text(it) } },
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
textStyle = textFieldStyle,
|
||||
maxLines = maxLines,
|
||||
singleLine = maxLines == 1,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TextFieldListItem(
|
||||
placeholder: String?,
|
||||
text: TextFieldValue,
|
||||
onTextChanged: (TextFieldValue) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
error: String? = null,
|
||||
maxLines: Int = 1,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
) {
|
||||
val textFieldStyle = ElementTheme.materialTypography.bodyLarge
|
||||
|
||||
OutlinedTextField(
|
||||
value = text,
|
||||
onValueChange = { onTextChanged(it) },
|
||||
placeholder = placeholder?.let { @Composable { Text(it) } },
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
disabledBorderColor = Color.Transparent,
|
||||
errorBorderColor = Color.Transparent,
|
||||
focusedBorderColor = Color.Transparent,
|
||||
unfocusedBorderColor = Color.Transparent,
|
||||
),
|
||||
isError = error != null,
|
||||
supportingText = error?.let { @Composable { Text(it) } },
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
textStyle = textFieldStyle,
|
||||
maxLines = maxLines,
|
||||
singleLine = maxLines == 1,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -74,3 +121,15 @@ internal fun TextFieldListItemPreview() {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview("Text field List item - textfieldvalue", group = PreviewGroup.ListItems)
|
||||
@Composable
|
||||
internal fun TextFieldListItemTextFieldValuePreview() {
|
||||
ElementThemedPreview {
|
||||
TextFieldListItem(
|
||||
placeholder = "Placeholder",
|
||||
text = TextFieldValue("Text field value"),
|
||||
onTextChanged = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* 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.preferences
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ListDialog
|
||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.components.list.TextFieldListItem
|
||||
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 PreferenceTextField(
|
||||
headline: String,
|
||||
onChange: (String?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
placeholder: String? = null,
|
||||
value: String? = null,
|
||||
supportingText: String? = null,
|
||||
displayValue: (String?) -> Boolean = { !it.isNullOrBlank() },
|
||||
trailingContent: ListItemContent? = null,
|
||||
validation: (String?) -> Boolean = { true },
|
||||
onValidationErrorMessage: String? = null,
|
||||
enabled: Boolean = true,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
style: ListItemStyle = ListItemStyle.Default,
|
||||
) {
|
||||
var displayTextFieldDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val valueToDisplay = if (displayValue(value)) { value } else supportingText
|
||||
|
||||
ListItem(
|
||||
modifier = modifier,
|
||||
headlineContent = { Text(headline) },
|
||||
supportingContent = valueToDisplay?.let { @Composable { Text(it) } },
|
||||
trailingContent = trailingContent,
|
||||
style = style,
|
||||
enabled = enabled,
|
||||
onClick = { displayTextFieldDialog = true }
|
||||
)
|
||||
|
||||
if (displayTextFieldDialog) {
|
||||
TextFieldDialog(
|
||||
title = headline,
|
||||
onSubmit = {
|
||||
onChange(it.takeIf { it.isNotBlank() })
|
||||
displayTextFieldDialog = false
|
||||
},
|
||||
onDismissRequest = { displayTextFieldDialog = false },
|
||||
placeholder = placeholder.orEmpty(),
|
||||
value = value.orEmpty(),
|
||||
validation = validation,
|
||||
onValidationErrorMessage = onValidationErrorMessage,
|
||||
keyboardOptions = keyboardOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TextFieldDialog(
|
||||
title: String,
|
||||
onSubmit: (String) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
value: String?,
|
||||
placeholder: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
validation: (String?) -> Boolean = { true },
|
||||
onValidationErrorMessage: String? = null,
|
||||
autoSelectOnDisplay: Boolean = true,
|
||||
maxLines: Int = 1,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
var textFieldContents by rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(value.orEmpty(), selection = TextRange(value.orEmpty().length)))
|
||||
}
|
||||
var error by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val canSubmit by remember { derivedStateOf { validation(textFieldContents.text) } }
|
||||
ListDialog(
|
||||
title = title,
|
||||
onSubmit = { onSubmit(textFieldContents.text) },
|
||||
onDismissRequest = onDismissRequest,
|
||||
enabled = canSubmit,
|
||||
modifier = modifier,
|
||||
) {
|
||||
item {
|
||||
TextFieldListItem(
|
||||
placeholder = placeholder.orEmpty(),
|
||||
text = textFieldContents,
|
||||
onTextChanged = {
|
||||
error = if (!validation(it.text)) onValidationErrorMessage else null
|
||||
textFieldContents = it
|
||||
},
|
||||
error = error,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = KeyboardActions(onAny = {
|
||||
if (validation(textFieldContents.text)) {
|
||||
onSubmit(textFieldContents.text)
|
||||
}
|
||||
}),
|
||||
maxLines = maxLines,
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (autoSelectOnDisplay) {
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -96,6 +96,7 @@ internal fun SimpleAlertDialogContent(
|
|||
thirdButtonText: String? = null,
|
||||
onThirdButtonClicked: () -> Unit = {},
|
||||
applyPaddingToContents: Boolean = true,
|
||||
enabled: Boolean = true,
|
||||
icon: @Composable (() -> Unit)? = null,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
|
|
@ -122,6 +123,7 @@ internal fun SimpleAlertDialogContent(
|
|||
if (submitText != null) {
|
||||
Button(
|
||||
text = submitText,
|
||||
enabled = enabled,
|
||||
size = ButtonSize.Medium,
|
||||
onClick = onSubmitClicked,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -55,4 +55,10 @@ enum class FeatureFlags(
|
|||
description = "Allow user to lock/unlock the app with a pin code or biometrics",
|
||||
defaultValue = false,
|
||||
),
|
||||
InRoomCalls(
|
||||
key = "feature.elementcall",
|
||||
title = "Element call in rooms",
|
||||
description = "Allow user to start or join a call in a room",
|
||||
defaultValue = false,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ class StaticFeatureFlagProvider @Inject constructor() :
|
|||
FeatureFlags.NotificationSettings -> true
|
||||
FeatureFlags.VoiceMessages -> false
|
||||
FeatureFlags.PinUnlock -> false
|
||||
FeatureFlags.InRoomCalls -> false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ anvil {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.appconfig)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(libs.dagger)
|
||||
implementation(projects.libraries.core)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
package io.element.android.libraries.matrix.api.permalink
|
||||
|
||||
import android.net.Uri
|
||||
import io.element.android.libraries.matrix.api.config.MatrixConfiguration
|
||||
import io.element.android.appconfig.MatrixConfiguration
|
||||
|
||||
/**
|
||||
* Mapping of an input URI to a matrix.to compliant URI.
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
package io.element.android.libraries.matrix.api.permalink
|
||||
|
||||
import io.element.android.libraries.matrix.api.config.MatrixConfiguration
|
||||
import io.element.android.appconfig.MatrixConfiguration
|
||||
import io.element.android.libraries.matrix.api.core.MatrixPatterns
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ 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 io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
|
|
@ -192,5 +194,27 @@ interface MatrixRoom : Closeable {
|
|||
progressCallback: ProgressCallback?
|
||||
): Result<MediaUploadHandler>
|
||||
|
||||
/**
|
||||
* Generates a Widget url to display in a [android.webkit.WebView] given the provided parameters.
|
||||
* @param widgetSettings The widget settings to use.
|
||||
* @param clientId The client id to use. It should be unique per app install.
|
||||
* @param languageTag The language tag to use. If null, the default language will be used.
|
||||
* @param theme The theme to use. If null, the default theme will be used.
|
||||
* @return The resulting url, or a failure.
|
||||
*/
|
||||
suspend fun generateWidgetWebViewUrl(
|
||||
widgetSettings: MatrixWidgetSettings,
|
||||
clientId: String,
|
||||
languageTag: String? = null,
|
||||
theme: String? = null,
|
||||
): Result<String>
|
||||
|
||||
/**
|
||||
* Get a [MatrixWidgetDriver] for the provided [widgetSettings].
|
||||
* @param widgetSettings The widget settings to use.
|
||||
* @return The resulting [MatrixWidgetDriver], or a failure.
|
||||
*/
|
||||
fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver>
|
||||
|
||||
override fun close() = destroy()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,13 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.api.config
|
||||
package io.element.android.libraries.matrix.api.widget
|
||||
|
||||
object MatrixConfiguration {
|
||||
const val matrixToPermalinkBaseUrl: String = "https://matrix.to/#/"
|
||||
val clientPermalinkBaseUrl: String? = null
|
||||
import java.util.UUID
|
||||
|
||||
interface CallWidgetSettingsProvider {
|
||||
fun provide(
|
||||
baseUrl: String,
|
||||
widgetId: String = UUID.randomUUID().toString()
|
||||
): MatrixWidgetSettings
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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.matrix.api.widget
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface MatrixWidgetDriver : AutoCloseable {
|
||||
val id: String
|
||||
val incomingMessages: Flow<String>
|
||||
|
||||
suspend fun run()
|
||||
suspend fun send(message: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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.matrix.api.widget
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class MatrixWidgetSettings(
|
||||
val id: String,
|
||||
val initAfterContentLoad: Boolean,
|
||||
val rawUrl: String,
|
||||
) : Parcelable {
|
||||
companion object
|
||||
}
|
||||
|
|
@ -40,6 +40,8 @@ import io.element.android.libraries.matrix.api.room.location.AssetType
|
|||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
||||
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
|
||||
|
|
@ -48,6 +50,8 @@ 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
|
||||
import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver
|
||||
import io.element.android.libraries.matrix.impl.widget.generateWidgetWebViewUrl
|
||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
|
@ -65,6 +69,8 @@ import org.matrix.rustcomponents.sdk.RoomListItem
|
|||
import org.matrix.rustcomponents.sdk.RoomMember
|
||||
import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation
|
||||
import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle
|
||||
import org.matrix.rustcomponents.sdk.WidgetPermissions
|
||||
import org.matrix.rustcomponents.sdk.WidgetPermissionsProvider
|
||||
import org.matrix.rustcomponents.sdk.messageEventContentFromHtml
|
||||
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
|
||||
import timber.log.Timber
|
||||
|
|
@ -478,6 +484,27 @@ class RustMatrixRoom(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun generateWidgetWebViewUrl(
|
||||
widgetSettings: MatrixWidgetSettings,
|
||||
clientId: String,
|
||||
languageTag: String?,
|
||||
theme: String?,
|
||||
) = runCatching {
|
||||
widgetSettings.generateWidgetWebViewUrl(innerRoom, clientId, languageTag, theme)
|
||||
}
|
||||
|
||||
override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver> = runCatching {
|
||||
RustWidgetDriver(
|
||||
widgetSettings = widgetSettings,
|
||||
room = innerRoom,
|
||||
widgetPermissionsProvider = object : WidgetPermissionsProvider {
|
||||
override fun acquirePermissions(permissions: WidgetPermissions): WidgetPermissions {
|
||||
return permissions
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun sendAttachment(files: List<File>, handle: () -> SendAttachmentJoinHandle): Result<MediaUploadHandler> {
|
||||
return runCatching {
|
||||
MediaUploadHandlerImpl(files, handle())
|
||||
|
|
|
|||
|
|
@ -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.matrix.impl.widget
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
||||
import org.matrix.rustcomponents.sdk.VirtualElementCallWidgetOptions
|
||||
import org.matrix.rustcomponents.sdk.newVirtualElementCallWidget
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultCallWidgetSettingsProvider @Inject constructor() : CallWidgetSettingsProvider {
|
||||
override fun provide(baseUrl: String, widgetId: String): MatrixWidgetSettings {
|
||||
val options = VirtualElementCallWidgetOptions(
|
||||
elementCallUrl = baseUrl,
|
||||
widgetId = widgetId,
|
||||
parentUrl = null,
|
||||
hideHeader = null,
|
||||
preload = null,
|
||||
fontScale = null,
|
||||
appPrompt = false,
|
||||
skipLobby = true,
|
||||
confineToRoom = true,
|
||||
fonts = null,
|
||||
analyticsId = null
|
||||
)
|
||||
val rustWidgetSettings = newVirtualElementCallWidget(options)
|
||||
return MatrixWidgetSettings.fromRustWidgetSettings(rustWidgetSettings)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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.matrix.impl.widget
|
||||
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
||||
import org.matrix.rustcomponents.sdk.ClientProperties
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.WidgetSettings
|
||||
import org.matrix.rustcomponents.sdk.generateWebviewUrl
|
||||
|
||||
fun MatrixWidgetSettings.toRustWidgetSettings() = WidgetSettings(
|
||||
id = this.id,
|
||||
initAfterContentLoad = this.initAfterContentLoad,
|
||||
rawUrl = this.rawUrl,
|
||||
)
|
||||
|
||||
fun MatrixWidgetSettings.Companion.fromRustWidgetSettings(widgetSettings: WidgetSettings) = MatrixWidgetSettings(
|
||||
id = widgetSettings.id,
|
||||
initAfterContentLoad = widgetSettings.initAfterContentLoad,
|
||||
rawUrl = widgetSettings.rawUrl,
|
||||
)
|
||||
|
||||
suspend fun MatrixWidgetSettings.generateWidgetWebViewUrl(
|
||||
room: Room,
|
||||
clientId: String,
|
||||
languageTag: String? = null,
|
||||
theme: String? = null
|
||||
) = generateWebviewUrl(
|
||||
widgetSettings = this.toRustWidgetSettings(),
|
||||
room = room,
|
||||
props = ClientProperties(
|
||||
clientId = clientId,
|
||||
languageTag = languageTag,
|
||||
theme = theme,
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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.matrix.impl.widget
|
||||
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.WidgetPermissionsProvider
|
||||
import org.matrix.rustcomponents.sdk.makeWidgetDriver
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
class RustWidgetDriver(
|
||||
widgetSettings: MatrixWidgetSettings,
|
||||
private val room: Room,
|
||||
private val widgetPermissionsProvider: WidgetPermissionsProvider,
|
||||
): MatrixWidgetDriver {
|
||||
|
||||
override val incomingMessages = MutableSharedFlow<String>()
|
||||
|
||||
private val driverAndHandle = makeWidgetDriver(widgetSettings.toRustWidgetSettings())
|
||||
private var receiveMessageJob: Job? = null
|
||||
|
||||
private var isRunning = AtomicBoolean(false)
|
||||
|
||||
override val id: String = widgetSettings.id
|
||||
|
||||
override suspend fun run() {
|
||||
// Don't run the driver if it's already running
|
||||
if (!isRunning.compareAndSet(false, true)) {
|
||||
return
|
||||
}
|
||||
|
||||
val coroutineScope = CoroutineScope(coroutineContext)
|
||||
coroutineScope.launch {
|
||||
// This call will suspend the coroutine while the driver is running, so it needs to be launched separately
|
||||
driverAndHandle.driver.run(room, widgetPermissionsProvider)
|
||||
}
|
||||
receiveMessageJob = coroutineScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
while (isActive) {
|
||||
driverAndHandle.handle.recv()?.let { incomingMessages.emit(it) }
|
||||
}
|
||||
} finally {
|
||||
driverAndHandle.handle.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun send(message: String) {
|
||||
driverAndHandle.handle.send(message)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
receiveMessageJob?.cancel()
|
||||
driverAndHandle.driver.close()
|
||||
}
|
||||
}
|
||||
|
|
@ -208,8 +208,12 @@ class FakeMatrixClient(
|
|||
findDmResult = result
|
||||
}
|
||||
|
||||
fun givenGetRoomResult(roomId: RoomId, result: MatrixRoom) {
|
||||
getRoomResults[roomId] = result
|
||||
fun givenGetRoomResult(roomId: RoomId, result: MatrixRoom?) {
|
||||
if (result == null) {
|
||||
getRoomResults.remove(roomId)
|
||||
} else {
|
||||
getRoomResults[roomId] = result
|
||||
}
|
||||
}
|
||||
|
||||
fun givenSearchUsersResult(searchTerm: String, result: Result<MatrixSearchUserResults>) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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.matrix.test
|
||||
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
class FakeMatrixClientProvider(
|
||||
private val getClient: (SessionId) -> Result<MatrixClient> = { Result.success(FakeMatrixClient()) }
|
||||
) : MatrixClientProvider {
|
||||
override suspend fun getOrRestore(sessionId: SessionId): Result<MatrixClient> = getClient(sessionId)
|
||||
}
|
||||
|
|
@ -36,11 +36,14 @@ import io.element.android.libraries.matrix.api.room.MessageEventType
|
|||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
|
||||
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
||||
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -92,6 +95,8 @@ class FakeMatrixRoom(
|
|||
private var sendPollResponseResult = Result.success(Unit)
|
||||
private var endPollResult = Result.success(Unit)
|
||||
private var progressCallbackValues = emptyList<Pair<Long, Long>>()
|
||||
private var generateWidgetWebViewUrlResult = Result.success("https://call.element.io")
|
||||
private var getWidgetDriverResult: Result<MatrixWidgetDriver> = Result.success(FakeWidgetDriver())
|
||||
val editMessageCalls = mutableListOf<Pair<String, String?>>()
|
||||
|
||||
var sendMediaCount = 0
|
||||
|
|
@ -368,6 +373,15 @@ class FakeMatrixRoom(
|
|||
progressCallback: ProgressCallback?
|
||||
): Result<MediaUploadHandler> = fakeSendMedia(progressCallback)
|
||||
|
||||
override suspend fun generateWidgetWebViewUrl(
|
||||
widgetSettings: MatrixWidgetSettings,
|
||||
clientId: String,
|
||||
languageTag: String?,
|
||||
theme: String?,
|
||||
): Result<String> = generateWidgetWebViewUrlResult
|
||||
|
||||
override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver> = getWidgetDriverResult
|
||||
|
||||
fun givenLeaveRoomError(throwable: Throwable?) {
|
||||
this.leaveRoomError = throwable
|
||||
}
|
||||
|
|
@ -475,6 +489,14 @@ class FakeMatrixRoom(
|
|||
fun givenProgressCallbackValues(values: List<Pair<Long, Long>>) {
|
||||
progressCallbackValues = values
|
||||
}
|
||||
|
||||
fun givenGenerateWidgetWebViewUrlResult(result: Result<String>) {
|
||||
generateWidgetWebViewUrlResult = result
|
||||
}
|
||||
|
||||
fun givenGetWidgetDriverResult(result: Result<MatrixWidgetDriver>) {
|
||||
getWidgetDriverResult = result
|
||||
}
|
||||
}
|
||||
|
||||
data class SendLocationInvocation(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.matrix.test.widget
|
||||
|
||||
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
||||
|
||||
class FakeCallWidgetSettingsProvider(
|
||||
private val provideFn: (String, String) -> MatrixWidgetSettings = { _, _ -> MatrixWidgetSettings("id", true, "url") }
|
||||
) : CallWidgetSettingsProvider {
|
||||
|
||||
val providedBaseUrls = mutableListOf<String>()
|
||||
|
||||
override fun provide(baseUrl: String, widgetId: String): MatrixWidgetSettings {
|
||||
providedBaseUrls += baseUrl
|
||||
return provideFn(baseUrl, widgetId)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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.matrix.test.widget
|
||||
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import java.util.UUID
|
||||
|
||||
class FakeWidgetDriver(
|
||||
override val id: String = UUID.randomUUID().toString(),
|
||||
) : MatrixWidgetDriver {
|
||||
|
||||
private val _sentMessages = mutableListOf<String>()
|
||||
val sentMessages: List<String> = _sentMessages
|
||||
|
||||
var runCalledCount = 0
|
||||
private set
|
||||
var closeCalledCount = 0
|
||||
private set
|
||||
|
||||
override val incomingMessages = MutableSharedFlow<String>(extraBufferCapacity = 1)
|
||||
|
||||
override suspend fun run() {
|
||||
runCalledCount++
|
||||
}
|
||||
|
||||
override suspend fun send(message: String) {
|
||||
_sentMessages.add(message)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
closeCalledCount++
|
||||
}
|
||||
|
||||
fun givenIncomingMessage(message: String) {
|
||||
incomingMessages.tryEmit(message)
|
||||
}
|
||||
}
|
||||
|
|
@ -25,5 +25,8 @@ interface PreferencesStore {
|
|||
suspend fun setDeveloperModeEnabled(enabled: Boolean)
|
||||
fun isDeveloperModeEnabledFlow(): Flow<Boolean>
|
||||
|
||||
suspend fun setCustomElementCallBaseUrl(string: String?)
|
||||
fun getCustomElementCallBaseUrlFlow(): Flow<String?>
|
||||
|
||||
suspend fun reset()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import androidx.datastore.core.DataStore
|
|||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.preferences.api.store.PreferencesStore
|
||||
|
|
@ -37,6 +38,7 @@ private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(na
|
|||
|
||||
private val richTextEditorKey = booleanPreferencesKey("richTextEditor")
|
||||
private val developerModeKey = booleanPreferencesKey("developerMode")
|
||||
private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseUrl")
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultPreferencesStore @Inject constructor(
|
||||
|
|
@ -71,6 +73,22 @@ class DefaultPreferencesStore @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun setCustomElementCallBaseUrl(string: String?) {
|
||||
store.edit { prefs ->
|
||||
if (string != null) {
|
||||
prefs[customElementCallBaseUrlKey] = string
|
||||
} else {
|
||||
prefs.remove(customElementCallBaseUrlKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCustomElementCallBaseUrlFlow(): Flow<String?> {
|
||||
return store.data.map { prefs ->
|
||||
prefs[customElementCallBaseUrlKey]
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun reset() {
|
||||
store.edit { it.clear() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,11 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
class InMemoryPreferencesStore(
|
||||
isRichTextEditorEnabled: Boolean = false,
|
||||
isDeveloperModeEnabled: Boolean = false,
|
||||
customElementCallBaseUrl: String? = null,
|
||||
) : PreferencesStore {
|
||||
private var _isRichTextEditorEnabled = MutableStateFlow(isRichTextEditorEnabled)
|
||||
private var _isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled)
|
||||
private var _customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl)
|
||||
|
||||
override suspend fun setRichTextEditorEnabled(enabled: Boolean) {
|
||||
_isRichTextEditorEnabled.value = enabled
|
||||
|
|
@ -43,6 +45,14 @@ class InMemoryPreferencesStore(
|
|||
return _isDeveloperModeEnabled
|
||||
}
|
||||
|
||||
override suspend fun setCustomElementCallBaseUrl(string: String?) {
|
||||
_customElementCallBaseUrl.tryEmit(string)
|
||||
}
|
||||
|
||||
override fun getCustomElementCallBaseUrlFlow(): Flow<String?> {
|
||||
return _customElementCallBaseUrl
|
||||
}
|
||||
|
||||
override suspend fun reset() {
|
||||
// No op
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,6 @@
|
|||
<string name="common_room_name_placeholder">"např. název vašeho projektu"</string>
|
||||
<string name="common_search_for_someone">"Hledat někoho"</string>
|
||||
<string name="common_search_results">"Výsledky hledání"</string>
|
||||
<string name="common_secure_backup">"Zabezpečená záloha"</string>
|
||||
<string name="common_security">"Zabezpečení"</string>
|
||||
<string name="common_sending">"Odesílání…"</string>
|
||||
<string name="common_server_not_supported">"Server není podporován"</string>
|
||||
|
|
@ -208,9 +207,9 @@
|
|||
<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í.
|
||||
<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í.
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -126,7 +126,6 @@
|
|||
<string name="common_room_name_placeholder">"например, название вашего проекта"</string>
|
||||
<string name="common_search_for_someone">"Поиск человека"</string>
|
||||
<string name="common_search_results">"Результаты поиска"</string>
|
||||
<string name="common_secure_backup">"Безопасное резервное копирование"</string>
|
||||
<string name="common_security">"Безопасность"</string>
|
||||
<string name="common_sending">"Отправка…"</string>
|
||||
<string name="common_server_not_supported">"Сервер не поддерживается"</string>
|
||||
|
|
|
|||
|
|
@ -131,7 +131,6 @@
|
|||
<string name="common_room_name_placeholder">"napr. názov vášho projektu"</string>
|
||||
<string name="common_search_for_someone">"Vyhľadať niekoho"</string>
|
||||
<string name="common_search_results">"Výsledky hľadania"</string>
|
||||
<string name="common_secure_backup">"Bezpečné zálohovanie"</string>
|
||||
<string name="common_security">"Bezpečnosť"</string>
|
||||
<string name="common_sending">"Odosiela sa…"</string>
|
||||
<string name="common_server_not_supported">"Server nie je podporovaný"</string>
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@
|
|||
<string name="action_share">"Share"</string>
|
||||
<string name="action_share_link">"Share link"</string>
|
||||
<string name="action_sign_in_again">"Sign in again"</string>
|
||||
<string name="action_signout">"Sign out"</string>
|
||||
<string name="action_signout_anyway">"Sign out anyway"</string>
|
||||
<string name="action_skip">"Skip"</string>
|
||||
<string name="action_start">"Start"</string>
|
||||
<string name="action_start_chat">"Start chat"</string>
|
||||
|
|
@ -84,6 +86,7 @@
|
|||
<string name="common_analytics">"Analytics"</string>
|
||||
<string name="common_audio">"Audio"</string>
|
||||
<string name="common_bubbles">"Bubbles"</string>
|
||||
<string name="common_chat_backup">"Chat backup"</string>
|
||||
<string name="common_copyright">"Copyright"</string>
|
||||
<string name="common_creating_room">"Creating room…"</string>
|
||||
<string name="common_current_user_left_room">"Left room"</string>
|
||||
|
|
@ -122,6 +125,7 @@
|
|||
<string name="common_privacy_policy">"Privacy policy"</string>
|
||||
<string name="common_reaction">"Reaction"</string>
|
||||
<string name="common_reactions">"Reactions"</string>
|
||||
<string name="common_recovery_key">"Recovery key"</string>
|
||||
<string name="common_refreshing">"Refreshing…"</string>
|
||||
<string name="common_replying_to">"Replying to %1$s"</string>
|
||||
<string name="common_report_a_bug">"Report a bug"</string>
|
||||
|
|
@ -131,7 +135,6 @@
|
|||
<string name="common_room_name_placeholder">"e.g. your project name"</string>
|
||||
<string name="common_search_for_someone">"Search for someone"</string>
|
||||
<string name="common_search_results">"Search results"</string>
|
||||
<string name="common_secure_backup">"Secure backup"</string>
|
||||
<string name="common_security">"Security"</string>
|
||||
<string name="common_sending">"Sending…"</string>
|
||||
<string name="common_server_not_supported">"Server not supported"</string>
|
||||
|
|
@ -200,6 +203,22 @@
|
|||
<string name="room_timeline_beginning_of_room_no_name">"This is the beginning of this conversation."</string>
|
||||
<string name="room_timeline_read_marker_title">"New"</string>
|
||||
<string name="screen_analytics_settings_share_data">"Share analytics data"</string>
|
||||
<string name="screen_chat_backup_key_backup_action_disable">"Turn off backup"</string>
|
||||
<string name="screen_chat_backup_key_backup_action_enable">"Turn on backup"</string>
|
||||
<string name="screen_chat_backup_key_backup_description">"Backup ensures that you don\'t lose your message history."</string>
|
||||
<string name="screen_chat_backup_key_backup_title">"Backup"</string>
|
||||
<string name="screen_chat_backup_recovery_action_change">"Change recovery key"</string>
|
||||
<string name="screen_chat_backup_recovery_action_confirm">"Confirm recovery key"</string>
|
||||
<string name="screen_chat_backup_recovery_action_confirm_description">"Your chat backup is currently out of sync."</string>
|
||||
<string name="screen_chat_backup_recovery_action_setup">"Set up recovery"</string>
|
||||
<string name="screen_chat_backup_recovery_action_setup_description">"Get access to your encrypted messages if you lose all your devices or are signed out of %1$@ everywhere."</string>
|
||||
<string name="screen_key_backup_disable_confirmation_action_turn_off">"Turn off"</string>
|
||||
<string name="screen_key_backup_disable_confirmation_description">"You will lose your encrypted messages if you are signed out of all devices."</string>
|
||||
<string name="screen_key_backup_disable_confirmation_title">"Are you sure you want to turn off backup?"</string>
|
||||
<string name="screen_key_backup_disable_description">"Turning off backup will remove your current encryption key backup and turn off other security features. In this case, you will:"</string>
|
||||
<string name="screen_key_backup_disable_description_point_1">"Not have encrypted message history on new devices"</string>
|
||||
<string name="screen_key_backup_disable_description_point_2">"Lose access to your encrypted messages if you are signed out of %1$@ everywhere"</string>
|
||||
<string name="screen_key_backup_disable_title">"Are you sure you want to turn off backup?"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Failed uploading media, please try again."</string>
|
||||
|
|
@ -228,6 +247,27 @@ If you proceed, some of your settings may change."</string>
|
|||
<string name="screen_notification_settings_system_notifications_action_required_content_link">"system settings"</string>
|
||||
<string name="screen_notification_settings_system_notifications_turned_off">"System notifications turned off"</string>
|
||||
<string name="screen_notification_settings_title">"Notifications"</string>
|
||||
<string name="screen_recovery_key_change_description">"Get a new recovery key if you\'ve lost your existing one. After changing your recovery key, your old one will no longer work."</string>
|
||||
<string name="screen_recovery_key_change_generate_key">"Generate a new recovery key"</string>
|
||||
<string name="screen_recovery_key_change_generate_key_description">"Make sure you can store your recovery key somewhere safe"</string>
|
||||
<string name="screen_recovery_key_change_success">"Recovery key changed"</string>
|
||||
<string name="screen_recovery_key_change_title">"Change recovery key?"</string>
|
||||
<string name="screen_recovery_key_confirm_description">"Enter your recovery key to confirm access to your chat backup."</string>
|
||||
<string name="screen_recovery_key_confirm_key_description">"Enter the 48 character code."</string>
|
||||
<string name="screen_recovery_key_confirm_key_placeholder">"Enter…"</string>
|
||||
<string name="screen_recovery_key_confirm_success">"Recovery key confirmed"</string>
|
||||
<string name="screen_recovery_key_confirm_title">"Confirm your recovery key"</string>
|
||||
<string name="screen_recovery_key_save_action">"Save recovery key"</string>
|
||||
<string name="screen_recovery_key_save_description">"Write down your recovery key somewhere safe or save it in a password manager."</string>
|
||||
<string name="screen_recovery_key_save_key_description">"Tap to copy recovery key"</string>
|
||||
<string name="screen_recovery_key_save_title">"Save your recovery key"</string>
|
||||
<string name="screen_recovery_key_setup_confirmation_description">"You will not be able to access your new recovery key after this step."</string>
|
||||
<string name="screen_recovery_key_setup_confirmation_title">"Have you saved your recovery key?"</string>
|
||||
<string name="screen_recovery_key_setup_description">"Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting ‘Change recovery key’."</string>
|
||||
<string name="screen_recovery_key_setup_generate_key">"Generate your recovery key"</string>
|
||||
<string name="screen_recovery_key_setup_generate_key_description">"Make sure you can store your recovery key somewhere safe"</string>
|
||||
<string name="screen_recovery_key_setup_success">"Recovery setup successful"</string>
|
||||
<string name="screen_recovery_key_setup_title">"Set up recovery"</string>
|
||||
<string name="screen_report_content_block_user_hint">"Check if you want to hide all current and future messages from this user"</string>
|
||||
<string name="screen_share_location_title">"Share location"</string>
|
||||
<string name="screen_share_my_location_action">"Share my location"</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue