diff --git a/platform/feedback/src/com/intellij/platform/feedback/ExplicitUserFeedbackConfig.kt b/platform/feedback/src/com/intellij/platform/feedback/ExplicitUserFeedbackConfig.kt new file mode 100644 index 000000000000..d5652a686ace --- /dev/null +++ b/platform/feedback/src/com/intellij/platform/feedback/ExplicitUserFeedbackConfig.kt @@ -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 + + /** + * 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) + } +} diff --git a/platform/feedback/src/com/intellij/platform/feedback/FeedbackSurveyConfig.kt b/platform/feedback/src/com/intellij/platform/feedback/FeedbackSurveyConfig.kt index 9dc01075b352..5c2d7d5e792f 100644 --- a/platform/feedback/src/com/intellij/platform/feedback/FeedbackSurveyConfig.kt +++ b/platform/feedback/src/com/intellij/platform/feedback/FeedbackSurveyConfig.kt @@ -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) -} diff --git a/platform/feedback/src/com/intellij/platform/feedback/InIdeFeedbackSurveyConfig.kt b/platform/feedback/src/com/intellij/platform/feedback/InIdeFeedbackSurveyConfig.kt index f02576832e3e..0b6a4ca83bcb 100644 --- a/platform/feedback/src/com/intellij/platform/feedback/InIdeFeedbackSurveyConfig.kt +++ b/platform/feedback/src/com/intellij/platform/feedback/InIdeFeedbackSurveyConfig.kt @@ -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) - } -} diff --git a/platform/feedback/src/com/intellij/platform/feedback/impl/FeedbackSurveyUtils.kt b/platform/feedback/src/com/intellij/platform/feedback/impl/FeedbackSurveyUtils.kt index 0f83c66e6723..6885ce36ebf8 100644 --- a/platform/feedback/src/com/intellij/platform/feedback/impl/FeedbackSurveyUtils.kt +++ b/platform/feedback/src/com/intellij/platform/feedback/impl/FeedbackSurveyUtils.kt @@ -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) } diff --git a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/action/TerminalFeedbackAction.kt b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/action/TerminalFeedbackAction.kt index fc4758851b40..1c788dfe8c13 100644 --- a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/action/TerminalFeedbackAction.kt +++ b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/action/TerminalFeedbackAction.kt @@ -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) } } \ No newline at end of file diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/block/feedback/ReworkedTerminalFeedbackSurvey.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/block/feedback/ReworkedTerminalFeedbackSurvey.kt index 53ded0fc9bf4..41e9f01d14ca 100644 --- a/plugins/terminal/src/org/jetbrains/plugins/terminal/block/feedback/ReworkedTerminalFeedbackSurvey.kt +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/block/feedback/ReworkedTerminalFeedbackSurvey.kt @@ -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 { - 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 = + 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 = + 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 { + return ReworkedTerminalFeedbackDialog(project, forTest) + } }