Vectrum
Sign In
148 lines · 6.0 KB · Rust
//! The Archive page, laid out like T3 Code's: settled threads by project,
//! newest first, each with when it was archived and created, an Unarchive
//! button, and Unarchive and Delete on right-click.

use gpui_kit::component::{
    Icon, WindowExt as _, h_flex,
    menu::{ContextMenuExt as _, PopupMenuItem},
};
use gpui_kit::*;

use super::SettingsView;
use super::widgets::{Section, SettingRow, ago};
use crate::settings;
use crate::ui::icons::Lucide;
use crate::ui::metal::{self, MetalButton};
use crate::ui::pickers;
use crate::ui::workspace::{SettledThread, Workspace};

impl SettingsView {
    pub(super) fn render_archive(&mut self, cx: &mut Context<Self>) -> Vec<AnyElement> {
        let p = metal::palette(cx);
        let threads = self
            .workspace
            .upgrade()
            .map(|workspace| workspace.read(cx).settled_threads(cx))
            .unwrap_or_default();
        if threads.is_empty() {
            return vec![
                Section::new("Archived threads")
                    .id("archive")
                    .row(
                        SettingRow::new("No archived threads")
                            .icon(Icon::new(Lucide::Archive).size(px(14.)).text_color(p.ink3))
                            .description("Archived threads will appear here."),
                    )
                    .render(cx),
            ];
        }

        // One section per project, in the order their newest thread was archived.
        let mut projects: Vec<(SharedString, SharedString, usize, Vec<AnyElement>)> = Vec::new();
        for (ix, thread) in threads.iter().enumerate() {
            let row = archive_row(ix, thread, self.workspace.clone(), cx);
            match projects.iter_mut().find(|(name, ..)| *name == thread.project) {
                Some((.., rows)) => rows.push(row),
                None => projects.push((thread.project.clone(), thread.initials.clone(), thread.tint, vec![row])),
            }
        }
        projects
            .into_iter()
            .enumerate()
            .map(|(ix, (name, initials, tint, rows))| {
                let section = Section::new(name).icon(metal::badge(initials, tint, p)).rows(rows);
                if ix == 0 { section.id("archive") } else { section }.render(cx)
            })
            .collect()
    }
}

fn archive_row(ix: usize, thread: &SettledThread, workspace: WeakEntity<Workspace>, cx: &mut Context<SettingsView>) -> AnyElement {
    let p = metal::palette(cx);
    let created = thread.session.read(cx).created;
    let archived = match thread.age.as_str() {
        "now" => "just now".to_string(),
        age => format!("{age} ago"),
    };
    let session = thread.session.clone();
    let view = cx.weak_entity();
    let unarchive = {
        let session = session.clone();
        let view = view.clone();
        let workspace = workspace.clone();
        move |window: &mut Window, cx: &mut App| {
            let _ = workspace.update(cx, |workspace, cx| workspace.restore_thread(session.clone(), window, cx));
            let _ = view.update(cx, |_, cx| cx.notify());
        }
    };
    let delete = {
        let session = session.clone();
        let view = view.clone();
        move |window: &mut Window, cx: &mut App| {
            let session = session.clone();
            let workspace = workspace.clone();
            let view = view.clone();
            let forget = move |window: &mut Window, cx: &mut App| {
                let _ = workspace.update(cx, |workspace, cx| workspace.forget_thread(session.clone(), window, cx));
                let _ = view.update(cx, |_, cx| cx.notify());
            };
            if settings::get(|settings| settings.confirm_delete) {
                confirm_delete(forget, window, cx);
            } else {
                forget(window, cx);
            }
        }
    };
    let button_unarchive = unarchive.clone();
    let (menu_unarchive, menu_delete) = (unarchive.clone(), delete.clone());
    div()
        .id(("archived-thread", ix))
        .context_menu(move |menu, _, _| {
            let (unarchive, delete) = (menu_unarchive.clone(), menu_delete.clone());
            menu.item(PopupMenuItem::new("Unarchive").on_click(move |_, window, cx| unarchive(window, cx)))
                .item(PopupMenuItem::new("Delete").on_click(move |_, window, cx| delete(window, cx)))
        })
        .child(
            SettingRow::new(thread.title.clone())
                .icon(pickers::provider_icon(thread.provider, px(12.), p))
                .description(format!("Archived {archived} · Created {}", ago(created)))
                .control(
                    MetalButton::new(("unarchive-thread", ix))
                        .compact()
                        .icon(Lucide::ArchiveX)
                        .label("Unarchive")
                        .on_click(move |_, window, cx| button_unarchive(window, cx)),
                ),
        )
        .into_any_element()
}

fn confirm_delete(forget: impl Fn(&mut Window, &mut App) + Clone + 'static, window: &mut Window, cx: &mut App) {
    window.open_dialog(cx, move |dialog, _, cx| {
        let p = metal::palette(cx);
        let forget = forget.clone();
        dialog
            .title("Delete thread?")
            .w(px(420.))
            .child(div().text_size(px(13.)).text_color(p.ink2).child("This deletes the thread and its chat history."))
            .footer(
                h_flex()
                    .w_full()
                    .justify_end()
                    .gap(px(8.))
                    .child(
                        MetalButton::new("delete-cancel")
                            .ghost()
                            .compact()
                            .label("Cancel")
                            .on_click(|_, window, cx| window.close_dialog(cx)),
                    )
                    .child(MetalButton::new("delete-confirm").danger().compact().label("Delete").on_click(
                        move |_, window, cx| {
                            window.close_dialog(cx);
                            forget(window, cx);
                        },
                    )),
            )
    });
}