Cargo.lock π
@@ -11186,6 +11186,7 @@ dependencies = [
"dev_server_projects",
"editor",
"extensions_ui",
+ "feature_flags",
"feedback",
"gpui",
"http_client",
Marshall Bowers and Max created
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>
Cargo.lock | 1
crates/client/src/user.rs | 19 +++++++++++++
crates/collab/src/rpc.rs | 23 ++++++++++++++++
crates/feature_flags/src/feature_flags.rs | 5 +++
crates/proto/proto/zed.proto | 12 +++++++
crates/proto/src/proto.rs | 1
crates/title_bar/Cargo.toml | 1
crates/title_bar/src/title_bar.rs | 35 ++++++++++++++++++------
crates/zed/src/zed.rs | 8 +++++
crates/zed_actions/src/lib.rs | 1
10 files changed, 95 insertions(+), 11 deletions(-)
@@ -11186,6 +11186,7 @@ dependencies = [
"dev_server_projects",
"editor",
"extensions_ui",
+ "feature_flags",
"feedback",
"gpui",
"http_client",
@@ -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()
}
@@ -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"))?,
@@ -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
@@ -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 {
@@ -359,6 +359,7 @@ messages!(
(UpdateParticipantLocation, Foreground),
(UpdateProject, Foreground),
(UpdateProjectCollaborator, Foreground),
+ (UpdateUserPlan, Foreground),
(UpdateWorktree, Foreground),
(UpdateWorktreeSettings, Foreground),
(UsersResponse, Foreground),
@@ -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
@@ -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()
})
@@ -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(
@@ -26,6 +26,7 @@ actions!(
zed,
[
OpenSettings,
+ OpenAccountSettings,
Quit,
OpenKeymap,
About,