init
This commit is contained in:
619
chat/chat.js
Normal file
619
chat/chat.js
Normal file
@@ -0,0 +1,619 @@
|
||||
import server from "/@server/server.js"
|
||||
|
||||
css(`
|
||||
chat-, chat- * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
chat- {
|
||||
font-family: 'Arial';
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
chat-::-webkit-scrollbar { display: none; }
|
||||
chat- input::placeholder {
|
||||
color: var(--headertext);
|
||||
opacity: 0.35;
|
||||
}
|
||||
`)
|
||||
|
||||
class Chat extends Shadow {
|
||||
selectedChatId = null
|
||||
searchText = ""
|
||||
searchOpen = false
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this.currentUser = { id: 1, name: "You" }
|
||||
|
||||
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 = [
|
||||
{
|
||||
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),
|
||||
]
|
||||
},
|
||||
{
|
||||
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),
|
||||
]
|
||||
},
|
||||
{
|
||||
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),
|
||||
]
|
||||
},
|
||||
{
|
||||
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.", sentAt: ago(12) },
|
||||
messages: [
|
||||
msg(4, "Priya Anand", "Welcome to the team, Jordan! 🎉 Jordan joins us as a Product Designer.", 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),
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
get selectedChat() {
|
||||
return this.chats.find(c => c.id === this.selectedChatId) || null
|
||||
}
|
||||
|
||||
get filteredChats() {
|
||||
if (!this.searchText) return this.chats
|
||||
const q = this.searchText.toLowerCase()
|
||||
return this.chats.filter(c => c.name.toLowerCase().includes(q))
|
||||
}
|
||||
|
||||
render() {
|
||||
VStack(() => {
|
||||
if (this.selectedChatId === null) {
|
||||
this.renderList()
|
||||
} else {
|
||||
this.renderThread()
|
||||
}
|
||||
})
|
||||
.height(100, pct)
|
||||
.width(100, pct)
|
||||
.overflow("hidden")
|
||||
}
|
||||
|
||||
// ── List view ────────────────────────────────────────────────────────────
|
||||
|
||||
renderList() {
|
||||
VStack(() => {
|
||||
this.renderListHeader()
|
||||
this.renderChatList()
|
||||
})
|
||||
.height(100, pct)
|
||||
.width(100, vw)
|
||||
.overflow("hidden")
|
||||
}
|
||||
|
||||
renderListHeader() {
|
||||
VStack(() => {
|
||||
|
||||
// Search bar
|
||||
if (this.searchOpen) {
|
||||
HStack(() => {
|
||||
p("🔍").margin(0).fontSize(0.78, em).opacity(0.4).flexShrink(0)
|
||||
input()
|
||||
.attr({ type: "text", placeholder: "Search…", autofocus: "true" })
|
||||
.flex(1).border("none").outline("none")
|
||||
.background("transparent")
|
||||
.color("var(--headertext)").fontSize(0.9, em)
|
||||
.onInput((e) => { this.searchText = e.target.value; this.rerender() })
|
||||
})
|
||||
.gap(0.5, em)
|
||||
.paddingHorizontal(0.85, em).paddingVertical(0.55, em)
|
||||
.background("var(--darkaccent)")
|
||||
.border("1px solid var(--divider)")
|
||||
.borderRadius(0.6, em)
|
||||
.alignItems("center")
|
||||
.marginHorizontal(1.1, em)
|
||||
.marginBottom(0.5, em)
|
||||
}
|
||||
})
|
||||
.flexShrink(0)
|
||||
}
|
||||
|
||||
renderChatList() {
|
||||
const chats = this.filteredChats
|
||||
const groups = {
|
||||
dms: chats.filter(c => c.type === "dm"),
|
||||
groups: chats.filter(c => c.type === "group"),
|
||||
channels: chats.filter(c => c.type === "channel" || c.type === "announcement"),
|
||||
}
|
||||
|
||||
VStack(() => {
|
||||
if (chats.length === 0) {
|
||||
VStack(() => {
|
||||
p("No results").margin(0).fontSize(0.88, em)
|
||||
.color("var(--headertext)").opacity(0.35).textAlign("center")
|
||||
}).flex(1).justifyContent("center").alignItems("center")
|
||||
} else {
|
||||
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))
|
||||
}
|
||||
}
|
||||
})
|
||||
.flex(1).overflowY("auto")
|
||||
.paddingBottom(2, em)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
renderRow(chat) {
|
||||
const hasUnread = chat.unread > 0
|
||||
HStack(() => {
|
||||
this.renderIcon(chat)
|
||||
|
||||
VStack(() => {
|
||||
HStack(() => {
|
||||
p(chat.name)
|
||||
.margin(0).fontSize(0.95, 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.7, em)
|
||||
.color("var(--headertext)")
|
||||
.opacity(hasUnread ? 0.7 : 0.35)
|
||||
.fontWeight(hasUnread ? "600" : "400")
|
||||
.flexShrink(0)
|
||||
})
|
||||
.alignItems("center").width(100, pct)
|
||||
|
||||
HStack(() => {
|
||||
p(this.previewText(chat))
|
||||
.margin(0).fontSize(0.82, em)
|
||||
.color("var(--headertext)")
|
||||
.opacity(hasUnread ? 0.65 : 0.38)
|
||||
.fontWeight(hasUnread ? "500" : "400")
|
||||
.flex(1).minWidth(0)
|
||||
.overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis")
|
||||
|
||||
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.2, em)
|
||||
})
|
||||
.flex(1).minWidth(0)
|
||||
})
|
||||
.gap(0.75, em)
|
||||
.paddingHorizontal(1.1, em).paddingVertical(0.8, em)
|
||||
.alignItems("center")
|
||||
.borderBottom("1px solid var(--divider)")
|
||||
.onTouch((start) => {
|
||||
if (!start) {
|
||||
this.selectedChatId = chat.id
|
||||
chat.unread = 0
|
||||
this.rerender()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ── Thread view ──────────────────────────────────────────────────────────
|
||||
|
||||
renderThread() {
|
||||
const chat = this.selectedChat
|
||||
if (!chat) return
|
||||
|
||||
VStack(() => {
|
||||
this.renderThreadHeader(chat)
|
||||
this.renderMessages(chat)
|
||||
this.renderComposer(chat)
|
||||
})
|
||||
.height(100, pct)
|
||||
.width(100, pct)
|
||||
.overflow("hidden")
|
||||
}
|
||||
|
||||
renderThreadHeader(chat) {
|
||||
HStack(() => {
|
||||
// Back button
|
||||
p("‹")
|
||||
.margin(0).fontSize(1.8, em).lineHeight("1")
|
||||
.color("var(--headertext)").paddingRight(0.5, em)
|
||||
.cursor("pointer")
|
||||
.onTouch((start) => {
|
||||
if (!start) {
|
||||
this.selectedChatId = null
|
||||
this.rerender()
|
||||
}
|
||||
})
|
||||
|
||||
this.renderHeaderIcon(chat)
|
||||
|
||||
VStack(() => {
|
||||
p(chat.type === "channel" ? "#" + chat.name : chat.name)
|
||||
.margin(0).fontSize(0.95, em).fontWeight("700")
|
||||
.color("var(--headertext)")
|
||||
.overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis")
|
||||
|
||||
p(this.headerSubtitle(chat))
|
||||
.margin(0).marginTop(0.06, em)
|
||||
.fontSize(0.7, em).color("var(--headertext)").opacity(0.4)
|
||||
})
|
||||
.flex(1).minWidth(0)
|
||||
})
|
||||
.gap(0.6, em)
|
||||
.paddingHorizontal(1.1, em).paddingVertical(0.85, em)
|
||||
.borderBottom("1px solid var(--divider)")
|
||||
.alignItems("center")
|
||||
.flexShrink(0)
|
||||
}
|
||||
|
||||
renderMessages(chat) {
|
||||
const messages = 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.75, em)
|
||||
.fontSize(0.65, em).fontWeight("600")
|
||||
.color("var(--headertext)").opacity(0.38).whiteSpace("nowrap")
|
||||
VStack(() => {}).flex(1).height(1, px).background("var(--divider)")
|
||||
})
|
||||
.alignItems("center")
|
||||
.paddingHorizontal(1.1, em).paddingVertical(0.75, em)
|
||||
.flexShrink(0)
|
||||
|
||||
const runs = this.groupIntoRuns(dayMsgs)
|
||||
runs.forEach(run => this.renderRun(run, chat))
|
||||
})
|
||||
}
|
||||
})
|
||||
.flex(1).overflowY("auto")
|
||||
.paddingBottom(0.5, em)
|
||||
.width(100, pct).boxSizing("border-box")
|
||||
.onAppear(function () { this.scrollTop = this.scrollHeight })
|
||||
}
|
||||
|
||||
renderRun(run, chat) {
|
||||
const isMe = run.senderId === this.currentUser.id
|
||||
const showSenderName = !isMe && chat.type !== "dm"
|
||||
|
||||
VStack(() => {
|
||||
if (showSenderName) {
|
||||
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.5, em).alignItems("baseline").marginBottom(0.2, em)
|
||||
}
|
||||
|
||||
run.messages.forEach((msg, i) => {
|
||||
this.renderBubble(msg, isMe, i === run.messages.length - 1)
|
||||
})
|
||||
})
|
||||
.paddingHorizontal(1.1, 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.92, em).lineHeight("1.5")
|
||||
.color(isMe ? "white" : "var(--headertext)")
|
||||
.whiteSpace("pre-wrap").wordBreak("break-word")
|
||||
})
|
||||
.paddingHorizontal(0.9, em).paddingVertical(0.6, em)
|
||||
.background(isMe ? "var(--quillred)" : "var(--darkaccent)")
|
||||
.borderRadius(isMe ? "1em 1em 0.25em 1em" : "1em 1em 1em 0.25em")
|
||||
.maxWidth("75vw").boxSizing("border-box")
|
||||
|
||||
if (showTime) {
|
||||
p(this.formatMsgTime(msg.sentAt))
|
||||
.margin(0).marginTop(0.22, em)
|
||||
.fontSize(0.62, em).color("var(--headertext)").opacity(0.32)
|
||||
.alignSelf(isMe ? "flex-end" : "flex-start")
|
||||
}
|
||||
})
|
||||
.alignItems(isMe ? "flex-end" : "flex-start")
|
||||
.marginBottom(0.2, em)
|
||||
}
|
||||
|
||||
renderComposer(chat) {
|
||||
HStack(() => {
|
||||
HStack(() => {
|
||||
input()
|
||||
.attr({ type: "text", placeholder: `Message…`, id: `composer-${chat.id}` })
|
||||
.flex(1).border("none").outline("none")
|
||||
.background("transparent")
|
||||
.color("var(--headertext)").fontSize(0.95, em)
|
||||
})
|
||||
.flex(1)
|
||||
.paddingHorizontal(1, em).paddingVertical(0.7, em)
|
||||
.background("var(--darkaccent)")
|
||||
.border("1px solid var(--divider)")
|
||||
.borderRadius(1.5, em)
|
||||
.alignItems("center")
|
||||
|
||||
button("↑")
|
||||
.paddingHorizontal(0.85, em).paddingVertical(0.7, em)
|
||||
.background("var(--quillred)").color("white")
|
||||
.border("none").borderRadius(50, pct)
|
||||
.fontSize(1.05, em).fontWeight("700")
|
||||
.cursor("pointer").flexShrink(0)
|
||||
.onClick((done) => { if (done) this.sendMessage(chat) })
|
||||
})
|
||||
.gap(0.55, em)
|
||||
.paddingHorizontal(1.1, em).paddingVertical(0.75, em)
|
||||
.borderTop("1px solid var(--divider)")
|
||||
.alignItems("center")
|
||||
.flexShrink(0)
|
||||
}
|
||||
|
||||
sendMessage(chat) {
|
||||
const input = this.$(`#composer-${chat.id}`)
|
||||
if (!input) return
|
||||
const text = input.value.trim()
|
||||
if (!text) return
|
||||
|
||||
chat.messages.push({
|
||||
id: Date.now(),
|
||||
senderId: this.currentUser.id,
|
||||
senderName: this.currentUser.name,
|
||||
text,
|
||||
sentAt: new Date()
|
||||
})
|
||||
chat.lastMessage = { senderId: this.currentUser.id, senderName: this.currentUser.name, text, sentAt: new Date() }
|
||||
chat.unread = 0
|
||||
input.value = ""
|
||||
this.rerender()
|
||||
}
|
||||
|
||||
// ── Shared icon rendering ─────────────────────────────────────────────────
|
||||
|
||||
renderIcon(chat, size = 2.5) {
|
||||
if (chat.type === "channel") {
|
||||
VStack(() => {
|
||||
p("#").margin(0).fontSize(1.05, em).fontWeight("700").color("white").lineHeight("1")
|
||||
})
|
||||
.width(size, em).height(size, 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(size, em).height(size, em).borderRadius(0.45, em)
|
||||
.background("#f59e0b").justifyContent("center").alignItems("center").flexShrink(0)
|
||||
} else if (chat.type === "group") {
|
||||
const s = size * 0.76
|
||||
ZStack(() => {
|
||||
VStack(() => {
|
||||
p((chat.members[1] || "B")[0].toUpperCase())
|
||||
.margin(0).fontSize(0.6, em).fontWeight("700").color("white")
|
||||
})
|
||||
.width(s, em).height(s, em).borderRadius(50, pct)
|
||||
.background(this.avatarColor(chat.members[1] || "B"))
|
||||
.justifyContent("center").alignItems("center")
|
||||
.position("absolute").bottom(0).right(0)
|
||||
|
||||
VStack(() => {
|
||||
p((chat.members[0] || "A")[0].toUpperCase())
|
||||
.margin(0).fontSize(0.6, em).fontWeight("700").color("white")
|
||||
})
|
||||
.width(s, em).height(s, em).borderRadius(50, pct)
|
||||
.background(this.avatarColor(chat.members[0] || "A"))
|
||||
.justifyContent("center").alignItems("center")
|
||||
.position("absolute").top(0).left(0)
|
||||
})
|
||||
.width(size, em).height(size, em).position("relative").flexShrink(0)
|
||||
} else {
|
||||
VStack(() => {
|
||||
p(chat.name[0].toUpperCase())
|
||||
.margin(0).fontSize(0.9, em).fontWeight("700").color("white").lineHeight("1")
|
||||
})
|
||||
.width(size, em).height(size, em).borderRadius(50, pct)
|
||||
.background(this.avatarColor(chat.name))
|
||||
.justifyContent("center").alignItems("center").flexShrink(0)
|
||||
}
|
||||
}
|
||||
|
||||
renderHeaderIcon(chat) {
|
||||
this.renderIcon(chat, 2.2)
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
headerSubtitle(chat) {
|
||||
if (chat.type === "channel" || chat.type === "announcement") return `${chat.memberCount} members`
|
||||
if (chat.type === "group") return chat.members?.join(", ") || ""
|
||||
return "Active recently"
|
||||
}
|
||||
|
||||
formatTime(date) {
|
||||
if (!date) return ""
|
||||
const d = new Date(date), 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" })
|
||||
}
|
||||
|
||||
formatMsgTime(date) {
|
||||
return new Date(date).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })
|
||||
}
|
||||
|
||||
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" })
|
||||
}
|
||||
|
||||
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(Chat)
|
||||
Reference in New Issue
Block a user