splitting the notification logic
This commit is contained in:
parent
6725a0648e
commit
a8218b7e66
|
@ -75,7 +75,7 @@ internal class RoomPersistence(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun observeUnread(): Flow<Map<RoomOverview, List<RoomEvent>>> {
|
override fun observeUnread(): Flow<Map<RoomOverview, List<RoomEvent>>> {
|
||||||
return database.roomEventQueries.selectAllUnread()
|
return database.roomEventQueries.selectAllUnread()
|
||||||
.asFlow()
|
.asFlow()
|
||||||
.mapToList()
|
.mapToList()
|
||||||
|
@ -107,7 +107,7 @@ internal class RoomPersistence(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun observeEvent(eventId: EventId): Flow<EventId> {
|
override fun observeEvent(eventId: EventId): Flow<EventId> {
|
||||||
return database.roomEventQueries.selectEvent(event_id = eventId.value)
|
return database.roomEventQueries.selectEvent(event_id = eventId.value)
|
||||||
.asFlow()
|
.asFlow()
|
||||||
.mapToOneNotNull()
|
.mapToOneNotNull()
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package app.dapk.st.directory
|
package app.dapk.st.directory
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import app.dapk.st.matrix.common.CredentialsStore
|
import app.dapk.st.matrix.common.CredentialsStore
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.common.RoomMember
|
import app.dapk.st.matrix.common.RoomMember
|
||||||
|
@ -17,7 +18,11 @@ value class UnreadCount(val value: Int)
|
||||||
|
|
||||||
typealias DirectoryState = List<RoomFoo>
|
typealias DirectoryState = List<RoomFoo>
|
||||||
|
|
||||||
data class RoomFoo(val overview: RoomOverview, val unreadCount: UnreadCount, val typing: Typing?)
|
data class RoomFoo(
|
||||||
|
val overview: RoomOverview,
|
||||||
|
val unreadCount: UnreadCount,
|
||||||
|
val typing: Typing?
|
||||||
|
)
|
||||||
|
|
||||||
class DirectoryUseCase(
|
class DirectoryUseCase(
|
||||||
private val syncService: SyncService,
|
private val syncService: SyncService,
|
||||||
|
@ -35,6 +40,7 @@ class DirectoryUseCase(
|
||||||
roomStore.observeUnreadCountById(),
|
roomStore.observeUnreadCountById(),
|
||||||
syncService.events()
|
syncService.events()
|
||||||
) { userId, overviewState, localEchos, unread, events ->
|
) { userId, overviewState, localEchos, unread, events ->
|
||||||
|
Log.e("!!!", "got states")
|
||||||
overviewState.mergeWithLocalEchos(localEchos, userId).map { roomOverview ->
|
overviewState.mergeWithLocalEchos(localEchos, userId).map { roomOverview ->
|
||||||
RoomFoo(
|
RoomFoo(
|
||||||
overview = roomOverview,
|
overview = roomOverview,
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
|
||||||
|
private const val channelId = "message"
|
||||||
|
|
||||||
|
class NotificationChannels(
|
||||||
|
private val notificationManager: NotificationManager
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun initChannels() {
|
||||||
|
if (notificationManager.getNotificationChannel(channelId) == null) {
|
||||||
|
notificationManager.createNotificationChannel(
|
||||||
|
NotificationChannel(
|
||||||
|
channelId,
|
||||||
|
"messages",
|
||||||
|
NotificationManager.IMPORTANCE_HIGH,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.Person
|
||||||
|
import android.content.Context
|
||||||
|
import app.dapk.st.imageloader.IconLoader
|
||||||
|
import app.dapk.st.matrix.sync.RoomEvent
|
||||||
|
import app.dapk.st.matrix.sync.RoomOverview
|
||||||
|
import app.dapk.st.messenger.MessengerActivity
|
||||||
|
|
||||||
|
private const val GROUP_ID = "st"
|
||||||
|
private const val channelId = "message"
|
||||||
|
|
||||||
|
class NotificationFactory(
|
||||||
|
private val iconLoader: IconLoader,
|
||||||
|
private val context: Context,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun createNotifications(events: Map<RoomOverview, List<RoomEvent>>): Notifications {
|
||||||
|
val notifications = events.map { (roomOverview, events) ->
|
||||||
|
val messageEvents = events.filterIsInstance<RoomEvent.Message>()
|
||||||
|
when (messageEvents.isEmpty()) {
|
||||||
|
true -> NotificationDelegate.DismissRoom(roomOverview.roomId)
|
||||||
|
false -> createNotification(messageEvents, roomOverview)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val summaryNotification = if (notifications.filterIsInstance<NotificationDelegate.Room>().size > 1) {
|
||||||
|
createSummary(notifications)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
return Notifications(summaryNotification, notifications)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSummary(notifications: List<NotificationDelegate>): Notification {
|
||||||
|
val summaryInboxStyle = Notification.InboxStyle().also { style ->
|
||||||
|
notifications.forEach {
|
||||||
|
when (it) {
|
||||||
|
is NotificationDelegate.DismissRoom -> {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
is NotificationDelegate.Room -> style.addLine(it.summary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Notification.Builder(context, channelId)
|
||||||
|
.setStyle(summaryInboxStyle)
|
||||||
|
.setSmallIcon(R.drawable.ic_notification_small_icon)
|
||||||
|
.setCategory(Notification.CATEGORY_MESSAGE)
|
||||||
|
.setGroupSummary(true)
|
||||||
|
.setGroup(GROUP_ID)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun createNotification(events: List<RoomEvent.Message>, roomOverview: RoomOverview): NotificationDelegate {
|
||||||
|
val messageStyle = Notification.MessagingStyle(
|
||||||
|
Person.Builder()
|
||||||
|
.setName("me")
|
||||||
|
.setKey(roomOverview.roomId.value)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
messageStyle.conversationTitle = roomOverview.roomName.takeIf { roomOverview.isGroup }
|
||||||
|
messageStyle.isGroupConversation = roomOverview.isGroup
|
||||||
|
|
||||||
|
events.sortedBy { it.utcTimestamp }.forEach { message ->
|
||||||
|
val sender = Person.Builder()
|
||||||
|
.setName(message.author.displayName ?: message.author.id.value)
|
||||||
|
.setIcon(message.author.avatarUrl?.let { iconLoader.load(it.value) })
|
||||||
|
.setKey(message.author.id.value)
|
||||||
|
.build()
|
||||||
|
messageStyle.addMessage(
|
||||||
|
Notification.MessagingStyle.Message(
|
||||||
|
message.content,
|
||||||
|
message.utcTimestamp,
|
||||||
|
sender,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val openRoomIntent = PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
55,
|
||||||
|
MessengerActivity.newInstance(context, roomOverview.roomId),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
return NotificationDelegate.Room(
|
||||||
|
Notification.Builder(context, channelId)
|
||||||
|
.setWhen(messageStyle.messages.last().timestamp)
|
||||||
|
.setShowWhen(true)
|
||||||
|
.setGroup(GROUP_ID)
|
||||||
|
.setOnlyAlertOnce(roomOverview.isGroup)
|
||||||
|
.setContentIntent(openRoomIntent)
|
||||||
|
.setStyle(messageStyle)
|
||||||
|
.setCategory(Notification.CATEGORY_MESSAGE)
|
||||||
|
.setShortcutId(roomOverview.roomId.value)
|
||||||
|
.setSmallIcon(R.drawable.ic_notification_small_icon)
|
||||||
|
.setLargeIcon(roomOverview.roomAvatarUrl?.let { iconLoader.load(it.value) })
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.build(),
|
||||||
|
roomId = roomOverview.roomId,
|
||||||
|
summary = messageStyle.messages.last().text.toString()
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Notifications(val summaryNotification: Notification?, val delegates: List<NotificationDelegate>)
|
|
@ -0,0 +1,43 @@
|
||||||
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import app.dapk.st.core.extensions.ifNull
|
||||||
|
import app.dapk.st.matrix.common.RoomId
|
||||||
|
import app.dapk.st.matrix.sync.RoomEvent
|
||||||
|
import app.dapk.st.matrix.sync.RoomOverview
|
||||||
|
|
||||||
|
private const val SUMMARY_NOTIFICATION_ID = 101
|
||||||
|
private const val MESSAGE_NOTIFICATION_ID = 100
|
||||||
|
|
||||||
|
class NotificationRenderer(
|
||||||
|
private val notificationManager: NotificationManager,
|
||||||
|
private val notificationFactory: NotificationFactory,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun render(result: Map<RoomOverview, List<RoomEvent>>, removedRooms: Set<RoomId>) {
|
||||||
|
removedRooms.forEach { notificationManager.cancel(it.value, MESSAGE_NOTIFICATION_ID) }
|
||||||
|
val notifications = notificationFactory.createNotifications(result)
|
||||||
|
|
||||||
|
notifications.summaryNotification.ifNull {
|
||||||
|
notificationManager.cancel(SUMMARY_NOTIFICATION_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
notifications.delegates.forEach {
|
||||||
|
when (it) {
|
||||||
|
is NotificationDelegate.DismissRoom -> notificationManager.cancel(it.roomId.value, MESSAGE_NOTIFICATION_ID)
|
||||||
|
is NotificationDelegate.Room -> notificationManager.notify(it.roomId.value, MESSAGE_NOTIFICATION_ID, it.notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notifications.summaryNotification?.let {
|
||||||
|
notificationManager.notify(SUMMARY_NOTIFICATION_ID, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface NotificationDelegate {
|
||||||
|
data class Room(val notification: Notification, val roomId: RoomId, val summary: String) : NotificationDelegate
|
||||||
|
data class DismissRoom(val roomId: RoomId) : NotificationDelegate
|
||||||
|
}
|
|
@ -23,13 +23,12 @@ class NotificationsModule(
|
||||||
fun pushUseCase() = pushService
|
fun pushUseCase() = pushService
|
||||||
fun syncService() = syncService
|
fun syncService() = syncService
|
||||||
fun credentialProvider() = credentialsStore
|
fun credentialProvider() = credentialsStore
|
||||||
fun roomStore() = roomStore
|
|
||||||
fun iconLoader() = iconLoader
|
|
||||||
fun firebasePushTokenUseCase() = firebasePushTokenUseCase
|
fun firebasePushTokenUseCase() = firebasePushTokenUseCase
|
||||||
fun notificationsUseCase() = NotificationsUseCase(
|
fun notificationsUseCase() = NotificationsUseCase(
|
||||||
roomStore,
|
roomStore,
|
||||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager,
|
NotificationRenderer(notificationManager(), NotificationFactory(iconLoader, context)),
|
||||||
iconLoader,
|
NotificationChannels(notificationManager()),
|
||||||
context,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun notificationManager() = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,49 +1,26 @@
|
||||||
package app.dapk.st.notifications
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
import android.app.*
|
|
||||||
import android.app.Notification.InboxStyle
|
|
||||||
import android.content.Context
|
|
||||||
import app.dapk.st.core.AppLogTag.NOTIFICATION
|
import app.dapk.st.core.AppLogTag.NOTIFICATION
|
||||||
import app.dapk.st.core.log
|
import app.dapk.st.core.log
|
||||||
import app.dapk.st.imageloader.IconLoader
|
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.sync.RoomEvent
|
|
||||||
import app.dapk.st.matrix.sync.RoomOverview
|
|
||||||
import app.dapk.st.matrix.sync.RoomStore
|
import app.dapk.st.matrix.sync.RoomStore
|
||||||
import app.dapk.st.messenger.MessengerActivity
|
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
||||||
private const val SUMMARY_NOTIFICATION_ID = 101
|
|
||||||
private const val MESSAGE_NOTIFICATION_ID = 100
|
|
||||||
private const val GROUP_ID = "st"
|
|
||||||
|
|
||||||
class NotificationsUseCase(
|
class NotificationsUseCase(
|
||||||
private val roomStore: RoomStore,
|
private val roomStore: RoomStore,
|
||||||
private val notificationManager: NotificationManager,
|
private val notificationRenderer: NotificationRenderer,
|
||||||
private val iconLoader: IconLoader,
|
notificationChannels: NotificationChannels,
|
||||||
private val context: Context,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val inferredCurrentNotifications = mutableSetOf<RoomId>()
|
private val inferredCurrentNotifications = mutableSetOf<RoomId>()
|
||||||
private val channelId = "message"
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (notificationManager.getNotificationChannel(channelId) == null) {
|
notificationChannels.initChannels()
|
||||||
notificationManager.createNotificationChannel(
|
|
||||||
NotificationChannel(
|
|
||||||
channelId,
|
|
||||||
"messages",
|
|
||||||
NotificationManager.IMPORTANCE_HIGH,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun listenForNotificationChanges() {
|
suspend fun listenForNotificationChanges() {
|
||||||
// TODO handle redactions by removing and edits by not notifying
|
|
||||||
|
|
||||||
roomStore.observeUnread()
|
roomStore.observeUnread()
|
||||||
.drop(1)
|
.drop(1)
|
||||||
.onEach { result ->
|
.onEach { result ->
|
||||||
|
@ -51,125 +28,12 @@ class NotificationsUseCase(
|
||||||
|
|
||||||
val asRooms = result.keys.map { it.roomId }.toSet()
|
val asRooms = result.keys.map { it.roomId }.toSet()
|
||||||
val removedRooms = inferredCurrentNotifications - asRooms
|
val removedRooms = inferredCurrentNotifications - asRooms
|
||||||
removedRooms.forEach { notificationManager.cancel(it.value, MESSAGE_NOTIFICATION_ID) }
|
|
||||||
|
|
||||||
inferredCurrentNotifications.clear()
|
inferredCurrentNotifications.clear()
|
||||||
inferredCurrentNotifications.addAll(asRooms)
|
inferredCurrentNotifications.addAll(asRooms)
|
||||||
|
|
||||||
val notifications = result.map { (roomOverview, events) ->
|
notificationRenderer.render(result, removedRooms)
|
||||||
val messageEvents = events.filterIsInstance<RoomEvent.Message>()
|
|
||||||
when (messageEvents.isEmpty()) {
|
|
||||||
true -> NotificationDelegate.DismissRoom(roomOverview.roomId)
|
|
||||||
false -> createNotification(messageEvents, roomOverview)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val summaryNotification = if (notifications.filterIsInstance<NotificationDelegate.Room>().size > 1) {
|
|
||||||
createSummary(notifications)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (summaryNotification == null) {
|
|
||||||
notificationManager.cancel(SUMMARY_NOTIFICATION_ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
notifications.forEach {
|
|
||||||
when (it) {
|
|
||||||
is NotificationDelegate.DismissRoom -> notificationManager.cancel(it.roomId.value, MESSAGE_NOTIFICATION_ID)
|
|
||||||
is NotificationDelegate.Room -> notificationManager.notify(it.roomId.value, MESSAGE_NOTIFICATION_ID, it.notification)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (summaryNotification != null) {
|
|
||||||
notificationManager.notify(SUMMARY_NOTIFICATION_ID, summaryNotification)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSummary(notifications: List<NotificationDelegate>): Notification {
|
|
||||||
val summaryInboxStyle = InboxStyle().also { style ->
|
|
||||||
notifications.forEach {
|
|
||||||
when (it) {
|
|
||||||
is NotificationDelegate.DismissRoom -> {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
is NotificationDelegate.Room -> style.addLine(it.summary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Notification.Builder(context, channelId)
|
|
||||||
.setStyle(summaryInboxStyle)
|
|
||||||
.setSmallIcon(R.drawable.ic_notification_small_icon)
|
|
||||||
.setCategory(Notification.CATEGORY_MESSAGE)
|
|
||||||
.setGroupSummary(true)
|
|
||||||
.setGroup(GROUP_ID)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun createNotification(events: List<RoomEvent.Message>, roomOverview: RoomOverview): NotificationDelegate {
|
|
||||||
val messageStyle = Notification.MessagingStyle(
|
|
||||||
Person.Builder()
|
|
||||||
.setName("me")
|
|
||||||
.setKey(roomOverview.roomId.value)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
messageStyle.conversationTitle = roomOverview.roomName.takeIf { roomOverview.isGroup }
|
|
||||||
messageStyle.isGroupConversation = roomOverview.isGroup
|
|
||||||
|
|
||||||
events.sortedBy { it.utcTimestamp }.forEach { message ->
|
|
||||||
val sender = Person.Builder()
|
|
||||||
.setName(message.author.displayName ?: message.author.id.value)
|
|
||||||
.setIcon(message.author.avatarUrl?.let { iconLoader.load(it.value) })
|
|
||||||
.setKey(message.author.id.value)
|
|
||||||
.build()
|
|
||||||
messageStyle.addMessage(
|
|
||||||
Notification.MessagingStyle.Message(
|
|
||||||
message.content,
|
|
||||||
message.utcTimestamp,
|
|
||||||
sender,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val openRoomIntent = PendingIntent.getActivity(
|
|
||||||
context,
|
|
||||||
55,
|
|
||||||
MessengerActivity.newInstance(context, roomOverview.roomId),
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
||||||
)
|
|
||||||
|
|
||||||
return NotificationDelegate.Room(
|
|
||||||
Notification.Builder(context, channelId)
|
|
||||||
.setWhen(messageStyle.messages.last().timestamp)
|
|
||||||
.setShowWhen(true)
|
|
||||||
.setGroup(GROUP_ID)
|
|
||||||
.setOnlyAlertOnce(roomOverview.isGroup)
|
|
||||||
.setContentIntent(openRoomIntent)
|
|
||||||
.setStyle(messageStyle)
|
|
||||||
.setCategory(Notification.CATEGORY_MESSAGE)
|
|
||||||
.setShortcutId(roomOverview.roomId.value)
|
|
||||||
.setSmallIcon(R.drawable.ic_notification_small_icon)
|
|
||||||
.setLargeIcon(roomOverview.roomAvatarUrl?.let { iconLoader.load(it.value) })
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.build(),
|
|
||||||
roomId = roomOverview.roomId,
|
|
||||||
summary = messageStyle.messages.last().text.toString()
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface NotificationDelegate {
|
|
||||||
|
|
||||||
data class Room(val notification: Notification, val roomId: RoomId, val summary: String) : NotificationDelegate
|
|
||||||
data class DismissRoom(val roomId: RoomId) : NotificationDelegate
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -12,9 +12,9 @@ interface RoomStore {
|
||||||
fun latest(roomId: RoomId): Flow<RoomState>
|
fun latest(roomId: RoomId): Flow<RoomState>
|
||||||
suspend fun insertUnread(roomId: RoomId, eventIds: List<EventId>)
|
suspend fun insertUnread(roomId: RoomId, eventIds: List<EventId>)
|
||||||
suspend fun markRead(roomId: RoomId)
|
suspend fun markRead(roomId: RoomId)
|
||||||
suspend fun observeUnread(): Flow<Map<RoomOverview, List<RoomEvent>>>
|
fun observeUnread(): Flow<Map<RoomOverview, List<RoomEvent>>>
|
||||||
fun observeUnreadCountById(): Flow<Map<RoomId, Int>>
|
fun observeUnreadCountById(): Flow<Map<RoomId, Int>>
|
||||||
suspend fun observeEvent(eventId: EventId): Flow<EventId>
|
fun observeEvent(eventId: EventId): Flow<EventId>
|
||||||
suspend fun findEvent(eventId: EventId): RoomEvent?
|
suspend fun findEvent(eventId: EventId): RoomEvent?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue