Fetch and wait for channels when opening channel notes via URL (#33291)

Release Notes:

* Collaboration: Now fetches and waits for channels when opening channel
notes via URL.
This commit is contained in:
Michael Sloan 2025-06-23 23:14:15 -06:00 committed by GitHub
parent 24c94d474e
commit 786e724684
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 99 additions and 49 deletions

1
Cargo.lock generated
View file

@ -2697,6 +2697,7 @@ dependencies = [
"http_client", "http_client",
"language", "language",
"log", "log",
"postage",
"rand 0.8.5", "rand 0.8.5",
"release_channel", "release_channel",
"rpc", "rpc",

View file

@ -24,6 +24,7 @@ futures.workspace = true
gpui.workspace = true gpui.workspace = true
language.workspace = true language.workspace = true
log.workspace = true log.workspace = true
postage.workspace = true
rand.workspace = true rand.workspace = true
release_channel.workspace = true release_channel.workspace = true
rpc.workspace = true rpc.workspace = true

View file

@ -4,13 +4,14 @@ use crate::{ChannelMessage, channel_buffer::ChannelBuffer, channel_chat::Channel
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
use channel_index::ChannelIndex; use channel_index::ChannelIndex;
use client::{ChannelId, Client, ClientSettings, Subscription, User, UserId, UserStore}; use client::{ChannelId, Client, ClientSettings, Subscription, User, UserId, UserStore};
use collections::{HashMap, HashSet, hash_map}; use collections::{HashMap, HashSet};
use futures::{Future, FutureExt, StreamExt, channel::mpsc, future::Shared}; use futures::{Future, FutureExt, StreamExt, channel::mpsc, future::Shared};
use gpui::{ use gpui::{
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, SharedString, Task, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, SharedString, Task,
WeakEntity, WeakEntity,
}; };
use language::Capability; use language::Capability;
use postage::{sink::Sink, watch};
use rpc::{ use rpc::{
TypedEnvelope, TypedEnvelope,
proto::{self, ChannelRole, ChannelVisibility}, proto::{self, ChannelRole, ChannelVisibility},
@ -43,6 +44,7 @@ pub struct ChannelStore {
opened_chats: HashMap<ChannelId, OpenEntityHandle<ChannelChat>>, opened_chats: HashMap<ChannelId, OpenEntityHandle<ChannelChat>>,
client: Arc<Client>, client: Arc<Client>,
did_subscribe: bool, did_subscribe: bool,
channels_loaded: (watch::Sender<bool>, watch::Receiver<bool>),
user_store: Entity<UserStore>, user_store: Entity<UserStore>,
_rpc_subscriptions: [Subscription; 2], _rpc_subscriptions: [Subscription; 2],
_watch_connection_status: Task<Option<()>>, _watch_connection_status: Task<Option<()>>,
@ -219,6 +221,7 @@ impl ChannelStore {
}), }),
channel_states: Default::default(), channel_states: Default::default(),
did_subscribe: false, did_subscribe: false,
channels_loaded: watch::channel_with(false),
} }
} }
@ -234,6 +237,48 @@ impl ChannelStore {
} }
} }
pub fn wait_for_channels(
&mut self,
timeout: Duration,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let mut channels_loaded_rx = self.channels_loaded.1.clone();
if *channels_loaded_rx.borrow() {
return Task::ready(Ok(()));
}
let mut status_receiver = self.client.status();
if status_receiver.borrow().is_connected() {
self.initialize();
}
let mut timer = cx.background_executor().timer(timeout).fuse();
cx.spawn(async move |this, cx| {
loop {
futures::select_biased! {
channels_loaded = channels_loaded_rx.next().fuse() => {
if let Some(true) = channels_loaded {
return Ok(());
}
}
status = status_receiver.next().fuse() => {
if let Some(status) = status {
if status.is_connected() {
this.update(cx, |this, _cx| {
this.initialize();
}).ok();
}
}
continue;
}
_ = timer => {
return Err(anyhow!("{:?} elapsed without receiving channels", timeout));
}
}
}
})
}
pub fn client(&self) -> Arc<Client> { pub fn client(&self) -> Arc<Client> {
self.client.clone() self.client.clone()
} }
@ -309,6 +354,7 @@ impl ChannelStore {
let channel_store = cx.entity(); let channel_store = cx.entity();
self.open_channel_resource( self.open_channel_resource(
channel_id, channel_id,
"notes",
|this| &mut this.opened_buffers, |this| &mut this.opened_buffers,
async move |channel, cx| { async move |channel, cx| {
ChannelBuffer::new(channel, client, user_store, channel_store, cx).await ChannelBuffer::new(channel, client, user_store, channel_store, cx).await
@ -439,6 +485,7 @@ impl ChannelStore {
let this = cx.entity(); let this = cx.entity();
self.open_channel_resource( self.open_channel_resource(
channel_id, channel_id,
"chat",
|this| &mut this.opened_chats, |this| &mut this.opened_chats,
async move |channel, cx| ChannelChat::new(channel, this, user_store, client, cx).await, async move |channel, cx| ChannelChat::new(channel, this, user_store, client, cx).await,
cx, cx,
@ -453,6 +500,7 @@ impl ChannelStore {
fn open_channel_resource<T, F>( fn open_channel_resource<T, F>(
&mut self, &mut self,
channel_id: ChannelId, channel_id: ChannelId,
resource_name: &'static str,
get_map: fn(&mut Self) -> &mut HashMap<ChannelId, OpenEntityHandle<T>>, get_map: fn(&mut Self) -> &mut HashMap<ChannelId, OpenEntityHandle<T>>,
load: F, load: F,
cx: &mut Context<Self>, cx: &mut Context<Self>,
@ -462,58 +510,56 @@ impl ChannelStore {
T: 'static, T: 'static,
{ {
let task = loop { let task = loop {
match get_map(self).entry(channel_id) { match get_map(self).get(&channel_id) {
hash_map::Entry::Occupied(e) => match e.get() { Some(OpenEntityHandle::Open(entity)) => {
OpenEntityHandle::Open(entity) => { if let Some(entity) = entity.upgrade() {
if let Some(entity) = entity.upgrade() { break Task::ready(Ok(entity)).shared();
break Task::ready(Ok(entity)).shared(); } else {
} else { get_map(self).remove(&channel_id);
get_map(self).remove(&channel_id); continue;
continue;
}
} }
OpenEntityHandle::Loading(task) => {
break task.clone();
}
},
hash_map::Entry::Vacant(e) => {
let task = cx
.spawn(async move |this, cx| {
let channel = this.read_with(cx, |this, _| {
this.channel_for_id(channel_id).cloned().ok_or_else(|| {
Arc::new(anyhow!("no channel for id: {channel_id}"))
})
})??;
load(channel, cx).await.map_err(Arc::new)
})
.shared();
e.insert(OpenEntityHandle::Loading(task.clone()));
cx.spawn({
let task = task.clone();
async move |this, cx| {
let result = task.await;
this.update(cx, |this, _| match result {
Ok(model) => {
get_map(this).insert(
channel_id,
OpenEntityHandle::Open(model.downgrade()),
);
}
Err(_) => {
get_map(this).remove(&channel_id);
}
})
.ok();
}
})
.detach();
break task;
} }
Some(OpenEntityHandle::Loading(task)) => break task.clone(),
None => {}
} }
let channels_ready = self.wait_for_channels(Duration::from_secs(10), cx);
let task = cx
.spawn(async move |this, cx| {
channels_ready.await?;
let channel = this.read_with(cx, |this, _| {
this.channel_for_id(channel_id)
.cloned()
.ok_or_else(|| Arc::new(anyhow!("no channel for id: {channel_id}")))
})??;
load(channel, cx).await.map_err(Arc::new)
})
.shared();
get_map(self).insert(channel_id, OpenEntityHandle::Loading(task.clone()));
let task = cx.spawn({
async move |this, cx| {
let result = task.await;
this.update(cx, |this, _| match &result {
Ok(model) => {
get_map(this)
.insert(channel_id, OpenEntityHandle::Open(model.downgrade()));
}
Err(_) => {
get_map(this).remove(&channel_id);
}
})?;
result
}
});
break task.shared();
}; };
cx.background_spawn(async move { task.await.map_err(|error| anyhow!("{error}")) }) cx.background_spawn(async move {
task.await.map_err(|error| {
anyhow!("{error}").context(format!("failed to open channel {resource_name}"))
})
})
} }
pub fn is_channel_admin(&self, channel_id: ChannelId) -> bool { pub fn is_channel_admin(&self, channel_id: ChannelId) -> bool {
@ -1147,6 +1193,8 @@ impl ChannelStore {
.or_default() .or_default()
.update_latest_message_id(latest_channel_message.message_id); .update_latest_message_id(latest_channel_message.message_id);
} }
self.channels_loaded.0.try_send(true).log_err();
} }
cx.notify(); cx.notify();