From 08902e61cfefaa8f4897b1ef8fd8a349d3dfbb08 Mon Sep 17 00:00:00 2001 From: Konstantin Ivanov <54908981+konstantiniiv@users.noreply.github.com> Date: Thu, 29 May 2025 11:11:24 +0200 Subject: [PATCH] DROID-3707 Notifications | Clear push notifications for opened chat (#2475) --- ...nBuilder.kt => NotificationBuilderImpl.kt} | 95 ++++---- .../anytype/device/PushMessageProcessor.kt | 1 + .../anytype/di/feature/chats/ChatsDI.kt | 2 + .../di/feature/notifications/PushDI.kt | 25 +- .../anytype/di/main/NotificationsModule.kt | 24 ++ .../anytype/ui/chats/ChatFragment.kt | 5 + .../anytypeio/anytype/ui/main/MainActivity.kt | 1 - .../device/DefaultPushMessageProcessorTest.kt | 1 + .../anytype/device/NotificationBuilderTest.kt | 220 ++++++++++++++++++ .../notifications/NotificationBuilder.kt | 9 + .../resources/StringResourceProvider.kt | 1 + .../presentation/ChatViewModel.kt | 11 +- .../presentation/ChatViewModelFactory.kt | 13 +- ...hKeyProviderImpl.kt => PushKeyProvider.kt} | 0 .../util/StringResourceProviderImpl.kt | 4 + 15 files changed, 343 insertions(+), 69 deletions(-) rename app/src/main/java/com/anytypeio/anytype/device/{NotificationBuilder.kt => NotificationBuilderImpl.kt} (64%) create mode 100644 app/src/test/java/com/anytypeio/anytype/device/NotificationBuilderTest.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/notifications/NotificationBuilder.kt rename presentation/src/main/java/com/anytypeio/anytype/presentation/notifications/{PushKeyProviderImpl.kt => PushKeyProvider.kt} (100%) diff --git a/app/src/main/java/com/anytypeio/anytype/device/NotificationBuilder.kt b/app/src/main/java/com/anytypeio/anytype/device/NotificationBuilderImpl.kt similarity index 64% rename from app/src/main/java/com/anytypeio/anytype/device/NotificationBuilder.kt rename to app/src/main/java/com/anytypeio/anytype/device/NotificationBuilderImpl.kt index 669b231140..21a5da9979 100644 --- a/app/src/main/java/com/anytypeio/anytype/device/NotificationBuilder.kt +++ b/app/src/main/java/com/anytypeio/anytype/device/NotificationBuilderImpl.kt @@ -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() - 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: " 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 _-]"), "_") } diff --git a/app/src/main/java/com/anytypeio/anytype/device/PushMessageProcessor.kt b/app/src/main/java/com/anytypeio/anytype/device/PushMessageProcessor.kt index e688dec518..cec9077d72 100644 --- a/app/src/main/java/com/anytypeio/anytype/device/PushMessageProcessor.kt +++ b/app/src/main/java/com/anytypeio/anytype/device/PushMessageProcessor.kt @@ -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 { diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/chats/ChatsDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/chats/ChatsDI.kt index af96907586..0d93614809 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/chats/ChatsDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/chats/ChatsDI.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/notifications/PushDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/notifications/PushDI.kt index 09371b98b6..b9c525ba9f 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/notifications/PushDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/notifications/PushDI.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/main/NotificationsModule.kt b/app/src/main/java/com/anytypeio/anytype/di/main/NotificationsModule.kt index b963f5c73f..cd547ace04 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/main/NotificationsModule.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/main/NotificationsModule.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/chats/ChatFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/chats/ChatFragment.kt index 031efe126c..7870862764 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/chats/ChatFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/chats/ChatFragment.kt @@ -321,6 +321,11 @@ class ChatFragment : BaseComposeFragment() { } } + override fun onResume() { + super.onResume() + vm.onResume() + } + // DI override fun injectDependencies() { diff --git a/app/src/main/java/com/anytypeio/anytype/ui/main/MainActivity.kt b/app/src/main/java/com/anytypeio/anytype/ui/main/MainActivity.kt index ca0f20002c..df35b12e83 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/main/MainActivity.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/main/MainActivity.kt @@ -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)) } diff --git a/app/src/test/java/com/anytypeio/anytype/device/DefaultPushMessageProcessorTest.kt b/app/src/test/java/com/anytypeio/anytype/device/DefaultPushMessageProcessorTest.kt index 426073af52..3dd1099dbd 100644 --- a/app/src/test/java/com/anytypeio/anytype/device/DefaultPushMessageProcessorTest.kt +++ b/app/src/test/java/com/anytypeio/anytype/device/DefaultPushMessageProcessorTest.kt @@ -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 diff --git a/app/src/test/java/com/anytypeio/anytype/device/NotificationBuilderTest.kt b/app/src/test/java/com/anytypeio/anytype/device/NotificationBuilderTest.kt new file mode 100644 index 0000000000..42f6295bef --- /dev/null +++ b/app/src/test/java/com/anytypeio/anytype/device/NotificationBuilderTest.kt @@ -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 { + 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()) + } + + @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}") + } +} diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/notifications/NotificationBuilder.kt b/domain/src/main/java/com/anytypeio/anytype/domain/notifications/NotificationBuilder.kt new file mode 100644 index 0000000000..0de5566c22 --- /dev/null +++ b/domain/src/main/java/com/anytypeio/anytype/domain/notifications/NotificationBuilder.kt @@ -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) +} \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/resources/StringResourceProvider.kt b/domain/src/main/java/com/anytypeio/anytype/domain/resources/StringResourceProvider.kt index de87cb36f7..e5090dc3c8 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/resources/StringResourceProvider.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/resources/StringResourceProvider.kt @@ -10,4 +10,5 @@ interface StringResourceProvider { fun getSetOfObjectsTitle(): String fun getPropertiesFormatPrettyString(format: RelationFormat): String fun getDefaultSpaceName(): String + fun getAttachmentText(): String } \ No newline at end of file diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt index c34fac0f14..cfc65867de 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt @@ -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>( @@ -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 diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModelFactory.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModelFactory.kt index 3e15a1c5d1..bfca3d0f3b 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModelFactory.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModelFactory.kt @@ -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 create(modelClass: Class): 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 } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/notifications/PushKeyProviderImpl.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/notifications/PushKeyProvider.kt similarity index 100% rename from presentation/src/main/java/com/anytypeio/anytype/presentation/notifications/PushKeyProviderImpl.kt rename to presentation/src/main/java/com/anytypeio/anytype/presentation/notifications/PushKeyProvider.kt diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/util/StringResourceProviderImpl.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/util/StringResourceProviderImpl.kt index da1076bc65..a70800028f 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/util/StringResourceProviderImpl.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/util/StringResourceProviderImpl.kt @@ -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) + } } \ No newline at end of file