//! Calendars and address books: the collection list beside each view, the //! dialog that manages one collection, the client setup help and the import //! of an `.ics` or `.vcf` file. use std::collections::HashSet; use api_types::{CreatePimCollection, CreatePimLink, UpdatePimCollection}; use leptos::prelude::*; use wasm_bindgen_futures::spawn_local; use crate::api::{ self, Me, PimCollectionInfo, PimCollectionKind, PimImportResult, PimLinkInfo, PimShareInfo, PimShareMode, }; use crate::components::icon::{Icon, icon_svg}; use crate::components::modal::Modal; use crate::components::toast::{ToastMsg, show, show_error}; use crate::i18n::{self, k}; use crate::icons::IconName; use crate::router::{Location, Section, navigate, server_url}; use crate::util::{copy_to_clipboard, local_date_time, storage_get, storage_set}; use crate::views::calendar::CalendarMain; use crate::views::contacts::ContactsMain; use crate::views::dialogs::ConfirmDialog; use crate::views::shares::expiry_to_rfc3339; /// Colors offered for a collection; a custom one comes from the color input. const PALETTE: [&str; 8] = [ "#3b82f6", "#22c55e", "#f59e0b", "#ef4444", "#a855f7", "#14b8a6", "#ec4899", "#64748b", ]; /// The collections the lists do not show, by id. const HIDDEN_KEY: &str = "dovenest.pim-hidden"; /// Collection ids hidden in the calendar and contact views. pub fn load_hidden() -> HashSet { storage_get(HIDDEN_KEY) .unwrap_or_default() .split(',') .filter_map(|s| s.parse().ok()) .collect() } fn save_hidden(ids: &HashSet) { let list: Vec = ids.iter().map(i64::to_string).collect(); storage_set(HIDDEN_KEY, &list.join(",")); } /// A collection's color for a style attribute. Colors of lent collections /// come from other clients, so anything but a hex color falls back. pub fn safe_color(c: Option<&str>) -> String { match c.and_then(|c| c.strip_prefix('#')) { Some(h) if [3, 6, 8].contains(&h.len()) && h.bytes().all(|b| b.is_ascii_hexdigit()) => { format!("#{h}") } _ => "var(--accent)".to_string(), } } /// The name to show: generated collections have translated names. pub fn display_name(c: &PimCollectionInfo) -> String { match (c.generated, c.kind) { (true, PimCollectionKind::Calendar) => i18n::t(k::PIM_BIRTHDAYS).to_string(), (true, PimCollectionKind::Addressbook) => i18n::t(k::PIM_DIRECTORY).to_string(), _ => c.name.clone(), } } /// Whether the signed-in user may add objects to a collection. pub fn writable(c: &PimCollectionInfo) -> bool { !c.generated && c.mode != Some(PimShareMode::Ro) } fn mode_label(m: PimShareMode) -> &'static str { match m { PimShareMode::Ro => i18n::t(k::PIM_MODE_RO), PimShareMode::Rw => i18n::t(k::PIM_MODE_RW), PimShareMode::RwSchedule => i18n::t(k::PIM_MODE_SCHEDULE), } } /// The browser's IANA time zone, for dates the server resolves. pub fn browser_tz() -> String { let opts = js_sys::Intl::DateTimeFormat::new(&js_sys::Array::new(), &js_sys::Object::new()) .resolved_options(); js_sys::Reflect::get(&opts, &"timeZone".into()) .ok() .and_then(|v| v.as_string()) .unwrap_or_else(|| "UTC".to_string()) } /// The calendar or contacts section: the collection list, and the view of /// what the visible collections hold. #[component] pub fn PimView( kind: PimCollectionKind, me: ReadSignal>, loc: ReadSignal, ) -> impl IntoView { let (collections, set_collections) = signal(Option::>::None); let (load_err, set_load_err) = signal(Option::::None); let hidden = RwSignal::new(load_hidden()); let reload = Callback::new(move |_| { set_load_err.set(None); spawn_local(async move { match api::pim_collections().await { Ok(v) => set_collections.set(Some(v)), Err(e) => set_load_err.set(Some(e.to_string())), } }); }); reload.run(()); let main = match kind { PimCollectionKind::Calendar => { view! {