Show current plan in user menu (#15513)

This PR updates the user menu to show the user's current plan.

Also adds a new RPC message to send this information down to the client
when Zed starts.

This is behind a feature flag.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
This commit is contained in:
Marshall Bowers 2024-07-30 17:38:16 -04:00 committed by GitHub
parent 161c6ca6a4
commit a7ffc2b6f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 95 additions and 11 deletions

1
Cargo.lock generated
View File

@ -11186,6 +11186,7 @@ dependencies = [
"dev_server_projects",
"editor",
"extensions_ui",
"feature_flags",
"feedback",
"gpui",
"http_client",

View File

@ -92,6 +92,7 @@ pub struct UserStore {
by_github_login: HashMap<String, u64>,
participant_indices: HashMap<u64, ParticipantIndex>,
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
current_plan: Option<proto::Plan>,
current_user: watch::Receiver<Option<Arc<User>>>,
contacts: Vec<Arc<Contact>>,
incoming_contact_requests: Vec<Arc<User>>,
@ -139,6 +140,7 @@ impl UserStore {
let (mut current_user_tx, current_user_rx) = watch::channel();
let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded();
let rpc_subscriptions = vec![
client.add_message_handler(cx.weak_model(), Self::handle_update_plan),
client.add_message_handler(cx.weak_model(), Self::handle_update_contacts),
client.add_message_handler(cx.weak_model(), Self::handle_update_invite_info),
client.add_message_handler(cx.weak_model(), Self::handle_show_contacts),
@ -147,6 +149,7 @@ impl UserStore {
users: Default::default(),
by_github_login: Default::default(),
current_user: current_user_rx,
current_plan: None,
contacts: Default::default(),
incoming_contact_requests: Default::default(),
participant_indices: Default::default(),
@ -280,6 +283,18 @@ impl UserStore {
Ok(())
}
async fn handle_update_plan(
this: Model<Self>,
message: TypedEnvelope<proto::UpdateUserPlan>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.current_plan = Some(message.payload.plan());
cx.notify();
})?;
Ok(())
}
fn update_contacts(
&mut self,
message: UpdateContacts,
@ -657,6 +672,10 @@ impl UserStore {
self.current_user.borrow().clone()
}
pub fn current_plan(&self) -> Option<proto::Plan> {
self.current_plan
}
pub fn watch_current_user(&self) -> watch::Receiver<Option<Arc<User>>> {
self.current_user.clone()
}

View File

@ -1137,6 +1137,8 @@ impl Server {
.await?;
}
update_user_plan(user.id, session).await?;
let (contacts, dev_server_projects) = future::try_join(
self.app_state.db.get_contacts(user.id),
self.app_state.db.dev_server_projects_update(user.id),
@ -3535,6 +3537,27 @@ fn should_auto_subscribe_to_channels(version: ZedVersion) -> bool {
version.0.minor() < 139
}
async fn update_user_plan(user_id: UserId, session: &Session) -> Result<()> {
let db = session.db().await;
let active_subscriptions = db.get_active_billing_subscriptions(user_id).await?;
let plan = if session.is_staff() || !active_subscriptions.is_empty() {
proto::Plan::ZedPro
} else {
proto::Plan::Free
};
session
.peer
.send(
session.connection_id,
proto::UpdateUserPlan { plan: plan.into() },
)
.trace_err();
Ok(())
}
async fn subscribe_to_channels(_: proto::SubscribeToChannels, session: Session) -> Result<()> {
subscribe_user_to_channels(
session.user_id().ok_or_else(|| anyhow!("must be a user"))?,

View File

@ -48,6 +48,11 @@ impl FeatureFlag for GroupedDiagnostics {
const NAME: &'static str = "grouped-diagnostics";
}
pub struct ZedPro {}
impl FeatureFlag for ZedPro {
const NAME: &'static str = "zed-pro";
}
pub trait FeatureFlagViewExt<V: 'static> {
fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
where

View File

@ -126,6 +126,7 @@ message Envelope {
Unfollow unfollow = 101;
GetPrivateUserInfo get_private_user_info = 102;
GetPrivateUserInfoResponse get_private_user_info_response = 103;
UpdateUserPlan update_user_plan = 234; // current max
UpdateDiffBase update_diff_base = 104;
OnTypeFormatting on_type_formatting = 105;
@ -256,7 +257,7 @@ message Envelope {
OpenContext open_context = 212;
OpenContextResponse open_context_response = 213;
CreateContext create_context = 232;
CreateContextResponse create_context_response = 233; // current max
CreateContextResponse create_context_response = 233;
UpdateContext update_context = 214;
SynchronizeContexts synchronize_contexts = 215;
SynchronizeContextsResponse synchronize_contexts_response = 216;
@ -1680,6 +1681,15 @@ message GetPrivateUserInfoResponse {
repeated string flags = 3;
}
enum Plan {
Free = 0;
ZedPro = 1;
}
message UpdateUserPlan {
Plan plan = 1;
}
// Entities
message ViewId {

View File

@ -359,6 +359,7 @@ messages!(
(UpdateParticipantLocation, Foreground),
(UpdateProject, Foreground),
(UpdateProjectCollaborator, Foreground),
(UpdateUserPlan, Foreground),
(UpdateWorktree, Foreground),
(UpdateWorktreeSettings, Foreground),
(UsersResponse, Foreground),

View File

@ -36,6 +36,7 @@ command_palette.workspace = true
dev_server_projects.workspace = true
extensions_ui.workspace = true
feedback.workspace = true
feature_flags.workspace = true
gpui.workspace = true
notifications.workspace = true
project.workspace = true

View File

@ -11,6 +11,7 @@ use crate::platforms::{platform_linux, platform_mac, platform_windows};
use auto_update::AutoUpdateStatus;
use call::ActiveCall;
use client::{Client, UserStore};
use feature_flags::{FeatureFlagAppExt, ZedPro};
use gpui::{
actions, div, px, Action, AnyElement, AppContext, Decorations, Element, InteractiveElement,
Interactivity, IntoElement, Model, MouseButton, ParentElement, Render, Stateful,
@ -18,7 +19,7 @@ use gpui::{
};
use project::{Project, RepositoryEntry};
use recent_projects::RecentProjects;
use rpc::proto::DevServerStatus;
use rpc::proto::{self, DevServerStatus};
use smallvec::SmallVec;
use std::sync::Arc;
use theme::ActiveTheme;
@ -507,16 +508,32 @@ impl TitleBar {
}
pub fn render_user_menu_button(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
if let Some(user) = self.user_store.read(cx).current_user() {
let user_store = self.user_store.read(cx);
if let Some(user) = user_store.current_user() {
let plan = user_store.current_plan();
PopoverMenu::new("user-menu")
.menu(|cx| {
ContextMenu::build(cx, |menu, _| {
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
.action("Key Bindings", Box::new(zed_actions::OpenKeymap))
.action("Themes…", theme_selector::Toggle::default().boxed_clone())
.action("Extensions", extensions_ui::Extensions.boxed_clone())
.menu(move |cx| {
ContextMenu::build(cx, |menu, cx| {
menu.when(cx.has_flag::<ZedPro>(), |menu| {
menu.action(
format!(
"Current Plan: {}",
match plan {
None => "",
Some(proto::Plan::Free) => "Free",
Some(proto::Plan::ZedPro) => "Pro",
}
),
zed_actions::OpenAccountSettings.boxed_clone(),
)
.separator()
.action("Sign Out", client::SignOut.boxed_clone())
})
.action("Settings", zed_actions::OpenSettings.boxed_clone())
.action("Key Bindings", Box::new(zed_actions::OpenKeymap))
.action("Themes…", theme_selector::Toggle::default().boxed_clone())
.action("Extensions", extensions_ui::Extensions.boxed_clone())
.separator()
.action("Sign Out", client::SignOut.boxed_clone())
})
.into()
})

View File

@ -47,7 +47,7 @@ use workspace::{
open_new, AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings,
};
use workspace::{notifications::DetachAndPromptErr, Pane};
use zed_actions::{OpenBrowser, OpenSettings, OpenZedUrl, Quit};
use zed_actions::{OpenAccountSettings, OpenBrowser, OpenSettings, OpenZedUrl, Quit};
actions!(
zed,
@ -422,6 +422,12 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
);
},
)
.register_action(
|_: &mut Workspace, _: &OpenAccountSettings, cx: &mut ViewContext<Workspace>| {
let server_url = &client::ClientSettings::get_global(cx).server_url;
cx.open_url(&format!("{server_url}/settings"));
},
)
.register_action(
move |_: &mut Workspace, _: &OpenTasks, cx: &mut ViewContext<Workspace>| {
open_settings_file(

View File

@ -26,6 +26,7 @@ actions!(
zed,
[
OpenSettings,
OpenAccountSettings,
Quit,
OpenKeymap,
About,