Merge branch 'release/26.04.3'

This commit is contained in:
Benoit Marty 2026-04-16 16:40:07 +02:00
commit 1dbdd4905d
686 changed files with 13035 additions and 3862 deletions

View file

@ -56,7 +56,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug APKs

View file

@ -61,7 +61,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug Gplay Enterprise APK

View file

@ -16,14 +16,14 @@ jobs:
contents: write
steps:
- name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@1c185ad576953eab13e35ffe1bffef437d97e9d2 # v1.2.4
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
- name: Use JDK 21
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12

View file

@ -50,7 +50,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug APK

View file

@ -34,7 +34,7 @@ jobs:
swap-storage: false
- name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@1c185ad576953eab13e35ffe1bffef437d97e9d2 # v1.2.4
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
- name: Use JDK 21
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
@ -43,7 +43,7 @@ jobs:
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
cache-read-only: false
@ -85,7 +85,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Dependency analysis

View file

@ -74,7 +74,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12
@ -113,7 +113,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Konsist tests
@ -154,7 +154,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run compose tests
@ -188,7 +188,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Build Gplay Debug
@ -233,7 +233,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Detekt
@ -274,7 +274,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Ktlint check

View file

@ -43,13 +43,13 @@ jobs:
labels: Record-Screenshots
- name: ⏬ Checkout with LFS (PR)
if: github.event.label.name == 'Record-Screenshots'
uses: nschloe/action-cached-lfs-checkout@1c185ad576953eab13e35ffe1bffef437d97e9d2 # v1.2.4
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
with:
persist-credentials: false
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }}
- name: ⏬ Checkout with LFS (Branch)
if: github.event_name == 'workflow_dispatch'
uses: nschloe/action-cached-lfs-checkout@1c185ad576953eab13e35ffe1bffef437d97e9d2 # v1.2.4
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
with:
persist-credentials: false
- name: ☕️ Use JDK 21
@ -59,7 +59,7 @@ jobs:
java-version: '21'
# Add gradle cache, this should speed up the process
- name: Configure gradle
uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Record screenshots

View file

@ -43,7 +43,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
- name: Create app bundle
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
@ -87,7 +87,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
- name: Create Enterprise app bundle
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
@ -131,7 +131,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
- name: Create APKs
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}

View file

@ -50,7 +50,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Build debug code and test fixtures

View file

@ -22,7 +22,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12

View file

@ -49,7 +49,7 @@ jobs:
sudo swapon /mnt/swapfile
sudo swapon --show
- name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@1c185ad576953eab13e35ffe1bffef437d97e9d2 # v1.2.4
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@ -68,7 +68,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
@ -108,7 +108,7 @@ jobs:
# https://github.com/codecov/codecov-action
- name: ☂️ Upload coverage reports to codecov
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}

View file

@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
name: Validate
steps:
- uses: nschloe/action-cached-lfs-checkout@1c185ad576953eab13e35ffe1bffef437d97e9d2 # v1.2.4
- uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
- run: |
./tools/git/validate_lfs.sh

View file

@ -1,3 +1,14 @@
Changes in Element X v26.04.2
=============================
## What's Changed
### 🐛 Bugfixes
* Restore enterprise submodule. by @bmarty in https://github.com/element-hq/element-x-android/pull/6541
### Dependency upgrades
* fix(deps): update dependency io.element.android:element-call-embedded to v0.19.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6538
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.04.1...v26.04.2
Changes in Element X v26.04.1
=============================

View file

@ -52,7 +52,7 @@ class MainActivity : NodeActivity() {
private lateinit var appBindings: AppBindings
override fun onCreate(savedInstanceState: Bundle?) {
Timber.tag(loggerTag.value).w("onCreate, with savedInstanceState: ${savedInstanceState != null}")
Timber.tag(loggerTag.value).d("onCreate, with savedInstanceState: ${savedInstanceState != null}")
installSplashScreen()
super.onCreate(savedInstanceState)
appBindings = bindings()
@ -108,7 +108,7 @@ class MainActivity : NodeActivity() {
plugins = listOf(
object : NodeReadyObserver<MainNode> {
override fun init(node: MainNode) {
Timber.tag(loggerTag.value).w("onMainNodeInit")
Timber.tag(loggerTag.value).d("onMainNodeInit")
mainNode = node
mainNode.handleIntent(intent)
}
@ -144,7 +144,7 @@ class MainActivity : NodeActivity() {
*/
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
Timber.tag(loggerTag.value).w("onNewIntent")
Timber.tag(loggerTag.value).d("onNewIntent")
// If the mainNode is not init yet, keep the intent for later.
// It can happen when the activity is killed by the system. The methods are called in this order :
// onCreate(savedInstanceState=true) -> onNewIntent -> onResume -> onMainNodeInit
@ -157,16 +157,16 @@ class MainActivity : NodeActivity() {
override fun onPause() {
super.onPause()
Timber.tag(loggerTag.value).w("onPause")
Timber.tag(loggerTag.value).d("onPause")
}
override fun onResume() {
super.onResume()
Timber.tag(loggerTag.value).w("onResume")
Timber.tag(loggerTag.value).d("onResume")
}
override fun onDestroy() {
super.onDestroy()
Timber.tag(loggerTag.value).w("onDestroy")
Timber.tag(loggerTag.value).d("onDestroy")
}
}

View file

@ -19,6 +19,7 @@
<locale android:name="hu"/>
<locale android:name="in"/>
<locale android:name="it"/>
<locale android:name="ja"/>
<locale android:name="ka"/>
<locale android:name="ko"/>
<locale android:name="lt"/>
@ -35,6 +36,7 @@
<locale android:name="uk"/>
<locale android:name="ur"/>
<locale android:name="uz"/>
<locale android:name="vi"/>
<locale android:name="zh-CN"/>
<locale android:name="zh-TW"/>
</locale-config>

View file

@ -18,7 +18,6 @@ object TimelineConfig {
*/
val excludedEvents = listOf(
StateEventType.CallMember,
StateEventType.RoomAliases,
StateEventType.RoomCanonicalAlias,
StateEventType.RoomGuestAccess,
StateEventType.RoomHistoryVisibility,

View file

@ -51,7 +51,7 @@ dependencies {
implementation(projects.features.linknewdevice.api)
implementation(projects.features.share.api)
implementation(projects.services.apperror.impl)
implementation(projects.services.apperror.api)
implementation(projects.services.appnavstate.api)
implementation(projects.services.analytics.api)
@ -67,8 +67,7 @@ dependencies {
testImplementation(projects.features.messages.test)
testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.features.rageshake.test)
testImplementation(projects.services.appnavstate.impl)
testImplementation(projects.services.apperror.test)
testImplementation(projects.services.appnavstate.test)
testImplementation(projects.services.analytics.test)
testImplementation(projects.services.toolbox.test)
}

View file

@ -252,7 +252,8 @@ class RootFlowNode(
val transitionHandler = rememberDelegateTransitionHandler<NavTarget, BackStack.State> { navTarget ->
when (navTarget) {
is NavTarget.SplashScreen,
is NavTarget.LoggedInFlow -> backstackFader
is NavTarget.LoggedInFlow,
is NavTarget.NotLoggedInFlow -> backstackFader
else -> backstackSlider
}
}

View file

@ -21,6 +21,8 @@ import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject
import im.vector.app.features.analytics.plan.CryptoSessionStateChange
import im.vector.app.features.analytics.plan.UserProperties
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.extensions.runCatchingExceptions
@ -56,6 +58,7 @@ class LoggedInPresenter(
private val analyticsService: AnalyticsService,
private val encryptionService: EncryptionService,
private val buildMeta: BuildMeta,
private val networkMonitor: NetworkMonitor,
) : Presenter<LoggedInState> {
@Composable
override fun present(): LoggedInState {
@ -107,6 +110,14 @@ class LoggedInPresenter(
}.launchIn(this)
}
val networkConnectivity by networkMonitor.connectivity.collectAsState()
LaunchedEffect(networkConnectivity) {
if (networkConnectivity == NetworkStatus.Connected) {
// Refresh homeserver capabilities when the network is back
matrixClient.homeserverCapabilities().refresh()
}
}
fun handleEvent(event: LoggedInEvents) {
when (event) {
is LoggedInEvents.CloseErrorDialog -> {

View file

@ -22,7 +22,7 @@ import io.element.android.features.rageshake.api.detection.RageshakeDetectionVie
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.services.apperror.impl.AppErrorView
import io.element.android.services.apperror.api.AppErrorView
@Composable
fun RootView(

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"ログアウトしてアップグレード"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%1$s は古いプロトコルに非対応になりました。アプリを引き続き使用するには、ログアウトしてから再度ログインしてください。"</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"使用しているホームサーバーは古いプロトコルに非対応になりました。アプリケーションを引き続き使用するには、ログアウトしてから再度ログインしてください。"</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Đăng xuất &amp; Nâng cấp"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%1$s không còn hỗ trợ giao thức cũ. Vui lòng đăng xuất và đăng nhập lại để tiếp tục sử dụng ứng dụng."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Homeserver của bạn không còn hỗ trợ giao thức cũ. Vui lòng đăng xuất và đăng nhập lại để tiếp tục sử dụng ứng dụng."</string>
</resources>

View file

@ -43,7 +43,7 @@ import io.element.android.services.analytics.api.watchers.AnalyticsSendMessageWa
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.analytics.test.watchers.FakeAnalyticsSendMessageWatcher
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder
import io.element.android.services.appnavstate.test.FakeActiveRoomsHolder
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
@ -128,7 +128,7 @@ class JoinedRoomLoadedFlowNodeTest {
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
spaceEntryPoint: SpaceEntryPoint = FakeSpaceEntryPoint(),
forwardEntryPoint: ForwardEntryPoint = FakeForwardEntryPoint(),
activeRoomsHolder: ActiveRoomsHolder = DefaultActiveRoomsHolder(),
activeRoomsHolder: ActiveRoomsHolder = FakeActiveRoomsHolder(),
matrixClient: FakeMatrixClient = FakeMatrixClient(),
) = JoinedRoomLoadedFlowNode(
buildContext = BuildContext.root(savedStateMap = null),
@ -213,7 +213,7 @@ class JoinedRoomLoadedFlowNodeTest {
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
val activeRoomsHolder = DefaultActiveRoomsHolder()
val activeRoomsHolder = FakeActiveRoomsHolder()
val roomFlowNode = createJoinedRoomLoadedFlowNode(
plugins = listOf(inputs, FakeJoinedRoomLoadedFlowNodeCallback()),
messagesEntryPoint = fakeMessagesEntryPoint,
@ -236,7 +236,7 @@ class JoinedRoomLoadedFlowNodeTest {
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
val activeRoomsHolder = DefaultActiveRoomsHolder().apply {
val activeRoomsHolder = FakeActiveRoomsHolder().apply {
addRoom(room)
}
val roomFlowNode = createJoinedRoomLoadedFlowNode(

View file

@ -19,8 +19,7 @@ import io.element.android.libraries.matrix.test.FakeSdkMetadata
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.apperror.api.AppErrorState
import io.element.android.services.apperror.api.AppErrorStateService
import io.element.android.services.apperror.impl.DefaultAppErrorStateService
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.services.apperror.test.FakeAppErrorStateService
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest
import org.junit.Rule
@ -44,10 +43,16 @@ class RootPresenterTest {
@Test
fun `present - passes app error state`() = runTest {
val presenter = createRootPresenter(
appErrorService = DefaultAppErrorStateService(
stringProvider = FakeStringProvider(),
).apply {
showError("Bad news", "Something bad happened")
appErrorService = FakeAppErrorStateService().apply {
setAppErrorState(
AppErrorState.Error(
title = "Bad news",
body = "Something bad happened",
dismiss = {
setAppErrorState(AppErrorState.NoError)
}
)
)
}
)
moleculeFlow(RecompositionMode.Immediate) {
@ -65,9 +70,7 @@ class RootPresenterTest {
}
private fun createRootPresenter(
appErrorService: AppErrorStateService = DefaultAppErrorStateService(
stringProvider = FakeStringProvider(),
),
appErrorService: AppErrorStateService = FakeAppErrorStateService(),
): RootPresenter {
return RootPresenter(
crashDetectionPresenter = { aCrashDetectionState() },

View file

@ -14,6 +14,8 @@ import app.cash.turbine.ReceiveTurbine
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.CryptoSessionStateChange
import im.vector.app.features.analytics.plan.UserProperties
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId
@ -27,6 +29,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationS
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeHomeserverCapabilitiesProvider
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
@ -109,6 +112,7 @@ class LoggedInPresenterTest {
val verificationService = FakeSessionVerificationService()
val encryptionService = FakeEncryptionService()
val buildMeta = aBuildMeta()
val networkMonitor = FakeNetworkMonitor()
LoggedInPresenter(
matrixClient = FakeMatrixClient(
roomListService = roomListService,
@ -122,6 +126,7 @@ class LoggedInPresenterTest {
analyticsService = analyticsService,
encryptionService = encryptionService,
buildMeta = buildMeta,
networkMonitor = networkMonitor,
).test {
encryptionService.emitRecoveryState(RecoveryState.UNKNOWN)
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
@ -319,6 +324,27 @@ class LoggedInPresenterTest {
}
}
@Test
fun `present - refreshes homeserver capabilities when network is back`() = runTest {
val refreshLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val matrixClient = FakeMatrixClient(
homeserverCapabilitiesProvider = FakeHomeserverCapabilitiesProvider(refresh = refreshLambda),
accountManagementUrlResult = { Result.success(null) },
)
val networkMonitor = FakeNetworkMonitor()
createLoggedInPresenter(
matrixClient = matrixClient,
networkMonitor = networkMonitor,
).test {
awaitItem()
networkMonitor.connectivity.value = NetworkStatus.Connected
advanceUntilIdle()
refreshLambda.assertions().isCalledOnce()
}
}
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
skipItems(1)
return awaitItem()
@ -334,6 +360,7 @@ class LoggedInPresenterTest {
accountManagementUrlResult = { Result.success(null) },
),
buildMeta: BuildMeta = aBuildMeta(),
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
): LoggedInPresenter {
return LoggedInPresenter(
matrixClient = matrixClient,
@ -343,6 +370,7 @@ class LoggedInPresenterTest {
analyticsService = analyticsService,
encryptionService = encryptionService,
buildMeta = buildMeta,
networkMonitor = networkMonitor,
)
}
}

View file

@ -0,0 +1,2 @@
Main changes in this version: bug fixes and improvements.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"問題発見のため、匿名の使用データの共有にご協力ください。"</string>
<string name="screen_analytics_settings_read_terms">"利用規約の全文を%1$sから確認することができます。"</string>
<string name="screen_analytics_settings_read_terms_content_link">"こちら"</string>
<string name="screen_analytics_settings_share_data">"使用データを共有"</string>
</resources>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"Dalinkitės anoniminiais naudojimo duomenimis ir padėkite mums nustatyti problemas."</string>
<string name="screen_analytics_settings_help_us_improve">"Bendrinkite anoniminius naudojimo duomenis, kad padėtumėte mums nustatyti problemas."</string>
<string name="screen_analytics_settings_read_terms">"Galite perskaityti visas mūsų sąlygas %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"čia"</string>
<string name="screen_analytics_settings_share_data">"Dalytis analitiniais duomenimis"</string>
<string name="screen_analytics_settings_share_data">"Bendrinti analitinius duomenis"</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"Chia sẻ dữ liệu sử dụng ẩn danh để giúp chúng tôi xác định vấn đề."</string>
<string name="screen_analytics_settings_read_terms">"Bạn có thể xem tất cả điều khoản của chúng tôi tại %1$s"</string>
<string name="screen_analytics_settings_read_terms_content_link">"tại đây"</string>
<string name="screen_analytics_settings_share_data">"Chia sẻ dữ liệu phân tích"</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"いかなる個人情報も記録, 分析されることはありません"</string>
<string name="screen_analytics_prompt_help_us_improve">"問題発見のため、匿名の使用データの共有にご協力ください。"</string>
<string name="screen_analytics_prompt_read_terms">"利用規約の全文を%1$sから確認することができます。"</string>
<string name="screen_analytics_prompt_read_terms_content_link">"こちら"</string>
<string name="screen_analytics_prompt_settings">"いつでも設定は変更できます"</string>
<string name="screen_analytics_prompt_third_party_sharing">"情報が第三者に共有されることはありません"</string>
<string name="screen_analytics_prompt_title">"%1$s の改善にご協力ください"</string>
</resources>

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"Mes nekaupsime ir neprofiliuosime jokių asmens duomenų"</string>
<string name="screen_analytics_prompt_help_us_improve">"Dalinkitės anoniminiais naudojimo duomenimis ir padėkite mums nustatyti problemas."</string>
<string name="screen_analytics_prompt_data_usage">"Mes neįrašysime ar neprofiliuosime jokių asmeninių duomenų."</string>
<string name="screen_analytics_prompt_help_us_improve">"Bendrinkite anoniminius naudojimo duomenis, kad padėtumėte mums nustatyti problemas."</string>
<string name="screen_analytics_prompt_read_terms">"Galite perskaityti visas mūsų sąlygas %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"čia"</string>
<string name="screen_analytics_prompt_settings">"Tai galite bet kada išjungti"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Mes nesidalinsime Jūsų duomenimis su trečiosiomis šalimis"</string>
<string name="screen_analytics_prompt_title">"Padėkite pagerinti %1$s"</string>
<string name="screen_analytics_prompt_settings">"Tai galite išjungti bet kuriuo metu."</string>
<string name="screen_analytics_prompt_third_party_sharing">"Mes nebendrinsime jūsų duomenų su trečiosiomis šalimis."</string>
<string name="screen_analytics_prompt_title">"Padėkite patobulinti „%1$s“"</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"Chúng tôi sẽ không ghi lại hoặc lập hồ sơ bất kỳ dữ liệu cá nhân nào."</string>
<string name="screen_analytics_prompt_help_us_improve">"Chia sẻ dữ liệu sử dụng ẩn danh để giúp chúng tôi xác định vấn đề."</string>
<string name="screen_analytics_prompt_read_terms">"Bạn có thể xem tất cả điều khoản của chúng tôi tại %1$s"</string>
<string name="screen_analytics_prompt_read_terms_content_link">"tại đây"</string>
<string name="screen_analytics_prompt_settings">"Bạn có thể tắt tính năng này bất cứ lúc nào"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Chúng tôi sẽ không chia sẻ dữ liệu của bạn với bên thứ ba."</string>
<string name="screen_analytics_prompt_title">"Giúp cải thiện %1$s"</string>
</resources>

View file

@ -8,7 +8,13 @@
package io.element.android.features.announcement.api
enum class Announcement {
import androidx.compose.runtime.Immutable
@Immutable
sealed interface Announcement {
enum class Fullscreen : Announcement {
Space,
NewNotificationSound,
}
data object NewNotificationSound : Announcement
}

View file

@ -0,0 +1,17 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl
import io.element.android.features.announcement.api.Announcement
sealed interface AnnouncementEvent {
data class Continue(
val announcement: Announcement.Fullscreen,
) : AnnouncementEvent
}

View file

@ -12,12 +12,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.impl.store.AnnouncementStatus
import io.element.android.features.announcement.impl.store.AnnouncementStore
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@Inject
class AnnouncementPresenter(
@ -25,13 +29,39 @@ class AnnouncementPresenter(
) : Presenter<AnnouncementState> {
@Composable
override fun present(): AnnouncementState {
val showSpaceAnnouncement by remember {
announcementStore.announcementStatusFlow(Announcement.Space).map {
val coroutineScope = rememberCoroutineScope()
val fullscreenAnnouncementToShow by remember {
combine(
flowOf(Unit),
announcementStore.announcementStatusFlow(Announcement.Fullscreen.Space).map {
it == AnnouncementStatus.Show
},
// Add other announcements here when needed
) { _, showFullscreenSpace ->
when {
showFullscreenSpace -> Announcement.Fullscreen.Space
else -> {
null
}
}.collectAsState(false)
return AnnouncementState(
showSpaceAnnouncement = showSpaceAnnouncement,
}
}
}.collectAsState(null)
fun handle(event: AnnouncementEvent) {
when (event) {
is AnnouncementEvent.Continue -> coroutineScope.launch {
announcementStore.setAnnouncementStatus(
announcement = event.announcement,
status = AnnouncementStatus.Shown,
)
}
}
}
return AnnouncementState(
announcement = fullscreenAnnouncementToShow,
eventSink = ::handle,
)
}
}

View file

@ -8,12 +8,9 @@
package io.element.android.features.announcement.impl
data class AnnouncementState(
val showSpaceAnnouncement: Boolean,
)
import io.element.android.features.announcement.api.Announcement
fun anAnnouncementState(
showSpaceAnnouncement: Boolean = false,
) = AnnouncementState(
showSpaceAnnouncement = showSpaceAnnouncement,
data class AnnouncementState(
val announcement: Announcement.Fullscreen?,
val eventSink: (AnnouncementEvent) -> Unit,
)

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.announcement.api.Announcement
open class AnnouncementStateProvider : PreviewParameterProvider<AnnouncementState> {
override val values: Sequence<AnnouncementState>
get() = sequenceOf(
anAnnouncementState(),
anAnnouncementState(
announcement = Announcement.Fullscreen.Space,
),
)
}
fun anAnnouncementState(
announcement: Announcement.Fullscreen? = null,
eventSink: (AnnouncementEvent) -> Unit = {},
) = AnnouncementState(
announcement = announcement,
eventSink = eventSink,
)

View file

@ -8,35 +8,28 @@
package io.element.android.features.announcement.impl
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementView
import io.element.android.features.announcement.impl.fullscreen.FullscreenAnnouncementView
import io.element.android.features.announcement.impl.store.AnnouncementStatus
import io.element.android.features.announcement.impl.store.AnnouncementStore
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
@ContributesBinding(AppScope::class)
class DefaultAnnouncementService(
private val announcementStore: AnnouncementStore,
private val announcementPresenter: Presenter<AnnouncementState>,
private val spaceAnnouncementPresenter: Presenter<SpaceAnnouncementState>,
private val announcementPresenter: AnnouncementPresenter,
) : AnnouncementService {
override suspend fun showAnnouncement(announcement: Announcement) {
when (announcement) {
Announcement.Space -> showSpaceAnnouncement()
is Announcement.Fullscreen -> showFullscreenAnnouncement(announcement)
Announcement.NewNotificationSound -> {
announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show)
}
@ -49,13 +42,10 @@ class DefaultAnnouncementService(
override fun announcementsToShowFlow(): Flow<List<Announcement>> {
return combine(
announcementStore.announcementStatusFlow(Announcement.Space),
flowOf(Unit),
announcementStore.announcementStatusFlow(Announcement.NewNotificationSound),
) { spaceAnnouncementStatus, newNotificationSoundStatus ->
) { _, newNotificationSoundStatus ->
buildList {
if (spaceAnnouncementStatus == AnnouncementStatus.Show) {
add(Announcement.Space)
}
if (newNotificationSoundStatus == AnnouncementStatus.Show) {
add(Announcement.NewNotificationSound)
}
@ -63,27 +53,19 @@ class DefaultAnnouncementService(
}
}
private suspend fun showSpaceAnnouncement() {
val currentValue = announcementStore.announcementStatusFlow(Announcement.Space).first()
private suspend fun showFullscreenAnnouncement(announcement: Announcement.Fullscreen) {
val currentValue = announcementStore.announcementStatusFlow(announcement).first()
if (currentValue == AnnouncementStatus.NeverShown) {
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
announcementStore.setAnnouncementStatus(announcement, AnnouncementStatus.Show)
}
}
@Composable
override fun Render(modifier: Modifier) {
val announcementState = announcementPresenter.present()
Box(modifier = modifier.fillMaxSize()) {
AnimatedVisibility(
visible = announcementState.showSpaceAnnouncement,
enter = fadeIn(),
exit = fadeOut(),
) {
val spaceAnnouncementState = spaceAnnouncementPresenter.present()
SpaceAnnouncementView(
state = spaceAnnouncementState,
FullscreenAnnouncementView(
state = announcementState,
modifier = modifier,
)
}
}
}
}

View file

@ -1,29 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl.di
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.Binds
import dev.zacsweers.metro.ContributesTo
import io.element.android.features.announcement.impl.AnnouncementPresenter
import io.element.android.features.announcement.impl.AnnouncementState
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementPresenter
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState
import io.element.android.libraries.architecture.Presenter
@ContributesTo(AppScope::class)
@BindingContainer
interface AnnouncementModule {
@Binds
fun bindAnnouncementPresenter(presenter: AnnouncementPresenter): Presenter<AnnouncementState>
@Binds
fun bindSpaceAnnouncementPresenter(presenter: SpaceAnnouncementPresenter): Presenter<SpaceAnnouncementState>
}

View file

@ -0,0 +1,225 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl.fullscreen
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
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.features.announcement.api.Announcement
import io.element.android.features.announcement.impl.AnnouncementEvent
import io.element.android.features.announcement.impl.AnnouncementState
import io.element.android.features.announcement.impl.AnnouncementStateProvider
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem
import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
* Ref: https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=4593-40181
*/
@Composable
fun FullscreenAnnouncementView(
state: AnnouncementState,
modifier: Modifier = Modifier,
) {
// Ensure that the content stays visible during the exit animation
var fullscreenAnnouncement by remember { mutableStateOf<Announcement.Fullscreen?>(null) }
if (state.announcement != null) {
fullscreenAnnouncement = state.announcement
}
Box(modifier = modifier.fillMaxSize()) {
AnimatedVisibility(
visible = state.announcement != null,
enter = fadeIn(),
exit = fadeOut(),
) {
fullscreenAnnouncement?.let {
FullscreenAnnouncementView(
announcement = it,
eventSink = state.eventSink,
)
}
}
}
}
@Composable
private fun FullscreenAnnouncementView(
announcement: Announcement.Fullscreen,
eventSink: (AnnouncementEvent) -> Unit,
modifier: Modifier = Modifier
) {
fun onContinue() {
eventSink(AnnouncementEvent.Continue(announcement))
}
BackHandler(onBack = ::onContinue)
HeaderFooterPage(
modifier = modifier,
isScrollable = true,
contentPadding = PaddingValues(top = 24.dp, start = 16.dp, end = 16.dp, bottom = 24.dp),
header = {
FullscreenAnnouncementHeader(announcement)
},
content = {
FullscreenAnnouncementContent(
modifier = Modifier.padding(horizontal = 8.dp),
announcement = announcement,
)
},
footer = {
FullscreenAnnouncementFooter(
onContinue = ::onContinue,
)
}
)
}
@Composable
private fun FullscreenAnnouncementHeader(
announcement: Announcement.Fullscreen,
modifier: Modifier = Modifier,
) {
IconTitleSubtitleMolecule(
modifier = modifier.padding(top = 16.dp, bottom = 16.dp),
title = announcement.title(),
showBetaLabel = true,
subTitle = announcement.subtitle(),
iconStyle = BigIcon.Style.Default(
vectorIcon = announcement.icon(),
usePrimaryTint = true,
),
)
}
@Composable
private fun FullscreenAnnouncementContent(
announcement: Announcement.Fullscreen,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.fillMaxSize(),
) {
InfoListOrganism(
modifier = Modifier.fillMaxWidth(),
items = announcement.items(),
textStyle = ElementTheme.typography.fontBodyLgMedium,
iconTint = ElementTheme.colors.iconSecondary,
iconSize = 24.dp
)
announcement.notice()?.let { notice ->
Text(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
text = notice,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
}
}
}
@Composable
private fun FullscreenAnnouncementFooter(
onContinue: () -> Unit,
) {
ButtonColumnMolecule(
modifier = Modifier.padding(bottom = 8.dp)
) {
Button(
text = stringResource(id = CommonStrings.action_continue),
onClick = onContinue,
modifier = Modifier.fillMaxWidth(),
)
}
}
@Composable
private fun Announcement.Fullscreen.title() = when (this) {
Announcement.Fullscreen.Space -> "Introducing Spaces"
}
@Composable
private fun Announcement.Fullscreen.subtitle() = when (this) {
Announcement.Fullscreen.Space -> "Welcome to the beta version of Spaces! With this first version you can:"
}
@Composable
private fun Announcement.Fullscreen.icon() = when (this) {
Announcement.Fullscreen.Space -> CompoundIcons.SpaceSolid()
}
@Composable
private fun Announcement.Fullscreen.items(): ImmutableList<InfoListItem> = when (this) {
Announcement.Fullscreen.Space -> persistentListOf(
InfoListItem(
message = "View spaces you\'ve created or joined",
iconVector = CompoundIcons.VisibilityOn(),
),
InfoListItem(
message = "Accept or decline invites to spaces",
iconVector = CompoundIcons.Email(),
),
InfoListItem(
message = "Discover any rooms you can join in your spaces",
iconVector = CompoundIcons.Search(),
),
InfoListItem(
message = "Join public spaces",
iconVector = CompoundIcons.Explore(),
),
InfoListItem(
message = "Leave any spaces youve joined",
iconVector = CompoundIcons.Leave(),
),
)
}
@Composable
private fun Announcement.Fullscreen.notice(): String? = when (this) {
Announcement.Fullscreen.Space -> "Filtering, creating and managing spaces is coming soon."
}
@PreviewsDayNight
@Composable
internal fun FullscreenAnnouncementViewPreview(@PreviewParameter(AnnouncementStateProvider::class) state: AnnouncementState) = ElementPreview {
FullscreenAnnouncementView(
state = state,
)
}

View file

@ -1,13 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl.spaces
sealed interface SpaceAnnouncementEvents {
data object Continue : SpaceAnnouncementEvents
}

View file

@ -1,40 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl.spaces
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.impl.store.AnnouncementStatus
import io.element.android.features.announcement.impl.store.AnnouncementStore
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.launch
@Inject
class SpaceAnnouncementPresenter(
private val announcementStore: AnnouncementStore,
) : Presenter<SpaceAnnouncementState> {
@Composable
override fun present(): SpaceAnnouncementState {
val localCoroutineScope = rememberCoroutineScope()
fun handleEvent(event: SpaceAnnouncementEvents) {
when (event) {
SpaceAnnouncementEvents.Continue -> localCoroutineScope.launch {
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
}
}
}
return SpaceAnnouncementState(
eventSink = ::handleEvent,
)
}
}

View file

@ -1,13 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl.spaces
data class SpaceAnnouncementState(
val eventSink: (SpaceAnnouncementEvents) -> Unit
)

View file

@ -1,24 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl.spaces
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
open class SpaceAnnouncementStateProvider : PreviewParameterProvider<SpaceAnnouncementState> {
override val values: Sequence<SpaceAnnouncementState>
get() = sequenceOf(
aSpaceAnnouncementState(),
)
}
fun aSpaceAnnouncementState(
eventSink: (SpaceAnnouncementEvents) -> Unit = {},
) = SpaceAnnouncementState(
eventSink = eventSink,
)

View file

@ -1,158 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl.spaces
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
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.features.announcement.impl.R
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem
import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.persistentListOf
/**
* Ref: https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=4593-40181
*/
@Composable
fun SpaceAnnouncementView(
state: SpaceAnnouncementState,
modifier: Modifier = Modifier,
) {
val eventSink = state.eventSink
fun onContinue() {
eventSink(SpaceAnnouncementEvents.Continue)
}
BackHandler(onBack = ::onContinue)
HeaderFooterPage(
modifier = modifier,
isScrollable = true,
contentPadding = PaddingValues(top = 24.dp, start = 16.dp, end = 16.dp, bottom = 24.dp),
header = {
SpaceAnnouncementHeader()
},
content = {
SpaceAnnouncementContent(
modifier = Modifier.padding(horizontal = 8.dp),
)
},
footer = {
SpaceAnnouncementFooter(
onContinue = ::onContinue,
)
}
)
}
@Composable
private fun SpaceAnnouncementHeader(
modifier: Modifier = Modifier,
) {
IconTitleSubtitleMolecule(
modifier = modifier.padding(top = 16.dp, bottom = 16.dp),
title = stringResource(id = R.string.screen_space_announcement_title),
showBetaLabel = true,
subTitle = stringResource(id = R.string.screen_space_announcement_subtitle),
iconStyle = BigIcon.Style.Default(
vectorIcon = CompoundIcons.SpaceSolid(),
usePrimaryTint = true,
),
)
}
@Composable
private fun SpaceAnnouncementContent(
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.fillMaxSize(),
) {
InfoListOrganism(
modifier = Modifier.fillMaxWidth(),
items = persistentListOf(
InfoListItem(
message = stringResource(id = R.string.screen_space_announcement_item1),
iconVector = CompoundIcons.VisibilityOn(),
),
InfoListItem(
message = stringResource(id = R.string.screen_space_announcement_item2),
iconVector = CompoundIcons.Email(),
),
InfoListItem(
message = stringResource(id = R.string.screen_space_announcement_item3),
iconVector = CompoundIcons.Search(),
),
InfoListItem(
message = stringResource(id = R.string.screen_space_announcement_item4),
iconVector = CompoundIcons.Explore(),
),
InfoListItem(
message = stringResource(id = R.string.screen_space_announcement_item5),
iconVector = CompoundIcons.Leave(),
),
),
textStyle = ElementTheme.typography.fontBodyLgMedium,
iconTint = ElementTheme.colors.iconSecondary,
iconSize = 24.dp
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
text = stringResource(id = R.string.screen_space_announcement_notice),
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
}
}
@Composable
private fun SpaceAnnouncementFooter(
onContinue: () -> Unit,
) {
ButtonColumnMolecule(
modifier = Modifier.padding(bottom = 8.dp)
) {
Button(
text = stringResource(id = CommonStrings.action_continue),
onClick = onContinue,
modifier = Modifier.fillMaxWidth(),
)
}
}
@PreviewsDayNight
@Composable
internal fun SpaceAnnouncementViewPreview(@PreviewParameter(SpaceAnnouncementStateProvider::class) state: SpaceAnnouncementState) = ElementPreview {
SpaceAnnouncementView(
state = state,
)
}

View file

@ -35,9 +35,10 @@ class DefaultAnnouncementStore(
override fun announcementStatusFlow(announcement: Announcement): Flow<AnnouncementStatus> {
val key = announcement.toKey()
// Announcement.Fullscreen.Space is disabled, consider it's shown
// For NewNotificationSound, a migration will set it to Show on application upgrade (see AppMigration08)
val defaultStatus = when (announcement) {
Announcement.Space -> AnnouncementStatus.NeverShown
Announcement.Fullscreen.Space -> AnnouncementStatus.Shown
Announcement.NewNotificationSound -> AnnouncementStatus.Shown
}
return store.data.map { prefs ->
@ -52,6 +53,6 @@ class DefaultAnnouncementStore(
}
private fun Announcement.toKey() = when (this) {
Announcement.Space -> spaceAnnouncementKey
Announcement.Fullscreen.Space -> spaceAnnouncementKey
Announcement.NewNotificationSound -> newNotificationSoundKey
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_space_announcement_item1">"作成または参加したスペースを表示できます"</string>
<string name="screen_space_announcement_item2">"スペースへの招待を受諾または拒否できます"</string>
<string name="screen_space_announcement_item3">"スペース内の参加可能なルームを検索できます"</string>
<string name="screen_space_announcement_item4">"公開スペースに参加できます"</string>
<string name="screen_space_announcement_item5">"参加したスペースを退出できます"</string>
<string name="screen_space_announcement_notice">"スペースの作成や管理, フィルター検索は近日実装予定です。"</string>
<string name="screen_space_announcement_subtitle">"ベータ版のスペースにようこそ。この最新のバージョンでは:"</string>
<string name="screen_space_announcement_title">"スペースの紹介"</string>
</resources>

View file

@ -6,6 +6,6 @@
<string name="screen_space_announcement_item4">"加入公共空间"</string>
<string name="screen_space_announcement_item5">"离开你加入的所有空间"</string>
<string name="screen_space_announcement_notice">"筛选、创建及管理空间功能即将上线。"</string>
<string name="screen_space_announcement_subtitle">"欢迎使用 Spaces 测试版!使用首个版本,您可以:"</string>
<string name="screen_space_announcement_title">"Spaces 简介"</string>
<string name="screen_space_announcement_subtitle">"欢迎使用空间测试版!使用首个版本,您可以:"</string>
<string name="screen_space_announcement_title">"空间简介"</string>
</resources>

View file

@ -14,6 +14,7 @@ import io.element.android.features.announcement.impl.store.AnnouncementStatus
import io.element.android.features.announcement.impl.store.AnnouncementStore
import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore
import io.element.android.tests.testutils.test
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -23,25 +24,47 @@ class AnnouncementPresenterTest {
val presenter = createAnnouncementPresenter()
presenter.test {
val state = awaitItem()
assertThat(state.showSpaceAnnouncement).isFalse()
assertThat(state.announcement).isNull()
}
}
@Test
fun `present - showSpaceAnnouncement value depends on the value in the store`() = runTest {
fun `present - showFullscreen value depends on the value in the store`() = runTest {
val store = InMemoryAnnouncementStore()
val presenter = createAnnouncementPresenter(
announcementStore = store,
)
presenter.test {
val state = awaitItem()
assertThat(state.showSpaceAnnouncement).isFalse()
store.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
assertThat(state.announcement).isNull()
store.setAnnouncementStatus(Announcement.Fullscreen.Space, AnnouncementStatus.Show)
val updatedState = awaitItem()
assertThat(updatedState.showSpaceAnnouncement).isTrue()
store.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
assertThat(updatedState.announcement).isEqualTo(Announcement.Fullscreen.Space)
store.setAnnouncementStatus(Announcement.Fullscreen.Space, AnnouncementStatus.Shown)
val finalState = awaitItem()
assertThat(finalState.showSpaceAnnouncement).isFalse()
assertThat(finalState.announcement).isNull()
}
}
@Test
fun `present - continue event will mark the announcement as Shown`() = runTest {
val store = InMemoryAnnouncementStore()
val presenter = createAnnouncementPresenter(
announcementStore = store,
)
presenter.test {
val state = awaitItem()
assertThat(state.announcement).isNull()
store.setAnnouncementStatus(Announcement.Fullscreen.Space, AnnouncementStatus.Show)
val statusShow = store.announcementStatusFlow(Announcement.Fullscreen.Space).first()
assertThat(statusShow).isEqualTo(AnnouncementStatus.Show)
val updatedState = awaitItem()
assertThat(updatedState.announcement).isEqualTo(Announcement.Fullscreen.Space)
updatedState.eventSink(AnnouncementEvent.Continue(Announcement.Fullscreen.Space))
val statusShown = store.announcementStatusFlow(Announcement.Fullscreen.Space).first()
assertThat(statusShown).isEqualTo(AnnouncementStatus.Shown)
val finalState = awaitItem()
assertThat(finalState.announcement).isNull()
}
}
}

View file

@ -11,31 +11,28 @@ package io.element.android.features.announcement.impl
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState
import io.element.android.features.announcement.impl.spaces.aSpaceAnnouncementState
import io.element.android.features.announcement.impl.store.AnnouncementStatus
import io.element.android.features.announcement.impl.store.AnnouncementStore
import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultAnnouncementServiceTest {
@Test
fun `when showing Space announcement, space announcement is set to show only if it was never shown`() = runTest {
fun `when showing Fullscreen announcement, Fullscreen announcement is set to show only if it was never shown`() = runTest {
val announcementStore = InMemoryAnnouncementStore()
val sut = createDefaultAnnouncementService(
announcementStore = announcementStore,
)
assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.NeverShown)
sut.showAnnouncement(Announcement.Space)
assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Show)
assertThat(announcementStore.announcementStatusFlow(Announcement.Fullscreen.Space).first()).isEqualTo(AnnouncementStatus.NeverShown)
sut.showAnnouncement(Announcement.Fullscreen.Space)
assertThat(announcementStore.announcementStatusFlow(Announcement.Fullscreen.Space).first()).isEqualTo(AnnouncementStatus.Show)
// Simulate user close the announcement
sut.onAnnouncementDismissed(Announcement.Space)
sut.onAnnouncementDismissed(Announcement.Fullscreen.Space)
// Entering again the space tab should not change the value
sut.showAnnouncement(Announcement.Space)
assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Shown)
sut.showAnnouncement(Announcement.Fullscreen.Space)
assertThat(announcementStore.announcementStatusFlow(Announcement.Fullscreen.Space).first()).isEqualTo(AnnouncementStatus.Shown)
}
@Test
@ -62,11 +59,7 @@ class DefaultAnnouncementServiceTest {
)
sut.announcementsToShowFlow().test {
assertThat(awaitItem()).isEmpty()
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
assertThat(awaitItem()).containsExactly(Announcement.Space)
announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show)
assertThat(awaitItem()).containsExactly(Announcement.Space, Announcement.NewNotificationSound)
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
assertThat(awaitItem()).containsExactly(Announcement.NewNotificationSound)
announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Shown)
assertThat(awaitItem()).isEmpty()
@ -75,11 +68,9 @@ class DefaultAnnouncementServiceTest {
private fun createDefaultAnnouncementService(
announcementStore: AnnouncementStore = InMemoryAnnouncementStore(),
announcementPresenter: Presenter<AnnouncementState> = Presenter { anAnnouncementState() },
spaceAnnouncementPresenter: Presenter<SpaceAnnouncementState> = Presenter { aSpaceAnnouncementState() },
announcementPresenter: AnnouncementPresenter = AnnouncementPresenter(announcementStore),
) = DefaultAnnouncementService(
announcementStore = announcementStore,
announcementPresenter = announcementPresenter,
spaceAnnouncementPresenter = spaceAnnouncementPresenter,
)
}

View file

@ -6,12 +6,16 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl.spaces
package io.element.android.features.announcement.impl.fullscreen
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.impl.AnnouncementEvent
import io.element.android.features.announcement.impl.AnnouncementState
import io.element.android.features.announcement.impl.anAnnouncementState
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
@ -22,39 +26,41 @@ import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SpaceAnnouncementViewTest {
class FullscreenAnnouncementViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on back sends a SpaceAnnouncementEvents`() {
val eventsRecorder = EventsRecorder<SpaceAnnouncementEvents>()
rule.setSpaceAnnouncementView(
aSpaceAnnouncementState(
fun `clicking on back sends a AnnouncementEvent`() {
val eventsRecorder = EventsRecorder<AnnouncementEvent>()
rule.setFullscreenAnnouncementView(
anAnnouncementState(
announcement = Announcement.Fullscreen.Space,
eventSink = eventsRecorder,
),
)
rule.pressBackKey()
eventsRecorder.assertSingle(SpaceAnnouncementEvents.Continue)
eventsRecorder.assertSingle(AnnouncementEvent.Continue(Announcement.Fullscreen.Space))
}
@Test
fun `clicking on Continue sends a SpaceAnnouncementEvents`() {
val eventsRecorder = EventsRecorder<SpaceAnnouncementEvents>()
rule.setSpaceAnnouncementView(
aSpaceAnnouncementState(
fun `clicking on Continue sends a AnnouncementEvent`() {
val eventsRecorder = EventsRecorder<AnnouncementEvent>()
rule.setFullscreenAnnouncementView(
anAnnouncementState(
announcement = Announcement.Fullscreen.Space,
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle(SpaceAnnouncementEvents.Continue)
eventsRecorder.assertSingle(AnnouncementEvent.Continue(Announcement.Fullscreen.Space))
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpaceAnnouncementView(
state: SpaceAnnouncementState,
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setFullscreenAnnouncementView(
state: AnnouncementState,
) {
setContent {
SpaceAnnouncementView(
FullscreenAnnouncementView(
state = state,
)
}

View file

@ -1,41 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl.spaces
import com.google.common.truth.Truth.assertThat
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.impl.store.AnnouncementStatus
import io.element.android.features.announcement.impl.store.AnnouncementStore
import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore
import io.element.android.tests.testutils.test
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Test
class SpaceAnnouncementPresenterTest {
@Test
fun `present - when user continues, the store is updated`() = runTest {
val store = InMemoryAnnouncementStore()
val presenter = createSpaceAnnouncementPresenter(
announcementStore = store,
)
presenter.test {
assertThat(store.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.NeverShown)
val state = awaitItem()
state.eventSink(SpaceAnnouncementEvents.Continue)
assertThat(store.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Shown)
}
}
}
private fun createSpaceAnnouncementPresenter(
announcementStore: AnnouncementStore = InMemoryAnnouncementStore(),
) = SpaceAnnouncementPresenter(
announcementStore = announcementStore,
)

View file

@ -14,10 +14,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class InMemoryAnnouncementStore(
initialSpaceAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown,
initialFullscreenAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown,
initialNewNotificationSoundAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown,
) : AnnouncementStore {
private val spaceAnnouncement = MutableStateFlow(initialSpaceAnnouncementStatus)
private val fullScreenAnnouncement = MutableStateFlow(initialFullscreenAnnouncementStatus)
private val newNotificationSoundAnnouncement = MutableStateFlow(initialNewNotificationSoundAnnouncementStatus)
override suspend fun setAnnouncementStatus(announcement: Announcement, status: AnnouncementStatus) {
@ -29,12 +29,12 @@ class InMemoryAnnouncementStore(
}
override suspend fun reset() {
spaceAnnouncement.value = AnnouncementStatus.NeverShown
fullScreenAnnouncement.value = AnnouncementStatus.NeverShown
newNotificationSoundAnnouncement.value = AnnouncementStatus.NeverShown
}
private fun Announcement.toMutableStateFlow() = when (this) {
Announcement.Space -> spaceAnnouncement
is Announcement.Fullscreen -> fullScreenAnnouncement
Announcement.NewNotificationSound -> newNotificationSoundAnnouncement
}
}

View file

@ -73,7 +73,7 @@ dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.matrix.impl)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixmedia.api)
implementation(projects.libraries.network)
implementation(projects.libraries.preferences.api)

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"通話中"</string>
<string name="call_foreground_service_message_android">"タップして通話に戻る"</string>
<string name="call_foreground_service_title_android">"☎️ 通話中"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"Element CallはこのAndroidバージョンにおいて、Bluetoothオーディオデバイスの使用をサポートしていません。別のオーディオデバイスを選択してください。"</string>
<string name="screen_incoming_call_subtitle_android">"Element Call の着信"</string>
</resources>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"Cuộc gọi đang diễn ra"</string>
<string name="call_foreground_service_message_android">"Nhấn để quay lại cuộc gọi."</string>
<string name="call_foreground_service_title_android">"☎️ Cuộc gọi đang diễn ra"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"Ứng dụng Element Call không hỗ trợ sử dụng thiết bị âm thanh Bluetooth trên phiên bản Android này. Vui lòng chọn thiết bị âm thanh khác."</string>
<string name="screen_incoming_call_subtitle_android">"Cuộc gọi Element đến"</string>
</resources>

View file

@ -6,9 +6,8 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.call.test
package io.element.android.features.call.impl.notifications
import io.element.android.features.call.impl.notifications.CallNotificationData
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId

View file

@ -15,11 +15,11 @@ import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import io.element.android.features.call.api.CallType
import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator
import io.element.android.features.call.impl.notifications.aCallNotificationData
import io.element.android.features.call.impl.utils.ActiveCall
import io.element.android.features.call.impl.utils.CallState
import io.element.android.features.call.impl.utils.DefaultActiveCallManager
import io.element.android.features.call.impl.utils.DefaultCurrentCallService
import io.element.android.features.call.test.aCallNotificationData
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId

View file

@ -20,7 +20,6 @@ dependencies {
implementation(projects.libraries.core)
api(projects.features.call.api)
implementation(projects.features.call.impl)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrix.test)
implementation(projects.tests.testutils)

View file

@ -39,7 +39,6 @@ dependencies {
implementation(projects.libraries.mediaupload.api)
implementation(projects.libraries.permissions.api)
implementation(projects.libraries.previewutils)
implementation(projects.libraries.usersearch.impl)
implementation(projects.services.analytics.api)
implementation(libs.coil.compose)
implementation(projects.libraries.featureflag.api)
@ -52,7 +51,6 @@ dependencies {
testImplementation(projects.libraries.mediapickers.test)
testImplementation(projects.libraries.mediaupload.test)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.usersearch.test)
testImplementation(projects.features.startchat.test)
testImplementation(projects.libraries.featureflag.test)
}

View file

@ -84,7 +84,6 @@ class ConfigureRoomPresenter(
@Composable
override fun present(): ConfigureRoomState {
val canAddRoomToSpace by featureFlagService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false)
val cameraPermissionState = cameraPermissionPresenter.present()
val createRoomConfig by dataStore.getCreateRoomConfigFlow().collectAsState()
val homeserverName = remember { matrixClient.userIdServerName() }
@ -113,12 +112,8 @@ class ConfigureRoomPresenter(
}
var spaces by remember { mutableStateOf<ImmutableList<SpaceRoom>>(persistentListOf()) }
LaunchedEffect(canAddRoomToSpace) {
spaces = if (canAddRoomToSpace) {
matrixClient.spaceService.editableSpaces().getOrElse { emptyList() }.toImmutableList()
} else {
persistentListOf()
}
LaunchedEffect(Unit) {
spaces = matrixClient.spaceService.editableSpaces().getOrElse { emptyList() }.toImmutableList()
val parentSpace = spaces.find { it.roomId == initialParentSpaceId }
parentSpace?.let {
dataStore.setParentSpace(parentSpace = parentSpace, updateVisibility = true)

View file

@ -3,14 +3,34 @@
<string name="screen_create_room_action_create_room">"Nuova stanza"</string>
<string name="screen_create_room_add_people_title">"Invita persone"</string>
<string name="screen_create_room_error_creating_room">"Si è verificato un errore durante la creazione della stanza"</string>
<string name="screen_create_room_private_option_description">"Solo le persone invitate possono accedere a questa stanza. Tutti i messaggi sono cifrati end-to-end."</string>
<string name="screen_create_room_error_creating_space">"Non è stato possibile creare lo spazio a causa di un errore sconosciuto. Riprova più tardi."</string>
<string name="screen_create_room_name_placeholder">"Aggiungi nome…"</string>
<string name="screen_create_room_new_room_title">"Nuova stanza"</string>
<string name="screen_create_room_new_space_title">"Nuovo spazio"</string>
<string name="screen_create_room_private_option_description">"Possono partecipare solo le persone invitate."</string>
<string name="screen_create_room_private_option_title">"Privato"</string>
<string name="screen_create_room_public_option_description">"Chiunque può trovare questa stanza.
Puoi modificarlo in qualsiasi momento nelle impostazioni della stanza."</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Chiunque può chiedere di entrare nella stanza, ma un amministratore o un moderatore dovrà accettare la richiesta"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Chiedi di entrare"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Chiunque può entrare in questa stanza"</string>
<string name="screen_create_room_room_address_section_footer">"Affinché questa stanza sia visibile nell\'elenco delle stanze pubbliche, è necessario un indirizzo della stanza."</string>
<string name="screen_create_room_room_address_section_title">"Indirizzo della stanza"</string>
<string name="screen_create_room_public_option_short_description">"Chiunque può partecipare."</string>
<string name="screen_create_room_public_option_title">"Pubblico"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Chiunque può chiedere di partecipare, ma un amministratore o un moderatore deve accettare la richiesta."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Consenti di chiedere di partecipare"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Chiunque sia membro di %1$s può partecipare, mentre tutti gli altri devono richiedere l\'accesso."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Richiedi accesso"</string>
<string name="screen_create_room_room_access_section_private_option_description">"Possono partecipare solo le persone invitate."</string>
<string name="screen_create_room_room_access_section_private_option_title">"Privato"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Chiunque può partecipare."</string>
<string name="screen_create_room_room_access_section_public_option_title">"Pubblico"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"Chiunque in %1$s può unirsi."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"Standard"</string>
<string name="screen_create_room_room_access_section_title">"Chi ha accesso"</string>
<string name="screen_create_room_room_address_section_footer">"Avrai bisogno di un indirizzo per renderlo visibile nella directory pubblica."</string>
<string name="screen_create_room_room_address_section_title">"Indirizzo"</string>
<string name="screen_create_room_room_visibility_section_title">"Visibilità della stanza"</string>
<string name="screen_create_room_space_selection_no_space_description">"(nessuno spazio)"</string>
<string name="screen_create_room_space_selection_no_space_option">"Non aggiungere a uno spazio"</string>
<string name="screen_create_room_space_selection_no_space_title">"Nessuno spazio selezionato"</string>
<string name="screen_create_room_space_selection_sheet_title">"Aggiungi allo spazio"</string>
<string name="screen_create_room_topic_label">"Argomento (facoltativo)"</string>
<string name="screen_create_room_topic_placeholder">"Aggiungi descrizione…"</string>
</resources>

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_room_action_create_room">"新しいルーム"</string>
<string name="screen_create_room_add_people_title">"ユーザーを招待"</string>
<string name="screen_create_room_error_creating_room">"ルームの作成中に問題が発生しました"</string>
<string name="screen_create_room_error_creating_space">"不明な問題のためスペースを作成できませんでした。再度お試しください。"</string>
<string name="screen_create_room_name_placeholder">"名前を追加…"</string>
<string name="screen_create_room_new_room_title">"新しいルーム"</string>
<string name="screen_create_room_new_space_title">"新しいスペース"</string>
<string name="screen_create_room_private_option_description">"招待されたユーザーのみ参加できます。"</string>
<string name="screen_create_room_private_option_title">"非公開"</string>
<string name="screen_create_room_public_option_description">"ルームは全世界に公開されます。
ルーム設定でいつでも変更できます。"</string>
<string name="screen_create_room_public_option_short_description">"誰でも参加できます。"</string>
<string name="screen_create_room_public_option_title">"公開"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"誰でも参加できますが、管理者またはモデレーターの承認が必要です。"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"参加の要求を許可"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"%1$s にいる全員が参加することができますが、事前に参加の要求をする必要があります。"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"参加を要求"</string>
<string name="screen_create_room_room_access_section_private_option_description">"招待されたユーザーのみが参加できます。"</string>
<string name="screen_create_room_room_access_section_private_option_title">"非公開"</string>
<string name="screen_create_room_room_access_section_public_option_description">"誰でも参加できます。"</string>
<string name="screen_create_room_room_access_section_public_option_title">"公開"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"%1$s にいる全員が参加することができます。"</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"スタンダード"</string>
<string name="screen_create_room_room_access_section_title">"参加できるユーザー"</string>
<string name="screen_create_room_room_address_section_footer">"公開ディレクトリで自分を見つけられるようにするには、アドレスが必要です。"</string>
<string name="screen_create_room_room_address_section_title">"アドレス"</string>
<string name="screen_create_room_room_visibility_section_title">"ルームの公開度"</string>
<string name="screen_create_room_space_selection_no_space_description">"(スペースなし)"</string>
<string name="screen_create_room_space_selection_no_space_option">"スペースに追加しない"</string>
<string name="screen_create_room_space_selection_no_space_title">"スペースが選択されていません"</string>
<string name="screen_create_room_space_selection_sheet_title">"スペースに追加"</string>
<string name="screen_create_room_topic_label">"トピック (任意)"</string>
<string name="screen_create_room_topic_placeholder">"説明を追加…"</string>
</resources>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_room_action_create_room">"Phòng mới"</string>
<string name="screen_create_room_add_people_title">"Mời ai đó"</string>
<string name="screen_create_room_error_creating_room">"Đã xảy ra lỗi khi tạo phòng."</string>
<string name="screen_create_room_private_option_description">"Chỉ những người được mời mới có thể tham gia."</string>
<string name="screen_create_room_public_option_description">"Bất kỳ ai cũng có thể tìm thấy phòng này.
Bạn có thể thay đổi cài đặt phòng bất cứ lúc nào."</string>
<string name="screen_create_room_topic_label">"Chủ đề (tùy chọn)"</string>
<string name="screen_create_room_topic_placeholder">"Thêm mô tả…"</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"アカウントを無効化することを再度確認します。この操作は元に戻せません。"</string>
<string name="screen_deactivate_account_delete_all_messages">"メッセージをすべて削除"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"注意: 新しいユーザーには断片的な会話が表示されます"</string>
<string name="screen_deactivate_account_description">"アカウントを無効化することは %1$s であり、次の変化が生じます:"</string>
<string name="screen_deactivate_account_description_bold_part">"不可逆"</string>
<string name="screen_deactivate_account_list_item_1">"アカウントを %1$s (再度ログイン不可, 同一のIDを再利用不可)"</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"恒久的に無効化する"</string>
<string name="screen_deactivate_account_list_item_2">"すべてのチャットルームから退出します。"</string>
<string name="screen_deactivate_account_list_item_3">"アカウント提供元サーバーからアカウント情報を削除します。"</string>
<string name="screen_deactivate_account_list_item_4">"あなたの会話は、既存ユーザーには引き続き表示されますが、新規ユーザーには表示されなくなります。"</string>
<string name="screen_deactivate_account_title">"アカウントを無効化"</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_delete_all_messages">"Xóa tất cả tin nhắn của tôi"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Cảnh báo: Người dùng sau này có thể thấy các cuộc trò chuyện chưa hoàn chỉnh."</string>
<string name="screen_deactivate_account_list_item_4">"Tin nhắn của bạn vẫn sẽ hiển thị cho người dùng đã đăng ký nhưng sẽ không hiển thị cho người dùng mới hoặc chưa đăng ký nếu bạn chọn xóa chúng."</string>
<string name="screen_deactivate_account_title">"Vô hiệu hóa tài khoản"</string>
</resources>

View file

@ -2,8 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Nemůžete potvrdit?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Vytvoření nového klíče pro obnovení"</string>
<string name="screen_identity_confirmation_subtitle">"Ověřte toto zařízení a nastavte zabezpečené zasílání zpráv."</string>
<string name="screen_identity_confirmation_title">"Potvrďte, že jste to vy"</string>
<string name="screen_identity_confirmation_subtitle">"Vyberte způsob ověření pro nastavení zabezpečeného zasílání zpráv."</string>
<string name="screen_identity_confirmation_title">"Potvrďte svou digitální identitu"</string>
<string name="screen_identity_confirmation_use_another_device">"Použít jiné zařízení"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Použít klíč pro obnovení"</string>
<string name="screen_identity_confirmed_subtitle">"Nyní můžete bezpečně číst nebo odesílat zprávy, a kdokoli, s kým chatujete, může tomuto zařízení důvěřovat."</string>

View file

@ -2,8 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Non puoi confermare?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Crea una nuova chiave di recupero"</string>
<string name="screen_identity_confirmation_subtitle">"Verifica questo dispositivo per segnare i tuoi messaggi come sicuri."</string>
<string name="screen_identity_confirmation_title">"Conferma la tua identità"</string>
<string name="screen_identity_confirmation_subtitle">"Scegli come effettuare la verifica per configurare la messaggistica sicura."</string>
<string name="screen_identity_confirmation_title">"Conferma la tua identità digitale"</string>
<string name="screen_identity_confirmation_use_another_device">"Usa un altro dispositivo"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Usa la chiave di recupero"</string>
<string name="screen_identity_confirmed_subtitle">"Ora puoi leggere o inviare messaggi in tutta sicurezza e anche chi chatta con te può fidarsi di questo dispositivo."</string>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"認証できませんか?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"回復鍵を新規作成します"</string>
<string name="screen_identity_confirmation_subtitle">"安全なメッセージを設定するための検証方法を選択してください。"</string>
<string name="screen_identity_confirmation_title">"デジタルIDの認証"</string>
<string name="screen_identity_confirmation_use_another_device">"他の端末を使用"</string>
<string name="screen_identity_confirmation_use_recovery_key">"回復鍵を使用"</string>
<string name="screen_identity_confirmed_subtitle">"メッセージのやり取りを安全に行えるようになりました。他のユーザーはこの端末を信頼できます。"</string>
<string name="screen_identity_confirmed_title">"検証済みの端末"</string>
<string name="screen_identity_use_another_device">"他の端末を使用"</string>
<string name="screen_identity_waiting_on_other_device">"一方の端末を待機中…"</string>
<string name="screen_notification_optin_subtitle">"設定は後で変更することができます。"</string>
<string name="screen_notification_optin_title">"メッセージを見逃さないため通知を許可"</string>
</resources>

View file

@ -2,7 +2,7 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Не можете подтвердить?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Создайте новый ключ восстановления"</string>
<string name="screen_identity_confirmation_subtitle">"Подтвердите это устройство, чтобы настроить безопасный обмен сообщениями."</string>
<string name="screen_identity_confirmation_subtitle">"Выберите способ подтверждения для настройки защищенного обмена сообщениями."</string>
<string name="screen_identity_confirmation_title">"Подтвердите личность"</string>
<string name="screen_identity_confirmation_use_another_device">"Использовать другое устройство"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Использовать ключ восстановления"</string>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_subtitle">"Chọn phương thức xác minh để bật nhắn tin bảo mật."</string>
<string name="screen_identity_confirmation_title">"Xác nhận danh tính kỹ thuật số của bạn"</string>
<string name="screen_identity_confirmed_subtitle">"Giờ đây bạn có thể đọc và gửi tin nhắn một cách an toàn, và những người bạn trò chuyện cũng có thể tin tưởng thiết bị này."</string>
<string name="screen_identity_confirmed_title">"Thiết bị được xác thực"</string>
<string name="screen_identity_waiting_on_other_device">"Đang chờ trên thiết bị khác…"</string>
<string name="screen_notification_optin_subtitle">"Bạn có thể thay đổi cài đặt sau."</string>
<string name="screen_notification_optin_title">"Cho phép thông báo để không bỏ lỡ bất kỳ tin nhắn nào"</string>
</resources>

View file

@ -2,8 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"无法确认?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"创建新的恢复密钥"</string>
<string name="screen_identity_confirmation_subtitle">"验证此设备以开始安全地收发消息。"</string>
<string name="screen_identity_confirmation_title">"确认这是你"</string>
<string name="screen_identity_confirmation_subtitle">"选择验证方式以设置安全的消息传输。"</string>
<string name="screen_identity_confirmation_title">"确认您的数字身份"</string>
<string name="screen_identity_confirmation_use_another_device">"使用其他设备"</string>
<string name="screen_identity_confirmation_use_recovery_key">"使用恢复密钥"</string>
<string name="screen_identity_confirmed_subtitle">"现在,您可以安全地阅读或发送消息,与您聊天的人也会信任此设备。"</string>

View file

@ -19,8 +19,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.spaces.HomeSpacesState
import io.element.android.features.logout.api.direct.DirectLogoutState
@ -47,7 +45,6 @@ class HomePresenter(
private val logoutPresenter: Presenter<DirectLogoutState>,
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val sessionStore: SessionStore,
private val announcementService: AnnouncementService,
) : Presenter<HomeState> {
private val currentUserWithNeighborsBuilder = CurrentUserWithNeighborsBuilder()
@ -82,10 +79,7 @@ class HomePresenter(
fun handleEvent(event: HomeEvent) {
when (event) {
is HomeEvent.SelectHomeNavigationBarItem -> coroutineState.launch {
if (event.item == HomeNavigationBarItem.Spaces) {
announcementService.showAnnouncement(Announcement.Space)
}
is HomeEvent.SelectHomeNavigationBarItem -> {
currentHomeNavigationBarItemOrdinal = event.item.ordinal
}
is HomeEvent.SwitchToAccount -> coroutineState.launch {
@ -94,12 +88,6 @@ class HomePresenter(
}
}
LaunchedEffect(homeSpacesState.canCreateSpaces, homeSpacesState.spaceRooms.isEmpty()) {
// If the flag to create spaces is disabled and the last space is left, ensure that the Chat view is rendered.
if (!homeSpacesState.canCreateSpaces && homeSpacesState.spaceRooms.isEmpty()) {
currentHomeNavigationBarItemOrdinal = HomeNavigationBarItem.Chats.ordinal
}
}
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
return HomeState(
currentUserAndNeighbors = currentUserAndNeighbors,

View file

@ -34,5 +34,4 @@ data class HomeState(
) {
val isBackHandlerEnabled = currentHomeNavigationBarItem != HomeNavigationBarItem.Chats || roomListState.spaceFiltersState is SpaceFiltersState.Selected
val displayRoomListFilters = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats && roomListState.displayFilters
val showNavigationBar = homeSpacesState.canCreateSpaces || homeSpacesState.spaceRooms.isNotEmpty()
}

View file

@ -199,7 +199,6 @@ private fun HomeScaffold(
)
},
floatingActionButton = {
if (state.showNavigationBar) {
val coroutineScope = rememberCoroutineScope()
HomeBottomBar(
currentHomeNavigationBarItem = state.currentHomeNavigationBarItem,
@ -222,27 +221,19 @@ private fun HomeScaffold(
state.eventSink(HomeEvent.SelectHomeNavigationBarItem(item))
}
},
floatingActionButton = when (state.currentHomeNavigationBarItem) {
floatingActionButton = {
when (state.currentHomeNavigationBarItem) {
HomeNavigationBarItem.Chats -> {
{
HomeFloatingActionButton(onStartChatClick, CommonStrings.action_create_room)
}
}
HomeNavigationBarItem.Spaces -> if (state.homeSpacesState.canCreateSpaces) {
{
HomeNavigationBarItem.Spaces -> {
HomeFloatingActionButton(onCreateSpaceClick, CommonStrings.action_create_space)
}
} else {
// No FAB for spaces if we cannot create spaces
null
}
},
)
} else {
HomeFloatingActionButton(onStartChatClick, CommonStrings.action_create_room)
}
},
floatingActionButtonPosition = if (state.showNavigationBar) FabPosition.Center else FabPosition.End,
floatingActionButtonPosition = FabPosition.Center,
content = { padding ->
val contentPadding = PaddingValues(
bottom = 96.dp,

View file

@ -60,6 +60,7 @@ import io.element.android.libraries.designsystem.theme.roomListRoomMessage
import io.element.android.libraries.designsystem.theme.roomListRoomMessageDate
import io.element.android.libraries.designsystem.theme.roomListRoomName
import io.element.android.libraries.designsystem.theme.unreadIndicator
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.ui.components.InviteSenderView
import io.element.android.libraries.matrix.ui.model.InviteSender
@ -349,6 +350,7 @@ private fun MessagePreviewAndIndicatorRow(
if (room.hasRoomCall) {
OnGoingCallIcon(
color = tint,
isAudio = room.activeCallIntent == CallIntent.AUDIO
)
}
if (room.userDefinedNotificationMode == RoomNotificationMode.MUTE) {
@ -398,10 +400,11 @@ private fun InviteNameAndIndicatorRow(
@Composable
private fun OnGoingCallIcon(
color: Color,
isAudio: Boolean
) {
Icon(
modifier = Modifier.size(16.dp),
imageVector = CompoundIcons.VideoCallSolid(),
imageVector = if (isAudio) CompoundIcons.VoiceCallSolid() else CompoundIcons.VideoCallSolid(),
contentDescription = stringResource(CommonStrings.a11y_notifications_ongoing_call),
tint = color,
)

View file

@ -17,6 +17,7 @@ import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
import io.element.android.libraries.matrix.api.room.CallIntentConsensus
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.roomlist.LatestEventValue
@ -50,6 +51,11 @@ class RoomListRoomSummaryFactory(
avatarData = avatarData,
userDefinedNotificationMode = roomInfo.userDefinedNotificationMode,
hasRoomCall = roomInfo.hasRoomCall,
activeCallIntent = when (val consensus = roomInfo.activeCallIntentConsensus) {
is CallIntentConsensus.Full -> consensus.callIntent
is CallIntentConsensus.Partial -> consensus.callIntent
CallIntentConsensus.None -> null
},
isDirect = roomInfo.isDirect,
isFavorite = roomInfo.isFavorite,
inviteSender = roomInfo.inviter?.toInviteSender(),

View file

@ -13,6 +13,7 @@ import io.element.android.features.invite.api.InviteData
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.ui.model.InviteSender
import kotlinx.collections.immutable.ImmutableList
@ -33,6 +34,7 @@ data class RoomListRoomSummary(
val avatarData: AvatarData,
val userDefinedNotificationMode: RoomNotificationMode?,
val hasRoomCall: Boolean,
val activeCallIntent: CallIntent?,
val isDirect: Boolean,
val isDm: Boolean,
val isFavorite: Boolean,

View file

@ -14,6 +14,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.ui.model.InviteSender
import kotlinx.collections.immutable.toImmutableList
@ -132,6 +133,14 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
listOf(
aRoomListRoomSummary(latestEvent = LatestEvent.Sending("A sending message")),
aRoomListRoomSummary(latestEvent = LatestEvent.Error),
),
listOf(
aRoomListRoomSummary(
name = "Active voice call",
latestEvent = LatestEvent.Synced("No activity, call"),
hasRoomCall = true,
activeCallIntent = CallIntent.AUDIO
),
)
).flatten()
}
@ -158,6 +167,7 @@ internal fun aRoomListRoomSummary(
timestamp: String? = latestEvent.takeIf { it !is LatestEvent.None }?.let { "88:88" },
notificationMode: RoomNotificationMode? = null,
hasRoomCall: Boolean = false,
activeCallIntent: CallIntent? = null,
avatarData: AvatarData = AvatarData(id, name, size = AvatarSize.RoomListItem),
isDirect: Boolean = false,
isDm: Boolean = false,
@ -181,6 +191,7 @@ internal fun aRoomListRoomSummary(
avatarData = avatarData,
userDefinedNotificationMode = notificationMode,
hasRoomCall = hasRoomCall,
activeCallIntent = activeCallIntent,
isDirect = isDirect,
isDm = isDm,
isFavorite = isFavorite,

View file

@ -17,8 +17,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
import kotlinx.collections.immutable.persistentListOf
@ -27,20 +25,15 @@ import kotlinx.coroutines.flow.map
@Inject
class SpaceFiltersPresenter(
private val featureFlagService: FeatureFlagService,
private val matrixClient: MatrixClient,
) : Presenter<SpaceFiltersState> {
@Composable
override fun present(): SpaceFiltersState {
val isFeatureEnabled by featureFlagService
.isFeatureEnabledFlow(FeatureFlags.RoomListSpaceFilters)
.collectAsState(initial = false)
val availableFilters by remember {
matrixClient.spaceService.spaceFiltersFlow.map { it.toImmutableList() }
}.collectAsState(initial = persistentListOf())
if (!isFeatureEnabled || availableFilters.isEmpty()) {
if (availableFilters.isEmpty()) {
return SpaceFiltersState.Disabled
}

View file

@ -15,8 +15,6 @@ import androidx.compose.runtime.remember
import dev.zacsweers.metro.Inject
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
import kotlinx.collections.immutable.persistentListOf
@ -29,11 +27,9 @@ import kotlinx.coroutines.flow.map
class HomeSpacesPresenter(
private val client: MatrixClient,
private val seenInvitesStore: SeenInvitesStore,
private val featureFlagsService: FeatureFlagService,
) : Presenter<HomeSpacesState> {
@Composable
override fun present(): HomeSpacesState {
val canCreateSpaces by featureFlagsService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false)
val hideInvitesAvatar by client.rememberHideInvitesAvatar()
val spaceRooms by remember {
client.spaceService.topLevelSpacesFlow.map { it.toImmutableList() }
@ -52,7 +48,6 @@ class HomeSpacesPresenter(
spaceRooms = spaceRooms,
seenSpaceInvites = seenSpaceInvites,
hideInvitesAvatar = hideInvitesAvatar,
canCreateSpaces = canCreateSpaces,
// TODO enable once we can link to the screen to explore public spaces
canExploreSpaces = false,
eventSink = ::handleEvent,

View file

@ -18,7 +18,6 @@ data class HomeSpacesState(
val spaceRooms: ImmutableList<SpaceRoom>,
val seenSpaceInvites: ImmutableSet<RoomId>,
val hideInvitesAvatar: Boolean,
val canCreateSpaces: Boolean,
val canExploreSpaces: Boolean,
val eventSink: (HomeSpacesEvents) -> Unit,
)

View file

@ -30,17 +30,9 @@ open class HomeSpacesStateProvider : PreviewParameterProvider<HomeSpacesState> {
),
spaceRooms = aListOfSpaceRooms(),
),
aHomeSpacesState(
space = CurrentSpace.Space(
spaceRoom = aSpaceRoom(roomId = RoomId("!mySpace:example.com"))
),
spaceRooms = aListOfSpaceRooms(),
canCreateSpaces = false,
),
aHomeSpacesState(
space = CurrentSpace.Root,
spaceRooms = emptyList(),
canCreateSpaces = true,
),
)
}
@ -50,7 +42,6 @@ internal fun aHomeSpacesState(
spaceRooms: List<SpaceRoom> = aListOfSpaceRooms(),
seenSpaceInvites: Set<RoomId> = emptySet(),
hideInvitesAvatar: Boolean = false,
canCreateSpaces: Boolean = true,
canExploreSpaces: Boolean = true,
eventSink: (HomeSpacesEvents) -> Unit = {},
) = HomeSpacesState(
@ -58,7 +49,6 @@ internal fun aHomeSpacesState(
spaceRooms = spaceRooms.toImmutableList(),
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
hideInvitesAvatar = hideInvitesAvatar,
canCreateSpaces = canCreateSpaces,
canExploreSpaces = canExploreSpaces,
eventSink = eventSink,
)

View file

@ -55,7 +55,7 @@ fun HomeSpacesView(
onExploreClick: () -> Unit,
modifier: Modifier = Modifier,
) {
if (state.canCreateSpaces && state.spaceRooms.isEmpty()) {
if (state.spaceRooms.isEmpty()) {
EmptySpaceHomeView(
modifier = modifier.padding(contentPadding),
onCreateSpaceClick = onCreateSpaceClick,

View file

@ -5,9 +5,9 @@
<string name="banner_battery_optimization_title_android">"Nepřicházejí vám oznámení?"</string>
<string name="banner_new_sound_message">"Váš zvuk oznámení byl aktualizován je jasnější, rychlejší a méně rušivý."</string>
<string name="banner_new_sound_title">"Aktualizovali jsme vaše zvuky"</string>
<string name="banner_set_up_recovery_content">"Vygenerujte nový klíč pro obnovení, který lze použít k obnovení historie šifrovaných zpráv v případě, že ztratíte přístup ke svým zařízením."</string>
<string name="banner_set_up_recovery_submit">"Nastavení obnovy"</string>
<string name="banner_set_up_recovery_title">"Nastavení obnovy"</string>
<string name="banner_set_up_recovery_content">"Vaše chaty jsou automaticky zálohovány pomocí koncového šifrování. Chcete-li tuto zálohu obnovit a zachovat si svou digitální identitu v případě, že ztratíte přístup ke všem svým zařízením, budete potřebovat svůj klíč pro obnovení."</string>
<string name="banner_set_up_recovery_submit">"Získat klíč pro obnovení"</string>
<string name="banner_set_up_recovery_title">"Zálohujte své chaty"</string>
<string name="confirm_recovery_key_banner_message">"Potvrďte klíč pro obnovení, abyste zachovali přístup k úložišti klíčů a historii zpráv."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Zadejte klíč pro obnovení"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Zapomněli jste klíč pro obnovení?"</string>

View file

@ -5,9 +5,9 @@
<string name="banner_battery_optimization_title_android">"Le notifiche non arrivano?"</string>
<string name="banner_new_sound_message">"Il ping delle notifiche è stato aggiornato: ora è più chiaro, più rapido e meno fastidioso."</string>
<string name="banner_new_sound_title">"Abbiamo rinnovato i tuoi suoni"</string>
<string name="banner_set_up_recovery_content">"Recupera la tua identità crittografica e la cronologia dei messaggi con una chiave di recupero se hai perso tutti i tuoi dispositivi."</string>
<string name="banner_set_up_recovery_submit">"Configura il recupero"</string>
<string name="banner_set_up_recovery_title">"Configura il ripristino"</string>
<string name="banner_set_up_recovery_content">"Le tue conversazioni vengono automaticamente salvate con crittografia end-to-end. Per ripristinare questo backup e conservare la tua identità digitale quando perdi l\'accesso a tutti i tuoi dispositivi, avrai bisogno della tua chiave di recupero."</string>
<string name="banner_set_up_recovery_submit">"Ottieni la chiave di recupero"</string>
<string name="banner_set_up_recovery_title">"Esegui il backup delle tue conversazioni"</string>
<string name="confirm_recovery_key_banner_message">"Conferma la chiave di recupero per mantenere l\'accesso all\'archiviazione delle chiavi e alla cronologia dei messaggi."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Inserisci la tua chiave di recupero"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Hai dimenticato la chiave di recupero?"</string>
@ -50,6 +50,7 @@ Non hai messaggi non letti!"</string>
<string name="screen_roomlist_mark_as_read">"Segna come letto"</string>
<string name="screen_roomlist_mark_as_unread">"Segna come non letto"</string>
<string name="screen_roomlist_tombstoned_room_description">"Questa stanza è stata aggiornata"</string>
<string name="screen_roomlist_your_spaces">"I tuoi spazi"</string>
<string name="session_verification_banner_message">"Sembra che tu stia usando un nuovo dispositivo. Verificati con un altro dispositivo per accedere ai tuoi messaggi cifrati."</string>
<string name="session_verification_banner_title">"Verifica che sei tu"</string>
</resources>

View file

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"すべての通知を確実に受信するために、このアプリのバッテリー最適化を無効にしてください。"</string>
<string name="banner_battery_optimization_submit_android">"最適化を無効にする"</string>
<string name="banner_battery_optimization_title_android">"通知が届いていませんか?"</string>
<string name="banner_new_sound_message">"通知音が更新され、より明確で速く、そして邪魔にならなくなりました。"</string>
<string name="banner_new_sound_title">"サウンドを刷新しました"</string>
<string name="banner_set_up_recovery_content">"あなたのチャットはエンドツーエンド暗号化を使用して自動的にバックアップされています。すべての端末を使用できない状況で、このバックアップからデジタルIDを復元するには、回復鍵が必要となります。"</string>
<string name="banner_set_up_recovery_submit">"回復鍵を作成"</string>
<string name="banner_set_up_recovery_title">"チャットをバックアップ"</string>
<string name="confirm_recovery_key_banner_message">"鍵の保管庫と過去のメッセージにアクセスするために、回復鍵を認証してください。"</string>
<string name="confirm_recovery_key_banner_primary_button_title">"回復鍵を入力してください"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"回復鍵を忘れましたか?"</string>
<string name="confirm_recovery_key_banner_title">"鍵の保管庫を同期できません。"</string>
<string name="full_screen_intent_banner_message">"重要な電話を確実に受け取るため、端末がロックされている状態での全画面通知を、設定から許可してください。"</string>
<string name="full_screen_intent_banner_title">"通話品質を高める"</string>
<string name="screen_home_tab_chats">"チャット"</string>
<string name="screen_home_tab_spaces">"スペース"</string>
<string name="screen_invites_decline_chat_message">"%1$sへの招待を本当に破棄しますか"</string>
<string name="screen_invites_decline_chat_title">"招待を破棄"</string>
<string name="screen_invites_decline_direct_chat_message">"%1$sとのチャットを本当に拒否しますか"</string>
<string name="screen_invites_decline_direct_chat_title">"チャットを拒否"</string>
<string name="screen_invites_empty_list">"招待はありません"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) があなたを招待しました"</string>
<string name="screen_migration_message">"一度限りの工程です。お待ちください。"</string>
<string name="screen_migration_title">"アカウントを設定しています。"</string>
<string name="screen_roomlist_a11y_create_message">"新しい会話またはルームを作成"</string>
<string name="screen_roomlist_clear_filters">"フィルターを解除"</string>
<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_invites">"招待"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"承認待ちの招待はありません"</string>
<string name="screen_roomlist_filter_low_priority">"低い優先度"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"低い優先度のチャットはまだありません"</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_knock_event_sent_description">"参加リクエストを送信しました"</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_tombstoned_room_description">"このルームはアップグレードされました"</string>
<string name="screen_roomlist_your_spaces">"あなたのスペース"</string>
<string name="session_verification_banner_message">"新しいデバイスをご利用のようです。暗号化されたメッセージにアクセスするには、別のデバイスで検証してください。"</string>
<string name="session_verification_banner_title">"本人確認"</string>
</resources>

View file

@ -5,7 +5,7 @@
<string name="banner_battery_optimization_title_android">"Уведомления не приходят?"</string>
<string name="banner_new_sound_message">"Ваши уведомления были обновлены — теперь они понятнее, быстрее и менее отвлекающие."</string>
<string name="banner_new_sound_title">"Мы обновили ваши звуки"</string>
<string name="banner_set_up_recovery_content">"Создайте новый ключ восстановления, который можно использовать для восстановления зашифрованной истории сообщений в случае потери доступа к своим устройствам."</string>
<string name="banner_set_up_recovery_content">"Ваши чаты автоматически резервируются с использованием сквозного шифрования. Для восстановления этой резервной копии и сохранения вашей цифровой личности в случае потери доступа ко всем вашим устройствам вам потребуется ключ восстановления."</string>
<string name="banner_set_up_recovery_submit">"Получить ключ восстановления"</string>
<string name="banner_set_up_recovery_title">"Сделайте резервную копию своих чатов."</string>
<string name="confirm_recovery_key_banner_message">"Подтвердите ключ восстановления, чтобы сохранить доступ к хранилищу ключей и истории сообщений."</string>

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Tắt tính năng tối ưu hóa pin cho ứng dụng này để đảm bảo nhận được mọi thông báo."</string>
<string name="banner_battery_optimization_submit_android">"Tắt tối ưu hóa"</string>
<string name="banner_battery_optimization_title_android">"Có nhận được thông báo không?"</string>
<string name="banner_new_sound_message">"Thông báo của bạn đã được cập nhật — rõ ràng hơn, nhanh hơn và ít gây khó chịu hơn."</string>
<string name="banner_new_sound_title">"Chúng tôi đã làm mới âm thanh của bạn."</string>
<string name="banner_set_up_recovery_content">"Các cuộc trò chuyện của bạn được tự động sao lưu bằng mã hóa đầu cuối. Để khôi phục bản sao lưu này và giữ lại danh tính kỹ thuật số của bạn khi bạn mất quyền truy cập vào tất cả các thiết bị, bạn sẽ cần khóa khôi phục."</string>
<string name="banner_set_up_recovery_submit">"Lấy khóa khôi phục."</string>
<string name="banner_set_up_recovery_title">"Sao lưu tin nhắn của bạn"</string>
<string name="confirm_recovery_key_banner_message">"Xác nhận khóa khôi phục để không bị mất quyền truy cập vào tin nhắn."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Nhập khóa khôi phục của bạn."</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Bạn quên khóa khôi phục?”"</string>
<string name="confirm_recovery_key_banner_title">"Dữ liệu khóa của bạn không còn đồng bộ"</string>
<string name="screen_home_tab_chats">"Cuộc trò chuyện"</string>
<string name="screen_invites_decline_chat_message">"Bạn có chắc muốn từ chối lời mời tham gia %1$s không?"</string>
<string name="screen_invites_decline_chat_title">"Từ chối lời mời"</string>
<string name="screen_invites_decline_direct_chat_message">"Bạn có chắc muốn từ chối cuộc trò chuyện riêng với %1$s không?"</string>
<string name="screen_invites_decline_direct_chat_title">"Từ chối trò chuyện"</string>
<string name="screen_invites_empty_list">"Không có lời mời"</string>
<string name="screen_invites_invited_you">"%1$s(%2$s ) đã mời bạn"</string>
<string name="screen_migration_message">"Quá trình này chỉ thực hiện một lần, cảm ơn bạn đã kiên nhẫn."</string>
<string name="screen_migration_title">"Đang thiết lập tài khoản của bạn."</string>
<string name="screen_roomlist_a11y_create_message">"Tạo một cuộc trò chuyện hoặc phòng mới"</string>
<string name="screen_roomlist_empty_message">"Bắt đầu bằng cách nhắn tin cho ai đó."</string>
<string name="screen_roomlist_empty_title">"Chưa có cuộc trò chuyện nào."</string>
<string name="screen_roomlist_filter_favourites">"Yêu thích"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Bạn có thể thêm cuộc trò chuyện vào mục yêu thích trong cài đặt chat.
Hiện tại, bạn có thể bỏ chọn bộ lọc để xem các cuộc trò chuyện khác."</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Bạn chưa có cuộc trò chuyện yêu thích nào."</string>
<string name="screen_roomlist_filter_invites">"Lời mời"</string>
<string name="screen_roomlist_filter_low_priority">"Ưu tiên thấp"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Bạn có thể bỏ chọn bộ lọc để xem các cuộc trò chuyện khác"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Bạn không có cuộc trò chuyện nào cho lựa chọn này"</string>
<string name="screen_roomlist_filter_people">"Danh bạ"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Bạn chưa có tin nhắn riêng nào cả"</string>
<string name="screen_roomlist_filter_rooms">"Phòng"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Bạn chưa tham gia phòng nào"</string>
<string name="screen_roomlist_filter_unreads">"Chưa đọc"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Chúc mừng!
Bạn không còn tin nhắn nào chưa đọc nữa!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Yêu cầu tham gia đã được gửi"</string>
<string name="screen_roomlist_main_space_title">"Cuộc trò chuyện"</string>
<string name="screen_roomlist_mark_as_read">"Đánh dấu đã đọc"</string>
<string name="screen_roomlist_mark_as_unread">"Đánh dấu chưa đọc"</string>
<string name="session_verification_banner_message">"Có vẻ như bạn đang sử dụng thiết bị mới. Hãy xác minh bằng một thiết bị khác để truy cập tin nhắn được mã hóa của bạn."</string>
<string name="session_verification_banner_title">"Xác thực danh tính của bạn"</string>
</resources>

View file

@ -6,7 +6,7 @@
<string name="banner_new_sound_message">"您的通知提示音已升级 - 更清晰、更快速、干扰更少。"</string>
<string name="banner_new_sound_title">"我们已更新您的声音"</string>
<string name="banner_set_up_recovery_content">"生成新的恢复密钥,该密钥可用于在您无法访问设备时恢复加密的消息历史记录。"</string>
<string name="banner_set_up_recovery_submit">"设置恢复"</string>
<string name="banner_set_up_recovery_submit">"获取恢复密钥"</string>
<string name="banner_set_up_recovery_title">"设置恢复"</string>
<string name="confirm_recovery_key_banner_message">"确认恢复密钥,以保持对密钥存储和消息历史的访问。"</string>
<string name="confirm_recovery_key_banner_primary_button_title">"输入恢复密钥"</string>

View file

@ -9,14 +9,11 @@
package io.element.android.features.home.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.home.impl.roomlist.aRoomListState
import io.element.android.features.home.impl.spaces.HomeSpacesState
import io.element.android.features.home.impl.spaces.aHomeSpacesState
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
import io.element.android.features.rageshake.test.logs.FakeAnnouncementService
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.indicator.api.IndicatorService
@ -33,10 +30,7 @@ import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.tests.testutils.MutablePresenter
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
@ -79,7 +73,6 @@ class HomePresenterTest {
MatrixUser(A_USER_ID, A_USER_NAME, AN_AVATAR_URL)
)
assertThat(withUserState.showAvatarIndicator).isFalse()
assertThat(withUserState.showNavigationBar).isTrue()
}
}
@ -139,14 +132,10 @@ class HomePresenterTest {
@Test
fun `present - NavigationBar change`() = runTest {
val showAnnouncementResult = lambdaRecorder<Announcement, Unit> { }
val presenter = createHomePresenter(
sessionStore = InMemorySessionStore(
updateUserProfileResult = { _, _, _ -> },
),
announcementService = FakeAnnouncementService(
showAnnouncementResult = showAnnouncementResult,
)
)
presenter.test {
val initialState = awaitItem()
@ -154,38 +143,6 @@ class HomePresenterTest {
initialState.eventSink(HomeEvent.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces))
val finalState = awaitItem()
assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces)
showAnnouncementResult.assertions().isCalledOnce()
.with(value(Announcement.Space))
}
}
@Test
fun `present - NavigationBar is hidden when the last space is left when the user can't create new spaces`() = runTest {
val homeSpacesPresenter = MutablePresenter(aHomeSpacesState())
val presenter = createHomePresenter(
sessionStore = InMemorySessionStore(
updateUserProfileResult = { _, _, _ -> },
),
homeSpacesPresenter = homeSpacesPresenter,
announcementService = FakeAnnouncementService(
showAnnouncementResult = {},
)
)
presenter.test {
val initialState = awaitItem()
assertThat(initialState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats)
assertThat(initialState.showNavigationBar).isTrue()
// User navigate to Spaces
initialState.eventSink(HomeEvent.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces))
val spaceState = awaitItem()
assertThat(spaceState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces)
// The last space is left
homeSpacesPresenter.updateState(aHomeSpacesState(spaceRooms = emptyList(), canCreateSpaces = false))
skipItems(1)
val finalState = awaitItem()
// We are back to Chats
assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats)
assertThat(finalState.showNavigationBar).isFalse()
}
}
}
@ -198,7 +155,6 @@ internal fun createHomePresenter(
indicatorService: IndicatorService = FakeIndicatorService(),
homeSpacesPresenter: Presenter<HomeSpacesState> = Presenter { aHomeSpacesState() },
sessionStore: SessionStore = InMemorySessionStore(),
announcementService: AnnouncementService = FakeAnnouncementService(),
) = HomePresenter(
client = client,
syncService = syncService,
@ -209,5 +165,4 @@ internal fun createHomePresenter(
logoutPresenter = { aDirectLogoutState() },
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
sessionStore = sessionStore,
announcementService = announcementService,
)

View file

@ -101,6 +101,7 @@ internal fun createRoomListRoomSummary(
displayType = displayType,
userDefinedNotificationMode = userDefinedNotificationMode,
hasRoomCall = false,
activeCallIntent = null,
isDirect = false,
isFavorite = isFavorite,
canonicalAlias = null,

View file

@ -8,8 +8,6 @@
package io.element.android.features.home.impl.spacefilters
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
@ -21,26 +19,9 @@ import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class SpaceFiltersPresenterTest {
@Test
fun `present - when feature flag is disabled returns Disabled state`() = runTest {
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to false)
)
)
presenter.test {
val state = awaitItem()
assertThat(state).isEqualTo(SpaceFiltersState.Disabled)
}
}
@Test
fun `present - when available filters is empty returns Disabled state`() = runTest {
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
)
)
val presenter = createSpaceFiltersPresenter()
presenter.test {
val state = awaitLastSequentialItem()
assertThat(state).isEqualTo(SpaceFiltersState.Disabled)
@ -48,15 +29,12 @@ class SpaceFiltersPresenterTest {
}
@Test
fun `present - when feature flag is enabled and filters exist returns Unselected state`() = runTest {
fun `present - when filters exist returns Unselected state`() = runTest {
val spaceFilter = aSpaceServiceFilter(displayName = "Test Space")
val spaceService = FakeSpaceService()
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
@ -75,9 +53,6 @@ class SpaceFiltersPresenterTest {
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
@ -99,9 +74,6 @@ class SpaceFiltersPresenterTest {
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
@ -129,9 +101,6 @@ class SpaceFiltersPresenterTest {
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
@ -159,9 +128,6 @@ class SpaceFiltersPresenterTest {
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
@ -196,9 +162,6 @@ class SpaceFiltersPresenterTest {
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
@ -224,9 +187,6 @@ class SpaceFiltersPresenterTest {
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
@ -271,9 +231,6 @@ class SpaceFiltersPresenterTest {
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
@ -302,11 +259,9 @@ class SpaceFiltersPresenterTest {
}
private fun createSpaceFiltersPresenter(
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
matrixClient: FakeMatrixClient = FakeMatrixClient(),
): SpaceFiltersPresenter {
return SpaceFiltersPresenter(
featureFlagService = featureFlagService,
matrixClient = matrixClient,
)
}

View file

@ -11,9 +11,6 @@ package io.element.android.features.home.impl.spaces
import com.google.common.truth.Truth.assertThat
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.test.InMemorySeenInvitesStore
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.tests.testutils.test
@ -26,25 +23,18 @@ class HomeSpacesPresenterTest {
val presenter = createPresenter()
presenter.test {
val state = awaitItem()
// canCreateSpaces is initially false
assertThat(state.canCreateSpaces).isFalse()
assertThat(state.space).isEqualTo(CurrentSpace.Root)
assertThat(state.spaceRooms).isEmpty()
assertThat(state.hideInvitesAvatar).isFalse()
assertThat(state.seenSpaceInvites).isEmpty()
// It'll eventually be true
assertThat(awaitItem().canCreateSpaces).isTrue()
}
}
private fun createPresenter(
client: MatrixClient = FakeMatrixClient(),
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
featureFlagsService: FeatureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.CreateSpaces.key to true)),
) = HomeSpacesPresenter(
client = client,
seenInvitesStore = seenInvitesStore,
featureFlagsService = featureFlagsService,
)
}

View file

@ -1,10 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_title">"Заблакіраваць карыстальніка"</string>
<string name="screen_decline_and_block_title">"Адхіліць і заблакіраваць"</string>
<string name="screen_invites_decline_chat_message">"Вы ўпэўненыя, што хочаце адхіліць запрашэнне ў %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Адхіліць запрашэнне"</string>
<string name="screen_invites_decline_direct_chat_message">"Вы ўпэўненыя, што хочаце адмовіцца ад прыватных зносін з %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Адхіліць чат"</string>
<string name="screen_invites_empty_list">"Няма запрашэнняў"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) запрасіў(-ла) вас"</string>
<string name="screen_join_room_decline_and_block_button_title">"Адхіліць і заблакіраваць"</string>
</resources>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"このユーザーからのメッセージと招待を非表示します"</string>
<string name="screen_decline_and_block_block_user_option_title">"ユーザーをブロック"</string>
<string name="screen_decline_and_block_report_user_option_description">"アカウント提供元にこのルームを報告"</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"報告の理由を説明してください…"</string>
<string name="screen_decline_and_block_title">"拒否してブロック"</string>
<string name="screen_invites_decline_chat_message">"%1$sへの招待を本当に破棄しますか"</string>
<string name="screen_invites_decline_chat_title">"招待を破棄"</string>
<string name="screen_invites_decline_direct_chat_message">"%1$sとのチャットを本当に拒否しますか"</string>
<string name="screen_invites_decline_direct_chat_title">"チャットを拒否"</string>
<string name="screen_invites_empty_list">"招待はありません"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) があなたを招待しました"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"拒否してブロックする"</string>
<string name="screen_join_room_decline_and_block_alert_message">"本当にこのルームへの参加の招待を拒否しますか?%1$s は、あなたと会話することやルームに招待することができなくなります。"</string>
<string name="screen_join_room_decline_and_block_alert_title">"招待を拒否してブロック"</string>
<string name="screen_join_room_decline_and_block_button_title">"拒否してブロック"</string>
</resources>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Bạn sẽ không nhận được bất kỳ tin nhắn hoặc lời mời tham gia phòng nào từ người dùng này."</string>
<string name="screen_decline_and_block_block_user_option_title">"Chặn người dùng"</string>
<string name="screen_decline_and_block_report_user_option_description">"Báo cáo phòng này cho nhà cung cấp tài khoản của bạn."</string>
<string name="screen_decline_and_block_title">"Từ chối và chặn"</string>
<string name="screen_invites_decline_chat_message">"Bạn có chắc muốn từ chối lời mời tham gia %1$s không?"</string>
<string name="screen_invites_decline_chat_title">"Từ chối lời mời"</string>
<string name="screen_invites_decline_direct_chat_message">"Bạn có chắc muốn từ chối cuộc trò chuyện riêng với %1$s không?"</string>
<string name="screen_invites_decline_direct_chat_title">"Từ chối trò chuyện"</string>
<string name="screen_invites_empty_list">"Không có lời mời"</string>
<string name="screen_invites_invited_you">"%1$s(%2$s ) đã mời bạn"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Có, từ chối &amp; chặn"</string>
<string name="screen_join_room_decline_and_block_alert_message">"Bạn có chắc muốn từ chối lời mời tham gia phòng này không? Điều này cũng sẽ ngăn %1$s liên hệ với bạn hoặc mời bạn vào các phòng."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Từ chối lời mời và chặn"</string>
<string name="screen_join_room_decline_and_block_button_title">"Từ chối và chặn"</string>
</resources>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"您不会看到来自该用户的任何信息或房间邀请"</string>
<string name="screen_decline_and_block_block_user_option_title">"封禁用户"</string>
<string name="screen_decline_and_block_block_user_option_description">"您不会看到来自该用户的任何信息或房间邀请"</string>
<string name="screen_decline_and_block_block_user_option_title">"屏蔽用户"</string>
<string name="screen_decline_and_block_report_user_option_description">"向您的帐户提供商举报此房间。"</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"描述举报的原因…"</string>
<string name="screen_decline_and_block_title">"拒绝并屏蔽"</string>

View file

@ -34,13 +34,15 @@ dependencies {
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.usersearch.impl)
implementation(projects.libraries.usersearch.api)
implementation(libs.coil.compose)
implementation(projects.services.apperror.api)
implementation(projects.libraries.featureflag.api)
api(projects.features.invitepeople.api)
testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.usersearch.test)
testImplementation(projects.services.apperror.test)
testImplementation(projects.libraries.featureflag.test)
}

View file

@ -0,0 +1,16 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invitepeople.impl
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
data class ConfirmingUnknownUserInvitation(
val users: ImmutableList<MatrixUser>
) : AsyncAction.Confirming

Some files were not shown because too many files have changed in this diff Show more