diff --git a/reflection-app/src/application.rs b/reflection-app/src/application.rs index ecadf5a..6dfcc4d 100644 --- a/reflection-app/src/application.rs +++ b/reflection-app/src/application.rs @@ -21,7 +21,7 @@ use adw::prelude::*; use adw::subclass::prelude::*; use gettextrs::gettext; -use gtk::{gio, glib, glib::Properties, glib::clone}; +use gtk::{gdk, gio, glib, glib::Properties, glib::clone}; use reflection_doc::{document::DocumentId, identity::PrivateKey, service::Service}; use std::{cell::RefCell, fs}; use thiserror::Error; @@ -156,7 +156,9 @@ impl ReflectionApplication { .activate(move |app: &Self, _, _| app.show_about()) .build(); let new_window_action = gio::ActionEntry::builder("new-window") - .activate(move |app: &Self, _, _| app.new_window()) + .activate(move |app: &Self, _, _| { + app.new_window(); + }) .build(); let new_document_action = gio::ActionEntry::builder("new-document") .activate(move |app: &Self, _, _| app.new_document()) @@ -169,11 +171,11 @@ impl ReflectionApplication { let parameter = parameter.unwrap(); if parameter.n_children() == 0 { - app.open_join_document_dialog(); + app.open_join_document_dialog(false); } else { for i in 0..parameter.n_children() { if let Some(document_id) = parameter.child_value(i).get() { - app.join_document(&document_id); + app.join_document(&document_id, false); // FIXME: open all documents with it's own window break; } else { @@ -183,6 +185,31 @@ impl ReflectionApplication { } }) .build(); + + let join_document_in_new_window_action = + gio::ActionEntry::builder("join-document-in-new-window") + .parameter_type(Some(&glib::VariantType::new_array( + &DocumentId::static_variant_type(), + ))) + .activate(move |app: &Self, _, parameter| { + let parameter = parameter.unwrap(); + + if parameter.n_children() == 0 { + app.open_join_document_dialog(true); + } else { + for i in 0..parameter.n_children() { + if let Some(document_id) = parameter.child_value(i).get() { + app.join_document(&document_id, true); + // FIXME: open all documents with it's own window + break; + } else { + error!("Failed to join document: Invalid document id specified"); + } + } + } + }) + .build(); + let delete_document_action = gio::ActionEntry::builder("delete-document") .parameter_type(Some(&glib::VariantType::new_array( &DocumentId::static_variant_type(), @@ -200,6 +227,25 @@ impl ReflectionApplication { } }) .build(); + + let copy_document_id_action = gio::ActionEntry::builder("copy-document-id") + .parameter_type(Some(&glib::VariantType::new_array( + &DocumentId::static_variant_type(), + ))) + .activate(move |app: &Self, _, parameter| { + let parameter = parameter.unwrap(); + + for i in 0..parameter.n_children() { + if let Some(document_id) = parameter.child_value(i).get() { + app.copy_document_id(&document_id); + break; + } else { + error!("Failed to copy document id: Invalid document id specified"); + } + } + }) + .build(); + let temporary_identity_action = gio::ActionEntry::builder("new-temporary-identity") .activate(move |app: &Self, _, _| { glib::spawn_future_local(clone!( @@ -218,43 +264,57 @@ impl ReflectionApplication { new_window_action, new_document_action, join_document_action, + join_document_in_new_window_action, delete_document_action, + copy_document_id_action, temporary_identity_action, ]); } - fn new_window(&self) { + fn new_window(&self) -> Window { let window = Window::new(self); window.set_service(self.service()); if let Some(error) = self.imp().startup_error.borrow().as_ref() { window.display_startup_error(error); } window.present(); + window } fn new_document(&self) { - self.join_document(&DocumentId::new()); + self.join_document(&DocumentId::new(), false); } - fn open_join_document_dialog(&self) { - let active = self.active_window(); + fn open_join_document_dialog(&self, new_window: bool) { + let window = if new_window { + self.new_window() + } else if let Some(active) = self.active_window().and_downcast::() { + active + } else { + self.new_window() + }; let dialog = OpenDialog::new(); - adw::prelude::AdwDialogExt::present(&dialog, active.as_ref()); + adw::prelude::AdwDialogExt::present(&dialog, Some(&window)); } - fn join_document(&self, document_id: &DocumentId) { + fn join_document(&self, document_id: &DocumentId, new_window: bool) { if let Some(window) = self.window_for_document_id(document_id) { window.present(); } else { let Some(service) = self.service() else { return; }; - let Some(active) = self.active_window().and_downcast::() else { - return; + + let window = if new_window { + self.new_window() + } else if let Some(active) = self.active_window().and_downcast::() { + active + } else { + self.new_window() }; let document = service.join_document(document_id); - active.set_document(Some(&document)); + window.set_document(Some(&document)); let hold_guard = self.hold(); glib::spawn_future_local(clone!( #[weak] @@ -300,6 +360,13 @@ impl ReflectionApplication { } } + fn copy_document_id(&self, document_id: &DocumentId) { + let Some(display) = gdk::Display::default() else { + return; + }; + display.clipboard().set_text(&document_id.to_string()); + } + async fn new_temporary_identity(&self) { let private_key = PrivateKey::new(); let service = Service::new(&private_key, None); diff --git a/reflection-app/src/document_view.rs b/reflection-app/src/document_view.rs index 629c412..5bf28e7 100644 --- a/reflection-app/src/document_view.rs +++ b/reflection-app/src/document_view.rs @@ -30,7 +30,7 @@ use crate::{ components::{MultilineEntry, ZoomLevelSelector}, }; -const BASE_TEXT_FONT_SIZE: f64 = 24.0; +const BASE_TEXT_FONT_SIZE: f64 = 11.0; mod imp { use super::*; @@ -134,7 +134,13 @@ mod imp { let buffer = ReflectionTextBuffer::new(); self.text_view.set_buffer(Some(&buffer)); - self.font_size.set(BASE_TEXT_FONT_SIZE); + let size = ReflectionApplication::default() + .system_settings() + .monospace_font_name() + .map_or(BASE_TEXT_FONT_SIZE, |font| { + font.size() as f64 / gtk::pango::SCALE as f64 + }); + self.font_size.set(size); self.obj().set_font_scale(0.0); gtk::style_context_add_provider_for_display( >k::Widget::display(self.obj().upcast_ref()), @@ -215,7 +221,7 @@ mod imp { self.zoom_level.set(size / font_size); self.obj().notify_zoom_level(); self.css_provider - .load_from_string(&format!(".sourceview {{ font-size: {size}px; }}")); + .load_from_string(&format!(".sourceview {{ font-size: {size}pt; }}")); self.obj().action_set_enabled("window.zoom-out", size > 1.0); } diff --git a/reflection-app/src/landing_view/document_row.blp b/reflection-app/src/landing_view/document_row.blp index a418200..3af7a16 100644 --- a/reflection-app/src/landing_view/document_row.blp +++ b/reflection-app/src/landing_view/document_row.blp @@ -9,6 +9,7 @@ template $ReflectionDocumentRow: Adw.ActionRow { action-target: bind $transform_action_target(template.document as <$Document>.id) as <$GVariant>; title: bind $transform_name(template.document as <$Document>.name) as ; + title-lines: 2; subtitle: bind $transform_last_accessed(template.document as <$Document>.last_accessed, template.document as <$Document>.subscribed) as ; $ReflectionAuthorsStack { @@ -30,16 +31,14 @@ template $ReflectionDocumentRow: Adw.ActionRow { menu menu_model { section { item { - label: _("_Open in new Window"); - action: "app.open-document-in-window"; + label: _("_Open in New Window"); + action: "app.join-document-in-new-window"; hidden-when: "action-missing"; } - } - section { item { - label: _("_Share Document..."); - action: "app.share-document"; + label: _("_Copy Invite Code"); + action: "app.copy-document-id"; hidden-when: "action-missing"; } @@ -48,9 +47,7 @@ menu menu_model { action: "app.export-to-file"; hidden-when: "action-missing"; } - } - section { item { label: _("_Delete Document..."); action: "app.delete-document"; diff --git a/reflection-app/src/utils.rs b/reflection-app/src/utils.rs index f89fd73..103bae7 100644 --- a/reflection-app/src/utils.rs +++ b/reflection-app/src/utils.rs @@ -16,14 +16,14 @@ pub fn format_datetime(last_string: &str, datetime: &glib::DateTime) -> String { // This was ported from Nautilus and simplified for our use case. // See: https://gitlab.gnome.org/GNOME/nautilus/-/blob/1c5bd3614a35cfbb49de087bc10381cdef5a218f/src/nautilus-file.c#L5001 - let now = glib::DateTime::now_local().unwrap(); + let now = glib::DateTime::now_utc().unwrap(); let format; let days_ago = { let today_midnight = - glib::DateTime::from_local(now.year(), now.month(), now.day_of_month(), 0, 0, 0f64) + glib::DateTime::from_utc(now.year(), now.month(), now.day_of_month(), 0, 0, 0f64) .expect("constructing GDateTime works"); - let date = glib::DateTime::from_local( + let date = glib::DateTime::from_utc( datetime.year(), datetime.month(), datetime.day_of_month(), @@ -159,6 +159,8 @@ pub fn format_datetime(last_string: &str, datetime: &glib::DateTime) -> String { } datetime + .to_local() + .expect("constructing local GDateTime works") .format(&format) .expect("formatting GDateTime works") .into() diff --git a/reflection-doc/src/author.rs b/reflection-doc/src/author.rs index 84518e0..ae4b80a 100644 --- a/reflection-doc/src/author.rs +++ b/reflection-doc/src/author.rs @@ -172,7 +172,7 @@ impl Author { let was_online = self.imp().is_online.get(); self.imp().is_online.set(is_online); if !is_online && was_online { - *self.imp().last_seen.lock().unwrap() = glib::DateTime::now_local().ok(); + *self.imp().last_seen.lock().unwrap() = glib::DateTime::now_utc().ok(); self.notify_last_seen(); } self.notify_is_online(); diff --git a/reflection-doc/src/authors.rs b/reflection-doc/src/authors.rs index 0c0fc72..0f58a3b 100644 --- a/reflection-doc/src/authors.rs +++ b/reflection-doc/src/authors.rs @@ -82,7 +82,7 @@ impl Authors { pub(crate) fn add_this_device(&self, author_key: PublicKey) { let mut list = self.imp().list.write().unwrap(); - let now = glib::DateTime::now_local().ok(); + let now = glib::DateTime::now_utc().ok(); assert!(list.is_empty()); diff --git a/reflection-doc/src/document.rs b/reflection-doc/src/document.rs index bb5fa1b..4b1b6a4 100644 --- a/reflection-doc/src/document.rs +++ b/reflection-doc/src/document.rs @@ -105,7 +105,7 @@ mod imp { /// Loro documents can contain multiple different CRDT types in one document. static TEXT_CONTAINER_ID: LazyLock = LazyLock::new(|| loro::ContainerID::new_root("document", loro::ContainerType::Text)); - const DOCUMENT_NAME_LENGTH: usize = 32; + const DOCUMENT_NAME_LENGTH: usize = 124; const SNAPSHOT_TIMEOUT: Duration = Duration::from_secs(5); #[derive(Properties, Default)] @@ -155,10 +155,10 @@ mod imp { let mut name = String::with_capacity(DOCUMENT_NAME_LENGTH); crdt_text.iter(|slice| { for char in slice.chars() { - if char == '\n' { + if char == '\n' || name.len() > DOCUMENT_NAME_LENGTH { // Only use the first line as name for the document return false; - } else if char.is_whitespace() || char.is_alphanumeric() { + } else if (!name.is_empty() && char.is_whitespace()) || char.is_alphanumeric() { name.push(char); } }