Make scrolling up in chat panel smoother

This increases the threshold at which we start loading new messages
as well as the amount of messages we get back from the server every
time we fetch.

Also, we restructured the seed binary to use the methods in `Db` to
generate seed data and added random chat messages.

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
Antonio Scandurra 2021-09-02 16:05:34 +02:00
parent 6a071e865f
commit a27a17b8e2
7 changed files with 183 additions and 125 deletions

11
Cargo.lock generated
View file

@ -2736,6 +2736,16 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "lipsum"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ee7271c76a89032dcc7e595c0a739a9c5514eab483deb0e82981fe2098c56a"
dependencies = [
"rand 0.8.3",
"rand_chacha 0.3.0",
]
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.2" version = "0.4.2"
@ -5872,6 +5882,7 @@ dependencies = [
"http-auth-basic", "http-auth-basic",
"jwt-simple", "jwt-simple",
"lazy_static", "lazy_static",
"lipsum",
"oauth2", "oauth2",
"oauth2-surf", "oauth2-surf",
"parking_lot", "parking_lot",

View file

@ -2,4 +2,4 @@
set -e set -e
cd server cd server
cargo run --bin seed cargo run --features seed-dependencies --bin seed

View file

@ -5,6 +5,10 @@ edition = "2018"
name = "zed-server" name = "zed-server"
version = "0.1.0" version = "0.1.0"
[[bin]]
name = "seed"
required-features = ["seed-support"]
[dependencies] [dependencies]
anyhow = "1.0.40" anyhow = "1.0.40"
async-std = { version = "1.8.0", features = ["attributes"] } async-std = { version = "1.8.0", features = ["attributes"] }
@ -19,6 +23,7 @@ futures = "0.3"
handlebars = "3.5" handlebars = "3.5"
http-auth-basic = "0.1.3" http-auth-basic = "0.1.3"
jwt-simple = "0.10.0" jwt-simple = "0.10.0"
lipsum = { version = "0.8", optional = true }
oauth2 = { version = "4.0.0", default_features = false } oauth2 = { version = "4.0.0", default_features = false }
oauth2-surf = "0.1.1" oauth2-surf = "0.1.1"
parking_lot = "0.11.1" parking_lot = "0.11.1"
@ -46,6 +51,9 @@ features = ["runtime-async-std-rustls", "postgres", "time"]
[dev-dependencies] [dev-dependencies]
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
zed = { path = "../zed", features = ["test-support"] }
lazy_static = "1.4" lazy_static = "1.4"
serde_json = { version = "1.0.64", features = ["preserve_order"] } serde_json = { version = "1.0.64", features = ["preserve_order"] }
zed = { path = "../zed", features = ["test-support"] }
[features]
seed-support = ["lipsum"]

View file

@ -1,6 +1,11 @@
use sqlx::postgres::PgPoolOptions; use db::{Db, UserId};
use rand::prelude::*;
use tide::log; use tide::log;
use time::{Duration, OffsetDateTime};
#[allow(unused)]
#[path = "../db.rs"]
mod db;
#[path = "../env.rs"] #[path = "../env.rs"]
mod env; mod env;
@ -13,95 +18,74 @@ async fn main() {
); );
} }
let mut rng = StdRng::from_entropy();
let database_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL env var"); let database_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL env var");
let db = PgPoolOptions::new() let db = Db::new(&database_url, 5)
.max_connections(5)
.connect(&database_url)
.await .await
.expect("failed to connect to postgres database"); .expect("failed to connect to postgres database");
let zed_users = ["nathansobo", "maxbrunsfeld", "as-cii", "iamnbutler"]; let zed_users = ["nathansobo", "maxbrunsfeld", "as-cii", "iamnbutler"];
let mut zed_user_ids = Vec::<i32>::new(); let mut zed_user_ids = Vec::<UserId>::new();
for zed_user in zed_users { for zed_user in zed_users {
zed_user_ids.push( if let Some(user_id) = db.get_user(zed_user).await.expect("failed to fetch user") {
sqlx::query_scalar( zed_user_ids.push(user_id);
r#" } else {
INSERT INTO users zed_user_ids.push(
(github_login, admin) db.create_user(zed_user, true)
VALUES .await
($1, true) .expect("failed to insert user"),
ON CONFLICT (github_login) DO UPDATE SET );
github_login=EXCLUDED.github_login }
RETURNING id
"#,
)
.bind(zed_user)
.fetch_one(&db)
.await
.expect("failed to insert user"),
)
} }
let zed_org_id: i32 = sqlx::query_scalar( let zed_org_id = if let Some(org) = db
r#" .find_org_by_slug("zed")
INSERT INTO orgs .await
(name, slug) .expect("failed to fetch org")
VALUES {
('Zed', 'zed') org.id
ON CONFLICT (slug) DO UPDATE SET } else {
slug=EXCLUDED.slug db.create_org("Zed", "zed")
RETURNING id .await
"#, .expect("failed to insert org")
) };
.fetch_one(&db)
.await
.expect("failed to insert org");
let general_channel_id: i32 = sqlx::query_scalar( let general_channel_id = if let Some(channel) = db
r#" .get_org_channels(zed_org_id)
INSERT INTO channels .await
(owner_is_user, owner_id, name) .expect("failed to fetch channels")
VALUES .iter()
(false, $1, 'General') .find(|c| c.name == "General")
ON CONFLICT (owner_is_user, owner_id, name) DO UPDATE SET {
name=EXCLUDED.name channel.id
RETURNING id } else {
"#, let channel_id = db
) .create_org_channel(zed_org_id, "General")
.bind(zed_org_id) .await
.fetch_one(&db) .expect("failed to insert channel");
.await
.expect("failed to insert channel"); let now = OffsetDateTime::now_utc();
let max_seconds = Duration::days(100).as_seconds_f64();
let mut timestamps = (0..1000)
.map(|_| now - Duration::seconds_f64(rng.gen_range(0_f64..=max_seconds)))
.collect::<Vec<_>>();
timestamps.sort();
for timestamp in timestamps {
let sender_id = *zed_user_ids.choose(&mut rng).unwrap();
let body = lipsum::lipsum_words(rng.gen_range(1..=50));
db.create_channel_message(channel_id, sender_id, &body, timestamp)
.await
.expect("failed to insert message");
}
channel_id
};
for user_id in zed_user_ids { for user_id in zed_user_ids {
sqlx::query( db.add_org_member(zed_org_id, user_id, true)
r#" .await
INSERT INTO org_memberships .expect("failed to insert org membership");
(org_id, user_id, admin) db.add_channel_member(general_channel_id, user_id, true)
VALUES .await
($1, $2, true) .expect("failed to insert channel membership");
ON CONFLICT DO NOTHING
"#,
)
.bind(zed_org_id)
.bind(user_id)
.execute(&db)
.await
.expect("failed to insert org membership");
sqlx::query(
r#"
INSERT INTO channel_memberships
(channel_id, user_id, admin)
VALUES
($1, $2, true)
ON CONFLICT DO NOTHING
"#,
)
.bind(general_channel_id)
.bind(user_id)
.execute(&db)
.await
.expect("failed to insert channel membership");
} }
} }

