This commit is contained in:
metacryst
2026-04-28 20:05:00 -05:00
commit 0d6c7683ff
123 changed files with 20922 additions and 0 deletions

View File

@@ -0,0 +1,317 @@
class DesktopChatSidebar extends Shadow {
constructor(chats, selectedId, onSelect) {
super()
this.chats = chats
this.selectedId = selectedId
this.onSelect = onSelect
this.searchText = ""
}
get filtered() {
const q = this.searchText.toLowerCase();
if (!q) return this.chats;
return this.chats.filter(c => c.name.toLowerCase().includes(q));
}
render() {
VStack(() => {
// ── Header ────────────────────────────────────────────────
VStack(() => {
// Search
HStack(() => {
p("🔍")
.margin(0)
.fontSize(0.78, em)
.opacity(0.4)
.flexShrink(0)
input()
.attr({ type: "text", placeholder: "Search…" })
.flex(1)
.border("none")
.outline("none")
.background("transparent")
.color("var(--headertext)")
.fontSize(0.85, em)
.onInput((e) => {
this.searchText = e.target.value;
this.rerender();
})
})
.gap(0.5, em)
.paddingHorizontal(0.75, em)
.paddingVertical(0.52, em)
.background("var(--darkaccent)")
.border("1px solid var(--divider)")
.borderRadius(0.5, em)
.alignItems("center")
.marginTop(0.75, em)
})
.paddingHorizontal(1, em)
.paddingTop(1.1, em)
.paddingBottom(0.75, em)
.flexShrink(0)
// ── Chat list ─────────────────────────────────────────────
VStack(() => {
const groups = this.groupChats(this.filtered);
if (groups.dms.length > 0) {
this.sectionLabel("DIRECT MESSAGES")
groups.dms.forEach(c => this.renderRow(c))
}
if (groups.groups.length > 0) {
this.sectionLabel("GROUPS")
groups.groups.forEach(c => this.renderRow(c))
}
if (groups.channels.length > 0) {
this.sectionLabel("CHANNELS")
groups.channels.forEach(c => this.renderRow(c))
}
if (this.filtered.length === 0) {
p("No results")
.margin(0)
.marginTop(2, em)
.fontSize(0.85, em)
.color("var(--headertext)")
.opacity(0.35)
.textAlign("center")
.width(100, pct)
}
})
.gap(0)
.flex(1)
.overflowY("auto")
.paddingBottom(1, em)
})
.height(100, pct)
.width(100, pct)
.boxSizing("border-box")
}
groupChats(chats) {
return {
dms: chats.filter(c => c.type === "dm"),
groups: chats.filter(c => c.type === "group"),
channels: chats.filter(c => c.type === "channel" || c.type === "announcement"),
};
}
sectionLabel(text) {
p(text)
.margin(0)
.marginTop(0.85, em)
.marginBottom(0.2, em)
.paddingHorizontal(1.1, em)
.fontSize(0.62, em)
.fontWeight("700")
.letterSpacing("0.07em")
.color("var(--headertext)")
.opacity(0.35)
.flexShrink(0)
}
renderRow(chat) {
const self = this
const isSelected = chat.id === this.selectedId;
const hasUnread = chat.unread > 0;
HStack(() => {
// Avatar / icon
this.renderIcon(chat)
// Name + preview
VStack(() => {
HStack(() => {
p(chat.name)
.margin(0)
.fontSize(0.88, em)
.fontWeight(hasUnread ? "700" : "500")
.color("var(--headertext)")
.flex(1)
.minWidth(0)
.overflow("hidden")
.whiteSpace("nowrap")
.textOverflow("ellipsis")
p(this.formatTime(chat.lastMessage?.sentAt))
.margin(0)
.fontSize(0.68, em)
.color("var(--headertext)")
.opacity(hasUnread ? 0.7 : 0.35)
.flexShrink(0)
.fontWeight(hasUnread ? "600" : "400")
})
.alignItems("center")
.width(100, pct)
HStack(() => {
p(this.previewText(chat))
.margin(0)
.fontSize(0.78, em)
.color("var(--headertext)")
.opacity(hasUnread ? 0.65 : 0.38)
.flex(1)
.minWidth(0)
.overflow("hidden")
.whiteSpace("nowrap")
.textOverflow("ellipsis")
.fontWeight(hasUnread ? "500" : "400")
if (hasUnread) {
p(chat.unread > 99 ? "99+" : String(chat.unread))
.margin(0)
.paddingHorizontal(0.45, em)
.paddingVertical(0.1, em)
.background("var(--quillred)")
.color("white")
.fontSize(0.65, em)
.fontWeight("700")
.borderRadius(100, px)
.minWidth(1.2, em)
.textAlign("center")
.flexShrink(0)
}
})
.alignItems("center")
.width(100, pct)
.marginTop(0.18, em)
})
.flex(1)
.minWidth(0)
})
.gap(0.65, em)
.paddingHorizontal(1, em)
.paddingVertical(0.6, em)
.marginHorizontal(0.4, em)
.borderRadius(0.55, em)
.background(isSelected ? "var(--app)" : "transparent")
.cursor("pointer")
.alignItems("center")
.width("calc(100% - 0.8em)")
.boxSizing("border-box")
.onClick(function(done){ if(done){ self.onSelect(chat.id) } })
}
renderIcon(chat) {
if (chat.type === "channel") {
VStack(() => {
p("#")
.margin(0)
.fontSize(1.05, em)
.fontWeight("700")
.color("white")
.lineHeight("1")
})
.width(2.35, em)
.height(2.35, em)
.borderRadius(0.45, em)
.background("#5865f2")
.justifyContent("center")
.alignItems("center")
.flexShrink(0)
} else if (chat.type === "announcement") {
VStack(() => {
p("📣")
.margin(0)
.fontSize(0.9, em)
.lineHeight("1")
})
.width(2.35, em)
.height(2.35, em)
.borderRadius(0.45, em)
.background("#f59e0b")
.justifyContent("center")
.alignItems("center")
.flexShrink(0)
} else if (chat.type === "group") {
// Stacked initials for group
ZStack(() => {
// Back circle
VStack(() => {
p(chat.members[1] ? chat.members[1][0].toUpperCase() : "?")
.margin(0)
.fontSize(0.6, em)
.fontWeight("700")
.color("white")
})
.width(1.8, em)
.height(1.8, em)
.borderRadius(50, pct)
.background(this.avatarColor(chat.members[1] || "B"))
.justifyContent("center")
.alignItems("center")
.position("absolute")
.bottom(0).right(0)
.boxSizing("border-box")
// Front circle
VStack(() => {
p(chat.members[0] ? chat.members[0][0].toUpperCase() : "?")
.margin(0)
.fontSize(0.6, em)
.fontWeight("700")
.color("white")
})
.width(1.8, em)
.height(1.8, em)
.borderRadius(50, pct)
.background(this.avatarColor(chat.members[0] || "A"))
.justifyContent("center")
.alignItems("center")
.position("absolute")
.top(0).left(0)
.boxSizing("border-box")
})
.width(2.35, em)
.height(2.35, em)
.position("relative")
.flexShrink(0)
} else {
// DM — single avatar
VStack(() => {
p(chat.name[0].toUpperCase())
.margin(0)
.fontSize(0.88, em)
.fontWeight("700")
.color("white")
.lineHeight("1")
})
.width(2.35, em)
.height(2.35, em)
.borderRadius(50, pct)
.background(this.avatarColor(chat.name))
.justifyContent("center")
.alignItems("center")
.flexShrink(0)
}
}
previewText(chat) {
const msg = chat.lastMessage;
if (!msg) return "No messages yet";
const sender = chat.type === "dm" ? "" : (msg.senderName?.split(" ")[0] + ": ");
return sender + msg.text;
}
formatTime(date) {
if (!date) return "";
const d = new Date(date);
const now = new Date();
const diffDays = Math.floor((now - d) / 86400000);
if (diffDays === 0) return d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
if (diffDays === 1) return "Yesterday";
if (diffDays < 7) return d.toLocaleDateString([], { weekday: "short" });
return d.toLocaleDateString([], { month: "short", day: "numeric" });
}
avatarColor(name) {
const colors = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6", "#ec4899", "#06b6d4", "#84cc16"];
let hash = 0;
for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash);
return colors[Math.abs(hash) % colors.length];
}
}
register(DesktopChatSidebar)

