auto_update_ui.rs

  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 _;
  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_locally(
 43    workspace: &mut Workspace,
 44    _window: &mut Window,
 45    cx: &mut Context<Workspace>,
 46) {
 47    struct ViewReleaseNotesLocallyError;
 48    workspace.show_notification(
 49        NotificationId::unique::<ViewReleaseNotesLocallyError>(),
 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    workspace
 96        .with_local_workspace(window, cx, move |_, window, cx| {
 97            cx.spawn_in(window, async move |workspace, cx| {
 98                let markdown = markdown.await.log_err();
 99                let response = client.get(&url, Default::default(), true).await;
100                let Some(mut response) = response.log_err() else {
101                    workspace
102                        .update_in(cx, notify_release_notes_failed_to_show_locally)
103                        .log_err();
104                    return;
105                };
106
107                let mut body = Vec::new();
108                response.body_mut().read_to_end(&mut body).await.ok();
109
110                let body: serde_json::Result<ReleaseNotesBody> =
111                    serde_json::from_slice(body.as_slice());
112
113                if let Ok(body) = body {
114                    workspace
115                        .update_in(cx, |workspace, window, cx| {
116                            let project = workspace.project().clone();
117                            let buffer = project.update(cx, |project, cx| {
118                                project.create_local_buffer("", markdown, false, cx)
119                            });
120                            buffer.update(cx, |buffer, cx| {
121                                buffer.edit([(0..0, body.release_notes)], None, cx)
122                            });
123                            let language_registry = project.read(cx).languages().clone();
124
125                            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
126
127                            let editor = cx.new(|cx| {
128                                Editor::for_multibuffer(buffer, Some(project), window, cx)
129                            });
130                            let workspace_handle = workspace.weak_handle();
131                            let markdown_preview: Entity<MarkdownPreviewView> =
132                                MarkdownPreviewView::new(
133                                    MarkdownPreviewMode::Default,
134                                    editor,
135                                    workspace_handle,
136                                    language_registry,
137                                    window,
138                                    cx,
139                                );
140                            workspace.add_item_to_active_pane(
141                                Box::new(markdown_preview),
142                                None,
143                                true,
144                                window,
145                                cx,
146                            );
147                            cx.notify();
148                        })
149                        .log_err();
150                } else {
151                    workspace
152                        .update_in(cx, notify_release_notes_failed_to_show_locally)
153                        .log_err();
154                }
155            })
156            .detach();
157        })
158        .detach();
159}
160
161/// Shows a notification across all workspaces if an update was previously automatically installed
162/// and this notification had not yet been shown.
163pub fn notify_if_app_was_updated(cx: &mut App) {
164    let Some(updater) = AutoUpdater::get(cx) else {
165        return;
166    };
167
168    if let ReleaseChannel::Nightly = ReleaseChannel::global(cx) {
169        return;
170    }
171
172    struct UpdateNotification;
173
174    let should_show_notification = updater.read(cx).should_show_update_notification(cx);
175    cx.spawn(async move |cx| {
176        let should_show_notification = should_show_notification.await?;
177        if should_show_notification {
178            cx.update(|cx| {
179                let mut version = updater.read(cx).current_version();
180                version.build = semver::BuildMetadata::EMPTY;
181                version.pre = semver::Prerelease::EMPTY;
182                let app_name = ReleaseChannel::global(cx).display_name();
183                show_app_notification(
184                    NotificationId::unique::<UpdateNotification>(),
185                    cx,
186                    move |cx| {
187                        let workspace_handle = cx.entity().downgrade();
188                        cx.new(|cx| {
189                            MessageNotification::new(
190                                format!("Updated to {app_name} {}", version),
191                                cx,
192                            )
193                            .primary_message("View Release Notes")
194                            .primary_on_click(move |window, cx| {
195                                if let Some(workspace) = workspace_handle.upgrade() {
196                                    workspace.update(cx, |workspace, cx| {
197                                        crate::view_release_notes_locally(workspace, window, cx);
198                                    })
199                                }
200                                cx.emit(DismissEvent);
201                            })
202                            .show_suppress_button(false)
203                        })
204                    },
205                );
206                updater.update(cx, |updater, cx| {
207                    updater
208                        .set_should_show_update_notification(false, cx)
209                        .detach_and_log_err(cx);
210                });
211            });
212        }
213        anyhow::Ok(())
214    })
215    .detach();
216}