Merge develop into feature/fga/permalink_timeline
This commit is contained in:
commit
6808edda86
398 changed files with 2667 additions and 2461 deletions
2
.github/workflows/danger.yml
vendored
2
.github/workflows/danger.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
||||||
- run: |
|
- run: |
|
||||||
npm install --save-dev @babel/plugin-transform-flow-strip-types
|
npm install --save-dev @babel/plugin-transform-flow-strip-types
|
||||||
- name: Danger
|
- name: Danger
|
||||||
uses: danger/danger-js@11.3.1
|
uses: danger/danger-js@12.1.0
|
||||||
with:
|
with:
|
||||||
args: "--dangerfile ./tools/danger/dangerfile.js"
|
args: "--dangerfile ./tools/danger/dangerfile.js"
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
2
.github/workflows/quality.yml
vendored
2
.github/workflows/quality.yml
vendored
|
|
@ -72,7 +72,7 @@ jobs:
|
||||||
yarn add danger-plugin-lint-report --dev
|
yarn add danger-plugin-lint-report --dev
|
||||||
- name: Danger lint
|
- name: Danger lint
|
||||||
if: always()
|
if: always()
|
||||||
uses: danger/danger-js@11.3.1
|
uses: danger/danger-js@12.1.0
|
||||||
with:
|
with:
|
||||||
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
|
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
17
CHANGES.md
17
CHANGES.md
|
|
@ -1,3 +1,20 @@
|
||||||
|
Changes in Element X v0.4.10 (2024-04-17)
|
||||||
|
=========================================
|
||||||
|
|
||||||
|
Matrix Rust SDK 0.2.14
|
||||||
|
|
||||||
|
Features ✨
|
||||||
|
----------
|
||||||
|
- Rework room navigation to handle unknown room and prepare work on permalink. ([#2695](https://github.com/element-hq/element-x-android/issues/2695))
|
||||||
|
|
||||||
|
Other changes
|
||||||
|
-------------
|
||||||
|
- Encrypt new session data with a passphrase ([#2703](https://github.com/element-hq/element-x-android/issues/2703))
|
||||||
|
- Use sdk API to build permalinks ([#2708](https://github.com/element-hq/element-x-android/issues/2708))
|
||||||
|
- Parse permalink using parseMatrixEntityFrom from the SDK ([#2709](https://github.com/element-hq/element-x-android/issues/2709))
|
||||||
|
- Fix compile for forks that use the `noop` analytics module ([#2698](https://github.com/element-hq/element-x-android/issues/2698))
|
||||||
|
|
||||||
|
|
||||||
Changes in Element X v0.4.9 (2024-04-12)
|
Changes in Element X v0.4.9 (2024-04-12)
|
||||||
========================================
|
========================================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,4 @@ class IntentProviderImpl @Inject constructor(
|
||||||
data = deepLinkCreator.room(sessionId, roomId, threadId).toUri()
|
data = deepLinkCreator.room(sessionId, roomId, threadId).toUri()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getInviteListIntent(sessionId: SessionId): Intent {
|
|
||||||
return Intent(context, MainActivity::class.java).apply {
|
|
||||||
action = Intent.ACTION_VIEW
|
|
||||||
data = deepLinkCreator.inviteList(sessionId).toUri()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,16 +67,6 @@ class IntentProviderImplTest {
|
||||||
assertThat(result.data.toString()).isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId")
|
assertThat(result.data.toString()).isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test getInviteListIntent`() {
|
|
||||||
val sut = createIntentProviderImpl()
|
|
||||||
val result = sut.getInviteListIntent(
|
|
||||||
sessionId = A_SESSION_ID,
|
|
||||||
)
|
|
||||||
result.commonAssertions()
|
|
||||||
assertThat(result.data.toString()).isEqualTo("elementx://open/@alice:server.org/invites")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createIntentProviderImpl(): IntentProviderImpl {
|
private fun createIntentProviderImpl(): IntentProviderImpl {
|
||||||
return IntentProviderImpl(
|
return IntentProviderImpl(
|
||||||
context = RuntimeEnvironment.getApplication() as Context,
|
context = RuntimeEnvironment.getApplication() as Context,
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.core.plugin.plugins
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.singleTop
|
import com.bumble.appyx.navmodel.backstack.operation.singleTop
|
||||||
|
|
@ -48,7 +47,6 @@ 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.FtueService
|
||||||
import io.element.android.features.ftue.api.state.FtueState
|
import io.element.android.features.ftue.api.state.FtueState
|
||||||
import io.element.android.features.invite.api.InviteListEntryPoint
|
|
||||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||||
import io.element.android.features.lockscreen.api.LockScreenLockState
|
import io.element.android.features.lockscreen.api.LockScreenLockState
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
|
|
@ -62,17 +60,16 @@ import io.element.android.features.securebackup.api.SecureBackupEntryPoint
|
||||||
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.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.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
|
||||||
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.roomlist.RoomList
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
|
@ -81,7 +78,6 @@ import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
|
@ -95,11 +91,9 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
||||||
private val appNavigationStateService: AppNavigationStateService,
|
private val appNavigationStateService: AppNavigationStateService,
|
||||||
private val secureBackupEntryPoint: SecureBackupEntryPoint,
|
private val secureBackupEntryPoint: SecureBackupEntryPoint,
|
||||||
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 ftueService: FtueService,
|
private val ftueService: FtueService,
|
||||||
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
||||||
private val lockScreenStateService: LockScreenService,
|
private val lockScreenStateService: LockScreenService,
|
||||||
|
|
@ -160,23 +154,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
observeSyncStateAndNetworkStatus()
|
observeSyncStateAndNetworkStatus()
|
||||||
observeInvitesLoadingState()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun observeInvitesLoadingState() {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
||||||
matrixClient.roomListService.invites.loadingState
|
|
||||||
.collect { inviteState ->
|
|
||||||
when (inviteState) {
|
|
||||||
is RoomList.LoadingState.Loaded -> if (inviteState.numberOfRooms == 0) {
|
|
||||||
backstack.removeLast(NavTarget.InviteList)
|
|
||||||
}
|
|
||||||
RoomList.LoadingState.NotLoaded -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
|
|
@ -215,9 +192,9 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Room(
|
data class Room(
|
||||||
val roomId: RoomId,
|
val roomIdOrAlias: RoomIdOrAlias,
|
||||||
val roomDescription: RoomDescription? = null,
|
val roomDescription: RoomDescription? = null,
|
||||||
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages
|
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages()
|
||||||
) : NavTarget
|
) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|
@ -233,9 +210,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
val initialElement: SecureBackupEntryPoint.InitialTarget = SecureBackupEntryPoint.InitialTarget.Root
|
val initialElement: SecureBackupEntryPoint.InitialTarget = SecureBackupEntryPoint.InitialTarget.Root
|
||||||
) : NavTarget
|
) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object InviteList : NavTarget
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object Ftue : NavTarget
|
data object Ftue : NavTarget
|
||||||
|
|
||||||
|
|
@ -257,7 +231,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
NavTarget.RoomList -> {
|
NavTarget.RoomList -> {
|
||||||
val callback = object : RoomListEntryPoint.Callback {
|
val callback = object : RoomListEntryPoint.Callback {
|
||||||
override fun onRoomClicked(roomId: RoomId) {
|
override fun onRoomClicked(roomId: RoomId) {
|
||||||
backstack.push(NavTarget.Room(roomId))
|
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSettingsClicked() {
|
override fun onSettingsClicked() {
|
||||||
|
|
@ -272,12 +246,8 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey))
|
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onInvitesClicked() {
|
|
||||||
backstack.push(NavTarget.InviteList)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRoomSettingsClicked(roomId: RoomId) {
|
override fun onRoomSettingsClicked(roomId: RoomId) {
|
||||||
backstack.push(NavTarget.Room(roomId, initialElement = RoomNavigationTarget.Details))
|
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Details))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReportBugClicked() {
|
override fun onReportBugClicked() {
|
||||||
|
|
@ -296,19 +266,41 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
is NavTarget.Room -> {
|
is NavTarget.Room -> {
|
||||||
val callback = object : JoinedRoomLoadedFlowNode.Callback {
|
val callback = object : JoinedRoomLoadedFlowNode.Callback {
|
||||||
override fun onOpenRoom(roomId: RoomId) {
|
override fun onOpenRoom(roomId: RoomId) {
|
||||||
backstack.push(NavTarget.Room(roomId))
|
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onForwardedToSingleRoom(roomId: RoomId) {
|
override fun onForwardedToSingleRoom(roomId: RoomId) {
|
||||||
coroutineScope.launch { attachRoom(roomId) }
|
coroutineScope.launch { attachRoom(roomId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPermalinkClicked(data: PermalinkData) {
|
||||||
|
when (data) {
|
||||||
|
is PermalinkData.UserLink -> {
|
||||||
|
// FIXME Add a user profile screen.
|
||||||
|
Timber.e("User link clicked: ${data.userId}. TODO Add a user profile screen")
|
||||||
|
}
|
||||||
|
is PermalinkData.RoomLink -> {
|
||||||
|
backstack.push(
|
||||||
|
NavTarget.Room(
|
||||||
|
data.roomIdOrAlias,
|
||||||
|
initialElement = RoomNavigationTarget.Messages(data.eventId),
|
||||||
|
// TODO Use the viaParameters
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is PermalinkData.FallbackLink,
|
||||||
|
is PermalinkData.RoomEmailInviteLink -> {
|
||||||
|
// Should not happen (handled by MessagesNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onOpenGlobalNotificationSettings() {
|
override fun onOpenGlobalNotificationSettings() {
|
||||||
backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.NotificationSettings))
|
backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.NotificationSettings))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val inputs = RoomFlowNode.Inputs(
|
val inputs = RoomFlowNode.Inputs(
|
||||||
roomId = navTarget.roomId,
|
roomIdOrAlias = navTarget.roomIdOrAlias,
|
||||||
roomDescription = Optional.ofNullable(navTarget.roomDescription),
|
roomDescription = Optional.ofNullable(navTarget.roomDescription),
|
||||||
initialElement = navTarget.initialElement
|
initialElement = navTarget.initialElement
|
||||||
)
|
)
|
||||||
|
|
@ -325,7 +317,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenRoomNotificationSettings(roomId: RoomId) {
|
override fun onOpenRoomNotificationSettings(roomId: RoomId) {
|
||||||
backstack.push(NavTarget.Room(roomId, initialElement = RoomNavigationTarget.NotificationSettings))
|
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.NotificationSettings))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
|
val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
|
||||||
|
|
@ -337,7 +329,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
NavTarget.CreateRoom -> {
|
NavTarget.CreateRoom -> {
|
||||||
val callback = object : CreateRoomEntryPoint.Callback {
|
val callback = object : CreateRoomEntryPoint.Callback {
|
||||||
override fun onSuccess(roomId: RoomId) {
|
override fun onSuccess(roomId: RoomId) {
|
||||||
backstack.replace(NavTarget.Room(roomId))
|
backstack.replace(NavTarget.Room(roomId.toRoomIdOrAlias()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -351,25 +343,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
.params(SecureBackupEntryPoint.Params(initialElement = navTarget.initialElement))
|
.params(SecureBackupEntryPoint.Params(initialElement = navTarget.initialElement))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
NavTarget.InviteList -> {
|
|
||||||
val callback = object : InviteListEntryPoint.Callback {
|
|
||||||
override fun onBackClicked() {
|
|
||||||
backstack.pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInviteClicked(roomId: RoomId) {
|
|
||||||
backstack.push(NavTarget.Room(roomId))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInviteAccepted(roomId: RoomId) {
|
|
||||||
backstack.push(NavTarget.Room(roomId))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inviteListEntryPoint.nodeBuilder(this, buildContext)
|
|
||||||
.callback(callback)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
NavTarget.Ftue -> {
|
NavTarget.Ftue -> {
|
||||||
ftueEntryPoint.nodeBuilder(this, buildContext)
|
ftueEntryPoint.nodeBuilder(this, buildContext)
|
||||||
.callback(object : FtueEntryPoint.Callback {
|
.callback(object : FtueEntryPoint.Callback {
|
||||||
|
|
@ -383,11 +356,11 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
roomDirectoryEntryPoint.nodeBuilder(this, buildContext)
|
roomDirectoryEntryPoint.nodeBuilder(this, buildContext)
|
||||||
.callback(object : RoomDirectoryEntryPoint.Callback {
|
.callback(object : RoomDirectoryEntryPoint.Callback {
|
||||||
override fun onRoomJoined(roomId: RoomId) {
|
override fun onRoomJoined(roomId: RoomId) {
|
||||||
backstack.push(NavTarget.Room(roomId))
|
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResultClicked(roomDescription: RoomDescription) {
|
override fun onResultClicked(roomDescription: RoomDescription) {
|
||||||
backstack.push(NavTarget.Room(roomDescription.roomId, roomDescription))
|
backstack.push(NavTarget.Room(roomDescription.roomId.toRoomIdOrAlias(), roomDescription))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
|
|
@ -406,17 +379,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
if (!canShowRoomList()) return
|
if (!canShowRoomList()) return
|
||||||
attachChild<RoomFlowNode> {
|
attachChild<RoomFlowNode> {
|
||||||
backstack.singleTop(NavTarget.RoomList)
|
backstack.singleTop(NavTarget.RoomList)
|
||||||
backstack.push(NavTarget.Room(roomId))
|
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal suspend fun attachInviteList(deeplinkData: DeeplinkData.InviteList) = withContext(lifecycleScope.coroutineContext) {
|
|
||||||
if (!canShowRoomList()) return@withContext
|
|
||||||
notificationDrawerManager.clearMembershipNotificationForSession(deeplinkData.sessionId)
|
|
||||||
backstack.singleTop(NavTarget.RoomList)
|
|
||||||
backstack.push(NavTarget.InviteList)
|
|
||||||
waitForChildAttached<Node, NavTarget> { navTarget ->
|
|
||||||
navTarget is NavTarget.InviteList
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -290,7 +290,6 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
when (deeplinkData) {
|
when (deeplinkData) {
|
||||||
is DeeplinkData.Root -> attachRoomList()
|
is DeeplinkData.Root -> attachRoomList()
|
||||||
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId)
|
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId)
|
||||||
is DeeplinkData.InviteList -> attachInviteList(deeplinkData)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.appnav.room.joined.JoinedRoomFlowNode
|
import io.element.android.appnav.room.joined.JoinedRoomFlowNode
|
||||||
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
||||||
import io.element.android.features.joinroom.api.JoinRoomEntryPoint
|
import io.element.android.features.joinroom.api.JoinRoomEntryPoint
|
||||||
|
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint
|
||||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
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
|
||||||
|
|
@ -46,12 +47,15 @@ import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
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.RoomAlias
|
||||||
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.RoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
|
@ -64,6 +68,7 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
private val roomMembershipObserver: RoomMembershipObserver,
|
private val roomMembershipObserver: RoomMembershipObserver,
|
||||||
private val joinRoomEntryPoint: JoinRoomEntryPoint,
|
private val joinRoomEntryPoint: JoinRoomEntryPoint,
|
||||||
|
private val roomAliasResolverEntryPoint: RoomAliasResolverEntryPoint,
|
||||||
) : BaseFlowNode<RoomFlowNode.NavTarget>(
|
) : BaseFlowNode<RoomFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Loading,
|
initialElement = NavTarget.Loading,
|
||||||
|
|
@ -73,9 +78,9 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
plugins = plugins
|
plugins = plugins
|
||||||
) {
|
) {
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
val roomId: RoomId,
|
val roomIdOrAlias: RoomIdOrAlias,
|
||||||
val roomDescription: Optional<RoomDescription>,
|
val roomDescription: Optional<RoomDescription>,
|
||||||
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages,
|
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages(),
|
||||||
) : NodeInputs
|
) : NodeInputs
|
||||||
|
|
||||||
private val inputs: Inputs = inputs()
|
private val inputs: Inputs = inputs()
|
||||||
|
|
@ -85,29 +90,51 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
data object Loading : NavTarget
|
data object Loading : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object JoinRoom : NavTarget
|
data class Resolving(val roomAlias: RoomAlias) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object JoinedRoom : NavTarget
|
data class JoinRoom(val roomId: RoomId) : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class JoinedRoom(val roomId: RoomId) : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
client.getRoomInfoFlow(
|
resolveRoomId()
|
||||||
inputs.roomId
|
}
|
||||||
).onEach { roomInfo ->
|
|
||||||
Timber.d("Room membership: ${roomInfo.map { it.currentUserMembership }}")
|
private fun resolveRoomId() {
|
||||||
if (roomInfo.getOrNull()?.currentUserMembership == CurrentUserMembership.JOINED) {
|
lifecycleScope.launch {
|
||||||
backstack.newRoot(NavTarget.JoinedRoom)
|
when (val i = inputs.roomIdOrAlias) {
|
||||||
} else {
|
is RoomIdOrAlias.Alias -> {
|
||||||
backstack.newRoot(NavTarget.JoinRoom)
|
backstack.newRoot(NavTarget.Resolving(i.roomAlias))
|
||||||
|
}
|
||||||
|
is RoomIdOrAlias.Id -> {
|
||||||
|
subscribeToRoomInfoFlow(i.roomId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun subscribeToRoomInfoFlow(roomId: RoomId) {
|
||||||
|
client.getRoomInfoFlow(
|
||||||
|
roomId = roomId
|
||||||
|
)
|
||||||
|
.onEach { roomInfo ->
|
||||||
|
Timber.d("Room membership: ${roomInfo.map { it.currentUserMembership }}")
|
||||||
|
val info = roomInfo.getOrNull()
|
||||||
|
if (info?.currentUserMembership == CurrentUserMembership.JOINED) {
|
||||||
|
backstack.newRoot(NavTarget.JoinedRoom(roomId))
|
||||||
|
} else {
|
||||||
|
backstack.newRoot(NavTarget.JoinRoom(roomId))
|
||||||
|
}
|
||||||
|
}
|
||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
|
|
||||||
// When leaving the room from this session only, navigate up.
|
// When leaving the room from this session only, navigate up.
|
||||||
roomMembershipObserver.updates
|
roomMembershipObserver.updates
|
||||||
.filter { update -> update.roomId == inputs.roomId && !update.isUserInRoom }
|
.filter { update -> update.roomId == roomId && !update.isUserInRoom }
|
||||||
.onEach {
|
.onEach {
|
||||||
navigateUp()
|
navigateUp()
|
||||||
}
|
}
|
||||||
|
|
@ -116,14 +143,30 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
NavTarget.Loading -> loadingNode(buildContext)
|
is NavTarget.Loading -> loadingNode(buildContext)
|
||||||
NavTarget.JoinRoom -> {
|
is NavTarget.Resolving -> {
|
||||||
val inputs = JoinRoomEntryPoint.Inputs(inputs.roomId, roomDescription = inputs.roomDescription)
|
val callback = object : RoomAliasResolverEntryPoint.Callback {
|
||||||
|
override fun onAliasResolved(roomId: RoomId) {
|
||||||
|
subscribeToRoomInfoFlow(roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val params = RoomAliasResolverEntryPoint.Params(navTarget.roomAlias)
|
||||||
|
roomAliasResolverEntryPoint.nodeBuilder(this, buildContext)
|
||||||
|
.callback(callback)
|
||||||
|
.params(params)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
is NavTarget.JoinRoom -> {
|
||||||
|
val inputs = JoinRoomEntryPoint.Inputs(
|
||||||
|
roomId = navTarget.roomId,
|
||||||
|
roomIdOrAlias = inputs.roomIdOrAlias,
|
||||||
|
roomDescription = inputs.roomDescription,
|
||||||
|
)
|
||||||
joinRoomEntryPoint.createNode(this, buildContext, inputs)
|
joinRoomEntryPoint.createNode(this, buildContext, inputs)
|
||||||
}
|
}
|
||||||
NavTarget.JoinedRoom -> {
|
is NavTarget.JoinedRoom -> {
|
||||||
val roomFlowNodeCallback = plugins<JoinedRoomLoadedFlowNode.Callback>()
|
val roomFlowNodeCallback = plugins<JoinedRoomLoadedFlowNode.Callback>()
|
||||||
val inputs = JoinedRoomFlowNode.Inputs(inputs.roomId, initialElement = inputs.initialElement)
|
val inputs = JoinedRoomFlowNode.Inputs(navTarget.roomId, initialElement = inputs.initialElement)
|
||||||
createNode<JoinedRoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
|
createNode<JoinedRoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,17 @@
|
||||||
|
|
||||||
package io.element.android.appnav.room
|
package io.element.android.appnav.room
|
||||||
|
|
||||||
enum class RoomNavigationTarget {
|
import android.os.Parcelable
|
||||||
Messages,
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
Details,
|
import kotlinx.parcelize.Parcelize
|
||||||
NotificationSettings,
|
|
||||||
|
sealed interface RoomNavigationTarget : Parcelable {
|
||||||
|
@Parcelize
|
||||||
|
data class Messages(val focusedEventId: EventId? = null) : RoomNavigationTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object Details : RoomNavigationTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object NotificationSettings : RoomNavigationTarget
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ class JoinedRoomFlowNode @AssistedInject constructor(
|
||||||
) {
|
) {
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages,
|
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages(),
|
||||||
) : NodeInputs
|
) : NodeInputs
|
||||||
|
|
||||||
private val inputs: Inputs = inputs()
|
private val inputs: Inputs = inputs()
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,10 @@ 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.MatrixClient
|
||||||
|
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.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -63,8 +65,8 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||||
roomComponentFactory: RoomComponentFactory,
|
roomComponentFactory: RoomComponentFactory,
|
||||||
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
|
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = when (plugins.filterIsInstance(Inputs::class.java).first().initialElement) {
|
initialElement = when (val input = plugins.filterIsInstance(Inputs::class.java).first().initialElement) {
|
||||||
RoomNavigationTarget.Messages -> NavTarget.Messages
|
is RoomNavigationTarget.Messages -> NavTarget.Messages(input.focusedEventId)
|
||||||
RoomNavigationTarget.Details -> NavTarget.RoomDetails
|
RoomNavigationTarget.Details -> NavTarget.RoomDetails
|
||||||
RoomNavigationTarget.NotificationSettings -> NavTarget.RoomNotificationSettings
|
RoomNavigationTarget.NotificationSettings -> NavTarget.RoomNotificationSettings
|
||||||
},
|
},
|
||||||
|
|
@ -75,13 +77,14 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||||
), DaggerComponentOwner {
|
), DaggerComponentOwner {
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onOpenRoom(roomId: RoomId)
|
fun onOpenRoom(roomId: RoomId)
|
||||||
|
fun onPermalinkClicked(data: PermalinkData)
|
||||||
fun onForwardedToSingleRoom(roomId: RoomId)
|
fun onForwardedToSingleRoom(roomId: RoomId)
|
||||||
fun onOpenGlobalNotificationSettings()
|
fun onOpenGlobalNotificationSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
val room: MatrixRoom,
|
val room: MatrixRoom,
|
||||||
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages,
|
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages(),
|
||||||
) : NodeInputs
|
) : NodeInputs
|
||||||
|
|
||||||
private val inputs: Inputs = inputs()
|
private val inputs: Inputs = inputs()
|
||||||
|
|
@ -139,7 +142,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
NavTarget.Messages -> {
|
is NavTarget.Messages -> {
|
||||||
val callback = object : MessagesEntryPoint.Callback {
|
val callback = object : MessagesEntryPoint.Callback {
|
||||||
override fun onRoomDetailsClicked() {
|
override fun onRoomDetailsClicked() {
|
||||||
backstack.push(NavTarget.RoomDetails)
|
backstack.push(NavTarget.RoomDetails)
|
||||||
|
|
@ -149,11 +152,18 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||||
backstack.push(NavTarget.RoomMemberDetails(userId))
|
backstack.push(NavTarget.RoomMemberDetails(userId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPermalinkClicked(data: PermalinkData) {
|
||||||
|
callbacks.forEach { it.onPermalinkClicked(data) }
|
||||||
|
}
|
||||||
|
|
||||||
override fun onForwardedToSingleRoom(roomId: RoomId) {
|
override fun onForwardedToSingleRoom(roomId: RoomId) {
|
||||||
callbacks.forEach { it.onForwardedToSingleRoom(roomId) }
|
callbacks.forEach { it.onForwardedToSingleRoom(roomId) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
messagesEntryPoint.createNode(this, buildContext, callback)
|
messagesEntryPoint.nodeBuilder(this, buildContext)
|
||||||
|
.params(MessagesEntryPoint.Params(navTarget.focusedEventId))
|
||||||
|
.callback(callback)
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
NavTarget.RoomDetails -> {
|
NavTarget.RoomDetails -> {
|
||||||
createRoomDetailsNode(buildContext, RoomDetailsEntryPoint.InitialTarget.RoomDetails)
|
createRoomDetailsNode(buildContext, RoomDetailsEntryPoint.InitialTarget.RoomDetails)
|
||||||
|
|
@ -169,7 +179,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object Messages : NavTarget
|
data class Messages(val focusedEventId: EventId? = null) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object RoomDetails : NavTarget
|
data object RoomDetails : NavTarget
|
||||||
|
|
|
||||||
|
|
@ -47,14 +47,30 @@ class JoinRoomLoadedFlowNodeTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val mainDispatcherRule = MainDispatcherRule()
|
val mainDispatcherRule = MainDispatcherRule()
|
||||||
|
|
||||||
private class FakeMessagesEntryPoint : MessagesEntryPoint {
|
private class FakeMessagesEntryPoint : MessagesEntryPoint, MessagesEntryPoint.NodeBuilder {
|
||||||
|
var buildContext: BuildContext? = null
|
||||||
var nodeId: String? = null
|
var nodeId: String? = null
|
||||||
|
var parameters: MessagesEntryPoint.Params? = null
|
||||||
var callback: MessagesEntryPoint.Callback? = null
|
var callback: MessagesEntryPoint.Callback? = null
|
||||||
|
|
||||||
override fun createNode(parentNode: Node, buildContext: BuildContext, callback: MessagesEntryPoint.Callback): Node {
|
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): MessagesEntryPoint.NodeBuilder {
|
||||||
return node(buildContext) {}.also {
|
this.buildContext = buildContext
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun params(params: MessagesEntryPoint.Params): MessagesEntryPoint.NodeBuilder {
|
||||||
|
parameters = params
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun callback(callback: MessagesEntryPoint.Callback): MessagesEntryPoint.NodeBuilder {
|
||||||
|
this.callback = callback
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun build(): Node {
|
||||||
|
return node(buildContext!!) {}.also {
|
||||||
nodeId = it.id
|
nodeId = it.id
|
||||||
this.callback = callback
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -118,9 +134,9 @@ class JoinRoomLoadedFlowNodeTest {
|
||||||
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
assertThat(roomFlowNode.backstack.activeElement).isEqualTo(JoinedRoomLoadedFlowNode.NavTarget.Messages)
|
assertThat(roomFlowNode.backstack.activeElement).isEqualTo(JoinedRoomLoadedFlowNode.NavTarget.Messages())
|
||||||
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages, Lifecycle.State.CREATED)
|
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(), Lifecycle.State.CREATED)
|
||||||
val messagesNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.Messages)!!
|
val messagesNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.Messages())!!
|
||||||
assertThat(messagesNode.id).isEqualTo(fakeMessagesEntryPoint.nodeId)
|
assertThat(messagesNode.id).isEqualTo(fakeMessagesEntryPoint.nodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Fix compile for forks that use the `noop` analytics module
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Encrypt new session data with a passphrase
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Use sdk API to build permalinks
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Parse permalink using parseMatrixEntityFrom from the SDK
|
|
||||||
1
changelog.d/2721.misc
Normal file
1
changelog.d/2721.misc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
RoomMember screen: fallback to userProfile data, if the member is not a user of the room.
|
||||||
2
fastlane/metadata/android/en-US/changelogs/40004100.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40004100.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: Prepare navigation with permalink.
|
||||||
|
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||||
|
|
@ -50,7 +50,6 @@ dependencies {
|
||||||
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.libraries.push.test)
|
||||||
testImplementation(projects.features.invite.test)
|
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,56 +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.invite.impl
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.datastore.core.DataStore
|
|
||||||
import androidx.datastore.preferences.core.Preferences
|
|
||||||
import androidx.datastore.preferences.core.edit
|
|
||||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
|
||||||
import io.element.android.features.invite.api.SeenInvitesStore
|
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
|
||||||
import io.element.android.libraries.di.SessionScope
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_seeninvites")
|
|
||||||
private val seenInvitesKey = stringSetPreferencesKey("seenInvites")
|
|
||||||
|
|
||||||
@ContributesBinding(SessionScope::class)
|
|
||||||
class DefaultSeenInvitesStore @Inject constructor(
|
|
||||||
@ApplicationContext context: Context
|
|
||||||
) : SeenInvitesStore {
|
|
||||||
private val store = context.dataStore
|
|
||||||
|
|
||||||
override fun seenRoomIds(): Flow<Set<RoomId>> =
|
|
||||||
store.data.map { prefs ->
|
|
||||||
prefs[seenInvitesKey]
|
|
||||||
.orEmpty()
|
|
||||||
.map { RoomId(it) }
|
|
||||||
.toSet()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun markAsSeen(roomIds: Set<RoomId>) {
|
|
||||||
store.edit { prefs ->
|
|
||||||
prefs[seenInvitesKey] = roomIds.map { it.value }.toSet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,195 +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.invite.impl.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.SpanStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.element.android.compound.theme.ElementTheme
|
|
||||||
import io.element.android.features.invite.impl.R
|
|
||||||
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
|
||||||
import io.element.android.features.invite.impl.model.InviteListInviteSummaryProvider
|
|
||||||
import io.element.android.features.invite.impl.model.InviteSender
|
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.ButtonSize
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
|
||||||
|
|
||||||
private val minHeight = 72.dp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
internal fun InviteSummaryRow(
|
|
||||||
invite: InviteListInviteSummary,
|
|
||||||
onAcceptClicked: () -> Unit,
|
|
||||||
onDeclineClicked: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.heightIn(min = minHeight)
|
|
||||||
) {
|
|
||||||
DefaultInviteSummaryRow(
|
|
||||||
invite = invite,
|
|
||||||
onAcceptClicked = onAcceptClicked,
|
|
||||||
onDeclineClicked = onDeclineClicked,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun DefaultInviteSummaryRow(
|
|
||||||
invite: InviteListInviteSummary,
|
|
||||||
onAcceptClicked: () -> Unit,
|
|
||||||
onDeclineClicked: () -> Unit,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp)
|
|
||||||
.height(IntrinsicSize.Min),
|
|
||||||
verticalAlignment = Alignment.Top
|
|
||||||
) {
|
|
||||||
Avatar(
|
|
||||||
invite.roomAvatarData,
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(start = 16.dp, end = 4.dp)
|
|
||||||
.alignByBaseline()
|
|
||||||
.weight(1f)
|
|
||||||
) {
|
|
||||||
val bonusPadding = if (invite.isNew) 12.dp else 0.dp
|
|
||||||
|
|
||||||
// Name
|
|
||||||
Text(
|
|
||||||
text = invite.roomName,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
style = ElementTheme.typography.fontBodyLgMedium,
|
|
||||||
modifier = Modifier.padding(end = bonusPadding),
|
|
||||||
)
|
|
||||||
|
|
||||||
// ID or Alias
|
|
||||||
invite.roomAlias?.let {
|
|
||||||
Text(
|
|
||||||
style = ElementTheme.typography.fontBodyMdRegular,
|
|
||||||
text = it,
|
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier.padding(end = bonusPadding),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sender
|
|
||||||
invite.sender?.let { sender ->
|
|
||||||
SenderRow(sender = sender)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CTAs
|
|
||||||
Row(Modifier.padding(top = 12.dp)) {
|
|
||||||
OutlinedButton(
|
|
||||||
text = stringResource(CommonStrings.action_decline),
|
|
||||||
onClick = onDeclineClicked,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
size = ButtonSize.Medium,
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
|
|
||||||
Button(
|
|
||||||
text = stringResource(CommonStrings.action_accept),
|
|
||||||
onClick = onAcceptClicked,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
size = ButtonSize.Medium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UnreadIndicatorAtom(isVisible = invite.isNew)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SenderRow(sender: InviteSender) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
modifier = Modifier.padding(top = 6.dp),
|
|
||||||
) {
|
|
||||||
Avatar(
|
|
||||||
avatarData = sender.avatarData,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.screen_invites_invited_you, sender.displayName, sender.userId.value).let { text ->
|
|
||||||
val senderNameStart = LocalContext.current.getString(R.string.screen_invites_invited_you).indexOf("%1\$s")
|
|
||||||
AnnotatedString(
|
|
||||||
text = text,
|
|
||||||
spanStyles = listOf(
|
|
||||||
AnnotatedString.Range(
|
|
||||||
SpanStyle(
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
),
|
|
||||||
start = senderNameStart,
|
|
||||||
end = senderNameStart + sender.displayName.length
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
style = ElementTheme.typography.fontBodyMdRegular,
|
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreviewsDayNight
|
|
||||||
@Composable
|
|
||||||
internal fun InviteSummaryRowPreview(@PreviewParameter(InviteListInviteSummaryProvider::class) data: InviteListInviteSummary) = ElementPreview {
|
|
||||||
InviteSummaryRow(
|
|
||||||
invite = data,
|
|
||||||
onAcceptClicked = {},
|
|
||||||
onDeclineClicked = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,157 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.invite.impl.invitelist
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import io.element.android.features.invite.api.SeenInvitesStore
|
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
|
||||||
import io.element.android.features.invite.api.response.InviteData
|
|
||||||
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
|
||||||
import io.element.android.features.invite.impl.model.InviteSender
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class InviteListPresenter @Inject constructor(
|
|
||||||
private val client: MatrixClient,
|
|
||||||
private val store: SeenInvitesStore,
|
|
||||||
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
|
||||||
) : Presenter<InviteListState> {
|
|
||||||
@Composable
|
|
||||||
override fun present(): InviteListState {
|
|
||||||
val invites by client
|
|
||||||
.roomListService
|
|
||||||
.invites
|
|
||||||
.summaries
|
|
||||||
.collectAsState(initial = emptyList())
|
|
||||||
|
|
||||||
var seenInvites by remember { mutableStateOf<Set<RoomId>>(emptySet()) }
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
seenInvites = store.seenRoomIds().first()
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(invites) {
|
|
||||||
store.markAsSeen(
|
|
||||||
invites
|
|
||||||
.filterIsInstance<RoomSummary.Filled>()
|
|
||||||
.map { it.details.roomId }
|
|
||||||
.toSet()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
|
|
||||||
|
|
||||||
fun handleEvent(event: InviteListEvents) {
|
|
||||||
when (event) {
|
|
||||||
is InviteListEvents.AcceptInvite -> {
|
|
||||||
acceptDeclineInviteState.eventSink(
|
|
||||||
AcceptDeclineInviteEvents.AcceptInvite(event.invite.toInviteData())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is InviteListEvents.DeclineInvite -> {
|
|
||||||
acceptDeclineInviteState.eventSink(
|
|
||||||
AcceptDeclineInviteEvents.DeclineInvite(event.invite.toInviteData())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val inviteList = remember(seenInvites, invites) {
|
|
||||||
invites
|
|
||||||
.filterIsInstance<RoomSummary.Filled>()
|
|
||||||
.map {
|
|
||||||
it.toInviteSummary(seenInvites.contains(it.details.roomId))
|
|
||||||
}
|
|
||||||
.toPersistentList()
|
|
||||||
}
|
|
||||||
|
|
||||||
return InviteListState(
|
|
||||||
inviteList = inviteList,
|
|
||||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
|
||||||
eventSink = ::handleEvent
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun RoomSummary.Filled.toInviteSummary(seen: Boolean) = details.run {
|
|
||||||
val i = inviter
|
|
||||||
val avatarData = if (isDirect && i != null) {
|
|
||||||
AvatarData(
|
|
||||||
id = i.userId.value,
|
|
||||||
name = i.displayName,
|
|
||||||
url = i.avatarUrl,
|
|
||||||
size = AvatarSize.RoomInviteItem,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
AvatarData(
|
|
||||||
id = roomId.value,
|
|
||||||
name = name,
|
|
||||||
url = avatarUrl,
|
|
||||||
size = AvatarSize.RoomInviteItem,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val alias = if (isDirect) {
|
|
||||||
inviter?.userId?.value
|
|
||||||
} else {
|
|
||||||
canonicalAlias
|
|
||||||
}
|
|
||||||
|
|
||||||
InviteListInviteSummary(
|
|
||||||
roomId = roomId,
|
|
||||||
roomName = name,
|
|
||||||
roomAlias = alias,
|
|
||||||
roomAvatarData = avatarData,
|
|
||||||
isDirect = isDirect,
|
|
||||||
isNew = !seen,
|
|
||||||
sender = inviter
|
|
||||||
?.takeIf { !isDirect }
|
|
||||||
?.run {
|
|
||||||
InviteSender(
|
|
||||||
userId = userId,
|
|
||||||
displayName = displayName ?: "",
|
|
||||||
avatarData = AvatarData(
|
|
||||||
id = userId.value,
|
|
||||||
name = displayName,
|
|
||||||
url = avatarUrl,
|
|
||||||
size = AvatarSize.InviteSender,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun InviteListInviteSummary.toInviteData() = InviteData(
|
|
||||||
roomId = roomId,
|
|
||||||
roomName = roomName,
|
|
||||||
isDirect = isDirect,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.invite.impl.invitelist
|
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider
|
|
||||||
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
|
||||||
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
|
||||||
import io.element.android.features.invite.impl.model.InviteSender
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
|
|
||||||
open class InviteListStateProvider : PreviewParameterProvider<InviteListState> {
|
|
||||||
private val acceptDeclineInviteStateProvider = AcceptDeclineInviteStateProvider()
|
|
||||||
|
|
||||||
override val values: Sequence<InviteListState>
|
|
||||||
get() = sequenceOf(
|
|
||||||
anInviteListState(),
|
|
||||||
anInviteListState(inviteList = persistentListOf()),
|
|
||||||
) + acceptDeclineInviteStateProvider.values.map { acceptDeclineInviteState ->
|
|
||||||
anInviteListState(acceptDeclineInviteState = acceptDeclineInviteState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun anInviteListState(
|
|
||||||
inviteList: ImmutableList<InviteListInviteSummary> = aInviteListInviteSummaryList(),
|
|
||||||
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
|
||||||
eventSink: (InviteListEvents) -> Unit = {}
|
|
||||||
) = InviteListState(
|
|
||||||
inviteList = inviteList,
|
|
||||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
|
||||||
eventSink = eventSink,
|
|
||||||
)
|
|
||||||
|
|
||||||
internal fun aInviteListInviteSummaryList(): ImmutableList<InviteListInviteSummary> {
|
|
||||||
return persistentListOf(
|
|
||||||
InviteListInviteSummary(
|
|
||||||
roomId = RoomId("!id1:example.com"),
|
|
||||||
roomName = "Room 1",
|
|
||||||
roomAlias = "#room:example.org",
|
|
||||||
sender = InviteSender(
|
|
||||||
userId = UserId("@alice:example.org"),
|
|
||||||
displayName = "Alice"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
InviteListInviteSummary(
|
|
||||||
roomId = RoomId("!id2:example.com"),
|
|
||||||
roomName = "Room 2",
|
|
||||||
sender = InviteSender(
|
|
||||||
userId = UserId("@bob:example.org"),
|
|
||||||
displayName = "Bob"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
InviteListInviteSummary(
|
|
||||||
roomId = RoomId("!id3:example.com"),
|
|
||||||
roomName = "Alice",
|
|
||||||
roomAlias = "@alice:example.com"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.invite.impl.invitelist
|
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.element.android.compound.theme.ElementTheme
|
|
||||||
import io.element.android.features.invite.impl.R
|
|
||||||
import io.element.android.features.invite.impl.components.InviteSummaryRow
|
|
||||||
import io.element.android.features.invite.impl.response.AcceptDeclineInviteView
|
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|
||||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
|
||||||
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.TopAppBar
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun InviteListView(
|
|
||||||
state: InviteListState,
|
|
||||||
onBackClicked: () -> Unit,
|
|
||||||
onInviteAccepted: (RoomId) -> Unit,
|
|
||||||
onInviteDeclined: (RoomId) -> Unit,
|
|
||||||
onInviteClicked: (RoomId) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
InviteListContent(
|
|
||||||
state = state,
|
|
||||||
modifier = modifier,
|
|
||||||
onInviteClicked = onInviteClicked,
|
|
||||||
onBackClicked = onBackClicked,
|
|
||||||
)
|
|
||||||
AcceptDeclineInviteView(
|
|
||||||
state = state.acceptDeclineInviteState,
|
|
||||||
onInviteAccepted = onInviteAccepted,
|
|
||||||
onInviteDeclined = onInviteDeclined,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private fun InviteListContent(
|
|
||||||
state: InviteListState,
|
|
||||||
onBackClicked: () -> Unit,
|
|
||||||
onInviteClicked: (RoomId) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Scaffold(
|
|
||||||
modifier = modifier,
|
|
||||||
topBar = {
|
|
||||||
TopAppBar(
|
|
||||||
navigationIcon = {
|
|
||||||
BackButton(onClick = onBackClicked)
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(CommonStrings.action_invites_list),
|
|
||||||
style = ElementTheme.typography.aliasScreenTitle,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
content = { padding ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(padding)
|
|
||||||
.consumeWindowInsets(padding)
|
|
||||||
) {
|
|
||||||
if (state.inviteList.isEmpty()) {
|
|
||||||
Spacer(Modifier.size(80.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.screen_invites_empty_list),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
color = MaterialTheme.colorScheme.tertiary,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
itemsIndexed(
|
|
||||||
items = state.inviteList,
|
|
||||||
) { index, invite ->
|
|
||||||
InviteSummaryRow(
|
|
||||||
modifier = Modifier.clickable(
|
|
||||||
onClick = { onInviteClicked(invite.roomId) }
|
|
||||||
),
|
|
||||||
invite = invite,
|
|
||||||
onAcceptClicked = { state.eventSink(InviteListEvents.AcceptInvite(invite)) },
|
|
||||||
onDeclineClicked = { state.eventSink(InviteListEvents.DeclineInvite(invite)) },
|
|
||||||
)
|
|
||||||
|
|
||||||
if (index != state.inviteList.lastIndex) {
|
|
||||||
HorizontalDivider()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreviewsDayNight
|
|
||||||
@Composable
|
|
||||||
internal fun InviteListViewPreview(@PreviewParameter(InviteListStateProvider::class) state: InviteListState) = ElementPreview {
|
|
||||||
InviteListView(
|
|
||||||
state = state,
|
|
||||||
onBackClicked = {},
|
|
||||||
onInviteAccepted = {},
|
|
||||||
onInviteDeclined = {},
|
|
||||||
onInviteClicked = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +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.invite.impl.model
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
data class InviteListInviteSummary(
|
|
||||||
val roomId: RoomId,
|
|
||||||
val roomName: String = "",
|
|
||||||
val roomAlias: String? = null,
|
|
||||||
val roomAvatarData: AvatarData = AvatarData(roomId.value, roomName, size = AvatarSize.RoomInviteItem),
|
|
||||||
val sender: InviteSender? = null,
|
|
||||||
val isDirect: Boolean = false,
|
|
||||||
val isNew: Boolean = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class InviteSender(
|
|
||||||
val userId: UserId,
|
|
||||||
val displayName: String,
|
|
||||||
val avatarData: AvatarData = AvatarData(userId.value, displayName, size = AvatarSize.InviteSender),
|
|
||||||
)
|
|
||||||
|
|
@ -1,41 +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.invite.impl.model
|
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
|
||||||
|
|
||||||
open class InviteListInviteSummaryProvider : PreviewParameterProvider<InviteListInviteSummary> {
|
|
||||||
override val values: Sequence<InviteListInviteSummary>
|
|
||||||
get() = sequenceOf(
|
|
||||||
aInviteListInviteSummary(),
|
|
||||||
aInviteListInviteSummary().copy(roomAlias = "#someroom-with-a-long-alias:example.com"),
|
|
||||||
aInviteListInviteSummary().copy(roomAlias = "#someroom-with-a-long-alias:example.com", isNew = true),
|
|
||||||
aInviteListInviteSummary().copy(roomName = "Alice", sender = null),
|
|
||||||
aInviteListInviteSummary().copy(isNew = true)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun aInviteListInviteSummary() = InviteListInviteSummary(
|
|
||||||
roomId = RoomId("!room1:example.com"),
|
|
||||||
roomName = "Some room with a long name that will truncate",
|
|
||||||
sender = InviteSender(
|
|
||||||
userId = UserId("@alice-with-a-long-mxid:example.org"),
|
|
||||||
displayName = "Alice with a long name"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
@ -20,7 +20,6 @@ import androidx.compose.foundation.layout.Box
|
||||||
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.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider
|
||||||
|
|
@ -29,6 +28,7 @@ import io.element.android.features.invite.impl.R
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
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.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlin.jvm.optionals.getOrNull
|
import kotlin.jvm.optionals.getOrNull
|
||||||
|
|
@ -102,9 +102,9 @@ private fun DeclineConfirmationDialog(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewLightDark
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AcceptDeclineInviteViewLightPreview(@PreviewParameter(AcceptDeclineInviteStateProvider::class) state: AcceptDeclineInviteState) =
|
internal fun AcceptDeclineInviteViewPreview(@PreviewParameter(AcceptDeclineInviteStateProvider::class) state: AcceptDeclineInviteState) =
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
AcceptDeclineInviteView(
|
AcceptDeclineInviteView(
|
||||||
state = state,
|
state = state,
|
||||||
|
|
|
||||||
|
|
@ -1,266 +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.invite.impl.invitelist
|
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionMode
|
|
||||||
import app.cash.molecule.moleculeFlow
|
|
||||||
import app.cash.turbine.TurbineTestContext
|
|
||||||
import app.cash.turbine.test
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import io.element.android.features.invite.api.SeenInvitesStore
|
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
|
||||||
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
|
||||||
import io.element.android.features.invite.test.FakeSeenInvitesStore
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
|
||||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
|
||||||
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.room.aRoomMember
|
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails
|
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class InviteListPresenterTests {
|
|
||||||
@get:Rule
|
|
||||||
val warmUpRule = WarmUpRule()
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - starts empty, adds invites when received`() = runTest {
|
|
||||||
val roomListService = FakeRoomListService()
|
|
||||||
val presenter = createInviteListPresenter(
|
|
||||||
FakeMatrixClient(roomListService = roomListService)
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(initialState.inviteList).isEmpty()
|
|
||||||
|
|
||||||
roomListService.postInviteRooms(listOf(aRoomSummary()))
|
|
||||||
|
|
||||||
val withInviteState = awaitItem()
|
|
||||||
assertThat(withInviteState.inviteList.size).isEqualTo(1)
|
|
||||||
assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
|
|
||||||
assertThat(withInviteState.inviteList[0].roomName).isEqualTo(A_ROOM_NAME)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - uses user ID and avatar for direct invites`() = runTest {
|
|
||||||
val roomListService = FakeRoomListService().withDirectChatInvitation()
|
|
||||||
val presenter = createInviteListPresenter(
|
|
||||||
FakeMatrixClient(roomListService = roomListService)
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val withInviteState = awaitInitialItem()
|
|
||||||
assertThat(withInviteState.inviteList.size).isEqualTo(1)
|
|
||||||
assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
|
|
||||||
assertThat(withInviteState.inviteList[0].roomAlias).isEqualTo(A_USER_ID.value)
|
|
||||||
assertThat(withInviteState.inviteList[0].roomName).isEqualTo(A_ROOM_NAME)
|
|
||||||
assertThat(withInviteState.inviteList[0].roomAvatarData).isEqualTo(
|
|
||||||
AvatarData(
|
|
||||||
id = A_USER_ID.value,
|
|
||||||
name = A_USER_NAME,
|
|
||||||
url = AN_AVATAR_URL,
|
|
||||||
size = AvatarSize.RoomInviteItem,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
assertThat(withInviteState.inviteList[0].sender).isNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - includes sender details for room invites`() = runTest {
|
|
||||||
val roomListService = FakeRoomListService().withRoomInvitation()
|
|
||||||
val presenter = createInviteListPresenter(
|
|
||||||
FakeMatrixClient(roomListService = roomListService)
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val withInviteState = awaitInitialItem()
|
|
||||||
assertThat(withInviteState.inviteList.size).isEqualTo(1)
|
|
||||||
assertThat(withInviteState.inviteList[0].sender?.displayName).isEqualTo(A_USER_NAME)
|
|
||||||
assertThat(withInviteState.inviteList[0].sender?.userId).isEqualTo(A_USER_ID)
|
|
||||||
assertThat(withInviteState.inviteList[0].sender?.avatarData).isEqualTo(
|
|
||||||
AvatarData(
|
|
||||||
id = A_USER_ID.value,
|
|
||||||
name = A_USER_NAME,
|
|
||||||
url = AN_AVATAR_URL,
|
|
||||||
size = AvatarSize.InviteSender,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - stores seen invites when received`() = runTest {
|
|
||||||
val roomListService = FakeRoomListService()
|
|
||||||
val store = FakeSeenInvitesStore()
|
|
||||||
val presenter = createInviteListPresenter(
|
|
||||||
FakeMatrixClient(
|
|
||||||
roomListService = roomListService,
|
|
||||||
),
|
|
||||||
store,
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
awaitItem()
|
|
||||||
|
|
||||||
// When one invite is received, that ID is saved
|
|
||||||
roomListService.postInviteRooms(listOf(aRoomSummary()))
|
|
||||||
|
|
||||||
awaitItem()
|
|
||||||
assertThat(store.getProvidedRoomIds()).isEqualTo(setOf(A_ROOM_ID))
|
|
||||||
|
|
||||||
// When a second is added, both are saved
|
|
||||||
roomListService.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
|
|
||||||
|
|
||||||
awaitItem()
|
|
||||||
assertThat(store.getProvidedRoomIds()).isEqualTo(setOf(A_ROOM_ID, A_ROOM_ID_2))
|
|
||||||
|
|
||||||
// When they're both dismissed, an empty set is saved
|
|
||||||
roomListService.postInviteRooms(listOf())
|
|
||||||
|
|
||||||
awaitItem()
|
|
||||||
assertThat(store.getProvidedRoomIds()).isEmpty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - marks invite as new if they're unseen`() = runTest {
|
|
||||||
val roomListService = FakeRoomListService()
|
|
||||||
val store = FakeSeenInvitesStore()
|
|
||||||
store.publishRoomIds(setOf(A_ROOM_ID))
|
|
||||||
val presenter = createInviteListPresenter(
|
|
||||||
FakeMatrixClient(
|
|
||||||
roomListService = roomListService,
|
|
||||||
),
|
|
||||||
store,
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
awaitItem()
|
|
||||||
|
|
||||||
roomListService.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
|
|
||||||
skipItems(1)
|
|
||||||
|
|
||||||
val withInviteState = awaitItem()
|
|
||||||
assertThat(withInviteState.inviteList.size).isEqualTo(2)
|
|
||||||
assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
|
|
||||||
assertThat(withInviteState.inviteList[0].isNew).isFalse()
|
|
||||||
assertThat(withInviteState.inviteList[1].roomId).isEqualTo(A_ROOM_ID_2)
|
|
||||||
assertThat(withInviteState.inviteList[1].isNew).isTrue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun FakeRoomListService.withRoomInvitation(): FakeRoomListService {
|
|
||||||
postInviteRooms(
|
|
||||||
listOf(
|
|
||||||
RoomSummary.Filled(
|
|
||||||
aRoomSummaryDetails(
|
|
||||||
roomId = A_ROOM_ID,
|
|
||||||
name = A_ROOM_NAME,
|
|
||||||
avatarUrl = null,
|
|
||||||
isDirect = false,
|
|
||||||
lastMessage = null,
|
|
||||||
inviter = aRoomMember(
|
|
||||||
userId = A_USER_ID,
|
|
||||||
displayName = A_USER_NAME,
|
|
||||||
avatarUrl = AN_AVATAR_URL,
|
|
||||||
membership = RoomMembershipState.JOIN,
|
|
||||||
isNameAmbiguous = false,
|
|
||||||
powerLevel = 0,
|
|
||||||
normalizedPowerLevel = 0,
|
|
||||||
isIgnored = false,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun FakeRoomListService.withDirectChatInvitation(): FakeRoomListService {
|
|
||||||
postInviteRooms(
|
|
||||||
listOf(
|
|
||||||
RoomSummary.Filled(
|
|
||||||
aRoomSummaryDetails(
|
|
||||||
roomId = A_ROOM_ID,
|
|
||||||
name = A_ROOM_NAME,
|
|
||||||
avatarUrl = null,
|
|
||||||
isDirect = true,
|
|
||||||
lastMessage = null,
|
|
||||||
inviter = aRoomMember(
|
|
||||||
userId = A_USER_ID,
|
|
||||||
displayName = A_USER_NAME,
|
|
||||||
avatarUrl = AN_AVATAR_URL,
|
|
||||||
membership = RoomMembershipState.JOIN,
|
|
||||||
isNameAmbiguous = false,
|
|
||||||
powerLevel = 0,
|
|
||||||
normalizedPowerLevel = 0,
|
|
||||||
isIgnored = false,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun aRoomSummary(id: RoomId = A_ROOM_ID) = RoomSummary.Filled(
|
|
||||||
aRoomSummaryDetails(
|
|
||||||
roomId = id,
|
|
||||||
name = A_ROOM_NAME,
|
|
||||||
avatarUrl = null,
|
|
||||||
isDirect = false,
|
|
||||||
lastMessage = null,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
private suspend fun TurbineTestContext<InviteListState>.awaitInitialItem(): InviteListState {
|
|
||||||
skipItems(1)
|
|
||||||
return awaitItem()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createInviteListPresenter(
|
|
||||||
client: MatrixClient,
|
|
||||||
seenInvitesStore: SeenInvitesStore = FakeSeenInvitesStore(),
|
|
||||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
|
|
||||||
) = InviteListPresenter(
|
|
||||||
client,
|
|
||||||
seenInvitesStore,
|
|
||||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +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.invite.test
|
|
||||||
|
|
||||||
import io.element.android.features.invite.api.SeenInvitesStore
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
|
|
||||||
class FakeSeenInvitesStore : SeenInvitesStore {
|
|
||||||
private val existing = MutableStateFlow(emptySet<RoomId>())
|
|
||||||
private var provided: Set<RoomId>? = null
|
|
||||||
|
|
||||||
fun publishRoomIds(invites: Set<RoomId>) {
|
|
||||||
existing.value = invites
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getProvidedRoomIds() = provided
|
|
||||||
|
|
||||||
override fun seenRoomIds(): Flow<Set<RoomId>> = existing
|
|
||||||
|
|
||||||
override suspend fun markAsSeen(roomIds: Set<RoomId>) {
|
|
||||||
provided = roomIds.toSet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -22,6 +22,7 @@ import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
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.RoomIdOrAlias
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
|
||||||
interface JoinRoomEntryPoint : FeatureEntryPoint {
|
interface JoinRoomEntryPoint : FeatureEntryPoint {
|
||||||
|
|
@ -29,6 +30,7 @@ interface JoinRoomEntryPoint : FeatureEntryPoint {
|
||||||
|
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
|
val roomIdOrAlias: RoomIdOrAlias,
|
||||||
val roomDescription: Optional<RoomDescription>,
|
val roomDescription: Optional<RoomDescription>,
|
||||||
) : NodeInputs
|
) : NodeInputs
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,6 @@ 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.features.invite.test)
|
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.features.joinroom.impl
|
package io.element.android.features.joinroom.impl
|
||||||
|
|
||||||
sealed interface JoinRoomEvents {
|
sealed interface JoinRoomEvents {
|
||||||
|
data object RetryFetchingContent : JoinRoomEvents
|
||||||
data object JoinRoom : JoinRoomEvents
|
data object JoinRoom : JoinRoomEvents
|
||||||
data object AcceptInvite : JoinRoomEvents
|
data object AcceptInvite : JoinRoomEvents
|
||||||
data object DeclineInvite : JoinRoomEvents
|
data object DeclineInvite : JoinRoomEvents
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,11 @@ class JoinRoomNode @AssistedInject constructor(
|
||||||
private val acceptDeclineInviteView: AcceptDeclineInviteView,
|
private val acceptDeclineInviteView: AcceptDeclineInviteView,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
private val inputs: JoinRoomEntryPoint.Inputs = inputs()
|
private val inputs: JoinRoomEntryPoint.Inputs = inputs()
|
||||||
private val presenter = presenterFactory.create(inputs.roomId, inputs.roomDescription)
|
private val presenter = presenterFactory.create(
|
||||||
|
inputs.roomId,
|
||||||
|
inputs.roomIdOrAlias,
|
||||||
|
inputs.roomDescription,
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,14 @@
|
||||||
|
|
||||||
package io.element.android.features.joinroom.impl
|
package io.element.android.features.joinroom.impl
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
||||||
|
|
@ -29,34 +33,57 @@ import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
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.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||||
import org.jetbrains.annotations.VisibleForTesting
|
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
|
||||||
class JoinRoomPresenter @AssistedInject constructor(
|
class JoinRoomPresenter @AssistedInject constructor(
|
||||||
@Assisted private val roomId: RoomId,
|
@Assisted private val roomId: RoomId,
|
||||||
|
@Assisted private val roomIdOrAlias: RoomIdOrAlias,
|
||||||
@Assisted private val roomDescription: Optional<RoomDescription>,
|
@Assisted private val roomDescription: Optional<RoomDescription>,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||||
) : Presenter<JoinRoomState> {
|
) : Presenter<JoinRoomState> {
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(roomId: RoomId, roomDescription: Optional<RoomDescription>): JoinRoomPresenter
|
fun create(
|
||||||
|
roomId: RoomId,
|
||||||
|
roomIdOrAlias: RoomIdOrAlias,
|
||||||
|
roomDescription: Optional<RoomDescription>,
|
||||||
|
): JoinRoomPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): JoinRoomState {
|
override fun present(): JoinRoomState {
|
||||||
|
var retryCount by remember { mutableIntStateOf(0) }
|
||||||
val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty())
|
val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty())
|
||||||
val contentState by produceState<ContentState>(initialValue = ContentState.Loading(roomId), key1 = roomInfo) {
|
val contentState by produceState<ContentState>(
|
||||||
value = when {
|
initialValue = ContentState.Loading(roomIdOrAlias),
|
||||||
|
key1 = roomInfo,
|
||||||
|
key2 = retryCount,
|
||||||
|
) {
|
||||||
|
when {
|
||||||
roomInfo.isPresent -> {
|
roomInfo.isPresent -> {
|
||||||
roomInfo.get().toContentState()
|
value = roomInfo.get().toContentState()
|
||||||
}
|
}
|
||||||
roomDescription.isPresent -> {
|
roomDescription.isPresent -> {
|
||||||
roomDescription.get().toContentState()
|
value = roomDescription.get().toContentState()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
ContentState.Loading(roomId)
|
value = ContentState.Loading(roomIdOrAlias)
|
||||||
|
val result = matrixClient.getRoomPreview(roomId.toRoomIdOrAlias())
|
||||||
|
value = result.fold(
|
||||||
|
onSuccess = { it.toContentState() },
|
||||||
|
onFailure = { throwable ->
|
||||||
|
if (throwable.message?.contains("403") == true) {
|
||||||
|
ContentState.UnknownRoom(roomIdOrAlias)
|
||||||
|
} else {
|
||||||
|
ContentState.Failure(roomIdOrAlias, throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -64,7 +91,8 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
fun handleEvents(event: JoinRoomEvents) {
|
fun handleEvents(event: JoinRoomEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
JoinRoomEvents.AcceptInvite, JoinRoomEvents.JoinRoom -> {
|
JoinRoomEvents.AcceptInvite,
|
||||||
|
JoinRoomEvents.JoinRoom -> {
|
||||||
val inviteData = contentState.toInviteData() ?: return
|
val inviteData = contentState.toInviteData() ?: return
|
||||||
acceptDeclineInviteState.eventSink(
|
acceptDeclineInviteState.eventSink(
|
||||||
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
|
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
|
||||||
|
|
@ -76,6 +104,9 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
|
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
JoinRoomEvents.RetryFetchingContent -> {
|
||||||
|
retryCount++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,6 +118,24 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun RoomPreview.toContentState(): ContentState {
|
||||||
|
return ContentState.Loaded(
|
||||||
|
roomId = roomId,
|
||||||
|
name = name,
|
||||||
|
topic = topic,
|
||||||
|
alias = canonicalAlias,
|
||||||
|
numberOfMembers = numberOfJoinedMembers,
|
||||||
|
isDirect = false,
|
||||||
|
roomAvatarUrl = avatarUrl,
|
||||||
|
joinAuthorisationStatus = when {
|
||||||
|
isInvited -> JoinAuthorisationStatus.IsInvited
|
||||||
|
canKnock -> JoinAuthorisationStatus.CanKnock
|
||||||
|
isPublic -> JoinAuthorisationStatus.CanJoin
|
||||||
|
else -> JoinAuthorisationStatus.Unknown
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
internal fun RoomDescription.toContentState(): ContentState {
|
internal fun RoomDescription.toContentState(): ContentState {
|
||||||
return ContentState.Loaded(
|
return ContentState.Loaded(
|
||||||
|
|
@ -108,7 +157,7 @@ internal fun RoomDescription.toContentState(): ContentState {
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
internal fun MatrixRoomInfo.toContentState(): ContentState {
|
internal fun MatrixRoomInfo.toContentState(): ContentState {
|
||||||
return ContentState.Loaded(
|
return ContentState.Loaded(
|
||||||
roomId = RoomId(id),
|
roomId = id,
|
||||||
name = name,
|
name = name,
|
||||||
topic = topic,
|
topic = topic,
|
||||||
alias = canonicalAlias,
|
alias = canonicalAlias,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
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.RoomAlias
|
||||||
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.RoomIdOrAlias
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class JoinRoomState(
|
data class JoinRoomState(
|
||||||
|
|
@ -35,13 +37,14 @@ data class JoinRoomState(
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface ContentState {
|
sealed interface ContentState {
|
||||||
data class Loading(val roomId: RoomId) : ContentState
|
data class Loading(val roomIdOrAlias: RoomIdOrAlias) : ContentState
|
||||||
data class UnknownRoom(val roomId: RoomId) : ContentState
|
data class Failure(val roomIdOrAlias: RoomIdOrAlias, val error: Throwable) : ContentState
|
||||||
|
data class UnknownRoom(val roomIdOrAlias: RoomIdOrAlias) : ContentState
|
||||||
data class Loaded(
|
data class Loaded(
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val topic: String?,
|
val topic: String?,
|
||||||
val alias: String?,
|
val alias: RoomAlias?,
|
||||||
val numberOfMembers: Long?,
|
val numberOfMembers: Long?,
|
||||||
val isDirect: Boolean,
|
val isDirect: Boolean,
|
||||||
val roomAvatarUrl: String?,
|
val roomAvatarUrl: String?,
|
||||||
|
|
@ -50,7 +53,7 @@ sealed interface ContentState {
|
||||||
val computedTitle = name ?: roomId.value
|
val computedTitle = name ?: roomId.value
|
||||||
|
|
||||||
val computedSubtitle = when {
|
val computedSubtitle = when {
|
||||||
alias != null -> alias
|
alias != null -> alias.value
|
||||||
name == null -> ""
|
name == null -> ""
|
||||||
else -> roomId.value
|
else -> roomId.value
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,10 @@ package io.element.android.features.joinroom.impl
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
|
|
||||||
open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
||||||
override val values: Sequence<JoinRoomState>
|
override val values: Sequence<JoinRoomState>
|
||||||
|
|
@ -34,22 +37,45 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
||||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin)
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin)
|
||||||
),
|
),
|
||||||
aJoinRoomState(
|
aJoinRoomState(
|
||||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock)
|
contentState = aLoadedContentState(
|
||||||
|
joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock,
|
||||||
|
topic = "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt" +
|
||||||
|
" ut labore et dolore magna aliqua ut enim ad minim veniam quis nostrud exercitation ullamco" +
|
||||||
|
" laboris nisi ut aliquip ex ea commodo consequat duis aute irure dolor in reprehenderit in" +
|
||||||
|
" voluptate velit esse cillum dolore eu fugiat nulla pariatur excepteur sint occaecat cupidatat" +
|
||||||
|
" non proident sunt in culpa qui officia deserunt mollit anim id est laborum",
|
||||||
|
numberOfMembers = 888,
|
||||||
|
)
|
||||||
),
|
),
|
||||||
aJoinRoomState(
|
aJoinRoomState(
|
||||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited)
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited)
|
||||||
),
|
),
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aFailureContentState()
|
||||||
|
),
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aFailureContentState(roomIdOrAlias = A_ROOM_ALIAS.toRoomIdOrAlias())
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun anUnknownContentState(roomId: RoomId = A_ROOM_ID) = ContentState.UnknownRoom(roomId)
|
fun aFailureContentState(
|
||||||
|
roomIdOrAlias: RoomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias()
|
||||||
|
): ContentState {
|
||||||
|
return ContentState.Failure(
|
||||||
|
roomIdOrAlias = roomIdOrAlias,
|
||||||
|
error = Exception("Error"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun aLoadingContentState(roomId: RoomId = A_ROOM_ID) = ContentState.Loading(roomId)
|
fun anUnknownContentState(roomId: RoomId = A_ROOM_ID) = ContentState.UnknownRoom(roomId.toRoomIdOrAlias())
|
||||||
|
|
||||||
|
fun aLoadingContentState(roomId: RoomId = A_ROOM_ID) = ContentState.Loading(roomId.toRoomIdOrAlias())
|
||||||
|
|
||||||
fun aLoadedContentState(
|
fun aLoadedContentState(
|
||||||
roomId: RoomId = A_ROOM_ID,
|
roomId: RoomId = A_ROOM_ID,
|
||||||
name: String = "Element X android",
|
name: String = "Element X android",
|
||||||
alias: String? = "#exa:matrix.org",
|
alias: RoomAlias? = RoomAlias("#exa:matrix.org"),
|
||||||
topic: String? = "Element X is a secure, private and decentralized messenger.",
|
topic: String? = "Element X is a secure, private and decentralized messenger.",
|
||||||
numberOfMembers: Long? = null,
|
numberOfMembers: Long? = null,
|
||||||
isDirect: Boolean = false,
|
isDirect: Boolean = false,
|
||||||
|
|
@ -77,3 +103,4 @@ fun aJoinRoomState(
|
||||||
)
|
)
|
||||||
|
|
||||||
private val A_ROOM_ID = RoomId("!exa:matrix.org")
|
private val A_ROOM_ID = RoomId("!exa:matrix.org")
|
||||||
|
private val A_ROOM_ALIAS = RoomAlias("#exa:matrix.org")
|
||||||
|
|
|
||||||
|
|
@ -16,42 +16,36 @@
|
||||||
|
|
||||||
package io.element.android.features.joinroom.impl
|
package io.element.android.features.joinroom.impl
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.widthIn
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
|
||||||
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.compound.theme.ElementTheme
|
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
|
||||||
|
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom
|
||||||
|
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewSubtitleAtom
|
||||||
|
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAtom
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
|
||||||
|
import io.element.android.libraries.designsystem.atomic.molecules.RoomPreviewMembersCountMolecule
|
||||||
|
import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism
|
||||||
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.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
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.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.OutlinedButton
|
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||||
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.RoomIdOrAlias
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -71,7 +65,7 @@ fun JoinRoomView(
|
||||||
},
|
},
|
||||||
footer = {
|
footer = {
|
||||||
JoinRoomFooter(
|
JoinRoomFooter(
|
||||||
joinAuthorisationStatus = state.joinAuthorisationStatus,
|
state = state,
|
||||||
onAcceptInvite = {
|
onAcceptInvite = {
|
||||||
state.eventSink(JoinRoomEvents.AcceptInvite)
|
state.eventSink(JoinRoomEvents.AcceptInvite)
|
||||||
},
|
},
|
||||||
|
|
@ -81,6 +75,9 @@ fun JoinRoomView(
|
||||||
onJoinRoom = {
|
onJoinRoom = {
|
||||||
state.eventSink(JoinRoomEvents.JoinRoom)
|
state.eventSink(JoinRoomEvents.JoinRoom)
|
||||||
},
|
},
|
||||||
|
onRetry = {
|
||||||
|
state.eventSink(JoinRoomEvents.RetryFetchingContent)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -88,46 +85,57 @@ fun JoinRoomView(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun JoinRoomFooter(
|
private fun JoinRoomFooter(
|
||||||
joinAuthorisationStatus: JoinAuthorisationStatus,
|
state: JoinRoomState,
|
||||||
onAcceptInvite: () -> Unit,
|
onAcceptInvite: () -> Unit,
|
||||||
onDeclineInvite: () -> Unit,
|
onDeclineInvite: () -> Unit,
|
||||||
onJoinRoom: () -> Unit,
|
onJoinRoom: () -> Unit,
|
||||||
|
onRetry: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
when (joinAuthorisationStatus) {
|
if (state.contentState is ContentState.Failure) {
|
||||||
JoinAuthorisationStatus.IsInvited -> {
|
Button(
|
||||||
ButtonRowMolecule(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(20.dp)) {
|
text = stringResource(CommonStrings.action_retry),
|
||||||
OutlinedButton(
|
onClick = onRetry,
|
||||||
text = stringResource(CommonStrings.action_decline),
|
modifier = modifier.fillMaxWidth(),
|
||||||
onClick = onDeclineInvite,
|
size = ButtonSize.Medium,
|
||||||
modifier = Modifier.weight(1f),
|
)
|
||||||
size = ButtonSize.Medium,
|
} else {
|
||||||
)
|
val joinAuthorisationStatus = state.joinAuthorisationStatus
|
||||||
|
when (joinAuthorisationStatus) {
|
||||||
|
JoinAuthorisationStatus.IsInvited -> {
|
||||||
|
ButtonRowMolecule(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(20.dp)) {
|
||||||
|
OutlinedButton(
|
||||||
|
text = stringResource(CommonStrings.action_decline),
|
||||||
|
onClick = onDeclineInvite,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
size = ButtonSize.Medium,
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
text = stringResource(CommonStrings.action_accept),
|
||||||
|
onClick = onAcceptInvite,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
size = ButtonSize.Medium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JoinAuthorisationStatus.CanJoin -> {
|
||||||
Button(
|
Button(
|
||||||
text = stringResource(CommonStrings.action_accept),
|
text = stringResource(R.string.screen_join_room_join_action),
|
||||||
onClick = onAcceptInvite,
|
onClick = onJoinRoom,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = modifier.fillMaxWidth(),
|
||||||
size = ButtonSize.Medium,
|
size = ButtonSize.Medium,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
JoinAuthorisationStatus.CanKnock -> {
|
||||||
|
Button(
|
||||||
|
text = stringResource(R.string.screen_join_room_knock_action),
|
||||||
|
onClick = onJoinRoom,
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
size = ButtonSize.Medium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
JoinAuthorisationStatus.Unknown -> Unit
|
||||||
}
|
}
|
||||||
JoinAuthorisationStatus.CanJoin -> {
|
|
||||||
Button(
|
|
||||||
text = stringResource(R.string.screen_join_room_join_action),
|
|
||||||
onClick = onJoinRoom,
|
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
size = ButtonSize.Medium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
JoinAuthorisationStatus.CanKnock -> {
|
|
||||||
Button(
|
|
||||||
text = stringResource(R.string.screen_join_room_knock_action),
|
|
||||||
onClick = onJoinRoom,
|
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
size = ButtonSize.Medium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
JoinAuthorisationStatus.Unknown -> Unit
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,43 +146,43 @@ private fun JoinRoomContent(
|
||||||
) {
|
) {
|
||||||
when (contentState) {
|
when (contentState) {
|
||||||
is ContentState.Loaded -> {
|
is ContentState.Loaded -> {
|
||||||
ContentScaffold(
|
RoomPreviewOrganism(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
avatar = {
|
avatar = {
|
||||||
Avatar(contentState.avatarData(AvatarSize.RoomHeader))
|
Avatar(contentState.avatarData(AvatarSize.RoomHeader))
|
||||||
},
|
},
|
||||||
title = {
|
title = {
|
||||||
Title(contentState.computedTitle)
|
RoomPreviewTitleAtom(contentState.computedTitle)
|
||||||
},
|
},
|
||||||
subtitle = {
|
subtitle = {
|
||||||
Subtitle(contentState.computedSubtitle)
|
RoomPreviewSubtitleAtom(contentState.computedSubtitle)
|
||||||
},
|
},
|
||||||
description = {
|
description = {
|
||||||
Description(contentState.topic ?: "")
|
RoomPreviewDescriptionAtom(contentState.topic ?: "")
|
||||||
},
|
},
|
||||||
memberCount = {
|
memberCount = {
|
||||||
if (contentState.showMemberCount) {
|
if (contentState.showMemberCount) {
|
||||||
MembersCount(memberCount = contentState.numberOfMembers ?: 0)
|
RoomPreviewMembersCountMolecule(memberCount = contentState.numberOfMembers ?: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is ContentState.UnknownRoom -> {
|
is ContentState.UnknownRoom -> {
|
||||||
ContentScaffold(
|
RoomPreviewOrganism(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
avatar = {
|
avatar = {
|
||||||
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
||||||
},
|
},
|
||||||
title = {
|
title = {
|
||||||
Title(stringResource(R.string.screen_join_room_title_no_preview))
|
RoomPreviewTitleAtom(stringResource(R.string.screen_join_room_title_no_preview))
|
||||||
},
|
},
|
||||||
subtitle = {
|
subtitle = {
|
||||||
Subtitle(stringResource(R.string.screen_join_room_subtitle_no_preview))
|
RoomPreviewSubtitleAtom(stringResource(R.string.screen_join_room_subtitle_no_preview))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is ContentState.Loading -> {
|
is ContentState.Loading -> {
|
||||||
ContentScaffold(
|
RoomPreviewOrganism(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
avatar = {
|
avatar = {
|
||||||
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
||||||
|
|
@ -187,94 +195,31 @@ private fun JoinRoomContent(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
is ContentState.Failure -> {
|
||||||
}
|
RoomPreviewOrganism(
|
||||||
|
modifier = modifier,
|
||||||
@Composable
|
avatar = {
|
||||||
private fun ContentScaffold(
|
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
||||||
avatar: @Composable () -> Unit,
|
},
|
||||||
title: @Composable () -> Unit,
|
title = {
|
||||||
subtitle: @Composable () -> Unit,
|
when (contentState.roomIdOrAlias) {
|
||||||
modifier: Modifier = Modifier,
|
is RoomIdOrAlias.Alias -> {
|
||||||
description: @Composable (() -> Unit)? = null,
|
RoomPreviewTitleAtom(contentState.roomIdOrAlias.identifier)
|
||||||
memberCount: @Composable (() -> Unit)? = null,
|
}
|
||||||
) {
|
is RoomIdOrAlias.Id -> {
|
||||||
Column(
|
PlaceholderAtom(width = 200.dp, height = 22.dp)
|
||||||
modifier = modifier.fillMaxWidth(),
|
}
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
}
|
||||||
) {
|
},
|
||||||
avatar()
|
subtitle = {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Text(
|
||||||
title()
|
text = "Failed to get information about the room",
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
textAlign = TextAlign.Center,
|
||||||
subtitle()
|
color = MaterialTheme.colorScheme.error,
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
)
|
||||||
if (memberCount != null) {
|
},
|
||||||
memberCount()
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
if (description != null) {
|
|
||||||
description()
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Title(title: String, modifier: Modifier = Modifier) {
|
|
||||||
Text(
|
|
||||||
modifier = modifier,
|
|
||||||
text = title,
|
|
||||||
style = ElementTheme.typography.fontHeadingMdBold,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
color = ElementTheme.colors.textPrimary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Subtitle(subtitle: String, modifier: Modifier = Modifier) {
|
|
||||||
Text(
|
|
||||||
modifier = modifier,
|
|
||||||
text = subtitle,
|
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
color = ElementTheme.colors.textSecondary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Description(description: String, modifier: Modifier = Modifier) {
|
|
||||||
Text(
|
|
||||||
modifier = modifier,
|
|
||||||
text = description,
|
|
||||||
style = ElementTheme.typography.fontBodySmRegular,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
color = ElementTheme.colors.textSecondary,
|
|
||||||
maxLines = 3,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MembersCount(memberCount: Long) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(color = ElementTheme.colors.bgSubtleSecondary, shape = CircleShape)
|
|
||||||
.widthIn(min = 48.dp)
|
|
||||||
.padding(all = 2.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = CompoundIcons.UserProfile(),
|
|
||||||
contentDescription = null,
|
|
||||||
tint = ElementTheme.colors.iconSecondary,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "$memberCount",
|
|
||||||
style = ElementTheme.typography.fontBodySmMedium,
|
|
||||||
color = ElementTheme.colors.textSecondary,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,7 +236,7 @@ private fun JoinRoomTopBar(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewLightDark
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview {
|
internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview {
|
||||||
JoinRoomView(
|
JoinRoomView(
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import io.element.android.libraries.architecture.Presenter
|
||||||
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.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|
@ -37,9 +38,14 @@ object JoinRoomModule {
|
||||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||||
): JoinRoomPresenter.Factory {
|
): JoinRoomPresenter.Factory {
|
||||||
return object : JoinRoomPresenter.Factory {
|
return object : JoinRoomPresenter.Factory {
|
||||||
override fun create(roomId: RoomId, roomDescription: Optional<RoomDescription>): JoinRoomPresenter {
|
override fun create(
|
||||||
|
roomId: RoomId,
|
||||||
|
roomIdOrAlias: RoomIdOrAlias,
|
||||||
|
roomDescription: Optional<RoomDescription>,
|
||||||
|
): JoinRoomPresenter {
|
||||||
return JoinRoomPresenter(
|
return JoinRoomPresenter(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
|
roomIdOrAlias = roomIdOrAlias,
|
||||||
roomDescription = roomDescription,
|
roomDescription = roomDescription,
|
||||||
matrixClient = client,
|
matrixClient = client,
|
||||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,12 @@ import io.element.android.features.invite.api.response.anAcceptDeclineInviteStat
|
||||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
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.core.RoomAlias
|
||||||
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.toRoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
|
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
|
@ -49,9 +53,10 @@ class JoinRoomPresenterTest {
|
||||||
val presenter = createJoinRoomPresenter()
|
val presenter = createJoinRoomPresenter()
|
||||||
presenter.test {
|
presenter.test {
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.contentState).isEqualTo(ContentState.Loading(A_ROOM_ID))
|
assertThat(state.contentState).isEqualTo(ContentState.Loading(A_ROOM_ID.toRoomIdOrAlias()))
|
||||||
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown)
|
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown)
|
||||||
assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
|
assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -237,6 +242,110 @@ class JoinRoomPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when room is not known RoomPreview is loaded`() = runTest {
|
||||||
|
val client = FakeMatrixClient(
|
||||||
|
getRoomPreviewResult = {
|
||||||
|
Result.success(
|
||||||
|
RoomPreview(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
canonicalAlias = RoomAlias("#alias:matrix.org"),
|
||||||
|
name = "Room name",
|
||||||
|
topic = "Room topic",
|
||||||
|
avatarUrl = "avatarUrl",
|
||||||
|
numberOfJoinedMembers = 2,
|
||||||
|
roomType = null,
|
||||||
|
isHistoryWorldReadable = false,
|
||||||
|
isJoined = false,
|
||||||
|
isInvited = false,
|
||||||
|
isPublic = true,
|
||||||
|
canKnock = false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val presenter = createJoinRoomPresenter(
|
||||||
|
matrixClient = client
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.contentState).isEqualTo(
|
||||||
|
ContentState.Loaded(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
name = "Room name",
|
||||||
|
topic = "Room topic",
|
||||||
|
alias = RoomAlias("#alias:matrix.org"),
|
||||||
|
numberOfMembers = 2,
|
||||||
|
isDirect = false,
|
||||||
|
roomAvatarUrl = "avatarUrl",
|
||||||
|
joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when room is not known RoomPreview is loaded with error`() = runTest {
|
||||||
|
val client = FakeMatrixClient(
|
||||||
|
getRoomPreviewResult = {
|
||||||
|
Result.failure(AN_EXCEPTION)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val presenter = createJoinRoomPresenter(
|
||||||
|
matrixClient = client
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.contentState).isEqualTo(
|
||||||
|
ContentState.Failure(
|
||||||
|
roomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias(),
|
||||||
|
error = AN_EXCEPTION
|
||||||
|
)
|
||||||
|
)
|
||||||
|
state.eventSink(JoinRoomEvents.RetryFetchingContent)
|
||||||
|
}
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.contentState).isEqualTo(
|
||||||
|
ContentState.Loading(A_ROOM_ID.toRoomIdOrAlias())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.contentState).isEqualTo(
|
||||||
|
ContentState.Failure(
|
||||||
|
roomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias(),
|
||||||
|
error = AN_EXCEPTION
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when room is not known RoomPreview is loaded with error 403`() = runTest {
|
||||||
|
val client = FakeMatrixClient(
|
||||||
|
getRoomPreviewResult = {
|
||||||
|
Result.failure(Exception("403"))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val presenter = createJoinRoomPresenter(
|
||||||
|
matrixClient = client
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.contentState).isEqualTo(
|
||||||
|
ContentState.UnknownRoom(
|
||||||
|
roomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createJoinRoomPresenter(
|
private fun createJoinRoomPresenter(
|
||||||
roomId: RoomId = A_ROOM_ID,
|
roomId: RoomId = A_ROOM_ID,
|
||||||
roomDescription: Optional<RoomDescription> = Optional.empty(),
|
roomDescription: Optional<RoomDescription> = Optional.empty(),
|
||||||
|
|
@ -245,6 +354,7 @@ class JoinRoomPresenterTest {
|
||||||
): JoinRoomPresenter {
|
): JoinRoomPresenter {
|
||||||
return JoinRoomPresenter(
|
return JoinRoomPresenter(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
|
roomIdOrAlias = roomId.toRoomIdOrAlias(),
|
||||||
roomDescription = roomDescription,
|
roomDescription = roomDescription,
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter
|
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter
|
||||||
|
|
@ -255,7 +365,7 @@ class JoinRoomPresenterTest {
|
||||||
roomId: RoomId = A_ROOM_ID,
|
roomId: RoomId = A_ROOM_ID,
|
||||||
name: String? = A_ROOM_NAME,
|
name: String? = A_ROOM_NAME,
|
||||||
topic: String? = "A room about something",
|
topic: String? = "A room about something",
|
||||||
alias: String? = "#alias:matrix.org",
|
alias: RoomAlias? = RoomAlias("#alias:matrix.org"),
|
||||||
avatarUrl: String? = null,
|
avatarUrl: String? = null,
|
||||||
joinRule: RoomDescription.JoinRule = RoomDescription.JoinRule.UNKNOWN,
|
joinRule: RoomDescription.JoinRule = RoomDescription.JoinRule.UNKNOWN,
|
||||||
numberOfMembers: Long = 2L
|
numberOfMembers: Long = 2L
|
||||||
|
|
|
||||||
|
|
@ -20,19 +20,28 @@ import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
|
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
|
||||||
|
|
||||||
interface MessagesEntryPoint : FeatureEntryPoint {
|
interface MessagesEntryPoint : FeatureEntryPoint {
|
||||||
fun createNode(
|
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||||
parentNode: Node,
|
|
||||||
buildContext: BuildContext,
|
interface NodeBuilder {
|
||||||
callback: Callback,
|
fun params(params: Params): NodeBuilder
|
||||||
): Node
|
fun callback(callback: Callback): NodeBuilder
|
||||||
|
fun build(): Node
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Params(
|
||||||
|
val focusedEventId: EventId?,
|
||||||
|
)
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onRoomDetailsClicked()
|
fun onRoomDetailsClicked()
|
||||||
fun onUserDataClicked(userId: UserId)
|
fun onUserDataClicked(userId: UserId)
|
||||||
|
fun onPermalinkClicked(data: PermalinkData)
|
||||||
fun onForwardedToSingleRoom(roomId: RoomId)
|
fun onForwardedToSingleRoom(roomId: RoomId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.messages.impl
|
||||||
|
|
||||||
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
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
|
|
@ -26,11 +27,23 @@ import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultMessagesEntryPoint @Inject constructor() : MessagesEntryPoint {
|
class DefaultMessagesEntryPoint @Inject constructor() : MessagesEntryPoint {
|
||||||
override fun createNode(
|
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): MessagesEntryPoint.NodeBuilder {
|
||||||
parentNode: Node,
|
val plugins = ArrayList<Plugin>()
|
||||||
buildContext: BuildContext,
|
|
||||||
callback: MessagesEntryPoint.Callback
|
return object : MessagesEntryPoint.NodeBuilder {
|
||||||
): Node {
|
override fun params(params: MessagesEntryPoint.Params): MessagesEntryPoint.NodeBuilder {
|
||||||
return parentNode.createNode<MessagesFlowNode>(buildContext, listOf(callback))
|
plugins += MessagesNode.Inputs(focusedEventId = params.focusedEventId)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun callback(callback: MessagesEntryPoint.Callback): MessagesEntryPoint.NodeBuilder {
|
||||||
|
plugins += callback
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun build(): Node {
|
||||||
|
return parentNode.createNode<MessagesFlowNode>(buildContext, plugins)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ import io.element.android.features.poll.api.create.CreatePollEntryPoint
|
||||||
import io.element.android.features.poll.api.create.CreatePollMode
|
import io.element.android.features.poll.api.create.CreatePollMode
|
||||||
import io.element.android.libraries.architecture.BackstackWithOverlayBox
|
import io.element.android.libraries.architecture.BackstackWithOverlayBox
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.architecture.overlay.Overlay
|
import io.element.android.libraries.architecture.overlay.Overlay
|
||||||
import io.element.android.libraries.architecture.overlay.operation.show
|
import io.element.android.libraries.architecture.overlay.operation.show
|
||||||
|
|
@ -62,6 +63,7 @@ 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.media.MediaSource
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
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.mediaviewer.api.local.MediaInfo
|
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
|
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
|
||||||
|
|
@ -79,7 +81,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
private val createPollEntryPoint: CreatePollEntryPoint,
|
private val createPollEntryPoint: CreatePollEntryPoint,
|
||||||
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
|
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Messages,
|
initialElement = NavTarget.Messages(plugins.filterIsInstance<Inputs>().firstOrNull()?.focusedEventId),
|
||||||
savedStateMap = buildContext.savedStateMap,
|
savedStateMap = buildContext.savedStateMap,
|
||||||
),
|
),
|
||||||
overlay = Overlay(
|
overlay = Overlay(
|
||||||
|
|
@ -88,12 +90,16 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = plugins
|
plugins = plugins
|
||||||
) {
|
) {
|
||||||
|
data class Inputs(val focusedEventId: EventId?) : NodeInputs
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object Empty : NavTarget
|
data object Empty : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object Messages : NavTarget
|
data class Messages(
|
||||||
|
val focusedEventId: EventId? = null,
|
||||||
|
) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class MediaViewer(
|
data class MediaViewer(
|
||||||
|
|
@ -149,6 +155,10 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
callback?.onUserDataClicked(userId)
|
callback?.onUserDataClicked(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPermalinkClicked(data: PermalinkData) {
|
||||||
|
callback?.onPermalinkClicked(data)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
|
override fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
|
||||||
backstack.push(NavTarget.EventDebugInfo(eventId, debugInfo))
|
backstack.push(NavTarget.EventDebugInfo(eventId, debugInfo))
|
||||||
}
|
}
|
||||||
|
|
@ -181,7 +191,10 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
ElementCallActivity.start(context, inputs)
|
ElementCallActivity.start(context, inputs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
createNode<MessagesNode>(buildContext, listOf(callback))
|
val params = MessagesNode.Inputs(
|
||||||
|
focusedEventId = navTarget.focusedEventId,
|
||||||
|
)
|
||||||
|
createNode<MessagesNode>(buildContext, listOf(callback, params))
|
||||||
}
|
}
|
||||||
is NavTarget.MediaViewer -> {
|
is NavTarget.MediaViewer -> {
|
||||||
val inputs = MediaViewerNode.Inputs(
|
val inputs = MediaViewerNode.Inputs(
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,10 @@ import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPr
|
||||||
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.androidutils.system.openUrlInExternalApp
|
||||||
|
import io.element.android.libraries.androidutils.system.toast
|
||||||
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
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
|
||||||
|
|
@ -42,6 +45,7 @@ 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.PermalinkData
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
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.alias.matches
|
||||||
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
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
|
@ -58,15 +62,21 @@ class MessagesNode @AssistedInject constructor(
|
||||||
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
|
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
|
||||||
private val mediaPlayer: MediaPlayer,
|
private val mediaPlayer: MediaPlayer,
|
||||||
private val permalinkParser: PermalinkParser,
|
private val permalinkParser: PermalinkParser,
|
||||||
|
@ApplicationContext
|
||||||
|
private val context: Context,
|
||||||
) : 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()
|
||||||
|
|
||||||
|
// TODO Handle navigation to the Event
|
||||||
|
data class Inputs(val focusedEventId: EventId?) : NodeInputs
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onRoomDetailsClicked()
|
fun onRoomDetailsClicked()
|
||||||
fun onEventClicked(event: TimelineItem.Event): Boolean
|
fun onEventClicked(event: TimelineItem.Event): Boolean
|
||||||
fun onPreviewAttachments(attachments: ImmutableList<Attachment>)
|
fun onPreviewAttachments(attachments: ImmutableList<Attachment>)
|
||||||
fun onUserDataClicked(userId: UserId)
|
fun onUserDataClicked(userId: UserId)
|
||||||
|
fun onPermalinkClicked(data: PermalinkData)
|
||||||
fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo)
|
fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo)
|
||||||
fun onForwardEventClicked(eventId: EventId)
|
fun onForwardEventClicked(eventId: EventId)
|
||||||
fun onReportMessage(eventId: EventId, senderId: UserId)
|
fun onReportMessage(eventId: EventId, senderId: UserId)
|
||||||
|
|
@ -109,16 +119,12 @@ class MessagesNode @AssistedInject constructor(
|
||||||
) {
|
) {
|
||||||
when (val permalink = permalinkParser.parse(url)) {
|
when (val permalink = permalinkParser.parse(url)) {
|
||||||
is PermalinkData.UserLink -> {
|
is PermalinkData.UserLink -> {
|
||||||
|
// Open the room member profile, it will fallback to
|
||||||
|
// the user profile if the user is not in the room
|
||||||
callback?.onUserDataClicked(permalink.userId)
|
callback?.onUserDataClicked(permalink.userId)
|
||||||
}
|
}
|
||||||
is PermalinkData.RoomLink -> {
|
is PermalinkData.RoomLink -> {
|
||||||
// TODO Implement room link handling
|
handleRoomLinkClicked(permalink)
|
||||||
}
|
|
||||||
is PermalinkData.EventIdAliasLink -> {
|
|
||||||
// TODO Implement room and Event link handling
|
|
||||||
}
|
|
||||||
is PermalinkData.EventIdLink -> {
|
|
||||||
// TODO Implement room and Event link handling
|
|
||||||
}
|
}
|
||||||
is PermalinkData.FallbackLink,
|
is PermalinkData.FallbackLink,
|
||||||
is PermalinkData.RoomEmailInviteLink -> {
|
is PermalinkData.RoomEmailInviteLink -> {
|
||||||
|
|
@ -127,6 +133,20 @@ class MessagesNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleRoomLinkClicked(roomLink: PermalinkData.RoomLink) {
|
||||||
|
if (room.matches(roomLink.roomIdOrAlias)) {
|
||||||
|
if (roomLink.eventId != null) {
|
||||||
|
// TODO Handle navigation to the Event
|
||||||
|
context.toast("TODO Handle navigation to the Event ${roomLink.eventId}")
|
||||||
|
} else {
|
||||||
|
// Click on the same room, ignore
|
||||||
|
context.toast("Already viewing this room!")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback?.onPermalinkClicked(roomLink)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
|
override fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
|
||||||
callback?.onShowEventDebugInfoClicked(eventId, debugInfo)
|
callback?.onShowEventDebugInfoClicked(eventId, debugInfo)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -258,7 +258,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
private fun MatrixRoomInfo.avatarData(): AvatarData {
|
private fun MatrixRoomInfo.avatarData(): AvatarData {
|
||||||
return AvatarData(
|
return AvatarData(
|
||||||
id = id,
|
id = id.value,
|
||||||
name = name,
|
name = name,
|
||||||
url = avatarUrl ?: room.avatarUrl,
|
url = avatarUrl ?: room.avatarUrl,
|
||||||
size = AvatarSize.TimelineRoom
|
size = AvatarSize.TimelineRoom
|
||||||
|
|
|
||||||
|
|
@ -720,7 +720,7 @@ class MessagesPresenterTest {
|
||||||
private fun TestScope.createMessagesPresenter(
|
private fun TestScope.createMessagesPresenter(
|
||||||
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||||
matrixRoom: MatrixRoom = FakeMatrixRoom().apply {
|
matrixRoom: MatrixRoom = FakeMatrixRoom().apply {
|
||||||
givenRoomInfo(aRoomInfo(id = roomId.value, name = ""))
|
givenRoomInfo(aRoomInfo(id = roomId, name = ""))
|
||||||
},
|
},
|
||||||
navigator: FakeMessagesNavigator = FakeMessagesNavigator(),
|
navigator: FakeMessagesNavigator = FakeMessagesNavigator(),
|
||||||
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
|
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
|
||||||
|
|
|
||||||
|
|
@ -201,7 +201,7 @@ class TypingNotificationPresenterTest {
|
||||||
|
|
||||||
private fun createPresenter(
|
private fun createPresenter(
|
||||||
matrixRoom: MatrixRoom = FakeMatrixRoom().apply {
|
matrixRoom: MatrixRoom = FakeMatrixRoom().apply {
|
||||||
givenRoomInfo(aRoomInfo(id = roomId.value, name = ""))
|
givenRoomInfo(aRoomInfo(id = roomId, name = ""))
|
||||||
},
|
},
|
||||||
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(
|
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(
|
||||||
isRenderTypingNotificationsEnabled = true
|
isRenderTypingNotificationsEnabled = true
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2022 New Vector Ltd
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -19,11 +19,10 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.invite.test"
|
namespace = "io.element.android.features.roomaliasresolver.api"
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.coroutines.core)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
api(projects.features.invite.api)
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2023 New Vector Ltd
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -14,25 +14,30 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invite.api
|
package io.element.android.features.roomaliasesolver.api
|
||||||
|
|
||||||
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
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
interface InviteListEntryPoint : FeatureEntryPoint {
|
interface RoomAliasResolverEntryPoint : FeatureEntryPoint {
|
||||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||||
|
|
||||||
interface NodeBuilder {
|
interface NodeBuilder {
|
||||||
fun callback(callback: Callback): NodeBuilder
|
fun callback(callback: Callback): NodeBuilder
|
||||||
|
fun params(params: Params): NodeBuilder
|
||||||
fun build(): Node
|
fun build(): Node
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onBackClicked()
|
fun onAliasResolved(roomId: RoomId)
|
||||||
fun onInviteClicked(roomId: RoomId)
|
|
||||||
fun onInviteAccepted(roomId: RoomId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class Params(
|
||||||
|
val roomAlias: RoomAlias
|
||||||
|
) : NodeInputs
|
||||||
}
|
}
|
||||||
53
features/roomaliasresolver/impl/build.gradle.kts
Normal file
53
features/roomaliasresolver/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-compose-library")
|
||||||
|
alias(libs.plugins.anvil)
|
||||||
|
alias(libs.plugins.ksp)
|
||||||
|
id("kotlin-parcelize")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.features.roomaliasresolver.impl"
|
||||||
|
}
|
||||||
|
|
||||||
|
anvil {
|
||||||
|
generateDaggerFactories.set(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.anvilannotations)
|
||||||
|
anvil(projects.anvilcodegen)
|
||||||
|
api(projects.features.roomaliasresolver.api)
|
||||||
|
implementation(projects.libraries.core)
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(projects.libraries.androidutils)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.matrixui)
|
||||||
|
implementation(projects.libraries.designsystem)
|
||||||
|
implementation(projects.libraries.uiStrings)
|
||||||
|
|
||||||
|
testImplementation(libs.test.junit)
|
||||||
|
testImplementation(libs.coroutines.test)
|
||||||
|
testImplementation(libs.molecule.runtime)
|
||||||
|
testImplementation(libs.test.truth)
|
||||||
|
testImplementation(libs.test.turbine)
|
||||||
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.tests.testutils)
|
||||||
|
|
||||||
|
ksp(libs.showkase.processor)
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2023 New Vector Ltd
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -14,31 +14,35 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invite.impl
|
package io.element.android.features.roomaliasresolver.impl
|
||||||
|
|
||||||
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
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.features.invite.api.InviteListEntryPoint
|
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint
|
||||||
import io.element.android.features.invite.impl.invitelist.InviteListNode
|
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultInviteListEntryPoint @Inject constructor() : InviteListEntryPoint {
|
class DefaultRoomAliasResolverEntryPoint @Inject constructor() : RoomAliasResolverEntryPoint {
|
||||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): InviteListEntryPoint.NodeBuilder {
|
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomAliasResolverEntryPoint.NodeBuilder {
|
||||||
val plugins = ArrayList<Plugin>()
|
val plugins = ArrayList<Plugin>()
|
||||||
|
|
||||||
return object : InviteListEntryPoint.NodeBuilder {
|
return object : RoomAliasResolverEntryPoint.NodeBuilder {
|
||||||
override fun callback(callback: InviteListEntryPoint.Callback): InviteListEntryPoint.NodeBuilder {
|
override fun callback(callback: RoomAliasResolverEntryPoint.Callback): RoomAliasResolverEntryPoint.NodeBuilder {
|
||||||
plugins += callback
|
plugins += callback
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun params(params: RoomAliasResolverEntryPoint.Params): RoomAliasResolverEntryPoint.NodeBuilder {
|
||||||
|
plugins += params
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
override fun build(): Node {
|
override fun build(): Node {
|
||||||
return parentNode.createNode<InviteListNode>(buildContext, plugins)
|
return parentNode.createNode<RoomAliasResolverNode>(buildContext, plugins)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2023 New Vector Ltd
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -14,12 +14,8 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.roomlist.impl.datasource
|
package io.element.android.features.roomaliasresolver.impl
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
sealed interface RoomAliasResolverEvents {
|
||||||
import io.element.android.features.roomlist.impl.InvitesState
|
data object Retry : RoomAliasResolverEvents
|
||||||
|
|
||||||
interface InviteStateDataSource {
|
|
||||||
@Composable
|
|
||||||
fun inviteState(): InvitesState
|
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invite.impl.invitelist
|
package io.element.android.features.roomaliasresolver.impl
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -25,37 +25,35 @@ import com.bumble.appyx.core.plugin.plugins
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.invite.api.InviteListEntryPoint
|
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint
|
||||||
|
import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
class InviteListNode @AssistedInject constructor(
|
class RoomAliasResolverNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val presenter: InviteListPresenter,
|
presenterFactory: RoomAliasResolverPresenter.Factory,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
private fun onBackClicked() {
|
private val inputs = inputs<RoomAliasResolverEntryPoint.Params>()
|
||||||
plugins<InviteListEntryPoint.Callback>().forEach { it.onBackClicked() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onInviteAccepted(roomId: RoomId) {
|
private val presenter = presenterFactory.create(
|
||||||
plugins<InviteListEntryPoint.Callback>().forEach { it.onInviteAccepted(roomId) }
|
inputs.roomAlias
|
||||||
}
|
)
|
||||||
|
|
||||||
private fun onInviteClicked(roomId: RoomId) {
|
private fun onAliasResolved(roomId: RoomId) {
|
||||||
plugins<InviteListEntryPoint.Callback>().forEach { it.onInviteClicked(roomId) }
|
plugins<RoomAliasResolverEntryPoint.Callback>().forEach { it.onAliasResolved(roomId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
InviteListView(
|
RoomAliasResolverView(
|
||||||
state = state,
|
state = state,
|
||||||
onBackClicked = ::onBackClicked,
|
onAliasResolved = ::onAliasResolved,
|
||||||
onInviteAccepted = ::onInviteAccepted,
|
onBackPressed = ::navigateUp,
|
||||||
onInviteDeclined = {},
|
modifier = modifier
|
||||||
onInviteClicked = ::onInviteClicked,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* 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.roomaliasresolver.impl
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class RoomAliasResolverPresenter @AssistedInject constructor(
|
||||||
|
@Assisted private val roomAlias: RoomAlias,
|
||||||
|
private val matrixClient: MatrixClient,
|
||||||
|
) : Presenter<RoomAliasResolverState> {
|
||||||
|
interface Factory {
|
||||||
|
fun create(
|
||||||
|
roomAlias: RoomAlias,
|
||||||
|
): RoomAliasResolverPresenter
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): RoomAliasResolverState {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val resolveState: MutableState<AsyncData<RoomId>> = remember { mutableStateOf(AsyncData.Uninitialized) }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
resolveAlias(resolveState)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleEvents(event: RoomAliasResolverEvents) {
|
||||||
|
when (event) {
|
||||||
|
RoomAliasResolverEvents.Retry -> coroutineScope.resolveAlias(resolveState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return RoomAliasResolverState(
|
||||||
|
roomAlias = roomAlias,
|
||||||
|
resolveState = resolveState.value,
|
||||||
|
eventSink = ::handleEvents
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.resolveAlias(resolveState: MutableState<AsyncData<RoomId>>) = launch {
|
||||||
|
suspend {
|
||||||
|
matrixClient.resolveRoomAlias(roomAlias).getOrThrow()
|
||||||
|
}.runCatchingUpdatingState(resolveState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,16 +14,16 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invite.impl.invitelist
|
package io.element.android.features.roomaliasresolver.impl
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class InviteListState(
|
data class RoomAliasResolverState(
|
||||||
val inviteList: ImmutableList<InviteListInviteSummary>,
|
val roomAlias: RoomAlias,
|
||||||
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
val resolveState: AsyncData<RoomId>,
|
||||||
val eventSink: (InviteListEvents) -> Unit
|
val eventSink: (RoomAliasResolverEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* 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.roomaliasresolver.impl
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
|
open class RoomAliasResolverStateProvider : PreviewParameterProvider<RoomAliasResolverState> {
|
||||||
|
override val values: Sequence<RoomAliasResolverState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aRoomAliasResolverState(),
|
||||||
|
aRoomAliasResolverState(
|
||||||
|
resolveState = AsyncData.Loading(),
|
||||||
|
),
|
||||||
|
aRoomAliasResolverState(
|
||||||
|
resolveState = AsyncData.Failure(Exception("Error")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aRoomAliasResolverState(
|
||||||
|
roomAlias: RoomAlias = A_ROOM_ALIAS,
|
||||||
|
resolveState: AsyncData<RoomId> = AsyncData.Uninitialized,
|
||||||
|
eventSink: (RoomAliasResolverEvents) -> Unit = {}
|
||||||
|
) = RoomAliasResolverState(
|
||||||
|
roomAlias = roomAlias,
|
||||||
|
resolveState = resolveState,
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val A_ROOM_ALIAS = RoomAlias("#exa:matrix.org")
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
/*
|
||||||
|
* 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.roomaliasresolver.impl
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
|
||||||
|
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAtom
|
||||||
|
import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism
|
||||||
|
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ButtonSize
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RoomAliasResolverView(
|
||||||
|
state: RoomAliasResolverState,
|
||||||
|
onBackPressed: () -> Unit,
|
||||||
|
onAliasResolved: (RoomId) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val latestOnAliasResolved by rememberUpdatedState(onAliasResolved)
|
||||||
|
LaunchedEffect(state.resolveState) {
|
||||||
|
if (state.resolveState is AsyncData.Success) {
|
||||||
|
latestOnAliasResolved(state.resolveState.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HeaderFooterPage(
|
||||||
|
modifier = modifier,
|
||||||
|
paddingValues = PaddingValues(16.dp),
|
||||||
|
topBar = {
|
||||||
|
RoomAliasResolverTopBar(onBackClicked = onBackPressed)
|
||||||
|
},
|
||||||
|
content = {
|
||||||
|
RoomAliasResolverContent(state = state)
|
||||||
|
},
|
||||||
|
footer = {
|
||||||
|
RoomAliasResolverFooter(
|
||||||
|
state = state,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RoomAliasResolverFooter(
|
||||||
|
state: RoomAliasResolverState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
when (state.resolveState) {
|
||||||
|
is AsyncData.Failure -> {
|
||||||
|
Button(
|
||||||
|
text = stringResource(CommonStrings.action_retry),
|
||||||
|
onClick = {
|
||||||
|
state.eventSink(RoomAliasResolverEvents.Retry)
|
||||||
|
},
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
size = ButtonSize.Medium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is AsyncData.Loading -> {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AsyncData.Uninitialized,
|
||||||
|
is AsyncData.Success -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RoomAliasResolverContent(
|
||||||
|
state: RoomAliasResolverState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
RoomPreviewOrganism(
|
||||||
|
modifier = modifier,
|
||||||
|
avatar = {
|
||||||
|
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
RoomPreviewTitleAtom(state.roomAlias.value)
|
||||||
|
},
|
||||||
|
subtitle = {
|
||||||
|
},
|
||||||
|
description = {
|
||||||
|
if (state.resolveState.isFailure()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.screen_room_alias_resolver_resolve_alias_failure),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
memberCount = {
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun RoomAliasResolverTopBar(
|
||||||
|
onBackClicked: () -> Unit,
|
||||||
|
) {
|
||||||
|
TopAppBar(
|
||||||
|
navigationIcon = {
|
||||||
|
BackButton(onClick = onBackClicked)
|
||||||
|
},
|
||||||
|
title = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun RoomAliasResolverViewPreview(@PreviewParameter(RoomAliasResolverStateProvider::class) state: RoomAliasResolverState) = ElementPreview {
|
||||||
|
RoomAliasResolverView(
|
||||||
|
state = state,
|
||||||
|
onAliasResolved = { },
|
||||||
|
onBackPressed = { }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* 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.roomaliasresolver.impl.di
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import io.element.android.features.roomaliasresolver.impl.RoomAliasResolverPresenter
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@ContributesTo(SessionScope::class)
|
||||||
|
object RoomAliasResolverModule {
|
||||||
|
@Provides
|
||||||
|
fun providesJoinRoomPresenterFactory(
|
||||||
|
client: MatrixClient,
|
||||||
|
): RoomAliasResolverPresenter.Factory {
|
||||||
|
return object : RoomAliasResolverPresenter.Factory {
|
||||||
|
override fun create(roomAlias: RoomAlias): RoomAliasResolverPresenter {
|
||||||
|
return RoomAliasResolverPresenter(
|
||||||
|
roomAlias = roomAlias,
|
||||||
|
matrixClient = client,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_room_alias_resolver_resolve_alias_failure">"Failed to resolve room alias."</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
* 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.roomaliasresolver.impl
|
||||||
|
|
||||||
|
import app.cash.molecule.RecompositionMode
|
||||||
|
import app.cash.molecule.moleculeFlow
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class RoomAliasResolverPresenterTest {
|
||||||
|
@get:Rule
|
||||||
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - initial state`() = runTest {
|
||||||
|
val presenter = createPresenter()
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
assertThat(awaitItem().resolveState.isUninitialized()).isTrue()
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - resolve alias to roomId`() = runTest {
|
||||||
|
val client = FakeMatrixClient(
|
||||||
|
resolveRoomAliasResult = { Result.success(A_ROOM_ID) }
|
||||||
|
)
|
||||||
|
val presenter = createPresenter(matrixClient = client)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
assertThat(awaitItem().resolveState.isUninitialized()).isTrue()
|
||||||
|
assertThat(awaitItem().resolveState.isLoading()).isTrue()
|
||||||
|
val resultState = awaitItem()
|
||||||
|
assertThat(resultState.roomAlias).isEqualTo(A_ROOM_ALIAS)
|
||||||
|
assertThat(resultState.resolveState.dataOrNull()).isEqualTo(A_ROOM_ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - resolve alias error and retry`() = runTest {
|
||||||
|
val client = FakeMatrixClient(
|
||||||
|
resolveRoomAliasResult = { Result.failure(AN_EXCEPTION) }
|
||||||
|
)
|
||||||
|
val presenter = createPresenter(matrixClient = client)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
assertThat(awaitItem().resolveState.isUninitialized()).isTrue()
|
||||||
|
assertThat(awaitItem().resolveState.isLoading()).isTrue()
|
||||||
|
val resultState = awaitItem()
|
||||||
|
assertThat(resultState.resolveState.errorOrNull()).isEqualTo(AN_EXCEPTION)
|
||||||
|
resultState.eventSink(RoomAliasResolverEvents.Retry)
|
||||||
|
val retryLoadingState = awaitItem()
|
||||||
|
assertThat(retryLoadingState.resolveState.isLoading()).isTrue()
|
||||||
|
val retryState = awaitItem()
|
||||||
|
assertThat(retryState.resolveState.errorOrNull()).isEqualTo(AN_EXCEPTION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPresenter(
|
||||||
|
roomAlias: RoomAlias = A_ROOM_ALIAS,
|
||||||
|
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||||
|
) = RoomAliasResolverPresenter(
|
||||||
|
roomAlias = roomAlias,
|
||||||
|
matrixClient = matrixClient,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -128,7 +128,7 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
val roomMemberDetailsState = roomMemberDetailsPresenter?.present()
|
val roomMemberDetailsState = roomMemberDetailsPresenter?.present()
|
||||||
|
|
||||||
return RoomDetailsState(
|
return RoomDetailsState(
|
||||||
roomId = room.roomId.value,
|
roomId = room.roomId,
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
roomAlias = room.alias,
|
roomAlias = room.alias,
|
||||||
roomAvatarUrl = roomAvatar,
|
roomAvatarUrl = roomAvatar,
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,15 @@ package io.element.android.features.roomdetails.impl
|
||||||
|
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
|
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
|
||||||
|
|
||||||
data class RoomDetailsState(
|
data class RoomDetailsState(
|
||||||
val roomId: String,
|
val roomId: RoomId,
|
||||||
val roomName: String,
|
val roomName: String,
|
||||||
val roomAlias: String?,
|
val roomAlias: RoomAlias?,
|
||||||
val roomAvatarUrl: String?,
|
val roomAvatarUrl: String?,
|
||||||
val roomTopic: RoomTopicState,
|
val roomTopic: RoomTopicState,
|
||||||
val memberCount: Long,
|
val memberCount: Long,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||||
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
|
||||||
import io.element.android.features.roomdetails.impl.members.details.aRoomMemberDetailsState
|
import io.element.android.features.roomdetails.impl.members.details.aRoomMemberDetailsState
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
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.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
|
|
@ -71,9 +73,9 @@ fun aDmRoomMember(
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aRoomDetailsState(
|
fun aRoomDetailsState(
|
||||||
roomId: String = "a room id",
|
roomId: RoomId = RoomId("!aRoomId:domain.com"),
|
||||||
roomName: String = "Marketing",
|
roomName: String = "Marketing",
|
||||||
roomAlias: String? = "#marketing:domain.com",
|
roomAlias: RoomAlias? = RoomAlias("#marketing:domain.com"),
|
||||||
roomAvatarUrl: String? = null,
|
roomAvatarUrl: String? = null,
|
||||||
roomTopic: RoomTopicState = RoomTopicState.ExistingTopic(
|
roomTopic: RoomTopicState = RoomTopicState.ExistingTopic(
|
||||||
"Welcome to #marketing, home of the Marketing team " +
|
"Welcome to #marketing, home of the Marketing team " +
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,8 @@ 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.designsystem.utils.CommonDrawables
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
import io.element.android.libraries.matrix.api.room.getBestName
|
import io.element.android.libraries.matrix.api.room.getBestName
|
||||||
|
|
@ -302,9 +304,9 @@ private fun MainActionsSection(
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomHeaderSection(
|
private fun RoomHeaderSection(
|
||||||
avatarUrl: String?,
|
avatarUrl: String?,
|
||||||
roomId: String,
|
roomId: RoomId,
|
||||||
roomName: String,
|
roomName: String,
|
||||||
roomAlias: String?,
|
roomAlias: RoomAlias?,
|
||||||
openAvatarPreview: (url: String) -> Unit,
|
openAvatarPreview: (url: String) -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -314,7 +316,7 @@ private fun RoomHeaderSection(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Avatar(
|
Avatar(
|
||||||
avatarData = AvatarData(roomId, roomName, avatarUrl, AvatarSize.RoomHeader),
|
avatarData = AvatarData(roomId.value, roomName, avatarUrl, AvatarSize.RoomHeader),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(70.dp)
|
.size(70.dp)
|
||||||
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
|
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
|
||||||
|
|
@ -329,7 +331,7 @@ private fun RoomHeaderSection(
|
||||||
if (roomAlias != null) {
|
if (roomAlias != null) {
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
Text(
|
Text(
|
||||||
text = roomAlias,
|
text = roomAlias.value,
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,13 @@ 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
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState
|
import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class RoomMemberDetailsPresenter @AssistedInject constructor(
|
class RoomMemberDetailsPresenter @AssistedInject constructor(
|
||||||
|
|
@ -56,20 +61,24 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) }
|
var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) }
|
||||||
val roomMember by room.getRoomMemberAsState(roomMemberId)
|
val roomMember by room.getRoomMemberAsState(roomMemberId)
|
||||||
|
var userProfile by remember { mutableStateOf<MatrixUser?>(null) }
|
||||||
val startDmActionState: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
val startDmActionState: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
// the room member is not really live...
|
val isBlocked: MutableState<AsyncData<Boolean>> = remember { mutableStateOf(AsyncData.Uninitialized) }
|
||||||
val isBlocked: MutableState<AsyncData<Boolean>> = remember(roomMember) {
|
LaunchedEffect(Unit) {
|
||||||
val isIgnored = roomMember?.isIgnored
|
client.ignoredUsersFlow
|
||||||
if (isIgnored == null) {
|
.map { ignoredUsers -> roomMemberId in ignoredUsers }
|
||||||
mutableStateOf(AsyncData.Uninitialized)
|
.distinctUntilChanged()
|
||||||
} else {
|
.onEach { isBlocked.value = AsyncData.Success(it) }
|
||||||
mutableStateOf(AsyncData.Success(isIgnored))
|
.launchIn(this)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
// Update room member info when opening this screen
|
// Update room member info when opening this screen
|
||||||
// We don't need to assign the result as it will be automatically propagated by `room.getRoomMemberAsState`
|
// We don't need to assign the result as it will be automatically propagated by `room.getRoomMemberAsState`
|
||||||
room.getUpdatedMember(roomMemberId)
|
room.getUpdatedMember(roomMemberId)
|
||||||
|
.onFailure {
|
||||||
|
// Not a member of the room, try to get the user profile
|
||||||
|
userProfile = client.getProfile(roomMemberId).getOrNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleEvents(event: RoomMemberDetailsEvents) {
|
fun handleEvents(event: RoomMemberDetailsEvents) {
|
||||||
|
|
@ -105,16 +114,34 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val userName by produceState(initialValue = roomMember?.displayName) {
|
val userName: String? by produceState(
|
||||||
room.userDisplayName(roomMemberId).onSuccess { displayName ->
|
initialValue = roomMember?.displayName ?: userProfile?.displayName,
|
||||||
if (displayName != null) value = displayName
|
key1 = roomMember,
|
||||||
}
|
key2 = userProfile,
|
||||||
|
) {
|
||||||
|
value = room.userDisplayName(roomMemberId)
|
||||||
|
.fold(
|
||||||
|
onSuccess = { it },
|
||||||
|
onFailure = {
|
||||||
|
// Fallback to user profile
|
||||||
|
userProfile?.displayName
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val userAvatar by produceState(initialValue = roomMember?.avatarUrl) {
|
val userAvatar: String? by produceState(
|
||||||
room.userAvatarUrl(roomMemberId).onSuccess { avatarUrl ->
|
initialValue = roomMember?.avatarUrl ?: userProfile?.avatarUrl,
|
||||||
if (avatarUrl != null) value = avatarUrl
|
key1 = roomMember,
|
||||||
}
|
key2 = userProfile,
|
||||||
|
) {
|
||||||
|
value = room.userAvatarUrl(roomMemberId)
|
||||||
|
.fold(
|
||||||
|
onSuccess = { it },
|
||||||
|
onFailure = {
|
||||||
|
// Fallback to user profile
|
||||||
|
userProfile?.avatarUrl
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return RoomMemberDetailsState(
|
return RoomMemberDetailsState(
|
||||||
|
|
@ -124,7 +151,7 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
|
||||||
isBlocked = isBlocked.value,
|
isBlocked = isBlocked.value,
|
||||||
startDmActionState = startDmActionState.value,
|
startDmActionState = startDmActionState.value,
|
||||||
displayConfirmationDialog = confirmationDialog,
|
displayConfirmationDialog = confirmationDialog,
|
||||||
isCurrentUser = client.isMe(roomMember?.userId),
|
isCurrentUser = client.isMe(roomMemberId),
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -132,28 +159,18 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
|
||||||
private fun CoroutineScope.blockUser(userId: UserId, isBlockedState: MutableState<AsyncData<Boolean>>) = launch {
|
private fun CoroutineScope.blockUser(userId: UserId, isBlockedState: MutableState<AsyncData<Boolean>>) = launch {
|
||||||
isBlockedState.value = AsyncData.Loading(false)
|
isBlockedState.value = AsyncData.Loading(false)
|
||||||
client.ignoreUser(userId)
|
client.ignoreUser(userId)
|
||||||
.fold(
|
.onFailure {
|
||||||
onSuccess = {
|
isBlockedState.value = AsyncData.Failure(it, false)
|
||||||
isBlockedState.value = AsyncData.Success(true)
|
}
|
||||||
room.getUpdatedMember(userId)
|
// Note: on success, ignoredUserList will be updated.
|
||||||
},
|
|
||||||
onFailure = {
|
|
||||||
isBlockedState.value = AsyncData.Failure(it, false)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.unblockUser(userId: UserId, isBlockedState: MutableState<AsyncData<Boolean>>) = launch {
|
private fun CoroutineScope.unblockUser(userId: UserId, isBlockedState: MutableState<AsyncData<Boolean>>) = launch {
|
||||||
isBlockedState.value = AsyncData.Loading(true)
|
isBlockedState.value = AsyncData.Loading(true)
|
||||||
client.unignoreUser(userId)
|
client.unignoreUser(userId)
|
||||||
.fold(
|
.onFailure {
|
||||||
onSuccess = {
|
isBlockedState.value = AsyncData.Failure(it, true)
|
||||||
isBlockedState.value = AsyncData.Success(false)
|
}
|
||||||
room.getUpdatedMember(userId)
|
// Note: on success, ignoredUserList will be updated.
|
||||||
},
|
|
||||||
onFailure = {
|
|
||||||
isBlockedState.value = AsyncData.Failure(it, true)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ class RoomDetailsPresenterTests {
|
||||||
val presenter = createRoomDetailsPresenter(room)
|
val presenter = createRoomDetailsPresenter(room)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.roomId).isEqualTo(room.roomId.value)
|
assertThat(initialState.roomId).isEqualTo(room.roomId)
|
||||||
assertThat(initialState.roomName).isEqualTo(room.name)
|
assertThat(initialState.roomName).isEqualTo(room.name)
|
||||||
assertThat(initialState.roomAvatarUrl).isEqualTo(room.avatarUrl)
|
assertThat(initialState.roomAvatarUrl).isEqualTo(room.avatarUrl)
|
||||||
assertThat(initialState.roomTopic).isEqualTo(RoomTopicState.ExistingTopic(room.topic!!))
|
assertThat(initialState.roomTopic).isEqualTo(RoomTopicState.ExistingTopic(room.topic!!))
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.roomdetails.members.details
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionMode
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
|
import app.cash.turbine.ReceiveTurbine
|
||||||
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.features.createroom.api.StartDMAction
|
import io.element.android.features.createroom.api.StartDMAction
|
||||||
|
|
@ -30,12 +31,13 @@ import io.element.android.features.roomdetails.impl.members.details.RoomMemberDe
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
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
|
||||||
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.RoomMember
|
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
|
@ -58,12 +60,12 @@ class RoomMemberDetailsPresenterTests {
|
||||||
}
|
}
|
||||||
val presenter = createRoomMemberDetailsPresenter(
|
val presenter = createRoomMemberDetailsPresenter(
|
||||||
room = room,
|
room = room,
|
||||||
roomMember = roomMember
|
roomMemberId = roomMember.userId
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitFirstItem()
|
||||||
assertThat(initialState.userId).isEqualTo(roomMember.userId.value)
|
assertThat(initialState.userId).isEqualTo(roomMember.userId.value)
|
||||||
assertThat(initialState.userName).isEqualTo(roomMember.displayName)
|
assertThat(initialState.userName).isEqualTo(roomMember.displayName)
|
||||||
assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl)
|
assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl)
|
||||||
|
|
@ -85,12 +87,12 @@ class RoomMemberDetailsPresenterTests {
|
||||||
}
|
}
|
||||||
val presenter = createRoomMemberDetailsPresenter(
|
val presenter = createRoomMemberDetailsPresenter(
|
||||||
room = room,
|
room = room,
|
||||||
roomMember = roomMember
|
roomMemberId = roomMember.userId
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitFirstItem()
|
||||||
assertThat(initialState.userName).isEqualTo(roomMember.displayName)
|
assertThat(initialState.userName).isEqualTo(roomMember.displayName)
|
||||||
assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl)
|
assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl)
|
||||||
|
|
||||||
|
|
@ -108,12 +110,12 @@ class RoomMemberDetailsPresenterTests {
|
||||||
}
|
}
|
||||||
val presenter = createRoomMemberDetailsPresenter(
|
val presenter = createRoomMemberDetailsPresenter(
|
||||||
room = room,
|
room = room,
|
||||||
roomMember = roomMember
|
roomMemberId = roomMember.userId
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitFirstItem()
|
||||||
assertThat(initialState.userName).isEqualTo(roomMember.displayName)
|
assertThat(initialState.userName).isEqualTo(roomMember.displayName)
|
||||||
assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl)
|
assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl)
|
||||||
|
|
||||||
|
|
@ -121,13 +123,40 @@ class RoomMemberDetailsPresenterTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - will fallback to user profile if user is not a member of the room`() = runTest {
|
||||||
|
val bobProfile = aMatrixUser("@bob:server.org", "Bob", avatarUrl = "anAvatarUrl")
|
||||||
|
val room = aMatrixRoom().apply {
|
||||||
|
givenUserDisplayNameResult(Result.failure(Exception("Not a member!")))
|
||||||
|
givenUserAvatarUrlResult(Result.failure(Exception("Not a member!")))
|
||||||
|
}
|
||||||
|
val client = FakeMatrixClient().apply {
|
||||||
|
givenGetProfileResult(bobProfile.userId, Result.success(bobProfile))
|
||||||
|
}
|
||||||
|
val presenter = createRoomMemberDetailsPresenter(
|
||||||
|
client = client,
|
||||||
|
room = room,
|
||||||
|
roomMemberId = UserId("@bob:server.org")
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
skipItems(2)
|
||||||
|
val initialState = awaitFirstItem()
|
||||||
|
assertThat(initialState.userName).isEqualTo("Bob")
|
||||||
|
assertThat(initialState.avatarUrl).isEqualTo("anAvatarUrl")
|
||||||
|
|
||||||
|
ensureAllEventsConsumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - BlockUser needing confirmation displays confirmation dialog`() = runTest {
|
fun `present - BlockUser needing confirmation displays confirmation dialog`() = runTest {
|
||||||
val presenter = createRoomMemberDetailsPresenter()
|
val presenter = createRoomMemberDetailsPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = true))
|
initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = true))
|
||||||
|
|
||||||
val dialogState = awaitItem()
|
val dialogState = awaitItem()
|
||||||
|
|
@ -142,17 +171,24 @@ class RoomMemberDetailsPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest {
|
fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest {
|
||||||
val presenter = createRoomMemberDetailsPresenter()
|
val client = FakeMatrixClient()
|
||||||
|
val roomMember = aRoomMember()
|
||||||
|
val presenter = createRoomMemberDetailsPresenter(
|
||||||
|
client = client,
|
||||||
|
roomMemberId = roomMember.userId
|
||||||
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false))
|
initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false))
|
||||||
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
|
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
|
||||||
|
client.emitIgnoreUserList(listOf(roomMember.userId))
|
||||||
assertThat(awaitItem().isBlocked.dataOrNull()).isTrue()
|
assertThat(awaitItem().isBlocked.dataOrNull()).isTrue()
|
||||||
|
|
||||||
initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false))
|
initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false))
|
||||||
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
|
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
|
||||||
|
client.emitIgnoreUserList(listOf())
|
||||||
assertThat(awaitItem().isBlocked.dataOrNull()).isFalse()
|
assertThat(awaitItem().isBlocked.dataOrNull()).isFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -165,7 +201,7 @@ class RoomMemberDetailsPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false))
|
initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false))
|
||||||
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
|
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
|
||||||
val errorState = awaitItem()
|
val errorState = awaitItem()
|
||||||
|
|
@ -176,13 +212,32 @@ class RoomMemberDetailsPresenterTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - UnblockUser with error`() = runTest {
|
||||||
|
val matrixClient = FakeMatrixClient()
|
||||||
|
matrixClient.givenUnignoreUserResult(Result.failure(A_THROWABLE))
|
||||||
|
val presenter = createRoomMemberDetailsPresenter(client = matrixClient)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitFirstItem()
|
||||||
|
initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false))
|
||||||
|
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
|
||||||
|
val errorState = awaitItem()
|
||||||
|
assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(A_THROWABLE)
|
||||||
|
// Clear error
|
||||||
|
initialState.eventSink(RoomMemberDetailsEvents.ClearBlockUserError)
|
||||||
|
assertThat(awaitItem().isBlocked).isEqualTo(AsyncData.Success(true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - UnblockUser needing confirmation displays confirmation dialog`() = runTest {
|
fun `present - UnblockUser needing confirmation displays confirmation dialog`() = runTest {
|
||||||
val presenter = createRoomMemberDetailsPresenter()
|
val presenter = createRoomMemberDetailsPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = true))
|
initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = true))
|
||||||
|
|
||||||
val dialogState = awaitItem()
|
val dialogState = awaitItem()
|
||||||
|
|
@ -202,7 +257,7 @@ class RoomMemberDetailsPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitFirstItem()
|
||||||
assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID)
|
val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID)
|
||||||
val startDMFailureResult = AsyncAction.Failure(A_THROWABLE)
|
val startDMFailureResult = AsyncAction.Failure(A_THROWABLE)
|
||||||
|
|
@ -229,14 +284,19 @@ class RoomMemberDetailsPresenterTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
||||||
|
skipItems(1)
|
||||||
|
return awaitItem()
|
||||||
|
}
|
||||||
|
|
||||||
private fun createRoomMemberDetailsPresenter(
|
private fun createRoomMemberDetailsPresenter(
|
||||||
client: MatrixClient = FakeMatrixClient(),
|
client: MatrixClient = FakeMatrixClient(),
|
||||||
room: MatrixRoom = aMatrixRoom(),
|
room: MatrixRoom = aMatrixRoom(),
|
||||||
roomMember: RoomMember = aRoomMember(),
|
roomMemberId: UserId = UserId("@alice:server.org"),
|
||||||
startDMAction: StartDMAction = FakeStartDMAction()
|
startDMAction: StartDMAction = FakeStartDMAction()
|
||||||
): RoomMemberDetailsPresenter {
|
): RoomMemberDetailsPresenter {
|
||||||
return RoomMemberDetailsPresenter(
|
return RoomMemberDetailsPresenter(
|
||||||
roomMemberId = roomMember.userId,
|
roomMemberId = roomMemberId,
|
||||||
client = client,
|
client = client,
|
||||||
room = room,
|
room = room,
|
||||||
startDMAction = startDMAction
|
startDMAction = startDMAction
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
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.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
@ -29,7 +30,7 @@ import kotlinx.parcelize.Parcelize
|
||||||
data class RoomDescription(
|
data class RoomDescription(
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val alias: String?,
|
val alias: RoomAlias?,
|
||||||
val topic: String?,
|
val topic: String?,
|
||||||
val avatarUrl: String?,
|
val avatarUrl: String?,
|
||||||
val joinRule: JoinRule,
|
val joinRule: JoinRule,
|
||||||
|
|
@ -42,14 +43,14 @@ data class RoomDescription(
|
||||||
}
|
}
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
val computedName = name ?: alias ?: roomId.value
|
val computedName = name ?: alias?.value ?: roomId.value
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
val computedDescription: String
|
val computedDescription: String
|
||||||
get() {
|
get() {
|
||||||
return when {
|
return when {
|
||||||
topic != null -> topic
|
topic != null -> topic
|
||||||
name != null && alias != null -> alias
|
name != null && alias != null -> alias.value
|
||||||
name == null && alias == null -> ""
|
name == null && alias == null -> ""
|
||||||
else -> roomId.value
|
else -> roomId.value
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.roomdirectory.impl.root
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -69,7 +70,7 @@ fun aRoomDescriptionList(): ImmutableList<RoomDescription> {
|
||||||
roomId = RoomId("!exa:matrix.org"),
|
roomId = RoomId("!exa:matrix.org"),
|
||||||
name = "Element X Android",
|
name = "Element X Android",
|
||||||
topic = "Element X is a secure, private and decentralized messenger.",
|
topic = "Element X is a secure, private and decentralized messenger.",
|
||||||
alias = "#element-x-android:matrix.org",
|
alias = RoomAlias("#element-x-android:matrix.org"),
|
||||||
avatarUrl = null,
|
avatarUrl = null,
|
||||||
joinRule = RoomDescription.JoinRule.PUBLIC,
|
joinRule = RoomDescription.JoinRule.PUBLIC,
|
||||||
numberOfMembers = 2765,
|
numberOfMembers = 2765,
|
||||||
|
|
@ -78,7 +79,7 @@ fun aRoomDescriptionList(): ImmutableList<RoomDescription> {
|
||||||
roomId = RoomId("!exi:matrix.org"),
|
roomId = RoomId("!exi:matrix.org"),
|
||||||
name = "Element X iOS",
|
name = "Element X iOS",
|
||||||
topic = "Element X is a secure, private and decentralized messenger.",
|
topic = "Element X is a secure, private and decentralized messenger.",
|
||||||
alias = "#element-x-ios:matrix.org",
|
alias = RoomAlias("#element-x-ios:matrix.org"),
|
||||||
avatarUrl = null,
|
avatarUrl = null,
|
||||||
joinRule = RoomDescription.JoinRule.UNKNOWN,
|
joinRule = RoomDescription.JoinRule.UNKNOWN,
|
||||||
numberOfMembers = 356,
|
numberOfMembers = 356,
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ interface RoomListEntryPoint : FeatureEntryPoint {
|
||||||
fun onCreateRoomClicked()
|
fun onCreateRoomClicked()
|
||||||
fun onSettingsClicked()
|
fun onSettingsClicked()
|
||||||
fun onSessionConfirmRecoveryKeyClicked()
|
fun onSessionConfirmRecoveryKeyClicked()
|
||||||
fun onInvitesClicked()
|
|
||||||
fun onRoomSettingsClicked(roomId: RoomId)
|
fun onRoomSettingsClicked(roomId: RoomId)
|
||||||
fun onReportBugClicked()
|
fun onReportBugClicked()
|
||||||
fun onRoomDirectorySearchClicked()
|
fun onRoomDirectorySearchClicked()
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,6 @@ dependencies {
|
||||||
testImplementation(projects.libraries.indicator.impl)
|
testImplementation(projects.libraries.indicator.impl)
|
||||||
testImplementation(projects.libraries.permissions.noop)
|
testImplementation(projects.libraries.permissions.noop)
|
||||||
testImplementation(projects.libraries.preferences.test)
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.features.invite.test)
|
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.features.networkmonitor.test)
|
testImplementation(projects.features.networkmonitor.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
|
|
||||||
|
|
@ -1,81 +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.roomlist.impl
|
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.semantics.Role
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.element.android.compound.theme.ElementTheme
|
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun InvitesEntryPointView(
|
|
||||||
onInvitesClicked: () -> Unit,
|
|
||||||
state: InvitesState,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(RoundedCornerShape(8.dp))
|
|
||||||
.clickable(role = Role.Button, onClick = onInvitesClicked)
|
|
||||||
.padding(start = 24.dp, end = 16.dp)
|
|
||||||
.align(Alignment.CenterEnd)
|
|
||||||
.heightIn(min = 40.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(CommonStrings.action_invites_list),
|
|
||||||
style = ElementTheme.typography.fontBodyMdMedium,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (state == InvitesState.NewInvites) {
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
UnreadIndicatorAtom()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreviewsDayNight
|
|
||||||
@Composable
|
|
||||||
internal fun InvitesEntryPointViewPreview(@PreviewParameter(InvitesStateProvider::class) state: InvitesState) = ElementPreview {
|
|
||||||
InvitesEntryPointView(
|
|
||||||
onInvitesClicked = {},
|
|
||||||
state = state,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -33,11 +33,9 @@ open class RoomListContentStateProvider : PreviewParameterProvider<RoomListConte
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun aRoomsContentState(
|
internal fun aRoomsContentState(
|
||||||
invitesState: InvitesState = InvitesState.NoInvites,
|
|
||||||
securityBannerState: SecurityBannerState = SecurityBannerState.None,
|
securityBannerState: SecurityBannerState = SecurityBannerState.None,
|
||||||
summaries: ImmutableList<RoomListRoomSummary> = aRoomListRoomSummaryList(),
|
summaries: ImmutableList<RoomListRoomSummary> = aRoomListRoomSummaryList(),
|
||||||
) = RoomListContentState.Rooms(
|
) = RoomListContentState.Rooms(
|
||||||
invitesState = invitesState,
|
|
||||||
securityBannerState = securityBannerState,
|
securityBannerState = securityBannerState,
|
||||||
summaries = summaries,
|
summaries = summaries,
|
||||||
)
|
)
|
||||||
|
|
@ -46,6 +44,4 @@ internal fun aMigrationContentState() = RoomListContentState.Migration
|
||||||
|
|
||||||
internal fun aSkeletonContentState() = RoomListContentState.Skeleton(16)
|
internal fun aSkeletonContentState() = RoomListContentState.Skeleton(16)
|
||||||
|
|
||||||
internal fun anEmptyContentState(
|
internal fun anEmptyContentState() = RoomListContentState.Empty
|
||||||
invitesState: InvitesState = InvitesState.NoInvites,
|
|
||||||
) = RoomListContentState.Empty(invitesState)
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ sealed interface RoomListEvents {
|
||||||
data object DismissRequestVerificationPrompt : RoomListEvents
|
data object DismissRequestVerificationPrompt : RoomListEvents
|
||||||
data object DismissRecoveryKeyPrompt : RoomListEvents
|
data object DismissRecoveryKeyPrompt : RoomListEvents
|
||||||
data object ToggleSearchResults : RoomListEvents
|
data object ToggleSearchResults : RoomListEvents
|
||||||
|
data class AcceptInvite(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents
|
||||||
|
data class DeclineInvite(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents
|
||||||
data class ShowContextMenu(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents
|
data class ShowContextMenu(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents
|
||||||
|
|
||||||
sealed interface ContextMenuEvents : RoomListEvents
|
sealed interface ContextMenuEvents : RoomListEvents
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.MobileScreen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteView
|
||||||
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
||||||
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
|
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
|
||||||
import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
|
import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
|
||||||
|
|
@ -43,6 +44,7 @@ class RoomListNode @AssistedInject constructor(
|
||||||
private val presenter: RoomListPresenter,
|
private val presenter: RoomListPresenter,
|
||||||
private val inviteFriendsUseCase: InviteFriendsUseCase,
|
private val inviteFriendsUseCase: InviteFriendsUseCase,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
|
private val acceptDeclineInviteView: AcceptDeclineInviteView,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
init {
|
init {
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
|
|
@ -68,10 +70,6 @@ class RoomListNode @AssistedInject constructor(
|
||||||
plugins<RoomListEntryPoint.Callback>().forEach { it.onSessionConfirmRecoveryKeyClicked() }
|
plugins<RoomListEntryPoint.Callback>().forEach { it.onSessionConfirmRecoveryKeyClicked() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onInvitesClicked() {
|
|
||||||
plugins<RoomListEntryPoint.Callback>().forEach { it.onInvitesClicked() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onRoomSettingsClicked(roomId: RoomId) {
|
private fun onRoomSettingsClicked(roomId: RoomId) {
|
||||||
plugins<RoomListEntryPoint.Callback>().forEach { it.onRoomSettingsClicked(roomId) }
|
plugins<RoomListEntryPoint.Callback>().forEach { it.onRoomSettingsClicked(roomId) }
|
||||||
}
|
}
|
||||||
|
|
@ -101,11 +99,17 @@ class RoomListNode @AssistedInject constructor(
|
||||||
onSettingsClicked = this::onOpenSettings,
|
onSettingsClicked = this::onOpenSettings,
|
||||||
onCreateRoomClicked = this::onCreateRoomClicked,
|
onCreateRoomClicked = this::onCreateRoomClicked,
|
||||||
onConfirmRecoveryKeyClicked = this::onSessionConfirmRecoveryKeyClicked,
|
onConfirmRecoveryKeyClicked = this::onSessionConfirmRecoveryKeyClicked,
|
||||||
onInvitesClicked = this::onInvitesClicked,
|
|
||||||
onRoomSettingsClicked = this::onRoomSettingsClicked,
|
onRoomSettingsClicked = this::onRoomSettingsClicked,
|
||||||
onMenuActionClicked = { onMenuActionClicked(activity, it) },
|
onMenuActionClicked = { onMenuActionClicked(activity, it) },
|
||||||
onRoomDirectorySearchClicked = this::onRoomDirectorySearchClicked,
|
onRoomDirectorySearchClicked = this::onRoomDirectorySearchClicked,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
) {
|
||||||
|
acceptDeclineInviteView.Render(
|
||||||
|
state = state.acceptDeclineInviteState,
|
||||||
|
onInviteAccepted = this::onRoomClicked,
|
||||||
|
onInviteDeclined = { },
|
||||||
|
modifier = Modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.roomlist.impl
|
package io.element.android.features.roomlist.impl
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
|
|
@ -32,15 +33,18 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import im.vector.app.features.analytics.plan.Interaction
|
import im.vector.app.features.analytics.plan.Interaction
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
|
import io.element.android.features.invite.api.response.InviteData
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
||||||
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.store.SessionPreferencesStore
|
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||||
import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource
|
|
||||||
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
||||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||||
import io.element.android.features.roomlist.impl.migration.MigrationScreenState
|
import io.element.android.features.roomlist.impl.migration.MigrationScreenState
|
||||||
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
|
import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
@ -79,7 +83,6 @@ class RoomListPresenter @Inject constructor(
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
private val inviteStateDataSource: InviteStateDataSource,
|
|
||||||
private val leaveRoomPresenter: LeaveRoomPresenter,
|
private val leaveRoomPresenter: LeaveRoomPresenter,
|
||||||
private val roomListDataSource: RoomListDataSource,
|
private val roomListDataSource: RoomListDataSource,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
|
@ -89,6 +92,7 @@ class RoomListPresenter @Inject constructor(
|
||||||
private val migrationScreenPresenter: Presenter<MigrationScreenState>,
|
private val migrationScreenPresenter: Presenter<MigrationScreenState>,
|
||||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
|
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||||
) : Presenter<RoomListState> {
|
) : Presenter<RoomListState> {
|
||||||
private val encryptionService: EncryptionService = client.encryptionService()
|
private val encryptionService: EncryptionService = client.encryptionService()
|
||||||
private val syncService: SyncService = client.syncService()
|
private val syncService: SyncService = client.syncService()
|
||||||
|
|
@ -101,6 +105,7 @@ class RoomListPresenter @Inject constructor(
|
||||||
val networkConnectionStatus by networkMonitor.connectivity.collectAsState()
|
val networkConnectionStatus by networkMonitor.connectivity.collectAsState()
|
||||||
val filtersState = filtersPresenter.present()
|
val filtersState = filtersPresenter.present()
|
||||||
val searchState = searchPresenter.present()
|
val searchState = searchPresenter.present()
|
||||||
|
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
roomListDataSource.launchIn(this)
|
roomListDataSource.launchIn(this)
|
||||||
|
|
@ -131,6 +136,16 @@ class RoomListPresenter @Inject constructor(
|
||||||
is RoomListEvents.SetRoomIsFavorite -> coroutineScope.setRoomIsFavorite(event.roomId, event.isFavorite)
|
is RoomListEvents.SetRoomIsFavorite -> coroutineScope.setRoomIsFavorite(event.roomId, event.isFavorite)
|
||||||
is RoomListEvents.MarkAsRead -> coroutineScope.markAsRead(event.roomId)
|
is RoomListEvents.MarkAsRead -> coroutineScope.markAsRead(event.roomId)
|
||||||
is RoomListEvents.MarkAsUnread -> coroutineScope.markAsUnread(event.roomId)
|
is RoomListEvents.MarkAsUnread -> coroutineScope.markAsUnread(event.roomId)
|
||||||
|
is RoomListEvents.AcceptInvite -> {
|
||||||
|
acceptDeclineInviteState.eventSink(
|
||||||
|
AcceptDeclineInviteEvents.AcceptInvite(event.roomListRoomSummary.toInviteData())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is RoomListEvents.DeclineInvite -> {
|
||||||
|
acceptDeclineInviteState.eventSink(
|
||||||
|
AcceptDeclineInviteEvents.DeclineInvite(event.roomListRoomSummary.toInviteData())
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,6 +163,7 @@ class RoomListPresenter @Inject constructor(
|
||||||
filtersState = filtersState,
|
filtersState = filtersState,
|
||||||
searchState = searchState,
|
searchState = searchState,
|
||||||
contentState = contentState,
|
contentState = contentState,
|
||||||
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -192,16 +208,11 @@ class RoomListPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
return when {
|
return when {
|
||||||
showMigration -> RoomListContentState.Migration
|
showMigration -> RoomListContentState.Migration
|
||||||
showEmpty -> {
|
showEmpty -> RoomListContentState.Empty
|
||||||
val invitesState = inviteStateDataSource.inviteState()
|
|
||||||
RoomListContentState.Empty(invitesState)
|
|
||||||
}
|
|
||||||
showSkeleton -> RoomListContentState.Skeleton(count = 16)
|
showSkeleton -> RoomListContentState.Skeleton(count = 16)
|
||||||
else -> {
|
else -> {
|
||||||
val invitesState = inviteStateDataSource.inviteState()
|
|
||||||
val securityBannerState by securityBannerState(securityBannerDismissed)
|
val securityBannerState by securityBannerState(securityBannerDismissed)
|
||||||
RoomListContentState.Rooms(
|
RoomListContentState.Rooms(
|
||||||
invitesState = invitesState,
|
|
||||||
securityBannerState = securityBannerState,
|
securityBannerState = securityBannerState,
|
||||||
summaries = roomSummaries.dataOrNull().orEmpty().toPersistentList()
|
summaries = roomSummaries.dataOrNull().orEmpty().toPersistentList()
|
||||||
)
|
)
|
||||||
|
|
@ -283,3 +294,10 @@ class RoomListPresenter @Inject constructor(
|
||||||
client.roomListService.updateAllRoomsVisibleRange(extendedRange)
|
client.roomListService.updateAllRoomsVisibleRange(extendedRange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun RoomListRoomSummary.toInviteData() = InviteData(
|
||||||
|
roomId = roomId,
|
||||||
|
roomName = name,
|
||||||
|
isDirect = isDirect,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.features.roomlist.impl
|
package io.element.android.features.roomlist.impl
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
|
|
@ -37,6 +38,7 @@ data class RoomListState(
|
||||||
val filtersState: RoomListFiltersState,
|
val filtersState: RoomListFiltersState,
|
||||||
val searchState: RoomListSearchState,
|
val searchState: RoomListSearchState,
|
||||||
val contentState: RoomListContentState,
|
val contentState: RoomListContentState,
|
||||||
|
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
||||||
val eventSink: (RoomListEvents) -> Unit,
|
val eventSink: (RoomListEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
val displayFilters = filtersState.isFeatureEnabled && contentState is RoomListContentState.Rooms
|
val displayFilters = filtersState.isFeatureEnabled && contentState is RoomListContentState.Rooms
|
||||||
|
|
@ -70,9 +72,8 @@ enum class SecurityBannerState {
|
||||||
sealed interface RoomListContentState {
|
sealed interface RoomListContentState {
|
||||||
data object Migration : RoomListContentState
|
data object Migration : RoomListContentState
|
||||||
data class Skeleton(val count: Int) : RoomListContentState
|
data class Skeleton(val count: Int) : RoomListContentState
|
||||||
data class Empty(val invitesState: InvitesState) : RoomListContentState
|
data object Empty : RoomListContentState
|
||||||
data class Rooms(
|
data class Rooms(
|
||||||
val invitesState: InvitesState,
|
|
||||||
val securityBannerState: SecurityBannerState,
|
val securityBannerState: SecurityBannerState,
|
||||||
val summaries: ImmutableList<RoomListRoomSummary>,
|
val summaries: ImmutableList<RoomListRoomSummary>,
|
||||||
) : RoomListContentState
|
) : RoomListContentState
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,15 @@
|
||||||
package io.element.android.features.roomlist.impl
|
package io.element.android.features.roomlist.impl
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
|
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||||
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
||||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||||
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
|
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
|
||||||
|
import io.element.android.features.roomlist.impl.model.InviteSender
|
||||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
|
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
|
||||||
import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary
|
import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
||||||
import io.element.android.features.roomlist.impl.search.aRoomListSearchState
|
import io.element.android.features.roomlist.impl.search.aRoomListSearchState
|
||||||
|
|
@ -41,8 +45,6 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
||||||
aRoomListState(),
|
aRoomListState(),
|
||||||
aRoomListState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)),
|
aRoomListState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)),
|
||||||
aRoomListState(hasNetworkConnection = false),
|
aRoomListState(hasNetworkConnection = false),
|
||||||
aRoomListState(contentState = aRoomsContentState(invitesState = InvitesState.SeenInvites)),
|
|
||||||
aRoomListState(contentState = aRoomsContentState(invitesState = InvitesState.NewInvites)),
|
|
||||||
aRoomListState(contextMenu = aContextMenuShown(roomName = "A nice room name")),
|
aRoomListState(contextMenu = aContextMenuShown(roomName = "A nice room name")),
|
||||||
aRoomListState(contextMenu = aContextMenuShown(isFavorite = true)),
|
aRoomListState(contextMenu = aContextMenuShown(isFavorite = true)),
|
||||||
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)),
|
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)),
|
||||||
|
|
@ -64,6 +66,7 @@ internal fun aRoomListState(
|
||||||
searchState: RoomListSearchState = aRoomListSearchState(),
|
searchState: RoomListSearchState = aRoomListSearchState(),
|
||||||
filtersState: RoomListFiltersState = aRoomListFiltersState(isFeatureEnabled = false),
|
filtersState: RoomListFiltersState = aRoomListFiltersState(isFeatureEnabled = false),
|
||||||
contentState: RoomListContentState = aRoomsContentState(),
|
contentState: RoomListContentState = aRoomsContentState(),
|
||||||
|
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
||||||
eventSink: (RoomListEvents) -> Unit = {}
|
eventSink: (RoomListEvents) -> Unit = {}
|
||||||
) = RoomListState(
|
) = RoomListState(
|
||||||
matrixUser = matrixUser,
|
matrixUser = matrixUser,
|
||||||
|
|
@ -75,11 +78,23 @@ internal fun aRoomListState(
|
||||||
filtersState = filtersState,
|
filtersState = filtersState,
|
||||||
searchState = searchState,
|
searchState = searchState,
|
||||||
contentState = contentState,
|
contentState = contentState,
|
||||||
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
|
internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
|
||||||
return persistentListOf(
|
return persistentListOf(
|
||||||
|
aRoomListRoomSummary(
|
||||||
|
name = "Room Invited",
|
||||||
|
avatarData = AvatarData("!roomId", "Room with Alice and Bob", size = AvatarSize.RoomListItem),
|
||||||
|
id = "!roomId:domain",
|
||||||
|
inviteSender = InviteSender(
|
||||||
|
userId = UserId("@bob:domain"),
|
||||||
|
displayName = "Bob",
|
||||||
|
avatarData = AvatarData("@bob:domain", "Bob", size = AvatarSize.InviteSender),
|
||||||
|
),
|
||||||
|
displayType = RoomSummaryDisplayType.INVITE,
|
||||||
|
),
|
||||||
aRoomListRoomSummary(
|
aRoomListRoomSummary(
|
||||||
name = "Room",
|
name = "Room",
|
||||||
numberOfUnreadMessages = 1,
|
numberOfUnreadMessages = 1,
|
||||||
|
|
@ -98,11 +113,11 @@ internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
|
||||||
),
|
),
|
||||||
aRoomListRoomSummary(
|
aRoomListRoomSummary(
|
||||||
id = "!roomId3:domain",
|
id = "!roomId3:domain",
|
||||||
isPlaceholder = true,
|
displayType = RoomSummaryDisplayType.PLACEHOLDER,
|
||||||
),
|
),
|
||||||
aRoomListRoomSummary(
|
aRoomListRoomSummary(
|
||||||
id = "!roomId4:domain",
|
id = "!roomId4:domain",
|
||||||
isPlaceholder = true,
|
displayType = RoomSummaryDisplayType.PLACEHOLDER,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,23 +55,17 @@ fun RoomListView(
|
||||||
onSettingsClicked: () -> Unit,
|
onSettingsClicked: () -> Unit,
|
||||||
onConfirmRecoveryKeyClicked: () -> Unit,
|
onConfirmRecoveryKeyClicked: () -> Unit,
|
||||||
onCreateRoomClicked: () -> Unit,
|
onCreateRoomClicked: () -> Unit,
|
||||||
onInvitesClicked: () -> Unit,
|
|
||||||
onRoomSettingsClicked: (roomId: RoomId) -> Unit,
|
onRoomSettingsClicked: (roomId: RoomId) -> Unit,
|
||||||
onMenuActionClicked: (RoomListMenuAction) -> Unit,
|
onMenuActionClicked: (RoomListMenuAction) -> Unit,
|
||||||
onRoomDirectorySearchClicked: () -> Unit,
|
onRoomDirectorySearchClicked: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
acceptDeclineInviteView: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
ConnectivityIndicatorContainer(
|
ConnectivityIndicatorContainer(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
isOnline = state.hasNetworkConnection,
|
isOnline = state.hasNetworkConnection,
|
||||||
) { topPadding ->
|
) { topPadding ->
|
||||||
Box {
|
Box {
|
||||||
fun onRoomLongClicked(
|
|
||||||
roomListRoomSummary: RoomListRoomSummary
|
|
||||||
) {
|
|
||||||
state.eventSink(RoomListEvents.ShowContextMenu(roomListRoomSummary))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.contextMenu is RoomListState.ContextMenu.Shown) {
|
if (state.contextMenu is RoomListState.ContextMenu.Shown) {
|
||||||
RoomListContextMenu(
|
RoomListContextMenu(
|
||||||
contextMenu = state.contextMenu,
|
contextMenu = state.contextMenu,
|
||||||
|
|
@ -83,21 +77,19 @@ fun RoomListView(
|
||||||
LeaveRoomView(state = state.leaveRoomState)
|
LeaveRoomView(state = state.leaveRoomState)
|
||||||
|
|
||||||
RoomListScaffold(
|
RoomListScaffold(
|
||||||
modifier = Modifier.padding(top = topPadding),
|
|
||||||
state = state,
|
state = state,
|
||||||
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
||||||
onRoomClicked = onRoomClicked,
|
onRoomClicked = onRoomClicked,
|
||||||
onRoomLongClicked = { onRoomLongClicked(it) },
|
|
||||||
onOpenSettings = onSettingsClicked,
|
onOpenSettings = onSettingsClicked,
|
||||||
onCreateRoomClicked = onCreateRoomClicked,
|
onCreateRoomClicked = onCreateRoomClicked,
|
||||||
onInvitesClicked = onInvitesClicked,
|
|
||||||
onMenuActionClicked = onMenuActionClicked,
|
onMenuActionClicked = onMenuActionClicked,
|
||||||
|
modifier = Modifier.padding(top = topPadding),
|
||||||
)
|
)
|
||||||
// This overlaid view will only be visible when state.displaySearchResults is true
|
// This overlaid view will only be visible when state.displaySearchResults is true
|
||||||
RoomListSearchView(
|
RoomListSearchView(
|
||||||
state = state.searchState,
|
state = state.searchState,
|
||||||
|
eventSink = state.eventSink,
|
||||||
onRoomClicked = onRoomClicked,
|
onRoomClicked = onRoomClicked,
|
||||||
onRoomLongClicked = { onRoomLongClicked(it) },
|
|
||||||
onRoomDirectorySearchClicked = onRoomDirectorySearchClicked,
|
onRoomDirectorySearchClicked = onRoomDirectorySearchClicked,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.statusBarsPadding()
|
.statusBarsPadding()
|
||||||
|
|
@ -105,6 +97,7 @@ fun RoomListView(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.background)
|
.background(MaterialTheme.colorScheme.background)
|
||||||
)
|
)
|
||||||
|
acceptDeclineInviteView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -115,10 +108,8 @@ private fun RoomListScaffold(
|
||||||
state: RoomListState,
|
state: RoomListState,
|
||||||
onConfirmRecoveryKeyClicked: () -> Unit,
|
onConfirmRecoveryKeyClicked: () -> Unit,
|
||||||
onRoomClicked: (RoomId) -> Unit,
|
onRoomClicked: (RoomId) -> Unit,
|
||||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
|
||||||
onOpenSettings: () -> Unit,
|
onOpenSettings: () -> Unit,
|
||||||
onCreateRoomClicked: () -> Unit,
|
onCreateRoomClicked: () -> Unit,
|
||||||
onInvitesClicked: () -> Unit,
|
|
||||||
onMenuActionClicked: (RoomListMenuAction) -> Unit,
|
onMenuActionClicked: (RoomListMenuAction) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -153,9 +144,7 @@ private fun RoomListScaffold(
|
||||||
eventSink = state.eventSink,
|
eventSink = state.eventSink,
|
||||||
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
||||||
onRoomClicked = ::onRoomClicked,
|
onRoomClicked = ::onRoomClicked,
|
||||||
onRoomLongClicked = onRoomLongClicked,
|
|
||||||
onCreateRoomClicked = onCreateRoomClicked,
|
onCreateRoomClicked = onCreateRoomClicked,
|
||||||
onInvitesClicked = onInvitesClicked,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.consumeWindowInsets(padding)
|
.consumeWindowInsets(padding)
|
||||||
|
|
@ -180,7 +169,7 @@ private fun RoomListScaffold(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun RoomListRoomSummary.contentType() = isPlaceholder
|
internal fun RoomListRoomSummary.contentType() = displayType.ordinal
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -191,9 +180,9 @@ internal fun RoomListViewPreview(@PreviewParameter(RoomListStateProvider::class)
|
||||||
onSettingsClicked = {},
|
onSettingsClicked = {},
|
||||||
onConfirmRecoveryKeyClicked = {},
|
onConfirmRecoveryKeyClicked = {},
|
||||||
onCreateRoomClicked = {},
|
onCreateRoomClicked = {},
|
||||||
onInvitesClicked = {},
|
|
||||||
onRoomSettingsClicked = {},
|
onRoomSettingsClicked = {},
|
||||||
onMenuActionClicked = {},
|
onMenuActionClicked = {},
|
||||||
onRoomDirectorySearchClicked = {},
|
onRoomDirectorySearchClicked = {},
|
||||||
|
acceptDeclineInviteView = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,6 @@ import androidx.compose.ui.unit.Velocity
|
||||||
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.roomlist.impl.InvitesEntryPointView
|
|
||||||
import io.element.android.features.roomlist.impl.InvitesState
|
|
||||||
import io.element.android.features.roomlist.impl.R
|
import io.element.android.features.roomlist.impl.R
|
||||||
import io.element.android.features.roomlist.impl.RoomListContentState
|
import io.element.android.features.roomlist.impl.RoomListContentState
|
||||||
import io.element.android.features.roomlist.impl.RoomListContentStateProvider
|
import io.element.android.features.roomlist.impl.RoomListContentStateProvider
|
||||||
|
|
@ -75,9 +73,7 @@ fun RoomListContentView(
|
||||||
eventSink: (RoomListEvents) -> Unit,
|
eventSink: (RoomListEvents) -> Unit,
|
||||||
onConfirmRecoveryKeyClicked: () -> Unit,
|
onConfirmRecoveryKeyClicked: () -> Unit,
|
||||||
onRoomClicked: (RoomListRoomSummary) -> Unit,
|
onRoomClicked: (RoomListRoomSummary) -> Unit,
|
||||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
|
||||||
onCreateRoomClicked: () -> Unit,
|
onCreateRoomClicked: () -> Unit,
|
||||||
onInvitesClicked: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
|
|
@ -92,8 +88,6 @@ fun RoomListContentView(
|
||||||
}
|
}
|
||||||
is RoomListContentState.Empty -> {
|
is RoomListContentState.Empty -> {
|
||||||
EmptyView(
|
EmptyView(
|
||||||
state = contentState,
|
|
||||||
onInvitesClicked = onInvitesClicked,
|
|
||||||
onCreateRoomClicked = onCreateRoomClicked,
|
onCreateRoomClicked = onCreateRoomClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -104,8 +98,6 @@ fun RoomListContentView(
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
||||||
onRoomClicked = onRoomClicked,
|
onRoomClicked = onRoomClicked,
|
||||||
onRoomLongClicked = onRoomLongClicked,
|
|
||||||
onInvitesClicked = onInvitesClicked,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -128,30 +120,21 @@ private fun SkeletonView(count: Int, modifier: Modifier = Modifier) {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EmptyView(
|
private fun EmptyView(
|
||||||
state: RoomListContentState.Empty,
|
|
||||||
onCreateRoomClicked: () -> Unit,
|
onCreateRoomClicked: () -> Unit,
|
||||||
onInvitesClicked: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Box(
|
EmptyScaffold(
|
||||||
|
title = R.string.screen_roomlist_empty_title,
|
||||||
|
subtitle = R.string.screen_roomlist_empty_message,
|
||||||
|
action = {
|
||||||
|
Button(
|
||||||
|
text = stringResource(CommonStrings.action_start_chat),
|
||||||
|
leadingIcon = IconSource.Vector(CompoundIcons.Compose()),
|
||||||
|
onClick = onCreateRoomClicked,
|
||||||
|
)
|
||||||
|
},
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
) {
|
)
|
||||||
if (state.invitesState != InvitesState.NoInvites) {
|
|
||||||
InvitesEntryPointView(onInvitesClicked, state.invitesState)
|
|
||||||
}
|
|
||||||
EmptyScaffold(
|
|
||||||
title = R.string.screen_roomlist_empty_title,
|
|
||||||
subtitle = R.string.screen_roomlist_empty_message,
|
|
||||||
action = {
|
|
||||||
Button(
|
|
||||||
text = stringResource(CommonStrings.action_start_chat),
|
|
||||||
leadingIcon = IconSource.Vector(CompoundIcons.Compose()),
|
|
||||||
onClick = onCreateRoomClicked,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -161,8 +144,6 @@ private fun RoomsView(
|
||||||
eventSink: (RoomListEvents) -> Unit,
|
eventSink: (RoomListEvents) -> Unit,
|
||||||
onConfirmRecoveryKeyClicked: () -> Unit,
|
onConfirmRecoveryKeyClicked: () -> Unit,
|
||||||
onRoomClicked: (RoomListRoomSummary) -> Unit,
|
onRoomClicked: (RoomListRoomSummary) -> Unit,
|
||||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
|
||||||
onInvitesClicked: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) {
|
if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) {
|
||||||
|
|
@ -176,8 +157,6 @@ private fun RoomsView(
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
||||||
onRoomClicked = onRoomClicked,
|
onRoomClicked = onRoomClicked,
|
||||||
onRoomLongClicked = onRoomLongClicked,
|
|
||||||
onInvitesClicked = onInvitesClicked,
|
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -189,8 +168,6 @@ private fun RoomsViewList(
|
||||||
eventSink: (RoomListEvents) -> Unit,
|
eventSink: (RoomListEvents) -> Unit,
|
||||||
onConfirmRecoveryKeyClicked: () -> Unit,
|
onConfirmRecoveryKeyClicked: () -> Unit,
|
||||||
onRoomClicked: (RoomListRoomSummary) -> Unit,
|
onRoomClicked: (RoomListRoomSummary) -> Unit,
|
||||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
|
||||||
onInvitesClicked: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
|
|
@ -228,11 +205,6 @@ private fun RoomsViewList(
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.invitesState != InvitesState.NoInvites) {
|
|
||||||
item {
|
|
||||||
InvitesEntryPointView(onInvitesClicked, state.invitesState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Note: do not use a key for the LazyColumn, or the scroll will not behave as expected if a room
|
// Note: do not use a key for the LazyColumn, or the scroll will not behave as expected if a room
|
||||||
// is moved to the top of the list.
|
// is moved to the top of the list.
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
|
|
@ -242,7 +214,7 @@ private fun RoomsViewList(
|
||||||
RoomSummaryRow(
|
RoomSummaryRow(
|
||||||
room = room,
|
room = room,
|
||||||
onClick = onRoomClicked,
|
onClick = onRoomClicked,
|
||||||
onLongClick = onRoomLongClicked,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
if (index != state.summaries.lastIndex) {
|
if (index != state.summaries.lastIndex) {
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
|
|
@ -305,8 +277,6 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
onConfirmRecoveryKeyClicked = {},
|
onConfirmRecoveryKeyClicked = {},
|
||||||
onRoomClicked = {},
|
onRoomClicked = {},
|
||||||
onRoomLongClicked = {},
|
|
||||||
onCreateRoomClicked = {},
|
onCreateRoomClicked = {},
|
||||||
onInvitesClicked = {}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,15 +20,18 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.Spacer
|
||||||
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.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
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.layout.width
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -36,26 +39,36 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
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.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.roomlist.impl.RoomListEvents
|
||||||
|
import io.element.android.features.roomlist.impl.model.InviteSender
|
||||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryProvider
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryProvider
|
||||||
|
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
|
||||||
import io.element.android.libraries.core.extensions.orEmpty
|
import io.element.android.libraries.core.extensions.orEmpty
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
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.Button
|
||||||
|
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.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||||
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.roomListRoomMessage
|
import io.element.android.libraries.designsystem.theme.roomListRoomMessage
|
||||||
import io.element.android.libraries.designsystem.theme.roomListRoomMessageDate
|
import io.element.android.libraries.designsystem.theme.roomListRoomMessageDate
|
||||||
import io.element.android.libraries.designsystem.theme.roomListRoomName
|
import io.element.android.libraries.designsystem.theme.roomListRoomName
|
||||||
import io.element.android.libraries.designsystem.theme.unreadIndicator
|
import io.element.android.libraries.designsystem.theme.unreadIndicator
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
internal val minHeight = 84.dp
|
internal val minHeight = 84.dp
|
||||||
|
|
||||||
|
|
@ -63,30 +76,67 @@ internal val minHeight = 84.dp
|
||||||
internal fun RoomSummaryRow(
|
internal fun RoomSummaryRow(
|
||||||
room: RoomListRoomSummary,
|
room: RoomListRoomSummary,
|
||||||
onClick: (RoomListRoomSummary) -> Unit,
|
onClick: (RoomListRoomSummary) -> Unit,
|
||||||
onLongClick: (RoomListRoomSummary) -> Unit,
|
eventSink: (RoomListEvents) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (room.isPlaceholder) {
|
when (room.displayType) {
|
||||||
RoomSummaryPlaceholderRow(
|
RoomSummaryDisplayType.PLACEHOLDER -> {
|
||||||
modifier = modifier,
|
RoomSummaryPlaceholderRow(modifier = modifier)
|
||||||
)
|
}
|
||||||
} else {
|
RoomSummaryDisplayType.INVITE -> {
|
||||||
RoomSummaryRealRow(
|
RoomSummaryScaffoldRow(
|
||||||
room = room,
|
room = room,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = {
|
||||||
modifier = modifier
|
Timber.d("Long click on invite room")
|
||||||
)
|
},
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
InviteNameAndIndicatorRow(name = room.name)
|
||||||
|
InviteSubtitle(isDirect = room.isDirect, inviteSender = room.inviteSender, canonicalAlias = room.canonicalAlias)
|
||||||
|
if (!room.isDirect && room.inviteSender != null) {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
InviteSenderRow(sender = room.inviteSender)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
InviteButtonsRow(
|
||||||
|
onAcceptClicked = {
|
||||||
|
eventSink(RoomListEvents.AcceptInvite(room))
|
||||||
|
},
|
||||||
|
onDeclineClicked = {
|
||||||
|
eventSink(RoomListEvents.DeclineInvite(room))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RoomSummaryDisplayType.ROOM -> {
|
||||||
|
RoomSummaryScaffoldRow(
|
||||||
|
room = room,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = {
|
||||||
|
eventSink(RoomListEvents.ShowContextMenu(room))
|
||||||
|
},
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
NameAndTimestampRow(
|
||||||
|
name = room.name,
|
||||||
|
timestamp = room.timestamp,
|
||||||
|
isHighlighted = room.isHighlighted
|
||||||
|
)
|
||||||
|
LastMessageAndIndicatorRow(room = room)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomSummaryRealRow(
|
private fun RoomSummaryScaffoldRow(
|
||||||
room: RoomListRoomSummary,
|
room: RoomListRoomSummary,
|
||||||
onClick: (RoomListRoomSummary) -> Unit,
|
onClick: (RoomListRoomSummary) -> Unit,
|
||||||
onLongClick: (RoomListRoomSummary) -> Unit,
|
onLongClick: (RoomListRoomSummary) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
) {
|
) {
|
||||||
val clickModifier = Modifier.combinedClickable(
|
val clickModifier = Modifier.combinedClickable(
|
||||||
onClick = { onClick(room) },
|
onClick = { onClick(room) },
|
||||||
|
|
@ -100,94 +150,186 @@ private fun RoomSummaryRealRow(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(min = minHeight)
|
.heightIn(min = minHeight)
|
||||||
.then(clickModifier)
|
.then(clickModifier)
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 11.dp)
|
.padding(horizontal = 16.dp, vertical = 11.dp)
|
||||||
.height(IntrinsicSize.Min),
|
.height(IntrinsicSize.Min),
|
||||||
) {
|
) {
|
||||||
Avatar(
|
Avatar(room.avatarData)
|
||||||
room
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
.avatarData,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.CenterVertically)
|
|
||||||
)
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth()
|
content = content,
|
||||||
.padding(start = 16.dp)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NameAndTimestampRow(
|
||||||
|
name: String,
|
||||||
|
timestamp: String?,
|
||||||
|
isHighlighted: Boolean,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
// Name
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
|
text = name,
|
||||||
|
color = MaterialTheme.roomListRoomName(),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
// Timestamp
|
||||||
|
Text(
|
||||||
|
text = timestamp ?: "",
|
||||||
|
style = ElementTheme.typography.fontBodySmMedium,
|
||||||
|
color = if (isHighlighted) {
|
||||||
|
ElementTheme.colors.unreadIndicator
|
||||||
|
} else {
|
||||||
|
MaterialTheme.roomListRoomMessageDate()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InviteSubtitle(
|
||||||
|
isDirect: Boolean,
|
||||||
|
inviteSender: InviteSender?,
|
||||||
|
canonicalAlias: RoomAlias?,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val subtitle = if (isDirect) {
|
||||||
|
inviteSender?.userId?.value
|
||||||
|
} else {
|
||||||
|
canonicalAlias?.value
|
||||||
|
}
|
||||||
|
if (subtitle != null) {
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
color = MaterialTheme.roomListRoomMessage(),
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LastMessageAndIndicatorRow(
|
||||||
|
room: RoomListRoomSummary,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = spacedBy(28.dp)
|
||||||
|
) {
|
||||||
|
// Last Message
|
||||||
|
val attributedLastMessage = room.lastMessage as? AnnotatedString
|
||||||
|
?: AnnotatedString(room.lastMessage.orEmpty().toString())
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
text = attributedLastMessage,
|
||||||
|
color = MaterialTheme.roomListRoomMessage(),
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
minLines = 2,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
// Call and unread
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.height(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
val tint = if (room.isHighlighted) ElementTheme.colors.unreadIndicator else ElementTheme.colors.iconQuaternary
|
||||||
NameAndTimestampRow(room = room)
|
if (room.hasRoomCall) {
|
||||||
|
OnGoingCallIcon(
|
||||||
|
color = tint,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
if (room.userDefinedNotificationMode == RoomNotificationMode.MUTE) {
|
||||||
LastMessageAndIndicatorRow(room = room)
|
NotificationOffIndicatorAtom()
|
||||||
|
} else if (room.numberOfUnreadMentions > 0) {
|
||||||
|
MentionIndicatorAtom()
|
||||||
|
}
|
||||||
|
if (room.hasNewContent) {
|
||||||
|
UnreadIndicatorAtom(
|
||||||
|
color = tint
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RowScope.NameAndTimestampRow(room: RoomListRoomSummary) {
|
private fun InviteNameAndIndicatorRow(
|
||||||
// Name
|
name: String,
|
||||||
Text(
|
modifier: Modifier = Modifier,
|
||||||
modifier = Modifier
|
) {
|
||||||
.weight(1f)
|
Row(
|
||||||
.padding(end = 16.dp),
|
modifier = modifier.fillMaxWidth(),
|
||||||
style = ElementTheme.typography.fontBodyLgMedium,
|
horizontalArrangement = spacedBy(16.dp),
|
||||||
text = room.name,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
color = MaterialTheme.roomListRoomName(),
|
) {
|
||||||
maxLines = 1,
|
Text(
|
||||||
overflow = TextOverflow.Ellipsis
|
modifier = Modifier.weight(1f),
|
||||||
)
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
// Timestamp
|
text = name,
|
||||||
Text(
|
color = MaterialTheme.roomListRoomName(),
|
||||||
text = room.timestamp ?: "",
|
maxLines = 1,
|
||||||
style = ElementTheme.typography.fontBodySmMedium,
|
overflow = TextOverflow.Ellipsis
|
||||||
color = if (room.isHighlighted) {
|
)
|
||||||
ElementTheme.colors.unreadIndicator
|
UnreadIndicatorAtom(
|
||||||
} else {
|
color = ElementTheme.colors.unreadIndicator
|
||||||
MaterialTheme.roomListRoomMessageDate()
|
)
|
||||||
},
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RowScope.LastMessageAndIndicatorRow(room: RoomListRoomSummary) {
|
private fun InviteSenderRow(
|
||||||
// Last Message
|
sender: InviteSender,
|
||||||
val attributedLastMessage = room.lastMessage as? AnnotatedString
|
modifier: Modifier = Modifier
|
||||||
?: AnnotatedString(room.lastMessage.orEmpty().toString())
|
) {
|
||||||
Text(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(end = 28.dp),
|
|
||||||
text = attributedLastMessage,
|
|
||||||
color = MaterialTheme.roomListRoomMessage(),
|
|
||||||
style = ElementTheme.typography.fontBodyMdRegular,
|
|
||||||
minLines = 2,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
// Call and unread
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.height(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
modifier = modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
) {
|
||||||
val tint = if (room.isHighlighted) ElementTheme.colors.unreadIndicator else ElementTheme.colors.iconQuaternary
|
Avatar(avatarData = sender.avatarData)
|
||||||
if (room.hasRoomCall) {
|
Text(
|
||||||
OnGoingCallIcon(
|
text = sender.annotatedString(),
|
||||||
color = tint,
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
)
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
}
|
)
|
||||||
if (room.userDefinedNotificationMode == RoomNotificationMode.MUTE) {
|
}
|
||||||
NotificationOffIndicatorAtom()
|
}
|
||||||
} else if (room.numberOfUnreadMentions > 0) {
|
|
||||||
MentionIndicatorAtom()
|
@Composable
|
||||||
}
|
private fun InviteButtonsRow(
|
||||||
if (room.hasNewContent) {
|
onAcceptClicked: () -> Unit,
|
||||||
UnreadIndicatorAtom(
|
onDeclineClicked: () -> Unit,
|
||||||
color = tint
|
modifier: Modifier = Modifier
|
||||||
)
|
) {
|
||||||
}
|
Row(
|
||||||
|
modifier = modifier.padding(),
|
||||||
|
horizontalArrangement = spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
OutlinedButton(
|
||||||
|
text = stringResource(CommonStrings.action_decline),
|
||||||
|
onClick = onDeclineClicked,
|
||||||
|
size = ButtonSize.Medium,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
text = stringResource(CommonStrings.action_accept),
|
||||||
|
onClick = onAcceptClicked,
|
||||||
|
size = ButtonSize.Medium,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -229,6 +371,6 @@ internal fun RoomSummaryRowPreview(@PreviewParameter(RoomListRoomSummaryProvider
|
||||||
RoomSummaryRow(
|
RoomSummaryRow(
|
||||||
room = data,
|
room = data,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onLongClick = {}
|
eventSink = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,72 +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.roomlist.impl.datasource
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
|
||||||
import io.element.android.features.invite.api.SeenInvitesStore
|
|
||||||
import io.element.android.features.roomlist.impl.InvitesState
|
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
|
||||||
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.roomlist.RoomSummary
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@ContributesBinding(SessionScope::class)
|
|
||||||
class DefaultInviteStateDataSource @Inject constructor(
|
|
||||||
private val client: MatrixClient,
|
|
||||||
private val seenInvitesStore: SeenInvitesStore,
|
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
|
||||||
) : InviteStateDataSource {
|
|
||||||
@Composable
|
|
||||||
override fun inviteState(): InvitesState {
|
|
||||||
val invites by client
|
|
||||||
.roomListService
|
|
||||||
.invites
|
|
||||||
.summaries
|
|
||||||
.collectAsState(initial = emptyList())
|
|
||||||
|
|
||||||
val seenInvites by seenInvitesStore
|
|
||||||
.seenRoomIds()
|
|
||||||
.collectAsState(initial = emptySet())
|
|
||||||
|
|
||||||
var state by remember { mutableStateOf(InvitesState.NoInvites) }
|
|
||||||
|
|
||||||
LaunchedEffect(invites, seenInvites) {
|
|
||||||
withContext(coroutineDispatchers.computation) {
|
|
||||||
state = when {
|
|
||||||
invites.isEmpty() -> InvitesState.NoInvites
|
|
||||||
seenInvites.containsAll(invites.roomIds) -> InvitesState.SeenInvites
|
|
||||||
else -> InvitesState.NewInvites
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val List<RoomSummary>.roomIds: Collection<RoomId>
|
|
||||||
get() = filterIsInstance<RoomSummary.Filled>().map { it.details.roomId }
|
|
||||||
|
|
@ -16,13 +16,16 @@
|
||||||
|
|
||||||
package io.element.android.features.roomlist.impl.datasource
|
package io.element.android.features.roomlist.impl.datasource
|
||||||
|
|
||||||
|
import io.element.android.features.roomlist.impl.model.InviteSender
|
||||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
|
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
|
||||||
import io.element.android.libraries.core.extensions.orEmpty
|
import io.element.android.libraries.core.extensions.orEmpty
|
||||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||||
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.eventformatter.api.RoomLastMessageFormatter
|
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
||||||
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.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -35,7 +38,7 @@ class RoomListRoomSummaryFactory @Inject constructor(
|
||||||
return RoomListRoomSummary(
|
return RoomListRoomSummary(
|
||||||
id = id,
|
id = id,
|
||||||
roomId = RoomId(id),
|
roomId = RoomId(id),
|
||||||
isPlaceholder = true,
|
displayType = RoomSummaryDisplayType.PLACEHOLDER,
|
||||||
name = "Short name",
|
name = "Short name",
|
||||||
timestamp = "hh:mm",
|
timestamp = "hh:mm",
|
||||||
lastMessage = "Last message for placeholder",
|
lastMessage = "Last message for placeholder",
|
||||||
|
|
@ -46,8 +49,11 @@ class RoomListRoomSummaryFactory @Inject constructor(
|
||||||
isMarkedUnread = false,
|
isMarkedUnread = false,
|
||||||
userDefinedNotificationMode = null,
|
userDefinedNotificationMode = null,
|
||||||
hasRoomCall = false,
|
hasRoomCall = false,
|
||||||
isDm = false,
|
isDirect = false,
|
||||||
isFavorite = false,
|
isFavorite = false,
|
||||||
|
inviteSender = null,
|
||||||
|
isDm = false,
|
||||||
|
canonicalAlias = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -73,11 +79,29 @@ class RoomListRoomSummaryFactory @Inject constructor(
|
||||||
roomLastMessageFormatter.format(message.event, roomSummary.details.isDirect)
|
roomLastMessageFormatter.format(message.event, roomSummary.details.isDirect)
|
||||||
}.orEmpty(),
|
}.orEmpty(),
|
||||||
avatarData = avatarData,
|
avatarData = avatarData,
|
||||||
isPlaceholder = false,
|
|
||||||
userDefinedNotificationMode = roomSummary.details.userDefinedNotificationMode,
|
userDefinedNotificationMode = roomSummary.details.userDefinedNotificationMode,
|
||||||
hasRoomCall = roomSummary.details.hasRoomCall,
|
hasRoomCall = roomSummary.details.hasRoomCall,
|
||||||
isDm = roomSummary.details.isDm,
|
isDirect = roomSummary.details.isDirect,
|
||||||
isFavorite = roomSummary.details.isFavorite,
|
isFavorite = roomSummary.details.isFavorite,
|
||||||
|
inviteSender = roomSummary.details.inviter?.run {
|
||||||
|
InviteSender(
|
||||||
|
userId = userId,
|
||||||
|
displayName = displayName ?: "",
|
||||||
|
avatarData = AvatarData(
|
||||||
|
id = userId.value,
|
||||||
|
name = displayName,
|
||||||
|
url = avatarUrl,
|
||||||
|
size = AvatarSize.InviteSender,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
isDm = roomSummary.details.isDm,
|
||||||
|
canonicalAlias = roomSummary.details.canonicalAlias,
|
||||||
|
displayType = if (roomSummary.details.currentUserMembership == CurrentUserMembership.INVITED) {
|
||||||
|
RoomSummaryDisplayType.INVITE
|
||||||
|
} else {
|
||||||
|
RoomSummaryDisplayType.ROOM
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,15 @@ enum class RoomListFilter(val stringResource: Int) {
|
||||||
Unread(R.string.screen_roomlist_filter_unreads),
|
Unread(R.string.screen_roomlist_filter_unreads),
|
||||||
People(R.string.screen_roomlist_filter_people),
|
People(R.string.screen_roomlist_filter_people),
|
||||||
Rooms(R.string.screen_roomlist_filter_rooms),
|
Rooms(R.string.screen_roomlist_filter_rooms),
|
||||||
Favourites(R.string.screen_roomlist_filter_favourites);
|
Favourites(R.string.screen_roomlist_filter_favourites),
|
||||||
|
Invites(R.string.screen_roomlist_filter_invites);
|
||||||
|
|
||||||
val oppositeFilter: RoomListFilter?
|
val incompatibleFilters: Set<RoomListFilter>
|
||||||
get() = when (this) {
|
get() = when (this) {
|
||||||
Rooms -> People
|
Rooms -> setOf(People, Invites)
|
||||||
People -> Rooms
|
People -> setOf(Rooms, Invites)
|
||||||
Unread -> null
|
Unread -> setOf(Invites)
|
||||||
Favourites -> null
|
Favourites -> setOf(Invites)
|
||||||
|
Invites -> setOf(Rooms, People, Unread, Favourites)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,10 @@ data class RoomListFiltersEmptyStateResources(
|
||||||
title = R.string.screen_roomlist_filter_favourites_empty_state_title,
|
title = R.string.screen_roomlist_filter_favourites_empty_state_title,
|
||||||
subtitle = R.string.screen_roomlist_filter_favourites_empty_state_subtitle
|
subtitle = R.string.screen_roomlist_filter_favourites_empty_state_subtitle
|
||||||
)
|
)
|
||||||
|
RoomListFilter.Invites -> RoomListFiltersEmptyStateResources(
|
||||||
|
title = R.string.screen_roomlist_filter_invites_empty_state_title,
|
||||||
|
subtitle = R.string.screen_roomlist_filter_mixed_empty_state_subtitle
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> RoomListFiltersEmptyStateResources(
|
else -> RoomListFiltersEmptyStateResources(
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ class RoomListFiltersPresenter @Inject constructor(
|
||||||
RoomListFilter.People -> MatrixRoomListFilter.Category.People
|
RoomListFilter.People -> MatrixRoomListFilter.Category.People
|
||||||
RoomListFilter.Unread -> MatrixRoomListFilter.Unread
|
RoomListFilter.Unread -> MatrixRoomListFilter.Unread
|
||||||
RoomListFilter.Favourites -> MatrixRoomListFilter.Favorite
|
RoomListFilter.Favourites -> MatrixRoomListFilter.Favorite
|
||||||
|
RoomListFilter.Invites -> MatrixRoomListFilter.Invite
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ class DefaultFilterSelectionStrategy @Inject constructor() : FilterSelectionStra
|
||||||
isSelected = true
|
isSelected = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val unselectedFilters = RoomListFilter.entries - selectedFilters - selectedFilters.mapNotNull { it.oppositeFilter }.toSet()
|
val unselectedFilters = RoomListFilter.entries - selectedFilters - selectedFilters.flatMap { it.incompatibleFilters }.toSet()
|
||||||
val unselectedFilterStates = unselectedFilters.map {
|
val unselectedFilterStates = unselectedFilters.map {
|
||||||
FilterSelectionState(
|
FilterSelectionState(
|
||||||
filter = it,
|
filter = it,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* 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.roomlist.impl.model
|
||||||
|
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import io.element.android.features.roomlist.impl.R
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class InviteSender(
|
||||||
|
val userId: UserId,
|
||||||
|
val displayName: String,
|
||||||
|
val avatarData: AvatarData,
|
||||||
|
) {
|
||||||
|
@Composable
|
||||||
|
fun annotatedString(): AnnotatedString {
|
||||||
|
return stringResource(R.string.screen_invites_invited_you, displayName, userId.value).let { text ->
|
||||||
|
val senderNameStart = LocalContext.current.getString(R.string.screen_invites_invited_you).indexOf("%1\$s")
|
||||||
|
AnnotatedString(
|
||||||
|
text = text,
|
||||||
|
spanStyles = listOf(
|
||||||
|
AnnotatedString.Range(
|
||||||
|
SpanStyle(
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
),
|
||||||
|
start = senderNameStart,
|
||||||
|
end = senderNameStart + displayName.length
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,14 +18,17 @@ package io.element.android.features.roomlist.impl.model
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class RoomListRoomSummary(
|
data class RoomListRoomSummary(
|
||||||
val id: String,
|
val id: String,
|
||||||
|
val displayType: RoomSummaryDisplayType,
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
val name: String,
|
val name: String,
|
||||||
|
val canonicalAlias: RoomAlias?,
|
||||||
val numberOfUnreadMessages: Int,
|
val numberOfUnreadMessages: Int,
|
||||||
val numberOfUnreadMentions: Int,
|
val numberOfUnreadMentions: Int,
|
||||||
val numberOfUnreadNotifications: Int,
|
val numberOfUnreadNotifications: Int,
|
||||||
|
|
@ -33,18 +36,21 @@ data class RoomListRoomSummary(
|
||||||
val timestamp: String?,
|
val timestamp: String?,
|
||||||
val lastMessage: CharSequence?,
|
val lastMessage: CharSequence?,
|
||||||
val avatarData: AvatarData,
|
val avatarData: AvatarData,
|
||||||
val isPlaceholder: Boolean,
|
|
||||||
val userDefinedNotificationMode: RoomNotificationMode?,
|
val userDefinedNotificationMode: RoomNotificationMode?,
|
||||||
val hasRoomCall: Boolean,
|
val hasRoomCall: Boolean,
|
||||||
|
val isDirect: Boolean,
|
||||||
val isDm: Boolean,
|
val isDm: Boolean,
|
||||||
val isFavorite: Boolean,
|
val isFavorite: Boolean,
|
||||||
) {
|
val inviteSender: InviteSender?,
|
||||||
|
) {
|
||||||
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
|
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
|
||||||
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
|
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
|
||||||
isMarkedUnread
|
isMarkedUnread ||
|
||||||
|
displayType == RoomSummaryDisplayType.INVITE
|
||||||
|
|
||||||
val hasNewContent = numberOfUnreadMessages > 0 ||
|
val hasNewContent = numberOfUnreadMessages > 0 ||
|
||||||
numberOfUnreadMentions > 0 ||
|
numberOfUnreadMentions > 0 ||
|
||||||
numberOfUnreadNotifications > 0 ||
|
numberOfUnreadNotifications > 0 ||
|
||||||
isMarkedUnread
|
isMarkedUnread ||
|
||||||
|
displayType == RoomSummaryDisplayType.INVITE
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,16 @@ package io.element.android.features.roomlist.impl.model
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
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.RoomAlias
|
||||||
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.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
|
|
||||||
open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSummary> {
|
open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSummary> {
|
||||||
override val values: Sequence<RoomListRoomSummary>
|
override val values: Sequence<RoomListRoomSummary>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
listOf(
|
listOf(
|
||||||
aRoomListRoomSummary(isPlaceholder = true),
|
aRoomListRoomSummary(displayType = RoomSummaryDisplayType.PLACEHOLDER),
|
||||||
aRoomListRoomSummary(),
|
aRoomListRoomSummary(),
|
||||||
aRoomListRoomSummary(lastMessage = null),
|
aRoomListRoomSummary(lastMessage = null),
|
||||||
aRoomListRoomSummary(
|
aRoomListRoomSummary(
|
||||||
|
|
@ -80,9 +82,37 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
|
||||||
)
|
)
|
||||||
}.flatten()
|
}.flatten()
|
||||||
}.flatten(),
|
}.flatten(),
|
||||||
|
listOf(
|
||||||
|
aRoomListRoomSummary(
|
||||||
|
displayType = RoomSummaryDisplayType.INVITE,
|
||||||
|
inviteSender = anInviteSender(
|
||||||
|
userId = "@alice:matrix.org",
|
||||||
|
displayName = "Alice",
|
||||||
|
),
|
||||||
|
canonicalAlias = RoomAlias("#alias:matrix.org"),
|
||||||
|
),
|
||||||
|
aRoomListRoomSummary(
|
||||||
|
name = "Bob",
|
||||||
|
displayType = RoomSummaryDisplayType.INVITE,
|
||||||
|
inviteSender = anInviteSender(
|
||||||
|
userId = "@bob:matrix.org",
|
||||||
|
displayName = "Bob",
|
||||||
|
),
|
||||||
|
isDirect = true,
|
||||||
|
)
|
||||||
|
),
|
||||||
).flatten()
|
).flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun anInviteSender(
|
||||||
|
userId: String,
|
||||||
|
displayName: String,
|
||||||
|
) = InviteSender(
|
||||||
|
userId = UserId(userId),
|
||||||
|
displayName = displayName,
|
||||||
|
avatarData = AvatarData(userId, displayName, size = AvatarSize.InviteSender),
|
||||||
|
)
|
||||||
|
|
||||||
internal fun aRoomListRoomSummary(
|
internal fun aRoomListRoomSummary(
|
||||||
id: String = "!roomId:domain",
|
id: String = "!roomId:domain",
|
||||||
name: String = "Room name",
|
name: String = "Room name",
|
||||||
|
|
@ -92,12 +122,15 @@ internal fun aRoomListRoomSummary(
|
||||||
isMarkedUnread: Boolean = false,
|
isMarkedUnread: Boolean = false,
|
||||||
lastMessage: String? = "Last message",
|
lastMessage: String? = "Last message",
|
||||||
timestamp: String? = lastMessage?.let { "88:88" },
|
timestamp: String? = lastMessage?.let { "88:88" },
|
||||||
isPlaceholder: Boolean = false,
|
|
||||||
notificationMode: RoomNotificationMode? = null,
|
notificationMode: RoomNotificationMode? = null,
|
||||||
hasRoomCall: Boolean = false,
|
hasRoomCall: Boolean = false,
|
||||||
avatarData: AvatarData = AvatarData(id, name, size = AvatarSize.RoomListItem),
|
avatarData: AvatarData = AvatarData(id, name, size = AvatarSize.RoomListItem),
|
||||||
|
isDirect: Boolean = false,
|
||||||
isDm: Boolean = false,
|
isDm: Boolean = false,
|
||||||
isFavorite: Boolean = false,
|
isFavorite: Boolean = false,
|
||||||
|
inviteSender: InviteSender? = null,
|
||||||
|
displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
|
||||||
|
canonicalAlias: RoomAlias? = null,
|
||||||
) = RoomListRoomSummary(
|
) = RoomListRoomSummary(
|
||||||
id = id,
|
id = id,
|
||||||
roomId = RoomId(id),
|
roomId = RoomId(id),
|
||||||
|
|
@ -109,9 +142,12 @@ internal fun aRoomListRoomSummary(
|
||||||
timestamp = timestamp,
|
timestamp = timestamp,
|
||||||
lastMessage = lastMessage,
|
lastMessage = lastMessage,
|
||||||
avatarData = avatarData,
|
avatarData = avatarData,
|
||||||
isPlaceholder = isPlaceholder,
|
|
||||||
userDefinedNotificationMode = notificationMode,
|
userDefinedNotificationMode = notificationMode,
|
||||||
hasRoomCall = hasRoomCall,
|
hasRoomCall = hasRoomCall,
|
||||||
|
isDirect = isDirect,
|
||||||
isDm = isDm,
|
isDm = isDm,
|
||||||
isFavorite = isFavorite,
|
isFavorite = isFavorite,
|
||||||
|
inviteSender = inviteSender,
|
||||||
|
displayType = displayType,
|
||||||
|
canonicalAlias = canonicalAlias,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2023 New Vector Ltd
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -14,12 +14,13 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invite.api
|
package io.element.android.features.roomlist.impl.model
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
/**
|
||||||
import kotlinx.coroutines.flow.Flow
|
* Represents the type of display for a room list item.
|
||||||
|
*/
|
||||||
interface SeenInvitesStore {
|
enum class RoomSummaryDisplayType {
|
||||||
fun seenRoomIds(): Flow<Set<RoomId>>
|
PLACEHOLDER,
|
||||||
suspend fun markAsSeen(roomIds: Set<RoomId>)
|
ROOM,
|
||||||
|
INVITE
|
||||||
}
|
}
|
||||||
|
|
@ -44,6 +44,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.roomlist.impl.R
|
import io.element.android.features.roomlist.impl.R
|
||||||
|
import io.element.android.features.roomlist.impl.RoomListEvents
|
||||||
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
|
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
|
||||||
import io.element.android.features.roomlist.impl.contentType
|
import io.element.android.features.roomlist.impl.contentType
|
||||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
|
|
@ -65,8 +66,8 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@Composable
|
@Composable
|
||||||
internal fun RoomListSearchView(
|
internal fun RoomListSearchView(
|
||||||
state: RoomListSearchState,
|
state: RoomListSearchState,
|
||||||
|
eventSink: (RoomListEvents) -> Unit,
|
||||||
onRoomClicked: (RoomId) -> Unit,
|
onRoomClicked: (RoomId) -> Unit,
|
||||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
|
||||||
onRoomDirectorySearchClicked: () -> Unit,
|
onRoomDirectorySearchClicked: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -90,7 +91,7 @@ internal fun RoomListSearchView(
|
||||||
RoomListSearchContent(
|
RoomListSearchContent(
|
||||||
state = state,
|
state = state,
|
||||||
onRoomClicked = onRoomClicked,
|
onRoomClicked = onRoomClicked,
|
||||||
onRoomLongClicked = onRoomLongClicked,
|
eventSink = eventSink,
|
||||||
onRoomDirectorySearchClicked = onRoomDirectorySearchClicked,
|
onRoomDirectorySearchClicked = onRoomDirectorySearchClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -102,8 +103,8 @@ internal fun RoomListSearchView(
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomListSearchContent(
|
private fun RoomListSearchContent(
|
||||||
state: RoomListSearchState,
|
state: RoomListSearchState,
|
||||||
|
eventSink: (RoomListEvents) -> Unit,
|
||||||
onRoomClicked: (RoomId) -> Unit,
|
onRoomClicked: (RoomId) -> Unit,
|
||||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
|
||||||
onRoomDirectorySearchClicked: () -> Unit,
|
onRoomDirectorySearchClicked: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val borderColor = MaterialTheme.colorScheme.tertiary
|
val borderColor = MaterialTheme.colorScheme.tertiary
|
||||||
|
|
@ -193,7 +194,7 @@ private fun RoomListSearchContent(
|
||||||
RoomSummaryRow(
|
RoomSummaryRow(
|
||||||
room = room,
|
room = room,
|
||||||
onClick = ::onRoomClicked,
|
onClick = ::onRoomClicked,
|
||||||
onLongClick = onRoomLongClicked,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -220,7 +221,7 @@ internal fun RoomListSearchResultContentPreview(@PreviewParameter(RoomListSearch
|
||||||
RoomListSearchContent(
|
RoomListSearchContent(
|
||||||
state = state,
|
state = state,
|
||||||
onRoomClicked = {},
|
onRoomClicked = {},
|
||||||
onRoomLongClicked = {},
|
eventSink = {},
|
||||||
onRoomDirectorySearchClicked = {},
|
onRoomDirectorySearchClicked = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,12 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="confirm_recovery_key_banner_message">"Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup."</string>
|
<string name="confirm_recovery_key_banner_message">"Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup."</string>
|
||||||
<string name="confirm_recovery_key_banner_title">"Enter your recovery key"</string>
|
<string name="confirm_recovery_key_banner_title">"Enter your recovery key"</string>
|
||||||
|
<string name="screen_invites_decline_chat_message">"Are you sure you want to decline the invitation to join %1$s?"</string>
|
||||||
|
<string name="screen_invites_decline_chat_title">"Decline invite"</string>
|
||||||
|
<string name="screen_invites_decline_direct_chat_message">"Are you sure you want to decline this private chat with %1$s?"</string>
|
||||||
|
<string name="screen_invites_decline_direct_chat_title">"Decline chat"</string>
|
||||||
|
<string name="screen_invites_empty_list">"No Invites"</string>
|
||||||
|
<string name="screen_invites_invited_you">"%1$s (%2$s) invited you"</string>
|
||||||
<string name="screen_migration_message">"This is a one time process, thanks for waiting."</string>
|
<string name="screen_migration_message">"This is a one time process, thanks for waiting."</string>
|
||||||
<string name="screen_migration_title">"Setting up your account."</string>
|
<string name="screen_migration_title">"Setting up your account."</string>
|
||||||
<string name="screen_roomlist_a11y_create_message">"Create a new conversation or room"</string>
|
<string name="screen_roomlist_a11y_create_message">"Create a new conversation or room"</string>
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,15 @@ 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 im.vector.app.features.analytics.plan.Interaction
|
import im.vector.app.features.analytics.plan.Interaction
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
|
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
||||||
import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter
|
import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter
|
||||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||||
import io.element.android.features.roomlist.impl.datasource.FakeInviteDataSource
|
|
||||||
import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource
|
|
||||||
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
||||||
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
||||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||||
|
|
@ -52,6 +53,7 @@ import io.element.android.libraries.indicator.impl.DefaultIndicatorService
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
|
|
@ -77,11 +79,14 @@ import io.element.android.tests.testutils.EventsRecorder
|
||||||
import io.element.android.tests.testutils.MutablePresenter
|
import io.element.android.tests.testutils.MutablePresenter
|
||||||
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 io.element.android.tests.testutils.lambda.assert
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.value
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -303,38 +308,6 @@ class RoomListPresenterTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - sets invite state`() = runTest {
|
|
||||||
val inviteStateFlow = MutableStateFlow(InvitesState.NoInvites)
|
|
||||||
val inviteStateDataSource = FakeInviteDataSource(inviteStateFlow)
|
|
||||||
val roomListService = FakeRoomListService()
|
|
||||||
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
|
||||||
val presenter = createRoomListPresenter(
|
|
||||||
inviteStateDataSource = inviteStateDataSource,
|
|
||||||
coroutineScope = scope,
|
|
||||||
client = FakeMatrixClient(roomListService = roomListService),
|
|
||||||
)
|
|
||||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val firstItem = consumeItemsUntilPredicate {
|
|
||||||
it.contentState is RoomListContentState.Rooms
|
|
||||||
}.last()
|
|
||||||
assertThat(firstItem.contentAsRooms().invitesState).isEqualTo(InvitesState.NoInvites)
|
|
||||||
|
|
||||||
inviteStateFlow.value = InvitesState.SeenInvites
|
|
||||||
assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.SeenInvites)
|
|
||||||
|
|
||||||
inviteStateFlow.value = InvitesState.NewInvites
|
|
||||||
assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.NewInvites)
|
|
||||||
|
|
||||||
inviteStateFlow.value = InvitesState.NoInvites
|
|
||||||
assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.NoInvites)
|
|
||||||
scope.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - show context menu`() = runTest {
|
fun `present - show context menu`() = runTest {
|
||||||
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
|
|
@ -609,11 +582,53 @@ class RoomListPresenterTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when a room is invited then accept and decline events are sent to acceptDeclinePresenter`() = runTest {
|
||||||
|
val eventSinkRecorder = lambdaRecorder { _: AcceptDeclineInviteEvents -> }
|
||||||
|
val acceptDeclinePresenter = Presenter {
|
||||||
|
anAcceptDeclineInviteState(eventSink = eventSinkRecorder)
|
||||||
|
}
|
||||||
|
val roomListService = FakeRoomListService()
|
||||||
|
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
|
val matrixClient = FakeMatrixClient(
|
||||||
|
roomListService = roomListService,
|
||||||
|
)
|
||||||
|
val roomSummary = aRoomSummaryFilled(
|
||||||
|
currentUserMembership = CurrentUserMembership.INVITED
|
||||||
|
)
|
||||||
|
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
||||||
|
roomListService.postAllRooms(listOf(roomSummary))
|
||||||
|
val presenter = createRoomListPresenter(
|
||||||
|
coroutineScope = scope,
|
||||||
|
client = matrixClient,
|
||||||
|
acceptDeclineInvitePresenter = acceptDeclinePresenter
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val state = consumeItemsUntilPredicate {
|
||||||
|
it.contentState is RoomListContentState.Rooms
|
||||||
|
}.last()
|
||||||
|
|
||||||
|
val roomListRoomSummary = state.contentAsRooms().summaries.first {
|
||||||
|
it.id == roomSummary.identifier()
|
||||||
|
}
|
||||||
|
state.eventSink(RoomListEvents.AcceptInvite(roomListRoomSummary))
|
||||||
|
state.eventSink(RoomListEvents.DeclineInvite(roomListRoomSummary))
|
||||||
|
|
||||||
|
val inviteData = roomListRoomSummary.toInviteData()
|
||||||
|
|
||||||
|
assert(eventSinkRecorder)
|
||||||
|
.isCalledExactly(2)
|
||||||
|
.withSequence(
|
||||||
|
listOf(value(AcceptDeclineInviteEvents.AcceptInvite(inviteData))),
|
||||||
|
listOf(value(AcceptDeclineInviteEvents.DeclineInvite(inviteData))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun TestScope.createRoomListPresenter(
|
private fun TestScope.createRoomListPresenter(
|
||||||
client: MatrixClient = FakeMatrixClient(),
|
client: MatrixClient = FakeMatrixClient(),
|
||||||
networkMonitor: NetworkMonitor = FakeNetworkMonitor(),
|
networkMonitor: NetworkMonitor = FakeNetworkMonitor(),
|
||||||
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||||
inviteStateDataSource: InviteStateDataSource = FakeInviteDataSource(),
|
|
||||||
leaveRoomPresenter: LeaveRoomPresenter = FakeLeaveRoomPresenter(),
|
leaveRoomPresenter: LeaveRoomPresenter = FakeLeaveRoomPresenter(),
|
||||||
lastMessageTimestampFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter().apply {
|
lastMessageTimestampFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter().apply {
|
||||||
givenFormat(A_FORMATTED_DATE)
|
givenFormat(A_FORMATTED_DATE)
|
||||||
|
|
@ -626,11 +641,11 @@ class RoomListPresenterTests {
|
||||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||||
filtersPresenter: Presenter<RoomListFiltersState> = Presenter { aRoomListFiltersState() },
|
filtersPresenter: Presenter<RoomListFiltersState> = Presenter { aRoomListFiltersState() },
|
||||||
searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
|
searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
|
||||||
|
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
|
||||||
) = RoomListPresenter(
|
) = RoomListPresenter(
|
||||||
client = client,
|
client = client,
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
snackbarDispatcher = snackbarDispatcher,
|
snackbarDispatcher = snackbarDispatcher,
|
||||||
inviteStateDataSource = inviteStateDataSource,
|
|
||||||
leaveRoomPresenter = leaveRoomPresenter,
|
leaveRoomPresenter = leaveRoomPresenter,
|
||||||
roomListDataSource = RoomListDataSource(
|
roomListDataSource = RoomListDataSource(
|
||||||
roomListService = client.roomListService,
|
roomListService = client.roomListService,
|
||||||
|
|
@ -652,5 +667,6 @@ class RoomListPresenterTests {
|
||||||
sessionPreferencesStore = sessionPreferencesStore,
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
filtersPresenter = filtersPresenter,
|
filtersPresenter = filtersPresenter,
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
|
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import androidx.compose.ui.test.performClick
|
||||||
import androidx.compose.ui.test.performTouchInput
|
import androidx.compose.ui.test.performTouchInput
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
|
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
|
||||||
|
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
|
|
@ -93,7 +94,9 @@ class RoomListViewTest {
|
||||||
val state = aRoomListState(
|
val state = aRoomListState(
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
)
|
)
|
||||||
val room0 = state.contentAsRooms().summaries.first()
|
val room0 = state.contentAsRooms().summaries.first {
|
||||||
|
it.displayType == RoomSummaryDisplayType.ROOM
|
||||||
|
}
|
||||||
ensureCalledOnceWithParam(room0.roomId) { callback ->
|
ensureCalledOnceWithParam(room0.roomId) { callback ->
|
||||||
rule.setRoomListView(
|
rule.setRoomListView(
|
||||||
state = state,
|
state = state,
|
||||||
|
|
@ -109,7 +112,9 @@ class RoomListViewTest {
|
||||||
val state = aRoomListState(
|
val state = aRoomListState(
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
)
|
)
|
||||||
val room0 = state.contentAsRooms().summaries.first()
|
val room0 = state.contentAsRooms().summaries.first {
|
||||||
|
it.displayType == RoomSummaryDisplayType.ROOM
|
||||||
|
}
|
||||||
rule.setRoomListView(
|
rule.setRoomListView(
|
||||||
state = state,
|
state = state,
|
||||||
)
|
)
|
||||||
|
|
@ -136,19 +141,20 @@ class RoomListViewTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on invites invokes the expected callback`() {
|
fun `clicking on accept and decline invite emits the expected Events`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
||||||
val state = aRoomListState(
|
val state = aRoomListState(
|
||||||
contentState = aRoomsContentState(invitesState = InvitesState.NewInvites),
|
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
)
|
)
|
||||||
ensureCalledOnce { callback ->
|
val invitedRoom = state.contentAsRooms().summaries.first {
|
||||||
rule.setRoomListView(
|
it.displayType == RoomSummaryDisplayType.INVITE
|
||||||
state = state,
|
|
||||||
onInvitesClicked = callback,
|
|
||||||
)
|
|
||||||
rule.clickOn(CommonStrings.action_invites_list)
|
|
||||||
}
|
}
|
||||||
|
rule.setRoomListView(state = state)
|
||||||
|
rule.clickOn(CommonStrings.action_accept)
|
||||||
|
rule.clickOn(CommonStrings.action_decline)
|
||||||
|
eventsRecorder.assertList(
|
||||||
|
listOf(RoomListEvents.AcceptInvite(invitedRoom), RoomListEvents.DeclineInvite(invitedRoom)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,7 +164,6 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
|
||||||
onSettingsClicked: () -> Unit = EnsureNeverCalled(),
|
onSettingsClicked: () -> Unit = EnsureNeverCalled(),
|
||||||
onConfirmRecoveryKeyClicked: () -> Unit = EnsureNeverCalled(),
|
onConfirmRecoveryKeyClicked: () -> Unit = EnsureNeverCalled(),
|
||||||
onCreateRoomClicked: () -> Unit = EnsureNeverCalled(),
|
onCreateRoomClicked: () -> Unit = EnsureNeverCalled(),
|
||||||
onInvitesClicked: () -> Unit = EnsureNeverCalled(),
|
|
||||||
onRoomSettingsClicked: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
onRoomSettingsClicked: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onMenuActionClicked: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(),
|
onMenuActionClicked: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onRoomDirectorySearchClicked: () -> Unit = EnsureNeverCalled(),
|
onRoomDirectorySearchClicked: () -> Unit = EnsureNeverCalled(),
|
||||||
|
|
@ -170,10 +175,10 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
|
||||||
onSettingsClicked = onSettingsClicked,
|
onSettingsClicked = onSettingsClicked,
|
||||||
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
||||||
onCreateRoomClicked = onCreateRoomClicked,
|
onCreateRoomClicked = onCreateRoomClicked,
|
||||||
onInvitesClicked = onInvitesClicked,
|
|
||||||
onRoomSettingsClicked = onRoomSettingsClicked,
|
onRoomSettingsClicked = onRoomSettingsClicked,
|
||||||
onMenuActionClicked = onMenuActionClicked,
|
onMenuActionClicked = onMenuActionClicked,
|
||||||
onRoomDirectorySearchClicked = onRoomDirectorySearchClicked,
|
onRoomDirectorySearchClicked = onRoomDirectorySearchClicked,
|
||||||
|
acceptDeclineInviteView = { },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,134 +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.roomlist.impl.datasource
|
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionMode
|
|
||||||
import app.cash.molecule.moleculeFlow
|
|
||||||
import app.cash.turbine.test
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import io.element.android.features.invite.test.FakeSeenInvitesStore
|
|
||||||
import io.element.android.features.roomlist.impl.InvitesState
|
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
|
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
internal class DefaultInviteStateDataSourceTest {
|
|
||||||
@Test
|
|
||||||
fun `emits NoInvites state if invites list is empty`() = runTest {
|
|
||||||
val roomListService = FakeRoomListService()
|
|
||||||
val client = FakeMatrixClient(roomListService = roomListService)
|
|
||||||
val seenStore = FakeSeenInvitesStore()
|
|
||||||
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
|
|
||||||
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
dataSource.inviteState()
|
|
||||||
}.test {
|
|
||||||
assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `emits NewInvites state if unseen invite exists`() = runTest {
|
|
||||||
val roomListService = FakeRoomListService()
|
|
||||||
roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
|
|
||||||
val client = FakeMatrixClient(roomListService = roomListService)
|
|
||||||
val seenStore = FakeSeenInvitesStore()
|
|
||||||
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
|
|
||||||
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
dataSource.inviteState()
|
|
||||||
}.test {
|
|
||||||
skipItems(2)
|
|
||||||
assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `emits NewInvites state if multiple invites exist and at least one is unseen`() = runTest {
|
|
||||||
val roomListService = FakeRoomListService()
|
|
||||||
roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(roomId = A_ROOM_ID_2)))
|
|
||||||
val client = FakeMatrixClient(roomListService = roomListService)
|
|
||||||
val seenStore = FakeSeenInvitesStore()
|
|
||||||
seenStore.publishRoomIds(setOf(A_ROOM_ID))
|
|
||||||
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers(useUnconfinedTestDispatcher = true))
|
|
||||||
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
dataSource.inviteState()
|
|
||||||
}.test {
|
|
||||||
skipItems(1)
|
|
||||||
assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `emits SeenInvites state if invite exists in seen store`() = runTest {
|
|
||||||
val roomListService = FakeRoomListService()
|
|
||||||
roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
|
|
||||||
val client = FakeMatrixClient(roomListService = roomListService)
|
|
||||||
val seenStore = FakeSeenInvitesStore()
|
|
||||||
seenStore.publishRoomIds(setOf(A_ROOM_ID))
|
|
||||||
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers(useUnconfinedTestDispatcher = true))
|
|
||||||
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
dataSource.inviteState()
|
|
||||||
}.test {
|
|
||||||
skipItems(1)
|
|
||||||
|
|
||||||
assertThat(awaitItem()).isEqualTo(InvitesState.SeenInvites)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `emits new state in response to upstream events`() = runTest {
|
|
||||||
val roomListService = FakeRoomListService()
|
|
||||||
val client = FakeMatrixClient(roomListService = roomListService)
|
|
||||||
val seenStore = FakeSeenInvitesStore()
|
|
||||||
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
|
|
||||||
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
dataSource.inviteState()
|
|
||||||
}.test {
|
|
||||||
// Initially there are no invites
|
|
||||||
assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites)
|
|
||||||
|
|
||||||
// When a single invite is received, state should be NewInvites
|
|
||||||
roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
|
|
||||||
skipItems(1)
|
|
||||||
assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
|
|
||||||
|
|
||||||
// If that invite is marked as seen, then the state becomes SeenInvites
|
|
||||||
seenStore.publishRoomIds(setOf(A_ROOM_ID))
|
|
||||||
skipItems(1)
|
|
||||||
assertThat(awaitItem()).isEqualTo(InvitesState.SeenInvites)
|
|
||||||
|
|
||||||
// Another new invite resets it to NewInvites
|
|
||||||
roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(roomId = A_ROOM_ID_2)))
|
|
||||||
skipItems(1)
|
|
||||||
assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
|
|
||||||
|
|
||||||
// All of the invites going away reverts to NoInvites
|
|
||||||
roomListService.postInviteRooms(emptyList())
|
|
||||||
skipItems(1)
|
|
||||||
assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -64,6 +64,15 @@ class RoomListFiltersEmptyStateResourcesTest {
|
||||||
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_favourites_empty_state_subtitle)
|
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_favourites_empty_state_subtitle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only invites filter`() {
|
||||||
|
val selectedFilters = listOf(RoomListFilter.Invites)
|
||||||
|
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters)
|
||||||
|
assertThat(result).isNotNull()
|
||||||
|
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_invites_empty_state_title)
|
||||||
|
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has multiple filters`() {
|
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has multiple filters`() {
|
||||||
val selectedFilters = listOf(RoomListFilter.Unread, RoomListFilter.People, RoomListFilter.Rooms, RoomListFilter.Favourites)
|
val selectedFilters = listOf(RoomListFilter.Unread, RoomListFilter.People, RoomListFilter.Rooms, RoomListFilter.Favourites)
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ class RoomListFiltersPresenterTests {
|
||||||
filterSelectionState(RoomListFilter.People, false),
|
filterSelectionState(RoomListFilter.People, false),
|
||||||
filterSelectionState(RoomListFilter.Rooms, false),
|
filterSelectionState(RoomListFilter.Rooms, false),
|
||||||
filterSelectionState(RoomListFilter.Favourites, false),
|
filterSelectionState(RoomListFilter.Favourites, false),
|
||||||
|
filterSelectionState(RoomListFilter.Invites, false),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
|
@ -84,6 +85,7 @@ class RoomListFiltersPresenterTests {
|
||||||
filterSelectionState(RoomListFilter.People, false),
|
filterSelectionState(RoomListFilter.People, false),
|
||||||
filterSelectionState(RoomListFilter.Rooms, false),
|
filterSelectionState(RoomListFilter.Rooms, false),
|
||||||
filterSelectionState(RoomListFilter.Favourites, false),
|
filterSelectionState(RoomListFilter.Favourites, false),
|
||||||
|
filterSelectionState(RoomListFilter.Invites, false),
|
||||||
).inOrder()
|
).inOrder()
|
||||||
assertThat(state.selectedFilters()).isEmpty()
|
assertThat(state.selectedFilters()).isEmpty()
|
||||||
val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All
|
val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,15 @@ class RoomListRoomSummaryTest {
|
||||||
assertThat(sut.isHighlighted).isTrue()
|
assertThat(sut.isHighlighted).isTrue()
|
||||||
assertThat(sut.hasNewContent).isTrue()
|
assertThat(sut.hasNewContent).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when display type is invite then isHighlighted and hasNewContent are true`() {
|
||||||
|
val sut = createRoomListRoomSummary(
|
||||||
|
displayType = RoomSummaryDisplayType.INVITE,
|
||||||
|
)
|
||||||
|
assertThat(sut.isHighlighted).isTrue()
|
||||||
|
assertThat(sut.hasNewContent).isTrue()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun createRoomListRoomSummary(
|
internal fun createRoomListRoomSummary(
|
||||||
|
|
@ -81,6 +90,7 @@ internal fun createRoomListRoomSummary(
|
||||||
isMarkedUnread: Boolean = false,
|
isMarkedUnread: Boolean = false,
|
||||||
userDefinedNotificationMode: RoomNotificationMode? = null,
|
userDefinedNotificationMode: RoomNotificationMode? = null,
|
||||||
isFavorite: Boolean = false,
|
isFavorite: Boolean = false,
|
||||||
|
displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
|
||||||
) = RoomListRoomSummary(
|
) = RoomListRoomSummary(
|
||||||
id = A_ROOM_ID.value,
|
id = A_ROOM_ID.value,
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
|
|
@ -92,9 +102,12 @@ internal fun createRoomListRoomSummary(
|
||||||
timestamp = A_FORMATTED_DATE,
|
timestamp = A_FORMATTED_DATE,
|
||||||
lastMessage = "",
|
lastMessage = "",
|
||||||
avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME, size = AvatarSize.RoomListItem),
|
avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME, size = AvatarSize.RoomListItem),
|
||||||
isPlaceholder = false,
|
displayType = displayType,
|
||||||
userDefinedNotificationMode = userDefinedNotificationMode,
|
userDefinedNotificationMode = userDefinedNotificationMode,
|
||||||
hasRoomCall = false,
|
hasRoomCall = false,
|
||||||
isDm = false,
|
isDirect = false,
|
||||||
isFavorite = isFavorite,
|
isFavorite = isFavorite,
|
||||||
|
canonicalAlias = null,
|
||||||
|
inviteSender = null,
|
||||||
|
isDm = false,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ ksp = "1.9.23-1.0.20"
|
||||||
firebaseAppDistribution = "4.2.0"
|
firebaseAppDistribution = "4.2.0"
|
||||||
|
|
||||||
# AndroidX
|
# AndroidX
|
||||||
core = "1.12.0"
|
core = "1.13.0"
|
||||||
datastore = "1.0.0"
|
datastore = "1.1.0"
|
||||||
constraintlayout = "2.1.4"
|
constraintlayout = "2.1.4"
|
||||||
constraintlayout_compose = "1.0.1"
|
constraintlayout_compose = "1.0.1"
|
||||||
lifecycle = "2.7.0"
|
lifecycle = "2.7.0"
|
||||||
|
|
@ -18,8 +18,8 @@ activity = "1.8.2"
|
||||||
media3 = "1.3.1"
|
media3 = "1.3.1"
|
||||||
|
|
||||||
# Compose
|
# Compose
|
||||||
compose_bom = "2024.04.00"
|
compose_bom = "2024.04.01"
|
||||||
composecompiler = "1.5.11"
|
composecompiler = "1.5.12"
|
||||||
|
|
||||||
# Coroutines
|
# Coroutines
|
||||||
coroutines = "1.8.0"
|
coroutines = "1.8.0"
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,3 @@ package io.element.android.libraries.deeplink
|
||||||
|
|
||||||
internal const val SCHEME = "elementx"
|
internal const val SCHEME = "elementx"
|
||||||
internal const val HOST = "open"
|
internal const val HOST = "open"
|
||||||
|
|
||||||
object DeepLinkPaths {
|
|
||||||
const val INVITE_LIST = "invites"
|
|
||||||
}
|
|
||||||
|
|
|
||||||
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