1use auto_update::{AutoUpdater, release_notes_url};
2use editor::{Editor, MultiBuffer};
3use gpui::{
4 App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Window, actions, prelude::*,
5};
6use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
7use release_channel::{AppVersion, ReleaseChannel};
8use semver::Version;
9use serde::Deserialize;
10use smol::io::AsyncReadExt;
11use ui::{AnnouncementToast, ListBulletItem, prelude::*};
12use util::{ResultExt as _, maybe};
13use workspace::{
14 Workspace,
15 notifications::{
16 ErrorMessagePrompt, Notification, NotificationId, SuppressEvent, show_app_notification,
17 simple_message_notification::MessageNotification,
18 },
19};
20
21actions!(
22 auto_update,
23 [
24 /// Opens the release notes for the current version in a new tab.
25 ViewReleaseNotesLocally
26 ]
27);
28
29pub fn init(cx: &mut App) {
30 notify_if_app_was_updated(cx);
31 cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
32 workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, window, cx| {
33 view_release_notes_locally(workspace, window, cx);
34 });
35 })
36 .detach();
37}
38
39#[derive(Deserialize)]
40struct ReleaseNotesBody {
41 title: String,
42 release_notes: String,
43}
44
45fn notify_release_notes_failed_to_show(
46 workspace: &mut Workspace,
47 _window: &mut Window,
48 cx: &mut Context<Workspace>,
49) {
50 struct ViewReleaseNotesError;
51 workspace.show_notification(
52 NotificationId::unique::<ViewReleaseNotesError>(),
53 cx,
54 |cx| {
55 cx.new(move |cx| {
56 let url = release_notes_url(cx);
57 let mut prompt = ErrorMessagePrompt::new("Couldn't load release notes", cx);
58 if let Some(url) = url {
59 prompt = prompt.with_link_button("View in Browser".to_string(), url);
60 }
61 prompt
62 })
63 },
64 );
65}
66
67fn view_release_notes_locally(
68 workspace: &mut Workspace,
69 window: &mut Window,
70 cx: &mut Context<Workspace>,
71) {
72 let release_channel = ReleaseChannel::global(cx);
73
74 if matches!(
75 release_channel,
76 ReleaseChannel::Nightly | ReleaseChannel::Dev
77 ) {
78 if let Some(url) = release_notes_url(cx) {
79 cx.open_url(&url);
80 }
81 return;
82 }
83
84 let version = AppVersion::global(cx).to_string();
85
86 let client = client::Client::global(cx).http_client();
87 let url = client.build_url(&format!(
88 "/api/release_notes/v2/{}/{}",
89 release_channel.dev_name(),
90 version
91 ));
92
93 let markdown = workspace
94 .app_state()
95 .languages
96 .language_for_name("Markdown");
97
98 cx.spawn_in(window, async move |workspace, cx| {
99 let markdown = markdown.await.log_err();
100 let response = client.get(&url, Default::default(), true).await;
101 let Some(mut response) = response.log_err() else {
102 workspace
103 .update_in(cx, notify_release_notes_failed_to_show)
104 .log_err();
105 return;
106 };
107
108 let mut body = Vec::new();
109 response.body_mut().read_to_end(&mut body).await.ok();
110
111 let body: serde_json::Result<ReleaseNotesBody> = serde_json::from_slice(body.as_slice());
112
113 let res: Option<()> = maybe!(async {
114 let body = body.ok()?;
115 let project = workspace
116 .read_with(cx, |workspace, _| workspace.project().clone())
117 .ok()?;
118 let (language_registry, buffer) = project.update(cx, |project, cx| {
119 (
120 project.languages().clone(),
121 project.create_buffer(markdown, false, cx),
122 )
123 });
124 let buffer = buffer.await.ok()?;
125 buffer.update(cx, |buffer, cx| {
126 buffer.edit([(0..0, body.release_notes)], None, cx)
127 });
128
129 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(body.title));
130
131 let ws_handle = workspace.clone();
132 workspace
133 .update_in(cx, |workspace, window, cx| {
134 let editor =
135 cx.new(|cx| Editor::for_multibuffer(buffer, Some(project), window, cx));
136 let markdown_preview: Entity<MarkdownPreviewView> = MarkdownPreviewView::new(
137 MarkdownPreviewMode::Default,
138 editor,
139 ws_handle,
140 language_registry,
141 window,
142 cx,
143 );
144 workspace.add_item_to_active_pane(
145 Box::new(markdown_preview),
146 None,
147 true,
148 window,
149 cx,
150 );
151 cx.notify();
152 })
153 .ok()
154 })
155 .await;
156 if res.is_none() {
157 workspace
158 .update_in(cx, notify_release_notes_failed_to_show)
159 .log_err();
160 }
161 })
162 .detach();
163}
164
165#[derive(Clone)]
166struct AnnouncementContent {
167 heading: SharedString,
168 description: SharedString,
169 bullet_items: Vec<SharedString>,
170 primary_action_label: SharedString,
171 primary_action_url: Option<SharedString>,
172}
173
174fn announcement_for_version(version: &Version) -> Option<AnnouncementContent> {
175 #[allow(clippy::match_single_binding)]
176 match (version.major, version.minor, version.patch) {
177 // TODO: Add real version when we have it
178 // (0, 225, 0) => Some(AnnouncementContent {
179 // heading: "What's new in Zed 0.225".into(),
180 // description: "This release includes some exciting improvements.".into(),
181 // bullet_items: vec![
182 // "Improved agent performance".into(),
183 // "New agentic features".into(),
184 // "Better agent capabilities".into(),
185 // ],
186 // primary_action_label: "Learn More".into(),
187 // primary_action_url: Some("https://zed.dev/".into()),
188 // }),
189 _ => None,
190 }
191}
192
193struct AnnouncementToastNotification {
194 focus_handle: FocusHandle,
195 content: AnnouncementContent,
196}
197
198impl AnnouncementToastNotification {
199 fn new(content: AnnouncementContent, cx: &mut App) -> Self {
200 Self {
201 focus_handle: cx.focus_handle(),
202 content,
203 }
204 }
205}
206
207impl Focusable for AnnouncementToastNotification {
208 fn focus_handle(&self, _cx: &App) -> FocusHandle {
209 self.focus_handle.clone()
210 }
211}
212
213impl EventEmitter<DismissEvent> for AnnouncementToastNotification {}
214impl EventEmitter<SuppressEvent> for AnnouncementToastNotification {}
215impl Notification for AnnouncementToastNotification {}
216
217impl Render for AnnouncementToastNotification {
218 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
219 AnnouncementToast::new()
220 .heading(self.content.heading.clone())
221 .description(self.content.description.clone())
222 .bullet_items(
223 self.content
224 .bullet_items
225 .iter()
226 .map(|item| ListBulletItem::new(item.clone())),
227 )
228 .primary_action_label(self.content.primary_action_label.clone())
229 .primary_on_click(cx.listener({
230 let url = self.content.primary_action_url.clone();
231 move |_, _, _window, cx| {
232 if let Some(url) = &url {
233 cx.open_url(url);
234 }
235 cx.emit(DismissEvent);
236 }
237 }))
238 .secondary_on_click(cx.listener({
239 let url = self.content.primary_action_url.clone();
240 move |_, _, _window, cx| {
241 if let Some(url) = &url {
242 cx.open_url(url);
243 }
244 cx.emit(DismissEvent);
245 }
246 }))
247 .dismiss_on_click(cx.listener(|_, _, _window, cx| {
248 cx.emit(DismissEvent);
249 }))
250 }
251}
252
253/// Shows a notification across all workspaces if an update was previously automatically installed
254/// and this notification had not yet been shown.
255pub fn notify_if_app_was_updated(cx: &mut App) {
256 let Some(updater) = AutoUpdater::get(cx) else {
257 return;
258 };
259
260 if let ReleaseChannel::Nightly = ReleaseChannel::global(cx) {
261 return;
262 }
263
264 struct UpdateNotification;
265
266 let should_show_notification = updater.read(cx).should_show_update_notification(cx);
267 cx.spawn(async move |cx| {
268 let should_show_notification = should_show_notification.await?;
269 // if true { // Hardcode it to true for testing it outside of the component preview
270 if should_show_notification {
271 cx.update(|cx| {
272 let mut version = updater.read(cx).current_version();
273 version.build = semver::BuildMetadata::EMPTY;
274 version.pre = semver::Prerelease::EMPTY;
275 let app_name = ReleaseChannel::global(cx).display_name();
276
277 if let Some(content) = announcement_for_version(&version) {
278 show_app_notification(
279 NotificationId::unique::<UpdateNotification>(),
280 cx,
281 move |cx| {
282 cx.new(|cx| AnnouncementToastNotification::new(content.clone(), cx))
283 },
284 );
285 } else {
286 show_app_notification(
287 NotificationId::unique::<UpdateNotification>(),
288 cx,
289 move |cx| {
290 let workspace_handle = cx.entity().downgrade();
291 cx.new(|cx| {
292 MessageNotification::new(
293 format!("Updated to {app_name} {}", version),
294 cx,
295 )
296 .primary_message("View Release Notes")
297 .primary_on_click(move |window, cx| {
298 if let Some(workspace) = workspace_handle.upgrade() {
299 workspace.update(cx, |workspace, cx| {
300 crate::view_release_notes_locally(
301 workspace, window, cx,
302 );
303 })
304 }
305 cx.emit(DismissEvent);
306 })
307 .show_suppress_button(false)
308 })
309 },
310 );
311 }
312
313 updater.update(cx, |updater, cx| {
314 updater
315 .set_should_show_update_notification(false, cx)
316 .detach_and_log_err(cx);
317 });
318 });
319 }
320 anyhow::Ok(())
321 })
322 .detach();
323}