IJ-CR-161866 Decouple action-based and notification-based feedback APIs

According to the code review request, introducing separate
interfaces for configuring feedbacks submitted via in-IDE
notifications and feedbacks submitted via explicit user actions.

There is some code duplication now, as some stuff is common.
If needed, we can extract it into a common base interface later.

GitOrigin-RevId: ce271dd6cdb4b2630035d5e423ce4635eab49d51
This commit is contained in:
Sergei Tachenov
2025-05-09 15:30:47 +00:00
committed by intellij-monorepo-bot
parent 07c333c46e
commit 4f83f58b6e
6 changed files with 198 additions and 90 deletions
@@ -0,0 +1,119 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.feedback
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.registry.Registry
import com.intellij.platform.feedback.dialog.BlockBasedFeedbackDialog
import com.intellij.platform.feedback.dialog.SystemDataJsonSerializable
import com.intellij.platform.feedback.impl.checkIsFeedbackCollectionDeadlineNotPast
import com.intellij.platform.feedback.impl.checkIsIdeEAPIfRequired
import com.intellij.platform.feedback.impl.state.CommonFeedbackSurveyService
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.concurrency.annotations.RequiresEdt
import kotlinx.datetime.LocalDate
/**
* Represents the config of a feedback action that can be explicitly invoked by the user.
*
* Similar to [InIdeFeedbackSurveyConfig], but instead of asking the user to submit feedback,
* this config is intended to be used in cases when feedback is submitted by an explicit user
* action (e.g., an action button, an action link, etc.).
*/
interface ExplicitUserFeedbackConfig {
/**
* Unique identifier reflecting the survey.
*
* If the same feedback can be provided both by an in-IDE notification using [InIdeFeedbackSurveyConfig]
* and by an explicit action, then this survey ID must be equal to that of the in-IDE survey config
* to prevent showing survey notifications if the user has already sent feedback explicitly.
*/
val surveyId: String
/**
* Date of the last day of feedback collection.
*
* Used to not letting the user submit feedback when it's no longer relevant.
*
* Often set to something like the planned date of the next release.
*/
val lastDayOfFeedbackCollection: LocalDate
/**
* Whether the IDE must be of a EAP version.
*/
val requireIdeEAP: Boolean
/**
* Checks whether the IDE is suitable for the feedback survey.
*
* Usually needed when you want to show a survey only to users of a particular IDE.
*/
fun checkIdeIsSuitable(): Boolean
/**
* Checks whether the extra conditions for submitting feedback are satisfied.
*
* Normally it imposes fewer restrictions than [InIdeFeedbackSurveyConfig.checkExtraConditionSatisfied],
* as, for example, there may be a check whether the user has been using a specific feature long enough
* before showing a survey notification.
* But when the user explicitly invoked an action to submit feedback, such checks are not needed.
*
* This check might include, for example, a check that the feature to submit feedback about is currently enabled.
*/
fun checkExtraConditionSatisfied(project: Project): Boolean = true
/**
* Returns a dialog with the feedback survey.
*
* @see BlockBasedFeedbackDialog
*/
fun createFeedbackDialog(project: Project, forTest: Boolean): BlockBasedFeedbackDialog<out SystemDataJsonSerializable>
/**
* Performs additional custom state updates after the feedback dialog is closed by the ok button.
*/
fun updateStateAfterDialogClosedOk(project: Project)
}
/**
* Checks whether it's possible for the user to submit feedback now.
*
* Intended to be used in [com.intellij.openapi.actionSystem.AnAction.update].
*
* @see showFeedbackDialog
*/
@RequiresBackgroundThread
fun ExplicitUserFeedbackConfig.isSuitableToShow(project: Project): Boolean {
val commonConditionsForAllSurveys = if (Registry.`is`("platform.feedback.ignore.common.conditions.for.all.surveys", false)) {
true
}
else {
// Only a subset is checked as compared to com.intellij.platform.feedback.impl.FeedbackSurveyUtilsKt.isSuitableToShow.
// This is because if the user wants to explicitly submit feedback, we shouldn't prevent it just because the feedback is already sent,
// or because a feedback notification was shown several times.
// But we still want feedback submission to be available only for the right IDE, right EAP/release mode
// and don't want to allow outdated feedback.
checkIdeIsSuitable() &&
checkIsFeedbackCollectionDeadlineNotPast(lastDayOfFeedbackCollection) &&
checkIsIdeEAPIfRequired(requireIdeEAP)
}
return commonConditionsForAllSurveys && checkExtraConditionSatisfied(project)
}
/**
* Shows the feedback dialog for the user to submit feedback.
*
* Intended to be used in [com.intellij.openapi.actionSystem.AnAction.actionPerformed].
*
* @see isSuitableToShow
*/
@RequiresEdt
fun ExplicitUserFeedbackConfig.showFeedbackDialog(project: Project, forTest: Boolean) {
val dialog = createFeedbackDialog(project, forTest)
val isOk = dialog.showAndGet()
if (isOk && !forTest) {
updateStateAfterDialogClosedOk(project)
CommonFeedbackSurveyService.feedbackSurveyAnswerSent(surveyId)
}
}
@@ -2,12 +2,8 @@
package com.intellij.platform.feedback
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.registry.Registry
import com.intellij.platform.feedback.impl.bundle.CommonFeedbackBundle
import com.intellij.platform.feedback.impl.checkIsFeedbackCollectionDeadlineNotPast
import com.intellij.platform.feedback.impl.checkIsIdeEAPIfRequired
import com.intellij.platform.feedback.impl.notification.RequestFeedbackNotification
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import kotlinx.datetime.LocalDate
import org.jetbrains.annotations.Nls
@@ -59,18 +55,6 @@ interface FeedbackSurveyConfig {
*/
fun checkExtraConditionSatisfied(project: Project): Boolean
/**
* Checks whether the extra conditions for showing the survey by an explicit user action are satisfied.
*
* The same as [checkExtraConditionSatisfied] but for the case when the user intentionally submits
* feedback through an explicit feature-specific action and not through a regular survey notification.
*
* Normally it imposes less restrictions, as, for example, [checkExtraConditionSatisfied] might check whether
* the user has been using a specific feature long enough before showing a survey notification,
* but when the user explicitly wants to provide feedback already, such conditions are usually irrelevant.
*/
fun checkExtraConditionSatisfiedForExplicitUserAction(project: Project): Boolean = true
/**
* Returns a notification encouraging the user to leave feedback, which will be shown to the user.
*/
@@ -107,29 +91,3 @@ interface FeedbackSurveyConfig {
}
}
/**
* Checks whether it's possible for the user to explicitly submit feedback.
*
* Intended to be used by feature-specific feedback actions,
* where this function is used for [com.intellij.openapi.actionSystem.AnAction.update]
* and [com.intellij.platform.feedback.InIdeFeedbackSurveyConfig.showFeedbackDialog]
* is used for [com.intellij.openapi.actionSystem.AnAction.actionPerformed].
*/
@RequiresBackgroundThread
fun FeedbackSurveyConfig.isSuitableToShowByExplicitUserAction(project: Project): Boolean {
val commonConditionsForAllSurveys = if (Registry.`is`("platform.feedback.ignore.common.conditions.for.all.surveys", false)) {
true
}
else {
// Only a subset is checked as compared to com.intellij.platform.feedback.impl.FeedbackSurveyUtilsKt.isSuitableToShow.
// This is because if the user wants to explicitly submit feedback, we shouldn't prevent it just because the feedback is already sent,
// or because a feedback notification was shown several times.
// But we still want feedback submission to be available only for the right IDE, right EAP/release mode
// and don't want to allow outdated feedback.
checkIdeIsSuitable() &&
checkIsFeedbackCollectionDeadlineNotPast() &&
checkIsIdeEAPIfRequired()
}
return commonConditionsForAllSurveys && checkExtraConditionSatisfiedForExplicitUserAction(project)
}
@@ -4,8 +4,6 @@ package com.intellij.platform.feedback
import com.intellij.openapi.project.Project
import com.intellij.platform.feedback.dialog.BlockBasedFeedbackDialog
import com.intellij.platform.feedback.dialog.SystemDataJsonSerializable
import com.intellij.platform.feedback.impl.updateCommonFeedbackSurveysStateAfterSent
import com.intellij.util.concurrency.annotations.RequiresEdt
/**
* Represents the config for In-IDE feedback survey.
@@ -26,22 +24,3 @@ interface InIdeFeedbackSurveyConfig : FeedbackSurveyConfig {
*/
fun updateStateAfterDialogClosedOk(project: Project)
}
/**
* Shows the feedback dialog for the user to submit feedback.
*
* Normally the dialog is shown when the user clicks a survey notification.
* This function is intended for feature-specific feedback actions,
* where [com.intellij.platform.feedback.FeedbackSurveyConfig.isSuitableToShowByExplicitUserAction]
* is used for [com.intellij.openapi.actionSystem.AnAction.update]
* and this function is used for [com.intellij.openapi.actionSystem.AnAction.actionPerformed].
*/
@RequiresEdt
fun InIdeFeedbackSurveyConfig.showFeedbackDialog(project: Project, forTest: Boolean) {
val dialog = createFeedbackDialog(project, forTest)
val isOk = dialog.showAndGet()
if (isOk && !forTest) {
updateStateAfterDialogClosedOk(project)
updateCommonFeedbackSurveysStateAfterSent(this)
}
}
@@ -11,16 +11,25 @@ import com.intellij.platform.feedback.impl.state.CommonFeedbackSurveyService
import com.intellij.platform.feedback.impl.state.DontShowAgainFeedbackService
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn
internal const val MAX_FEEDBACK_SURVEY_NUMBER_SHOWS: Int = 2
internal fun FeedbackSurveyConfig.checkIsFeedbackCollectionDeadlineNotPast(): Boolean {
private fun FeedbackSurveyConfig.checkIsFeedbackCollectionDeadlineNotPast(): Boolean {
return checkIsFeedbackCollectionDeadlineNotPast(this.lastDayOfFeedbackCollection)
}
internal fun checkIsFeedbackCollectionDeadlineNotPast(lastDayOfFeedbackCollection: LocalDate): Boolean {
return Clock.System.todayIn(TimeZone.currentSystemDefault()) < lastDayOfFeedbackCollection
}
internal fun FeedbackSurveyConfig.checkIsIdeEAPIfRequired(): Boolean {
private fun FeedbackSurveyConfig.checkIsIdeEAPIfRequired(): Boolean {
return checkIsIdeEAPIfRequired(this.requireIdeEAP)
}
internal fun checkIsIdeEAPIfRequired(requireIdeEAP: Boolean): Boolean {
if (requireIdeEAP) {
return ApplicationInfo.getInstance().isEAP
}
@@ -90,7 +99,16 @@ private fun invokeRespondNotificationAction(feedbackSurveyType: FeedbackSurveyTy
}
}
internal fun updateCommonFeedbackSurveysStateAfterSent(feedbackSurveyConfig: FeedbackSurveyConfig) {
private fun InIdeFeedbackSurveyConfig.showFeedbackDialog(project: Project, forTest: Boolean) {
val dialog = createFeedbackDialog(project, forTest)
val isOk = dialog.showAndGet()
if (isOk && !forTest) {
updateStateAfterDialogClosedOk(project)
CommonFeedbackSurveyService.feedbackSurveyAnswerSent(surveyId)
}
}
private fun updateCommonFeedbackSurveysStateAfterSent(feedbackSurveyConfig: FeedbackSurveyConfig) {
CommonFeedbackSurveyService.feedbackSurveyAnswerSent(feedbackSurveyConfig.surveyId)
}
@@ -4,20 +4,20 @@ import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.remoting.ActionRemoteBehaviorSpecification
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.platform.feedback.isSuitableToShowByExplicitUserAction
import com.intellij.platform.feedback.isSuitableToShow
import com.intellij.platform.feedback.showFeedbackDialog
import org.jetbrains.plugins.terminal.block.feedback.ReworkedTerminalSurveyConfig
import org.jetbrains.plugins.terminal.block.feedback.ReworkedTerminalFeedbackActionConfig
internal class TerminalFeedbackAction : DumbAwareAction(), ActionRemoteBehaviorSpecification.Frontend {
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
override fun update(e: AnActionEvent) {
val project = e.project
e.presentation.isEnabledAndVisible = project != null && ReworkedTerminalSurveyConfig.isSuitableToShowByExplicitUserAction(project)
e.presentation.isEnabledAndVisible = project != null && ReworkedTerminalFeedbackActionConfig.isSuitableToShow(project)
}
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
ReworkedTerminalSurveyConfig.showFeedbackDialog(project, forTest = false)
ReworkedTerminalFeedbackActionConfig.showFeedbackDialog(project, forTest = false)
}
}
@@ -5,10 +5,7 @@ import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.platform.feedback.FeedbackSurvey
import com.intellij.platform.feedback.FeedbackSurveyType
import com.intellij.platform.feedback.InIdeFeedbackSurveyConfig
import com.intellij.platform.feedback.InIdeFeedbackSurveyType
import com.intellij.platform.feedback.*
import com.intellij.platform.feedback.dialog.BlockBasedFeedbackDialog
import com.intellij.platform.feedback.dialog.SystemDataJsonSerializable
import com.intellij.platform.feedback.impl.OnDemandFeedbackResolver
@@ -54,24 +51,25 @@ internal fun getFeedbackMoment(project: Project): TerminalFeedbackMoment {
}
internal class ReworkedTerminalFeedbackSurvey : FeedbackSurvey() {
override val feedbackSurveyType: FeedbackSurveyType<*> = InIdeFeedbackSurveyType(ReworkedTerminalSurveyConfig)
override val feedbackSurveyType: FeedbackSurveyType<*> = InIdeFeedbackSurveyType(ReworkedTerminalFeedbackNotificationConfig)
}
@ApiStatus.Internal
object ReworkedTerminalSurveyConfig : InIdeFeedbackSurveyConfig {
override val surveyId: String = "reworked_terminal"
object ReworkedTerminalFeedbackNotificationConfig : InIdeFeedbackSurveyConfig {
override val surveyId: String
get() = ReworkedTerminalSurveyConfig.SURVEY_ID
override fun createFeedbackDialog(project: Project, forTest: Boolean): BlockBasedFeedbackDialog<out SystemDataJsonSerializable> {
return ReworkedTerminalFeedbackDialog(project, forTest)
}
override val lastDayOfFeedbackCollection: LocalDate
get() = ReworkedTerminalSurveyConfig.lastDayOfFeedbackCollection
override fun updateStateAfterDialogClosedOk(project: Project) { }
override val requireIdeEAP: Boolean
get() = ReworkedTerminalSurveyConfig.REQUIRE_EAP
override val lastDayOfFeedbackCollection: LocalDate = LocalDate(2025, 7, 15)
override fun checkIdeIsSuitable(): Boolean =
ReworkedTerminalSurveyConfig.checkIdeIsSuitable()
override val requireIdeEAP: Boolean = false
override fun checkIdeIsSuitable(): Boolean = PlatformUtils.isJetBrainsProduct()
override fun createFeedbackDialog(project: Project, forTest: Boolean): BlockBasedFeedbackDialog<out SystemDataJsonSerializable> =
ReworkedTerminalSurveyConfig.createFeedbackDialog(project, forTest)
override fun checkExtraConditionSatisfied(project: Project): Boolean {
val usageStorage = TerminalUsageLocalStorage.getInstance()
@@ -83,11 +81,6 @@ object ReworkedTerminalSurveyConfig : InIdeFeedbackSurveyConfig {
)
}
override fun checkExtraConditionSatisfiedForExplicitUserAction(project: Project): Boolean {
// Explicitly sending feedback is only enabled when the reworked terminal is enabled.
return TerminalOptionsProvider.instance.terminalEngine == TerminalEngine.REWORKED
}
override fun createNotification(project: Project, forTest: Boolean): RequestFeedbackNotification {
return RequestFeedbackNotification("Feedback In IDE",
TerminalBundle.message("feedback.notification.title"),
@@ -97,4 +90,45 @@ object ReworkedTerminalSurveyConfig : InIdeFeedbackSurveyConfig {
override fun updateStateAfterNotificationShowed(project: Project) {
TerminalUsageLocalStorage.getInstance().recordFeedbackNotificationShown()
}
override fun updateStateAfterDialogClosedOk(project: Project) { }
}
@ApiStatus.Internal
object ReworkedTerminalFeedbackActionConfig : ExplicitUserFeedbackConfig {
override val surveyId: String
get() = ReworkedTerminalSurveyConfig.SURVEY_ID
override val lastDayOfFeedbackCollection: LocalDate
get() = ReworkedTerminalSurveyConfig.lastDayOfFeedbackCollection
override val requireIdeEAP: Boolean
get() = ReworkedTerminalSurveyConfig.REQUIRE_EAP
override fun checkIdeIsSuitable(): Boolean =
ReworkedTerminalSurveyConfig.checkIdeIsSuitable()
override fun createFeedbackDialog(project: Project, forTest: Boolean): BlockBasedFeedbackDialog<out SystemDataJsonSerializable> =
ReworkedTerminalSurveyConfig.createFeedbackDialog(project, forTest)
override fun updateStateAfterDialogClosedOk(project: Project) { }
override fun checkExtraConditionSatisfied(project: Project): Boolean {
// Explicitly sending feedback is only enabled when the reworked terminal is enabled.
return TerminalOptionsProvider.instance.terminalEngine == TerminalEngine.REWORKED
}
}
private object ReworkedTerminalSurveyConfig {
const val SURVEY_ID: String = "reworked_terminal"
val lastDayOfFeedbackCollection: LocalDate = LocalDate(2025, 7, 15)
const val REQUIRE_EAP: Boolean = false
fun checkIdeIsSuitable(): Boolean = PlatformUtils.isJetBrainsProduct()
fun createFeedbackDialog(project: Project, forTest: Boolean): BlockBasedFeedbackDialog<out SystemDataJsonSerializable> {
return ReworkedTerminalFeedbackDialog(project, forTest)
}
}