1use auto_update::{AutoUpdater, release_notes_url};
2use editor::{Editor, MultiBuffer};
3use gpui::{App, Context, DismissEvent, Entity, Window, actions, prelude::*};
4use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
5use release_channel::{AppVersion, ReleaseChannel};
6use serde::Deserialize;
7use smol::io::AsyncReadExt;
8use util::{ResultExt as _, maybe};
9use workspace::Workspace;
10use workspace::notifications::ErrorMessagePrompt;
11use workspace::notifications::simple_message_notification::MessageNotification;
12use workspace::notifications::{NotificationId, show_app_notification};
13
14actions!(
15 auto_update,
16 [
17 /// Opens the release notes for the current version in a new tab.
18 ViewReleaseNotesLocally
19 ]
20);
21
22pub fn init(cx: &mut App) {
23 notify_if_app_was_updated(cx);
24 cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
25 workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, window, cx| {
26 view_release_notes_locally(workspace, window, cx);
27 });
28 })
29 .detach();
30}
31
32#[derive(Deserialize)]
33struct ReleaseNotesBody {
34 #[expect(
35 unused,
36 reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove"
37 )]
38 title: String,
39 release_notes: String,
40}
41
42fn notify_release_notes_failed_to_show(
43 workspace: &mut Workspace,
44 _window: &mut Window,
45 cx: &mut Context<Workspace>,
46) {
47 struct ViewReleaseNotesError;
48 workspace.show_notification(
49 NotificationId::unique::<ViewReleaseNotesError>(),
50 cx,
51 |cx| {
52 cx.new(move |cx| {
53 let url = release_notes_url(cx);
54 let mut prompt = ErrorMessagePrompt::new("Couldn't load release notes", cx);
55 if let Some(url) = url {
56 prompt = prompt.with_link_button("View in Browser".to_string(), url);
57 }
58 prompt
59 })
60 },
61 );
62}
63
64fn view_release_notes_locally(
65 workspace: &mut Workspace,
66 window: &mut Window,
67 cx: &mut Context<Workspace>,
68) {
69 let release_channel = ReleaseChannel::global(cx);
70
71 if matches!(
72 release_channel,
73 ReleaseChannel::Nightly | ReleaseChannel::Dev
74 ) {
75 if let Some(url) = release_notes_url(cx) {
76 cx.open_url(&url);
77 }
78 return;
79 }
80
81 let version = AppVersion::global(cx).to_string();
82
83 let client = client::Client::global(cx).http_client();
84 let url = client.build_url(&format!(
85 "/api/release_notes/v2/{}/{}",
86 release_channel.dev_name(),
87 version
88 ));
89
90 let markdown = workspace
91 .app_state()
92 .languages
93 .language_for_name("Markdown");
94
95 cx.spawn_in(window, async move |workspace, cx| {
96 let markdown = markdown.await.log_err();
97 let response = client.get(&url, Default::default(), true).await;
98 let Some(mut response) = response.log_err() else {
99 workspace
100 .update_in(cx, notify_release_notes_failed_to_show)
101 .log_err();
102 return;
103 };
104
105 let mut body = Vec::new();
106 response.body_mut().read_to_end(&mut body).await.ok();
107
108 let body: serde_json::Result<ReleaseNotesBody> = serde_json::from_slice(body.as_slice());
109
110 let res: Option<()> = maybe!(async {
111 let body = body.ok()?;
112 let project = workspace
113 .read_with(cx, |workspace, _| workspace.project().clone())
114 .ok()?;
115 let (language_registry, buffer) = project.update(cx, |project, cx| {
116 (
117 project.languages().clone(),
118 project.create_buffer(markdown, false, cx),
119 )
120 });
121 let buffer = buffer.await.ok()?;
122 buffer.update(cx, |buffer, cx| {
123 buffer.edit([(0..0, body.release_notes)], None, cx)
124 });
125
126 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
127
128 let ws_handle = workspace.clone();
129 workspace
130 .update_in(cx, |workspace, window, cx| {
131 let editor =
132 cx.new(|cx| Editor::for_multibuffer(buffer, Some(project), window, cx));
133 let markdown_preview: Entity<MarkdownPreviewView> = MarkdownPreviewView::new(
134 MarkdownPreviewMode::Default,
135 editor,
136 ws_handle,
137 language_registry,
138 window,
139 cx,
140 );
141 workspace.add_item_to_active_pane(
142 Box::new(markdown_preview),
143 None,
144 true,
145 window,
146 cx,
147 );
148 cx.notify();
149 })
150 .ok()
151 })
152 .await;
153 if res.is_none() {
154 workspace
155 .update_in(cx, notify_release_notes_failed_to_show)
156 .log_err();
157 }
158 })
159 .detach();
160}
161
162/// Shows a notification across all workspaces if an update was previously automatically installed
163/// and this notification had not yet been shown.
164pub fn notify_if_app_was_updated(cx: &mut App) {
165 let Some(updater) = AutoUpdater::get(cx) else {
166 return;
167 };
168
169 if let ReleaseChannel::Nightly = ReleaseChannel::global(cx) {
170 return;
171 }
172
173 struct UpdateNotification;
174
175 let should_show_notification = updater.read(cx).should_show_update_notification(cx);
176 cx.spawn(async move |cx| {
177 let should_show_notification = should_show_notification.await?;
178 if should_show_notification {
179 cx.update(|cx| {
180 let mut version = updater.read(cx).current_version();
181 version.build = semver::BuildMetadata::EMPTY;
182 version.pre = semver::Prerelease::EMPTY;
183 let app_name = ReleaseChannel::global(cx).display_name();
184 show_app_notification(
185 NotificationId::unique::<UpdateNotification>(),
186 cx,
187 move |cx| {
188 let workspace_handle = cx.entity().downgrade();
189 cx.new(|cx| {
190 MessageNotification::new(
191 format!("Updated to {app_name} {}", version),
192 cx,
193 )
194 .primary_message("View Release Notes")
195 .primary_on_click(move |window, cx| {
196 if let Some(workspace) = workspace_handle.upgrade() {
197 workspace.update(cx, |workspace, cx| {
198 crate::view_release_notes_locally(workspace, window, cx);
199 })
200 }
201 cx.emit(DismissEvent);
202 })
203 .show_suppress_button(false)
204 })
205 },
206 );
207 updater.update(cx, |updater, cx| {
208 updater
209 .set_should_show_update_notification(false, cx)
210 .detach_and_log_err(cx);
211 });
212 });
213 }
214 anyhow::Ok(())
215 })
216 .detach();
217}