class DesktopAnnouncementsFeed extends Shadow { constructor(announcements, selectedId, searchText, onSelect, onSearch) { super() this.announcements = announcements this.selectedId = selectedId this.searchText = searchText this.onSelect = onSelect this.onSearch = onSearch } render() { VStack(() => { // Search bar HStack(() => { p("šŸ”") .margin(0).fontSize(0.78, em).opacity(0.38).flexShrink(0) input() .attr({ type: "text", placeholder: "Search announcements…", value: this.searchText }) .flex(1).border("none").outline("none").background("transparent") .color("var(--headertext)").fontSize(0.85, em) .onInput((e) => this.onSearch(e.target.value)) }) .gap(0.5, em).paddingHorizontal(0.85, em).paddingVertical(0.58, em) .background("var(--darkaccent)").border("1px solid var(--divider)") .borderRadius(0.5, em).alignItems("center") .marginHorizontal(0.75, em).marginTop(0.75, em).marginBottom(0.5, em) .flexShrink(0) // Count p(`${this.announcements.length} announcement${this.announcements.length !== 1 ? "s" : ""}`) .margin(0).paddingHorizontal(1.1, em).paddingBottom(0.35, em) .fontSize(0.68, em).fontWeight("700").letterSpacing("0.05em") .color("var(--headertext)").opacity(0.32) .flexShrink(0) // List VStack(() => { if (this.announcements.length === 0) { VStack(() => { p(this.searchText ? "No results" : "No announcements yet") .margin(0).fontSize(0.85, em) .color("var(--headertext)").opacity(0.32).textAlign("center") }) .flex(1).justifyContent("center").alignItems("center").paddingTop(3, em) } else { this.announcements.forEach(ann => this.renderRow(ann)) } }) .flex(1).overflowY("auto").gap(0).paddingBottom(0.75, em) }) .height(100, pct).width(100, pct).boxSizing("border-box") } renderRow(ann) { const isSelected = ann.id === this.selectedId const isEdited = ann.created !== ann.updated_at const isMe = ann.creator_id === global.profile.id const author = this.getAuthor(ann.creator_id) const authorName = isMe ? "You" : author const initials = this.getInitials(ann.creator_id) VStack(() => { HStack(() => { // Avatar VStack(() => { p(initials) .margin(0).fontSize(0.6, em).fontWeight("700") .color("white").lineHeight("1") }) .width(2.1, em).height(2.1, em).borderRadius(50, pct) .background(this.avatarColor(author)) .justifyContent("center").alignItems("center").flexShrink(0) VStack(() => { // Author + date HStack(() => { p(authorName) .margin(0).fontSize(0.8, em).fontWeight("600") .color(isMe ? "var(--quillred)" : "var(--headertext)") .flex(1).minWidth(0) .overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis") p(this.relativeDate(ann.created)) .margin(0).fontSize(0.68, em) .color("var(--headertext)").opacity(0.38).flexShrink(0) }) .alignItems("center").gap(0.4, em) // Preview text p(ann.text) .margin(0).marginTop(0.12, em).fontSize(0.78, em) .color("var(--headertext)") .opacity(isSelected ? 0.75 : 0.45) .overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis") .lineHeight("1.4") }) .flex(1).minWidth(0) }) .gap(0.62, em).alignItems("flex-start") if (isEdited) { p("edited") .margin(0).marginTop(0.3, em) .fontSize(0.62, em).fontStyle("italic") .color("var(--headertext)").opacity(0.28) .alignSelf("flex-end") } }) .paddingHorizontal(0.85, em).paddingVertical(0.7, em) .marginHorizontal(0.4, em) .borderRadius(0.55, em) .background(isSelected ? "var(--accent)" : "transparent") .cursor("pointer") .width("calc(100% - 0.8em)").boxSizing("border-box") .onClick((done) => { if(done) this.onSelect(ann.id) }) } getAuthor(creatorId) { const members = global.currentNetwork.data?.members || [] const m = members.find(m => m.id === creatorId) return m ? `${m.first_name} ${m.last_name}` : "Unknown" } getInitials(creatorId) { const members = global.currentNetwork.data?.members || [] const m = members.find(m => m.id === creatorId) if (!m) return "?" return [m.first_name?.[0], m.last_name?.[0]].filter(Boolean).join("").toUpperCase() } relativeDate(raw) { const d = new Date(raw) const diff = Date.now() - d const mins = Math.floor(diff / 60000) const hours = Math.floor(diff / 3600000) const days = Math.floor(diff / 86400000) if (mins < 1) return "just now" if (mins < 60) return `${mins}m ago` if (hours < 24) return `${hours}h ago` if (days === 1) return "yesterday" if (days < 7) return `${days}d ago` 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(DesktopAnnouncementsFeed)