View File

@@ -0,0 +1,387 @@
class DesktopChatThread extends Shadow {
constructor(chat, currentUser) {
super()
this.chat = chat
this.currentUser = currentUser
this.draftText = ""
}
render() {
if (!this.chat) {
VStack(() => {
p("💬")
.margin(0)
.fontSize(2.8, em)
.opacity(0.18)
p("Select a conversation")
.margin(0)
.marginTop(0.75, em)
.fontSize(0.92, em)
.color("var(--headertext)")
.opacity(0.32)
})
.flex(1)
.justifyContent("center")
.alignItems("center")
.height(100, pct)
return;
}
VStack(() => {
this.renderHeader()
this.renderMessages()
this.renderComposer()
})
.height(100, pct)
.width(100, pct)
.overflow("hidden")
}
renderHeader() {
HStack(() => {
// Chat icon (same logic as sidebar, compact)
this.renderHeaderIcon()
VStack(() => {
p(this.chat.name)
.margin(0)
.fontSize(0.95, em)
.fontWeight("700")
.color("var(--headertext)")
p(this.headerSubtitle())
.margin(0)
.marginTop(0.08, em)
.fontSize(0.72, em)
.color("var(--headertext)")
.opacity(0.42)
})
.flex(1)
.minWidth(0)
})
.gap(0.75, em)
.paddingHorizontal(1.4, em)
.paddingVertical(0.85, em)
.borderBottom("1px solid var(--divider)")
.alignItems("center")
.flexShrink(0)
}
renderHeaderIcon() {
const chat = this.chat;
if (chat.type === "channel") {
VStack(() => {
p("#")
.margin(0)
.fontSize(1.05, em)
.fontWeight("700")
.color("white")
.lineHeight("1")
})
.width(2.2, em).height(2.2, em)
.borderRadius(0.45, em)
.background("#5865f2")
.justifyContent("center").alignItems("center")
.flexShrink(0)
} else if (chat.type === "announcement") {
VStack(() => {
p("📣").margin(0).fontSize(0.88, em).lineHeight("1")
})
.width(2.2, em).height(2.2, em)
.borderRadius(0.45, em)
.background("#f59e0b")
.justifyContent("center").alignItems("center")
.flexShrink(0)
} else if (chat.type === "group") {
ZStack(() => {
VStack(() => {
p(chat.members[1]?.[0]?.toUpperCase() || "?")
.margin(0).fontSize(0.55, em).fontWeight("700").color("white")
})
.width(1.65, em).height(1.65, em).borderRadius(50, pct)
.background(this.avatarColor(chat.members[1] || "B"))
.justifyContent("center").alignItems("center")
.position("absolute").bottom(0).right(0)
.boxSizing("border-box")
VStack(() => {
p(chat.members[0]?.[0]?.toUpperCase() || "?")
.margin(0).fontSize(0.55, em).fontWeight("700").color("white")
})
.width(1.65, em).height(1.65, em).borderRadius(50, pct)
.background(this.avatarColor(chat.members[0] || "A"))
.justifyContent("center").alignItems("center")
.position("absolute").top(0).left(0)
.boxSizing("border-box")
})
.width(2.2, em).height(2.2, em).position("relative").flexShrink(0)
} else {
VStack(() => {
p(chat.name[0].toUpperCase())
.margin(0).fontSize(0.85, em).fontWeight("700").color("white").lineHeight("1")
})
.width(2.2, em).height(2.2, em).borderRadius(50, pct)
.background(this.avatarColor(chat.name))
.justifyContent("center").alignItems("center").flexShrink(0)
}
}
headerSubtitle() {
const { type, members } = this.chat;
if (type === "channel" || type === "announcement") {
return `${this.chat.memberCount || members?.length || 0} members`;
}
if (type === "group") {
return members?.join(", ") || "";
}
return "Active recently";
}
renderMessages() {
const messages = this.chat.messages || [];
const grouped = this.groupByDate(messages);
VStack(() => {
if (messages.length === 0) {
VStack(() => {
p("No messages yet. Say hello!")
.margin(0)
.fontSize(0.88, em)
.color("var(--headertext)")
.opacity(0.3)
})
.flex(1)
.justifyContent("center")
.alignItems("center")
} else {
grouped.forEach(({ label, messages: dayMsgs }) => {
// Date separator
HStack(() => {
VStack(() => {}).flex(1).height(1, px).background("var(--divider)")
p(label)
.margin(0)
.marginHorizontal(0.85, em)
.fontSize(0.68, em)
.fontWeight("600")
.color("var(--headertext)")
.opacity(0.38)
.whiteSpace("nowrap")
VStack(() => {}).flex(1).height(1, px).background("var(--divider)")
})
.alignItems("center")
.paddingHorizontal(1.4, em)
.paddingVertical(0.85, em)
.flexShrink(0)
// Render message runs (consecutive messages from same sender)
const runs = this.groupIntoRuns(dayMsgs);
runs.forEach(run => this.renderRun(run))
})
}
})
.flex(1)
.overflowY("auto")
.paddingBottom(0.5, em)
.gap(0)
.width(100, pct)
.boxSizing("border-box")
.attr({ id: `thread-${this.chat.id}` })
.onAppear(function () { this.scrollTop = this.scrollHeight; })
}
renderRun(run) {
const isMe = run.senderId === this.currentUser.id;
const showAvatar = !isMe && this.chat.type !== "dm";
VStack(() => {
// Sender name + time (only at run start, non-DM)
if (!isMe && this.chat.type !== "dm") {
HStack(() => {
p(run.senderName)
.margin(0)
.fontSize(0.75, em)
.fontWeight("600")
.color(this.avatarColor(run.senderName))
p(this.formatMsgTime(run.messages[0].sentAt))
.margin(0)
.fontSize(0.68, em)
.color("var(--headertext)")
.opacity(0.32)
})
.gap(0.55, em)
.alignItems("baseline")
.marginBottom(0.2, em)
}
// Bubble stack
run.messages.forEach((msg, i) => {
const isLast = i === run.messages.length - 1;
this.renderBubble(msg, isMe, isLast)
})
})
.paddingHorizontal(1.4, em)
.paddingTop(0.55, em)
.paddingBottom(0.08, em)
.alignItems(isMe ? "flex-end" : "flex-start")
.width(100, pct)
.boxSizing("border-box")
.flexShrink(0)
}
renderBubble(msg, isMe, showTime) {
VStack(() => {
VStack(() => {
p(msg.text)
.margin(0)
.fontSize(0.88, em)
.color(isMe ? "white" : "var(--headertext)")
.lineHeight("1.5")
.whiteSpace("pre-wrap")
.wordBreak("break-word")
})
.paddingHorizontal(0.85, em)
.paddingVertical(0.55, em)
.background(isMe ? "var(--quillred)" : "var(--darkaccent)")
.borderRadius(isMe ? "1em 1em 0.25em 1em" : "1em 1em 1em 0.25em")
.maxWidth(32, em)
.boxSizing("border-box")
if (showTime) {
p(this.formatMsgTime(msg.sentAt))
.margin(0)
.marginTop(0.22, em)
.fontSize(0.65, em)
.color("var(--headertext)")
.opacity(0.32)
.alignSelf(isMe ? "flex-end" : "flex-start")
}
})
.alignItems(isMe ? "flex-end" : "flex-start")
.marginBottom(0.2, em)
}
renderComposer() {
const self = this
HStack(() => {
// Input
HStack(() => {
input()
.attr({ type: "text", placeholder: `Message ${this.chat.name}`, id: `composer-${this.chat.id}` })
.flex(1)
.border("none")
.outline("none")
.background("transparent")
.color("var(--headertext)")
.fontSize(0.9, em)
.onKeyDown((e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
})
})
.flex(1)
.paddingHorizontal(1, em)
.paddingVertical(0.62, em)
.background("var(--darkaccent)")
.border("1px solid var(--divider)")
.borderRadius(0.65, em)
.alignItems("center")
// Send button
button("↑")
.paddingHorizontal(0.75, em)
.paddingVertical(0.62, em)
.background("var(--quillred)")
.color("white")
.border("none")
.borderRadius(0.65, em)
.fontSize(1, em)
.fontWeight("700")
.cursor("pointer")
.flexShrink(0)
.onClick(function(done){ if(done){ self.sendMessage() } })
})
.gap(0.55, em)
.paddingHorizontal(1.4, em)
.paddingVertical(0.9, em)
.borderTop("1px solid var(--divider)")
.alignItems("center")
.flexShrink(0)
}
sendMessage() {
const input = this.$(`#composer-${this.chat.id}`);
if (!input) return;
const text = input.value.trim();
if (!text) return;
this.chat.messages.push({
id: Date.now(),
senderId: this.currentUser.id,
senderName: this.currentUser.name,
text,
sentAt: new Date()
});
this.chat.lastMessage = {
senderId: this.currentUser.id,
senderName: this.currentUser.name,
text,
sentAt: new Date()
};
this.chat.unread = 0;
input.value = "";
this.rerender();
}
groupByDate(messages) {
const map = new Map();
messages.forEach(msg => {
const key = new Date(msg.sentAt).toDateString();
if (!map.has(key)) map.set(key, []);
map.get(key).push(msg);
});
return Array.from(map.entries()).map(([key, messages]) => ({
label: this.dateSeparatorLabel(new Date(key)),
messages
}));
}
groupIntoRuns(messages) {
const runs = [];
messages.forEach(msg => {
const last = runs[runs.length - 1];
if (last && last.senderId === msg.senderId) {
last.messages.push(msg);
} else {
runs.push({ senderId: msg.senderId, senderName: msg.senderName, messages: [msg] });
}
});
return runs;
}
dateSeparatorLabel(date) {
const now = new Date();
const diffDays = Math.floor((now - date) / 86400000);
if (diffDays === 0) return "Today";
if (diffDays === 1) return "Yesterday";
if (diffDays < 7) return date.toLocaleDateString([], { weekday: "long" });
return date.toLocaleDateString([], { month: "long", day: "numeric", year: "numeric" });
}
formatMsgTime(date) {
return new Date(date).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
}
avatarColor(name) {
const colors = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6", "#ec4899", "#06b6d4", "#84cc16"];
let hash = 0;
for (let i = 0; i < (name || "").length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash);
return colors[Math.abs(hash) % colors.length];
}
}
register(DesktopChatThread)

