logiguard fork v3: full patch set on verified 8c74db0 tree

Includes prior-session patches (carry forward so the app compiles):
  - crates/gpui/build.rs: cross-compile manifest fix
  - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method
  - crates/gpui/src/window.rs: Window::activate_with_token public API
  - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix

Plus the focus-serial fix:
  - serial.rs: SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; latest_serial_of()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 01:52:12 +03:30
commit b9819977a5
3984 changed files with 1487015 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
[package]
name = "notifications"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/notifications.rs"
doctest = false
[features]
test-support = [
"channel/test-support",
"gpui/test-support",
"rpc/test-support",
]
[dependencies]
anyhow.workspace = true
channel.workspace = true
client.workspace = true
futures-lite.workspace = true
component.workspace = true
gpui.workspace = true
rpc.workspace = true
sum_tree.workspace = true
time.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
zed_actions.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
rpc = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }

View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -0,0 +1,428 @@
use anyhow::{Context as _, Result};
use channel::ChannelStore;
use client::{ChannelId, Client, UserStore};
use futures_lite::stream::StreamExt;
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, Task};
use rpc::{Notification, TypedEnvelope, proto};
use std::{ops::Range, sync::Arc};
use sum_tree::{Bias, Dimensions, SumTree};
use time::OffsetDateTime;
use util::ResultExt;
pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
let notification_store = cx.new(|cx| NotificationStore::new(client, user_store, cx));
cx.set_global(GlobalNotificationStore(notification_store));
}
struct GlobalNotificationStore(Entity<NotificationStore>);
impl Global for GlobalNotificationStore {}
pub struct NotificationStore {
client: Arc<Client>,
user_store: Entity<UserStore>,
channel_store: Entity<ChannelStore>,
notifications: SumTree<NotificationEntry>,
loaded_all_notifications: bool,
_watch_connection_status: Task<Option<()>>,
_subscriptions: Vec<client::Subscription>,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum NotificationEvent {
NotificationsUpdated {
old_range: Range<usize>,
new_count: usize,
},
NewNotification {
entry: NotificationEntry,
},
NotificationRemoved {
entry: NotificationEntry,
},
NotificationRead {
entry: NotificationEntry,
},
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct NotificationEntry {
pub id: u64,
pub notification: Notification,
pub timestamp: OffsetDateTime,
pub is_read: bool,
pub response: Option<bool>,
}
#[derive(Clone, Debug, Default)]
pub struct NotificationSummary {
max_id: u64,
count: usize,
unread_count: usize,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
struct Count(usize);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
struct NotificationId(u64);
impl NotificationStore {
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalNotificationStore>().0.clone()
}
pub fn new(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut Context<Self>) -> Self {
let mut connection_status = client.status();
let watch_connection_status = cx.spawn(async move |this, cx| {
while let Some(status) = connection_status.next().await {
let this = this.upgrade()?;
match status {
client::Status::Connected { .. } => {
if let Some(task) = this.update(cx, |this, cx| this.handle_connect(cx)) {
task.await.log_err()?;
}
}
_ => {
this.update(cx, |this, cx| this.handle_disconnect(cx));
}
}
}
Some(())
});
Self {
channel_store: ChannelStore::global(cx),
notifications: Default::default(),
loaded_all_notifications: false,
_watch_connection_status: watch_connection_status,
_subscriptions: vec![
client.add_message_handler(cx.weak_entity(), Self::handle_new_notification),
client.add_message_handler(cx.weak_entity(), Self::handle_delete_notification),
],
user_store,
client,
}
}
pub fn notification_count(&self) -> usize {
self.notifications.summary().count
}
pub fn unread_notification_count(&self) -> usize {
self.notifications.summary().unread_count
}
// Get the nth newest notification.
pub fn notification_at(&self, ix: usize) -> Option<&NotificationEntry> {
let count = self.notifications.summary().count;
if ix >= count {
return None;
}
let ix = count - 1 - ix;
let (.., item) = self
.notifications
.find::<Count, _>((), &Count(ix), Bias::Right);
item
}
pub fn notification_for_id(&self, id: u64) -> Option<&NotificationEntry> {
let (.., item) =
self.notifications
.find::<NotificationId, _>((), &NotificationId(id), Bias::Left);
if let Some(item) = item
&& item.id == id
{
return Some(item);
}
None
}
pub fn load_more_notifications(
&self,
clear_old: bool,
cx: &mut Context<Self>,
) -> Option<Task<Result<()>>> {
if self.loaded_all_notifications && !clear_old {
return None;
}
let before_id = if clear_old {
None
} else {
self.notifications.first().map(|entry| entry.id)
};
let request = self.client.request(proto::GetNotifications { before_id });
Some(cx.spawn(async move |this, cx| {
let this = this
.upgrade()
.context("Notification store was dropped while loading notifications")?;
let response = request.await?;
this.update(cx, |this, _| this.loaded_all_notifications = response.done);
Self::add_notifications(
this,
response.notifications,
AddNotificationsOptions {
is_new: false,
clear_old,
includes_first: response.done,
},
cx,
)
.await?;
Ok(())
}))
}
fn handle_connect(&mut self, cx: &mut Context<Self>) -> Option<Task<Result<()>>> {
self.notifications = Default::default();
cx.notify();
self.load_more_notifications(true, cx)
}
fn handle_disconnect(&mut self, cx: &mut Context<Self>) {
cx.notify()
}
async fn handle_new_notification(
this: Entity<Self>,
envelope: TypedEnvelope<proto::AddNotification>,
mut cx: AsyncApp,
) -> Result<()> {
Self::add_notifications(
this,
envelope.payload.notification.into_iter().collect(),
AddNotificationsOptions {
is_new: true,
clear_old: false,
includes_first: false,
},
&mut cx,
)
.await
}
async fn handle_delete_notification(
this: Entity<Self>,
envelope: TypedEnvelope<proto::DeleteNotification>,
mut cx: AsyncApp,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.splice_notifications([(envelope.payload.notification_id, None)], false, cx);
});
Ok(())
}
async fn add_notifications(
this: Entity<Self>,
notifications: Vec<proto::Notification>,
options: AddNotificationsOptions,
cx: &mut AsyncApp,
) -> Result<()> {
let mut user_ids = Vec::new();
let notifications = notifications
.into_iter()
.filter_map(|message| {
Some(NotificationEntry {
id: message.id,
is_read: message.is_read,
timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)
.ok()?,
notification: Notification::from_proto(&message)?,
response: message.response,
})
})
.collect::<Vec<_>>();
if notifications.is_empty() {
return Ok(());
}
for entry in &notifications {
match entry.notification {
Notification::ChannelInvitation { inviter_id, .. } => {
user_ids.push(inviter_id);
}
Notification::ContactRequest {
sender_id: requester_id,
} => {
user_ids.push(requester_id);
}
Notification::ContactRequestAccepted {
responder_id: contact_id,
} => {
user_ids.push(contact_id);
}
}
}
let user_store = this.read_with(cx, |this, _| this.user_store.clone());
user_store
.update(cx, |store, cx| store.get_users(user_ids, cx))
.await?;
this.update(cx, |this, cx| {
if options.clear_old {
cx.emit(NotificationEvent::NotificationsUpdated {
old_range: 0..this.notifications.summary().count,
new_count: 0,
});
this.notifications = SumTree::default();
this.loaded_all_notifications = false;
}
if options.includes_first {
this.loaded_all_notifications = true;
}
this.splice_notifications(
notifications
.into_iter()
.map(|notification| (notification.id, Some(notification))),
options.is_new,
cx,
);
});
Ok(())
}
fn splice_notifications(
&mut self,
notifications: impl IntoIterator<Item = (u64, Option<NotificationEntry>)>,
is_new: bool,
cx: &mut Context<NotificationStore>,
) {
let mut cursor = self
.notifications
.cursor::<Dimensions<NotificationId, Count>>(());
let mut new_notifications = SumTree::default();
let mut old_range = 0..0;
for (i, (id, new_notification)) in notifications.into_iter().enumerate() {
new_notifications.append(cursor.slice(&NotificationId(id), Bias::Left), ());
if i == 0 {
old_range.start = cursor.start().1.0;
}
let old_notification = cursor.item();
if let Some(old_notification) = old_notification {
if old_notification.id == id {
cursor.next();
if let Some(new_notification) = &new_notification {
if new_notification.is_read {
cx.emit(NotificationEvent::NotificationRead {
entry: new_notification.clone(),
});
}
} else {
cx.emit(NotificationEvent::NotificationRemoved {
entry: old_notification.clone(),
});
}
}
} else if let Some(new_notification) = &new_notification
&& is_new
{
cx.emit(NotificationEvent::NewNotification {
entry: new_notification.clone(),
});
}
if let Some(notification) = new_notification {
new_notifications.push(notification, ());
}
}
old_range.end = cursor.start().1.0;
let new_count = new_notifications.summary().count - old_range.start;
new_notifications.append(cursor.suffix(), ());
drop(cursor);
self.notifications = new_notifications;
cx.emit(NotificationEvent::NotificationsUpdated {
old_range,
new_count,
});
}
pub fn respond_to_notification(
&mut self,
notification: Notification,
response: bool,
cx: &mut Context<Self>,
) {
match notification {
Notification::ContactRequest { sender_id } => {
self.user_store
.update(cx, |store, cx| {
store.respond_to_contact_request(sender_id, response, cx)
})
.detach();
}
Notification::ChannelInvitation { channel_id, .. } => {
self.channel_store
.update(cx, |store, cx| {
store.respond_to_channel_invite(ChannelId(channel_id), response, cx)
})
.detach();
}
_ => {}
}
}
}
impl EventEmitter<NotificationEvent> for NotificationStore {}
impl sum_tree::Item for NotificationEntry {
type Summary = NotificationSummary;
fn summary(&self, _cx: ()) -> Self::Summary {
NotificationSummary {
max_id: self.id,
count: 1,
unread_count: if self.is_read { 0 } else { 1 },
}
}
}
impl sum_tree::ContextLessSummary for NotificationSummary {
fn zero() -> Self {
Default::default()
}
fn add_summary(&mut self, summary: &Self) {
self.max_id = self.max_id.max(summary.max_id);
self.count += summary.count;
self.unread_count += summary.unread_count;
}
}
impl sum_tree::Dimension<'_, NotificationSummary> for NotificationId {
fn zero(_cx: ()) -> Self {
Default::default()
}
fn add_summary(&mut self, summary: &NotificationSummary, _: ()) {
debug_assert!(summary.max_id > self.0);
self.0 = summary.max_id;
}
}
impl sum_tree::Dimension<'_, NotificationSummary> for Count {
fn zero(_cx: ()) -> Self {
Default::default()
}
fn add_summary(&mut self, summary: &NotificationSummary, _: ()) {
self.0 += summary.count;
}
}
struct AddNotificationsOptions {
is_new: bool,
clear_old: bool,
includes_first: bool,
}

