Merge branch 'release/0.4.7' into main
This commit is contained in:
34
.github/workflows/clear-cache.yml
vendored
34
.github/workflows/clear-cache.yml
vendored
@@ -1,34 +0,0 @@
|
||||
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
|
||||
2
.github/workflows/nightlyReports.yml
vendored
2
.github/workflows/nightlyReports.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES
|
||||
|
||||
- name: 📈 Generate kover report and verify coverage
|
||||
run: ./gradlew :app:koverHtmlReport :app:koverXmlReport :app:koverVerify $CI_GRADLE_ARG_PROPERTIES -Pci-build=true
|
||||
run: ./gradlew :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyGplayDebug $CI_GRADLE_ARG_PROPERTIES
|
||||
|
||||
- name: ✅ Upload kover report
|
||||
if: always()
|
||||
|
||||
2
.github/workflows/quality.yml
vendored
2
.github/workflows/quality.yml
vendored
@@ -63,7 +63,7 @@ jobs:
|
||||
with:
|
||||
name: linting-report
|
||||
path: |
|
||||
*/build/reports/**/*.*
|
||||
**/build/reports/**/*.*
|
||||
- name: Prepare Danger
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
12
.github/workflows/tests.yml
vendored
12
.github/workflows/tests.yml
vendored
@@ -48,14 +48,14 @@ jobs:
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
|
||||
- name: ⚙️ Run unit tests, debug and release
|
||||
run: ./gradlew test $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: ⚙️ Run unit tests for debug variant
|
||||
run: ./gradlew testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES
|
||||
|
||||
- name: 📸 Run screenshot tests
|
||||
run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES
|
||||
|
||||
- name: 📈Generate kover report and verify coverage
|
||||
run: ./gradlew :app:koverHtmlReport :app:koverXmlReport :app:koverVerify $CI_GRADLE_ARG_PROPERTIES -Pci-build=true
|
||||
run: ./gradlew :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyGplayDebug $CI_GRADLE_ARG_PROPERTIES
|
||||
|
||||
- name: 🚫 Upload kover failed coverage reports
|
||||
if: failure()
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
with:
|
||||
name: kover-error-report
|
||||
path: |
|
||||
app/build/reports/kover/verify.err
|
||||
app/build/reports/kover/verifyGplayDebug.err
|
||||
|
||||
- name: ✅ Upload kover report (disabled)
|
||||
if: always()
|
||||
@@ -85,5 +85,5 @@ jobs:
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
# with:
|
||||
# files: build/reports/kover/xml/report.xml
|
||||
files: app/build/reports/kover/reportGplayDebug.xml
|
||||
verbose: true
|
||||
|
||||
@@ -23,8 +23,6 @@ appId: ${MAESTRO_APP_ID}
|
||||
- inputText: ${MAESTRO_PASSWORD}
|
||||
- pressKey: Enter
|
||||
- tapOn: "Continue"
|
||||
- runFlow: ../assertions/assertWelcomeScreenDisplayed.yaml
|
||||
- tapOn: "Continue"
|
||||
- runFlow: ../assertions/assertAnalyticsDisplayed.yaml
|
||||
- tapOn: "Not now"
|
||||
- runFlow: ../assertions/assertHomeDisplayed.yaml
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
appId: ${MAESTRO_APP_ID}
|
||||
---
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "welcome_screen-title"
|
||||
timeout: 10000
|
||||
20
CHANGES.md
20
CHANGES.md
@@ -1,3 +1,23 @@
|
||||
Changes in Element X v0.4.7 (2024-03-26)
|
||||
========================================
|
||||
|
||||
Features ✨
|
||||
----------
|
||||
- Enable the feature "RoomList filters". ([#2603](https://github.com/element-hq/element-x-android/issues/2603))
|
||||
- Enable the feature "Mark as unread" ([#2261](https://github.com/element-hq/element-x-android/issues/2261))
|
||||
- Implement MSC2530 (Body field as media caption) ([#2521](https://github.com/element-hq/element-x-android/issues/2521))
|
||||
|
||||
Bugfixes 🐛
|
||||
----------
|
||||
- Use user avatar from cache if available. ([#2488](https://github.com/element-hq/element-x-android/issues/2488))
|
||||
- Update member list after changing member roles and when the room member list is opened. ([#2590](https://github.com/element-hq/element-x-android/issues/2590))
|
||||
|
||||
Other changes
|
||||
-------------
|
||||
- Compound: add `BigIcon`, `BigCheckmark` and `PageTitle` components. ([#2574](https://github.com/element-hq/element-x-android/issues/2574))
|
||||
- Remove Welcome screen from the FTUE. ([#2584](https://github.com/element-hq/element-x-android/issues/2584))
|
||||
|
||||
|
||||
Changes in Element X v0.4.6 (2024-03-15)
|
||||
========================================
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import extension.gitBranchName
|
||||
import extension.gitRevision
|
||||
import extension.koverDependencies
|
||||
import extension.setupKover
|
||||
import org.jetbrains.kotlin.cli.common.toBooleanLenient
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-application")
|
||||
@@ -189,7 +188,7 @@ androidComponents {
|
||||
val abiCode = abiVersionCodes[name] ?: 0
|
||||
// Assigns the new version code to output.versionCode, which changes the version code
|
||||
// for only the output APK, not for the variant itself.
|
||||
output.versionCode.set((output.versionCode.get() ?: 0) * 10 + abiCode)
|
||||
output.versionCode.set((output.versionCode.orNull ?: 0) * 10 + abiCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,26 +214,6 @@ knit {
|
||||
}
|
||||
}
|
||||
|
||||
val ciBuildProperty = "ci-build"
|
||||
val isCiBuild = if (project.hasProperty(ciBuildProperty)) {
|
||||
val raw = project.property(ciBuildProperty) as? String
|
||||
raw?.toBooleanLenient() == true || raw?.toIntOrNull() == 1
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
kover {
|
||||
// When running on the CI, run only debug test variants
|
||||
if (isCiBuild) {
|
||||
excludeTests {
|
||||
// Disable instrumentation for debug test tasks
|
||||
tasks(
|
||||
"testDebugUnitTest",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
allLibrariesImpl()
|
||||
allServicesImpl()
|
||||
|
||||
@@ -172,6 +172,7 @@ allprojects {
|
||||
|
||||
// Register quality check tasks.
|
||||
tasks.register("runQualityChecks") {
|
||||
dependsOn(":tests:konsist:testDebugUnitTest")
|
||||
project.subprojects {
|
||||
// For some reason `findByName("lint")` doesn't work
|
||||
tasks.findByPath("$path:lint")?.let { dependsOn(it) }
|
||||
|
||||
2
fastlane/metadata/android/en-US/changelogs/40004070.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40004070.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Main changes in this version: Enable the feature "RoomList filters" and "Mark as unread".
|
||||
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||
@@ -36,7 +36,6 @@ import io.element.android.features.ftue.api.FtueEntryPoint
|
||||
import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode
|
||||
import io.element.android.features.ftue.impl.state.DefaultFtueState
|
||||
import io.element.android.features.ftue.impl.state.FtueStep
|
||||
import io.element.android.features.ftue.impl.welcome.WelcomeNode
|
||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
@@ -73,9 +72,6 @@ class FtueFlowNode @AssistedInject constructor(
|
||||
@Parcelize
|
||||
data object Placeholder : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object WelcomeScreen : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object NotificationsOptIn : NavTarget
|
||||
|
||||
@@ -110,15 +106,6 @@ class FtueFlowNode @AssistedInject constructor(
|
||||
NavTarget.Placeholder -> {
|
||||
createNode<PlaceholderNode>(buildContext)
|
||||
}
|
||||
NavTarget.WelcomeScreen -> {
|
||||
val callback = object : WelcomeNode.Callback {
|
||||
override fun onContinueClicked() {
|
||||
ftueState.setWelcomeScreenShown()
|
||||
lifecycleScope.launch { moveToNextStep() }
|
||||
}
|
||||
}
|
||||
createNode<WelcomeNode>(buildContext, listOf(callback))
|
||||
}
|
||||
NavTarget.NotificationsOptIn -> {
|
||||
val callback = object : NotificationsOptInNode.Callback {
|
||||
override fun onNotificationsOptInFinished() {
|
||||
@@ -146,9 +133,6 @@ class FtueFlowNode @AssistedInject constructor(
|
||||
|
||||
private fun moveToNextStep() {
|
||||
when (ftueState.getNextStep()) {
|
||||
FtueStep.WelcomeScreen -> {
|
||||
backstack.newRoot(NavTarget.WelcomeScreen)
|
||||
}
|
||||
FtueStep.NotificationsOptIn -> {
|
||||
backstack.newRoot(NavTarget.NotificationsOptIn)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import android.os.Build
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.ftue.api.state.FtueState
|
||||
import io.element.android.features.ftue.impl.welcome.state.WelcomeScreenState
|
||||
import io.element.android.features.lockscreen.api.LockScreenService
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
||||
@@ -40,14 +39,12 @@ class DefaultFtueState @Inject constructor(
|
||||
private val sdkVersionProvider: BuildVersionSdkIntProvider,
|
||||
coroutineScope: CoroutineScope,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val welcomeScreenState: WelcomeScreenState,
|
||||
private val permissionStateProvider: PermissionStateProvider,
|
||||
private val lockScreenService: LockScreenService,
|
||||
) : FtueState {
|
||||
override val shouldDisplayFlow = MutableStateFlow(isAnyStepIncomplete())
|
||||
|
||||
override suspend fun reset() {
|
||||
welcomeScreenState.reset()
|
||||
analyticsService.reset()
|
||||
if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
|
||||
permissionStateProvider.resetPermission(Manifest.permission.POST_NOTIFICATIONS)
|
||||
@@ -62,12 +59,7 @@ class DefaultFtueState @Inject constructor(
|
||||
|
||||
fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
|
||||
when (currentStep) {
|
||||
null -> if (shouldDisplayWelcomeScreen()) {
|
||||
FtueStep.WelcomeScreen
|
||||
} else {
|
||||
getNextStep(FtueStep.WelcomeScreen)
|
||||
}
|
||||
FtueStep.WelcomeScreen -> if (shouldAskNotificationPermissions()) {
|
||||
null -> if (shouldAskNotificationPermissions()) {
|
||||
FtueStep.NotificationsOptIn
|
||||
} else {
|
||||
getNextStep(FtueStep.NotificationsOptIn)
|
||||
@@ -87,7 +79,6 @@ class DefaultFtueState @Inject constructor(
|
||||
|
||||
private fun isAnyStepIncomplete(): Boolean {
|
||||
return listOf(
|
||||
{ shouldDisplayWelcomeScreen() },
|
||||
{ shouldAskNotificationPermissions() },
|
||||
{ needsAnalyticsOptIn() },
|
||||
{ shouldDisplayLockscreenSetup() },
|
||||
@@ -99,10 +90,6 @@ class DefaultFtueState @Inject constructor(
|
||||
return runBlocking { analyticsService.didAskUserConsent().first().not() }
|
||||
}
|
||||
|
||||
private fun shouldDisplayWelcomeScreen(): Boolean {
|
||||
return welcomeScreenState.isWelcomeScreenNeeded()
|
||||
}
|
||||
|
||||
private fun shouldAskNotificationPermissions(): Boolean {
|
||||
return if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
|
||||
val permission = Manifest.permission.POST_NOTIFICATIONS
|
||||
@@ -120,11 +107,6 @@ class DefaultFtueState @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun setWelcomeScreenShown() {
|
||||
welcomeScreenState.setWelcomeScreenShown()
|
||||
updateState()
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal fun updateState() {
|
||||
shouldDisplayFlow.value = isAnyStepIncomplete()
|
||||
@@ -132,7 +114,6 @@ class DefaultFtueState @Inject constructor(
|
||||
}
|
||||
|
||||
sealed interface FtueStep {
|
||||
data object WelcomeScreen : FtueStep
|
||||
data object NotificationsOptIn : FtueStep
|
||||
data object AnalyticsOptIn : FtueStep
|
||||
data object LockscreenSetup : FtueStep
|
||||
|
||||
@@ -20,7 +20,6 @@ import android.os.Build
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.ftue.impl.state.DefaultFtueState
|
||||
import io.element.android.features.ftue.impl.state.FtueStep
|
||||
import io.element.android.features.ftue.impl.welcome.state.FakeWelcomeState
|
||||
import io.element.android.features.lockscreen.api.LockScreenService
|
||||
import io.element.android.features.lockscreen.test.FakeLockScreenService
|
||||
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
|
||||
@@ -47,7 +46,6 @@ class DefaultFtueStateTests {
|
||||
|
||||
@Test
|
||||
fun `given all checks being true, should display flow is false`() = runTest {
|
||||
val welcomeState = FakeWelcomeState()
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = true)
|
||||
val lockScreenService = FakeLockScreenService()
|
||||
@@ -55,13 +53,11 @@ class DefaultFtueStateTests {
|
||||
|
||||
val state = createState(
|
||||
coroutineScope = coroutineScope,
|
||||
welcomeState = welcomeState,
|
||||
analyticsService = analyticsService,
|
||||
permissionStateProvider = permissionStateProvider,
|
||||
lockScreenService = lockScreenService,
|
||||
)
|
||||
|
||||
welcomeState.setWelcomeScreenShown()
|
||||
analyticsService.setDidAskUserConsent()
|
||||
permissionStateProvider.setPermissionGranted()
|
||||
lockScreenService.setIsPinSetup(true)
|
||||
@@ -75,7 +71,6 @@ class DefaultFtueStateTests {
|
||||
|
||||
@Test
|
||||
fun `traverse flow`() = runTest {
|
||||
val welcomeState = FakeWelcomeState()
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
|
||||
val lockScreenService = FakeLockScreenService()
|
||||
@@ -83,26 +78,21 @@ class DefaultFtueStateTests {
|
||||
|
||||
val state = createState(
|
||||
coroutineScope = coroutineScope,
|
||||
welcomeState = welcomeState,
|
||||
analyticsService = analyticsService,
|
||||
permissionStateProvider = permissionStateProvider,
|
||||
lockScreenService = lockScreenService,
|
||||
)
|
||||
val steps = mutableListOf<FtueStep?>()
|
||||
|
||||
// First step, welcome screen
|
||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||
welcomeState.setWelcomeScreenShown()
|
||||
|
||||
// Second step, notifications opt in
|
||||
// Notifications opt in
|
||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||
permissionStateProvider.setPermissionGranted()
|
||||
|
||||
// Third step, entering PIN code
|
||||
// Entering PIN code
|
||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||
lockScreenService.setIsPinSetup(true)
|
||||
|
||||
// Fourth step, analytics opt in
|
||||
// Analytics opt in
|
||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||
analyticsService.setDidAskUserConsent()
|
||||
|
||||
@@ -110,7 +100,6 @@ class DefaultFtueStateTests {
|
||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||
|
||||
assertThat(steps).containsExactly(
|
||||
FtueStep.WelcomeScreen,
|
||||
FtueStep.NotificationsOptIn,
|
||||
FtueStep.LockscreenSetup,
|
||||
FtueStep.AnalyticsOptIn,
|
||||
@@ -135,15 +124,14 @@ class DefaultFtueStateTests {
|
||||
lockScreenService = lockScreenService,
|
||||
)
|
||||
|
||||
// Skip first 3 steps
|
||||
state.setWelcomeScreenShown()
|
||||
// Skip first 2 steps
|
||||
permissionStateProvider.setPermissionGranted()
|
||||
lockScreenService.setIsPinSetup(true)
|
||||
|
||||
assertThat(state.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn)
|
||||
|
||||
analyticsService.setDidAskUserConsent()
|
||||
assertThat(state.getNextStep(FtueStep.WelcomeScreen)).isNull()
|
||||
assertThat(state.getNextStep(null)).isNull()
|
||||
|
||||
// Cleanup
|
||||
coroutineScope.cancel()
|
||||
@@ -162,14 +150,12 @@ class DefaultFtueStateTests {
|
||||
lockScreenService = lockScreenService,
|
||||
)
|
||||
|
||||
assertThat(state.getNextStep()).isEqualTo(FtueStep.WelcomeScreen)
|
||||
state.setWelcomeScreenShown()
|
||||
lockScreenService.setIsPinSetup(true)
|
||||
|
||||
assertThat(state.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn)
|
||||
|
||||
analyticsService.setDidAskUserConsent()
|
||||
assertThat(state.getNextStep(FtueStep.WelcomeScreen)).isNull()
|
||||
assertThat(state.getNextStep(null)).isNull()
|
||||
|
||||
// Cleanup
|
||||
coroutineScope.cancel()
|
||||
@@ -177,7 +163,6 @@ class DefaultFtueStateTests {
|
||||
|
||||
private fun createState(
|
||||
coroutineScope: CoroutineScope,
|
||||
welcomeState: FakeWelcomeState = FakeWelcomeState(),
|
||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||
permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
|
||||
lockScreenService: LockScreenService = FakeLockScreenService(),
|
||||
@@ -187,7 +172,6 @@ class DefaultFtueStateTests {
|
||||
sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion),
|
||||
coroutineScope = coroutineScope,
|
||||
analyticsService = analyticsService,
|
||||
welcomeScreenState = welcomeState,
|
||||
permissionStateProvider = permissionStateProvider,
|
||||
lockScreenService = lockScreenService,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="leave_conversation_alert_subtitle">"Apakah Anda yakin ingin keluar dari percakapan ini? Percakapan ini tidak umum dan Anda tidak akan dapat bergabung lagi tanpa undangan."</string>
|
||||
<string name="leave_room_alert_empty_subtitle">"Apakah Anda yakin ingin meninggalkan ruangan ini? Anda adalah orang satu-satunya di sini. Jika Anda pergi, tidak akan ada yang bisa bergabung di masa depan, termasuk Anda."</string>
|
||||
<string name="leave_room_alert_private_subtitle">"Apakah Anda yakin ingin meninggalkan ruangan ini? Ruangan ini tidak umum dan Anda tidak akan dapat bergabung kembali tanpa undangan."</string>
|
||||
<string name="leave_room_alert_subtitle">"Apakah Anda yakin ingin meninggalkan ruangan?"</string>
|
||||
|
||||
@@ -50,6 +50,7 @@ dependencies {
|
||||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(libs.androidx.browser)
|
||||
implementation(platform(libs.network.retrofit.bom))
|
||||
implementation(libs.network.retrofit)
|
||||
implementation(libs.serialization.json)
|
||||
api(projects.features.login.api)
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
<string name="screen_change_account_provider_subtitle">"Gunakan penyedia akun yang berbeda, seperti server pribadi Anda sendiri atau akun kerja."</string>
|
||||
<string name="screen_change_account_provider_title">"Ubah penyedia akun"</string>
|
||||
<string name="screen_change_server_error_invalid_homeserver">"Kami tidak dapat menjangkau server ini. Periksa apakah Anda telah memasukkan URL homeserver dengan benar. Jika URL sudah benar, hubungi administrator homeserver Anda untuk bantuan lebih lanjut."</string>
|
||||
<string name="screen_change_server_error_invalid_well_known">"Sliding sync tidak tersedia karena adanya masalah dalam berkas .well-known:
|
||||
%1$s"</string>
|
||||
<string name="screen_change_server_error_no_sliding_sync_message">"Server ini saat ini tidak mendukung sinkronisasi geser."</string>
|
||||
<string name="screen_change_server_form_header">"URL Homeserver"</string>
|
||||
<string name="screen_change_server_form_notice">"Anda hanya dapat terhubung ke server yang ada yang mendukung sinkronisasi geser. Admin homeserver Anda perlu mengaturnya. %1$s"</string>
|
||||
@@ -22,6 +24,7 @@
|
||||
<string name="screen_login_error_deactivated_account">"Akun ini telah dinonaktifkan."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Nama pengguna dan/atau kata sandi salah"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"Ini bukan pengenal pengguna yang valid. Format yang diharapkan: \'@pengguna:homeserver.org\'"</string>
|
||||
<string name="screen_login_error_refresh_tokens">"Server ini diatur untuk menggunakan token penyegaran. Ini tidak didukung ketika menggunakan log masuk berbasis kata sandi."</string>
|
||||
<string name="screen_login_error_unsupported_authentication">"Homeserver yang dipilih tidak mendukung log masuk kata sandi atau OIDC. Silakan hubungi admin Anda atau pilih homeserver yang lain."</string>
|
||||
<string name="screen_login_form_header">"Masukkan detail Anda"</string>
|
||||
<string name="screen_login_subtitle">"Matrix adalah jaringan terbuka untuk komunikasi yang aman dan terdesentralisasi."</string>
|
||||
|
||||
@@ -295,8 +295,6 @@ class MessagesPresenter @AssistedInject constructor(
|
||||
private fun CoroutineScope.reinviteOtherUser(inviteProgress: MutableState<AsyncData<Unit>>) = launch(dispatchers.io) {
|
||||
inviteProgress.value = AsyncData.Loading()
|
||||
runCatching {
|
||||
room.updateMembers()
|
||||
|
||||
val memberList = when (val memberState = room.membersStateFlow.value) {
|
||||
is MatrixRoomMembersState.Ready -> memberState.roomMembers
|
||||
is MatrixRoomMembersState.Error -> memberState.prevRoomMembers.orEmpty()
|
||||
|
||||
@@ -463,7 +463,12 @@ class MessageComposerPresenter @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
mediaSender.sendMedia(uri, mimeType, compressIfPossible = false, progressCallback).getOrThrow()
|
||||
mediaSender.sendMedia(
|
||||
uri = uri,
|
||||
mimeType = mimeType,
|
||||
compressIfPossible = false,
|
||||
progressCallback = progressCallback
|
||||
).getOrThrow()
|
||||
}
|
||||
.onSuccess {
|
||||
attachmentState.value = AttachmentsState.None
|
||||
|
||||
@@ -615,9 +615,9 @@ private fun MessageEventBubbleContent(
|
||||
}
|
||||
|
||||
val timestampPosition = when (event.content) {
|
||||
is TimelineItemImageContent,
|
||||
is TimelineItemImageContent -> if (event.content.showCaption) TimestampPosition.Aligned else TimestampPosition.Overlay
|
||||
is TimelineItemVideoContent -> if (event.content.showCaption) TimestampPosition.Aligned else TimestampPosition.Overlay
|
||||
is TimelineItemStickerContent,
|
||||
is TimelineItemVideoContent,
|
||||
is TimelineItemLocationContent -> TimestampPosition.Overlay
|
||||
is TimelineItemPollContent -> TimestampPosition.Below
|
||||
else -> TimestampPosition.Default
|
||||
@@ -723,10 +723,10 @@ private fun ReplyToContentText(metadata: InReplyToMetadata?) {
|
||||
@Composable
|
||||
internal fun TimelineItemEventRowPreview() = ElementPreview {
|
||||
Column {
|
||||
sequenceOf(false, true).forEach {
|
||||
sequenceOf(false, true).forEach { isMine ->
|
||||
ATimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
isMine = it,
|
||||
isMine = isMine,
|
||||
content = aTimelineItemTextContent().copy(
|
||||
body = "A long text which will be displayed on several lines and" +
|
||||
" hopefully can be manually adjusted to test different behaviors."
|
||||
@@ -736,7 +736,7 @@ internal fun TimelineItemEventRowPreview() = ElementPreview {
|
||||
)
|
||||
ATimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
isMine = it,
|
||||
isMine = isMine,
|
||||
content = aTimelineItemImageContent().copy(
|
||||
aspectRatio = 2.5f
|
||||
),
|
||||
|
||||
@@ -101,7 +101,7 @@ open class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails>
|
||||
),
|
||||
aMessageContent(
|
||||
body = "Video",
|
||||
type = VideoMessageType("Video", MediaSource("url"), null),
|
||||
type = VideoMessageType("Video", null, null, MediaSource("url"), null),
|
||||
),
|
||||
aMessageContent(
|
||||
body = "Audio",
|
||||
@@ -113,7 +113,7 @@ open class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails>
|
||||
),
|
||||
aMessageContent(
|
||||
body = "Image",
|
||||
type = ImageMessageType("Image", MediaSource("url"), null),
|
||||
type = ImageMessageType("Image", null, null, MediaSource("url"), null),
|
||||
),
|
||||
aMessageContent(
|
||||
body = "Sticker",
|
||||
|
||||
@@ -25,8 +25,8 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
private const val MIN_HEIGHT_IN_DP = 100
|
||||
private const val MAX_HEIGHT_IN_DP = 360
|
||||
const val MIN_HEIGHT_IN_DP = 100
|
||||
const val MAX_HEIGHT_IN_DP = 360
|
||||
private const val DEFAULT_ASPECT_RATIO = 1.33f
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -77,6 +77,7 @@ fun TimelineItemEventContentView(
|
||||
)
|
||||
is TimelineItemImageContent -> TimelineItemImageView(
|
||||
content = content,
|
||||
onContentLayoutChanged = onContentLayoutChanged,
|
||||
modifier = modifier,
|
||||
)
|
||||
is TimelineItemStickerContent -> TimelineItemStickerView(
|
||||
@@ -85,6 +86,7 @@ fun TimelineItemEventContentView(
|
||||
)
|
||||
is TimelineItemVideoContent -> TimelineItemVideoView(
|
||||
content = content,
|
||||
onContentLayoutChanged = onContentLayoutChanged,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemFileContent -> TimelineItemFileView(
|
||||
|
||||
@@ -16,39 +16,134 @@
|
||||
|
||||
package io.element.android.features.messages.impl.timeline.components.event
|
||||
|
||||
import android.text.SpannedString
|
||||
import androidx.compose.foundation.background
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import coil.compose.AsyncImagePainter
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||
import io.element.android.features.messages.impl.timeline.components.ATimelineItemEventRow
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContentProvider
|
||||
import io.element.android.libraries.designsystem.components.BlurHashAsyncImage
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
||||
import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.wysiwyg.compose.EditorStyledText
|
||||
|
||||
@Composable
|
||||
fun TimelineItemImageView(
|
||||
content: TimelineItemImageContent,
|
||||
onContentLayoutChanged: (ContentAvoidingLayoutData) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val description = stringResource(CommonStrings.common_image)
|
||||
TimelineItemAspectRatioBox(
|
||||
aspectRatio = content.aspectRatio,
|
||||
Column(
|
||||
modifier = modifier.semantics { contentDescription = description },
|
||||
) {
|
||||
BlurHashAsyncImage(
|
||||
model = MediaRequestData(content.preferredMediaSource, MediaRequestData.Kind.File(content.body, content.mimeType)),
|
||||
blurHash = content.blurhash,
|
||||
)
|
||||
val containerModifier = if (content.showCaption) {
|
||||
Modifier
|
||||
.padding(top = 6.dp)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
TimelineItemAspectRatioBox(
|
||||
modifier = containerModifier.blurHashBackground(content.blurhash, alpha = 0.9f),
|
||||
aspectRatio = content.aspectRatio,
|
||||
) {
|
||||
var isLoaded by remember { mutableStateOf(false) }
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(if (isLoaded) Modifier.background(Color.White) else Modifier),
|
||||
model = MediaRequestData(content.preferredMediaSource, MediaRequestData.Kind.File(content.body, content.mimeType)),
|
||||
contentScale = ContentScale.Fit,
|
||||
alignment = Alignment.Center,
|
||||
contentDescription = description,
|
||||
onState = { isLoaded = it is AsyncImagePainter.State.Success },
|
||||
)
|
||||
}
|
||||
|
||||
if (content.showCaption) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
val caption = if (LocalInspectionMode.current) {
|
||||
SpannedString(content.caption)
|
||||
} else {
|
||||
content.formatted?.body?.takeIf { content.formatted.format == MessageFormat.HTML } ?: SpannedString(content.caption)
|
||||
}
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides ElementTheme.colors.textPrimary,
|
||||
LocalTextStyle provides ElementTheme.typography.fontBodyLgRegular
|
||||
) {
|
||||
EditorStyledText(
|
||||
modifier = Modifier
|
||||
.widthIn(min = MIN_HEIGHT_IN_DP.dp * content.aspectRatio!!, max = MAX_HEIGHT_IN_DP.dp * content.aspectRatio),
|
||||
text = caption,
|
||||
style = ElementRichTextEditorStyle.textStyle(),
|
||||
releaseOnDetach = false,
|
||||
onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChanged = onContentLayoutChanged),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun TimelineItemImageViewPreview(@PreviewParameter(TimelineItemImageContentProvider::class) content: TimelineItemImageContent) = ElementPreview {
|
||||
TimelineItemImageView(content)
|
||||
TimelineItemImageView(content, {})
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun TimelineImageWithCaptionRowPreview() = ElementPreview {
|
||||
Column {
|
||||
sequenceOf(false, true).forEach { isMine ->
|
||||
ATimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
isMine = isMine,
|
||||
content = aTimelineItemImageContent().copy(
|
||||
filename = "image.jpg",
|
||||
body = "A long caption that may wrap into several lines",
|
||||
aspectRatio = 2.5f,
|
||||
),
|
||||
groupPosition = TimelineItemGroupPosition.Last,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContentProvider
|
||||
import io.element.android.libraries.designsystem.components.BlurHashAsyncImage
|
||||
import io.element.android.libraries.designsystem.components.blurhash.BlurHashAsyncImage
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
|
||||
@@ -16,54 +16,124 @@
|
||||
|
||||
package io.element.android.features.messages.impl.timeline.components.event
|
||||
|
||||
import android.text.SpannedString
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import coil.compose.AsyncImagePainter
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||
import io.element.android.features.messages.impl.timeline.components.ATimelineItemEventRow
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContentProvider
|
||||
import io.element.android.libraries.designsystem.components.BlurHashAsyncImage
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVideoContent
|
||||
import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground
|
||||
import io.element.android.libraries.designsystem.modifiers.roundedBackground
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.wysiwyg.compose.EditorStyledText
|
||||
|
||||
@Composable
|
||||
fun TimelineItemVideoView(
|
||||
content: TimelineItemVideoContent,
|
||||
onContentLayoutChanged: (ContentAvoidingLayoutData) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val description = stringResource(CommonStrings.common_image)
|
||||
TimelineItemAspectRatioBox(
|
||||
aspectRatio = content.aspectRatio,
|
||||
modifier = modifier.semantics { contentDescription = description },
|
||||
contentAlignment = Alignment.Center,
|
||||
Column(
|
||||
modifier = modifier.semantics { contentDescription = description }
|
||||
) {
|
||||
BlurHashAsyncImage(
|
||||
model = MediaRequestData(content.thumbnailSource, MediaRequestData.Kind.File(content.body, content.mimeType)),
|
||||
blurHash = content.blurHash,
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.roundedBackground(),
|
||||
val containerModifier = if (content.showCaption) {
|
||||
Modifier.padding(top = 6.dp).clip(RoundedCornerShape(6.dp))
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
TimelineItemAspectRatioBox(
|
||||
modifier = containerModifier.blurHashBackground(content.blurHash, alpha = 0.9f),
|
||||
aspectRatio = content.aspectRatio,
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Image(
|
||||
Icons.Default.PlayArrow,
|
||||
contentDescription = stringResource(id = CommonStrings.a11y_play),
|
||||
colorFilter = ColorFilter.tint(Color.White),
|
||||
var isLoaded by remember { mutableStateOf(false) }
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(if (isLoaded) Modifier.background(Color.White) else Modifier),
|
||||
model = MediaRequestData(content.thumbnailSource, MediaRequestData.Kind.File(content.body, content.mimeType)),
|
||||
contentScale = ContentScale.Fit,
|
||||
alignment = Alignment.Center,
|
||||
contentDescription = description,
|
||||
onState = { isLoaded = it is AsyncImagePainter.State.Success },
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.roundedBackground(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Image(
|
||||
Icons.Default.PlayArrow,
|
||||
contentDescription = stringResource(id = CommonStrings.a11y_play),
|
||||
colorFilter = ColorFilter.tint(Color.White),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (content.showCaption) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
val caption = if (LocalInspectionMode.current) {
|
||||
SpannedString(content.caption)
|
||||
} else {
|
||||
content.formatted?.body?.takeIf { content.formatted.format == MessageFormat.HTML } ?: SpannedString(content.caption)
|
||||
}
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides ElementTheme.colors.textPrimary,
|
||||
LocalTextStyle provides ElementTheme.typography.fontBodyLgRegular,
|
||||
) {
|
||||
EditorStyledText(
|
||||
modifier = Modifier
|
||||
.widthIn(min = MIN_HEIGHT_IN_DP.dp * content.aspectRatio!!, max = MAX_HEIGHT_IN_DP.dp * content.aspectRatio),
|
||||
text = caption,
|
||||
style = ElementRichTextEditorStyle.textStyle(),
|
||||
releaseOnDetach = false,
|
||||
onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChanged = onContentLayoutChanged),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,5 +141,25 @@ fun TimelineItemVideoView(
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun TimelineItemVideoViewPreview(@PreviewParameter(TimelineItemVideoContentProvider::class) content: TimelineItemVideoContent) = ElementPreview {
|
||||
TimelineItemVideoView(content)
|
||||
TimelineItemVideoView(content, {})
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun TimelineVideoWithCaptionRowPreview() = ElementPreview {
|
||||
Column {
|
||||
sequenceOf(false, true).forEach { isMine ->
|
||||
ATimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
isMine = isMine,
|
||||
content = aTimelineItemVideoContent().copy(
|
||||
filename = "video.mp4",
|
||||
body = "A long caption that may wrap into several lines",
|
||||
aspectRatio = 2.5f,
|
||||
),
|
||||
groupPosition = TimelineItemGroupPosition.Last,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,8 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
||||
TimelineItemImageContent(
|
||||
body = messageType.body.trimEnd(),
|
||||
formatted = messageType.formatted,
|
||||
filename = messageType.filename,
|
||||
mediaSource = messageType.source,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||
@@ -91,7 +93,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||
height = messageType.info?.height?.toInt(),
|
||||
aspectRatio = aspectRatio,
|
||||
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
||||
fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
|
||||
fileExtension = messageType.filename?.let { fileExtensionExtractor.extractFromName(it) }.orEmpty()
|
||||
)
|
||||
}
|
||||
is StickerMessageType -> {
|
||||
@@ -132,6 +134,8 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
||||
TimelineItemVideoContent(
|
||||
body = messageType.body.trimEnd(),
|
||||
formatted = messageType.formatted,
|
||||
filename = messageType.filename,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
videoSource = messageType.source,
|
||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||
@@ -141,7 +145,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||
blurHash = messageType.info?.blurhash,
|
||||
aspectRatio = aspectRatio,
|
||||
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
||||
fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
|
||||
fileExtension = messageType.filename?.let { fileExtensionExtractor.extractFromName(it) }.orEmpty(),
|
||||
)
|
||||
}
|
||||
is AudioMessageType -> {
|
||||
|
||||
@@ -18,9 +18,12 @@ package io.element.android.features.messages.impl.timeline.model.event
|
||||
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||
|
||||
data class TimelineItemImageContent(
|
||||
val body: String,
|
||||
val formatted: FormattedBody?,
|
||||
val filename: String?,
|
||||
val mediaSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
val formattedFileSize: String,
|
||||
@@ -33,6 +36,9 @@ data class TimelineItemImageContent(
|
||||
) : TimelineItemEventContent {
|
||||
override val type: String = "TimelineItemImageContent"
|
||||
|
||||
val showCaption = filename != null && filename != body
|
||||
val caption = if (showCaption) body else ""
|
||||
|
||||
val preferredMediaSource = if (mimeType == MimeTypes.Gif) {
|
||||
mediaSource
|
||||
} else {
|
||||
|
||||
@@ -32,6 +32,8 @@ open class TimelineItemImageContentProvider : PreviewParameterProvider<TimelineI
|
||||
|
||||
fun aTimelineItemImageContent() = TimelineItemImageContent(
|
||||
body = "a body",
|
||||
formatted = null,
|
||||
filename = null,
|
||||
mediaSource = MediaSource(""),
|
||||
thumbnailSource = null,
|
||||
mimeType = MimeTypes.IMAGE_JPEG,
|
||||
|
||||
@@ -17,10 +17,13 @@
|
||||
package io.element.android.features.messages.impl.timeline.model.event
|
||||
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||
import kotlin.time.Duration
|
||||
|
||||
data class TimelineItemVideoContent(
|
||||
val body: String,
|
||||
val formatted: FormattedBody?,
|
||||
val filename: String?,
|
||||
val duration: Duration,
|
||||
val videoSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
@@ -33,4 +36,7 @@ data class TimelineItemVideoContent(
|
||||
val fileExtension: String,
|
||||
) : TimelineItemEventContent {
|
||||
override val type: String = "TimelineItemImageContent"
|
||||
|
||||
val showCaption = filename != null && filename != body
|
||||
val caption = if (showCaption) body else ""
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ open class TimelineItemVideoContentProvider : PreviewParameterProvider<TimelineI
|
||||
|
||||
fun aTimelineItemVideoContent() = TimelineItemVideoContent(
|
||||
body = "Video.mp4",
|
||||
formatted = null,
|
||||
filename = null,
|
||||
thumbnailSource = null,
|
||||
blurHash = A_BLUR_HASH,
|
||||
aspectRatio = 0.5f,
|
||||
|
||||
@@ -40,4 +40,11 @@
|
||||
<plurals name="screen_room_timeline_state_changes">
|
||||
<item quantity="other">"%1$d perubahan ruangan"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_typing_many_members">
|
||||
<item quantity="other">"%1$s, %2$s, dan %3$d lainnya"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_typing_notification">
|
||||
<item quantity="other">"%1$s sedang mengetik"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_typing_two_members">"%1$s dan %2$s"</string>
|
||||
</resources>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<string name="screen_room_attachment_text_formatting">"格式化文字"</string>
|
||||
<string name="screen_room_invite_again_alert_message">"您想要邀請他們回來嗎?"</string>
|
||||
<string name="screen_room_invite_again_alert_title">"此聊天室只有您一個人"</string>
|
||||
<string name="screen_room_mentions_at_room_subtitle">"通知整個聊天室"</string>
|
||||
<string name="screen_room_mentions_at_room_title">"所有人"</string>
|
||||
<string name="screen_room_retry_send_menu_send_again_action">"重傳"</string>
|
||||
<string name="screen_room_retry_send_menu_title">"無法傳送您的訊息"</string>
|
||||
|
||||
@@ -270,6 +270,8 @@ class MessagesPresenterTest {
|
||||
val mediaMessage = aMessageEvent(
|
||||
content = TimelineItemImageContent(
|
||||
body = "image.jpg",
|
||||
formatted = null,
|
||||
filename = null,
|
||||
mediaSource = MediaSource(AN_AVATAR_URL),
|
||||
thumbnailSource = null,
|
||||
mimeType = MimeTypes.Jpeg,
|
||||
@@ -300,6 +302,8 @@ class MessagesPresenterTest {
|
||||
val mediaMessage = aMessageEvent(
|
||||
content = TimelineItemVideoContent(
|
||||
body = "video.mp4",
|
||||
formatted = null,
|
||||
filename = null,
|
||||
duration = 10.milliseconds,
|
||||
videoSource = MediaSource(AN_AVATAR_URL),
|
||||
thumbnailSource = MediaSource(AN_AVATAR_URL),
|
||||
|
||||
@@ -227,12 +227,14 @@ class TimelineItemContentMessageFactoryTest {
|
||||
fun `test create VideoMessageType`() = runTest {
|
||||
val sut = createTimelineItemContentMessageFactory()
|
||||
val result = sut.create(
|
||||
content = createMessageContent(type = VideoMessageType("body", MediaSource("url"), null)),
|
||||
content = createMessageContent(type = VideoMessageType("body", null, null, MediaSource("url"), null)),
|
||||
senderDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemVideoContent(
|
||||
body = "body",
|
||||
formatted = null,
|
||||
filename = null,
|
||||
duration = Duration.ZERO,
|
||||
videoSource = MediaSource(url = "url", json = null),
|
||||
thumbnailSource = null,
|
||||
@@ -253,7 +255,9 @@ class TimelineItemContentMessageFactoryTest {
|
||||
val result = sut.create(
|
||||
content = createMessageContent(
|
||||
type = VideoMessageType(
|
||||
body = "body.mp4",
|
||||
body = "body.mp4 caption",
|
||||
formatted = FormattedBody(MessageFormat.HTML, "formatted"),
|
||||
filename = "body.mp4",
|
||||
source = MediaSource("url"),
|
||||
info = VideoInfo(
|
||||
duration = 1.minutes,
|
||||
@@ -276,7 +280,9 @@ class TimelineItemContentMessageFactoryTest {
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemVideoContent(
|
||||
body = "body.mp4",
|
||||
body = "body.mp4 caption",
|
||||
formatted = FormattedBody(MessageFormat.HTML, "formatted"),
|
||||
filename = "body.mp4",
|
||||
duration = 1.minutes,
|
||||
videoSource = MediaSource(url = "url", json = null),
|
||||
thumbnailSource = MediaSource("url_thumbnail"),
|
||||
@@ -420,12 +426,14 @@ class TimelineItemContentMessageFactoryTest {
|
||||
fun `test create ImageMessageType`() = runTest {
|
||||
val sut = createTimelineItemContentMessageFactory()
|
||||
val result = sut.create(
|
||||
content = createMessageContent(type = ImageMessageType("body", MediaSource("url"), null)),
|
||||
content = createMessageContent(type = ImageMessageType("body", null, null, MediaSource("url"), null)),
|
||||
senderDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemImageContent(
|
||||
body = "body",
|
||||
formatted = null,
|
||||
filename = null,
|
||||
mediaSource = MediaSource(url = "url", json = null),
|
||||
thumbnailSource = null,
|
||||
formattedFileSize = "0 Bytes",
|
||||
@@ -470,7 +478,9 @@ class TimelineItemContentMessageFactoryTest {
|
||||
val result = sut.create(
|
||||
content = createMessageContent(
|
||||
type = ImageMessageType(
|
||||
body = "body.jpg",
|
||||
body = "body.jpg caption",
|
||||
formatted = FormattedBody(MessageFormat.HTML, "formatted"),
|
||||
filename = "body.jpg",
|
||||
source = MediaSource("url"),
|
||||
info = ImageInfo(
|
||||
height = 10L,
|
||||
@@ -492,7 +502,9 @@ class TimelineItemContentMessageFactoryTest {
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemImageContent(
|
||||
body = "body.jpg",
|
||||
body = "body.jpg caption",
|
||||
formatted = FormattedBody(MessageFormat.HTML, "formatted"),
|
||||
filename = "body.jpg",
|
||||
mediaSource = MediaSource(url = "url", json = null),
|
||||
thumbnailSource = MediaSource("url_thumbnail"),
|
||||
formattedFileSize = "888 Bytes",
|
||||
|
||||
@@ -83,6 +83,8 @@ class InReplyToMetadataKtTest {
|
||||
eventContent = aMessageContent(
|
||||
messageType = ImageMessageType(
|
||||
body = "body",
|
||||
formatted = null,
|
||||
filename = null,
|
||||
source = aMediaSource(),
|
||||
info = anImageInfo(),
|
||||
)
|
||||
@@ -137,6 +139,8 @@ class InReplyToMetadataKtTest {
|
||||
eventContent = aMessageContent(
|
||||
messageType = VideoMessageType(
|
||||
body = "body",
|
||||
formatted = null,
|
||||
filename = null,
|
||||
source = aMediaSource(),
|
||||
info = aVideoInfo(),
|
||||
)
|
||||
|
||||
@@ -41,6 +41,11 @@ class PollHistoryStateProvider : PreviewParameterProvider<PollHistoryState> {
|
||||
activeFilter = PollHistoryFilter.PAST,
|
||||
currentItems = emptyList(),
|
||||
),
|
||||
aPollHistoryState(
|
||||
activeFilter = PollHistoryFilter.PAST,
|
||||
currentItems = emptyList(),
|
||||
hasMoreToLoad = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -191,6 +191,7 @@ private fun PollHistoryList(
|
||||
Column(
|
||||
modifier = Modifier.fillParentMaxSize().padding(bottom = 24.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
val emptyStringResource = if (filter == PollHistoryFilter.PAST) {
|
||||
stringResource(R.string.screen_polls_history_empty_past)
|
||||
|
||||
@@ -24,7 +24,6 @@ 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
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.meta.BuildType
|
||||
@@ -35,8 +34,6 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.indicator.api.IndicatorService
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
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
|
||||
@@ -58,11 +55,10 @@ class PreferencesRootPresenter @Inject constructor(
|
||||
) : Presenter<PreferencesRootState> {
|
||||
@Composable
|
||||
override fun present(): PreferencesRootState {
|
||||
val matrixUser: MutableState<MatrixUser?> = rememberSaveable {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
val matrixUser = matrixClient.userProfile.collectAsState()
|
||||
LaunchedEffect(Unit) {
|
||||
initialLoad(matrixUser)
|
||||
// Force a refresh of the profile
|
||||
matrixClient.getUserProfile()
|
||||
}
|
||||
|
||||
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
||||
@@ -121,10 +117,6 @@ class PreferencesRootPresenter @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.initialLoad(matrixUser: MutableState<MatrixUser?>) = launch {
|
||||
matrixUser.value = matrixClient.getCurrentUser()
|
||||
}
|
||||
|
||||
private fun CoroutineScope.initAccountManagementUrl(
|
||||
accountManagementUrl: MutableState<String?>,
|
||||
devicesManagementUrl: MutableState<String?>,
|
||||
|
||||
@@ -21,7 +21,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
data class PreferencesRootState(
|
||||
val myUser: MatrixUser?,
|
||||
val myUser: MatrixUser,
|
||||
val version: String,
|
||||
val deviceId: String?,
|
||||
val showCompleteVerification: Boolean,
|
||||
|
||||
@@ -18,10 +18,13 @@ package io.element.android.features.preferences.impl.root
|
||||
|
||||
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
fun aPreferencesRootState() = PreferencesRootState(
|
||||
myUser = null,
|
||||
fun aPreferencesRootState(
|
||||
myUser: MatrixUser,
|
||||
) = PreferencesRootState(
|
||||
myUser = myUser,
|
||||
version = "Version 1.1 (1)",
|
||||
deviceId = "ILAKNDNASDLK",
|
||||
showCompleteVerification = true,
|
||||
|
||||
@@ -77,7 +77,7 @@ fun PreferencesRootView(
|
||||
) {
|
||||
UserPreferences(
|
||||
modifier = Modifier.clickable {
|
||||
state.myUser?.let(onOpenUserProfile)
|
||||
onOpenUserProfile(state.myUser)
|
||||
},
|
||||
user = state.myUser,
|
||||
)
|
||||
@@ -225,7 +225,7 @@ internal fun PreferencesRootViewDarkPreview(@PreviewParameter(MatrixUserProvider
|
||||
@Composable
|
||||
private fun ContentToPreview(matrixUser: MatrixUser) {
|
||||
PreferencesRootView(
|
||||
state = aPreferencesRootState().copy(myUser = matrixUser),
|
||||
state = aPreferencesRootState(myUser = matrixUser),
|
||||
onBackPressed = {},
|
||||
onOpenAnalytics = {},
|
||||
onOpenRageShake = {},
|
||||
|
||||
@@ -6,10 +6,16 @@
|
||||
<string name="screen_advanced_settings_element_call_base_url_description">"Tetapkan URL dasar khusus untuk Element Call."</string>
|
||||
<string name="screen_advanced_settings_element_call_base_url_validation_error">"URL tidak valid, pastikan Anda menyertakan protokol (http/https) dan alamat yang benar."</string>
|
||||
<string name="screen_advanced_settings_rich_text_editor_description">"Nonaktifkan penyunting teks kaya untuk mengetik Markdown secara manual."</string>
|
||||
<string name="screen_advanced_settings_send_read_receipts">"Laporan dibaca"</string>
|
||||
<string name="screen_advanced_settings_send_read_receipts_description">"Jika dimatikan, laporan dibaca Anda tidak akan dikirim kepada siapa pun. Anda masih akan menerima laporan dibaca dari pengguna lain."</string>
|
||||
<string name="screen_advanced_settings_share_presence">"Bagikan presensi"</string>
|
||||
<string name="screen_advanced_settings_share_presence_description">"Jika dimatikan, Anda tidak akan dapat mengirim atau menerima laporan dibaca atau notifikasi pengetikan"</string>
|
||||
<string name="screen_advanced_settings_view_source_description">"Aktifkan opsi untuk melihat sumber pesan dalam lini masa."</string>
|
||||
<string name="screen_blocked_users_empty">"Anda tidak memiliki pengguna yang diblokir"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Buka blokir"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Anda akan dapat melihat semua pesan dari mereka lagi."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Buka blokir pengguna"</string>
|
||||
<string name="screen_blocked_users_unblocking">"Membatalkan pemblokiran…"</string>
|
||||
<string name="screen_edit_profile_display_name">"Nama tampilan"</string>
|
||||
<string name="screen_edit_profile_display_name_placeholder">"Nama tampilan Anda"</string>
|
||||
<string name="screen_edit_profile_error">"Terjadi kesalahan yang tidak diketahui dan informasi tidak dapat diubah."</string>
|
||||
|
||||
@@ -75,7 +75,13 @@ class PreferencesRootPresenterTest {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.myUser).isNull()
|
||||
assertThat(initialState.myUser).isEqualTo(
|
||||
MatrixUser(
|
||||
userId = matrixClient.sessionId,
|
||||
displayName = A_USER_NAME,
|
||||
avatarUrl = AN_AVATAR_URL
|
||||
)
|
||||
)
|
||||
assertThat(initialState.version).isEqualTo("A Version")
|
||||
val loadedState = awaitItem()
|
||||
assertThat(loadedState.myUser).isEqualTo(
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="crash_detection_dialog_content">"Пры апошнім выкарыстанні %1$s адбыўся збой. Жадаеце падзяліцца справаздачай аб збоі?"</string>
|
||||
<string name="rageshake_detection_dialog_content">"Падобна, што вы трасеце тэлефон. Жадаеце адкрыць экран паведамлення пра памылку?"</string>
|
||||
<string name="settings_rageshake">"Rageshake"</string>
|
||||
<string name="settings_rageshake_detection_threshold">"Парог выяўлення"</string>
|
||||
</resources>
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
<string name="screen_bug_report_editor_description">"Silakan jelaskan masalah tersebut. Apa yang Anda lakukan? Apa yang Anda harapkan untuk terjadi? Apa yang sebenarnya terjadi? Jelaskan sedetail mungkin."</string>
|
||||
<string name="screen_bug_report_editor_placeholder">"Jelaskan masalah tersebut…"</string>
|
||||
<string name="screen_bug_report_editor_supporting">"Jika memungkinkan, silakan tulis deskripsi dalam bahasa Inggris."</string>
|
||||
<string name="screen_bug_report_error_description_too_short">"Deskripsinya terlalu pendek, silakan menyediakan detail tambahan tentang apa yang terjadi. Terima kasih!"</string>
|
||||
<string name="screen_bug_report_include_crash_logs">"Kirim log kerusakan"</string>
|
||||
<string name="screen_bug_report_include_logs">"Izinkan log"</string>
|
||||
<string name="screen_bug_report_include_screenshot">"Kirim tangkapan layar"</string>
|
||||
<string name="screen_bug_report_logs_description">"Log akan disertakan dengan pesan Anda untuk memastikan bahwa semuanya berfungsi dengan baik. Untuk mengirimkan pesan Anda tanpa log, matikan pengaturan ini."</string>
|
||||
<string name="screen_bug_report_rash_logs_alert_title">"%1$s mengalami kemogokan saat terakhir kali digunakan. Apakah Anda ingin berbagi laporan kerusakan dengan kami?"</string>
|
||||
<string name="screen_bug_report_view_logs">"Tampilkan catatan"</string>
|
||||
</resources>
|
||||
|
||||
@@ -26,7 +26,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
||||
@@ -34,7 +33,6 @@ import io.element.android.features.roomdetails.impl.members.details.RoomMemberDe
|
||||
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.utils.OnLifecycleEvent
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
@@ -92,13 +90,6 @@ class RoomDetailsPresenter @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
// Update room members only when first presenting the node
|
||||
OnLifecycleEvent { _, event ->
|
||||
if (event == Lifecycle.Event.ON_CREATE) {
|
||||
scope.launch { room.updateMembers() }
|
||||
}
|
||||
}
|
||||
|
||||
val membersState by room.membersStateFlow.collectAsState()
|
||||
val canInvite by getCanInvite(membersState)
|
||||
val canEditName by getCanSendState(membersState, StateEventType.ROOM_NAME)
|
||||
|
||||
@@ -178,7 +178,7 @@ fun RoomDetailsView(
|
||||
|
||||
if (state.displayRolesAndPermissionsSettings) {
|
||||
ListItem(
|
||||
headlineContent = { Text("Roles and permissions") },
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_details_roles_and_permissions)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Admin())),
|
||||
onClick = openAdminSettings,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.analytics
|
||||
|
||||
import im.vector.app.features.analytics.plan.RoomModeration
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
|
||||
internal fun RoomMember.Role.toAnalyticsMemberRole(): RoomModeration.Role = when (this) {
|
||||
RoomMember.Role.ADMIN -> RoomModeration.Role.Administrator
|
||||
RoomMember.Role.MODERATOR -> RoomModeration.Role.Moderator
|
||||
RoomMember.Role.USER -> RoomModeration.Role.User
|
||||
}
|
||||
|
||||
internal fun analyticsMemberRoleForPowerLevel(powerLevel: Long): RoomModeration.Role {
|
||||
return RoomMember.Role.forPowerLevel(powerLevel).toAnalyticsMemberRole()
|
||||
}
|
||||
|
||||
internal fun AnalyticsService.trackPermissionChangeAnalytics(initial: MatrixRoomPowerLevels?, updated: MatrixRoomPowerLevels) {
|
||||
if (updated.ban != initial?.ban) {
|
||||
capture(RoomModeration(RoomModeration.Action.ChangePermissionsBanMembers, analyticsMemberRoleForPowerLevel(updated.ban)))
|
||||
}
|
||||
if (updated.invite != initial?.invite) {
|
||||
capture(RoomModeration(RoomModeration.Action.ChangePermissionsInviteUsers, analyticsMemberRoleForPowerLevel(updated.invite)))
|
||||
}
|
||||
if (updated.kick != initial?.kick) {
|
||||
capture(RoomModeration(RoomModeration.Action.ChangePermissionsKickMembers, analyticsMemberRoleForPowerLevel(updated.kick)))
|
||||
}
|
||||
if (updated.sendEvents != initial?.sendEvents) {
|
||||
capture(RoomModeration(RoomModeration.Action.ChangePermissionsSendMessages, analyticsMemberRoleForPowerLevel(updated.sendEvents)))
|
||||
}
|
||||
if (updated.redactEvents != initial?.redactEvents) {
|
||||
capture(RoomModeration(RoomModeration.Action.ChangePermissionsRedactMessages, analyticsMemberRoleForPowerLevel(updated.redactEvents)))
|
||||
}
|
||||
if (updated.roomName != initial?.roomName) {
|
||||
capture(RoomModeration(RoomModeration.Action.ChangePermissionsRoomName, analyticsMemberRoleForPowerLevel(updated.roomName)))
|
||||
}
|
||||
if (updated.roomAvatar != initial?.roomAvatar) {
|
||||
capture(RoomModeration(RoomModeration.Action.ChangePermissionsRoomAvatar, analyticsMemberRoleForPowerLevel(updated.roomAvatar)))
|
||||
}
|
||||
if (updated.roomTopic != initial?.roomTopic) {
|
||||
capture(RoomModeration(RoomModeration.Action.ChangePermissionsRoomTopic, analyticsMemberRoleForPowerLevel(updated.roomTopic)))
|
||||
}
|
||||
}
|
||||
@@ -80,8 +80,6 @@ class RoomInviteMembersNode @AssistedInject constructor(
|
||||
body = context.getString(CommonStrings.common_unable_to_invite_message),
|
||||
)
|
||||
}
|
||||
|
||||
room.updateMembers()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -30,11 +30,13 @@ class RoomMemberListDataSource @Inject constructor(
|
||||
) {
|
||||
suspend fun search(query: String): List<RoomMember> = withContext(coroutineDispatchers.io) {
|
||||
val roomMembersState = room.membersStateFlow.value
|
||||
val roomMembers = roomMembersState.roomMembers().orEmpty()
|
||||
val activeRoomMembers = roomMembersState.roomMembers()
|
||||
?.filter { it.membership.isActive() }
|
||||
.orEmpty()
|
||||
val filteredMembers = if (query.isBlank()) {
|
||||
roomMembers
|
||||
activeRoomMembers
|
||||
} else {
|
||||
roomMembers.filter { member ->
|
||||
activeRoomMembers.filter { member ->
|
||||
member.userId.value.contains(query, ignoreCase = true) ||
|
||||
member.displayName?.contains(query, ignoreCase = true).orFalse()
|
||||
}
|
||||
|
||||
@@ -84,6 +84,11 @@ class RoomMemberListPresenter @AssistedInject constructor(
|
||||
remember { roomMembersModerationPresenter.dummyState() }
|
||||
}
|
||||
|
||||
// Ensure we load the latest data when entering this screen
|
||||
LaunchedEffect(Unit) {
|
||||
room.updateMembers()
|
||||
}
|
||||
|
||||
LaunchedEffect(membersState) {
|
||||
if (membersState is MatrixRoomMembersState.Unknown) {
|
||||
return@LaunchedEffect
|
||||
|
||||
@@ -67,7 +67,9 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
room.updateMembers()
|
||||
// Update room member info when opening this screen
|
||||
// We don't need to assign the result as it will be automatically propagated by `room.getRoomMemberAsState`
|
||||
room.getUpdatedMember(roomMemberId)
|
||||
}
|
||||
|
||||
fun handleEvents(event: RoomMemberDetailsEvents) {
|
||||
@@ -133,7 +135,7 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
|
||||
.fold(
|
||||
onSuccess = {
|
||||
isBlockedState.value = AsyncData.Success(true)
|
||||
room.updateMembers()
|
||||
room.getUpdatedMember(userId)
|
||||
},
|
||||
onFailure = {
|
||||
isBlockedState.value = AsyncData.Failure(it, false)
|
||||
@@ -147,7 +149,7 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
|
||||
.fold(
|
||||
onSuccess = {
|
||||
isBlockedState.value = AsyncData.Success(false)
|
||||
room.updateMembers()
|
||||
room.getUpdatedMember(userId)
|
||||
},
|
||||
onFailure = {
|
||||
isBlockedState.value = AsyncData.Failure(it, true)
|
||||
|
||||
@@ -25,6 +25,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import im.vector.app.features.analytics.plan.RoomModeration
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
@@ -38,6 +39,7 @@ 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 io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -51,6 +53,7 @@ class DefaultRoomMembersModerationPresenter @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : RoomMembersModerationPresenter {
|
||||
private var selectedMember by mutableStateOf<RoomMember?>(null)
|
||||
|
||||
@@ -150,6 +153,7 @@ class DefaultRoomMembersModerationPresenter @Inject constructor(
|
||||
userId: UserId,
|
||||
kickUserAction: MutableState<AsyncAction<Unit>>,
|
||||
) = runActionAndWaitForMembershipChange(kickUserAction) {
|
||||
analyticsService.capture(RoomModeration(RoomModeration.Action.KickMember))
|
||||
room.kickUser(userId).finally { selectedMember = null }
|
||||
}
|
||||
|
||||
@@ -157,6 +161,7 @@ class DefaultRoomMembersModerationPresenter @Inject constructor(
|
||||
userId: UserId,
|
||||
banUserAction: MutableState<AsyncAction<Unit>>,
|
||||
) = runActionAndWaitForMembershipChange(banUserAction) {
|
||||
analyticsService.capture(RoomModeration(RoomModeration.Action.BanMember))
|
||||
room.banUser(userId).finally { selectedMember = null }
|
||||
}
|
||||
|
||||
@@ -164,6 +169,7 @@ class DefaultRoomMembersModerationPresenter @Inject constructor(
|
||||
userId: UserId,
|
||||
unbanUserAction: MutableState<AsyncAction<Unit>>,
|
||||
) = runActionAndWaitForMembershipChange(unbanUserAction) {
|
||||
analyticsService.capture(RoomModeration(RoomModeration.Action.UnbanMember))
|
||||
room.unbanUser(userId).finally { selectedMember = null }
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,6 @@ package io.element.android.features.roomdetails.impl.rolesandpermissions
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
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
|
||||
@@ -33,10 +30,8 @@ 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
|
||||
@@ -69,22 +64,12 @@ class RolesAndPermissionsNode @AssistedInject constructor(
|
||||
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 }
|
||||
room.roomInfoFlow
|
||||
.filter { info ->
|
||||
info.userPowerLevels[room.sessionId] != RoomMember.Role.ADMIN.powerLevel
|
||||
}
|
||||
.filter { it?.role != RoomMember.Role.ADMIN }
|
||||
.take(1)
|
||||
.onEach { navigateUp() }
|
||||
.collect()
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import im.vector.app.features.analytics.plan.RoomModeration
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
@@ -32,6 +33,7 @@ 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 io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
@@ -39,6 +41,7 @@ import javax.inject.Inject
|
||||
class RolesAndPermissionsPresenter @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : Presenter<RolesAndPermissionsState> {
|
||||
@Composable
|
||||
override fun present(): RolesAndPermissionsState {
|
||||
@@ -100,6 +103,7 @@ class RolesAndPermissionsPresenter @Inject constructor(
|
||||
resetPermissionsAction: MutableState<AsyncAction<Unit>>,
|
||||
) = launch(dispatchers.io) {
|
||||
runUpdatingState(resetPermissionsAction) {
|
||||
analyticsService.capture(RoomModeration(RoomModeration.Action.ResetPermissions))
|
||||
room.resetPowerLevels().map {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ import androidx.compose.runtime.setValue
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.RoomModeration
|
||||
import io.element.android.features.roomdetails.impl.analytics.toAnalyticsMemberRole
|
||||
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
|
||||
@@ -41,6 +43,7 @@ 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 io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -56,6 +59,7 @@ class ChangeRolesPresenter @AssistedInject constructor(
|
||||
@Assisted private val role: RoomMember.Role,
|
||||
private val room: MatrixRoom,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : Presenter<ChangeRolesState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
@@ -197,9 +201,11 @@ class ChangeRolesPresenter @AssistedInject constructor(
|
||||
|
||||
val changes: List<UserRoleChange> = buildList {
|
||||
for (selectedUser in toAdd) {
|
||||
analyticsService.capture(RoomModeration(RoomModeration.Action.ChangeMemberRole, role.toAnalyticsMemberRole()))
|
||||
add(UserRoleChange(selectedUser.userId, role))
|
||||
}
|
||||
for (selectedUser in toRemove) {
|
||||
analyticsService.capture(RoomModeration(RoomModeration.Action.ChangeMemberRole, RoomModeration.Role.User))
|
||||
add(UserRoleChange(selectedUser.userId, RoomMember.Role.USER))
|
||||
}
|
||||
}
|
||||
@@ -210,6 +216,8 @@ class ChangeRolesPresenter @AssistedInject constructor(
|
||||
}
|
||||
.onSuccess {
|
||||
saveState.value = AsyncAction.Success(Unit)
|
||||
// Asynchronously reload the room members
|
||||
launch { room.updateMembers() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,12 @@ import androidx.compose.runtime.setValue
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.features.roomdetails.impl.analytics.trackPermissionChangeAnalytics
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -39,6 +41,7 @@ import kotlinx.coroutines.launch
|
||||
class ChangeRoomPermissionsPresenter @AssistedInject constructor(
|
||||
@Assisted private val section: ChangeRoomPermissionsSection,
|
||||
private val room: MatrixRoom,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : Presenter<ChangeRoomPermissionsState> {
|
||||
companion object {
|
||||
internal fun itemsForSection(section: ChangeRoomPermissionsSection) = when (section) {
|
||||
@@ -135,6 +138,7 @@ class ChangeRoomPermissionsPresenter @AssistedInject constructor(
|
||||
}
|
||||
room.updatePowerLevels(updatedRoomPowerLevels)
|
||||
.onSuccess {
|
||||
analyticsService.trackPermissionChangeAnalytics(initialPermissions, updatedRoomPowerLevels)
|
||||
initialPermissions = currentPermissions
|
||||
saveAction = AsyncAction.Success(Unit)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
<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>
|
||||
@@ -56,27 +58,29 @@
|
||||
<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_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_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_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>
|
||||
<string name="screen_room_member_list_removing_user">"Выдаленне %1$s …"</string>
|
||||
<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_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>
|
||||
@@ -93,11 +97,16 @@
|
||||
<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>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<string name="screen_polls_history_title">"Umfragen"</string>
|
||||
<string name="screen_room_change_permissions_administrators">"Nur Administratoren"</string>
|
||||
<string name="screen_room_change_permissions_ban_people">"Mitglieder sperren"</string>
|
||||
<string name="screen_room_change_permissions_delete_messages">"Nachrichten löschen"</string>
|
||||
<string name="screen_room_change_permissions_delete_messages">"Nachrichten anderer Mitgliedern löschen"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Alle"</string>
|
||||
<string name="screen_room_change_permissions_invite_people">"Personen einladen"</string>
|
||||
<string name="screen_room_change_permissions_member_moderation">"Moderation der Mitglieder"</string>
|
||||
|
||||
@@ -103,9 +103,9 @@
|
||||
<string name="screen_room_roles_and_permissions_messages_and_content">"Messages et contenus"</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_reset">"Réinitialisation des permissions"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_description">"La réinitialisation des permissions entraîne la perte des réglages actuels."</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_title">"Réinitialisation des permissions?"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset">"Réinitialisation des autorisations"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_description">"La réinitialisation des autorisations entraîne la perte des réglages actuels."</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_title">"Réinitialisation des 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>
|
||||
|
||||
@@ -9,7 +9,29 @@
|
||||
<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_administrators">"Hanya admin"</string>
|
||||
<string name="screen_room_change_permissions_ban_people">"Cekal orang-orang"</string>
|
||||
<string name="screen_room_change_permissions_delete_messages">"Hapus pesan"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Semua orang"</string>
|
||||
<string name="screen_room_change_permissions_invite_people">"Undang orang-orang"</string>
|
||||
<string name="screen_room_change_permissions_member_moderation">"Moderasi anggota"</string>
|
||||
<string name="screen_room_change_permissions_messages_and_content">"Pesan dan konten"</string>
|
||||
<string name="screen_room_change_permissions_moderators">"Admin dan moderator"</string>
|
||||
<string name="screen_room_change_permissions_remove_people">"Keluarkan orang-orang"</string>
|
||||
<string name="screen_room_change_permissions_room_avatar">"Ubah avatar ruangan"</string>
|
||||
<string name="screen_room_change_permissions_room_details">"Detail ruangan"</string>
|
||||
<string name="screen_room_change_permissions_room_name">"Ubah nama ruangan"</string>
|
||||
<string name="screen_room_change_permissions_room_topic">"Ubah topik ruangan"</string>
|
||||
<string name="screen_room_change_permissions_send_messages">"Kirim pesan"</string>
|
||||
<string name="screen_room_change_role_administrators_title">"Sunting Admin"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"Anda tidak akan dapat mengurungkan tindakan ini. Anda mempromosikan pengguna untuk memiliki tingkat daya yang sama seperti Anda."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Tambahkan Admin?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Turunkan"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"Anda tidak akan dapat mengurungkan perubahan ini karena Anda sedang menurunkan Anda sendiri, jika Anda merupakan pengguna dengan hak khusus dalam ruangan maka tidak akan memungkinkan untuk mendapatkan hak tersebut lagi."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Turunkan Anda sendiri?"</string>
|
||||
<string name="screen_room_change_role_moderators_title">"Sunting Moderator"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_description">"Anda memiliki perubahan yang belum disimpan."</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_title">"Simpan perubahan?"</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>
|
||||
@@ -22,20 +44,42 @@
|
||||
<string name="screen_room_details_error_muting">"Gagal membisukan ruangan ini, silakan coba lagi."</string>
|
||||
<string name="screen_room_details_error_unmuting">"Gagal membunyikan ruangan ini, silakan coba lagi."</string>
|
||||
<string name="screen_room_details_invite_people_title">"Undang orang-orang"</string>
|
||||
<string name="screen_room_details_leave_conversation_title">"Tinggalkan percakapan"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Tinggalkan ruangan"</string>
|
||||
<string name="screen_room_details_notification_mode_custom">"Khusus"</string>
|
||||
<string name="screen_room_details_notification_mode_default">"Bawaan"</string>
|
||||
<string name="screen_room_details_notification_title">"Pemberitahuan"</string>
|
||||
<string name="screen_room_details_roles_and_permissions">"Peran dan perizinan"</string>
|
||||
<string name="screen_room_details_room_name_label">"Nama ruangan"</string>
|
||||
<string name="screen_room_details_security_title">"Keamanan"</string>
|
||||
<string name="screen_room_details_share_room_title">"Bagikan ruangan"</string>
|
||||
<string name="screen_room_details_topic_title">"Topik"</string>
|
||||
<string name="screen_room_details_updating_room">"Memperbarui ruangan…"</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_action">"Cekal"</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_description">"Mereka tidak akan dapat bergabung ke ruangan ini lagi jika diundang."</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_title">"Apakah Anda yakin ingin mencekal anggota ini?"</string>
|
||||
<string name="screen_room_member_list_banned_empty">"Tidak ada pengguna yang dicekal dalam ruangan ini."</string>
|
||||
<string name="screen_room_member_list_banning_user">"Mencekal %1$s"</string>
|
||||
<plurals name="screen_room_member_list_header_title">
|
||||
<item quantity="other">"%1$d orang"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_member_list_manage_member_ban">"Keluarkan dan cekal anggota"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove">"Keluarkan dari ruangan"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_ban">"Keluarkan dan cekal anggota"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_kick">"Hanya keluarkan anggota"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_title">"Keluarkan pengguna dan cekal pengguna bergabung lagi di masa mendatang?"</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_action">"Batalkan pencekalan"</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_message">"Pengguna dapat bergabung ke ruangan ini lagi jika diundang."</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_title">"Batalkan pencekalan pengguna"</string>
|
||||
<string name="screen_room_member_list_manage_member_user_info">"Tampilkan profil"</string>
|
||||
<string name="screen_room_member_list_mode_banned">"Tercekal"</string>
|
||||
<string name="screen_room_member_list_mode_members">"Anggota"</string>
|
||||
<string name="screen_room_member_list_pending_header_title">"Tertunda"</string>
|
||||
<string name="screen_room_member_list_removing_user">"Mengeluarkan %1$s…"</string>
|
||||
<string name="screen_room_member_list_role_administrator">"Admin"</string>
|
||||
<string name="screen_room_member_list_role_moderator">"Moderator"</string>
|
||||
<string name="screen_room_member_list_room_members_header_title">"Anggota ruangan"</string>
|
||||
<string name="screen_room_member_list_unbanning_user">"Membatalkan cekalan %1$s"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom">"Izinkan pengaturan khusus"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom_footnote">"Mengaktifkan ini akan mengganti pengaturan bawaan Anda"</string>
|
||||
<string name="screen_room_notification_settings_custom_settings_title">"Beri tahu saya di obrolan ini tentang"</string>
|
||||
@@ -50,5 +94,19 @@
|
||||
<string name="screen_room_notification_settings_mode_all_messages">"Semua pesan"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Sebutan dan Kata Kunci saja"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"Di ruangan ini, beri tahu saya tentang"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Admin"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_my_role">"Ubah peran saya"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_member">"Turunkan ke anggota"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"Turunkan ke moderator"</string>
|
||||
<string name="screen_room_roles_and_permissions_member_moderation">"Moderasi anggota"</string>
|
||||
<string name="screen_room_roles_and_permissions_messages_and_content">"Pesan dan konten"</string>
|
||||
<string name="screen_room_roles_and_permissions_moderators">"Moderator"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Perizinan"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset">"Atur ulang perizinan"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Setelah Anda mengatur ulang perizinan, Anda akan kehilangan pengaturan Anda saat ini."</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_title">"Atur ulang perizinan?"</string>
|
||||
<string name="screen_room_roles_and_permissions_roles_header">"Peran"</string>
|
||||
<string name="screen_room_roles_and_permissions_room_details">"Detail ruangan"</string>
|
||||
<string name="screen_room_roles_and_permissions_title">"Peran dan perizinan"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Terjadi kesalahan saat mencoba memulai obrolan"</string>
|
||||
</resources>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<string name="screen_room_details_error_muting">"無法關閉聊天室通知,請再試一次。"</string>
|
||||
<string name="screen_room_details_error_unmuting">"無法開啟聊天室通知,請再試一次。"</string>
|
||||
<string name="screen_room_details_invite_people_title">"邀請夥伴"</string>
|
||||
<string name="screen_room_details_leave_conversation_title">"離開對話"</string>
|
||||
<string name="screen_room_details_leave_room_title">"離開聊天室"</string>
|
||||
<string name="screen_room_details_notification_mode_custom">"自訂"</string>
|
||||
<string name="screen_room_details_notification_mode_default">"預設"</string>
|
||||
|
||||
@@ -223,7 +223,7 @@ private class FakeRoomMemberListNavigator : RoomMemberListNavigator {
|
||||
var openRoomMemberDetailsCallCount = 0
|
||||
private set
|
||||
|
||||
override fun openRoomMemberDetails(userId: UserId) {
|
||||
override fun openRoomMemberDetails(roomMemberId: UserId) {
|
||||
openRoomMemberDetailsCallCount++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.RoomModeration
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||
import io.element.android.features.roomdetails.impl.members.aVictor
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.DefaultRoomMembersModerationPresenter
|
||||
@@ -36,6 +37,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
@@ -150,13 +152,14 @@ class DefaultRoomMembersModerationPresenterTests {
|
||||
|
||||
@Test
|
||||
fun `present - Kick removes the user`() = runTest {
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenCanKickResult(Result.success(true))
|
||||
givenCanBanResult(Result.success(true))
|
||||
givenUserRoleResult(Result.success(RoomMember.Role.ADMIN))
|
||||
}
|
||||
val selectedMember = aVictor()
|
||||
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room)
|
||||
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -170,18 +173,20 @@ class DefaultRoomMembersModerationPresenterTests {
|
||||
assertThat(kickUserAsyncAction).isEqualTo(AsyncAction.Success(Unit))
|
||||
assertThat(selectedRoomMember).isNull()
|
||||
}
|
||||
assertThat(analyticsService.capturedEvents.last()).isEqualTo(RoomModeration(RoomModeration.Action.KickMember))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - BanUser requires confirmation and then bans the user`() = runTest {
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenCanKickResult(Result.success(true))
|
||||
givenCanBanResult(Result.success(true))
|
||||
givenUserRoleResult(Result.success(RoomMember.Role.ADMIN))
|
||||
}
|
||||
val selectedMember = aVictor()
|
||||
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room)
|
||||
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -200,11 +205,13 @@ class DefaultRoomMembersModerationPresenterTests {
|
||||
assertThat(banUserAsyncAction).isEqualTo(AsyncAction.Success(Unit))
|
||||
assertThat(selectedRoomMember).isNull()
|
||||
}
|
||||
assertThat(analyticsService.capturedEvents.last()).isEqualTo(RoomModeration(RoomModeration.Action.BanMember))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - UnbanUser requires confirmation and then unbans the user`() = runTest {
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val selectedMember = aRoomMember(A_USER_ID_2, membership = RoomMembershipState.BAN)
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenCanKickResult(Result.success(true))
|
||||
@@ -212,7 +219,7 @@ class DefaultRoomMembersModerationPresenterTests {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(selectedMember)))
|
||||
givenUserRoleResult(Result.success(RoomMember.Role.ADMIN))
|
||||
}
|
||||
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room)
|
||||
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -227,6 +234,7 @@ class DefaultRoomMembersModerationPresenterTests {
|
||||
assertThat(unbanUserAsyncAction).isEqualTo(AsyncAction.Success(Unit))
|
||||
assertThat(selectedRoomMember).isNull()
|
||||
}
|
||||
assertThat(analyticsService.capturedEvents.last()).isEqualTo(RoomModeration(RoomModeration.Action.UnbanMember))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,11 +311,13 @@ class DefaultRoomMembersModerationPresenterTests {
|
||||
matrixRoom: FakeMatrixRoom = FakeMatrixRoom(),
|
||||
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.RoomModeration.key to true)),
|
||||
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
||||
): DefaultRoomMembersModerationPresenter {
|
||||
return DefaultRoomMembersModerationPresenter(
|
||||
room = matrixRoom,
|
||||
featureFlagService = featureFlagService,
|
||||
dispatchers = dispatchers,
|
||||
analyticsService = analyticsService,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,14 @@ import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.RoomModeration
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsEvents
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsPresenter
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
@@ -120,7 +122,8 @@ class RolesAndPermissionPresenterTests {
|
||||
|
||||
@Test
|
||||
fun `present - ResetPermissions needs confirmation, then resets permissions`() = runTest {
|
||||
val presenter = createRolesAndPermissionsPresenter()
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val presenter = createRolesAndPermissionsPresenter(analyticsService = analyticsService)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -131,6 +134,7 @@ class RolesAndPermissionPresenterTests {
|
||||
|
||||
assertThat(awaitItem().resetPermissionsAction).isEqualTo(AsyncAction.Loading)
|
||||
assertThat(awaitItem().resetPermissionsAction).isEqualTo(AsyncAction.Success(Unit))
|
||||
assertThat(analyticsService.capturedEvents.last()).isEqualTo(RoomModeration(RoomModeration.Action.ResetPermissions))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +155,12 @@ class RolesAndPermissionPresenterTests {
|
||||
private fun TestScope.createRolesAndPermissionsPresenter(
|
||||
room: FakeMatrixRoom = FakeMatrixRoom(),
|
||||
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||
analyticsService: FakeAnalyticsService = FakeAnalyticsService()
|
||||
): RolesAndPermissionsPresenter {
|
||||
return RolesAndPermissionsPresenter(room = room, dispatchers = dispatchers)
|
||||
return RolesAndPermissionsPresenter(
|
||||
room = room,
|
||||
dispatchers = dispatchers,
|
||||
analyticsService = analyticsService
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.RoomModeration
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMemberList
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesEvent
|
||||
@@ -33,6 +34,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
@@ -315,11 +317,16 @@ class ChangeRolesPresenterTests {
|
||||
|
||||
@Test
|
||||
fun `present - Save will just save the data for moderators`() = runTest {
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 50)))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(role = RoomMember.Role.MODERATOR, room = room)
|
||||
val presenter = createChangeRolesPresenter(
|
||||
role = RoomMember.Role.MODERATOR,
|
||||
room = room,
|
||||
analyticsService = analyticsService
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -331,6 +338,7 @@ class ChangeRolesPresenterTests {
|
||||
|
||||
awaitItem().eventSink(ChangeRolesEvent.Save)
|
||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(Unit))
|
||||
assertThat(analyticsService.capturedEvents.last()).isEqualTo(RoomModeration(RoomModeration.Action.ChangeMemberRole, RoomModeration.Role.Moderator))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,11 +372,13 @@ class ChangeRolesPresenterTests {
|
||||
role: RoomMember.Role = RoomMember.Role.ADMIN,
|
||||
room: FakeMatrixRoom = FakeMatrixRoom(),
|
||||
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
||||
): ChangeRolesPresenter {
|
||||
return ChangeRolesPresenter(
|
||||
role = role,
|
||||
room = room,
|
||||
dispatchers = dispatchers,
|
||||
analyticsService = analyticsService,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import app.cash.turbine.Event
|
||||
import app.cash.turbine.TurbineTestContext
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.RoomModeration
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsEvent
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsPresenter
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsSection
|
||||
@@ -30,9 +31,11 @@ import io.element.android.features.roomdetails.impl.rolesandpermissions.permissi
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember.Role.ADMIN
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember.Role.MODERATOR
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember.Role.USER
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.defaultRoomPowerLevels
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@@ -160,7 +163,8 @@ class ChangeRoomPermissionsPresenterTests {
|
||||
|
||||
@Test
|
||||
fun `present - Save updates the current permissions and resets hasChanges`() = runTest {
|
||||
val presenter = createChangeRoomPermissionsPresenter()
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val presenter = createChangeRoomPermissionsPresenter(analyticsService = analyticsService)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -169,6 +173,14 @@ class ChangeRoomPermissionsPresenterTests {
|
||||
assertThat(state.hasChanges).isFalse()
|
||||
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, MODERATOR))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_AVATAR, MODERATOR))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_TOPIC, MODERATOR))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.SEND_EVENTS, MODERATOR))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.REDACT_EVENTS, USER))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.KICK, ADMIN))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.BAN, ADMIN))
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.INVITE, ADMIN))
|
||||
skipItems(7)
|
||||
assertThat(awaitItem().hasChanges).isTrue()
|
||||
|
||||
state.eventSink(ChangeRoomPermissionsEvent.Save)
|
||||
@@ -179,6 +191,18 @@ class ChangeRoomPermissionsPresenterTests {
|
||||
assertThat(currentPermissions?.roomName).isEqualTo(MODERATOR.powerLevel)
|
||||
assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit))
|
||||
}
|
||||
assertThat(analyticsService.capturedEvents).containsExactlyElementsIn(
|
||||
listOf(
|
||||
RoomModeration(RoomModeration.Action.ChangePermissionsRoomName, RoomModeration.Role.Moderator),
|
||||
RoomModeration(RoomModeration.Action.ChangePermissionsRoomAvatar, RoomModeration.Role.Moderator),
|
||||
RoomModeration(RoomModeration.Action.ChangePermissionsRoomTopic, RoomModeration.Role.Moderator),
|
||||
RoomModeration(RoomModeration.Action.ChangePermissionsSendMessages, RoomModeration.Role.Moderator),
|
||||
RoomModeration(RoomModeration.Action.ChangePermissionsRedactMessages, RoomModeration.Role.User),
|
||||
RoomModeration(RoomModeration.Action.ChangePermissionsKickMembers, RoomModeration.Role.Administrator),
|
||||
RoomModeration(RoomModeration.Action.ChangePermissionsBanMembers, RoomModeration.Role.Administrator),
|
||||
RoomModeration(RoomModeration.Action.ChangePermissionsInviteUsers, RoomModeration.Role.Administrator),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,9 +293,11 @@ class ChangeRoomPermissionsPresenterTests {
|
||||
private fun createChangeRoomPermissionsPresenter(
|
||||
section: ChangeRoomPermissionsSection = ChangeRoomPermissionsSection.RoomDetails,
|
||||
room: FakeMatrixRoom = FakeMatrixRoom(),
|
||||
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
||||
) = ChangeRoomPermissionsPresenter(
|
||||
section = section,
|
||||
room = room,
|
||||
analyticsService = analyticsService,
|
||||
)
|
||||
|
||||
private fun defaultPermissions() = defaultRoomPowerLevels().run {
|
||||
|
||||
@@ -58,8 +58,6 @@ import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
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 io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
@@ -101,16 +99,15 @@ class RoomListPresenter @Inject constructor(
|
||||
override fun present(): RoomListState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val leaveRoomState = leaveRoomPresenter.present()
|
||||
val matrixUser: MutableState<MatrixUser?> = rememberSaveable {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
val matrixUser = client.userProfile.collectAsState()
|
||||
val networkConnectionStatus by networkMonitor.connectivity.collectAsState()
|
||||
val filtersState = filtersPresenter.present()
|
||||
val searchState = searchPresenter.present()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
roomListDataSource.launchIn(this)
|
||||
initialLoad(matrixUser)
|
||||
// Force a refresh of the profile
|
||||
client.getUserProfile()
|
||||
}
|
||||
|
||||
var securityBannerDismissed by rememberSaveable { mutableStateOf(false) }
|
||||
@@ -157,10 +154,6 @@ class RoomListPresenter @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.initialLoad(matrixUser: MutableState<MatrixUser?>) = launch {
|
||||
matrixUser.value = client.getCurrentUser()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun securityBannerState(
|
||||
securityBannerDismissed: Boolean,
|
||||
|
||||
@@ -28,7 +28,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
data class RoomListState(
|
||||
val matrixUser: MatrixUser?,
|
||||
val matrixUser: MatrixUser,
|
||||
val showAvatarIndicator: Boolean,
|
||||
val hasNetworkConnection: Boolean,
|
||||
val snackbarMessage: SnackbarMessage?,
|
||||
|
||||
@@ -49,14 +49,14 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
||||
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)),
|
||||
aRoomListState(contentState = anEmptyContentState()),
|
||||
aRoomListState(contentState = aSkeletonContentState()),
|
||||
aRoomListState(matrixUser = null, contentState = aMigrationContentState()),
|
||||
aRoomListState(matrixUser = MatrixUser(userId = UserId("@id:domain")), contentState = aMigrationContentState()),
|
||||
aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
|
||||
aRoomListState(filtersState = aRoomListFiltersState(isFeatureEnabled = true)),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aRoomListState(
|
||||
matrixUser: MatrixUser? = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"),
|
||||
matrixUser: MatrixUser = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"),
|
||||
showAvatarIndicator: Boolean = false,
|
||||
hasNetworkConnection: Boolean = true,
|
||||
snackbarMessage: SnackbarMessage? = null,
|
||||
|
||||
@@ -21,10 +21,8 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
@@ -73,7 +71,6 @@ import io.element.android.libraries.designsystem.theme.components.HorizontalDivi
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
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.user.MatrixUser
|
||||
@@ -87,7 +84,7 @@ private val avatarBloomSize = 430.dp
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RoomListTopBar(
|
||||
matrixUser: MatrixUser?,
|
||||
matrixUser: MatrixUser,
|
||||
showAvatarIndicator: Boolean,
|
||||
areSearchResultsDisplayed: Boolean,
|
||||
onToggleSearch: () -> Unit,
|
||||
@@ -117,7 +114,7 @@ fun RoomListTopBar(
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun DefaultRoomListTopBar(
|
||||
matrixUser: MatrixUser?,
|
||||
matrixUser: MatrixUser,
|
||||
showAvatarIndicator: Boolean,
|
||||
areSearchResultsDisplayed: Boolean,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
@@ -142,7 +139,7 @@ private fun DefaultRoomListTopBar(
|
||||
|
||||
val avatarData by remember(matrixUser) {
|
||||
derivedStateOf {
|
||||
matrixUser?.getAvatarData(size = AvatarSize.CurrentUserTopBar)
|
||||
matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +292,7 @@ private fun DefaultRoomListTopBar(
|
||||
|
||||
@Composable
|
||||
private fun NavigationIcon(
|
||||
avatarData: AvatarData?,
|
||||
avatarData: AvatarData,
|
||||
showAvatarIndicator: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
@@ -304,20 +301,10 @@ private fun NavigationIcon(
|
||||
onClick = onClick,
|
||||
) {
|
||||
Box {
|
||||
if (avatarData != null) {
|
||||
Avatar(
|
||||
avatarData = avatarData,
|
||||
contentDescription = stringResource(CommonStrings.common_settings),
|
||||
)
|
||||
} else {
|
||||
// Placeholder avatar until the avatarData is available
|
||||
Surface(
|
||||
modifier = Modifier.size(AvatarSize.CurrentUserTopBar.dp),
|
||||
shape = CircleShape,
|
||||
color = ElementTheme.colors.iconSecondary,
|
||||
content = {}
|
||||
)
|
||||
}
|
||||
Avatar(
|
||||
avatarData = avatarData,
|
||||
contentDescription = stringResource(CommonStrings.common_settings),
|
||||
)
|
||||
if (showAvatarIndicator) {
|
||||
RedIndicatorAtom(
|
||||
modifier = Modifier.align(Alignment.TopEnd)
|
||||
|
||||
@@ -8,13 +8,23 @@
|
||||
<string name="screen_roomlist_empty_message">"Пачніце з паведамлення каму-небудзь."</string>
|
||||
<string name="screen_roomlist_empty_title">"Пакуль няма чатаў."</string>
|
||||
<string name="screen_roomlist_filter_favourites">"Абранае"</string>
|
||||
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Дадаць чат у абранае можна ў наладах чата.
|
||||
На дадзены момант вы можаце прыбраць фільтры, каб убачыць іншыя вашыя чаты."</string>
|
||||
<string name="screen_roomlist_filter_favourites_empty_state_title">"У вас пакуль няма абраных чатаў"</string>
|
||||
<string name="screen_roomlist_filter_low_priority">"Нізкі прыярытэт"</string>
|
||||
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Вы можаце прыбраць фільтры, каб убачыць іншыя вашыя чаты."</string>
|
||||
<string name="screen_roomlist_filter_mixed_empty_state_title">"У вас няма чатаў для гэтай катэгорыі"</string>
|
||||
<string name="screen_roomlist_filter_people">"Людзі"</string>
|
||||
<string name="screen_roomlist_filter_people_empty_state_title">"У вас пакуль няма асабістых паведамленняў"</string>
|
||||
<string name="screen_roomlist_filter_rooms">"Пакоі"</string>
|
||||
<string name="screen_roomlist_filter_rooms_empty_state_title">"Вас пакуль няма ў ніводным пакоі"</string>
|
||||
<string name="screen_roomlist_filter_unreads">"Непрачытаныя"</string>
|
||||
<string name="screen_roomlist_filter_unreads_empty_state_title">"Віншуем!
|
||||
У вас няма непрачытаных паведамленняў!"</string>
|
||||
<string name="screen_roomlist_main_space_title">"Усе чаты"</string>
|
||||
<string name="screen_roomlist_mark_as_read">"Пазначыць як прачытанае"</string>
|
||||
<string name="screen_roomlist_mark_as_unread">"Пазначыць як непрачытанае"</string>
|
||||
<string name="screen_roomlist_room_directory_button_title">"Праглядзець усе пакоі"</string>
|
||||
<string name="session_verification_banner_message">"Здаецца, вы карыстаецеся новай прыладай. Праверце з дапамогай іншай прылады, каб атрымаць доступ да зашыфраваных паведамленняў."</string>
|
||||
<string name="session_verification_banner_title">"Пацвердзіце, што гэта вы"</string>
|
||||
</resources>
|
||||
|
||||
@@ -24,6 +24,7 @@ Nemáte žádné nepřečtené zprávy!"</string>
|
||||
<string name="screen_roomlist_main_space_title">"Všechny chaty"</string>
|
||||
<string name="screen_roomlist_mark_as_read">"Označit jako přečtené"</string>
|
||||
<string name="screen_roomlist_mark_as_unread">"Označit jako nepřečtené"</string>
|
||||
<string name="screen_roomlist_room_directory_button_title">"Procházet všechny místnosti"</string>
|
||||
<string name="session_verification_banner_message">"Zdá se, že používáte nové zařízení. Ověřte přihlášení, abyste měli přístup k zašifrovaným zprávám."</string>
|
||||
<string name="session_verification_banner_title">"Ověřte, že jste to vy"</string>
|
||||
</resources>
|
||||
|
||||
@@ -24,6 +24,7 @@ Du hast keine ungelesenen Nachrichten!"</string>
|
||||
<string name="screen_roomlist_main_space_title">"Chats"</string>
|
||||
<string name="screen_roomlist_mark_as_read">"Als gelesen markieren"</string>
|
||||
<string name="screen_roomlist_mark_as_unread">"Als ungelesen markieren"</string>
|
||||
<string name="screen_roomlist_room_directory_button_title">"Alle Räume durchsuchen"</string>
|
||||
<string name="session_verification_banner_message">"Es sieht aus, als würdest du ein neues Gerät verwenden. Verifiziere es mit einem anderen Gerät, damit du auf deine verschlüsselten Nachrichten zugreifen kannst."</string>
|
||||
<string name="session_verification_banner_title">"Bestätige deine Identität"</string>
|
||||
</resources>
|
||||
|
||||
@@ -24,6 +24,7 @@ Vous n’avez plus de messages non-lus!"</string>
|
||||
<string name="screen_roomlist_main_space_title">"Conversations"</string>
|
||||
<string name="screen_roomlist_mark_as_read">"Marquer comme lu"</string>
|
||||
<string name="screen_roomlist_mark_as_unread">"Marquer comme non lu"</string>
|
||||
<string name="screen_roomlist_room_directory_button_title">"Parcourir tous les salons"</string>
|
||||
<string name="session_verification_banner_message">"Il semblerait que vous utilisiez un nouvel appareil. Vérifiez la session avec un autre de vos appareils pour accéder à vos messages chiffrés."</string>
|
||||
<string name="session_verification_banner_title">"Vérifier que c’est bien vous"</string>
|
||||
</resources>
|
||||
|
||||
@@ -7,8 +7,23 @@
|
||||
<string name="screen_roomlist_a11y_create_message">"Buat percakapan atau ruangan baru"</string>
|
||||
<string name="screen_roomlist_empty_message">"Mulailah dengan mengirim pesan kepada seseorang."</string>
|
||||
<string name="screen_roomlist_empty_title">"Belum ada obrolan."</string>
|
||||
<string name="screen_roomlist_filter_favourites">"Favorit"</string>
|
||||
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Anda dapat menambahkan percakapan ke favorit Anda dalam pengaturan percakapan.
|
||||
Untuk sementara, Anda dapat membatalkan pilihan saringan untuk melihat percakapan Anda yang lain"</string>
|
||||
<string name="screen_roomlist_filter_favourites_empty_state_title">"Anda belum memiliki percakapan favorit"</string>
|
||||
<string name="screen_roomlist_filter_low_priority">"Prioritas Rendah"</string>
|
||||
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Anda dapat membatalkan pilihan saringan untuk melihat percakapan Anda yang lain"</string>
|
||||
<string name="screen_roomlist_filter_mixed_empty_state_title">"Anda tidak memiliki percakapan untuk pemilihan ini"</string>
|
||||
<string name="screen_roomlist_filter_people">"Orang"</string>
|
||||
<string name="screen_roomlist_filter_people_empty_state_title">"Anda belum memiliki percakapan langsung"</string>
|
||||
<string name="screen_roomlist_filter_rooms">"Ruangan"</string>
|
||||
<string name="screen_roomlist_filter_rooms_empty_state_title">"Anda belum berada dalam ruangan"</string>
|
||||
<string name="screen_roomlist_filter_unreads">"Belum dibaca"</string>
|
||||
<string name="screen_roomlist_filter_unreads_empty_state_title">"Selamat!
|
||||
Anda tidak memiliki pesan yang belum dibaca!"</string>
|
||||
<string name="screen_roomlist_main_space_title">"Semua Obrolan"</string>
|
||||
<string name="screen_roomlist_mark_as_read">"Tandai sebagai dibaca"</string>
|
||||
<string name="screen_roomlist_mark_as_unread">"Tandai sebagai belum dibaca"</string>
|
||||
<string name="session_verification_banner_message">"Sepertinya Anda menggunakan perangkat baru. Verifikasi dengan perangkat lain untuk mengakses pesan terenkripsi Anda selanjutnya."</string>
|
||||
<string name="session_verification_banner_title">"Verifikasi bahwa ini Anda"</string>
|
||||
</resources>
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<string name="screen_roomlist_main_space_title">"Все чаты"</string>
|
||||
<string name="screen_roomlist_mark_as_read">"Пометить как прочитанное"</string>
|
||||
<string name="screen_roomlist_mark_as_unread">"Пометить как непрочитанное"</string>
|
||||
<string name="screen_roomlist_room_directory_button_title">"Просмотреть все комнаты"</string>
|
||||
<string name="session_verification_banner_message">"Похоже, вы используете новое устройство. Чтобы получить доступ к зашифрованным сообщениям пройдите верификацию с другим устройством."</string>
|
||||
<string name="session_verification_banner_title">"Подтвердите, что это вы"</string>
|
||||
</resources>
|
||||
|
||||
@@ -24,6 +24,7 @@ Nemáte žiadne neprečítané správy!"</string>
|
||||
<string name="screen_roomlist_main_space_title">"Všetky konverzácie"</string>
|
||||
<string name="screen_roomlist_mark_as_read">"Označiť ako prečítané"</string>
|
||||
<string name="screen_roomlist_mark_as_unread">"Označiť ako neprečítané"</string>
|
||||
<string name="screen_roomlist_room_directory_button_title">"Prehliadať všetky miestnosti"</string>
|
||||
<string name="session_verification_banner_message">"Vyzerá to tak, že používate nové zariadenie. Overte svoj prístup k zašifrovaným správam pomocou vášho druhého zariadenia."</string>
|
||||
<string name="session_verification_banner_title">"Overte, že ste to vy"</string>
|
||||
</resources>
|
||||
|
||||
@@ -24,6 +24,7 @@ You don’t have any unread messages!"</string>
|
||||
<string name="screen_roomlist_main_space_title">"Chats"</string>
|
||||
<string name="screen_roomlist_mark_as_read">"Mark as read"</string>
|
||||
<string name="screen_roomlist_mark_as_unread">"Mark as unread"</string>
|
||||
<string name="screen_roomlist_room_directory_button_title">"Browse all rooms"</string>
|
||||
<string name="session_verification_banner_message">"Looks like you’re using a new device. Verify with another device to access your encrypted messages."</string>
|
||||
<string name="session_verification_banner_title">"Verify it’s you"</string>
|
||||
</resources>
|
||||
|
||||
@@ -56,6 +56,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
@@ -93,17 +94,24 @@ class RoomListPresenterTests {
|
||||
@Test
|
||||
fun `present - should start with no user and then load user with success`() = runTest {
|
||||
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||
val presenter = createRoomListPresenter(coroutineScope = scope)
|
||||
val matrixClient = FakeMatrixClient(
|
||||
userDisplayName = null,
|
||||
userAvatarUrl = null,
|
||||
)
|
||||
matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.success(MatrixUser(matrixClient.sessionId, A_USER_NAME, AN_AVATAR_URL)))
|
||||
val presenter = createRoomListPresenter(
|
||||
client = matrixClient,
|
||||
coroutineScope = scope,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.matrixUser).isNull()
|
||||
assertThat(initialState.matrixUser).isEqualTo(MatrixUser(A_USER_ID))
|
||||
val withUserState = awaitItem()
|
||||
assertThat(withUserState.matrixUser).isNotNull()
|
||||
assertThat(withUserState.matrixUser!!.userId).isEqualTo(A_USER_ID)
|
||||
assertThat(withUserState.matrixUser!!.displayName).isEqualTo(A_USER_NAME)
|
||||
assertThat(withUserState.matrixUser!!.avatarUrl).isEqualTo(AN_AVATAR_URL)
|
||||
assertThat(withUserState.matrixUser.userId).isEqualTo(A_USER_ID)
|
||||
assertThat(withUserState.matrixUser.displayName).isEqualTo(A_USER_NAME)
|
||||
assertThat(withUserState.matrixUser.avatarUrl).isEqualTo(AN_AVATAR_URL)
|
||||
assertThat(withUserState.showAvatarIndicator).isTrue()
|
||||
scope.cancel()
|
||||
}
|
||||
@@ -128,7 +136,6 @@ class RoomListPresenterTests {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.showAvatarIndicator).isTrue()
|
||||
sessionVerificationService.givenCanVerifySession(false)
|
||||
assertThat(awaitItem().showAvatarIndicator).isTrue()
|
||||
encryptionService.emitBackupState(BackupState.ENABLED)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.showAvatarIndicator).isFalse()
|
||||
@@ -139,19 +146,18 @@ class RoomListPresenterTests {
|
||||
@Test
|
||||
fun `present - should start with no user and then load user with error`() = runTest {
|
||||
val matrixClient = FakeMatrixClient(
|
||||
userDisplayName = Result.failure(AN_EXCEPTION),
|
||||
userAvatarUrl = Result.failure(AN_EXCEPTION),
|
||||
userDisplayName = null,
|
||||
userAvatarUrl = null,
|
||||
)
|
||||
matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.failure(AN_EXCEPTION))
|
||||
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||
val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.matrixUser).isNull()
|
||||
val withUserState = awaitItem()
|
||||
assertThat(withUserState.matrixUser).isNotNull()
|
||||
scope.cancel()
|
||||
assertThat(initialState.matrixUser).isEqualTo(MatrixUser(matrixClient.sessionId))
|
||||
// No new state is coming
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +370,6 @@ class RoomListPresenterTests {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
val summary = createRoomListRoomSummary()
|
||||
initialState.eventSink(RoomListEvents.ShowContextMenu(summary))
|
||||
@@ -414,8 +419,6 @@ class RoomListPresenterTests {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
|
||||
val initialState = awaitItem()
|
||||
val summary = createRoomListRoomSummary()
|
||||
initialState.eventSink(RoomListEvents.ShowContextMenu(summary))
|
||||
@@ -473,7 +476,6 @@ class RoomListPresenterTests {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
eventRecorder.assertEmpty()
|
||||
initialState.eventSink(RoomListEvents.ToggleSearchResults)
|
||||
@@ -558,7 +560,6 @@ class RoomListPresenterTests {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
// The migration screen is shown if the migration screen has not been shown before
|
||||
assertThat(initialState.contentState).isInstanceOf(RoomListContentState.Migration::class.java)
|
||||
@@ -585,7 +586,6 @@ class RoomListPresenterTests {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem().contentState).isInstanceOf(RoomListContentState.Empty::class.java)
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_chat_backup_key_backup_action_disable">"Stäng av säkerhetskopiering"</string>
|
||||
<string name="screen_chat_backup_key_backup_action_enable">"Slå på säkerhetskopiering"</string>
|
||||
<string name="screen_chat_backup_key_backup_description">"Säkerhetskopior ser till att du inte blir av med din meddelandehistorik. %1$s."</string>
|
||||
<string name="screen_chat_backup_key_backup_title">"Säkerhetskopia"</string>
|
||||
<string name="screen_chat_backup_recovery_action_setup">"Ställ in återställning"</string>
|
||||
</resources>
|
||||
@@ -6,6 +6,7 @@
|
||||
<string name="screen_session_verification_compare_numbers_subtitle">"Konfirmasikan bahwa angka-angka di bawah ini sesuai dengan yang ditampilkan pada sesi Anda yang lain."</string>
|
||||
<string name="screen_session_verification_compare_numbers_title">"Bandingkan angka"</string>
|
||||
<string name="screen_session_verification_complete_subtitle">"Sesi baru Anda sekarang diverifikasi. Ini memiliki akses ke pesan terenkripsi Anda, dan pengguna lain akan melihatnya sebagai tepercaya."</string>
|
||||
<string name="screen_session_verification_enter_recovery_key">"Masukkan kunci pemulihan"</string>
|
||||
<string name="screen_session_verification_open_existing_session_subtitle">"Buktikan bahwa ini memang Anda untuk mengakses riwayat pesan terenkripsi Anda."</string>
|
||||
<string name="screen_session_verification_open_existing_session_title">"Buka sesi yang sudah ada"</string>
|
||||
<string name="screen_session_verification_positive_button_canceled">"Verifikasi ulang"</string>
|
||||
|
||||
@@ -49,7 +49,7 @@ signing.element.nightly.keyPassword=Secret
|
||||
|
||||
# Customise the Lint version to use a more recent version than the one bundled with AGP
|
||||
# https://googlesamples.github.io/android-custom-lint-rules/usage/newer-lint.md.html
|
||||
android.experimental.lint.version=8.3.0-alpha12
|
||||
android.experimental.lint.version=8.4.0-alpha13
|
||||
|
||||
# Enable test fixture for all modules by default
|
||||
android.experimental.enableTestFixtures=true
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
[versions]
|
||||
# Project
|
||||
android_gradle_plugin = "8.2.2"
|
||||
kotlin = "1.9.22"
|
||||
ksp = "1.9.22-1.0.17"
|
||||
android_gradle_plugin = "8.3.1"
|
||||
kotlin = "1.9.23"
|
||||
ksp = "1.9.23-1.0.19"
|
||||
firebaseAppDistribution = "4.2.0"
|
||||
|
||||
# AndroidX
|
||||
@@ -18,8 +18,8 @@ activity = "1.8.2"
|
||||
media3 = "1.3.0"
|
||||
|
||||
# Compose
|
||||
compose_bom = "2024.02.02"
|
||||
composecompiler = "1.5.10"
|
||||
compose_bom = "2024.03.00"
|
||||
composecompiler = "1.5.11"
|
||||
|
||||
# Coroutines
|
||||
coroutines = "1.8.0"
|
||||
@@ -38,8 +38,8 @@ serialization_json = "1.6.3"
|
||||
showkase = "1.0.2"
|
||||
appyx = "1.4.0"
|
||||
sqldelight = "2.0.1"
|
||||
wysiwyg = "2.33.0"
|
||||
telephoto = "0.8.0"
|
||||
wysiwyg = "2.34.0"
|
||||
telephoto = "0.9.0"
|
||||
|
||||
# DI
|
||||
dagger = "2.51"
|
||||
@@ -63,7 +63,7 @@ kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", v
|
||||
kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" }
|
||||
gms_google_services = "com.google.gms:google-services:4.4.1"
|
||||
# https://firebase.google.com/docs/android/setup#available-libraries
|
||||
google_firebase_bom = "com.google.firebase:firebase-bom:32.7.4"
|
||||
google_firebase_bom = "com.google.firebase:firebase-bom:32.8.0"
|
||||
firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" }
|
||||
autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" }
|
||||
|
||||
@@ -120,8 +120,9 @@ network_okhttp_logging = { module = "com.squareup.okhttp3:logging-interceptor" }
|
||||
network_okhttp_okhttp = { module = "com.squareup.okhttp3:okhttp" }
|
||||
network_okhttp = { module = "com.squareup.okhttp3:okhttp" }
|
||||
network_mockwebserver = { module = "com.squareup.okhttp3:mockwebserver" }
|
||||
network_retrofit = "com.squareup.retrofit2:retrofit:2.9.0"
|
||||
network_retrofit_converter_serialization = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0"
|
||||
network_retrofit_bom = "com.squareup.retrofit2:retrofit-bom:2.10.0"
|
||||
network_retrofit = { module = "com.squareup.retrofit2:retrofit" }
|
||||
network_retrofit_converter_serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization" }
|
||||
|
||||
# Test
|
||||
test_core = { module = "androidx.test:core", version.ref = "test_core" }
|
||||
@@ -153,7 +154,7 @@ jsoup = "org.jsoup:jsoup:1.17.2"
|
||||
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
||||
molecule-runtime = "app.cash.molecule:molecule-runtime:1.4.1"
|
||||
timber = "com.jakewharton.timber:timber:5.0.1"
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.9"
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.12"
|
||||
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
|
||||
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
|
||||
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
|
||||
@@ -163,7 +164,7 @@ sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.4"
|
||||
sqlite = "androidx.sqlite:sqlite-ktx:2.4.0"
|
||||
unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1"
|
||||
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5"
|
||||
vanniktech_blurhash = "com.vanniktech:blurhash:0.2.0"
|
||||
vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
|
||||
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
||||
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
|
||||
statemachine = "com.freeletics.flowredux:compose:1.2.1"
|
||||
@@ -213,10 +214,10 @@ kotlin_serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi
|
||||
kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
anvil = { id = "com.squareup.anvil", version.ref = "anvil" }
|
||||
detekt = "io.gitlab.arturbosch.detekt:1.23.5"
|
||||
detekt = "io.gitlab.arturbosch.detekt:1.23.6"
|
||||
ktlint = "org.jlleitschuh.gradle.ktlint:12.1.0"
|
||||
dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12"
|
||||
dependencycheck = "org.owasp.dependencycheck:9.0.9"
|
||||
dependencycheck = "org.owasp.dependencycheck:9.0.10"
|
||||
dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" }
|
||||
paparazzi = "app.cash.paparazzi:1.3.3"
|
||||
kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
|
||||
|
||||
@@ -20,6 +20,10 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.androidutils"
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
anvil {
|
||||
|
||||
@@ -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.libraries.androidutils.metadata
|
||||
|
||||
import io.element.android.libraries.androidutils.BuildConfig
|
||||
|
||||
/**
|
||||
* true if the app is built in debug mode.
|
||||
* For testing purpose, this can be changed with [withReleaseBehavior].
|
||||
*/
|
||||
var isInDebug: Boolean = BuildConfig.DEBUG
|
||||
private set
|
||||
|
||||
/**
|
||||
* Run the lambda simulating the app is in release mode.
|
||||
*
|
||||
* **IMPORTANT**: this should **ONLY** be used for testing purposes.
|
||||
*/
|
||||
fun withReleaseBehavior(lambda: () -> Unit) {
|
||||
isInDebug = false
|
||||
lambda()
|
||||
isInDebug = BuildConfig.DEBUG
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.components
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.bigCheckmarkBorderColor
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
/**
|
||||
* Compound component that displays a big checkmark centered in a rounded square.
|
||||
*
|
||||
* @param modifier the modifier to apply to this layout
|
||||
*/
|
||||
@Composable
|
||||
fun BigCheckmark(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier.size(120.dp),
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
color = ElementTheme.colors.bgCanvasDefault,
|
||||
border = BorderStroke(1.dp, ElementTheme.colors.bigCheckmarkBorderColor),
|
||||
shadowElevation = 4.dp,
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(
|
||||
modifier = Modifier.size(72.dp),
|
||||
tint = ElementTheme.colors.iconSuccessPrimary,
|
||||
imageVector = CompoundIcons.CheckCircleSolid(),
|
||||
contentDescription = stringResource(CommonStrings.common_success)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun BigCheckmarkPreview() {
|
||||
ElementPreview {
|
||||
Box(
|
||||
modifier = Modifier.padding(10.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
BigCheckmark()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CatchingPokemon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
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.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.bigIconDefaultBackgroundColor
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
/**
|
||||
* Compound component that display a big icon centered in a rounded square.
|
||||
*/
|
||||
object BigIcon {
|
||||
/**
|
||||
* The style of the [BigIcon].
|
||||
*/
|
||||
@Immutable
|
||||
sealed interface Style {
|
||||
/**
|
||||
* The default style.
|
||||
*
|
||||
* @param vectorIcon the [ImageVector] to display
|
||||
* @param contentDescription the content description of the icon, if any. It defaults to `null`
|
||||
*/
|
||||
data class Default(val vectorIcon: ImageVector, val contentDescription: String? = null) : Style
|
||||
|
||||
/**
|
||||
* An alert style with a transparent background.
|
||||
*/
|
||||
data object Alert : Style
|
||||
|
||||
/**
|
||||
* An alert style with a tinted background.
|
||||
*/
|
||||
data object AlertSolid : Style
|
||||
|
||||
/**
|
||||
* A success style with a transparent background.
|
||||
*/
|
||||
data object Success : Style
|
||||
|
||||
/**
|
||||
* A success style with a tinted background.
|
||||
*/
|
||||
data object SuccessSolid : Style
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a [BigIcon].
|
||||
*
|
||||
* @param style the style of the icon
|
||||
* @param modifier the modifier to apply to this layout
|
||||
*/
|
||||
@Composable
|
||||
operator fun invoke(
|
||||
style: Style,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val backgroundColor = when (style) {
|
||||
is Style.Default -> ElementTheme.colors.bigIconDefaultBackgroundColor
|
||||
Style.Alert, Style.Success -> Color.Transparent
|
||||
Style.AlertSolid -> ElementTheme.colors.bgCriticalSubtle
|
||||
Style.SuccessSolid -> ElementTheme.colors.bgSuccessSubtle
|
||||
}
|
||||
val icon = when (style) {
|
||||
is Style.Default -> style.vectorIcon
|
||||
Style.Alert, Style.AlertSolid -> CompoundIcons.Error()
|
||||
Style.Success, Style.SuccessSolid -> CompoundIcons.CheckCircleSolid()
|
||||
}
|
||||
val contentDescription = when (style) {
|
||||
is Style.Default -> style.contentDescription
|
||||
Style.Alert, Style.AlertSolid -> stringResource(CommonStrings.common_error)
|
||||
Style.Success, Style.SuccessSolid -> stringResource(CommonStrings.common_success)
|
||||
}
|
||||
val iconTint = when (style) {
|
||||
is Style.Default -> ElementTheme.colors.iconSecondaryAlpha
|
||||
Style.Alert, Style.AlertSolid -> ElementTheme.colors.iconCriticalPrimary
|
||||
Style.Success, Style.SuccessSolid -> ElementTheme.colors.iconSuccessPrimary
|
||||
}
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(64.dp)
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(backgroundColor),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = iconTint,
|
||||
imageVector = icon,
|
||||
contentDescription = contentDescription
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun BigIconPreview() {
|
||||
ElementPreview {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.padding(10.dp)) {
|
||||
val provider = BigIconStylePreviewProvider()
|
||||
for (style in provider.values) {
|
||||
BigIcon(style = style)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class BigIconStylePreviewProvider : PreviewParameterProvider<BigIcon.Style> {
|
||||
override val values: Sequence<BigIcon.Style>
|
||||
get() = sequenceOf(
|
||||
BigIcon.Style.Default(Icons.Filled.CatchingPokemon),
|
||||
BigIcon.Style.Alert,
|
||||
BigIcon.Style.AlertSolid,
|
||||
BigIcon.Style.Success,
|
||||
BigIcon.Style.SuccessSolid
|
||||
)
|
||||
}
|
||||
@@ -302,7 +302,7 @@ fun Modifier.bloom(
|
||||
/**
|
||||
* Bloom effect modifier for avatars. Applies a bloom effect to the component.
|
||||
* @param avatarData The avatar data to use as the bloom source.
|
||||
* If the avatar data has a URL it will be used as the bloom source, otherwise the initials will be used. If `null` is passed, no bloom effect will be applied.
|
||||
* If the avatar data has a URL it will be used as the bloom source, otherwise the initials will be used.
|
||||
* @param background The background color to use for the bloom effect. Since we use blend modes it must be non-transparent.
|
||||
* @param blurSize The size of the bloom effect. If not specified the bloom effect will be the size of the component.
|
||||
* @param offset The offset to use for the bloom effect. If not specified the bloom effect will be centered on the component.
|
||||
@@ -313,7 +313,7 @@ fun Modifier.bloom(
|
||||
* @param alpha The alpha value to apply to the bloom effect.
|
||||
*/
|
||||
fun Modifier.avatarBloom(
|
||||
avatarData: AvatarData?,
|
||||
avatarData: AvatarData,
|
||||
background: Color,
|
||||
blurSize: DpSize = DpSize.Unspecified,
|
||||
offset: DpOffset = DpOffset.Unspecified,
|
||||
@@ -327,7 +327,6 @@ fun Modifier.avatarBloom(
|
||||
) = composed {
|
||||
// Bloom only works on API 29+
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return@composed this
|
||||
avatarData ?: return@composed this
|
||||
|
||||
// Request the avatar contents to use as the bloom source
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
|
||||
/**
|
||||
* Compound component that displays a big icon, a title, an optional subtitle and an optional call to action component.
|
||||
*
|
||||
* @param title the title to display
|
||||
* @param iconStyle the style of the [BigIcon] to display
|
||||
* @param modifier the modifier to apply to this layout
|
||||
* @param subtitle the optional subtitle to display. It defaults to `null`
|
||||
* @param callToAction the optional call to action component to display. It defaults to `null`
|
||||
*/
|
||||
@Composable
|
||||
fun PageTitle(
|
||||
title: AnnotatedString,
|
||||
iconStyle: BigIcon.Style,
|
||||
modifier: Modifier = Modifier,
|
||||
subtitle: AnnotatedString? = null,
|
||||
callToAction: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 40.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
BigIcon(style = iconStyle)
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = title,
|
||||
style = ElementTheme.typography.fontHeadingMdBold,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
subtitle?.let {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = it,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
callToAction?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compound component that displays a big icon, a title, an optional subtitle and an optional call to action component.
|
||||
*
|
||||
* @param title the title to display
|
||||
* @param iconStyle the style of the [BigIcon] to display
|
||||
* @param modifier the modifier to apply to this layout
|
||||
* @param subtitle the optional subtitle to display. It defaults to `null`
|
||||
* @param callToAction the optional call to action component to display. It defaults to `null`
|
||||
*/
|
||||
@Composable
|
||||
fun PageTitle(
|
||||
title: String,
|
||||
iconStyle: BigIcon.Style,
|
||||
modifier: Modifier = Modifier,
|
||||
subtitle: String? = null,
|
||||
callToAction: @Composable (() -> Unit)? = null,
|
||||
) = PageTitle(
|
||||
title = AnnotatedString(title),
|
||||
iconStyle = iconStyle,
|
||||
modifier = modifier,
|
||||
subtitle = subtitle?.let { AnnotatedString(it) },
|
||||
callToAction = callToAction
|
||||
)
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun TitleWithIconFullPreview(@PreviewParameter(BigIconStylePreviewProvider::class) style: BigIcon.Style) {
|
||||
ElementPreview {
|
||||
PageTitle(
|
||||
modifier = Modifier.padding(top = 24.dp),
|
||||
title = AnnotatedString("Headline"),
|
||||
subtitle = AnnotatedString("Description goes here"),
|
||||
iconStyle = style,
|
||||
callToAction = {
|
||||
TextButton(text = "Learn more", onClick = {})
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun TitleWithIconMinimalPreview() {
|
||||
ElementPreview {
|
||||
PageTitle(
|
||||
modifier = Modifier.padding(top = 24.dp),
|
||||
title = "Headline",
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.CheckCircleSolid()),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
* 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.
|
||||
@@ -14,27 +14,22 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.components
|
||||
package io.element.android.libraries.designsystem.components.blurhash
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import coil.compose.AsyncImage
|
||||
import com.vanniktech.blurhash.BlurHash
|
||||
|
||||
@Composable
|
||||
fun BlurHashAsyncImage(
|
||||
@@ -69,31 +64,3 @@ fun BlurHashAsyncImage(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BlurHashImage(
|
||||
blurHash: String?,
|
||||
contentDescription: String? = null,
|
||||
contentScale: ContentScale = ContentScale.Fit,
|
||||
) {
|
||||
if (blurHash == null) return
|
||||
val bitmapState = remember(blurHash) {
|
||||
mutableStateOf(
|
||||
// Build a small blurhash image so that it's fast
|
||||
BlurHash.decode(blurHash, 10, 10)
|
||||
)
|
||||
}
|
||||
DisposableEffect(blurHash) {
|
||||
onDispose {
|
||||
bitmapState.value?.recycle()
|
||||
}
|
||||
}
|
||||
bitmapState.value?.let { bitmap ->
|
||||
Image(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
bitmap = bitmap.asImageBitmap(),
|
||||
contentScale = contentScale,
|
||||
contentDescription = contentDescription
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.components.blurhash
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
|
||||
fun Modifier.blurHashBackground(blurHash: String?, alpha: Float = 1f) = this.composed {
|
||||
val blurHashBitmap = rememberBlurHashImage(blurHash)
|
||||
if (blurHashBitmap != null) {
|
||||
Modifier.drawBehind {
|
||||
drawImage(blurHashBitmap, dstSize = IntSize(size.width.toInt(), size.height.toInt()), alpha = alpha)
|
||||
}
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.components.blurhash
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import com.vanniktech.blurhash.BlurHash
|
||||
|
||||
@Suppress("ModifierMissing")
|
||||
@Composable
|
||||
fun BlurHashImage(
|
||||
blurHash: String?,
|
||||
contentDescription: String? = null,
|
||||
contentScale: ContentScale = ContentScale.Fit,
|
||||
) {
|
||||
if (blurHash == null) return
|
||||
val blurHashImage = rememberBlurHashImage(blurHash)
|
||||
blurHashImage?.let { bitmap ->
|
||||
Image(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
bitmap = bitmap,
|
||||
contentScale = contentScale,
|
||||
contentDescription = contentDescription
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberBlurHashImage(blurHash: String?): ImageBitmap? {
|
||||
return if (LocalInspectionMode.current) {
|
||||
blurHash?.let { BlurHash.decode(it, 10, 10)?.asImageBitmap() }
|
||||
} else {
|
||||
produceState<ImageBitmap?>(initialValue = null, blurHash) {
|
||||
blurHash?.let { value = BlurHash.decode(it, 10, 10)?.asImageBitmap() }
|
||||
}.value
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,12 @@ package io.element.android.libraries.designsystem.theme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import io.element.android.compound.annotations.CoreColorToken
|
||||
import io.element.android.compound.previews.ColorListPreview
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.SemanticColors
|
||||
import io.element.android.compound.tokens.generated.internal.DarkColorTokens
|
||||
import io.element.android.compound.tokens.generated.internal.LightColorTokens
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
@@ -138,6 +141,14 @@ val SemanticColors.mentionPillBackground
|
||||
Color(0x26f4f7fa)
|
||||
}
|
||||
|
||||
@OptIn(CoreColorToken::class)
|
||||
val SemanticColors.bigIconDefaultBackgroundColor
|
||||
get() = if (isLight) LightColorTokens.colorAlphaGray300 else DarkColorTokens.colorAlphaGray300
|
||||
|
||||
@OptIn(CoreColorToken::class)
|
||||
val SemanticColors.bigCheckmarkBorderColor
|
||||
get() = if (isLight) LightColorTokens.colorGray400 else DarkColorTokens.colorGray400
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun ColorAliasesPreview() = ElementPreview {
|
||||
@@ -155,6 +166,7 @@ internal fun ColorAliasesPreview() = ElementPreview {
|
||||
"progressIndicatorTrackColor" to ElementTheme.colors.progressIndicatorTrackColor,
|
||||
"temporaryColorBgSpecial" to ElementTheme.colors.temporaryColorBgSpecial,
|
||||
"iconSuccessPrimaryBackground" to ElementTheme.colors.iconSuccessPrimaryBackground,
|
||||
"bigIconBackgroundColor" to ElementTheme.colors.bigIconDefaultBackgroundColor,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
<string name="state_event_avatar_changed_too">"(аватар таксама быў зменены)"</string>
|
||||
<string name="state_event_avatar_url_changed">"%1$s змяніў аватар"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"Вы змянілі свой аватар"</string>
|
||||
<string name="state_event_demoted_to_member">"%1$s быў паніжаны да ўдзельніка"</string>
|
||||
<string name="state_event_demoted_to_moderator">"%1$s быў паніжаны да мадэратара"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s змяніў сваё адлюстраванае імя з %2$s на %3$s"</string>
|
||||
<string name="state_event_display_name_changed_from_by_you">"Вы змянілі сваё адлюстраванае імя з %1$s на %2$s"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s выдаліў сваё адлюстраванае імя (яно было %2$s)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"Вы выдалілі сваё адлюстраванае імя (яно было %1$s)"</string>
|
||||
<string name="state_event_display_name_set">"%1$s усталявалі сваё адлюстраванае імя на %2$s"</string>
|
||||
<string name="state_event_display_name_set_by_you">"Вы ўстанавілі адлюстраванае імя на %1$s"</string>
|
||||
<string name="state_event_promoted_to_administrator">"%1$s быў павышаны да адміністратара"</string>
|
||||
<string name="state_event_promoted_to_moderator">"%1$s быў павышаны да мадэратара"</string>
|
||||
<string name="state_event_room_avatar_changed">"%1$s змяніў аватар пакоя"</string>
|
||||
<string name="state_event_room_avatar_changed_by_you">"Вы змянілі аватар пакоя"</string>
|
||||
<string name="state_event_room_avatar_removed">"%1$s выдаліў(-ла) аватар пакоя"</string>
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
<string name="state_event_avatar_changed_too">"(avatar byl také změněn)"</string>
|
||||
<string name="state_event_avatar_url_changed">"%1$s změnil(a) svůj profilový obrázek"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"Změnili jste svůj profilový obrázek"</string>
|
||||
<string name="state_event_demoted_to_member">"%1$s byl(a) degradován(a) na člena"</string>
|
||||
<string name="state_event_demoted_to_moderator">"%1$s byl(a) degradován(a) na moderátora"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s změnil(a) své zobrazované jméno z %2$s na %3$s"</string>
|
||||
<string name="state_event_display_name_changed_from_by_you">"Změnili jste své zobrazované jméno z %1$s na %2$s"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s odstranil(a) své zobrazované jméno (%2$s)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"Odstranili jste své zobrazované jméno (%1$s)"</string>
|
||||
<string name="state_event_display_name_set">"%1$s nastavil(a) své zobrazované jméno na %2$s"</string>
|
||||
<string name="state_event_display_name_set_by_you">"Změnili jste své zobrazované jméno na %1$s"</string>
|
||||
<string name="state_event_promoted_to_administrator">"%1$s byl(a) povýšen(a) na administrátora"</string>
|
||||
<string name="state_event_promoted_to_moderator">"%1$s byl(a) povýšen(a) na moderátora"</string>
|
||||
<string name="state_event_room_avatar_changed">"%1$s změnil(a) obrázek místnosti"</string>
|
||||
<string name="state_event_room_avatar_changed_by_you">"Změnili jste obrázek místnosti"</string>
|
||||
<string name="state_event_room_avatar_removed">"%1$s odstranili obrázek místnosti"</string>
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
<string name="state_event_avatar_changed_too">"(avatar juga diubah)"</string>
|
||||
<string name="state_event_avatar_url_changed">"%1$s mengubah avatarnya"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"Anda mengubah avatar sendiri"</string>
|
||||
<string name="state_event_demoted_to_member">"%1$s telah diturunkan menjadi anggota"</string>
|
||||
<string name="state_event_demoted_to_moderator">"%1$s telah diturunkan menjadi moderator"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s mengubah nama tampilannya dari %2$s menjadi %3$s"</string>
|
||||
<string name="state_event_display_name_changed_from_by_you">"Anda mengubah nama tampilan sendiri dari %1$s menjadi %2$s"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s menghapus nama tampilannya (sebelumnya %2$s)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"Anda menghapus nama tampilan sendiri (sebelumnya %1$s)"</string>
|
||||
<string name="state_event_display_name_set">"%1$s menetapkan nama tampilannya menjadi %2$s"</string>
|
||||
<string name="state_event_display_name_set_by_you">"Anda menetapkan nama tampilan sendiri menjadi %1$s"</string>
|
||||
<string name="state_event_promoted_to_administrator">"%1$s telah dipromosikan menjadi admin"</string>
|
||||
<string name="state_event_promoted_to_moderator">"%1$s telah dipromosikan menjadi moderator"</string>
|
||||
<string name="state_event_room_avatar_changed">"%1$s mengubah avatar ruangan"</string>
|
||||
<string name="state_event_room_avatar_changed_by_you">"Anda mengubah avatar ruangan"</string>
|
||||
<string name="state_event_room_avatar_removed">"%1$s menghapus avatar ruangan"</string>
|
||||
@@ -39,6 +43,8 @@
|
||||
<string name="state_event_room_name_changed_by_you">"Anda mengubah nama ruangan menjadi: %1$s"</string>
|
||||
<string name="state_event_room_name_removed">"%1$s menghapus nama ruangan"</string>
|
||||
<string name="state_event_room_name_removed_by_you">"Anda menghapus nama ruangan"</string>
|
||||
<string name="state_event_room_none">"%1$s tidak membuat perubahan"</string>
|
||||
<string name="state_event_room_none_by_you">"Anda tidak membuat perubahan"</string>
|
||||
<string name="state_event_room_reject">"%1$s menolak undangan"</string>
|
||||
<string name="state_event_room_reject_by_you">"Anda menolak undangan"</string>
|
||||
<string name="state_event_room_remove">"%1$s mengeluarkan %2$s"</string>
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
<string name="state_event_avatar_changed_too">"(аватар теж було змінено)"</string>
|
||||
<string name="state_event_avatar_url_changed">"%1$s змінив (-ла) свій аватар"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"Ви змінили свій аватар"</string>
|
||||
<string name="state_event_demoted_to_member">"%1$s був понижений до члена"</string>
|
||||
<string name="state_event_demoted_to_moderator">"%1$s був понижений до модератора"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s змінив (-ла) своє імʼя з %2$s на %3$s"</string>
|
||||
<string name="state_event_display_name_changed_from_by_you">"Ви змінили своє ім\'я з %1$s на %2$s"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s видалив (-ла) своє ім\'я (було %2$s)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"Ви видалили своє ім\'я (було%1$s)"</string>
|
||||
<string name="state_event_display_name_set">"%1$s змінив (-ла) своє ім\'я на %2$s"</string>
|
||||
<string name="state_event_display_name_set_by_you">"Ви змінили своє імʼя на %1$s"</string>
|
||||
<string name="state_event_promoted_to_administrator">"%1$s був підвищений до адміністратора"</string>
|
||||
<string name="state_event_promoted_to_moderator">"%1$s був підвищений до модератора"</string>
|
||||
<string name="state_event_room_avatar_changed">"%1$s змінив (-ла) аватар кімнати"</string>
|
||||
<string name="state_event_room_avatar_changed_by_you">"Ви змінили аватар кімнати"</string>
|
||||
<string name="state_event_room_avatar_removed">"%1$s видалив (-ла) аватар кімнати"</string>
|
||||
|
||||
@@ -161,10 +161,10 @@ class DefaultRoomLastMessageFormatterTest {
|
||||
|
||||
val sharedContentMessagesTypes = arrayOf(
|
||||
TextMessageType(body, null),
|
||||
VideoMessageType(body, MediaSource("url"), null),
|
||||
VideoMessageType(body, null, null, MediaSource("url"), null),
|
||||
AudioMessageType(body, MediaSource("url"), null),
|
||||
VoiceMessageType(body, MediaSource("url"), null, null),
|
||||
ImageMessageType(body, MediaSource("url"), null),
|
||||
ImageMessageType(body, null, null, MediaSource("url"), null),
|
||||
StickerMessageType(body, MediaSource("url"), null),
|
||||
FileMessageType(body, MediaSource("url"), null),
|
||||
LocationMessageType(body, "geo:1,2", null),
|
||||
|
||||
@@ -39,8 +39,8 @@ class StaticFeatureFlagProvider @Inject constructor() :
|
||||
FeatureFlags.VoiceMessages -> true
|
||||
FeatureFlags.PinUnlock -> true
|
||||
FeatureFlags.Mentions -> true
|
||||
FeatureFlags.MarkAsUnread -> false
|
||||
FeatureFlags.RoomListFilters -> false
|
||||
FeatureFlags.MarkAsUnread -> true
|
||||
FeatureFlags.RoomListFilters -> true
|
||||
FeatureFlags.RoomModeration -> false
|
||||
}
|
||||
} else {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user