import { UiRadius } from "ui_radius.slint"; import { ScrollView } from "std-widgets.slint"; import { IconButton, FontAwesomeIcons } from "common.slint"; import { SettingsDialog, SettingsButton } from "settings_controls.slint"; import { WorkbenchStyle } from "workbench_controls.slint"; import "../assets/fonts/message_spacing.ttf"; export struct AnnouncementItem { id: string, revision: string, title: string, date: string, read: bool, } export global MessageStrings { in-out property messages: "Messages"; in-out property unread: "Unread"; in-out property read: "Read"; in-out property read-all: "Mark all as read"; in-out property empty: "No announcements"; in-out property select-message: "Select an announcement to read"; in-out property loading: "Loading…"; in-out property read-save-error: "Unable to save reading status locally. Please try again."; in-out property close: "Close"; in-out property delete-message: "Delete"; in-out property delete-title: "Delete announcement"; in-out property delete-confirmation: "Delete this announcement?"; in-out property delete-error: "Unable to delete this announcement. Please try again."; in-out property cancel: "Cancel"; } export component MessagePanel inherits SettingsDialog { in property <[AnnouncementItem]> items; in property unread-count; in property total; in property loading; in property failed; in property read-save-failed; in property delete-failed; in property loaded; in property connected; in property selected-id; in property selected-revision; in property selected-title; in property selected-body; in property selected-date; in property selected-read; callback open-message(string, string); callback open-link(string); callback delete-message(string, string); callback load-more; callback read-all; private property notice-text: root.delete-failed ? MessageStrings.delete-error : root.read-save-failed ? MessageStrings.read-save-error : ""; private property pending-delete-id; private property pending-delete-revision; function request-delete(id: string, revision: string) { root.pending-delete-id = id; root.pending-delete-revision = revision; confirmation-keys.focus(); } confirmation-keys := FocusScope { width: 0px; height: 0px; key-pressed(event) => { if event.text == Key.Escape { if root.pending-delete-id != "" { root.pending-delete-id = ""; } else { root.close(); } return accept; } return reject; } } title: MessageStrings.messages; close-label: MessageStrings.close; badge-count: root.unread-count; badge-label: MessageStrings.unread + " " + root.unread-count; header-status: root.loading ? MessageStrings.loading : ""; action-text: MessageStrings.read-all; action-enabled: root.loaded && !root.loading && root.unread-count > 0; action => { root.read-all(); } notice := Rectangle { x: 24px; y: root.content-top; width: parent.width - 48px; height: root.notice-text == "" ? 0px : notice-label.preferred-height + 20px; visible: root.notice-text != ""; border-radius: UiRadius.control; background: #fef2f2; notice-label := Text { x: 10px; y: 10px; width: parent.width - 20px; height: self.preferred-height; text: root.notice-text; font-size: 12px; wrap: word-wrap; color: #b3261e; } } message-content := Rectangle { x: 24px; y: root.content-top + (root.notice-text == "" ? 0px : notice.height + 12px); width: parent.width - 48px; height: parent.height - self.y - 24px; background: WorkbenchStyle.surface; border-radius: UiRadius.surface; border-width: 1px; border-color: WorkbenchStyle.border; if root.items.length > 0 || root.selected-id != "": Rectangle { x: 1px; y: 1px; width: content-divider.x - 1px; height: parent.height - 2px; background: WorkbenchStyle.field-surface; border-top-left-radius: UiRadius.surface - 1px; border-bottom-left-radius: UiRadius.surface - 1px; } announcement-list := Rectangle { x: 8px; y: 8px; width: 176px; height: parent.height - 16px; announcements-scroll := ScrollView { width: parent.width; height: parent.height; viewport-width: self.width - 10px; viewport-height: announcement-entries.preferred-height; horizontal-scrollbar-policy: always-off; vertical-scrollbar-policy: as-needed; announcement-entries := VerticalLayout { width: announcements-scroll.viewport-width; spacing: 10px; padding: 0px; alignment: start; for item in root.items: Rectangle { height: 64px; background: root.selected-id == item.id ? #e8efff : entry-touch.pressed ? WorkbenchStyle.pressed-surface : entry-touch.has-hover ? WorkbenchStyle.hover-surface : WorkbenchStyle.surface; border-radius: UiRadius.control; border-width: 1px; border-color: root.selected-id == item.id ? #b5c9f5 : WorkbenchStyle.border; Text { x: 10px; y: 9px; width: parent.width - 30px; height: 20px; text: item.title; overflow: elide; font-size: 13px; font-weight: item.read ? 400 : 600; color: root.selected-id == item.id ? WorkbenchStyle.accent : WorkbenchStyle.text; vertical-alignment: center; } if !item.read: Rectangle { x: parent.width - 15px; y: 16px; width: 6px; height: 6px; border-radius: 3px; background: WorkbenchStyle.accent; accessible-role: none; } Text { x: 10px; y: 35px; width: parent.width - 20px; height: 18px; text: item.date; color: WorkbenchStyle.muted; font-size: 11px; overflow: elide; vertical-alignment: center; } accessible-role: button; accessible-label: item.title + " " + (item.read ? MessageStrings.read : MessageStrings.unread); accessible-action-default => { root.open-message(item.id, item.revision); } entry-touch := TouchArea { mouse-cursor: pointer; clicked => { root.open-message(item.id, item.revision); } } } } } } if root.items.length == 0 && root.total == 0 && root.loaded && root.connected && !root.loading && !root.failed: Text { x: 24px; width: parent.width - 48px; height: parent.height; text: MessageStrings.empty; wrap: word-wrap; color: WorkbenchStyle.muted; font-size: 13px; horizontal-alignment: center; vertical-alignment: center; } content-divider := Rectangle { x: announcement-list.x + announcement-list.width + 8px; y: 1px; width: 1px; height: parent.height - 2px; background: WorkbenchStyle.separator; visible: root.items.length > 0 || root.selected-id != ""; accessible-role: none; } announcement-details := Rectangle { x: content-divider.x + 17px; y: 16px; width: parent.width - self.x - 16px; height: parent.height - 32px; clip: true; if root.selected-id == "" && root.items.length > 0: Text { width: parent.width; height: parent.height; text: MessageStrings.select-message; color: WorkbenchStyle.muted; font-size: 13px; wrap: word-wrap; horizontal-alignment: center; vertical-alignment: center; } if root.selected-id != "": details-scroll := ScrollView { width: parent.width; height: parent.height; viewport-width: self.width - 12px; viewport-height: details-content.preferred-height; horizontal-scrollbar-policy: always-off; vertical-scrollbar-policy: as-needed; details-content := VerticalLayout { width: details-scroll.viewport-width; padding: 0px; padding-bottom: 4px; spacing: 14px; alignment: start; Rectangle { height: max(28px, selected-heading.preferred-height); selected-heading := VerticalLayout { x: 0px; y: 0px; width: parent.width - 40px; height: self.preferred-height; padding: 0px; spacing: 6px; Text { vertical-stretch: 0; text: root.selected-title; wrap: word-wrap; font-size: 16px; font-weight: 600; color: WorkbenchStyle.text; } Text { vertical-stretch: 0; text: root.selected-date; accessible-label: root.selected-date + " · " + (root.selected-read ? MessageStrings.read : MessageStrings.unread); color: WorkbenchStyle.muted; font-size: 11px; wrap: word-wrap; } } IconButton { x: parent.width - self.width; y: 0px; width: 28px; height: 28px; icon: FontAwesomeIcons.trash-can; icon-size: 12px; icon-color: WorkbenchStyle.muted; danger: true; accessible-role: button; accessible-label: MessageStrings.delete-message + " " + root.selected-title; accessible-action-default => { root.request-delete(root.selected-id, root.selected-revision); } clicked => { root.request-delete(root.selected-id, root.selected-revision); } } } Rectangle { height: 1px; background: WorkbenchStyle.separator; } StyledText { vertical-stretch: 0; text: root.selected-body; default-color: WorkbenchStyle.text; default-font-size: 13px; // Metrics-only family provides 1.5em line height; visible // glyphs continue to use Slint's normal font fallback. default-font-family: "CrossDesk Message Spacing"; link-color: WorkbenchStyle.accent; link-clicked(url) => { root.open-link(url); } } } } } } Timer { interval: 100ms; running: root.connected && !root.loading && !root.failed && root.pending-delete-id == "" && root.items.length > 0 && root.items.length < root.total && announcements-scroll.viewport-height > 0px && announcements-scroll.viewport-height + announcements-scroll.viewport-y <= announcements-scroll.height + 80px; triggered => { root.load-more(); } } if root.pending-delete-id != "": Rectangle { background: #15223850; border-radius: root.border-radius; z: 30; TouchArea { clicked => { confirmation-keys.focus(); } } SettingsDialog { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: min(360px, root.width - 48px); height: self.content-top + confirmation-content.preferred-height + 24px; title: MessageStrings.delete-title; close-label: MessageStrings.cancel; close => { root.pending-delete-id = ""; } confirmation-content := VerticalLayout { x: 24px; y: parent.content-top; width: parent.width - 48px; height: self.preferred-height; padding: 0px; spacing: 24px; Text { text: MessageStrings.delete-confirmation; font-size: 13px; color: WorkbenchStyle.text; wrap: word-wrap; } HorizontalLayout { alignment: end; spacing: 8px; cancel-delete-button := SettingsButton { width: 88px; text: MessageStrings.cancel; accessible-role: button; accessible-label: MessageStrings.cancel; accessible-action-default => { cancel-delete-button.clicked(); } clicked => { root.pending-delete-id = ""; } } confirm-delete-button := SettingsButton { width: 88px; text: MessageStrings.delete-message; primary: true; accessible-role: button; accessible-label: MessageStrings.delete-title; accessible-action-default => { confirm-delete-button.clicked(); } clicked => { root.delete-message(root.pending-delete-id, root.pending-delete-revision); root.pending-delete-id = ""; } } } } } } }