View File

@@ -0,0 +1,4 @@
mod notification_store;
pub use notification_store::*;
pub mod status_toast;

View File

@@ -0,0 +1,257 @@
use std::rc::Rc;
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement};
use ui::{Tooltip, prelude::*};
use workspace::{ToastAction, ToastView};
use zed_actions::toast;
#[derive(RegisterComponent)]
pub struct StatusToast {
icon: Option<Icon>,
text: SharedString,
action: Option<ToastAction>,
show_dismiss: bool,
auto_dismiss: bool,
this_handle: Entity<Self>,
focus_handle: FocusHandle,
}
impl StatusToast {
pub fn new(
text: impl Into<SharedString>,
cx: &mut App,
f: impl FnOnce(Self, &mut Context<Self>) -> Self,
) -> Entity<Self> {
cx.new(|cx| {
let focus_handle = cx.focus_handle();
f(
Self {
text: text.into(),
icon: None,
action: None,
show_dismiss: false,
auto_dismiss: true,
this_handle: cx.entity(),
focus_handle,
},
cx,
)
})
}
pub fn icon(mut self, icon: Icon) -> Self {
self.icon = Some(icon);
self
}
pub fn auto_dismiss(mut self, auto_dismiss: bool) -> Self {
self.auto_dismiss = auto_dismiss;
self
}
pub fn action(
mut self,
label: impl Into<SharedString>,
f: impl Fn(&mut Window, &mut App) + 'static,
) -> Self {
let this_handle = self.this_handle.clone();
self.action = Some(ToastAction::new(
label.into(),
Some(Rc::new(move |window, cx| {
this_handle.update(cx, |_, cx| {
cx.emit(DismissEvent);
});
f(window, cx);
})),
));
self
}
pub fn dismiss_button(mut self, show: bool) -> Self {
self.show_dismiss = show;
self
}
}
impl Render for StatusToast {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let has_action_or_dismiss = self.action.is_some() || self.show_dismiss;
h_flex()
.id("status-toast")
.elevation_3(cx)
.gap_2()
.py_1p5()
.pl_2p5()
.map(|this| {
if has_action_or_dismiss {
this.pr_1p5()
} else {
this.pr_2p5()
}
})
.flex_none()
.bg(cx.theme().colors().surface_background)
.shadow_lg()
.when_some(self.icon.clone(), |this, icon| this.child(icon))
.child(Label::new(self.text.clone()).color(Color::Default))
.when_some(self.action.as_ref(), |this, action| {
this.child(
Button::new(action.id.clone(), action.label.clone())
.tooltip(Tooltip::for_action_title(
action.label.clone(),
&toast::RunAction,
))
.color(Color::Muted)
.when_some(action.on_click.clone(), |el, handler| {
el.on_click(move |_click_event, window, cx| handler(window, cx))
}),
)
})
.when(self.show_dismiss, |this| {
let handle = self.this_handle.clone();
this.child(
IconButton::new("dismiss", IconName::Close)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(Tooltip::text("Dismiss"))
.on_click(move |_click_event, _window, cx| {
handle.update(cx, |_, cx| {
cx.emit(DismissEvent);
});
}),
)
})
}
}
impl ToastView for StatusToast {
fn action(&self) -> Option<ToastAction> {
self.action.clone()
}
fn auto_dismiss(&self) -> bool {
self.auto_dismiss
}
}
impl Focusable for StatusToast {
fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl EventEmitter<DismissEvent> for StatusToast {}
impl Component for StatusToast {
fn scope() -> ComponentScope {
ComponentScope::Notification
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let text_example = StatusToast::new("Operation completed", cx, |this, _| this);
let action_example = StatusToast::new("Update ready to install", cx, |this, _cx| {
this.action("Restart", |_, _| {})
});
let dismiss_button_example =
StatusToast::new("Dismiss Button", cx, |this, _| this.dismiss_button(true));
let icon_example = StatusToast::new(
"Nathan Sobo accepted your contact request",
cx,
|this, _| {
this.icon(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Muted),
)
},
);
let success_example = StatusToast::new("Pushed 4 changes to `zed/main`", cx, |this, _| {
this.icon(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
});
let error_example = StatusToast::new(
"git push: Couldn't find remote origin `iamnbutler/zed`",
cx,
|this, _cx| {
this.icon(
Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error),
)
.action("More Info", |_, _| {})
},
);
let warning_example = StatusToast::new("You have outdated settings", cx, |this, _cx| {
this.icon(
Icon::new(IconName::Warning)
.size(IconSize::Small)
.color(Color::Warning),
)
.action("More Info", |_, _| {})
});
let pr_example =
StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| {
this.icon(
Icon::new(IconName::GitBranch)
.size(IconSize::Small)
.color(Color::Muted),
)
.action("Open Pull Request", |_, cx| {
cx.open_url("https://github.com/")
})
});
Some(
v_flex()
.gap_6()
.p_4()
.children(vec![
example_group_with_title(
"Basic Toast",
vec![
single_example("Text", div().child(text_example).into_any_element()),
single_example(
"Action",
div().child(action_example).into_any_element(),
),
single_example("Icon", div().child(icon_example).into_any_element()),
single_example(
"Dismiss Button",
div().child(dismiss_button_example).into_any_element(),
),
],
),
example_group_with_title(
"Examples",
vec![
single_example(
"Success",
div().child(success_example).into_any_element(),
),
single_example("Error", div().child(error_example).into_any_element()),
single_example(
"Warning",
div().child(warning_example).into_any_element(),
),
single_example("Create PR", div().child(pr_example).into_any_element()),
],
)
.vertical(),
])
.into_any_element(),
)
}
}