Merge branch 'develop' into feature/fga/image_loading
This commit is contained in:
commit
4b60b14550
182 changed files with 2492 additions and 884 deletions
|
|
@ -47,7 +47,9 @@ suspend fun <T> (suspend () -> T).execute(state: MutableState<Async<T>>, errorMa
|
|||
}
|
||||
|
||||
suspend fun <T> (suspend () -> Result<T>).executeResult(state: MutableState<Async<T>>) {
|
||||
state.value = Async.Loading()
|
||||
if (state.value !is Async.Success) {
|
||||
state.value = Async.Loading()
|
||||
}
|
||||
this().fold(
|
||||
onSuccess = {
|
||||
state.value = Async.Success(it)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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.core.coroutine
|
||||
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
/** Create a Flow emitting a single error event. It should be useful for tests. */
|
||||
fun <T> errorFlow(throwable: Throwable) = flow<T> { throw throwable }
|
||||
|
|
@ -41,7 +41,11 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||
import timber.log.Timber
|
||||
|
||||
@Composable
|
||||
fun Avatar(avatarData: AvatarData, modifier: Modifier = Modifier) {
|
||||
fun Avatar(
|
||||
avatarData: AvatarData,
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescription: String? = null,
|
||||
) {
|
||||
val commonModifier = modifier
|
||||
.size(avatarData.size.dp)
|
||||
.clip(CircleShape)
|
||||
|
|
@ -54,6 +58,7 @@ fun Avatar(avatarData: AvatarData, modifier: Modifier = Modifier) {
|
|||
ImageAvatar(
|
||||
avatarData = avatarData,
|
||||
modifier = commonModifier,
|
||||
contentDescription = contentDescription,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -62,13 +67,14 @@ fun Avatar(avatarData: AvatarData, modifier: Modifier = Modifier) {
|
|||
private fun ImageAvatar(
|
||||
avatarData: AvatarData,
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescription: String? = null,
|
||||
) {
|
||||
AsyncImage(
|
||||
model = avatarData,
|
||||
onError = {
|
||||
Timber.e("TAG", "Error $it\n${it.result}", it.result.throwable)
|
||||
},
|
||||
contentDescription = null,
|
||||
contentDescription = contentDescription,
|
||||
contentScale = ContentScale.Crop,
|
||||
placeholder = debugPlaceholderAvatar(),
|
||||
modifier = modifier
|
||||
|
|
@ -89,11 +95,11 @@ private fun InitialsAvatar(
|
|||
end = Offset(100f, 0f)
|
||||
)
|
||||
Box(
|
||||
modifier.background(brush = initialsGradient)
|
||||
modifier.background(brush = initialsGradient),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = avatarData.getInitial(),
|
||||
text = avatarData.initial,
|
||||
fontSize = (avatarData.size.dp / 2).value.sp,
|
||||
color = Color.White,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -30,8 +30,37 @@ data class AvatarData(
|
|||
@IgnoredOnParcel
|
||||
val size: AvatarSize = AvatarSize.MEDIUM
|
||||
) : Parcelable {
|
||||
fun getInitial(): String {
|
||||
val firstChar = name?.firstOrNull() ?: id.getOrNull(1) ?: '?'
|
||||
return firstChar.uppercase()
|
||||
|
||||
@IgnoredOnParcel
|
||||
val initial by lazy {
|
||||
(name?.takeIf { it.isNotBlank() } ?: id)
|
||||
.let { dn ->
|
||||
var startIndex = 0
|
||||
val initial = dn[startIndex]
|
||||
|
||||
if (initial in listOf('@', '#', '+') && dn.length > 1) {
|
||||
startIndex++
|
||||
}
|
||||
|
||||
var length = 1
|
||||
var first = dn[startIndex]
|
||||
|
||||
// LEFT-TO-RIGHT MARK
|
||||
if (dn.length >= 2 && 0x200e == first.code) {
|
||||
startIndex++
|
||||
first = dn[startIndex]
|
||||
}
|
||||
|
||||
// check if it’s the start of a surrogate pair
|
||||
if (first.code in 0xD800..0xDBFF && dn.length > startIndex + 1) {
|
||||
val second = dn[startIndex + 1]
|
||||
if (second.code in 0xDC00..0xDFFF) {
|
||||
length++
|
||||
}
|
||||
}
|
||||
|
||||
dn.substring(startIndex, startIndex + length)
|
||||
}
|
||||
.uppercase()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.modifiers
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.platform.debugInspectorInfo
|
||||
|
||||
/**
|
||||
* Applies the [ifTrue] modifier when the [condition] is true, [ifFalse] otherwise.
|
||||
*/
|
||||
@SuppressLint("UnnecessaryComposedModifier") // It's actually necessary due to the `@Composable` lambdas
|
||||
fun Modifier.applyIf(
|
||||
condition: Boolean,
|
||||
ifTrue: @Composable Modifier.() -> Modifier,
|
||||
ifFalse: @Composable (Modifier.() -> Modifier)? = null
|
||||
): Modifier =
|
||||
composed(
|
||||
inspectorInfo = debugInspectorInfo {
|
||||
name = "applyIf"
|
||||
value = condition
|
||||
}
|
||||
) {
|
||||
when {
|
||||
condition -> then(ifTrue(Modifier))
|
||||
ifFalse != null -> then(ifFalse(Modifier))
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* 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.modifiers
|
||||
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||
import androidx.compose.ui.platform.debugInspectorInfo
|
||||
import kotlin.math.sqrt
|
||||
|
||||
// Note: these modifiers come from https://gist.github.com/darvld/eb3844474baf2f3fc6d3ab44a4b4b5f8
|
||||
|
||||
/**
|
||||
* A modifier that clips the composable content using an animated circle. The circle will
|
||||
* expand/shrink with an animation whenever [visible] changes.
|
||||
*
|
||||
* For more fine-grained control over the transition, see this method's overload, which allows passing
|
||||
* a [State] object to control the progress of the reveal animation.
|
||||
*
|
||||
* By default, the circle is centered in the content, but custom positions may be specified using
|
||||
* [revealFrom]. Specified offsets should be between 0 (left/top) and 1 (right/bottom).*/
|
||||
fun Modifier.circularReveal(
|
||||
visible: Boolean,
|
||||
showScrim: Boolean = false,
|
||||
revealFrom: Offset = Offset(0.5f, 0.5f),
|
||||
): Modifier = composed(
|
||||
factory = {
|
||||
val factor = updateTransition(visible, label = "Visibility")
|
||||
.animateFloat(label = "revealFactor") { if (it) 1f else 0f }
|
||||
|
||||
circularReveal(factor, showScrim, revealFrom)
|
||||
},
|
||||
inspectorInfo = debugInspectorInfo {
|
||||
name = "circularReveal"
|
||||
properties["visible"] = visible
|
||||
properties["revealFrom"] = revealFrom
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* A modifier that clips the composable content using a circular shape. The radius of the circle
|
||||
* will be determined by the [transitionProgress].
|
||||
*
|
||||
* The values of the progress should be between 0 and 1.
|
||||
*
|
||||
* By default, the circle is centered in the content, but custom positions may be specified using
|
||||
* [revealFrom]. Specified offsets should be between 0 (left/top) and 1 (right/bottom).
|
||||
* */
|
||||
fun Modifier.circularReveal(
|
||||
transitionProgress: State<Float>,
|
||||
showScrim: Boolean = false,
|
||||
revealFrom: Offset = Offset(0.5f, 0.5f)
|
||||
): Modifier {
|
||||
return drawWithCache {
|
||||
val path = Path()
|
||||
val center = revealFrom.mapTo(size)
|
||||
val radius = calculateRadius(revealFrom, size)
|
||||
val scrimColor = if (showScrim)
|
||||
Color.Gray
|
||||
else
|
||||
Color.Transparent
|
||||
|
||||
path.addOval(Rect(center, radius * transitionProgress.value))
|
||||
|
||||
onDrawWithContent {
|
||||
if (showScrim) {
|
||||
drawRect(scrimColor, alpha = transitionProgress.value * 0.75f)
|
||||
}
|
||||
clipPath(path) { this@onDrawWithContent.drawContent() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Offset.mapTo(size: Size): Offset {
|
||||
return Offset(x * size.width, y * size.height)
|
||||
}
|
||||
|
||||
private fun calculateRadius(normalizedOrigin: Offset, size: Size) = with(normalizedOrigin) {
|
||||
val x = (if (x > 0.5f) x else 1 - x) * size.width
|
||||
val y = (if (y > 0.5f) y else 1 - y) * size.height
|
||||
|
||||
sqrt(x * x + y * y)
|
||||
}
|
||||
|
|
@ -21,12 +21,12 @@ package io.element.android.libraries.designsystem.theme.components
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.ModalBottomSheetDefaults
|
||||
import androidx.compose.material.ModalBottomSheetState
|
||||
import androidx.compose.material.ModalBottomSheetValue
|
||||
import androidx.compose.material.contentColorFor
|
||||
import androidx.compose.material.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
|
@ -44,7 +44,7 @@ fun ModalBottomSheetLayout(
|
|||
sheetState: ModalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden),
|
||||
sheetShape: Shape = MaterialTheme.shapes.large,
|
||||
sheetElevation: Dp = ModalBottomSheetDefaults.Elevation,
|
||||
sheetBackgroundColor: Color = MaterialTheme.colors.surface,
|
||||
sheetBackgroundColor: Color = MaterialTheme.colorScheme.surface,
|
||||
sheetContentColor: Color = contentColorFor(sheetBackgroundColor),
|
||||
scrimColor: Color = ModalBottomSheetDefaults.scrimColor,
|
||||
content: @Composable () -> Unit = {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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.utils
|
||||
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
|
||||
@Composable
|
||||
fun WindowInsets.copy(
|
||||
top: Int? = null,
|
||||
right: Int? = null,
|
||||
bottom: Int? = null,
|
||||
left: Int? = null
|
||||
): WindowInsets {
|
||||
val density = LocalDensity.current
|
||||
val direction = LocalLayoutDirection.current
|
||||
return WindowInsets(
|
||||
top = top ?: this.getTop(density),
|
||||
right = right ?: this.getRight(density, direction),
|
||||
bottom = bottom ?: this.getBottom(density),
|
||||
left = left ?: this.getLeft(density, direction)
|
||||
)
|
||||
}
|
||||
|
|
@ -36,6 +36,8 @@ interface MatrixClient : Closeable {
|
|||
val mediaLoader: MatrixMediaLoader
|
||||
fun getRoom(roomId: RoomId): MatrixRoom?
|
||||
fun findDM(userId: UserId): MatrixRoom?
|
||||
suspend fun ignoreUser(userId: UserId): Result<Unit>
|
||||
suspend fun unignoreUser(userId: UserId): Result<Unit>
|
||||
suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId>
|
||||
suspend fun createDM(userId: UserId): Result<RoomId>
|
||||
fun startSync()
|
||||
|
|
|
|||
|
|
@ -16,12 +16,19 @@
|
|||
|
||||
package io.element.android.libraries.matrix.api.notification
|
||||
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
//TODO add content
|
||||
data class NotificationData(
|
||||
val item: MatrixTimelineItem,
|
||||
val title: String,
|
||||
val subtitle: String?,
|
||||
val senderId: UserId,
|
||||
val eventId: EventId,
|
||||
val roomId: RoomId,
|
||||
val senderAvatarUrl: String? = null,
|
||||
val senderDisplayName: String? = null,
|
||||
val roomAvatarUrl: String? = null,
|
||||
val isDirect: Boolean,
|
||||
val isEncrypted: Boolean,
|
||||
val isNoisy: Boolean,
|
||||
val avatarUrl: String?,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,12 +18,15 @@ package io.element.android.libraries.matrix.api.room
|
|||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.io.Closeable
|
||||
|
||||
interface MatrixRoom : Closeable {
|
||||
val sessionId: SessionId
|
||||
val roomId: RoomId
|
||||
val name: String?
|
||||
val bestName: String
|
||||
|
|
@ -36,20 +39,22 @@ interface MatrixRoom : Closeable {
|
|||
val isDirect: Boolean
|
||||
val isPublic: Boolean
|
||||
|
||||
suspend fun members(): List<RoomMember>
|
||||
/**
|
||||
* The current loaded members as a StateFlow.
|
||||
* Initial value is [MatrixRoomMembersState.Unknown].
|
||||
* To update them you should call [updateMembers].
|
||||
*/
|
||||
val membersStateFlow: StateFlow<MatrixRoomMembersState>
|
||||
|
||||
suspend fun memberCount(): Int
|
||||
|
||||
fun getMember(userId: UserId): RoomMember?
|
||||
|
||||
fun getDmMember(): RoomMember?
|
||||
/**
|
||||
* Try to load the room members and update the membersFlow.
|
||||
*/
|
||||
suspend fun updateMembers(): Result<Unit>
|
||||
|
||||
fun syncUpdateFlow(): Flow<Long>
|
||||
|
||||
fun timeline(): MatrixTimeline
|
||||
|
||||
suspend fun fetchMembers(): Result<Unit>
|
||||
|
||||
suspend fun userDisplayName(userId: UserId): Result<String?>
|
||||
|
||||
suspend fun userAvatarUrl(userId: UserId): Result<String?>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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.room
|
||||
|
||||
sealed interface MatrixRoomMembersState {
|
||||
object Unknown : MatrixRoomMembersState
|
||||
data class Pending(val prevRoomMembers: List<RoomMember>? = null) : MatrixRoomMembersState
|
||||
data class Error(val failure: Throwable, val prevRoomMembers: List<RoomMember>? = null) : MatrixRoomMembersState
|
||||
data class Ready(val roomMembers: List<RoomMember>) : MatrixRoomMembersState
|
||||
}
|
||||
|
||||
fun MatrixRoomMembersState.roomMembers(): List<RoomMember>? {
|
||||
return when (this) {
|
||||
is MatrixRoomMembersState.Ready -> roomMembers
|
||||
is MatrixRoomMembersState.Pending -> prevRoomMembers
|
||||
is MatrixRoomMembersState.Error -> prevRoomMembers
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -16,11 +16,8 @@
|
|||
|
||||
package io.element.android.libraries.matrix.api.room
|
||||
|
||||
import android.os.Parcelable
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class RoomMember(
|
||||
val userId: UserId,
|
||||
val displayName: String?,
|
||||
|
|
@ -30,7 +27,7 @@ data class RoomMember(
|
|||
val powerLevel: Long,
|
||||
val normalizedPowerLevel: Long,
|
||||
val isIgnored: Boolean,
|
||||
) : Parcelable
|
||||
)
|
||||
|
||||
enum class RoomMembershipState {
|
||||
BAN, INVITE, JOIN, KNOCK, LEAVE
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ class RustMatrixClient constructor(
|
|||
val slidingSyncRoom = slidingSync.getRoom(roomId.value) ?: return null
|
||||
val fullRoom = slidingSyncRoom.fullRoom() ?: return null
|
||||
return RustMatrixRoom(
|
||||
currentUserId = sessionId,
|
||||
sessionId = sessionId,
|
||||
slidingSyncUpdateFlow = slidingSyncObserverProxy.updateSummaryFlow,
|
||||
slidingSyncRoom = slidingSyncRoom,
|
||||
innerRoom = fullRoom,
|
||||
|
|
@ -214,6 +214,18 @@ class RustMatrixClient constructor(
|
|||
return roomId?.let { getRoom(it) }
|
||||
}
|
||||
|
||||
override suspend fun ignoreUser(userId: UserId): Result<Unit> = withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
client.ignoreUser(userId.value)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun unignoreUser(userId: UserId): Result<Unit> = withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
client.unignoreUser(userId.value)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId> = withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
val rustParams = RustCreateRoomParameters(
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ class RustMediaLoader(
|
|||
runCatching {
|
||||
mediaSourceFromUrl(url).use { mediaSource ->
|
||||
innerClient.getMediaFile(
|
||||
source = mediaSource,
|
||||
mediaSource = mediaSource,
|
||||
mimeType = mimeType ?: "application/octet-stream"
|
||||
).use {
|
||||
Path(it.path())
|
||||
|
|
|
|||
|
|
@ -16,35 +16,28 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.notification
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||
import io.element.android.libraries.matrix.impl.timeline.MatrixTimelineItemMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
|
||||
import org.matrix.rustcomponents.sdk.NotificationItem
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import javax.inject.Inject
|
||||
|
||||
class NotificationMapper @Inject constructor() {
|
||||
// TODO Inject and remove duplicate?
|
||||
private val timelineItemFactory = MatrixTimelineItemMapper(
|
||||
virtualTimelineItemMapper = VirtualTimelineItemMapper(),
|
||||
eventTimelineItemMapper = EventTimelineItemMapper(
|
||||
contentMapper = TimelineEventContentMapper(
|
||||
eventMessageMapper = EventMessageMapper()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
fun map(notificationItem: NotificationItem): NotificationData {
|
||||
return notificationItem.use {
|
||||
NotificationData(
|
||||
item = timelineItemFactory.map(it.item),
|
||||
title = it.title,
|
||||
subtitle = it.subtitle,
|
||||
isNoisy = it.isNoisy,
|
||||
avatarUrl = it.avatarUrl,
|
||||
senderId = UserId(it.event.senderId()),
|
||||
eventId = EventId(it.event.eventId()),
|
||||
roomId = RoomId(it.roomId),
|
||||
senderAvatarUrl = it.senderAvatarUrl,
|
||||
senderDisplayName = it.senderDisplayName,
|
||||
roomAvatarUrl = it.roomAvatarUrl,
|
||||
isDirect = it.isDirect,
|
||||
isEncrypted = it.isEncrypted,
|
||||
isNoisy = it.isNoisy
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,21 +17,23 @@
|
|||
package io.element.android.libraries.matrix.impl.room
|
||||
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.SlidingSyncRoom
|
||||
|
|
@ -40,7 +42,7 @@ import org.matrix.rustcomponents.sdk.genTransactionId
|
|||
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
|
||||
|
||||
class RustMatrixRoom(
|
||||
private val currentUserId: UserId,
|
||||
override val sessionId: SessionId,
|
||||
private val slidingSyncUpdateFlow: Flow<UpdateSummary>,
|
||||
private val slidingSyncRoom: SlidingSyncRoom,
|
||||
private val innerRoom: Room,
|
||||
|
|
@ -48,38 +50,19 @@ class RustMatrixRoom(
|
|||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
) : MatrixRoom {
|
||||
|
||||
private var loadMembersJob: Job? = null
|
||||
private var cachedMembers: List<RoomMember> = emptyList()
|
||||
override val membersStateFlow: StateFlow<MatrixRoomMembersState>
|
||||
get() = _membersStateFlow
|
||||
|
||||
override suspend fun members(): List<RoomMember> {
|
||||
return cachedMembers.ifEmpty {
|
||||
if (loadMembersJob == null) {
|
||||
loadMembersJob = coroutineScope.launch(coroutineDispatchers.io) {
|
||||
cachedMembers = tryOrNull {
|
||||
innerRoom.members().map(RoomMemberMapper::map)
|
||||
} ?: emptyList()
|
||||
}
|
||||
}
|
||||
loadMembersJob?.join()
|
||||
loadMembersJob = null
|
||||
cachedMembers
|
||||
}
|
||||
}
|
||||
private var _membersStateFlow = MutableStateFlow<MatrixRoomMembersState>(MatrixRoomMembersState.Unknown)
|
||||
|
||||
override suspend fun memberCount(): Int {
|
||||
return members().size
|
||||
}
|
||||
|
||||
override fun getMember(userId: UserId): RoomMember? {
|
||||
return cachedMembers.find { it.userId == userId }
|
||||
}
|
||||
|
||||
override fun getDmMember(): RoomMember? {
|
||||
return if (cachedMembers.size == 2 && isDirect && isEncrypted) {
|
||||
cachedMembers.find { it.userId != currentUserId }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
private val timeline by lazy {
|
||||
RustMatrixTimeline(
|
||||
matrixRoom = this,
|
||||
innerRoom = innerRoom,
|
||||
slidingSyncRoom = slidingSyncRoom,
|
||||
coroutineScope = coroutineScope,
|
||||
coroutineDispatchers = coroutineDispatchers
|
||||
)
|
||||
}
|
||||
|
||||
override fun syncUpdateFlow(): Flow<Long> {
|
||||
|
|
@ -94,13 +77,7 @@ class RustMatrixRoom(
|
|||
}
|
||||
|
||||
override fun timeline(): MatrixTimeline {
|
||||
return RustMatrixTimeline(
|
||||
matrixRoom = this,
|
||||
innerRoom = innerRoom,
|
||||
slidingSyncRoom = slidingSyncRoom,
|
||||
coroutineScope = coroutineScope,
|
||||
coroutineDispatchers = coroutineDispatchers
|
||||
)
|
||||
return timeline
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
|
|
@ -150,9 +127,16 @@ class RustMatrixRoom(
|
|||
override val isDirect: Boolean
|
||||
get() = innerRoom.isDirect()
|
||||
|
||||
override suspend fun fetchMembers(): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
override suspend fun updateMembers(): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
val currentState = _membersStateFlow.value
|
||||
val currentMembers = currentState.roomMembers()
|
||||
_membersStateFlow.value = MatrixRoomMembersState.Pending(prevRoomMembers = currentMembers)
|
||||
runCatching {
|
||||
innerRoom.fetchMembers()
|
||||
innerRoom.members().map(RoomMemberMapper::map)
|
||||
}.map {
|
||||
_membersStateFlow.value = MatrixRoomMembersState.Ready(it)
|
||||
}.onFailure {
|
||||
_membersStateFlow.value = MatrixRoomMembersState.Error(prevRoomMembers = currentMembers, failure = it)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -208,13 +192,13 @@ class RustMatrixRoom(
|
|||
}
|
||||
|
||||
override suspend fun acceptInvitation(): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
kotlin.runCatching {
|
||||
runCatching {
|
||||
innerRoom.acceptInvitation()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun rejectInvitation(): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
kotlin.runCatching {
|
||||
runCatching {
|
||||
innerRoom.rejectInvitation()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import org.matrix.rustcomponents.sdk.SlidingSyncRoom
|
|||
import org.matrix.rustcomponents.sdk.TimelineItem
|
||||
import org.matrix.rustcomponents.sdk.TimelineListener
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class RustMatrixTimeline(
|
||||
private val matrixRoom: MatrixRoom,
|
||||
|
|
@ -51,6 +52,8 @@ class RustMatrixTimeline(
|
|||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
) : MatrixTimeline {
|
||||
|
||||
private val isInit = AtomicBoolean(false)
|
||||
|
||||
private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>> =
|
||||
MutableStateFlow(emptyList())
|
||||
|
||||
|
|
@ -95,6 +98,7 @@ class RustMatrixTimeline(
|
|||
withContext(coroutineDispatchers.diffUpdateDispatcher) {
|
||||
this@RustMatrixTimeline.timelineItems.value = matrixTimelineItems
|
||||
}
|
||||
isInit.set(true)
|
||||
}
|
||||
.onFailure {
|
||||
Timber.e("Failed adding timeline listener on room with identifier: ${matrixRoom.roomId})")
|
||||
|
|
@ -105,6 +109,7 @@ class RustMatrixTimeline(
|
|||
override fun dispose() {
|
||||
Timber.v("Dispose timeline for room ${matrixRoom.roomId}")
|
||||
listenerTokens.dispose()
|
||||
isInit.set(false)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -125,6 +130,9 @@ class RustMatrixTimeline(
|
|||
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
Timber.v("Start back paginating for room ${matrixRoom.roomId} ")
|
||||
if (!isInit.get()) {
|
||||
throw IllegalStateException("Timeline is not init yet")
|
||||
}
|
||||
val paginationOptions = PaginationOptions.UntilNumItems(
|
||||
eventLimit = requestSize.toUShort(),
|
||||
items = untilNumberOfItems.toUShort()
|
||||
|
|
@ -139,10 +147,26 @@ class RustMatrixTimeline(
|
|||
|
||||
private suspend fun addListener(timelineListener: TimelineListener): Result<List<TimelineItem>> = withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
val settings = RoomSubscription(requiredState = listOf(RequiredState(key = "m.room.canonical_alias", value = "")), timelineLimit = null)
|
||||
val settings = RoomSubscription(
|
||||
requiredState = listOf(
|
||||
RequiredState(key = "m.room.canonical_alias", value = ""),
|
||||
RequiredState(key = "m.room.topic", value = ""),
|
||||
RequiredState(key = "m.room.join_rules", value = ""),
|
||||
),
|
||||
timelineLimit = null
|
||||
)
|
||||
val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, settings)
|
||||
launch {
|
||||
fetchMembers()
|
||||
}
|
||||
listenerTokens += result.taskHandle
|
||||
result.items
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchMembers() = withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
innerRoom.fetchMembers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.libraries.core)
|
||||
api(projects.libraries.matrix.api)
|
||||
api(libs.coroutines.core)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ class FakeMatrixClient(
|
|||
private val notificationService: FakeNotificationService = FakeNotificationService(),
|
||||
) : MatrixClient {
|
||||
|
||||
private var ignoreUserResult: Result<Unit> = Result.success(Unit)
|
||||
private var unignoreUserResult: Result<Unit> = Result.success(Unit)
|
||||
private var createRoomResult: Result<RoomId> = Result.success(A_ROOM_ID)
|
||||
private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID)
|
||||
private var createDmFailure: Throwable? = null
|
||||
|
|
@ -63,6 +65,14 @@ class FakeMatrixClient(
|
|||
return findDmResult
|
||||
}
|
||||
|
||||
override suspend fun ignoreUser(userId: UserId): Result<Unit> {
|
||||
return ignoreUserResult
|
||||
}
|
||||
|
||||
override suspend fun unignoreUser(userId: UserId): Result<Unit> {
|
||||
return unignoreUserResult
|
||||
}
|
||||
|
||||
override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId> {
|
||||
delay(100)
|
||||
return createRoomResult
|
||||
|
|
@ -119,6 +129,14 @@ class FakeMatrixClient(
|
|||
createDmResult = result
|
||||
}
|
||||
|
||||
fun givenIgnoreUserResult(result: Result<Unit>) {
|
||||
ignoreUserResult = result
|
||||
}
|
||||
|
||||
fun givenUnignoreUserResult(result: Result<Unit>) {
|
||||
unignoreUserResult = result
|
||||
}
|
||||
|
||||
fun givenCreateDmError(failure: Throwable?) {
|
||||
createDmFailure = failure
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,17 +18,21 @@ package io.element.android.libraries.matrix.test.room
|
|||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
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.timeline.FakeMatrixTimeline
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
|
||||
class FakeMatrixRoom(
|
||||
override val sessionId: SessionId = A_SESSION_ID,
|
||||
override val roomId: RoomId = A_ROOM_ID,
|
||||
override val name: String? = null,
|
||||
override val bestName: String = "",
|
||||
|
|
@ -40,19 +44,16 @@ class FakeMatrixRoom(
|
|||
override val alternativeAliases: List<String> = emptyList(),
|
||||
override val isPublic: Boolean = true,
|
||||
override val isDirect: Boolean = false,
|
||||
private val members: List<RoomMember> = emptyList(),
|
||||
private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(),
|
||||
) : MatrixRoom {
|
||||
|
||||
private var ignoreResult: Result<Unit> = Result.success(Unit)
|
||||
private var unignoreResult: Result<Unit> = Result.success(Unit)
|
||||
private var userDisplayNameResult = Result.success<String?>(null)
|
||||
private var userAvatarUrlResult = Result.success<String?>(null)
|
||||
private var updateMembersResult: Result<Unit> = Result.success(Unit)
|
||||
private var acceptInviteResult = Result.success(Unit)
|
||||
private var rejectInviteResult = Result.success(Unit)
|
||||
private var dmMember: RoomMember? = null
|
||||
private var fetchMemberResult: Result<Unit> = Result.success(Unit)
|
||||
|
||||
var areMembersFetched: Boolean = false
|
||||
private set
|
||||
|
||||
var isInviteAccepted: Boolean = false
|
||||
private set
|
||||
|
|
@ -62,6 +63,12 @@ class FakeMatrixRoom(
|
|||
|
||||
private var leaveRoomError: Throwable? = null
|
||||
|
||||
override val membersStateFlow: MutableStateFlow<MatrixRoomMembersState> = MutableStateFlow(MatrixRoomMembersState.Unknown)
|
||||
|
||||
override suspend fun updateMembers(): Result<Unit> {
|
||||
return updateMembersResult
|
||||
}
|
||||
|
||||
override fun syncUpdateFlow(): Flow<Long> {
|
||||
return emptyFlow()
|
||||
}
|
||||
|
|
@ -70,18 +77,6 @@ class FakeMatrixRoom(
|
|||
return matrixTimeline
|
||||
}
|
||||
|
||||
override suspend fun fetchMembers(): Result<Unit> {
|
||||
return fetchMemberResult.also { result ->
|
||||
if (result.isSuccess) {
|
||||
areMembersFetched = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDmMember(): RoomMember? {
|
||||
return dmMember
|
||||
}
|
||||
|
||||
override suspend fun userDisplayName(userId: UserId): Result<String?> {
|
||||
return userDisplayNameResult
|
||||
}
|
||||
|
|
@ -90,22 +85,6 @@ class FakeMatrixRoom(
|
|||
return userAvatarUrlResult
|
||||
}
|
||||
|
||||
override suspend fun members(): List<RoomMember> {
|
||||
return members
|
||||
}
|
||||
|
||||
override suspend fun memberCount(): Int {
|
||||
if (fetchMemberResult.isSuccess) {
|
||||
return members.count()
|
||||
} else {
|
||||
throw fetchMemberResult.exceptionOrNull()!!
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMember(userId: UserId): RoomMember? {
|
||||
return members.firstOrNull { it.userId == userId }
|
||||
}
|
||||
|
||||
override suspend fun sendMessage(message: String): Result<Unit> {
|
||||
delay(100)
|
||||
return Result.success(Unit)
|
||||
|
|
@ -155,12 +134,12 @@ class FakeMatrixRoom(
|
|||
this.leaveRoomError = throwable
|
||||
}
|
||||
|
||||
fun givenFetchMemberResult(result: Result<Unit>) {
|
||||
fetchMemberResult = result
|
||||
fun givenRoomMembersState(state: MatrixRoomMembersState) {
|
||||
membersStateFlow.value = state
|
||||
}
|
||||
|
||||
fun givenDmMember(roomMember: RoomMember) {
|
||||
this.dmMember = roomMember
|
||||
fun givenUpdateMembersResult(result: Result<Unit>) {
|
||||
updateMembersResult = result
|
||||
}
|
||||
|
||||
fun givenUserDisplayNameResult(displayName: Result<String?>) {
|
||||
|
|
@ -179,4 +158,11 @@ class FakeMatrixRoom(
|
|||
rejectInviteResult = result
|
||||
}
|
||||
|
||||
fun givenIgnoreResult(result: Result<Unit>) {
|
||||
ignoreResult = result
|
||||
}
|
||||
|
||||
fun givenUnIgnoreResult(result: Result<Unit>) {
|
||||
unignoreResult = result
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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.ui.room
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
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.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
|
||||
@Composable
|
||||
fun MatrixRoom.getRoomMember(userId: UserId): State<RoomMember?> {
|
||||
val roomMembersState by membersStateFlow.collectAsState()
|
||||
return getRoomMember(roomMembersState = roomMembersState, userId = userId)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getRoomMember(roomMembersState: MatrixRoomMembersState, userId: UserId): State<RoomMember?> {
|
||||
val roomMembers = roomMembersState.roomMembers()
|
||||
return remember(roomMembers) {
|
||||
derivedStateOf {
|
||||
roomMembers?.find {
|
||||
it.userId == userId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MatrixRoom.getDirectRoomMember(): State<RoomMember?> {
|
||||
val roomMembersState by membersStateFlow.collectAsState()
|
||||
return getDirectRoomMember(roomMembersState = roomMembersState)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MatrixRoom.getDirectRoomMember(roomMembersState: MatrixRoomMembersState): State<RoomMember?> {
|
||||
val roomMembers = roomMembersState.roomMembers()
|
||||
return remember(roomMembers) {
|
||||
derivedStateOf {
|
||||
if (roomMembers == null) {
|
||||
null
|
||||
} else if (roomMembers.size == 2 && isDirect && isEncrypted) {
|
||||
roomMembers.find { it.userId != this.sessionId }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
34
libraries/mediapickers/build.gradle.kts
Normal file
34
libraries/mediapickers/build.gradle.kts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright (c) 2022 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.mediapickers"
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(libs.inject)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.robolectric)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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.mediapickers
|
||||
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
|
||||
/**
|
||||
* Wrapper around [ManagedActivityResultLauncher] to be used with media/file pickers.
|
||||
*/
|
||||
interface PickerLauncher<Input, Output> {
|
||||
/** Starts the activity result launcher with its default input. */
|
||||
fun launch()
|
||||
|
||||
/** Starts the activity result launcher with a [customInput]. */
|
||||
fun launch(customInput: Input)
|
||||
}
|
||||
|
||||
class ComposePickerLauncher<Input, Output>(
|
||||
private val managedLauncher: ManagedActivityResultLauncher<Input, Output>,
|
||||
private val defaultRequest: Input,
|
||||
) : PickerLauncher<Input, Output> {
|
||||
override fun launch() {
|
||||
managedLauncher.launch(defaultRequest)
|
||||
}
|
||||
|
||||
override fun launch(customInput: Input) {
|
||||
managedLauncher.launch(customInput)
|
||||
}
|
||||
}
|
||||
|
||||
/** Needed for screenshot tests. */
|
||||
class NoOpPickerLauncher<Input, Output>(
|
||||
private val onResult: () -> Unit,
|
||||
) : PickerLauncher<Input, Output> {
|
||||
override fun launch() = onResult()
|
||||
override fun launch(customInput: Input) = onResult()
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* 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.mediapickers
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.core.content.FileProvider
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
class PickerProvider constructor(private val isInTest: Boolean) {
|
||||
|
||||
@Inject
|
||||
constructor(): this(false)
|
||||
|
||||
/**
|
||||
* Remembers and returns a [PickerLauncher] for a certain media/file [type].
|
||||
*/
|
||||
@Composable
|
||||
internal fun <Input, Output> rememberPickerLauncher(
|
||||
type: PickerType<Input, Output>,
|
||||
onResult: (Output) -> Unit,
|
||||
): PickerLauncher<Input, Output> {
|
||||
return if (LocalInspectionMode.current || isInTest) {
|
||||
NoOpPickerLauncher { }
|
||||
} else {
|
||||
val contract = type.getContract()
|
||||
val managedLauncher = rememberLauncherForActivityResult(contract = contract, onResult = onResult)
|
||||
remember(type) { ComposePickerLauncher(managedLauncher, type.getDefaultRequest()) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remembers and returns a [PickerLauncher] for a gallery item, either a picture or a video.
|
||||
* [onResult] will be called with either the selected file's [Uri] or `null` if nothing was selected.
|
||||
*/
|
||||
@Composable
|
||||
fun registerGalleryPicker(onResult: (Uri?) -> Unit): PickerLauncher<PickVisualMediaRequest, Uri?> {
|
||||
// Tests and UI preview can't handle Contexts, so we might as well disable the whole picker
|
||||
return if (LocalInspectionMode.current || isInTest) {
|
||||
NoOpPickerLauncher { onResult(null) }
|
||||
} else {
|
||||
rememberPickerLauncher(type = PickerType.ImageAndVideo) { uri -> onResult(uri) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remembers and returns a [PickerLauncher] for a file of a certain [mimeType] (any type of file, by default).
|
||||
* [onResult] will be called with either the selected file's [Uri] or `null` if nothing was selected.
|
||||
*/
|
||||
@Composable
|
||||
fun registerFilePicker(mimeType: String = MimeTypes.Any, onResult: (Uri?) -> Unit): PickerLauncher<String, Uri?> {
|
||||
// Tests and UI preview can't handle Context or FileProviders, so we might as well disable the whole picker
|
||||
return if (LocalInspectionMode.current || isInTest) {
|
||||
NoOpPickerLauncher { onResult(null) }
|
||||
} else {
|
||||
rememberPickerLauncher(type = PickerType.File(mimeType)) { uri -> onResult(uri) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remembers and returns a [PickerLauncher] for taking a photo with a camera app.
|
||||
* @param [onResult] will be called with either the photo's [Uri] or `null` if nothing was selected.
|
||||
* @param [deleteAfter] When it's `true`, the taken photo will be automatically removed after calling [onResult].
|
||||
* It's `true` by default.
|
||||
*/
|
||||
@Composable
|
||||
fun registerCameraPhotoPicker(onResult: (Uri?) -> Unit, deleteAfter: Boolean = true): PickerLauncher<Uri, Boolean> {
|
||||
// Tests and UI preview can't handle Context or FileProviders, so we might as well disable the whole picker
|
||||
return if (LocalInspectionMode.current || isInTest) {
|
||||
NoOpPickerLauncher { onResult(null) }
|
||||
} else {
|
||||
val context = LocalContext.current
|
||||
val tmpFile = remember { getTemporaryFile(context) }
|
||||
val tmpFileUri = remember(tmpFile) { getTemporaryUri(context, tmpFile) }
|
||||
rememberPickerLauncher(type = PickerType.Camera.Photo(tmpFileUri)) { success ->
|
||||
// Execute callback
|
||||
onResult(if (success) tmpFileUri else null)
|
||||
// Then remove the file and clear the picker
|
||||
if (deleteAfter) {
|
||||
tmpFile.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remembers and returns a [PickerLauncher] for recording a video with a camera app.
|
||||
* @param [onResult] will be called with either the video's [Uri] or `null` if nothing was selected.
|
||||
* @param [deleteAfter] When it's `true`, the recorded video will be automatically removed after calling [onResult].
|
||||
* It's `true` by default.
|
||||
*/
|
||||
@Composable
|
||||
fun registerCameraVideoPicker(onResult: (Uri?) -> Unit, deleteAfter: Boolean = true): PickerLauncher<Uri, Boolean> {
|
||||
// Tests and UI preview can't handle Context or FileProviders, so we might as well disable the whole picker
|
||||
return if (LocalInspectionMode.current || isInTest) {
|
||||
NoOpPickerLauncher { onResult(null) }
|
||||
} else {
|
||||
val context = LocalContext.current
|
||||
val tmpFile = remember { getTemporaryFile(context) }
|
||||
val tmpFileUri = remember(tmpFile) { getTemporaryUri(context, tmpFile) }
|
||||
rememberPickerLauncher(type = PickerType.Camera.Video(tmpFileUri)) { success ->
|
||||
// Execute callback
|
||||
onResult(if (success) tmpFileUri else null)
|
||||
// Then remove the file and clear the picker
|
||||
if (deleteAfter) {
|
||||
tmpFile.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTemporaryFile(
|
||||
context: Context,
|
||||
baseFolder: File = context.cacheDir,
|
||||
filename: String = UUID.randomUUID().toString(),
|
||||
): File {
|
||||
return File(baseFolder, filename)
|
||||
}
|
||||
|
||||
private fun getTemporaryUri(
|
||||
context: Context,
|
||||
file: File,
|
||||
): Uri {
|
||||
val authority = "${context.packageName}.fileprovider"
|
||||
return FileProvider.getUriForFile(context, authority, file)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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.mediapickers
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
|
||||
sealed interface PickerType<Input, Output> {
|
||||
fun getContract(): ActivityResultContract<Input, Output>
|
||||
fun getDefaultRequest(): Input
|
||||
|
||||
object ImageAndVideo : PickerType<PickVisualMediaRequest, Uri?> {
|
||||
override fun getContract() = ActivityResultContracts.PickVisualMedia()
|
||||
override fun getDefaultRequest(): PickVisualMediaRequest {
|
||||
return PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo)
|
||||
}
|
||||
}
|
||||
|
||||
object Camera {
|
||||
data class Photo(val destUri: Uri) : PickerType<Uri, Boolean> {
|
||||
override fun getContract() = ActivityResultContracts.TakePicture()
|
||||
override fun getDefaultRequest(): Uri {
|
||||
return destUri
|
||||
}
|
||||
}
|
||||
|
||||
data class Video(val destUri: Uri) : PickerType<Uri, Boolean> {
|
||||
override fun getContract() = ActivityResultContracts.CaptureVideo()
|
||||
override fun getDefaultRequest(): Uri {
|
||||
return destUri
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class File(val mimeType: String = MimeTypes.Any) : PickerType<String, Uri?> {
|
||||
override fun getContract() = ActivityResultContracts.GetContent()
|
||||
override fun getDefaultRequest(): String {
|
||||
return mimeType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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.mediapickers
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class PickerTypeTests {
|
||||
|
||||
@Test
|
||||
fun `ImageAndVideo - assert types`() {
|
||||
val pickerType = PickerType.ImageAndVideo
|
||||
assertThat(pickerType.getContract()).isInstanceOf(ActivityResultContracts.PickVisualMedia::class.java)
|
||||
assertThat(pickerType.getDefaultRequest().mediaType).isEqualTo(ActivityResultContracts.PickVisualMedia.ImageAndVideo)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `File - assert types`() {
|
||||
val pickerType = PickerType.File()
|
||||
assertThat(pickerType.getContract()).isInstanceOf(ActivityResultContracts.GetContent::class.java)
|
||||
assertThat(pickerType.getDefaultRequest()).isEqualTo(MimeTypes.Any)
|
||||
|
||||
val mimeType = MimeTypes.Images
|
||||
val customPickerType = PickerType.File(mimeType)
|
||||
assertThat(customPickerType.getContract()).isInstanceOf(ActivityResultContracts.GetContent::class.java)
|
||||
assertThat(customPickerType.getDefaultRequest()).isEqualTo(mimeType)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CameraPhoto - assert types`() {
|
||||
val uri = Uri.parse("file:///tmp/test")
|
||||
val pickerType = PickerType.Camera.Photo(uri)
|
||||
assertThat(pickerType.getContract()).isInstanceOf(ActivityResultContracts.TakePicture::class.java)
|
||||
assertThat(pickerType.getDefaultRequest()).isEqualTo(uri)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CameraVideo - assert types`() {
|
||||
val uri = Uri.parse("file:///tmp/test")
|
||||
val pickerType = PickerType.Camera.Video(uri)
|
||||
assertThat(pickerType.getContract()).isInstanceOf(ActivityResultContracts.CaptureVideo::class.java)
|
||||
assertThat(pickerType.getDefaultRequest()).isEqualTo(uri)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -24,11 +24,6 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||
import io.element.android.libraries.push.impl.log.pushLoggerTag
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
|
|
@ -74,28 +69,28 @@ class NotifiableEventResolver @Inject constructor(
|
|||
}
|
||||
).orDefault(roomId, eventId)
|
||||
|
||||
return notificationData.asNotifiableEvent(sessionId, roomId, eventId)
|
||||
return notificationData.asNotifiableEvent(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun NotificationData.asNotifiableEvent(userId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent {
|
||||
private fun NotificationData.asNotifiableEvent(userId: SessionId): NotifiableEvent {
|
||||
return NotifiableMessageEvent(
|
||||
sessionId = userId,
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
editedEventId = null,
|
||||
canBeReplaced = true,
|
||||
noisy = false,
|
||||
noisy = isNoisy,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
senderName = null,
|
||||
senderId = null,
|
||||
senderName = senderDisplayName,
|
||||
senderId = senderId.value,
|
||||
body = "Message ${eventId.value.take(8)}… in room ${roomId.value.take(8)}…",
|
||||
imageUriString = null,
|
||||
threadId = null,
|
||||
roomName = null,
|
||||
roomIsDirect = false,
|
||||
roomAvatarPath = null,
|
||||
senderAvatarPath = null,
|
||||
roomAvatarPath = roomAvatarUrl,
|
||||
senderAvatarPath = senderAvatarUrl,
|
||||
soundName = null,
|
||||
outGoingMessage = false,
|
||||
outGoingMessageFailed = false,
|
||||
|
|
@ -109,33 +104,11 @@ private fun NotificationData.asNotifiableEvent(userId: SessionId, roomId: RoomId
|
|||
*/
|
||||
private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): NotificationData {
|
||||
return this ?: NotificationData(
|
||||
item = MatrixTimelineItem.Event(
|
||||
event = EventTimelineItem(
|
||||
uniqueIdentifier = eventId.value,
|
||||
eventId = eventId,
|
||||
isEditable = false,
|
||||
isLocal = false,
|
||||
isOwn = false,
|
||||
isRemote = false,
|
||||
localSendState = null,
|
||||
reactions = emptyList(),
|
||||
sender = UserId(""),
|
||||
senderProfile = ProfileTimelineDetails.Unavailable,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
content = MessageContent(
|
||||
body = eventId.value,
|
||||
inReplyTo = null,
|
||||
isEdited = false,
|
||||
type = TextMessageType(
|
||||
body = eventId.value,
|
||||
formatted = null
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
title = roomId.value,
|
||||
subtitle = eventId.value,
|
||||
eventId = eventId,
|
||||
senderId = UserId("@user:domain"),
|
||||
roomId = roomId,
|
||||
isNoisy = false,
|
||||
avatarUrl = null,
|
||||
isEncrypted = false,
|
||||
isDirect = false
|
||||
)
|
||||
}
|
||||
|
|
|
|||
30
libraries/push/impl/src/main/res/values-de/translations.xml
Normal file
30
libraries/push/impl/src/main/res/values-de/translations.xml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="notification_channel_noisy">"Laute Benachrichtigungen"</string>
|
||||
<string name="notification_invitation_action_join">"Beitreten"</string>
|
||||
<string name="notification_invitation_action_reject">"Ablehnen"</string>
|
||||
<string name="notification_new_messages">"Neue Nachrichten"</string>
|
||||
<string name="notification_room_action_mark_as_read">"Als gelesen markieren"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room_and_invitation">"%1$s in %2$s und %3$s"</string>
|
||||
<plurals name="notification_compat_summary_line_for_room">
|
||||
<item quantity="one">"%1$s: %2$d Nachricht"</item>
|
||||
<item quantity="other">"%1$s: %2$d Nachrichten"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_invitations">
|
||||
<item quantity="one">"%d Einladung"</item>
|
||||
<item quantity="other">"%d Einladungen"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_new_messages_for_room">
|
||||
<item quantity="one">"%d neue Nachricht"</item>
|
||||
<item quantity="other">"%d neue Nachrichten"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_unread_notified_messages_in_room_rooms">
|
||||
<item quantity="one">"%d Raum"</item>
|
||||
<item quantity="other">"%d Räume"</item>
|
||||
</plurals>
|
||||
<string name="push_distributor_firebase_android">"Google-Dienste"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"Keine gültigen Google Play-Dienste gefunden. Benachrichtigungen funktionieren möglicherweise nicht richtig."</string>
|
||||
<string name="notification_room_action_quick_reply">"Schnellantwort"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="notification_room_action_quick_reply">"Respuesta rápida"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="notification_room_action_quick_reply">"Risposta rapida"</string>
|
||||
</resources>
|
||||
54
libraries/push/impl/src/main/res/values-ro/translations.xml
Normal file
54
libraries/push/impl/src/main/res/values-ro/translations.xml
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="notification_channel_call">"Apel"</string>
|
||||
<string name="notification_channel_listening_for_events">"Ascultare evenimente"</string>
|
||||
<string name="notification_channel_noisy">"Notificări zgomotoase"</string>
|
||||
<string name="notification_channel_silent">"Notificări silențioase"</string>
|
||||
<string name="notification_inline_reply_failed">"** Trimiterea eșuată - vă rugăm să deschideți camera"</string>
|
||||
<string name="notification_invitation_action_join">"Alăturați-vă"</string>
|
||||
<string name="notification_invitation_action_reject">"Respingeți"</string>
|
||||
<string name="notification_new_messages">"Mesaje noi"</string>
|
||||
<string name="notification_room_action_mark_as_read">"Marcați ca citit"</string>
|
||||
<string name="notification_sender_me">"Eu"</string>
|
||||
<string name="notification_test_push_notification_content">"Vizualizați o notificare! Faceți clic pe mine!"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
|
||||
<string name="notification_unread_notified_messages_and_invitation">"%1$s și %2$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room">"%1$s în %2$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room_and_invitation">"%1$s în %2$s și %3$s"</string>
|
||||
<plurals name="notification_compat_summary_line_for_room">
|
||||
<item quantity="one">"%1$s: %2$d mesaj"</item>
|
||||
<item quantity="few"></item>
|
||||
<item quantity="other">"%1$s: %2$d mesaje"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_compat_summary_title">
|
||||
<item quantity="one">"%d notificare"</item>
|
||||
<item quantity="few"></item>
|
||||
<item quantity="other">"%d notificări"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_invitations">
|
||||
<item quantity="one">"%d invitație"</item>
|
||||
<item quantity="few"></item>
|
||||
<item quantity="other">"%d invitații"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_new_messages_for_room">
|
||||
<item quantity="one">"%d mesaj nou"</item>
|
||||
<item quantity="few"></item>
|
||||
<item quantity="other">"%d mesaje noi"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_unread_notified_messages">
|
||||
<item quantity="one">"%d mesaj notificat necitit"</item>
|
||||
<item quantity="few"></item>
|
||||
<item quantity="other">"%d mesaje notificate necitite"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_unread_notified_messages_in_room_rooms">
|
||||
<item quantity="one">"%d cameră"</item>
|
||||
<item quantity="few"></item>
|
||||
<item quantity="other">"%d camere"</item>
|
||||
</plurals>
|
||||
<string name="push_choose_distributor_dialog_title_android">"Alegeți modul de primire a notificărilor"</string>
|
||||
<string name="push_distributor_background_sync_android">"Sincronizare în fundal"</string>
|
||||
<string name="push_distributor_firebase_android">"Servicii Google"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"Nu au fost găsite servicii Google Play valide. Este posibil ca notificările să nu funcționeze corect."</string>
|
||||
<string name="notification_room_action_quick_reply">"Raspuns rapid"</string>
|
||||
</resources>
|
||||
|
|
@ -9,7 +9,6 @@
|
|||
<string name="notification_invitation_action_reject">"Reject"</string>
|
||||
<string name="notification_new_messages">"New Messages"</string>
|
||||
<string name="notification_room_action_mark_as_read">"Mark as read"</string>
|
||||
<string name="notification_room_action_quick_reply">"Quick reply"</string>
|
||||
<string name="notification_sender_me">"Me"</string>
|
||||
<string name="notification_test_push_notification_content">"You are viewing the notification! Click me!"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
|
|
@ -45,4 +44,5 @@
|
|||
<string name="push_distributor_background_sync_android">"Background synchronization"</string>
|
||||
<string name="push_distributor_firebase_android">"Google Services"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"No valid Google Play Services found. Notifications may not work properly."</string>
|
||||
<string name="notification_room_action_quick_reply">"Quick reply"</string>
|
||||
</resources>
|
||||
|
|
@ -38,6 +38,11 @@ object TestTags {
|
|||
*/
|
||||
val changeServerServer = TestTag("change_server-server")
|
||||
val changeServerContinue = TestTag("change_server-continue")
|
||||
|
||||
/**
|
||||
* Room list / Home screen.
|
||||
*/
|
||||
val homeScreenSettings = TestTag("home_screen-settings")
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ fun TextComposer(
|
|||
onFullscreenToggle: () -> Unit = {},
|
||||
onCloseSpecialMode: () -> Unit = {},
|
||||
onComposerTextChange: (CharSequence) -> Unit = {},
|
||||
onAddAttachment:() -> Unit = {},
|
||||
) {
|
||||
if (LocalInspectionMode.current) {
|
||||
FakeComposer(modifier)
|
||||
|
|
@ -78,6 +79,7 @@ fun TextComposer(
|
|||
}
|
||||
|
||||
override fun onAddAttachment() {
|
||||
onAddAttachment()
|
||||
}
|
||||
|
||||
override fun onExpandOrCompactChange() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="rich_text_editor_composer_placeholder">"Nachricht…"</string>
|
||||
<string name="rich_text_editor_link">"Link setzen"</string>
|
||||
</resources>
|
||||
|
|
@ -1,5 +1,106 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="a11y_hide_password">"Passwort ausblenden"</string>
|
||||
<string name="a11y_send_files">"Dateien senden"</string>
|
||||
<string name="a11y_show_password">"Passwort anzeigen"</string>
|
||||
<string name="a11y_user_menu">"Benutzermenü"</string>
|
||||
<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_close">"Schließen"</string>
|
||||
<string name="action_complete_verification">"Verifizierung abschließen"</string>
|
||||
<string name="action_confirm">"Bestätigen"</string>
|
||||
<string name="action_copy">"Kopieren"</string>
|
||||
<string name="action_copy_link">"Link kopieren"</string>
|
||||
<string name="action_create">"Erstellen"</string>
|
||||
<string name="action_decline">"Ablehnen"</string>
|
||||
<string name="action_disable">"Deaktivieren"</string>
|
||||
<string name="action_done">"Fertig"</string>
|
||||
<string name="action_edit">"Bearbeiten"</string>
|
||||
<string name="action_enable">"Aktivieren"</string>
|
||||
<string name="action_invite">"Einladen"</string>
|
||||
<string name="action_invite_friends_to_app">"Freunde zu %1$s einladen"</string>
|
||||
<string name="action_invites_list">"Einladungen"</string>
|
||||
<string name="action_learn_more">"Mehr erfahren"</string>
|
||||
<string name="action_leave">"Verlassen"</string>
|
||||
<string name="action_leave_room">"Raum verlassen"</string>
|
||||
<string name="action_next">"Weiter"</string>
|
||||
<string name="action_no">"Nein"</string>
|
||||
<string name="action_ok">"OK"</string>
|
||||
<string name="action_quick_reply">"Schnellantwort"</string>
|
||||
<string name="action_quote">"Zitieren"</string>
|
||||
<string name="action_remove">"Entfernen"</string>
|
||||
<string name="action_report_bug">"Fehler melden"</string>
|
||||
<string name="action_report_content">"Inhalt melden"</string>
|
||||
<string name="action_retry">"Erneut versuchen"</string>
|
||||
<string name="action_retry_decryption">"Entschlüsselung erneut versuchen"</string>
|
||||
<string name="action_save">"Speichern"</string>
|
||||
<string name="action_search">"Suchen"</string>
|
||||
<string name="action_send">"Senden"</string>
|
||||
<string name="action_send_message">"Nachricht senden"</string>
|
||||
<string name="action_share">"Teilen"</string>
|
||||
<string name="action_share_link">"Link teilen"</string>
|
||||
<string name="action_skip">"Überspringen"</string>
|
||||
<string name="action_take_photo">"Foto aufnehmen"</string>
|
||||
<string name="action_yes">"Ja"</string>
|
||||
<string name="common_about">"Über"</string>
|
||||
<string name="common_analytics">"Analytik"</string>
|
||||
<string name="common_audio">"Audio"</string>
|
||||
<string name="common_bubbles">"Blasen"</string>
|
||||
<string name="common_decryption_error">"Entschlüsselungsfehler"</string>
|
||||
<string name="common_developer_options">"Entwickleroptionen"</string>
|
||||
<string name="common_edited_suffix">"(bearbeitet)"</string>
|
||||
<string name="common_encryption_enabled">"Verschlüsselung aktiviert"</string>
|
||||
<string name="common_error">"Fehler"</string>
|
||||
<string name="common_file">"Datei"</string>
|
||||
<string name="common_gif">"GIF"</string>
|
||||
<string name="common_image">"Bild"</string>
|
||||
<string name="common_link_copied_to_clipboard">"Link in Zwischenablage kopiert"</string>
|
||||
<string name="common_message">"Nachricht"</string>
|
||||
<string name="common_modern">"Modern"</string>
|
||||
<string name="common_offline">"Offline"</string>
|
||||
<string name="common_password">"Passwort"</string>
|
||||
<string name="common_reactions">"Reaktionen"</string>
|
||||
<string name="common_security">"Sicherheit"</string>
|
||||
<string name="common_settings">"Einstellungen"</string>
|
||||
<string name="common_sticker">"Sticker"</string>
|
||||
<string name="common_success">"Erfolg"</string>
|
||||
<string name="common_suggestions">"Vorschläge"</string>
|
||||
<string name="common_topic">"Thema"</string>
|
||||
<string name="common_unable_to_decrypt">"Entschlüsselung nicht möglich"</string>
|
||||
<string name="common_unsupported_event">"Nicht unterstütztes Ereignis"</string>
|
||||
<string name="common_username">"Benutzername"</string>
|
||||
<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="dialog_title_warning">"Warnung"</string>
|
||||
<string name="emoji_picker_category_activity">"Aktivitäten"</string>
|
||||
<string name="emoji_picker_category_flags">"Flaggen"</string>
|
||||
<string name="emoji_picker_category_foods">"Essen & Trinken"</string>
|
||||
<string name="emoji_picker_category_nature">"Tiere & Natur"</string>
|
||||
<string name="emoji_picker_category_objects">"Objekte"</string>
|
||||
<string name="emoji_picker_category_people">"Smileys & Personen"</string>
|
||||
<string name="emoji_picker_category_places">"Reisen & Orte"</string>
|
||||
<string name="emoji_picker_category_symbols">"Symbole"</string>
|
||||
<string name="error_failed_loading_messages">"Fehler beim Laden der Nachrichten"</string>
|
||||
<string name="error_unknown">"Entschuldigung, ein Fehler ist aufgetreten."</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<plurals name="common_member_count">
|
||||
<item quantity="one">"%1$d Mitglied"</item>
|
||||
<item quantity="other">"%1$d Mitglieder"</item>
|
||||
</plurals>
|
||||
<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_read_marker_title">"Neu"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Blockieren"</string>
|
||||
<string name="screen_room_member_details_block_user">"Nutzer blockieren"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Blockierung aufheben"</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Nutzer entblockieren"</string>
|
||||
<string name="settings_rageshake_detection_threshold">"Erkennungsschwelle"</string>
|
||||
<string name="settings_version_number">"Version: %1$s (%2$s)"</string>
|
||||
<string name="test_language_identifier">"de"</string>
|
||||
<string name="dialog_title_error">"Fehler"</string>
|
||||
<string name="dialog_title_success">"Erfolg"</string>
|
||||
<string name="screen_report_content_block_user">"Nutzer blockieren"</string>
|
||||
</resources>
|
||||
|
|
@ -94,8 +94,6 @@
|
|||
<string name="common_video">"Vídeo"</string>
|
||||
<string name="common_waiting">"Esperando…"</string>
|
||||
<string name="dialog_title_confirmation">"Confirmar"</string>
|
||||
<string name="dialog_title_error">"Error"</string>
|
||||
<string name="dialog_title_success">"Terminado"</string>
|
||||
<string name="dialog_title_warning">"Atención"</string>
|
||||
<string name="emoji_picker_category_activity">"Actividades"</string>
|
||||
<string name="emoji_picker_category_flags">"Banderas"</string>
|
||||
|
|
@ -129,7 +127,6 @@
|
|||
<string name="room_timeline_beginning_of_room">"Este es el principio de %1$s."</string>
|
||||
<string name="room_timeline_beginning_of_room_no_name">"Este es el principio de esta conversación."</string>
|
||||
<string name="room_timeline_read_marker_title">"Nuevos"</string>
|
||||
<string name="screen_report_content_block_user">"Bloquear usuario"</string>
|
||||
<string name="screen_report_content_block_user_hint">"Marque si quieres ocultar todos los mensajes actuales y futuros de este usuario"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Bloquear"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Los usuarios bloqueados no podrán enviarte mensajes y se ocultarán todos sus mensajes. Puede revertir esta acción en cualquier momento."</string>
|
||||
|
|
@ -142,4 +139,7 @@
|
|||
<string name="settings_title_general">"General"</string>
|
||||
<string name="settings_version_number">"Versión: %1$s (%2$s)"</string>
|
||||
<string name="test_language_identifier">"es"</string>
|
||||
<string name="dialog_title_error">"Error"</string>
|
||||
<string name="dialog_title_success">"Terminado"</string>
|
||||
<string name="screen_report_content_block_user">"Bloquear usuario"</string>
|
||||
</resources>
|
||||
|
|
@ -94,8 +94,6 @@
|
|||
<string name="common_video">"Video"</string>
|
||||
<string name="common_waiting">"In attesa…"</string>
|
||||
<string name="dialog_title_confirmation">"Conferma"</string>
|
||||
<string name="dialog_title_error">"Errore"</string>
|
||||
<string name="dialog_title_success">"Operazione riuscita"</string>
|
||||
<string name="dialog_title_warning">"Attenzione"</string>
|
||||
<string name="emoji_picker_category_activity">"Attività"</string>
|
||||
<string name="emoji_picker_category_flags">"Bandiere"</string>
|
||||
|
|
@ -129,7 +127,6 @@
|
|||
<string name="room_timeline_beginning_of_room">"Questo è l\'inizio di %1$s."</string>
|
||||
<string name="room_timeline_beginning_of_room_no_name">"Questo è l\'inizio della conversazione."</string>
|
||||
<string name="room_timeline_read_marker_title">"Nuovo"</string>
|
||||
<string name="screen_report_content_block_user">"Blocca utente"</string>
|
||||
<string name="screen_report_content_block_user_hint">"Seleziona se vuoi nascondere tutti i messaggi attuali e futuri di questo utente"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Blocca"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Gli utenti bloccati non saranno in grado di inviarti nuovi messaggi e tutti quelli già esistenti saranno nascosti. Potrai annullare questa azione in qualsiasi momento."</string>
|
||||
|
|
@ -142,4 +139,7 @@
|
|||
<string name="settings_title_general">"Generali"</string>
|
||||
<string name="settings_version_number">"Versione: %1$s (%2$s)"</string>
|
||||
<string name="test_language_identifier">"it"</string>
|
||||
<string name="dialog_title_error">"Errore"</string>
|
||||
<string name="dialog_title_success">"Operazione riuscita"</string>
|
||||
<string name="screen_report_content_block_user">"Blocca utente"</string>
|
||||
</resources>
|
||||
|
|
@ -4,8 +4,10 @@
|
|||
<string name="a11y_send_files">"Trimiteți fișiere"</string>
|
||||
<string name="a11y_show_password">"Afișați parola"</string>
|
||||
<string name="a11y_user_menu">"Meniu utilizator"</string>
|
||||
<string name="action_accept">"Acceptați"</string>
|
||||
<string name="action_back">"Înapoi"</string>
|
||||
<string name="action_cancel">"Anulați"</string>
|
||||
<string name="action_choose_photo">"Alegeți o fotografie"</string>
|
||||
<string name="action_clear">"Ștergeți"</string>
|
||||
<string name="action_close">"Închideți"</string>
|
||||
<string name="action_complete_verification">"Verificare completă"</string>
|
||||
|
|
@ -13,13 +15,16 @@
|
|||
<string name="action_continue">"Continuați"</string>
|
||||
<string name="action_copy">"Copiați"</string>
|
||||
<string name="action_copy_link">"Copiați linkul"</string>
|
||||
<string name="action_create">"Creați"</string>
|
||||
<string name="action_create_a_room">"Creați o cameră"</string>
|
||||
<string name="action_decline">"Refuzați"</string>
|
||||
<string name="action_disable">"Dezactivați"</string>
|
||||
<string name="action_done">"Efectuat"</string>
|
||||
<string name="action_edit">"Editați"</string>
|
||||
<string name="action_enable">"Activați"</string>
|
||||
<string name="action_invite">"Invitați"</string>
|
||||
<string name="action_invite_friends_to_app">"Invitați prieteni în %1$s"</string>
|
||||
<string name="action_invites_list">"Invitații"</string>
|
||||
<string name="action_learn_more">"Aflați mai multe"</string>
|
||||
<string name="action_leave">"Părăsiți"</string>
|
||||
<string name="action_leave_room">"Părăsiți camera"</string>
|
||||
|
|
@ -38,15 +43,18 @@
|
|||
<string name="action_save">"Salvați"</string>
|
||||
<string name="action_search">"Căutați"</string>
|
||||
<string name="action_send">"Trimiteți"</string>
|
||||
<string name="action_send_message">"Trimiteți mesajul"</string>
|
||||
<string name="action_share">"Partajați"</string>
|
||||
<string name="action_share_link">"Partajați linkul"</string>
|
||||
<string name="action_skip">"Omiteți"</string>
|
||||
<string name="action_start">"Începeți"</string>
|
||||
<string name="action_start_chat">"Începeți discuția"</string>
|
||||
<string name="action_start_verification">"Începeți verificarea"</string>
|
||||
<string name="action_take_photo">"Faceți o fotografie"</string>
|
||||
<string name="action_view_source">"Vedeți sursă"</string>
|
||||
<string name="action_yes">"Da"</string>
|
||||
<string name="common_about">"Despre"</string>
|
||||
<string name="common_analytics">"Analitice"</string>
|
||||
<string name="common_audio">"Audio"</string>
|
||||
<string name="common_bubbles">"Baloane"</string>
|
||||
<string name="common_creating_room">"Se creează camera…"</string>
|
||||
|
|
@ -94,8 +102,6 @@
|
|||
<string name="common_video">"Video"</string>
|
||||
<string name="common_waiting">"Se aşteaptă…"</string>
|
||||
<string name="dialog_title_confirmation">"Confirmare"</string>
|
||||
<string name="dialog_title_error">"Eroare"</string>
|
||||
<string name="dialog_title_success">"Succes"</string>
|
||||
<string name="dialog_title_warning">"Avertisment"</string>
|
||||
<string name="emoji_picker_category_activity">"Activități"</string>
|
||||
<string name="emoji_picker_category_flags">"Steaguri"</string>
|
||||
|
|
@ -131,7 +137,17 @@
|
|||
<string name="room_timeline_beginning_of_room">"Acesta este începutul conversației %1$s."</string>
|
||||
<string name="room_timeline_beginning_of_room_no_name">"Acesta este începutul acestei conversații."</string>
|
||||
<string name="room_timeline_read_marker_title">"Nou"</string>
|
||||
<string name="screen_report_content_block_user">"Blocați utilizatorul"</string>
|
||||
<string name="screen_analytics_help_us_improve">"Ajutați-ne să identificăm problemele și să îmbunătățim %1$s prin partajarea datelor de utilizare anonime."</string>
|
||||
<string name="screen_analytics_prompt_data_usage"><b>"Nu"</b>" înregistrăm sau profilăm datele contului"</string>
|
||||
<string name="screen_analytics_prompt_help_us_improve">"Ajutați-ne să identificăm problemele și să îmbunătățim %1$s prin partajarea datelor de utilizare anonime."</string>
|
||||
<string name="screen_analytics_prompt_read_terms">"Puteți citi toate condițiile noastre %1$s."</string>
|
||||
<string name="screen_analytics_prompt_read_terms_content_link">"aici"</string>
|
||||
<string name="screen_analytics_prompt_settings">"Puteți dezactiva această opțiune oricând din setări"</string>
|
||||
<string name="screen_analytics_prompt_third_party_sharing"><b>"Nu"</b>" împărtășim informații cu terți"</string>
|
||||
<string name="screen_analytics_prompt_title">"Ajutați la îmbunătățirea %1$s"</string>
|
||||
<string name="screen_analytics_read_terms">"Puteți citi toate condițiile noastre %1$s."</string>
|
||||
<string name="screen_analytics_read_terms_content_link">"aici"</string>
|
||||
<string name="screen_analytics_share_data">"Partajați datele analitice"</string>
|
||||
<string name="screen_report_content_block_user_hint">"Confirmați că doriți să ascundeți toate mesajele curente și viitoare de la acest utilizator"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Blocați"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând."</string>
|
||||
|
|
@ -144,4 +160,7 @@
|
|||
<string name="settings_title_general">"General"</string>
|
||||
<string name="settings_version_number">"Versiunea: %1$s (%2$s)"</string>
|
||||
<string name="test_language_identifier">"ro"</string>
|
||||
<string name="dialog_title_error">"Eroare"</string>
|
||||
<string name="dialog_title_success">"Succes"</string>
|
||||
<string name="screen_report_content_block_user">"Blocați utilizatorul"</string>
|
||||
</resources>
|
||||
|
|
@ -102,8 +102,6 @@
|
|||
<string name="common_video">"Video"</string>
|
||||
<string name="common_waiting">"Waiting…"</string>
|
||||
<string name="dialog_title_confirmation">"Confirmation"</string>
|
||||
<string name="dialog_title_error">"Error"</string>
|
||||
<string name="dialog_title_success">"Success"</string>
|
||||
<string name="dialog_title_warning">"Warning"</string>
|
||||
<string name="emoji_picker_category_activity">"Activities"</string>
|
||||
<string name="emoji_picker_category_flags">"Flags"</string>
|
||||
|
|
@ -122,60 +120,15 @@
|
|||
<string name="leave_room_alert_private_subtitle">"Are you sure that you want to leave this room? This room is not public and you will not be able to rejoin without an invite."</string>
|
||||
<string name="leave_room_alert_subtitle">"Are you sure that you want to leave the room?"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="notification_channel_call">"Call"</string>
|
||||
<string name="notification_channel_listening_for_events">"Listening for events"</string>
|
||||
<string name="notification_channel_noisy">"Noisy notifications"</string>
|
||||
<string name="notification_channel_silent">"Silent notifications"</string>
|
||||
<string name="notification_inline_reply_failed">"** Failed to send - please open room"</string>
|
||||
<string name="notification_invitation_action_join">"Join"</string>
|
||||
<string name="notification_invitation_action_reject">"Reject"</string>
|
||||
<string name="notification_new_messages">"New Messages"</string>
|
||||
<string name="notification_room_action_mark_as_read">"Mark as read"</string>
|
||||
<string name="notification_room_action_quick_reply">"Quick reply"</string>
|
||||
<string name="notification_sender_me">"Me"</string>
|
||||
<string name="notification_test_push_notification_content">"You are viewing the notification! Click me!"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
|
||||
<string name="notification_unread_notified_messages_and_invitation">"%1$s and %2$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room">"%1$s in %2$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room_and_invitation">"%1$s in %2$s and %3$s"</string>
|
||||
<plurals name="common_member_count">
|
||||
<item quantity="one">"%1$d member"</item>
|
||||
<item quantity="other">"%1$d members"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_compat_summary_line_for_room">
|
||||
<item quantity="one">"%1$s: %2$d message"</item>
|
||||
<item quantity="other">"%1$s: %2$d messages"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_compat_summary_title">
|
||||
<item quantity="one">"%d notification"</item>
|
||||
<item quantity="other">"%d notifications"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_invitations">
|
||||
<item quantity="one">"%d invitation"</item>
|
||||
<item quantity="other">"%d invitations"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_new_messages_for_room">
|
||||
<item quantity="one">"%d new message"</item>
|
||||
<item quantity="other">"%d new messages"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_unread_notified_messages">
|
||||
<item quantity="one">"%d unread notified message"</item>
|
||||
<item quantity="other">"%d unread notified messages"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_unread_notified_messages_in_room_rooms">
|
||||
<item quantity="one">"%d room"</item>
|
||||
<item quantity="other">"%d rooms"</item>
|
||||
</plurals>
|
||||
<plurals name="room_timeline_state_changes">
|
||||
<item quantity="one">"%1$d room change"</item>
|
||||
<item quantity="other">"%1$d room changes"</item>
|
||||
</plurals>
|
||||
<string name="preference_rageshake">"Rageshake to report bug"</string>
|
||||
<string name="push_choose_distributor_dialog_title_android">"Choose how to receive notifications"</string>
|
||||
<string name="push_distributor_background_sync_android">"Background synchronization"</string>
|
||||
<string name="push_distributor_firebase_android">"Google Services"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"No valid Google Play Services found. Notifications may not work properly."</string>
|
||||
<string name="rageshake_dialog_content">"You seem to be shaking the phone in frustration. Would you like to open the bug report screen?"</string>
|
||||
<string name="report_content_explanation">"This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages."</string>
|
||||
<string name="report_content_hint">"Reason for reporting this content"</string>
|
||||
|
|
@ -193,7 +146,6 @@
|
|||
<string name="screen_analytics_read_terms">"You can read all our terms %1$s."</string>
|
||||
<string name="screen_analytics_read_terms_content_link">"here"</string>
|
||||
<string name="screen_analytics_share_data">"Share analytics data"</string>
|
||||
<string name="screen_report_content_block_user">"Block user"</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_room_member_details_block_alert_action">"Block"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime."</string>
|
||||
|
|
@ -207,4 +159,7 @@
|
|||
<string name="settings_version_number">"Version: %1$s (%2$s)"</string>
|
||||
<string name="test_language_identifier">"en"</string>
|
||||
<string name="test_untranslated_default_language_identifier">"en"</string>
|
||||
<string name="dialog_title_error">"Error"</string>
|
||||
<string name="dialog_title_success">"Success"</string>
|
||||
<string name="screen_report_content_block_user">"Block user"</string>
|
||||
</resources>
|
||||
Loading…
Add table
Add a link
Reference in a new issue