init
This commit is contained in:
317
chat/desktop/DesktopChatSidebar.js
Normal file
317
chat/desktop/DesktopChatSidebar.js
Normal 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)
|
||||
Reference in New Issue
Block a user