Merge branch 'main' of github.com:ouchadam/helium into feature/share-images-via-small-talk

This commit is contained in:
Adam Brown 2022-07-28 22:08:44 +01:00
commit 9c99b6a0a9
27 changed files with 239 additions and 75 deletions

View File

@ -25,7 +25,7 @@ jobs:
- name: Create pip requirements
run: |
echo "matrix-synapse" > requirements.txt
echo "matrix-synapse==v1.60.0" > requirements.txt
- name: Set up Python 3.8
uses: actions/setup-python@v2

View File

@ -61,7 +61,7 @@ android {
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
coreLibraryDesugaring Dependencies.google.jdkLibs
implementation project(":features:home")
implementation project(":features:directory")

View File

@ -9,7 +9,7 @@ buildscript {
classpath Dependencies.mavenCentral.kotlinGradlePlugin
classpath Dependencies.mavenCentral.sqldelightGradlePlugin
classpath Dependencies.mavenCentral.kotlinSerializationGradlePlugin
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1'
classpath Dependencies.google.firebaseCrashlyticsPlugin
}
}
@ -132,11 +132,11 @@ ext.kotlinTest = { dependencies ->
dependencies.testImplementation Dependencies.mavenCentral.kluent
dependencies.testImplementation Dependencies.mavenCentral.kotlinTest
dependencies.testImplementation "org.jetbrains.kotlin:kotlin-test-junit:1.6.10"
dependencies.testImplementation 'io.mockk:mockk:1.12.4'
dependencies.testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.2'
dependencies.testImplementation 'io.mockk:mockk:1.12.5'
dependencies.testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
dependencies.testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
dependencies.testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
dependencies.testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0'
dependencies.testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0'
}
ext.kotlinFixtures = { dependencies ->

View File

@ -31,10 +31,12 @@ class SingletonFlows(
}
}
@Suppress("UNCHECKED_CAST")
fun <T> get(key: String): Flow<T> {
return cache[key]!! as Flow<T>
}
@Suppress("UNCHECKED_CAST")
suspend fun <T> update(key: String, value: T) {
(cache[key] as? MutableSharedFlow<T>)?.emit(value)
}

View File

@ -88,10 +88,10 @@ ext.Dependencies.with {
}
}
def kotlinVer = "1.6.10"
def kotlinVer = "1.7.0"
def sqldelightVer = "1.5.3"
def composeVer = "1.1.1"
def ktorVer = "2.0.2"
def ktorVer = "2.0.3"
google = new DependenciesContainer()
google.with {
@ -102,7 +102,10 @@ ext.Dependencies.with {
androidxComposeMaterial = "androidx.compose.material:material:${composeVer}"
androidxComposeIconsExtended = "androidx.compose.material:material-icons-extended:${composeVer}"
androidxActivityCompose = "androidx.activity:activity-compose:1.4.0"
kotlinCompilerExtensionVersion = "${composeVer}"
kotlinCompilerExtensionVersion = "1.2.0"
firebaseCrashlyticsPlugin = "com.google.firebase:firebase-crashlytics-gradle:2.9.1"
jdkLibs = "com.android.tools:desugar_jdk_libs:1.1.5"
}
mavenCentral = new DependenciesContainer()
@ -110,8 +113,8 @@ ext.Dependencies.with {
kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVer}"
kotlinSerializationGradlePlugin = "org.jetbrains.kotlin:kotlin-serialization:${kotlinVer}"
kotlinSerializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3"
kotlinCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2"
kotlinCoroutinesTest = 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.2'
kotlinCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
kotlinCoroutinesTest = 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
kotlinTest = "org.jetbrains.kotlin:kotlin-test-junit:${kotlinVer}"
sqldelightGradlePlugin = "com.squareup.sqldelight:gradle-plugin:${sqldelightVer}"
@ -129,11 +132,11 @@ ext.Dependencies.with {
ktorContentNegotiation = "io.ktor:ktor-client-content-negotiation:${ktorVer}"
coil = "io.coil-kt:coil-compose:2.1.0"
accompanistSystemuicontroller = "com.google.accompanist:accompanist-systemuicontroller:0.24.10-beta"
accompanistSystemuicontroller = "com.google.accompanist:accompanist-systemuicontroller:0.24.13-rc"
junit = "junit:junit:4.13.2"
kluent = "org.amshove.kluent:kluent:1.68"
mockk = 'io.mockk:mockk:1.12.4'
mockk = 'io.mockk:mockk:1.12.5'
matrixOlm = "org.matrix.android:olm-sdk:3.2.12"
}

View File

@ -41,6 +41,9 @@ fun LifecycleEffect(onStart: () -> Unit = {}, onStop: () -> Unit = {}) {
when (event) {
Lifecycle.Event.ON_START -> onStart()
Lifecycle.Event.ON_STOP -> onStop()
else -> {
// ignored
}
}
}

View File

@ -1,5 +1,5 @@
@file:JvmName("SnapshotStateKt")
@file:Suppress("UNUSED")
package androidx.compose.runtime
import kotlin.reflect.KProperty

View File

@ -6,6 +6,7 @@ import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import test.ExpectTest
@Suppress("UNCHECKED_CAST")
class ViewModelTest {
var instance: TestMutableState<Any>? = null

View File

@ -93,8 +93,7 @@ internal fun MessengerScreen(roomId: RoomId, attachments: List<MessageAttachment
private fun MessengerViewModel.ObserveEvents() {
StartObserving {
this@ObserveEvents.events.launch {
when (it) {
}
// TODO()
}
}
}
@ -438,6 +437,10 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
)
Spacer(modifier = Modifier.height(4.dp))
}
is RoomEvent.Reply -> {
// TODO - a reply to a reply
}
}
}
@ -475,6 +478,10 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
)
Spacer(modifier = Modifier.height(4.dp))
}
is RoomEvent.Reply -> {
// TODO - a reply to a reply
}
}
Spacer(modifier = Modifier.height(2.dp))

View File

@ -34,6 +34,8 @@ class AndroidNotificationStyleBuilder(
.setKey(person.key)
.build()
).also { style ->
style.conversationTitle = title
style.isGroupConversation = isGroup
content.forEach {
val sender = personBuilderFactory()
.setName(it.sender.name)

View File

@ -1,10 +1,15 @@
package app.dapk.st.notifications
import android.app.NotificationChannel
import android.app.NotificationChannelGroup
import android.app.NotificationManager
import android.os.Build
private const val channelId = "message"
const val DIRECT_CHANNEL_ID = "direct_channel_id"
const val GROUP_CHANNEL_ID = "group_channel_id"
const val SUMMARY_CHANNEL_ID = "summary_channel_id"
private const val CHATS_NOTIFICATION_GROUP_ID = "chats_notification_group"
class NotificationChannels(
private val notificationManager: NotificationManager
@ -12,13 +17,43 @@ class NotificationChannels(
fun initChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (notificationManager.getNotificationChannel(channelId) == null) {
notificationManager.createNotificationChannelGroup(NotificationChannelGroup(CHATS_NOTIFICATION_GROUP_ID, "Chats"))
if (notificationManager.getNotificationChannel(DIRECT_CHANNEL_ID) == null) {
notificationManager.createNotificationChannel(
NotificationChannel(
channelId,
"messages",
DIRECT_CHANNEL_ID,
"Direct notifications",
NotificationManager.IMPORTANCE_HIGH,
)
).also {
it.enableVibration(true)
it.enableLights(true)
it.group = CHATS_NOTIFICATION_GROUP_ID
}
)
}
if (notificationManager.getNotificationChannel(GROUP_CHANNEL_ID) == null) {
notificationManager.createNotificationChannel(
NotificationChannel(
GROUP_CHANNEL_ID,
"Group notifications",
NotificationManager.IMPORTANCE_HIGH,
).also {
it.group = CHATS_NOTIFICATION_GROUP_ID
}
)
}
if (notificationManager.getNotificationChannel(SUMMARY_CHANNEL_ID) == null) {
notificationManager.createNotificationChannel(
NotificationChannel(
SUMMARY_CHANNEL_ID,
"Other notifications",
NotificationManager.IMPORTANCE_DEFAULT,
).also {
it.group = CHATS_NOTIFICATION_GROUP_ID
}
)
}
}

View File

@ -10,7 +10,6 @@ import app.dapk.st.matrix.sync.RoomOverview
import app.dapk.st.navigator.IntentFactory
private const val GROUP_ID = "st"
private const val channelId = "message"
class NotificationFactory(
private val context: Context,
@ -35,17 +34,18 @@ class NotificationFactory(
else -> newRooms.contains(roomOverview.roomId)
}
val last = sortedEvents.last()
return NotificationTypes.Room(
AndroidNotification(
channelId = channelId,
whenTimestamp = sortedEvents.last().utcTimestamp,
channelId = SUMMARY_CHANNEL_ID,
whenTimestamp = last.utcTimestamp,
groupId = GROUP_ID,
groupAlertBehavior = deviceMeta.whenPOrHigher(
block = { Notification.GROUP_ALERT_SUMMARY },
fallback = { null }
),
shortcutId = roomOverview.roomId.value,
alertMoreThanOnce = shouldAlertMoreThanOnce,
alertMoreThanOnce = false,
contentIntent = openRoomIntent,
messageStyle = messageStyle,
category = Notification.CATEGORY_MESSAGE,
@ -54,25 +54,38 @@ class NotificationFactory(
autoCancel = true
),
roomId = roomOverview.roomId,
summary = sortedEvents.last().content,
summary = last.content,
messageCount = sortedEvents.size,
isAlerting = shouldAlertMoreThanOnce
isAlerting = shouldAlertMoreThanOnce,
summaryChannelId = when {
roomOverview.isDm() -> DIRECT_CHANNEL_ID
else -> GROUP_CHANNEL_ID
}
)
}
fun createSummary(notifications: List<NotificationTypes.Room>): AndroidNotification {
val summaryInboxStyle = notificationStyleFactory.summary(notifications)
val openAppIntent = intentFactory.notificationOpenApp(context)
val mostRecent = notifications.mostRecent()
return AndroidNotification(
channelId = channelId,
channelId = mostRecent.summaryChannelId,
messageStyle = summaryInboxStyle,
whenTimestamp = mostRecent.notification.whenTimestamp,
alertMoreThanOnce = notifications.any { it.isAlerting },
smallIcon = R.drawable.ic_notification_small_icon,
contentIntent = openAppIntent,
groupId = GROUP_ID,
groupAlertBehavior = deviceMeta.whenPOrHigher(
block = { Notification.GROUP_ALERT_SUMMARY },
fallback = { null }
),
isGroupSummary = true,
category = Notification.CATEGORY_MESSAGE,
)
}
}
private fun List<NotificationTypes.Room>.mostRecent() = this.sortedBy { it.notification.whenTimestamp }.first()
private fun RoomOverview.isDm() = !this.isGroup

View File

@ -68,7 +68,8 @@ sealed interface NotificationTypes {
val roomId: RoomId,
val summary: String,
val messageCount: Int,
val isAlerting: Boolean
val isAlerting: Boolean,
val summaryChannelId: String,
) : NotificationTypes
data class DismissRoom(val roomId: RoomId) : NotificationTypes

View File

@ -16,7 +16,14 @@ class NotificationStateMapper(
val messageEvents = roomEventsToNotifiableMapper.map(events)
when (messageEvents.isEmpty()) {
true -> NotificationTypes.DismissRoom(roomOverview.roomId)
false -> notificationFactory.createMessageNotification(messageEvents, roomOverview, state.roomsWithNewEvents, state.newRooms)
false -> {
notificationFactory.createMessageNotification(
events = messageEvents,
roomOverview = roomOverview,
roomsWithNewEvents = state.roomsWithNewEvents,
newRooms = state.newRooms
)
}
}
}

View File

@ -34,10 +34,12 @@ private fun Flow<UnreadNotifications>.onlyRenderableChanges(): Flow<UnreadNotifi
log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no renderable changes")
false
}
inferredCurrentNotifications.isEmpty() && diff.removed.isNotEmpty() -> {
log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no currently showing messages and changes are all messages marked as read")
false
}
else -> true
}
}
@ -45,29 +47,48 @@ private fun Flow<UnreadNotifications>.onlyRenderableChanges(): Flow<UnreadNotifi
}
private fun Flow<Map<RoomOverview, List<RoomEvent>>>.mapWithDiff(): Flow<Pair<Map<RoomOverview, List<RoomEvent>>, NotificationDiff>> {
val previousUnreadEvents = mutableMapOf<RoomId, List<EventId>>()
val previousUnreadEvents = mutableMapOf<RoomId, List<TimestampedEventId>>()
return this.map { each ->
val allUnreadIds = each.toIds()
val allUnreadIds = each.toTimestampedIds()
val notificationDiff = calculateDiff(allUnreadIds, previousUnreadEvents)
previousUnreadEvents.clearAndPutAll(allUnreadIds)
each to notificationDiff
}
}
private fun calculateDiff(allUnread: Map<RoomId, List<EventId>>, previousUnread: Map<RoomId, List<EventId>>?): NotificationDiff {
private fun calculateDiff(allUnread: Map<RoomId, List<TimestampedEventId>>, previousUnread: Map<RoomId, List<TimestampedEventId>>?): NotificationDiff {
val previousLatestEventTimestamps = previousUnread.toLatestTimestamps()
val newRooms = allUnread.filter { !previousUnread.containsKey(it.key) }.keys
val unchanged = previousUnread?.filter { allUnread.containsKey(it.key) && it.value == allUnread[it.key] } ?: emptyMap()
val changedOrNew = allUnread.filterNot { unchanged.containsKey(it.key) }
val unchanged = previousUnread?.filter {
allUnread.containsKey(it.key) && (it.value == allUnread[it.key])
} ?: emptyMap()
val changedOrNew = allUnread.filterNot { unchanged.containsKey(it.key) }.mapValues { (key, value) ->
val isChangedRoom = !newRooms.contains(key)
if (isChangedRoom) {
val latest = previousLatestEventTimestamps[key] ?: 0L
value.filter {
val isExistingEvent = (previousUnread?.get(key)?.contains(it) ?: false)
!isExistingEvent && it.second > latest
}
} else {
value
}
}.filter { it.value.isNotEmpty() }
val removed = previousUnread?.filter { !allUnread.containsKey(it.key) } ?: emptyMap()
return NotificationDiff(unchanged, changedOrNew, removed, newRooms)
return NotificationDiff(unchanged.toEventIds(), changedOrNew.toEventIds(), removed.toEventIds(), newRooms)
}
private fun List<RoomEvent>.toEventIds() = this.map { it.eventId }
private fun Map<RoomId, List<TimestampedEventId>>?.toLatestTimestamps() = this?.mapValues { it.value.maxOf { it.second } } ?: emptyMap()
private fun Map<RoomOverview, List<RoomEvent>>.toIds() = this
private fun Map<RoomId, List<TimestampedEventId>>.toEventIds() = this.mapValues { it.value.map { it.first } }
private fun Map<RoomOverview, List<RoomEvent>>.toTimestampedIds() = this
.mapValues { it.value.toEventIds() }
.mapKeys { it.key.roomId }
private fun List<RoomEvent>.toEventIds() = this.map { it.eventId to it.utcTimestamp }
private fun <T> Flow<T>.avoidShowingPreviousNotificationsOnLaunch() = drop(1)
data class NotificationDiff(
@ -76,3 +97,5 @@ data class NotificationDiff(
val removed: Map<RoomId, List<EventId>>,
val newRooms: Set<RoomId>
)
typealias TimestampedEventId = Pair<EventId, Long>

View File

@ -1,30 +1,25 @@
package app.dapk.st.notifications
import app.dapk.st.core.AppLogTag.NOTIFICATION
import app.dapk.st.core.log
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomOverview
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
class RenderNotificationsUseCase(
private val notificationRenderer: NotificationRenderer,
private val observeRenderableUnreadEventsUseCase: ObserveUnreadNotificationsUseCase,
notificationChannels: NotificationChannels,
private val notificationChannels: NotificationChannels,
) {
init {
notificationChannels.initChannels()
}
suspend fun listenForNotificationChanges() {
observeRenderableUnreadEventsUseCase()
.onStart { notificationChannels.initChannels() }
.onEach { (each, diff) -> renderUnreadChange(each, diff) }
.collect()
}
private suspend fun renderUnreadChange(allUnread: Map<RoomOverview, List<RoomEvent>>, diff: NotificationDiff) {
log(NOTIFICATION, "unread changed - render notifications")
notificationRenderer.render(
NotificationState(
allUnread = allUnread,

View File

@ -7,6 +7,7 @@ import app.dapk.st.core.DeviceMeta
import app.dapk.st.matrix.common.AvatarUrl
import app.dapk.st.matrix.sync.RoomOverview
import fake.FakeContext
import fixture.NotificationDelegateFixtures.anAndroidNotification
import fixture.NotificationDelegateFixtures.anInboxStyle
import fixture.NotificationFixtures.aRoomNotification
import fixture.aRoomId
@ -16,6 +17,7 @@ import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
private const val A_CHANNEL_ID = "a channel id"
private val AN_OPEN_APP_INTENT = aPendingIntent()
private val AN_OPEN_ROOM_INTENT = aPendingIntent()
private val A_NOTIFICATION_STYLE = anInboxStyle()
@ -30,7 +32,6 @@ private val EVENTS = listOf(
aNotifiable("message two", utcTimestamp = 2),
)
class NotificationFactoryTest {
private val fakeContext = FakeContext()
@ -48,24 +49,34 @@ class NotificationFactoryTest {
@Test
fun `given alerting room notification, when creating summary, then is alerting`() {
val notifications = listOf(aRoomNotification(isAlerting = true))
val notifications = listOf(
aRoomNotification(
summaryChannelId = A_CHANNEL_ID,
notification = anAndroidNotification(channelId = A_CHANNEL_ID), isAlerting = true
)
)
fakeIntentFactory.givenNotificationOpenApp(fakeContext.instance).returns(AN_OPEN_APP_INTENT)
fakeNotificationStyleFactory.givenSummary(notifications).returns(anInboxStyle())
val result = notificationFactory.createSummary(notifications)
result shouldBeEqualTo expectedSummary(shouldAlertMoreThanOnce = true)
result shouldBeEqualTo expectedSummary(notifications.first().notification, shouldAlertMoreThanOnce = true)
}
@Test
fun `given non alerting room notification, when creating summary, then is alerting`() {
val notifications = listOf(aRoomNotification(isAlerting = false))
val notifications = listOf(
aRoomNotification(
summaryChannelId = A_CHANNEL_ID,
notification = anAndroidNotification(channelId = A_CHANNEL_ID), isAlerting = false
)
)
fakeIntentFactory.givenNotificationOpenApp(fakeContext.instance).returns(AN_OPEN_APP_INTENT)
fakeNotificationStyleFactory.givenSummary(notifications).returns(anInboxStyle())
val result = notificationFactory.createSummary(notifications)
result shouldBeEqualTo expectedSummary(shouldAlertMoreThanOnce = false)
result shouldBeEqualTo expectedSummary(notifications.first().notification, shouldAlertMoreThanOnce = false)
}
@Test
@ -75,6 +86,7 @@ class NotificationFactoryTest {
val result = notificationFactory.createMessageNotification(EVENTS, A_GROUP_ROOM_OVERVIEW, setOf(A_ROOM_ID), newRooms = setOf(A_ROOM_ID))
result shouldBeEqualTo expectedMessage(
channel = GROUP_CHANNEL_ID,
shouldAlertMoreThanOnce = true,
)
}
@ -86,6 +98,7 @@ class NotificationFactoryTest {
val result = notificationFactory.createMessageNotification(EVENTS, A_GROUP_ROOM_OVERVIEW, setOf(A_ROOM_ID), newRooms = emptySet())
result shouldBeEqualTo expectedMessage(
channel = GROUP_CHANNEL_ID,
shouldAlertMoreThanOnce = false,
)
}
@ -97,6 +110,7 @@ class NotificationFactoryTest {
val result = notificationFactory.createMessageNotification(EVENTS, A_DM_ROOM_OVERVIEW, setOf(A_ROOM_ID), newRooms = setOf(A_ROOM_ID))
result shouldBeEqualTo expectedMessage(
channel = DIRECT_CHANNEL_ID,
shouldAlertMoreThanOnce = true,
)
}
@ -108,6 +122,7 @@ class NotificationFactoryTest {
val result = notificationFactory.createMessageNotification(EVENTS, A_DM_ROOM_OVERVIEW, setOf(A_ROOM_ID), newRooms = emptySet())
result shouldBeEqualTo expectedMessage(
channel = DIRECT_CHANNEL_ID,
shouldAlertMoreThanOnce = true,
)
}
@ -119,15 +134,16 @@ class NotificationFactoryTest {
}
private fun expectedMessage(
channel: String,
shouldAlertMoreThanOnce: Boolean,
) = NotificationTypes.Room(
AndroidNotification(
channelId = "message",
channelId = SUMMARY_CHANNEL_ID,
whenTimestamp = LATEST_EVENT.utcTimestamp,
groupId = "st",
groupAlertBehavior = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) Notification.GROUP_ALERT_SUMMARY else null,
shortcutId = A_ROOM_ID.value,
alertMoreThanOnce = shouldAlertMoreThanOnce,
alertMoreThanOnce = false,
contentIntent = AN_OPEN_ROOM_INTENT,
messageStyle = A_NOTIFICATION_STYLE,
category = Notification.CATEGORY_MESSAGE,
@ -139,15 +155,18 @@ class NotificationFactoryTest {
summary = LATEST_EVENT.content,
messageCount = EVENTS.size,
isAlerting = shouldAlertMoreThanOnce,
summaryChannelId = channel,
)
private fun expectedSummary(shouldAlertMoreThanOnce: Boolean) = AndroidNotification(
channelId = "message",
private fun expectedSummary(notification: AndroidNotification, shouldAlertMoreThanOnce: Boolean) = AndroidNotification(
channelId = notification.channelId,
whenTimestamp = notification.whenTimestamp,
messageStyle = A_NOTIFICATION_STYLE,
alertMoreThanOnce = shouldAlertMoreThanOnce,
smallIcon = R.drawable.ic_notification_small_icon,
contentIntent = AN_OPEN_APP_INTENT,
groupId = "st",
category = Notification.CATEGORY_MESSAGE,
isGroupSummary = true,
autoCancel = true
)

View File

@ -3,8 +3,11 @@ package app.dapk.st.notifications
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomOverview
import fake.FakeRoomStore
import fixture.*
import fixture.NotificationDiffFixtures.aNotificationDiff
import fixture.aRoomId
import fixture.aRoomMessageEvent
import fixture.aRoomOverview
import fixture.anEventId
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest
@ -12,8 +15,8 @@ import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
private val NO_UNREADS = emptyMap<RoomOverview, List<RoomEvent>>()
private val A_MESSAGE = aRoomMessageEvent(eventId = anEventId("1"), content = "hello")
private val A_MESSAGE_2 = aRoomMessageEvent(eventId = anEventId("2"), content = "world")
private val A_MESSAGE = aRoomMessageEvent(eventId = anEventId("1"), content = "hello", utcTimestamp = 1000)
private val A_MESSAGE_2 = aRoomMessageEvent(eventId = anEventId("2"), content = "world", utcTimestamp = 2000)
private val A_ROOM_OVERVIEW = aRoomOverview(roomId = aRoomId("1"))
private val A_ROOM_OVERVIEW_2 = aRoomOverview(roomId = aRoomId("2"))
@ -48,7 +51,7 @@ class ObserveUnreadRenderNotificationsUseCaseTest {
changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE),
newRooms = setOf(A_ROOM_OVERVIEW.roomId)
),
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2) to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE, A_MESSAGE_2))
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2) to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2))
)
}
@ -61,7 +64,7 @@ class ObserveUnreadRenderNotificationsUseCaseTest {
val result = useCase.invoke().toList()
result shouldBeEqualTo listOf(
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2) to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE, A_MESSAGE_2))
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2) to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2))
)
}
@ -76,6 +79,26 @@ class ObserveUnreadRenderNotificationsUseCaseTest {
result shouldBeEqualTo emptyList()
}
@Test
fun `given new and then historical message, when reading a message, then only emits the latest`() = runTest {
fakeRoomStore.givenUnreadEvents(
flowOf(
NO_UNREADS,
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE),
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE.copy(eventId = anEventId("old"), utcTimestamp = -1))
)
)
val result = useCase.invoke().toList()
result shouldBeEqualTo listOf(
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE) to aNotificationDiff(
changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE),
newRooms = setOf(A_ROOM_OVERVIEW.roomId)
),
)
}
@Test
fun `given initial unreads, when reading a duplicate unread, then emits nothing`() = runTest {
fakeRoomStore.givenUnreadEvents(

View File

@ -25,7 +25,12 @@ class RenderNotificationsUseCaseTest {
)
@Test
fun `when creating use case instance, then initiates channels`() {
fun `given events, when listening for changes then initiates channels once`() = runTest {
fakeNotificationRenderer.instance.expect { it.render(any()) }
fakeObserveUnreadNotificationsUseCase.given().emits(AN_UNREAD_NOTIFICATIONS)
renderNotificationsUseCase.listenForNotificationChanges()
fakeNotificationChannels.verifyInitiated()
}

View File

@ -14,15 +14,18 @@ object NotificationFixtures {
) = Notifications(summaryNotification, delegates)
fun aRoomNotification(
notification: AndroidNotification = anAndroidNotification(),
summary: String = "a summary line",
messageCount: Int = 1,
isAlerting: Boolean = false,
summaryChannelId: String = "a-summary-channel-id",
) = NotificationTypes.Room(
anAndroidNotification(),
notification,
aRoomId(),
summary = summary,
messageCount = messageCount,
isAlerting = isAlerting
isAlerting = isAlerting,
summaryChannelId = summaryChannelId
)
fun aDismissRoomNotification(

View File

@ -199,6 +199,13 @@ private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit) {
item { Spacer(Modifier.height(12.dp)) }
}
}
is Lce.Error -> {
// TODO
}
is Lce.Loading -> {
// TODO
}
}
}

View File

@ -42,6 +42,13 @@ fun EventLogScreen(viewModel: EventLoggerViewModel) {
}
}
}
is Lce.Error -> {
// TODO
}
is Lce.Loading -> {
// TODO
}
}
}

Binary file not shown.

View File

@ -1,6 +1,6 @@
#Sat Apr 02 22:45:08 BST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
distributionSha256Sum=cb87f222c5585bd46838ad4db78463a5c5f3d336e5e2b98dc7c0c586527351c2
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

6
gradlew vendored
View File

@ -205,6 +205,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.

14
gradlew.bat vendored
View File

@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@ -25,7 +25,7 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal

View File

@ -484,8 +484,8 @@ internal sealed class ApiTimelineEvent {
@Serializable
internal data class Info(
@SerialName("h") val height: Int,
@SerialName("w") val width: Int,
@SerialName("h") val height: Int? = null,
@SerialName("w") val width: Int? = null,
)
}