Files
galaxy/app/src/workspace/bonus_grant_notification_model.rs
T

131 lines
4.6 KiB
Rust

use crate::ai::request_usage_model::{
AIRequestUsageModel, AIRequestUsageModelEvent, BonusGrant, BonusGrantScope,
};
use crate::terminal::general_settings::GeneralSettings;
use chrono::{Duration, Utc};
use std::collections::HashSet;
use warp_core::settings::Setting;
use warpui::{Entity, ModelContext, SingletonEntity};
pub struct BonusGrantNotificationModel {
/// In-memory tracking of grants shown during this session. This prevents duplicate
/// notifications when multiple `AIRequestUsageModelEvent::RequestUsageUpdated` events
/// fire in quick succession before the persisted settings can be updated.
shown_grants_session: HashSet<String>,
}
#[derive(Debug, Clone)]
pub enum BonusGrantNotificationEvent {
ShowNotification { grant: BonusGrant, message: String },
}
impl Entity for BonusGrantNotificationModel {
type Event = BonusGrantNotificationEvent;
}
impl SingletonEntity for BonusGrantNotificationModel {}
impl BonusGrantNotificationModel {
pub fn new(ctx: &mut ModelContext<Self>) -> Self {
ctx.subscribe_to_model(&AIRequestUsageModel::handle(ctx), |me, event, ctx| {
if let AIRequestUsageModelEvent::RequestUsageUpdated = event {
me.check_for_new_bonus_grants(ctx);
}
});
Self {
shown_grants_session: HashSet::new(),
}
}
fn check_for_new_bonus_grants(&mut self, ctx: &mut ModelContext<Self>) {
let usage_model = AIRequestUsageModel::as_ref(ctx);
let bonus_grants = usage_model.bonus_grants();
let shown_grants = GeneralSettings::as_ref(ctx)
.bonus_grants_shown
.value()
.clone();
// Only show grants created in the past 2 weeks
let cutoff_date = Utc::now() - Duration::days(14);
let mut grants_to_notify = Vec::new();
let mut grants_to_persist_to_settings = Vec::new();
for grant in bonus_grants {
// Only notify about Warp-granted credits (cost = 0), not user purchases
if grant.cost_cents != 0 {
continue;
}
// Only show grants created in the past 2 weeks to avoid overwhelming users
// with old grant notifications
if grant.created_at < cutoff_date {
continue;
}
// doesn't make sense to show "you've got a bonus grant" message if no credits remain (i.e. your teammate used them all)
if grant.request_credits_remaining <= 0 {
continue;
}
// Use server-provided message if available, otherwise fall back to generic message
let message = if let Some(user_facing_message) = &grant.user_facing_message {
user_facing_message.clone()
} else {
Self::format_generic_grant_message(grant)
};
let grant_key = Self::create_grant_key(grant);
let in_persisted = shown_grants.contains(&grant_key);
let in_session = self.shown_grants_session.contains(&grant_key);
if !in_persisted && !in_session {
grants_to_notify.push((grant.clone(), message, grant_key.clone()));
}
if !in_persisted {
grants_to_persist_to_settings.push(grant_key.clone());
}
}
for (grant, message, grant_key) in grants_to_notify {
self.shown_grants_session.insert(grant_key);
ctx.emit(BonusGrantNotificationEvent::ShowNotification { grant, message });
}
for grant_key in grants_to_persist_to_settings {
self.mark_grant_as_shown(&grant_key, ctx);
self.shown_grants_session.insert(grant_key);
}
}
fn format_generic_grant_message(grant: &BonusGrant) -> String {
let scope_text = match grant.scope {
BonusGrantScope::User => "account",
BonusGrantScope::Workspace(_) => "team",
};
format!(
"{} Reload Credits have been added to your {}.",
grant.request_credits_granted, scope_text
)
}
fn create_grant_key(grant: &BonusGrant) -> String {
format!("{}:{}", grant.reason, grant.created_at.timestamp())
}
fn mark_grant_as_shown(&self, grant_key: &str, ctx: &mut ModelContext<Self>) {
GeneralSettings::handle(ctx).update(ctx, |settings, ctx| {
let mut shown_grants = settings.bonus_grants_shown.value().clone();
shown_grants.insert(grant_key.to_string());
if let Err(e) = settings.bonus_grants_shown.set_value(shown_grants, ctx) {
log::warn!("Failed to mark bonus grant as shown: {e}");
}
});
}
}