213
chat/desktop/chat.js Normal file
View File

@@ -0,0 +1,213 @@
import "./DesktopChatSidebar.js"
import "./DesktopChatThread.js"
css(`
chat- {
font-family: 'Arial';
scrollbar-width: none;
-ms-overflow-style: none;
}
chat- input::placeholder {
color: var(--headertext);
opacity: 0.35;
}
`)
class Chat extends Shadow {
constructor() {
super()
this.currentUser = { id: 1, name: "You" }
this.selectedChatId = null
const ago = (h) => new Date(Date.now() - h * 3600000);
const msg = (senderId, senderName, text, hoursAgo) => ({ id: Math.random(), senderId, senderName, text, sentAt: ago(hoursAgo) });
this.chats = [
// ── Direct messages ───────────────────────────────────────
{
id: 1,
type: "dm",
name: "Sarah McIntyre",
members: ["Sarah McIntyre"],
memberCount: 2,
unread: 2,
lastMessage: { senderId: 2, senderName: "Sarah McIntyre", text: "Can you review the PR when you get a chance?", sentAt: ago(0.3) },
messages: [
msg(1, "You", "Hey Sarah, did you see the new design mockups?", 24),
msg(2, "Sarah McIntyre", "Just looked — they're really clean. I love the new sidebar.", 23.5),
msg(1, "You", "Agreed. Alex did a great job.", 23.4),
msg(2, "Sarah McIntyre", "Are we going to ship this week or wait for the backend?", 5),
msg(1, "You", "Let's aim for Thursday. I'll sync with Marcus.", 4.8),
msg(2, "Sarah McIntyre", "Sounds good 👍", 4.7),
msg(2, "Sarah McIntyre", "Can you review the PR when you get a chance?", 0.3),
]
},
{
id: 2,
type: "dm",
name: "Marcus Webb",
members: ["Marcus Webb"],
memberCount: 2,
unread: 0,
lastMessage: { senderId: 1, senderName: "You", text: "I'll send over the specs by EOD", sentAt: ago(1.5) },
messages: [
msg(3, "Marcus Webb", "Hey, the API endpoint is returning 500s on staging.", 3),
msg(1, "You", "Oh no — is it the auth middleware again?", 2.9),
msg(3, "Marcus Webb", "Yep. Same issue as last week.", 2.8),
msg(1, "You", "I'll patch it now. Give me 20 mins.", 2.75),
msg(3, "Marcus Webb", "Thanks, no rush.", 2.7),
msg(1, "You", "Fixed. Can you redeploy and check?", 2.2),
msg(3, "Marcus Webb", "All green 🎉 Thanks!", 2.1),
msg(1, "You", "I'll send over the specs by EOD", 1.5),
]
},
{
id: 3,
type: "dm",
name: "Priya Anand",
members: ["Priya Anand"],
memberCount: 2,
unread: 0,
lastMessage: { senderId: 4, senderName: "Priya Anand", text: "See you at the standup!", sentAt: ago(18) },
messages: [
msg(4, "Priya Anand", "Quick question — what's the launch date for v2?", 20),
msg(1, "You", "Still TBD, but we're targeting end of May.", 19.8),
msg(4, "Priya Anand", "Got it. I'll update the roadmap doc.", 19.5),
msg(1, "You", "Perfect, thanks Priya.", 19.4),
msg(4, "Priya Anand", "See you at the standup!", 18),
]
},
// ── Groups ────────────────────────────────────────────────
{
id: 4,
type: "group",
name: "Product Team",
members: ["Sarah McIntyre", "Marcus Webb", "Priya Anand", "You"],
memberCount: 4,
unread: 5,
lastMessage: { senderId: 2, senderName: "Sarah McIntyre", text: "I've updated the Figma file with the new flows", sentAt: ago(0.15) },
messages: [
msg(3, "Marcus Webb", "Morning everyone! API docs are updated.", 8),
msg(4, "Priya Anand", "Nice work Marcus 🙌", 7.9),
msg(1, "You", "I'll start on the integration tests today.", 7.8),
msg(2, "Sarah McIntyre", "Great. I'm finishing up the onboarding screens.", 7.5),
msg(4, "Priya Anand", "Can we do a quick sync at 2pm?", 2),
msg(1, "You", "Works for me.", 1.95),
msg(3, "Marcus Webb", "Same", 1.9),
msg(2, "Sarah McIntyre", "I'll send the invite.", 1.85),
msg(2, "Sarah McIntyre", "I've updated the Figma file with the new flows", 0.15),
]
},
{
id: 5,
type: "group",
name: "Design Review",
members: ["Sarah McIntyre", "You", "Jordan Kim"],
memberCount: 3,
unread: 0,
lastMessage: { senderId: 5, senderName: "Jordan Kim", text: "The contrast on mobile looks off — can we bump it?", sentAt: ago(26) },
messages: [
msg(5, "Jordan Kim", "Hey, sharing the first round of designs for the settings page.", 30),
msg(2, "Sarah McIntyre", "These look great! Love the card layout.", 29.5),
msg(1, "You", "Agreed. One thought — the spacing on the form feels a bit tight.", 29),
msg(5, "Jordan Kim", "Good call. I'll loosen it up.", 28.8),
msg(2, "Sarah McIntyre", "Also maybe we increase the font size slightly?", 28),
msg(5, "Jordan Kim", "The contrast on mobile looks off — can we bump it?", 26),
]
},
// ── Channels ──────────────────────────────────────────────
{
id: 6,
type: "channel",
name: "general",
members: ["Sarah McIntyre", "Marcus Webb", "Priya Anand", "Jordan Kim", "You"],
memberCount: 24,
unread: 11,
lastMessage: { senderId: 3, senderName: "Marcus Webb", text: "Just pushed the hotfix to production", sentAt: ago(0.08) },
messages: [
msg(4, "Priya Anand", "Good morning team! Reminder: all-hands is Thursday at 10am.", 9),
msg(2, "Sarah McIntyre", "Thanks for the reminder!", 8.9),
msg(5, "Jordan Kim", "Will there be a recording for those in other time zones?", 8.7),
msg(4, "Priya Anand", "Yes — I'll post the link in #announcements after.", 8.6),
msg(1, "You", "Thanks Priya 🙏", 8.5),
msg(3, "Marcus Webb", "Staging is back up btw, had a brief outage this morning.", 4),
msg(2, "Sarah McIntyre", "Oh I didn't even notice, nice quick fix!", 3.8),
msg(3, "Marcus Webb", "Just pushed the hotfix to production", 0.08),
]
},
{
id: 7,
type: "channel",
name: "engineering",
members: ["Marcus Webb", "You"],
memberCount: 8,
unread: 0,
lastMessage: { senderId: 1, senderName: "You", text: "PR is up: #247 — adds rate limiting to the auth routes", sentAt: ago(3) },
messages: [
msg(3, "Marcus Webb", "Heads up: I'm updating the CI pipeline today. Builds might be slow for a bit.", 5),
msg(1, "You", "Noted, thanks for the warning.", 4.9),
msg(3, "Marcus Webb", "Back to normal now.", 4),
msg(1, "You", "PR is up: #247 — adds rate limiting to the auth routes", 3),
]
},
// ── Announcements ─────────────────────────────────────────
{
id: 8,
type: "announcement",
name: "Announcements",
members: [],
memberCount: 24,
unread: 1,
lastMessage: { senderId: 4, senderName: "Priya Anand", text: "Q2 planning kick-off is next Monday at 9am. Please come prepared with your team's priorities.", sentAt: ago(12) },
messages: [
msg(4, "Priya Anand", "Welcome to the team, Jordan! 🎉 Jordan joins us as a Product Designer and will be working closely with Sarah.", 72),
msg(4, "Priya Anand", "Reminder: expense reports for March are due this Friday.", 48),
msg(4, "Priya Anand", "Q2 planning kick-off is next Monday at 9am. Please come prepared with your team's priorities.", 12),
]
},
]
this.selectedChatId = this.chats[0].id
}
get selectedChat() {
return this.chats.find(c => c.id === this.selectedChatId) || null;
}
render() {
HStack(() => {
// Left sidebar
VStack(() => {
DesktopChatSidebar(this.chats, this.selectedChatId, (id) => {
this.selectedChatId = id;
const chat = this.chats.find(c => c.id === id);
if (chat) chat.unread = 0;
this.rerender();
})
})
.width(280, px)
.minWidth(240, px)
.height(100, pct)
.borderRight("1px solid var(--divider)")
.flexShrink(0)
.overflow("hidden")
// Right thread
VStack(() => {
DesktopChatThread(this.selectedChat, this.currentUser)
})
.flex(1)
.height(100, pct)
.overflow("hidden")
})
.height(100, pct)
.width(100, pct)
.overflow("hidden")
}
}
register(Chat)