mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-27 04:44:30 +00:00
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:
parent
161c6ca6a4
commit
a7ffc2b6f3
10 changed files with 95 additions and 11 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue