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)