Merge branch 'develop' into feature/fga/room_list_filter_iteration
This commit is contained in:
commit
23b276dfcb
368 changed files with 6450 additions and 2317 deletions
|
|
@ -79,9 +79,9 @@ sealed interface AsyncAction<out T> {
|
|||
|
||||
fun isUninitialized(): Boolean = this == Uninitialized
|
||||
|
||||
fun isConfirming(): Boolean = this is Confirming
|
||||
fun isConfirming(): Boolean = this == Confirming
|
||||
|
||||
fun isLoading(): Boolean = this is Loading
|
||||
fun isLoading(): Boolean = this == Loading
|
||||
|
||||
fun isFailure(): Boolean = this is Failure
|
||||
|
||||
|
|
|
|||
|
|
@ -47,3 +47,9 @@ inline fun <R, T> Result<T>.flatMapCatching(transform: (T) -> Result<R>): Result
|
|||
onFailure = { Result.failure(it) }
|
||||
)
|
||||
}
|
||||
|
||||
inline fun <T> Result<T>.finally(block: (exception: Throwable?) -> Unit): Result<T> {
|
||||
onSuccess { block(null) }
|
||||
onFailure(block)
|
||||
return this
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ enum class AvatarSize(val dp: Dp) {
|
|||
InviteSender(16.dp),
|
||||
|
||||
EditRoomDetails(70.dp),
|
||||
RoomListManageUser(70.dp),
|
||||
|
||||
NotificationsOptIn(32.dp),
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -58,10 +59,11 @@ fun ModalBottomSheet(
|
|||
windowInsets: WindowInsets = BottomSheetDefaults.windowInsets,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val safeSheetState = if (LocalInspectionMode.current) sheetStateForPreview() else sheetState
|
||||
androidx.compose.material3.ModalBottomSheet(
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier = modifier,
|
||||
sheetState = sheetState,
|
||||
sheetState = safeSheetState,
|
||||
shape = shape,
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
|
|
@ -102,7 +104,6 @@ private fun ContentToPreview() {
|
|||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = {},
|
||||
sheetState = sheetStateForPreview(),
|
||||
) {
|
||||
Text(
|
||||
text = "Sheet Content",
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ class AsyncIndicatorTests {
|
|||
currentAnimationState = TransitionStateSnapshot(transitionState),
|
||||
)
|
||||
}.test {
|
||||
var firstItem: Any? = null
|
||||
var firstItem: Any?
|
||||
skipItems(1)
|
||||
state.enqueue(composable = {})
|
||||
state.enqueue(composable = {})
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="state_event_avatar_changed_too">"(аватар тоже был изменен)"</string>
|
||||
<string name="state_event_avatar_url_changed">"%1$s сменили свой аватар"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"Вы сменили аватар"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"Вы сменили изображение"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s изменил свое отображаемое имя с %2$s на %3$s"</string>
|
||||
<string name="state_event_display_name_changed_from_by_you">"Вы изменили свое отображаемое имя с %1$s на %2$s"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s удалил свое отображаемое имя (оно было %2$s)"</string>
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
<string name="state_event_room_avatar_changed_by_you">"Вы изменили аватар комнаты"</string>
|
||||
<string name="state_event_room_avatar_removed">"%1$s удалил аватар комнаты"</string>
|
||||
<string name="state_event_room_avatar_removed_by_you">"Вы удалили аватар комнаты"</string>
|
||||
<string name="state_event_room_ban">"%1$s заблокирован %2$s"</string>
|
||||
<string name="state_event_room_ban">"%1$s заблокировал %2$s"</string>
|
||||
<string name="state_event_room_ban_by_you">"Вы заблокировали %1$s"</string>
|
||||
<string name="state_event_room_created">"%1$s создал комнату"</string>
|
||||
<string name="state_event_room_created_by_you">"Вы создали комнату"</string>
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
<string name="state_event_room_invite_by_you">"Вы пригласили %1$s"</string>
|
||||
<string name="state_event_room_invite_you">"Пользователь %1$s пригласил вас"</string>
|
||||
<string name="state_event_room_join">"%1$s присоединился к комнате"</string>
|
||||
<string name="state_event_room_join_by_you">"Вы вошли в комнату"</string>
|
||||
<string name="state_event_room_join_by_you">"Вы присоединились к комнате"</string>
|
||||
<string name="state_event_room_knock">"%1$s запросил присоединение"</string>
|
||||
<string name="state_event_room_knock_accepted">"%1$s разрешил %2$s присоединиться"</string>
|
||||
<string name="state_event_room_knock_accepted_by_you">"%1$s разрешил вам присоединиться"</string>
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
<string name="state_event_room_name_changed_by_you">"Вы изменили название комнаты на: %1$s"</string>
|
||||
<string name="state_event_room_name_removed">"%1$s удалил название комнаты"</string>
|
||||
<string name="state_event_room_name_removed_by_you">"Вы удалили название комнаты"</string>
|
||||
<string name="state_event_room_none">"%1$s ничего не изменилось"</string>
|
||||
<string name="state_event_room_none">"%1$s ничего не изменил"</string>
|
||||
<string name="state_event_room_none_by_you">"Вы не внесли никаких изменений"</string>
|
||||
<string name="state_event_room_reject">"%1$s отклонил приглашение"</string>
|
||||
<string name="state_event_room_reject_by_you">"Вы отклонили приглашение"</string>
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
<string name="state_event_room_topic_changed_by_you">"Вы изменили тему на: %1$s"</string>
|
||||
<string name="state_event_room_topic_removed">"%1$s удалил тему комнаты"</string>
|
||||
<string name="state_event_room_topic_removed_by_you">"Вы удалили тему комнаты"</string>
|
||||
<string name="state_event_room_unban">"%1$s разблокирован %2$s"</string>
|
||||
<string name="state_event_room_unban">"%1$s разблокировал %2$s"</string>
|
||||
<string name="state_event_room_unban_by_you">"Вы разблокировали %1$s"</string>
|
||||
<string name="state_event_room_unknown_membership_change">"%1$s внес неизвестное изменение для своих участников"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,12 +3,16 @@
|
|||
<string name="state_event_avatar_changed_too">"(avatar was changed too)"</string>
|
||||
<string name="state_event_avatar_url_changed">"%1$s changed their avatar"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"You changed your avatar"</string>
|
||||
<string name="state_event_demoted_to_member">"%1$s was demoted to member"</string>
|
||||
<string name="state_event_demoted_to_moderator">"%1$s was demoted to moderator"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s changed their display name from %2$s to %3$s"</string>
|
||||
<string name="state_event_display_name_changed_from_by_you">"You changed your display name from %1$s to %2$s"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s removed their display name (it was %2$s)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"You removed your display name (it was %1$s)"</string>
|
||||
<string name="state_event_display_name_set">"%1$s set their display name to %2$s"</string>
|
||||
<string name="state_event_display_name_set_by_you">"You set your display name to %1$s"</string>
|
||||
<string name="state_event_promoted_to_administrator">"%1$s was promoted to admin"</string>
|
||||
<string name="state_event_promoted_to_moderator">"%1$s was promoted to moderator"</string>
|
||||
<string name="state_event_room_avatar_changed">"%1$s changed the room avatar"</string>
|
||||
<string name="state_event_room_avatar_changed_by_you">"You changed the room avatar"</string>
|
||||
<string name="state_event_room_avatar_removed">"%1$s removed the room avatar"</string>
|
||||
|
|
|
|||
|
|
@ -82,4 +82,11 @@ enum class FeatureFlags(
|
|||
defaultValue = true,
|
||||
isFinished = false,
|
||||
),
|
||||
RoomModeration(
|
||||
key = "feature.roomModeration",
|
||||
title = "Room moderation",
|
||||
description = "Add moderation features to the room for users with permissions",
|
||||
defaultValue = true,
|
||||
isFinished = false,
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ class StaticFeatureFlagProvider @Inject constructor() :
|
|||
FeatureFlags.Mentions -> true
|
||||
FeatureFlags.MarkAsUnread -> false
|
||||
FeatureFlags.RoomListFilters -> false
|
||||
FeatureFlags.RoomModeration -> false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
|
||||
interface SdkMetadata {
|
||||
val sdkGitSha: String
|
||||
}
|
||||
|
|
@ -29,12 +29,18 @@ import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
|||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
|
||||
|
|
@ -91,6 +97,10 @@ interface MatrixRoom : Closeable {
|
|||
|
||||
suspend fun unsubscribeFromSync()
|
||||
|
||||
suspend fun userRole(userId: UserId): Result<RoomMember.Role>
|
||||
|
||||
suspend fun updateUsersRoles(changes: List<UserRoleChange>): Result<Unit>
|
||||
|
||||
suspend fun userDisplayName(userId: UserId): Result<String?>
|
||||
|
||||
suspend fun userAvatarUrl(userId: UserId): Result<String?>
|
||||
|
|
@ -129,6 +139,8 @@ interface MatrixRoom : Closeable {
|
|||
|
||||
suspend fun canUserInvite(userId: UserId): Result<Boolean>
|
||||
|
||||
suspend fun canUserKick(userId: UserId): Result<Boolean>
|
||||
|
||||
suspend fun canUserBan(userId: UserId): Result<Boolean>
|
||||
|
||||
suspend fun canUserRedactOwn(userId: UserId): Result<Boolean>
|
||||
|
|
@ -144,6 +156,18 @@ interface MatrixRoom : Closeable {
|
|||
suspend fun canUserJoinCall(userId: UserId): Result<Boolean> =
|
||||
canUserSendState(userId, StateEventType.CALL_MEMBER)
|
||||
|
||||
fun usersWithRole(role: RoomMember.Role): Flow<ImmutableList<RoomMember>> {
|
||||
return roomInfoFlow
|
||||
.map { it.userPowerLevels.filter { (_, powerLevel) -> RoomMember.Role.forPowerLevel(powerLevel) == role } }
|
||||
.distinctUntilChanged()
|
||||
.combine(membersStateFlow) { powerLevels, membersState ->
|
||||
membersState.roomMembers()
|
||||
.orEmpty()
|
||||
.filter { powerLevels.containsKey(it.userId) }
|
||||
.toPersistentList()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit>
|
||||
|
||||
suspend fun removeAvatar(): Result<Unit>
|
||||
|
|
@ -154,6 +178,12 @@ interface MatrixRoom : Closeable {
|
|||
|
||||
suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit>
|
||||
|
||||
suspend fun kickUser(userId: UserId, reason: String? = null): Result<Unit>
|
||||
|
||||
suspend fun banUser(userId: UserId, reason: String? = null): Result<Unit>
|
||||
|
||||
suspend fun unbanUser(userId: UserId, reason: String? = null): Result<Unit>
|
||||
|
||||
suspend fun setIsFavorite(isFavorite: Boolean): Result<Unit>
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@
|
|||
package io.element.android.libraries.matrix.api.room
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
|
||||
@Immutable
|
||||
data class MatrixRoomInfo(
|
||||
|
|
@ -39,6 +41,7 @@ data class MatrixRoomInfo(
|
|||
val activeMembersCount: Long,
|
||||
val invitedMembersCount: Long,
|
||||
val joinedMembersCount: Long,
|
||||
val userPowerLevels: ImmutableMap<UserId, Long>,
|
||||
val highlightCount: Long,
|
||||
val notificationCount: Long,
|
||||
val userDefinedNotificationMode: RoomNotificationMode?,
|
||||
|
|
|
|||
|
|
@ -32,10 +32,20 @@ data class RoomMember(
|
|||
/**
|
||||
* Role of the RoomMember, based on its [powerLevel].
|
||||
*/
|
||||
enum class Role {
|
||||
ADMIN,
|
||||
MODERATOR,
|
||||
USER
|
||||
enum class Role(val powerLevel: Long) {
|
||||
ADMIN(100L),
|
||||
MODERATOR(50L),
|
||||
USER(0L);
|
||||
|
||||
companion object {
|
||||
fun forPowerLevel(powerLevel: Long): Role {
|
||||
return when {
|
||||
powerLevel >= ADMIN.powerLevel -> ADMIN
|
||||
powerLevel >= MODERATOR.powerLevel -> MODERATOR
|
||||
else -> USER
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@ import io.element.android.libraries.matrix.api.room.StateEventType
|
|||
*/
|
||||
suspend fun MatrixRoom.canInvite(): Result<Boolean> = canUserInvite(sessionId)
|
||||
|
||||
/**
|
||||
* Shortcut for calling [MatrixRoom.canUserKick] with our own user.
|
||||
*/
|
||||
suspend fun MatrixRoom.canKick(): Result<Boolean> = canUserKick(sessionId)
|
||||
|
||||
/**
|
||||
* Shortcut for calling [MatrixRoom.canBanUser] with our own user.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.powerlevels
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
||||
data class UserRoleChange(
|
||||
val userId: UserId,
|
||||
val role: RoomMember.Role,
|
||||
) {
|
||||
val powerLevel: Long = role.powerLevel
|
||||
}
|
||||
|
|
@ -39,6 +39,7 @@ interface MatrixTimeline : AutoCloseable {
|
|||
|
||||
val paginationState: StateFlow<PaginationState>
|
||||
val timelineItems: Flow<List<MatrixTimelineItem>>
|
||||
val membershipChangeEventReceived: Flow<Unit>
|
||||
|
||||
suspend fun paginateBackwards(requestSize: Int): Result<Unit>
|
||||
suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ data class TracingFilterConfiguration(
|
|||
Target.MATRIX_SDK_SLIDING_SYNC to LogLevel.TRACE,
|
||||
Target.MATRIX_SDK_BASE_SLIDING_SYNC to LogLevel.TRACE,
|
||||
Target.MATRIX_SDK_UI_TIMELINE to LogLevel.TRACE,
|
||||
Target.MATRIX_SDK_BASE_READ_RECEIPTS to LogLevel.TRACE,
|
||||
)
|
||||
|
||||
fun getLogLevel(target: Target): LogLevel {
|
||||
|
|
@ -87,7 +86,6 @@ object TracingFilterConfigurations {
|
|||
val nightly = TracingFilterConfiguration(
|
||||
overrides = mapOf(
|
||||
Target.ELEMENT to LogLevel.TRACE,
|
||||
Target.MATRIX_SDK_BASE_READ_RECEIPTS to LogLevel.TRACE,
|
||||
),
|
||||
)
|
||||
val debug = TracingFilterConfiguration(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.SdkMetadata
|
||||
import org.matrix.rustcomponents.sdk.sdkGitSha
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class RustSdkMetadata @Inject constructor() : SdkMetadata {
|
||||
override val sdkGitSha: String
|
||||
get() = sdkGitSha()
|
||||
}
|
||||
|
|
@ -16,12 +16,15 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.room
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentMap
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import org.matrix.rustcomponents.sdk.Membership as RustMembership
|
||||
import org.matrix.rustcomponents.sdk.RoomInfo as RustRoomInfo
|
||||
|
|
@ -45,10 +48,11 @@ class MatrixRoomInfoMapper(
|
|||
alternativeAliases = it.alternativeAliases.toImmutableList(),
|
||||
currentUserMembership = it.membership.map(),
|
||||
latestEvent = it.latestEvent?.use(timelineItemMapper::map),
|
||||
inviter = it.inviter?.use(RoomMemberMapper::map),
|
||||
inviter = it.inviter?.let(RoomMemberMapper::map),
|
||||
activeMembersCount = it.activeMembersCount.toLong(),
|
||||
invitedMembersCount = it.invitedMembersCount.toLong(),
|
||||
joinedMembersCount = it.joinedMembersCount.toLong(),
|
||||
userPowerLevels = mapPowerLevels(it.userPowerLevels),
|
||||
highlightCount = it.highlightCount.toLong(),
|
||||
notificationCount = it.notificationCount.toLong(),
|
||||
userDefinedNotificationMode = it.userDefinedNotificationMode?.map(),
|
||||
|
|
@ -69,3 +73,7 @@ fun RustRoomNotificationMode.map(): RoomNotificationMode = when (this) {
|
|||
RustRoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
|
||||
RustRoomNotificationMode.MUTE -> RoomNotificationMode.MUTE
|
||||
}
|
||||
|
||||
fun mapPowerLevels(powerLevels: Map<String, Long>): ImmutableMap<UserId, Long> {
|
||||
return powerLevels.mapKeys { (key, _) -> UserId(key) }.toPersistentMap()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,8 +36,10 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
|||
import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState
|
||||
import io.element.android.libraries.matrix.api.room.Mention
|
||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
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.room.powerlevels.UserRoleChange
|
||||
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.timeline.ReceiptType
|
||||
|
|
@ -51,6 +53,7 @@ import io.element.android.libraries.matrix.impl.notificationsettings.RustNotific
|
|||
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.room.member.RoomMemberListFetcher
|
||||
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
|
||||
import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType
|
||||
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
|
||||
|
|
@ -65,6 +68,8 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.EventTimelineItem
|
||||
|
|
@ -74,6 +79,7 @@ import org.matrix.rustcomponents.sdk.RoomListItem
|
|||
import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation
|
||||
import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle
|
||||
import org.matrix.rustcomponents.sdk.TypingNotificationsListener
|
||||
import org.matrix.rustcomponents.sdk.UserPowerLevelUpdate
|
||||
import org.matrix.rustcomponents.sdk.WidgetCapabilities
|
||||
import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider
|
||||
import org.matrix.rustcomponents.sdk.messageEventContentFromHtml
|
||||
|
|
@ -151,6 +157,12 @@ class RustMatrixRoom(
|
|||
|
||||
override val syncUpdateFlow: StateFlow<Long> = _syncUpdateFlow.asStateFlow()
|
||||
|
||||
init {
|
||||
timeline.membershipChangeEventReceived
|
||||
.onEach { roomMemberListFetcher.fetchRoomMembers() }
|
||||
.launchIn(roomCoroutineScope)
|
||||
}
|
||||
|
||||
override suspend fun subscribeToSync() = roomSyncSubscriber.subscribe(roomId)
|
||||
|
||||
override suspend fun unsubscribeFromSync() = roomSyncSubscriber.unsubscribe(roomId)
|
||||
|
|
@ -228,6 +240,19 @@ class RustMatrixRoom(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun userRole(userId: UserId): Result<RoomMember.Role> = withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
RoomMemberMapper.mapRole(innerRoom.suggestedRoleForUser(userId.value))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateUsersRoles(changes: List<UserRoleChange>): Result<Unit> {
|
||||
return runCatching {
|
||||
val powerLevelChanges = changes.map { UserPowerLevelUpdate(it.userId.value, it.powerLevel) }
|
||||
innerRoom.updatePowerLevelsForUsers(powerLevelChanges)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun userAvatarUrl(userId: UserId): Result<String?> = withContext(roomDispatcher) {
|
||||
runCatching {
|
||||
innerRoom.memberAvatarUrl(userId.value)
|
||||
|
|
@ -319,6 +344,12 @@ class RustMatrixRoom(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun canUserKick(userId: UserId): Result<Boolean> {
|
||||
return runCatching {
|
||||
innerRoom.canUserKick(userId.value)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun canUserBan(userId: UserId): Result<Boolean> {
|
||||
return runCatching {
|
||||
innerRoom.canUserBan(userId.value)
|
||||
|
|
@ -448,6 +479,24 @@ class RustMatrixRoom(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun kickUser(userId: UserId, reason: String?): Result<Unit> = withContext(roomDispatcher) {
|
||||
runCatching {
|
||||
innerRoom.kickUser(userId.value, reason)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun banUser(userId: UserId, reason: String?): Result<Unit> = withContext(roomDispatcher) {
|
||||
runCatching {
|
||||
innerRoom.banUser(userId.value, reason)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun unbanUser(userId: UserId, reason: String?): Result<Unit> = withContext(roomDispatcher) {
|
||||
runCatching {
|
||||
innerRoom.unbanUser(userId.value, reason)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setIsFavorite(isFavorite: Boolean): Result<Unit> = withContext(roomDispatcher) {
|
||||
runCatching {
|
||||
innerRoom.setIsFavourite(isFavorite, null)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ package io.element.android.libraries.matrix.impl.room.member
|
|||
import io.element.android.libraries.core.coroutine.parallelMap
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import io.element.android.libraries.matrix.impl.util.destroyAll
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.ensureActive
|
||||
|
|
@ -41,7 +40,7 @@ import kotlin.coroutines.coroutineContext
|
|||
internal class RoomMemberListFetcher(
|
||||
private val room: RoomInterface,
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
private val pageSize: Int = 1000,
|
||||
private val pageSize: Int = 10_000,
|
||||
) {
|
||||
private val updatedRoomMemberMutex = Mutex()
|
||||
private val roomId = room.id()
|
||||
|
|
@ -66,7 +65,7 @@ internal class RoomMemberListFetcher(
|
|||
if (_membersFlow.value !is MatrixRoomMembersState.Ready) {
|
||||
fetchCachedRoomMembers()
|
||||
} else {
|
||||
Timber.i("No need to load cached members found for room $roomId")
|
||||
Timber.i("Cached members not found for $roomId")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -109,13 +108,8 @@ internal class RoomMemberListFetcher(
|
|||
// We should probably implement some sort of paging in the future.
|
||||
coroutineContext.ensureActive()
|
||||
val chunk = iterator.nextChunk(pageSize.toUInt())
|
||||
val members = try {
|
||||
// Load next chunk. If null (no more items), exit the loop
|
||||
chunk?.parallelMap(RoomMemberMapper::map) ?: break
|
||||
} finally {
|
||||
// Make sure we clear all member references
|
||||
chunk?.destroyAll()
|
||||
}
|
||||
// Load next chunk. If null (no more items), exit the loop
|
||||
val members = chunk?.parallelMap(RoomMemberMapper::map) ?: break
|
||||
addAll(members)
|
||||
Timber.i("Emitting first $size members for room $roomId")
|
||||
_membersFlow.value = MatrixRoomMembersState.Ready(toImmutableList())
|
||||
|
|
|
|||
|
|
@ -24,19 +24,17 @@ import org.matrix.rustcomponents.sdk.MembershipState as RustMembershipState
|
|||
import org.matrix.rustcomponents.sdk.RoomMember as RustRoomMember
|
||||
|
||||
object RoomMemberMapper {
|
||||
fun map(roomMember: RustRoomMember): RoomMember = roomMember.use {
|
||||
RoomMember(
|
||||
UserId(it.userId()),
|
||||
it.displayName(),
|
||||
it.avatarUrl(),
|
||||
mapMembership(it.membership()),
|
||||
it.isNameAmbiguous(),
|
||||
it.powerLevel(),
|
||||
it.normalizedPowerLevel(),
|
||||
it.isIgnored(),
|
||||
mapRole(it.suggestedRoleForPowerLevel())
|
||||
fun map(roomMember: RustRoomMember): RoomMember = RoomMember(
|
||||
UserId(roomMember.userId),
|
||||
roomMember.displayName,
|
||||
roomMember.avatarUrl,
|
||||
mapMembership(roomMember.membership),
|
||||
roomMember.isNameAmbiguous,
|
||||
roomMember.powerLevel,
|
||||
roomMember.normalizedPowerLevel,
|
||||
roomMember.isIgnored,
|
||||
mapRole(roomMember.suggestedRoleForPowerLevel),
|
||||
)
|
||||
}
|
||||
|
||||
fun mapRole(role: RoomMemberRole): RoomMember.Role =
|
||||
when (role) {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import kotlinx.coroutines.CoroutineStart
|
|||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
|
|
@ -55,6 +56,8 @@ class AsyncMatrixTimeline(
|
|||
}
|
||||
private val closeSignal = CompletableDeferred<Unit>()
|
||||
|
||||
override val membershipChangeEventReceived = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||
|
||||
init {
|
||||
coroutineScope.launch {
|
||||
val delegateTimeline = timeline.await()
|
||||
|
|
@ -64,6 +67,9 @@ class AsyncMatrixTimeline(
|
|||
delegateTimeline.paginationState
|
||||
.onEach { _paginationState.value = it }
|
||||
.launchIn(this)
|
||||
delegateTimeline.membershipChangeEventReceived
|
||||
.onEach { membershipChangeEventReceived.emit(it) }
|
||||
.launchIn(this)
|
||||
|
||||
launch {
|
||||
withContext(NonCancellable) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
package io.element.android.libraries.matrix.impl.timeline
|
||||
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
|
@ -31,6 +34,9 @@ internal class MatrixTimelineDiffProcessor(
|
|||
) {
|
||||
private val mutex = Mutex()
|
||||
|
||||
private val _membershipChangeEventReceived = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||
val membershipChangeEventReceived: Flow<Unit> = _membershipChangeEventReceived
|
||||
|
||||
suspend fun postItems(items: List<TimelineItem>) {
|
||||
updateTimelineItems {
|
||||
Timber.v("Update timeline items from postItems (with ${items.size} items) on ${Thread.currentThread()}")
|
||||
|
|
@ -63,6 +69,11 @@ internal class MatrixTimelineDiffProcessor(
|
|||
}
|
||||
TimelineChange.PUSH_BACK -> {
|
||||
val item = diff.pushBack()?.asMatrixTimelineItem() ?: return
|
||||
if (item is MatrixTimelineItem.Event && item.event.content is RoomMembershipContent) {
|
||||
// TODO - This is a temporary solution to notify the room screen about membership changes
|
||||
// Ideally, this should be implemented by the Rust SDK
|
||||
_membershipChangeEventReceived.tryEmit(Unit)
|
||||
}
|
||||
add(item)
|
||||
}
|
||||
TimelineChange.PUSH_FRONT -> {
|
||||
|
|
|
|||
|
|
@ -114,6 +114,8 @@ class RustMatrixTimeline(
|
|||
)
|
||||
}
|
||||
|
||||
override val membershipChangeEventReceived: Flow<Unit> = timelineDiffProcessor.membershipChangeEventReceived
|
||||
|
||||
init {
|
||||
Timber.d("Initialize timeline for room ${matrixRoom.roomId}")
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ class RustTracingService @Inject constructor(private val buildMeta: BuildMeta) :
|
|||
is WriteToFilesConfiguration.Enabled -> TracingFileConfiguration(
|
||||
path = writeToFilesConfiguration.directory,
|
||||
filePrefix = writeToFilesConfiguration.filenamePrefix,
|
||||
fileSuffix = null,
|
||||
maxFiles = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -42,9 +42,9 @@ class RoomMemberListFetcherTest {
|
|||
val room = FakeRustRoom(getMembersNoSync = {
|
||||
FakeRoomMembersIterator(
|
||||
listOf(
|
||||
FakeRustRoomMember(A_USER_ID),
|
||||
FakeRustRoomMember(A_USER_ID_2),
|
||||
FakeRustRoomMember(A_USER_ID_3),
|
||||
fakeRustRoomMember(A_USER_ID),
|
||||
fakeRustRoomMember(A_USER_ID_2),
|
||||
fakeRustRoomMember(A_USER_ID_3),
|
||||
)
|
||||
)
|
||||
})
|
||||
|
|
@ -94,9 +94,9 @@ class RoomMemberListFetcherTest {
|
|||
val room = FakeRustRoom(getMembersNoSync = {
|
||||
FakeRoomMembersIterator(
|
||||
listOf(
|
||||
FakeRustRoomMember(A_USER_ID),
|
||||
FakeRustRoomMember(A_USER_ID_2),
|
||||
FakeRustRoomMember(A_USER_ID_3),
|
||||
fakeRustRoomMember(A_USER_ID),
|
||||
fakeRustRoomMember(A_USER_ID_2),
|
||||
fakeRustRoomMember(A_USER_ID_3),
|
||||
)
|
||||
)
|
||||
})
|
||||
|
|
@ -118,9 +118,9 @@ class RoomMemberListFetcherTest {
|
|||
val room = FakeRustRoom(getMembers = {
|
||||
FakeRoomMembersIterator(
|
||||
listOf(
|
||||
FakeRustRoomMember(A_USER_ID),
|
||||
FakeRustRoomMember(A_USER_ID_2),
|
||||
FakeRustRoomMember(A_USER_ID_3),
|
||||
fakeRustRoomMember(A_USER_ID),
|
||||
fakeRustRoomMember(A_USER_ID_2),
|
||||
fakeRustRoomMember(A_USER_ID_3),
|
||||
)
|
||||
)
|
||||
})
|
||||
|
|
@ -153,14 +153,14 @@ class RoomMemberListFetcherTest {
|
|||
fun `fetchRoomMembers - with 'withCache' returns cached items first, then new ones`() = runTest {
|
||||
val room = FakeRustRoom(
|
||||
getMembersNoSync = {
|
||||
FakeRoomMembersIterator(listOf(FakeRustRoomMember(A_USER_ID_4)))
|
||||
FakeRoomMembersIterator(listOf(fakeRustRoomMember(A_USER_ID_4)))
|
||||
},
|
||||
getMembers = {
|
||||
FakeRoomMembersIterator(
|
||||
listOf(
|
||||
FakeRustRoomMember(A_USER_ID),
|
||||
FakeRustRoomMember(A_USER_ID_2),
|
||||
FakeRustRoomMember(A_USER_ID_3),
|
||||
fakeRustRoomMember(A_USER_ID),
|
||||
fakeRustRoomMember(A_USER_ID_2),
|
||||
fakeRustRoomMember(A_USER_ID_3),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -189,14 +189,14 @@ class RoomMemberListFetcherTest {
|
|||
fun `fetchRoomMembers - with 'withCache' skips cache if there is already a ready state`() = runTest {
|
||||
val room = FakeRustRoom(
|
||||
getMembersNoSync = {
|
||||
FakeRoomMembersIterator(listOf(FakeRustRoomMember(A_USER_ID_4)))
|
||||
FakeRoomMembersIterator(listOf(fakeRustRoomMember(A_USER_ID_4)))
|
||||
},
|
||||
getMembers = {
|
||||
FakeRoomMembersIterator(
|
||||
listOf(
|
||||
FakeRustRoomMember(A_USER_ID),
|
||||
FakeRustRoomMember(A_USER_ID_2),
|
||||
FakeRustRoomMember(A_USER_ID_3),
|
||||
fakeRustRoomMember(A_USER_ID),
|
||||
fakeRustRoomMember(A_USER_ID_2),
|
||||
fakeRustRoomMember(A_USER_ID_3),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -262,48 +262,23 @@ class FakeRoomMembersIterator(
|
|||
}
|
||||
}
|
||||
|
||||
class FakeRustRoomMember(
|
||||
private val userId: UserId,
|
||||
private val displayName: String? = null,
|
||||
private val avatarUrl: String? = null,
|
||||
private val membership: MembershipState = MembershipState.JOIN,
|
||||
private val isNameAmbiguous: Boolean = false,
|
||||
private val powerLevel: Long = 0L,
|
||||
private val role: RoomMemberRole = RoomMemberRole.USER,
|
||||
) : RoomMember(NoPointer) {
|
||||
override fun userId(): String {
|
||||
return userId.value
|
||||
}
|
||||
|
||||
override fun displayName(): String? {
|
||||
return displayName
|
||||
}
|
||||
|
||||
override fun avatarUrl(): String? {
|
||||
return avatarUrl
|
||||
}
|
||||
|
||||
override fun membership(): MembershipState {
|
||||
return membership
|
||||
}
|
||||
|
||||
override fun isNameAmbiguous(): Boolean {
|
||||
return isNameAmbiguous
|
||||
}
|
||||
|
||||
override fun powerLevel(): Long {
|
||||
return powerLevel
|
||||
}
|
||||
|
||||
override fun normalizedPowerLevel(): Long {
|
||||
return powerLevel
|
||||
}
|
||||
|
||||
override fun isIgnored(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun suggestedRoleForPowerLevel(): RoomMemberRole {
|
||||
return role
|
||||
}
|
||||
}
|
||||
private fun fakeRustRoomMember(
|
||||
userId: UserId,
|
||||
displayName: String? = null,
|
||||
avatarUrl: String? = null,
|
||||
membership: MembershipState = MembershipState.JOIN,
|
||||
isNameAmbiguous: Boolean = false,
|
||||
powerLevel: Long = 0L,
|
||||
isIgnored: Boolean = false,
|
||||
role: RoomMemberRole = RoomMemberRole.USER,
|
||||
) = RoomMember(
|
||||
userId = userId.value,
|
||||
displayName = displayName,
|
||||
avatarUrl = avatarUrl,
|
||||
membership = membership,
|
||||
isNameAmbiguous = isNameAmbiguous,
|
||||
powerLevel = powerLevel,
|
||||
normalizedPowerLevel = powerLevel,
|
||||
isIgnored = isIgnored,
|
||||
suggestedRoleForPowerLevel = role,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.SdkMetadata
|
||||
|
||||
class FakeSdkMetadata(override val sdkGitSha: String) : SdkMetadata
|
||||
|
|
@ -40,6 +40,7 @@ import io.element.android.libraries.matrix.api.room.RoomMember
|
|||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
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.room.powerlevels.UserRoleChange
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
|
|
@ -54,6 +55,8 @@ import io.element.android.libraries.matrix.test.notificationsettings.FakeNotific
|
|||
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.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -86,10 +89,12 @@ class FakeMatrixRoom(
|
|||
private var unignoreResult: Result<Unit> = Result.success(Unit)
|
||||
private var userDisplayNameResult = Result.success<String?>(null)
|
||||
private var userAvatarUrlResult = Result.success<String?>(null)
|
||||
private var userRoleResult = Result.success(RoomMember.Role.USER)
|
||||
private var updateMembersResult: Result<Unit> = Result.success(Unit)
|
||||
private var joinRoomResult = Result.success(Unit)
|
||||
private var inviteUserResult = Result.success(Unit)
|
||||
private var canInviteResult = Result.success(true)
|
||||
private var canKickResult = Result.success(false)
|
||||
private var canBanResult = Result.success(false)
|
||||
private var canRedactOwnResult = Result.success(canRedactOwn)
|
||||
private var canRedactOtherResult = Result.success(canRedactOther)
|
||||
|
|
@ -100,11 +105,15 @@ class FakeMatrixRoom(
|
|||
private var setTopicResult = Result.success(Unit)
|
||||
private var updateAvatarResult = Result.success(Unit)
|
||||
private var removeAvatarResult = Result.success(Unit)
|
||||
private var updateUserRoleResult = Result.success(Unit)
|
||||
private var toggleReactionResult = Result.success(Unit)
|
||||
private var retrySendMessageResult = Result.success(Unit)
|
||||
private var cancelSendResult = Result.success(Unit)
|
||||
private var forwardEventResult = Result.success(Unit)
|
||||
private var reportContentResult = Result.success(Unit)
|
||||
private var kickUserResult = Result.success(Unit)
|
||||
private var banUserResult = Result.success(Unit)
|
||||
private var unBanUserResult = Result.success(Unit)
|
||||
private var sendLocationResult = Result.success(Unit)
|
||||
private var createPollResult = Result.success(Unit)
|
||||
private var editPollResult = Result.success(Unit)
|
||||
|
|
@ -206,6 +215,14 @@ class FakeMatrixRoom(
|
|||
userAvatarUrlResult
|
||||
}
|
||||
|
||||
override suspend fun userRole(userId: UserId): Result<RoomMember.Role> {
|
||||
return userRoleResult
|
||||
}
|
||||
|
||||
override suspend fun updateUsersRoles(changes: List<UserRoleChange>): Result<Unit> {
|
||||
return updateUserRoleResult
|
||||
}
|
||||
|
||||
override suspend fun sendMessage(body: String, htmlBody: String?, mentions: List<Mention>) = simulateLongTask {
|
||||
sendMessageMentions = mentions
|
||||
Result.success(Unit)
|
||||
|
|
@ -285,6 +302,10 @@ class FakeMatrixRoom(
|
|||
return canBanResult
|
||||
}
|
||||
|
||||
override suspend fun canUserKick(userId: UserId): Result<Boolean> {
|
||||
return canKickResult
|
||||
}
|
||||
|
||||
override suspend fun canUserInvite(userId: UserId): Result<Boolean> {
|
||||
return canInviteResult
|
||||
}
|
||||
|
|
@ -384,6 +405,18 @@ class FakeMatrixRoom(
|
|||
return reportContentResult
|
||||
}
|
||||
|
||||
override suspend fun kickUser(userId: UserId, reason: String?): Result<Unit> {
|
||||
return kickUserResult
|
||||
}
|
||||
|
||||
override suspend fun banUser(userId: UserId, reason: String?): Result<Unit> {
|
||||
return banUserResult
|
||||
}
|
||||
|
||||
override suspend fun unbanUser(userId: UserId, reason: String?): Result<Unit> {
|
||||
return unBanUserResult
|
||||
}
|
||||
|
||||
val setIsFavoriteCalls = mutableListOf<Boolean>()
|
||||
|
||||
override suspend fun setIsFavorite(isFavorite: Boolean): Result<Unit> {
|
||||
|
|
@ -496,10 +529,22 @@ class FakeMatrixRoom(
|
|||
userAvatarUrlResult = avatarUrl
|
||||
}
|
||||
|
||||
fun givenUserRoleResult(role: Result<RoomMember.Role>) {
|
||||
userRoleResult = role
|
||||
}
|
||||
|
||||
fun givenUpdateUserRoleResult(result: Result<Unit>) {
|
||||
updateUserRoleResult = result
|
||||
}
|
||||
|
||||
fun givenJoinRoomResult(result: Result<Unit>) {
|
||||
joinRoomResult = result
|
||||
}
|
||||
|
||||
fun givenCanKickResult(result: Result<Boolean>) {
|
||||
canKickResult = result
|
||||
}
|
||||
|
||||
fun givenCanBanResult(result: Result<Boolean>) {
|
||||
canBanResult = result
|
||||
}
|
||||
|
|
@ -576,6 +621,18 @@ class FakeMatrixRoom(
|
|||
reportContentResult = result
|
||||
}
|
||||
|
||||
fun givenKickUserResult(result: Result<Unit>) {
|
||||
kickUserResult = result
|
||||
}
|
||||
|
||||
fun givenBanUserResult(result: Result<Unit>) {
|
||||
banUserResult = result
|
||||
}
|
||||
|
||||
fun givenUnbanUserResult(result: Result<Unit>) {
|
||||
unBanUserResult = result
|
||||
}
|
||||
|
||||
fun givenSendLocationResult(result: Result<Unit>) {
|
||||
sendLocationResult = result
|
||||
}
|
||||
|
|
@ -668,6 +725,7 @@ fun aRoomInfo(
|
|||
notificationCount: Long = 0,
|
||||
userDefinedNotificationMode: RoomNotificationMode? = null,
|
||||
hasRoomCall: Boolean = false,
|
||||
userPowerLevels: ImmutableMap<UserId, Long> = persistentMapOf(),
|
||||
activeRoomCallParticipants: List<String> = emptyList()
|
||||
) = MatrixRoomInfo(
|
||||
id = id,
|
||||
|
|
@ -691,5 +749,6 @@ fun aRoomInfo(
|
|||
notificationCount = notificationCount,
|
||||
userDefinedNotificationMode = userDefinedNotificationMode,
|
||||
hasRoomCall = hasRoomCall,
|
||||
userPowerLevels = userPowerLevels,
|
||||
activeRoomCallParticipants = activeRoomCallParticipants.toImmutableList(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import io.element.android.tests.testutils.simulateLongTask
|
|||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.getAndUpdate
|
||||
|
|
@ -59,6 +60,8 @@ class FakeMatrixTimeline(
|
|||
override suspend fun paginateBackwards(requestSize: Int) = paginateBackwards()
|
||||
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int) = paginateBackwards()
|
||||
|
||||
override val membershipChangeEventReceived = MutableSharedFlow<Unit>()
|
||||
|
||||
private suspend fun paginateBackwards(): Result<Unit> {
|
||||
updatePaginationState {
|
||||
copy(isBackPaginating = true)
|
||||
|
|
@ -73,6 +76,10 @@ class FakeMatrixTimeline(
|
|||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
fun givenMembershipChangeEventReceived() {
|
||||
membershipChangeEventReceived.tryEmit(Unit)
|
||||
}
|
||||
|
||||
override suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit> = simulateLongTask {
|
||||
Result.success(Unit)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||
@Composable
|
||||
fun SelectedUser(
|
||||
matrixUser: MatrixUser,
|
||||
canRemove: Boolean,
|
||||
onUserRemoved: (MatrixUser) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -70,24 +71,26 @@ fun SelectedUser(
|
|||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.size(20.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
.clickable(
|
||||
indication = rememberRipple(),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = { onUserRemoved(matrixUser) }
|
||||
),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Close(),
|
||||
contentDescription = stringResource(id = CommonStrings.action_remove),
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
modifier = Modifier.padding(2.dp)
|
||||
)
|
||||
if (canRemove) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.size(20.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
.clickable(
|
||||
indication = rememberRipple(),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = { onUserRemoved(matrixUser) }
|
||||
),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Close(),
|
||||
contentDescription = stringResource(id = CommonStrings.action_remove),
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
modifier = Modifier.padding(2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -97,6 +100,17 @@ fun SelectedUser(
|
|||
internal fun SelectedUserPreview() = ElementPreview {
|
||||
SelectedUser(
|
||||
aMatrixUser(),
|
||||
canRemove = true,
|
||||
onUserRemoved = {},
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun SelectedUserCannotRemovePreview() = ElementPreview {
|
||||
SelectedUser(
|
||||
aMatrixUser(),
|
||||
canRemove = false,
|
||||
onUserRemoved = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,11 +46,12 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
import kotlin.math.floor
|
||||
|
||||
@Composable
|
||||
fun SelectedUsersList(
|
||||
fun SelectedUsersRowList(
|
||||
selectedUsers: ImmutableList<MatrixUser>,
|
||||
onUserRemoved: (MatrixUser) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
autoScroll: Boolean = false,
|
||||
canDeselect: (MatrixUser) -> Boolean = { true },
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
|
@ -105,11 +106,12 @@ fun SelectedUsersList(
|
|||
.fillMaxWidth(),
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
itemsIndexed(selectedUsers.toList()) { index, matrixUser ->
|
||||
itemsIndexed(selectedUsers.toList()) { index, selectedUser ->
|
||||
Layout(
|
||||
content = {
|
||||
SelectedUser(
|
||||
matrixUser = matrixUser,
|
||||
matrixUser = selectedUser,
|
||||
canRemove = canDeselect(selectedUser),
|
||||
onUserRemoved = onUserRemoved,
|
||||
)
|
||||
},
|
||||
|
|
@ -133,7 +135,7 @@ fun SelectedUsersList(
|
|||
internal fun SelectedUsersListPreview() = ElementPreview {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
// Two users that will be visible with no scrolling
|
||||
SelectedUsersList(
|
||||
SelectedUsersRowList(
|
||||
selectedUsers = aMatrixUserList().take(2).toImmutableList(),
|
||||
onUserRemoved = {},
|
||||
modifier = Modifier
|
||||
|
|
@ -143,7 +145,7 @@ internal fun SelectedUsersListPreview() = ElementPreview {
|
|||
|
||||
// Multiple users that don't fit, so will be spaced out per the measure policy
|
||||
for (i in 0..5) {
|
||||
SelectedUsersList(
|
||||
SelectedUsersRowList(
|
||||
selectedUsers = aMatrixUserList().take(6).toImmutableList(),
|
||||
onUserRemoved = {},
|
||||
modifier = Modifier
|
||||
|
|
@ -18,9 +18,12 @@ 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.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage
|
||||
|
|
@ -45,3 +48,10 @@ fun MatrixRoom.canRedactOtherAsState(updateKey: Long): State<Boolean> {
|
|||
value = canRedactOther().getOrElse { false }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MatrixRoom.isOwnUserAdmin(): Boolean {
|
||||
val roomInfo by roomInfoFlow.collectAsState(initial = null)
|
||||
val powerLevel = roomInfo?.userPowerLevels?.get(sessionId) ?: 0L
|
||||
return RoomMember.Role.forPowerLevel(powerLevel) == RoomMember.Role.ADMIN
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import androidx.compose.foundation.layout.Spacer
|
|||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.GraphicEq
|
||||
|
|
@ -57,7 +58,6 @@ import androidx.compose.ui.viewinterop.AndroidView
|
|||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.ui.AspectRatioFrameLayout
|
||||
import androidx.media3.ui.PlayerView
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
|
|
@ -80,7 +80,6 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||
import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage
|
||||
import me.saket.telephoto.zoomable.rememberZoomableImageState
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
@Composable
|
||||
fun LocalMediaView(
|
||||
localMedia: LocalMedia?,
|
||||
|
|
@ -147,13 +146,37 @@ private fun MediaImageView(
|
|||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
@Composable
|
||||
private fun MediaVideoView(
|
||||
localMediaViewState: LocalMediaViewState,
|
||||
localMedia: LocalMedia?,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (LocalInspectionMode.current) {
|
||||
Text(
|
||||
modifier = modifier
|
||||
.background(ElementTheme.colors.bgSubtlePrimary)
|
||||
.wrapContentSize(),
|
||||
text = "A Video Player will render here",
|
||||
)
|
||||
} else {
|
||||
ExoPlayerMediaVideoView(
|
||||
localMediaViewState = localMediaViewState,
|
||||
localMedia = localMedia,
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
@Composable
|
||||
private fun ExoPlayerMediaVideoView(
|
||||
localMediaViewState: LocalMediaViewState,
|
||||
localMedia: LocalMedia?,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var playableState: PlayableState.Playable by remember {
|
||||
mutableStateOf(PlayableState.Playable(isPlaying = false, isShowingControls = false))
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ fun TextComposer(
|
|||
}
|
||||
}
|
||||
|
||||
val canSendMessage by remember { derivedStateOf { state.messageHtml.isNotEmpty() } }
|
||||
val canSendMessage by remember { derivedStateOf { state.messageMarkdown.isNotBlank() } }
|
||||
val sendButton = @Composable {
|
||||
SendButton(
|
||||
canSendMessage = canSendMessage,
|
||||
|
|
@ -598,7 +598,7 @@ internal fun TextComposerSimplePreview() = ElementPreview {
|
|||
items = persistentListOf(
|
||||
{
|
||||
ATextComposer(
|
||||
RichTextEditorState("", initialFocus = true),
|
||||
aRichTextEditorState(initialText = "", initialFocus = true),
|
||||
voiceMessageState = VoiceMessageState.Idle,
|
||||
composerMode = MessageComposerMode.Normal,
|
||||
enableTextFormatting = true,
|
||||
|
|
@ -608,7 +608,7 @@ internal fun TextComposerSimplePreview() = ElementPreview {
|
|||
},
|
||||
{
|
||||
ATextComposer(
|
||||
RichTextEditorState("A message", initialFocus = true),
|
||||
aRichTextEditorState(initialText = "A message", initialFocus = true),
|
||||
voiceMessageState = VoiceMessageState.Idle,
|
||||
composerMode = MessageComposerMode.Normal,
|
||||
enableTextFormatting = true,
|
||||
|
|
@ -618,8 +618,8 @@ internal fun TextComposerSimplePreview() = ElementPreview {
|
|||
},
|
||||
{
|
||||
ATextComposer(
|
||||
RichTextEditorState(
|
||||
"A message\nWith several lines\nTo preview larger textfields and long lines with overflow",
|
||||
aRichTextEditorState(
|
||||
initialText = "A message\nWith several lines\nTo preview larger textfields and long lines with overflow",
|
||||
initialFocus = true
|
||||
),
|
||||
voiceMessageState = VoiceMessageState.Idle,
|
||||
|
|
@ -631,7 +631,7 @@ internal fun TextComposerSimplePreview() = ElementPreview {
|
|||
},
|
||||
{
|
||||
ATextComposer(
|
||||
RichTextEditorState("A message without focus", initialFocus = false),
|
||||
aRichTextEditorState(initialText = "A message without focus", initialFocus = false),
|
||||
voiceMessageState = VoiceMessageState.Idle,
|
||||
composerMode = MessageComposerMode.Normal,
|
||||
enableTextFormatting = true,
|
||||
|
|
@ -648,7 +648,7 @@ internal fun TextComposerSimplePreview() = ElementPreview {
|
|||
internal fun TextComposerFormattingPreview() = ElementPreview {
|
||||
PreviewColumn(items = persistentListOf({
|
||||
ATextComposer(
|
||||
RichTextEditorState("", initialFocus = false),
|
||||
aRichTextEditorState(initialText = "", initialFocus = false),
|
||||
voiceMessageState = VoiceMessageState.Idle,
|
||||
showTextFormatting = true,
|
||||
composerMode = MessageComposerMode.Normal,
|
||||
|
|
@ -658,7 +658,7 @@ internal fun TextComposerFormattingPreview() = ElementPreview {
|
|||
)
|
||||
}, {
|
||||
ATextComposer(
|
||||
RichTextEditorState("A message", initialFocus = false),
|
||||
aRichTextEditorState(initialText = "A message", initialFocus = false),
|
||||
voiceMessageState = VoiceMessageState.Idle,
|
||||
showTextFormatting = true,
|
||||
composerMode = MessageComposerMode.Normal,
|
||||
|
|
@ -668,7 +668,10 @@ internal fun TextComposerFormattingPreview() = ElementPreview {
|
|||
)
|
||||
}, {
|
||||
ATextComposer(
|
||||
RichTextEditorState("A message\nWith several lines\nTo preview larger textfields and long lines with overflow", initialFocus = false),
|
||||
aRichTextEditorState(
|
||||
initialText = "A message\nWith several lines\nTo preview larger textfields and long lines with overflow",
|
||||
initialFocus = false
|
||||
),
|
||||
voiceMessageState = VoiceMessageState.Idle,
|
||||
showTextFormatting = true,
|
||||
composerMode = MessageComposerMode.Normal,
|
||||
|
|
@ -684,7 +687,7 @@ internal fun TextComposerFormattingPreview() = ElementPreview {
|
|||
internal fun TextComposerEditPreview() = ElementPreview {
|
||||
PreviewColumn(items = persistentListOf({
|
||||
ATextComposer(
|
||||
RichTextEditorState("A message", initialFocus = true),
|
||||
aRichTextEditorState(initialText = "A message", initialFocus = true),
|
||||
voiceMessageState = VoiceMessageState.Idle,
|
||||
composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text", TransactionId("1234")),
|
||||
enableTextFormatting = true,
|
||||
|
|
@ -701,7 +704,7 @@ internal fun TextComposerReplyPreview() = ElementPreview {
|
|||
items = persistentListOf(
|
||||
{
|
||||
ATextComposer(
|
||||
RichTextEditorState(""),
|
||||
aRichTextEditorState(""),
|
||||
voiceMessageState = VoiceMessageState.Idle,
|
||||
composerMode = MessageComposerMode.Reply(
|
||||
isThreaded = false,
|
||||
|
|
@ -737,7 +740,7 @@ internal fun TextComposerReplyPreview() = ElementPreview {
|
|||
},
|
||||
{
|
||||
ATextComposer(
|
||||
RichTextEditorState("A message"),
|
||||
aRichTextEditorState(initialText = "A message"),
|
||||
voiceMessageState = VoiceMessageState.Idle,
|
||||
composerMode = MessageComposerMode.Reply(
|
||||
isThreaded = true,
|
||||
|
|
@ -758,7 +761,7 @@ internal fun TextComposerReplyPreview() = ElementPreview {
|
|||
},
|
||||
{
|
||||
ATextComposer(
|
||||
RichTextEditorState("A message"),
|
||||
aRichTextEditorState(initialText = "A message"),
|
||||
voiceMessageState = VoiceMessageState.Idle,
|
||||
composerMode = MessageComposerMode.Reply(
|
||||
isThreaded = false,
|
||||
|
|
@ -779,7 +782,7 @@ internal fun TextComposerReplyPreview() = ElementPreview {
|
|||
},
|
||||
{
|
||||
ATextComposer(
|
||||
RichTextEditorState("A message"),
|
||||
aRichTextEditorState(initialText = "A message"),
|
||||
voiceMessageState = VoiceMessageState.Idle,
|
||||
composerMode = MessageComposerMode.Reply(
|
||||
isThreaded = false,
|
||||
|
|
@ -800,7 +803,7 @@ internal fun TextComposerReplyPreview() = ElementPreview {
|
|||
},
|
||||
{
|
||||
ATextComposer(
|
||||
RichTextEditorState("A message", initialFocus = true),
|
||||
aRichTextEditorState(initialText = "A message", initialFocus = true),
|
||||
voiceMessageState = VoiceMessageState.Idle,
|
||||
composerMode = MessageComposerMode.Reply(
|
||||
isThreaded = false,
|
||||
|
|
@ -923,3 +926,14 @@ private fun ATextComposer(
|
|||
onRichContentSelected = null,
|
||||
)
|
||||
}
|
||||
|
||||
fun aRichTextEditorState(
|
||||
initialText: String = "",
|
||||
initialHtml: String = initialText,
|
||||
initialMarkdown: String = initialText,
|
||||
initialFocus: Boolean = false,
|
||||
) = RichTextEditorState(
|
||||
initialHtml = initialHtml,
|
||||
initialMarkdown = initialMarkdown,
|
||||
initialFocus = initialFocus,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,4 +21,5 @@
|
|||
<string name="rich_text_editor_remove_link">"Ta bort länk"</string>
|
||||
<string name="rich_text_editor_unindent">"Ta bort indrag"</string>
|
||||
<string name="rich_text_editor_url_placeholder">"Länk"</string>
|
||||
<string name="screen_room_voice_message_tooltip">"Håll för att spela in"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@
|
|||
<string name="common_analytics">"Статистика"</string>
|
||||
<string name="common_appearance">"Облик"</string>
|
||||
<string name="common_audio">"Аудио"</string>
|
||||
<string name="common_blocked_users">"Блокирани потребители"</string>
|
||||
<string name="common_chat_backup">"Резервно копие на чатовете"</string>
|
||||
<string name="common_creating_room">"Създаване на стая…"</string>
|
||||
<string name="common_dark">"Тъмен"</string>
|
||||
|
|
@ -114,6 +115,7 @@
|
|||
<string name="common_encryption_enabled">"Шифроването е включено"</string>
|
||||
<string name="common_enter_your_pin">"Въведете своя PIN"</string>
|
||||
<string name="common_error">"Грешка"</string>
|
||||
<string name="common_favourite">"Фаворизиране"</string>
|
||||
<string name="common_file">"Файл"</string>
|
||||
<string name="common_forward_message">"Препращане на съобщението"</string>
|
||||
<string name="common_gif">"GIF"</string>
|
||||
|
|
@ -134,6 +136,7 @@
|
|||
<string name="common_mute">"Заглушаване"</string>
|
||||
<string name="common_no_results">"Няма резултати"</string>
|
||||
<string name="common_offline">"Офлайн"</string>
|
||||
<string name="common_or">"или"</string>
|
||||
<string name="common_password">"Парола"</string>
|
||||
<string name="common_people">"Хора"</string>
|
||||
<string name="common_permalink">"Постоянна връзка"</string>
|
||||
|
|
@ -185,6 +188,7 @@
|
|||
<string name="common_username">"Потребителско име"</string>
|
||||
<string name="common_verification_cancelled">"Потвърждаването е отменено"</string>
|
||||
<string name="common_verification_complete">"Потвърждаването е завършено"</string>
|
||||
<string name="common_verify_device">"Потвърждаване на устройството"</string>
|
||||
<string name="common_video">"Видео"</string>
|
||||
<string name="common_voice_message">"Гласово съобщение"</string>
|
||||
<string name="common_waiting_for_decryption_key">"В очакване на това съобщение"</string>
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@
|
|||
<string name="action_enter_pin">"Zadejte PIN"</string>
|
||||
<string name="action_forgot_password">"Zapomněli jste heslo?"</string>
|
||||
<string name="action_forward">"Přeposlat"</string>
|
||||
<string name="action_go_back">"Přejít zpět"</string>
|
||||
<string name="action_invite">"Pozvat"</string>
|
||||
<string name="action_invite_friends">"Pozvat přátele"</string>
|
||||
<string name="action_invite_friends_to_app">"Pozvat přátele do %1$s"</string>
|
||||
|
|
@ -86,6 +87,7 @@
|
|||
<string name="action_reply_in_thread">"Odpovědět ve vlákně"</string>
|
||||
<string name="action_report_bug">"Nahlásit chybu"</string>
|
||||
<string name="action_report_content">"Nahlásit obsah"</string>
|
||||
<string name="action_reset">"Obnovit"</string>
|
||||
<string name="action_retry">"Zkusit znovu"</string>
|
||||
<string name="action_retry_decryption">"Opakovat dešifrování"</string>
|
||||
<string name="action_save">"Uložit"</string>
|
||||
|
|
@ -115,6 +117,7 @@
|
|||
<string name="common_audio">"Zvuk"</string>
|
||||
<string name="common_blocked_users">"Blokovaní uživatelé"</string>
|
||||
<string name="common_bubbles">"Bubliny"</string>
|
||||
<string name="common_call_invite">"Probíhá hovor (nepodporováno)"</string>
|
||||
<string name="common_chat_backup">"Záloha chatu"</string>
|
||||
<string name="common_copyright">"Autorská práva"</string>
|
||||
<string name="common_creating_room">"Vytváření místnosti…"</string>
|
||||
|
|
@ -185,6 +188,8 @@
|
|||
<string name="common_room">"Místnost"</string>
|
||||
<string name="common_room_name">"Název místnosti"</string>
|
||||
<string name="common_room_name_placeholder">"např. název vašeho projektu"</string>
|
||||
<string name="common_saved_changes">"Uložené změny"</string>
|
||||
<string name="common_saving">"Ukládání"</string>
|
||||
<string name="common_screen_lock">"Zámek obrazovky"</string>
|
||||
<string name="common_search_for_someone">"Hledat někoho"</string>
|
||||
<string name="common_search_results">"Výsledky hledání"</string>
|
||||
|
|
@ -227,6 +232,8 @@
|
|||
<string name="dialog_title_error">"Chyba"</string>
|
||||
<string name="dialog_title_success">"Úspěch"</string>
|
||||
<string name="dialog_title_warning">"Upozornění"</string>
|
||||
<string name="dialog_unsaved_changes_description_android">"Vaše změny nebyly uloženy. Opravdu se chcete vrátit?"</string>
|
||||
<string name="dialog_unsaved_changes_title">"Uložit změny?"</string>
|
||||
<string name="error_failed_creating_the_permalink">"Vytvoření trvalého odkazu se nezdařilo"</string>
|
||||
<string name="error_failed_loading_map">"%1$s nemohl načíst mapu. Zkuste to prosím později."</string>
|
||||
<string name="error_failed_loading_messages">"Načítání zpráv se nezdařilo"</string>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="a11y_delete">"Удалить"</string>
|
||||
<plurals name="a11y_digits_entered">
|
||||
<item quantity="one">"Введена цифра %1$d"</item>
|
||||
<item quantity="few">"Ведено %1$d цифр"</item>
|
||||
<item quantity="one">"Введена %1$d цифра"</item>
|
||||
<item quantity="few">"Ведено %1$d цифры"</item>
|
||||
<item quantity="many">"Введено много цифр"</item>
|
||||
</plurals>
|
||||
<string name="a11y_hide_password">"Скрыть пароль"</string>
|
||||
|
|
@ -59,6 +59,7 @@
|
|||
<string name="action_enter_pin">"Введите PIN-код"</string>
|
||||
<string name="action_forgot_password">"Забыли пароль?"</string>
|
||||
<string name="action_forward">"Переслать"</string>
|
||||
<string name="action_go_back">"Вернуться"</string>
|
||||
<string name="action_invite">"Пригласить"</string>
|
||||
<string name="action_invite_friends">"Пригласить друзей"</string>
|
||||
<string name="action_invite_friends_to_app">"Пригласить друзей в %1$s"</string>
|
||||
|
|
@ -86,6 +87,7 @@
|
|||
<string name="action_reply_in_thread">"Ответить в теме"</string>
|
||||
<string name="action_report_bug">"Сообщить об ошибке"</string>
|
||||
<string name="action_report_content">"Пожаловаться на содержание"</string>
|
||||
<string name="action_reset">"Сбросить"</string>
|
||||
<string name="action_retry">"Повторить"</string>
|
||||
<string name="action_retry_decryption">"Повторите расшифровку"</string>
|
||||
<string name="action_save">"Сохранить"</string>
|
||||
|
|
@ -115,6 +117,7 @@
|
|||
<string name="common_audio">"Аудио"</string>
|
||||
<string name="common_blocked_users">"Заблокированные пользователи"</string>
|
||||
<string name="common_bubbles">"Пузыри"</string>
|
||||
<string name="common_call_invite">"Выполняется звонок (не поддерживается)"</string>
|
||||
<string name="common_chat_backup">"Резервная копия чатов"</string>
|
||||
<string name="common_copyright">"Авторское право"</string>
|
||||
<string name="common_creating_room">"Создание комнаты…"</string>
|
||||
|
|
@ -122,7 +125,7 @@
|
|||
<string name="common_dark">"Темная"</string>
|
||||
<string name="common_decryption_error">"Ошибка расшифровки"</string>
|
||||
<string name="common_developer_options">"Для разработчика"</string>
|
||||
<string name="common_direct_chat">"Прямой чат"</string>
|
||||
<string name="common_direct_chat">"Личный чат"</string>
|
||||
<string name="common_edited_suffix">"(изменено)"</string>
|
||||
<string name="common_editing">"Редактирование"</string>
|
||||
<string name="common_emote">"%1$s%2$s"</string>
|
||||
|
|
@ -185,6 +188,8 @@
|
|||
<string name="common_room">"Комната"</string>
|
||||
<string name="common_room_name">"Название комнаты"</string>
|
||||
<string name="common_room_name_placeholder">"например, название вашего проекта"</string>
|
||||
<string name="common_saved_changes">"Сохраненные изменения"</string>
|
||||
<string name="common_saving">"Сохранение"</string>
|
||||
<string name="common_screen_lock">"Блокировка экрана"</string>
|
||||
<string name="common_search_for_someone">"Поиск человека"</string>
|
||||
<string name="common_search_results">"Результаты поиска"</string>
|
||||
|
|
@ -227,6 +232,8 @@
|
|||
<string name="dialog_title_error">"Ошибка"</string>
|
||||
<string name="dialog_title_success">"Успешно"</string>
|
||||
<string name="dialog_title_warning">"Предупреждение"</string>
|
||||
<string name="dialog_unsaved_changes_description_android">"Изменения не сохранены. Вы действительно хотите вернуться?"</string>
|
||||
<string name="dialog_unsaved_changes_title">"Сохранить изменения?"</string>
|
||||
<string name="error_failed_creating_the_permalink">"Не удалось создать постоянную ссылку"</string>
|
||||
<string name="error_failed_loading_map">"Не удалось загрузить карту %1$s. Пожалуйста, повторите попытку позже."</string>
|
||||
<string name="error_failed_loading_messages">"Не удалось загрузить сообщения"</string>
|
||||
|
|
@ -252,5 +259,5 @@
|
|||
<string name="screen_share_this_location_action">"Поделиться этим местоположением"</string>
|
||||
<string name="screen_view_location_title">"Местоположение"</string>
|
||||
<string name="settings_version_number">"Версия: %1$s (%2$s)"</string>
|
||||
<string name="test_language_identifier">"en"</string>
|
||||
<string name="test_language_identifier">"ru"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@
|
|||
<string name="action_enter_pin">"Zadajte PIN"</string>
|
||||
<string name="action_forgot_password">"Zabudnuté heslo?"</string>
|
||||
<string name="action_forward">"Preposlať"</string>
|
||||
<string name="action_go_back">"Ísť späť"</string>
|
||||
<string name="action_invite">"Pozvať"</string>
|
||||
<string name="action_invite_friends">"Pozvať ľudí"</string>
|
||||
<string name="action_invite_friends_to_app">"Pozvať ľudí do %1$s"</string>
|
||||
|
|
@ -86,6 +87,7 @@
|
|||
<string name="action_reply_in_thread">"Odpovedať vo vlákne"</string>
|
||||
<string name="action_report_bug">"Nahlásiť chybu"</string>
|
||||
<string name="action_report_content">"Nahlásiť obsah"</string>
|
||||
<string name="action_reset">"Obnoviť"</string>
|
||||
<string name="action_retry">"Skúsiť znova"</string>
|
||||
<string name="action_retry_decryption">"Opakovať dešifrovanie"</string>
|
||||
<string name="action_save">"Uložiť"</string>
|
||||
|
|
@ -115,6 +117,7 @@
|
|||
<string name="common_audio">"Zvuk"</string>
|
||||
<string name="common_blocked_users">"Blokovaní používatelia"</string>
|
||||
<string name="common_bubbles">"Bubliny"</string>
|
||||
<string name="common_call_invite">"Prebieha hovor (nepodporované)"</string>
|
||||
<string name="common_chat_backup">"Záloha konverzácie"</string>
|
||||
<string name="common_copyright">"Autorské práva"</string>
|
||||
<string name="common_creating_room">"Vytváranie miestnosti…"</string>
|
||||
|
|
@ -184,6 +187,8 @@
|
|||
<string name="common_room">"Miestnosť"</string>
|
||||
<string name="common_room_name">"Názov miestnosti"</string>
|
||||
<string name="common_room_name_placeholder">"napr. názov vášho projektu"</string>
|
||||
<string name="common_saved_changes">"Uložené zmeny"</string>
|
||||
<string name="common_saving">"Ukladá sa"</string>
|
||||
<string name="common_screen_lock">"Zámok obrazovky"</string>
|
||||
<string name="common_search_for_someone">"Vyhľadať niekoho"</string>
|
||||
<string name="common_search_results">"Výsledky hľadania"</string>
|
||||
|
|
@ -226,6 +231,8 @@
|
|||
<string name="dialog_title_error">"Chyba"</string>
|
||||
<string name="dialog_title_success">"Úspech"</string>
|
||||
<string name="dialog_title_warning">"Upozornenie"</string>
|
||||
<string name="dialog_unsaved_changes_description_android">"Vaše zmeny neboli uložené. Naozaj sa chcete vrátiť?"</string>
|
||||
<string name="dialog_unsaved_changes_title">"Uložiť zmeny?"</string>
|
||||
<string name="error_failed_creating_the_permalink">"Nepodarilo sa vytvoriť trvalý odkaz"</string>
|
||||
<string name="error_failed_loading_map">"%1$s nedokázal načítať mapu. Skúste to prosím neskôr."</string>
|
||||
<string name="error_failed_loading_messages">"Načítanie správ zlyhalo"</string>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="a11y_delete">"Radera"</string>
|
||||
<string name="a11y_hide_password">"Dölj lösenord"</string>
|
||||
<string name="a11y_notifications_mentions_only">"Endast omnämningar"</string>
|
||||
<string name="a11y_notifications_muted">"Tystad"</string>
|
||||
<string name="a11y_pause">"Pausa"</string>
|
||||
<string name="a11y_play">"Spela upp"</string>
|
||||
<string name="a11y_poll">"Omröstning"</string>
|
||||
<string name="a11y_poll_end">"Avslutade omröstning"</string>
|
||||
<string name="a11y_send_files">"Skicka filer"</string>
|
||||
<string name="a11y_show_password">"Visa lösenord"</string>
|
||||
<string name="a11y_user_menu">"Användarmeny"</string>
|
||||
<string name="a11y_voice_message_record">"Spela in röstmeddelande."</string>
|
||||
<string name="action_accept">"Godkänn"</string>
|
||||
<string name="action_add_to_timeline">"Lägg till i tidslinjen"</string>
|
||||
<string name="action_back">"Tillbaka"</string>
|
||||
|
|
@ -65,6 +69,9 @@
|
|||
<string name="action_send_message">"Skicka meddelande"</string>
|
||||
<string name="action_share">"Dela"</string>
|
||||
<string name="action_share_link">"Dela länk"</string>
|
||||
<string name="action_sign_in_again">"Logga in igen"</string>
|
||||
<string name="action_signout">"Logga ut"</string>
|
||||
<string name="action_signout_anyway">"Logga ut ändå"</string>
|
||||
<string name="action_skip">"Hoppa över"</string>
|
||||
<string name="action_start">"Starta"</string>
|
||||
<string name="action_start_chat">"Starta chat"</string>
|
||||
|
|
@ -89,6 +96,7 @@
|
|||
<string name="common_emote">"* %1$s %2$s"</string>
|
||||
<string name="common_encryption_enabled">"Kryptering aktiverad"</string>
|
||||
<string name="common_error">"Fel"</string>
|
||||
<string name="common_everyone">"Alla"</string>
|
||||
<string name="common_file">"Fil"</string>
|
||||
<string name="common_file_saved_on_disk_android">"Fil sparad i Download"</string>
|
||||
<string name="common_forward_message">"Vidarebefordra meddelande"</string>
|
||||
|
|
@ -126,6 +134,7 @@
|
|||
<string name="common_privacy_policy">"Integritetspolicy"</string>
|
||||
<string name="common_reaction">"Reaktion"</string>
|
||||
<string name="common_reactions">"Reaktioner"</string>
|
||||
<string name="common_recovery_key">"Återställningsnyckel"</string>
|
||||
<string name="common_refreshing">"Uppdaterar …"</string>
|
||||
<string name="common_replying_to">"Svarar till %1$s"</string>
|
||||
<string name="common_report_a_bug">"Rapportera en bugg"</string>
|
||||
|
|
@ -160,6 +169,7 @@
|
|||
<string name="common_verification_cancelled">"Verifiering avbruten"</string>
|
||||
<string name="common_verification_complete">"Verifieringen slutförd"</string>
|
||||
<string name="common_video">"Video"</string>
|
||||
<string name="common_voice_message">"Röstmeddelande"</string>
|
||||
<string name="common_waiting">"Väntar …"</string>
|
||||
<string name="dialog_title_confirmation">"Bekräftelse"</string>
|
||||
<string name="dialog_title_error">"Fel"</string>
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@
|
|||
<string name="common_acceptable_use_policy">"Політика прийнятного використання"</string>
|
||||
<string name="common_advanced_settings">"Додаткові налаштування"</string>
|
||||
<string name="common_analytics">"Аналітика"</string>
|
||||
<string name="common_appearance">"Вигляд"</string>
|
||||
<string name="common_appearance">"Тема"</string>
|
||||
<string name="common_audio">"Аудіо"</string>
|
||||
<string name="common_blocked_users">"Заблоковані користувачі"</string>
|
||||
<string name="common_bubbles">"Бульбашки"</string>
|
||||
|
|
@ -122,7 +122,7 @@
|
|||
<string name="common_dark">"Темна"</string>
|
||||
<string name="common_decryption_error">"Помилка розшифровки"</string>
|
||||
<string name="common_developer_options">"Налаштування розробника"</string>
|
||||
<string name="common_direct_chat">"Прямий чат"</string>
|
||||
<string name="common_direct_chat">"Особистий чат"</string>
|
||||
<string name="common_edited_suffix">"(відредаговано)"</string>
|
||||
<string name="common_editing">"Редагування"</string>
|
||||
<string name="common_emote">"* %1$s %2$s"</string>
|
||||
|
|
@ -131,7 +131,7 @@
|
|||
<string name="common_error">"Помилка"</string>
|
||||
<string name="common_everyone">"Усі"</string>
|
||||
<string name="common_failed">"Невдало"</string>
|
||||
<string name="common_favourite">"Вибрані"</string>
|
||||
<string name="common_favourite">"Улюблений"</string>
|
||||
<string name="common_favourited">"Вибране"</string>
|
||||
<string name="common_file">"Файл"</string>
|
||||
<string name="common_file_saved_on_disk_android">"Файл збережений у розділі \"Завантаження\""</string>
|
||||
|
|
@ -153,7 +153,7 @@
|
|||
<string name="common_message">"Повідомлення"</string>
|
||||
<string name="common_message_actions">"Дії з повідомленнями"</string>
|
||||
<string name="common_message_layout">"Макет повідомлень"</string>
|
||||
<string name="common_message_removed">"Повідомлення видалено"</string>
|
||||
<string name="common_message_removed">"Повідомлення вилучено"</string>
|
||||
<string name="common_modern">"Модерн"</string>
|
||||
<string name="common_mute">"Вимкнути звук"</string>
|
||||
<string name="common_no_results">"Немає результатів"</string>
|
||||
|
|
@ -185,6 +185,7 @@
|
|||
<string name="common_room">"Кімната"</string>
|
||||
<string name="common_room_name">"Назва кімнати"</string>
|
||||
<string name="common_room_name_placeholder">"напр., назва вашого проєкту"</string>
|
||||
<string name="common_saving">"Збереження"</string>
|
||||
<string name="common_screen_lock">"Блокування екрану"</string>
|
||||
<string name="common_search_for_someone">"Пошук когось"</string>
|
||||
<string name="common_search_results">"Результати пошуку"</string>
|
||||
|
|
@ -203,7 +204,7 @@
|
|||
<string name="common_success">"Успіх"</string>
|
||||
<string name="common_suggestions">"Пропозиції"</string>
|
||||
<string name="common_syncing">"Синхронізація"</string>
|
||||
<string name="common_system">"Системні"</string>
|
||||
<string name="common_system">"Системна"</string>
|
||||
<string name="common_text">"Текст"</string>
|
||||
<string name="common_third_party_notices">"Повідомлення третіх сторін"</string>
|
||||
<string name="common_thread">"Гілка"</string>
|
||||
|
|
@ -234,7 +235,7 @@
|
|||
<string name="error_failed_uploading_voice_message">"Не вдалося завантажити голосове повідомлення."</string>
|
||||
<string name="error_missing_location_auth_android">"%1$s не має дозволу на доступ до вашого місцезнаходження. Увімкнути доступ можна в Налаштуваннях."</string>
|
||||
<string name="error_missing_location_rationale_android">"%1$s не має дозволу на доступ до вашого місцезнаходження. Увімкніть доступ нижче."</string>
|
||||
<string name="error_missing_microphone_voice_rationale_android">"%1$s не має дозволу на доступ до мікрофона. Увімкнути доступ для запису голосового повідомлення."</string>
|
||||
<string name="error_missing_microphone_voice_rationale_android">"%1$s не має доступу до вашого мікрофона. Надайте доступ, щоб записати голосове повідомлення."</string>
|
||||
<string name="error_some_messages_have_not_been_sent">"Деякі повідомлення не були надіслані"</string>
|
||||
<string name="error_unknown">"Вибачте, сталася помилка"</string>
|
||||
<string name="invite_friends_rich_title">"🔐️ Приєднуйтеся до мене в %1$s"</string>
|
||||
|
|
|
|||
|
|
@ -121,6 +121,8 @@
|
|||
<string name="common_enter_your_pin">"輸入您的 PIN 碼"</string>
|
||||
<string name="common_error">"錯誤"</string>
|
||||
<string name="common_everyone">"所有人"</string>
|
||||
<string name="common_favourite">"我的最愛"</string>
|
||||
<string name="common_favourited">"我的最愛"</string>
|
||||
<string name="common_file">"檔案"</string>
|
||||
<string name="common_file_saved_on_disk_android">"檔案已儲存至 Downloads"</string>
|
||||
<string name="common_forward_message">"訊息轉寄"</string>
|
||||
|
|
@ -143,6 +145,7 @@
|
|||
<string name="common_mute">"關閉通知"</string>
|
||||
<string name="common_no_results">"查無結果"</string>
|
||||
<string name="common_offline">"離線"</string>
|
||||
<string name="common_or">"或"</string>
|
||||
<string name="common_password">"密碼"</string>
|
||||
<string name="common_people">"夥伴"</string>
|
||||
<string name="common_permalink">"永久連結"</string>
|
||||
|
|
@ -167,6 +170,7 @@
|
|||
<string name="common_room_name">"聊天室名稱"</string>
|
||||
<string name="common_room_name_placeholder">"範例:您的計畫名稱"</string>
|
||||
<string name="common_screen_lock">"螢幕鎖定"</string>
|
||||
<string name="common_search_for_someone">"搜尋使用者"</string>
|
||||
<string name="common_search_results">"搜尋結果"</string>
|
||||
<string name="common_security">"安全性"</string>
|
||||
<string name="common_seen_by">"已讀"</string>
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@
|
|||
<string name="action_decline">"Decline"</string>
|
||||
<string name="action_delete_poll">"Delete Poll"</string>
|
||||
<string name="action_disable">"Disable"</string>
|
||||
<string name="action_discard">"Discard"</string>
|
||||
<string name="action_done">"Done"</string>
|
||||
<string name="action_edit">"Edit"</string>
|
||||
<string name="action_edit_poll">"Edit poll"</string>
|
||||
|
|
@ -85,6 +86,7 @@
|
|||
<string name="action_reply_in_thread">"Reply in thread"</string>
|
||||
<string name="action_report_bug">"Report bug"</string>
|
||||
<string name="action_report_content">"Report content"</string>
|
||||
<string name="action_reset">"Reset"</string>
|
||||
<string name="action_retry">"Retry"</string>
|
||||
<string name="action_retry_decryption">"Retry decryption"</string>
|
||||
<string name="action_save">"Save"</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue