mirror of
https://github.com/ouchadam/small-talk.git
synced 2025-02-21 22:47:43 +01:00
Merge branch 'main' of github.com:ouchadam/helium into feature/share-images-via-small-talk
This commit is contained in:
commit
9c99b6a0a9
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -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
|
||||
|
@ -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")
|
||||
|
10
build.gradle
10
build.gradle
@ -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 ->
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -41,6 +41,9 @@ fun LifecycleEffect(onStart: () -> Unit = {}, onStop: () -> Unit = {}) {
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_START -> onStart()
|
||||
Lifecycle.Event.ON_STOP -> onStop()
|
||||
else -> {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
@file:JvmName("SnapshotStateKt")
|
||||
|
||||
@file:Suppress("UNUSED")
|
||||
package androidx.compose.runtime
|
||||
|
||||
import kotlin.reflect.KProperty
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
@ -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,
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,6 +42,13 @@ fun EventLogScreen(viewModel: EventLoggerViewModel) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is Lce.Error -> {
|
||||
// TODO
|
||||
}
|
||||
is Lce.Loading -> {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
6
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
6
gradlew
vendored
@ -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
14
gradlew.bat
vendored
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user