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
34
.github/workflows/clear-cache.yml
vendored
Normal file
34
.github/workflows/clear-cache.yml
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
name: Clear Gradle Cache
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# Every nights at 4
|
||||
- cron: "0 4 * * *"
|
||||
|
||||
# Enrich gradle.properties for CI/CD
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx8g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --warn
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: Clear Gradle cache
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: ⏬ Checkout with LFS
|
||||
uses: nschloe/action-cached-lfs-checkout@v1.2.2
|
||||
- name: ☕️ Use JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
|
||||
# This should build the project and run the tests, and the build files will be used to diff with the cache
|
||||
- name: ⚙️ Build the GPlay debug variant, run unit tests
|
||||
run: ./gradlew :app:assembleGplayDebug test $CI_GRADLE_ARG_PROPERTIES
|
||||
38
.github/workflows/generate_github_pages.yml
vendored
Normal file
38
.github/workflows/generate_github_pages.yml
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
name: Generate GitHub Pages
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# At 00:00 on every Tuesday UTC
|
||||
- cron: '0 0 * * 2'
|
||||
|
||||
jobs:
|
||||
generate-github-pages:
|
||||
runs-on: ubuntu-latest
|
||||
# Skip in forks
|
||||
if: github.repository == 'element-hq/element-x-android'
|
||||
steps:
|
||||
- name: ⏬ Checkout with LFS
|
||||
uses: nschloe/action-cached-lfs-checkout@v1.2.2
|
||||
- name: Use JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Run World screenshots generation script
|
||||
run: |
|
||||
./tools/test/generateWorldScreenshots.py
|
||||
mkdir -p screenshots/en
|
||||
cp tests/uitests/src/test/snapshots/images/* screenshots/en
|
||||
- name: Deploy GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./screenshots
|
||||
12
.github/workflows/quality.yml
vendored
12
.github/workflows/quality.yml
vendored
|
|
@ -21,6 +21,18 @@ jobs:
|
|||
- name: Run code quality check suite
|
||||
run: ./tools/check/check_code_quality.sh
|
||||
|
||||
checkScreesnhot:
|
||||
name: Search for invalid screenshot files
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Search for invalid screenshot files
|
||||
run: ./tools/test/checkInvalidScreenshots.py
|
||||
|
||||
check:
|
||||
name: Project Check Suite
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
2
.github/workflows/sync-sas-strings.yml
vendored
2
.github/workflows/sync-sas-strings.yml
vendored
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
# No concurrency required, runs every time on a schedule.
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.8
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -20,6 +20,9 @@ out/
|
|||
.gradle/
|
||||
build/
|
||||
|
||||
# Python cache
|
||||
__pycache__/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ allprojects {
|
|||
config.from(files("$rootDir/tools/detekt/detekt.yml"))
|
||||
}
|
||||
dependencies {
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.3.11")
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.3.12")
|
||||
}
|
||||
|
||||
// KtLint
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
Add `local_time`, `utc_time` and `sdk_sha` params to bug reports so they're easier to investigate.
|
||||
1
changelog.d/2198.bugfix
Normal file
1
changelog.d/2198.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
|||
Hide blocked users list when there are no blocked users.
|
||||
1
changelog.d/2257.feature
Normal file
1
changelog.d/2257.feature
Normal file
|
|
@ -0,0 +1 @@
|
|||
Admins can now change user roles in rooms.
|
||||
1
changelog.d/2258.feature
Normal file
1
changelog.d/2258.feature
Normal file
|
|
@ -0,0 +1 @@
|
|||
Room member moderation: kick, ban and unban users from a room.
|
||||
1
changelog.d/2322.misc
Normal file
1
changelog.d/2322.misc
Normal file
|
|
@ -0,0 +1 @@
|
|||
Improve room member list loading times, increase chunk size
|
||||
1
changelog.d/2511.misc
Normal file
1
changelog.d/2511.misc
Normal file
|
|
@ -0,0 +1 @@
|
|||
Remove the special log level for the Rust SDK read receipts.
|
||||
1
changelog.d/995.bugfix
Normal file
1
changelog.d/995.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
|||
Prevent sending empty messages.
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="call_foreground_service_channel_title_android">"進行中的通話"</string>
|
||||
<string name="call_foreground_service_message_android">"點擊以返回到通話頁面"</string>
|
||||
<string name="call_foreground_service_title_android">"☎️ 通話中"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ package io.element.android.features.createroom.impl.addpeople
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.createroom.impl.userlist.SelectionMode
|
||||
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||
import io.element.android.features.createroom.impl.userlist.aListOfSelectedUsers
|
||||
import io.element.android.features.createroom.impl.userlist.aUserListState
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
|
|
@ -32,7 +31,7 @@ open class AddPeopleUserListStateProvider : PreviewParameterProvider<UserListSta
|
|||
aUserListState(),
|
||||
aUserListState().copy(
|
||||
searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
|
||||
selectedUsers = aListOfSelectedUsers(),
|
||||
selectedUsers = aMatrixUserList().toImmutableList(),
|
||||
isSearchActive = false,
|
||||
selectionMode = SelectionMode.Multiple,
|
||||
),
|
||||
|
|
@ -44,7 +43,7 @@ open class AddPeopleUserListStateProvider : PreviewParameterProvider<UserListSta
|
|||
}
|
||||
.toImmutableList()
|
||||
),
|
||||
selectedUsers = aListOfSelectedUsers(),
|
||||
selectedUsers = aMatrixUserList().toImmutableList(),
|
||||
isSearchActive = true,
|
||||
selectionMode = SelectionMode.Multiple,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ import io.element.android.libraries.designsystem.theme.components.HorizontalDivi
|
|||
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -92,7 +92,7 @@ fun SearchUserBar(
|
|||
animationSpec = spring(stiffness = Spring.StiffnessMediumLow)
|
||||
)
|
||||
|
||||
SelectedUsersList(
|
||||
SelectedUsersRowList(
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
selectedUsers = selectedUsers,
|
||||
autoScroll = true,
|
||||
|
|
@ -114,7 +114,7 @@ fun SearchUserBar(
|
|||
SearchMultipleUsersResultItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
searchResult = searchResult,
|
||||
isUserSelected = selectedUsers.find { it.userId == searchResult.matrixUser.userId } != null,
|
||||
isUserSelected = selectedUsers.contains(searchResult.matrixUser),
|
||||
onCheckedChange = { checked ->
|
||||
if (checked) {
|
||||
onUserSelected(searchResult.matrixUser)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import io.element.android.features.createroom.impl.userlist.UserListStateProvide
|
|||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
|
||||
|
||||
@Composable
|
||||
fun UserListView(
|
||||
|
|
@ -64,7 +64,7 @@ fun UserListView(
|
|||
)
|
||||
|
||||
if (state.isMultiSelectionEnabled && !state.isSearchActive && state.selectedUsers.isNotEmpty()) {
|
||||
SelectedUsersList(
|
||||
SelectedUsersRowList(
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
selectedUsers = state.selectedUsers,
|
||||
autoScroll = true,
|
||||
|
|
|
|||
|
|
@ -18,10 +18,11 @@ package io.element.android.features.createroom.impl.configureroom
|
|||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||
import io.element.android.features.createroom.impl.userlist.aListOfSelectedUsers
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.permissions.api.aPermissionsState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> {
|
||||
override val values: Sequence<ConfigureRoomState>
|
||||
|
|
@ -31,7 +32,7 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
|||
config = CreateRoomConfig(
|
||||
roomName = "Room 101",
|
||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||
invites = aListOfSelectedUsers(),
|
||||
invites = aMatrixUserList().toImmutableList(),
|
||||
privacy = RoomPrivacy.Public,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ import io.element.android.libraries.designsystem.theme.components.TextButton
|
|||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
|
||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
|
||||
import io.element.android.libraries.matrix.ui.components.UnsavedAvatar
|
||||
import io.element.android.libraries.permissions.api.PermissionsView
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
|
@ -120,7 +120,7 @@ fun ConfigureRoomView(
|
|||
onTopicChanged = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
|
||||
)
|
||||
if (state.config.invites.isNotEmpty()) {
|
||||
SelectedUsersList(
|
||||
SelectedUsersRowList(
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
contentPadding = PaddingValues(horizontal = 24.dp),
|
||||
selectedUsers = state.config.invites,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class UserListDataStore @Inject constructor() {
|
|||
private val selectedUsers: MutableStateFlow<List<MatrixUser>> = MutableStateFlow(emptyList())
|
||||
|
||||
fun selectUser(user: MatrixUser) {
|
||||
if (user !in selectedUsers.value) {
|
||||
if (!selectedUsers.value.contains(user)) {
|
||||
selectedUsers.tryEmit(selectedUsers.value.plus(user))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,15 +38,15 @@ open class UserListStateProvider : PreviewParameterProvider<UserListState> {
|
|||
aUserListState().copy(
|
||||
isSearchActive = true,
|
||||
searchQuery = "@someone:matrix.org",
|
||||
selectedUsers = aListOfSelectedUsers(),
|
||||
searchResults = SearchBarResultState.Results(aMatrixUserList().map { UserSearchResult(it) }.toImmutableList()),
|
||||
selectedUsers = aMatrixUserList().toImmutableList(),
|
||||
searchResults = SearchBarResultState.Results(aListOfUserSearchResults()),
|
||||
),
|
||||
aUserListState().copy(
|
||||
isSearchActive = true,
|
||||
searchQuery = "@someone:matrix.org",
|
||||
selectionMode = SelectionMode.Multiple,
|
||||
selectedUsers = aListOfSelectedUsers(),
|
||||
searchResults = SearchBarResultState.Results(aMatrixUserList().map { UserSearchResult(it) }.toImmutableList()),
|
||||
selectedUsers = aMatrixUserList().toImmutableList(),
|
||||
searchResults = SearchBarResultState.Results(aListOfUserSearchResults()),
|
||||
),
|
||||
aUserListState().copy(
|
||||
isSearchActive = true,
|
||||
|
|
@ -68,3 +68,4 @@ fun aUserListState() = UserListState(
|
|||
)
|
||||
|
||||
fun aListOfSelectedUsers() = aMatrixUserList().take(6).toImmutableList()
|
||||
fun aListOfUserSearchResults() = aMatrixUserList().take(6).map { UserSearchResult(it) }.toImmutableList()
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<string name="screen_notification_optin_title">"Дозволити сповіщення і ніколи не пропускати повідомлення"</string>
|
||||
<string name="screen_welcome_bullet_1">"Дзвінки, опитування, пошук тощо будуть додані пізніше цього року."</string>
|
||||
<string name="screen_welcome_bullet_2">"Історія повідомлень для зашифрованих кімнат ще недоступна."</string>
|
||||
<string name="screen_welcome_bullet_3">"Ми хотіли б почути вас, розкажіть нам, ваші враження та ідеї щодо застосунку, на сторінці налаштувань."</string>
|
||||
<string name="screen_welcome_bullet_3">"Ми хотіли б почути вас, розкажіть нам ваші враження та ідеї щодо застосунку на сторінці налаштувань."</string>
|
||||
<string name="screen_welcome_button">"Пішли!"</string>
|
||||
<string name="screen_welcome_subtitle">"Ось що вам потрібно знати:"</string>
|
||||
<string name="screen_welcome_title">"Ласкаво просимо до %1$s!"</string>
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ fun SendLocationView(
|
|||
lon = cameraPositionState.position.target!!.longitude,
|
||||
zoom = cameraPositionState.position.zoom,
|
||||
),
|
||||
cameraPositionState.location?.let {
|
||||
location = cameraPositionState.location?.let {
|
||||
Location(
|
||||
lat = it.latitude,
|
||||
lon = it.longitude,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@
|
|||
<string name="screen_app_lock_setup_confirm_pin">"Confirmer le code PIN"</string>
|
||||
<string name="screen_app_lock_setup_pin_blacklisted_dialog_content">"Vous ne pouvez pas choisir ce code PIN pour des raisons de sécurité"</string>
|
||||
<string name="screen_app_lock_setup_pin_blacklisted_dialog_title">"Choisissez un code PIN différent"</string>
|
||||
<string name="screen_app_lock_setup_pin_context">"Verrouillez %1$s pour ajouter une sécurité supplémentaire à vos discussions. Choisissez un code facile à retenir. Si vous oubliez le code PIN, vous serez déconnecté."</string>
|
||||
<string name="screen_app_lock_setup_pin_context">"Verrouillez %1$s pour ajouter une sécurité supplémentaire à vos discussions.
|
||||
|
||||
Choisissez un code facile à retenir. Si vous oubliez le code PIN, vous serez déconnecté."</string>
|
||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Veuillez saisir le même code PIN deux fois"</string>
|
||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Les codes PIN ne correspondent pas"</string>
|
||||
<string name="screen_app_lock_signout_alert_message">"Pour continuer, vous devrez vous connecter à nouveau et créer un nouveau code PIN."</string>
|
||||
|
|
|
|||
|
|
@ -10,13 +10,18 @@
|
|||
<string name="screen_app_lock_settings_remove_pin_alert_message">"您確定要移除 PIN 碼嗎?"</string>
|
||||
<string name="screen_app_lock_settings_remove_pin_alert_title">"移除 PIN 碼"</string>
|
||||
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"允許 %1$s"</string>
|
||||
<string name="screen_app_lock_setup_biometric_unlock_skip">"我想使用 PIN 碼"</string>
|
||||
<string name="screen_app_lock_setup_choose_pin">"選擇 PIN 碼"</string>
|
||||
<string name="screen_app_lock_setup_confirm_pin">"確認 PIN 碼"</string>
|
||||
<string name="screen_app_lock_setup_pin_blacklisted_dialog_content">"基於安全性的考量,您選的 PIN 碼無法使用"</string>
|
||||
<string name="screen_app_lock_setup_pin_blacklisted_dialog_title">"選擇一個不一樣的 PIN 碼"</string>
|
||||
<string name="screen_app_lock_setup_pin_blacklisted_dialog_title">"選擇不一樣的 PIN 碼"</string>
|
||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"請輸入相同的 PIN 碼兩次"</string>
|
||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN 碼不一樣"</string>
|
||||
<string name="screen_app_lock_signout_alert_message">"您需要重新登入並建立新的 PIN 碼才能繼續"</string>
|
||||
<string name="screen_app_lock_signout_alert_title">"您即將登出"</string>
|
||||
<plurals name="screen_app_lock_subtitle">
|
||||
<item quantity="other">"您有 %1$d 次解鎖的機會"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_app_lock_subtitle_wrong_pin">
|
||||
<item quantity="other">"PIN 碼錯誤。您還有 %1$d 次機會"</item>
|
||||
</plurals>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_account_provider_change">"Промяна на доставчика на акаунт"</string>
|
||||
<string name="screen_account_provider_form_notice">"Въведете термин за търсене или адрес на домейн."</string>
|
||||
<string name="screen_account_provider_form_subtitle">"Търсене на компания, общност или частен сървър."</string>
|
||||
<string name="screen_account_provider_form_subtitle">"Потърсете компания, общност или частен сървър."</string>
|
||||
<string name="screen_account_provider_form_title">"Намерете доставчик на акаунт"</string>
|
||||
<string name="screen_account_provider_signin_subtitle">"Това е мястото, където ще живеят вашите разговори — точно както бихте използвали имейл доставчик, за да съхранявате вашите имейли."</string>
|
||||
<string name="screen_account_provider_signin_title">"На път сте да влезете в %s"</string>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
<string name="screen_change_server_form_header">"Hemserverns URL"</string>
|
||||
<string name="screen_change_server_form_notice">"Du kan bara ansluta till en befintlig server som stöder sliding sync. Din hemserveradministratör måste konfigurera det. %1$s"</string>
|
||||
<string name="screen_change_server_subtitle">"Vad är adressen till din server?"</string>
|
||||
<string name="screen_change_server_title">"Välj din server"</string>
|
||||
<string name="screen_login_error_deactivated_account">"Detta konto har avaktiverats."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Felaktigt användarnamn och/eller lösenord"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"Detta är inte en giltig användaridentifierare. Förväntat format: \'@användare:hemserver.org\'"</string>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_signout_confirmation_dialog_content">"Är du säker på att du vill logga ut?"</string>
|
||||
<string name="screen_signout_confirmation_dialog_submit">"Logga ut"</string>
|
||||
<string name="screen_signout_confirmation_dialog_title">"Logga ut"</string>
|
||||
<string name="screen_signout_in_progress_dialog_content">"Loggar ut …"</string>
|
||||
<string name="screen_signout_preference_item">"Logga ut"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ import io.element.android.libraries.architecture.AsyncData
|
|||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.textcomposer.aRichTextEditorState
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
import io.element.android.wysiwyg.compose.RichTextEditorState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ fun aMessagesState(
|
|||
userHasPermissionToRedactOther: Boolean = false,
|
||||
userHasPermissionToSendReaction: Boolean = true,
|
||||
composerState: MessageComposerState = aMessageComposerState(
|
||||
richTextEditorState = RichTextEditorState("Hello", initialFocus = true),
|
||||
richTextEditorState = aRichTextEditorState(initialText = "Hello", initialFocus = true),
|
||||
isFullScreen = false,
|
||||
mode = MessageComposerMode.Normal,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -28,12 +28,29 @@
|
|||
<string name="screen_room_attachment_source_location">"Poloha"</string>
|
||||
<string name="screen_room_attachment_source_poll">"Hlasování"</string>
|
||||
<string name="screen_room_attachment_text_formatting">"Formátování textu"</string>
|
||||
<string name="screen_room_change_permissions_administrators">"Pouze správci"</string>
|
||||
<string name="screen_room_change_permissions_ban_people">"Vykázat lidi"</string>
|
||||
<string name="screen_room_change_permissions_delete_messages">"Odstranit zprávy"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Všichni"</string>
|
||||
<string name="screen_room_change_permissions_invite_people">"Pozvat lidi"</string>
|
||||
<string name="screen_room_change_permissions_member_moderation">"Moderování členů"</string>
|
||||
<string name="screen_room_change_permissions_messages_and_content">"Zprávy a obsah"</string>
|
||||
<string name="screen_room_change_permissions_moderators">"Správci a moderátoři"</string>
|
||||
<string name="screen_room_change_permissions_remove_people">"Odebrat lidi"</string>
|
||||
<string name="screen_room_change_permissions_room_avatar">"Změnit avatar místnosti"</string>
|
||||
<string name="screen_room_change_permissions_room_details">"Podrobnosti místnosti"</string>
|
||||
<string name="screen_room_change_permissions_room_name">"Změnit název místnosti"</string>
|
||||
<string name="screen_room_change_permissions_room_topic">"Změnit téma místnosti"</string>
|
||||
<string name="screen_room_change_permissions_send_messages">"Odeslat zprávy"</string>
|
||||
<string name="screen_room_change_role_administrators_title">"Upravit správce"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"Tuto akci nebudete moci vrátit zpět. Upravujete oprávnění uživatele, tak aby měl stejnou úroveň jako vy."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Přidat správce?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Degradovat"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"Tuto změnu nebudete moci vrátit zpět, protože sami degradujete, pokud jste posledním privilegovaným uživatelem v místnosti, nebude možné znovu získat oprávnění."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Degradovat se?"</string>
|
||||
<string name="screen_room_change_role_moderators_title">"Upravit moderátory"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_description">"Máte neuložené změny."</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_title">"Uložit změny?"</string>
|
||||
<string name="screen_room_encrypted_history_banner">"Historie zpráv je momentálně v této místnosti nedostupná"</string>
|
||||
<string name="screen_room_encrypted_history_banner_unverified">"Historie zpráv není v této místnosti k dispozici. Ověřte toto zařízení, abyste viděli historii zpráv."</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
|
||||
|
|
@ -62,6 +79,20 @@
|
|||
<string name="screen_room_reactions_show_more">"Zobrazit více"</string>
|
||||
<string name="screen_room_retry_send_menu_send_again_action">"Odeslat znovu"</string>
|
||||
<string name="screen_room_retry_send_menu_title">"Vaši zprávu se nepodařilo odeslat"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Správci"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_my_role">"Změnit moji roli"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_member">"Degradovat na člena"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"Degradovat na moderátora"</string>
|
||||
<string name="screen_room_roles_and_permissions_member_moderation">"Moderování členů"</string>
|
||||
<string name="screen_room_roles_and_permissions_messages_and_content">"Zprávy a obsah"</string>
|
||||
<string name="screen_room_roles_and_permissions_moderators">"Moderátoři"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Oprávnění"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset">"Obnovit oprávnění"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Po obnovení oprávnění ztratíte aktuální nastavení."</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_title">"Obnovit oprávnění?"</string>
|
||||
<string name="screen_room_roles_and_permissions_roles_header">"Role"</string>
|
||||
<string name="screen_room_roles_and_permissions_room_details">"Podrobnosti místnosti"</string>
|
||||
<string name="screen_room_roles_and_permissions_title">"Role a oprávnění"</string>
|
||||
<string name="screen_room_timeline_add_reaction">"Přidat emoji"</string>
|
||||
<string name="screen_room_timeline_less_reactions">"Zobrazit méně"</string>
|
||||
<plurals name="screen_room_typing_many_members">
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@
|
|||
<string name="screen_room_attachment_source_poll">"Sondage"</string>
|
||||
<string name="screen_room_attachment_text_formatting">"Formatage du texte"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Tout le monde"</string>
|
||||
<string name="screen_room_change_permissions_room_avatar">"Changer l’avatar du salon"</string>
|
||||
<string name="screen_room_change_permissions_room_details">"Détails du salon"</string>
|
||||
<string name="screen_room_change_permissions_room_name">"Changer le nom du salon"</string>
|
||||
<string name="screen_room_change_permissions_room_topic">"Changer le sujet du salon"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"Vous ne pourrez pas annuler cette action. Vous êtes en train de promouvoir l’utilisateur pour qu’il ait le même niveau que vous."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Ajouter un administrateur ?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Rétrograder"</string>
|
||||
|
|
@ -61,6 +65,12 @@
|
|||
<string name="screen_room_reactions_show_more">"Afficher plus"</string>
|
||||
<string name="screen_room_retry_send_menu_send_again_action">"Envoyer à nouveau"</string>
|
||||
<string name="screen_room_retry_send_menu_title">"Votre message n’a pas pu être envoyé"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Administrateurs"</string>
|
||||
<string name="screen_room_roles_and_permissions_moderators">"Modérateurs"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Autorisations"</string>
|
||||
<string name="screen_room_roles_and_permissions_roles_header">"Rôles"</string>
|
||||
<string name="screen_room_roles_and_permissions_room_details">"Détails du salon"</string>
|
||||
<string name="screen_room_roles_and_permissions_title">"Rôles et autorisations"</string>
|
||||
<string name="screen_room_timeline_add_reaction">"Ajouter un émoji"</string>
|
||||
<string name="screen_room_timeline_less_reactions">"Afficher moins"</string>
|
||||
<plurals name="screen_room_typing_many_members">
|
||||
|
|
|
|||
|
|
@ -28,12 +28,29 @@
|
|||
<string name="screen_room_attachment_source_location">"Местоположение"</string>
|
||||
<string name="screen_room_attachment_source_poll">"Опрос"</string>
|
||||
<string name="screen_room_attachment_text_formatting">"Форматирование текста"</string>
|
||||
<string name="screen_room_change_permissions_administrators">"Только для администраторов"</string>
|
||||
<string name="screen_room_change_permissions_ban_people">"Заблокировать людей"</string>
|
||||
<string name="screen_room_change_permissions_delete_messages">"Удалить сообщения"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Для всех"</string>
|
||||
<string name="screen_room_change_permissions_invite_people">"Пригласить людей"</string>
|
||||
<string name="screen_room_change_permissions_member_moderation">"Модерация участников"</string>
|
||||
<string name="screen_room_change_permissions_messages_and_content">"Сообщения и содержание"</string>
|
||||
<string name="screen_room_change_permissions_moderators">"Администраторы и модераторы"</string>
|
||||
<string name="screen_room_change_permissions_remove_people">"Удалить людей"</string>
|
||||
<string name="screen_room_change_permissions_room_avatar">"Изменить изображение комнаты"</string>
|
||||
<string name="screen_room_change_permissions_room_details">"Информация о комнате"</string>
|
||||
<string name="screen_room_change_permissions_room_name">"Изменить название комнаты"</string>
|
||||
<string name="screen_room_change_permissions_room_topic">"Сменить тему комнаты"</string>
|
||||
<string name="screen_room_change_permissions_send_messages">"Отправить сообщение"</string>
|
||||
<string name="screen_room_change_role_administrators_title">"Редактировать роль администраторов"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"Вы не сможете отменить это действие. Вы устанавливаете уровень пользователю соответствующий вашему."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Добавить администратора?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Понизить уровень"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"Вы не сможете отменить это изменение, так как понижаете себя статус. Если вы являетесь последним привилегированным пользователем в комнате, восстановить привилегии будет невозможно."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Понизить свой уровень?"</string>
|
||||
<string name="screen_room_change_role_moderators_title">"Редактировать роль модераторов"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_description">"У вас есть несохраненные изменения."</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_title">"Сохранить изменения?"</string>
|
||||
<string name="screen_room_encrypted_history_banner">"В настоящее время история сообщений недоступна в этой комнате."</string>
|
||||
<string name="screen_room_encrypted_history_banner_unverified">"История сообщений в этой комнате недоступна. Проверьте это устройство, чтобы увидеть историю сообщений."</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Не удалось обработать медиафайл для загрузки, попробуйте еще раз."</string>
|
||||
|
|
@ -62,6 +79,20 @@
|
|||
<string name="screen_room_reactions_show_more">"Показать больше"</string>
|
||||
<string name="screen_room_retry_send_menu_send_again_action">"Отправить снова"</string>
|
||||
<string name="screen_room_retry_send_menu_title">"Не удалось отправить ваше сообщение"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Администраторы"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_my_role">"Измените мою роль"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_member">"Понижение до участника"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"Понизить до модератора"</string>
|
||||
<string name="screen_room_roles_and_permissions_member_moderation">"Модерация участников"</string>
|
||||
<string name="screen_room_roles_and_permissions_messages_and_content">"Сообщения и содержание"</string>
|
||||
<string name="screen_room_roles_and_permissions_moderators">"Модераторы"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Разрешения"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset">"Сбросить разрешения"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Как только вы сбросите разрешения, вы потеряете текущие настройки."</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_title">"Сбросить разрешения?"</string>
|
||||
<string name="screen_room_roles_and_permissions_roles_header">"Роли"</string>
|
||||
<string name="screen_room_roles_and_permissions_room_details">"Информация о комнате"</string>
|
||||
<string name="screen_room_roles_and_permissions_title">"Роли и разрешения"</string>
|
||||
<string name="screen_room_timeline_add_reaction">"Добавить эмодзи"</string>
|
||||
<string name="screen_room_timeline_less_reactions">"Показать меньше"</string>
|
||||
<plurals name="screen_room_typing_many_members">
|
||||
|
|
|
|||
|
|
@ -42,11 +42,15 @@
|
|||
<string name="screen_room_change_permissions_room_name">"Zmeniť názov miestnosti"</string>
|
||||
<string name="screen_room_change_permissions_room_topic">"Zmeniť tému miestnosti"</string>
|
||||
<string name="screen_room_change_permissions_send_messages">"Odoslať správy"</string>
|
||||
<string name="screen_room_change_role_administrators_title">"Upraviť správcov"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"Túto akciu nebudete môcť vrátiť späť. Zvyšujete úroveň používateľa na rovnakú úroveň výkonu ako máte vy."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Pridať správcu?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Znížiť"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"Túto zmenu nebudete môcť vrátiť späť, pretože znižujete svoju úroveň. Ak ste posledným privilegovaným používateľom v miestnosti, nebude možné získať znova oprávnenia."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Znížiť svoju úroveň?"</string>
|
||||
<string name="screen_room_change_role_moderators_title">"Upraviť moderátorov"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_description">"Máte neuložené zmeny."</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_title">"Uložiť zmeny?"</string>
|
||||
<string name="screen_room_encrypted_history_banner">"História správ v tejto miestnosti nie je momentálne k dispozícii"</string>
|
||||
<string name="screen_room_encrypted_history_banner_unverified">"História správ nie je v tejto miestnosti k dispozícii. Ak chcete zobraziť históriu správ, overte toto zariadenie."</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova."</string>
|
||||
|
|
@ -76,11 +80,16 @@
|
|||
<string name="screen_room_retry_send_menu_send_again_action">"Odoslať znova"</string>
|
||||
<string name="screen_room_retry_send_menu_title">"Vašu správu sa nepodarilo odoslať"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Správcovia"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_my_role">"Zmeniť moje oprávnenia"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_member">"Znížiť úroveň na člena"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"Znížiť úroveň na moderátora"</string>
|
||||
<string name="screen_room_roles_and_permissions_member_moderation">"Moderovanie členov"</string>
|
||||
<string name="screen_room_roles_and_permissions_messages_and_content">"Správy a obsah"</string>
|
||||
<string name="screen_room_roles_and_permissions_moderators">"Moderátori"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Oprávnenia"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset">"Obnoviť povolenia"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Po obnovení oprávnení prídete o aktuálne nastavenia."</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_title">"Obnoviť oprávnenia?"</string>
|
||||
<string name="screen_room_roles_and_permissions_roles_header">"Roly"</string>
|
||||
<string name="screen_room_roles_and_permissions_room_details">"Podrobnosti o miestnosti"</string>
|
||||
<string name="screen_room_roles_and_permissions_title">"Roly a povolenia"</string>
|
||||
|
|
|
|||
|
|
@ -27,11 +27,13 @@
|
|||
<string name="screen_room_attachment_source_location">"Plats"</string>
|
||||
<string name="screen_room_attachment_source_poll">"Omröstning"</string>
|
||||
<string name="screen_room_attachment_text_formatting">"Textformatering"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Alla"</string>
|
||||
<string name="screen_room_encrypted_history_banner">"Meddelandehistoriken är för närvarande otillgänglig."</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Misslyckades att bearbeta media för uppladdning, vänligen pröva igen."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Kunde inte hämta användarinformation"</string>
|
||||
<string name="screen_room_invite_again_alert_message">"Vill du bjuda tillbaka dem?"</string>
|
||||
<string name="screen_room_invite_again_alert_title">"Du är ensam i den här chatten"</string>
|
||||
<string name="screen_room_mentions_at_room_title">"Alla"</string>
|
||||
<string name="screen_room_message_copied">"Meddelande kopierat"</string>
|
||||
<string name="screen_room_no_permission_to_post">"Du är inte behörig att göra inlägg i det här rummet"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom">"Tillåt anpassad inställning"</string>
|
||||
|
|
@ -53,4 +55,5 @@
|
|||
<string name="screen_room_retry_send_menu_title">"Ditt meddelande kunde inte skickas"</string>
|
||||
<string name="screen_room_timeline_add_reaction">"Lägg till emoji"</string>
|
||||
<string name="screen_room_timeline_less_reactions">"Visa mindre"</string>
|
||||
<string name="screen_room_voice_message_tooltip">"Håll för att spela in"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -42,11 +42,13 @@
|
|||
<string name="screen_room_change_permissions_room_name">"Змінити назву кімнати"</string>
|
||||
<string name="screen_room_change_permissions_room_topic">"Змінити тему кімнати"</string>
|
||||
<string name="screen_room_change_permissions_send_messages">"Надіслати повідомлення"</string>
|
||||
<string name="screen_room_change_role_administrators_title">"Керувати адмінами"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"Ви не зможете скасувати цю дію. Ви просуваєте користувача, щоб він мав такий же рівень прав, як і ви."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Додати адміністратора?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Понизити"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"Ви не зможете скасувати цю зміну, оскільки ви знижуєте себе, якщо ви останній привілейований користувач у кімнаті, відновити привілеї буде неможливо."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Понизити себе?"</string>
|
||||
<string name="screen_room_change_role_moderators_title">"Керувати модераторами"</string>
|
||||
<string name="screen_room_encrypted_history_banner">"Історія повідомлень наразі недоступна."</string>
|
||||
<string name="screen_room_encrypted_history_banner_unverified">"Історія повідомлень недоступна в цій кімнаті. Перевірте цей пристрій, щоб побачити історію повідомлень."</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Не вдалося обробити медіафайл для завантаження, спробуйте ще раз."</string>
|
||||
|
|
|
|||
|
|
@ -40,4 +40,11 @@
|
|||
<string name="screen_room_retry_send_menu_title">"無法傳送您的訊息"</string>
|
||||
<string name="screen_room_timeline_add_reaction">"新增表情符號"</string>
|
||||
<string name="screen_room_timeline_less_reactions">"較少"</string>
|
||||
<plurals name="screen_room_typing_many_members">
|
||||
<item quantity="other">"%1$s、%2$s 和其他 %3$d 個人"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_typing_notification">
|
||||
<item quantity="other">"%1$s 正在打字"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_typing_two_members">"%1$s 和 %2$s"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -36,10 +36,10 @@
|
|||
<string name="screen_room_change_permissions_messages_and_content">"Messages and content"</string>
|
||||
<string name="screen_room_change_permissions_moderators">"Admins and moderators"</string>
|
||||
<string name="screen_room_change_permissions_remove_people">"Remove people"</string>
|
||||
<string name="screen_room_change_permissions_room_avatar">"Change Room Avatar"</string>
|
||||
<string name="screen_room_change_permissions_room_avatar">"Change room avatar"</string>
|
||||
<string name="screen_room_change_permissions_room_details">"Room details"</string>
|
||||
<string name="screen_room_change_permissions_room_name">"Change Room Name"</string>
|
||||
<string name="screen_room_change_permissions_room_topic">"Change Room Topic"</string>
|
||||
<string name="screen_room_change_permissions_room_name">"Change room name"</string>
|
||||
<string name="screen_room_change_permissions_room_topic">"Change room topic"</string>
|
||||
<string name="screen_room_change_permissions_send_messages">"Send messages"</string>
|
||||
<string name="screen_room_change_role_administrators_title">"Edit Admins"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"You will not be able to undo this action. You are promoting the user to have the same power level as you."</string>
|
||||
|
|
@ -87,6 +87,8 @@
|
|||
<string name="screen_room_roles_and_permissions_moderators">"Moderators"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Permissions"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset">"Reset permissions"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Once you reset permissions, you will lose the current settings."</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_title">"Reset permissions?"</string>
|
||||
<string name="screen_room_roles_and_permissions_roles_header">"Roles"</string>
|
||||
<string name="screen_room_roles_and_permissions_room_details">"Room details"</string>
|
||||
<string name="screen_room_roles_and_permissions_title">"Roles and permissions"</string>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.features.poll.api.pollcontent
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -83,9 +84,11 @@ fun aPollAnswerItem(
|
|||
)
|
||||
|
||||
fun aPollContentState(
|
||||
eventId: EventId? = null,
|
||||
isMine: Boolean = false,
|
||||
isEnded: Boolean = false,
|
||||
isDisclosed: Boolean = true,
|
||||
isPollEditable: Boolean = true,
|
||||
hasVotes: Boolean = true,
|
||||
question: String = aPollQuestion(),
|
||||
pollKind: PollKind = PollKind.Disclosed,
|
||||
|
|
@ -95,11 +98,11 @@ fun aPollContentState(
|
|||
hasVotes = hasVotes
|
||||
),
|
||||
) = PollContentState(
|
||||
eventId = null,
|
||||
eventId = eventId,
|
||||
question = question,
|
||||
answerItems = answerItems,
|
||||
pollKind = pollKind,
|
||||
isPollEditable = isMine && !isEnded,
|
||||
isPollEditable = isMine && !isEnded && isPollEditable,
|
||||
isPollEnded = isEnded,
|
||||
isMine = isMine,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace = "io.element.android.features.poll.impl"
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anvil {
|
||||
|
|
@ -48,12 +53,15 @@ dependencies {
|
|||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.features.messages.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(projects.libraries.dateformatter.test)
|
||||
testImplementation(projects.features.poll.test)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,44 +22,48 @@ import io.element.android.features.poll.api.pollcontent.aPollContentState
|
|||
import io.element.android.features.poll.impl.history.model.PollHistoryFilter
|
||||
import io.element.android.features.poll.impl.history.model.PollHistoryItem
|
||||
import io.element.android.features.poll.impl.history.model.PollHistoryItems
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
class PollHistoryStateProvider : PreviewParameterProvider<PollHistoryState> {
|
||||
override val values: Sequence<PollHistoryState>
|
||||
get() = sequenceOf(
|
||||
aPollHistoryState(
|
||||
isLoading = false,
|
||||
hasMoreToLoad = false,
|
||||
activeFilter = PollHistoryFilter.ONGOING,
|
||||
),
|
||||
aPollHistoryState(),
|
||||
aPollHistoryState(
|
||||
isLoading = true,
|
||||
hasMoreToLoad = true,
|
||||
activeFilter = PollHistoryFilter.PAST,
|
||||
),
|
||||
aPollHistoryState(
|
||||
activeFilter = PollHistoryFilter.ONGOING,
|
||||
currentItems = emptyList(),
|
||||
),
|
||||
aPollHistoryState(
|
||||
activeFilter = PollHistoryFilter.PAST,
|
||||
currentItems = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun aPollHistoryState(
|
||||
internal fun aPollHistoryState(
|
||||
isLoading: Boolean = false,
|
||||
hasMoreToLoad: Boolean = false,
|
||||
activeFilter: PollHistoryFilter = PollHistoryFilter.ONGOING,
|
||||
currentItems: ImmutableList<PollHistoryItem> = persistentListOf(
|
||||
currentItems: List<PollHistoryItem> = listOf(
|
||||
aPollHistoryItem(),
|
||||
),
|
||||
eventSink: (PollHistoryEvents) -> Unit = {},
|
||||
) = PollHistoryState(
|
||||
isLoading = isLoading,
|
||||
hasMoreToLoad = hasMoreToLoad,
|
||||
activeFilter = activeFilter,
|
||||
pollHistoryItems = PollHistoryItems(
|
||||
ongoing = currentItems,
|
||||
past = currentItems,
|
||||
ongoing = currentItems.toPersistentList(),
|
||||
past = currentItems.toPersistentList(),
|
||||
),
|
||||
eventSink = {},
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
private fun aPollHistoryItem(
|
||||
internal fun aPollHistoryItem(
|
||||
formattedDate: String = "01/12/2023",
|
||||
state: PollContentState = aPollContentState(),
|
||||
) = PollHistoryItem(
|
||||
|
|
|
|||
|
|
@ -4,10 +4,16 @@
|
|||
<string name="screen_create_poll_anonymous_desc">"只在投票結束後顯示結果"</string>
|
||||
<string name="screen_create_poll_anonymous_headline">"隱藏票數"</string>
|
||||
<string name="screen_create_poll_answer_hint">"選項 %1$d"</string>
|
||||
<string name="screen_create_poll_cancel_confirmation_content_android">"變更尚未儲存,您確定要返回嗎?"</string>
|
||||
<string name="screen_create_poll_question_desc">"問題或主題"</string>
|
||||
<string name="screen_create_poll_question_hint">"投什麼?"</string>
|
||||
<string name="screen_create_poll_title">"建立投票"</string>
|
||||
<string name="screen_edit_poll_delete_confirmation">"您確定要刪除投票嗎?"</string>
|
||||
<string name="screen_edit_poll_delete_confirmation_title">"刪除投票"</string>
|
||||
<string name="screen_edit_poll_title">"編輯投票"</string>
|
||||
<string name="screen_polls_history_empty_ongoing">"沒有進行中的投票。"</string>
|
||||
<string name="screen_polls_history_empty_past">"沒有已結束的投票。"</string>
|
||||
<string name="screen_polls_history_filter_ongoing">"進行中"</string>
|
||||
<string name="screen_polls_history_filter_past">"已結束"</string>
|
||||
<string name="screen_polls_history_title">"所有投票"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
* 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.features.poll.impl.history
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.features.poll.api.pollcontent.aPollContentState
|
||||
import io.element.android.features.poll.impl.R
|
||||
import io.element.android.features.poll.impl.history.model.PollHistoryFilter
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||
import io.element.android.tests.testutils.pressBack
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class PollHistoryViewTest {
|
||||
@get:Rule
|
||||
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on back invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<PollHistoryEvents>(expectEvents = false)
|
||||
ensureCalledOnce {
|
||||
rule.setPollHistoryViewView(
|
||||
aPollHistoryState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
goBack = it
|
||||
)
|
||||
rule.pressBack()
|
||||
}
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `clicking on edit poll invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<PollHistoryEvents>(expectEvents = false)
|
||||
val eventId = EventId("\$anEventId")
|
||||
val state = aPollHistoryState(
|
||||
currentItems = listOf(
|
||||
aPollHistoryItem(
|
||||
state = aPollContentState(
|
||||
eventId = eventId,
|
||||
isMine = true,
|
||||
isEnded = false,
|
||||
)
|
||||
)
|
||||
),
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
ensureCalledOnceWithParam(eventId) {
|
||||
rule.setPollHistoryViewView(
|
||||
state = state,
|
||||
onEditPoll = it
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_edit_poll)
|
||||
}
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `clicking on poll end emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<PollHistoryEvents>()
|
||||
val eventId = EventId("\$anEventId")
|
||||
val state = aPollHistoryState(
|
||||
currentItems = listOf(
|
||||
aPollHistoryItem(
|
||||
state = aPollContentState(
|
||||
eventId = eventId,
|
||||
isMine = true,
|
||||
isEnded = false,
|
||||
isPollEditable = false,
|
||||
)
|
||||
)
|
||||
),
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
rule.setPollHistoryViewView(
|
||||
state = state,
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_end_poll)
|
||||
// Cancel the dialog
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
// Do it again, and confirm the dialog
|
||||
rule.clickOn(CommonStrings.action_end_poll)
|
||||
eventsRecorder.assertEmpty()
|
||||
rule.clickOn(CommonStrings.action_ok)
|
||||
eventsRecorder.assertSingle(
|
||||
PollHistoryEvents.PollEndClicked(eventId)
|
||||
)
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `clicking on poll answer emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<PollHistoryEvents>()
|
||||
val eventId = EventId("\$anEventId")
|
||||
val state = aPollHistoryState(
|
||||
currentItems = listOf(
|
||||
aPollHistoryItem(
|
||||
state = aPollContentState(
|
||||
eventId = eventId,
|
||||
isMine = true,
|
||||
isEnded = false,
|
||||
isPollEditable = false,
|
||||
)
|
||||
)
|
||||
),
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
val answer = state.pollHistoryItems.ongoing.first().state.answerItems.first().answer
|
||||
rule.setPollHistoryViewView(
|
||||
state = state,
|
||||
)
|
||||
rule.onNodeWithText(answer.text).performClick()
|
||||
eventsRecorder.assertSingle(
|
||||
PollHistoryEvents.PollAnswerSelected(eventId, answer.id)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on past tab emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<PollHistoryEvents>()
|
||||
rule.setPollHistoryViewView(
|
||||
aPollHistoryState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.clickOn(R.string.screen_polls_history_filter_past)
|
||||
eventsRecorder.assertSingle(
|
||||
PollHistoryEvents.OnFilterSelected(filter = PollHistoryFilter.PAST)
|
||||
)
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `clicking on load more emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<PollHistoryEvents>()
|
||||
rule.setPollHistoryViewView(
|
||||
aPollHistoryState(
|
||||
hasMoreToLoad = true,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_load_more)
|
||||
eventsRecorder.assertSingle(
|
||||
PollHistoryEvents.LoadMore
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setPollHistoryViewView(
|
||||
state: PollHistoryState,
|
||||
onEditPoll: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
goBack: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
PollHistoryView(
|
||||
state = state,
|
||||
onEditPoll = onEditPoll,
|
||||
goBack = goBack,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ import androidx.compose.runtime.MutableState
|
|||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutPresenter
|
||||
|
|
@ -39,6 +40,8 @@ import io.element.android.libraries.matrix.api.user.getCurrentUser
|
|||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -86,6 +89,12 @@ class PreferencesRootPresenter @Inject constructor(
|
|||
mutableStateOf(null)
|
||||
}
|
||||
|
||||
val showBlockedUsersItem by produceState(initialValue = false) {
|
||||
matrixClient.ignoredUsersFlow
|
||||
.onEach { value = it.isNotEmpty() }
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
val directLogoutState = directLogoutPresenter.present()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
|
|
@ -106,6 +115,7 @@ class PreferencesRootPresenter @Inject constructor(
|
|||
showDeveloperSettings = showDeveloperSettings,
|
||||
showNotificationSettings = showNotificationSettings.value,
|
||||
showLockScreenSettings = showLockScreenSettings.value,
|
||||
showBlockedUsersItem = showBlockedUsersItem,
|
||||
directLogoutState = directLogoutState,
|
||||
snackbarMessage = snackbarMessage,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ data class PreferencesRootState(
|
|||
val showDeveloperSettings: Boolean,
|
||||
val showLockScreenSettings: Boolean,
|
||||
val showNotificationSettings: Boolean,
|
||||
val showBlockedUsersItem: Boolean,
|
||||
val directLogoutState: DirectLogoutState,
|
||||
val snackbarMessage: SnackbarMessage?,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ fun aPreferencesRootState() = PreferencesRootState(
|
|||
showDeveloperSettings = true,
|
||||
showNotificationSettings = true,
|
||||
showLockScreenSettings = true,
|
||||
showBlockedUsersItem = true,
|
||||
snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete),
|
||||
directLogoutState = aDirectLogoutState(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -122,11 +122,13 @@ fun PreferencesRootView(
|
|||
onClick = onOpenNotificationSettings,
|
||||
)
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.common_blocked_users)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())),
|
||||
onClick = onOpenBlockedUsers,
|
||||
)
|
||||
if (state.showBlockedUsersItem) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.common_blocked_users)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())),
|
||||
onClick = onOpenBlockedUsers,
|
||||
)
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.common_report_a_problem)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChatProblem())),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_advanced_settings_send_read_receipts">"Потвърждения за прочитане"</string>
|
||||
<string name="screen_advanced_settings_share_presence">"Споделяне на присъствието"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Отблокиране"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Отблокиране на потребителя"</string>
|
||||
<string name="screen_edit_profile_display_name">"Име"</string>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
<string name="screen_advanced_settings_share_presence">"Sdílejte přítomnost"</string>
|
||||
<string name="screen_advanced_settings_share_presence_description">"Pokud je tato funkce vypnutá, nebudete moci odesílat ani přijímat potvrzení o přečtení ani upozornění na psaní"</string>
|
||||
<string name="screen_advanced_settings_view_source_description">"Povolit možnost zobrazení zdroje zprávy na časové ose."</string>
|
||||
<string name="screen_blocked_users_empty">"Nemáte žádné blokované uživatele"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Odblokovat"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Znovu uvidíte všechny zprávy od nich."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Odblokovat uživatele"</string>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,9 @@
|
|||
<string name="screen_notification_settings_additional_settings_section_title">"Zusätzliche Einstellungen"</string>
|
||||
<string name="screen_notification_settings_calls_label">"Audio- und Videoanrufe"</string>
|
||||
<string name="screen_notification_settings_configuration_mismatch">"Konfiguration stimmt nicht überein"</string>
|
||||
<string name="screen_notification_settings_configuration_mismatch_description">"Wir haben die Einstellungen für Benachrichtigungen vereinfacht. Einige Einstellungen, die du gewählt hast, werden hier nicht angezeigt, sind aber immer noch aktiv. Wenn du fortfährst, können sich einige deiner Einstellungen ändern."</string>
|
||||
<string name="screen_notification_settings_configuration_mismatch_description">"Wir haben die Einstellungen für Benachrichtigungen vereinfacht. Einige Einstellungen, die du gewählt hast, werden hier nicht angezeigt, sind aber immer noch aktiv.
|
||||
|
||||
Wenn du fortfährst, können sich einige deiner Einstellungen ändern."</string>
|
||||
<string name="screen_notification_settings_direct_chats">"Direktnachrichten"</string>
|
||||
<string name="screen_notification_settings_edit_custom_settings_section_title">"Benutzerdefinierte Einstellung pro Chat"</string>
|
||||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Beim Aktualisieren der Benachrichtigungseinstellungen ist ein Fehler aufgetreten."</string>
|
||||
|
|
|
|||
|
|
@ -24,9 +24,7 @@
|
|||
<string name="screen_notification_settings_additional_settings_section_title">"Réglages supplémentaires"</string>
|
||||
<string name="screen_notification_settings_calls_label">"Appels audio et vidéo"</string>
|
||||
<string name="screen_notification_settings_configuration_mismatch">"Incompatibilité de configuration"</string>
|
||||
<string name="screen_notification_settings_configuration_mismatch_description">"Nous avons simplifié les paramètres des notifications pour que les options soient plus faciles à trouver.
|
||||
|
||||
Certains paramètres personnalisés que vous avez choisis par le passé ne sont pas affichés ici, mais ils sont toujours actifs.
|
||||
<string name="screen_notification_settings_configuration_mismatch_description">"Nous avons simplifié les paramètres des notifications pour que les options soient plus faciles à trouver. Certains paramètres personnalisés que vous avez choisis par le passé ne sont pas affichés ici, mais ils sont toujours actifs.
|
||||
|
||||
Si vous continuez, il est possible que certains de vos paramètres soient modifiés."</string>
|
||||
<string name="screen_notification_settings_direct_chats">"Discussions directes"</string>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
<string name="screen_advanced_settings_share_presence">"Поделиться присутствием"</string>
|
||||
<string name="screen_advanced_settings_share_presence_description">"Если выключено, вы не сможете отправлять, получать уведомления о прочтении и наборе текста"</string>
|
||||
<string name="screen_advanced_settings_view_source_description">"Включить опцию просмотра источника сообщения в ленте."</string>
|
||||
<string name="screen_blocked_users_empty">"У вас нет заблокированных пользователей"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Разблокировать"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Вы снова сможете увидеть все сообщения."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Разблокировать пользователя"</string>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_advanced_settings_developer_mode">"Utvecklarläge"</string>
|
||||
<string name="screen_advanced_settings_developer_mode_description">"Aktivera för att ha tillgång till funktionalitet för utvecklare."</string>
|
||||
<string name="screen_advanced_settings_rich_text_editor_description">"Inaktivera rik-text-redigeraren för att skriva Markdown manuellt."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Avblockera"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Du kommer att kunna se alla meddelanden från dem igen."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Avblockera användare"</string>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<string name="screen_advanced_settings_send_read_receipts">"Читати журнали"</string>
|
||||
<string name="screen_advanced_settings_send_read_receipts_description">"Якщо вимкнено, ваші сповіщення про прочитання нікому не надсилатимуться. Ви все одно отримуватимете сповіщення про прочитання від інших користувачів."</string>
|
||||
<string name="screen_advanced_settings_share_presence">"Поділіться присутністю"</string>
|
||||
<string name="screen_advanced_settings_share_presence_description">"Якщо вимкнено, ви не зможете надсилати або отримувати сповіщення про прочитання або сповіщення про введення тексту"</string>
|
||||
<string name="screen_advanced_settings_share_presence_description">"Якщо вимкнено, ви не зможете надсилати та отримувати сповіщення про прочитання або введення тексту"</string>
|
||||
<string name="screen_advanced_settings_view_source_description">"Увімкнути опцію для перегляду коду повідомлення в стрічці"</string>
|
||||
<string name="screen_blocked_users_empty">"У вас немає заблокованих користувачів."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Розблокувати"</string>
|
||||
|
|
@ -28,12 +28,12 @@
|
|||
<string name="screen_notification_settings_configuration_mismatch_description">"Ми спростили налаштування сповіщень, щоб полегшити пошук параметрів. Деякі користувацькі налаштування, які ви вибрали раніше, тут не відображаються, але вони все ще активні.
|
||||
|
||||
Якщо ви продовжите, деякі з ваших налаштувань можуть змінитися."</string>
|
||||
<string name="screen_notification_settings_direct_chats">"Прямі чати"</string>
|
||||
<string name="screen_notification_settings_direct_chats">"Особисті чати"</string>
|
||||
<string name="screen_notification_settings_edit_custom_settings_section_title">"Користувальницькі налаштування для чату"</string>
|
||||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Під час оновлення налаштувань сповіщень сталася помилка."</string>
|
||||
<string name="screen_notification_settings_edit_mode_all_messages">"Всі повідомлення"</string>
|
||||
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Тільки згадки та ключові слова"</string>
|
||||
<string name="screen_notification_settings_edit_screen_direct_section_header">"У прямих чатах повідомляти мене про"</string>
|
||||
<string name="screen_notification_settings_edit_screen_direct_section_header">"В особистих чатах сповіщати про"</string>
|
||||
<string name="screen_notification_settings_edit_screen_group_section_header">"У групових чатах повідомляти мене про"</string>
|
||||
<string name="screen_notification_settings_enable_notifications">"Увімкнути сповіщення на цьому пристрої"</string>
|
||||
<string name="screen_notification_settings_failed_fixing_configuration">"Конфігурацію не виправлено, спробуйте ще раз."</string>
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
<string name="screen_notification_settings_mode_all">"Усі"</string>
|
||||
<string name="screen_notification_settings_mode_mentions">"Згадки"</string>
|
||||
<string name="screen_notification_settings_notification_section_title">"Повідомляти мене про"</string>
|
||||
<string name="screen_notification_settings_room_mention_label">"Згадки про мене в @room"</string>
|
||||
<string name="screen_notification_settings_room_mention_label">"Сповіщати про @room"</string>
|
||||
<string name="screen_notification_settings_system_notifications_action_required">"Щоб отримувати сповіщення, будь ласка, змініть свої %1$s."</string>
|
||||
<string name="screen_notification_settings_system_notifications_action_required_content_link">"системні налаштування"</string>
|
||||
<string name="screen_notification_settings_system_notifications_turned_off">"Системні сповіщення вимкнені"</string>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_advanced_settings_developer_mode">"開發者模式"</string>
|
||||
<string name="screen_advanced_settings_share_presence">"分享動態"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"解除封鎖"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"解除封鎖使用者"</string>
|
||||
<string name="screen_edit_profile_display_name">"顯示名稱"</string>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ dependencies {
|
|||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.sessionStorage.api)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
api(libs.squareup.seismic)
|
||||
api(projects.features.rageshake.api)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import io.element.android.libraries.core.mimetype.MimeTypes
|
|||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.matrix.api.SdkMetadata
|
||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
|
|
@ -57,6 +58,10 @@ import java.io.File
|
|||
import java.io.IOException
|
||||
import java.io.OutputStreamWriter
|
||||
import java.net.HttpURLConnection
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
|
@ -78,6 +83,7 @@ class DefaultBugReporter @Inject constructor(
|
|||
private val sessionStore: SessionStore,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val bugReporterUrlProvider: BugReporterUrlProvider,
|
||||
private val sdkMetadata: SdkMetadata,
|
||||
) : BugReporter {
|
||||
companion object {
|
||||
// filenames
|
||||
|
|
@ -158,6 +164,9 @@ class DefaultBugReporter @Inject constructor(
|
|||
.addFormDataPart("device_id", deviceId)
|
||||
.addFormDataPart("device", Build.MODEL.trim())
|
||||
.addFormDataPart("locale", Locale.getDefault().toString())
|
||||
.addFormDataPart("sdk_sha", sdkMetadata.sdkGitSha)
|
||||
.addFormDataPart("local_time", LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME))
|
||||
.addFormDataPart("utc_time", LocalDateTime.ofInstant(Instant.now(), ZoneOffset.UTC).format(DateTimeFormatter.ISO_DATE_TIME))
|
||||
currentTracingFilter?.let {
|
||||
builder.addFormDataPart("tracing_filter", it)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import com.google.common.truth.Truth.assertThat
|
|||
import io.element.android.features.rageshake.api.reporter.BugReporterListener
|
||||
import io.element.android.features.rageshake.test.crash.FakeCrashDataStore
|
||||
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder
|
||||
import io.element.android.libraries.matrix.test.FakeSdkMetadata
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.network.useragent.DefaultUserAgentProvider
|
||||
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
|
||||
|
|
@ -150,11 +151,12 @@ class DefaultBugReporterTest {
|
|||
userAgentProvider = DefaultUserAgentProvider(buildMeta),
|
||||
sessionStore = InMemorySessionStore(),
|
||||
buildMeta = buildMeta,
|
||||
bugReporterUrlProvider = { server.url("/") }
|
||||
bugReporterUrlProvider = { server.url("/") },
|
||||
sdkMetadata = FakeSdkMetadata("123456789"),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXPECTED_NUMBER_OF_PROGRESS_VALUE = 12
|
||||
private const val EXPECTED_NUMBER_OF_PROGRESS_VALUE = 15
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import io.element.android.features.roomdetails.impl.members.RoomMemberListNode
|
|||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode
|
||||
import io.element.android.features.roomdetails.impl.members.details.avatar.AvatarPreviewNode
|
||||
import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsNode
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsFlowNode
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
|
@ -91,6 +92,9 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
|
||||
@Parcelize
|
||||
data object PollHistory : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object AdminSettings : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
|
|
@ -120,6 +124,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
override fun openPollHistory() {
|
||||
backstack.push(NavTarget.PollHistory)
|
||||
}
|
||||
|
||||
override fun openAdminSettings() {
|
||||
backstack.push(NavTarget.AdminSettings)
|
||||
}
|
||||
}
|
||||
createNode<RoomDetailsNode>(buildContext, listOf(roomDetailsCallback))
|
||||
}
|
||||
|
|
@ -189,6 +197,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
is NavTarget.PollHistory -> {
|
||||
pollHistoryEntryPoint.createNode(this, buildContext)
|
||||
}
|
||||
|
||||
is NavTarget.AdminSettings -> {
|
||||
createNode<RolesAndPermissionsFlowNode>(buildContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
fun openRoomNotificationSettings()
|
||||
fun openAvatarPreview(name: String, url: String)
|
||||
fun openPollHistory()
|
||||
fun openAdminSettings()
|
||||
}
|
||||
|
||||
private val callbacks = plugins<Callback>()
|
||||
|
|
@ -119,6 +120,10 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
callbacks.forEach { it.openAvatarPreview(name, url) }
|
||||
}
|
||||
|
||||
private fun openAdminSettings() {
|
||||
callbacks.forEach { it.openAdminSettings() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val context = LocalContext.current
|
||||
|
|
@ -151,6 +156,7 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
invitePeople = ::invitePeople,
|
||||
openAvatarPreview = ::openAvatarPreview,
|
||||
openPollHistory = ::openPollHistory,
|
||||
openAdminSettings = this::openAdminSettings,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
|
|||
import io.element.android.libraries.matrix.api.room.powerlevels.canSendState
|
||||
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
|
||||
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
|
||||
import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -71,6 +72,7 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
val leaveRoomState = leaveRoomPresenter.present()
|
||||
val canShowNotificationSettings = remember { mutableStateOf(false) }
|
||||
val roomInfo by room.roomInfoFlow.collectAsState(initial = null)
|
||||
val isUserAdmin = room.isOwnUserAdmin()
|
||||
|
||||
val roomAvatar by remember { derivedStateOf { roomInfo?.avatarUrl ?: room.avatarUrl } }
|
||||
|
||||
|
|
@ -78,6 +80,10 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
val roomTopic by remember { derivedStateOf { roomInfo?.topic ?: room.topic } }
|
||||
val isFavorite by remember { derivedStateOf { roomInfo?.isFavorite.orFalse() } }
|
||||
|
||||
val isRoomModerationEnabled by produceState(initialValue = false) {
|
||||
value = featureFlagService.isFeatureEnabled(FeatureFlags.RoomModeration)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
canShowNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings)
|
||||
if (canShowNotificationSettings.value) {
|
||||
|
|
@ -150,6 +156,7 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
leaveRoomState = leaveRoomState,
|
||||
roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(),
|
||||
isFavorite = isFavorite,
|
||||
displayRolesAndPermissionsSettings = isRoomModerationEnabled && !room.isDm && isUserAdmin,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ data class RoomDetailsState(
|
|||
val leaveRoomState: LeaveRoomState,
|
||||
val roomNotificationSettings: RoomNotificationSettings?,
|
||||
val isFavorite: Boolean,
|
||||
val displayRolesAndPermissionsSettings: Boolean,
|
||||
val eventSink: (RoomDetailsEvent) -> Unit
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
|
|||
open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState> {
|
||||
override val values: Sequence<RoomDetailsState>
|
||||
get() = sequenceOf(
|
||||
aRoomDetailsState(),
|
||||
aRoomDetailsState(displayAdminSettings = true),
|
||||
aRoomDetailsState(roomTopic = RoomTopicState.Hidden),
|
||||
aRoomDetailsState(roomTopic = RoomTopicState.CanAddTopic),
|
||||
aRoomDetailsState(isEncrypted = false),
|
||||
|
|
@ -92,6 +92,7 @@ fun aRoomDetailsState(
|
|||
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
|
||||
roomNotificationSettings: RoomNotificationSettings = aRoomNotificationSettings(),
|
||||
isFavorite: Boolean = false,
|
||||
displayAdminSettings: Boolean = false,
|
||||
eventSink: (RoomDetailsEvent) -> Unit = {},
|
||||
) = RoomDetailsState(
|
||||
roomId = roomId,
|
||||
|
|
@ -109,6 +110,7 @@ fun aRoomDetailsState(
|
|||
leaveRoomState = leaveRoomState,
|
||||
roomNotificationSettings = roomNotificationSettings,
|
||||
isFavorite = isFavorite,
|
||||
displayRolesAndPermissionsSettings = displayAdminSettings,
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ fun RoomDetailsView(
|
|||
invitePeople: () -> Unit,
|
||||
openAvatarPreview: (name: String, url: String) -> Unit,
|
||||
openPollHistory: () -> Unit,
|
||||
openAdminSettings: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun onShareMember() {
|
||||
|
|
@ -160,30 +161,45 @@ fun RoomDetailsView(
|
|||
)
|
||||
}
|
||||
|
||||
if (state.canShowNotificationSettings && state.roomNotificationSettings != null) {
|
||||
NotificationSection(
|
||||
isDefaultMode = state.roomNotificationSettings.isDefault,
|
||||
openRoomNotificationSettings = openRoomNotificationSettings
|
||||
PreferenceCategory {
|
||||
if (state.canShowNotificationSettings && state.roomNotificationSettings != null) {
|
||||
NotificationItem(
|
||||
isDefaultMode = state.roomNotificationSettings.isDefault,
|
||||
openRoomNotificationSettings = openRoomNotificationSettings
|
||||
)
|
||||
}
|
||||
|
||||
FavoriteItem(
|
||||
isFavorite = state.isFavorite,
|
||||
onFavoriteChanges = {
|
||||
state.eventSink(RoomDetailsEvent.SetFavorite(it))
|
||||
}
|
||||
)
|
||||
|
||||
if (state.displayRolesAndPermissionsSettings) {
|
||||
ListItem(
|
||||
headlineContent = { Text("Roles and permissions") },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Admin())),
|
||||
onClick = openAdminSettings,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FavoriteSection(
|
||||
isFavorite = state.isFavorite,
|
||||
onFavoriteChanges = {
|
||||
state.eventSink(RoomDetailsEvent.SetFavorite(it))
|
||||
}
|
||||
)
|
||||
|
||||
if (state.roomType is RoomDetailsType.Room) {
|
||||
MembersSection(
|
||||
memberCount = state.memberCount,
|
||||
openRoomMemberList = openRoomMemberList,
|
||||
)
|
||||
|
||||
if (state.canInvite) {
|
||||
InviteSection(
|
||||
invitePeople = invitePeople
|
||||
)
|
||||
val displayMemberListItem = state.roomType is RoomDetailsType.Room
|
||||
val displayInviteMembersItem = state.canInvite
|
||||
if (displayMemberListItem || displayInviteMembersItem) {
|
||||
PreferenceCategory {
|
||||
if (displayMemberListItem) {
|
||||
MembersItem(
|
||||
memberCount = state.memberCount,
|
||||
openRoomMemberList = openRoomMemberList,
|
||||
)
|
||||
}
|
||||
if (displayInviteMembersItem) {
|
||||
InviteItem(
|
||||
invitePeople = invitePeople
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -349,7 +365,7 @@ private fun TopicSection(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationSection(
|
||||
private fun NotificationItem(
|
||||
isDefaultMode: Boolean,
|
||||
openRoomNotificationSettings: () -> Unit,
|
||||
) {
|
||||
|
|
@ -358,58 +374,49 @@ private fun NotificationSection(
|
|||
} else {
|
||||
stringResource(R.string.screen_room_details_notification_mode_custom)
|
||||
}
|
||||
PreferenceCategory {
|
||||
ListItem(
|
||||
headlineContent = { Text(text = stringResource(R.string.screen_room_details_notification_title)) },
|
||||
supportingContent = { Text(text = subtitle) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Notifications())),
|
||||
onClick = openRoomNotificationSettings,
|
||||
)
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = { Text(text = stringResource(R.string.screen_room_details_notification_title)) },
|
||||
supportingContent = { Text(text = subtitle) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Notifications())),
|
||||
onClick = openRoomNotificationSettings,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FavoriteSection(
|
||||
private fun FavoriteItem(
|
||||
isFavorite: Boolean,
|
||||
onFavoriteChanges: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
PreferenceCategory(modifier = modifier) {
|
||||
PreferenceSwitch(
|
||||
icon = CompoundIcons.Favourite(),
|
||||
title = stringResource(id = CommonStrings.common_favourite),
|
||||
isChecked = isFavorite,
|
||||
onCheckedChange = onFavoriteChanges
|
||||
)
|
||||
}
|
||||
PreferenceSwitch(
|
||||
icon = CompoundIcons.Favourite(),
|
||||
title = stringResource(id = CommonStrings.common_favourite),
|
||||
isChecked = isFavorite,
|
||||
onCheckedChange = onFavoriteChanges
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MembersSection(
|
||||
private fun MembersItem(
|
||||
memberCount: Long,
|
||||
openRoomMemberList: () -> Unit,
|
||||
) {
|
||||
PreferenceCategory {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(CommonStrings.common_people)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.User())),
|
||||
trailingContent = ListItemContent.Text(memberCount.toString()),
|
||||
onClick = openRoomMemberList,
|
||||
)
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(CommonStrings.common_people)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.User())),
|
||||
trailingContent = ListItemContent.Text(memberCount.toString()),
|
||||
onClick = openRoomMemberList,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InviteSection(
|
||||
private fun InviteItem(
|
||||
invitePeople: () -> Unit,
|
||||
) {
|
||||
PreferenceCategory {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_details_invite_people_title)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserAdd())),
|
||||
onClick = invitePeople,
|
||||
)
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_details_invite_people_title)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserAdd())),
|
||||
onClick = invitePeople,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -481,5 +488,6 @@ private fun ContentToPreview(state: RoomDetailsState) {
|
|||
invitePeople = {},
|
||||
openAvatarPreview = { _, _ -> },
|
||||
openPollHistory = {},
|
||||
openAdminSettings = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class RoomInviteMembersPresenter @Inject constructor(
|
|||
val searchResults = remember { mutableStateOf<SearchBarResultState<ImmutableList<InvitableUser>>>(SearchBarResultState.Initial()) }
|
||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
var searchActive by rememberSaveable { mutableStateOf(false) }
|
||||
var showSearchLoader = rememberSaveable { mutableStateOf(false) }
|
||||
val showSearchLoader = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
fetchMembers(roomMembers)
|
||||
|
|
@ -99,7 +99,7 @@ class RoomInviteMembersPresenter @Inject constructor(
|
|||
@JvmName("toggleUserInSelectedUsers")
|
||||
private fun MutableState<ImmutableList<MatrixUser>>.toggleUser(user: MatrixUser) {
|
||||
value = if (value.contains(user)) {
|
||||
value.filterNot { it == user }
|
||||
value.filterNot { it.userId == user.userId }
|
||||
} else {
|
||||
value + user
|
||||
}.toImmutableList()
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
|||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.CheckableUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.CheckableUserRowData
|
||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.matrix.ui.model.getBestName
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
|
@ -97,7 +97,7 @@ fun RoomInviteMembersView(
|
|||
)
|
||||
|
||||
if (!state.isSearchActive) {
|
||||
SelectedUsersList(
|
||||
SelectedUsersRowList(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
selectedUsers = state.selectedUsers,
|
||||
autoScroll = true,
|
||||
|
|
@ -157,7 +157,7 @@ private fun RoomInviteMembersSearchBar(
|
|||
placeHolderTitle = placeHolderTitle,
|
||||
contentPrefix = {
|
||||
if (selectedUsers.isNotEmpty()) {
|
||||
SelectedUsersList(
|
||||
SelectedUsersRowList(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
selectedUsers = selectedUsers,
|
||||
autoScroll = true,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,10 @@
|
|||
|
||||
package io.element.android.features.roomdetails.impl.members
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
||||
sealed interface RoomMemberListEvents {
|
||||
data class UpdateSearchQuery(val query: String) : RoomMemberListEvents
|
||||
data class OnSearchActiveChanged(val active: Boolean) : RoomMemberListEvents
|
||||
data class RoomMemberSelected(val roomMember: RoomMember) : RoomMemberListEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,15 +35,16 @@ import io.element.android.services.analytics.api.AnalyticsService
|
|||
class RoomMemberListNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: RoomMemberListPresenter,
|
||||
presenterFactory: RoomMemberListPresenter.Factory,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
) : Node(buildContext, plugins = plugins), RoomMemberListNavigator {
|
||||
interface Callback : Plugin {
|
||||
fun openRoomMemberDetails(roomMemberId: UserId)
|
||||
fun openInviteMembers()
|
||||
}
|
||||
|
||||
private val callbacks = plugins<Callback>()
|
||||
private val presenter = presenterFactory.create(this)
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
|
|
@ -53,27 +54,35 @@ class RoomMemberListNode @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun openRoomMemberDetails(roomMemberId: UserId) {
|
||||
override fun openRoomMemberDetails(roomMemberId: UserId) {
|
||||
callbacks.forEach {
|
||||
it.openRoomMemberDetails(roomMemberId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openInviteMembers() {
|
||||
override fun openInviteMembers() {
|
||||
callbacks.forEach {
|
||||
it.openInviteMembers()
|
||||
}
|
||||
}
|
||||
|
||||
override fun exitRoomMemberList() {
|
||||
navigateUp()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
RoomMemberListView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onBackPressed = { navigateUp() },
|
||||
onMemberSelected = this::openRoomMemberDetails,
|
||||
onInvitePressed = this::openInviteMembers,
|
||||
navigator = this,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface RoomMemberListNavigator {
|
||||
fun exitRoomMemberList() {}
|
||||
fun openRoomMemberDetails(roomMemberId: UserId) {}
|
||||
fun openInviteMembers() {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,32 +23,45 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationPresenter
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
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.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
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.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.roomMembers
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomMemberListPresenter @Inject constructor(
|
||||
class RoomMemberListPresenter @AssistedInject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val roomMemberListDataSource: RoomMemberListDataSource,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val roomMembersModerationPresenter: RoomMembersModerationPresenter,
|
||||
@Assisted private val navigator: RoomMemberListNavigator,
|
||||
) : Presenter<RoomMemberListState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(navigator: RoomMemberListNavigator): RoomMemberListPresenter
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomMemberListState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var roomMembers by remember { mutableStateOf<AsyncData<RoomMembers>>(AsyncData.Loading()) }
|
||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
var searchResults by remember {
|
||||
|
|
@ -61,18 +74,14 @@ class RoomMemberListPresenter @Inject constructor(
|
|||
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
|
||||
}
|
||||
val isRoomModerationEnabled by produceState(initialValue = false) {
|
||||
value = featureFlagService.isFeatureEnabled(FeatureFlags.RoomModeration)
|
||||
}
|
||||
|
||||
val roomModerationState = if (isRoomModerationEnabled) {
|
||||
roomMembersModerationPresenter.present()
|
||||
} else {
|
||||
remember { roomMembersModerationPresenter.dummyState() }
|
||||
}
|
||||
|
||||
LaunchedEffect(membersState) {
|
||||
|
|
@ -116,19 +125,28 @@ class RoomMemberListPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun handleEvents(event: RoomMemberListEvents) {
|
||||
when (event) {
|
||||
is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active
|
||||
is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query
|
||||
is RoomMemberListEvents.RoomMemberSelected -> coroutineScope.launch {
|
||||
if (roomMembersModerationPresenter.canDisplayModerationActions()) {
|
||||
roomModerationState.eventSink(RoomMembersModerationEvents.SelectRoomMember(event.roomMember))
|
||||
} else {
|
||||
navigator.openRoomMemberDetails(event.roomMember.userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RoomMemberListState(
|
||||
roomMembers = roomMembers,
|
||||
searchQuery = searchQuery,
|
||||
searchResults = searchResults,
|
||||
isSearchActive = isSearchActive,
|
||||
canInvite = canInvite,
|
||||
canDisplayBannedUsers = canDisplayBannedUsers,
|
||||
eventSink = { event ->
|
||||
when (event) {
|
||||
is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active
|
||||
is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query
|
||||
}
|
||||
},
|
||||
moderationState = roomModerationState,
|
||||
eventSink = { handleEvents(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.features.roomdetails.impl.members
|
||||
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
|
@ -27,7 +28,7 @@ data class RoomMemberListState(
|
|||
val searchResults: SearchBarResultState<RoomMembers>,
|
||||
val isSearchActive: Boolean,
|
||||
val canInvite: Boolean,
|
||||
val canDisplayBannedUsers: Boolean,
|
||||
val moderationState: RoomMembersModerationState,
|
||||
val eventSink: (RoomMemberListEvents) -> Unit,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
package io.element.android.features.roomdetails.impl.members
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.aRoomMembersModerationState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
|
@ -57,30 +59,20 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMember
|
|||
searchQuery = "something-with-no-results",
|
||||
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(
|
||||
roomMembers: AsyncData<RoomMembers> = AsyncData.Uninitialized,
|
||||
searchResults: SearchBarResultState<RoomMembers> = SearchBarResultState.Initial(),
|
||||
canDisplayBannedUsers: Boolean = false,
|
||||
moderationState: RoomMembersModerationState = aRoomMembersModerationState(),
|
||||
) = RoomMemberListState(
|
||||
roomMembers = roomMembers,
|
||||
searchQuery = "",
|
||||
searchResults = searchResults,
|
||||
isSearchActive = false,
|
||||
canInvite = false,
|
||||
canDisplayBannedUsers = canDisplayBannedUsers,
|
||||
moderationState = moderationState,
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -46,6 +47,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.roomdetails.impl.R
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationView
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.aRoomMembersModerationState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
|
|
@ -76,14 +79,12 @@ private enum class SelectedSection {
|
|||
@Composable
|
||||
fun RoomMemberListView(
|
||||
state: RoomMemberListState,
|
||||
onBackPressed: () -> Unit,
|
||||
onInvitePressed: () -> Unit,
|
||||
onMemberSelected: (UserId) -> Unit,
|
||||
navigator: RoomMemberListNavigator,
|
||||
modifier: Modifier = Modifier,
|
||||
initialSelectedSectionIndex: Int = 0,
|
||||
) {
|
||||
fun onUserSelected(roomMember: RoomMember) {
|
||||
onMemberSelected(roomMember.userId)
|
||||
state.eventSink(RoomMemberListEvents.RoomMemberSelected(roomMember))
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
|
|
@ -92,13 +93,18 @@ fun RoomMemberListView(
|
|||
if (!state.isSearchActive) {
|
||||
RoomMemberListTopBar(
|
||||
canInvite = state.canInvite,
|
||||
onBackPressed = onBackPressed,
|
||||
onInvitePressed = onInvitePressed,
|
||||
onBackPressed = navigator::exitRoomMemberList,
|
||||
onInvitePressed = navigator::openInviteMembers,
|
||||
)
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
var selectedSection by remember { mutableStateOf(SelectedSection.entries[initialSelectedSectionIndex]) }
|
||||
if (!state.moderationState.canDisplayBannedUsers && selectedSection == SelectedSection.BANNED) {
|
||||
SideEffect {
|
||||
selectedSection = SelectedSection.MEMBERS
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -123,7 +129,7 @@ fun RoomMemberListView(
|
|||
RoomMemberList(
|
||||
roomMembers = state.roomMembers.data,
|
||||
showMembersCount = true,
|
||||
canDisplayBannedUsersControls = state.canDisplayBannedUsers,
|
||||
canDisplayBannedUsersControls = state.moderationState.canDisplayBannedUsers,
|
||||
selectedSection = selectedSection,
|
||||
onSelectedSectionChanged = { selectedSection = it },
|
||||
onUserSelected = ::onUserSelected,
|
||||
|
|
@ -136,6 +142,11 @@ fun RoomMemberListView(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
RoomMembersModerationView(
|
||||
state = state.moderationState,
|
||||
onDisplayMemberProfile = navigator::openRoomMemberDetails
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
|
|
@ -328,9 +339,7 @@ private fun RoomMemberSearchBar(
|
|||
internal fun RoomMemberListPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) = ElementPreview {
|
||||
RoomMemberListView(
|
||||
state = state,
|
||||
onBackPressed = {},
|
||||
onMemberSelected = {},
|
||||
onInvitePressed = {},
|
||||
navigator = object : RoomMemberListNavigator {},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -351,10 +360,8 @@ internal fun RoomMemberBannedListPreview() = ElementPreview {
|
|||
),
|
||||
)
|
||||
),
|
||||
canDisplayBannedUsers = true,
|
||||
moderationState = aRoomMembersModerationState(canDisplayBannedUsers = true),
|
||||
),
|
||||
onBackPressed = {},
|
||||
onMemberSelected = {},
|
||||
onInvitePressed = {},
|
||||
navigator = object : RoomMemberListNavigator {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* 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.features.roomdetails.impl.members.moderation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.extensions.finally
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canBan
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canKick
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(RoomScope::class)
|
||||
class DefaultRoomMembersModerationPresenter @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : RoomMembersModerationPresenter {
|
||||
private var selectedMember by mutableStateOf<RoomMember?>(null)
|
||||
|
||||
private suspend fun canBan() = room.canBan().getOrDefault(false)
|
||||
private suspend fun canKick() = room.canKick().getOrDefault(false)
|
||||
|
||||
override suspend fun canDisplayModerationActions(): Boolean {
|
||||
val isRoomModerationEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.RoomModeration)
|
||||
val isDm = room.isDm && room.isEncrypted
|
||||
return isRoomModerationEnabled && !isDm && (canBan() || canKick())
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomMembersModerationState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var moderationActions by remember { mutableStateOf(persistentListOf<ModerationAction>()) }
|
||||
|
||||
val kickUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
|
||||
val banUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
|
||||
val unbanUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
|
||||
|
||||
val canDisplayBannedUsers by produceState(initialValue = false) {
|
||||
value = featureFlagService.isFeatureEnabled(FeatureFlags.RoomModeration) && !room.isDm && canBan()
|
||||
}
|
||||
|
||||
fun handleEvent(event: RoomMembersModerationEvents) {
|
||||
when (event) {
|
||||
is RoomMembersModerationEvents.SelectRoomMember -> {
|
||||
coroutineScope.launch {
|
||||
selectedMember = event.roomMember
|
||||
if (event.roomMember.membership == RoomMembershipState.BAN && canBan()) {
|
||||
unbanUserAsyncAction.value = AsyncAction.Confirming
|
||||
} else {
|
||||
moderationActions = buildList {
|
||||
add(ModerationAction.DisplayProfile(event.roomMember.userId))
|
||||
val currentUserMemberPowerLevel = room.userRole(room.sessionId).getOrDefault(RoomMember.Role.USER).powerLevel
|
||||
if (currentUserMemberPowerLevel > event.roomMember.powerLevel) {
|
||||
if (canKick()) {
|
||||
add(ModerationAction.KickUser(event.roomMember.userId))
|
||||
}
|
||||
if (canBan()) {
|
||||
add(ModerationAction.BanUser(event.roomMember.userId))
|
||||
}
|
||||
}
|
||||
}.toPersistentList()
|
||||
}
|
||||
}
|
||||
}
|
||||
is RoomMembersModerationEvents.KickUser -> {
|
||||
moderationActions = persistentListOf()
|
||||
selectedMember?.let {
|
||||
coroutineScope.kickUser(it.userId, kickUserAsyncAction)
|
||||
}
|
||||
}
|
||||
is RoomMembersModerationEvents.BanUser -> {
|
||||
if (banUserAsyncAction.value.isConfirming()) {
|
||||
moderationActions = persistentListOf()
|
||||
selectedMember?.let {
|
||||
coroutineScope.banUser(it.userId, banUserAsyncAction)
|
||||
}
|
||||
} else {
|
||||
banUserAsyncAction.value = AsyncAction.Confirming
|
||||
}
|
||||
}
|
||||
is RoomMembersModerationEvents.UnbanUser -> {
|
||||
if (unbanUserAsyncAction.value.isConfirming()) {
|
||||
moderationActions = persistentListOf()
|
||||
selectedMember?.let {
|
||||
coroutineScope.unbanUser(it.userId, unbanUserAsyncAction)
|
||||
}
|
||||
} else {
|
||||
unbanUserAsyncAction.value = AsyncAction.Confirming
|
||||
}
|
||||
}
|
||||
is RoomMembersModerationEvents.Reset -> {
|
||||
selectedMember = null
|
||||
moderationActions = persistentListOf()
|
||||
kickUserAsyncAction.value = AsyncAction.Uninitialized
|
||||
banUserAsyncAction.value = AsyncAction.Uninitialized
|
||||
unbanUserAsyncAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RoomMembersModerationState(
|
||||
selectedRoomMember = selectedMember,
|
||||
actions = moderationActions,
|
||||
kickUserAsyncAction = kickUserAsyncAction.value,
|
||||
banUserAsyncAction = banUserAsyncAction.value,
|
||||
unbanUserAsyncAction = unbanUserAsyncAction.value,
|
||||
canDisplayBannedUsers = canDisplayBannedUsers,
|
||||
eventSink = { handleEvent(it) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.kickUser(
|
||||
userId: UserId,
|
||||
kickUserAction: MutableState<AsyncAction<Unit>>,
|
||||
) = runActionAndWaitForMembershipChange(kickUserAction) {
|
||||
room.kickUser(userId).finally { selectedMember = null }
|
||||
}
|
||||
|
||||
private fun CoroutineScope.banUser(
|
||||
userId: UserId,
|
||||
banUserAction: MutableState<AsyncAction<Unit>>,
|
||||
) = runActionAndWaitForMembershipChange(banUserAction) {
|
||||
room.banUser(userId).finally { selectedMember = null }
|
||||
}
|
||||
|
||||
private fun CoroutineScope.unbanUser(
|
||||
userId: UserId,
|
||||
unbanUserAction: MutableState<AsyncAction<Unit>>,
|
||||
) = runActionAndWaitForMembershipChange(unbanUserAction) {
|
||||
room.unbanUser(userId).finally { selectedMember = null }
|
||||
}
|
||||
|
||||
private fun <T> CoroutineScope.runActionAndWaitForMembershipChange(action: MutableState<AsyncAction<T>>, block: suspend () -> Result<T>) {
|
||||
launch(dispatchers.io) {
|
||||
action.runUpdatingState {
|
||||
val result = block()
|
||||
if (result.isSuccess) {
|
||||
room.membersStateFlow.drop(1).take(1)
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.features.roomdetails.impl.members.moderation
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
||||
sealed interface RoomMembersModerationEvents {
|
||||
data class SelectRoomMember(val roomMember: RoomMember) : RoomMembersModerationEvents
|
||||
data object KickUser : RoomMembersModerationEvents
|
||||
data object BanUser : RoomMembersModerationEvents
|
||||
data object UnbanUser : RoomMembersModerationEvents
|
||||
data object Reset : RoomMembersModerationEvents
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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.features.roomdetails.impl.members.moderation
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
interface RoomMembersModerationPresenter : Presenter<RoomMembersModerationState> {
|
||||
suspend fun canDisplayModerationActions(): Boolean
|
||||
|
||||
fun dummyState() = RoomMembersModerationState(
|
||||
selectedRoomMember = null,
|
||||
actions = persistentListOf(),
|
||||
kickUserAsyncAction = AsyncAction.Uninitialized,
|
||||
banUserAsyncAction = AsyncAction.Uninitialized,
|
||||
unbanUserAsyncAction = AsyncAction.Uninitialized,
|
||||
canDisplayBannedUsers = false,
|
||||
eventSink = {}
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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.features.roomdetails.impl.members.moderation
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class RoomMembersModerationState(
|
||||
val selectedRoomMember: RoomMember?,
|
||||
val actions: ImmutableList<ModerationAction>,
|
||||
val kickUserAsyncAction: AsyncAction<Unit>,
|
||||
val banUserAsyncAction: AsyncAction<Unit>,
|
||||
val unbanUserAsyncAction: AsyncAction<Unit>,
|
||||
val canDisplayBannedUsers: Boolean,
|
||||
val eventSink: (RoomMembersModerationEvents) -> Unit,
|
||||
)
|
||||
|
||||
sealed interface ModerationAction {
|
||||
data class DisplayProfile(val userId: UserId) : ModerationAction
|
||||
data class KickUser(val userId: UserId) : ModerationAction
|
||||
data class BanUser(val userId: UserId) : ModerationAction
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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.features.roomdetails.impl.members.moderation
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.roomdetails.impl.members.anAlice
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
class RoomMembersModerationStatePreviewProvider : PreviewParameterProvider<RoomMembersModerationState> {
|
||||
override val values: Sequence<RoomMembersModerationState>
|
||||
get() = sequenceOf(
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = anAlice(),
|
||||
actions = listOf(
|
||||
ModerationAction.DisplayProfile(anAlice().userId),
|
||||
),
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = anAlice(),
|
||||
actions = listOf(
|
||||
ModerationAction.DisplayProfile(anAlice().userId),
|
||||
ModerationAction.KickUser(userId = anAlice().userId),
|
||||
),
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = anAlice(),
|
||||
actions = listOf(
|
||||
ModerationAction.DisplayProfile(anAlice().userId),
|
||||
ModerationAction.KickUser(userId = anAlice().userId),
|
||||
ModerationAction.BanUser(userId = anAlice().userId),
|
||||
),
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = anAlice(),
|
||||
kickUserAsyncAction = AsyncAction.Loading,
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = anAlice(),
|
||||
banUserAsyncAction = AsyncAction.Loading,
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = anAlice(),
|
||||
unbanUserAsyncAction = AsyncAction.Loading,
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
kickUserAsyncAction = AsyncAction.Failure(Exception("Failed to kick user")),
|
||||
banUserAsyncAction = AsyncAction.Failure(Exception("Failed to ban user")),
|
||||
unbanUserAsyncAction = AsyncAction.Failure(Exception("Failed to unban user")),
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = anAlice(),
|
||||
banUserAsyncAction = AsyncAction.Confirming,
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = anAlice(),
|
||||
unbanUserAsyncAction = AsyncAction.Confirming,
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
kickUserAsyncAction = AsyncAction.Success(Unit),
|
||||
banUserAsyncAction = AsyncAction.Success(Unit),
|
||||
unbanUserAsyncAction = AsyncAction.Success(Unit),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aRoomMembersModerationState(
|
||||
selectedRoomMember: RoomMember? = null,
|
||||
actions: List<ModerationAction> = emptyList(),
|
||||
kickUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
banUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
unbanUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
canDisplayBannedUsers: Boolean = false,
|
||||
eventSink: (RoomMembersModerationEvents) -> Unit = {},
|
||||
) = RoomMembersModerationState(
|
||||
selectedRoomMember = selectedRoomMember,
|
||||
actions = actions.toPersistentList(),
|
||||
kickUserAsyncAction = kickUserAsyncAction,
|
||||
banUserAsyncAction = banUserAsyncAction,
|
||||
unbanUserAsyncAction = unbanUserAsyncAction,
|
||||
canDisplayBannedUsers = canDisplayBannedUsers,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
@ -0,0 +1,316 @@
|
|||
/*
|
||||
* 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.features.roomdetails.impl.members.moderation
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.roomdetails.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncIndicator
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
|
||||
import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
|
||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.getBestName
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@Composable
|
||||
fun RoomMembersModerationView(
|
||||
state: RoomMembersModerationState,
|
||||
onDisplayMemberProfile: (UserId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
if (state.actions.isNotEmpty()) {
|
||||
RoomMemberActionsBottomSheet(
|
||||
roomMember = state.selectedRoomMember,
|
||||
actions = state.actions,
|
||||
onActionSelected = { action ->
|
||||
when (action) {
|
||||
is ModerationAction.DisplayProfile -> {
|
||||
onDisplayMemberProfile(action.userId)
|
||||
}
|
||||
is ModerationAction.KickUser -> {
|
||||
state.eventSink(RoomMembersModerationEvents.KickUser)
|
||||
}
|
||||
is ModerationAction.BanUser -> {
|
||||
state.eventSink(RoomMembersModerationEvents.BanUser)
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismiss = { state.eventSink(RoomMembersModerationEvents.Reset) },
|
||||
)
|
||||
}
|
||||
|
||||
val asyncIndicatorState = rememberAsyncIndicatorState()
|
||||
AsyncIndicatorHost(modifier = Modifier.statusBarsPadding(), state = asyncIndicatorState)
|
||||
|
||||
when (val action = state.kickUserAsyncAction) {
|
||||
is AsyncAction.Loading -> {
|
||||
LaunchedEffect(action) {
|
||||
val userDisplayName = state.selectedRoomMember?.getBestName().orEmpty()
|
||||
asyncIndicatorState.enqueue {
|
||||
AsyncIndicator.Loading(text = stringResource(R.string.screen_room_member_list_removing_user, userDisplayName))
|
||||
}
|
||||
}
|
||||
}
|
||||
is AsyncAction.Failure -> {
|
||||
Timber.e(action.error, "Failed to kick user.")
|
||||
LaunchedEffect(action) {
|
||||
asyncIndicatorState.enqueue(AsyncIndicator.DURATION_SHORT) {
|
||||
AsyncIndicator.Failure(
|
||||
text = stringResource(CommonStrings.common_failed),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is AsyncAction.Success -> {
|
||||
LaunchedEffect(action) { asyncIndicatorState.clear() }
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
when (val action = state.banUserAsyncAction) {
|
||||
is AsyncAction.Confirming -> {
|
||||
ConfirmationDialog(
|
||||
title = stringResource(R.string.screen_room_member_list_ban_member_confirmation_title),
|
||||
content = stringResource(R.string.screen_room_member_list_ban_member_confirmation_description),
|
||||
submitText = stringResource(R.string.screen_room_member_list_ban_member_confirmation_action),
|
||||
onSubmitClicked = { state.selectedRoomMember?.userId?.let { state.eventSink(RoomMembersModerationEvents.BanUser) } },
|
||||
onDismiss = { state.eventSink(RoomMembersModerationEvents.Reset) }
|
||||
)
|
||||
}
|
||||
is AsyncAction.Loading -> {
|
||||
LaunchedEffect(action) {
|
||||
val userDisplayName = state.selectedRoomMember?.getBestName().orEmpty()
|
||||
asyncIndicatorState.enqueue {
|
||||
AsyncIndicator.Loading(text = stringResource(R.string.screen_room_member_list_banning_user, userDisplayName))
|
||||
}
|
||||
}
|
||||
}
|
||||
is AsyncAction.Failure -> {
|
||||
Timber.e(action.error, "Failed to ban user.")
|
||||
LaunchedEffect(action) {
|
||||
asyncIndicatorState.enqueue(AsyncIndicator.DURATION_SHORT) {
|
||||
AsyncIndicator.Failure(
|
||||
text = stringResource(CommonStrings.common_failed),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is AsyncAction.Success -> {
|
||||
LaunchedEffect(action) { asyncIndicatorState.clear() }
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
when (val action = state.unbanUserAsyncAction) {
|
||||
is AsyncAction.Confirming -> {
|
||||
state.selectedRoomMember?.let {
|
||||
ConfirmationDialog(
|
||||
title = stringResource(R.string.screen_room_member_list_manage_member_unban_title),
|
||||
content = stringResource(R.string.screen_room_member_list_manage_member_unban_message),
|
||||
submitText = stringResource(R.string.screen_room_member_list_manage_member_unban_action),
|
||||
onSubmitClicked = { state.eventSink(RoomMembersModerationEvents.UnbanUser) },
|
||||
onDismiss = { state.eventSink(RoomMembersModerationEvents.Reset) },
|
||||
)
|
||||
}
|
||||
}
|
||||
is AsyncAction.Loading -> {
|
||||
LaunchedEffect(action) {
|
||||
val userDisplayName = state.selectedRoomMember?.getBestName().orEmpty()
|
||||
asyncIndicatorState.enqueue {
|
||||
AsyncIndicator.Loading(text = stringResource(R.string.screen_room_member_list_unbanning_user, userDisplayName))
|
||||
}
|
||||
}
|
||||
}
|
||||
is AsyncAction.Failure -> {
|
||||
Timber.e(action.error, "Failed to unban user.")
|
||||
LaunchedEffect(action) {
|
||||
asyncIndicatorState.enqueue(AsyncIndicator.DURATION_SHORT) {
|
||||
AsyncIndicator.Failure(
|
||||
text = stringResource(CommonStrings.common_failed),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is AsyncAction.Success -> {
|
||||
LaunchedEffect(action) { asyncIndicatorState.clear() }
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun RoomMemberActionsBottomSheet(
|
||||
roomMember: RoomMember?,
|
||||
actions: ImmutableList<ModerationAction>,
|
||||
onActionSelected: (ModerationAction) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
if (roomMember != null && actions.isNotEmpty()) {
|
||||
val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
ModalBottomSheet(
|
||||
modifier = Modifier.systemBarsPadding(),
|
||||
sheetState = bottomSheetState,
|
||||
onDismissRequest = {
|
||||
coroutineScope.launch {
|
||||
bottomSheetState.hide()
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 16.dp)
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = AvatarData(
|
||||
id = roomMember.userId.value,
|
||||
name = roomMember.displayName,
|
||||
url = roomMember.avatarUrl,
|
||||
size = AvatarSize.RoomListManageUser,
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(bottom = 28.dp)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
roomMember.displayName?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = ElementTheme.typography.fontHeadingLgBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = roomMember.userId.toString(),
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
for (action in actions) {
|
||||
when (action) {
|
||||
is ModerationAction.DisplayProfile -> {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_member_list_manage_member_user_info)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Info())),
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
onActionSelected(action)
|
||||
bottomSheetState.hide()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
is ModerationAction.KickUser -> {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_member_list_manage_member_remove)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())),
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
bottomSheetState.hide()
|
||||
onActionSelected(action)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
is ModerationAction.BanUser -> {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_member_list_manage_member_remove_confirmation_ban)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())),
|
||||
style = ListItemStyle.Destructive,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
bottomSheetState.hide()
|
||||
onActionSelected(action)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun RoomMembersModerationViewPreview(@PreviewParameter(RoomMembersModerationStatePreviewProvider::class) state: RoomMembersModerationState) {
|
||||
ElementPreview {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 64.dp)
|
||||
) {
|
||||
RoomMembersModerationView(
|
||||
state = state,
|
||||
onDisplayMemberProfile = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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.features.roomdetails.impl.rolesandpermissions
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
||||
sealed interface RolesAndPermissionsEvents {
|
||||
data object ChangeOwnRole : RolesAndPermissionsEvents
|
||||
data class DemoteSelfTo(val role: RoomMember.Role) : RolesAndPermissionsEvents
|
||||
data object CancelPendingAction : RolesAndPermissionsEvents
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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.features.roomdetails.impl.rolesandpermissions
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesNode
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class RolesAndPermissionsFlowNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
) : BaseFlowNode<RolesAndPermissionsFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.AdminSettings,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
) {
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
data object AdminSettings : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object AdminList : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object ModeratorList : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
is NavTarget.AdminSettings -> {
|
||||
val callback = object : RolesAndPermissionsNode.Callback {
|
||||
override fun openAdminList() {
|
||||
backstack.push(NavTarget.AdminList)
|
||||
}
|
||||
|
||||
override fun openModeratorList() {
|
||||
backstack.push(NavTarget.ModeratorList)
|
||||
}
|
||||
}
|
||||
createNode<RolesAndPermissionsNode>(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(callback),
|
||||
)
|
||||
}
|
||||
is NavTarget.AdminList -> {
|
||||
val inputs = ChangeRolesNode.Inputs(ChangeRolesNode.ListType.Admins)
|
||||
createNode<ChangeRolesNode>(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(inputs),
|
||||
)
|
||||
}
|
||||
is NavTarget.ModeratorList -> {
|
||||
val inputs = ChangeRolesNode.Inputs(ChangeRolesNode.ListType.Moderators)
|
||||
createNode<ChangeRolesNode>(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(inputs),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
BackstackView()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdetails.impl.rolesandpermissions
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class RolesAndPermissionsNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: RolesAndPermissionsPresenter,
|
||||
private val room: MatrixRoom,
|
||||
) : Node(buildContext, plugins = plugins), RoomDetailsAdminSettingsNavigator {
|
||||
interface Callback : Plugin {
|
||||
fun openAdminList()
|
||||
fun openModeratorList()
|
||||
}
|
||||
|
||||
private val callback = plugins<Callback>().first()
|
||||
|
||||
override fun onBackPressed() = navigateUp()
|
||||
override fun openAdminList() = callback.openAdminList()
|
||||
override fun openModeratorList() = callback.openModeratorList()
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
|
||||
// Reload members when the user sees this screen
|
||||
lifecycle.addObserver(object : LifecycleEventObserver {
|
||||
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
lifecycleScope.launch { room.updateMembers() }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// If the user is not an admin anymore, exit this section since they won't have permissions to use it
|
||||
lifecycleScope.launch {
|
||||
room.membersStateFlow
|
||||
.map { state ->
|
||||
state.roomMembers().orEmpty().find { it.userId == room.sessionId }
|
||||
}
|
||||
.filter { it?.role != RoomMember.Role.ADMIN }
|
||||
.take(1)
|
||||
.onEach { navigateUp() }
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
RolesAndPermissionsView(
|
||||
state = state,
|
||||
roomDetailsAdminSettingsNavigator = this,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface RoomDetailsAdminSettingsNavigator {
|
||||
fun onBackPressed() {}
|
||||
fun openAdminList() {}
|
||||
fun openModeratorList() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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.features.roomdetails.impl.rolesandpermissions
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class RolesAndPermissionsPresenter @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : Presenter<RolesAndPermissionsState> {
|
||||
@Composable
|
||||
override fun present(): RolesAndPermissionsState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val roomInfo by room.roomInfoFlow.collectAsState(initial = null)
|
||||
val moderatorCount by remember {
|
||||
derivedStateOf {
|
||||
roomInfo.userCountWithRole(RoomMember.Role.MODERATOR)
|
||||
}
|
||||
}
|
||||
val adminCount by remember {
|
||||
derivedStateOf {
|
||||
roomInfo.userCountWithRole(RoomMember.Role.ADMIN)
|
||||
}
|
||||
}
|
||||
val changeOwnRoleAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
|
||||
fun handleEvent(event: RolesAndPermissionsEvents) {
|
||||
when (event) {
|
||||
is RolesAndPermissionsEvents.ChangeOwnRole -> {
|
||||
changeOwnRoleAction.value = AsyncAction.Confirming
|
||||
}
|
||||
is RolesAndPermissionsEvents.CancelPendingAction -> {
|
||||
changeOwnRoleAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
is RolesAndPermissionsEvents.DemoteSelfTo -> coroutineScope.demoteSelfTo(
|
||||
role = event.role,
|
||||
changeOwnRoleAction = changeOwnRoleAction,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return RolesAndPermissionsState(
|
||||
adminCount = adminCount,
|
||||
moderatorCount = moderatorCount,
|
||||
changeOwnRoleAction = changeOwnRoleAction.value,
|
||||
eventSink = { handleEvent(it) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.demoteSelfTo(
|
||||
role: RoomMember.Role,
|
||||
changeOwnRoleAction: MutableState<AsyncAction<Unit>>,
|
||||
) = launch(dispatchers.io) {
|
||||
runUpdatingState(changeOwnRoleAction) {
|
||||
room.updateUsersRoles(listOf(UserRoleChange(room.sessionId, role)))
|
||||
}
|
||||
}
|
||||
|
||||
private fun MatrixRoomInfo?.userCountWithRole(role: RoomMember.Role): Int {
|
||||
return if (this != null) {
|
||||
userPowerLevels.count { (_, level) -> RoomMember.Role.forPowerLevel(level) == role }
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.features.roomdetails.impl.rolesandpermissions
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
|
||||
data class RolesAndPermissionsState(
|
||||
val adminCount: Int,
|
||||
val moderatorCount: Int,
|
||||
val changeOwnRoleAction: AsyncAction<Unit>,
|
||||
val eventSink: (RolesAndPermissionsEvents) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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.features.roomdetails.impl.rolesandpermissions
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
|
||||
class RolesAndPermissionsStateProvider : PreviewParameterProvider<RolesAndPermissionsState> {
|
||||
override val values: Sequence<RolesAndPermissionsState>
|
||||
get() = sequenceOf(
|
||||
aRolesAndPermissionsState(),
|
||||
aRolesAndPermissionsState(adminCount = 1, moderatorCount = 2),
|
||||
aRolesAndPermissionsState(
|
||||
adminCount = 1,
|
||||
moderatorCount = 2,
|
||||
changeOwnRoleAction = AsyncAction.Confirming,
|
||||
),
|
||||
aRolesAndPermissionsState(
|
||||
adminCount = 1,
|
||||
moderatorCount = 2,
|
||||
changeOwnRoleAction = AsyncAction.Loading,
|
||||
),
|
||||
aRolesAndPermissionsState(
|
||||
adminCount = 1,
|
||||
moderatorCount = 2,
|
||||
changeOwnRoleAction = AsyncAction.Failure(IllegalStateException("Failed to change role")),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aRolesAndPermissionsState(
|
||||
adminCount: Int = 0,
|
||||
moderatorCount: Int = 0,
|
||||
changeOwnRoleAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
eventSink: (RolesAndPermissionsEvents) -> Unit = {},
|
||||
) = RolesAndPermissionsState(
|
||||
adminCount = adminCount,
|
||||
moderatorCount = moderatorCount,
|
||||
changeOwnRoleAction = changeOwnRoleAction,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* 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.features.roomdetails.impl.rolesandpermissions
|
||||
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.roomdetails.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.preview.sheetStateForPreview
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
|
||||
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
|
||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.hide
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun RolesAndPermissionsView(
|
||||
state: RolesAndPermissionsState,
|
||||
roomDetailsAdminSettingsNavigator: RoomDetailsAdminSettingsNavigator,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
PreferencePage(
|
||||
modifier = modifier,
|
||||
title = stringResource(R.string.screen_room_roles_and_permissions_title),
|
||||
onBackPressed = roomDetailsAdminSettingsNavigator::onBackPressed,
|
||||
) {
|
||||
ListSectionHeader(title = stringResource(R.string.screen_room_roles_and_permissions_roles_header), hasDivider = false)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_admins)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Admin())),
|
||||
trailingContent = ListItemContent.Text("${state.adminCount}"),
|
||||
onClick = { roomDetailsAdminSettingsNavigator.openAdminList() },
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_moderators)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChatProblem())),
|
||||
trailingContent = ListItemContent.Text("${state.moderatorCount}"),
|
||||
onClick = { roomDetailsAdminSettingsNavigator.openModeratorList() },
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_my_role)) },
|
||||
onClick = { state.eventSink(RolesAndPermissionsEvents.ChangeOwnRole) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Edit()))
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
when (state.changeOwnRoleAction) {
|
||||
is AsyncAction.Confirming -> {
|
||||
ChangeOwnRoleBottomSheet(
|
||||
eventSink = state.eventSink,
|
||||
)
|
||||
}
|
||||
is AsyncAction.Loading -> {
|
||||
ProgressDialog()
|
||||
}
|
||||
is AsyncAction.Failure -> {
|
||||
ErrorDialog(
|
||||
content = stringResource(CommonStrings.error_unknown),
|
||||
onDismiss = { state.eventSink(RolesAndPermissionsEvents.CancelPendingAction) }
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ChangeOwnRoleBottomSheet(
|
||||
eventSink: (RolesAndPermissionsEvents) -> Unit,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val sheetState = if (LocalInspectionMode.current) {
|
||||
sheetStateForPreview()
|
||||
} else {
|
||||
rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
}
|
||||
fun dismiss() {
|
||||
sheetState.hide(coroutineScope) {
|
||||
eventSink(RolesAndPermissionsEvents.CancelPendingAction)
|
||||
}
|
||||
}
|
||||
ModalBottomSheet(
|
||||
modifier = Modifier
|
||||
.systemBarsPadding()
|
||||
.navigationBarsPadding(),
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = ::dismiss,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(14.dp),
|
||||
text = stringResource(R.string.screen_room_roles_and_permissions_change_my_role),
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 14.dp, end = 14.dp, bottom = 16.dp),
|
||||
text = stringResource(R.string.screen_room_change_role_confirm_demote_self_description),
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_role_demote_to_moderator)) },
|
||||
onClick = {
|
||||
sheetState.hide(coroutineScope) {
|
||||
eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.MODERATOR))
|
||||
}
|
||||
},
|
||||
style = ListItemStyle.Destructive,
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_role_demote_to_member)) },
|
||||
onClick = {
|
||||
sheetState.hide(coroutineScope) {
|
||||
eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.USER))
|
||||
}
|
||||
},
|
||||
style = ListItemStyle.Destructive,
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(CommonStrings.action_cancel)) },
|
||||
onClick = {
|
||||
sheetState.hide(coroutineScope) {
|
||||
eventSink(RolesAndPermissionsEvents.CancelPendingAction)
|
||||
}
|
||||
},
|
||||
style = ListItemStyle.Primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun RoomDetailsAdminSettingsViewPreview(@PreviewParameter(RolesAndPermissionsStateProvider::class) state: RolesAndPermissionsState) {
|
||||
ElementPreview {
|
||||
RolesAndPermissionsView(
|
||||
state = state,
|
||||
roomDetailsAdminSettingsNavigator = object : RoomDetailsAdminSettingsNavigator {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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.features.roomdetails.impl.rolesandpermissions.changeroles
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
||||
sealed interface ChangeRolesEvent {
|
||||
data object ToggleSearchActive : ChangeRolesEvent
|
||||
data class QueryChanged(val query: String?) : ChangeRolesEvent
|
||||
data class UserSelectionToggled(val roomMember: RoomMember) : ChangeRolesEvent
|
||||
data object Save : ChangeRolesEvent
|
||||
data object Exit : ChangeRolesEvent
|
||||
data object CancelExit : ChangeRolesEvent
|
||||
data object ClearError : ChangeRolesEvent
|
||||
data object CancelSave : ChangeRolesEvent
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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.features.roomdetails.impl.rolesandpermissions.changeroles
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class ChangeRolesNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
presenterFactory: ChangeRolesPresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
sealed interface ListType : Parcelable {
|
||||
@Parcelize
|
||||
data object Admins : ListType
|
||||
@Parcelize
|
||||
data object Moderators : ListType
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Inputs(
|
||||
val listType: ListType,
|
||||
) : NodeInputs, Parcelable
|
||||
|
||||
private val inputs: Inputs = inputs()
|
||||
|
||||
private val presenter = presenterFactory.run {
|
||||
val role = when (inputs.listType) {
|
||||
is ListType.Admins -> RoomMember.Role.ADMIN
|
||||
is ListType.Moderators -> RoomMember.Role.MODERATOR
|
||||
}
|
||||
create(role)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
ChangeRolesView(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
onBackPressed = this::navigateUp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
/*
|
||||
* 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.features.roomdetails.impl.rolesandpermissions.changeroles
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.features.roomdetails.impl.members.PowerLevelRoomMemberComparator
|
||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ChangeRolesPresenter @AssistedInject constructor(
|
||||
@Assisted private val role: RoomMember.Role,
|
||||
private val room: MatrixRoom,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : Presenter<ChangeRolesState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(role: RoomMember.Role): ChangeRolesPresenter
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): ChangeRolesState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val dataSource = remember { RoomMemberListDataSource(room, dispatchers) }
|
||||
var query by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
var searchActive by rememberSaveable { mutableStateOf(false) }
|
||||
var searchResults by remember {
|
||||
mutableStateOf<SearchBarResultState<ImmutableList<RoomMember>>>(SearchBarResultState.Initial())
|
||||
}
|
||||
val selectedUsers = remember {
|
||||
mutableStateOf<ImmutableList<MatrixUser>>(persistentListOf())
|
||||
}
|
||||
val exitState: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
val saveState: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
val usersWithRole = produceState(initialValue = persistentListOf()) {
|
||||
room.usersWithRole(role)
|
||||
.map { members -> members.map { it.toMatrixUser() } }
|
||||
.onEach { users ->
|
||||
val previous: PersistentList<MatrixUser> = value
|
||||
value = users.toPersistentList()
|
||||
// Users who were selected but didn't have the role, so their role change was pending
|
||||
val toAdd = selectedUsers.value.filter { user -> users.none { it.userId == user.userId } && previous.none { it.userId == user.userId } }
|
||||
// Users who no longer have the role
|
||||
val toRemove = previous.filter { user -> users.none { it.userId == user.userId } }
|
||||
selectedUsers.value = (users + toAdd - toRemove).toImmutableList()
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
val roomMemberState by room.membersStateFlow.collectAsState()
|
||||
|
||||
// Update search results for every query change
|
||||
LaunchedEffect(query, roomMemberState) {
|
||||
val results = dataSource
|
||||
.search(query.orEmpty())
|
||||
.sorted()
|
||||
|
||||
searchResults = if (results.isEmpty()) {
|
||||
SearchBarResultState.NoResultsFound()
|
||||
} else {
|
||||
SearchBarResultState.Results(results)
|
||||
}
|
||||
}
|
||||
|
||||
val hasPendingChanges = usersWithRole.value != selectedUsers.value
|
||||
|
||||
val roomInfo by room.roomInfoFlow.collectAsState(initial = null)
|
||||
fun canChangeMemberRole(userId: UserId): Boolean {
|
||||
// An admin can't remove or demote another admin
|
||||
val powerLevel = roomInfo?.userPowerLevels?.get(userId) ?: 0L
|
||||
return RoomMember.Role.forPowerLevel(powerLevel) != RoomMember.Role.ADMIN
|
||||
}
|
||||
|
||||
fun handleEvent(event: ChangeRolesEvent) {
|
||||
when (event) {
|
||||
is ChangeRolesEvent.ToggleSearchActive -> {
|
||||
searchActive = !searchActive
|
||||
}
|
||||
is ChangeRolesEvent.QueryChanged -> {
|
||||
query = event.query
|
||||
}
|
||||
is ChangeRolesEvent.UserSelectionToggled -> {
|
||||
val newList = selectedUsers.value.toMutableList()
|
||||
val index = newList.indexOfFirst { it.userId == event.roomMember.userId }
|
||||
if (index >= 0) {
|
||||
newList.removeAt(index)
|
||||
} else {
|
||||
newList.add(event.roomMember.toMatrixUser())
|
||||
}
|
||||
selectedUsers.value = newList.toImmutableList()
|
||||
}
|
||||
is ChangeRolesEvent.Save -> {
|
||||
if (role == RoomMember.Role.ADMIN && selectedUsers != usersWithRole && !saveState.value.isConfirming()) {
|
||||
// Confirm adding admin
|
||||
saveState.value = AsyncAction.Confirming
|
||||
} else if (!saveState.value.isLoading()) {
|
||||
coroutineScope.save(usersWithRole.value, selectedUsers, saveState)
|
||||
}
|
||||
}
|
||||
is ChangeRolesEvent.ClearError -> {
|
||||
saveState.value = AsyncAction.Uninitialized
|
||||
}
|
||||
is ChangeRolesEvent.Exit -> {
|
||||
exitState.value = if (exitState.value.isUninitialized() && hasPendingChanges) {
|
||||
// Has pending changes, confirm exit
|
||||
AsyncAction.Confirming
|
||||
} else {
|
||||
// No pending changes, exit immediately
|
||||
AsyncAction.Success(Unit)
|
||||
}
|
||||
}
|
||||
is ChangeRolesEvent.CancelExit -> {
|
||||
exitState.value = AsyncAction.Uninitialized
|
||||
}
|
||||
is ChangeRolesEvent.CancelSave -> {
|
||||
saveState.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
return ChangeRolesState(
|
||||
role = role,
|
||||
query = query,
|
||||
isSearchActive = searchActive,
|
||||
searchResults = searchResults,
|
||||
selectedUsers = selectedUsers.value,
|
||||
hasPendingChanges = hasPendingChanges,
|
||||
exitState = exitState.value,
|
||||
savingState = saveState.value,
|
||||
canChangeMemberRole = ::canChangeMemberRole,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Iterable<RoomMember>.sorted(): ImmutableList<RoomMember> {
|
||||
return sortedWith(PowerLevelRoomMemberComparator()).toImmutableList()
|
||||
}
|
||||
|
||||
private fun RoomMember.toMatrixUser() = MatrixUser(
|
||||
userId = userId,
|
||||
displayName = displayName,
|
||||
avatarUrl = avatarUrl,
|
||||
)
|
||||
|
||||
private fun CoroutineScope.save(
|
||||
usersWithRole: ImmutableList<MatrixUser>,
|
||||
selectedUsers: MutableState<ImmutableList<MatrixUser>>,
|
||||
saveState: MutableState<AsyncAction<Unit>>,
|
||||
) = launch {
|
||||
saveState.value = AsyncAction.Loading
|
||||
|
||||
val toAdd = selectedUsers.value - usersWithRole
|
||||
val toRemove = usersWithRole - selectedUsers.value
|
||||
|
||||
val changes: List<UserRoleChange> = buildList {
|
||||
for (selectedUser in toAdd) {
|
||||
add(UserRoleChange(selectedUser.userId, role))
|
||||
}
|
||||
for (selectedUser in toRemove) {
|
||||
add(UserRoleChange(selectedUser.userId, RoomMember.Role.USER))
|
||||
}
|
||||
}
|
||||
|
||||
room.updateUsersRoles(changes)
|
||||
.onFailure {
|
||||
saveState.value = AsyncAction.Failure(it)
|
||||
}
|
||||
.onSuccess {
|
||||
saveState.value = AsyncAction.Success(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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.features.roomdetails.impl.rolesandpermissions.changeroles
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class ChangeRolesState(
|
||||
val role: RoomMember.Role,
|
||||
val query: String?,
|
||||
val isSearchActive: Boolean,
|
||||
val searchResults: SearchBarResultState<ImmutableList<RoomMember>>,
|
||||
val selectedUsers: ImmutableList<MatrixUser>,
|
||||
val hasPendingChanges: Boolean,
|
||||
val exitState: AsyncAction<Unit>,
|
||||
val savingState: AsyncAction<Unit>,
|
||||
val canChangeMemberRole: (UserId) -> Boolean,
|
||||
val eventSink: (ChangeRolesEvent) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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.features.roomdetails.impl.rolesandpermissions.changeroles
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMemberList
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
class ChangeRolesStateProvider : PreviewParameterProvider<ChangeRolesState> {
|
||||
override val values: Sequence<ChangeRolesState>
|
||||
get() = sequenceOf(
|
||||
aChangeRolesState(),
|
||||
aChangeRolesState(role = RoomMember.Role.MODERATOR),
|
||||
aChangeRolesStateWithSelectedUsers().copy(hasPendingChanges = false),
|
||||
aChangeRolesStateWithSelectedUsers(),
|
||||
aChangeRolesStateWithSelectedUsers().copy(
|
||||
selectedUsers = aMatrixUserList().take(2).toImmutableList(),
|
||||
),
|
||||
aChangeRolesStateWithSelectedUsers().copy(
|
||||
query = "Alice",
|
||||
isSearchActive = true,
|
||||
searchResults = SearchBarResultState.Results(aRoomMemberList().take(1).toImmutableList()),
|
||||
selectedUsers = aMatrixUserList().take(1).toImmutableList(),
|
||||
),
|
||||
aChangeRolesStateWithSelectedUsers().copy(exitState = AsyncAction.Confirming),
|
||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Confirming),
|
||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Loading),
|
||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Success(Unit)),
|
||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Failure(Exception("boom"))),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aChangeRolesState(
|
||||
role: RoomMember.Role = RoomMember.Role.ADMIN,
|
||||
query: String? = null,
|
||||
isSearchActive: Boolean = false,
|
||||
searchResults: SearchBarResultState<ImmutableList<RoomMember>> = SearchBarResultState.NoResultsFound(),
|
||||
selectedUsers: ImmutableList<MatrixUser> = persistentListOf(),
|
||||
hasPendingChanges: Boolean = false,
|
||||
exitState: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
savingState: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
canRemoveMember: (UserId) -> Boolean = { true },
|
||||
) = ChangeRolesState(
|
||||
role = role,
|
||||
query = query,
|
||||
isSearchActive = isSearchActive,
|
||||
searchResults = searchResults,
|
||||
selectedUsers = selectedUsers,
|
||||
hasPendingChanges = hasPendingChanges,
|
||||
exitState = exitState,
|
||||
savingState = savingState,
|
||||
canChangeMemberRole = canRemoveMember,
|
||||
eventSink = {},
|
||||
)
|
||||
|
||||
internal fun aChangeRolesStateWithSelectedUsers() = aChangeRolesState(
|
||||
selectedUsers = aMatrixUserList().toImmutableList(),
|
||||
searchResults = SearchBarResultState.Results(aRoomMemberList().toImmutableList()),
|
||||
hasPendingChanges = true,
|
||||
canRemoveMember = { it != UserId("@alice:server.org") },
|
||||
)
|
||||
|
|
@ -0,0 +1,295 @@
|
|||
/*
|
||||
* 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.features.roomdetails.impl.rolesandpermissions.changeroles
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.roomdetails.impl.R
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncIndicator
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
|
||||
import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
||||
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.SearchBarResultState
|
||||
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.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
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.SelectedUsersRowList
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ChangeRolesView(
|
||||
state: ChangeRolesState,
|
||||
onBackPressed: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val updatedOnBackPressed by rememberUpdatedState(newValue = onBackPressed)
|
||||
BackHandler {
|
||||
if (state.isSearchActive) {
|
||||
state.eventSink(ChangeRolesEvent.ToggleSearchActive)
|
||||
} else {
|
||||
state.eventSink(ChangeRolesEvent.Exit)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = modifier) {
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding(),
|
||||
topBar = {
|
||||
AnimatedVisibility(visible = !state.isSearchActive) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
val title = when (state.role) {
|
||||
RoomMember.Role.ADMIN -> stringResource(R.string.screen_room_change_role_administrators_title)
|
||||
RoomMember.Role.MODERATOR -> stringResource(R.string.screen_room_change_role_moderators_title)
|
||||
RoomMember.Role.USER -> error("This should never be reached")
|
||||
}
|
||||
Text(
|
||||
text = title,
|
||||
style = ElementTheme.typography.aliasScreenTitle,
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
BackButton(onClick = { state.eventSink(ChangeRolesEvent.Exit) })
|
||||
},
|
||||
actions = {
|
||||
TextButton(
|
||||
text = stringResource(CommonStrings.action_save),
|
||||
enabled = state.hasPendingChanges,
|
||||
onClick = { state.eventSink(ChangeRolesEvent.Save) }
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
SearchBar(
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
placeHolderTitle = stringResource(CommonStrings.common_search_for_someone),
|
||||
query = state.query.orEmpty(),
|
||||
onQueryChange = { state.eventSink(ChangeRolesEvent.QueryChanged(it)) },
|
||||
active = state.isSearchActive,
|
||||
onActiveChange = { state.eventSink(ChangeRolesEvent.ToggleSearchActive) },
|
||||
resultState = state.searchResults,
|
||||
) { members ->
|
||||
SearchResultsList(
|
||||
isSearchActive = true,
|
||||
lazyListState = lazyListState,
|
||||
searchResults = members,
|
||||
selectedUsers = state.selectedUsers,
|
||||
canRemoveMember = state.canChangeMemberRole,
|
||||
onSelectionToggled = { state.eventSink(ChangeRolesEvent.UserSelectionToggled(it)) },
|
||||
selectedUsersList = {},
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = !state.isSearchActive,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
Column {
|
||||
SearchResultsList(
|
||||
isSearchActive = false,
|
||||
lazyListState = lazyListState,
|
||||
searchResults = (state.searchResults as? SearchBarResultState.Results)?.results ?: persistentListOf(),
|
||||
selectedUsers = state.selectedUsers,
|
||||
canRemoveMember = state.canChangeMemberRole,
|
||||
onSelectionToggled = { state.eventSink(ChangeRolesEvent.UserSelectionToggled(it)) },
|
||||
selectedUsersList = { users ->
|
||||
SelectedUsersRowList(
|
||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||
selectedUsers = users,
|
||||
onUserRemoved = {
|
||||
state.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(it.userId)))
|
||||
},
|
||||
canDeselect = { state.canChangeMemberRole(it.userId) },
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val asyncIndicatorState = rememberAsyncIndicatorState()
|
||||
AsyncIndicatorHost(modifier = Modifier.statusBarsPadding(), asyncIndicatorState)
|
||||
|
||||
AsyncActionView(
|
||||
async = state.exitState,
|
||||
onSuccess = { updatedOnBackPressed() },
|
||||
confirmationDialog = {
|
||||
ConfirmationDialog(
|
||||
title = stringResource(CommonStrings.dialog_unsaved_changes_title),
|
||||
content = stringResource(CommonStrings.dialog_unsaved_changes_description_android),
|
||||
onSubmitClicked = { state.eventSink(ChangeRolesEvent.Exit) },
|
||||
onDismiss = { state.eventSink(ChangeRolesEvent.CancelExit) }
|
||||
)
|
||||
},
|
||||
onErrorDismiss = { /* Cannot happen */ },
|
||||
)
|
||||
|
||||
when (state.savingState) {
|
||||
is AsyncAction.Confirming -> {
|
||||
if (state.role == RoomMember.Role.ADMIN) {
|
||||
// Confirm adding new admins dialogs
|
||||
ConfirmationDialog(
|
||||
title = stringResource(R.string.screen_room_change_role_confirm_add_admin_title),
|
||||
content = stringResource(R.string.screen_room_change_role_confirm_add_admin_description),
|
||||
onSubmitClicked = { state.eventSink(ChangeRolesEvent.Save) },
|
||||
onDismiss = { state.eventSink(ChangeRolesEvent.ClearError) }
|
||||
)
|
||||
}
|
||||
}
|
||||
is AsyncAction.Loading -> {
|
||||
ProgressDialog()
|
||||
}
|
||||
is AsyncAction.Failure -> {
|
||||
ErrorDialog(
|
||||
content = stringResource(CommonStrings.error_unknown),
|
||||
onDismiss = { state.eventSink(ChangeRolesEvent.ClearError) }
|
||||
)
|
||||
}
|
||||
is AsyncAction.Success -> {
|
||||
LaunchedEffect(state.savingState) {
|
||||
asyncIndicatorState.enqueue(durationMs = AsyncIndicator.DURATION_SHORT) {
|
||||
AsyncIndicator.Custom(text = stringResource(CommonStrings.common_saved_changes))
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun SearchResultsList(
|
||||
isSearchActive: Boolean,
|
||||
searchResults: ImmutableList<RoomMember>,
|
||||
selectedUsers: ImmutableList<MatrixUser>,
|
||||
canRemoveMember: (UserId) -> Boolean,
|
||||
onSelectionToggled: (RoomMember) -> Unit,
|
||||
lazyListState: LazyListState,
|
||||
selectedUsersList: @Composable (ImmutableList<MatrixUser>) -> Unit,
|
||||
) {
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
) {
|
||||
item {
|
||||
selectedUsersList(selectedUsers)
|
||||
}
|
||||
stickyHeader {
|
||||
val textResId = if (isSearchActive) {
|
||||
CommonStrings.common_search_results
|
||||
} else {
|
||||
R.string.screen_room_member_list_room_members_header_title
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.background(ElementTheme.colors.bgCanvasDefault)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
text = stringResource(textResId),
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
)
|
||||
}
|
||||
items(searchResults, key = { it.userId }) { roomMember ->
|
||||
val canToggle = canRemoveMember(roomMember.userId)
|
||||
val trailingContent: @Composable (() -> Unit)? = if (canToggle) {
|
||||
{
|
||||
Checkbox(
|
||||
checked = selectedUsers.any { it.userId == roomMember.userId },
|
||||
onCheckedChange = { onSelectionToggled(roomMember) },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
MatrixUserRow(
|
||||
modifier = Modifier.clickable(enabled = canToggle, onClick = { onSelectionToggled(roomMember) }),
|
||||
matrixUser = MatrixUser(
|
||||
userId = roomMember.userId,
|
||||
displayName = roomMember.displayName,
|
||||
avatarUrl = roomMember.avatarUrl,
|
||||
),
|
||||
trailingContent = trailingContent,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun ChangeRolesViewPreview(@PreviewParameter(ChangeRolesStateProvider::class) state: ChangeRolesState) {
|
||||
ElementPreview {
|
||||
ChangeRolesView(
|
||||
state = state,
|
||||
onBackPressed = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Пры абнаўленні налад апавяшчэнняў адбылася памылка."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Ваш хатні сервер не падтрымлівае гэтую опцыю ў зашыфраваных пакоях, вы можаце не атрымаць апавяшчэнне ў некаторых пакоях."</string>
|
||||
<string name="screen_polls_history_title">"Апытанні"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Усе"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Дадаць тэму"</string>
|
||||
<string name="screen_room_details_already_a_member">"Ужо ўдзельнік"</string>
|
||||
<string name="screen_room_details_already_invited">"Ужо запрасілі"</string>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,29 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Při aktualizaci nastavení oznámení došlo k chybě."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Váš domovský server tuto možnost v zašifrovaných místnostech nepodporuje, v některých místnostech nemusíte být upozorněni."</string>
|
||||
<string name="screen_polls_history_title">"Hlasování"</string>
|
||||
<string name="screen_room_change_permissions_administrators">"Pouze správci"</string>
|
||||
<string name="screen_room_change_permissions_ban_people">"Vykázat lidi"</string>
|
||||
<string name="screen_room_change_permissions_delete_messages">"Odstranit zprávy"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Všichni"</string>
|
||||
<string name="screen_room_change_permissions_invite_people">"Pozvat lidi"</string>
|
||||
<string name="screen_room_change_permissions_member_moderation">"Moderování členů"</string>
|
||||
<string name="screen_room_change_permissions_messages_and_content">"Zprávy a obsah"</string>
|
||||
<string name="screen_room_change_permissions_moderators">"Správci a moderátoři"</string>
|
||||
<string name="screen_room_change_permissions_remove_people">"Odebrat lidi"</string>
|
||||
<string name="screen_room_change_permissions_room_avatar">"Změnit avatar místnosti"</string>
|
||||
<string name="screen_room_change_permissions_room_details">"Podrobnosti místnosti"</string>
|
||||
<string name="screen_room_change_permissions_room_name">"Změnit název místnosti"</string>
|
||||
<string name="screen_room_change_permissions_room_topic">"Změnit téma místnosti"</string>
|
||||
<string name="screen_room_change_permissions_send_messages">"Odeslat zprávy"</string>
|
||||
<string name="screen_room_change_role_administrators_title">"Upravit správce"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"Tuto akci nebudete moci vrátit zpět. Upravujete oprávnění uživatele, tak aby měl stejnou úroveň jako vy."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Přidat správce?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Degradovat"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"Tuto změnu nebudete moci vrátit zpět, protože sami degradujete, pokud jste posledním privilegovaným uživatelem v místnosti, nebude možné znovu získat oprávnění."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Degradovat se?"</string>
|
||||
<string name="screen_room_change_role_moderators_title">"Upravit moderátory"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_description">"Máte neuložené změny."</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_title">"Uložit změny?"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Přidat téma"</string>
|
||||
<string name="screen_room_details_already_a_member">"Již členem"</string>
|
||||
<string name="screen_room_details_already_invited">"Již pozván(a)"</string>
|
||||
|
|
@ -26,25 +49,31 @@
|
|||
<string name="screen_room_details_notification_mode_custom">"Vlastní"</string>
|
||||
<string name="screen_room_details_notification_mode_default">"Výchozí"</string>
|
||||
<string name="screen_room_details_notification_title">"Oznámení"</string>
|
||||
<string name="screen_room_details_roles_and_permissions">"Role a oprávnění"</string>
|
||||
<string name="screen_room_details_room_name_label">"Název místnosti"</string>
|
||||
<string name="screen_room_details_security_title">"Zabezpečení"</string>
|
||||
<string name="screen_room_details_share_room_title">"Sdílet místnost"</string>
|
||||
<string name="screen_room_details_topic_title">"Téma"</string>
|
||||
<string name="screen_room_details_updating_room">"Aktualizace místnosti…"</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_action">"Vykázat"</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_description">"Nebudou se moci znovu připojit k této místnosti, pokud budou pozváni."</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_title">"Jste si jisti, že chcete vykázat tohoto člena?"</string>
|
||||
<string name="screen_room_member_list_banned_empty">"V této místnosti nejsou žádní vykázaní uživatelé."</string>
|
||||
<string name="screen_room_member_list_banning_user">"Vykazování %1$s"</string>
|
||||
<plurals name="screen_room_member_list_header_title">
|
||||
<item quantity="one">"%1$d osoba"</item>
|
||||
<item quantity="few">"%1$d osoby"</item>
|
||||
<item quantity="other">"%1$d osob"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_member_list_manage_member_remove">"Odebrat člena"</string>
|
||||
<string name="screen_room_member_list_manage_member_ban">"Odebrat a vykázat člena"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove">"Odebrat z místnosti"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_ban">"Odebrat a vykázat člena"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_kick">"Pouze odebrat člena"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_title">"Odebrat člena a zakázat mu připojení v budoucnu?"</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_action">"Zrušit vykázání"</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_message">"Pokud budou pozváni, budou se moci do této místnosti znovu připojit."</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_title">"Zrušit vykázání uživatele"</string>
|
||||
<string name="screen_room_member_list_manage_member_user_info">"Zobrazit informace o uživateli"</string>
|
||||
<string name="screen_room_member_list_manage_member_user_info">"Zobrazit profil"</string>
|
||||
<string name="screen_room_member_list_mode_banned">"Vykázaní"</string>
|
||||
<string name="screen_room_member_list_mode_members">"Členové"</string>
|
||||
<string name="screen_room_member_list_pending_header_title">"Nevyřízeno"</string>
|
||||
|
|
@ -67,5 +96,19 @@
|
|||
<string name="screen_room_notification_settings_mode_all_messages">"Všechny zprávy"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Pouze zmínky a klíčová slova"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"V této místnosti mě upozornit na"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Správci"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_my_role">"Změnit moji roli"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_member">"Degradovat na člena"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"Degradovat na moderátora"</string>
|
||||
<string name="screen_room_roles_and_permissions_member_moderation">"Moderování členů"</string>
|
||||
<string name="screen_room_roles_and_permissions_messages_and_content">"Zprávy a obsah"</string>
|
||||
<string name="screen_room_roles_and_permissions_moderators">"Moderátoři"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Oprávnění"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset">"Obnovit oprávnění"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Po obnovení oprávnění ztratíte aktuální nastavení."</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_title">"Obnovit oprávnění?"</string>
|
||||
<string name="screen_room_roles_and_permissions_roles_header">"Role"</string>
|
||||
<string name="screen_room_roles_and_permissions_room_details">"Podrobnosti místnosti"</string>
|
||||
<string name="screen_room_roles_and_permissions_title">"Role a oprávnění"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Při pokusu o zahájení chatu došlo k chybě"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,15 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Beim Aktualisieren der Benachrichtigungseinstellungen ist ein Fehler aufgetreten."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Dein Homeserver unterstützt diese Option in verschlüsselten Räumen nicht. In einigen Räumen wirst du möglicherweise nicht benachrichtigt."</string>
|
||||
<string name="screen_polls_history_title">"Umfragen"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Alle"</string>
|
||||
<string name="screen_room_change_permissions_member_moderation">"Moderation der Mitglieder"</string>
|
||||
<string name="screen_room_change_permissions_messages_and_content">"Nachrichten und Inhalte"</string>
|
||||
<string name="screen_room_change_permissions_room_details">"Raumdetails"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"Du vergibst das selbe Rolle, die auch Du hast. Diese Aktion kann daher nicht mehr rückgängig gemacht werden."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Als Administrator hinzufügen?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Zurückstufen"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"Du stufst dich selbst herab. Diese Änderung kann nicht rückgängig gemacht werden. Wenn du der letzte Benutzer mit dieser Rolle bist, ist es nicht möglich, diese Rolle wiederzuerlangen."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Möchtest Du Dich selbst herabstufen?"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Thema hinzufügen"</string>
|
||||
<string name="screen_room_details_already_a_member">"Bereits Mitglied"</string>
|
||||
<string name="screen_room_details_already_invited">"Bereits eingeladen"</string>
|
||||
|
|
@ -67,5 +76,14 @@
|
|||
<string name="screen_room_notification_settings_mode_all_messages">"Alle Nachrichten"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Nur Erwähnungen und Schlüsselwörter"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"Benachrichtige mich in diesem Raum bei"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Administratoren"</string>
|
||||
<string name="screen_room_roles_and_permissions_member_moderation">"Moderation der Mitglieder"</string>
|
||||
<string name="screen_room_roles_and_permissions_messages_and_content">"Nachrichten und Inhalte"</string>
|
||||
<string name="screen_room_roles_and_permissions_moderators">"Moderatoren"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Berechtigungen"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset">"Rollen und Berechtigungen zurücksetzen"</string>
|
||||
<string name="screen_room_roles_and_permissions_roles_header">"Rollen"</string>
|
||||
<string name="screen_room_roles_and_permissions_room_details">"Raumdetails"</string>
|
||||
<string name="screen_room_roles_and_permissions_title">"Rollen und Berechtigungen"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Se ha producido un error al actualizar la configuración de notificaciones."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Tu servidor principal no admite esta opción en salas cifradas, puede que no recibas notificaciones en algunas salas."</string>
|
||||
<string name="screen_polls_history_title">"Encuestas"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Todos"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Añadir tema"</string>
|
||||
<string name="screen_room_details_already_a_member">"Ya eres miembro"</string>
|
||||
<string name="screen_room_details_already_invited">"Ya estás invitado"</string>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,16 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Une erreur s’est produite lors de la mise à jour du paramètre de notification."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Votre serveur d’accueil ne supporte pas cette option pour les salons chiffrés, vous pourriez ne pas être notifié(e) dans certains salons."</string>
|
||||
<string name="screen_polls_history_title">"Sondages"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Tout le monde"</string>
|
||||
<string name="screen_room_change_permissions_room_avatar">"Changer l’avatar du salon"</string>
|
||||
<string name="screen_room_change_permissions_room_details">"Détails du salon"</string>
|
||||
<string name="screen_room_change_permissions_room_name">"Changer le nom du salon"</string>
|
||||
<string name="screen_room_change_permissions_room_topic">"Changer le sujet du salon"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"Vous ne pourrez pas annuler cette action. Vous êtes en train de promouvoir l’utilisateur pour qu’il ait le même niveau que vous."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Ajouter un administrateur ?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Rétrograder"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"Vous ne pourrez pas annuler ce changement car vous vous rétrogradez, si vous êtes le dernier utilisateur privilégié du salon il sera impossible de retrouver les privilèges."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Vous rétrograder ?"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Ajouter un sujet"</string>
|
||||
<string name="screen_room_details_already_a_member">"Déjà membre"</string>
|
||||
<string name="screen_room_details_already_invited">"Déjà invité(e)"</string>
|
||||
|
|
@ -26,6 +36,7 @@
|
|||
<string name="screen_room_details_notification_mode_custom">"Personnalisé"</string>
|
||||
<string name="screen_room_details_notification_mode_default">"Défaut"</string>
|
||||
<string name="screen_room_details_notification_title">"Notifications"</string>
|
||||
<string name="screen_room_details_roles_and_permissions">"Rôles et autorisations"</string>
|
||||
<string name="screen_room_details_room_name_label">"Nom du salon"</string>
|
||||
<string name="screen_room_details_security_title">"Sécurité"</string>
|
||||
<string name="screen_room_details_share_room_title">"Partager le salon"</string>
|
||||
|
|
@ -64,5 +75,11 @@
|
|||
<string name="screen_room_notification_settings_mode_all_messages">"Tous les messages"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Mentions et mots clés uniquement"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"Dans ce salon, prévenez-moi pour"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Administrateurs"</string>
|
||||
<string name="screen_room_roles_and_permissions_moderators">"Modérateurs"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Autorisations"</string>
|
||||
<string name="screen_room_roles_and_permissions_roles_header">"Rôles"</string>
|
||||
<string name="screen_room_roles_and_permissions_room_details">"Détails du salon"</string>
|
||||
<string name="screen_room_roles_and_permissions_title">"Rôles et autorisations"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Une erreur s’est produite lors de la tentative de création de la discussion"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Hiba történt az értesítési beállítás frissítésekor."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"A Matrix-kiszolgálója nem támogatja ezt a beállítást a titkosított szobákban, előfordulhat, hogy egyes szobákban nem kap értesítést."</string>
|
||||
<string name="screen_polls_history_title">"Szavazások"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Mindenki"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Adminisztrátor hozzáadása?"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Téma hozzáadása"</string>
|
||||
<string name="screen_room_details_already_a_member">"Már tag"</string>
|
||||
<string name="screen_room_details_already_invited">"Már meghívták"</string>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Terjadi kesalahan saat memperbarui pengaturan pemberitahuan."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Homeserver Anda tidak mendukung opsi ini dalam ruangan terenkripsi, Anda mungkin tidak diberi tahu dalam beberapa ruangan."</string>
|
||||
<string name="screen_polls_history_title">"Pemungutan suara"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Semua orang"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Tambahkan topik"</string>
|
||||
<string name="screen_room_details_already_a_member">"Sudah menjadi anggota"</string>
|
||||
<string name="screen_room_details_already_invited">"Sudah diundang"</string>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Si è verificato un errore durante l\'aggiornamento delle impostazioni di notifica."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Il tuo homeserver non supporta questa opzione nelle stanze criptate, quindi potresti non ricevere notifiche in alcune stanze."</string>
|
||||
<string name="screen_polls_history_title">"Sondaggi"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Tutti"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Aggiungi argomento"</string>
|
||||
<string name="screen_room_details_already_a_member">"Già membro"</string>
|
||||
<string name="screen_room_details_already_invited">"Già invitato"</string>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"A apărut o eroare în timpul actualizării setărilor pentru notificari."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Serverul dumneavoastră nu acceptă această opțiune în camerele criptate, este posibil să nu primiți notificări în unele camere."</string>
|
||||
<string name="screen_polls_history_title">"Sondaje"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Toți"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Adăugare subiect"</string>
|
||||
<string name="screen_room_details_already_a_member">"Deja membru"</string>
|
||||
<string name="screen_room_details_already_invited">"Deja invitat"</string>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,29 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"При обновлении настроек уведомления произошла ошибка."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Ваш домашний сервер не поддерживает эту опцию в зашифрованных комнатах, в некоторых комнатах вы можете не получать уведомления."</string>
|
||||
<string name="screen_polls_history_title">"Опросы"</string>
|
||||
<string name="screen_room_change_permissions_administrators">"Только для администраторов"</string>
|
||||
<string name="screen_room_change_permissions_ban_people">"Заблокировать людей"</string>
|
||||
<string name="screen_room_change_permissions_delete_messages">"Удалить сообщения"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Для всех"</string>
|
||||
<string name="screen_room_change_permissions_invite_people">"Пригласить людей"</string>
|
||||
<string name="screen_room_change_permissions_member_moderation">"Модерация участников"</string>
|
||||
<string name="screen_room_change_permissions_messages_and_content">"Сообщения и содержание"</string>
|
||||
<string name="screen_room_change_permissions_moderators">"Администраторы и модераторы"</string>
|
||||
<string name="screen_room_change_permissions_remove_people">"Удалить людей"</string>
|
||||
<string name="screen_room_change_permissions_room_avatar">"Изменить изображение комнаты"</string>
|
||||
<string name="screen_room_change_permissions_room_details">"Информация о комнате"</string>
|
||||
<string name="screen_room_change_permissions_room_name">"Изменить название комнаты"</string>
|
||||
<string name="screen_room_change_permissions_room_topic">"Сменить тему комнаты"</string>
|
||||
<string name="screen_room_change_permissions_send_messages">"Отправить сообщение"</string>
|
||||
<string name="screen_room_change_role_administrators_title">"Редактировать роль администраторов"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"Вы не сможете отменить это действие. Вы устанавливаете уровень пользователю соответствующий вашему."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Добавить администратора?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Понизить уровень"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"Вы не сможете отменить это изменение, так как понижаете себя статус. Если вы являетесь последним привилегированным пользователем в комнате, восстановить привилегии будет невозможно."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Понизить свой уровень?"</string>
|
||||
<string name="screen_room_change_role_moderators_title">"Редактировать роль модераторов"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_description">"У вас есть несохраненные изменения."</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_title">"Сохранить изменения?"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Добавить тему"</string>
|
||||
<string name="screen_room_details_already_a_member">"Уже зарегистрирован"</string>
|
||||
<string name="screen_room_details_already_invited">"Уже приглашены"</string>
|
||||
|
|
@ -26,22 +49,31 @@
|
|||
<string name="screen_room_details_notification_mode_custom">"Пользовательский"</string>
|
||||
<string name="screen_room_details_notification_mode_default">"По умолчанию"</string>
|
||||
<string name="screen_room_details_notification_title">"Уведомления"</string>
|
||||
<string name="screen_room_details_roles_and_permissions">"Роли и разрешения"</string>
|
||||
<string name="screen_room_details_room_name_label">"Название комнаты"</string>
|
||||
<string name="screen_room_details_security_title">"Безопасность"</string>
|
||||
<string name="screen_room_details_share_room_title">"Поделиться комнатой"</string>
|
||||
<string name="screen_room_details_topic_title">"Тема"</string>
|
||||
<string name="screen_room_details_updating_room">"Обновление комнаты…"</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_action">"Заблокировать"</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_description">"Они не смогут снова присоединиться к этой комнате, если их пригласят."</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_title">"Вы уверены, что хотите заблокировать этого участника?"</string>
|
||||
<string name="screen_room_member_list_banned_empty">"В этой комнате нет заблокированных пользователей."</string>
|
||||
<string name="screen_room_member_list_banning_user">"Блокировка %1$s"</string>
|
||||
<plurals name="screen_room_member_list_header_title">
|
||||
<item quantity="one">"%1$d пользователь"</item>
|
||||
<item quantity="few">"%1$d пользователя"</item>
|
||||
<item quantity="many">"%1$d пользователей"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_member_list_manage_member_remove">"Удалить участника"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_ban">"Удалить и заблокировать участника"</string>
|
||||
<string name="screen_room_member_list_manage_member_ban">"Удалить и заблокировать участника"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove">"Удалить участника из комнаты"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_ban">"Удалить и запретить участника"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_kick">"Только удалить участника"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_title">"Удалить участника и запретить присоединяться в будущем?"</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_action">"Разблокировать"</string>
|
||||
<string name="screen_room_member_list_manage_member_user_info">"Посмотреть информацию о пользователе"</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_message">"Они снова смогут присоединиться в эту комнату если их пригласят."</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_title">"Разбанить пользователя?"</string>
|
||||
<string name="screen_room_member_list_manage_member_user_info">"Посмотреть профиль"</string>
|
||||
<string name="screen_room_member_list_mode_banned">"Заблокирован"</string>
|
||||
<string name="screen_room_member_list_mode_members">"Участники"</string>
|
||||
<string name="screen_room_member_list_pending_header_title">"В ожидании"</string>
|
||||
|
|
@ -49,6 +81,7 @@
|
|||
<string name="screen_room_member_list_role_administrator">"Администратор"</string>
|
||||
<string name="screen_room_member_list_role_moderator">"Модератор"</string>
|
||||
<string name="screen_room_member_list_room_members_header_title">"Участники комнаты"</string>
|
||||
<string name="screen_room_member_list_unbanning_user">"Разблокировка %1$s"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom">"Разрешить пользовательские настройки"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom_footnote">"Включение этого параметра отменяет настройки по умолчанию"</string>
|
||||
<string name="screen_room_notification_settings_custom_settings_title">"Уведомить меня в этом чате"</string>
|
||||
|
|
@ -63,5 +96,19 @@
|
|||
<string name="screen_room_notification_settings_mode_all_messages">"Все сообщения"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Только упоминания и ключевые слова"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"В этой комнате уведомить меня о"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Администраторы"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_my_role">"Измените мою роль"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_member">"Понижение до участника"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"Понизить до модератора"</string>
|
||||
<string name="screen_room_roles_and_permissions_member_moderation">"Модерация участников"</string>
|
||||
<string name="screen_room_roles_and_permissions_messages_and_content">"Сообщения и содержание"</string>
|
||||
<string name="screen_room_roles_and_permissions_moderators">"Модераторы"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Разрешения"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset">"Сбросить разрешения"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Как только вы сбросите разрешения, вы потеряете текущие настройки."</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_title">"Сбросить разрешения?"</string>
|
||||
<string name="screen_room_roles_and_permissions_roles_header">"Роли"</string>
|
||||
<string name="screen_room_roles_and_permissions_room_details">"Информация о комнате"</string>
|
||||
<string name="screen_room_roles_and_permissions_title">"Роли и разрешения"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Произошла ошибка при попытке открытия комнаты"</string>
|
||||
</resources>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue