/// TEMPORARY AI GENERATED FRONTEND \\\ /// NOT TO BE USED IN PRODUCTION \\\ import { ListView } from "std-widgets.slint"; import { Theme } from "../theme.slint"; import { Icons } from "../icons.slint"; import { MessageItem, MessageKind, ReactionItem } from "../types.slint"; import { Icon } from "icon.slint"; import { Avatar } from "avatar.slint"; import { ToolButton } from "tool-button.slint"; component ReactionChip inherits Rectangle { in property r; callback clicked(); height: 24px; width: label.preferred-width + 20px; border-radius: 8px; border-width: 1px; border-color: r.mine ? Theme.accent : Theme.sidebar-bg; background: r.mine ? Theme.accent.with-alpha(0.22) : Theme.sidebar-bg; label := Text { text: "\{r.key} \{r.count}"; font-size: 13px; color: r.mine ? Theme.text-header : Theme.text-muted; horizontal-alignment: center; vertical-alignment: center; } TouchArea { mouse-cursor: pointer; clicked => { root.clicked(); } } } // Floating toolbar shown on message hover (react / reply / edit / delete). component HoverBar inherits Rectangle { in property own; callback react(); callback reply(); callback edit(); callback delete(); height: 32px; width: (own ? 4 : 2) * 32px + 8px; background: Theme.chat-bg; border-radius: 6px; border-width: 1px; border-color: Theme.divider; HorizontalLayout { padding: 2px; spacing: 0px; ToolButton { glyph: Icons.smile; clicked => { root.react(); } } ToolButton { glyph: Icons.reply; clicked => { root.reply(); } } if root.own: ToolButton { glyph: Icons.edit; clicked => { root.edit(); } } if root.own: ToolButton { glyph: Icons.trash; tint: Theme.danger; clicked => { root.delete(); } } } } component ChatMessage inherits Rectangle { in property m; callback reply(); callback edit(); callback delete(); callback react(string); background: hover.has-hover ? Theme.message-hover-bg : transparent; // The TouchArea *wraps* the content so hover stays true while the pointer // is over children (text, chips, the toolbar). hover := TouchArea { VerticalLayout { padding-top: root.m.show-header ? 14px : 1px; padding-bottom: 2px; spacing: 2px; if root.m.has-reply: Rectangle { height: 20px; Text { x: Theme.gutter; width: parent.width - Theme.gutter - 16px; height: parent.height; text: "↳ \{root.m.reply-sender} \{root.m.reply-body}"; color: Theme.text-muted; font-size: 13px; vertical-alignment: center; overflow: elide; } } HorizontalLayout { padding-left: 16px; padding-right: 16px; spacing: 16px; // Gutter: avatar on the first message of a group, hover-time otherwise. Rectangle { width: 40px; if root.m.show-header: Avatar { initial: root.m.initial; tint: root.m.color; diameter: 40px; y: 2px; } if !root.m.show-header: Text { visible: hover.has-hover; text: root.m.short-time; color: Theme.text-faint; font-size: 10px; horizontal-alignment: center; y: 4px; } } VerticalLayout { spacing: 2px; if root.m.show-header: HorizontalLayout { height: 20px; spacing: 8px; alignment: start; Text { text: root.m.sender; color: root.m.color.brighter(0.6); font-size: 15px; font-weight: 600; vertical-alignment: center; } Text { text: root.m.time; color: Theme.text-faint; font-size: 12px; vertical-alignment: center; } } // Read-only TextInput rather than Text: lets people select + copy. TextInput { text: root.m.body; read-only: true; single-line: false; wrap: word-wrap; color: root.m.pending ? Theme.text-muted : Theme.text-normal; font-size: 15px; selection-background-color: Theme.accent; } if root.m.edited: Text { text: "(edited)"; color: Theme.text-faint; font-size: 11px; } if root.m.reactions.length > 0: HorizontalLayout { height: 28px; padding-top: 4px; spacing: 4px; alignment: start; for r in root.m.reactions: ReactionChip { r: r; clicked => { root.react(r.key); } } } } } } // Absolutely positioned => excluded from the row's height calculation. if hover.has-hover && !root.m.pending: HoverBar { own: root.m.own; x: parent.width - self.width - 16px; y: -14px; react => { root.react("👍"); } reply => { root.reply(); } edit => { root.edit(); } delete => { root.delete(); } } } } component DateDivider inherits Rectangle { in property text; height: 40px; Rectangle { x: 16px; width: parent.width - 32px; y: parent.height / 2; height: 1px; background: Theme.divider; } Rectangle { width: label.preferred-width + 16px; height: 18px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; background: Theme.chat-bg; label := Text { text: root.text; color: Theme.text-muted; font-size: 12px; font-weight: 600; horizontal-alignment: center; vertical-alignment: center; } } } component NoticeRow inherits Rectangle { in property text; height: 28px; Text { x: Theme.gutter; width: parent.width - Theme.gutter - 16px; height: parent.height; text: root.text; color: Theme.text-faint; font-size: 13px; vertical-alignment: center; overflow: elide; } } component RoomStart inherits Rectangle { in property room-name; VerticalLayout { padding: 16px; padding-top: 32px; spacing: 6px; Text { text: "Welcome to #\{root.room-name}!"; color: Theme.text-header; font-size: 28px; font-weight: 700; wrap: word-wrap; } Text { text: "This is the start of the #\{root.room-name} channel."; color: Theme.text-muted; font-size: 15px; wrap: word-wrap; } } } component RowSwitch inherits Rectangle { in property m; in property room-name; callback reply(); callback edit(); callback delete(); callback react(string); VerticalLayout { if root.m.kind == MessageKind.message: ChatMessage { m: root.m; reply => { root.reply(); } edit => { root.edit(); } delete => { root.delete(); } react(key) => { root.react(key); } } if root.m.kind == MessageKind.date-divider: DateDivider { text: root.m.body; } if root.m.kind == MessageKind.notice: NoticeRow { text: root.m.body; } if root.m.kind == MessageKind.start-of-room: RoomStart { room-name: root.room-name; } } } export component MessageList inherits Rectangle { in property <[MessageItem]> messages; in property room-name; in property loading-older; in property room-nonce; // bumped by Rust whenever a different room is opened callback reply(string, string); // id, sender callback edit(string, string); // id, body callback delete(string); callback react(string, string); // id, key callback load-older(); // "Stick to bottom": follow new messages unless the user scrolled up. property stick: true; property last-height: 0px; pure function bottom-y(h: length, v: length) -> length { return min(0px, v - h); } changed room-nonce => { root.stick = true; } background: Theme.chat-bg; list := ListView { height: parent.height - 8px; // small gap above the composer changed content-height => { if (root.stick) { self.content-y = bottom-y(self.content-height, self.visible-height); } else if (self.content-y > -40px && root.last-height > 0px) { // Older messages were prepended: keep what the user was reading in place. self.content-y = self.content-y - (self.content-height - root.last-height); } root.last-height = self.content-height; } changed content-y => { root.stick = self.content-y <= bottom-y(self.content-height, self.visible-height) + 24px; if (self.content-y > -24px && self.content-height > self.visible-height && !root.loading-older) { root.load-older(); } } for m in root.messages: RowSwitch { m: m; room-name: root.room-name; reply => { root.reply(m.id, m.sender); } edit => { root.edit(m.id, m.body); } delete => { root.delete(m.id); } react(key) => { root.react(m.id, key); } } } if root.loading-older: Rectangle { y: 8px; width: 140px; height: 24px; x: (parent.width - self.width) / 2; border-radius: 12px; background: Theme.popup-bg; Text { text: "Loading history…"; color: Theme.text-muted; font-size: 12px; horizontal-alignment: center; vertical-alignment: center; } } }