View file

@ -27,35 +27,6 @@ pub struct Db {
test_mode: bool, test_mode: bool,
} }
#[derive(Debug, FromRow, Serialize)]
pub struct User {
pub id: UserId,
pub github_login: String,
pub admin: bool,
}
#[derive(Debug, FromRow, Serialize)]
pub struct Signup {
pub id: SignupId,
pub github_login: String,
pub email_address: String,
pub about: String,
}
#[derive(Debug, FromRow, Serialize)]
pub struct Channel {
pub id: ChannelId,
pub name: String,
}
#[derive(Debug, FromRow)]
pub struct ChannelMessage {
pub id: MessageId,
pub sender_id: UserId,
pub body: String,
pub sent_at: OffsetDateTime,
}
impl Db { impl Db {
pub async fn new(url: &str, max_connections: u32) -> tide::Result<Self> { pub async fn new(url: &str, max_connections: u32) -> tide::Result<Self> {
let pool = DbOptions::new() let pool = DbOptions::new()
@ -113,6 +84,22 @@ impl Db {
// users // users
#[allow(unused)] // Help rust-analyzer
#[cfg(any(test, feature = "seed-support"))]
pub async fn get_user(&self, github_login: &str) -> Result<Option<UserId>> {
test_support!(self, {
let query = "
SELECT id
FROM users
WHERE github_login = $1
";
sqlx::query_scalar(query)
.bind(github_login)
.fetch_optional(&self.pool)
.await
})
}
pub async fn create_user(&self, github_login: &str, admin: bool) -> Result<UserId> { pub async fn create_user(&self, github_login: &str, admin: bool) -> Result<UserId> {
test_support!(self, { test_support!(self, {
let query = " let query = "
@ -231,7 +218,23 @@ impl Db {
// orgs // orgs
#[cfg(test)] #[allow(unused)] // Help rust-analyzer
#[cfg(any(test, feature = "seed-support"))]
pub async fn find_org_by_slug(&self, slug: &str) -> Result<Option<Org>> {
test_support!(self, {
let query = "
SELECT *
FROM orgs
WHERE slug = $1
";
sqlx::query_as(query)
.bind(slug)
.fetch_optional(&self.pool)
.await
})
}
#[cfg(any(test, feature = "seed-support"))]
pub async fn create_org(&self, name: &str, slug: &str) -> Result<OrgId> { pub async fn create_org(&self, name: &str, slug: &str) -> Result<OrgId> {
test_support!(self, { test_support!(self, {
let query = " let query = "
@ -248,7 +251,7 @@ impl Db {
}) })
} }
#[cfg(test)] #[cfg(any(test, feature = "seed-support"))]
pub async fn add_org_member( pub async fn add_org_member(
&self, &self,
org_id: OrgId, org_id: OrgId,
@ -259,6 +262,7 @@ impl Db {
let query = " let query = "
INSERT INTO org_memberships (org_id, user_id, admin) INSERT INTO org_memberships (org_id, user_id, admin)
VALUES ($1, $2, $3) VALUES ($1, $2, $3)
ON CONFLICT DO NOTHING
"; ";
sqlx::query(query) sqlx::query(query)
.bind(org_id.0) .bind(org_id.0)
@ -272,7 +276,7 @@ impl Db {
// channels // channels
#[cfg(test)] #[cfg(any(test, feature = "seed-support"))]
pub async fn create_org_channel(&self, org_id: OrgId, name: &str) -> Result<ChannelId> { pub async fn create_org_channel(&self, org_id: OrgId, name: &str) -> Result<ChannelId> {
test_support!(self, { test_support!(self, {
let query = " let query = "
@ -289,7 +293,25 @@ impl Db {
}) })
} }
pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<Vec<Channel>> { #[allow(unused)] // Help rust-analyzer
#[cfg(any(test, feature = "seed-support"))]
pub async fn get_org_channels(&self, org_id: OrgId) -> Result<Vec<Channel>> {
test_support!(self, {
let query = "
SELECT *
FROM channels
WHERE
channels.owner_is_user = false AND
channels.owner_id = $1
";
sqlx::query_as(query)
.bind(org_id.0)
.fetch_all(&self.pool)
.await
})
}
pub async fn get_accessible_channels(&self, user_id: UserId) -> Result<Vec<Channel>> {
test_support!(self, { test_support!(self, {
let query = " let query = "
SELECT SELECT
@ -328,7 +350,7 @@ impl Db {
}) })
} }
#[cfg(test)] #[cfg(any(test, feature = "seed-support"))]
pub async fn add_channel_member( pub async fn add_channel_member(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
@ -339,6 +361,7 @@ impl Db {
let query = " let query = "
INSERT INTO channel_memberships (channel_id, user_id, admin) INSERT INTO channel_memberships (channel_id, user_id, admin)
VALUES ($1, $2, $3) VALUES ($1, $2, $3)
ON CONFLICT DO NOTHING
"; ";
sqlx::query(query) sqlx::query(query)
.bind(channel_id.0) .bind(channel_id.0)
@ -432,10 +455,45 @@ macro_rules! id_type {
} }
id_type!(UserId); id_type!(UserId);
#[derive(Debug, FromRow, Serialize)]
pub struct User {
pub id: UserId,
pub github_login: String,
pub admin: bool,
}
id_type!(OrgId); id_type!(OrgId);
id_type!(ChannelId); #[derive(FromRow)]
pub struct Org {
pub id: OrgId,
pub name: String,
pub slug: String,
}
id_type!(SignupId); id_type!(SignupId);
#[derive(Debug, FromRow, Serialize)]
pub struct Signup {
pub id: SignupId,
pub github_login: String,
pub email_address: String,
pub about: String,
}
id_type!(ChannelId);
#[derive(Debug, FromRow, Serialize)]
pub struct Channel {
pub id: ChannelId,
pub name: String,
}
id_type!(MessageId); id_type!(MessageId);
#[derive(Debug, FromRow)]
pub struct ChannelMessage {
pub id: MessageId,
pub sender_id: UserId,
pub body: String,
pub sent_at: OffsetDateTime,
}
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {

View file

@ -77,12 +77,7 @@ struct Channel {
connection_ids: HashSet<ConnectionId>, connection_ids: HashSet<ConnectionId>,
} }
#[cfg(debug_assertions)] const MESSAGE_COUNT_PER_PAGE: usize = 100;
const MESSAGE_COUNT_PER_PAGE: usize = 10;
#[cfg(not(debug_assertions))]
const MESSAGE_COUNT_PER_PAGE: usize = 50;
const MAX_MESSAGE_LEN: usize = 1024; const MAX_MESSAGE_LEN: usize = 1024;
impl Server { impl Server {
@ -528,7 +523,7 @@ impl Server {
.read() .read()
.await .await
.user_id_for_connection(request.sender_id)?; .user_id_for_connection(request.sender_id)?;
let channels = self.app_state.db.get_channels_for_user(user_id).await?; let channels = self.app_state.db.get_accessible_channels(user_id).await?;
self.peer self.peer
.respond( .respond(
request.receipt(), request.receipt(),

View file

@ -16,6 +16,8 @@ use gpui::{
use postage::watch; use postage::watch;
use time::{OffsetDateTime, UtcOffset}; use time::{OffsetDateTime, UtcOffset};
const MESSAGE_LOADING_THRESHOLD: usize = 50;
pub struct ChatPanel { pub struct ChatPanel {
channel_list: ModelHandle<ChannelList>, channel_list: ModelHandle<ChannelList>,
active_channel: Option<(ModelHandle<Channel>, Subscription)>, active_channel: Option<(ModelHandle<Channel>, Subscription)>,
@ -80,7 +82,7 @@ impl ChatPanel {
} }
}); });
message_list.set_scroll_handler(|visible_range, cx| { message_list.set_scroll_handler(|visible_range, cx| {
if visible_range.start < 5 { if visible_range.start < MESSAGE_LOADING_THRESHOLD {
cx.dispatch_action(LoadMoreMessages); cx.dispatch_action(LoadMoreMessages);
} }
}); });