Skip to content

Commit 26caf31

Browse files
Notification dismissal state tracking (#5129)
* split notifications into separated lists. * add persistent notification dismissal state logic * boolean changes * group persistant states * comments * Service initialized automatically * isStartup global * Deserialized notification schedule type * tests * persistent state syntax * convert to light services * Remove state from companion object * detekt
1 parent 84ed1ef commit 26caf31

File tree

10 files changed

+306
-45
lines changed

10 files changed

+306
-45
lines changed

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationCustomDeserializers.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ class NotConditionDeserializer : JsonDeserializer<NotificationExpression.NotCond
114114
}
115115
}
116116

117+
// Create a custom deserializer if needed
118+
class NotificationTypeDeserializer : JsonDeserializer<NotificationScheduleType>() {
119+
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): NotificationScheduleType =
120+
NotificationScheduleType.fromString(p.valueAsString)
121+
}
122+
117123
private fun JsonNode.toNotificationExpressions(p: JsonParser): List<NotificationExpression> = this.map { element ->
118124
val parser = element.traverse(p.codec)
119125
parser.nextToken()

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationFormatUtils.kt

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,32 @@ data class NotificationData(
2727
)
2828

2929
data class NotificationSchedule(
30-
val type: String,
31-
)
30+
@JsonDeserialize(using = NotificationTypeDeserializer::class)
31+
val type: NotificationScheduleType,
32+
) {
33+
constructor(type: String) : this(NotificationScheduleType.fromString(type))
34+
}
3235

3336
enum class NotificationSeverity {
3437
INFO,
3538
WARNING,
3639
CRITICAL,
3740
}
3841

