Display banned users in room member list (#2415)

* Display banned users in room member list

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa 2024-02-20 10:07:06 +01:00 committed by GitHub
parent cc721b6c87
commit c5dcd419ce
34 changed files with 197 additions and 31 deletions

View file

@ -1,3 +1,4 @@
Add moderation to rooms: Add moderation to rooms:
- Sort member in room member list by powerlevel, display their roles. - Sort member in room member list by powerlevel, display their roles.
- Display banner users in room member list for users with enough power level to ban/unban.

View file

@ -27,14 +27,18 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.room.MatrixRoom 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.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.powerlevels.canBan
import io.element.android.libraries.matrix.api.room.powerlevels.canInvite import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.room.roomMembers
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@ -57,6 +61,20 @@ class RoomMemberListPresenter @Inject constructor(
value = room.canInvite().getOrElse { false } value = room.canInvite().getOrElse { false }
} }
val canDisplayBannedUsers by produceState(initialValue = false) {
val roomIsNotDmAndUserCanBan = !room.isDm && room.canBan().getOrElse { false }
if (roomIsNotDmAndUserCanBan) {
room.membersStateFlow
.onEach { members ->
val hasBannedUsers = members.roomMembers()?.any { it.membership == RoomMembershipState.BAN }.orFalse()
value = hasBannedUsers
}
.collect()
} else {
value = false
}
}
LaunchedEffect(membersState) { LaunchedEffect(membersState) {
if (membersState is MatrixRoomMembersState.Unknown) { if (membersState is MatrixRoomMembersState.Unknown) {
return@LaunchedEffect return@LaunchedEffect
@ -69,6 +87,7 @@ class RoomMemberListPresenter @Inject constructor(
joined = members.getOrDefault(RoomMembershipState.JOIN, emptyList()) joined = members.getOrDefault(RoomMembershipState.JOIN, emptyList())
.sortedWith(PowerLevelRoomMemberComparator()) .sortedWith(PowerLevelRoomMemberComparator())
.toImmutableList(), .toImmutableList(),
banned = members.getOrDefault(RoomMembershipState.BAN, emptyList()).sortedBy { it.userId.value }.toImmutableList(),
) )
) )
} }
@ -89,6 +108,7 @@ class RoomMemberListPresenter @Inject constructor(
joined = results.getOrDefault(RoomMembershipState.JOIN, emptyList()) joined = results.getOrDefault(RoomMembershipState.JOIN, emptyList())
.sortedWith(PowerLevelRoomMemberComparator()) .sortedWith(PowerLevelRoomMemberComparator())
.toImmutableList(), .toImmutableList(),
banned = results.getOrDefault(RoomMembershipState.BAN, emptyList()).sortedBy { it.userId.value }.toImmutableList(),
) )
) )
} }
@ -102,6 +122,7 @@ class RoomMemberListPresenter @Inject constructor(
searchResults = searchResults, searchResults = searchResults,
isSearchActive = isSearchActive, isSearchActive = isSearchActive,
canInvite = canInvite, canInvite = canInvite,
canDisplayBannedUsers = canDisplayBannedUsers,
eventSink = { event -> eventSink = { event ->
when (event) { when (event) {
is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active

View file

@ -27,10 +27,12 @@ data class RoomMemberListState(
val searchResults: SearchBarResultState<RoomMembers>, val searchResults: SearchBarResultState<RoomMembers>,
val isSearchActive: Boolean, val isSearchActive: Boolean,
val canInvite: Boolean, val canInvite: Boolean,
val canDisplayBannedUsers: Boolean,
val eventSink: (RoomMemberListEvents) -> Unit, val eventSink: (RoomMemberListEvents) -> Unit,
) )
data class RoomMembers( data class RoomMembers(
val invited: ImmutableList<RoomMember>, val invited: ImmutableList<RoomMember>,
val joined: ImmutableList<RoomMember> val joined: ImmutableList<RoomMember>,
val banned: ImmutableList<RoomMember>,
) )

View file

@ -32,6 +32,7 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMember
RoomMembers( RoomMembers(
invited = persistentListOf(aVictor(), aWalter()), invited = persistentListOf(aVictor(), aWalter()),
joined = persistentListOf(anAlice(), aBob(), aWalter()), joined = persistentListOf(anAlice(), aBob(), aWalter()),
banned = persistentListOf(),
) )
) )
), ),
@ -47,6 +48,7 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMember
RoomMembers( RoomMembers(
invited = persistentListOf(aVictor()), invited = persistentListOf(aVictor()),
joined = persistentListOf(anAlice()), joined = persistentListOf(anAlice()),
banned = persistentListOf(),
) )
), ),
), ),
@ -55,18 +57,30 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMember
searchQuery = "something-with-no-results", searchQuery = "something-with-no-results",
searchResults = SearchBarResultState.NoResultsFound() searchResults = SearchBarResultState.NoResultsFound()
), ),
aRoomMemberListState().copy(
roomMembers = AsyncData.Success(
RoomMembers(
invited = persistentListOf(aVictor(), aWalter()),
joined = persistentListOf(anAlice(), aBob(), aWalter()),
banned = persistentListOf(),
)
),
canDisplayBannedUsers = true,
),
) )
} }
internal fun aRoomMemberListState( internal fun aRoomMemberListState(
roomMembers: AsyncData<RoomMembers> = AsyncData.Uninitialized, roomMembers: AsyncData<RoomMembers> = AsyncData.Uninitialized,
searchResults: SearchBarResultState<RoomMembers> = SearchBarResultState.Initial(), searchResults: SearchBarResultState<RoomMembers> = SearchBarResultState.Initial(),
canDisplayBannedUsers: Boolean = false,
) = RoomMemberListState( ) = RoomMemberListState(
roomMembers = roomMembers, roomMembers = roomMembers,
searchQuery = "", searchQuery = "",
searchResults = searchResults, searchResults = searchResults,
isSearchActive = false, isSearchActive = false,
canInvite = false, canInvite = false,
canDisplayBannedUsers = canDisplayBannedUsers,
eventSink = {} eventSink = {}
) )

View file

@ -16,6 +16,8 @@
package io.element.android.features.roomdetails.impl.members package io.element.android.features.roomdetails.impl.members
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -30,7 +32,12 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
@ -49,6 +56,7 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.SearchBar import io.element.android.libraries.designsystem.theme.components.SearchBar
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.designsystem.theme.components.SegmentedButton
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
@ -58,6 +66,12 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.MatrixUserRow import io.element.android.libraries.matrix.ui.components.MatrixUserRow
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
private enum class SelectedSection {
MEMBERS,
BANNED
}
@Composable @Composable
fun RoomMemberListView( fun RoomMemberListView(
@ -66,6 +80,7 @@ fun RoomMemberListView(
onInvitePressed: () -> Unit, onInvitePressed: () -> Unit,
onMemberSelected: (UserId) -> Unit, onMemberSelected: (UserId) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
initialSelectedSectionIndex: Int = 0,
) { ) {
fun onUserSelected(roomMember: RoomMember) { fun onUserSelected(roomMember: RoomMember) {
onMemberSelected(roomMember.userId) onMemberSelected(roomMember.userId)
@ -83,6 +98,7 @@ fun RoomMemberListView(
} }
} }
) { padding -> ) { padding ->
var selectedSection by remember { mutableStateOf(SelectedSection.entries[initialSelectedSectionIndex]) }
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -98,7 +114,8 @@ fun RoomMemberListView(
onActiveChanged = { state.eventSink(RoomMemberListEvents.OnSearchActiveChanged(it)) }, onActiveChanged = { state.eventSink(RoomMemberListEvents.OnSearchActiveChanged(it)) },
onTextChanged = { state.eventSink(RoomMemberListEvents.UpdateSearchQuery(it)) }, onTextChanged = { state.eventSink(RoomMemberListEvents.UpdateSearchQuery(it)) },
onUserSelected = ::onUserSelected, onUserSelected = ::onUserSelected,
modifier = Modifier.fillMaxWidth() selectedSection = selectedSection,
modifier = Modifier.fillMaxWidth(),
) )
if (!state.isSearchActive) { if (!state.isSearchActive) {
@ -106,7 +123,10 @@ fun RoomMemberListView(
RoomMemberList( RoomMemberList(
roomMembers = state.roomMembers.data, roomMembers = state.roomMembers.data,
showMembersCount = true, showMembersCount = true,
onUserSelected = ::onUserSelected canDisplayBannedUsersControls = state.canDisplayBannedUsers,
selectedSection = selectedSection,
onSelectedSectionChanged = { selectedSection = it },
onUserSelected = ::onUserSelected,
) )
} else if (state.roomMembers.isLoading()) { } else if (state.roomMembers.isLoading()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@ -118,49 +138,90 @@ fun RoomMemberListView(
} }
} }
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable @Composable
private fun RoomMemberList( private fun RoomMemberList(
roomMembers: RoomMembers, roomMembers: RoomMembers,
showMembersCount: Boolean, showMembersCount: Boolean,
selectedSection: SelectedSection,
onSelectedSectionChanged: (SelectedSection) -> Unit,
canDisplayBannedUsersControls: Boolean,
onUserSelected: (RoomMember) -> Unit, onUserSelected: (RoomMember) -> Unit,
) { ) {
LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) { LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) {
if (roomMembers.invited.isNotEmpty()) { if (canDisplayBannedUsersControls) {
roomMemberListSection( stickyHeader {
headerText = { stringResource(id = R.string.screen_room_member_list_pending_header_title) }, val segmentedButtonTitles = persistentListOf(
members = roomMembers.invited, stringResource(id = R.string.screen_room_member_list_mode_members),
onMemberSelected = { onUserSelected(it) } stringResource(id = R.string.screen_room_member_list_mode_banned),
) )
} SingleChoiceSegmentedButtonRow(
if (roomMembers.joined.isNotEmpty()) { modifier = Modifier
roomMemberListSection( .background(ElementTheme.colors.bgCanvasDefault)
headerText = { .fillMaxWidth()
if (showMembersCount) { .padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
val memberCount = roomMembers.joined.count() ) {
pluralStringResource(id = R.plurals.screen_room_member_list_header_title, count = memberCount, memberCount) for ((index, title) in segmentedButtonTitles.withIndex()) {
} else { SegmentedButton(
stringResource(id = R.string.screen_room_member_list_room_members_header_title) index = index,
count = segmentedButtonTitles.size,
selected = selectedSection.ordinal == index,
onClick = { onSelectedSectionChanged(SelectedSection.entries[index]) },
text = title,
)
} }
}, }
members = roomMembers.joined, }
onMemberSelected = { onUserSelected(it) } }
) when (selectedSection) {
SelectedSection.MEMBERS -> {
if (roomMembers.invited.isNotEmpty()) {
roomMemberListSection(
headerText = { stringResource(id = R.string.screen_room_member_list_pending_header_title) },
members = roomMembers.invited,
onMemberSelected = { onUserSelected(it) }
)
}
if (roomMembers.joined.isNotEmpty()) {
roomMemberListSection(
headerText = {
if (showMembersCount) {
val memberCount = roomMembers.joined.count()
pluralStringResource(id = R.plurals.screen_room_member_list_header_title, count = memberCount, memberCount)
} else {
stringResource(id = R.string.screen_room_member_list_room_members_header_title)
}
},
members = roomMembers.joined,
onMemberSelected = { onUserSelected(it) }
)
}
}
SelectedSection.BANNED -> { // Banned users
roomMemberListSection(
headerText = null,
members = roomMembers.banned,
onMemberSelected = { onUserSelected(it) }
)
}
} }
} }
} }
private fun LazyListScope.roomMemberListSection( private fun LazyListScope.roomMemberListSection(
headerText: @Composable () -> String, headerText: @Composable (() -> String)?,
members: ImmutableList<RoomMember>, members: ImmutableList<RoomMember>,
onMemberSelected: (RoomMember) -> Unit, onMemberSelected: (RoomMember) -> Unit,
) { ) {
item { headerText?.let {
Text( item {
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), Text(
text = headerText(), modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
style = ElementTheme.typography.fontBodyLgRegular, text = it(),
color = MaterialTheme.colorScheme.secondary, style = ElementTheme.typography.fontBodyLgRegular,
) color = MaterialTheme.colorScheme.secondary,
)
}
} }
items(members) { matrixUser -> items(members) { matrixUser ->
RoomMemberListItem( RoomMemberListItem(
@ -238,6 +299,7 @@ private fun RoomMemberSearchBar(
onActiveChanged: (Boolean) -> Unit, onActiveChanged: (Boolean) -> Unit,
onTextChanged: (String) -> Unit, onTextChanged: (String) -> Unit,
onUserSelected: (RoomMember) -> Unit, onUserSelected: (RoomMember) -> Unit,
selectedSection: SelectedSection,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
SearchBar( SearchBar(
@ -252,7 +314,10 @@ private fun RoomMemberSearchBar(
RoomMemberList( RoomMemberList(
roomMembers = results, roomMembers = results,
showMembersCount = false, showMembersCount = false,
onUserSelected = { onUserSelected(it) } onUserSelected = { onUserSelected(it) },
canDisplayBannedUsersControls = false,
selectedSection = selectedSection,
onSelectedSectionChanged = {},
) )
}, },
) )
@ -268,3 +333,28 @@ internal fun RoomMemberListPreview(@PreviewParameter(RoomMemberListStateProvider
onInvitePressed = {}, onInvitePressed = {},
) )
} }
@PreviewsDayNight
@Composable
internal fun RoomMemberBannedListPreview() = ElementPreview {
RoomMemberListView(
initialSelectedSectionIndex = 1,
state = aRoomMemberListState(
roomMembers = AsyncData.Success(
RoomMembers(
invited = persistentListOf(),
joined = persistentListOf(),
banned = persistentListOf(
aRoomMember(userId = UserId("@alice:example.com"), displayName = "Alice"),
aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob"),
aRoomMember(userId = UserId("@charlie:example.com"), displayName = "Charlie"),
),
)
),
canDisplayBannedUsers = true,
),
onBackPressed = {},
onMemberSelected = {},
onInvitePressed = {},
)
}

View file

@ -24,6 +24,8 @@
<string name="screen_room_details_notification_title">"Notifications"</string> <string name="screen_room_details_notification_title">"Notifications"</string>
<string name="screen_room_details_share_room_title">"Share room"</string> <string name="screen_room_details_share_room_title">"Share room"</string>
<string name="screen_room_details_updating_room">"Updating room…"</string> <string name="screen_room_details_updating_room">"Updating room…"</string>
<string name="screen_room_member_list_mode_banned">"Banned"</string>
<string name="screen_room_member_list_mode_members">"Members"</string>
<string name="screen_room_member_list_pending_header_title">"Pending"</string> <string name="screen_room_member_list_pending_header_title">"Pending"</string>
<string name="screen_room_member_list_role_administrator">"Admin"</string> <string name="screen_room_member_list_role_administrator">"Admin"</string>
<string name="screen_room_member_list_role_moderator">"Moderator"</string> <string name="screen_room_member_list_role_moderator">"Moderator"</string>

View file

@ -6,6 +6,7 @@
<string name="screen_session_verification_compare_numbers_subtitle">"Confirm that the numbers below match those shown on your other session."</string> <string name="screen_session_verification_compare_numbers_subtitle">"Confirm that the numbers below match those shown on your other session."</string>
<string name="screen_session_verification_compare_numbers_title">"Compare numbers"</string> <string name="screen_session_verification_compare_numbers_title">"Compare numbers"</string>
<string name="screen_session_verification_complete_subtitle">"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted."</string> <string name="screen_session_verification_complete_subtitle">"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted."</string>
<string name="screen_session_verification_enter_recovery_key">"Enter recovery key"</string>
<string name="screen_session_verification_open_existing_session_subtitle">"Prove its you in order to access your encrypted message history."</string> <string name="screen_session_verification_open_existing_session_subtitle">"Prove its you in order to access your encrypted message history."</string>
<string name="screen_session_verification_open_existing_session_title">"Open an existing session"</string> <string name="screen_session_verification_open_existing_session_title">"Open an existing session"</string>
<string name="screen_session_verification_positive_button_canceled">"Retry verification"</string> <string name="screen_session_verification_positive_button_canceled">"Retry verification"</string>

View file

@ -129,6 +129,8 @@ interface MatrixRoom : Closeable {
suspend fun canUserInvite(userId: UserId): Result<Boolean> suspend fun canUserInvite(userId: UserId): Result<Boolean>
suspend fun canUserBan(userId: UserId): Result<Boolean>
suspend fun canUserRedactOwn(userId: UserId): Result<Boolean> suspend fun canUserRedactOwn(userId: UserId): Result<Boolean>
suspend fun canUserRedactOther(userId: UserId): Result<Boolean> suspend fun canUserRedactOther(userId: UserId): Result<Boolean>

View file

@ -25,6 +25,11 @@ import io.element.android.libraries.matrix.api.room.StateEventType
*/ */
suspend fun MatrixRoom.canInvite(): Result<Boolean> = canUserInvite(sessionId) suspend fun MatrixRoom.canInvite(): Result<Boolean> = canUserInvite(sessionId)
/**
* Shortcut for calling [MatrixRoom.canBanUser] with our own user.
*/
suspend fun MatrixRoom.canBan(): Result<Boolean> = canUserBan(sessionId)
/** /**
* Shortcut for calling [MatrixRoom.canUserSendState] with our own user. * Shortcut for calling [MatrixRoom.canUserSendState] with our own user.
*/ */

View file

@ -319,6 +319,12 @@ class RustMatrixRoom(
} }
} }
override suspend fun canUserBan(userId: UserId): Result<Boolean> {
return runCatching {
innerRoom.canUserBan(userId.value)
}
}
override suspend fun canUserRedactOwn(userId: UserId): Result<Boolean> { override suspend fun canUserRedactOwn(userId: UserId): Result<Boolean> {
return runCatching { return runCatching {
innerRoom.canUserRedactOwn(userId.value) innerRoom.canUserRedactOwn(userId.value)

View file

@ -90,6 +90,7 @@ class FakeMatrixRoom(
private var joinRoomResult = Result.success(Unit) private var joinRoomResult = Result.success(Unit)
private var inviteUserResult = Result.success(Unit) private var inviteUserResult = Result.success(Unit)
private var canInviteResult = Result.success(true) private var canInviteResult = Result.success(true)
private var canBanResult = Result.success(false)
private var canRedactOwnResult = Result.success(canRedactOwn) private var canRedactOwnResult = Result.success(canRedactOwn)
private var canRedactOtherResult = Result.success(canRedactOther) private var canRedactOtherResult = Result.success(canRedactOther)
private val canSendStateResults = mutableMapOf<StateEventType, Result<Boolean>>() private val canSendStateResults = mutableMapOf<StateEventType, Result<Boolean>>()
@ -280,6 +281,10 @@ class FakeMatrixRoom(
inviteUserResult inviteUserResult
} }
override suspend fun canUserBan(userId: UserId): Result<Boolean> {
return canBanResult
}
override suspend fun canUserInvite(userId: UserId): Result<Boolean> { override suspend fun canUserInvite(userId: UserId): Result<Boolean> {
return canInviteResult return canInviteResult
} }
@ -495,6 +500,10 @@ class FakeMatrixRoom(
joinRoomResult = result joinRoomResult = result
} }
fun givenCanBanResult(result: Result<Boolean>) {
canBanResult = result
}
fun givenInviteUserResult(result: Result<Unit>) { fun givenInviteUserResult(result: Result<Unit>) {
inviteUserResult = result inviteUserResult = result
} }

View file

@ -120,6 +120,7 @@
<string name="common_error">"Error"</string> <string name="common_error">"Error"</string>
<string name="common_everyone">"Everyone"</string> <string name="common_everyone">"Everyone"</string>
<string name="common_favourite">"Favourite"</string> <string name="common_favourite">"Favourite"</string>
<string name="common_favourited">"Favourited"</string>
<string name="common_file">"File"</string> <string name="common_file">"File"</string>
<string name="common_file_saved_on_disk_android">"File saved to Downloads"</string> <string name="common_file_saved_on_disk_android">"File saved to Downloads"</string>
<string name="common_forward_message">"Forward message"</string> <string name="common_forward_message">"Forward message"</string>

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5f6ce6e7d31871ff45ca2494fbb12bb1bc6e44a1fb6baee547b001743d1b4702
size 35078

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5b5bd1bfd3ff184ae96c61ee639b0114b4612125f3284360f547fb2bdf73befb
size 34121

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:efc266bca406673c57731b4d6a283d217e7199f4b8a9c70c45ed3e00611d165d
size 52344

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:65dd3de9356356a9bf651cecd725c1be89a0d1b1ba816e4df9b25ce0a76313dd
size 51139