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 title: String,
35 release_notes: String,
36}
37
38fn notify_release_notes_failed_to_show(
39 workspace: &mut Workspace,
40 _window: &mut Window,
41 cx: &mut Context<Workspace>,
42) {
43 struct ViewReleaseNotesError;
44 workspace.show_notification(
45 NotificationId::unique::<ViewReleaseNotesError>(),
46 cx,
47 |cx| {
48 cx.new(move |cx| {
49 let url = release_notes_url(cx);
50 let mut prompt = ErrorMessagePrompt::new("Couldn't load release notes", cx);
51 if let Some(url) = url {
52 prompt = prompt.with_link_button("View in Browser".to_string(), url);
53 }
54 prompt
55 })
56 },
57 );
58}
59
60fn view_release_notes_locally(
61 workspace: &mut Workspace,
62 window: &mut Window,
63 cx: &mut Context<Workspace>,
64) {
65 let release_channel = ReleaseChannel::global(cx);
66
67 if matches!(
68 release_channel,
69 ReleaseChannel::Nightly | ReleaseChannel::Dev
70 ) {
71 if let Some(url) = release_notes_url(cx) {
72 cx.open_url(&url);
73 }
74 return;
75 }
76
77 let version = AppVersion::global(cx).to_string();
78
79 let client = client::Client::global(cx).http_client();
80 let url = client.build_url(&format!(
81 "/api/release_notes/v2/{}/{}",
82 release_channel.dev_name(),
83 version
84 ));
85
86 let markdown = workspace
87 .app_state()
88 .languages
89 .language_for_name("Markdown");
90
91 cx.spawn_in(window, async move |workspace, cx| {
92 let markdown = markdown.await.log_err();
93 let response = client.get(&url, Default::default(), true).await;
94 let Some(mut response) = response.log_err() else {
95 workspace
96 .update_in(cx, notify_release_notes_failed_to_show)
97 .log_err();
98 return;
99 };
100
101 let mut body = Vec::new();
102 response.body_mut().read_to_end(&mut body).await.ok();
103
104 let body: serde_json::Result<ReleaseNotesBody> = serde_json::from_slice(body.as_slice());
105
106 let res: Option<()> = maybe!(async {
107 let body = body.ok()?;
108 let project = workspace
109 .read_with(cx, |workspace, _| workspace.project().clone())
110 .ok()?;
111 let (language_registry, buffer) = project.update(cx, |project, cx| {
112 (
113 project.languages().clone(),
114 project.create_buffer(markdown, false, cx),
115 )
116 });
117 let buffer = buffer.await.ok()?;
118 buffer.update(cx, |buffer, cx| {
119 buffer.edit([(0..0, body.release_notes)], None, cx)
120 });
121
122 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(body.title));
123
124 let ws_handle = workspace.clone();
125 workspace
126 .update_in(cx, |workspace, window, cx| {
127 let editor =
128 cx.new(|cx| Editor::for_multibuffer(buffer, Some(project), window, cx));
129 let markdown_preview: Entity<MarkdownPreviewView> = MarkdownPreviewView::new(
130 MarkdownPreviewMode::Default,
131 editor,
132 ws_handle,
133 language_registry,
134 window,
135 cx,
136 );
137 workspace.add_item_to_active_pane(
138 Box::new(markdown_preview),
139 None,
140 true,
141 window,
142 cx,
143 );
144 cx.notify();
145 })
146 .ok()
147 })
148 .await;
149 if res.is_none() {
150 workspace
151 .update_in(cx, notify_release_notes_failed_to_show)
152 .log_err();
153 }
154 })
155 .detach();
156}
157
158/// Shows a notification across all workspaces if an update was previously automatically installed
159/// and this notification had not yet been shown.
160pub fn notify_if_app_was_updated(cx: &mut App) {
161 let Some(updater) = AutoUpdater::get(cx) else {
162 return;
163 };
164
165 if let ReleaseChannel::Nightly = ReleaseChannel::global(cx) {
166 return;
167 }
168
169 struct UpdateNotification;
170
171 let should_show_notification = updater.read(cx).should_show_update_notification(cx);
172 cx.spawn(async move |cx| {
173 let should_show_notification = should_show_notification.await?;
174 if should_show_notification {
175 cx.update(|cx| {
176 let mut version = updater.read(cx).current_version();
177 version.build = semver::BuildMetadata::EMPTY;
178 version.pre = semver::Prerelease::EMPTY;
179 let app_name = ReleaseChannel::global(cx).display_name();
180 show_app_notification(
181 NotificationId::unique::<UpdateNotification>(),
182 cx,
183 move |cx| {
184 let workspace_handle = cx.entity().downgrade();
185 cx.new(|cx| {
186 MessageNotification::new(
187 format!("Updated to {app_name} {}", version),
188 cx,
189 )
190 .primary_message("View Release Notes")
191 .primary_on_click(move |window, cx| {
192 if let Some(workspace) = workspace_handle.upgrade() {
193 workspace.update(cx, |workspace, cx| {
194 crate::view_release_notes_locally(workspace, window, cx);
195 })
196 }
197 cx.emit(DismissEvent);
198 })
199 .show_suppress_button(false)
200 })
201 },
202 );
203 updater.update(cx, |updater, cx| {
204 updater
205 .set_should_show_update_notification(false, cx)
206 .detach_and_log_err(cx);
207 });
208 });
209 }
210 anyhow::Ok(())
211 })
212 .detach();
213}