Merge branch 'develop' into feature/audio-focus-voice-recording

This commit is contained in:
celeste 2026-02-12 16:41:38 +01:00 committed by GitHub
commit a66c6ba74e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 260 additions and 438 deletions

View file

@ -18,9 +18,8 @@ jobs:
build-apk:
name: Build APK
runs-on: ubuntu-latest
# Allow one per PR.
concurrency:
group: ${{ format('maestro-{0}', github.ref) }}
group: ${{ format('maestro-build-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v6
@ -57,10 +56,10 @@ jobs:
name: Maestro test suite
runs-on: ubuntu-latest
needs: [ build-apk ]
# Allow one per PR.
# Allow only one to run at a time, since they use the same environment.
# Otherwise, tests running in parallel can break each other.
concurrency:
group: ${{ format('maestro-{0}', github.ref) }}
cancel-in-progress: true
group: maestro-test
steps:
- uses: actions/checkout@v6
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
@ -110,6 +109,21 @@ jobs:
retention-days: 5
overwrite: true
if-no-files-found: error
- name: Update summary (success)
if: steps.maestro_test.outcome == 'success'
run: |
echo "### Maestro tests worked :rocket:!"
- name: Update summary (failure)
if: steps.maestro_test.outcome != 'success'
run: |
LOG_FILE=$(find ~/.maestro/tests/ -name maestro.log)
echo "Log file: $LOG_FILE"
LOG_LINES="$(tail -n 30 $LOG_FILE)"
echo "### :x: Maestro tests failed...
\`\`\`
$LOG_LINES
\`\`\`" >> $GITHUB_STEP_SUMMARY
- name: Fail the workflow in case of error in test
if: steps.maestro_test.outcome != 'success'
run: |

View file

@ -8,6 +8,13 @@
# Please see LICENSE in the repository root for full details.
#
# First we disable the onboarding flow on Chrome, which is a source of issues
# (see https://stackoverflow.com/a/64629745)
echo "Disabling Chrome onboarding flow"
adb shell am set-debug-app --persistent com.android.chrome
adb shell 'echo "chrome --disable-fre --no-default-browser-check --no-first-run" > /data/local/tmp/chrome-command-line'
adb shell am start -n com.android.chrome/com.google.android.apps.chrome.Main
adb install -r $1
echo "Starting the screen recording..."
adb push .github/workflows/scripts/maestro/local-recording.sh /data/local/tmp/

View file

@ -8,27 +8,6 @@ appId: ${MAESTRO_APP_ID}
- tapOn:
id: "login-continue"
## MAS page
## Conditional workflow to pass the Chrome first launch welcome page.
- retry:
maxRetries: 3
commands:
- runFlow:
when:
visible: 'Use without an account'
commands:
- tapOn: "Use without an account"
## For older chrome versions
- runFlow:
when:
visible: 'Accept & continue'
commands:
- tapOn: "Accept & continue"
- runFlow:
when:
visible: 'No thanks'
commands:
- tapOn: "No thanks"
## Working when running Maestro locally, but not on the CI yet.
- retry:
maxRetries: 3
commands:

View file

@ -1,5 +1,8 @@
appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible: "Enter recovery key"
timeout: 30000
- takeScreenshot: build/maestro/150-Verify
- tapOn: "Enter recovery key"
- tapOn:
@ -7,7 +10,10 @@ appId: ${MAESTRO_APP_ID}
- inputText: ${MAESTRO_RECOVERY_KEY}
- hideKeyboard
- tapOn: "Continue"
- extendedWaitUntil:
visible: "Device verified"
timeout: 30000
- retry:
maxRetries: 3
commands:
- extendedWaitUntil:
visible: "Device verified"
timeout: 30000
- tapOn: "Continue"

View file

@ -2,4 +2,4 @@ appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible: "Be in your element"
timeout: 10000
timeout: 30000

View file

@ -2,4 +2,4 @@ appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible: "Confirm your identity"
timeout: 20000
timeout: 60000

View file

@ -79,7 +79,6 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.UserId
@ -211,8 +210,6 @@ class LoggedInFlowNode(
onCreate = {
analyticsRoomListStateWatcher.start()
appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId)
// TODO We do not support Space yet, so directly navigate to main space
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
loggedInFlowProcessor.observeEvents(sessionCoroutineScope)
matrixClient.sessionVerificationService.setListener(verificationListener)
mediaPreviewConfigMigration()
@ -242,7 +239,6 @@ class LoggedInFlowNode(
}
},
onDestroy = {
appNavigationStateService.onLeavingSpace(id)
appNavigationStateService.onLeavingSession(id)
loggedInFlowProcessor.stopObserving()
matrixClient.sessionVerificationService.setListener(null)

View file

@ -82,6 +82,13 @@ class WebViewAudioManager(
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
)
private val audioDeviceComparator = Comparator<AudioDeviceInfo> { a, b ->
// If the device type is not in the wantedDeviceTypes list, we give it a high index, (i.e. low priority)
val indexOfA = wantedDeviceTypes.indexOf(a.type).let { if (it == -1) Int.MAX_VALUE else it }
val indexOfB = wantedDeviceTypes.indexOf(b.type).let { if (it == -1) Int.MAX_VALUE else it }
indexOfA.compareTo(indexOfB)
}
private val audioManager = webView.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
/**
@ -134,7 +141,7 @@ class WebViewAudioManager(
if (validNewDevices.isEmpty()) return
// We need to calculate the available devices ourselves, since calling `listAudioDevices` will return an outdated list
val audioDevices = (listAudioDevices() + validNewDevices).distinctBy { it.id }
val audioDevices = (listAudioDevices() + validNewDevices).distinctBy { it.id }.sortedWith(audioDeviceComparator)
setAvailableAudioDevices(audioDevices.map(SerializableAudioDevice::fromAudioDeviceInfo))
// This should automatically switch to a new device if it has a higher priority than the current one
selectDefaultAudioDevice(audioDevices)
@ -294,7 +301,7 @@ class WebViewAudioManager(
}
/**
* Returns the list of available audio devices.
* Returns the list of available audio devices, sorted by likelihood of it being used for communication.
*
* On Android 11 ([Build.VERSION_CODES.R]) and lower, it returns the list of output devices as a fallback.
*/
@ -304,7 +311,7 @@ class WebViewAudioManager(
} else {
val rawAudioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
rawAudioDevices.filter { it.type in wantedDeviceTypes && it.isSink }
}
}.sortedWith(audioDeviceComparator)
}
/**
@ -323,19 +330,12 @@ class WebViewAudioManager(
}
/**
* Selects the default audio device based on the available devices.
* Selects the default audio device based on the sorted available devices.
*
* @param availableDevices The list of available audio devices to select from. If not provided, it will use the current list of audio devices.
*/
private fun selectDefaultAudioDevice(availableDevices: List<AudioDeviceInfo> = listAudioDevices()) {
val selectedDevice = availableDevices
.minByOrNull {
wantedDeviceTypes.indexOf(it.type).let { index ->
// If the device type is not in the wantedDeviceTypes list, we give it a low priority
if (index == -1) Int.MAX_VALUE else index
}
}
val selectedDevice = availableDevices.firstOrNull()
expectedNewCommunicationDeviceId = selectedDevice?.id
audioManager.selectAudioDevice(selectedDevice)

View file

@ -16,13 +16,13 @@ datastore = "1.2.0"
constraintlayout = "2.2.1"
constraintlayout_compose = "1.1.1"
lifecycle = "2.10.0"
activity = "1.12.3"
activity = "1.12.4"
media3 = "1.9.2"
camera = "1.5.3"
work = "2.11.1"
# Compose
compose_bom = "2026.01.00"
compose_bom = "2026.02.00"
# Coroutines
coroutines = "1.10.2"
@ -32,7 +32,7 @@ accompanist = "0.37.3"
# Test
test_core = "1.7.0"
roborazzi = "1.58.0"
roborazzi = "1.59.0"
# Jetbrain
datetime = "0.7.1"

View file

@ -16,8 +16,6 @@ import androidx.core.text.toSpannable
import androidx.core.text.util.LinkifyCompat
import io.element.android.libraries.core.extensions.runCatchingExceptions
import timber.log.Timber
import kotlin.collections.component1
import kotlin.collections.component2
/**
* Helper class to linkify text while preserving existing URL spans.
@ -59,7 +57,8 @@ object LinkifyHelper {
// Adapt the url in the URL span to the new end index too if needed
if (end != newEnd) {
val url = spannable.subSequence(start, newEnd).toString()
val diff = end - newEnd
val url = urlSpan.url.substring(0, urlSpan.url.length - diff)
spannable.removeSpan(urlSpan)
spannable.setSpan(URLSpan(url), start, newEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
} else {
@ -87,12 +86,12 @@ object LinkifyHelper {
var end = end
// Trailing punctuation found, adjust the end index
while (spannable[end - 1] in sequenceOf('.', ',', ';', ':', '!', '?', '…') && end > start) {
while (end > start && spannable[end - 1] in sequenceOf('.', ',', ';', ':', '!', '?', '…')) {
end--
}
// If the last character is a closing parenthesis, check if it's part of a pair
if (spannable[end - 1] == ')' && end > start) {
if (end > start && spannable[end - 1] == ')') {
val linkifiedTextLastPath = spannable.substring(start, end).substringAfterLast('/')
val closingParenthesisCount = linkifiedTextLastPath.count { it == ')' }
val openingParenthesisCount = linkifiedTextLastPath.count { it == '(' }

View file

@ -10,7 +10,9 @@ package io.element.android.libraries.androidutils.text
import android.telephony.TelephonyManager
import android.text.style.URLSpan
import androidx.core.text.buildSpannedString
import androidx.core.text.getSpans
import androidx.core.text.inSpans
import androidx.core.text.toSpannable
import com.google.common.truth.Truth.assertThat
import io.element.android.tests.testutils.WarmUpRule
@ -140,4 +142,27 @@ class LinkifierHelperTest {
assertThat(urlSpans.size).isEqualTo(1)
assertThat(urlSpans.first().url).isEqualTo("https://github.com/element-hq/element-android/READ(ME)")
}
@Test
fun `linkification handles trailing question marks`() {
val text = "A url: https://github.com/element-hq/element-android?"
val result = LinkifyHelper.linkify(text)
val urlSpans = result.toSpannable().getSpans<URLSpan>()
assertThat(urlSpans.size).isEqualTo(1)
assertThat(urlSpans.first().url).isEqualTo("https://github.com/element-hq/element-android")
}
@Test
fun `linkification doesn't modify existing URLSpan`() {
val text = buildSpannedString {
append("A url: ")
inSpans(URLSpan("https://github.com/element-hq/element-android?")) {
append("here")
}
}
val result = LinkifyHelper.linkify(text)
val urlSpans = result.toSpannable().getSpans<URLSpan>()
assertThat(urlSpans.size).isEqualTo(1)
assertThat(urlSpans.first().url).isEqualTo("https://github.com/element-hq/element-android?")
}
}

View file

@ -9,8 +9,3 @@
package io.element.android.libraries.matrix.api.core
typealias SpaceId = RoomId
/**
* Value to use when no space is selected by the user.
*/
val MAIN_SPACE = SpaceId("!mainSpace:local")

View file

@ -34,6 +34,7 @@ interface ActiveNotificationsProvider {
fun getMembershipNotificationForSession(sessionId: SessionId): List<StatusBarNotification>
fun getMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification>
fun getSummaryNotification(sessionId: SessionId): StatusBarNotification?
fun getFallbackNotification(sessionId: SessionId): StatusBarNotification?
fun count(sessionId: SessionId): Int
}
@ -76,6 +77,11 @@ class DefaultActiveNotificationsProvider(
return getNotificationsForSession(sessionId).find { it.id == summaryId }
}
override fun getFallbackNotification(sessionId: SessionId): StatusBarNotification? {
val fallbackId = NotificationIdProvider.getFallbackNotificationId(sessionId)
return getNotificationsForSession(sessionId).find { it.id == fallbackId }
}
override fun count(sessionId: SessionId): Int {
return getNotificationsForSession(sessionId).size
}

View file

@ -72,17 +72,16 @@ class DefaultNotificationDrawerManager(
when (navigationState) {
NavigationState.Root -> {}
is NavigationState.Session -> {}
is NavigationState.Space -> {}
is NavigationState.Room -> {
// Cleanup notification for current room
clearMessagesForRoom(
sessionId = navigationState.parentSpace.parentSession.sessionId,
sessionId = navigationState.parentSession.sessionId,
roomId = navigationState.roomId,
)
}
is NavigationState.Thread -> {
clearMessagesForThread(
sessionId = navigationState.parentRoom.parentSpace.parentSession.sessionId,
sessionId = navigationState.parentRoom.parentSession.sessionId,
roomId = navigationState.parentRoom.roomId,
threadId = navigationState.threadId,
)

View file

@ -12,15 +12,12 @@ import dev.zacsweers.metro.Inject
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
import io.element.android.services.toolbox.api.strings.StringProvider
import io.element.android.services.toolbox.api.systemclock.SystemClock
@Inject
class FallbackNotificationFactory(
private val clock: SystemClock,
private val stringProvider: StringProvider,
) {
fun create(
sessionId: SessionId,
@ -36,7 +33,7 @@ class FallbackNotificationFactory(
isRedacted = false,
isUpdated = false,
timestamp = clock.epochMillis(),
description = stringProvider.getString(R.string.notification_fallback_content),
description = "",
cause = cause,
)
}

View file

@ -9,24 +9,18 @@
package io.element.android.libraries.push.impl.notifications
import android.app.Notification
import android.graphics.Typeface
import android.text.style.StyleSpan
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import coil3.ImageLoader
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.factories.NotificationAccountParams
import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
import io.element.android.services.toolbox.api.strings.StringProvider
interface NotificationDataFactory {
suspend fun toNotifications(
@ -51,16 +45,15 @@ interface NotificationDataFactory {
@JvmName("toNotificationFallbackEvents")
@Suppress("INAPPLICABLE_JVM_NAME")
fun toNotifications(
fun toNotification(
fallback: List<FallbackNotifiableEvent>,
notificationAccountParams: NotificationAccountParams,
): List<OneShotNotification>
): OneShotNotification?
fun createSummaryNotification(
roomNotifications: List<RoomNotification>,
invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>,
fallbackNotifications: List<OneShotNotification>,
notificationAccountParams: NotificationAccountParams,
): SummaryNotification
}
@ -71,7 +64,6 @@ class DefaultNotificationDataFactory(
private val roomGroupMessageCreator: RoomGroupMessageCreator,
private val summaryGroupMessageCreator: SummaryGroupMessageCreator,
private val activeNotificationsProvider: ActiveNotificationsProvider,
private val stringProvider: StringProvider,
) : NotificationDataFactory {
override suspend fun toNotifications(
messages: List<NotifiableMessageEvent>,
@ -81,10 +73,7 @@ class DefaultNotificationDataFactory(
val messagesToDisplay = messages.filterNot { it.canNotBeDisplayed() }
.groupBy { it.roomId }
return messagesToDisplay.flatMap { (roomId, events) ->
val roomName = events.lastOrNull()?.roomName ?: roomId.value
val isDm = events.lastOrNull()?.roomIsDm ?: false
val eventsByThreadId = events.groupBy { it.threadId }
eventsByThreadId.map { (threadId, events) ->
val notification = roomGroupMessageCreator.createRoomMessage(
events = events,
@ -98,7 +87,6 @@ class DefaultNotificationDataFactory(
notification = notification,
roomId = roomId,
threadId = threadId,
summaryLine = createRoomMessagesGroupSummaryLine(events, roomName, isDm),
messageCount = events.size,
latestTimestamp = events.maxOf { it.timestamp },
shouldBing = events.any { it.noisy }
@ -123,7 +111,6 @@ class DefaultNotificationDataFactory(
OneShotNotification(
tag = event.roomId.value,
notification = notificationCreator.createRoomInvitationNotification(notificationAccountParams, event),
summaryLine = event.description,
isNoisy = event.noisy,
timestamp = event.timestamp
)
@ -140,7 +127,6 @@ class DefaultNotificationDataFactory(
OneShotNotification(
tag = event.eventId.value,
notification = notificationCreator.createSimpleEventNotification(notificationAccountParams, event),
summaryLine = event.description,
isNoisy = event.noisy,
timestamp = event.timestamp
)
@ -149,26 +135,31 @@ class DefaultNotificationDataFactory(
@JvmName("toNotificationFallbackEvents")
@Suppress("INAPPLICABLE_JVM_NAME")
override fun toNotifications(
override fun toNotification(
fallback: List<FallbackNotifiableEvent>,
notificationAccountParams: NotificationAccountParams,
): List<OneShotNotification> {
return fallback.map { event ->
OneShotNotification(
tag = event.eventId.value,
notification = notificationCreator.createFallbackNotification(notificationAccountParams, event),
summaryLine = event.description.orEmpty(),
isNoisy = false,
timestamp = event.timestamp
)
}
): OneShotNotification? {
if (fallback.isEmpty()) return null
val existingNotification = activeNotificationsProvider
.getFallbackNotification(notificationAccountParams.user.userId)
?.notification
val notification = notificationCreator.createFallbackNotification(
existingNotification,
notificationAccountParams,
fallback,
)
return OneShotNotification(
tag = "FALLBACK",
notification = notification,
isNoisy = false,
timestamp = fallback.first().timestamp
)
}
override fun createSummaryNotification(
roomNotifications: List<RoomNotification>,
invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>,
fallbackNotifications: List<OneShotNotification>,
notificationAccountParams: NotificationAccountParams,
): SummaryNotification {
return when {
@ -178,59 +169,17 @@ class DefaultNotificationDataFactory(
roomNotifications = roomNotifications,
invitationNotifications = invitationNotifications,
simpleNotifications = simpleNotifications,
fallbackNotifications = fallbackNotifications,
notificationAccountParams = notificationAccountParams,
)
)
}
}
private fun createRoomMessagesGroupSummaryLine(events: List<NotifiableMessageEvent>, roomName: String, roomIsDm: Boolean): CharSequence {
return when (events.size) {
1 -> createFirstMessageSummaryLine(events.first(), roomName, roomIsDm)
else -> {
stringProvider.getQuantityString(
R.plurals.notification_compat_summary_line_for_room,
events.size,
roomName,
events.size
)
}
}
}
private fun createFirstMessageSummaryLine(event: NotifiableMessageEvent, roomName: String, roomIsDm: Boolean): CharSequence {
return if (roomIsDm) {
buildSpannedString {
event.senderDisambiguatedDisplayName?.let {
inSpans(StyleSpan(Typeface.BOLD)) {
append(it)
append(": ")
}
}
append(event.description)
}
} else {
buildSpannedString {
inSpans(StyleSpan(Typeface.BOLD)) {
append(roomName)
append(": ")
event.senderDisambiguatedDisplayName?.let {
append(it)
append(" ")
}
}
append(event.description)
}
}
}
}
data class RoomNotification(
val notification: Notification,
val roomId: RoomId,
val threadId: ThreadId?,
val summaryLine: CharSequence,
val messageCount: Int,
val latestTimestamp: Long,
val shouldBing: Boolean,
@ -239,7 +188,6 @@ data class RoomNotification(
return notification == other.notification &&
roomId == other.roomId &&
threadId == other.threadId &&
summaryLine.toString() == other.summaryLine.toString() &&
messageCount == other.messageCount &&
latestTimestamp == other.latestTimestamp &&
shouldBing == other.shouldBing
@ -249,7 +197,6 @@ data class RoomNotification(
data class OneShotNotification(
val notification: Notification,
val tag: String,
val summaryLine: CharSequence,
val isNoisy: Boolean,
val timestamp: Long,
)

View file

@ -55,12 +55,11 @@ class NotificationRenderer(
val roomNotifications = notificationDataFactory.toNotifications(groupedEvents.roomEvents, imageLoader, notificationAccountParams)
val invitationNotifications = notificationDataFactory.toNotifications(groupedEvents.invitationEvents, notificationAccountParams)
val simpleNotifications = notificationDataFactory.toNotifications(groupedEvents.simpleEvents, notificationAccountParams)
val fallbackNotifications = notificationDataFactory.toNotifications(groupedEvents.fallbackEvents, notificationAccountParams)
val fallbackNotification = notificationDataFactory.toNotification(groupedEvents.fallbackEvents, notificationAccountParams)
val summaryNotification = notificationDataFactory.createSummaryNotification(
roomNotifications = roomNotifications,
invitationNotifications = invitationNotifications,
simpleNotifications = simpleNotifications,
fallbackNotifications = fallbackNotifications,
notificationAccountParams = notificationAccountParams,
)
@ -107,13 +106,12 @@ class NotificationRenderer(
}
}
// Show only the first fallback notification
if (fallbackNotifications.isNotEmpty()) {
Timber.tag(loggerTag.value).d("Showing fallback notification")
if (fallbackNotification != null) {
Timber.tag(loggerTag.value).d("Showing or updating fallback notification")
notificationDisplayer.showNotification(
tag = "FALLBACK",
tag = fallbackNotification.tag,
id = NotificationIdProvider.getFallbackNotificationId(currentUser.userId),
notification = fallbackNotifications.first().notification
notification = fallbackNotification.notification,
)
}

View file

@ -22,7 +22,6 @@ interface SummaryGroupMessageCreator {
roomNotifications: List<RoomNotification>,
invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>,
fallbackNotifications: List<OneShotNotification>,
): Notification
}
@ -45,7 +44,6 @@ class DefaultSummaryGroupMessageCreator(
roomNotifications: List<RoomNotification>,
invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>,
fallbackNotifications: List<OneShotNotification>,
): Notification {
val summaryIsNoisy = roomNotifications.any { it.shouldBing } ||
invitationNotifications.any { it.isNoisy } ||

View file

@ -75,8 +75,9 @@ interface NotificationCreator {
): Notification
fun createFallbackNotification(
existingNotification: Notification?,
notificationAccountParams: NotificationAccountParams,
fallbackNotifiableEvent: FallbackNotifiableEvent,
fallbackNotifiableEvents: List<FallbackNotifiableEvent>,
): Notification
/**
@ -240,11 +241,13 @@ class DefaultNotificationCreator(
.addAction(rejectInvitationActionFactory.create(inviteNotifiableEvent))
.addAction(acceptInvitationActionFactory.create(inviteNotifiableEvent))
// Build the pending intent for when the notification is clicked
.setContentIntent(pendingIntentFactory.createOpenRoomPendingIntent(
sessionId = inviteNotifiableEvent.sessionId,
roomId = inviteNotifiableEvent.roomId,
eventId = null,
))
.setContentIntent(
pendingIntentFactory.createOpenRoomPendingIntent(
sessionId = inviteNotifiableEvent.sessionId,
roomId = inviteNotifiableEvent.roomId,
eventId = null,
)
)
.apply {
if (inviteNotifiableEvent.noisy) {
// Compat
@ -276,12 +279,14 @@ class DefaultNotificationCreator(
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
.configureWith(notificationAccountParams)
.setAutoCancel(true)
.setContentIntent(pendingIntentFactory.createOpenRoomPendingIntent(
sessionId = simpleNotifiableEvent.sessionId,
roomId = simpleNotifiableEvent.roomId,
eventId = null,
extras = bundleOf(ROOM_OPENED_FROM_NOTIFICATION to true),
))
.setContentIntent(
pendingIntentFactory.createOpenRoomPendingIntent(
sessionId = simpleNotifiableEvent.sessionId,
roomId = simpleNotifiableEvent.roomId,
eventId = null,
extras = bundleOf(ROOM_OPENED_FROM_NOTIFICATION to true),
)
)
.apply {
if (simpleNotifiableEvent.noisy) {
// Compat
@ -295,28 +300,35 @@ class DefaultNotificationCreator(
}
override fun createFallbackNotification(
existingNotification: Notification?,
notificationAccountParams: NotificationAccountParams,
fallbackNotifiableEvent: FallbackNotifiableEvent,
fallbackNotifiableEvents: List<FallbackNotifiableEvent>,
): Notification {
val channelId = notificationChannels.getChannelIdForMessage(false)
val existingCounter = existingNotification
?.extras
?.getInt(FALLBACK_COUNTER_EXTRA)
?: 0
val counter = existingCounter + fallbackNotifiableEvents.size
val fallbackNotifiableEvent = fallbackNotifiableEvents.first()
return NotificationCompat.Builder(context, channelId)
.setOnlyAlertOnce(true)
.setContentTitle(buildMeta.applicationName.annotateForDebug(7))
.setContentText(fallbackNotifiableEvent.description.orEmpty().annotateForDebug(8))
.setContentText(
stringProvider.getQuantityString(R.plurals.notification_fallback_n_content, counter, counter)
.annotateForDebug(8)
)
.setExtras(
bundleOf(
FALLBACK_COUNTER_EXTRA to counter
)
)
.setNumber(counter)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
.configureWith(notificationAccountParams)
.setAutoCancel(true)
.setWhen(fallbackNotifiableEvent.timestamp)
// Ideally we'd use `createOpenRoomPendingIntent` here, but the broken notification might apply to an invite
// and the user won't have access to the room yet, resulting in an error screen.
.setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(fallbackNotifiableEvent.sessionId))
.setDeleteIntent(
pendingIntentFactory.createDismissEventPendingIntent(
fallbackNotifiableEvent.sessionId,
fallbackNotifiableEvent.roomId,
fallbackNotifiableEvent.eventId
)
)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
}
@ -503,6 +515,7 @@ class DefaultNotificationCreator(
companion object {
const val MESSAGE_EVENT_ID = "message_event_id"
private const val FALLBACK_COUNTER_EXTRA = "COUNTER"
}
}

View file

@ -15,6 +15,10 @@
</plurals>
<string name="notification_error_unified_push_unregistered_android">"The UnifiedPush notification distributor couldn\'t be registered, so you will not receive notifications anymore. Please check the notifications settings of the app and the status of the push distributor."</string>
<string name="notification_fallback_content">"You have new messages."</string>
<plurals name="notification_fallback_n_content">
<item quantity="one">"You have %d new message."</item>
<item quantity="other">"You have %d new messages."</item>
</plurals>
<string name="notification_incoming_call">"📹 Incoming call"</string>
<string name="notification_inline_reply_failed">"** Failed to send - please open room"</string>
<string name="notification_invitation_action_join">"Join"</string>

View file

@ -153,6 +153,19 @@ class DefaultActiveNotificationsProviderTest {
assertThat(activeNotificationsProvider.getSummaryNotification(A_SESSION_ID_2)).isNull()
}
@Test
fun `getFallbackNotification returns only the fallback notification for that session id if it exists`() {
val activeNotifications = listOf(
aStatusBarNotification(id = notificationIdProvider.getFallbackNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
aStatusBarNotification(id = notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID_2), groupId = A_SESSION_ID_2.value),
)
val activeNotificationsProvider = createActiveNotificationsProvider(activeNotifications = activeNotifications)
assertThat(activeNotificationsProvider.getFallbackNotification(A_SESSION_ID)).isNotNull()
assertThat(activeNotificationsProvider.getFallbackNotification(A_SESSION_ID_2)).isNull()
}
private fun aStatusBarNotification(id: Int, groupId: String, tag: String? = null) = mockk<StatusBarNotification> {
every { this@mockk.id } returns id
every { this@mockk.tag } returns tag

View file

@ -56,7 +56,6 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableMess
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
import io.element.android.libraries.push.test.notifications.FakeCallNotificationEventResolver
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import kotlinx.coroutines.test.runTest
@ -663,7 +662,7 @@ class DefaultNotifiableEventResolverTest {
roomId = A_ROOM_ID,
eventId = AN_EVENT_ID,
editedEventId = null,
description = "You have new messages.",
description = "",
canBeReplaced = true,
isRedacted = false,
isUpdated = false,
@ -895,7 +894,6 @@ class DefaultNotifiableEventResolverTest {
callNotificationEventResolver = callNotificationEventResolver,
fallbackNotificationFactory = FallbackNotificationFactory(
clock = FakeSystemClock(),
stringProvider = FakeStringProvider(defaultResult = "You have new messages.")
),
featureFlagService = FakeFeatureFlagService(),
)

View file

@ -16,7 +16,6 @@ import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_SPACE_ID
import io.element.android.libraries.matrix.test.A_THREAD_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
@ -39,7 +38,6 @@ import io.element.android.services.appnavstate.api.AppNavigationStateService
import io.element.android.services.appnavstate.api.NavigationState
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
import io.element.android.services.appnavstate.test.aNavigationState
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.tests.testutils.lambda.any
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
@ -108,11 +106,9 @@ class DefaultNotificationDrawerManagerTest {
runCurrent()
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID), isInForeground = true))
runCurrent()
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_SPACE_ID), isInForeground = true))
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_ROOM_ID), isInForeground = true))
runCurrent()
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID), isInForeground = true))
runCurrent()
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID, A_THREAD_ID), isInForeground = true))
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID), isInForeground = true))
runCurrent()
// Like a user sign out
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(), isInForeground = true))
@ -261,7 +257,6 @@ fun TestScope.createDefaultNotificationDrawerManager(
roomGroupMessageCreator = roomGroupMessageCreator,
summaryGroupMessageCreator = summaryGroupMessageCreator,
activeNotificationsProvider = activeNotificationsProvider,
stringProvider = FakeStringProvider(),
),
enterpriseService = enterpriseService,
sessionStore = sessionStore,

View file

@ -39,7 +39,6 @@ class DefaultSummaryGroupMessageCreatorTest {
RoomNotification(
notification = Notification(),
roomId = A_ROOM_ID,
summaryLine = "",
messageCount = 1,
latestTimestamp = A_FAKE_TIMESTAMP + 10,
shouldBing = true,
@ -48,7 +47,6 @@ class DefaultSummaryGroupMessageCreatorTest {
),
invitationNotifications = emptyList(),
simpleNotifications = emptyList(),
fallbackNotifications = emptyList(),
)
notificationCreator.createSummaryListNotificationResult.assertions()

View file

@ -20,10 +20,10 @@ import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotif
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator
import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.fixtures.aFallbackNotifiableEvent
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent
import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@ -34,6 +34,7 @@ private val MY_AVATAR_URL: String? = null
private val AN_INVITATION_EVENT = anInviteNotifiableEvent(roomId = A_ROOM_ID)
private val A_SIMPLE_EVENT = aSimpleNotifiableEvent(eventId = AN_EVENT_ID)
private val A_MESSAGE_EVENT = aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)
private val A_FALLBACK_EVENT = aFallbackNotifiableEvent()
@RunWith(RobolectricTestRunner::class)
class NotificationDataFactoryTest {
@ -47,7 +48,6 @@ class NotificationDataFactoryTest {
roomGroupMessageCreator = fakeRoomGroupMessageCreator,
summaryGroupMessageCreator = fakeSummaryGroupMessageCreator,
activeNotificationsProvider = activeNotificationsProvider,
stringProvider = FakeStringProvider(),
)
@Test
@ -64,7 +64,6 @@ class NotificationDataFactoryTest {
OneShotNotification(
notification = expectedNotification,
tag = A_ROOM_ID.value,
summaryLine = AN_INVITATION_EVENT.description,
isNoisy = AN_INVITATION_EVENT.noisy,
timestamp = AN_INVITATION_EVENT.timestamp
)
@ -72,6 +71,25 @@ class NotificationDataFactoryTest {
)
}
@Test
fun `given a fallback invitation when mapping to notification then it's added`() = testWith(notificationDataFactory) {
val fallbackEvents = listOf(A_FALLBACK_EVENT)
val expectedNotification = notificationCreator.createFallbackNotificationResult(
null,
aNotificationAccountParams(),
fallbackEvents,
)
val result = toNotification(fallbackEvents, aNotificationAccountParams())
assertThat(result).isEqualTo(
OneShotNotification(
notification = expectedNotification,
tag = "FALLBACK",
isNoisy = false,
timestamp = A_FALLBACK_EVENT.timestamp
)
)
}
@Test
fun `given a simple event when mapping to notification then it's added`() = testWith(notificationDataFactory) {
val expectedNotification = notificationCreator.createRoomInvitationNotificationResult(
@ -83,7 +101,6 @@ class NotificationDataFactoryTest {
OneShotNotification(
notification = expectedNotification,
tag = AN_EVENT_ID.value,
summaryLine = A_SIMPLE_EVENT.description,
isNoisy = A_SIMPLE_EVENT.noisy,
timestamp = AN_INVITATION_EVENT.timestamp
)
@ -105,7 +122,6 @@ class NotificationDataFactoryTest {
existingNotification = null,
),
roomId = A_ROOM_ID,
summaryLine = "A room name: Bob Hello world!",
messageCount = events.size,
latestTimestamp = events.maxOf { it.timestamp },
shouldBing = events.any { it.noisy },
@ -161,7 +177,6 @@ class NotificationDataFactoryTest {
existingNotification = null,
),
roomId = A_ROOM_ID,
summaryLine = "A room name: Bob Hello world!",
messageCount = withRedactedRemoved.size,
latestTimestamp = withRedactedRemoved.maxOf { it.timestamp },
shouldBing = withRedactedRemoved.any { it.noisy },

View file

@ -29,7 +29,6 @@ import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNot
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.test.runTest
@ -43,7 +42,7 @@ private const val USE_COMPLETE_NOTIFICATION_FORMAT = true
private val A_SUMMARY_NOTIFICATION = SummaryNotification.Update(A_NOTIFICATION)
private val ONE_SHOT_NOTIFICATION =
OneShotNotification(notification = A_NOTIFICATION, tag = "ignored", summaryLine = "ignored", isNoisy = false, timestamp = -1)
OneShotNotification(notification = A_NOTIFICATION, tag = "ignored", isNoisy = false, timestamp = -1)
@RunWith(RobolectricTestRunner::class)
class NotificationRendererTest {
@ -57,7 +56,6 @@ class NotificationRendererTest {
roomGroupMessageCreator = roomGroupMessageCreator,
summaryGroupMessageCreator = summaryGroupMessageCreator,
activeNotificationsProvider = FakeActiveNotificationsProvider(),
stringProvider = FakeStringProvider(),
)
private val notificationIdProvider = NotificationIdProvider

View file

@ -35,8 +35,8 @@ import io.element.android.libraries.push.impl.notifications.factories.action.Acc
import io.element.android.libraries.push.impl.notifications.factories.action.MarkAsReadActionFactory
import io.element.android.libraries.push.impl.notifications.factories.action.QuickReplyActionFactory
import io.element.android.libraries.push.impl.notifications.factories.action.RejectInvitationActionFactory
import io.element.android.libraries.push.impl.notifications.fixtures.aFallbackNotifiableEvent
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
@ -82,19 +82,11 @@ class DefaultNotificationCreatorTest {
fun `test createFallbackNotification`() {
val sut = createNotificationCreator()
val result = sut.createFallbackNotification(
existingNotification = null,
notificationAccountParams = aNotificationAccountParams(),
FallbackNotifiableEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
eventId = AN_EVENT_ID,
editedEventId = null,
description = "description",
canBeReplaced = false,
isRedacted = false,
isUpdated = false,
timestamp = A_FAKE_TIMESTAMP,
cause = null,
),
fallbackNotifiableEvents = listOf(
aFallbackNotifiableEvent(),
)
)
result.commonAssertions(
expectedCategory = null,

View file

@ -21,6 +21,7 @@ class FakeActiveNotificationsProvider(
private val getMembershipNotificationForSessionResult: (SessionId) -> List<StatusBarNotification> = { emptyList() },
private val getMembershipNotificationForRoomResult: (SessionId, RoomId) -> List<StatusBarNotification> = { _, _ -> emptyList() },
private val getSummaryNotificationResult: (SessionId) -> StatusBarNotification? = { null },
private val getFallbackNotificationResult: (SessionId) -> StatusBarNotification? = { null },
private val countResult: (SessionId) -> Int = { 0 },
) : ActiveNotificationsProvider {
override fun getMessageNotificationsForRoom(sessionId: SessionId, roomId: RoomId, threadId: ThreadId?): List<StatusBarNotification> {
@ -47,6 +48,10 @@ class FakeActiveNotificationsProvider(
return getSummaryNotificationResult(sessionId)
}
override fun getFallbackNotification(sessionId: SessionId): StatusBarNotification? {
return getFallbackNotificationResult(sessionId)
}
override fun count(sessionId: SessionId): Int {
return countResult(sessionId)
}

View file

@ -24,6 +24,7 @@ import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiab
import io.element.android.tests.testutils.lambda.LambdaFiveParamsRecorder
import io.element.android.tests.testutils.lambda.LambdaListAnyParamsRecorder
import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder
import io.element.android.tests.testutils.lambda.LambdaThreeParamsRecorder
import io.element.android.tests.testutils.lambda.LambdaTwoParamsRecorder
import io.element.android.tests.testutils.lambda.lambdaAnyRecorder
import io.element.android.tests.testutils.lambda.lambdaRecorder
@ -34,8 +35,8 @@ class FakeNotificationCreator(
lambdaRecorder { _, _ -> A_NOTIFICATION },
var createSimpleNotificationResult: LambdaTwoParamsRecorder<NotificationAccountParams, SimpleNotifiableEvent, Notification> =
lambdaRecorder { _, _ -> A_NOTIFICATION },
var createFallbackNotificationResult: LambdaTwoParamsRecorder<NotificationAccountParams, FallbackNotifiableEvent, Notification> =
lambdaRecorder { _, _ -> A_NOTIFICATION },
var createFallbackNotificationResult: LambdaThreeParamsRecorder<Notification?, NotificationAccountParams, List<FallbackNotifiableEvent>, Notification> =
lambdaRecorder { _, _, _ -> A_NOTIFICATION },
var createSummaryListNotificationResult: LambdaFiveParamsRecorder<
NotificationAccountParams, String, Boolean, Long, NotificationAccountParams, Notification
> = lambdaRecorder { _, _, _, _, _ -> A_NOTIFICATION },
@ -75,10 +76,15 @@ class FakeNotificationCreator(
}
override fun createFallbackNotification(
existingNotification: Notification?,
notificationAccountParams: NotificationAccountParams,
fallbackNotifiableEvent: FallbackNotifiableEvent,
fallbackNotifiableEvents: List<FallbackNotifiableEvent>,
): Notification {
return createFallbackNotificationResult(notificationAccountParams, fallbackNotifiableEvent)
return createFallbackNotificationResult(
existingNotification,
notificationAccountParams,
fallbackNotifiableEvents,
)
}
override fun createSummaryListNotification(

View file

@ -19,7 +19,7 @@ import io.element.android.libraries.push.impl.notifications.model.FallbackNotifi
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
import io.element.android.tests.testutils.lambda.LambdaFiveParamsRecorder
import io.element.android.tests.testutils.lambda.LambdaFourParamsRecorder
import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder
import io.element.android.tests.testutils.lambda.LambdaThreeParamsRecorder
import io.element.android.tests.testutils.lambda.lambdaRecorder
@ -28,18 +28,17 @@ class FakeNotificationDataFactory(
var messageEventToNotificationsResult: LambdaThreeParamsRecorder<
List<NotifiableMessageEvent>, ImageLoader, NotificationAccountParams, List<RoomNotification>
> = lambdaRecorder { _, _, _ -> emptyList() },
var summaryToNotificationsResult: LambdaFiveParamsRecorder<
var summaryToNotificationsResult: LambdaFourParamsRecorder<
List<RoomNotification>,
List<OneShotNotification>,
List<OneShotNotification>,
List<OneShotNotification>,
NotificationAccountParams,
SummaryNotification
> = lambdaRecorder { _, _, _, _, _ -> SummaryNotification.Update(A_NOTIFICATION) },
> = lambdaRecorder { _, _, _, _ -> SummaryNotification.Update(A_NOTIFICATION) },
var inviteToNotificationsResult: LambdaOneParamRecorder<List<InviteNotifiableEvent>, List<OneShotNotification>> = lambdaRecorder { _ -> emptyList() },
var simpleEventToNotificationsResult: LambdaOneParamRecorder<List<SimpleNotifiableEvent>, List<OneShotNotification>> = lambdaRecorder { _ -> emptyList() },
var fallbackEventToNotificationsResult: LambdaOneParamRecorder<List<FallbackNotifiableEvent>, List<OneShotNotification>> =
lambdaRecorder { _ -> emptyList() },
var fallbackEventToNotificationsResult: LambdaOneParamRecorder<List<FallbackNotifiableEvent>, OneShotNotification?> =
lambdaRecorder { _ -> null },
) : NotificationDataFactory {
override suspend fun toNotifications(
messages: List<NotifiableMessageEvent>,
@ -69,10 +68,10 @@ class FakeNotificationDataFactory(
@JvmName("toNotificationFallbackEvents")
@Suppress("INAPPLICABLE_JVM_NAME")
override fun toNotifications(
override fun toNotification(
fallback: List<FallbackNotifiableEvent>,
notificationAccountParams: NotificationAccountParams,
): List<OneShotNotification> {
): OneShotNotification? {
return fallbackEventToNotificationsResult(fallback)
}
@ -80,14 +79,12 @@ class FakeNotificationDataFactory(
roomNotifications: List<RoomNotification>,
invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>,
fallbackNotifications: List<OneShotNotification>,
notificationAccountParams: NotificationAccountParams,
): SummaryNotification {
return summaryToNotificationsResult(
roomNotifications,
invitationNotifications,
simpleNotifications,
fallbackNotifications,
notificationAccountParams,
)
}

View file

@ -14,27 +14,25 @@ import io.element.android.libraries.push.impl.notifications.RoomNotification
import io.element.android.libraries.push.impl.notifications.SummaryGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.factories.NotificationAccountParams
import io.element.android.libraries.push.impl.notifications.fixtures.A_NOTIFICATION
import io.element.android.tests.testutils.lambda.LambdaFiveParamsRecorder
import io.element.android.tests.testutils.lambda.LambdaFourParamsRecorder
import io.element.android.tests.testutils.lambda.lambdaRecorder
class FakeSummaryGroupMessageCreator(
var createSummaryNotificationResult: LambdaFiveParamsRecorder<
NotificationAccountParams, List<RoomNotification>, List<OneShotNotification>, List<OneShotNotification>, List<OneShotNotification>, Notification> =
lambdaRecorder { _, _, _, _, _ -> A_NOTIFICATION }
var createSummaryNotificationResult: LambdaFourParamsRecorder<
NotificationAccountParams, List<RoomNotification>, List<OneShotNotification>, List<OneShotNotification>, Notification> =
lambdaRecorder { _, _, _, _ -> A_NOTIFICATION }
) : SummaryGroupMessageCreator {
override fun createSummaryNotification(
notificationAccountParams: NotificationAccountParams,
roomNotifications: List<RoomNotification>,
invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>,
fallbackNotifications: List<OneShotNotification>,
): Notification {
return createSummaryNotificationResult(
notificationAccountParams,
roomNotifications,
invitationNotifications,
simpleNotifications,
fallbackNotifications,
)
}
}

View file

@ -24,10 +24,12 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_TIMESTAMP
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.A_USER_NAME_2
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
fun aSimpleNotifiableEvent(
sessionId: SessionId = A_SESSION_ID,
@ -141,3 +143,16 @@ fun aNotifiableCallEvent(
senderAvatarUrl = senderAvatarUrl,
rtcNotificationType = rtcNotificationType,
)
fun aFallbackNotifiableEvent() = FallbackNotifiableEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
eventId = AN_EVENT_ID,
editedEventId = null,
description = "A fallback notification",
canBeReplaced = false,
isRedacted = false,
isUpdated = false,
timestamp = A_FAKE_TIMESTAMP,
cause = "Unable to decrypt event",
)

View file

@ -40,9 +40,9 @@ import io.element.android.libraries.push.impl.notifications.DefaultNotificationR
import io.element.android.libraries.push.impl.notifications.FakeNotifiableEventResolver
import io.element.android.libraries.push.impl.notifications.FallbackNotificationFactory
import io.element.android.libraries.push.impl.notifications.channels.FakeNotificationChannels
import io.element.android.libraries.push.impl.notifications.fixtures.aFallbackNotifiableEvent
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableCallEvent
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
import io.element.android.libraries.push.impl.test.DefaultTestPush
@ -57,7 +57,6 @@ import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.Fa
import io.element.android.libraries.workmanager.api.WorkManagerRequest
import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import io.element.android.tests.testutils.lambda.any
import io.element.android.tests.testutils.lambda.lambdaError
@ -627,18 +626,7 @@ class DefaultPushHandlerTest {
@Test
fun `when receiving a fallback event, we notify the push history service about it not being resolved`() = runTest {
val aNotifiableFallbackEvent = FallbackNotifiableEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
eventId = AN_EVENT_ID,
editedEventId = null,
description = "A fallback notification",
canBeReplaced = false,
isRedacted = false,
isUpdated = false,
timestamp = 0L,
cause = "Unable to decrypt event",
)
val aNotifiableFallbackEvent = aFallbackNotifiableEvent()
val notifiableEventResult =
lambdaRecorder<SessionId, List<NotificationEventRequest>, Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> { _, _ ->
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
@ -724,7 +712,6 @@ class DefaultPushHandlerTest {
appCoroutineScope = backgroundScope,
fallbackNotificationFactory = FallbackNotificationFactory(
clock = FakeSystemClock(),
stringProvider = FakeStringProvider(),
),
syncOnNotifiableEvent = syncOnNotifiableEvent,
featureFlagService = featureFlagService,

View file

@ -10,7 +10,6 @@ package io.element.android.services.appnavstate.api
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.SpaceId
import io.element.android.libraries.matrix.api.core.ThreadId
import kotlinx.coroutines.flow.StateFlow
@ -23,9 +22,6 @@ interface AppNavigationStateService {
fun onNavigateToSession(owner: String, sessionId: SessionId)
fun onLeavingSession(owner: String)
fun onNavigateToSpace(owner: String, spaceId: SpaceId)
fun onLeavingSpace(owner: String)
fun onNavigateToRoom(owner: String, roomId: RoomId)
fun onLeavingRoom(owner: String)

View file

@ -10,7 +10,6 @@ package io.element.android.services.appnavstate.api
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.SpaceId
import io.element.android.libraries.matrix.api.core.ThreadId
/**
@ -29,17 +28,10 @@ sealed class NavigationState(open val owner: String) {
val sessionId: SessionId,
) : NavigationState(owner)
data class Space(
override val owner: String,
// Can be fake value, if no space is selected
val spaceId: SpaceId,
val parentSession: Session,
) : NavigationState(owner)
data class Room(
override val owner: String,
val roomId: RoomId,
val parentSpace: Space,
val parentSession: Session,
) : NavigationState(owner)
data class Thread(

View file

@ -10,26 +10,14 @@ package io.element.android.services.appnavstate.api
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.SpaceId
import io.element.android.libraries.matrix.api.core.ThreadId
fun NavigationState.currentSessionId(): SessionId? {
return when (this) {
NavigationState.Root -> null
is NavigationState.Session -> sessionId
is NavigationState.Space -> parentSession.sessionId
is NavigationState.Room -> parentSpace.parentSession.sessionId
is NavigationState.Thread -> parentRoom.parentSpace.parentSession.sessionId
}
}
fun NavigationState.currentSpaceId(): SpaceId? {
return when (this) {
NavigationState.Root -> null
is NavigationState.Session -> null
is NavigationState.Space -> spaceId
is NavigationState.Room -> parentSpace.spaceId
is NavigationState.Thread -> parentRoom.parentSpace.spaceId
is NavigationState.Room -> parentSession.sessionId
is NavigationState.Thread -> parentRoom.parentSession.sessionId
}
}
@ -37,7 +25,6 @@ fun NavigationState.currentRoomId(): RoomId? {
return when (this) {
NavigationState.Root -> null
is NavigationState.Session -> null
is NavigationState.Space -> null
is NavigationState.Room -> roomId
is NavigationState.Thread -> parentRoom.roomId
}
@ -47,7 +34,6 @@ fun NavigationState.currentThreadId(): ThreadId? {
return when (this) {
NavigationState.Root -> null
is NavigationState.Session -> null
is NavigationState.Space -> null
is NavigationState.Room -> null
is NavigationState.Thread -> threadId
}

View file

@ -15,7 +15,6 @@ import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.SpaceId
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.services.appnavstate.api.AppForegroundStateService
import io.element.android.services.appnavstate.api.AppNavigationState
@ -62,7 +61,6 @@ class DefaultAppNavigationStateService(
Timber.tag(loggerTag.value).d("Navigating to session $sessionId. Current state: $currentValue")
val newValue: NavigationState.Session = when (currentValue) {
is NavigationState.Session,
is NavigationState.Space,
is NavigationState.Room,
is NavigationState.Thread,
is NavigationState.Root -> NavigationState.Session(owner, sessionId)
@ -70,28 +68,14 @@ class DefaultAppNavigationStateService(
state.getAndUpdate { it.copy(navigationState = newValue) }
}
override fun onNavigateToSpace(owner: String, spaceId: SpaceId) {
val currentValue = state.value.navigationState
Timber.tag(loggerTag.value).d("Navigating to space $spaceId. Current state: $currentValue")
val newValue: NavigationState.Space = when (currentValue) {
NavigationState.Root -> return logError("onNavigateToSession()")
is NavigationState.Session -> NavigationState.Space(owner, spaceId, currentValue)
is NavigationState.Space -> NavigationState.Space(owner, spaceId, currentValue.parentSession)
is NavigationState.Room -> NavigationState.Space(owner, spaceId, currentValue.parentSpace.parentSession)
is NavigationState.Thread -> NavigationState.Space(owner, spaceId, currentValue.parentRoom.parentSpace.parentSession)
}
state.getAndUpdate { it.copy(navigationState = newValue) }
}
override fun onNavigateToRoom(owner: String, roomId: RoomId) {
val currentValue = state.value.navigationState
Timber.tag(loggerTag.value).d("Navigating to room $roomId. Current state: $currentValue")
val newValue: NavigationState.Room = when (currentValue) {
NavigationState.Root -> return logError("onNavigateToSession()")
is NavigationState.Session -> return logError("onNavigateToSpace()")
is NavigationState.Space -> NavigationState.Room(owner, roomId, currentValue)
is NavigationState.Room -> NavigationState.Room(owner, roomId, currentValue.parentSpace)
is NavigationState.Thread -> NavigationState.Room(owner, roomId, currentValue.parentRoom.parentSpace)
is NavigationState.Session -> NavigationState.Room(owner, roomId, currentValue)
is NavigationState.Room -> NavigationState.Room(owner, roomId, currentValue.parentSession)
is NavigationState.Thread -> NavigationState.Room(owner, roomId, currentValue.parentRoom.parentSession)
}
state.getAndUpdate { it.copy(navigationState = newValue) }
}
@ -101,8 +85,7 @@ class DefaultAppNavigationStateService(
Timber.tag(loggerTag.value).d("Navigating to thread $threadId. Current state: $currentValue")
val newValue: NavigationState.Thread = when (currentValue) {
NavigationState.Root -> return logError("onNavigateToSession()")
is NavigationState.Session -> return logError("onNavigateToSpace()")
is NavigationState.Space -> return logError("onNavigateToRoom()")
is NavigationState.Session -> return logError("onNavigateToRoom()")
is NavigationState.Room -> NavigationState.Thread(owner, threadId, currentValue)
is NavigationState.Thread -> NavigationState.Thread(owner, threadId, currentValue.parentRoom)
}
@ -115,8 +98,7 @@ class DefaultAppNavigationStateService(
if (!currentValue.assertOwner(owner)) return
val newValue: NavigationState.Room = when (currentValue) {
NavigationState.Root -> return logError("onNavigateToSession()")
is NavigationState.Session -> return logError("onNavigateToSpace()")
is NavigationState.Space -> return logError("onNavigateToRoom()")
is NavigationState.Session -> return logError("onNavigateToRoom()")
is NavigationState.Room -> return logError("onNavigateToThread()")
is NavigationState.Thread -> currentValue.parentRoom
}
@ -127,26 +109,11 @@ class DefaultAppNavigationStateService(
val currentValue = state.value.navigationState
Timber.tag(loggerTag.value).d("Leaving room. Current state: $currentValue")
if (!currentValue.assertOwner(owner)) return
val newValue: NavigationState.Space = when (currentValue) {
NavigationState.Root -> return logError("onNavigateToSession()")
is NavigationState.Session -> return logError("onNavigateToSpace()")
is NavigationState.Space -> return logError("onNavigateToRoom()")
is NavigationState.Room -> currentValue.parentSpace
is NavigationState.Thread -> currentValue.parentRoom.parentSpace
}
state.getAndUpdate { it.copy(navigationState = newValue) }
}
override fun onLeavingSpace(owner: String) {
val currentValue = state.value.navigationState
Timber.tag(loggerTag.value).d("Leaving space. Current state: $currentValue")
if (!currentValue.assertOwner(owner)) return
val newValue: NavigationState.Session = when (currentValue) {
NavigationState.Root -> return logError("onNavigateToSession()")
is NavigationState.Session -> return logError("onNavigateToSpace()")
is NavigationState.Space -> currentValue.parentSession
is NavigationState.Room -> currentValue.parentSpace.parentSession
is NavigationState.Thread -> currentValue.parentRoom.parentSpace.parentSession
is NavigationState.Session -> return logError("onNavigateToRoom()")
is NavigationState.Room -> currentValue.parentSession
is NavigationState.Thread -> currentValue.parentRoom.parentSession
}
state.getAndUpdate { it.copy(navigationState = newValue) }
}

View file

@ -13,15 +13,12 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
import io.element.android.libraries.matrix.test.A_SPACE_ID
import io.element.android.libraries.matrix.test.A_SPACE_ID_2
import io.element.android.libraries.matrix.test.A_THREAD_ID
import io.element.android.libraries.matrix.test.A_THREAD_ID_2
import io.element.android.services.appnavstate.api.AppNavigationStateService
import io.element.android.services.appnavstate.api.NavigationState
import io.element.android.services.appnavstate.test.A_ROOM_OWNER
import io.element.android.services.appnavstate.test.A_SESSION_OWNER
import io.element.android.services.appnavstate.test.A_SPACE_OWNER
import io.element.android.services.appnavstate.test.A_THREAD_OWNER
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
import kotlinx.coroutines.flow.first
@ -33,22 +30,17 @@ class DefaultNavigationStateServiceTest {
private val navigationStateRoot = NavigationState.Root
private val navigationStateSession = NavigationState.Session(
owner = A_SESSION_OWNER,
sessionId = A_SESSION_ID
)
private val navigationStateSpace = NavigationState.Space(
owner = A_SPACE_OWNER,
spaceId = A_SPACE_ID,
parentSession = navigationStateSession
sessionId = A_SESSION_ID,
)
private val navigationStateRoom = NavigationState.Room(
owner = A_ROOM_OWNER,
roomId = A_ROOM_ID,
parentSpace = navigationStateSpace
parentSession = navigationStateSession,
)
private val navigationStateThread = NavigationState.Thread(
owner = A_THREAD_OWNER,
threadId = A_THREAD_ID,
parentRoom = navigationStateRoom
parentRoom = navigationStateRoom,
)
@Test
@ -57,8 +49,6 @@ class DefaultNavigationStateServiceTest {
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot)
service.onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace)
service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoom)
service.onNavigateToThread(A_THREAD_OWNER, A_THREAD_ID)
@ -67,8 +57,6 @@ class DefaultNavigationStateServiceTest {
service.onLeavingThread(A_THREAD_OWNER)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoom)
service.onLeavingRoom(A_ROOM_OWNER)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace)
service.onLeavingSpace(A_SPACE_OWNER)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
service.onLeavingSession(A_SESSION_OWNER)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot)
@ -77,7 +65,7 @@ class DefaultNavigationStateServiceTest {
@Test
fun testFailure() = runTest {
val service = createStateService()
service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID)
service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID)
assertThat(service.appNavigationState.value.navigationState).isEqualTo(NavigationState.Root)
}
@ -92,11 +80,6 @@ class DefaultNavigationStateServiceTest {
service.navigateToSession()
service.onNavigateToThread(A_THREAD_OWNER, A_THREAD_ID)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
// From space (no effect)
service.reset()
service.navigateToSpace()
service.onNavigateToThread(A_THREAD_OWNER, A_THREAD_ID)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace)
// From room
service.reset()
service.navigateToRoom()
@ -116,15 +99,10 @@ class DefaultNavigationStateServiceTest {
// From root (no effect)
service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot)
// From session (no effect)
// From session
service.reset()
service.navigateToSession()
service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
// From space
service.reset()
service.navigateToSpace()
service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoom)
// From room
service.reset()
@ -139,35 +117,6 @@ class DefaultNavigationStateServiceTest {
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoom)
}
@Test
fun testOnNavigateToSpace() = runTest {
val service = createStateService()
// From root (no effect)
service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot)
// From session
service.reset()
service.navigateToSession()
service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace)
// From space
service.reset()
service.navigateToSpace()
// Navigate to another space
service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID_2)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace.copy(spaceId = A_SPACE_ID_2))
// From room (no effect)
service.reset()
service.navigateToRoom()
service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace)
// From thread (no effect)
service.reset()
service.navigateToThread()
service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace)
}
@Test
fun testOnNavigateToSession() = runTest {
val service = createStateService()
@ -180,11 +129,6 @@ class DefaultNavigationStateServiceTest {
// Navigate to another session
service.onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID_2)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession.copy(sessionId = A_SESSION_ID_2))
// From space
service.reset()
service.navigateToSpace()
service.onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
// From room
service.reset()
service.navigateToRoom()
@ -208,11 +152,6 @@ class DefaultNavigationStateServiceTest {
service.navigateToSession()
service.onLeavingThread(A_THREAD_OWNER)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
// From space (no effect)
service.reset()
service.navigateToSpace()
service.onLeavingThread(A_THREAD_OWNER)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace)
// From room (no effect)
service.reset()
service.navigateToRoom()
@ -236,16 +175,11 @@ class DefaultNavigationStateServiceTest {
service.navigateToSession()
service.onLeavingRoom(A_ROOM_OWNER)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
// From space (no effect)
service.reset()
service.navigateToSpace()
service.onLeavingRoom(A_ROOM_OWNER)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace)
// From room
service.reset()
service.navigateToRoom()
service.onLeavingRoom(A_ROOM_OWNER)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
// From thread (no effect)
service.reset()
service.navigateToThread()
@ -253,34 +187,6 @@ class DefaultNavigationStateServiceTest {
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateThread)
}
@Test
fun testOnLeavingSpace() = runTest {
val service = createStateService()
// From root (no effect)
service.onLeavingSpace(A_SPACE_OWNER)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot)
// From session (no effect)
service.reset()
service.navigateToSession()
service.onLeavingSpace(A_SPACE_OWNER)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
// From space
service.reset()
service.navigateToSpace()
service.onLeavingSpace(A_SPACE_OWNER)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession)
// From room (no effect)
service.reset()
service.navigateToRoom()
service.onLeavingSpace(A_SPACE_OWNER)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoom)
// From thread (no effect)
service.reset()
service.navigateToThread()
service.onLeavingSpace(A_SPACE_OWNER)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateThread)
}
@Test
fun testOnLeavingSession() = runTest {
val service = createStateService()
@ -292,11 +198,6 @@ class DefaultNavigationStateServiceTest {
service.navigateToSession()
service.onLeavingSession(A_SESSION_OWNER)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot)
// From space (no effect)
service.reset()
service.navigateToSpace()
service.onLeavingSession(A_SESSION_OWNER)
assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSpace)
// From room (no effect)
service.reset()
service.navigateToRoom()
@ -318,13 +219,8 @@ class DefaultNavigationStateServiceTest {
onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID)
}
private fun AppNavigationStateService.navigateToSpace() {
navigateToSession()
onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID)
}
private fun AppNavigationStateService.navigateToRoom() {
navigateToSpace()
navigateToSession()
onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID)
}

View file

@ -8,21 +8,17 @@
package io.element.android.services.appnavstate.test
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.SpaceId
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.services.appnavstate.api.NavigationState
const val A_SESSION_OWNER = "aSessionOwner"
const val A_SPACE_OWNER = "aSpaceOwner"
const val A_ROOM_OWNER = "aRoomOwner"
const val A_THREAD_OWNER = "aThreadOwner"
fun aNavigationState(
sessionId: SessionId? = null,
spaceId: SpaceId? = MAIN_SPACE,
roomId: RoomId? = null,
threadId: ThreadId? = null,
): NavigationState {
@ -30,14 +26,10 @@ fun aNavigationState(
return NavigationState.Root
}
val session = NavigationState.Session(A_SESSION_OWNER, sessionId)
if (spaceId == null) {
if (roomId == null) {
return session
}
val space = NavigationState.Space(A_SPACE_OWNER, spaceId, session)
if (roomId == null) {
return space
}
val room = NavigationState.Room(A_ROOM_OWNER, roomId, space)
val room = NavigationState.Room(A_ROOM_OWNER, roomId, session)
if (threadId == null) {
return room
}

View file

@ -10,7 +10,6 @@ package io.element.android.services.appnavstate.test
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.SpaceId
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.services.appnavstate.api.AppNavigationState
import io.element.android.services.appnavstate.api.AppNavigationStateService
@ -28,15 +27,9 @@ class FakeAppNavigationStateService(
override fun onNavigateToSession(owner: String, sessionId: SessionId) = Unit
override fun onLeavingSession(owner: String) = Unit
override fun onNavigateToSpace(owner: String, spaceId: SpaceId) = Unit
override fun onLeavingSpace(owner: String) = Unit
override fun onNavigateToRoom(owner: String, roomId: RoomId) = Unit
override fun onLeavingRoom(owner: String) = Unit
override fun onNavigateToThread(owner: String, threadId: ThreadId) = Unit
override fun onLeavingThread(owner: String) = Unit
}