Browse Source

Invite list view

Adds a CTA on the room list to view invites if there are any.

The invite list presents each invite with accept/decline buttons
and (for room invites) the sender details.

Fixes #102
test/jme/fix-danger-lint-duplicate-reports
Chris Smith 1 year ago
parent
commit
34cc352053
  1. 21
      appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
  2. 7
      docs/_developer_onboarding.md
  3. 27
      features/invitelist/api/build.gradle.kts
  4. 37
      features/invitelist/api/src/main/kotlin/io/element/android/features/invitelist/api/InviteListEntryPoint.kt
  5. 53
      features/invitelist/impl/build.gradle.kts
  6. 46
      features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/DefaultInviteListEntryPoint.kt
  7. 50
      features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListNode.kt
  8. 90
      features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt
  9. 26
      features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListState.kt
  10. 64
      features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListStateProvider.kt
  11. 129
      features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt
  12. 211
      features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/components/InviteSummaryRow.kt
  13. 37
      features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/model/InviteListInviteSummary.kt
  14. 39
      features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/model/InviteListInviteSummaryProvider.kt
  15. 9
      features/invitelist/impl/src/main/res/values/localazy.xml
  16. 184
      features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt
  17. 2
      features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt
  18. 5
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt
  19. 8
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
  20. 1
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt
  21. 2
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt
  22. 34
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt
  23. 29
      features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt
  24. 94
      libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/OutlinedButton.kt
  25. 1
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
  26. 2
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomSummary.kt
  27. 62
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
  28. 6
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomSummaryDetailsFactory.kt
  29. 1
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
  30. 14
      libraries/ui-strings/src/main/res/values/localazy.xml
  31. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl.components_null_DefaultGroup_InviteSummaryRowDarkPreview_0_null_0,NEXUS_5,1.0,en].png
  32. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl.components_null_DefaultGroup_InviteSummaryRowDarkPreview_0_null_1,NEXUS_5,1.0,en].png
  33. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl.components_null_DefaultGroup_InviteSummaryRowDarkPreview_0_null_2,NEXUS_5,1.0,en].png
  34. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl.components_null_DefaultGroup_InviteSummaryRowLightPreview_0_null_0,NEXUS_5,1.0,en].png
  35. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl.components_null_DefaultGroup_InviteSummaryRowLightPreview_0_null_1,NEXUS_5,1.0,en].png
  36. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl.components_null_DefaultGroup_InviteSummaryRowLightPreview_0_null_2,NEXUS_5,1.0,en].png
  37. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
  38. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
  39. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
  40. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
  41. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_0,NEXUS_5,1.0,en].png
  42. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_1,NEXUS_5,1.0,en].png
  43. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_2,NEXUS_5,1.0,en].png
  44. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_3,NEXUS_5,1.0,en].png
  45. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_0,NEXUS_5,1.0,en].png
  46. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_1,NEXUS_5,1.0,en].png
  47. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_2,NEXUS_5,1.0,en].png
  48. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_3,NEXUS_5,1.0,en].png
  49. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewDarkPreview_0_null,NEXUS_5,1.0,en].png
  50. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewLightPreview_0_null,NEXUS_5,1.0,en].png
  51. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
  52. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
  53. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
  54. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
  55. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
  56. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png
  57. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
  58. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
  59. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
  60. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png
  61. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png
  62. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png
  63. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
  64. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
  65. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
  66. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
  67. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null,NEXUS_5,1.0,en].png
  68. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null,NEXUS_5,1.0,en].png
  69. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
  70. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
  71. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
  72. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png
  73. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_OutlinedButtonsDarkPreview_0_null,NEXUS_5,1.0,en].png
  74. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_OutlinedButtonsLightPreview_0_null,NEXUS_5,1.0,en].png
  75. 6
      tools/localazy/config.json

21
appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt

@ -31,6 +31,7 @@ import com.bumble.appyx.core.node.node @@ -31,6 +31,7 @@ import com.bumble.appyx.core.node.node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
import com.bumble.appyx.navmodel.backstack.operation.replace
import com.bumble.appyx.navmodel.backstack.operation.singleTop
@ -39,6 +40,7 @@ import dagger.assisted.AssistedInject @@ -39,6 +40,7 @@ import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.appnav.loggedin.LoggedInNode
import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.invitelist.api.InviteListEntryPoint
import io.element.android.features.preferences.api.PreferencesEntryPoint
import io.element.android.features.roomlist.api.RoomListEntryPoint
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
@ -68,6 +70,7 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -68,6 +70,7 @@ class LoggedInFlowNode @AssistedInject constructor(
private val createRoomEntryPoint: CreateRoomEntryPoint,
private val appNavigationStateService: AppNavigationStateService,
private val verifySessionEntryPoint: VerifySessionEntryPoint,
private val inviteListEntryPoint: InviteListEntryPoint,
private val coroutineScope: CoroutineScope,
snackbarDispatcher: SnackbarDispatcher,
) : BackstackNode<LoggedInFlowNode.NavTarget>(
@ -142,6 +145,9 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -142,6 +145,9 @@ class LoggedInFlowNode @AssistedInject constructor(
@Parcelize
object VerifySession : NavTarget
@Parcelize
object InviteList: NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@ -166,6 +172,10 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -166,6 +172,10 @@ class LoggedInFlowNode @AssistedInject constructor(
override fun onSessionVerificationClicked() {
backstack.push(NavTarget.VerifySession)
}
override fun onInvitesClicked() {
backstack.push(NavTarget.InviteList)
}
}
roomListEntryPoint
.nodeBuilder(this, buildContext)
@ -212,6 +222,17 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -212,6 +222,17 @@ class LoggedInFlowNode @AssistedInject constructor(
NavTarget.VerifySession -> {
verifySessionEntryPoint.createNode(this, buildContext)
}
NavTarget.InviteList -> {
val callback = object : InviteListEntryPoint.Callback {
override fun onBackClicked() {
backstack.pop()
}
}
inviteListEntryPoint.nodeBuilder(this, buildContext)
.callback(callback)
.build()
}
}
}

7
docs/_developer_onboarding.md

@ -22,6 +22,7 @@ @@ -22,6 +22,7 @@
* [Code coverage](#code-coverage)
* [Other points](#other-points)
* [Logging](#logging)
* [Translations](#translations)
* [Rageshake](#rageshake)
* [Tips](#tips)
* [Happy coding!](#happy-coding)
@ -392,6 +393,12 @@ Also generally it is recommended to provide the `Throwable` to the Timber log fu @@ -392,6 +393,12 @@ Also generally it is recommended to provide the `Throwable` to the Timber log fu
Last point, note that `Timber.v` function may have no effect on some devices. Prefer using `Timber.d` and up.
#### Translations
Translations are handled through localazy. See [the dedicated README.md file](../tools/localazy/README.md) for information on how
to configure new modules etc.
#### Rageshake
Rageshake is a feature to send bug report directly from the application. Just shake your phone and you will be prompted to send a bug report.

27
features/invitelist/api/build.gradle.kts

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
/*
* 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.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.invitelist.api"
}
dependencies {
implementation(projects.libraries.architecture)
}

37
features/invitelist/api/src/main/kotlin/io/element/android/features/invitelist/api/InviteListEntryPoint.kt

@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
/*
* 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.invitelist.api
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint
interface InviteListEntryPoint : FeatureEntryPoint {
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
interface NodeBuilder {
fun callback(callback: Callback): NodeBuilder
fun build(): Node
}
interface Callback : Plugin {
fun onBackClicked()
}
}

53
features/invitelist/impl/build.gradle.kts

@ -0,0 +1,53 @@ @@ -0,0 +1,53 @@
/*
* 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.
*/
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
alias(libs.plugins.ksp)
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.features.invitelist.impl"
}
anvil {
generateDaggerFactories.set(true)
}
dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
api(projects.features.invitelist.api)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
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)
ksp(libs.showkase.processor)
}

46
features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/DefaultInviteListEntryPoint.kt

@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
/*
* 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.invitelist.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.invitelist.api.InviteListEntryPoint
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultInviteListEntryPoint @Inject constructor() : InviteListEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): InviteListEntryPoint.NodeBuilder {
val plugins = ArrayList<Plugin>()
return object : InviteListEntryPoint.NodeBuilder {
override fun callback(callback: InviteListEntryPoint.Callback): InviteListEntryPoint.NodeBuilder {
plugins += callback
return this
}
override fun build(): Node {
return parentNode.createNode<InviteListNode>(buildContext, plugins)
}
}
}
}

50
features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListNode.kt

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
/*
* 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.invitelist.impl
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.invitelist.api.InviteListEntryPoint
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
class InviteListNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: InviteListPresenter,
) : Node(buildContext, plugins = plugins) {
private fun onBackClicked() {
plugins<InviteListEntryPoint.Callback>().forEach { it.onBackClicked() }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
InviteListView(
state = state,
onBackClicked = ::onBackClicked,
)
}
}

90
features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt

@ -0,0 +1,90 @@ @@ -0,0 +1,90 @@
/*
* 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.invitelist.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
import io.element.android.features.invitelist.impl.model.InviteSender
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.room.RoomSummary
import kotlinx.collections.immutable.toPersistentList
import javax.inject.Inject
class InviteListPresenter @Inject constructor(
private val client: MatrixClient,
) : Presenter<InviteListState> {
@Composable
override fun present(): InviteListState {
val invites by client
.invitesDataSource
.roomSummaries()
.collectAsState()
return InviteListState(
inviteList = invites.mapNotNull(::toInviteSummary).toPersistentList(),
)
}
private fun toInviteSummary(roomSummary: RoomSummary): InviteListInviteSummary? {
return when (roomSummary) {
is RoomSummary.Filled -> roomSummary.details.run {
val i = inviter
val avatarData = if (isDirect && i != null)
AvatarData(
id = i.userId.value,
name = i.displayName,
url = i.avatarUrl,
)
else
AvatarData(
id = roomId.value,
name = name,
url = avatarURLString
)
val alias = if (isDirect)
inviter?.userId?.value
else
canonicalAlias
InviteListInviteSummary(
roomId = roomId,
roomName = name,
roomAlias = alias,
roomAvatarData = avatarData,
sender = if (isDirect) null else inviter?.let {
InviteSender(
userId = it.userId,
displayName = it.displayName ?: "",
avatarData = AvatarData(
id = it.userId.value,
name = it.displayName,
url = it.avatarUrl,
),
)
}
)
}
else -> null
}
}
}

26
features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListState.kt

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
/*
* 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.invitelist.impl
import androidx.compose.runtime.Immutable
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
import kotlinx.collections.immutable.ImmutableList
@Immutable
data class InviteListState(
val inviteList: ImmutableList<InviteListInviteSummary>
)

64
features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListStateProvider.kt

@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
/*
* 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.invitelist.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
import io.element.android.features.invitelist.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> {
override val values: Sequence<InviteListState>
get() = sequenceOf(
aInviteListState(),
aInviteListState().copy(inviteList = persistentListOf())
)
}
internal fun aInviteListState() = InviteListState(
inviteList = aInviteListInviteSummaryList(),
)
internal fun aInviteListInviteSummaryList(): ImmutableList<InviteListInviteSummary> {
return persistentListOf(
InviteListInviteSummary(
roomId = RoomId("!id1"),
roomName = "Room 1",
roomAlias = "#room:example.org",
sender = InviteSender(
userId = UserId("@alice:example.org"),
displayName = "Alice"
),
),
InviteListInviteSummary(
roomId = RoomId("!id2"),
roomName = "Room 2",
sender = InviteSender(
userId = UserId("@bob:example.org"),
displayName = "Bob"
),
),
InviteListInviteSummary(
roomId = RoomId("!id3"),
roomName = "Alice",
roomAlias = "@alice:example.com"
),
)
}

129
features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt

@ -0,0 +1,129 @@ @@ -0,0 +1,129 @@
/*
* 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.invitelist.impl
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
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.items
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.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.invitelist.impl.components.InviteSummaryRow
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
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.R as StringR
@Composable
fun InviteListView(
state: InviteListState,
modifier: Modifier = Modifier,
onBackClicked: () -> Unit = {},
onAcceptClicked: (RoomId) -> Unit = {},
onDeclineClicked: (RoomId) -> Unit = {},
) {
InviteListContent(
state = state,
modifier = modifier,
onBackClicked = onBackClicked,
onAcceptClicked = onAcceptClicked,
onDeclineClicked = onDeclineClicked,
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun InviteListContent(
state: InviteListState,
modifier: Modifier = Modifier,
onBackClicked: () -> Unit = {},
onAcceptClicked: (RoomId) -> Unit = {},
onDeclineClicked: (RoomId) -> Unit = {},
) {
Scaffold(
modifier = modifier,
topBar = {
TopAppBar(
navigationIcon = {
BackButton(onClick = onBackClicked)
},
title = {
Text(text = stringResource(StringR.string.action_invites_list))
}
)
},
content = { padding ->
Column(
modifier = Modifier.padding(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)
) {
items(
items = state.inviteList,
) { invite ->
InviteSummaryRow(
invite = invite,
onAcceptClicked = onAcceptClicked,
onDeclineClicked = onDeclineClicked,
)
}
}
}
}
}
)
}
@Preview
@Composable
internal fun RoomListViewLightPreview(@PreviewParameter(InviteListStateProvider::class) state: InviteListState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
internal fun RoomListViewDarkPreview(@PreviewParameter(InviteListStateProvider::class) state: InviteListState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: InviteListState) {
InviteListView(state)
}

211
features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/components/InviteSummaryRow.kt

@ -0,0 +1,211 @@ @@ -0,0 +1,211 @@
/*
* 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.invitelist.impl.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
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.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
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.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.features.invitelist.impl.R
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
import io.element.android.features.invitelist.impl.model.InviteListInviteSummaryProvider
import io.element.android.features.invitelist.impl.model.InviteSender
import io.element.android.libraries.designsystem.ElementTextStyles
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.collections.immutable.persistentMapOf
import io.element.android.libraries.ui.strings.R as StringR
private val minHeight = 72.dp
@Composable
internal fun InviteSummaryRow(
invite: InviteListInviteSummary,
modifier: Modifier = Modifier,
onAcceptClicked: (RoomId) -> Unit = {},
onDeclineClicked: (RoomId) -> Unit = {},
) {
Box(
modifier = modifier
.fillMaxWidth()
.heightIn(min = minHeight)
) {
DefaultInviteSummaryRow(
invite = invite,
onAcceptClicked = onAcceptClicked,
onDeclineClicked = onDeclineClicked,
)
}
}
@Composable
internal fun DefaultInviteSummaryRow(
invite: InviteListInviteSummary,
onAcceptClicked: (RoomId) -> Unit = {},
onDeclineClicked: (RoomId) -> Unit = {},
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp)
.height(IntrinsicSize.Min),
verticalAlignment = Alignment.Top
) {
Avatar(
invite.roomAvatarData,
)
Column(
modifier = Modifier
.padding(start = 12.dp, end = 4.dp)
.alignByBaseline()
.weight(1f)
) {
// Name
Text(
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
text = invite.roomName,
color = MaterialTheme.colorScheme.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
// ID or Alias
invite.roomAlias?.let {
Text(
fontSize = 14.sp,
fontWeight = FontWeight.Normal,
text = it,
color = MaterialTheme.colorScheme.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
// Sender
invite.sender?.let { sender ->
SenderRow(sender = sender)
}
// CTAs
Row(Modifier.padding(top = 12.dp)) {
OutlinedButton(
content = { Text(stringResource(StringR.string.action_decline), style = ElementTextStyles.Button) },
onClick = { onDeclineClicked(invite.roomId) },
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 7.dp),
)
Spacer(modifier = Modifier.width(12.dp))
Button(
content = { Text(stringResource(StringR.string.action_accept), style = ElementTextStyles.Button) },
onClick = { onAcceptClicked(invite.roomId) },
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 7.dp),
)
}
}
}
}
@Composable
private fun SenderRow(sender: InviteSender) {
Text(
text = buildAnnotatedString {
val placeholder = "$"
val text = stringResource(R.string.screen_invites_invited_you, placeholder)
val nameIndex = text.indexOf(placeholder)
// Text before the placeholder
append(text.take(nameIndex))
// Avatar and display name
appendInlineContent("avatar")
withStyle(SpanStyle(fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary)) {
append(sender.displayName)
}
// Text after the placeholder
append(text.drop(nameIndex + placeholder.length))
},
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier.padding(top = 6.dp),
inlineContent = persistentMapOf(
"avatar" to InlineTextContent(
with(LocalDensity.current) {
Placeholder(20.dp.toSp(), 20.dp.toSp(), PlaceholderVerticalAlign.Center)
}
) {
Box(Modifier.fillMaxHeight().padding(end = 4.dp)) {
Avatar(
avatarData = sender.avatarData.copy(size = AvatarSize.Custom(16.dp)),
modifier = Modifier.align(Alignment.Center)
)
}
}
)
)
}
@Preview
@Composable
internal fun InviteSummaryRowLightPreview(@PreviewParameter(InviteListInviteSummaryProvider::class) data: InviteListInviteSummary) =
ElementPreviewLight { ContentToPreview(data) }
@Preview
@Composable
internal fun InviteSummaryRowDarkPreview(@PreviewParameter(InviteListInviteSummaryProvider::class) data: InviteListInviteSummary) =
ElementPreviewDark { ContentToPreview(data) }
@Composable
private fun ContentToPreview(data: InviteListInviteSummary) {
InviteSummaryRow(data)
}

37
features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/model/InviteListInviteSummary.kt

@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
/*
* 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.invitelist.impl.model
import androidx.compose.runtime.Immutable
import io.element.android.libraries.designsystem.components.avatar.AvatarData
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),
val sender: InviteSender? = null
)
data class InviteSender(
val userId: UserId,
val displayName: String,
val avatarData: AvatarData = AvatarData(userId.value, displayName),
)

39
features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/model/InviteListInviteSummaryProvider.kt

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
/*
* 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.invitelist.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:example.com"),
aInviteListInviteSummary().copy(roomName = "Alice", sender = null),
)
}
fun aInviteListInviteSummary() = InviteListInviteSummary(
roomId = RoomId("!room1"),
roomName = "Some room",
sender = InviteSender(
userId = UserId("@alice:example.org"),
displayName = "Alice"
),
)

9
features/invitelist/impl/src/main/res/values/localazy.xml

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invites_decline_chat_message">"Are you sure you want to decline joining %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 to 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 invited you"</string>
</resources>

184
features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt

@ -0,0 +1,184 @@ @@ -0,0 +1,184 @@
/*
* 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.invitelist.impl
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth
import io.element.android.libraries.designsystem.components.avatar.AvatarData
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.RoomSummary
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
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_NAME
import io.element.android.libraries.matrix.test.A_SESSION_ID
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.FakeRoomSummaryDataSource
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class InviteListPresenterTests {
@Test
fun `present - starts empty, adds invites when received`() = runTest {
val invitesDataSource = FakeRoomSummaryDataSource()
val presenter = InviteListPresenter(
FakeMatrixClient(
sessionId = A_SESSION_ID,
invitesDataSource = invitesDataSource,
)
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
Truth.assertThat(initialState.inviteList).isEmpty()
invitesDataSource.postRoomSummary(
listOf(
RoomSummary.Filled(
RoomSummaryDetails(
roomId = A_ROOM_ID,
name = A_ROOM_NAME,
avatarURLString = null,
isDirect = false,
lastMessage = null,
lastMessageTimestamp = null,
unreadNotificationCount = 0,
)
)
)
)
val withInviteState = awaitItem()
Truth.assertThat(withInviteState.inviteList.size).isEqualTo(1)
Truth.assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
Truth.assertThat(withInviteState.inviteList[0].roomName).isEqualTo(A_ROOM_NAME)
}
}
@Test
fun `present - uses user ID and avatar for direct invites`() = runTest {
val invitesDataSource = FakeRoomSummaryDataSource()
invitesDataSource.postRoomSummary(
listOf(
RoomSummary.Filled(
RoomSummaryDetails(
roomId = A_ROOM_ID,
name = A_USER_NAME,
avatarURLString = null,
isDirect = true,
lastMessage = null,
lastMessageTimestamp = null,
unreadNotificationCount = 0,
inviter = RoomMember(
userId = A_USER_ID,
displayName = A_USER_NAME,
avatarUrl = AN_AVATAR_URL,
membership = RoomMembershipState.JOIN,
isNameAmbiguous = false,
powerLevel = 0,
normalizedPowerLevel = 0,
isIgnored = false,
)
)
)
)
)
val presenter = InviteListPresenter(
FakeMatrixClient(
sessionId = A_SESSION_ID,
invitesDataSource = invitesDataSource,
)
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val withInviteState = awaitItem()
Truth.assertThat(withInviteState.inviteList.size).isEqualTo(1)
Truth.assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
Truth.assertThat(withInviteState.inviteList[0].roomAlias).isEqualTo(A_USER_ID.value)
Truth.assertThat(withInviteState.inviteList[0].roomName).isEqualTo(A_USER_NAME)
Truth.assertThat(withInviteState.inviteList[0].roomAvatarData).isEqualTo(
AvatarData(
id = A_USER_ID.value,
name = A_USER_NAME,
url = AN_AVATAR_URL,
)
)
Truth.assertThat(withInviteState.inviteList[0].sender).isNull()
}
}
@Test
fun `present - includes sender details for room invites`() = runTest {
val invitesDataSource = FakeRoomSummaryDataSource()
invitesDataSource.postRoomSummary(
listOf(
RoomSummary.Filled(
RoomSummaryDetails(
roomId = A_ROOM_ID,
name = A_USER_NAME,
avatarURLString = null,
isDirect = false,
lastMessage = null,
lastMessageTimestamp = null,
unreadNotificationCount = 0,
inviter = RoomMember(
userId = A_USER_ID,
displayName = A_USER_NAME,
avatarUrl = AN_AVATAR_URL,
membership = RoomMembershipState.JOIN,
isNameAmbiguous = false,
powerLevel = 0,
normalizedPowerLevel = 0,
isIgnored = false,
)
)
)
)
)
val presenter = InviteListPresenter(
FakeMatrixClient(
sessionId = A_SESSION_ID,
invitesDataSource = invitesDataSource,
)
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val withInviteState = awaitItem()
Truth.assertThat(withInviteState.inviteList.size).isEqualTo(1)
Truth.assertThat(withInviteState.inviteList[0].sender?.displayName).isEqualTo(A_USER_NAME)
Truth.assertThat(withInviteState.inviteList[0].sender?.userId).isEqualTo(A_USER_ID)
Truth.assertThat(withInviteState.inviteList[0].sender?.avatarData).isEqualTo(
AvatarData(
id = A_USER_ID.value,
name = A_USER_NAME,
url = AN_AVATAR_URL,
)
)
}
}
}

2
features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt

@ -34,8 +34,8 @@ interface RoomListEntryPoint : FeatureEntryPoint { @@ -34,8 +34,8 @@ interface RoomListEntryPoint : FeatureEntryPoint {
fun onRoomClicked(roomId: RoomId)
fun onCreateRoomClicked()
fun onSettingsClicked()
fun onSessionVerificationClicked()
fun onInvitesClicked()
}
}

5
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt

@ -52,6 +52,10 @@ class RoomListNode @AssistedInject constructor( @@ -52,6 +52,10 @@ class RoomListNode @AssistedInject constructor(
plugins<RoomListEntryPoint.Callback>().forEach { it.onSessionVerificationClicked() }
}
private fun onInvitesClicked() {
plugins<RoomListEntryPoint.Callback>().forEach { it.onInvitesClicked() }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
@ -62,6 +66,7 @@ class RoomListNode @AssistedInject constructor( @@ -62,6 +66,7 @@ class RoomListNode @AssistedInject constructor(
onOpenSettings = this::onOpenSettings,
onCreateRoomClicked = this::onCreateRoomClicked,
onVerifyClicked = this::onSessionVerificationClicked,
onInvitesClicked = this::onInvitesClicked,
)
}
}

8
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt

@ -85,6 +85,13 @@ class RoomListPresenter @Inject constructor( @@ -85,6 +85,13 @@ class RoomListPresenter @Inject constructor(
initialLoad(matrixUser)
}
val invites by client
.invitesDataSource
.roomSummaries()
.collectAsState()
Timber.v("Invites size = ${invites.size}")
// Session verification status (unknown, not verified, verified)
val sessionVerifiedStatus by sessionVerificationService.sessionVerifiedStatus.collectAsState()
var verificationPromptDismissed by rememberSaveable { mutableStateOf(false) }
@ -114,6 +121,7 @@ class RoomListPresenter @Inject constructor( @@ -114,6 +121,7 @@ class RoomListPresenter @Inject constructor(
displayVerificationPrompt = displayVerificationPrompt,
snackbarMessage = snackbarMessage,
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
displayInvites = invites.isNotEmpty(),
eventSink = ::handleEvents
)
}

1
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt

@ -30,5 +30,6 @@ data class RoomListState( @@ -30,5 +30,6 @@ data class RoomListState(
val displayVerificationPrompt: Boolean,
val hasNetworkConnection: Boolean,
val snackbarMessage: SnackbarMessage?,
val displayInvites: Boolean,
val eventSink: (RoomListEvents) -> Unit
)

2
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt

@ -34,6 +34,7 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> { @@ -34,6 +34,7 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
aRoomListState().copy(displayVerificationPrompt = true),
aRoomListState().copy(snackbarMessage = SnackbarMessage(StringR.string.common_verification_complete)),
aRoomListState().copy(hasNetworkConnection = false),
aRoomListState().copy(displayInvites = true),
)
}
@ -44,6 +45,7 @@ internal fun aRoomListState() = RoomListState( @@ -44,6 +45,7 @@ internal fun aRoomListState() = RoomListState(
hasNetworkConnection = true,
snackbarMessage = null,
displayVerificationPrompt = false,
displayInvites = false,
eventSink = {}
)

34
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt

@ -17,18 +17,23 @@ @@ -17,18 +17,23 @@
package io.element.android.features.roomlist.impl
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ExperimentalMaterial3Api
@ -46,6 +51,7 @@ import androidx.compose.runtime.getValue @@ -46,6 +51,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
@ -66,6 +72,8 @@ import io.element.android.libraries.designsystem.theme.components.Icon @@ -66,6 +72,8 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.roomListUnreadIndicator
import io.element.android.libraries.designsystem.utils.LogCompositions
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
@ -81,6 +89,7 @@ fun RoomListView( @@ -81,6 +89,7 @@ fun RoomListView(
onOpenSettings: () -> Unit = {},
onVerifyClicked: () -> Unit = {},
onCreateRoomClicked: () -> Unit = {},
onInvitesClicked: () -> Unit = {},
) {
RoomListContent(
state = state,
@ -89,6 +98,7 @@ fun RoomListView( @@ -89,6 +98,7 @@ fun RoomListView(
onOpenSettings = onOpenSettings,
onVerifyClicked = onVerifyClicked,
onCreateRoomClicked = onCreateRoomClicked,
onInvitesClicked = onInvitesClicked,
)
}
@ -101,6 +111,7 @@ fun RoomListContent( @@ -101,6 +111,7 @@ fun RoomListContent(
onRoomClicked: (RoomId) -> Unit = {},
onOpenSettings: () -> Unit = {},
onCreateRoomClicked: () -> Unit = {},
onInvitesClicked: () -> Unit = {},
) {
fun onRoomClicked(room: RoomListRoomSummary) {
onRoomClicked(room.roomId)
@ -133,7 +144,7 @@ fun RoomListContent( @@ -133,7 +144,7 @@ fun RoomListContent(
}
val snackbarHostState = remember { SnackbarHostState() }
val snackbarMessageText = if (state.snackbarMessage != null ) {
val snackbarMessageText = if (state.snackbarMessage != null) {
stringResource(state.snackbarMessage.messageResId)
} else null
val coroutineScope = rememberCoroutineScope()
@ -181,6 +192,27 @@ fun RoomListContent( @@ -181,6 +192,27 @@ fun RoomListContent(
)
}
}
if (state.displayInvites) {
item {
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxSize()) {
TextButton(
content = {
Text(stringResource(StringR.string.action_invites_list))
Spacer(Modifier.size(8.dp))
Box(
modifier = Modifier
.size(12.dp)
.clip(CircleShape)
.background(MaterialTheme.roomListUnreadIndicator())
)
},
onClick = onInvitesClicked,
)
}
}
}
items(
items = state.roomList,
contentType = { room -> room.contentType() },

29
features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt

@ -257,6 +257,35 @@ class RoomListPresenterTests { @@ -257,6 +257,35 @@ class RoomListPresenterTests {
}
}
@Test
fun `present - displays invites row if any invites exist`() = runTest {
val invitesDataSource = FakeRoomSummaryDataSource()
val presenter = RoomListPresenter(
FakeMatrixClient(
sessionId = A_SESSION_ID,
invitesDataSource = invitesDataSource
),
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
FakeNetworkMonitor(),
SnackbarDispatcher(),
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
Truth.assertThat(awaitItem().displayInvites).isFalse()
invitesDataSource.postRoomSummary(listOf(aRoomSummaryFilled()))
Truth.assertThat(awaitItem().displayInvites).isTrue()
invitesDataSource.postRoomSummary(listOf())
Truth.assertThat(awaitItem().displayInvites).isFalse()
}
}
private fun createDateFormatter(): LastMessageTimestampFormatter {
return FakeLastMessageTimestampFormatter().apply {
givenFormat(A_FORMATTED_DATE)

94
libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/OutlinedButton.kt

@ -0,0 +1,94 @@ @@ -0,0 +1,94 @@
/*
* 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.libraries.designsystem.theme.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ButtonElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@Composable
fun OutlinedButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
shape: Shape = ElementOutlinedButtonDefaults.shape,
colors: ButtonColors = ElementOutlinedButtonDefaults.buttonColors(),
elevation: ButtonElevation? = ElementOutlinedButtonDefaults.buttonElevation(),
border: BorderStroke? = ElementOutlinedButtonDefaults.border,
contentPadding: PaddingValues = ElementOutlinedButtonDefaults.ContentPadding,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable RowScope.() -> Unit
) {
androidx.compose.material3.Button(
onClick = onClick,
modifier = modifier,
enabled = enabled,
shape = shape,
colors = colors,
elevation = elevation,
border = border,
contentPadding = contentPadding,
interactionSource = interactionSource,
content = content,
)
}
object ElementOutlinedButtonDefaults {
val ContentPadding = PaddingValues(horizontal = 24.dp, vertical = 14.dp)
val shape: Shape @Composable get() = ButtonDefaults.outlinedShape
val border: BorderStroke @Composable get() = ButtonDefaults.outlinedButtonBorder
@Composable
fun buttonElevation(): ButtonElevation = ButtonDefaults.buttonElevation()
@Composable
fun buttonColors(): ButtonColors = ButtonDefaults.outlinedButtonColors()
}
@Preview
@Composable
internal fun OutlinedButtonsLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
internal fun OutlinedButtonsDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
Column {
OutlinedButton(onClick = {}, enabled = true) {
Text(text = "Click me! - Enabled")
}
OutlinedButton(onClick = {}, enabled = false) {
Text(text = "Click me! - Disabled")
}
}
}

1
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt

@ -32,6 +32,7 @@ import java.io.Closeable @@ -32,6 +32,7 @@ import java.io.Closeable
interface MatrixClient : Closeable {
val sessionId: SessionId
val roomSummaryDataSource: RoomSummaryDataSource
val invitesDataSource: RoomSummaryDataSource
fun getRoom(roomId: RoomId): MatrixRoom?
fun findDM(userId: UserId): MatrixRoom?
suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId>

2
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomSummary.kt

@ -34,9 +34,11 @@ sealed interface RoomSummary { @@ -34,9 +34,11 @@ sealed interface RoomSummary {
data class RoomSummaryDetails(
val roomId: RoomId,
val name: String,
val canonicalAlias: String? = null,
val isDirect: Boolean,
val avatarURLString: String?,
val lastMessage: RoomMessage?,
val lastMessageTimestamp: Long?,
val unreadNotificationCount: Int,
val inviter: RoomMember? = null,
)

62
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt

@ -88,20 +88,18 @@ class RustMatrixClient constructor( @@ -88,20 +88,18 @@ class RustMatrixClient constructor(
}
}
private val slidingSyncFilters by lazy {
SlidingSyncRequestListFilters(
isDm = null,
spaces = emptyList(),
isEncrypted = null,
isInvite = false,
isTombstoned = false,
roomTypes = emptyList(),
notRoomTypes = listOf("m.space"),
roomNameLike = null,
tags = emptyList(),
notTags = emptyList()
)
}
private val visibleRoomsSlidingSyncFilters = SlidingSyncRequestListFilters(
isDm = null,
spaces = emptyList(),
isEncrypted = null,
isInvite = false,
isTombstoned = false,
roomTypes = emptyList(),
notRoomTypes = listOf("m.space"),
roomNameLike = null,
tags = emptyList(),
notTags = emptyList()
)
private val visibleRoomsSlidingSyncList = SlidingSyncListBuilder()
.timelineLimit(limit = 1u)
@ -112,7 +110,7 @@ class RustMatrixClient constructor( @@ -112,7 +110,7 @@ class RustMatrixClient constructor(
RequiredState(key = "m.room.join_rules", value = ""),
)
)
.filters(slidingSyncFilters)
.filters(visibleRoomsSlidingSyncFilters)
.name(name = "CurrentlyVisibleRooms")
.syncMode(mode = SlidingSyncMode.SELECTIVE)
.addRange(0u, 20u)
@ -120,12 +118,32 @@ class RustMatrixClient constructor( @@ -120,12 +118,32 @@ class RustMatrixClient constructor(
it.build()
}
private val invitesSlidingSyncFilters = visibleRoomsSlidingSyncFilters.copy(isInvite = true)
private val invitesSlidingSyncList = SlidingSyncListBuilder()
.timelineLimit(limit = 1u)
.requiredState(
requiredState = listOf(
RequiredState(key = "m.room.avatar", value = ""),
RequiredState(key = "m.room.encryption", value = ""),
RequiredState(key = "m.room.canonical_alias", value = ""),
)
)
.filters(invitesSlidingSyncFilters)
.name(name = "CurrentInvites")
.syncMode(mode = SlidingSyncMode.SELECTIVE)
.addRange(0u, 20u)
.use {
it.build()
}
private val slidingSync = client
.slidingSync()
.homeserver("https://slidingsync.lab.matrix.org")
.withCommonExtensions()
.storageKey("ElementX")
.addList(visibleRoomsSlidingSyncList)
.addList(invitesSlidingSyncList)
.use {
it.build()
}
@ -143,6 +161,18 @@ class RustMatrixClient constructor( @@ -143,6 +161,18 @@ class RustMatrixClient constructor(
override val roomSummaryDataSource: RoomSummaryDataSource
get() = rustRoomSummaryDataSource
private val rustInvitesDataSource: RustRoomSummaryDataSource =
RustRoomSummaryDataSource(
slidingSyncObserverProxy.updateSummaryFlow,
slidingSync,
invitesSlidingSyncList,
dispatchers,
::onRestartSync
)
override val invitesDataSource: RoomSummaryDataSource
get() = rustInvitesDataSource
private var slidingSyncObserverToken: TaskHandle? = null
private val mediaResolver = RustMediaResolver(this)
@ -153,6 +183,7 @@ class RustMatrixClient constructor( @@ -153,6 +183,7 @@ class RustMatrixClient constructor(
init {
client.setDelegate(clientDelegate)
rustRoomSummaryDataSource.init()
rustInvitesDataSource.init()
slidingSync.setObserver(slidingSyncObserverProxy)
slidingSyncUpdateJob = slidingSyncObserverProxy.updateSummaryFlow
.onEach { onSlidingSyncUpdate() }
@ -249,6 +280,7 @@ class RustMatrixClient constructor( @@ -249,6 +280,7 @@ class RustMatrixClient constructor(
stopSync()
slidingSync.setObserver(null)
rustRoomSummaryDataSource.close()
rustInvitesDataSource.close()
client.setDelegate(null)
visibleRoomsSlidingSyncList.destroy()
slidingSync.destroy()

6
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomSummaryDetailsFactory.kt

@ -16,7 +16,6 @@ @@ -16,7 +16,6 @@
package io.element.android.libraries.matrix.impl.room
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
@ -29,14 +28,17 @@ class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFacto @@ -29,14 +28,17 @@ class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFacto
val latestRoomMessage = slidingSyncRoom.latestRoomMessage()?.use {
roomMessageFactory.create(it)
}
return RoomSummaryDetails(
roomId = RoomId(slidingSyncRoom.roomId()),
name = slidingSyncRoom.name() ?: slidingSyncRoom.roomId(),
canonicalAlias = room?.canonicalAlias(),
isDirect = slidingSyncRoom.isDm() ?: false,
avatarURLString = room?.avatarUrl(),
unreadNotificationCount = slidingSyncRoom.unreadNotifications().use { it.notificationCount().toInt() },
lastMessage = latestRoomMessage,
lastMessageTimestamp = latestRoomMessage?.originServerTs
lastMessageTimestamp = latestRoomMessage?.originServerTs,
inviter = room?.inviter()?.let(RoomMemberMapper::map),
)
}

1
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt

@ -41,6 +41,7 @@ class FakeMatrixClient( @@ -41,6 +41,7 @@ class FakeMatrixClient(
private val userDisplayName: Result<String> = Result.success(A_USER_NAME),
private val userAvatarURLString: Result<String> = Result.success(AN_AVATAR_URL),
override val roomSummaryDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource(),
override val invitesDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource(),
private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
private val pushersService: FakePushersService = FakePushersService(),
private val notificationService: FakeNotificationService = FakeNotificationService(),

14
libraries/ui-strings/src/main/res/values/localazy.xml

@ -54,6 +54,7 @@ @@ -54,6 +54,7 @@
<string name="action_view_source">"View Source"</string>
<string name="action_yes">"Yes"</string>
<string name="common_about">"About"</string>
<string name="common_analytics">"Analytics"</string>
<string name="common_audio">"Audio"</string>
<string name="common_bubbles">"Bubbles"</string>
<string name="common_creating_room">"Creating room…"</string>
@ -181,8 +182,17 @@ @@ -181,8 +182,17 @@
<string name="room_timeline_beginning_of_room">"This is the beginning of %1$s."</string>
<string name="room_timeline_beginning_of_room_no_name">"This is the beginning of this conversation."</string>
<string name="room_timeline_read_marker_title">"New"</string>
<string name="screen_invites_empty_list">"No Invites"</string>
<string name="screen_invites_invited_you">"%1$s invited you"</string>
<string name="screen_analytics_help_us_improve">"Help us identify issues and improve %1$s by sharing anonymous usage data."</string>
<string name="screen_analytics_prompt_data_usage">"We "<b>"don\'t"</b>" record or profile any account data"</string>
<string name="screen_analytics_prompt_help_us_improve">"Help us identify issues and improve %1$s by sharing anonymous usage data."</string>
<string name="screen_analytics_prompt_read_terms">"You can read all our terms %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"here"</string>
<string name="screen_analytics_prompt_settings">"You can turn this off anytime in settings"</string>
<string name="screen_analytics_prompt_third_party_sharing">"We "<b>"don\'t"</b>" share information with third parties"</string>
<string name="screen_analytics_prompt_title">"Help improve %1$s"</string>
<string name="screen_analytics_read_terms">"You can read all our terms %1$s."</string>
<string name="screen_analytics_read_terms_content_link">"here"</string>
<string name="screen_analytics_share_data">"Share analytics data"</string>
<string name="screen_report_content_block_user">"Block user"</string>
<string name="screen_report_content_block_user_hint">"Check if you want to hide all current and future messages from this user"</string>
<string name="screen_room_member_details_block_alert_action">"Block"</string>

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl.components_null_DefaultGroup_InviteSummaryRowDarkPreview_0_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl.components_null_DefaultGroup_InviteSummaryRowDarkPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl.components_null_DefaultGroup_InviteSummaryRowDarkPreview_0_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl.components_null_DefaultGroup_InviteSummaryRowLightPreview_0_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl.components_null_DefaultGroup_InviteSummaryRowLightPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl.components_null_DefaultGroup_InviteSummaryRowLightPreview_0_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewDarkPreview_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewLightPreview_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewLightPreview_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_OutlinedButtonsDarkPreview_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_OutlinedButtonsLightPreview_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

6
tools/localazy/config.json

@ -25,6 +25,12 @@ @@ -25,6 +25,12 @@
"screen_onboarding_.*"
]
},
{
"name": ":features:invitelist:impl",
"includeRegex": [
"screen_invites_.*"
]
},
{
"name": ":features:createroom:impl",
"includeRegex": [

Loading…
Cancel
Save