Merge branch 'release/0.4.8' into main
This commit is contained in:
commit
06c34d69ab
1015 changed files with 12210 additions and 2845 deletions
11
.github/ISSUE_TEMPLATE/task-that-belongs-to-a-story-epic.md
vendored
Normal file
11
.github/ISSUE_TEMPLATE/task-that-belongs-to-a-story-epic.md
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
name: Task that belongs to a story/epic
|
||||||
|
about: A skeleton task where the details are all contained within a story or epic
|
||||||
|
on element-meta.
|
||||||
|
title: "[Task] "
|
||||||
|
labels: T-Task
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Please see and discuss the details in the meta issue.
|
||||||
2
.github/workflows/generate_github_pages.yml
vendored
2
.github/workflows/generate_github_pages.yml
vendored
|
|
@ -32,7 +32,7 @@ jobs:
|
||||||
mkdir -p screenshots/en
|
mkdir -p screenshots/en
|
||||||
cp tests/uitests/src/test/snapshots/images/* screenshots/en
|
cp tests/uitests/src/test/snapshots/images/* screenshots/en
|
||||||
- name: Deploy GitHub Pages
|
- name: Deploy GitHub Pages
|
||||||
uses: peaceiris/actions-gh-pages@v3
|
uses: peaceiris/actions-gh-pages@v4
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
publish_dir: ./screenshots
|
publish_dir: ./screenshots
|
||||||
|
|
|
||||||
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="1.9.22" />
|
<option name="version" value="1.9.23" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
|
|
@ -23,7 +23,8 @@ appId: ${MAESTRO_APP_ID}
|
||||||
- inputText: ${MAESTRO_PASSWORD}
|
- inputText: ${MAESTRO_PASSWORD}
|
||||||
- pressKey: Enter
|
- pressKey: Enter
|
||||||
- tapOn: "Continue"
|
- tapOn: "Continue"
|
||||||
|
- runFlow: ../assertions/assertSessionVerificationDisplayed.yaml
|
||||||
|
- runFlow: ./verifySession.yaml
|
||||||
- runFlow: ../assertions/assertAnalyticsDisplayed.yaml
|
- runFlow: ../assertions/assertAnalyticsDisplayed.yaml
|
||||||
- tapOn: "Not now"
|
- tapOn: "Not now"
|
||||||
- runFlow: ../assertions/assertHomeDisplayed.yaml
|
- runFlow: ../assertions/assertHomeDisplayed.yaml
|
||||||
- runFlow: ./verifySession.yaml
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
appId: ${MAESTRO_APP_ID}
|
appId: ${MAESTRO_APP_ID}
|
||||||
---
|
---
|
||||||
- tapOn: "Continue"
|
|
||||||
- takeScreenshot: build/maestro/150-Verify
|
- takeScreenshot: build/maestro/150-Verify
|
||||||
- tapOn: "Enter recovery key"
|
- tapOn: "Enter recovery key"
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "verification-recovery_key"
|
id: "verification-recovery_key"
|
||||||
- inputText: ${MAESTRO_RECOVERY_KEY}
|
- inputText: ${MAESTRO_RECOVERY_KEY}
|
||||||
- hideKeyboard
|
- hideKeyboard
|
||||||
- tapOn: "Confirm"
|
- tapOn: "Continue"
|
||||||
- runFlow: ../assertions/assertHomeDisplayed.yaml
|
- extendedWaitUntil:
|
||||||
|
visible: "Device verified"
|
||||||
|
timeout: 10000
|
||||||
|
- tapOn: "Continue"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
appId: ${MAESTRO_APP_ID}
|
||||||
|
---
|
||||||
|
- extendedWaitUntil:
|
||||||
|
visible: "Confirm that it's you"
|
||||||
|
timeout: 20000
|
||||||
|
|
@ -7,8 +7,4 @@ appId: ${MAESTRO_APP_ID}
|
||||||
- tapOn: ${MAESTRO_ROOM_NAME}
|
- tapOn: ${MAESTRO_ROOM_NAME}
|
||||||
# Back from timeline
|
# Back from timeline
|
||||||
- back
|
- back
|
||||||
- assertVisible: "MyR"
|
|
||||||
- hideKeyboard
|
|
||||||
# Back from search
|
|
||||||
- back
|
|
||||||
- runFlow: ../assertions/assertHomeDisplayed.yaml
|
- runFlow: ../assertions/assertHomeDisplayed.yaml
|
||||||
|
|
|
||||||
27
CHANGES.md
27
CHANGES.md
|
|
@ -1,3 +1,30 @@
|
||||||
|
Changes in Element X v0.4.8 (2024-04-10)
|
||||||
|
========================================
|
||||||
|
|
||||||
|
Features ✨
|
||||||
|
----------
|
||||||
|
- Move session recovery to the login flow. ([#2579](https://github.com/element-hq/element-x-android/issues/2579))
|
||||||
|
- Move session verification to the after login flow and make it mandatory. ([#2580](https://github.com/element-hq/element-x-android/issues/2580))
|
||||||
|
- Add a notification troubleshoot screen ([#2601](https://github.com/element-hq/element-x-android/issues/2601))
|
||||||
|
- Add action to copy permalink ([#2650](https://github.com/element-hq/element-x-android/issues/2650))
|
||||||
|
|
||||||
|
Bugfixes 🐛
|
||||||
|
----------
|
||||||
|
- Fix analytics issue around room considered as space by mistake. ([#2612](https://github.com/element-hq/element-x-android/issues/2612))
|
||||||
|
- Fix crash observed when going back to the room list. ([#2619](https://github.com/element-hq/element-x-android/issues/2619))
|
||||||
|
- Hide Event org.matrix.msc3401.call.member on the timeline. ([#2625](https://github.com/element-hq/element-x-android/issues/2625))
|
||||||
|
- Fall back to name-based generated avatars when image avatars don't load. ([#2667](https://github.com/element-hq/element-x-android/issues/2667))
|
||||||
|
|
||||||
|
Other changes
|
||||||
|
-------------
|
||||||
|
- Improve UI for notification permission screen in onboarding. ([#2581](https://github.com/element-hq/element-x-android/issues/2581))
|
||||||
|
- Categorise members by role in change roles screen. ([#2593](https://github.com/element-hq/element-x-android/issues/2593))
|
||||||
|
- Make completed poll more clearly visible ([#2608](https://github.com/element-hq/element-x-android/issues/2608))
|
||||||
|
- Show users from last visited DM as suggestion when starting a Chat or when creating a Room. ([#2634](https://github.com/element-hq/element-x-android/issues/2634))
|
||||||
|
- Enable room moderation feature. ([#2678](https://github.com/element-hq/element-x-android/issues/2678))
|
||||||
|
- Improve analytics opt-in screen UI. ([#2684](https://github.com/element-hq/element-x-android/issues/2684))
|
||||||
|
|
||||||
|
|
||||||
Changes in Element X v0.4.7 (2024-03-26)
|
Changes in Element X v0.4.7 (2024-03-26)
|
||||||
========================================
|
========================================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ package io.element.android.x
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
|
@ -31,7 +32,6 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.WindowCompat
|
|
||||||
import com.bumble.appyx.core.integration.NodeHost
|
import com.bumble.appyx.core.integration.NodeHost
|
||||||
import com.bumble.appyx.core.integrationpoint.NodeActivity
|
import com.bumble.appyx.core.integrationpoint.NodeActivity
|
||||||
import com.bumble.appyx.core.plugin.NodeReadyObserver
|
import com.bumble.appyx.core.plugin.NodeReadyObserver
|
||||||
|
|
@ -60,7 +60,7 @@ class MainActivity : NodeActivity() {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
appBindings = bindings()
|
appBindings = bindings()
|
||||||
appBindings.lockScreenService().handleSecureFlag(this)
|
appBindings.lockScreenService().handleSecureFlag(this)
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
MainContent(appBindings)
|
MainContent(appBindings)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,16 +14,13 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
plugins {
|
plugins {
|
||||||
id("java-library")
|
id("io.element.android-library")
|
||||||
id("com.android.lint")
|
|
||||||
alias(libs.plugins.kotlin.jvm)
|
|
||||||
alias(libs.plugins.anvil)
|
alias(libs.plugins.anvil)
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
android {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
namespace = "io.element.android.appconfig"
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
|
||||||
}
|
}
|
||||||
|
|
||||||
anvil {
|
anvil {
|
||||||
|
|
@ -33,4 +30,5 @@ anvil {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.dagger)
|
implementation(libs.dagger)
|
||||||
implementation(projects.libraries.di)
|
implementation(projects.libraries.di)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,29 @@
|
||||||
|
|
||||||
package io.element.android.appconfig
|
package io.element.android.appconfig
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||||
|
|
||||||
object TimelineConfig {
|
object TimelineConfig {
|
||||||
const val MAX_READ_RECEIPT_TO_DISPLAY = 3
|
const val MAX_READ_RECEIPT_TO_DISPLAY = 3
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event types that will be filtered out from the timeline (i.e. not displayed).
|
||||||
|
*/
|
||||||
|
val excludedEvents = listOf(
|
||||||
|
StateEventType.CALL_MEMBER,
|
||||||
|
StateEventType.ROOM_ALIASES,
|
||||||
|
StateEventType.ROOM_CANONICAL_ALIAS,
|
||||||
|
StateEventType.ROOM_GUEST_ACCESS,
|
||||||
|
StateEventType.ROOM_HISTORY_VISIBILITY,
|
||||||
|
StateEventType.ROOM_JOIN_RULES,
|
||||||
|
StateEventType.ROOM_PINNED_EVENTS,
|
||||||
|
StateEventType.ROOM_POWER_LEVELS,
|
||||||
|
StateEventType.ROOM_SERVER_ACL,
|
||||||
|
StateEventType.ROOM_TOMBSTONE,
|
||||||
|
StateEventType.SPACE_CHILD,
|
||||||
|
StateEventType.SPACE_PARENT,
|
||||||
|
StateEventType.POLICY_RULE_ROOM,
|
||||||
|
StateEventType.POLICY_RULE_SERVER,
|
||||||
|
StateEventType.POLICY_RULE_USER,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ dependencies {
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.features.networkmonitor.test)
|
testImplementation(projects.features.networkmonitor.test)
|
||||||
testImplementation(projects.features.login.impl)
|
testImplementation(projects.features.login.impl)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,6 @@ package io.element.android.appnav
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
|
||||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
|
@ -34,16 +32,12 @@ import javax.inject.Inject
|
||||||
class LoggedInEventProcessor @Inject constructor(
|
class LoggedInEventProcessor @Inject constructor(
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
roomMembershipObserver: RoomMembershipObserver,
|
roomMembershipObserver: RoomMembershipObserver,
|
||||||
sessionVerificationService: SessionVerificationService,
|
|
||||||
) {
|
) {
|
||||||
private var observingJob: Job? = null
|
private var observingJob: Job? = null
|
||||||
|
|
||||||
private val displayLeftRoomMessage = roomMembershipObserver.updates
|
private val displayLeftRoomMessage = roomMembershipObserver.updates
|
||||||
.map { !it.isUserInRoom }
|
.map { !it.isUserInRoom }
|
||||||
|
|
||||||
private val displayVerificationSuccessfulMessage = sessionVerificationService.verificationFlowState
|
|
||||||
.map { it == VerificationFlowState.Finished }
|
|
||||||
|
|
||||||
fun observeEvents(coroutineScope: CoroutineScope) {
|
fun observeEvents(coroutineScope: CoroutineScope) {
|
||||||
observingJob = coroutineScope.launch {
|
observingJob = coroutineScope.launch {
|
||||||
displayLeftRoomMessage
|
displayLeftRoomMessage
|
||||||
|
|
@ -52,13 +46,6 @@ class LoggedInEventProcessor @Inject constructor(
|
||||||
displayMessage(CommonStrings.common_current_user_left_room)
|
displayMessage(CommonStrings.common_current_user_left_room)
|
||||||
}
|
}
|
||||||
.launchIn(this)
|
.launchIn(this)
|
||||||
|
|
||||||
displayVerificationSuccessfulMessage
|
|
||||||
.filter { it }
|
|
||||||
.onEach {
|
|
||||||
displayMessage(CommonStrings.common_verification_complete)
|
|
||||||
}
|
|
||||||
.launchIn(this)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ import io.element.android.appnav.room.RoomFlowNode
|
||||||
import io.element.android.appnav.room.RoomLoadedFlowNode
|
import io.element.android.appnav.room.RoomLoadedFlowNode
|
||||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||||
import io.element.android.features.ftue.api.FtueEntryPoint
|
import io.element.android.features.ftue.api.FtueEntryPoint
|
||||||
|
import io.element.android.features.ftue.api.state.FtueService
|
||||||
import io.element.android.features.ftue.api.state.FtueState
|
import io.element.android.features.ftue.api.state.FtueState
|
||||||
import io.element.android.features.invitelist.api.InviteListEntryPoint
|
import io.element.android.features.invitelist.api.InviteListEntryPoint
|
||||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||||
|
|
@ -53,15 +54,16 @@ import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
||||||
|
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
|
||||||
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
||||||
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
|
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
|
||||||
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
|
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.architecture.waitForChildAttached
|
import io.element.android.libraries.architecture.waitForChildAttached
|
||||||
import io.element.android.libraries.deeplink.DeeplinkData
|
import io.element.android.libraries.deeplink.DeeplinkData
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
||||||
|
|
@ -74,6 +76,8 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
@ -87,21 +91,21 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
private val preferencesEntryPoint: PreferencesEntryPoint,
|
private val preferencesEntryPoint: PreferencesEntryPoint,
|
||||||
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
||||||
private val appNavigationStateService: AppNavigationStateService,
|
private val appNavigationStateService: AppNavigationStateService,
|
||||||
private val verifySessionEntryPoint: VerifySessionEntryPoint,
|
|
||||||
private val secureBackupEntryPoint: SecureBackupEntryPoint,
|
private val secureBackupEntryPoint: SecureBackupEntryPoint,
|
||||||
private val inviteListEntryPoint: InviteListEntryPoint,
|
private val inviteListEntryPoint: InviteListEntryPoint,
|
||||||
private val ftueEntryPoint: FtueEntryPoint,
|
private val ftueEntryPoint: FtueEntryPoint,
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val notificationDrawerManager: NotificationDrawerManager,
|
private val notificationDrawerManager: NotificationDrawerManager,
|
||||||
private val ftueState: FtueState,
|
private val ftueService: FtueService,
|
||||||
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
||||||
private val lockScreenStateService: LockScreenService,
|
private val lockScreenStateService: LockScreenService,
|
||||||
|
private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
snackbarDispatcher: SnackbarDispatcher,
|
snackbarDispatcher: SnackbarDispatcher,
|
||||||
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.RoomList,
|
initialElement = NavTarget.Placeholder,
|
||||||
savedStateMap = buildContext.savedStateMap,
|
savedStateMap = buildContext.savedStateMap,
|
||||||
),
|
),
|
||||||
permanentNavModel = PermanentNavModel(
|
permanentNavModel = PermanentNavModel(
|
||||||
|
|
@ -119,7 +123,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
private val loggedInFlowProcessor = LoggedInEventProcessor(
|
private val loggedInFlowProcessor = LoggedInEventProcessor(
|
||||||
snackbarDispatcher,
|
snackbarDispatcher,
|
||||||
matrixClient.roomMembershipObserver(),
|
matrixClient.roomMembershipObserver(),
|
||||||
matrixClient.sessionVerificationService(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
|
|
@ -131,9 +134,15 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
|
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
|
||||||
loggedInFlowProcessor.observeEvents(coroutineScope)
|
loggedInFlowProcessor.observeEvents(coroutineScope)
|
||||||
|
|
||||||
if (ftueState.shouldDisplayFlow.value) {
|
ftueService.state
|
||||||
backstack.push(NavTarget.Ftue)
|
.onEach { ftueState ->
|
||||||
}
|
when (ftueState) {
|
||||||
|
is FtueState.Unknown -> Unit // Nothing to do
|
||||||
|
is FtueState.Incomplete -> backstack.safeRoot(NavTarget.Ftue)
|
||||||
|
is FtueState.Complete -> backstack.safeRoot(NavTarget.RoomList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(lifecycleScope)
|
||||||
},
|
},
|
||||||
onStop = {
|
onStop = {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
|
|
@ -189,6 +198,9 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
|
@Parcelize
|
||||||
|
data object Placeholder : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object LoggedInPermanent : NavTarget
|
data object LoggedInPermanent : NavTarget
|
||||||
|
|
||||||
|
|
@ -212,9 +224,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object CreateRoom : NavTarget
|
data object CreateRoom : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object VerifySession : NavTarget
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class SecureBackup(
|
data class SecureBackup(
|
||||||
val initialElement: SecureBackupEntryPoint.InitialTarget = SecureBackupEntryPoint.InitialTarget.Root
|
val initialElement: SecureBackupEntryPoint.InitialTarget = SecureBackupEntryPoint.InitialTarget.Root
|
||||||
|
|
@ -225,10 +234,14 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object Ftue : NavTarget
|
data object Ftue : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object RoomDirectorySearch : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
|
NavTarget.Placeholder -> createNode<PlaceholderNode>(buildContext)
|
||||||
NavTarget.LoggedInPermanent -> {
|
NavTarget.LoggedInPermanent -> {
|
||||||
createNode<LoggedInNode>(buildContext)
|
createNode<LoggedInNode>(buildContext)
|
||||||
}
|
}
|
||||||
|
|
@ -251,10 +264,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
backstack.push(NavTarget.CreateRoom)
|
backstack.push(NavTarget.CreateRoom)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSessionVerificationClicked() {
|
|
||||||
backstack.push(NavTarget.VerifySession)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSessionConfirmRecoveryKeyClicked() {
|
override fun onSessionConfirmRecoveryKeyClicked() {
|
||||||
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey))
|
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey))
|
||||||
}
|
}
|
||||||
|
|
@ -270,6 +279,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
override fun onReportBugClicked() {
|
override fun onReportBugClicked() {
|
||||||
plugins<Callback>().forEach { it.onOpenBugReport() }
|
plugins<Callback>().forEach { it.onOpenBugReport() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRoomDirectorySearchClicked() {
|
||||||
|
backstack.push(NavTarget.RoomDirectorySearch)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
roomListEntryPoint
|
roomListEntryPoint
|
||||||
.nodeBuilder(this, buildContext)
|
.nodeBuilder(this, buildContext)
|
||||||
|
|
@ -299,10 +312,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
plugins<Callback>().forEach { it.onOpenBugReport() }
|
plugins<Callback>().forEach { it.onOpenBugReport() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onVerifyClicked() {
|
|
||||||
backstack.push(NavTarget.VerifySession)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSecureBackupClicked() {
|
override fun onSecureBackupClicked() {
|
||||||
backstack.push(NavTarget.SecureBackup())
|
backstack.push(NavTarget.SecureBackup())
|
||||||
}
|
}
|
||||||
|
|
@ -329,25 +338,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
.callback(callback)
|
.callback(callback)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
NavTarget.VerifySession -> {
|
|
||||||
val callback = object : VerifySessionEntryPoint.Callback {
|
|
||||||
override fun onEnterRecoveryKey() {
|
|
||||||
backstack.replace(
|
|
||||||
NavTarget.SecureBackup(
|
|
||||||
initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDone() {
|
|
||||||
backstack.pop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
verifySessionEntryPoint
|
|
||||||
.nodeBuilder(this, buildContext)
|
|
||||||
.callback(callback)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
is NavTarget.SecureBackup -> {
|
is NavTarget.SecureBackup -> {
|
||||||
secureBackupEntryPoint.nodeBuilder(this, buildContext)
|
secureBackupEntryPoint.nodeBuilder(this, buildContext)
|
||||||
.params(SecureBackupEntryPoint.Params(initialElement = navTarget.initialElement))
|
.params(SecureBackupEntryPoint.Params(initialElement = navTarget.initialElement))
|
||||||
|
|
@ -372,7 +362,16 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
ftueEntryPoint.nodeBuilder(this, buildContext)
|
ftueEntryPoint.nodeBuilder(this, buildContext)
|
||||||
.callback(object : FtueEntryPoint.Callback {
|
.callback(object : FtueEntryPoint.Callback {
|
||||||
override fun onFtueFlowFinished() {
|
override fun onFtueFlowFinished() {
|
||||||
backstack.pop()
|
lifecycleScope.launch { attachRoomList() }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
NavTarget.RoomDirectorySearch -> {
|
||||||
|
roomDirectoryEntryPoint.nodeBuilder(this, buildContext)
|
||||||
|
.callback(object : RoomDirectoryEntryPoint.Callback {
|
||||||
|
override fun onOpenRoom(roomId: RoomId) {
|
||||||
|
coroutineScope.launch { attachRoom(roomId) }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
|
|
@ -380,20 +379,23 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun attachRoot(): Node {
|
suspend fun attachRoomList() {
|
||||||
return attachChild {
|
if (!canShowRoomList()) return
|
||||||
|
attachChild<Node> {
|
||||||
backstack.singleTop(NavTarget.RoomList)
|
backstack.singleTop(NavTarget.RoomList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun attachRoom(roomId: RoomId): RoomFlowNode {
|
suspend fun attachRoom(roomId: RoomId) {
|
||||||
return attachChild {
|
if (!canShowRoomList()) return
|
||||||
|
attachChild<RoomFlowNode> {
|
||||||
backstack.singleTop(NavTarget.RoomList)
|
backstack.singleTop(NavTarget.RoomList)
|
||||||
backstack.push(NavTarget.Room(roomId))
|
backstack.push(NavTarget.Room(roomId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun attachInviteList(deeplinkData: DeeplinkData.InviteList) = withContext(lifecycleScope.coroutineContext) {
|
internal suspend fun attachInviteList(deeplinkData: DeeplinkData.InviteList) = withContext(lifecycleScope.coroutineContext) {
|
||||||
|
if (!canShowRoomList()) return@withContext
|
||||||
notificationDrawerManager.clearMembershipNotificationForSession(deeplinkData.sessionId)
|
notificationDrawerManager.clearMembershipNotificationForSession(deeplinkData.sessionId)
|
||||||
backstack.singleTop(NavTarget.RoomList)
|
backstack.singleTop(NavTarget.RoomList)
|
||||||
backstack.push(NavTarget.InviteList)
|
backstack.push(NavTarget.InviteList)
|
||||||
|
|
@ -402,13 +404,17 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun canShowRoomList(): Boolean {
|
||||||
|
return ftueService.state.value is FtueState.Complete
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
val lockScreenState by lockScreenStateService.lockState.collectAsState()
|
val lockScreenState by lockScreenStateService.lockState.collectAsState()
|
||||||
|
val isFtueDisplayed by ftueService.state.collectAsState()
|
||||||
BackstackView()
|
BackstackView()
|
||||||
val isFtueDisplayed by ftueState.shouldDisplayFlow.collectAsState()
|
if (isFtueDisplayed is FtueState.Complete) {
|
||||||
if (!isFtueDisplayed) {
|
|
||||||
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
|
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
|
||||||
}
|
}
|
||||||
if (lockScreenState == LockScreenLockState.Locked) {
|
if (lockScreenState == LockScreenLockState.Locked) {
|
||||||
|
|
@ -416,4 +422,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ContributesNode(AppScope::class)
|
||||||
|
class PlaceholderNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
) : Node(buildContext, plugins = plugins)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -288,7 +288,7 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
.attachSession()
|
.attachSession()
|
||||||
.apply {
|
.apply {
|
||||||
when (deeplinkData) {
|
when (deeplinkData) {
|
||||||
is DeeplinkData.Root -> attachRoot()
|
is DeeplinkData.Root -> attachRoomList()
|
||||||
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId)
|
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId)
|
||||||
is DeeplinkData.InviteList -> attachInviteList(deeplinkData)
|
is DeeplinkData.InviteList -> attachInviteList(deeplinkData)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,22 +27,32 @@ import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.push.api.PushService
|
import io.element.android.libraries.push.api.PushService
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class LoggedInPresenter @Inject constructor(
|
class LoggedInPresenter @Inject constructor(
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val pushService: PushService,
|
private val pushService: PushService,
|
||||||
|
private val sessionVerificationService: SessionVerificationService,
|
||||||
) : Presenter<LoggedInState> {
|
) : Presenter<LoggedInState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): LoggedInState {
|
override fun present(): LoggedInState {
|
||||||
LaunchedEffect(Unit) {
|
val isVerified by remember {
|
||||||
// Ensure pusher is registered
|
sessionVerificationService.sessionVerifiedStatus.map { it == SessionVerifiedStatus.Verified }
|
||||||
// TODO Manually select push provider for now
|
}.collectAsState(initial = false)
|
||||||
val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect
|
|
||||||
val distributor = pushProvider.getDistributors().firstOrNull() ?: return@LaunchedEffect
|
LaunchedEffect(isVerified) {
|
||||||
pushService.registerWith(matrixClient, pushProvider, distributor)
|
if (isVerified) {
|
||||||
|
// Ensure pusher is registered
|
||||||
|
// TODO Manually select push provider for now
|
||||||
|
val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect
|
||||||
|
val distributor = pushProvider.getDistributors().firstOrNull() ?: return@LaunchedEffect
|
||||||
|
pushService.registerWith(matrixClient, pushProvider, distributor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val syncIndicator by matrixClient.roomListService.syncIndicator.collectAsState()
|
val syncIndicator by matrixClient.roomListService.syncIndicator.collectAsState()
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ import io.element.android.libraries.architecture.NodeInputs
|
||||||
import io.element.android.libraries.architecture.inputs
|
import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.di.DaggerComponentOwner
|
import io.element.android.libraries.di.DaggerComponentOwner
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
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.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
|
@ -61,6 +62,7 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
||||||
private val roomDetailsEntryPoint: RoomDetailsEntryPoint,
|
private val roomDetailsEntryPoint: RoomDetailsEntryPoint,
|
||||||
private val appNavigationStateService: AppNavigationStateService,
|
private val appNavigationStateService: AppNavigationStateService,
|
||||||
private val appCoroutineScope: CoroutineScope,
|
private val appCoroutineScope: CoroutineScope,
|
||||||
|
private val matrixClient: MatrixClient,
|
||||||
roomComponentFactory: RoomComponentFactory,
|
roomComponentFactory: RoomComponentFactory,
|
||||||
roomMembershipObserver: RoomMembershipObserver,
|
roomMembershipObserver: RoomMembershipObserver,
|
||||||
) : BaseFlowNode<RoomLoadedFlowNode.NavTarget>(
|
) : BaseFlowNode<RoomLoadedFlowNode.NavTarget>(
|
||||||
|
|
@ -92,6 +94,7 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
||||||
Timber.v("OnCreate => ${inputs.room.roomId}")
|
Timber.v("OnCreate => ${inputs.room.roomId}")
|
||||||
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
||||||
fetchRoomMembers()
|
fetchRoomMembers()
|
||||||
|
trackVisitedRoom()
|
||||||
},
|
},
|
||||||
onResume = {
|
onResume = {
|
||||||
appCoroutineScope.launch {
|
appCoroutineScope.launch {
|
||||||
|
|
@ -117,6 +120,10 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
||||||
inputs<Inputs>()
|
inputs<Inputs>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun trackVisitedRoom() = lifecycleScope.launch {
|
||||||
|
matrixClient.trackRecentlyVisitedRoom(inputs.room.roomId)
|
||||||
|
}
|
||||||
|
|
||||||
private fun fetchRoomMembers() = lifecycleScope.launch {
|
private fun fetchRoomMembers() = lifecycleScope.launch {
|
||||||
inputs.room.updateMembers()
|
inputs.room.updateMembers()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||||
import io.element.android.libraries.architecture.childNode
|
import io.element.android.libraries.architecture.childNode
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -101,6 +102,7 @@ class RoomFlowNodeTest {
|
||||||
roomMembershipObserver = RoomMembershipObserver(),
|
roomMembershipObserver = RoomMembershipObserver(),
|
||||||
appCoroutineScope = coroutineScope,
|
appCoroutineScope = coroutineScope,
|
||||||
roomComponentFactory = FakeRoomComponentFactory(),
|
roomComponentFactory = FakeRoomComponentFactory(),
|
||||||
|
matrixClient = FakeMatrixClient(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,11 @@ import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.libraries.push.api.PushService
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
import io.element.android.libraries.pushproviders.api.Distributor
|
import io.element.android.libraries.push.test.FakePushService
|
||||||
import io.element.android.libraries.pushproviders.api.PushProvider
|
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -73,20 +71,8 @@ class LoggedInPresenterTest {
|
||||||
return LoggedInPresenter(
|
return LoggedInPresenter(
|
||||||
matrixClient = FakeMatrixClient(roomListService = roomListService),
|
matrixClient = FakeMatrixClient(roomListService = roomListService),
|
||||||
networkMonitor = FakeNetworkMonitor(networkStatus),
|
networkMonitor = FakeNetworkMonitor(networkStatus),
|
||||||
pushService = object : PushService {
|
pushService = FakePushService(),
|
||||||
override fun notificationStyleChanged() {
|
sessionVerificationService = FakeSessionVerificationService(),
|
||||||
}
|
|
||||||
|
|
||||||
override fun getAvailablePushProviders(): List<PushProvider> {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun testPush() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
fastlane/metadata/android/en-US/changelogs/40004080.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40004080.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: Enable room moderation feature.
|
||||||
|
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||||
|
|
@ -17,19 +17,14 @@
|
||||||
package io.element.android.features.analytics.impl
|
package io.element.android.features.analytics.impl
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.systemBarsPadding
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.text.ClickableText
|
import androidx.compose.foundation.text.ClickableText
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Poll
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
|
@ -45,18 +40,18 @@ import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
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.InfoListItem
|
||||||
import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism
|
import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
|
import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
|
import io.element.android.libraries.designsystem.components.OnboardingBackground
|
||||||
|
import io.element.android.libraries.designsystem.components.PageTitle
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart
|
import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.ButtonSize
|
import io.element.android.libraries.designsystem.theme.components.ButtonSize
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial
|
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
|
@ -82,6 +77,7 @@ fun AnalyticsOptInView(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.systemBarsPadding()
|
.systemBarsPadding()
|
||||||
.imePadding(),
|
.imePadding(),
|
||||||
|
background = { OnboardingBackground() },
|
||||||
header = { AnalyticsOptInHeader(state, onClickTerms) },
|
header = { AnalyticsOptInHeader(state, onClickTerms) },
|
||||||
content = { AnalyticsOptInContent() },
|
content = { AnalyticsOptInContent() },
|
||||||
footer = {
|
footer = {
|
||||||
|
|
@ -103,11 +99,11 @@ private fun AnalyticsOptInHeader(
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
IconTitleSubtitleMolecule(
|
PageTitle(
|
||||||
modifier = Modifier.padding(top = 60.dp, bottom = 12.dp),
|
modifier = Modifier.padding(top = 60.dp, bottom = 12.dp),
|
||||||
title = stringResource(id = R.string.screen_analytics_prompt_title, state.applicationName),
|
title = stringResource(id = R.string.screen_analytics_prompt_title, state.applicationName),
|
||||||
subTitle = stringResource(id = R.string.screen_analytics_prompt_help_us_improve),
|
subtitle = stringResource(id = R.string.screen_analytics_prompt_help_us_improve),
|
||||||
iconImageVector = Icons.Filled.Poll
|
iconStyle = BigIcon.Style.Default(CompoundIcons.Chart())
|
||||||
)
|
)
|
||||||
val text = buildAnnotatedStringWithStyledPart(
|
val text = buildAnnotatedStringWithStyledPart(
|
||||||
R.string.screen_analytics_prompt_read_terms,
|
R.string.screen_analytics_prompt_read_terms,
|
||||||
|
|
@ -136,19 +132,6 @@ private fun AnalyticsOptInHeader(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun CheckIcon() {
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(20.dp)
|
|
||||||
.background(color = MaterialTheme.colorScheme.background, shape = CircleShape)
|
|
||||||
.padding(2.dp),
|
|
||||||
imageVector = CompoundIcons.Check(),
|
|
||||||
contentDescription = null,
|
|
||||||
tint = ElementTheme.colors.textActionAccent,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AnalyticsOptInContent() {
|
private fun AnalyticsOptInContent() {
|
||||||
Box(
|
Box(
|
||||||
|
|
@ -162,20 +145,20 @@ private fun AnalyticsOptInContent() {
|
||||||
items = persistentListOf(
|
items = persistentListOf(
|
||||||
InfoListItem(
|
InfoListItem(
|
||||||
message = stringResource(id = R.string.screen_analytics_prompt_data_usage),
|
message = stringResource(id = R.string.screen_analytics_prompt_data_usage),
|
||||||
iconComposable = { CheckIcon() },
|
iconVector = CompoundIcons.CheckCircle(),
|
||||||
),
|
),
|
||||||
InfoListItem(
|
InfoListItem(
|
||||||
message = stringResource(id = R.string.screen_analytics_prompt_third_party_sharing),
|
message = stringResource(id = R.string.screen_analytics_prompt_third_party_sharing),
|
||||||
iconComposable = { CheckIcon() },
|
iconVector = CompoundIcons.CheckCircle(),
|
||||||
),
|
),
|
||||||
InfoListItem(
|
InfoListItem(
|
||||||
message = stringResource(id = R.string.screen_analytics_prompt_settings),
|
message = stringResource(id = R.string.screen_analytics_prompt_settings),
|
||||||
iconComposable = { CheckIcon() },
|
iconVector = CompoundIcons.CheckCircle(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
textStyle = ElementTheme.typography.fontBodyMdMedium,
|
textStyle = ElementTheme.typography.fontBodyLgMedium,
|
||||||
iconTint = ElementTheme.colors.textPrimary,
|
iconTint = ElementTheme.colors.iconSuccessPrimary,
|
||||||
backgroundColor = ElementTheme.colors.temporaryColorBgSpecial
|
backgroundColor = ElementTheme.colors.bgActionSecondaryHovered,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="call_foreground_service_channel_title_android">"Folyamatban lévő hívás"</string>
|
<string name="call_foreground_service_channel_title_android">"Folyamatban lévő hívás"</string>
|
||||||
<string name="call_foreground_service_message_android">"Koppints a híváshoz való visszatéréshez"</string>
|
<string name="call_foreground_service_message_android">"Koppintson a híváshoz való visszatéréshez"</string>
|
||||||
<string name="call_foreground_service_title_android">"☎️ Hívás folyamatban"</string>
|
<string name="call_foreground_service_title_android">"☎️ Hívás folyamatban"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,8 @@ dependencies {
|
||||||
testImplementation(projects.libraries.usersearch.test)
|
testImplementation(projects.libraries.usersearch.test)
|
||||||
testImplementation(projects.features.createroom.test)
|
testImplementation(projects.features.createroom.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.createroom.impl.addpeople
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.createroom.impl.userlist.SelectionMode
|
import io.element.android.features.createroom.impl.userlist.SelectionMode
|
||||||
import io.element.android.features.createroom.impl.userlist.UserListState
|
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||||
|
import io.element.android.features.createroom.impl.userlist.aRecentDirectRoomList
|
||||||
import io.element.android.features.createroom.impl.userlist.aUserListState
|
import io.element.android.features.createroom.impl.userlist.aUserListState
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
|
|
@ -29,13 +30,13 @@ open class AddPeopleUserListStateProvider : PreviewParameterProvider<UserListSta
|
||||||
override val values: Sequence<UserListState>
|
override val values: Sequence<UserListState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aUserListState(),
|
aUserListState(),
|
||||||
aUserListState().copy(
|
aUserListState(
|
||||||
searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
|
searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
|
||||||
selectedUsers = aMatrixUserList().toImmutableList(),
|
selectedUsers = aMatrixUserList().toImmutableList(),
|
||||||
isSearchActive = false,
|
isSearchActive = false,
|
||||||
selectionMode = SelectionMode.Multiple,
|
selectionMode = SelectionMode.Multiple,
|
||||||
),
|
),
|
||||||
aUserListState().copy(
|
aUserListState(
|
||||||
searchResults = SearchBarResultState.Results(
|
searchResults = SearchBarResultState.Results(
|
||||||
aMatrixUserList()
|
aMatrixUserList()
|
||||||
.mapIndexed { index, matrixUser ->
|
.mapIndexed { index, matrixUser ->
|
||||||
|
|
@ -46,6 +47,9 @@ open class AddPeopleUserListStateProvider : PreviewParameterProvider<UserListSta
|
||||||
selectedUsers = aMatrixUserList().toImmutableList(),
|
selectedUsers = aMatrixUserList().toImmutableList(),
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
selectionMode = SelectionMode.Multiple,
|
selectionMode = SelectionMode.Multiple,
|
||||||
)
|
),
|
||||||
|
aUserListState(
|
||||||
|
recentDirectRooms = aRecentDirectRoomList(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,8 @@
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.addpeople
|
package io.element.android.features.createroom.impl.addpeople
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -64,21 +62,16 @@ fun AddPeopleView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Column(
|
UserListView(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.consumeWindowInsets(padding),
|
.consumeWindowInsets(padding),
|
||||||
) {
|
state = state,
|
||||||
UserListView(
|
showBackButton = false,
|
||||||
modifier = Modifier
|
onUserSelected = {},
|
||||||
.fillMaxWidth(),
|
onUserDeselected = {},
|
||||||
state = state,
|
)
|
||||||
showBackButton = false,
|
|
||||||
onUserSelected = { },
|
|
||||||
onUserDeselected = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,17 +19,27 @@ package io.element.android.features.createroom.impl.components
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.createroom.impl.userlist.UserListEvents
|
import io.element.android.features.createroom.impl.userlist.UserListEvents
|
||||||
import io.element.android.features.createroom.impl.userlist.UserListState
|
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||||
import io.element.android.features.createroom.impl.userlist.UserListStateProvider
|
import io.element.android.features.createroom.impl.userlist.UserListStateProvider
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.components.CheckableUserRow
|
||||||
|
import io.element.android.libraries.matrix.ui.components.CheckableUserRowData
|
||||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
|
import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
|
||||||
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
|
import io.element.android.libraries.matrix.ui.model.getBestName
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UserListView(
|
fun UserListView(
|
||||||
|
|
@ -74,6 +84,43 @@ fun UserListView(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (!state.isSearchActive && state.recentDirectRooms.isNotEmpty()) {
|
||||||
|
LazyColumn {
|
||||||
|
item {
|
||||||
|
ListSectionHeader(
|
||||||
|
title = stringResource(id = CommonStrings.common_suggestions),
|
||||||
|
hasDivider = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
state.recentDirectRooms.forEachIndexed { index, recentDirectRoom ->
|
||||||
|
item {
|
||||||
|
val isSelected = state.selectedUsers.any {
|
||||||
|
recentDirectRoom.matrixUser.userId == it.userId
|
||||||
|
}
|
||||||
|
CheckableUserRow(
|
||||||
|
checked = isSelected,
|
||||||
|
onCheckedChange = {
|
||||||
|
if (isSelected) {
|
||||||
|
state.eventSink(UserListEvents.RemoveFromSelection(recentDirectRoom.matrixUser))
|
||||||
|
onUserDeselected(recentDirectRoom.matrixUser)
|
||||||
|
} else {
|
||||||
|
state.eventSink(UserListEvents.AddToSelection(recentDirectRoom.matrixUser))
|
||||||
|
onUserSelected(recentDirectRoom.matrixUser)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data = CheckableUserRowData.Resolved(
|
||||||
|
avatarData = recentDirectRoom.matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||||
|
name = recentDirectRoom.matrixUser.getBestName(),
|
||||||
|
subtext = recentDirectRoom.matrixUser.userId.value,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if (index < state.recentDirectRooms.lastIndex) {
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,12 @@
|
||||||
package io.element.android.features.createroom.impl.root
|
package io.element.android.features.createroom.impl.root
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||||
|
import io.element.android.features.createroom.impl.userlist.aRecentDirectRoomList
|
||||||
import io.element.android.features.createroom.impl.userlist.aUserListState
|
import io.element.android.features.createroom.impl.userlist.aUserListState
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -28,7 +31,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
|
||||||
override val values: Sequence<CreateRoomRootState>
|
override val values: Sequence<CreateRoomRootState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aCreateRoomRootState(),
|
aCreateRoomRootState(),
|
||||||
aCreateRoomRootState().copy(
|
aCreateRoomRootState(
|
||||||
startDmAction = AsyncAction.Loading,
|
startDmAction = AsyncAction.Loading,
|
||||||
userListState = aMatrixUser().let {
|
userListState = aMatrixUser().let {
|
||||||
aUserListState().copy(
|
aUserListState().copy(
|
||||||
|
|
@ -39,7 +42,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
aCreateRoomRootState().copy(
|
aCreateRoomRootState(
|
||||||
startDmAction = AsyncAction.Failure(Throwable("error")),
|
startDmAction = AsyncAction.Failure(Throwable("error")),
|
||||||
userListState = aMatrixUser().let {
|
userListState = aMatrixUser().let {
|
||||||
aUserListState().copy(
|
aUserListState().copy(
|
||||||
|
|
@ -50,12 +53,22 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
aCreateRoomRootState(
|
||||||
|
userListState = aUserListState(
|
||||||
|
recentDirectRooms = aRecentDirectRoomList()
|
||||||
|
)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aCreateRoomRootState() = CreateRoomRootState(
|
fun aCreateRoomRootState(
|
||||||
eventSink = {},
|
applicationName: String = "Element X Preview",
|
||||||
applicationName = "Element X Preview",
|
userListState: UserListState = aUserListState(),
|
||||||
startDmAction = AsyncAction.Uninitialized,
|
startDmAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||||
userListState = aUserListState(),
|
eventSink: (CreateRoomRootEvents) -> Unit = {},
|
||||||
|
) = CreateRoomRootState(
|
||||||
|
applicationName = applicationName,
|
||||||
|
userListState = userListState,
|
||||||
|
startDmAction = startDmAction,
|
||||||
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -46,11 +47,14 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CreateRoomRootView(
|
fun CreateRoomRootView(
|
||||||
|
|
@ -77,7 +81,11 @@ fun CreateRoomRootView(
|
||||||
) {
|
) {
|
||||||
UserListView(
|
UserListView(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
state = state.userListState,
|
// Do not render suggestions in this case, the suggestion will be rendered
|
||||||
|
// by CreateRoomActionButtonsList
|
||||||
|
state = state.userListState.copy(
|
||||||
|
recentDirectRooms = persistentListOf(),
|
||||||
|
),
|
||||||
onUserSelected = {
|
onUserSelected = {
|
||||||
state.eventSink(CreateRoomRootEvents.StartDM(it))
|
state.eventSink(CreateRoomRootEvents.StartDM(it))
|
||||||
},
|
},
|
||||||
|
|
@ -89,6 +97,7 @@ fun CreateRoomRootView(
|
||||||
state = state,
|
state = state,
|
||||||
onNewRoomClicked = onNewRoomClicked,
|
onNewRoomClicked = onNewRoomClicked,
|
||||||
onInvitePeopleClicked = onInviteFriendsClicked,
|
onInvitePeopleClicked = onInviteFriendsClicked,
|
||||||
|
onDmClicked = onOpenDM,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -106,7 +115,7 @@ fun CreateRoomRootView(
|
||||||
onRetry = {
|
onRetry = {
|
||||||
state.userListState.selectedUsers.firstOrNull()
|
state.userListState.selectedUsers.firstOrNull()
|
||||||
?.let { state.eventSink(CreateRoomRootEvents.StartDM(it)) }
|
?.let { state.eventSink(CreateRoomRootEvents.StartDM(it)) }
|
||||||
// Cancel start DM if there is no more selected user (should not happen)
|
// Cancel start DM if there is no more selected user (should not happen)
|
||||||
?: state.eventSink(CreateRoomRootEvents.CancelStartDM)
|
?: state.eventSink(CreateRoomRootEvents.CancelStartDM)
|
||||||
},
|
},
|
||||||
onErrorDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) },
|
onErrorDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) },
|
||||||
|
|
@ -139,18 +148,43 @@ private fun CreateRoomActionButtonsList(
|
||||||
state: CreateRoomRootState,
|
state: CreateRoomRootState,
|
||||||
onNewRoomClicked: () -> Unit,
|
onNewRoomClicked: () -> Unit,
|
||||||
onInvitePeopleClicked: () -> Unit,
|
onInvitePeopleClicked: () -> Unit,
|
||||||
|
onDmClicked: (RoomId) -> Unit,
|
||||||
) {
|
) {
|
||||||
Column {
|
LazyColumn {
|
||||||
CreateRoomActionButton(
|
item {
|
||||||
iconRes = CompoundDrawables.ic_compound_plus,
|
CreateRoomActionButton(
|
||||||
text = stringResource(id = R.string.screen_create_room_action_create_room),
|
iconRes = CompoundDrawables.ic_compound_plus,
|
||||||
onClick = onNewRoomClicked,
|
text = stringResource(id = R.string.screen_create_room_action_create_room),
|
||||||
)
|
onClick = onNewRoomClicked,
|
||||||
CreateRoomActionButton(
|
)
|
||||||
iconRes = CompoundDrawables.ic_compound_share_android,
|
}
|
||||||
text = stringResource(id = CommonStrings.action_invite_friends_to_app, state.applicationName),
|
item {
|
||||||
onClick = onInvitePeopleClicked,
|
CreateRoomActionButton(
|
||||||
)
|
iconRes = CompoundDrawables.ic_compound_share_android,
|
||||||
|
text = stringResource(id = CommonStrings.action_invite_friends_to_app, state.applicationName),
|
||||||
|
onClick = onInvitePeopleClicked,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (state.userListState.recentDirectRooms.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
ListSectionHeader(
|
||||||
|
title = stringResource(id = CommonStrings.common_suggestions),
|
||||||
|
hasDivider = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
state.userListState.recentDirectRooms.forEach { recentDirectRoom ->
|
||||||
|
item {
|
||||||
|
MatrixUserRow(
|
||||||
|
modifier = Modifier.clickable(
|
||||||
|
onClick = {
|
||||||
|
onDmClicked(recentDirectRoom.roomId)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
matrixUser = recentDirectRoom.matrixUser,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.room.recent.RecentDirectRoom
|
||||||
|
import io.element.android.libraries.matrix.api.room.recent.getRecentDirectRooms
|
||||||
import io.element.android.libraries.usersearch.api.UserRepository
|
import io.element.android.libraries.usersearch.api.UserRepository
|
||||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
@ -41,6 +44,7 @@ class DefaultUserListPresenter @AssistedInject constructor(
|
||||||
@Assisted val args: UserListPresenterArgs,
|
@Assisted val args: UserListPresenterArgs,
|
||||||
@Assisted val userRepository: UserRepository,
|
@Assisted val userRepository: UserRepository,
|
||||||
@Assisted val userListDataStore: UserListDataStore,
|
@Assisted val userListDataStore: UserListDataStore,
|
||||||
|
private val matrixClient: MatrixClient,
|
||||||
) : UserListPresenter {
|
) : UserListPresenter {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
@ContributesBinding(SessionScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
|
|
@ -54,6 +58,10 @@ class DefaultUserListPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): UserListState {
|
override fun present(): UserListState {
|
||||||
|
var recentDirectRooms by remember { mutableStateOf(emptyList<RecentDirectRoom>()) }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
recentDirectRooms = matrixClient.getRecentDirectRooms()
|
||||||
|
}
|
||||||
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
||||||
val selectedUsers by userListDataStore.selectedUsers().collectAsState(emptyList())
|
val selectedUsers by userListDataStore.selectedUsers().collectAsState(emptyList())
|
||||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||||
|
|
@ -82,6 +90,7 @@ class DefaultUserListPresenter @AssistedInject constructor(
|
||||||
isSearchActive = isSearchActive,
|
isSearchActive = isSearchActive,
|
||||||
showSearchLoader = showSearchLoader,
|
showSearchLoader = showSearchLoader,
|
||||||
selectionMode = args.selectionMode,
|
selectionMode = args.selectionMode,
|
||||||
|
recentDirectRooms = recentDirectRooms.toImmutableList(),
|
||||||
eventSink = { event ->
|
eventSink = { event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
is UserListEvents.OnSearchActiveChanged -> isSearchActive = event.active
|
is UserListEvents.OnSearchActiveChanged -> isSearchActive = event.active
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.features.createroom.impl.userlist
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import io.element.android.libraries.matrix.api.room.recent.RecentDirectRoom
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
@ -28,6 +29,7 @@ data class UserListState(
|
||||||
val selectedUsers: ImmutableList<MatrixUser>,
|
val selectedUsers: ImmutableList<MatrixUser>,
|
||||||
val isSearchActive: Boolean,
|
val isSearchActive: Boolean,
|
||||||
val selectionMode: SelectionMode,
|
val selectionMode: SelectionMode,
|
||||||
|
val recentDirectRooms: ImmutableList<RecentDirectRoom>,
|
||||||
val eventSink: (UserListEvents) -> Unit,
|
val eventSink: (UserListEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
val isMultiSelectionEnabled = selectionMode == SelectionMode.Multiple
|
val isMultiSelectionEnabled = selectionMode == SelectionMode.Multiple
|
||||||
|
|
|
||||||
|
|
@ -18,54 +18,82 @@ package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.room.recent.RecentDirectRoom
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
open class UserListStateProvider : PreviewParameterProvider<UserListState> {
|
open class UserListStateProvider : PreviewParameterProvider<UserListState> {
|
||||||
override val values: Sequence<UserListState>
|
override val values: Sequence<UserListState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aUserListState(),
|
aUserListState(),
|
||||||
aUserListState().copy(
|
aUserListState(
|
||||||
isSearchActive = false,
|
isSearchActive = false,
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
selectedUsers = aListOfSelectedUsers(),
|
||||||
selectionMode = SelectionMode.Multiple,
|
selectionMode = SelectionMode.Multiple,
|
||||||
),
|
),
|
||||||
aUserListState().copy(isSearchActive = true),
|
aUserListState(isSearchActive = true),
|
||||||
aUserListState().copy(isSearchActive = true, searchQuery = "someone"),
|
aUserListState(isSearchActive = true, searchQuery = "someone"),
|
||||||
aUserListState().copy(isSearchActive = true, searchQuery = "someone", selectionMode = SelectionMode.Multiple),
|
aUserListState(isSearchActive = true, searchQuery = "someone", selectionMode = SelectionMode.Multiple),
|
||||||
aUserListState().copy(
|
aUserListState(
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
searchQuery = "@someone:matrix.org",
|
searchQuery = "@someone:matrix.org",
|
||||||
selectedUsers = aMatrixUserList().toImmutableList(),
|
selectedUsers = aMatrixUserList().toImmutableList(),
|
||||||
searchResults = SearchBarResultState.Results(aListOfUserSearchResults()),
|
searchResults = SearchBarResultState.Results(aListOfUserSearchResults()),
|
||||||
),
|
),
|
||||||
aUserListState().copy(
|
aUserListState(
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
searchQuery = "@someone:matrix.org",
|
searchQuery = "@someone:matrix.org",
|
||||||
selectionMode = SelectionMode.Multiple,
|
selectionMode = SelectionMode.Multiple,
|
||||||
selectedUsers = aMatrixUserList().toImmutableList(),
|
selectedUsers = aMatrixUserList().toImmutableList(),
|
||||||
searchResults = SearchBarResultState.Results(aListOfUserSearchResults()),
|
searchResults = SearchBarResultState.Results(aListOfUserSearchResults()),
|
||||||
),
|
),
|
||||||
aUserListState().copy(
|
aUserListState(
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
searchQuery = "something-with-no-results",
|
searchQuery = "something-with-no-results",
|
||||||
searchResults = SearchBarResultState.NoResultsFound()
|
searchResults = SearchBarResultState.NoResultsFound()
|
||||||
),
|
),
|
||||||
aUserListState().copy(isSearchActive = true, searchQuery = "someone", selectionMode = SelectionMode.Single),
|
aUserListState(
|
||||||
|
isSearchActive = true,
|
||||||
|
searchQuery = "someone",
|
||||||
|
selectionMode = SelectionMode.Single,
|
||||||
|
),
|
||||||
|
aUserListState(
|
||||||
|
recentDirectRooms = aRecentDirectRoomList(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aUserListState() = UserListState(
|
fun aUserListState(
|
||||||
isSearchActive = false,
|
searchQuery: String = "",
|
||||||
searchQuery = "",
|
isSearchActive: Boolean = false,
|
||||||
searchResults = SearchBarResultState.Initial(),
|
searchResults: SearchBarResultState<ImmutableList<UserSearchResult>> = SearchBarResultState.Initial(),
|
||||||
selectedUsers = persistentListOf(),
|
selectedUsers: List<MatrixUser> = emptyList(),
|
||||||
selectionMode = SelectionMode.Single,
|
showSearchLoader: Boolean = false,
|
||||||
showSearchLoader = false,
|
selectionMode: SelectionMode = SelectionMode.Single,
|
||||||
eventSink = {}
|
recentDirectRooms: List<RecentDirectRoom> = emptyList(),
|
||||||
|
eventSink: (UserListEvents) -> Unit = {},
|
||||||
|
) = UserListState(
|
||||||
|
searchQuery = searchQuery,
|
||||||
|
isSearchActive = isSearchActive,
|
||||||
|
searchResults = searchResults,
|
||||||
|
selectedUsers = selectedUsers.toImmutableList(),
|
||||||
|
showSearchLoader = showSearchLoader,
|
||||||
|
selectionMode = selectionMode,
|
||||||
|
recentDirectRooms = recentDirectRooms.toImmutableList(),
|
||||||
|
eventSink = eventSink
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aListOfSelectedUsers() = aMatrixUserList().take(6).toImmutableList()
|
fun aListOfSelectedUsers() = aMatrixUserList().take(6).toImmutableList()
|
||||||
fun aListOfUserSearchResults() = aMatrixUserList().take(6).map { UserSearchResult(it) }.toImmutableList()
|
fun aListOfUserSearchResults() = aMatrixUserList().take(6).map { UserSearchResult(it) }.toImmutableList()
|
||||||
|
|
||||||
|
fun aRecentDirectRoomList(
|
||||||
|
count: Int = 5
|
||||||
|
): List<RecentDirectRoom> = aMatrixUserList()
|
||||||
|
.take(count)
|
||||||
|
.map {
|
||||||
|
RecentDirectRoom(RoomId("!aRoom:id"), it)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
<string name="screen_create_room_private_option_description">"Паведамленні ў гэтым пакоі зашыфраваны. Гэта шыфраванне нельга адключыць."</string>
|
<string name="screen_create_room_private_option_description">"Паведамленні ў гэтым пакоі зашыфраваны. Гэта шыфраванне нельга адключыць."</string>
|
||||||
<string name="screen_create_room_private_option_title">"Прыватны пакой (толькі па запрашэнні)"</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_description">"Паведамленні не зашыфраваны, і кожны можа іх прачытаць. Вы можаце ўключыць шыфраванне пазней."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Адкрыты пакой (для ўсіх)"</string>
|
<string name="screen_create_room_public_option_title">"Публічны пакой (для ўсіх)"</string>
|
||||||
<string name="screen_create_room_room_name_label">"Назва пакоя"</string>
|
<string name="screen_create_room_room_name_label">"Назва пакоя"</string>
|
||||||
<string name="screen_create_room_title">"Стварыце пакой"</string>
|
<string name="screen_create_room_title">"Стварыце пакой"</string>
|
||||||
<string name="screen_create_room_topic_label">"Тэма (неабавязкова)"</string>
|
<string name="screen_create_room_topic_label">"Тэма (неабавязкова)"</string>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.createroom.impl.addpeople
|
||||||
|
|
||||||
|
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.createroom.impl.userlist.UserListEvents
|
||||||
|
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||||
|
import io.element.android.features.createroom.impl.userlist.aUserListState
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
|
import io.element.android.tests.testutils.pressBack
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class AddPeopleViewTest {
|
||||||
|
@get:Rule
|
||||||
|
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on back invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<UserListEvents>()
|
||||||
|
ensureCalledOnce {
|
||||||
|
rule.setAddPeopleView(
|
||||||
|
aUserListState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onBackPressed = it
|
||||||
|
)
|
||||||
|
rule.pressBack()
|
||||||
|
}
|
||||||
|
eventsRecorder.assertSingle(UserListEvents.UpdateSearchQuery(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on back during search emits the expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<UserListEvents>()
|
||||||
|
rule.setAddPeopleView(
|
||||||
|
aUserListState(
|
||||||
|
isSearchActive = true,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.pressBack()
|
||||||
|
eventsRecorder.assertSingle(UserListEvents.OnSearchActiveChanged(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on skip invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<UserListEvents>()
|
||||||
|
ensureCalledOnce {
|
||||||
|
rule.setAddPeopleView(
|
||||||
|
aUserListState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onNextPressed = it
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_skip)
|
||||||
|
}
|
||||||
|
eventsRecorder.assertSingle(UserListEvents.UpdateSearchQuery(""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAddPeopleView(
|
||||||
|
state: UserListState,
|
||||||
|
onBackPressed: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onNextPressed: () -> Unit = EnsureNeverCalled(),
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
AddPeopleView(
|
||||||
|
state = state,
|
||||||
|
onBackPressed = onBackPressed,
|
||||||
|
onNextPressed = onNextPressed,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.createroom.impl.root
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.features.createroom.impl.R
|
||||||
|
import io.element.android.features.createroom.impl.userlist.aRecentDirectRoomList
|
||||||
|
import io.element.android.features.createroom.impl.userlist.aUserListState
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.ui.model.getBestName
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||||
|
import io.element.android.tests.testutils.pressBack
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class CreateRoomRootViewTest {
|
||||||
|
@get:Rule
|
||||||
|
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on back invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<CreateRoomRootEvents>(expectEvents = false)
|
||||||
|
ensureCalledOnce {
|
||||||
|
rule.setCreateRoomRootView(
|
||||||
|
aCreateRoomRootState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onClosePressed = it
|
||||||
|
)
|
||||||
|
rule.pressBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on New room invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<CreateRoomRootEvents>(expectEvents = false)
|
||||||
|
ensureCalledOnce {
|
||||||
|
rule.setCreateRoomRootView(
|
||||||
|
aCreateRoomRootState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onNewRoomClicked = it
|
||||||
|
)
|
||||||
|
rule.clickOn(R.string.screen_create_room_action_create_room)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Config(qualifiers = "h1024dp")
|
||||||
|
@Test
|
||||||
|
fun `clicking on Invite people invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<CreateRoomRootEvents>(expectEvents = false)
|
||||||
|
ensureCalledOnce {
|
||||||
|
rule.setCreateRoomRootView(
|
||||||
|
aCreateRoomRootState(
|
||||||
|
applicationName = "test",
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onInviteFriendsClicked = it
|
||||||
|
)
|
||||||
|
val text = rule.activity.getString(CommonStrings.action_invite_friends_to_app, "test")
|
||||||
|
rule.onNodeWithText(text).performClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Config(qualifiers = "h1024dp")
|
||||||
|
@Test
|
||||||
|
fun `clicking on a user suggestion invokes the expected callback`() {
|
||||||
|
val recentDirectRoomList = aRecentDirectRoomList()
|
||||||
|
val firstRoom = recentDirectRoomList[0]
|
||||||
|
val eventsRecorder = EventsRecorder<CreateRoomRootEvents>(expectEvents = false)
|
||||||
|
ensureCalledOnceWithParam(firstRoom.roomId) {
|
||||||
|
rule.setCreateRoomRootView(
|
||||||
|
aCreateRoomRootState(
|
||||||
|
userListState = aUserListState(
|
||||||
|
recentDirectRooms = recentDirectRoomList
|
||||||
|
),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onOpenDM = it
|
||||||
|
)
|
||||||
|
rule.onNodeWithText(firstRoom.matrixUser.getBestName()).performClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setCreateRoomRootView(
|
||||||
|
state: CreateRoomRootState,
|
||||||
|
onClosePressed: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onNewRoomClicked: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onOpenDM: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
|
onInviteFriendsClicked: () -> Unit = EnsureNeverCalled(),
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
CreateRoomRootView(
|
||||||
|
state = state,
|
||||||
|
onClosePressed = onClosePressed,
|
||||||
|
onNewRoomClicked = onNewRoomClicked,
|
||||||
|
onOpenDM = onOpenDM,
|
||||||
|
onInviteFriendsClicked = onInviteFriendsClicked,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||||
|
|
@ -45,6 +46,7 @@ class DefaultUserListPresenterTests {
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||||
userRepository,
|
userRepository,
|
||||||
UserListDataStore(),
|
UserListDataStore(),
|
||||||
|
FakeMatrixClient(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -66,6 +68,7 @@ class DefaultUserListPresenterTests {
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Multiple),
|
UserListPresenterArgs(selectionMode = SelectionMode.Multiple),
|
||||||
userRepository,
|
userRepository,
|
||||||
UserListDataStore(),
|
UserListDataStore(),
|
||||||
|
FakeMatrixClient(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -87,6 +90,7 @@ class DefaultUserListPresenterTests {
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||||
userRepository,
|
userRepository,
|
||||||
UserListDataStore(),
|
UserListDataStore(),
|
||||||
|
FakeMatrixClient(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -123,6 +127,7 @@ class DefaultUserListPresenterTests {
|
||||||
),
|
),
|
||||||
userRepository,
|
userRepository,
|
||||||
UserListDataStore(),
|
UserListDataStore(),
|
||||||
|
FakeMatrixClient(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -175,6 +180,7 @@ class DefaultUserListPresenterTests {
|
||||||
),
|
),
|
||||||
userRepository,
|
userRepository,
|
||||||
UserListDataStore(),
|
UserListDataStore(),
|
||||||
|
FakeMatrixClient(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -200,6 +206,7 @@ class DefaultUserListPresenterTests {
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||||
userRepository,
|
userRepository,
|
||||||
UserListDataStore(),
|
UserListDataStore(),
|
||||||
|
FakeMatrixClient(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.ftue.api.state
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to manage the First Time User Experience state (aka Onboarding).
|
||||||
|
*/
|
||||||
|
interface FtueService {
|
||||||
|
/** The current state of the FTUE. */
|
||||||
|
val state: StateFlow<FtueState>
|
||||||
|
|
||||||
|
/** Reset the FTUE state. */
|
||||||
|
suspend fun reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The state of the FTUE. */
|
||||||
|
sealed interface FtueState {
|
||||||
|
/** The FTUE state is unknown, nothing to do for now. */
|
||||||
|
data object Unknown : FtueState
|
||||||
|
|
||||||
|
/** The FTUE state is incomplete. The FTUE flow should be displayed. */
|
||||||
|
data object Incomplete : FtueState
|
||||||
|
|
||||||
|
/** The FTUE state is complete. The FTUE flow should not be displayed anymore. */
|
||||||
|
data object Complete : FtueState
|
||||||
|
}
|
||||||
|
|
@ -42,6 +42,8 @@ dependencies {
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.testtags)
|
||||||
implementation(projects.features.analytics.api)
|
implementation(projects.features.analytics.api)
|
||||||
|
implementation(projects.features.securebackup.api)
|
||||||
|
implementation(projects.features.verifysession.api)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(projects.features.lockscreen.api)
|
implementation(projects.features.lockscreen.api)
|
||||||
implementation(projects.libraries.permissions.api)
|
implementation(projects.libraries.permissions.api)
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@ import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.analytics.api.AnalyticsEntryPoint
|
import io.element.android.features.analytics.api.AnalyticsEntryPoint
|
||||||
import io.element.android.features.ftue.api.FtueEntryPoint
|
import io.element.android.features.ftue.api.FtueEntryPoint
|
||||||
import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode
|
import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode
|
||||||
import io.element.android.features.ftue.impl.state.DefaultFtueState
|
import io.element.android.features.ftue.impl.sessionverification.FtueSessionVerificationFlowNode
|
||||||
|
import io.element.android.features.ftue.impl.state.DefaultFtueService
|
||||||
import io.element.android.features.ftue.impl.state.FtueStep
|
import io.element.android.features.ftue.impl.state.FtueStep
|
||||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
|
|
@ -55,7 +56,7 @@ import kotlinx.parcelize.Parcelize
|
||||||
class FtueFlowNode @AssistedInject constructor(
|
class FtueFlowNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val ftueState: DefaultFtueState,
|
private val ftueState: DefaultFtueService,
|
||||||
private val analyticsEntryPoint: AnalyticsEntryPoint,
|
private val analyticsEntryPoint: AnalyticsEntryPoint,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
||||||
|
|
@ -72,6 +73,9 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object Placeholder : NavTarget
|
data object Placeholder : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object SessionVerification : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object NotificationsOptIn : NavTarget
|
data object NotificationsOptIn : NavTarget
|
||||||
|
|
||||||
|
|
@ -106,6 +110,14 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
NavTarget.Placeholder -> {
|
NavTarget.Placeholder -> {
|
||||||
createNode<PlaceholderNode>(buildContext)
|
createNode<PlaceholderNode>(buildContext)
|
||||||
}
|
}
|
||||||
|
NavTarget.SessionVerification -> {
|
||||||
|
val callback = object : FtueSessionVerificationFlowNode.Callback {
|
||||||
|
override fun onDone() {
|
||||||
|
lifecycleScope.launch { moveToNextStep() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNode<FtueSessionVerificationFlowNode>(buildContext, listOf(callback))
|
||||||
|
}
|
||||||
NavTarget.NotificationsOptIn -> {
|
NavTarget.NotificationsOptIn -> {
|
||||||
val callback = object : NotificationsOptInNode.Callback {
|
val callback = object : NotificationsOptInNode.Callback {
|
||||||
override fun onNotificationsOptInFinished() {
|
override fun onNotificationsOptInFinished() {
|
||||||
|
|
@ -133,6 +145,9 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
private fun moveToNextStep() {
|
private fun moveToNextStep() {
|
||||||
when (ftueState.getNextStep()) {
|
when (ftueState.getNextStep()) {
|
||||||
|
FtueStep.SessionVerification -> {
|
||||||
|
backstack.newRoot(NavTarget.SessionVerification)
|
||||||
|
}
|
||||||
FtueStep.NotificationsOptIn -> {
|
FtueStep.NotificationsOptIn -> {
|
||||||
backstack.newRoot(NavTarget.NotificationsOptIn)
|
backstack.newRoot(NavTarget.NotificationsOptIn)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.systemBarsPadding
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -40,8 +40,10 @@ import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.ftue.impl.R
|
import io.element.android.features.ftue.impl.R
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
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.pages.HeaderFooterPage
|
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
|
import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
|
import io.element.android.libraries.designsystem.components.OnboardingBackground
|
||||||
|
import io.element.android.libraries.designsystem.components.PageTitle
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
|
@ -62,8 +64,9 @@ fun NotificationsOptInView(
|
||||||
|
|
||||||
HeaderFooterPage(
|
HeaderFooterPage(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.systemBarsPadding()
|
.statusBarsPadding()
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
|
background = { OnboardingBackground() },
|
||||||
header = { NotificationsOptInHeader(modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)) },
|
header = { NotificationsOptInHeader(modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)) },
|
||||||
footer = { NotificationsOptInFooter(state) },
|
footer = { NotificationsOptInFooter(state) },
|
||||||
) {
|
) {
|
||||||
|
|
@ -75,11 +78,11 @@ fun NotificationsOptInView(
|
||||||
private fun NotificationsOptInHeader(
|
private fun NotificationsOptInHeader(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
IconTitleSubtitleMolecule(
|
PageTitle(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
title = stringResource(R.string.screen_notification_optin_title),
|
title = stringResource(R.string.screen_notification_optin_title),
|
||||||
subTitle = stringResource(R.string.screen_notification_optin_subtitle),
|
subtitle = stringResource(R.string.screen_notification_optin_subtitle),
|
||||||
iconImageVector = CompoundIcons.NotificationsSolid(),
|
iconStyle = BigIcon.Style.Default(CompoundIcons.NotificationsSolid()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.ftue.impl.sessionverification
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
|
||||||
|
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
|
||||||
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@ContributesNode(SessionScope::class)
|
||||||
|
class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val verifySessionEntryPoint: VerifySessionEntryPoint,
|
||||||
|
private val secureBackupEntryPoint: SecureBackupEntryPoint,
|
||||||
|
) : BaseFlowNode<FtueSessionVerificationFlowNode.NavTarget>(
|
||||||
|
backstack = BackStack(
|
||||||
|
initialElement = NavTarget.Root,
|
||||||
|
savedStateMap = buildContext.savedStateMap,
|
||||||
|
),
|
||||||
|
buildContext = buildContext,
|
||||||
|
plugins = plugins,
|
||||||
|
) {
|
||||||
|
sealed interface NavTarget : Parcelable {
|
||||||
|
@Parcelize
|
||||||
|
data object Root : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object EnterRecoveryKey : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object CreateNewRecoveryKey : NavTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun onDone()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val secureBackupEntryPointCallback = object : SecureBackupEntryPoint.Callback {
|
||||||
|
override fun onCreateNewRecoveryKey() {
|
||||||
|
backstack.push(NavTarget.CreateNewRecoveryKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDone() {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
// Move to the completed state view in the verification flow
|
||||||
|
backstack.newRoot(NavTarget.Root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
return when (navTarget) {
|
||||||
|
is NavTarget.Root -> {
|
||||||
|
verifySessionEntryPoint.nodeBuilder(this, buildContext)
|
||||||
|
.callback(object : VerifySessionEntryPoint.Callback {
|
||||||
|
override fun onEnterRecoveryKey() {
|
||||||
|
backstack.push(NavTarget.EnterRecoveryKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateNewRecoveryKey() {
|
||||||
|
backstack.push(NavTarget.CreateNewRecoveryKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDone() {
|
||||||
|
plugins<Callback>().forEach { it.onDone() }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
is NavTarget.EnterRecoveryKey -> {
|
||||||
|
secureBackupEntryPoint.nodeBuilder(this, buildContext)
|
||||||
|
.params(SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey))
|
||||||
|
.callback(secureBackupEntryPointCallback)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
is NavTarget.CreateNewRecoveryKey -> {
|
||||||
|
secureBackupEntryPoint.nodeBuilder(this, buildContext)
|
||||||
|
.params(SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.CreateNewRecoveryKey))
|
||||||
|
.callback(secureBackupEntryPointCallback)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
BackstackView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,9 +20,11 @@ import android.Manifest
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.ftue.api.state.FtueService
|
||||||
import io.element.android.features.ftue.api.state.FtueState
|
import io.element.android.features.ftue.api.state.FtueState
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||||
|
|
@ -35,14 +37,15 @@ import kotlinx.coroutines.runBlocking
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(SessionScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
class DefaultFtueState @Inject constructor(
|
class DefaultFtueService @Inject constructor(
|
||||||
private val sdkVersionProvider: BuildVersionSdkIntProvider,
|
private val sdkVersionProvider: BuildVersionSdkIntProvider,
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val permissionStateProvider: PermissionStateProvider,
|
private val permissionStateProvider: PermissionStateProvider,
|
||||||
private val lockScreenService: LockScreenService,
|
private val lockScreenService: LockScreenService,
|
||||||
) : FtueState {
|
private val sessionVerificationService: SessionVerificationService,
|
||||||
override val shouldDisplayFlow = MutableStateFlow(isAnyStepIncomplete())
|
) : FtueService {
|
||||||
|
override val state = MutableStateFlow<FtueState>(FtueState.Unknown)
|
||||||
|
|
||||||
override suspend fun reset() {
|
override suspend fun reset() {
|
||||||
analyticsService.reset()
|
analyticsService.reset()
|
||||||
|
|
@ -52,6 +55,10 @@ class DefaultFtueState @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
sessionVerificationService.needsVerificationFlow
|
||||||
|
.onEach { updateState() }
|
||||||
|
.launchIn(coroutineScope)
|
||||||
|
|
||||||
analyticsService.didAskUserConsent()
|
analyticsService.didAskUserConsent()
|
||||||
.onEach { updateState() }
|
.onEach { updateState() }
|
||||||
.launchIn(coroutineScope)
|
.launchIn(coroutineScope)
|
||||||
|
|
@ -59,7 +66,12 @@ class DefaultFtueState @Inject constructor(
|
||||||
|
|
||||||
fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
|
fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
|
||||||
when (currentStep) {
|
when (currentStep) {
|
||||||
null -> if (shouldAskNotificationPermissions()) {
|
null -> if (isSessionNotVerified()) {
|
||||||
|
FtueStep.SessionVerification
|
||||||
|
} else {
|
||||||
|
getNextStep(FtueStep.SessionVerification)
|
||||||
|
}
|
||||||
|
FtueStep.SessionVerification -> if (shouldAskNotificationPermissions()) {
|
||||||
FtueStep.NotificationsOptIn
|
FtueStep.NotificationsOptIn
|
||||||
} else {
|
} else {
|
||||||
getNextStep(FtueStep.NotificationsOptIn)
|
getNextStep(FtueStep.NotificationsOptIn)
|
||||||
|
|
@ -79,12 +91,17 @@ class DefaultFtueState @Inject constructor(
|
||||||
|
|
||||||
private fun isAnyStepIncomplete(): Boolean {
|
private fun isAnyStepIncomplete(): Boolean {
|
||||||
return listOf(
|
return listOf(
|
||||||
|
{ isSessionNotVerified() },
|
||||||
{ shouldAskNotificationPermissions() },
|
{ shouldAskNotificationPermissions() },
|
||||||
{ needsAnalyticsOptIn() },
|
{ needsAnalyticsOptIn() },
|
||||||
{ shouldDisplayLockscreenSetup() },
|
{ shouldDisplayLockscreenSetup() },
|
||||||
).any { it() }
|
).any { it() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isSessionNotVerified(): Boolean {
|
||||||
|
return sessionVerificationService.needsVerificationFlow.value
|
||||||
|
}
|
||||||
|
|
||||||
private fun needsAnalyticsOptIn(): Boolean {
|
private fun needsAnalyticsOptIn(): Boolean {
|
||||||
// We need this function to not be suspend, so we need to load the value through runBlocking
|
// We need this function to not be suspend, so we need to load the value through runBlocking
|
||||||
return runBlocking { analyticsService.didAskUserConsent().first().not() }
|
return runBlocking { analyticsService.didAskUserConsent().first().not() }
|
||||||
|
|
@ -109,11 +126,15 @@ class DefaultFtueState @Inject constructor(
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
internal fun updateState() {
|
internal fun updateState() {
|
||||||
shouldDisplayFlow.value = isAnyStepIncomplete()
|
state.value = when {
|
||||||
|
isAnyStepIncomplete() -> FtueState.Incomplete
|
||||||
|
else -> FtueState.Complete
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface FtueStep {
|
sealed interface FtueStep {
|
||||||
|
data object SessionVerification : FtueStep
|
||||||
data object NotificationsOptIn : FtueStep
|
data object NotificationsOptIn : FtueStep
|
||||||
data object AnalyticsOptIn : FtueStep
|
data object AnalyticsOptIn : FtueStep
|
||||||
data object LockscreenSetup : FtueStep
|
data object LockscreenSetup : FtueStep
|
||||||
|
|
@ -2,6 +2,13 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"Вы можаце змяніць налады пазней."</string>
|
<string name="screen_notification_optin_subtitle">"Вы можаце змяніць налады пазней."</string>
|
||||||
<string name="screen_notification_optin_title">"Дазвольце апавяшчэнні і ніколі не прапускайце іх"</string>
|
<string name="screen_notification_optin_title">"Дазвольце апавяшчэнні і ніколі не прапускайце іх"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_1">"Адкрыйце Element на настольнай прыладзе"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_2">"Націсніце на свой аватар"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3">"Выберыце %1$s"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3_action">"“Звязаць новую прыладу”"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_4">"Выберыце %1$s"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_4_action">"“Паказаць QR-код”"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_title">"Адкрыйце Element на іншай прыладзе, каб атрымаць QR-код"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Званкі, апытанні, пошук і многае іншае будзе дададзена пазней у гэтым годзе."</string>
|
<string name="screen_welcome_bullet_1">"Званкі, апытанні, пошук і многае іншае будзе дададзена пазней у гэтым годзе."</string>
|
||||||
<string name="screen_welcome_bullet_2">"Гісторыя паведамленняў для зашыфраваных пакояў пакуль недаступна."</string>
|
<string name="screen_welcome_bullet_2">"Гісторыя паведамленняў для зашыфраваных пакояў пакуль недаступна."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Мы будзем рады пачуць вашае меркаванне, паведаміце нам аб гэтым праз старонку налад."</string>
|
<string name="screen_welcome_bullet_3">"Мы будзем рады пачуць вашае меркаванне, паведаміце нам аб гэтым праз старонку налад."</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,13 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"Вы можете изменить настройки позже."</string>
|
<string name="screen_notification_optin_subtitle">"Вы можете изменить настройки позже."</string>
|
||||||
<string name="screen_notification_optin_title">"Разрешите уведомления и никогда не пропустите сообщение"</string>
|
<string name="screen_notification_optin_title">"Разрешите уведомления и никогда не пропустите сообщение"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_1">"Откройте Element на настольном устройстве"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_2">"Нажмите на свое изображение"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3">"Выбрать %1$s"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Привязать новое устройство\""</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_4">"Выбрать %1$s"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_4_action">"\"Показать QR-код\""</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_title">"Откройте Element на другом устройстве, чтобы получить QR-код"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string>
|
<string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string>
|
||||||
<string name="screen_welcome_bullet_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string>
|
<string name="screen_welcome_bullet_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."</string>
|
<string name="screen_welcome_bullet_3">"Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,13 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"You can change your settings later."</string>
|
<string name="screen_notification_optin_subtitle">"You can change your settings later."</string>
|
||||||
<string name="screen_notification_optin_title">"Allow notifications and never miss a message"</string>
|
<string name="screen_notification_optin_title">"Allow notifications and never miss a message"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_1">"Open Element on a desktop device"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_2">"Click on your avatar"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3">"Select %1$s"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3_action">"“Link new device”"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_4">"Select %1$s"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_4_action">"“Show QR code”"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_title">"Open Element on another device to get the QR code"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Calls, polls, search and more will be added later this year."</string>
|
<string name="screen_welcome_bullet_1">"Calls, polls, search and more will be added later this year."</string>
|
||||||
<string name="screen_welcome_bullet_2">"Message history for encrypted rooms isn’t available yet."</string>
|
<string name="screen_welcome_bullet_2">"Message history for encrypted rooms isn’t available yet."</string>
|
||||||
<string name="screen_welcome_bullet_3">"We’d love to hear from you, let us know what you think via the settings page."</string>
|
<string name="screen_welcome_bullet_3">"We’d love to hear from you, let us know what you think via the settings page."</string>
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,15 @@
|
||||||
package io.element.android.features.ftue.impl
|
package io.element.android.features.ftue.impl
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.ftue.impl.state.DefaultFtueState
|
import io.element.android.features.ftue.api.state.FtueState
|
||||||
|
import io.element.android.features.ftue.impl.state.DefaultFtueService
|
||||||
import io.element.android.features.ftue.impl.state.FtueStep
|
import io.element.android.features.ftue.impl.state.FtueStep
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
import io.element.android.features.lockscreen.test.FakeLockScreenService
|
import io.element.android.features.lockscreen.test.FakeLockScreenService
|
||||||
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
|
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
|
|
@ -32,38 +36,51 @@ import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class DefaultFtueStateTests {
|
class DefaultFtueServiceTests {
|
||||||
@Test
|
@Test
|
||||||
fun `given any check being false, should display flow is true`() = runTest {
|
fun `given any check being false and session verification state being loaded, FtueState is Incomplete`() = runTest {
|
||||||
|
val sessionVerificationService = FakeSessionVerificationService().apply {
|
||||||
|
givenVerifiedStatus(SessionVerifiedStatus.Unknown)
|
||||||
|
}
|
||||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
val state = createState(coroutineScope)
|
val state = createState(coroutineScope, sessionVerificationService)
|
||||||
|
|
||||||
assertThat(state.shouldDisplayFlow.value).isTrue()
|
state.state.test {
|
||||||
|
// Verification state is unknown, we don't display the flow yet
|
||||||
|
assertThat(awaitItem()).isEqualTo(FtueState.Unknown)
|
||||||
|
|
||||||
|
// Verification state is known, we should display the flow if any check is false
|
||||||
|
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
||||||
|
assertThat(awaitItem()).isEqualTo(FtueState.Incomplete)
|
||||||
|
}
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
coroutineScope.cancel()
|
coroutineScope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given all checks being true, should display flow is false`() = runTest {
|
fun `given all checks being true, FtueState is Complete`() = runTest {
|
||||||
val analyticsService = FakeAnalyticsService()
|
val analyticsService = FakeAnalyticsService()
|
||||||
|
val sessionVerificationService = FakeSessionVerificationService()
|
||||||
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = true)
|
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = true)
|
||||||
val lockScreenService = FakeLockScreenService()
|
val lockScreenService = FakeLockScreenService()
|
||||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
|
|
||||||
val state = createState(
|
val state = createState(
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = coroutineScope,
|
||||||
|
sessionVerificationService = sessionVerificationService,
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
permissionStateProvider = permissionStateProvider,
|
permissionStateProvider = permissionStateProvider,
|
||||||
lockScreenService = lockScreenService,
|
lockScreenService = lockScreenService,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||||
analyticsService.setDidAskUserConsent()
|
analyticsService.setDidAskUserConsent()
|
||||||
permissionStateProvider.setPermissionGranted()
|
permissionStateProvider.setPermissionGranted()
|
||||||
lockScreenService.setIsPinSetup(true)
|
lockScreenService.setIsPinSetup(true)
|
||||||
state.updateState()
|
state.updateState()
|
||||||
|
|
||||||
assertThat(state.shouldDisplayFlow.value).isFalse()
|
assertThat(state.state.value).isEqualTo(FtueState.Complete)
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
coroutineScope.cancel()
|
coroutineScope.cancel()
|
||||||
|
|
@ -71,6 +88,10 @@ class DefaultFtueStateTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `traverse flow`() = runTest {
|
fun `traverse flow`() = runTest {
|
||||||
|
val sessionVerificationService = FakeSessionVerificationService().apply {
|
||||||
|
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
||||||
|
givenNeedsVerification(true)
|
||||||
|
}
|
||||||
val analyticsService = FakeAnalyticsService()
|
val analyticsService = FakeAnalyticsService()
|
||||||
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
|
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
|
||||||
val lockScreenService = FakeLockScreenService()
|
val lockScreenService = FakeLockScreenService()
|
||||||
|
|
@ -78,12 +99,17 @@ class DefaultFtueStateTests {
|
||||||
|
|
||||||
val state = createState(
|
val state = createState(
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = coroutineScope,
|
||||||
|
sessionVerificationService = sessionVerificationService,
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
permissionStateProvider = permissionStateProvider,
|
permissionStateProvider = permissionStateProvider,
|
||||||
lockScreenService = lockScreenService,
|
lockScreenService = lockScreenService,
|
||||||
)
|
)
|
||||||
val steps = mutableListOf<FtueStep?>()
|
val steps = mutableListOf<FtueStep?>()
|
||||||
|
|
||||||
|
// Session verification
|
||||||
|
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||||
|
sessionVerificationService.givenNeedsVerification(false)
|
||||||
|
|
||||||
// Notifications opt in
|
// Notifications opt in
|
||||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||||
permissionStateProvider.setPermissionGranted()
|
permissionStateProvider.setPermissionGranted()
|
||||||
|
|
@ -100,6 +126,7 @@ class DefaultFtueStateTests {
|
||||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||||
|
|
||||||
assertThat(steps).containsExactly(
|
assertThat(steps).containsExactly(
|
||||||
|
FtueStep.SessionVerification,
|
||||||
FtueStep.NotificationsOptIn,
|
FtueStep.NotificationsOptIn,
|
||||||
FtueStep.LockscreenSetup,
|
FtueStep.LockscreenSetup,
|
||||||
FtueStep.AnalyticsOptIn,
|
FtueStep.AnalyticsOptIn,
|
||||||
|
|
@ -114,17 +141,20 @@ class DefaultFtueStateTests {
|
||||||
@Test
|
@Test
|
||||||
fun `if a check for a step is true, start from the next one`() = runTest {
|
fun `if a check for a step is true, start from the next one`() = runTest {
|
||||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
|
val sessionVerificationService = FakeSessionVerificationService()
|
||||||
val analyticsService = FakeAnalyticsService()
|
val analyticsService = FakeAnalyticsService()
|
||||||
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
|
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
|
||||||
val lockScreenService = FakeLockScreenService()
|
val lockScreenService = FakeLockScreenService()
|
||||||
val state = createState(
|
val state = createState(
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = coroutineScope,
|
||||||
|
sessionVerificationService = sessionVerificationService,
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
permissionStateProvider = permissionStateProvider,
|
permissionStateProvider = permissionStateProvider,
|
||||||
lockScreenService = lockScreenService,
|
lockScreenService = lockScreenService,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Skip first 2 steps
|
// Skip first 3 steps
|
||||||
|
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||||
permissionStateProvider.setPermissionGranted()
|
permissionStateProvider.setPermissionGranted()
|
||||||
lockScreenService.setIsPinSetup(true)
|
lockScreenService.setIsPinSetup(true)
|
||||||
|
|
||||||
|
|
@ -140,16 +170,19 @@ class DefaultFtueStateTests {
|
||||||
@Test
|
@Test
|
||||||
fun `if version is older than 13 we don't display the notification opt in screen`() = runTest {
|
fun `if version is older than 13 we don't display the notification opt in screen`() = runTest {
|
||||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
|
val sessionVerificationService = FakeSessionVerificationService()
|
||||||
val analyticsService = FakeAnalyticsService()
|
val analyticsService = FakeAnalyticsService()
|
||||||
val lockScreenService = FakeLockScreenService()
|
val lockScreenService = FakeLockScreenService()
|
||||||
|
|
||||||
val state = createState(
|
val state = createState(
|
||||||
sdkIntVersion = Build.VERSION_CODES.M,
|
sdkIntVersion = Build.VERSION_CODES.M,
|
||||||
|
sessionVerificationService = sessionVerificationService,
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = coroutineScope,
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
lockScreenService = lockScreenService,
|
lockScreenService = lockScreenService,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||||
lockScreenService.setIsPinSetup(true)
|
lockScreenService.setIsPinSetup(true)
|
||||||
|
|
||||||
assertThat(state.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn)
|
assertThat(state.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn)
|
||||||
|
|
@ -163,14 +196,16 @@ class DefaultFtueStateTests {
|
||||||
|
|
||||||
private fun createState(
|
private fun createState(
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
|
sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
|
||||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||||
permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
|
permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
|
||||||
lockScreenService: LockScreenService = FakeLockScreenService(),
|
lockScreenService: LockScreenService = FakeLockScreenService(),
|
||||||
// First version where notification permission is required
|
// First version where notification permission is required
|
||||||
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
|
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
|
||||||
) = DefaultFtueState(
|
) = DefaultFtueService(
|
||||||
sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion),
|
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = coroutineScope,
|
||||||
|
sessionVerificationService = sessionVerificationService,
|
||||||
|
sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion),
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
permissionStateProvider = permissionStateProvider,
|
permissionStateProvider = permissionStateProvider,
|
||||||
lockScreenService = lockScreenService,
|
lockScreenService = lockScreenService,
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_invites_decline_chat_message">"Вы ўпэўненыя, што жадаеце адхіліць запрашэнне ў %1$s?"</string>
|
<string name="screen_invites_decline_chat_message">"Вы ўпэўненыя, што хочаце адхіліць запрашэнне ў %1$s?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Адхіліць запрашэнне"</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_message">"Вы ўпэўненыя, што хочаце адмовіцца ад прыватных зносін з %1$s?"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_title">"Адхіліць чат"</string>
|
<string name="screen_invites_decline_direct_chat_title">"Адхіліць чат"</string>
|
||||||
<string name="screen_invites_empty_list">"Няма запрашэнняў"</string>
|
<string name="screen_invites_empty_list">"Няма запрашэнняў"</string>
|
||||||
<string name="screen_invites_invited_you">"%1$s (%2$s) запрасіў вас"</string>
|
<string name="screen_invites_invited_you">"%1$s (%2$s) запрасіў вас"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="leave_conversation_alert_subtitle">"Вы ўпэўнены, што хочаце пакінуць гэту размову? Гэта размова не з\'яўляецца публічнай, і вы не зможаце далучыцца зноў без запрашэння."</string>
|
<string name="leave_conversation_alert_subtitle">"Вы ўпэўнены, што хочаце пакінуць гэту размову? Гэта размова не з\'яўляецца публічнай, і вы не зможаце далучыцца зноў без запрашэння."</string>
|
||||||
<string name="leave_room_alert_empty_subtitle">"Вы ўпэўнены, што жадаеце пакінуць гэты пакой? Вы тут адзіны карыстальнік. Калі вы выйдзеце, ніхто не зможа далучыцца ў будучыні, у тым ліку і вы."</string>
|
<string name="leave_room_alert_empty_subtitle">"Вы ўпэўнены, што хочаце пакінуць гэты пакой? Вы тут адзіны карыстальнік. Калі вы выйдзеце, ніхто не зможа далучыцца ў будучыні, у тым ліку і вы."</string>
|
||||||
<string name="leave_room_alert_private_subtitle">"Вы ўпэўнены, што жадаеце пакінуць гэты пакой? Гэты пакой не агульнадаступны, і вы не зможаце далучыцца да яго зноў без запрашэння."</string>
|
<string name="leave_room_alert_private_subtitle">"Вы ўпэўнены, што жхочаце пакінуць гэты пакой? Гэты пакой не агульнадаступны, і вы не зможаце далучыцца да яго зноў без запрашэння."</string>
|
||||||
<string name="leave_room_alert_subtitle">"Вы ўпэўнены, што жадаеце пакінуць пакой?"</string>
|
<string name="leave_room_alert_subtitle">"Вы ўпэўнены, што хочаце пакінуць пакой?"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,6 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="leave_conversation_alert_subtitle">"Biztos, hogy elhagyja ezt a beszélgetést? Ez a beszélgetés nem nyilvános, és meghívás nélkül nem fog tudni visszacsatlakozni."</string>
|
<string name="leave_conversation_alert_subtitle">"Biztos, hogy elhagyja ezt a beszélgetést? Ez a beszélgetés nem nyilvános, és meghívás nélkül nem fog tudni visszacsatlakozni."</string>
|
||||||
<string name="leave_room_alert_empty_subtitle">"Biztos, hogy elhagyja ezt a szobát? Ön az egyedüli ember itt. Ha kilép, akkor senki sem fog tudni csatlakozni a jövőben, Önt is beleértve."</string>
|
<string name="leave_room_alert_empty_subtitle">"Biztos, hogy elhagyja ezt a szobát? Ön az egyedüli ember itt. Ha kilép, akkor senki sem fog tudni csatlakozni a jövőben, Önt is beleértve."</string>
|
||||||
<string name="leave_room_alert_private_subtitle">"Biztos, hogy elhagyod ezt a szobát? Ez a szoba nem nyilvános, és meghívó nélkül nem fogsz tudni újra belépni."</string>
|
<string name="leave_room_alert_private_subtitle">"Biztos, hogy elhagyja ezt a szobát? Ez a szoba nem nyilvános, és meghívó nélkül nem fog tudni újra belépni."</string>
|
||||||
<string name="leave_room_alert_subtitle">"Biztos, hogy elhagyod a szobát?"</string>
|
<string name="leave_room_alert_subtitle">"Biztos, hogy elhagyja a szobát?"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
<string name="screen_app_lock_settings_change_pin">"Змяніць PIN-код"</string>
|
<string name="screen_app_lock_settings_change_pin">"Змяніць PIN-код"</string>
|
||||||
<string name="screen_app_lock_settings_enable_biometric_unlock">"Дазволіць біяметрычную разблакіроўку"</string>
|
<string name="screen_app_lock_settings_enable_biometric_unlock">"Дазволіць біяметрычную разблакіроўку"</string>
|
||||||
<string name="screen_app_lock_settings_remove_pin">"Выдаліць PIN-код"</string>
|
<string name="screen_app_lock_settings_remove_pin">"Выдаліць PIN-код"</string>
|
||||||
<string name="screen_app_lock_settings_remove_pin_alert_message">"Вы ўпэўнены, што жадаеце выдаліць PIN-код?"</string>
|
<string name="screen_app_lock_settings_remove_pin_alert_message">"Вы ўпэўнены, што хочаце выдаліць PIN-код?"</string>
|
||||||
<string name="screen_app_lock_settings_remove_pin_alert_title">"Выдаліць PIN-код?"</string>
|
<string name="screen_app_lock_settings_remove_pin_alert_title">"Выдаліць PIN-код?"</string>
|
||||||
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Дазволіць %1$s"</string>
|
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Дазволіць %1$s"</string>
|
||||||
<string name="screen_app_lock_setup_biometric_unlock_skip">"Я хацеў бы выкарыстоўваць PIN-код"</string>
|
<string name="screen_app_lock_setup_biometric_unlock_skip">"Я хацеў бы выкарыстоўваць PIN-код"</string>
|
||||||
|
|
@ -25,12 +25,12 @@
|
||||||
<string name="screen_app_lock_signout_alert_title">"Вы выходзіце з сістэмы"</string>
|
<string name="screen_app_lock_signout_alert_title">"Вы выходзіце з сістэмы"</string>
|
||||||
<plurals name="screen_app_lock_subtitle">
|
<plurals name="screen_app_lock_subtitle">
|
||||||
<item quantity="one">"У вас %1$d спроба разблакіроўкі"</item>
|
<item quantity="one">"У вас %1$d спроба разблакіроўкі"</item>
|
||||||
<item quantity="few">"У вас %1$d спроб разблакіроўкі"</item>
|
<item quantity="few">"У вас %1$d спробы разблакіроўкі"</item>
|
||||||
<item quantity="many">"У вас %1$d спроб разблакіроўкі"</item>
|
<item quantity="many">"У вас %1$d спроб разблакіроўкі"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="screen_app_lock_subtitle_wrong_pin">
|
<plurals name="screen_app_lock_subtitle_wrong_pin">
|
||||||
<item quantity="one">"Няправільны PIN-код. У вас застаўся %1$d шанец"</item>
|
<item quantity="one">"Няправільны PIN-код. У вас застаўся %1$d шанец"</item>
|
||||||
<item quantity="few">"Няправільны PIN-код. У вас застаўася %1$d шанцаў"</item>
|
<item quantity="few">"Няправільны PIN-код. У вас застаўася %1$d шанца"</item>
|
||||||
<item quantity="many">"Няправільны PIN-код. У вас застаўася %1$d шанцаў"</item>
|
<item quantity="many">"Няправільны PIN-код. У вас застаўася %1$d шанцаў"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="screen_app_lock_use_biometric_android">"Выкарыстоўваць біяметрыю"</string>
|
<string name="screen_app_lock_use_biometric_android">"Выкарыстоўваць біяметрыю"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_app_lock_settings_change_pin">"Byt PIN-kod"</string>
|
||||||
|
<string name="screen_app_lock_settings_enable_biometric_unlock">"Tillåt biometrisk upplåsning"</string>
|
||||||
|
<string name="screen_app_lock_settings_remove_pin">"Ta bort PIN-kod"</string>
|
||||||
|
<string name="screen_app_lock_settings_remove_pin_alert_title">"Ta bort PIN-koden?"</string>
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Loggar ut …"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Loggar ut …"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<string name="screen_account_provider_signin_subtitle">"Itt lesznek a beszélgetései – ahogyan egy e-mail-szolgáltatást is használna a levelei kezeléséhez."</string>
|
<string name="screen_account_provider_signin_subtitle">"Itt lesznek a beszélgetései – ahogyan egy e-mail-szolgáltatást is használna a levelei kezeléséhez."</string>
|
||||||
<string name="screen_account_provider_signin_title">"Hamarosan bejelentkezik ide: %s"</string>
|
<string name="screen_account_provider_signin_title">"Hamarosan bejelentkezik ide: %s"</string>
|
||||||
<string name="screen_account_provider_signup_subtitle">"Itt lesznek a beszélgetései – ahogyan egy e-mail-szolgáltatást is használna a levelei kezeléséhez."</string>
|
<string name="screen_account_provider_signup_subtitle">"Itt lesznek a beszélgetései – ahogyan egy e-mail-szolgáltatást is használna a levelei kezeléséhez."</string>
|
||||||
<string name="screen_account_provider_signup_title">"Hamarosan létrehozol egy fiókot itt: %s"</string>
|
<string name="screen_account_provider_signup_title">"Hamarosan létrehoz egy fiókot itt: %s"</string>
|
||||||
<string name="screen_change_account_provider_matrix_org_subtitle">"A Matrix.org egy nagy, ingyenes kiszolgáló a nyilvános Matrix-hálózaton, a biztonságos, decentralizált kommunikáció érdekében, amelyet a Matrix.org Alapítvány üzemeltet."</string>
|
<string name="screen_change_account_provider_matrix_org_subtitle">"A Matrix.org egy nagy, ingyenes kiszolgáló a nyilvános Matrix-hálózaton, a biztonságos, decentralizált kommunikáció érdekében, amelyet a Matrix.org Alapítvány üzemeltet."</string>
|
||||||
<string name="screen_change_account_provider_other">"Egyéb"</string>
|
<string name="screen_change_account_provider_other">"Egyéb"</string>
|
||||||
<string name="screen_change_account_provider_subtitle">"Másik fiókszolgáltató, például a saját privát kiszolgáló vagy egy munkahelyi fiók használata."</string>
|
<string name="screen_change_account_provider_subtitle">"Másik fiókszolgáltató, például a saját privát kiszolgáló vagy egy munkahelyi fiók használata."</string>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_signout_confirmation_dialog_content">"Вы ўпэўнены, што жадаеце выйсці?"</string>
|
<string name="screen_signout_confirmation_dialog_content">"Вы ўпэўнены, што хочаце выйсці?"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_submit">"Выйсці"</string>
|
<string name="screen_signout_confirmation_dialog_submit">"Выйсці"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_title">"Выйсці"</string>
|
<string name="screen_signout_confirmation_dialog_title">"Выйсці"</string>
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Выхад…"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Выхад…"</string>
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,12 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl
|
package io.element.android.features.messages.impl
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import com.bumble.appyx.core.lifecycle.subscribe
|
import com.bumble.appyx.core.lifecycle.subscribe
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
|
|
@ -31,11 +34,14 @@ import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
||||||
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
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.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||||
import io.element.android.libraries.mediaplayer.api.MediaPlayer
|
import io.element.android.libraries.mediaplayer.api.MediaPlayer
|
||||||
|
|
@ -52,6 +58,7 @@ class MessagesNode @AssistedInject constructor(
|
||||||
presenterFactory: MessagesPresenter.Factory,
|
presenterFactory: MessagesPresenter.Factory,
|
||||||
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
|
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
|
||||||
private val mediaPlayer: MediaPlayer,
|
private val mediaPlayer: MediaPlayer,
|
||||||
|
private val permalinkParser: PermalinkParser,
|
||||||
) : Node(buildContext, plugins = plugins), MessagesNavigator {
|
) : Node(buildContext, plugins = plugins), MessagesNavigator {
|
||||||
private val presenter = presenterFactory.create(this)
|
private val presenter = presenterFactory.create(this)
|
||||||
private val callback = plugins<Callback>().firstOrNull()
|
private val callback = plugins<Callback>().firstOrNull()
|
||||||
|
|
@ -96,6 +103,25 @@ class MessagesNode @AssistedInject constructor(
|
||||||
private fun onUserDataClicked(userId: UserId) {
|
private fun onUserDataClicked(userId: UserId) {
|
||||||
callback?.onUserDataClicked(userId)
|
callback?.onUserDataClicked(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onLinkClicked(
|
||||||
|
context: Context,
|
||||||
|
url: String,
|
||||||
|
) {
|
||||||
|
when (val permalink = permalinkParser.parse(Uri.parse(url))) {
|
||||||
|
is PermalinkData.UserLink -> {
|
||||||
|
callback?.onUserDataClicked(UserId(permalink.userId))
|
||||||
|
}
|
||||||
|
is PermalinkData.RoomLink -> {
|
||||||
|
// TODO Implement room link handling
|
||||||
|
}
|
||||||
|
is PermalinkData.FallbackLink,
|
||||||
|
is PermalinkData.RoomEmailInviteLink -> {
|
||||||
|
context.openUrlInExternalApp(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
|
override fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
|
||||||
callback?.onShowEventDebugInfoClicked(eventId, debugInfo)
|
callback?.onShowEventDebugInfoClicked(eventId, debugInfo)
|
||||||
}
|
}
|
||||||
|
|
@ -126,6 +152,7 @@ class MessagesNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
|
val context = LocalContext.current
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
|
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
|
||||||
) {
|
) {
|
||||||
|
|
@ -137,6 +164,7 @@ class MessagesNode @AssistedInject constructor(
|
||||||
onEventClicked = this::onEventClicked,
|
onEventClicked = this::onEventClicked,
|
||||||
onPreviewAttachments = this::onPreviewAttachments,
|
onPreviewAttachments = this::onPreviewAttachments,
|
||||||
onUserDataClicked = this::onUserDataClicked,
|
onUserDataClicked = this::onUserDataClicked,
|
||||||
|
onLinkClicked = { onLinkClicked(context, it) },
|
||||||
onSendLocationClicked = this::onSendLocationClicked,
|
onSendLocationClicked = this::onSendLocationClicked,
|
||||||
onCreatePollClicked = this::onCreatePollClicked,
|
onCreatePollClicked = this::onCreatePollClicked,
|
||||||
onJoinCallClicked = this::onJoinCallClicked,
|
onJoinCallClicked = this::onJoinCallClicked,
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ import io.element.android.libraries.matrix.ui.room.canRedactOtherAsState
|
||||||
import io.element.android.libraries.matrix.ui.room.canRedactOwnAsState
|
import io.element.android.libraries.matrix.ui.room.canRedactOwnAsState
|
||||||
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
@ -273,6 +274,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
) = launch {
|
) = launch {
|
||||||
when (action) {
|
when (action) {
|
||||||
TimelineItemAction.Copy -> handleCopyContents(targetEvent)
|
TimelineItemAction.Copy -> handleCopyContents(targetEvent)
|
||||||
|
TimelineItemAction.CopyLink -> handleCopyLink(targetEvent)
|
||||||
TimelineItemAction.Redact -> handleActionRedact(targetEvent)
|
TimelineItemAction.Redact -> handleActionRedact(targetEvent)
|
||||||
TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState, enableTextFormatting)
|
TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState, enableTextFormatting)
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
|
|
@ -435,6 +437,20 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
event.eventId?.let { timelineState.eventSink(TimelineEvents.PollEndClicked(it)) }
|
event.eventId?.let { timelineState.eventSink(TimelineEvents.PollEndClicked(it)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun handleCopyLink(event: TimelineItem.Event) {
|
||||||
|
event.eventId ?: return
|
||||||
|
room.getPermalinkFor(event.eventId).fold(
|
||||||
|
onSuccess = { permalink ->
|
||||||
|
clipboardHelper.copyPlainText(permalink)
|
||||||
|
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_link_copied_to_clipboard))
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
Timber.e(it, "Failed to get permalink for event ${event.eventId}")
|
||||||
|
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_error))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun handleCopyContents(event: TimelineItem.Event) {
|
private suspend fun handleCopyContents(event: TimelineItem.Event) {
|
||||||
val content = when (event.content) {
|
val content = when (event.content) {
|
||||||
is TimelineItemTextBasedContent -> event.content.body
|
is TimelineItemTextBasedContent -> event.content.body
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,7 @@ fun MessagesView(
|
||||||
onRoomDetailsClicked: () -> Unit,
|
onRoomDetailsClicked: () -> Unit,
|
||||||
onEventClicked: (event: TimelineItem.Event) -> Boolean,
|
onEventClicked: (event: TimelineItem.Event) -> Boolean,
|
||||||
onUserDataClicked: (UserId) -> Unit,
|
onUserDataClicked: (UserId) -> Unit,
|
||||||
|
onLinkClicked: (String) -> Unit,
|
||||||
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit,
|
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit,
|
||||||
onSendLocationClicked: () -> Unit,
|
onSendLocationClicked: () -> Unit,
|
||||||
onCreatePollClicked: () -> Unit,
|
onCreatePollClicked: () -> Unit,
|
||||||
|
|
@ -213,6 +214,7 @@ fun MessagesView(
|
||||||
onMessageClicked = ::onMessageClicked,
|
onMessageClicked = ::onMessageClicked,
|
||||||
onMessageLongClicked = ::onMessageLongClicked,
|
onMessageLongClicked = ::onMessageLongClicked,
|
||||||
onUserDataClicked = onUserDataClicked,
|
onUserDataClicked = onUserDataClicked,
|
||||||
|
onLinkClicked = onLinkClicked,
|
||||||
onTimestampClicked = { event ->
|
onTimestampClicked = { event ->
|
||||||
if (event.localSendState is LocalEventSendState.SendingFailed) {
|
if (event.localSendState is LocalEventSendState.SendingFailed) {
|
||||||
state.retrySendMenuState.eventSink(RetrySendMenuEvents.EventSelected(event))
|
state.retrySendMenuState.eventSink(RetrySendMenuEvents.EventSelected(event))
|
||||||
|
|
@ -313,6 +315,7 @@ private fun MessagesViewContent(
|
||||||
state: MessagesState,
|
state: MessagesState,
|
||||||
onMessageClicked: (TimelineItem.Event) -> Unit,
|
onMessageClicked: (TimelineItem.Event) -> Unit,
|
||||||
onUserDataClicked: (UserId) -> Unit,
|
onUserDataClicked: (UserId) -> Unit,
|
||||||
|
onLinkClicked: (String) -> Unit,
|
||||||
onReactionClicked: (key: String, TimelineItem.Event) -> Unit,
|
onReactionClicked: (key: String, TimelineItem.Event) -> Unit,
|
||||||
onReactionLongClicked: (key: String, TimelineItem.Event) -> Unit,
|
onReactionLongClicked: (key: String, TimelineItem.Event) -> Unit,
|
||||||
onMoreReactionsClicked: (TimelineItem.Event) -> Unit,
|
onMoreReactionsClicked: (TimelineItem.Event) -> Unit,
|
||||||
|
|
@ -386,6 +389,7 @@ private fun MessagesViewContent(
|
||||||
onMessageClicked = onMessageClicked,
|
onMessageClicked = onMessageClicked,
|
||||||
onMessageLongClicked = onMessageLongClicked,
|
onMessageLongClicked = onMessageLongClicked,
|
||||||
onUserDataClicked = onUserDataClicked,
|
onUserDataClicked = onUserDataClicked,
|
||||||
|
onLinkClicked = onLinkClicked,
|
||||||
onTimestampClicked = onTimestampClicked,
|
onTimestampClicked = onTimestampClicked,
|
||||||
onReactionClicked = onReactionClicked,
|
onReactionClicked = onReactionClicked,
|
||||||
onReactionLongClicked = onReactionLongClicked,
|
onReactionLongClicked = onReactionLongClicked,
|
||||||
|
|
@ -570,6 +574,7 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class)
|
||||||
onEventClicked = { false },
|
onEventClicked = { false },
|
||||||
onPreviewAttachments = {},
|
onPreviewAttachments = {},
|
||||||
onUserDataClicked = {},
|
onUserDataClicked = {},
|
||||||
|
onLinkClicked = {},
|
||||||
onSendLocationClicked = {},
|
onSendLocationClicked = {},
|
||||||
onCreatePollClicked = {},
|
onCreatePollClicked = {},
|
||||||
onJoinCallClicked = {},
|
onJoinCallClicked = {},
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ class ActionListPresenter @Inject constructor(
|
||||||
is TimelineItemStateContent -> {
|
is TimelineItemStateContent -> {
|
||||||
buildList {
|
buildList {
|
||||||
add(TimelineItemAction.Copy)
|
add(TimelineItemAction.Copy)
|
||||||
|
add(TimelineItemAction.CopyLink)
|
||||||
if (isDeveloperModeEnabled) {
|
if (isDeveloperModeEnabled) {
|
||||||
add(TimelineItemAction.ViewSource)
|
add(TimelineItemAction.ViewSource)
|
||||||
}
|
}
|
||||||
|
|
@ -119,6 +120,7 @@ class ActionListPresenter @Inject constructor(
|
||||||
if (timelineItem.content.canBeCopied()) {
|
if (timelineItem.content.canBeCopied()) {
|
||||||
add(TimelineItemAction.Copy)
|
add(TimelineItemAction.Copy)
|
||||||
}
|
}
|
||||||
|
add(TimelineItemAction.CopyLink)
|
||||||
if (isDeveloperModeEnabled) {
|
if (isDeveloperModeEnabled) {
|
||||||
add(TimelineItemAction.ViewSource)
|
add(TimelineItemAction.ViewSource)
|
||||||
}
|
}
|
||||||
|
|
@ -136,6 +138,7 @@ class ActionListPresenter @Inject constructor(
|
||||||
add(TimelineItemAction.Reply)
|
add(TimelineItemAction.Reply)
|
||||||
add(TimelineItemAction.Forward)
|
add(TimelineItemAction.Forward)
|
||||||
}
|
}
|
||||||
|
add(TimelineItemAction.CopyLink)
|
||||||
if (isDeveloperModeEnabled) {
|
if (isDeveloperModeEnabled) {
|
||||||
add(TimelineItemAction.ViewSource)
|
add(TimelineItemAction.ViewSource)
|
||||||
}
|
}
|
||||||
|
|
@ -176,6 +179,7 @@ class ActionListPresenter @Inject constructor(
|
||||||
if (timelineItem.content.canBeCopied()) {
|
if (timelineItem.content.canBeCopied()) {
|
||||||
add(TimelineItemAction.Copy)
|
add(TimelineItemAction.Copy)
|
||||||
}
|
}
|
||||||
|
add(TimelineItemAction.CopyLink)
|
||||||
if (isDeveloperModeEnabled) {
|
if (isDeveloperModeEnabled) {
|
||||||
add(TimelineItemAction.ViewSource)
|
add(TimelineItemAction.ViewSource)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,7 @@ fun aTimelineItemActionList(): ImmutableList<TimelineItemAction> {
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.Copy,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
|
|
@ -146,6 +147,7 @@ fun aTimelineItemPollActionList(): ImmutableList<TimelineItemAction> {
|
||||||
TimelineItemAction.EndPoll,
|
TimelineItemAction.EndPoll,
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.Copy,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ sealed class TimelineItemAction(
|
||||||
) {
|
) {
|
||||||
data object Forward : TimelineItemAction(CommonStrings.action_forward, CompoundDrawables.ic_compound_forward)
|
data object Forward : TimelineItemAction(CommonStrings.action_forward, CompoundDrawables.ic_compound_forward)
|
||||||
data object Copy : TimelineItemAction(CommonStrings.action_copy, CompoundDrawables.ic_compound_copy)
|
data object Copy : TimelineItemAction(CommonStrings.action_copy, CompoundDrawables.ic_compound_copy)
|
||||||
|
data object CopyLink : TimelineItemAction(CommonStrings.action_copy_link_to_message, CompoundDrawables.ic_compound_link)
|
||||||
data object Redact : TimelineItemAction(CommonStrings.action_remove, CompoundDrawables.ic_compound_delete, destructive = true)
|
data object Redact : TimelineItemAction(CommonStrings.action_remove, CompoundDrawables.ic_compound_delete, destructive = true)
|
||||||
data object Reply : TimelineItemAction(CommonStrings.action_reply, CompoundDrawables.ic_compound_reply)
|
data object Reply : TimelineItemAction(CommonStrings.action_reply, CompoundDrawables.ic_compound_reply)
|
||||||
data object ReplyInThread : TimelineItemAction(CommonStrings.action_reply_in_thread, CompoundDrawables.ic_compound_reply)
|
data object ReplyInThread : TimelineItemAction(CommonStrings.action_reply_in_thread, CompoundDrawables.ic_compound_reply)
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.Mention
|
import io.element.android.libraries.matrix.api.room.Mention
|
||||||
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
|
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
|
||||||
|
|
@ -96,6 +97,8 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
private val messageComposerContext: MessageComposerContextImpl,
|
private val messageComposerContext: MessageComposerContextImpl,
|
||||||
private val richTextEditorStateFactory: RichTextEditorStateFactory,
|
private val richTextEditorStateFactory: RichTextEditorStateFactory,
|
||||||
private val currentSessionIdHolder: CurrentSessionIdHolder,
|
private val currentSessionIdHolder: CurrentSessionIdHolder,
|
||||||
|
private val permalinkParser: PermalinkParser,
|
||||||
|
private val permalinkBuilder: PermalinkBuilder,
|
||||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||||
) : Presenter<MessageComposerState> {
|
) : Presenter<MessageComposerState> {
|
||||||
private val cameraPermissionPresenter = permissionsPresenterFactory.create(Manifest.permission.CAMERA)
|
private val cameraPermissionPresenter = permissionsPresenterFactory.create(Manifest.permission.CAMERA)
|
||||||
|
|
@ -334,7 +337,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
is MentionSuggestion.Member -> {
|
is MentionSuggestion.Member -> {
|
||||||
val text = mention.roomMember.displayName?.prependIndent("@") ?: mention.roomMember.userId.value
|
val text = mention.roomMember.displayName?.prependIndent("@") ?: mention.roomMember.userId.value
|
||||||
val link = PermalinkBuilder.permalinkForUser(mention.roomMember.userId).getOrNull() ?: return@launch
|
val link = permalinkBuilder.permalinkForUser(mention.roomMember.userId).getOrNull() ?: return@launch
|
||||||
richTextEditorState.insertMentionAtSuggestion(text = text, link = link)
|
richTextEditorState.insertMentionAtSuggestion(text = text, link = link)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -345,6 +348,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
|
|
||||||
return MessageComposerState(
|
return MessageComposerState(
|
||||||
richTextEditorState = richTextEditorState,
|
richTextEditorState = richTextEditorState,
|
||||||
|
permalinkParser = permalinkParser,
|
||||||
isFullScreen = isFullScreen.value,
|
isFullScreen = isFullScreen.value,
|
||||||
mode = messageComposerContext.composerMode,
|
mode = messageComposerContext.composerMode,
|
||||||
showAttachmentSourcePicker = showAttachmentSourcePicker,
|
showAttachmentSourcePicker = showAttachmentSourcePicker,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import androidx.compose.runtime.Stable
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.features.messages.impl.mentions.MentionSuggestion
|
import io.element.android.features.messages.impl.mentions.MentionSuggestion
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.wysiwyg.compose.RichTextEditorState
|
import io.element.android.wysiwyg.compose.RichTextEditorState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
@ -28,6 +29,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
@Stable
|
@Stable
|
||||||
data class MessageComposerState(
|
data class MessageComposerState(
|
||||||
val richTextEditorState: RichTextEditorState,
|
val richTextEditorState: RichTextEditorState,
|
||||||
|
val permalinkParser: PermalinkParser,
|
||||||
val isFullScreen: Boolean,
|
val isFullScreen: Boolean,
|
||||||
val mode: MessageComposerMode,
|
val mode: MessageComposerMode,
|
||||||
val showAttachmentSourcePicker: Boolean,
|
val showAttachmentSourcePicker: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,12 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.messagecomposer
|
package io.element.android.features.messages.impl.messagecomposer
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.messages.impl.mentions.MentionSuggestion
|
import io.element.android.features.messages.impl.mentions.MentionSuggestion
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.wysiwyg.compose.RichTextEditorState
|
import io.element.android.wysiwyg.compose.RichTextEditorState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
@ -43,6 +46,10 @@ fun aMessageComposerState(
|
||||||
memberSuggestions: ImmutableList<MentionSuggestion> = persistentListOf(),
|
memberSuggestions: ImmutableList<MentionSuggestion> = persistentListOf(),
|
||||||
) = MessageComposerState(
|
) = MessageComposerState(
|
||||||
richTextEditorState = richTextEditorState,
|
richTextEditorState = richTextEditorState,
|
||||||
|
permalinkParser = object : PermalinkParser {
|
||||||
|
override fun parse(uriString: String): PermalinkData = TODO()
|
||||||
|
override fun parse(uri: Uri): PermalinkData = TODO()
|
||||||
|
},
|
||||||
isFullScreen = isFullScreen,
|
isFullScreen = isFullScreen,
|
||||||
mode = mode,
|
mode = mode,
|
||||||
showTextFormatting = showTextFormatting,
|
showTextFormatting = showTextFormatting,
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,7 @@ internal fun MessageComposerView(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
state = state.richTextEditorState,
|
state = state.richTextEditorState,
|
||||||
voiceMessageState = voiceMessageState.voiceMessageState,
|
voiceMessageState = voiceMessageState.voiceMessageState,
|
||||||
|
permalinkParser = state.permalinkParser,
|
||||||
subcomposing = subcomposing,
|
subcomposing = subcomposing,
|
||||||
onRequestFocus = ::onRequestFocus,
|
onRequestFocus = ::onRequestFocus,
|
||||||
onSendMessage = ::sendMessage,
|
onSendMessage = ::sendMessage,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import io.element.android.libraries.core.bool.orFalse
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
|
import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
|
||||||
import io.element.android.libraries.textcomposer.mentions.rememberMentionSpanProvider
|
import io.element.android.libraries.textcomposer.mentions.rememberMentionSpanProvider
|
||||||
import io.element.android.wysiwyg.compose.StyledHtmlConverter
|
import io.element.android.wysiwyg.compose.StyledHtmlConverter
|
||||||
|
|
@ -39,7 +40,9 @@ import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(SessionScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
@SingleIn(SessionScope::class)
|
@SingleIn(SessionScope::class)
|
||||||
class DefaultHtmlConverterProvider @Inject constructor() : HtmlConverterProvider {
|
class DefaultHtmlConverterProvider @Inject constructor(
|
||||||
|
private val permalinkParser: PermalinkParser,
|
||||||
|
) : HtmlConverterProvider {
|
||||||
private val htmlConverter: MutableState<HtmlConverter?> = mutableStateOf(null)
|
private val htmlConverter: MutableState<HtmlConverter?> = mutableStateOf(null)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -50,7 +53,10 @@ class DefaultHtmlConverterProvider @Inject constructor() : HtmlConverterProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
val editorStyle = ElementRichTextEditorStyle.textStyle()
|
val editorStyle = ElementRichTextEditorStyle.textStyle()
|
||||||
val mentionSpanProvider = rememberMentionSpanProvider(currentUserId = currentUserId)
|
val mentionSpanProvider = rememberMentionSpanProvider(
|
||||||
|
currentUserId = currentUserId,
|
||||||
|
permalinkParser = permalinkParser,
|
||||||
|
)
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ import io.element.android.features.messages.impl.MessagesNavigator
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
||||||
import io.element.android.features.poll.api.actions.EndPollAction
|
import io.element.android.features.poll.api.actions.EndPollAction
|
||||||
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
||||||
|
|
@ -41,15 +40,11 @@ import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
|
||||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
|
||||||
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -68,8 +63,6 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
private val appScope: CoroutineScope,
|
private val appScope: CoroutineScope,
|
||||||
@Assisted private val navigator: MessagesNavigator,
|
@Assisted private val navigator: MessagesNavigator,
|
||||||
private val verificationService: SessionVerificationService,
|
|
||||||
private val encryptionService: EncryptionService,
|
|
||||||
private val redactedVoiceMessageManager: RedactedVoiceMessageManager,
|
private val redactedVoiceMessageManager: RedactedVoiceMessageManager,
|
||||||
private val sendPollResponseAction: SendPollResponseAction,
|
private val sendPollResponseAction: SendPollResponseAction,
|
||||||
private val endPollAction: EndPollAction,
|
private val endPollAction: EndPollAction,
|
||||||
|
|
@ -101,21 +94,9 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
val prevMostRecentItemId = rememberSaveable { mutableStateOf<String?>(null) }
|
val prevMostRecentItemId = rememberSaveable { mutableStateOf<String?>(null) }
|
||||||
val newItemState = remember { mutableStateOf(NewEventState.None) }
|
val newItemState = remember { mutableStateOf(NewEventState.None) }
|
||||||
|
|
||||||
val sessionVerifiedStatus by verificationService.sessionVerifiedStatus.collectAsState()
|
|
||||||
val keyBackupState by encryptionService.backupStateStateFlow.collectAsState()
|
|
||||||
|
|
||||||
val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true)
|
val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true)
|
||||||
val renderReadReceipts by sessionPreferencesStore.isRenderReadReceiptsEnabled().collectAsState(initial = true)
|
val renderReadReceipts by sessionPreferencesStore.isRenderReadReceiptsEnabled().collectAsState(initial = true)
|
||||||
|
|
||||||
val sessionState by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
SessionState(
|
|
||||||
isSessionVerified = sessionVerifiedStatus == SessionVerifiedStatus.Verified,
|
|
||||||
isKeyBackupEnabled = keyBackupState == BackupState.ENABLED
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleEvents(event: TimelineEvents) {
|
fun handleEvents(event: TimelineEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
TimelineEvents.LoadMore -> localScope.paginateBackwards()
|
TimelineEvents.LoadMore -> localScope.paginateBackwards()
|
||||||
|
|
@ -184,7 +165,6 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
timelineItems = timelineItems,
|
timelineItems = timelineItems,
|
||||||
renderReadReceipts = renderReadReceipts,
|
renderReadReceipts = renderReadReceipts,
|
||||||
newEventState = newItemState.value,
|
newEventState = newItemState.value,
|
||||||
sessionState = sessionState,
|
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = { handleEvents(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ package io.element.android.features.messages.impl.timeline
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
@ -32,7 +31,6 @@ data class TimelineState(
|
||||||
val highlightedEventId: EventId?,
|
val highlightedEventId: EventId?,
|
||||||
val paginationState: MatrixTimeline.PaginationState,
|
val paginationState: MatrixTimeline.PaginationState,
|
||||||
val newEventState: NewEventState,
|
val newEventState: NewEventState,
|
||||||
val sessionState: SessionState,
|
|
||||||
val eventSink: (TimelineEvents) -> Unit
|
val eventSink: (TimelineEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel
|
||||||
import io.element.android.features.messages.impl.timeline.session.aSessionState
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
@ -58,10 +57,6 @@ fun aTimelineState(
|
||||||
renderReadReceipts = renderReadReceipts,
|
renderReadReceipts = renderReadReceipts,
|
||||||
highlightedEventId = null,
|
highlightedEventId = null,
|
||||||
newEventState = NewEventState.None,
|
newEventState = NewEventState.None,
|
||||||
sessionState = aSessionState(
|
|
||||||
isSessionVerified = true,
|
|
||||||
isKeyBackupEnabled = true,
|
|
||||||
),
|
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ fun TimelineView(
|
||||||
typingNotificationState: TypingNotificationState,
|
typingNotificationState: TypingNotificationState,
|
||||||
roomName: String?,
|
roomName: String?,
|
||||||
onUserDataClicked: (UserId) -> Unit,
|
onUserDataClicked: (UserId) -> Unit,
|
||||||
|
onLinkClicked: (String) -> Unit,
|
||||||
onMessageClicked: (TimelineItem.Event) -> Unit,
|
onMessageClicked: (TimelineItem.Event) -> Unit,
|
||||||
onMessageLongClicked: (TimelineItem.Event) -> Unit,
|
onMessageLongClicked: (TimelineItem.Event) -> Unit,
|
||||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||||
|
|
@ -140,13 +141,13 @@ fun TimelineView(
|
||||||
onClick = onMessageClicked,
|
onClick = onMessageClicked,
|
||||||
onLongClick = onMessageLongClicked,
|
onLongClick = onMessageLongClicked,
|
||||||
onUserDataClick = onUserDataClicked,
|
onUserDataClick = onUserDataClicked,
|
||||||
|
onLinkClicked = onLinkClicked,
|
||||||
inReplyToClick = ::inReplyToClicked,
|
inReplyToClick = ::inReplyToClicked,
|
||||||
onReactionClick = onReactionClicked,
|
onReactionClick = onReactionClicked,
|
||||||
onReactionLongClick = onReactionLongClicked,
|
onReactionLongClick = onReactionLongClicked,
|
||||||
onMoreReactionsClick = onMoreReactionsClicked,
|
onMoreReactionsClick = onMoreReactionsClicked,
|
||||||
onReadReceiptClick = onReadReceiptClick,
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
onTimestampClicked = onTimestampClicked,
|
onTimestampClicked = onTimestampClicked,
|
||||||
sessionState = state.sessionState,
|
|
||||||
eventSink = state.eventSink,
|
eventSink = state.eventSink,
|
||||||
onSwipeToReply = onSwipeToReply,
|
onSwipeToReply = onSwipeToReply,
|
||||||
)
|
)
|
||||||
|
|
@ -276,6 +277,7 @@ internal fun TimelineViewPreview(
|
||||||
onMessageClicked = {},
|
onMessageClicked = {},
|
||||||
onTimestampClicked = {},
|
onTimestampClicked = {},
|
||||||
onUserDataClicked = {},
|
onUserDataClicked = {},
|
||||||
|
onLinkClicked = {},
|
||||||
onMessageLongClicked = {},
|
onMessageLongClicked = {},
|
||||||
onReactionClicked = { _, _ -> },
|
onReactionClicked = { _, _ -> },
|
||||||
onReactionLongClicked = { _, _ -> },
|
onReactionLongClicked = { _, _ -> },
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ internal fun ATimelineItemEventRow(
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onLongClick = {},
|
onLongClick = {},
|
||||||
onUserDataClick = {},
|
onUserDataClick = {},
|
||||||
|
onLinkClicked = {},
|
||||||
inReplyToClick = {},
|
inReplyToClick = {},
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
package io.element.android.features.messages.impl.timeline.components
|
package io.element.android.features.messages.impl.timeline.components
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.net.Uri
|
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
|
@ -50,7 +49,6 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||||
import androidx.compose.ui.platform.ViewConfiguration
|
import androidx.compose.ui.platform.ViewConfiguration
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -94,7 +92,6 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo
|
import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo
|
||||||
import io.element.android.features.messages.impl.timeline.model.metadata
|
import io.element.android.features.messages.impl.timeline.model.metadata
|
||||||
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
|
|
||||||
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
|
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
|
||||||
import io.element.android.libraries.designsystem.components.EqualWidthColumn
|
import io.element.android.libraries.designsystem.components.EqualWidthColumn
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
|
|
@ -109,9 +106,6 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
|
||||||
import io.element.android.libraries.matrix.api.room.Mention
|
|
||||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
@ -128,6 +122,7 @@ fun TimelineItemEventRow(
|
||||||
isHighlighted: Boolean,
|
isHighlighted: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
|
onLinkClicked: (String) -> Unit,
|
||||||
onUserDataClick: (UserId) -> Unit,
|
onUserDataClick: (UserId) -> Unit,
|
||||||
inReplyToClick: (EventId) -> Unit,
|
inReplyToClick: (EventId) -> Unit,
|
||||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||||
|
|
@ -151,13 +146,6 @@ fun TimelineItemEventRow(
|
||||||
inReplyToClick(inReplyToEventId)
|
inReplyToClick(inReplyToEventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMentionClicked(mention: Mention) {
|
|
||||||
when (mention) {
|
|
||||||
is Mention.User -> onUserDataClick(mention.userId)
|
|
||||||
else -> Unit // TODO implement actions for other mentions being clicked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(modifier = modifier.fillMaxWidth()) {
|
Column(modifier = modifier.fillMaxWidth()) {
|
||||||
if (event.groupPosition.isNew()) {
|
if (event.groupPosition.isNew()) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
@ -203,7 +191,7 @@ fun TimelineItemEventRow(
|
||||||
onReactionClicked = { emoji -> onReactionClick(emoji, event) },
|
onReactionClicked = { emoji -> onReactionClick(emoji, event) },
|
||||||
onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) },
|
onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) },
|
||||||
onMoreReactionsClicked = { onMoreReactionsClick(event) },
|
onMoreReactionsClicked = { onMoreReactionsClick(event) },
|
||||||
onMentionClicked = ::onMentionClicked,
|
onLinkClicked = onLinkClicked,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -222,7 +210,7 @@ fun TimelineItemEventRow(
|
||||||
onReactionClicked = { emoji -> onReactionClick(emoji, event) },
|
onReactionClicked = { emoji -> onReactionClick(emoji, event) },
|
||||||
onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) },
|
onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) },
|
||||||
onMoreReactionsClicked = { onMoreReactionsClick(event) },
|
onMoreReactionsClicked = { onMoreReactionsClick(event) },
|
||||||
onMentionClicked = ::onMentionClicked,
|
onLinkClicked = onLinkClicked,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -278,7 +266,7 @@ private fun TimelineItemEventRowContent(
|
||||||
onReactionClicked: (emoji: String) -> Unit,
|
onReactionClicked: (emoji: String) -> Unit,
|
||||||
onReactionLongClicked: (emoji: String) -> Unit,
|
onReactionLongClicked: (emoji: String) -> Unit,
|
||||||
onMoreReactionsClicked: (event: TimelineItem.Event) -> Unit,
|
onMoreReactionsClicked: (event: TimelineItem.Event) -> Unit,
|
||||||
onMentionClicked: (Mention) -> Unit,
|
onLinkClicked: (String) -> Unit,
|
||||||
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
|
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -346,7 +334,7 @@ private fun TimelineItemEventRowContent(
|
||||||
onTimestampClicked = {
|
onTimestampClicked = {
|
||||||
onTimestampClicked(event)
|
onTimestampClicked(event)
|
||||||
},
|
},
|
||||||
onMentionClicked = onMentionClicked,
|
onLinkClicked = onLinkClicked,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -429,7 +417,7 @@ private fun MessageEventBubbleContent(
|
||||||
@Suppress("UNUSED_PARAMETER")
|
@Suppress("UNUSED_PARAMETER")
|
||||||
inReplyToClick: () -> Unit,
|
inReplyToClick: () -> Unit,
|
||||||
onTimestampClicked: () -> Unit,
|
onTimestampClicked: () -> Unit,
|
||||||
onMentionClicked: (Mention) -> Unit,
|
onLinkClicked: (String) -> Unit,
|
||||||
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
|
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
|
||||||
@SuppressLint("ModifierParameter")
|
@SuppressLint("ModifierParameter")
|
||||||
// need to rename this modifier to prevent linter false positives
|
// need to rename this modifier to prevent linter false positives
|
||||||
|
|
@ -530,7 +518,6 @@ private fun MessageEventBubbleContent(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
canShrinkContent: Boolean = false,
|
canShrinkContent: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
val timestampLayoutModifier: Modifier
|
val timestampLayoutModifier: Modifier
|
||||||
val contentModifier: Modifier
|
val contentModifier: Modifier
|
||||||
when {
|
when {
|
||||||
|
|
@ -566,20 +553,7 @@ private fun MessageEventBubbleContent(
|
||||||
) { onContentLayoutChanged ->
|
) { onContentLayoutChanged ->
|
||||||
TimelineItemEventContentView(
|
TimelineItemEventContentView(
|
||||||
content = event.content,
|
content = event.content,
|
||||||
onLinkClicked = { url ->
|
onLinkClicked = onLinkClicked,
|
||||||
when (val permalink = PermalinkParser.parse(Uri.parse(url))) {
|
|
||||||
is PermalinkData.UserLink -> {
|
|
||||||
onMentionClicked(Mention.User(UserId(permalink.userId)))
|
|
||||||
}
|
|
||||||
is PermalinkData.RoomLink -> {
|
|
||||||
onMentionClicked(Mention.Room(permalink.getRoomId(), permalink.getRoomAlias()))
|
|
||||||
}
|
|
||||||
is PermalinkData.FallbackLink,
|
|
||||||
is PermalinkData.RoomEmailInviteLink -> {
|
|
||||||
context.openUrlInExternalApp(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
onContentLayoutChanged = onContentLayoutChanged,
|
onContentLayoutChanged = onContentLayoutChanged,
|
||||||
modifier = contentModifier
|
modifier = contentModifier
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,6 @@ import io.element.android.features.messages.impl.timeline.components.group.Group
|
||||||
import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState
|
import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState
|
||||||
import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView
|
import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
|
||||||
import io.element.android.features.messages.impl.timeline.session.aSessionState
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
@ -46,11 +44,11 @@ fun TimelineItemGroupedEventsRow(
|
||||||
renderReadReceipts: Boolean,
|
renderReadReceipts: Boolean,
|
||||||
isLastOutgoingMessage: Boolean,
|
isLastOutgoingMessage: Boolean,
|
||||||
highlightedItem: String?,
|
highlightedItem: String?,
|
||||||
sessionState: SessionState,
|
|
||||||
onClick: (TimelineItem.Event) -> Unit,
|
onClick: (TimelineItem.Event) -> Unit,
|
||||||
onLongClick: (TimelineItem.Event) -> Unit,
|
onLongClick: (TimelineItem.Event) -> Unit,
|
||||||
inReplyToClick: (EventId) -> Unit,
|
inReplyToClick: (EventId) -> Unit,
|
||||||
onUserDataClick: (UserId) -> Unit,
|
onUserDataClick: (UserId) -> Unit,
|
||||||
|
onLinkClicked: (String) -> Unit,
|
||||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||||
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
|
|
@ -73,11 +71,11 @@ fun TimelineItemGroupedEventsRow(
|
||||||
highlightedItem = highlightedItem,
|
highlightedItem = highlightedItem,
|
||||||
renderReadReceipts = renderReadReceipts,
|
renderReadReceipts = renderReadReceipts,
|
||||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
sessionState = sessionState,
|
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
|
onLinkClicked = onLinkClicked,
|
||||||
onTimestampClicked = onTimestampClicked,
|
onTimestampClicked = onTimestampClicked,
|
||||||
onReactionClick = onReactionClick,
|
onReactionClick = onReactionClick,
|
||||||
onReactionLongClick = onReactionLongClick,
|
onReactionLongClick = onReactionLongClick,
|
||||||
|
|
@ -97,11 +95,11 @@ private fun TimelineItemGroupedEventsRowContent(
|
||||||
highlightedItem: String?,
|
highlightedItem: String?,
|
||||||
renderReadReceipts: Boolean,
|
renderReadReceipts: Boolean,
|
||||||
isLastOutgoingMessage: Boolean,
|
isLastOutgoingMessage: Boolean,
|
||||||
sessionState: SessionState,
|
|
||||||
onClick: (TimelineItem.Event) -> Unit,
|
onClick: (TimelineItem.Event) -> Unit,
|
||||||
onLongClick: (TimelineItem.Event) -> Unit,
|
onLongClick: (TimelineItem.Event) -> Unit,
|
||||||
inReplyToClick: (EventId) -> Unit,
|
inReplyToClick: (EventId) -> Unit,
|
||||||
onUserDataClick: (UserId) -> Unit,
|
onUserDataClick: (UserId) -> Unit,
|
||||||
|
onLinkClicked: (String) -> Unit,
|
||||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||||
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
|
|
@ -130,11 +128,11 @@ private fun TimelineItemGroupedEventsRowContent(
|
||||||
renderReadReceipts = renderReadReceipts,
|
renderReadReceipts = renderReadReceipts,
|
||||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
highlightedItem = highlightedItem,
|
highlightedItem = highlightedItem,
|
||||||
sessionState = sessionState,
|
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
|
onLinkClicked = onLinkClicked,
|
||||||
onTimestampClicked = onTimestampClicked,
|
onTimestampClicked = onTimestampClicked,
|
||||||
onReactionClick = onReactionClick,
|
onReactionClick = onReactionClick,
|
||||||
onReactionLongClick = onReactionLongClick,
|
onReactionLongClick = onReactionLongClick,
|
||||||
|
|
@ -170,11 +168,11 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi
|
||||||
highlightedItem = null,
|
highlightedItem = null,
|
||||||
renderReadReceipts = true,
|
renderReadReceipts = true,
|
||||||
isLastOutgoingMessage = false,
|
isLastOutgoingMessage = false,
|
||||||
sessionState = aSessionState(),
|
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onLongClick = {},
|
onLongClick = {},
|
||||||
inReplyToClick = {},
|
inReplyToClick = {},
|
||||||
onUserDataClick = {},
|
onUserDataClick = {},
|
||||||
|
onLinkClicked = {},
|
||||||
onTimestampClicked = {},
|
onTimestampClicked = {},
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
|
|
@ -195,11 +193,11 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi
|
||||||
highlightedItem = null,
|
highlightedItem = null,
|
||||||
renderReadReceipts = true,
|
renderReadReceipts = true,
|
||||||
isLastOutgoingMessage = false,
|
isLastOutgoingMessage = false,
|
||||||
sessionState = aSessionState(),
|
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onLongClick = {},
|
onLongClick = {},
|
||||||
inReplyToClick = {},
|
inReplyToClick = {},
|
||||||
onUserDataClick = {},
|
onUserDataClick = {},
|
||||||
|
onLinkClicked = {},
|
||||||
onTimestampClicked = {},
|
onTimestampClicked = {},
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import io.element.android.features.messages.impl.R
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||||
import io.element.android.features.messages.impl.timeline.model.AggregatedReaction
|
import io.element.android.features.messages.impl.timeline.model.AggregatedReaction
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
|
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
|
||||||
|
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
||||||
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
|
@ -166,6 +167,7 @@ internal fun TimelineItemReactionsViewOutgoingPreview() = ElementPreview {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExcludeFromCoverage
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview(
|
private fun ContentToPreview(
|
||||||
reactions: ImmutableList<AggregatedReaction>,
|
reactions: ImmutableList<AggregatedReaction>,
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
||||||
|
|
@ -34,8 +33,8 @@ internal fun TimelineItemRow(
|
||||||
renderReadReceipts: Boolean,
|
renderReadReceipts: Boolean,
|
||||||
isLastOutgoingMessage: Boolean,
|
isLastOutgoingMessage: Boolean,
|
||||||
highlightedItem: String?,
|
highlightedItem: String?,
|
||||||
sessionState: SessionState,
|
|
||||||
onUserDataClick: (UserId) -> Unit,
|
onUserDataClick: (UserId) -> Unit,
|
||||||
|
onLinkClicked: (String) -> Unit,
|
||||||
onClick: (TimelineItem.Event) -> Unit,
|
onClick: (TimelineItem.Event) -> Unit,
|
||||||
onLongClick: (TimelineItem.Event) -> Unit,
|
onLongClick: (TimelineItem.Event) -> Unit,
|
||||||
inReplyToClick: (EventId) -> Unit,
|
inReplyToClick: (EventId) -> Unit,
|
||||||
|
|
@ -52,7 +51,6 @@ internal fun TimelineItemRow(
|
||||||
is TimelineItem.Virtual -> {
|
is TimelineItem.Virtual -> {
|
||||||
TimelineItemVirtualRow(
|
TimelineItemVirtualRow(
|
||||||
virtual = timelineItem,
|
virtual = timelineItem,
|
||||||
sessionState = sessionState,
|
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -79,6 +77,7 @@ internal fun TimelineItemRow(
|
||||||
onClick = { onClick(timelineItem) },
|
onClick = { onClick(timelineItem) },
|
||||||
onLongClick = { onLongClick(timelineItem) },
|
onLongClick = { onLongClick(timelineItem) },
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
|
onLinkClicked = onLinkClicked,
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
onReactionClick = onReactionClick,
|
onReactionClick = onReactionClick,
|
||||||
onReactionLongClick = onReactionLongClick,
|
onReactionLongClick = onReactionLongClick,
|
||||||
|
|
@ -98,11 +97,11 @@ internal fun TimelineItemRow(
|
||||||
renderReadReceipts = renderReadReceipts,
|
renderReadReceipts = renderReadReceipts,
|
||||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
highlightedItem = highlightedItem,
|
highlightedItem = highlightedItem,
|
||||||
sessionState = sessionState,
|
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
|
onLinkClicked = onLinkClicked,
|
||||||
onTimestampClicked = onTimestampClicked,
|
onTimestampClicked = onTimestampClicked,
|
||||||
onReactionClick = onReactionClick,
|
onReactionClick = onReactionClick,
|
||||||
onReactionLongClick = onReactionLongClick,
|
onReactionLongClick = onReactionLongClick,
|
||||||
|
|
|
||||||
|
|
@ -25,17 +25,15 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemVirtualRow(
|
fun TimelineItemVirtualRow(
|
||||||
virtual: TimelineItem.Virtual,
|
virtual: TimelineItem.Virtual,
|
||||||
sessionState: SessionState,
|
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
when (virtual.model) {
|
when (virtual.model) {
|
||||||
is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model, modifier)
|
is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model, modifier)
|
||||||
TimelineItemReadMarkerModel -> TimelineItemReadMarkerView()
|
TimelineItemReadMarkerModel -> TimelineItemReadMarkerView()
|
||||||
is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView(sessionState, modifier)
|
is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView(modifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.components.event
|
|
||||||
|
|
||||||
import androidx.compose.material3.LocalTextStyle
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.TextMeasurer
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.rememberTextMeasurer
|
|
||||||
import androidx.compose.ui.unit.Density
|
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.element.android.compound.theme.ElementTheme
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
|
||||||
import kotlin.math.ceil
|
|
||||||
|
|
||||||
// Allow to not overlap the timestamp with the text, in the message bubble.
|
|
||||||
// Compute the size of the worst case.
|
|
||||||
data class ExtraPadding(val extraWidth: Dp)
|
|
||||||
|
|
||||||
val noExtraPadding = ExtraPadding(0.dp)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* See [io.element.android.features.messages.impl.timeline.components.TimelineEventTimestampView] for the related View.
|
|
||||||
* And https://www.figma.com/file/0MMNu7cTOzLOlWb7ctTkv3/Element-X?node-id=1819%253A99506 for the design.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun TimelineItem.Event.toExtraPadding(): ExtraPadding {
|
|
||||||
val formattedTime = sentTime
|
|
||||||
val hasMessageSendingFailed = localSendState is LocalEventSendState.SendingFailed
|
|
||||||
val isMessageEdited = (content as? TimelineItemTextBasedContent)?.isEdited.orFalse()
|
|
||||||
|
|
||||||
val textMeasurer = rememberTextMeasurer(cacheSize = 128)
|
|
||||||
val density = LocalDensity.current
|
|
||||||
|
|
||||||
var strLen = 2.dp // Extra space char
|
|
||||||
if (isMessageEdited) {
|
|
||||||
val editedText = stringResource(id = CommonStrings.common_edited_suffix)
|
|
||||||
val extraLen = remember(editedText, density) { textMeasurer.getExtraPadding(editedText, density) } + 10.dp // Text + spacing
|
|
||||||
strLen += extraLen
|
|
||||||
}
|
|
||||||
strLen += remember(formattedTime, density) { textMeasurer.getExtraPadding(formattedTime, density) }
|
|
||||||
if (hasMessageSendingFailed) {
|
|
||||||
strLen += 19.dp // Image + spacing
|
|
||||||
// I do not know why, but adding extra widths avoid overlapping when the
|
|
||||||
// message is edited and in error.
|
|
||||||
if (isMessageEdited) {
|
|
||||||
strLen += 2.dp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ExtraPadding(strLen)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun TextMeasurer.getExtraPadding(text: String, density: Density): Dp {
|
|
||||||
val timestampTextStyle = ElementTheme.typography.fontBodyXsRegular
|
|
||||||
val textWidth = measure(text = text, style = timestampTextStyle).size.width
|
|
||||||
return (textWidth / density.density).dp
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a string to add to the content of the message to avoid overlapping the timestamp.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun ExtraPadding.getStr(textStyle: TextStyle = LocalTextStyle.current): String {
|
|
||||||
if (extraWidth == 0.dp) return ""
|
|
||||||
val density = LocalDensity.current
|
|
||||||
val textMeasurer = rememberTextMeasurer(128)
|
|
||||||
val charWidth = remember(textStyle) { textMeasurer.measure(text = "\u00A0", style = textStyle).size.width }
|
|
||||||
val widthPx = remember(density, extraWidth) { with(density) { extraWidth.toPx() } }
|
|
||||||
// A space and some unbreakable spaces, always rounding the result to the next value if not a integer
|
|
||||||
return " " + "\u00A0".repeat(ceil(widthPx / charWidth).toInt())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ExtraPadding.getDpSize(): Dp {
|
|
||||||
return extraWidth
|
|
||||||
}
|
|
||||||
|
|
@ -20,7 +20,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
|
||||||
sealed interface RetrySendMenuEvents {
|
sealed interface RetrySendMenuEvents {
|
||||||
data class EventSelected(val event: TimelineItem.Event) : RetrySendMenuEvents
|
data class EventSelected(val event: TimelineItem.Event) : RetrySendMenuEvents
|
||||||
data object RetrySend : RetrySendMenuEvents
|
data object Retry : RetrySendMenuEvents
|
||||||
data object RemoveFailed : RetrySendMenuEvents
|
data object Remove : RetrySendMenuEvents
|
||||||
data object Dismiss : RetrySendMenuEvents
|
data object Dismiss : RetrySendMenuEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ class RetrySendMenuPresenter @Inject constructor(
|
||||||
is RetrySendMenuEvents.EventSelected -> {
|
is RetrySendMenuEvents.EventSelected -> {
|
||||||
selectedEvent = event.event
|
selectedEvent = event.event
|
||||||
}
|
}
|
||||||
RetrySendMenuEvents.RetrySend -> {
|
RetrySendMenuEvents.Retry -> {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
selectedEvent?.transactionId?.let { transactionId ->
|
selectedEvent?.transactionId?.let { transactionId ->
|
||||||
room.retrySendMessage(transactionId)
|
room.retrySendMessage(transactionId)
|
||||||
|
|
@ -49,7 +49,7 @@ class RetrySendMenuPresenter @Inject constructor(
|
||||||
selectedEvent = null
|
selectedEvent = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RetrySendMenuEvents.RemoveFailed -> {
|
RetrySendMenuEvents.Remove -> {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
selectedEvent?.transactionId?.let { transactionId ->
|
selectedEvent?.transactionId?.let { transactionId ->
|
||||||
room.cancelSend(transactionId)
|
room.cancelSend(transactionId)
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
package io.element.android.features.messages.impl.timeline.components.retrysendmenu
|
package io.element.android.features.messages.impl.timeline.components.retrysendmenu
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
|
@ -54,18 +53,18 @@ internal fun RetrySendMessageMenu(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onRetry() {
|
fun onRetry() {
|
||||||
state.eventSink(RetrySendMenuEvents.RetrySend)
|
state.eventSink(RetrySendMenuEvents.Retry)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onRemoveFailed() {
|
fun onRemove() {
|
||||||
state.eventSink(RetrySendMenuEvents.RemoveFailed)
|
state.eventSink(RetrySendMenuEvents.Remove)
|
||||||
}
|
}
|
||||||
|
|
||||||
RetrySendMessageMenuBottomSheet(
|
RetrySendMessageMenuBottomSheet(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
isVisible = isVisible,
|
isVisible = isVisible,
|
||||||
onRetry = ::onRetry,
|
onRetry = ::onRetry,
|
||||||
onRemoveFailed = ::onRemoveFailed,
|
onRemove = ::onRemove,
|
||||||
onDismiss = ::onDismiss
|
onDismiss = ::onDismiss
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +74,7 @@ internal fun RetrySendMessageMenu(
|
||||||
private fun RetrySendMessageMenuBottomSheet(
|
private fun RetrySendMessageMenuBottomSheet(
|
||||||
isVisible: Boolean,
|
isVisible: Boolean,
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit,
|
||||||
onRemoveFailed: () -> Unit,
|
onRemove: () -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -95,7 +94,10 @@ private fun RetrySendMessageMenuBottomSheet(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
RetrySendMenuContents(onRetry = onRetry, onRemoveFailed = onRemoveFailed)
|
RetrySendMenuContents(
|
||||||
|
onRetry = onRetry,
|
||||||
|
onRemove = onRemove,
|
||||||
|
)
|
||||||
// FIXME remove after https://issuetracker.google.com/issues/275849044
|
// FIXME remove after https://issuetracker.google.com/issues/275849044
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
}
|
}
|
||||||
|
|
@ -106,7 +108,7 @@ private fun RetrySendMessageMenuBottomSheet(
|
||||||
@Composable
|
@Composable
|
||||||
private fun ColumnScope.RetrySendMenuContents(
|
private fun ColumnScope.RetrySendMenuContents(
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit,
|
||||||
onRemoveFailed: () -> Unit,
|
onRemove: () -> Unit,
|
||||||
sheetState: SheetState = rememberModalBottomSheetState(),
|
sheetState: SheetState = rememberModalBottomSheetState(),
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
@ -142,22 +144,16 @@ private fun ColumnScope.RetrySendMenuContents(
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
sheetState.hide()
|
sheetState.hide()
|
||||||
onRemoveFailed()
|
onRemove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun RetrySendMessageMenuPreview(@PreviewParameter(RetrySendMenuStateProvider::class) state: RetrySendMenuState) = ElementPreview {
|
internal fun RetrySendMessageMenuPreview(@PreviewParameter(RetrySendMenuStateProvider::class) state: RetrySendMenuState) = ElementPreview {
|
||||||
// TODO restore RetrySendMessageMenuBottomSheet once the issue with bottom sheet not being previewable is fixed
|
RetrySendMessageMenu(
|
||||||
Column {
|
state = state,
|
||||||
RetrySendMenuContents(
|
)
|
||||||
onRetry = {},
|
|
||||||
onRemoveFailed = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.components.virtual
|
package io.element.android.features.messages.impl.timeline.components.virtual
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
|
@ -29,20 +28,16 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.messages.impl.R
|
import io.element.android.features.messages.impl.R
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionStateProvider
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineEncryptedHistoryBannerView(
|
fun TimelineEncryptedHistoryBannerView(
|
||||||
sessionState: SessionState,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -61,26 +56,15 @@ fun TimelineEncryptedHistoryBannerView(
|
||||||
tint = ElementTheme.colors.iconInfoPrimary
|
tint = ElementTheme.colors.iconInfoPrimary
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(sessionState.toStringResId()),
|
text = stringResource(R.string.screen_room_encrypted_history_banner),
|
||||||
style = ElementTheme.typography.fontBodyMdMedium,
|
style = ElementTheme.typography.fontBodyMdMedium,
|
||||||
color = ElementTheme.colors.textInfoPrimary
|
color = ElementTheme.colors.textInfoPrimary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@StringRes
|
|
||||||
private fun SessionState.toStringResId(): Int {
|
|
||||||
return when {
|
|
||||||
isSessionVerified.not() -> R.string.screen_room_encrypted_history_banner_unverified
|
|
||||||
isKeyBackupEnabled.not() -> R.string.screen_room_encrypted_history_banner
|
|
||||||
else -> R.string.screen_room_encrypted_history_banner // TODO strings need to be updated
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun EncryptedHistoryBannerViewPreview(
|
internal fun EncryptedHistoryBannerViewPreview() = ElementPreview {
|
||||||
@PreviewParameter(SessionStateProvider::class) sessionState: SessionState,
|
TimelineEncryptedHistoryBannerView()
|
||||||
) = ElementPreview {
|
|
||||||
TimelineEncryptedHistoryBannerView(sessionState = sessionState)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import io.element.android.libraries.core.mimetype.MimeTypes
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
||||||
|
|
@ -67,6 +68,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||||
private val fileExtensionExtractor: FileExtensionExtractor,
|
private val fileExtensionExtractor: FileExtensionExtractor,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
private val htmlConverterProvider: HtmlConverterProvider,
|
private val htmlConverterProvider: HtmlConverterProvider,
|
||||||
|
private val permalinkParser: PermalinkParser,
|
||||||
) {
|
) {
|
||||||
suspend fun create(content: MessageContent, senderDisplayName: String, eventId: EventId?): TimelineItemEventContent {
|
suspend fun create(content: MessageContent, senderDisplayName: String, eventId: EventId?): TimelineItemEventContent {
|
||||||
return when (val messageType = content.type) {
|
return when (val messageType = content.type) {
|
||||||
|
|
@ -74,7 +76,10 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||||
val emoteBody = "* $senderDisplayName ${messageType.body.trimEnd()}"
|
val emoteBody = "* $senderDisplayName ${messageType.body.trimEnd()}"
|
||||||
TimelineItemEmoteContent(
|
TimelineItemEmoteContent(
|
||||||
body = emoteBody,
|
body = emoteBody,
|
||||||
htmlDocument = messageType.formatted?.toHtmlDocument(prefix = "* $senderDisplayName"),
|
htmlDocument = messageType.formatted?.toHtmlDocument(
|
||||||
|
permalinkParser = permalinkParser,
|
||||||
|
prefix = "* $senderDisplayName",
|
||||||
|
),
|
||||||
formattedBody = parseHtml(messageType.formatted, prefix = "* $senderDisplayName") ?: emoteBody.withLinks(),
|
formattedBody = parseHtml(messageType.formatted, prefix = "* $senderDisplayName") ?: emoteBody.withLinks(),
|
||||||
isEdited = content.isEdited,
|
isEdited = content.isEdited,
|
||||||
)
|
)
|
||||||
|
|
@ -197,7 +202,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||||
val body = messageType.body.trimEnd()
|
val body = messageType.body.trimEnd()
|
||||||
TimelineItemNoticeContent(
|
TimelineItemNoticeContent(
|
||||||
body = body,
|
body = body,
|
||||||
htmlDocument = messageType.formatted?.toHtmlDocument(),
|
htmlDocument = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser),
|
||||||
formattedBody = parseHtml(messageType.formatted) ?: body.withLinks(),
|
formattedBody = parseHtml(messageType.formatted) ?: body.withLinks(),
|
||||||
isEdited = content.isEdited,
|
isEdited = content.isEdited,
|
||||||
)
|
)
|
||||||
|
|
@ -206,7 +211,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||||
val body = messageType.body.trimEnd()
|
val body = messageType.body.trimEnd()
|
||||||
TimelineItemTextContent(
|
TimelineItemTextContent(
|
||||||
body = body,
|
body = body,
|
||||||
htmlDocument = messageType.formatted?.toHtmlDocument(),
|
htmlDocument = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser),
|
||||||
formattedBody = parseHtml(messageType.formatted) ?: body.withLinks(),
|
formattedBody = parseHtml(messageType.formatted) ?: body.withLinks(),
|
||||||
isEdited = content.isEdited,
|
isEdited = content.isEdited,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormat
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||||
|
|
@ -43,6 +44,7 @@ class TimelineItemEventFactory @Inject constructor(
|
||||||
private val contentFactory: TimelineItemContentFactory,
|
private val contentFactory: TimelineItemContentFactory,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
|
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
|
||||||
|
private val permalinkParser: PermalinkParser,
|
||||||
) {
|
) {
|
||||||
suspend fun create(
|
suspend fun create(
|
||||||
currentTimelineItem: MatrixTimelineItem.Event,
|
currentTimelineItem: MatrixTimelineItem.Event,
|
||||||
|
|
@ -80,7 +82,7 @@ class TimelineItemEventFactory @Inject constructor(
|
||||||
reactionsState = currentTimelineItem.computeReactionsState(),
|
reactionsState = currentTimelineItem.computeReactionsState(),
|
||||||
readReceiptState = currentTimelineItem.computeReadReceiptState(roomMembers),
|
readReceiptState = currentTimelineItem.computeReadReceiptState(roomMembers),
|
||||||
localSendState = currentTimelineItem.event.localSendState,
|
localSendState = currentTimelineItem.event.localSendState,
|
||||||
inReplyTo = currentTimelineItem.event.inReplyTo()?.map(),
|
inReplyTo = currentTimelineItem.event.inReplyTo()?.map(permalinkParser = permalinkParser),
|
||||||
isThreaded = currentTimelineItem.event.isThreaded(),
|
isThreaded = currentTimelineItem.event.isThreaded(),
|
||||||
debugInfo = currentTimelineItem.event.debugInfo,
|
debugInfo = currentTimelineItem.event.debugInfo,
|
||||||
origin = currentTimelineItem.event.origin,
|
origin = currentTimelineItem.event.origin,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.model
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||||
|
|
@ -34,7 +35,9 @@ data class InReplyToDetails(
|
||||||
val textContent: String?,
|
val textContent: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun InReplyTo.map() = when (this) {
|
fun InReplyTo.map(
|
||||||
|
permalinkParser: PermalinkParser,
|
||||||
|
) = when (this) {
|
||||||
is InReplyTo.Ready -> InReplyToDetails(
|
is InReplyTo.Ready -> InReplyToDetails(
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
senderId = senderId,
|
senderId = senderId,
|
||||||
|
|
@ -44,7 +47,7 @@ fun InReplyTo.map() = when (this) {
|
||||||
textContent = when (content) {
|
textContent = when (content) {
|
||||||
is MessageContent -> {
|
is MessageContent -> {
|
||||||
val messageContent = content as MessageContent
|
val messageContent = content as MessageContent
|
||||||
(messageContent.type as? TextMessageType)?.toPlainText() ?: messageContent.body
|
(messageContent.type as? TextMessageType)?.toPlainText(permalinkParser = permalinkParser) ?: messageContent.body
|
||||||
}
|
}
|
||||||
is StickerContent -> {
|
is StickerContent -> {
|
||||||
val stickerContent = content as StickerContent
|
val stickerContent = content as StickerContent
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.session
|
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|
||||||
|
|
||||||
open class SessionStateProvider : PreviewParameterProvider<SessionState> {
|
|
||||||
override val values: Sequence<SessionState>
|
|
||||||
get() = sequenceOf(
|
|
||||||
aSessionState(isSessionVerified = false, isKeyBackupEnabled = false),
|
|
||||||
aSessionState(isSessionVerified = true, isKeyBackupEnabled = false),
|
|
||||||
aSessionState(isSessionVerified = true, isKeyBackupEnabled = true),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun aSessionState(
|
|
||||||
isSessionVerified: Boolean = false,
|
|
||||||
isKeyBackupEnabled: Boolean = false,
|
|
||||||
) = SessionState(
|
|
||||||
isSessionVerified = isSessionVerified,
|
|
||||||
isKeyBackupEnabled = isKeyBackupEnabled,
|
|
||||||
)
|
|
||||||
|
|
@ -35,6 +35,7 @@ internal fun MessagesViewWithTypingPreview(
|
||||||
onEventClicked = { false },
|
onEventClicked = { false },
|
||||||
onPreviewAttachments = {},
|
onPreviewAttachments = {},
|
||||||
onUserDataClicked = {},
|
onUserDataClicked = {},
|
||||||
|
onLinkClicked = {},
|
||||||
onSendLocationClicked = {},
|
onSendLocationClicked = {},
|
||||||
onCreatePollClicked = {},
|
onCreatePollClicked = {},
|
||||||
onJoinCallClicked = {},
|
onJoinCallClicked = {},
|
||||||
|
|
|
||||||
|
|
@ -5,24 +5,24 @@
|
||||||
<string name="emoji_picker_category_foods">"Ежа & Напоі"</string>
|
<string name="emoji_picker_category_foods">"Ежа & Напоі"</string>
|
||||||
<string name="emoji_picker_category_nature">"Жывёлы & Прырода"</string>
|
<string name="emoji_picker_category_nature">"Жывёлы & Прырода"</string>
|
||||||
<string name="emoji_picker_category_objects">"Аб\'екты"</string>
|
<string name="emoji_picker_category_objects">"Аб\'екты"</string>
|
||||||
<string name="emoji_picker_category_people">"Усмешкі & Людзі"</string>
|
<string name="emoji_picker_category_people">"Усмешкі & Удзельнікі"</string>
|
||||||
<string name="emoji_picker_category_places">"Падарожжы & Месцы"</string>
|
<string name="emoji_picker_category_places">"Падарожжы & Месцы"</string>
|
||||||
<string name="emoji_picker_category_symbols">"Сімвалы"</string>
|
<string name="emoji_picker_category_symbols">"Сімвалы"</string>
|
||||||
<string name="screen_report_content_block_user">"Заблакіраваць карыстальніка"</string>
|
<string name="screen_report_content_block_user">"Заблакіраваць карыстальніка"</string>
|
||||||
<string name="screen_report_content_block_user_hint">"Адзначце, ці жадаеце вы схаваць усе бягучыя і будучыя паведамленні ад гэтага карыстальніка"</string>
|
<string name="screen_report_content_block_user_hint">"Адзначце, ці хочаце вы схаваць усе бягучыя і будучыя паведамленні ад гэтага карыстальніка"</string>
|
||||||
<string name="screen_report_content_explanation">"Гэтае паведамленне будзе перададзена адміністратару вашага хатняга сервера. Яны не змогуць прачытаць зашыфраваныя паведамленні."</string>
|
<string name="screen_report_content_explanation">"Гэтае паведамленне будзе перададзена адміністратару вашага хатняга сервера. Яны не змогуць прачытаць зашыфраваныя паведамленні."</string>
|
||||||
<string name="screen_report_content_hint">"Прычына, па якой вы паскардзіліся на гэты змест"</string>
|
<string name="screen_report_content_hint">"Прычына, па якой вы паскардзіліся на гэты змест"</string>
|
||||||
<string name="screen_room_attachment_source_camera">"Камера"</string>
|
<string name="screen_room_attachment_source_camera">"Камера"</string>
|
||||||
<string name="screen_room_attachment_source_camera_photo">"Зрабіць фота"</string>
|
<string name="screen_room_attachment_source_camera_photo">"Зрабіць фота"</string>
|
||||||
<string name="screen_room_attachment_source_camera_video">"Запісаць відэа"</string>
|
<string name="screen_room_attachment_source_camera_video">"Запісаць відэа"</string>
|
||||||
<string name="screen_room_attachment_source_files">"Далучэнне"</string>
|
<string name="screen_room_attachment_source_files">"Далучэнне"</string>
|
||||||
<string name="screen_room_attachment_source_gallery">"Фота & Відэа Бібліятэка"</string>
|
<string name="screen_room_attachment_source_gallery">"Бібліятэка фота & відэа"</string>
|
||||||
<string name="screen_room_attachment_source_location">"Месцазнаходжанне"</string>
|
<string name="screen_room_attachment_source_location">"Месцазнаходжанне"</string>
|
||||||
<string name="screen_room_attachment_source_poll">"Апытанне"</string>
|
<string name="screen_room_attachment_source_poll">"Апытанне"</string>
|
||||||
<string name="screen_room_attachment_text_formatting">"Фармаціраванне тэксту"</string>
|
<string name="screen_room_attachment_text_formatting">"Фармаціраванне тэксту"</string>
|
||||||
<string name="screen_room_encrypted_history_banner">"Гісторыя паведамленняў зараз недаступна."</string>
|
<string name="screen_room_encrypted_history_banner">"Гісторыя паведамленняў зараз недаступна."</string>
|
||||||
<string name="screen_room_encrypted_history_banner_unverified">"Гісторыя паведамленняў у гэтым пакоі недаступная. Праверце гэтую прыладу, каб убачыць гісторыю паведамленняў."</string>
|
<string name="screen_room_encrypted_history_banner_unverified">"Гісторыя паведамленняў у гэтым пакоі недаступная. Праверце гэтую прыладу, каб убачыць гісторыю паведамленняў."</string>
|
||||||
<string name="screen_room_invite_again_alert_message">"Вы жадаеце запрасіць іх назад?"</string>
|
<string name="screen_room_invite_again_alert_message">"Вы хочаце запрасіць іх назад?"</string>
|
||||||
<string name="screen_room_invite_again_alert_title">"Вы адзін у гэтым чаце"</string>
|
<string name="screen_room_invite_again_alert_title">"Вы адзін у гэтым чаце"</string>
|
||||||
<string name="screen_room_mentions_at_room_subtitle">"Апавясціць увесь пакой"</string>
|
<string name="screen_room_mentions_at_room_subtitle">"Апавясціць увесь пакой"</string>
|
||||||
<string name="screen_room_mentions_at_room_title">"Усе"</string>
|
<string name="screen_room_mentions_at_room_title">"Усе"</string>
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
<string name="screen_room_timeline_read_marker_title">"Новы"</string>
|
<string name="screen_room_timeline_read_marker_title">"Новы"</string>
|
||||||
<plurals name="screen_room_timeline_state_changes">
|
<plurals name="screen_room_timeline_state_changes">
|
||||||
<item quantity="one">"%1$d змена ў пакоі"</item>
|
<item quantity="one">"%1$d змена ў пакоі"</item>
|
||||||
<item quantity="few">"%1$d змен у пакоі"</item>
|
<item quantity="few">"%1$d змены ў пакоі"</item>
|
||||||
<item quantity="many">"%1$d змен у пакоі"</item>
|
<item quantity="many">"%1$d змен у пакоі"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="screen_room_typing_many_members">
|
<plurals name="screen_room_typing_many_members">
|
||||||
|
|
|
||||||
|
|
@ -76,11 +76,11 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
||||||
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
|
||||||
import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
||||||
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
|
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
|
||||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||||
|
|
@ -231,6 +231,27 @@ class MessagesPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - handle action copy link`() = runTest {
|
||||||
|
val clipboardHelper = FakeClipboardHelper()
|
||||||
|
val event = aMessageEvent()
|
||||||
|
val matrixRoom = FakeMatrixRoom(
|
||||||
|
permalinkResult = { Result.success("a link") },
|
||||||
|
)
|
||||||
|
val presenter = createMessagesPresenter(
|
||||||
|
clipboardHelper = clipboardHelper,
|
||||||
|
matrixRoom = matrixRoom,
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitFirstItem()
|
||||||
|
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.CopyLink, event))
|
||||||
|
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||||
|
assertThat(clipboardHelper.clipboardContents).isEqualTo("a link")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle action reply`() = runTest {
|
fun `present - handle action reply`() = runTest {
|
||||||
val presenter = createMessagesPresenter()
|
val presenter = createMessagesPresenter()
|
||||||
|
|
@ -725,6 +746,8 @@ class MessagesPresenterTest {
|
||||||
richTextEditorStateFactory = TestRichTextEditorStateFactory(),
|
richTextEditorStateFactory = TestRichTextEditorStateFactory(),
|
||||||
permissionsPresenterFactory = permissionsPresenterFactory,
|
permissionsPresenterFactory = permissionsPresenterFactory,
|
||||||
currentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)),
|
currentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)),
|
||||||
|
permalinkParser = FakePermalinkParser(),
|
||||||
|
permalinkBuilder = FakePermalinkBuilder(),
|
||||||
)
|
)
|
||||||
val voiceMessageComposerPresenter = VoiceMessageComposerPresenter(
|
val voiceMessageComposerPresenter = VoiceMessageComposerPresenter(
|
||||||
this,
|
this,
|
||||||
|
|
@ -741,8 +764,6 @@ class MessagesPresenterTest {
|
||||||
dispatchers = coroutineDispatchers,
|
dispatchers = coroutineDispatchers,
|
||||||
appScope = this,
|
appScope = this,
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
encryptionService = FakeEncryptionService(),
|
|
||||||
verificationService = FakeSessionVerificationService(),
|
|
||||||
redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
||||||
endPollAction = endPollAction,
|
endPollAction = endPollAction,
|
||||||
sendPollResponseAction = FakeSendPollResponseAction(),
|
sendPollResponseAction = FakeSendPollResponseAction(),
|
||||||
|
|
|
||||||
|
|
@ -478,6 +478,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa
|
||||||
onRoomDetailsClicked: () -> Unit = EnsureNeverCalled(),
|
onRoomDetailsClicked: () -> Unit = EnsureNeverCalled(),
|
||||||
onEventClicked: (event: TimelineItem.Event) -> Boolean = EnsureNeverCalledWithParamAndResult(),
|
onEventClicked: (event: TimelineItem.Event) -> Boolean = EnsureNeverCalledWithParamAndResult(),
|
||||||
onUserDataClicked: (UserId) -> Unit = EnsureNeverCalledWithParam(),
|
onUserDataClicked: (UserId) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
|
onLinkClicked: (String) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit = EnsureNeverCalledWithParam(),
|
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onSendLocationClicked: () -> Unit = EnsureNeverCalled(),
|
onSendLocationClicked: () -> Unit = EnsureNeverCalled(),
|
||||||
onCreatePollClicked: () -> Unit = EnsureNeverCalled(),
|
onCreatePollClicked: () -> Unit = EnsureNeverCalled(),
|
||||||
|
|
@ -492,6 +493,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa
|
||||||
onRoomDetailsClicked = onRoomDetailsClicked,
|
onRoomDetailsClicked = onRoomDetailsClicked,
|
||||||
onEventClicked = onEventClicked,
|
onEventClicked = onEventClicked,
|
||||||
onUserDataClicked = onUserDataClicked,
|
onUserDataClicked = onUserDataClicked,
|
||||||
|
onLinkClicked = onLinkClicked,
|
||||||
onPreviewAttachments = onPreviewAttachments,
|
onPreviewAttachments = onPreviewAttachments,
|
||||||
onSendLocationClicked = onSendLocationClicked,
|
onSendLocationClicked = onSendLocationClicked,
|
||||||
onCreatePollClicked = onCreatePollClicked,
|
onCreatePollClicked = onCreatePollClicked,
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,7 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.Copy,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
)
|
)
|
||||||
|
|
@ -193,6 +194,7 @@ class ActionListPresenterTest {
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.Copy,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
)
|
)
|
||||||
|
|
@ -232,6 +234,7 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.Copy,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
|
|
@ -272,6 +275,7 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.Copy,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
|
|
@ -315,6 +319,7 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.Copy,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
|
|
@ -357,6 +362,7 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.Copy,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -396,6 +402,7 @@ class ActionListPresenterTest {
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
|
|
@ -435,6 +442,7 @@ class ActionListPresenterTest {
|
||||||
displayEmojiReactions = false,
|
displayEmojiReactions = false,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.Copy,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -473,6 +481,7 @@ class ActionListPresenterTest {
|
||||||
displayEmojiReactions = false,
|
displayEmojiReactions = false,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.Copy,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -513,6 +522,7 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.Copy,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -595,6 +605,7 @@ class ActionListPresenterTest {
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.Copy,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -632,6 +643,7 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
TimelineItemAction.EndPoll,
|
TimelineItemAction.EndPoll,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -668,6 +680,7 @@ class ActionListPresenterTest {
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.EndPoll,
|
TimelineItemAction.EndPoll,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -703,6 +716,7 @@ class ActionListPresenterTest {
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -738,6 +752,7 @@ class ActionListPresenterTest {
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation
|
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
|
@ -57,6 +58,7 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
|
||||||
fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
|
fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
|
||||||
featureFlagService = FakeFeatureFlagService(),
|
featureFlagService = FakeFeatureFlagService(),
|
||||||
htmlConverterProvider = FakeHtmlConverterProvider(),
|
htmlConverterProvider = FakeHtmlConverterProvider(),
|
||||||
|
permalinkParser = FakePermalinkParser(),
|
||||||
),
|
),
|
||||||
redactedMessageFactory = TimelineItemContentRedactedFactory(),
|
redactedMessageFactory = TimelineItemContentRedactedFactory(),
|
||||||
stickerFactory = TimelineItemContentStickerFactory(
|
stickerFactory = TimelineItemContentStickerFactory(
|
||||||
|
|
@ -73,6 +75,7 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
|
||||||
),
|
),
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(),
|
lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(),
|
||||||
|
permalinkParser = FakePermalinkParser(),
|
||||||
),
|
),
|
||||||
virtualItemFactory = TimelineItemVirtualFactory(
|
virtualItemFactory = TimelineItemVirtualFactory(
|
||||||
daySeparatorFactory = TimelineItemDaySeparatorFactory(
|
daySeparatorFactory = TimelineItemDaySeparatorFactory(
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.Mention
|
import io.element.android.libraries.matrix.api.room.Mention
|
||||||
|
|
@ -60,6 +61,8 @@ import io.element.android.libraries.matrix.test.A_USER_ID_3
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_4
|
import io.element.android.libraries.matrix.test.A_USER_ID_4
|
||||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
||||||
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||||
import io.element.android.libraries.mediapickers.api.PickerProvider
|
import io.element.android.libraries.mediapickers.api.PickerProvider
|
||||||
|
|
@ -805,7 +808,14 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - insertMention`() = runTest {
|
fun `present - insertMention`() = runTest {
|
||||||
val presenter = createPresenter(this)
|
val presenter = createPresenter(
|
||||||
|
coroutineScope = this,
|
||||||
|
permalinkBuilder = FakePermalinkBuilder(
|
||||||
|
result = {
|
||||||
|
Result.success("https://matrix.to/#/${A_USER_ID_2.value}")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -941,6 +951,7 @@ class MessageComposerPresenterTest {
|
||||||
mediaPreProcessor: MediaPreProcessor = this.mediaPreProcessor,
|
mediaPreProcessor: MediaPreProcessor = this.mediaPreProcessor,
|
||||||
snackbarDispatcher: SnackbarDispatcher = this.snackbarDispatcher,
|
snackbarDispatcher: SnackbarDispatcher = this.snackbarDispatcher,
|
||||||
permissionPresenter: PermissionsPresenter = FakePermissionsPresenter(),
|
permissionPresenter: PermissionsPresenter = FakePermissionsPresenter(),
|
||||||
|
permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder()
|
||||||
) = MessageComposerPresenter(
|
) = MessageComposerPresenter(
|
||||||
coroutineScope,
|
coroutineScope,
|
||||||
room,
|
room,
|
||||||
|
|
@ -955,6 +966,8 @@ class MessageComposerPresenterTest {
|
||||||
TestRichTextEditorStateFactory(),
|
TestRichTextEditorStateFactory(),
|
||||||
currentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)),
|
currentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)),
|
||||||
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionPresenter),
|
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionPresenter),
|
||||||
|
permalinkParser = FakePermalinkParser(),
|
||||||
|
permalinkBuilder = permalinkBuilder,
|
||||||
)
|
)
|
||||||
|
|
||||||
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
import androidx.compose.ui.test.junit4.createComposeRule
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
@ -32,7 +33,9 @@ class DefaultHtmlConverterProviderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `calling provide without calling Update first should throw an exception`() {
|
fun `calling provide without calling Update first should throw an exception`() {
|
||||||
val provider = DefaultHtmlConverterProvider()
|
val provider = DefaultHtmlConverterProvider(
|
||||||
|
permalinkParser = FakePermalinkParser(),
|
||||||
|
)
|
||||||
|
|
||||||
val exception = runCatching { provider.provide() }.exceptionOrNull()
|
val exception = runCatching { provider.provide() }.exceptionOrNull()
|
||||||
|
|
||||||
|
|
@ -41,7 +44,9 @@ class DefaultHtmlConverterProviderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `calling provide after calling Update first should return an HtmlConverter`() {
|
fun `calling provide after calling Update first should return an HtmlConverter`() {
|
||||||
val provider = DefaultHtmlConverterProvider()
|
val provider = DefaultHtmlConverterProvider(
|
||||||
|
permalinkParser = FakePermalinkParser(),
|
||||||
|
)
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
CompositionLocalProvider(LocalInspectionMode provides true) {
|
CompositionLocalProvider(LocalInspectionMode provides true) {
|
||||||
provider.Update(currentUserId = A_USER_ID)
|
provider.Update(currentUserId = A_USER_ID)
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.FakeRedactedVoiceMessageManager
|
import io.element.android.features.messages.impl.voicemessages.timeline.FakeRedactedVoiceMessageManager
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.aRedactedMatrixTimeline
|
import io.element.android.features.messages.impl.voicemessages.timeline.aRedactedMatrixTimeline
|
||||||
|
|
@ -47,13 +46,11 @@ import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTime
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||||
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
||||||
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
||||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||||
|
|
@ -89,7 +86,6 @@ class TimelinePresenterTest {
|
||||||
assertThat(initialState.timelineItems).isEmpty()
|
assertThat(initialState.timelineItems).isEmpty()
|
||||||
val loadedNoTimelineState = awaitItem()
|
val loadedNoTimelineState = awaitItem()
|
||||||
assertThat(loadedNoTimelineState.timelineItems).isEmpty()
|
assertThat(loadedNoTimelineState.timelineItems).isEmpty()
|
||||||
assertThat(loadedNoTimelineState.sessionState).isEqualTo(SessionState(isSessionVerified = false, isKeyBackupEnabled = false))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -512,8 +508,6 @@ class TimelinePresenterTest {
|
||||||
dispatchers = testCoroutineDispatchers(),
|
dispatchers = testCoroutineDispatchers(),
|
||||||
appScope = this,
|
appScope = this,
|
||||||
navigator = messagesNavigator,
|
navigator = messagesNavigator,
|
||||||
encryptionService = FakeEncryptionService(),
|
|
||||||
verificationService = FakeSessionVerificationService(),
|
|
||||||
redactedVoiceMessageManager = redactedVoiceMessageManager,
|
redactedVoiceMessageManager = redactedVoiceMessageManager,
|
||||||
endPollAction = endPollAction,
|
endPollAction = endPollAction,
|
||||||
sendPollResponseAction = sendPollResponseAction,
|
sendPollResponseAction = sendPollResponseAction,
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ class TimelineViewTest {
|
||||||
typingNotificationState = aTypingNotificationState(),
|
typingNotificationState = aTypingNotificationState(),
|
||||||
roomName = null,
|
roomName = null,
|
||||||
onUserDataClicked = EnsureNeverCalledWithParam(),
|
onUserDataClicked = EnsureNeverCalledWithParam(),
|
||||||
|
onLinkClicked = EnsureNeverCalledWithParam(),
|
||||||
onMessageClicked = EnsureNeverCalledWithParam(),
|
onMessageClicked = EnsureNeverCalledWithParam(),
|
||||||
onMessageLongClicked = EnsureNeverCalledWithParam(),
|
onMessageLongClicked = EnsureNeverCalledWithParam(),
|
||||||
onTimestampClicked = EnsureNeverCalledWithParam(),
|
onTimestampClicked = EnsureNeverCalledWithParam(),
|
||||||
|
|
@ -72,6 +73,7 @@ class TimelineViewTest {
|
||||||
typingNotificationState = aTypingNotificationState(),
|
typingNotificationState = aTypingNotificationState(),
|
||||||
roomName = null,
|
roomName = null,
|
||||||
onUserDataClicked = EnsureNeverCalledWithParam(),
|
onUserDataClicked = EnsureNeverCalledWithParam(),
|
||||||
|
onLinkClicked = EnsureNeverCalledWithParam(),
|
||||||
onMessageClicked = EnsureNeverCalledWithParam(),
|
onMessageClicked = EnsureNeverCalledWithParam(),
|
||||||
onMessageLongClicked = EnsureNeverCalledWithParam(),
|
onMessageLongClicked = EnsureNeverCalledWithParam(),
|
||||||
onTimestampClicked = EnsureNeverCalledWithParam(),
|
onTimestampClicked = EnsureNeverCalledWithParam(),
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ class RetrySendMenuPresenterTests {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
val selectedEvent = aTimelineItemEvent()
|
val selectedEvent = aTimelineItemEvent()
|
||||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||||
|
|
||||||
assertThat(awaitItem().selectedEvent).isSameInstanceAs(selectedEvent)
|
assertThat(awaitItem().selectedEvent).isSameInstanceAs(selectedEvent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -57,8 +56,9 @@ class RetrySendMenuPresenterTests {
|
||||||
val selectedEvent = aTimelineItemEvent()
|
val selectedEvent = aTimelineItemEvent()
|
||||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
initialState.eventSink(RetrySendMenuEvents.Dismiss)
|
initialState.eventSink(RetrySendMenuEvents.Dismiss)
|
||||||
|
assertThat(room.cancelSendCount).isEqualTo(0)
|
||||||
|
assertThat(room.retrySendMessageCount).isEqualTo(0)
|
||||||
assertThat(awaitItem().selectedEvent).isNull()
|
assertThat(awaitItem().selectedEvent).isNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -72,8 +72,8 @@ class RetrySendMenuPresenterTests {
|
||||||
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
|
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
|
||||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
initialState.eventSink(RetrySendMenuEvents.Retry)
|
||||||
initialState.eventSink(RetrySendMenuEvents.RetrySend)
|
assertThat(room.cancelSendCount).isEqualTo(0)
|
||||||
assertThat(room.retrySendMessageCount).isEqualTo(1)
|
assertThat(room.retrySendMessageCount).isEqualTo(1)
|
||||||
assertThat(awaitItem().selectedEvent).isNull()
|
assertThat(awaitItem().selectedEvent).isNull()
|
||||||
}
|
}
|
||||||
|
|
@ -88,8 +88,8 @@ class RetrySendMenuPresenterTests {
|
||||||
val selectedEvent = aTimelineItemEvent(transactionId = null)
|
val selectedEvent = aTimelineItemEvent(transactionId = null)
|
||||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
initialState.eventSink(RetrySendMenuEvents.Retry)
|
||||||
initialState.eventSink(RetrySendMenuEvents.RetrySend)
|
assertThat(room.cancelSendCount).isEqualTo(0)
|
||||||
assertThat(room.retrySendMessageCount).isEqualTo(0)
|
assertThat(room.retrySendMessageCount).isEqualTo(0)
|
||||||
assertThat(awaitItem().selectedEvent).isNull()
|
assertThat(awaitItem().selectedEvent).isNull()
|
||||||
}
|
}
|
||||||
|
|
@ -105,8 +105,8 @@ class RetrySendMenuPresenterTests {
|
||||||
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
|
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
|
||||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
initialState.eventSink(RetrySendMenuEvents.Retry)
|
||||||
initialState.eventSink(RetrySendMenuEvents.RetrySend)
|
assertThat(room.cancelSendCount).isEqualTo(0)
|
||||||
assertThat(room.retrySendMessageCount).isEqualTo(1)
|
assertThat(room.retrySendMessageCount).isEqualTo(1)
|
||||||
assertThat(awaitItem().selectedEvent).isNull()
|
assertThat(awaitItem().selectedEvent).isNull()
|
||||||
}
|
}
|
||||||
|
|
@ -121,9 +121,9 @@ class RetrySendMenuPresenterTests {
|
||||||
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
|
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
|
||||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
initialState.eventSink(RetrySendMenuEvents.Remove)
|
||||||
initialState.eventSink(RetrySendMenuEvents.RemoveFailed)
|
|
||||||
assertThat(room.cancelSendCount).isEqualTo(1)
|
assertThat(room.cancelSendCount).isEqualTo(1)
|
||||||
|
assertThat(room.retrySendMessageCount).isEqualTo(0)
|
||||||
assertThat(awaitItem().selectedEvent).isNull()
|
assertThat(awaitItem().selectedEvent).isNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -137,9 +137,9 @@ class RetrySendMenuPresenterTests {
|
||||||
val selectedEvent = aTimelineItemEvent(transactionId = null)
|
val selectedEvent = aTimelineItemEvent(transactionId = null)
|
||||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
initialState.eventSink(RetrySendMenuEvents.Remove)
|
||||||
initialState.eventSink(RetrySendMenuEvents.RemoveFailed)
|
|
||||||
assertThat(room.cancelSendCount).isEqualTo(0)
|
assertThat(room.cancelSendCount).isEqualTo(0)
|
||||||
|
assertThat(room.retrySendMessageCount).isEqualTo(0)
|
||||||
assertThat(awaitItem().selectedEvent).isNull()
|
assertThat(awaitItem().selectedEvent).isNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -154,9 +154,9 @@ class RetrySendMenuPresenterTests {
|
||||||
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
|
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
|
||||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
initialState.eventSink(RetrySendMenuEvents.Remove)
|
||||||
initialState.eventSink(RetrySendMenuEvents.RemoveFailed)
|
|
||||||
assertThat(room.cancelSendCount).isEqualTo(1)
|
assertThat(room.cancelSendCount).isEqualTo(1)
|
||||||
|
assertThat(room.retrySendMessageCount).isEqualTo(0)
|
||||||
assertThat(awaitItem().selectedEvent).isNull()
|
assertThat(awaitItem().selectedEvent).isNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.components.retrysendmenu
|
||||||
|
|
||||||
|
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.messages.impl.R
|
||||||
|
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
import io.element.android.tests.testutils.pressBackKey
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class RetrySendMessageMenuTest {
|
||||||
|
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `dismiss the bottom sheet emits the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<RetrySendMenuEvents>()
|
||||||
|
rule.setRetrySendMessageMenu(
|
||||||
|
aRetrySendMenuState(
|
||||||
|
event = aTimelineItemEvent(),
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.pressBackKey()
|
||||||
|
// Cannot test this for now.
|
||||||
|
// eventsRecorder.assertSingle(RetrySendMenuEvents.Dismiss)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Config(qualifiers = "h1024dp")
|
||||||
|
@Test
|
||||||
|
fun `retry to send the event emits the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<RetrySendMenuEvents>()
|
||||||
|
rule.setRetrySendMessageMenu(
|
||||||
|
aRetrySendMenuState(
|
||||||
|
event = aTimelineItemEvent(),
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(R.string.screen_room_retry_send_menu_send_again_action)
|
||||||
|
eventsRecorder.assertSingle(RetrySendMenuEvents.Retry)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Config(qualifiers = "h1024dp")
|
||||||
|
@Test
|
||||||
|
fun `remove the event emits the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<RetrySendMenuEvents>()
|
||||||
|
rule.setRetrySendMessageMenu(
|
||||||
|
aRetrySendMenuState(
|
||||||
|
event = aTimelineItemEvent(),
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_remove)
|
||||||
|
eventsRecorder.assertSingle(RetrySendMenuEvents.Remove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRetrySendMessageMenu(
|
||||||
|
state: RetrySendMenuState,
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
RetrySendMessageMenu(
|
||||||
|
state = state,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -63,6 +63,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH
|
import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH
|
||||||
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation
|
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -664,6 +665,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
|
fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
htmlConverterProvider = FakeHtmlConverterProvider(htmlConverterTransform),
|
htmlConverterProvider = FakeHtmlConverterProvider(htmlConverterTransform),
|
||||||
|
permalinkParser = FakePermalinkParser(),
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun createStickerContent(
|
private fun createStickerContent(
|
||||||
|
|
|
||||||
|
|
@ -26,14 +26,27 @@ import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershi
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class InReplyToDetailTest {
|
class InReplyToDetailTest {
|
||||||
@Test
|
@Test
|
||||||
fun `map - with a not ready InReplyTo does not work`() {
|
fun `map - with a not ready InReplyTo does not work`() {
|
||||||
assertThat(InReplyTo.Pending.map()).isNull()
|
assertThat(
|
||||||
assertThat(InReplyTo.NotLoaded(AN_EVENT_ID).map()).isNull()
|
InReplyTo.Pending.map(
|
||||||
assertThat(InReplyTo.Error.map()).isNull()
|
permalinkParser = FakePermalinkParser()
|
||||||
|
)
|
||||||
|
).isNull()
|
||||||
|
assertThat(
|
||||||
|
InReplyTo.NotLoaded(AN_EVENT_ID).map(
|
||||||
|
permalinkParser = FakePermalinkParser()
|
||||||
|
)
|
||||||
|
).isNull()
|
||||||
|
assertThat(
|
||||||
|
InReplyTo.Error.map(
|
||||||
|
permalinkParser = FakePermalinkParser()
|
||||||
|
)
|
||||||
|
).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -48,7 +61,9 @@ class InReplyToDetailTest {
|
||||||
change = MembershipChange.INVITED,
|
change = MembershipChange.INVITED,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val inReplyToDetails = inReplyTo.map()
|
val inReplyToDetails = inReplyTo.map(
|
||||||
|
permalinkParser = FakePermalinkParser()
|
||||||
|
)
|
||||||
assertThat(inReplyToDetails).isNotNull()
|
assertThat(inReplyToDetails).isNotNull()
|
||||||
assertThat(inReplyToDetails?.textContent).isNull()
|
assertThat(inReplyToDetails?.textContent).isNull()
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +89,11 @@ class InReplyToDetailTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(inReplyTo.map()?.textContent).isEqualTo("Hello!")
|
assertThat(
|
||||||
|
inReplyTo.map(
|
||||||
|
permalinkParser = FakePermalinkParser()
|
||||||
|
)?.textContent
|
||||||
|
).isEqualTo("Hello!")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -95,6 +114,10 @@ class InReplyToDetailTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(inReplyTo.map()?.textContent).isEqualTo("**Hello!**")
|
assertThat(
|
||||||
|
inReplyTo.map(
|
||||||
|
permalinkParser = FakePermalinkParser()
|
||||||
|
)?.textContent
|
||||||
|
).isEqualTo("**Hello!**")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||||
* @property isSelected whether the user has selected this answer.
|
* @property isSelected whether the user has selected this answer.
|
||||||
* @property isEnabled whether the answer can be voted.
|
* @property isEnabled whether the answer can be voted.
|
||||||
* @property isWinner whether this is the winner answer in the poll.
|
* @property isWinner whether this is the winner answer in the poll.
|
||||||
* @property isDisclosed whether the votes for this answer should be disclosed.
|
* @property showVotes whether the votes for this answer should be displayed.
|
||||||
* @property votesCount the number of votes for this answer.
|
* @property votesCount the number of votes for this answer.
|
||||||
* @property percentage the percentage of votes for this answer.
|
* @property percentage the percentage of votes for this answer.
|
||||||
*/
|
*/
|
||||||
|
|
@ -34,7 +34,7 @@ data class PollAnswerItem(
|
||||||
val isSelected: Boolean,
|
val isSelected: Boolean,
|
||||||
val isEnabled: Boolean,
|
val isEnabled: Boolean,
|
||||||
val isWinner: Boolean,
|
val isWinner: Boolean,
|
||||||
val isDisclosed: Boolean,
|
val showVotes: Boolean,
|
||||||
val votesCount: Int,
|
val votesCount: Int,
|
||||||
val percentage: Float,
|
val percentage: Float,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import io.element.android.libraries.designsystem.theme.components.LinearProgress
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor
|
import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor
|
||||||
import io.element.android.libraries.designsystem.toEnabledColor
|
import io.element.android.libraries.designsystem.toEnabledColor
|
||||||
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
import io.element.android.libraries.ui.strings.CommonPlurals
|
import io.element.android.libraries.ui.strings.CommonPlurals
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -79,17 +80,36 @@ internal fun PollAnswerView(
|
||||||
text = answerItem.answer.text,
|
text = answerItem.answer.text,
|
||||||
style = if (answerItem.isWinner) ElementTheme.typography.fontBodyLgMedium else ElementTheme.typography.fontBodyLgRegular,
|
style = if (answerItem.isWinner) ElementTheme.typography.fontBodyLgMedium else ElementTheme.typography.fontBodyLgRegular,
|
||||||
)
|
)
|
||||||
if (answerItem.isDisclosed) {
|
if (answerItem.showVotes) {
|
||||||
Text(
|
val text = pluralStringResource(
|
||||||
modifier = Modifier.align(Alignment.Bottom),
|
id = CommonPlurals.common_poll_votes_count,
|
||||||
text = pluralStringResource(
|
count = answerItem.votesCount,
|
||||||
id = CommonPlurals.common_poll_votes_count,
|
answerItem.votesCount
|
||||||
count = answerItem.votesCount,
|
|
||||||
answerItem.votesCount
|
|
||||||
),
|
|
||||||
style = if (answerItem.isWinner) ElementTheme.typography.fontBodySmMedium else ElementTheme.typography.fontBodySmRegular,
|
|
||||||
color = if (answerItem.isWinner) ElementTheme.colors.textPrimary else ElementTheme.colors.textSecondary,
|
|
||||||
)
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.align(Alignment.Bottom),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
if (answerItem.isWinner) {
|
||||||
|
Icon(
|
||||||
|
resourceId = CommonDrawables.ic_winner,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = ElementTheme.colors.iconAccentTertiary,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(2.dp))
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = ElementTheme.typography.fontBodySmMedium,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
@ -98,7 +118,7 @@ internal fun PollAnswerView(
|
||||||
color = if (answerItem.isWinner) ElementTheme.colors.textSuccessPrimary else answerItem.isEnabled.toEnabledColor(),
|
color = if (answerItem.isWinner) ElementTheme.colors.textSuccessPrimary else answerItem.isEnabled.toEnabledColor(),
|
||||||
progress = {
|
progress = {
|
||||||
when {
|
when {
|
||||||
answerItem.isDisclosed -> answerItem.percentage
|
answerItem.showVotes -> answerItem.percentage
|
||||||
answerItem.isSelected -> 1f
|
answerItem.isSelected -> 1f
|
||||||
else -> 0f
|
else -> 0f
|
||||||
}
|
}
|
||||||
|
|
@ -114,7 +134,7 @@ internal fun PollAnswerView(
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PollAnswerDisclosedNotSelectedPreview() = ElementPreview {
|
internal fun PollAnswerDisclosedNotSelectedPreview() = ElementPreview {
|
||||||
PollAnswerView(
|
PollAnswerView(
|
||||||
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = false),
|
answerItem = aPollAnswerItem(showVotes = true, isSelected = false),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,7 +142,7 @@ internal fun PollAnswerDisclosedNotSelectedPreview() = ElementPreview {
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PollAnswerDisclosedSelectedPreview() = ElementPreview {
|
internal fun PollAnswerDisclosedSelectedPreview() = ElementPreview {
|
||||||
PollAnswerView(
|
PollAnswerView(
|
||||||
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true),
|
answerItem = aPollAnswerItem(showVotes = true, isSelected = true),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,7 +150,7 @@ internal fun PollAnswerDisclosedSelectedPreview() = ElementPreview {
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PollAnswerUndisclosedNotSelectedPreview() = ElementPreview {
|
internal fun PollAnswerUndisclosedNotSelectedPreview() = ElementPreview {
|
||||||
PollAnswerView(
|
PollAnswerView(
|
||||||
answerItem = aPollAnswerItem(isDisclosed = false, isSelected = false),
|
answerItem = aPollAnswerItem(showVotes = false, isSelected = false),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,7 +158,7 @@ internal fun PollAnswerUndisclosedNotSelectedPreview() = ElementPreview {
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PollAnswerUndisclosedSelectedPreview() = ElementPreview {
|
internal fun PollAnswerUndisclosedSelectedPreview() = ElementPreview {
|
||||||
PollAnswerView(
|
PollAnswerView(
|
||||||
answerItem = aPollAnswerItem(isDisclosed = false, isSelected = true),
|
answerItem = aPollAnswerItem(showVotes = false, isSelected = true),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,7 +166,7 @@ internal fun PollAnswerUndisclosedSelectedPreview() = ElementPreview {
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PollAnswerEndedWinnerNotSelectedPreview() = ElementPreview {
|
internal fun PollAnswerEndedWinnerNotSelectedPreview() = ElementPreview {
|
||||||
PollAnswerView(
|
PollAnswerView(
|
||||||
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = false, isEnabled = false, isWinner = true),
|
answerItem = aPollAnswerItem(showVotes = true, isSelected = false, isEnabled = false, isWinner = true),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,7 +174,7 @@ internal fun PollAnswerEndedWinnerNotSelectedPreview() = ElementPreview {
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PollAnswerEndedWinnerSelectedPreview() = ElementPreview {
|
internal fun PollAnswerEndedWinnerSelectedPreview() = ElementPreview {
|
||||||
PollAnswerView(
|
PollAnswerView(
|
||||||
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true, isEnabled = false, isWinner = true),
|
answerItem = aPollAnswerItem(showVotes = true, isSelected = true, isEnabled = false, isWinner = true),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,6 +182,6 @@ internal fun PollAnswerEndedWinnerSelectedPreview() = ElementPreview {
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PollAnswerEndedSelectedPreview() = ElementPreview {
|
internal fun PollAnswerEndedSelectedPreview() = ElementPreview {
|
||||||
PollAnswerView(
|
PollAnswerView(
|
||||||
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true, isEnabled = false, isWinner = false),
|
answerItem = aPollAnswerItem(showVotes = true, isSelected = true, isEnabled = false, isWinner = false),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,11 @@ fun aPollQuestion() = "What type of food should we have at the party?"
|
||||||
fun aPollAnswerItemList(
|
fun aPollAnswerItemList(
|
||||||
hasVotes: Boolean = true,
|
hasVotes: Boolean = true,
|
||||||
isEnded: Boolean = false,
|
isEnded: Boolean = false,
|
||||||
isDisclosed: Boolean = true,
|
showVotes: Boolean = true,
|
||||||
) = persistentListOf(
|
) = persistentListOf(
|
||||||
aPollAnswerItem(
|
aPollAnswerItem(
|
||||||
answer = PollAnswer("option_1", "Italian \uD83C\uDDEE\uD83C\uDDF9"),
|
answer = PollAnswer("option_1", "Italian \uD83C\uDDEE\uD83C\uDDF9"),
|
||||||
isDisclosed = isDisclosed,
|
showVotes = showVotes,
|
||||||
isEnabled = !isEnded,
|
isEnabled = !isEnded,
|
||||||
isWinner = isEnded,
|
isWinner = isEnded,
|
||||||
votesCount = if (hasVotes) 5 else 0,
|
votesCount = if (hasVotes) 5 else 0,
|
||||||
|
|
@ -39,7 +39,7 @@ fun aPollAnswerItemList(
|
||||||
),
|
),
|
||||||
aPollAnswerItem(
|
aPollAnswerItem(
|
||||||
answer = PollAnswer("option_2", "Chinese \uD83C\uDDE8\uD83C\uDDF3"),
|
answer = PollAnswer("option_2", "Chinese \uD83C\uDDE8\uD83C\uDDF3"),
|
||||||
isDisclosed = isDisclosed,
|
showVotes = showVotes,
|
||||||
isEnabled = !isEnded,
|
isEnabled = !isEnded,
|
||||||
isWinner = false,
|
isWinner = false,
|
||||||
votesCount = 0,
|
votesCount = 0,
|
||||||
|
|
@ -47,7 +47,7 @@ fun aPollAnswerItemList(
|
||||||
),
|
),
|
||||||
aPollAnswerItem(
|
aPollAnswerItem(
|
||||||
answer = PollAnswer("option_3", "Brazilian \uD83C\uDDE7\uD83C\uDDF7"),
|
answer = PollAnswer("option_3", "Brazilian \uD83C\uDDE7\uD83C\uDDF7"),
|
||||||
isDisclosed = isDisclosed,
|
showVotes = showVotes,
|
||||||
isEnabled = !isEnded,
|
isEnabled = !isEnded,
|
||||||
isWinner = false,
|
isWinner = false,
|
||||||
isSelected = true,
|
isSelected = true,
|
||||||
|
|
@ -55,7 +55,7 @@ fun aPollAnswerItemList(
|
||||||
percentage = if (hasVotes) 0.1f else 0f
|
percentage = if (hasVotes) 0.1f else 0f
|
||||||
),
|
),
|
||||||
aPollAnswerItem(
|
aPollAnswerItem(
|
||||||
isDisclosed = isDisclosed,
|
showVotes = showVotes,
|
||||||
isEnabled = !isEnded,
|
isEnabled = !isEnded,
|
||||||
votesCount = if (hasVotes) 4 else 0,
|
votesCount = if (hasVotes) 4 else 0,
|
||||||
percentage = if (hasVotes) 0.4f else 0f,
|
percentage = if (hasVotes) 0.4f else 0f,
|
||||||
|
|
@ -70,7 +70,7 @@ fun aPollAnswerItem(
|
||||||
isSelected: Boolean = false,
|
isSelected: Boolean = false,
|
||||||
isEnabled: Boolean = true,
|
isEnabled: Boolean = true,
|
||||||
isWinner: Boolean = false,
|
isWinner: Boolean = false,
|
||||||
isDisclosed: Boolean = true,
|
showVotes: Boolean = true,
|
||||||
votesCount: Int = 4,
|
votesCount: Int = 4,
|
||||||
percentage: Float = 0.4f,
|
percentage: Float = 0.4f,
|
||||||
) = PollAnswerItem(
|
) = PollAnswerItem(
|
||||||
|
|
@ -78,7 +78,7 @@ fun aPollAnswerItem(
|
||||||
isSelected = isSelected,
|
isSelected = isSelected,
|
||||||
isEnabled = isEnabled,
|
isEnabled = isEnabled,
|
||||||
isWinner = isWinner,
|
isWinner = isWinner,
|
||||||
isDisclosed = isDisclosed,
|
showVotes = showVotes,
|
||||||
votesCount = votesCount,
|
votesCount = votesCount,
|
||||||
percentage = percentage
|
percentage = percentage
|
||||||
)
|
)
|
||||||
|
|
@ -87,14 +87,14 @@ fun aPollContentState(
|
||||||
eventId: EventId? = null,
|
eventId: EventId? = null,
|
||||||
isMine: Boolean = false,
|
isMine: Boolean = false,
|
||||||
isEnded: Boolean = false,
|
isEnded: Boolean = false,
|
||||||
isDisclosed: Boolean = true,
|
showVotes: Boolean = true,
|
||||||
isPollEditable: Boolean = true,
|
isPollEditable: Boolean = true,
|
||||||
hasVotes: Boolean = true,
|
hasVotes: Boolean = true,
|
||||||
question: String = aPollQuestion(),
|
question: String = aPollQuestion(),
|
||||||
pollKind: PollKind = PollKind.Disclosed,
|
pollKind: PollKind = PollKind.Disclosed,
|
||||||
answerItems: ImmutableList<PollAnswerItem> = aPollAnswerItemList(
|
answerItems: ImmutableList<PollAnswerItem> = aPollAnswerItemList(
|
||||||
isEnded = isEnded,
|
isEnded = isEnded,
|
||||||
isDisclosed = isDisclosed,
|
showVotes = showVotes,
|
||||||
hasVotes = hasVotes
|
hasVotes = hasVotes
|
||||||
),
|
),
|
||||||
) = PollContentState(
|
) = PollContentState(
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,7 @@ internal fun PollContentUndisclosedPreview() = ElementPreview {
|
||||||
PollContentView(
|
PollContentView(
|
||||||
eventId = EventId("\$anEventId"),
|
eventId = EventId("\$anEventId"),
|
||||||
question = "What type of food should we have at the party?",
|
question = "What type of food should we have at the party?",
|
||||||
answerItems = aPollAnswerItemList(isDisclosed = false),
|
answerItems = aPollAnswerItemList(showVotes = false),
|
||||||
pollKind = PollKind.Undisclosed,
|
pollKind = PollKind.Undisclosed,
|
||||||
isPollEnded = false,
|
isPollEnded = false,
|
||||||
isPollEditable = false,
|
isPollEditable = false,
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue