Skip to content

Commit 9bc2c4a

Browse files
jmartinespnetworkExceptionElementBot
authored
Add shortcut suggestions for rooms, remove then when leaving (#5180)
* Report shortcut usage for outgoing messages This patch adds support for creating and pushing dynamic long-lived shortcuts for outgoing messages. This together with an existing reference to the roomId used by the shortcuts as an identifer allows conversations to be prioritized. See https://developer.android.com/training/sharing/direct-share-targets#report-usage-outgoing * Simplify how to get the other user in a DM room * Add initial avatar icons to shortcuts * Remove room shortcuts when they're no longer joined * Try using API 33 for the new tests. They worked locally with API 30, so it's weird the CI asks for a higher API version. * Add observers for the pin code and session logout states. With this we can prevent new shortcuts from being created and remove existing ones when needed. * Wrap all calls to `ShortcutManagerCompat` with `runCatchingExceptions` to avoid crashes * Make `DefaultNotificationConversationService` a singleton. --------- Co-authored-by: networkException <[email protected]> Co-authored-by: ElementBot <[email protected]>
1 parent 35928e3 commit 9bc2c4a

File tree

27 files changed

+681
-27
lines changed

27 files changed

+681
-27
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
8181
import io.element.android.libraries.matrix.api.permalink.PermalinkData
8282
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
8383
import io.element.android.libraries.matrix.api.verification.VerificationRequest
84+
import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
8485
import io.element.android.services.appnavstate.api.AppNavigationStateService
8586
import kotlinx.coroutines.CoroutineScope
8687
import kotlinx.coroutines.flow.first
@@ -121,6 +122,7 @@ class LoggedInFlowNode @AssistedInject constructor(
121122
private val mediaPreviewConfigMigration: MediaPreviewConfigMigration,
122123
private val sessionEnterpriseService: SessionEnterpriseService,
123124
private val networkMonitor: NetworkMonitor,
125+
private val notificationConversationService: NotificationConversationService,
124126
snackbarDispatcher: SnackbarDispatcher,
125127
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
126128
backstack = BackStack(
@@ -206,6 +208,12 @@ class LoggedInFlowNode @AssistedInject constructor(
206208
}
207209
.launchIn(lifecycleScope)
208210
},
211+
onResume = {
212+
lifecycleScope.launch {
213+
val availableRoomIds = matrixClient.getJoinedRoomIds().getOrNull() ?: return@launch
214+
notificationConversationService.onAvailableRoomsChanged(sessionId = matrixClient.sessionId, roomIds = availableRoomIds)
215+
}
216+
},
209217
onDestroy = {
210218
appNavigationStateService.onLeavingSpace(id)
211219
appNavigationStateService.onLeavingSession(id)

features/leaveroom/impl/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ dependencies {
2424
implementation(projects.libraries.matrix.api)
2525
implementation(projects.libraries.designsystem)
2626
implementation(projects.libraries.uiStrings)
27+
implementation(projects.libraries.push.api)
2728

2829
testImplementation(libs.test.junit)
2930
testImplementation(libs.coroutines.test)
@@ -32,5 +33,6 @@ dependencies {
3233
testImplementation(libs.test.truth)
3334
testImplementation(libs.test.turbine)
3435
testImplementation(projects.libraries.matrix.test)
36+
testImplementation(projects.libraries.push.test)
3537
testImplementation(projects.tests.testutils)
3638
}

features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.room.BaseRoom
2424
import io.element.android.libraries.matrix.api.room.RoomMember
2525
import io.element.android.libraries.matrix.api.room.isDm
2626
import io.element.android.libraries.matrix.api.room.powerlevels.usersWithRole
27+
import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
2728
import kotlinx.coroutines.CoroutineScope
2829
import kotlinx.coroutines.flow.first
2930
import kotlinx.coroutines.launch
@@ -33,6 +34,7 @@ import javax.inject.Inject
3334
class LeaveRoomPresenter @Inject constructor(
3435
private val client: MatrixClient,
3536
private val dispatchers: CoroutineDispatchers,
37+
private val notificationConversationService: NotificationConversationService,
3638
) : Presenter<LeaveRoomState> {
3739
@Composable
3840
override fun present(): LeaveRoomState {
@@ -78,6 +80,7 @@ class LeaveRoomPresenter @Inject constructor(
7880
client.getRoom(roomId)!!.use { room ->
7981
room
8082
.leave()
83+
.onSuccess { notificationConversationService.onLeftRoom(client.sessionId, roomId) }
8184
.onFailure { Timber.e(it, "Error while leaving room ${room.roomId}") }
8285
.getOrThrow()
8386
}

features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveBaseRoomPresenterTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
1818
import io.element.android.libraries.matrix.test.FakeMatrixClient
1919
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
2020
import io.element.android.libraries.matrix.test.room.aRoomInfo
21+
import io.element.android.libraries.push.test.notifications.conversations.FakeNotificationConversationService
2122
import io.element.android.tests.testutils.WarmUpRule
2223
import io.element.android.tests.testutils.lambda.assert
2324
import io.element.android.tests.testutils.lambda.lambdaRecorder
@@ -209,4 +210,5 @@ private fun TestScope.createLeaveRoomPresenter(
209210
): LeaveRoomPresenter = LeaveRoomPresenter(
210211
client = client,
211212
dispatchers = testCoroutineDispatchers(false),
213+
notificationConversationService = FakeNotificationConversationService(),
212214
)

features/messages/impl/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ dependencies {
5050
implementation(projects.libraries.voiceplayer.api)
5151
implementation(projects.libraries.voicerecorder.api)
5252
implementation(projects.libraries.mediaplayer.api)
53+
implementation(projects.libraries.push.api)
5354
implementation(projects.libraries.uiUtils)
5455
implementation(projects.libraries.testtags)
5556
implementation(projects.features.networkmonitor.api)
@@ -76,6 +77,7 @@ dependencies {
7677
testImplementation(libs.test.turbine)
7778
testImplementation(projects.libraries.matrix.test)
7879
testImplementation(projects.libraries.dateformatter.test)
80+
testImplementation(projects.libraries.push.test)
7981
testImplementation(projects.features.location.test)
8082
testImplementation(projects.features.networkmonitor.test)
8183
testImplementation(projects.features.messages.test)

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,22 @@ import io.element.android.libraries.matrix.api.room.IntentionalMention
5252
import io.element.android.libraries.matrix.api.room.JoinedRoom
5353
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
5454
import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
55+
import io.element.android.libraries.matrix.api.room.getDirectRoomMember
5556
import io.element.android.libraries.matrix.api.room.isDm
57+
import io.element.android.libraries.matrix.api.room.roomMembers
5658
import io.element.android.libraries.matrix.api.timeline.TimelineException
5759
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
5860
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
5961
import io.element.android.libraries.matrix.ui.messages.reply.map
62+
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
6063
import io.element.android.libraries.mediapickers.api.PickerProvider
6164
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
6265
import io.element.android.libraries.mediaupload.api.MediaSender
6366
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
6467
import io.element.android.libraries.permissions.api.PermissionsEvents
6568
import io.element.android.libraries.permissions.api.PermissionsPresenter
6669
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
70+
import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
6771
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
6872
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
6973
import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState
@@ -118,6 +122,7 @@ class MessageComposerPresenter @AssistedInject constructor(
118122
private val pillificationHelper: TextPillificationHelper,
119123
private val suggestionsProcessor: SuggestionsProcessor,
120124
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
125+
private val notificationConversationService: NotificationConversationService,
121126
) : Presenter<MessageComposerState> {
122127
@AssistedFactory
123128
interface Factory {
@@ -466,6 +471,18 @@ class MessageComposerPresenter @AssistedInject constructor(
466471
}
467472
}
468473
}
474+
475+
val roomInfo = room.info()
476+
val roomMembers = room.membersStateFlow.value
477+
478+
notificationConversationService.onSendMessage(
479+
sessionId = room.sessionId,
480+
roomId = roomInfo.id,
481+
roomName = roomInfo.name ?: roomInfo.id.value,
482+
roomIsDirect = roomInfo.isDm,
483+
roomAvatarUrl = roomInfo.avatarUrl ?: roomMembers.getDirectRoomMember(roomInfo = roomInfo, sessionId = room.sessionId)?.avatarUrl,
484+
)
485+
469486
analyticsService.capture(
470487
Composer(
471488
inThread = capturedMode.inThread,

features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ import io.element.android.libraries.permissions.test.FakePermissionsPresenterFac
8585
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
8686
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
8787
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
88+
import io.element.android.libraries.push.test.notifications.conversations.FakeNotificationConversationService
8889
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
8990
import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme
9091
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
@@ -128,6 +129,7 @@ class MessageComposerPresenterTest {
128129
private val mockMediaUrl: Uri = mockk("localMediaUri")
129130
private val localMediaFactory = FakeLocalMediaFactory(mockMediaUrl)
130131
private val analyticsService = FakeAnalyticsService()
132+
private val notificationConversationService = FakeNotificationConversationService()
131133

132134
@Test
133135
fun `present - initial state`() = runTest {
@@ -1578,6 +1580,7 @@ class MessageComposerPresenterTest {
15781580
pillificationHelper = textPillificationHelper,
15791581
suggestionsProcessor = SuggestionsProcessor(),
15801582
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
1583+
notificationConversationService = notificationConversationService,
15811584
).apply {
15821585
isTesting = true
15831586
showTextFormatting = isRichTextEditorEnabled

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ interface MatrixClient {
5353
suspend fun getJoinedRoom(roomId: RoomId): JoinedRoom?
5454
suspend fun getRoom(roomId: RoomId): BaseRoom?
5555
suspend fun findDM(userId: UserId): Result<RoomId?>
56+
suspend fun getJoinedRoomIds(): Result<Set<RoomId>>
5657
suspend fun ignoreUser(userId: UserId): Result<Unit>
5758
suspend fun unignoreUser(userId: UserId): Result<Unit>
5859
suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId>

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package io.element.android.libraries.matrix.api.room
99

1010
import androidx.compose.runtime.Immutable
11+
import io.element.android.libraries.matrix.api.core.SessionId
1112
import kotlinx.collections.immutable.ImmutableList
1213

1314
@Immutable
@@ -34,3 +35,9 @@ fun RoomMembersState.joinedRoomMembers(): List<RoomMember> {
3435
fun RoomMembersState.activeRoomMembers(): List<RoomMember> {
3536
return roomMembers().orEmpty().filter { it.membership.isActive() }
3637
}
38+
39+
fun RoomMembersState.getDirectRoomMember(roomInfo: RoomInfo, sessionId: SessionId): RoomMember? {
40+
return roomMembers()
41+
?.takeIf { roomInfo.isDm }
42+
?.find { it.userId != sessionId && it.membership.isActive() }
43+
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ import org.matrix.rustcomponents.sdk.AuthDataPasswordDetails
107107
import org.matrix.rustcomponents.sdk.Client
108108
import org.matrix.rustcomponents.sdk.ClientException
109109
import org.matrix.rustcomponents.sdk.IgnoredUsersListener
110+
import org.matrix.rustcomponents.sdk.Membership
110111
import org.matrix.rustcomponents.sdk.NotificationProcessSetup
111112
import org.matrix.rustcomponents.sdk.PowerLevels
112113
import org.matrix.rustcomponents.sdk.RoomInfoListener
@@ -277,6 +278,7 @@ class RustMatrixClient(
277278
}
278279

279280
override suspend fun getRoom(roomId: RoomId): BaseRoom? = withContext(sessionDispatcher) {
281+
innerClient.rooms()
280282
roomFactory.getBaseRoom(roomId)
281283
}
282284

@@ -311,6 +313,15 @@ class RustMatrixClient(
311313
}
312314
}
313315

316+
override suspend fun getJoinedRoomIds(): Result<Set<RoomId>> = withContext(sessionDispatcher) {
317+
runCatchingExceptions {
318+
innerClient.rooms()
319+
.filter { it.membership() == Membership.JOINED }
320+
.map { RoomId(it.id()) }
321+
.toSet()
322+
}
323+
}
324+
314325
override suspend fun ignoreUser(userId: UserId): Result<Unit> = withContext(sessionDispatcher) {
315326
runCatchingExceptions {
316327
innerClient.ignoreUser(userId.value)

0 commit comments

Comments
 (0)