mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
DROID-3707 Notifications | Clear push notifications for opened chat (#2475)
This commit is contained in:
parent
6790b13882
commit
08902e61cf
15 changed files with 343 additions and 69 deletions
|
@ -7,28 +7,35 @@ import android.app.PendingIntent
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.anytypeio.anytype.R
|
||||
import com.anytypeio.anytype.core_models.DecryptedPushContent
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.Relations
|
||||
import com.anytypeio.anytype.domain.notifications.NotificationBuilder
|
||||
import com.anytypeio.anytype.domain.resources.StringResourceProvider
|
||||
import com.anytypeio.anytype.ui.main.MainActivity
|
||||
import kotlin.math.absoluteValue
|
||||
import timber.log.Timber
|
||||
|
||||
class NotificationBuilder(
|
||||
class NotificationBuilderImpl(
|
||||
private val context: Context,
|
||||
private val notificationManager: NotificationManager
|
||||
) {
|
||||
|
||||
private val attachmentText get() = context.getString(R.string.attachment)
|
||||
private val notificationManager: NotificationManager,
|
||||
private val resourceProvider: StringResourceProvider
|
||||
) : NotificationBuilder {
|
||||
|
||||
private val attachmentText get() = resourceProvider.getAttachmentText()
|
||||
private val createdChannels = mutableSetOf<String>()
|
||||
|
||||
fun buildAndNotify(message: DecryptedPushContent.Message, spaceId: Id) {
|
||||
override fun buildAndNotify(message: DecryptedPushContent.Message, spaceId: Id) {
|
||||
val channelId = "${spaceId}_${message.chatId}"
|
||||
|
||||
// 1) Build the intent that'll open your MainActivity in the right chat
|
||||
ensureChannelExists(
|
||||
channelId = channelId,
|
||||
channelName = sanitizeChannelName(message.spaceName)
|
||||
)
|
||||
|
||||
// Create pending intent to open chat
|
||||
val pending = createChatPendingIntent(
|
||||
context = context,
|
||||
chatId = message.chatId,
|
||||
|
@ -37,18 +44,9 @@ class NotificationBuilder(
|
|||
|
||||
// Format the notification body text
|
||||
val bodyText = message.formatNotificationBody(attachmentText)
|
||||
|
||||
// 2) put it all on one line: "Author: <bodyText>"
|
||||
val singleLine = "${message.senderName.trim()}: $bodyText"
|
||||
|
||||
val channelName = sanitizeChannelName(message.spaceName)
|
||||
|
||||
createNotificationChannelIfNeeded(
|
||||
channelId = spaceId,
|
||||
channelName = channelName
|
||||
)
|
||||
|
||||
val notif = NotificationCompat.Builder(context, spaceId)
|
||||
val notif = NotificationCompat.Builder(context, channelId)
|
||||
.setSmallIcon(R.drawable.ic_app_notification)
|
||||
.setContentTitle(message.spaceName.trim())
|
||||
.setContentText(singleLine)
|
||||
|
@ -68,29 +66,28 @@ class NotificationBuilder(
|
|||
notificationManager.notify(System.currentTimeMillis().toInt(), notif)
|
||||
}
|
||||
|
||||
private fun createNotificationChannelIfNeeded(
|
||||
channelId: String,
|
||||
channelName: String
|
||||
) {
|
||||
/**
|
||||
* Ensures the notification channel (and group) exist before notifying.
|
||||
*/
|
||||
private fun ensureChannelExists(channelId: String, channelName: String) {
|
||||
createChannelGroupIfNeeded()
|
||||
if (createdChannels.contains(channelId)) return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
channelId,
|
||||
channelName,
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
).apply {
|
||||
description = "New messages notifications"
|
||||
enableLights(true)
|
||||
enableVibration(true)
|
||||
setShowBadge(true)
|
||||
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
group = CHANNEL_GROUP_ID
|
||||
}
|
||||
val channel = NotificationChannel(
|
||||
channelId,
|
||||
channelName,
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
).apply {
|
||||
description = "New messages notifications"
|
||||
enableLights(true)
|
||||
enableVibration(true)
|
||||
setShowBadge(true)
|
||||
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
group = CHANNEL_GROUP_ID
|
||||
}
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
createdChannels.add(channelId)
|
||||
}
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
createdChannels.add(channelId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -124,7 +121,8 @@ class NotificationBuilder(
|
|||
fun createChannelGroupIfNeeded() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
try {
|
||||
val existingGroup = notificationManager.getNotificationChannelGroup(CHANNEL_GROUP_ID)
|
||||
val existingGroup =
|
||||
notificationManager.getNotificationChannelGroup(CHANNEL_GROUP_ID)
|
||||
if (existingGroup == null) {
|
||||
val group = NotificationChannelGroup(CHANNEL_GROUP_ID, CHANNEL_GROUP_NAME)
|
||||
notificationManager.createNotificationChannelGroup(group)
|
||||
|
@ -135,7 +133,7 @@ class NotificationBuilder(
|
|||
// Just create the group without checking if it exists
|
||||
val group = NotificationChannelGroup(CHANNEL_GROUP_ID, CHANNEL_GROUP_NAME)
|
||||
notificationManager.createNotificationChannelGroup(group)
|
||||
} catch (e : Exception) {
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error while creating or getting notification group")
|
||||
val group = NotificationChannelGroup(CHANNEL_GROUP_ID, CHANNEL_GROUP_NAME)
|
||||
notificationManager.createNotificationChannelGroup(group)
|
||||
|
@ -143,6 +141,23 @@ class NotificationBuilder(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes notifications and the channel for a specific chat in a space, so that
|
||||
* when the user opens that chat, old notifications are cleared.
|
||||
*/
|
||||
override fun clearNotificationChannel(spaceId: String, chatId: String) {
|
||||
val channelId = "${spaceId}_${chatId}"
|
||||
|
||||
// Remove posted notifications for this specific chat channel
|
||||
notificationManager.activeNotifications
|
||||
.filter { it.notification.channelId == channelId }
|
||||
.forEach { notificationManager.cancel(it.id) }
|
||||
|
||||
// Delete the specific chat channel
|
||||
notificationManager.deleteNotificationChannel(channelId)
|
||||
createdChannels.remove(channelId)
|
||||
}
|
||||
|
||||
private fun sanitizeChannelName(name: String): String {
|
||||
return name.trim().replace(Regex("[^a-zA-Z0-9 _-]"), "_")
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package com.anytypeio.anytype.device
|
||||
|
||||
import android.util.Base64
|
||||
import com.anytypeio.anytype.domain.notifications.NotificationBuilder
|
||||
import com.anytypeio.anytype.presentation.notifications.DecryptionPushContentService
|
||||
|
||||
interface PushMessageProcessor {
|
||||
|
|
|
@ -17,6 +17,7 @@ import com.anytypeio.anytype.domain.misc.UrlBuilder
|
|||
import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer
|
||||
import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer
|
||||
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
|
||||
import com.anytypeio.anytype.domain.notifications.NotificationBuilder
|
||||
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
|
||||
import com.anytypeio.anytype.domain.spaces.ClearLastOpenedSpace
|
||||
import com.anytypeio.anytype.domain.workspace.SpaceManager
|
||||
|
@ -103,4 +104,5 @@ interface ChatComponentDependencies : ComponentDependencies {
|
|||
fun spaceManager(): SpaceManager
|
||||
fun notificationPermissionManager(): NotificationPermissionManager
|
||||
fun storelessSubscriptionContainer(): StorelessSubscriptionContainer
|
||||
fun notificationBuilder(): NotificationBuilder
|
||||
}
|
|
@ -1,15 +1,15 @@
|
|||
package com.anytypeio.anytype.di.feature.notifications
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import com.anytypeio.anytype.device.AnytypePushService
|
||||
import com.anytypeio.anytype.device.DefaultPushMessageProcessor
|
||||
import com.anytypeio.anytype.device.NotificationBuilder
|
||||
import com.anytypeio.anytype.device.PushMessageProcessor
|
||||
import com.anytypeio.anytype.di.common.ComponentDependencies
|
||||
import com.anytypeio.anytype.di.main.ConfigModule.DEFAULT_APP_COROUTINE_SCOPE
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.device.DeviceTokenStoringService
|
||||
import com.anytypeio.anytype.domain.notifications.NotificationBuilder
|
||||
import com.anytypeio.anytype.domain.resources.StringResourceProvider
|
||||
import com.anytypeio.anytype.presentation.notifications.CryptoService
|
||||
import com.anytypeio.anytype.presentation.notifications.CryptoServiceImpl
|
||||
import com.anytypeio.anytype.presentation.notifications.DecryptionPushContentService
|
||||
|
@ -38,25 +38,6 @@ interface PushContentComponent {
|
|||
|
||||
@Module
|
||||
object PushContentModule {
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNotificationManager(
|
||||
context: Context
|
||||
): NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNotificationBuilder(
|
||||
context: Context,
|
||||
notificationManager: NotificationManager
|
||||
): NotificationBuilder = NotificationBuilder(
|
||||
context = context,
|
||||
notificationManager = notificationManager
|
||||
).apply {
|
||||
createChannelGroupIfNeeded()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
|
@ -92,4 +73,6 @@ interface PushContentDependencies : ComponentDependencies {
|
|||
fun context(): Context
|
||||
@Named(DEFAULT_APP_COROUTINE_SCOPE) fun scope(): CoroutineScope
|
||||
fun dispatchers(): AppCoroutineDispatchers
|
||||
fun provider(): StringResourceProvider
|
||||
fun notificationBuilder(): NotificationBuilder
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package com.anytypeio.anytype.di.main
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.anytypeio.anytype.app.AnytypeNotificationService
|
||||
|
@ -7,10 +8,13 @@ import com.anytypeio.anytype.data.auth.event.NotificationsDateChannel
|
|||
import com.anytypeio.anytype.data.auth.event.NotificationsRemoteChannel
|
||||
import com.anytypeio.anytype.data.auth.event.PushKeyDataChannel
|
||||
import com.anytypeio.anytype.data.auth.event.PushKeyRemoteChannel
|
||||
import com.anytypeio.anytype.device.NotificationBuilderImpl
|
||||
import com.anytypeio.anytype.domain.account.AwaitAccountStartManager
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.chats.PushKeyChannel
|
||||
import com.anytypeio.anytype.domain.notifications.NotificationBuilder
|
||||
import com.anytypeio.anytype.domain.notifications.SystemNotificationService
|
||||
import com.anytypeio.anytype.domain.resources.StringResourceProvider
|
||||
import com.anytypeio.anytype.domain.workspace.NotificationsChannel
|
||||
import com.anytypeio.anytype.middleware.EventProxy
|
||||
import com.anytypeio.anytype.middleware.interactor.EventHandlerChannel
|
||||
|
@ -128,4 +132,24 @@ object NotificationsModule {
|
|||
context = context
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNotificationBuilder(
|
||||
context: Context,
|
||||
notificationManager: NotificationManager,
|
||||
stringResourceProvider: StringResourceProvider
|
||||
): NotificationBuilder = NotificationBuilderImpl(
|
||||
context = context,
|
||||
notificationManager = notificationManager,
|
||||
resourceProvider = stringResourceProvider
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNotificationManager(
|
||||
context: Context
|
||||
): NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
}
|
|
@ -321,6 +321,11 @@ class ChatFragment : BaseComposeFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
vm.onResume()
|
||||
}
|
||||
|
||||
// DI
|
||||
|
||||
override fun injectDependencies() {
|
||||
|
|
|
@ -728,7 +728,6 @@ class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Pr
|
|||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
NotificationManagerCompat.from(this).cancelAll()
|
||||
mdnsProvider.start()
|
||||
navigator.bind(findNavController(R.id.fragment))
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.anytypeio.anytype.device
|
|||
import android.os.Build
|
||||
import android.util.Base64
|
||||
import com.anytypeio.anytype.core_models.DecryptedPushContent
|
||||
import com.anytypeio.anytype.domain.notifications.NotificationBuilder
|
||||
import com.anytypeio.anytype.presentation.notifications.DecryptionPushContentService
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
|
|
|
@ -0,0 +1,220 @@
|
|||
package com.anytypeio.anytype.device
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.service.notification.StatusBarNotification
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.anytypeio.anytype.core_models.DecryptedPushContent
|
||||
import com.anytypeio.anytype.domain.resources.StringResourceProvider
|
||||
import kotlin.test.Test
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mockito.clearInvocations
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.argThat
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.never
|
||||
import org.mockito.kotlin.verify
|
||||
import org.mockito.kotlin.whenever
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [Build.VERSION_CODES.P]) // API 28
|
||||
class NotificationBuilderTest {
|
||||
|
||||
private val context: Context = ApplicationProvider.getApplicationContext()
|
||||
lateinit var notificationManager: NotificationManager
|
||||
lateinit var stringResourceProvider: StringResourceProvider
|
||||
private lateinit var builder: NotificationBuilderImpl
|
||||
private val testSpaceId = "space123"
|
||||
private val testChatId = "chat456"
|
||||
|
||||
// A simple stub for DecryptedPushContent.Message
|
||||
private val message = DecryptedPushContent.Message(
|
||||
chatId = testChatId,
|
||||
senderName = "Alice",
|
||||
spaceName = "My Space",
|
||||
msgId = "msg789",
|
||||
text = "Hello, this is a test message.",
|
||||
hasAttachments = false
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
stringResourceProvider = mock<StringResourceProvider> {
|
||||
on { getAttachmentText() } doReturn "[attachment]"
|
||||
}
|
||||
notificationManager = mock()
|
||||
builder = NotificationBuilderImpl(context, notificationManager, stringResourceProvider)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
clearInvocations(notificationManager)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `buildAndNotify should create channel and post notification`() {
|
||||
// When
|
||||
builder.buildAndNotify(message, testSpaceId)
|
||||
|
||||
// Then: a channel should be created with correct id and name
|
||||
verify(notificationManager).createNotificationChannel(argThat {
|
||||
id == "${testSpaceId}_${testChatId}" && name == "My Space"
|
||||
})
|
||||
// And a notification should be posted
|
||||
verify(notificationManager).notify(any(), any<android.app.Notification>())
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [Build.VERSION_CODES.O]) // API 26+ for channels
|
||||
fun `clearNotificationChannel should cancel active and delete channel`() {
|
||||
val channelId = "${testSpaceId}_${testChatId}"
|
||||
// Prepare two mock StatusBarNotifications
|
||||
val notif1: android.app.Notification = mock()
|
||||
val notif2: android.app.Notification = mock()
|
||||
|
||||
whenever(notif1.channelId).thenReturn(channelId)
|
||||
whenever(notif2.channelId).thenReturn("other")
|
||||
|
||||
// Wrap them in StatusBarNotification mocks
|
||||
val sbn1: StatusBarNotification = mock()
|
||||
val sbn2: StatusBarNotification = mock()
|
||||
|
||||
whenever(sbn1.notification).thenReturn(notif1)
|
||||
whenever(sbn1.id).thenReturn(1)
|
||||
whenever(sbn2.notification).thenReturn(notif2)
|
||||
whenever(sbn2.id).thenReturn(2)
|
||||
|
||||
whenever(notificationManager.activeNotifications).thenReturn(arrayOf(sbn1, sbn2))
|
||||
|
||||
// Ensure channel exists
|
||||
builder.buildAndNotify(message, testSpaceId)
|
||||
|
||||
// When
|
||||
builder.clearNotificationChannel(testSpaceId, testChatId)
|
||||
|
||||
// Then active notifications for this channel are cancelled
|
||||
verify(notificationManager).cancel(sbn1.id)
|
||||
// Other channels remain
|
||||
verify(notificationManager, never()).cancel(sbn2.id)
|
||||
// And channel is deleted
|
||||
verify(notificationManager).deleteNotificationChannel(channelId)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [Build.VERSION_CODES.O]) // API 26+ for channels
|
||||
fun `clearNotificationChannel with multiple chats in same space should only clear specified chat`() {
|
||||
// Prepare three mock notifications for different chats in the same space
|
||||
val chat1 = "chat1"
|
||||
val chat2 = "chat2"
|
||||
val chat3 = "chat3"
|
||||
val notif1: Notification = mock {
|
||||
on { channelId } doReturn "${testSpaceId}_${chat1}"
|
||||
}
|
||||
val notif2: Notification = mock {
|
||||
on { channelId } doReturn "${testSpaceId}_${chat2}"
|
||||
}
|
||||
val notif3: Notification = mock {
|
||||
on { channelId } doReturn "${testSpaceId}_${chat3}"
|
||||
}
|
||||
val sbn1: StatusBarNotification = mock {
|
||||
on { notification } doReturn notif1
|
||||
on { id } doReturn 10
|
||||
}
|
||||
val sbn2: StatusBarNotification = mock {
|
||||
on { notification } doReturn notif2
|
||||
on { id } doReturn 20
|
||||
}
|
||||
val sbn3: StatusBarNotification = mock {
|
||||
on { notification } doReturn notif3
|
||||
on { id } doReturn 30
|
||||
}
|
||||
whenever(notificationManager.activeNotifications).thenReturn(arrayOf(sbn1, sbn2, sbn3))
|
||||
|
||||
// Ensure channels exist by sending a dummy notification for each chat
|
||||
builder.buildAndNotify(message.copy(chatId = chat1), testSpaceId)
|
||||
builder.buildAndNotify(message.copy(chatId = chat2), testSpaceId)
|
||||
builder.buildAndNotify(message.copy(chatId = chat3), testSpaceId)
|
||||
|
||||
// Clear only chat2
|
||||
builder.clearNotificationChannel(testSpaceId, chat2)
|
||||
|
||||
// Verify only notifications for chat2 were cancelled
|
||||
verify(notificationManager, never()).cancel(10)
|
||||
verify(notificationManager).cancel(20)
|
||||
verify(notificationManager, never()).cancel(30)
|
||||
// Verify only the specified channel was deleted
|
||||
verify(notificationManager).deleteNotificationChannel("${testSpaceId}_${chat2}")
|
||||
verify(notificationManager, never()).deleteNotificationChannel("${testSpaceId}_${chat1}")
|
||||
verify(notificationManager, never()).deleteNotificationChannel("${testSpaceId}_${chat3}")
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [Build.VERSION_CODES.O]) // API 26+ for channels
|
||||
fun `clearNotificationChannel with multiple spaces and chats should only clear specified chat`() {
|
||||
// Prepare notifications for different spaces and chats
|
||||
val space1 = "space1"
|
||||
val space2 = "space2"
|
||||
val chat1 = "chat1"
|
||||
val chat2 = "chat2"
|
||||
|
||||
val notif1: Notification = mock {
|
||||
on { channelId } doReturn "${space1}_${chat1}"
|
||||
}
|
||||
val notif2: Notification = mock {
|
||||
on { channelId } doReturn "${space1}_${chat2}"
|
||||
}
|
||||
val notif3: Notification = mock {
|
||||
on { channelId } doReturn "${space2}_${chat1}"
|
||||
}
|
||||
val notif4: Notification = mock {
|
||||
on { channelId } doReturn "${space2}_${chat2}"
|
||||
}
|
||||
|
||||
val sbn1: StatusBarNotification = mock {
|
||||
on { notification } doReturn notif1
|
||||
on { id } doReturn 10
|
||||
}
|
||||
val sbn2: StatusBarNotification = mock {
|
||||
on { notification } doReturn notif2
|
||||
on { id } doReturn 20
|
||||
}
|
||||
val sbn3: StatusBarNotification = mock {
|
||||
on { notification } doReturn notif3
|
||||
on { id } doReturn 30
|
||||
}
|
||||
val sbn4: StatusBarNotification = mock {
|
||||
on { notification } doReturn notif4
|
||||
on { id } doReturn 40
|
||||
}
|
||||
|
||||
whenever(notificationManager.activeNotifications).thenReturn(arrayOf(sbn1, sbn2, sbn3, sbn4))
|
||||
|
||||
// Ensure channels exist by sending a dummy notification for each
|
||||
builder.buildAndNotify(message.copy(chatId = chat1), space1)
|
||||
builder.buildAndNotify(message.copy(chatId = chat2), space1)
|
||||
builder.buildAndNotify(message.copy(chatId = chat1), space2)
|
||||
builder.buildAndNotify(message.copy(chatId = chat2), space2)
|
||||
|
||||
// Clear only space1_chat2
|
||||
builder.clearNotificationChannel(space1, chat2)
|
||||
|
||||
// Verify only notifications for space1_chat2 were cancelled
|
||||
verify(notificationManager, never()).cancel(10)
|
||||
verify(notificationManager).cancel(20)
|
||||
verify(notificationManager, never()).cancel(30)
|
||||
verify(notificationManager, never()).cancel(40)
|
||||
|
||||
// Verify only the specified channel was deleted
|
||||
verify(notificationManager).deleteNotificationChannel("${space1}_${chat2}")
|
||||
verify(notificationManager, never()).deleteNotificationChannel("${space1}_${chat1}")
|
||||
verify(notificationManager, never()).deleteNotificationChannel("${space2}_${chat1}")
|
||||
verify(notificationManager, never()).deleteNotificationChannel("${space2}_${chat2}")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.anytypeio.anytype.domain.notifications
|
||||
|
||||
import com.anytypeio.anytype.core_models.DecryptedPushContent
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
|
||||
interface NotificationBuilder {
|
||||
fun buildAndNotify(message: DecryptedPushContent.Message, spaceId: Id)
|
||||
fun clearNotificationChannel(spaceId: String, chatId: String)
|
||||
}
|
|
@ -10,4 +10,5 @@ interface StringResourceProvider {
|
|||
fun getSetOfObjectsTitle(): String
|
||||
fun getPropertiesFormatPrettyString(format: RelationFormat): String
|
||||
fun getDefaultSpaceName(): String
|
||||
fun getAttachmentText(): String
|
||||
}
|
|
@ -33,6 +33,7 @@ import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionCon
|
|||
import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer.Store
|
||||
import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer
|
||||
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
|
||||
import com.anytypeio.anytype.domain.notifications.NotificationBuilder
|
||||
import com.anytypeio.anytype.domain.objects.CreateObjectFromUrl
|
||||
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
|
||||
import com.anytypeio.anytype.domain.objects.getTypeOfObject
|
||||
|
@ -85,7 +86,8 @@ class ChatViewModel @Inject constructor(
|
|||
private val getLinkPreview: GetLinkPreview,
|
||||
private val createObjectFromUrl: CreateObjectFromUrl,
|
||||
private val notificationPermissionManager: NotificationPermissionManager,
|
||||
private val spacePermissionProvider: UserPermissionProvider
|
||||
private val spacePermissionProvider: UserPermissionProvider,
|
||||
private val notificationBuilder: NotificationBuilder
|
||||
) : BaseViewModel(), ExitToVaultDelegate by exitToVaultDelegate {
|
||||
|
||||
private val visibleRangeUpdates = MutableSharedFlow<Pair<Id, Id>>(
|
||||
|
@ -168,6 +170,13 @@ class ChatViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun onResume() {
|
||||
notificationBuilder.clearNotificationChannel(
|
||||
spaceId = vmParams.space.id,
|
||||
chatId = vmParams.ctx
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun proceedWithObservingChatMessages(
|
||||
account: Id,
|
||||
chat: Id
|
||||
|
|
|
@ -15,8 +15,7 @@ import com.anytypeio.anytype.domain.misc.UrlBuilder
|
|||
import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer
|
||||
import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer
|
||||
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
|
||||
import com.anytypeio.anytype.domain.`object`.OpenObject
|
||||
import com.anytypeio.anytype.domain.`object`.SetObjectDetails
|
||||
import com.anytypeio.anytype.domain.notifications.NotificationBuilder
|
||||
import com.anytypeio.anytype.domain.objects.CreateObjectFromUrl
|
||||
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
|
||||
import com.anytypeio.anytype.presentation.notifications.NotificationPermissionManager
|
||||
|
@ -43,19 +42,20 @@ class ChatViewModelFactory @Inject constructor(
|
|||
private val getLinkPreview: GetLinkPreview,
|
||||
private val createObjectFromUrl: CreateObjectFromUrl,
|
||||
private val notificationPermissionManager: NotificationPermissionManager,
|
||||
private val spacePermissionProvider: UserPermissionProvider
|
||||
private val spacePermissionProvider: UserPermissionProvider,
|
||||
private val notificationBuilder: NotificationBuilder
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = ChatViewModel(
|
||||
vmParams = params,
|
||||
chatContainer = chatContainer,
|
||||
addChatMessage = addChatMessage,
|
||||
editChatMessage = editChatMessage,
|
||||
deleteChatMessage = deleteChatMessage,
|
||||
toggleChatMessageReaction = toggleChatMessageReaction,
|
||||
members = members,
|
||||
getAccount = getAccount,
|
||||
deleteChatMessage = deleteChatMessage,
|
||||
urlBuilder = urlBuilder,
|
||||
editChatMessage = editChatMessage,
|
||||
spaceViews = spaceViews,
|
||||
dispatchers = dispatchers,
|
||||
uploadFile = uploadFile,
|
||||
|
@ -65,6 +65,7 @@ class ChatViewModelFactory @Inject constructor(
|
|||
getLinkPreview = getLinkPreview,
|
||||
createObjectFromUrl = createObjectFromUrl,
|
||||
notificationPermissionManager = notificationPermissionManager,
|
||||
spacePermissionProvider = spacePermissionProvider
|
||||
spacePermissionProvider = spacePermissionProvider,
|
||||
notificationBuilder = notificationBuilder
|
||||
) as T
|
||||
}
|
|
@ -55,4 +55,8 @@ class StringResourceProviderImpl @Inject constructor(private val context: Contex
|
|||
override fun getDefaultSpaceName(): String {
|
||||
return context.getString(R.string.onboarding_my_first_space)
|
||||
}
|
||||
|
||||
override fun getAttachmentText(): String {
|
||||
return context.getString(R.string.attachment)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue