mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
fix(gui): show remote names as text, not Pango markup
Rows, toasts and status page descriptions parse their text as markup by default. Shared item names, inviter addresses, device names, link URLs, paths, EXIF values and daemon errors went in unescaped, so a name with `&` showed as an empty row and other names were drawn as markup. Those rows and toasts now use plain text, and status page descriptions are escaped (B232).
This commit is contained in:
1 parent
519ba0591a
commit
ef0f046939
19 files changed
+89
-14
No files matched your search
@@ -1065,8 +1065,17 @@ pub(crate) fn set_sidebar_badge(ui: &Ui, page: &str, count: u64) {
|
||||
/// Show a transient toast. Non-blocking by design: an action's outcome is
|
||||
/// reported without stealing focus or forcing a click, so the user can keep
|
||||
/// working while a slow upload lands.
|
||||
///
|
||||
/// A toast's title is Pango markup unless told otherwise. Messages carry file
|
||||
/// names and daemon errors, and one with a `&` or `<` in it showed as an empty
|
||||
/// toast, so every toast here is plain text.
|
||||
fn toast(ui: &Rc<Ui>, message: &str) {
|
||||
ui.toasts.add_toast(adw::Toast::new(message));
|
||||
ui.toasts.add_toast(
|
||||
adw::Toast::builder()
|
||||
.title(message)
|
||||
.use_markup(false)
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Show a toast that offers one follow-up action — an Undo, or a way to the page
|
||||
@@ -1077,6 +1086,7 @@ fn toast(ui: &Rc<Ui>, message: &str) {
|
||||
fn toast_action(ui: &Rc<Ui>, message: &str, label: &str, action: impl Fn(&Rc<Ui>) + 'static) {
|
||||
let toast = adw::Toast::builder()
|
||||
.title(message)
|
||||
.use_markup(false)
|
||||
.button_label(label)
|
||||
.timeout(8)
|
||||
.build();
|
||||
@@ -1099,7 +1109,11 @@ fn toast_error(ui: &Rc<Ui>, what: &str, detail: &str) {
|
||||
gettext_f("{what}: {detail}", &[("what", what), ("detail", detail)])
|
||||
};
|
||||
tracing::warn!("{message}");
|
||||
let toast = adw::Toast::builder().title(&message).timeout(6).build();
|
||||
let toast = adw::Toast::builder()
|
||||
.title(&message)
|
||||
.use_markup(false)
|
||||
.timeout(6)
|
||||
.build();
|
||||
ui.toasts.add_toast(toast);
|
||||
}
|
||||
|
||||
|
||||
@@ -378,7 +378,9 @@ pub(crate) fn activity_status(
|
||||
) {
|
||||
ui.activity.status.set_icon_name(Some(icon));
|
||||
ui.activity.status.set_title(title);
|
||||
ui.activity.status.set_description(Some(description));
|
||||
ui.activity
|
||||
.status
|
||||
.set_description(Some(>k4::glib::markup_escape_text(description)));
|
||||
ui.activity.retry.set_visible(retry);
|
||||
ui.activity.content.set_visible_child_name("status");
|
||||
}
|
||||
|
||||
@@ -645,7 +645,9 @@ pub(crate) fn close_album(ui: &Rc<Ui>) {
|
||||
fn albums_status(ui: &Rc<Ui>, icon: &str, title: &str, description: &str) {
|
||||
ui.gallery.albums_status.set_icon_name(Some(icon));
|
||||
ui.gallery.albums_status.set_title(title);
|
||||
ui.gallery.albums_status.set_description(Some(description));
|
||||
ui.gallery
|
||||
.albums_status
|
||||
.set_description(Some(>k4::glib::markup_escape_text(description)));
|
||||
ui.gallery.albums_stack.set_visible_child_name("status");
|
||||
}
|
||||
|
||||
|
||||
@@ -500,7 +500,9 @@ pub(crate) fn build_browser_page() -> (gtk4::Widget, BrowserWidgets) {
|
||||
pub(crate) fn browser_status(ui: &Rc<Ui>, icon: &str, title: &str, description: &str, retry: bool) {
|
||||
ui.browser.status.set_icon_name(Some(icon));
|
||||
ui.browser.status.set_title(title);
|
||||
ui.browser.status.set_description(Some(description));
|
||||
ui.browser
|
||||
.status
|
||||
.set_description(Some(>k4::glib::markup_escape_text(description)));
|
||||
ui.browser.retry.set_visible(retry);
|
||||
// Only the empty-folder state offers a way to fill the folder; every other
|
||||
// status (loading, offline, error) turns them back off.
|
||||
|
||||
@@ -204,7 +204,9 @@ pub(crate) fn wire_devices(ui: &Rc<Ui>, widgets: &DevicesWidgets) {
|
||||
pub(crate) fn devices_status(ui: &Rc<Ui>, icon: &str, title: &str, description: &str, retry: bool) {
|
||||
ui.devices.status.set_icon_name(Some(icon));
|
||||
ui.devices.status.set_title(title);
|
||||
ui.devices.status.set_description(Some(description));
|
||||
ui.devices
|
||||
.status
|
||||
.set_description(Some(>k4::glib::markup_escape_text(description)));
|
||||
ui.devices.retry.set_visible(retry);
|
||||
ui.devices.content.set_visible_child_name("status");
|
||||
}
|
||||
@@ -390,6 +392,7 @@ fn this_computer_row(
|
||||
let row = adw::ActionRow::builder()
|
||||
.title(me.map_or_else(|| gettext("This computer"), |d| d.name.clone()))
|
||||
.subtitle(this_computer_subtitle(folders))
|
||||
.use_markup(false)
|
||||
.activatable(true)
|
||||
.build();
|
||||
row.add_prefix(>k4::Image::from_icon_name("computer-symbolic"));
|
||||
@@ -426,6 +429,7 @@ fn other_computer_row(ui: &Rc<Ui>, dev: &DeviceInfo) -> adw::ActionRow {
|
||||
let row = adw::ActionRow::builder()
|
||||
.title(&dev.name)
|
||||
.subtitle(device_subtitle(dev))
|
||||
.use_markup(false)
|
||||
.activatable(true)
|
||||
.build();
|
||||
row.add_prefix(>k4::Image::from_icon_name("computer-symbolic"));
|
||||
@@ -763,7 +767,10 @@ fn show_restore_picker(
|
||||
.active(true)
|
||||
.valign(gtk4::Align::Center)
|
||||
.build();
|
||||
let row = adw::EntryRow::builder().title(&f.name).build();
|
||||
let row = adw::EntryRow::builder()
|
||||
.title(&f.name)
|
||||
.use_markup(false)
|
||||
.build();
|
||||
row.set_text(&f.local_path);
|
||||
row.add_prefix(&check);
|
||||
group.add(&row);
|
||||
|
||||
@@ -209,7 +209,8 @@ fn ask(view: &Rc<FinderView>, finder: Finder, generation: u32) {
|
||||
Ok(Ok(Response::Error { message, .. })) => {
|
||||
view.status.set_icon_name(Some("dialog-warning-symbolic"));
|
||||
view.status.set_title(&failed);
|
||||
view.status.set_description(Some(&message));
|
||||
view.status
|
||||
.set_description(Some(>k4::glib::markup_escape_text(&message)));
|
||||
view.progress.set_visible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1145,7 +1145,9 @@ pub(crate) fn locations_status(
|
||||
) {
|
||||
ui.locations.status.set_icon_name(Some(icon));
|
||||
ui.locations.status.set_title(title);
|
||||
ui.locations.status.set_description(Some(description));
|
||||
ui.locations
|
||||
.status
|
||||
.set_description(Some(>k4::glib::markup_escape_text(description)));
|
||||
ui.locations.retry.set_visible(retry);
|
||||
ui.locations.content.set_visible_child_name("status");
|
||||
}
|
||||
|
||||
@@ -481,6 +481,7 @@ pub(crate) fn show_info(viewer: &Rc<Viewer>, path: &str, info: ExifInfo) {
|
||||
let row = adw::ActionRow::builder()
|
||||
.title(label.as_str())
|
||||
.subtitle(value)
|
||||
.use_markup(false)
|
||||
.subtitle_selectable(true)
|
||||
.build();
|
||||
row.add_css_class("property");
|
||||
|
||||
@@ -3459,7 +3459,9 @@ pub(crate) fn load_gallery(ui: &Rc<Ui>, append: bool) {
|
||||
pub(crate) fn gallery_status(ui: &Rc<Ui>, icon: &str, title: &str, description: &str, retry: bool) {
|
||||
ui.gallery.status.set_icon_name(Some(icon));
|
||||
ui.gallery.status.set_title(title);
|
||||
ui.gallery.status.set_description(Some(description));
|
||||
ui.gallery
|
||||
.status
|
||||
.set_description(Some(>k4::glib::markup_escape_text(description)));
|
||||
ui.gallery.retry.set_visible(retry);
|
||||
// Only the empty timeline offers a way to fill it; every other status turns
|
||||
// those buttons back off.
|
||||
|
||||
@@ -448,7 +448,9 @@ pub(crate) fn close_place(ui: &Rc<Ui>) {
|
||||
fn places_status(ui: &Rc<Ui>, icon: &str, title: &str, description: &str) {
|
||||
ui.gallery.places_status.set_icon_name(Some(icon));
|
||||
ui.gallery.places_status.set_title(title);
|
||||
ui.gallery.places_status.set_description(Some(description));
|
||||
ui.gallery
|
||||
.places_status
|
||||
.set_description(Some(>k4::glib::markup_escape_text(description)));
|
||||
ui.gallery.places_stack.set_visible_child_name("status");
|
||||
}
|
||||
|
||||
|
||||
@@ -311,7 +311,9 @@ pub(crate) fn wire_shared(ui: &Rc<Ui>, widgets: &SharedWidgets) {
|
||||
pub(crate) fn shared_status(ui: &Rc<Ui>, icon: &str, title: &str, description: &str, retry: bool) {
|
||||
ui.shared.status.set_icon_name(Some(icon));
|
||||
ui.shared.status.set_title(title);
|
||||
ui.shared.status.set_description(Some(description));
|
||||
ui.shared
|
||||
.status
|
||||
.set_description(Some(>k4::glib::markup_escape_text(description)));
|
||||
ui.shared.retry.set_visible(retry);
|
||||
ui.shared.content.set_visible_child_name("status");
|
||||
}
|
||||
@@ -818,6 +820,7 @@ fn invitation_row(inv: &InvitationInfo) -> adw::ActionRow {
|
||||
.title(&item)
|
||||
// Translators: {email} is the address of the person who sent the invitation.
|
||||
.subtitle(gettext_f("From {email}", &[("email", &inv.inviter_email)]))
|
||||
.use_markup(false)
|
||||
.build();
|
||||
row.add_prefix(>k4::Image::from_icon_name(if inv.is_dir {
|
||||
"folder-symbolic"
|
||||
@@ -862,6 +865,7 @@ fn repaint_links(ui: &Rc<Ui>, bookmarks: &[BookmarkInfo]) {
|
||||
let row = adw::ActionRow::builder()
|
||||
.title(&title)
|
||||
.subtitle(&bm.url)
|
||||
.use_markup(false)
|
||||
.subtitle_lines(1)
|
||||
.activatable(true)
|
||||
.tooltip_text(gettext("Open in browser"))
|
||||
|
||||
@@ -329,7 +329,9 @@ pub(crate) fn shared_by_me_status(
|
||||
) {
|
||||
ui.shared_by_me.status.set_icon_name(Some(icon));
|
||||
ui.shared_by_me.status.set_title(title);
|
||||
ui.shared_by_me.status.set_description(Some(description));
|
||||
ui.shared_by_me
|
||||
.status
|
||||
.set_description(Some(>k4::glib::markup_escape_text(description)));
|
||||
ui.shared_by_me.retry.set_visible(retry);
|
||||
ui.shared_by_me.content.set_visible_child_name("status");
|
||||
ui.shared_by_me.subtitle.set_subtitle("");
|
||||
|
||||
@@ -1566,6 +1566,7 @@ pub(crate) fn repaint_pins(ui: &Rc<Ui>, pins: &[pdfs_core::cache::Pin], mounted:
|
||||
let row = adw::ActionRow::builder()
|
||||
.title(&name)
|
||||
.subtitle(&pin.path)
|
||||
.use_markup(false)
|
||||
.build();
|
||||
let icon = gtk4::Image::from_icon_name("pdfs-offline-symbolic");
|
||||
row.add_prefix(&icon);
|
||||
|
||||
@@ -510,6 +510,7 @@ pub(crate) fn repaint_archives(ui: &Rc<Ui>) {
|
||||
.unwrap_or_else(|| path.display().to_string()),
|
||||
)
|
||||
.subtitle(human_bytes(size))
|
||||
.use_markup(false)
|
||||
.build();
|
||||
let remove = gtk4::Button::builder()
|
||||
.icon_name("edit-delete-symbolic")
|
||||
|
||||
@@ -330,7 +330,9 @@ pub(crate) fn trash_unreachable(ui: &Rc<Ui>) {
|
||||
pub(crate) fn trash_status(ui: &Rc<Ui>, icon: &str, title: &str, description: &str, retry: bool) {
|
||||
ui.trash.status.set_icon_name(Some(icon));
|
||||
ui.trash.status.set_title(title);
|
||||
ui.trash.status.set_description(Some(description));
|
||||
ui.trash
|
||||
.status
|
||||
.set_description(Some(>k4::glib::markup_escape_text(description)));
|
||||
ui.trash.retry.set_visible(retry);
|
||||
ui.trash.content.set_visible_child_name("status");
|
||||
ui.trash.subtitle.set_subtitle("");
|
||||
|
||||
@@ -98,6 +98,8 @@ pub(crate) fn build_details_pane() -> (gtk4::Widget, DetailsWidgets) {
|
||||
.title(gettext("Location"))
|
||||
.subtitle("—")
|
||||
.subtitle_lines(3)
|
||||
// A folder path, which may hold `&` or `<`.
|
||||
.use_markup(false)
|
||||
.build();
|
||||
path_row.add_css_class("property");
|
||||
props.add(&size_row);
|
||||
|
||||
@@ -491,6 +491,7 @@ pub(crate) fn repaint_share_people(state: &Rc<ShareDialog>, entries: &[ShareEntr
|
||||
let row = adw::ActionRow::builder()
|
||||
.title(&entry.email)
|
||||
.subtitle(&subtitle)
|
||||
.use_markup(false)
|
||||
.build();
|
||||
row.add_prefix(&adw::Avatar::new(32, Some(&entry.email), true));
|
||||
|
||||
@@ -605,6 +606,7 @@ pub(crate) fn repaint_share_link(state: &Rc<ShareDialog>, link: Option<&PublicLi
|
||||
url.clone()
|
||||
})
|
||||
.subtitle(&subtitle)
|
||||
.use_markup(false)
|
||||
.build();
|
||||
row.add_css_class("property");
|
||||
|
||||
|
||||
@@ -219,6 +219,7 @@ fn repaint_versions(state: &Rc<VersionsDialog>, items: &[RevisionInfo]) {
|
||||
let row = adw::ActionRow::builder()
|
||||
.title(title)
|
||||
.subtitle(subtitle)
|
||||
.use_markup(false)
|
||||
.build();
|
||||
|
||||
let save = gtk4::Button::builder()
|
||||
|
||||
@@ -100,6 +100,7 @@ a live account since.
|
||||
|
||||
| Entry | Title |
|
||||
|---|---|
|
||||
| [B232](#b232--names-with--or--show-as-empty-rows-or-are-drawn-as-markup) | Names with & or < show as empty rows or are drawn as markup |
|
||||
| [B231](#b231--a-large-upload-under-a-low-upload-limit-never-finishes) | A large upload under a low upload limit never finishes |
|
||||
| [B230](#b230--a-create-queued-offline-adopts-another-devices-file-of-the-same-name) | A create queued offline adopts another device's file of the same name |
|
||||
| [B229](#b229--a-queued-write-can-be-lost-to-a-power-cut-after-close-returned) | A queued write can be lost to a power cut after close returned |
|
||||
@@ -334,6 +335,30 @@ a live account since.
|
||||
|
||||
Newest first.
|
||||
|
||||
### B232 — Names with & or < show as empty rows or are drawn as markup
|
||||
|
||||
**Status:** Fixed, not verified live
|
||||
**Found:** 2026-10-16, architecture audit (`FINDINGS.md` section 3, Pango markup injection).
|
||||
**Where:** the toast helpers in `crates/pdfs-gui/src/app/main.rs`; rows in
|
||||
`app/pages/shared.rs`, `devices.rs`, `status.rs`, `takeout.rs`, `photo_viewer.rs`,
|
||||
`app/widgets/share_dialog.rs`, `details.rs`, `versions_dialog.rs`; every status page helper
|
||||
|
||||
**Repro:** Accept an invitation to a folder named `R&D`, or name a computer `Tom & Jerry`.
|
||||
Its row in the app is empty. A name such as `<span size="80000">x</span>` is drawn as markup.
|
||||
|
||||
**Cause.** Libadwaita rows, toasts and status page descriptions read their text as Pango markup
|
||||
unless told otherwise. Names that other people control (shared items, inviter addresses, device
|
||||
names, link URLs), file paths, EXIF values and daemon error messages went in unescaped. Text that
|
||||
did not parse showed as an empty row; text that did was drawn as markup.
|
||||
|
||||
**Fix.** Rows and toasts that show such text are built with `use_markup(false)`. Status page
|
||||
descriptions, which have no such switch, are escaped with `markup_escape_text`.
|
||||
|
||||
**Test:** none automated; the change only sets a widget property. Checked by reading every row,
|
||||
toast and status page that takes text from outside the program.
|
||||
|
||||
---
|
||||
|
||||
### B231 — A large upload under a low upload limit never finishes
|
||||
|
||||
**Status:** Fixed, not verified live
|
||||
|
||||
Reference in new issue
Block a user