Merge pull request #1049 from zed-industries/invite-codes-2

Support inviting new Zed users
This commit is contained in:
Nathan Sobo 2022-05-23 18:29:51 -06:00 committed by GitHub
commit d30d2d67e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 13325 additions and 133 deletions

12
Cargo.lock generated
View File

@ -851,11 +851,13 @@ dependencies = [
"envy",
"futures",
"gpui",
"hyper",
"language",
"lazy_static",
"lipsum",
"log",
"lsp",
"nanoid",
"opentelemetry",
"opentelemetry-otlp",
"parking_lot",
@ -934,6 +936,7 @@ checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7"
name = "contacts_panel"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"editor",
"futures",
@ -2762,6 +2765,15 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
[[package]]
name = "nanoid"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8"
dependencies = [
"rand 0.8.3",
]
[[package]]
name = "native-tls"
version = "0.2.10"

1734
assets/themes/cave-dark.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -65,6 +65,7 @@ pub struct UserStore {
incoming_contact_requests: Vec<Arc<User>>,
outgoing_contact_requests: Vec<Arc<User>>,
pending_contact_requests: HashMap<u64, usize>,
invite_info: Option<InviteInfo>,
client: Weak<Client>,
http: Arc<dyn HttpClient>,
_maintain_contacts: Task<()>,
@ -72,9 +73,17 @@ pub struct UserStore {
}
#[derive(Clone)]
pub struct ContactEvent {
pub user: Arc<User>,
pub kind: ContactEventKind,
pub struct InviteInfo {
pub count: u32,
pub url: Arc<str>,
}
pub enum Event {
Contact {
user: Arc<User>,
kind: ContactEventKind,
},
ShowContacts,
}
#[derive(Clone, Copy)]
@ -85,7 +94,7 @@ pub enum ContactEventKind {
}
impl Entity for UserStore {
type Event = ContactEvent;
type Event = Event;
}
enum UpdateContacts {
@ -101,19 +110,23 @@ impl UserStore {
) -> Self {
let (mut current_user_tx, current_user_rx) = watch::channel();
let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded();
let rpc_subscription =
client.add_message_handler(cx.handle(), Self::handle_update_contacts);
let rpc_subscriptions = vec![
client.add_message_handler(cx.handle(), Self::handle_update_contacts),
client.add_message_handler(cx.handle(), Self::handle_update_invite_info),
client.add_message_handler(cx.handle(), Self::handle_show_contacts),
];
Self {
users: Default::default(),
current_user: current_user_rx,
contacts: Default::default(),
incoming_contact_requests: Default::default(),
outgoing_contact_requests: Default::default(),
invite_info: None,
client: Arc::downgrade(&client),
update_contacts_tx,
http,
_maintain_contacts: cx.spawn_weak(|this, mut cx| async move {
let _subscription = rpc_subscription;
let _subscriptions = rpc_subscriptions;
while let Some(message) = update_contacts_rx.next().await {
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| this.update_contacts(message, cx))
@ -154,15 +167,45 @@ impl UserStore {
}
}
async fn handle_update_invite_info(
this: ModelHandle<Self>,
message: TypedEnvelope<proto::UpdateInviteInfo>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.invite_info = Some(InviteInfo {
url: Arc::from(message.payload.url),
count: message.payload.count,
});
cx.notify();
});
Ok(())
}
async fn handle_show_contacts(
this: ModelHandle<Self>,
_: TypedEnvelope<proto::ShowContacts>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |_, cx| cx.emit(Event::ShowContacts));
Ok(())
}
pub fn invite_info(&self) -> Option<&InviteInfo> {
self.invite_info.as_ref()
}
async fn handle_update_contacts(
this: ModelHandle<Self>,
msg: TypedEnvelope<proto::UpdateContacts>,
message: TypedEnvelope<proto::UpdateContacts>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, _| {
this.update_contacts_tx
.unbounded_send(UpdateContacts::Update(msg.payload))
.unbounded_send(UpdateContacts::Update(message.payload))
.unwrap();
});
Ok(())
@ -244,7 +287,7 @@ impl UserStore {
// Update existing contacts and insert new ones
for (updated_contact, should_notify) in updated_contacts {
if should_notify {
cx.emit(ContactEvent {
cx.emit(Event::Contact {
user: updated_contact.user.clone(),
kind: ContactEventKind::Accepted,
});
@ -261,7 +304,7 @@ impl UserStore {
// Remove incoming contact requests
this.incoming_contact_requests.retain(|user| {
if removed_incoming_requests.contains(&user.id) {
cx.emit(ContactEvent {
cx.emit(Event::Contact {
user: user.clone(),
kind: ContactEventKind::Cancelled,
});
@ -273,7 +316,7 @@ impl UserStore {
// Update existing incoming requests and insert new ones
for (user, should_notify) in incoming_requests {
if should_notify {
cx.emit(ContactEvent {
cx.emit(Event::Contact {
user: user.clone(),
kind: ContactEventKind::Requested,
});

View File

@ -1,2 +1,9 @@
DATABASE_URL = "postgres://postgres@localhost/zed"
HTTP_PORT = 8080
API_TOKEN = "secret"
INVITE_LINK_PREFIX = "http://localhost:3000/invites/"
# HONEYCOMB_API_KEY=
# HONEYCOMB_DATASET=
# RUST_LOG=info
# LOG_JSON=true

View File

@ -25,8 +25,10 @@ base64 = "0.13"
clap = { version = "3.1", features = ["derive"], optional = true }
envy = "0.4.2"
futures = "0.3"
hyper = "0.14"
lazy_static = "1.4"
lipsum = { version = "0.8", optional = true }
nanoid = "0.4"
opentelemetry = { version = "0.17", features = ["rt-tokio"] }
opentelemetry-otlp = { version = "0.10", features = ["tls-roots"] }
parking_lot = "0.11.1"

View File

@ -1,2 +1,3 @@
ZED_ENVIRONMENT=production
RUST_LOG=info,rpc=debug
INVITE_LINK_PREFIX=https://zed.dev/invites/

View File

@ -1,2 +1,3 @@
ZED_ENVIRONMENT=staging
RUST_LOG=info,rpc=debug
INVITE_LINK_PREFIX=https://staging.zed.dev/invites/

View File

@ -81,6 +81,8 @@ spec:
secretKeyRef:
name: api
key: token
- name: INVITE_LINK_PREFIX
value: ${INVITE_LINK_PREFIX}
- name: RUST_LOG
value: ${RUST_LOG}
- name: LOG_JSON

View File

@ -0,0 +1,9 @@
ALTER TABLE users
ADD email_address VARCHAR(255) DEFAULT NULL,
ADD invite_code VARCHAR(64),
ADD invite_count INTEGER NOT NULL DEFAULT 0,
ADD inviter_id INTEGER REFERENCES users (id),
ADD connected_once BOOLEAN NOT NULL DEFAULT false,
ADD created_at TIMESTAMP NOT NULL DEFAULT NOW();
CREATE UNIQUE INDEX "index_invite_code_users" ON "users" ("invite_code");

View File

@ -0,0 +1,6 @@
ALTER TABLE contacts DROP CONSTRAINT contacts_user_id_a_fkey;
ALTER TABLE contacts DROP CONSTRAINT contacts_user_id_b_fkey;
ALTER TABLE contacts ADD CONSTRAINT contacts_user_id_a_fkey FOREIGN KEY (user_id_a) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE contacts ADD CONSTRAINT contacts_user_id_b_fkey FOREIGN KEY (user_id_b) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE users DROP CONSTRAINT users_inviter_id_fkey;
ALTER TABLE users ADD CONSTRAINT users_inviter_id_fkey FOREIGN KEY (inviter_id) REFERENCES users(id) ON DELETE SET NULL;

View File

@ -1,6 +1,7 @@
use crate::{
auth,
db::{User, UserId},
rpc::ResultExt,
AppState, Error, Result,
};
use anyhow::anyhow;
@ -18,7 +19,7 @@ use std::sync::Arc;
use tower::ServiceBuilder;
use tracing::instrument;
pub fn routes(state: Arc<AppState>) -> Router<Body> {
pub fn routes(rpc_server: &Arc<crate::rpc::Server>, state: Arc<AppState>) -> Router<Body> {
Router::new()
.route("/users", get(get_users).post(create_user))
.route(
@ -26,13 +27,14 @@ pub fn routes(state: Arc<AppState>) -> Router<Body> {
put(update_user).delete(destroy_user).get(get_user),
)
.route("/users/:id/access_tokens", post(create_access_token))
.route("/invite_codes/:code", get(get_user_for_invite_code))
.route("/panic", post(trace_panic))
.layer(
ServiceBuilder::new()
.layer(Extension(state))
.layer(Extension(rpc_server.clone()))
.layer(middleware::from_fn(validate_api_token)),
)
// TODO: Compression on API routes?
}
pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoResponse {
@ -71,20 +73,44 @@ async fn get_users(Extension(app): Extension<Arc<AppState>>) -> Result<Json<Vec<
Ok(Json(users))
}
#[derive(Deserialize)]
#[derive(Deserialize, Debug)]
struct CreateUserParams {
github_login: String,
invite_code: Option<String>,
email_address: Option<String>,
admin: bool,
}
async fn create_user(
Json(params): Json<CreateUserParams>,
Extension(app): Extension<Arc<AppState>>,
Extension(rpc_server): Extension<Arc<crate::rpc::Server>>,
) -> Result<Json<User>> {
let user_id = app
.db
.create_user(&params.github_login, params.admin)
.await?;
println!("{:?}", params);
let user_id = if let Some(invite_code) = params.invite_code {
let invitee_id = app
.db
.redeem_invite_code(
&invite_code,
&params.github_login,
params.email_address.as_deref(),
)
.await?;
rpc_server
.invite_code_redeemed(&invite_code, invitee_id)
.await
.trace_err();
invitee_id
} else {
app.db
.create_user(
&params.github_login,
params.email_address.as_deref(),
params.admin,
)
.await?
};
let user = app
.db
@ -97,7 +123,8 @@ async fn create_user(
#[derive(Deserialize)]
struct UpdateUserParams {
admin: bool,
admin: Option<bool>,
invite_count: Option<u32>,
}
async fn update_user(
@ -105,9 +132,16 @@ async fn update_user(
Json(params): Json<UpdateUserParams>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<()> {
app.db
.set_user_is_admin(UserId(user_id), params.admin)
.await?;
if let Some(admin) = params.admin {
app.db.set_user_is_admin(UserId(user_id), admin).await?;
}
if let Some(invite_count) = params.invite_count {
app.db
.set_invite_count(UserId(user_id), invite_count)
.await?;
}
Ok(())
}
@ -127,7 +161,7 @@ async fn get_user(
.db
.get_user_by_github_login(&login)
.await?
.ok_or_else(|| anyhow!("user not found"))?;
.ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "User not found".to_string()))?;
Ok(Json(user))
}
@ -196,3 +230,10 @@ async fn create_access_token(
encrypted_access_token,
}))
}
async fn get_user_for_invite_code(
Path(code): Path<String>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<Json<User>> {
Ok(Json(app.db.get_user_for_invite_code(&code).await?))
}

View File

@ -1,8 +1,8 @@
use std::sync::Arc;
use super::db::{self, UserId};
use crate::{AppState, Error};
use anyhow::{anyhow, Context, Result};
use crate::{AppState, Error, Result};
use anyhow::{anyhow, Context};
use axum::{
http::{self, Request, StatusCode},
middleware::Next,
@ -91,7 +91,8 @@ fn hash_access_token(token: &str) -> Result<String> {
None,
params,
&SaltString::generate(thread_rng()),
)?
)
.map_err(anyhow::Error::new)?
.to_string())
}
@ -105,6 +106,6 @@ pub fn encrypt_access_token(access_token: &str, public_key: String) -> Result<St
}
pub fn verify_access_token(token: &str, hash: &str) -> Result<bool> {
let hash = PasswordHash::new(hash)?;
let hash = PasswordHash::new(hash).map_err(anyhow::Error::new)?;
Ok(Scrypt.verify_password(token.as_bytes(), &hash).is_ok())
}

View File

@ -1,6 +1,9 @@
use anyhow::{anyhow, Context, Result};
use crate::{Error, Result};
use anyhow::{anyhow, Context};
use async_trait::async_trait;
use axum::http::StatusCode;
use futures::StreamExt;
use nanoid::nanoid;
use serde::Serialize;
pub use sqlx::postgres::PgPoolOptions as DbOptions;
use sqlx::{types::Uuid, FromRow};
@ -8,15 +11,31 @@ use time::OffsetDateTime;
#[async_trait]
pub trait Db: Send + Sync {
async fn create_user(&self, github_login: &str, admin: bool) -> Result<UserId>;
async fn create_user(
&self,
github_login: &str,
email_address: Option<&str>,
admin: bool,
) -> Result<UserId>;
async fn get_all_users(&self) -> Result<Vec<User>>;
async fn fuzzy_search_users(&self, query: &str, limit: u32) -> Result<Vec<User>>;
async fn get_user_by_id(&self, id: UserId) -> Result<Option<User>>;
async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<User>>;
async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>>;
async fn set_user_is_admin(&self, id: UserId, is_admin: bool) -> Result<()>;
async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()>;
async fn destroy_user(&self, id: UserId) -> Result<()>;
async fn set_invite_count(&self, id: UserId, count: u32) -> Result<()>;
async fn get_invite_code_for_user(&self, id: UserId) -> Result<Option<(String, u32)>>;
async fn get_user_for_invite_code(&self, code: &str) -> Result<User>;
async fn redeem_invite_code(
&self,
code: &str,
login: &str,
email_address: Option<&str>,
) -> Result<UserId>;
async fn get_contacts(&self, id: UserId) -> Result<Vec<Contact>>;
async fn has_contact(&self, user_id_a: UserId, user_id_b: UserId) -> Result<bool>;
async fn send_contact_request(&self, requester_id: UserId, responder_id: UserId) -> Result<()>;
@ -101,15 +120,21 @@ impl PostgresDb {
impl Db for PostgresDb {
// users
async fn create_user(&self, github_login: &str, admin: bool) -> Result<UserId> {
async fn create_user(
&self,
github_login: &str,
email_address: Option<&str>,
admin: bool,
) -> Result<UserId> {
let query = "
INSERT INTO users (github_login, admin)
VALUES ($1, $2)
INSERT INTO users (github_login, email_address, admin)
VALUES ($1, $2, $3)
ON CONFLICT (github_login) DO UPDATE SET github_login = excluded.github_login
RETURNING id
";
Ok(sqlx::query_scalar(query)
.bind(github_login)
.bind(email_address)
.bind(admin)
.fetch_one(&self.pool)
.await
@ -174,6 +199,16 @@ impl Db for PostgresDb {
.map(drop)?)
}
async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> {
let query = "UPDATE users SET connected_once = $1 WHERE id = $2";
Ok(sqlx::query(query)
.bind(connected_once)
.bind(id.0)
.execute(&self.pool)
.await
.map(drop)?)
}
async fn destroy_user(&self, id: UserId) -> Result<()> {
let query = "DELETE FROM access_tokens WHERE user_id = $1;";
sqlx::query(query)
@ -189,6 +224,153 @@ impl Db for PostgresDb {
.map(drop)?)
}
// invite codes
async fn set_invite_count(&self, id: UserId, count: u32) -> Result<()> {
let mut tx = self.pool.begin().await?;
if count > 0 {
sqlx::query(
"
UPDATE users
SET invite_code = $1
WHERE id = $2 AND invite_code IS NULL
",
)
.bind(nanoid!(16))
.bind(id)
.execute(&mut tx)
.await?;
}
sqlx::query(
"
UPDATE users
SET invite_count = $1
WHERE id = $2
",
)
.bind(count)
.bind(id)
.execute(&mut tx)
.await?;
tx.commit().await?;
Ok(())
}
async fn get_invite_code_for_user(&self, id: UserId) -> Result<Option<(String, u32)>> {
let result: Option<(String, i32)> = sqlx::query_as(
"
SELECT invite_code, invite_count
FROM users
WHERE id = $1 AND invite_code IS NOT NULL
",
)
.bind(id)
.fetch_optional(&self.pool)
.await?;
if let Some((code, count)) = result {
Ok(Some((code, count.try_into().map_err(anyhow::Error::new)?)))
} else {
Ok(None)
}
}
async fn get_user_for_invite_code(&self, code: &str) -> Result<User> {
sqlx::query_as(
"
SELECT *
FROM users
WHERE invite_code = $1
",
)
.bind(code)
.fetch_optional(&self.pool)
.await?
.ok_or_else(|| {
Error::Http(
StatusCode::NOT_FOUND,
"that invite code does not exist".to_string(),
)
})
}
async fn redeem_invite_code(
&self,
code: &str,
login: &str,
email_address: Option<&str>,
) -> Result<UserId> {
let mut tx = self.pool.begin().await?;
let inviter_id: Option<UserId> = sqlx::query_scalar(
"
UPDATE users
SET invite_count = invite_count - 1
WHERE
invite_code = $1 AND
invite_count > 0
RETURNING id
",
)
.bind(code)
.fetch_optional(&mut tx)
.await?;
let inviter_id = match inviter_id {
Some(inviter_id) => inviter_id,
None => {
if sqlx::query_scalar::<_, i32>("SELECT 1 FROM users WHERE invite_code = $1")
.bind(code)
.fetch_optional(&mut tx)
.await?
.is_some()
{
Err(Error::Http(
StatusCode::UNAUTHORIZED,
"no invites remaining".to_string(),
))?
} else {
Err(Error::Http(
StatusCode::NOT_FOUND,
"invite code not found".to_string(),
))?
}
}
};
let invitee_id = sqlx::query_scalar(
"
INSERT INTO users
(github_login, email_address, admin, inviter_id)
VALUES
($1, $2, 'f', $3)
RETURNING id
",
)
.bind(login)
.bind(email_address)
.bind(inviter_id)
.fetch_one(&mut tx)
.await
.map(UserId)?;
sqlx::query(
"
INSERT INTO contacts
(user_id_a, user_id_b, a_to_b, should_notify, accepted)
VALUES
($1, $2, 't', 't', 't')
",
)
.bind(inviter_id)
.bind(invitee_id)
.execute(&mut tx)
.await?;
tx.commit().await?;
Ok(invitee_id)
}
// contacts
async fn get_contacts(&self, user_id: UserId) -> Result<Vec<Contact>> {
@ -293,7 +475,7 @@ impl Db for PostgresDb {
if result.rows_affected() == 1 {
Ok(())
} else {
Err(anyhow!("contact already requested"))
Err(anyhow!("contact already requested"))?
}
}
@ -316,7 +498,7 @@ impl Db for PostgresDb {
if result.rows_affected() == 1 {
Ok(())
} else {
Err(anyhow!("no such contact"))
Err(anyhow!("no such contact"))?
}
}
@ -394,7 +576,7 @@ impl Db for PostgresDb {
if result.rows_affected() == 1 {
Ok(())
} else {
Err(anyhow!("no such contact request"))
Err(anyhow!("no such contact request"))?
}
}
@ -694,11 +876,15 @@ macro_rules! id_type {
}
id_type!(UserId);
#[derive(Clone, Debug, FromRow, Serialize, PartialEq)]
#[derive(Clone, Debug, Default, FromRow, Serialize, PartialEq)]
pub struct User {
pub id: UserId,
pub github_login: String,
pub email_address: Option<String>,
pub admin: bool,
pub invite_code: Option<String>,
pub invite_count: i32,
pub connected_once: bool,
}
id_type!(OrgId);
@ -796,10 +982,10 @@ pub mod tests {
] {
let db = test_db.db();
let user = db.create_user("user", false).await.unwrap();
let friend1 = db.create_user("friend-1", false).await.unwrap();
let friend2 = db.create_user("friend-2", false).await.unwrap();
let friend3 = db.create_user("friend-3", false).await.unwrap();
let user = db.create_user("user", None, false).await.unwrap();
let friend1 = db.create_user("friend-1", None, false).await.unwrap();
let friend2 = db.create_user("friend-2", None, false).await.unwrap();
let friend3 = db.create_user("friend-3", None, false).await.unwrap();
assert_eq!(
db.get_users_by_ids(vec![user, friend1, friend2, friend3])
@ -810,21 +996,25 @@ pub mod tests {
id: user,
github_login: "user".to_string(),
admin: false,
..Default::default()
},
User {
id: friend1,
github_login: "friend-1".to_string(),
admin: false,
..Default::default()
},
User {
id: friend2,
github_login: "friend-2".to_string(),
admin: false,
..Default::default()
},
User {
id: friend3,
github_login: "friend-3".to_string(),
admin: false,
..Default::default()
}
]
);
@ -838,7 +1028,7 @@ pub mod tests {
TestDb::fake(Arc::new(gpui::executor::Background::new())),
] {
let db = test_db.db();
let user = db.create_user("user", false).await.unwrap();
let user = db.create_user("user", None, false).await.unwrap();
let org = db.create_org("org", "org").await.unwrap();
let channel = db.create_org_channel(org, "channel").await.unwrap();
for i in 0..10 {
@ -877,7 +1067,7 @@ pub mod tests {
TestDb::fake(Arc::new(gpui::executor::Background::new())),
] {
let db = test_db.db();
let user = db.create_user("user", false).await.unwrap();
let user = db.create_user("user", None, false).await.unwrap();
let org = db.create_org("org", "org").await.unwrap();
let channel = db.create_org_channel(org, "channel").await.unwrap();
@ -908,7 +1098,7 @@ pub mod tests {
async fn test_create_access_tokens() {
let test_db = TestDb::postgres().await;
let db = test_db.db();
let user = db.create_user("the-user", false).await.unwrap();
let user = db.create_user("the-user", None, false).await.unwrap();
db.create_access_token_hash(user, "h1", 3).await.unwrap();
db.create_access_token_hash(user, "h2", 3).await.unwrap();
@ -956,7 +1146,7 @@ pub mod tests {
"delaware",
"rhode-island",
] {
db.create_user(github_login, false).await.unwrap();
db.create_user(github_login, None, false).await.unwrap();
}
assert_eq!(
@ -986,9 +1176,9 @@ pub mod tests {
] {
let db = test_db.db();
let user_1 = db.create_user("user1", false).await.unwrap();
let user_2 = db.create_user("user2", false).await.unwrap();
let user_3 = db.create_user("user3", false).await.unwrap();
let user_1 = db.create_user("user1", None, false).await.unwrap();
let user_2 = db.create_user("user2", None, false).await.unwrap();
let user_3 = db.create_user("user3", None, false).await.unwrap();
// User starts with no contacts
assert_eq!(
@ -1198,6 +1388,159 @@ pub mod tests {
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_invite_codes() {
let postgres = TestDb::postgres().await;
let db = postgres.db();
let user1 = db.create_user("user-1", None, false).await.unwrap();
// Initially, user 1 has no invite code
assert_eq!(db.get_invite_code_for_user(user1).await.unwrap(), None);
// Setting invite count to 0 when no code is assigned does not assign a new code
db.set_invite_count(user1, 0).await.unwrap();
assert!(db.get_invite_code_for_user(user1).await.unwrap().is_none());
// User 1 creates an invite code that can be used twice.
db.set_invite_count(user1, 2).await.unwrap();
let (invite_code, invite_count) =
db.get_invite_code_for_user(user1).await.unwrap().unwrap();
assert_eq!(invite_count, 2);
// User 2 redeems the invite code and becomes a contact of user 1.
let user2 = db
.redeem_invite_code(&invite_code, "user-2", None)
.await
.unwrap();
let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
assert_eq!(invite_count, 1);
assert_eq!(
db.get_contacts(user1).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user2,
should_notify: true
}
]
);
assert_eq!(
db.get_contacts(user2).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user2,
should_notify: false
}
]
);
// User 3 redeems the invite code and becomes a contact of user 1.
let user3 = db
.redeem_invite_code(&invite_code, "user-3", None)
.await
.unwrap();
let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
assert_eq!(invite_count, 0);
assert_eq!(
db.get_contacts(user1).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user2,
should_notify: true
},
Contact::Accepted {
user_id: user3,
should_notify: true
}
]
);
assert_eq!(
db.get_contacts(user3).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user3,
should_notify: false
},
]
);
// Trying to reedem the code for the third time results in an error.
db.redeem_invite_code(&invite_code, "user-4", None)
.await
.unwrap_err();
// Invite count can be updated after the code has been created.
db.set_invite_count(user1, 2).await.unwrap();
let (latest_code, invite_count) =
db.get_invite_code_for_user(user1).await.unwrap().unwrap();
assert_eq!(latest_code, invite_code); // Invite code doesn't change when we increment above 0
assert_eq!(invite_count, 2);
// User 4 can now redeem the invite code and becomes a contact of user 1.
let user4 = db
.redeem_invite_code(&invite_code, "user-4", None)
.await
.unwrap();
let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
assert_eq!(invite_count, 1);
assert_eq!(
db.get_contacts(user1).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user2,
should_notify: true
},
Contact::Accepted {
user_id: user3,
should_notify: true
},
Contact::Accepted {
user_id: user4,
should_notify: true
}
]
);
assert_eq!(
db.get_contacts(user4).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user4,
should_notify: false
},
]
);
// An existing user cannot redeem invite codes.
db.redeem_invite_code(&invite_code, "user-2", None)
.await
.unwrap_err();
let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
assert_eq!(invite_count, 1);
}
pub struct TestDb {
pub db: Option<Arc<dyn Db>>,
pub url: String,
@ -1290,7 +1633,12 @@ pub mod tests {
#[async_trait]
impl Db for FakeDb {
async fn create_user(&self, github_login: &str, admin: bool) -> Result<UserId> {
async fn create_user(
&self,
github_login: &str,
email_address: Option<&str>,
admin: bool,
) -> Result<UserId> {
self.background.simulate_random_delay().await;
let mut users = self.users.lock();
@ -1306,7 +1654,11 @@ pub mod tests {
User {
id: user_id,
github_login: github_login.to_string(),
email_address: email_address.map(str::to_string),
admin,
invite_code: None,
invite_count: 0,
connected_once: false,
},
);
Ok(user_id)
@ -1344,10 +1696,45 @@ pub mod tests {
unimplemented!()
}
async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> {
self.background.simulate_random_delay().await;
let mut users = self.users.lock();
let mut user = users
.get_mut(&id)
.ok_or_else(|| anyhow!("user not found"))?;
user.connected_once = connected_once;
Ok(())
}
async fn destroy_user(&self, _id: UserId) -> Result<()> {
unimplemented!()
}
// invite codes
async fn set_invite_count(&self, _id: UserId, _count: u32) -> Result<()> {
unimplemented!()
}
async fn get_invite_code_for_user(&self, _id: UserId) -> Result<Option<(String, u32)>> {
Ok(None)
}
async fn get_user_for_invite_code(&self, _code: &str) -> Result<User> {
unimplemented!()
}
async fn redeem_invite_code(
&self,
_code: &str,
_login: &str,
_email_address: Option<&str>,
) -> Result<UserId> {
unimplemented!()
}
// contacts
async fn get_contacts(&self, id: UserId) -> Result<Vec<Contact>> {
self.background.simulate_random_delay().await;
let mut contacts = vec![Contact::Accepted {
@ -1457,7 +1844,7 @@ pub mod tests {
return Ok(());
}
}
Err(anyhow!("no such notification"))
Err(anyhow!("no such notification"))?
}
async fn respond_to_contact_request(
@ -1470,7 +1857,7 @@ pub mod tests {
for (ix, contact) in contacts.iter_mut().enumerate() {
if contact.requester_id == requester_id && contact.responder_id == responder_id {
if contact.accepted {
return Err(anyhow!("contact already confirmed"));
Err(anyhow!("contact already confirmed"))?;
}
if accept {
contact.accepted = true;
@ -1481,7 +1868,7 @@ pub mod tests {
return Ok(());
}
}
Err(anyhow!("no such contact request"))
Err(anyhow!("no such contact request"))?
}
async fn create_access_token_hash(
@ -1505,7 +1892,7 @@ pub mod tests {
self.background.simulate_random_delay().await;
let mut orgs = self.orgs.lock();
if orgs.values().any(|org| org.slug == slug) {
Err(anyhow!("org already exists"))
Err(anyhow!("org already exists"))?
} else {
let org_id = OrgId(post_inc(&mut *self.next_org_id.lock()));
orgs.insert(
@ -1528,10 +1915,10 @@ pub mod tests {
) -> Result<()> {
self.background.simulate_random_delay().await;
if !self.orgs.lock().contains_key(&org_id) {
return Err(anyhow!("org does not exist"));
Err(anyhow!("org does not exist"))?;
}
if !self.users.lock().contains_key(&user_id) {
return Err(anyhow!("user does not exist"));
Err(anyhow!("user does not exist"))?;
}
self.org_memberships
@ -1544,7 +1931,7 @@ pub mod tests {
async fn create_org_channel(&self, org_id: OrgId, name: &str) -> Result<ChannelId> {
self.background.simulate_random_delay().await;
if !self.orgs.lock().contains_key(&org_id) {
return Err(anyhow!("org does not exist"));
Err(anyhow!("org does not exist"))?;
}
let mut channels = self.channels.lock();
@ -1603,10 +1990,10 @@ pub mod tests {
) -> Result<()> {
self.background.simulate_random_delay().await;
if !self.channels.lock().contains_key(&channel_id) {
return Err(anyhow!("channel does not exist"));
Err(anyhow!("channel does not exist"))?;
}
if !self.users.lock().contains_key(&user_id) {
return Err(anyhow!("user does not exist"));
Err(anyhow!("user does not exist"))?;
}
self.channel_memberships
@ -1626,10 +2013,10 @@ pub mod tests {
) -> Result<MessageId> {
self.background.simulate_random_delay().await;
if !self.channels.lock().contains_key(&channel_id) {
return Err(anyhow!("channel does not exist"));
Err(anyhow!("channel does not exist"))?;
}
if !self.users.lock().contains_key(&sender_id) {
return Err(anyhow!("user does not exist"));
Err(anyhow!("user does not exist"))?;
}
let mut messages = self.channel_messages.lock();

View File

@ -20,6 +20,7 @@ pub struct Config {
pub http_port: u16,
pub database_url: String,
pub api_token: String,
pub invite_link_prefix: String,
pub honeycomb_api_key: Option<String>,
pub honeycomb_dataset: Option<String>,
pub rust_log: Option<String>,
@ -29,6 +30,7 @@ pub struct Config {
pub struct AppState {
db: Arc<dyn Db>,
api_token: String,
invite_link_prefix: String,
}
impl AppState {
@ -37,6 +39,7 @@ impl AppState {
let this = Self {
db: Arc::new(db),
api_token: config.api_token.clone(),
invite_link_prefix: config.invite_link_prefix.clone(),
};
Ok(Arc::new(this))
}
@ -57,10 +60,11 @@ async fn main() -> Result<()> {
let listener = TcpListener::bind(&format!("0.0.0.0:{}", config.http_port))
.expect("failed to bind TCP listener");
let rpc_server = rpc::Server::new(state.clone(), None);
let app = Router::<Body>::new()
.merge(api::routes(state.clone()))
.merge(rpc::routes(state));
.merge(api::routes(&rpc_server, state.clone()))
.merge(rpc::routes(rpc_server));
axum::Server::from_tcp(listener)?
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
@ -76,11 +80,26 @@ pub enum Error {
Internal(anyhow::Error),
}
impl<E> From<E> for Error
where
E: Into<anyhow::Error>,
{
fn from(error: E) -> Self {
impl From<anyhow::Error> for Error {
fn from(error: anyhow::Error) -> Self {
Self::Internal(error)
}
}
impl From<sqlx::Error> for Error {
fn from(error: sqlx::Error) -> Self {
Self::Internal(error.into())
}
}
impl From<axum::Error> for Error {
fn from(error: axum::Error) -> Self {
Self::Internal(error.into())
}
}
impl From<hyper::Error> for Error {
fn from(error: hyper::Error) -> Self {
Self::Internal(error.into())
}
}
@ -114,6 +133,8 @@ impl std::fmt::Display for Error {
}
}
impl std::error::Error for Error {}
pub fn init_tracing(config: &Config) -> Option<()> {
use opentelemetry::KeyValue;
use opentelemetry_otlp::WithExportConfig;

View File

@ -23,7 +23,11 @@ use axum::{
Extension, Router, TypedHeader,
};
use collections::HashMap;
use futures::{channel::mpsc, future::BoxFuture, FutureExt, SinkExt, StreamExt, TryStreamExt};
use futures::{
channel::mpsc,
future::{self, BoxFuture},
FutureExt, SinkExt, StreamExt, TryStreamExt,
};
use lazy_static::lazy_static;
use rpc::{
proto::{self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage},
@ -276,12 +280,27 @@ impl Server {
let _ = send_connection_id.send(connection_id).await;
}
let contacts = this.app_state.db.get_contacts(user_id).await?;
if !user.connected_once {
this.peer.send(connection_id, proto::ShowContacts {})?;
this.app_state.db.set_user_connected_once(user_id, true).await?;
}
let (contacts, invite_code) = future::try_join(
this.app_state.db.get_contacts(user_id),
this.app_state.db.get_invite_code_for_user(user_id)
).await?;
{
let mut store = this.store_mut().await;
store.add_connection(connection_id, user_id);
this.peer.send(connection_id, store.build_initial_contacts_update(contacts))?;
if let Some((code, count)) = invite_code {
this.peer.send(connection_id, proto::UpdateInviteInfo {
url: format!("{}{}", this.app_state.invite_link_prefix, code),
count,
})?;
}
}
this.update_user_contacts(user_id).await?;
@ -398,6 +417,23 @@ impl Server {
Ok(())
}
pub async fn invite_code_redeemed(self: &Arc<Self>, code: &str, invitee_id: UserId) -> Result<()> {
let user = self.app_state.db.get_user_for_invite_code(code).await?;
let store = self.store().await;
let invitee_contact = store.contact_for_user(invitee_id, true);
for connection_id in store.connection_ids_for_user(user.id) {
self.peer.send(connection_id, proto::UpdateContacts {
contacts: vec![invitee_contact.clone()],
..Default::default()
})?;
self.peer.send(connection_id, proto::UpdateInviteInfo {
url: format!("{}{}", self.app_state.invite_link_prefix, code),
count: user.invite_count as u32,
})?;
}
Ok(())
}
async fn ping(
self: Arc<Server>,
_: TypedEnvelope<proto::Ping>,
@ -1529,13 +1565,12 @@ impl Header for ProtocolVersion {
}
}
pub fn routes(app_state: Arc<AppState>) -> Router<Body> {
let server = Server::new(app_state.clone(), None);
pub fn routes(server: Arc<Server>) -> Router<Body> {
Router::new()
.route("/rpc", get(handle_websocket_request))
.layer(
ServiceBuilder::new()
.layer(Extension(app_state))
.layer(Extension(server.app_state.clone()))
.layer(middleware::from_fn(auth::validate_header))
.layer(Extension(server)),
)
@ -6039,9 +6074,9 @@ mod tests {
let mut server = TestServer::start(cx.foreground(), cx.background()).await;
let db = server.app_state.db.clone();
let host_user_id = db.create_user("host", false).await.unwrap();
let host_user_id = db.create_user("host", None, false).await.unwrap();
for username in ["guest-1", "guest-2", "guest-3", "guest-4"] {
let guest_user_id = db.create_user(username, false).await.unwrap();
let guest_user_id = db.create_user(username, None, false).await.unwrap();
server
.app_state
.db
@ -6556,7 +6591,7 @@ mod tests {
if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await {
user.id
} else {
self.app_state.db.create_user(name, false).await.unwrap()
self.app_state.db.create_user(name, None, false).await.unwrap()
};
let client_name = name.to_string();
let mut client = Client::new(http.clone());
@ -6687,6 +6722,7 @@ mod tests {
Arc::new(AppState {
db: test_db.db().clone(),
api_token: Default::default(),
invite_link_prefix: Default::default(),
})
}

View File

@ -18,6 +18,7 @@ settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow = "1.0"
futures = "0.3"
log = "0.4"
postage = { version = "0.4.1", features = ["futures-traits"] }

View File

@ -1,5 +1,7 @@
use std::sync::Arc;
use crate::notifications::render_user_notification;
use client::{ContactEvent, ContactEventKind, UserStore};
use client::{ContactEventKind, User, UserStore};
use gpui::{
elements::*, impl_internal_actions, Entity, ModelHandle, MutableAppContext, RenderContext,
View, ViewContext,
@ -15,7 +17,8 @@ pub fn init(cx: &mut MutableAppContext) {
pub struct ContactNotification {
user_store: ModelHandle<UserStore>,
event: ContactEvent,
user: Arc<User>,
kind: client::ContactEventKind,
}
#[derive(Clone)]
@ -41,27 +44,27 @@ impl View for ContactNotification {
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
match self.event.kind {
match self.kind {
ContactEventKind::Requested => render_user_notification(
self.event.user.clone(),
self.user.clone(),
"wants to add you as a contact",
Some("They won't know if you decline."),
RespondToContactRequest {
user_id: self.event.user.id,
user_id: self.user.id,
accept: false,
},
vec![
(
"Decline",
Box::new(RespondToContactRequest {
user_id: self.event.user.id,
user_id: self.user.id,
accept: false,
}),
),
(
"Accept",
Box::new(RespondToContactRequest {
user_id: self.event.user.id,
user_id: self.user.id,
accept: true,
}),
),
@ -69,10 +72,10 @@ impl View for ContactNotification {
cx,
),
ContactEventKind::Accepted => render_user_notification(
self.event.user.clone(),
self.user.clone(),
"accepted your contact request",
None,
Dismiss(self.event.user.id),
Dismiss(self.user.id),
vec![],
cx,
),
@ -89,30 +92,35 @@ impl Notification for ContactNotification {
impl ContactNotification {
pub fn new(
event: ContactEvent,
user: Arc<User>,
kind: client::ContactEventKind,
user_store: ModelHandle<UserStore>,
cx: &mut ViewContext<Self>,
) -> Self {
cx.subscribe(&user_store, move |this, _, event, cx| {
if let client::ContactEvent {
if let client::Event::Contact {
kind: ContactEventKind::Cancelled,
user,
} = event
{
if user.id == this.event.user.id {
if user.id == this.user.id {
cx.emit(Event::Dismiss);
}
}
})
.detach();
Self { event, user_store }
Self {
user,
kind,
user_store,
}
}
fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
self.user_store.update(cx, |store, cx| {
store
.dismiss_contact_request(self.event.user.id, cx)
.dismiss_contact_request(self.user.id, cx)
.detach_and_log_err(cx);
});
cx.emit(Event::Dismiss);

View File

@ -12,8 +12,8 @@ use gpui::{
geometry::{rect::RectF, vector::vec2f},
impl_actions, impl_internal_actions,
platform::CursorStyle,
AppContext, Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext,
RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
AppContext, ClipboardItem, Element, ElementBox, Entity, LayoutContext, ModelHandle,
MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
};
use join_project_notification::JoinProjectNotification;
use serde::Deserialize;
@ -157,16 +157,28 @@ impl ContactsPanel {
if let Some((workspace, user_store)) =
workspace.upgrade(cx).zip(user_store.upgrade(cx))
{
workspace.update(cx, |workspace, cx| match event.kind {
ContactEventKind::Requested | ContactEventKind::Accepted => workspace
.show_notification(event.user.id as usize, cx, |cx| {
cx.add_view(|cx| {
ContactNotification::new(event.clone(), user_store, cx)
})
}),
workspace.update(cx, |workspace, cx| match event {
client::Event::Contact { user, kind } => match kind {
ContactEventKind::Requested | ContactEventKind::Accepted => workspace
.show_notification(user.id as usize, cx, |cx| {
cx.add_view(|cx| {
ContactNotification::new(
user.clone(),
*kind,
user_store,
cx,
)
})
}),
_ => {}
},
_ => {}
});
}
if let client::Event::ShowContacts = event {
cx.emit(Event::Activate);
}
}
})
.detach();
@ -793,6 +805,10 @@ impl SidebarItem for ContactsPanel {
fn contains_focused_view(&self, cx: &AppContext) -> bool {
self.filter_editor.is_focused(cx)
}
fn should_activate_item_on_event(&self, event: &Event, _: &AppContext) -> bool {
matches!(event, Event::Activate)
}
}
fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element {
@ -808,7 +824,9 @@ fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Elemen
.with_height(style.button_width)
}
pub enum Event {}
pub enum Event {
Activate,
}
impl Entity for ContactsPanel {
type Event = Event;
@ -855,6 +873,59 @@ impl View for ContactsPanel {
.boxed(),
)
.with_child(List::new(self.list_state.clone()).flex(1., false).boxed())
.with_children(
self.user_store
.read(cx)
.invite_info()
.cloned()
.and_then(|info| {
enum InviteLink {}
if info.count > 0 {
Some(
MouseEventHandler::new::<InviteLink, _, _>(
0,
cx,
|state, cx| {
let style =
theme.invite_row.style_for(state, false).clone();
let copied =
cx.read_from_clipboard().map_or(false, |item| {
item.text().as_str() == info.url.as_ref()
});
Label::new(
format!(
"{} invite link ({} left)",
if copied { "Copied" } else { "Copy" },
info.count
),
style.label.clone(),
)
.aligned()
.left()
.constrained()
.with_height(theme.row_height)
.contained()
.with_style(style.container)
.boxed()
},
)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(move |_, cx| {
cx.write_to_clipboard(ClipboardItem::new(
info.url.to_string(),
));
cx.notify();
})
.boxed(),
)
} else {
None
}
}),
)
.boxed(),
)
.with_style(theme.container)

View File

@ -87,18 +87,20 @@ message Envelope {
GetChannelMessagesResponse get_channel_messages_response = 75;
UpdateContacts update_contacts = 76;
UpdateInviteInfo update_invite_info = 77;
ShowContacts show_contacts = 78;
GetUsers get_users = 77;
FuzzySearchUsers fuzzy_search_users = 78;
UsersResponse users_response = 79;
RequestContact request_contact = 80;
RespondToContactRequest respond_to_contact_request = 81;
RemoveContact remove_contact = 82;
GetUsers get_users = 79;
FuzzySearchUsers fuzzy_search_users = 80;
UsersResponse users_response = 81;
RequestContact request_contact = 82;
RespondToContactRequest respond_to_contact_request = 83;
RemoveContact remove_contact = 84;
Follow follow = 83;
FollowResponse follow_response = 84;
UpdateFollowers update_followers = 85;
Unfollow unfollow = 86;
Follow follow = 85;
FollowResponse follow_response = 86;
UpdateFollowers update_followers = 87;
Unfollow unfollow = 88;
}
}
@ -634,6 +636,13 @@ message UpdateContacts {
repeated uint64 remove_outgoing_requests = 6;
}
message UpdateInviteInfo {
string url = 1;
uint32 count = 2;
}
message ShowContacts {}
message IncomingContactRequest {
uint64 requester_id = 1;
bool should_notify = 2;

View File

@ -145,6 +145,7 @@ messages!(
(SearchProjectResponse, Background),
(SendChannelMessage, Foreground),
(SendChannelMessageResponse, Foreground),
(ShowContacts, Foreground),
(StartLanguageServer, Foreground),
(Test, Foreground),
(Unfollow, Foreground),
@ -155,6 +156,7 @@ messages!(
(UpdateContacts, Foreground),
(UpdateDiagnosticSummary, Foreground),
(UpdateFollowers, Foreground),
(UpdateInviteInfo, Foreground),
(UpdateLanguageServer, Foreground),
(UpdateWorktree, Foreground),
);

View File

@ -6,4 +6,4 @@ pub use conn::Connection;
pub use peer::*;
mod macros;
pub const PROTOCOL_VERSION: u32 = 18;
pub const PROTOCOL_VERSION: u32 = 19;

View File

@ -262,6 +262,16 @@ pub struct ContactsPanel {
pub disabled_contact_button: IconButton,
pub tree_branch: Interactive<TreeBranch>,
pub section_icon_size: f32,
pub invite_row: Interactive<ContainedLabel>,
}
#[derive(Deserialize, Default)]
pub struct InviteLink {
#[serde(flatten)]
pub container: ContainerStyle,
#[serde(flatten)]
pub label: LabelStyle,
pub icon: Icon,
}
#[derive(Deserialize, Default, Clone, Copy)]
@ -279,6 +289,15 @@ pub struct ContactFinder {
pub disabled_contact_button: IconButton,
}
#[derive(Deserialize, Default)]
pub struct Icon {
#[serde(flatten)]
pub container: ContainerStyle,
pub color: Color,
pub width: f32,
pub path: String,
}
#[derive(Deserialize, Default)]
pub struct IconButton {
#[serde(flatten)]

View File

@ -9,6 +9,9 @@ use std::{cell::RefCell, rc::Rc};
use theme::Theme;
pub trait SidebarItem: View {
fn should_activate_item_on_event(&self, _: &Self::Event, _: &AppContext) -> bool {
false
}
fn should_show_badge(&self, cx: &AppContext) -> bool;
fn contains_focused_view(&self, _: &AppContext) -> bool {
false
@ -16,6 +19,7 @@ pub trait SidebarItem: View {
}
pub trait SidebarItemHandle {
fn id(&self) -> usize;
fn should_show_badge(&self, cx: &AppContext) -> bool;
fn is_focused(&self, cx: &AppContext) -> bool;
fn to_any(&self) -> AnyViewHandle;
@ -25,6 +29,10 @@ impl<T> SidebarItemHandle for ViewHandle<T>
where
T: SidebarItem,
{
fn id(&self) -> usize {
self.id()
}
fn should_show_badge(&self, cx: &AppContext) -> bool {
self.read(cx).should_show_badge(cx)
}
@ -61,7 +69,7 @@ pub enum Side {
struct Item {
icon_path: &'static str,
view: Rc<dyn SidebarItemHandle>,
_observation: Subscription,
_subscriptions: [Subscription; 2],
}
pub struct SidebarButtons {
@ -99,11 +107,24 @@ impl Sidebar {
view: ViewHandle<T>,
cx: &mut ViewContext<Self>,
) {
let subscription = cx.observe(&view, |_, _, cx| cx.notify());
let subscriptions = [
cx.observe(&view, |_, _, cx| cx.notify()),
cx.subscribe(&view, |this, view, event, cx| {
if view.read(cx).should_activate_item_on_event(event, cx) {
if let Some(ix) = this
.items
.iter()
.position(|item| item.view.id() == view.id())
{
this.activate_item(ix, cx);
}
}
}),
];
self.items.push(Item {
icon_path,
view: Rc::new(view),
_observation: subscription,
_subscriptions: subscriptions,
});
cx.notify()
}

519
styles/dist/cave-dark.json vendored Normal file
View File

@ -0,0 +1,519 @@
{
"meta": {
"themeName": "cave-dark"
},
"text": {
"primary": {
"value": "#e2dfe7",
"type": "color"
},
"secondary": {
"value": "#8b8792",
"type": "color"
},
"muted": {
"value": "#8b8792",
"type": "color"
},
"placeholder": {
"value": "#7e7887",
"type": "color"
},
"active": {
"value": "#efecf4",
"type": "color"
},
"feature": {
"value": "#576ddb",
"type": "color"
},
"ok": {
"value": "#2a9292",
"type": "color"
},
"error": {
"value": "#be4678",
"type": "color"
},
"warning": {
"value": "#a06e3b",
"type": "color"
},
"info": {
"value": "#576ddb",
"type": "color"
}
},
"icon": {
"primary": {
"value": "#e2dfe7",
"type": "color"
},
"secondary": {
"value": "#8b8792",
"type": "color"
},
"muted": {
"value": "#8b8792",
"type": "color"
},
"placeholder": {
"value": "#7e7887",
"type": "color"
},
"active": {
"value": "#efecf4",
"type": "color"
},
"feature": {
"value": "#576ddb",
"type": "color"
},
"ok": {
"value": "#2a9292",
"type": "color"
},
"error": {
"value": "#be4678",
"type": "color"
},
"warning": {
"value": "#a06e3b",
"type": "color"
},
"info": {
"value": "#576ddb",
"type": "color"
}
},
"background": {
"100": {
"base": {
"value": "#332f38",
"type": "color"
},
"hovered": {
"value": "#3f3b45",
"type": "color"
},
"active": {
"value": "#4c4653",
"type": "color"
}
},
"300": {
"base": {
"value": "#26232a",
"type": "color"
},
"hovered": {
"value": "#332f38",
"type": "color"
},
"active": {
"value": "#3f3b45",
"type": "color"
}
},
"500": {
"base": {
"value": "#19171c",
"type": "color"
},
"hovered": {
"value": "#1c1a20",
"type": "color"
},
"active": {
"value": "#201d23",
"type": "color"
}
},
"on300": {
"base": {
"value": "#19171c",
"type": "color"
},
"hovered": {
"value": "#1c1a20",
"type": "color"
},
"active": {
"value": "#201d23",
"type": "color"
}
},
"on500": {
"base": {
"value": "#332f38",
"type": "color"
},
"hovered": {
"value": "#3f3b45",
"type": "color"
},
"active": {
"value": "#4c4653",
"type": "color"
}
},
"ok": {
"base": {
"value": "#2a929226",
"type": "color"
},
"hovered": {
"value": "#2a929233",
"type": "color"
},
"active": {
"value": "#2a929240",
"type": "color"
}
},
"error": {
"base": {
"value": "#be467826",
"type": "color"
},
"hovered": {
"value": "#be467833",
"type": "color"
},
"active": {
"value": "#be467840",
"type": "color"
}
},
"warning": {
"base": {
"value": "#a06e3b26",
"type": "color"
},
"hovered": {
"value": "#a06e3b33",
"type": "color"
},
"active": {
"value": "#a06e3b40",
"type": "color"
}
},
"info": {
"base": {
"value": "#576ddb26",
"type": "color"
},
"hovered": {
"value": "#576ddb33",
"type": "color"
},
"active": {
"value": "#576ddb40",
"type": "color"
}
}
},
"border": {
"primary": {
"value": "#19171c",
"type": "color"
},
"secondary": {
"value": "#26232a",
"type": "color"
},
"muted": {
"value": "#655f6d",
"type": "color"
},
"active": {
"value": "#655f6d",
"type": "color"
},
"onMedia": {
"value": "#19171c1a",
"type": "color"
},
"ok": {
"value": "#2a929226",
"type": "color"
},
"error": {
"value": "#be467826",
"type": "color"
},
"warning": {
"value": "#a06e3b26",
"type": "color"
},
"info": {
"value": "#576ddb26",
"type": "color"
}
},
"editor": {
"background": {
"value": "#19171c",
"type": "color"
},
"indent_guide": {
"value": "#655f6d",
"type": "color"
},
"indent_guide_active": {
"value": "#26232a",
"type": "color"
},
"line": {
"active": {
"value": "#26232a",
"type": "color"
},
"highlighted": {
"value": "#332f38",
"type": "color"
}
},
"highlight": {
"selection": {
"value": "#576ddb3d",
"type": "color"
},
"occurrence": {
"value": "#5852603d",
"type": "color"
},
"activeOccurrence": {
"value": "#5852607a",
"type": "color"
},
"matchingBracket": {
"value": "#201d23",
"type": "color"
},
"match": {
"value": "#3d1576",
"type": "color"
},
"activeMatch": {
"value": "#782edf7a",
"type": "color"
},
"related": {
"value": "#1c1a20",
"type": "color"
}
},
"gutter": {
"primary": {
"value": "#7e7887",
"type": "color"
},
"active": {
"value": "#efecf4",
"type": "color"
}
}
},
"syntax": {
"primary": {
"value": "#efecf4",
"type": "color"
},
"comment": {
"value": "#8b8792",
"type": "color"
},
"keyword": {
"value": "#576ddb",
"type": "color"
},
"function": {
"value": "#a06e3b",
"type": "color"
},
"type": {
"value": "#398bc6",
"type": "color"
},
"variant": {
"value": "#576ddb",
"type": "color"
},
"property": {
"value": "#576ddb",
"type": "color"
},
"enum": {
"value": "#aa573c",
"type": "color"
},
"operator": {
"value": "#aa573c",
"type": "color"
},
"string": {
"value": "#aa573c",
"type": "color"
},
"number": {
"value": "#2a9292",
"type": "color"
},
"boolean": {
"value": "#2a9292",
"type": "color"
}
},
"player": {
"1": {
"baseColor": {
"value": "#576ddb",
"type": "color"
},
"cursorColor": {
"value": "#576ddb",
"type": "color"
},
"selectionColor": {
"value": "#576ddb3d",
"type": "color"
},
"borderColor": {
"value": "#576ddbcc",
"type": "color"
}
},
"2": {
"baseColor": {
"value": "#2a9292",
"type": "color"
},
"cursorColor": {
"value": "#2a9292",
"type": "color"
},
"selectionColor": {
"value": "#2a92923d",
"type": "color"
},
"borderColor": {
"value": "#2a9292cc",
"type": "color"
}
},
"3": {
"baseColor": {
"value": "#bf40bf",
"type": "color"
},
"cursorColor": {
"value": "#bf40bf",
"type": "color"
},
"selectionColor": {
"value": "#bf40bf3d",
"type": "color"
},
"borderColor": {
"value": "#bf40bfcc",
"type": "color"
}
},
"4": {
"baseColor": {
"value": "#aa573c",
"type": "color"
},
"cursorColor": {
"value": "#aa573c",
"type": "color"
},
"selectionColor": {
"value": "#aa573c3d",
"type": "color"
},
"borderColor": {
"value": "#aa573ccc",
"type": "color"
}
},
"5": {
"baseColor": {
"value": "#955ae7",
"type": "color"
},
"cursorColor": {
"value": "#955ae7",
"type": "color"
},
"selectionColor": {
"value": "#955ae73d",
"type": "color"
},
"borderColor": {
"value": "#955ae7cc",
"type": "color"
}
},
"6": {
"baseColor": {
"value": "#398bc6",
"type": "color"
},
"cursorColor": {
"value": "#398bc6",
"type": "color"
},
"selectionColor": {
"value": "#398bc63d",
"type": "color"
},
"borderColor": {
"value": "#398bc6cc",
"type": "color"
}
},
"7": {
"baseColor": {
"value": "#be4678",
"type": "color"
},
"cursorColor": {
"value": "#be4678",
"type": "color"
},
"selectionColor": {
"value": "#be46783d",
"type": "color"
},
"borderColor": {
"value": "#be4678cc",
"type": "color"
}
},
"8": {
"baseColor": {
"value": "#a06e3b",
"type": "color"
},
"cursorColor": {
"value": "#a06e3b",
"type": "color"
},
"selectionColor": {
"value": "#a06e3b3d",
"type": "color"
},
"borderColor": {
"value": "#a06e3bcc",
"type": "color"
}
}
},
"shadowAlpha": {
"value": 0.24,
"type": "number"
}
}

519
styles/dist/cave-light.json vendored Normal file
View File

@ -0,0 +1,519 @@
{
"meta": {
"themeName": "cave-light"
},
"text": {
"primary": {
"value": "#26232a",
"type": "color"
},
"secondary": {
"value": "#585260",
"type": "color"
},
"muted": {
"value": "#585260",
"type": "color"
},
"placeholder": {
"value": "#655f6d",
"type": "color"
},
"active": {
"value": "#19171c",
"type": "color"
},
"feature": {
"value": "#576ddb",
"type": "color"
},
"ok": {
"value": "#2a9292",
"type": "color"
},
"error": {
"value": "#be4678",
"type": "color"
},
"warning": {
"value": "#a06e3b",
"type": "color"
},
"info": {
"value": "#576ddb",
"type": "color"
}
},
"icon": {
"primary": {
"value": "#26232a",
"type": "color"
},
"secondary": {
"value": "#585260",
"type": "color"
},
"muted": {
"value": "#585260",
"type": "color"
},
"placeholder": {
"value": "#655f6d",
"type": "color"
},
"active": {
"value": "#19171c",
"type": "color"
},
"feature": {
"value": "#576ddb",
"type": "color"
},
"ok": {
"value": "#2a9292",
"type": "color"
},
"error": {
"value": "#be4678",
"type": "color"
},
"warning": {
"value": "#a06e3b",
"type": "color"
},
"info": {
"value": "#576ddb",
"type": "color"
}
},
"background": {
"100": {
"base": {
"value": "#ccc9d2",
"type": "color"
},
"hovered": {
"value": "#b7b3bd",
"type": "color"
},
"active": {
"value": "#a19da7",
"type": "color"
}
},
"300": {
"base": {
"value": "#e2dfe7",
"type": "color"
},
"hovered": {
"value": "#ccc9d2",
"type": "color"
},
"active": {
"value": "#b7b3bd",
"type": "color"
}
},
"500": {
"base": {
"value": "#efecf4",
"type": "color"
},
"hovered": {
"value": "#ece9f1",
"type": "color"
},
"active": {
"value": "#e9e6ee",
"type": "color"
}
},
"on300": {
"base": {
"value": "#efecf4",
"type": "color"
},
"hovered": {
"value": "#ece9f1",
"type": "color"
},
"active": {
"value": "#e9e6ee",
"type": "color"
}
},
"on500": {
"base": {
"value": "#ccc9d2",
"type": "color"
},
"hovered": {
"value": "#b7b3bd",
"type": "color"
},
"active": {
"value": "#a19da7",
"type": "color"
}
},
"ok": {
"base": {
"value": "#2a929226",
"type": "color"
},
"hovered": {
"value": "#2a929233",
"type": "color"
},
"active": {
"value": "#2a929240",
"type": "color"
}
},
"error": {
"base": {
"value": "#be467826",
"type": "color"
},
"hovered": {
"value": "#be467833",
"type": "color"
},
"active": {
"value": "#be467840",
"type": "color"
}
},
"warning": {
"base": {
"value": "#a06e3b26",
"type": "color"
},
"hovered": {
"value": "#a06e3b33",
"type": "color"
},
"active": {
"value": "#a06e3b40",
"type": "color"
}
},
"info": {
"base": {
"value": "#576ddb26",
"type": "color"
},
"hovered": {
"value": "#576ddb33",
"type": "color"
},
"active": {
"value": "#576ddb40",
"type": "color"
}
}
},
"border": {
"primary": {
"value": "#b7b3bd",
"type": "color"
},
"secondary": {
"value": "#ccc9d2",
"type": "color"
},
"muted": {
"value": "#e2dfe7",
"type": "color"
},
"active": {
"value": "#655f6d",
"type": "color"
},
"onMedia": {
"value": "#efecf41a",
"type": "color"
},
"ok": {
"value": "#2a929226",
"type": "color"
},
"error": {
"value": "#be467826",
"type": "color"
},
"warning": {
"value": "#a06e3b26",
"type": "color"
},
"info": {
"value": "#576ddb26",
"type": "color"
}
},
"editor": {
"background": {
"value": "#efecf4",
"type": "color"
},
"indent_guide": {
"value": "#e2dfe7",
"type": "color"
},
"indent_guide_active": {
"value": "#ccc9d2",
"type": "color"
},
"line": {
"active": {
"value": "#e2dfe7",
"type": "color"
},
"highlighted": {
"value": "#ccc9d2",
"type": "color"
}
},
"highlight": {
"selection": {
"value": "#576ddb3d",
"type": "color"
},
"occurrence": {
"value": "#8b87921f",
"type": "color"
},
"activeOccurrence": {
"value": "#8b87923d",
"type": "color"
},
"matchingBracket": {
"value": "#e9e6ee",
"type": "color"
},
"match": {
"value": "#d5bdfa",
"type": "color"
},
"activeMatch": {
"value": "#a775ee3d",
"type": "color"
},
"related": {
"value": "#ece9f1",
"type": "color"
}
},
"gutter": {
"primary": {
"value": "#655f6d",
"type": "color"
},
"active": {
"value": "#19171c",
"type": "color"
}
}
},
"syntax": {
"primary": {
"value": "#19171c",
"type": "color"
},
"comment": {
"value": "#585260",
"type": "color"
},
"keyword": {
"value": "#576ddb",
"type": "color"
},
"function": {
"value": "#a06e3b",
"type": "color"
},
"type": {
"value": "#398bc6",
"type": "color"
},
"variant": {
"value": "#576ddb",
"type": "color"
},
"property": {
"value": "#576ddb",
"type": "color"
},
"enum": {
"value": "#aa573c",
"type": "color"
},
"operator": {
"value": "#aa573c",
"type": "color"
},
"string": {
"value": "#aa573c",
"type": "color"
},
"number": {
"value": "#2a9292",
"type": "color"
},
"boolean": {
"value": "#2a9292",
"type": "color"
}
},
"player": {
"1": {
"baseColor": {
"value": "#576ddb",
"type": "color"
},
"cursorColor": {
"value": "#576ddb",
"type": "color"
},
"selectionColor": {
"value": "#576ddb3d",
"type": "color"
},
"borderColor": {
"value": "#576ddbcc",
"type": "color"
}
},
"2": {
"baseColor": {
"value": "#2a9292",
"type": "color"
},
"cursorColor": {
"value": "#2a9292",
"type": "color"
},
"selectionColor": {
"value": "#2a92923d",
"type": "color"
},
"borderColor": {
"value": "#2a9292cc",
"type": "color"
}
},
"3": {
"baseColor": {
"value": "#bf40bf",
"type": "color"
},
"cursorColor": {
"value": "#bf40bf",
"type": "color"
},
"selectionColor": {
"value": "#bf40bf3d",
"type": "color"
},
"borderColor": {
"value": "#bf40bfcc",
"type": "color"
}
},
"4": {
"baseColor": {
"value": "#aa573c",
"type": "color"
},
"cursorColor": {
"value": "#aa573c",
"type": "color"
},
"selectionColor": {
"value": "#aa573c3d",
"type": "color"
},
"borderColor": {
"value": "#aa573ccc",
"type": "color"
}
},
"5": {
"baseColor": {
"value": "#955ae7",
"type": "color"
},
"cursorColor": {
"value": "#955ae7",
"type": "color"
},
"selectionColor": {
"value": "#955ae73d",
"type": "color"
},
"borderColor": {
"value": "#955ae7cc",
"type": "color"
}
},
"6": {
"baseColor": {
"value": "#398bc6",
"type": "color"
},
"cursorColor": {
"value": "#398bc6",
"type": "color"
},
"selectionColor": {
"value": "#398bc63d",
"type": "color"
},
"borderColor": {
"value": "#398bc6cc",
"type": "color"
}
},
"7": {
"baseColor": {
"value": "#be4678",
"type": "color"
},
"cursorColor": {
"value": "#be4678",
"type": "color"
},
"selectionColor": {
"value": "#be46783d",
"type": "color"
},
"borderColor": {
"value": "#be4678cc",
"type": "color"
}
},
"8": {
"baseColor": {
"value": "#a06e3b",
"type": "color"
},
"cursorColor": {
"value": "#a06e3b",
"type": "color"
},
"selectionColor": {
"value": "#a06e3b3d",
"type": "color"
},
"borderColor": {
"value": "#a06e3bcc",
"type": "color"
}
}
},
"shadowAlpha": {
"value": 0.12,
"type": "number"
}
}

View File

@ -216,15 +216,15 @@
},
"border": {
"primary": {
"value": "#93a1a1",
"value": "#c1c5bb",
"type": "color"
},
"secondary": {
"value": "#93a1a1",
"value": "#d7d6c8",
"type": "color"
},
"muted": {
"value": "#657b83",
"value": "#eee8d5",
"type": "color"
},
"active": {
@ -258,11 +258,11 @@
"type": "color"
},
"indent_guide": {
"value": "#657b83",
"value": "#eee8d5",
"type": "color"
},
"indent_guide_active": {
"value": "#93a1a1",
"value": "#d7d6c8",
"type": "color"
},
"line": {

519
styles/dist/sulphurpool-dark.json vendored Normal file
View File

@ -0,0 +1,519 @@
{
"meta": {
"themeName": "sulphurpool-dark"
},
"text": {
"primary": {
"value": "#dfe2f1",
"type": "color"
},
"secondary": {
"value": "#979db4",
"type": "color"
},
"muted": {
"value": "#979db4",
"type": "color"
},
"placeholder": {
"value": "#898ea4",
"type": "color"
},
"active": {
"value": "#f5f7ff",
"type": "color"
},
"feature": {
"value": "#3d8fd1",
"type": "color"
},
"ok": {
"value": "#ac9739",
"type": "color"
},
"error": {
"value": "#c94922",
"type": "color"
},
"warning": {
"value": "#c08b30",
"type": "color"
},
"info": {
"value": "#3d8fd1",
"type": "color"
}
},
"icon": {
"primary": {
"value": "#dfe2f1",
"type": "color"
},
"secondary": {
"value": "#979db4",
"type": "color"
},
"muted": {
"value": "#979db4",
"type": "color"
},
"placeholder": {
"value": "#898ea4",
"type": "color"
},
"active": {
"value": "#f5f7ff",
"type": "color"
},
"feature": {
"value": "#3d8fd1",
"type": "color"
},
"ok": {
"value": "#ac9739",
"type": "color"
},
"error": {
"value": "#c94922",
"type": "color"
},
"warning": {
"value": "#c08b30",
"type": "color"
},
"info": {
"value": "#3d8fd1",
"type": "color"
}
},
"background": {
"100": {
"base": {
"value": "#363f62",
"type": "color"
},
"hovered": {
"value": "#444c6f",
"type": "color"
},
"active": {
"value": "#51597b",
"type": "color"
}
},
"300": {
"base": {
"value": "#293256",
"type": "color"
},
"hovered": {
"value": "#363f62",
"type": "color"
},
"active": {
"value": "#444c6f",
"type": "color"
}
},
"500": {
"base": {
"value": "#202746",
"type": "color"
},
"hovered": {
"value": "#222a4a",
"type": "color"
},
"active": {
"value": "#252d4e",
"type": "color"
}
},
"on300": {
"base": {
"value": "#202746",
"type": "color"
},
"hovered": {
"value": "#222a4a",
"type": "color"
},
"active": {
"value": "#252d4e",
"type": "color"
}
},
"on500": {
"base": {
"value": "#363f62",
"type": "color"
},
"hovered": {
"value": "#444c6f",
"type": "color"
},
"active": {
"value": "#51597b",
"type": "color"
}
},
"ok": {
"base": {
"value": "#ac973926",
"type": "color"
},
"hovered": {
"value": "#ac973933",
"type": "color"
},
"active": {
"value": "#ac973940",
"type": "color"
}
},
"error": {
"base": {
"value": "#c9492226",
"type": "color"
},
"hovered": {
"value": "#c9492233",
"type": "color"
},
"active": {
"value": "#c9492240",
"type": "color"
}
},
"warning": {
"base": {
"value": "#c08b3026",
"type": "color"
},
"hovered": {
"value": "#c08b3033",
"type": "color"
},
"active": {
"value": "#c08b3040",
"type": "color"
}
},
"info": {
"base": {
"value": "#3d8fd126",
"type": "color"
},
"hovered": {
"value": "#3d8fd133",
"type": "color"
},
"active": {
"value": "#3d8fd140",
"type": "color"
}
}
},
"border": {
"primary": {
"value": "#202746",
"type": "color"
},
"secondary": {
"value": "#293256",
"type": "color"
},
"muted": {
"value": "#6b7394",
"type": "color"
},
"active": {
"value": "#6b7394",
"type": "color"
},
"onMedia": {
"value": "#2027461a",
"type": "color"
},
"ok": {
"value": "#ac973926",
"type": "color"
},
"error": {
"value": "#c9492226",
"type": "color"
},
"warning": {
"value": "#c08b3026",
"type": "color"
},
"info": {
"value": "#3d8fd126",
"type": "color"
}
},
"editor": {
"background": {
"value": "#202746",
"type": "color"
},
"indent_guide": {
"value": "#6b7394",
"type": "color"
},
"indent_guide_active": {
"value": "#293256",
"type": "color"
},
"line": {
"active": {
"value": "#293256",
"type": "color"
},
"highlighted": {
"value": "#363f62",
"type": "color"
}
},
"highlight": {
"selection": {
"value": "#3d8fd13d",
"type": "color"
},
"occurrence": {
"value": "#5e66873d",
"type": "color"
},
"activeOccurrence": {
"value": "#5e66877a",
"type": "color"
},
"matchingBracket": {
"value": "#252d4e",
"type": "color"
},
"match": {
"value": "#1a2a6d",
"type": "color"
},
"activeMatch": {
"value": "#3d56c47a",
"type": "color"
},
"related": {
"value": "#222a4a",
"type": "color"
}
},
"gutter": {
"primary": {
"value": "#898ea4",
"type": "color"
},
"active": {
"value": "#f5f7ff",
"type": "color"
}
}
},
"syntax": {
"primary": {
"value": "#f5f7ff",
"type": "color"
},
"comment": {
"value": "#979db4",
"type": "color"
},
"keyword": {
"value": "#3d8fd1",
"type": "color"
},
"function": {
"value": "#c08b30",
"type": "color"
},
"type": {
"value": "#22a2c9",
"type": "color"
},
"variant": {
"value": "#3d8fd1",
"type": "color"
},
"property": {
"value": "#3d8fd1",
"type": "color"
},
"enum": {
"value": "#c76b29",
"type": "color"
},
"operator": {
"value": "#c76b29",
"type": "color"
},
"string": {
"value": "#c76b29",
"type": "color"
},
"number": {
"value": "#ac9739",
"type": "color"
},
"boolean": {
"value": "#ac9739",
"type": "color"
}
},
"player": {
"1": {
"baseColor": {
"value": "#3d8fd1",
"type": "color"
},
"cursorColor": {
"value": "#3d8fd1",
"type": "color"
},
"selectionColor": {
"value": "#3d8fd13d",
"type": "color"
},
"borderColor": {
"value": "#3d8fd1cc",
"type": "color"
}
},
"2": {
"baseColor": {
"value": "#ac9739",
"type": "color"
},
"cursorColor": {
"value": "#ac9739",
"type": "color"
},
"selectionColor": {
"value": "#ac97393d",
"type": "color"
},
"borderColor": {
"value": "#ac9739cc",
"type": "color"
}
},
"3": {
"baseColor": {
"value": "#9c637a",
"type": "color"
},
"cursorColor": {
"value": "#9c637a",
"type": "color"
},
"selectionColor": {
"value": "#9c637a3d",
"type": "color"
},
"borderColor": {
"value": "#9c637acc",
"type": "color"
}
},
"4": {
"baseColor": {
"value": "#c76b29",
"type": "color"
},
"cursorColor": {
"value": "#c76b29",
"type": "color"
},
"selectionColor": {
"value": "#c76b293d",
"type": "color"
},
"borderColor": {
"value": "#c76b29cc",
"type": "color"
}
},
"5": {
"baseColor": {
"value": "#6679cc",
"type": "color"
},
"cursorColor": {
"value": "#6679cc",
"type": "color"
},
"selectionColor": {
"value": "#6679cc3d",
"type": "color"
},
"borderColor": {
"value": "#6679cccc",
"type": "color"
}
},
"6": {
"baseColor": {
"value": "#22a2c9",
"type": "color"
},
"cursorColor": {
"value": "#22a2c9",
"type": "color"
},
"selectionColor": {
"value": "#22a2c93d",
"type": "color"
},
"borderColor": {
"value": "#22a2c9cc",
"type": "color"
}
},
"7": {
"baseColor": {
"value": "#c94922",
"type": "color"
},
"cursorColor": {
"value": "#c94922",
"type": "color"
},
"selectionColor": {
"value": "#c949223d",
"type": "color"
},
"borderColor": {
"value": "#c94922cc",
"type": "color"
}
},
"8": {
"baseColor": {
"value": "#c08b30",
"type": "color"
},
"cursorColor": {
"value": "#c08b30",
"type": "color"
},
"selectionColor": {
"value": "#c08b303d",
"type": "color"
},
"borderColor": {
"value": "#c08b30cc",
"type": "color"
}
}
},
"shadowAlpha": {
"value": 0.24,
"type": "number"
}
}

519
styles/dist/sulphurpool-light.json vendored Normal file
View File

@ -0,0 +1,519 @@
{
"meta": {
"themeName": "sulphurpool-light"
},
"text": {
"primary": {
"value": "#293256",
"type": "color"
},
"secondary": {
"value": "#5e6687",
"type": "color"
},
"muted": {
"value": "#5e6687",
"type": "color"
},
"placeholder": {
"value": "#6b7394",
"type": "color"
},
"active": {
"value": "#202746",
"type": "color"
},
"feature": {
"value": "#3d8fd1",
"type": "color"
},
"ok": {
"value": "#ac9739",
"type": "color"
},
"error": {
"value": "#c94922",
"type": "color"
},
"warning": {
"value": "#c08b30",
"type": "color"
},
"info": {
"value": "#3d8fd1",
"type": "color"
}
},
"icon": {
"primary": {
"value": "#293256",
"type": "color"
},
"secondary": {
"value": "#5e6687",
"type": "color"
},
"muted": {
"value": "#5e6687",
"type": "color"
},
"placeholder": {
"value": "#6b7394",
"type": "color"
},
"active": {
"value": "#202746",
"type": "color"
},
"feature": {
"value": "#3d8fd1",
"type": "color"
},
"ok": {
"value": "#ac9739",
"type": "color"
},
"error": {
"value": "#c94922",
"type": "color"
},
"warning": {
"value": "#c08b30",
"type": "color"
},
"info": {
"value": "#3d8fd1",
"type": "color"
}
},
"background": {
"100": {
"base": {
"value": "#cdd1e2",
"type": "color"
},
"hovered": {
"value": "#bbc0d3",
"type": "color"
},
"active": {
"value": "#a9aec3",
"type": "color"
}
},
"300": {
"base": {
"value": "#dfe2f1",
"type": "color"
},
"hovered": {
"value": "#cdd1e2",
"type": "color"
},
"active": {
"value": "#bbc0d3",
"type": "color"
}
},
"500": {
"base": {
"value": "#f5f7ff",
"type": "color"
},
"hovered": {
"value": "#f0f2fc",
"type": "color"
},
"active": {
"value": "#eaedf8",
"type": "color"
}
},
"on300": {
"base": {
"value": "#f5f7ff",
"type": "color"
},
"hovered": {
"value": "#f0f2fc",
"type": "color"
},
"active": {
"value": "#eaedf8",
"type": "color"
}
},
"on500": {
"base": {
"value": "#cdd1e2",
"type": "color"
},
"hovered": {
"value": "#bbc0d3",
"type": "color"
},
"active": {
"value": "#a9aec3",
"type": "color"
}
},
"ok": {
"base": {
"value": "#ac973926",
"type": "color"
},
"hovered": {
"value": "#ac973933",
"type": "color"
},
"active": {
"value": "#ac973940",
"type": "color"
}
},
"error": {
"base": {
"value": "#c9492226",
"type": "color"
},
"hovered": {
"value": "#c9492233",
"type": "color"
},
"active": {
"value": "#c9492240",
"type": "color"
}
},
"warning": {
"base": {
"value": "#c08b3026",
"type": "color"
},
"hovered": {
"value": "#c08b3033",
"type": "color"
},
"active": {
"value": "#c08b3040",
"type": "color"
}
},
"info": {
"base": {
"value": "#3d8fd126",
"type": "color"
},
"hovered": {
"value": "#3d8fd133",
"type": "color"
},
"active": {
"value": "#3d8fd140",
"type": "color"
}
}
},
"border": {
"primary": {
"value": "#bbc0d3",
"type": "color"
},
"secondary": {
"value": "#cdd1e2",
"type": "color"
},
"muted": {
"value": "#dfe2f1",
"type": "color"
},
"active": {
"value": "#6b7394",
"type": "color"
},
"onMedia": {
"value": "#f5f7ff1a",
"type": "color"
},
"ok": {
"value": "#ac973926",
"type": "color"
},
"error": {
"value": "#c9492226",
"type": "color"
},
"warning": {
"value": "#c08b3026",
"type": "color"
},
"info": {
"value": "#3d8fd126",
"type": "color"
}
},
"editor": {
"background": {
"value": "#f5f7ff",
"type": "color"
},
"indent_guide": {
"value": "#dfe2f1",
"type": "color"
},
"indent_guide_active": {
"value": "#cdd1e2",
"type": "color"
},
"line": {
"active": {
"value": "#dfe2f1",
"type": "color"
},
"highlighted": {
"value": "#cdd1e2",
"type": "color"
}
},
"highlight": {
"selection": {
"value": "#3d8fd13d",
"type": "color"
},
"occurrence": {
"value": "#979db41f",
"type": "color"
},
"activeOccurrence": {
"value": "#979db43d",
"type": "color"
},
"matchingBracket": {
"value": "#eaedf8",
"type": "color"
},
"match": {
"value": "#bcc6f7",
"type": "color"
},
"activeMatch": {
"value": "#7b8ddc3d",
"type": "color"
},
"related": {
"value": "#f0f2fc",
"type": "color"
}
},
"gutter": {
"primary": {
"value": "#6b7394",
"type": "color"
},
"active": {
"value": "#202746",
"type": "color"
}
}
},
"syntax": {
"primary": {
"value": "#202746",
"type": "color"
},
"comment": {
"value": "#5e6687",
"type": "color"
},
"keyword": {
"value": "#3d8fd1",
"type": "color"
},
"function": {
"value": "#c08b30",
"type": "color"
},
"type": {
"value": "#22a2c9",
"type": "color"
},
"variant": {
"value": "#3d8fd1",
"type": "color"
},
"property": {
"value": "#3d8fd1",
"type": "color"
},
"enum": {
"value": "#c76b29",
"type": "color"
},
"operator": {
"value": "#c76b29",
"type": "color"
},
"string": {
"value": "#c76b29",
"type": "color"
},
"number": {
"value": "#ac9739",
"type": "color"
},
"boolean": {
"value": "#ac9739",
"type": "color"
}
},
"player": {
"1": {
"baseColor": {
"value": "#3d8fd1",
"type": "color"
},
"cursorColor": {
"value": "#3d8fd1",
"type": "color"
},
"selectionColor": {
"value": "#3d8fd13d",
"type": "color"
},
"borderColor": {
"value": "#3d8fd1cc",
"type": "color"
}
},
"2": {
"baseColor": {
"value": "#ac9739",
"type": "color"
},
"cursorColor": {
"value": "#ac9739",
"type": "color"
},
"selectionColor": {
"value": "#ac97393d",
"type": "color"
},
"borderColor": {
"value": "#ac9739cc",
"type": "color"
}
},
"3": {
"baseColor": {
"value": "#9c637a",
"type": "color"
},
"cursorColor": {
"value": "#9c637a",
"type": "color"
},
"selectionColor": {
"value": "#9c637a3d",
"type": "color"
},
"borderColor": {
"value": "#9c637acc",
"type": "color"
}
},
"4": {
"baseColor": {
"value": "#c76b29",
"type": "color"
},
"cursorColor": {
"value": "#c76b29",
"type": "color"
},
"selectionColor": {
"value": "#c76b293d",
"type": "color"
},
"borderColor": {
"value": "#c76b29cc",
"type": "color"
}
},
"5": {
"baseColor": {
"value": "#6679cc",
"type": "color"
},
"cursorColor": {
"value": "#6679cc",
"type": "color"
},
"selectionColor": {
"value": "#6679cc3d",
"type": "color"
},
"borderColor": {
"value": "#6679cccc",
"type": "color"
}
},
"6": {
"baseColor": {
"value": "#22a2c9",
"type": "color"
},
"cursorColor": {
"value": "#22a2c9",
"type": "color"
},
"selectionColor": {
"value": "#22a2c93d",
"type": "color"
},
"borderColor": {
"value": "#22a2c9cc",
"type": "color"
}
},
"7": {
"baseColor": {
"value": "#c94922",
"type": "color"
},
"cursorColor": {
"value": "#c94922",
"type": "color"
},
"selectionColor": {
"value": "#c949223d",
"type": "color"
},
"borderColor": {
"value": "#c94922cc",
"type": "color"
}
},
"8": {
"baseColor": {
"value": "#c08b30",
"type": "color"
},
"cursorColor": {
"value": "#c08b30",
"type": "color"
},
"selectionColor": {
"value": "#c08b303d",
"type": "color"
},
"borderColor": {
"value": "#c08b30cc",
"type": "color"
}
}
},
"shadowAlpha": {
"value": 0.12,
"type": "number"
}
}

View File

@ -1917,15 +1917,15 @@
},
"border": {
"primary": {
"value": "#8b8792",
"value": "#b7b3bd",
"type": "color"
},
"secondary": {
"value": "#8b8792",
"value": "#ccc9d2",
"type": "color"
},
"muted": {
"value": "#655f6d",
"value": "#e2dfe7",
"type": "color"
},
"active": {
@ -1959,11 +1959,11 @@
"type": "color"
},
"indent_guide": {
"value": "#655f6d",
"value": "#e2dfe7",
"type": "color"
},
"indent_guide_active": {
"value": "#8b8792",
"value": "#ccc9d2",
"type": "color"
},
"line": {
@ -2955,15 +2955,15 @@
},
"border": {
"primary": {
"value": "#93a1a1",
"value": "#c1c5bb",
"type": "color"
},
"secondary": {
"value": "#93a1a1",
"value": "#d7d6c8",
"type": "color"
},
"muted": {
"value": "#657b83",
"value": "#eee8d5",
"type": "color"
},
"active": {
@ -2997,11 +2997,11 @@
"type": "color"
},
"indent_guide": {
"value": "#657b83",
"value": "#eee8d5",
"type": "color"
},
"indent_guide_active": {
"value": "#93a1a1",
"value": "#d7d6c8",
"type": "color"
},
"line": {
@ -3993,15 +3993,15 @@
},
"border": {
"primary": {
"value": "#979db4",
"value": "#bbc0d3",
"type": "color"
},
"secondary": {
"value": "#979db4",
"value": "#cdd1e2",
"type": "color"
},
"muted": {
"value": "#6b7394",
"value": "#dfe2f1",
"type": "color"
},
"active": {
@ -4035,11 +4035,11 @@
"type": "color"
},
"indent_guide": {
"value": "#6b7394",
"value": "#dfe2f1",
"type": "color"
},
"indent_guide_active": {
"value": "#979db4",
"value": "#cdd1e2",
"type": "color"
},
"line": {

View File

@ -42,6 +42,7 @@ export default function contactsPanel(theme: Theme) {
return {
...panel,
padding: { top: panel.padding.top, bottom: 0 },
userQueryEditor: {
background: backgroundColor(theme, 500),
cornerRadius: 6,
@ -136,5 +137,16 @@ export default function contactsPanel(theme: Theme) {
background: backgroundColor(theme, 300, "active"),
}
},
inviteRow: {
padding: {
left: sidePadding,
right: sidePadding
},
border: { top: true, width: 1, color: borderColor(theme, "primary") },
text: text(theme, "sans", "primary", { size: "sm" }),
hover: {
text: text(theme, "sans", "primary", { size: "sm", underline: true })
}
}
}
}