This includes temporary AI-generated frontend scaffolding, none of this will be used in production. It was just used during development for the backend.
400 lines
12 KiB
Plaintext
400 lines
12 KiB
Plaintext
/// 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 <ReactionItem> 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 <bool> 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 <MessageItem> 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 <string> 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 <string> 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 <string> 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 <MessageItem> m;
|
|
in property <string> 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 <string> room-name;
|
|
in property <bool> loading-older;
|
|
in property <int> 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 <bool> stick: true;
|
|
property <length> 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;
|
|
}
|
|
}
|
|
}
|