Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set expiry for dismissed notifications #5186

Merged
merged 22 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ebdce9b
dismiss notification expiry
samgst-amazon Dec 9, 2024
bcfbb27
test
samgst-amazon Dec 10, 2024
a23cd78
Merge branch 'main' into samgst/NotificationDismissedExpire
samgst-amazon Dec 10, 2024
4367447
fix tests
samgst-amazon Dec 10, 2024
824314d
redundant cleanExpired function calls
samgst-amazon Dec 11, 2024
dc61742
converter
samgst-amazon Dec 11, 2024
3d26f16
detekt
samgst-amazon Dec 11, 2024
3dcac05
Merge branch 'main' into samgst/NotificationDismissedExpire
samgst-amazon Dec 11, 2024
97acf1c
Merge branch 'main' into samgst/NotificationDismissedExpire
samgst-amazon Dec 11, 2024
ef6ec85
remove whitespace from conflict
samgst-amazon Dec 11, 2024
ba2638f
detekt
samgst-amazon Dec 12, 2024
9d3e162
Merge branch 'main' into samgst/NotificationDismissedExpire
samgst-amazon Dec 12, 2024
ed92b9f
Merge branch 'main' into samgst/NotificationDismissedExpire
samgst-amazon Jan 6, 2025
2a96250
remove attribute tags, unnecessary clear
samgst-amazon Jan 6, 2025
35d1134
replace attribute tags
samgst-amazon Jan 6, 2025
960f5f1
change properties to var for serialization
samgst-amazon Jan 7, 2025
2475c07
simplify getState
samgst-amazon Jan 7, 2025
5ae5b7f
detekt
samgst-amazon Jan 7, 2025
1c244db
Merge branch 'main' into samgst/NotificationDismissedExpire
samgst-amazon Jan 7, 2025
66d27e6
Merge branch 'main' into samgst/NotificationDismissedExpire
samgst-amazon Jan 7, 2025
cb7d9f9
reduntant test
samgst-amazon Jan 8, 2025
3afef56
Merge branch 'main' into samgst/NotificationDismissedExpire
samgst-amazon Jan 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,54 @@
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.service
import software.aws.toolkits.core.utils.ETagProvider
import java.time.Duration
import java.time.Instant

data class DismissedNotification(
var id: String = "",
var dismissedAt: String = Instant.now().toEpochMilli().toString(),
)

data class NotificationDismissalConfiguration(
var dismissedNotifications: MutableSet<DismissedNotification> = mutableSetOf(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var dismissedNotifications: MutableSet<DismissedNotification> = mutableSetOf(),
val dismissedNotifications: MutableSet<DismissedNotification> = mutableSetOf(),

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These properties need to be var for the serialization to work without the property/attribute tags

)

@Service
@State(name = "notificationDismissals", storages = [Storage("aws.xml")])
class NotificationDismissalState : PersistentStateComponent<NotificationDismissalConfiguration> {
private val state = NotificationDismissalConfiguration()
private var state = NotificationDismissalConfiguration()
private val retentionPeriod = Duration.ofDays(60) // 2 months

override fun getState(): NotificationDismissalConfiguration = state

override fun loadState(state: NotificationDismissalConfiguration) {
this.state.dismissedNotificationIds.clear()
this.state.dismissedNotificationIds.addAll(state.dismissedNotificationIds)
this.state = state
cleanExpiredNotifications()
}

fun isDismissed(notificationId: String): Boolean =
state.dismissedNotificationIds.contains(notificationId)
state.dismissedNotifications.any { it.id == notificationId }

fun dismissNotification(notificationId: String) {
state.dismissedNotificationIds.add(notificationId)
state.dismissedNotifications.add(
DismissedNotification(
id = notificationId
)
)
}

private fun cleanExpiredNotifications() {
val now = Instant.now()
state.dismissedNotifications.removeAll { notification ->
Duration.between(Instant.ofEpochMilli(notification.dismissedAt.toLong()), now) > retentionPeriod
}
}

companion object {
fun getInstance(): NotificationDismissalState =
service()
fun getInstance(): NotificationDismissalState = service()

Check warning on line 56 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationStateUtils.kt

View check run for this annotation

Codecov / codecov/patch

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationStateUtils.kt#L56

Added line #L56 was not covered by tests
}
}

data class NotificationDismissalConfiguration(
var dismissedNotificationIds: MutableSet<String> = mutableSetOf(),
)

@Service
@State(name = "notificationEtag", storages = [Storage("aws.xml")])
class NotificationEtagState : PersistentStateComponent<NotificationEtagConfiguration>, ETagProvider {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.core.notifications

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.time.Instant
import java.time.temporal.ChronoUnit

class NotificationDismissalStateTest {
private lateinit var state: NotificationDismissalState

@BeforeEach
fun setUp() {
state = NotificationDismissalState()
}

@Test
fun `notifications less than 2 months old are not removed`() {
val recentNotification = DismissedNotification(
id = "recent-notification",
dismissedAt = Instant.now().minus(30, ChronoUnit.DAYS).toEpochMilli().toString()
)

state.loadState(NotificationDismissalConfiguration(mutableSetOf(recentNotification)))

val persistedState = state.getState()

assertEquals(1, persistedState.dismissedNotifications.size)
assertTrue(persistedState.dismissedNotifications.any { it.id == "recent-notification" })
assertTrue(state.isDismissed("recent-notification"))
}

@Test
fun `notifications older than 2 months are removed`() {
val oldNotification = DismissedNotification(
id = "old-notification",
dismissedAt = Instant.now().minus(61, ChronoUnit.DAYS).toEpochMilli().toString()
)

state.loadState(NotificationDismissalConfiguration(mutableSetOf(oldNotification)))

val persistedState = state.getState()

assertEquals(0, persistedState.dismissedNotifications.size)
assertFalse(state.isDismissed("old-notification"))
}

@Test
fun `mixed age notifications are handled correctly`() {
val recentNotification = DismissedNotification(
id = "recent-notification",
dismissedAt = Instant.now().minus(30, ChronoUnit.DAYS).toEpochMilli().toString()
)
val oldNotification = DismissedNotification(
id = "old-notification",
dismissedAt = Instant.now().minus(61, ChronoUnit.DAYS).toEpochMilli().toString()
)

state.loadState(
NotificationDismissalConfiguration(
mutableSetOf(recentNotification, oldNotification)
)
)

val persistedState = state.getState()

assertEquals(1, persistedState.dismissedNotifications.size)
assertTrue(state.isDismissed("recent-notification"))
assertFalse(state.isDismissed("old-notification"))
}

@Test
fun `dismissing new notification retains it`() {
state.dismissNotification("new-notification")

assertTrue(state.isDismissed("new-notification"))
}
}
Loading