42+
enum class NotificationScheduleType {
43+
STARTUP,
44+
EMERGENCY,
45+
;
46+
47+
companion object {
48+
fun fromString(value: String): NotificationScheduleType =
49+
when (value.lowercase()) {
50+
"startup" -> STARTUP
51+
else -> EMERGENCY
52+
}
53+
}
54+
}
55+
3956
data class NotificationContentDescriptionLocale(
4057
@JsonProperty("en-US")
4158
val locale: NotificationContentDescription,

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPanel.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ class NotificationPanel : BorderLayoutPanel() {
1515
init {
1616
isOpaque = false
1717
addToCenter(wrapper)
18-
ProcessNotificationsBase.showBannerNotification.forEach {
19-
updateNotificationPanel(it.value)
18+
BannerNotificationService.getInstance().getNotifications().forEach { (_, content) ->
19+
updateNotificationPanel(content)
2020
}
2121
}
2222

2323
private fun removeNotificationPanel(notificationId: String) = runInEdt {
24-
ProcessNotificationsBase.showBannerNotification.remove(notificationId) // TODO: add id to dismissed notification list
24+
BannerNotificationService.getInstance().removeNotification(notificationId)
25+
NotificationDismissalState.getInstance().dismissNotification(notificationId)
2526
wrapper.removeAll()
2627
}
2728

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@ package software.aws.toolkits.jetbrains.core.notifications
66
import com.fasterxml.jackson.module.kotlin.readValue
77
import com.intellij.openapi.Disposable
88
import com.intellij.openapi.application.ApplicationManager
9-
import com.intellij.openapi.components.PersistentStateComponent
109
import com.intellij.openapi.components.Service
11-
import com.intellij.openapi.components.State
12-
import com.intellij.openapi.components.Storage
1310
import com.intellij.util.Alarm
1411
import com.intellij.util.AlarmFactory
1512
import com.intellij.util.io.HttpRequests
@@ -60,32 +57,6 @@ object NotificationEndpoint {
6057
private const val DEFAULT_ENDPOINT = "" // TODO: Replace with actual endpoint
6158
}
6259

63-
@State(name = "notificationEtag", storages = [Storage("aws.xml")])
64-
class NotificationEtagState : PersistentStateComponent<NotificationEtagConfiguration> {
65-
private var state = NotificationEtagConfiguration()
66-
67-
override fun getState(): NotificationEtagConfiguration = state
68-
69-
override fun loadState(state: NotificationEtagConfiguration) {
70-
this.state = state
71-
}
72-
73-
var etag: String?
74-
get() = state.etag
75-
set(value) {
76-
state.etag = value
77-
}
78-
79-
companion object {
80-
fun getInstance(): NotificationEtagState =
81-
ApplicationManager.getApplication().getService(NotificationEtagState::class.java)
82-
}
83-
}
84-
85-
data class NotificationEtagConfiguration(
86-
var etag: String? = null,
87-
)
88-
8960
@Service(Service.Level.APP)
9061
internal final class NotificationPollingService : Disposable {
9162
private val isFirstPoll = AtomicBoolean(true)

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationServiceInitializer.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ internal class NotificationServiceInitializer : ProjectActivity {
1414
override suspend fun execute(project: Project) {
1515
if (initialized.compareAndSet(false, true)) {
1616
val service = NotificationPollingService.getInstance()
17-
ProcessNotificationsBase()
1817
service.startPolling()
1918
}
2019
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.core.notifications
5+
6+
import com.intellij.openapi.components.PersistentStateComponent
7+
import com.intellij.openapi.components.RoamingType
8+
import com.intellij.openapi.components.Service
9+
import com.intellij.openapi.components.State
10+
import com.intellij.openapi.components.Storage
11+
import com.intellij.openapi.components.service
12+
13+
@Service
14+
@State(name = "notificationDismissals", storages = [Storage("aws.xml", roamingType = RoamingType.DISABLED)])
15+
class NotificationDismissalState : PersistentStateComponent<NotificationDismissalConfiguration> {
16+
private val state = NotificationDismissalConfiguration()
17+
18+
override fun getState(): NotificationDismissalConfiguration = state
19+
20+
override fun loadState(state: NotificationDismissalConfiguration) {
21+
this.state.dismissedNotificationIds.clear()
22+
this.state.dismissedNotificationIds.addAll(state.dismissedNotificationIds)
23+
}
24+
25+
fun isDismissed(notificationId: String): Boolean =
26+
state.dismissedNotificationIds.contains(notificationId)
27+
28+
fun dismissNotification(notificationId: String) {
29+
state.dismissedNotificationIds.add(notificationId)
30+
}
31+
32+
companion object {
33+
fun getInstance(): NotificationDismissalState =
34+
service()
35+
}
36+
}
37+
38+
data class NotificationDismissalConfiguration(
39+
var dismissedNotificationIds: MutableSet<String> = mutableSetOf(),
40+
)
41+
42+
@Service
43+
@State(name = "notificationEtag", storages = [Storage("aws.xml", roamingType = RoamingType.DISABLED)])
44+
class NotificationEtagState : PersistentStateComponent<NotificationEtagConfiguration> {
45+
private val state = NotificationEtagConfiguration()
46+
47+
override fun getState(): NotificationEtagConfiguration = state
48+
49+
override fun loadState(state: NotificationEtagConfiguration) {
50+
this.state.etag = state.etag
51+
}
52+
53+
var etag: String?
54+
get() = state.etag
55+
set(value) {
56+
state.etag = value
57+
}
58+
59+
companion object {
60+
fun getInstance(): NotificationEtagState =
61+
service()
62+
}
63+
}
64+
65+
data class NotificationEtagConfiguration(
66+
var etag: String? = null,
67+
)
68+
69+
@Service
70+
class BannerNotificationService {
71+
private val notifications = mutableMapOf<String, BannerContent>()
72+
73+
fun addNotification(id: String, content: BannerContent) {
74+
notifications[id] = content
75+
}
76+
77+
fun getNotifications(): Map<String, BannerContent> = notifications
78+
79+
fun removeNotification(id: String) {
80+
notifications.remove(id)
81+
}
82+
83+
companion object {
84+
fun getInstance(): BannerNotificationService =
85+
service()
86+
}
87+
}

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/ProcessNotificationsBase.kt

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package software.aws.toolkits.jetbrains.core.notifications
55

66
import com.fasterxml.jackson.databind.DeserializationFeature
7+
import com.fasterxml.jackson.databind.ObjectMapper
78
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
89
import com.fasterxml.jackson.module.kotlin.readValue
910
import com.intellij.notification.NotificationType
@@ -15,13 +16,17 @@ import com.intellij.openapi.project.Project
1516
import software.aws.toolkits.core.utils.inputStream
1617
import software.aws.toolkits.jetbrains.utils.notifyStickyWithData
1718
import java.nio.file.Paths
19+
import java.util.concurrent.atomic.AtomicBoolean
1820

1921
object NotificationMapperUtil {
20-
val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
22+
val mapper: ObjectMapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
2123
}
24+
private var isStartup: AtomicBoolean = AtomicBoolean(true)
2225

2326
@Service(Service.Level.PROJECT)
24-
class ProcessNotificationsBase {
27+
class ProcessNotificationsBase(
28+
private val project: Project,
29+
) {
2530
private val notifListener = mutableListOf<NotifListener>()
2631
init {
2732
NotificationPollingService.getInstance().addObserver {
@@ -39,8 +44,23 @@ class ProcessNotificationsBase {
3944
}
4045

4146
fun retrieveStartupAndEmergencyNotifications() {
42-
// TODO: separates notifications into startup and emergency
43-
// iterates through the 2 lists and processes each notification(if it isn't dismissed)
47+
val isStartupPoll = isStartup.compareAndSet(true, false)
48+
val notifications = getNotificationsFromFile()
49+
notifications?.let { notificationsList ->
50+
val activeNotifications = notificationsList.notifications
51+
?.filter { notification ->
52+
// Keep notification if:
53+
// - it's not a startup notification, OR
54+
// - it is a startup notification AND this is the first poll
55+
notification.schedule.type != NotificationScheduleType.STARTUP || isStartupPoll
56+
}
57+
?.filter { notification ->
58+
!NotificationDismissalState.getInstance().isDismissed(notification.id)
59+
}
60+
.orEmpty()
61+
62+
activeNotifications.forEach { processNotification(project, it) }
63+
}
4464
}
4565

4666
fun processNotification(project: Project, notificationData: NotificationData) {
@@ -63,7 +83,7 @@ class ProcessNotificationsBase {
6383
)
6484
if (severity == "Critical") {
6585
val bannerContent = BannerContent(notificationContent.title, notificationContent.description, followupActions, notificationData.id)
66-
showBannerNotification[notificationData.id] = bannerContent
86+
BannerNotificationService.getInstance().addNotification(notificationData.id, bannerContent)
6787
notifyListenerForNotification(bannerContent)
6888
}
6989
}
@@ -87,7 +107,6 @@ class ProcessNotificationsBase {
87107
companion object {
88108
fun getInstance(project: Project): ProcessNotificationsBase = project.service()
89109

90-
val showBannerNotification = mutableMapOf<String, BannerContent>()
91110
private const val NOTIFICATIONS_PATH = "aws-static-resources/notifications.json"
92111
}
93112
}

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/utils/NotificationUtils.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import com.intellij.ui.ScrollPaneFactory
1717
import org.slf4j.LoggerFactory
1818
import software.aws.toolkits.core.utils.warn
1919
import software.aws.toolkits.jetbrains.core.help.HelpIds
20-
import software.aws.toolkits.jetbrains.core.notifications.ProcessNotificationsBase
20+
import software.aws.toolkits.jetbrains.core.notifications.BannerNotificationService
21+
import software.aws.toolkits.jetbrains.core.notifications.NotificationDismissalState
2122
import software.aws.toolkits.resources.AwsCoreBundle
2223
import javax.swing.JLabel
2324
import javax.swing.JTextArea
@@ -66,8 +67,8 @@ fun notifyStickyWithData(
6667
createNotificationExpiringAction(
6768
object : AnAction("Dismiss") {
6869
override fun actionPerformed(e: AnActionEvent) {
69-
ProcessNotificationsBase.showBannerNotification.remove(id)
70-
// TODO: add id to dismissed notification list
70+
BannerNotificationService.getInstance().removeNotification(id)
71+
NotificationDismissalState.getInstance().dismissNotification(id)
7172
}
7273
}
7374
)

0 commit comments

Comments
 (0)