// social.jsx — Messages inbox, DM thread, member profile with follow/message
const { useState: useStateS, useEffect: useEffectS, useRef: useRefS } = React;
const { Card, Chip, CoverArt, Waveform, TopBar, IconBtn, DisplayHeading, Icon, Avatar } = window.LAUNCH_SCREENS_1;
// ─────────────────────────────────────────────────────────────
// Member directory — referenced from Feed + Messages
// ─────────────────────────────────────────────────────────────
const MEMBERS = {
'kai.park': {
handle: 'kai.park', name: 'Kai Park', lbl: 'KP', color: '#C72820',
pal: ['#C72820', '#0A0A0A'],
bio: 'producer / engineer. brooklyn. dark synth, neon kicks. open to collabs.',
tier: 'ORBIT', city: 'BROOKLYN', genres: ['ALT POP', 'SYNTH'],
followers: 1240, following: 89, releases: 6, streak: 21,
online: true,
},
'sasha.vee': {
handle: 'sasha.vee', name: 'Sasha Vee', lbl: 'SV', color: '#CAFF33',
pal: ['#CAFF33', '#7A5AE0'],
bio: 'songwriter / lead vox. los angeles. just got on fresh finds ☁️',
tier: 'STRATOSPHERE', city: 'LA', genres: ['SOUL', 'R&B'],
followers: 8420, following: 142, releases: 11, streak: 64,
online: true,
},
'theo.m': {
handle: 'theo.m', name: 'Theo Marlow', lbl: 'TM', color: '#7A5AE0',
pal: ['#7A5AE0', '#FBBF24'],
bio: 'r&b producer & topliner. atlanta. low end specialist.',
tier: 'ORBIT', city: 'ATL', genres: ['R&B', 'HIP HOP'],
followers: 612, following: 203, releases: 3, streak: 8,
online: false,
},
'isla.loop': {
handle: 'isla.loop', name: 'Isla Loop', lbl: 'IL', color: '#FBBF24',
pal: ['#FBBF24', '#7A5AE0'],
bio: 'ambient / electronic. lisbon. sound design nerd. always demoing.',
tier: 'LAUNCH PAD', city: 'LISBON', genres: ['AMBIENT', 'ELECTRONIC'],
followers: 327, following: 88, releases: 4, streak: 15,
online: false,
},
};
// Conversations — sample data
const THREADS = [
{
id: 'kai.park',
member: 'kai.park',
unread: 2,
lastTime: '12:42 PM',
messages: [
{ from: 'them', t: 'yo that mix from your assignment is FIRE', ts: '11:30 AM' },
{ from: 'them', t: 'the way you sat the 808 in there 🤯', ts: '11:30 AM' },
{ from: 'me', t: 'thank you bro!! took me forever lol', ts: '11:45 AM' },
{ from: 'me', t: 'noah\'s lesson on bus comp changed my life fr', ts: '11:45 AM' },
{ from: 'them', t: 'fr fr. yo i\'m working on a remix — would u send me your vocal stems?', ts: '12:38 PM' },
{ from: 'them', t: 'i\'ll cut you in on splits ofc', ts: '12:42 PM', kind: 'voice' },
],
},
{
id: 'sasha.vee',
member: 'sasha.vee',
unread: 0,
lastTime: 'YESTERDAY',
messages: [
{ from: 'them', t: 'congrats on the streak 🔥', ts: 'yesterday' },
{ from: 'me', t: 'thank u!! you\'re a huge inspo', ts: 'yesterday' },
],
},
{
id: 'theo.m',
member: 'theo.m',
unread: 1,
lastTime: 'TUE',
messages: [
{ from: 'them', t: 'pull up on the discord later? im sharing a vocal chain preset', ts: 'tue' },
],
},
{
id: 'isla.loop',
member: 'isla.loop',
unread: 0,
lastTime: 'MAR 28',
messages: [
{ from: 'them', t: 'thx for the feedback on low tide 🙏', ts: 'mar 28' },
],
},
];
// Suggested-to-follow
const SUGGESTIONS = ['theo.m', 'isla.loop'];
// ─────────────────────────────────────────────────────────────
// FollowButton — used on Feed + Member profile
// ─────────────────────────────────────────────────────────────
function FollowButton({ theme, following, onToggle, size = 'sm' }) {
return (
{following ? '✓ FOLLOWING' : '+ FOLLOW'}
);
}
// ─────────────────────────────────────────────────────────────
// MessagesScreen — inbox
// ─────────────────────────────────────────────────────────────
function MessagesScreen({ theme, onBack, onOpenThread, onOpenMember, threads, following, onToggleFollow, members = MEMBERS }) {
const [tab, setTab] = useStateS('inbox'); // 'inbox' | 'requests'
const memberOr = (h) => members[h] || { handle: h, name: h, lbl: (h || '?').slice(0, 2).toUpperCase(), color: '#888', online: false };
return (
{/* Top bar */}
{threads.reduce((a, t) => a + t.unread, 0)} UNREAD
MESSAGES
{/* Tabs */}
{[
{ id: 'inbox', l: `INBOX · ${threads.length}` },
{ id: 'requests', l: 'REQUESTS · 3' },
].map(t => (
setTab(t.id)} style={{
padding: '8px 14px', borderRadius: 99, cursor: 'pointer',
border: 'none',
background: tab === t.id ? theme.text : 'transparent',
color: tab === t.id ? theme.bg : theme.text,
fontFamily: theme.fontMono, fontSize: 11, fontWeight: 700,
letterSpacing: 0.8, textTransform: 'uppercase',
boxShadow: tab === t.id ? 'none' : `inset 0 0 0 1px ${theme.border}`,
}}>{t.l}
))}
{tab === 'inbox' ? (
<>
{/* Active now strip */}
● ACTIVE NOW
{Object.values(members).filter(m => m.online).map(m => (
onOpenMember(m.handle)} style={{
background: 'transparent', border: 'none', cursor: 'pointer',
display: 'flex', flexDirection: 'column', alignItems: 'center',
gap: 6, padding: 0, flexShrink: 0,
}}>
{m.handle.split('.')[0]}
))}
{/* Threads */}
{threads.map((th, i) => {
const m = memberOr(th.member);
const last = th.messages[th.messages.length - 1] || { t: '', from: 'them' };
const lastText = last.kind === 'voice' ? '🎙 Voice note · 0:23' : last.t;
return (
onOpenThread(th.id)} style={{
background: 'transparent', border: 'none', cursor: 'pointer',
display: 'flex', alignItems: 'center', gap: 12,
padding: `12px ${theme.pad}px`, textAlign: 'left',
borderBottom: i < threads.length - 1 ? `1px solid ${theme.border}` : 'none',
}}>
0 ? 700 : 600 }}>
{m.handle}
0 ? theme.accent : theme.muted, letterSpacing: 0.5, fontWeight: 700, flexShrink: 0 }}>
{th.lastTime}
0 ? theme.text : theme.muted,
fontWeight: th.unread > 0 ? 500 : 400,
marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}>
{last.from === 'me' ? 'You: ' : ''}{lastText}
{th.unread > 0 && (
{th.unread}
)}
);
})}
{/* Suggestions to follow */}
SUGGESTED · BASED ON YOUR GENRE
{SUGGESTIONS.filter(handle => members[handle]).map(handle => {
const m = members[handle];
return (
onOpenMember(handle)} style={{
width: 44, height: 44, borderRadius: 99, background: m.color,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontFamily: theme.fontDisplay, color: '#0A0A0A', fontSize: 14,
border: 'none', cursor: 'pointer', padding: 0,
}}>{m.lbl}
{m.handle}
{m.genres.join(' · ')} · {m.city}
onToggleFollow(handle)} />
);
})}
>
) : (
{Icon.bell(theme.muted)}
3 PEOPLE WANT TO MESSAGE YOU
REQUESTS COMING SOON
)}
);
}
// ─────────────────────────────────────────────────────────────
// ThreadScreen — DM conversation
// ─────────────────────────────────────────────────────────────
function ThreadScreen({ theme, threadId, threads, onBack, onSend, onOpenMember, following, onToggleFollow, members = MEMBERS }) {
const thread = threads.find(t => t.id === threadId);
if (!thread) return null;
const m = members[thread.member] || { handle: thread.member, name: thread.member, lbl: (thread.member || '?').slice(0, 2).toUpperCase(), color: '#888', online: false };
const [input, setInput] = useStateS('');
const scrollRef = useRefS(null);
useEffectS(() => {
if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}, [thread.messages.length]);
const send = () => {
if (!input.trim()) return;
onSend(threadId, input.trim());
setInput('');
};
return (
{/* Top bar */}
onOpenMember(m.handle)} style={{
flex: 1, display: 'flex', alignItems: 'center', gap: 10,
background: 'transparent', border: 'none', cursor: 'pointer', padding: 0, textAlign: 'left',
}}>
{m.handle}
{m.online ? '● ACTIVE NOW' : '◌ OFFLINE'}
onToggleFollow(m.handle)} />
{/* Messages */}
{/* Day divider */}
★ TODAY ★
{thread.messages.map((msg, i) => {
const mine = msg.from === 'me';
const prev = thread.messages[i - 1];
const grouped = prev && prev.from === msg.from;
if (msg.kind === 'voice') {
return (
);
}
return (
{msg.t}
);
})}
{/* Typing indicator */}
{m.online && (
{[0, 1, 2].map(i => (
))}
)}
{/* Composer */}
{Icon.plus(theme.text)}
setInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && send()}
placeholder="Drop a message…"
style={{
flex: 1, padding: '11px 16px', borderRadius: 99,
background: theme.surface, border: `1px solid ${theme.border}`,
color: theme.text, fontFamily: theme.fontBody, fontSize: 14,
outline: 'none', minWidth: 0,
}}
/>
{input.trim() ? (
) : (
)}
);
}
// ─────────────────────────────────────────────────────────────
// MemberProfileScreen — tap an artist's name → their profile
// ─────────────────────────────────────────────────────────────
function MemberProfileScreen({ theme, handle, onBack, following, onToggleFollow, onMessage, members = MEMBERS }) {
// Never blank out: if the member isn't in the loaded directory (e.g. not yet
// synced, or it's you — you're excluded from the directory), show a minimal
// profile built from the handle instead of a black screen.
const m = members[handle] || {
handle, name: handle || 'Member', lbl: (handle || '?').slice(0, 2).toUpperCase(),
color: '#888', pal: ['#888', '#0A0A0A'], tier: 'MEMBER', city: '', bio: '',
genres: [], followers: 0, following: 0, releases: 0, streak: 0, avatarUrl: null,
};
const isFollowing = following.includes(handle);
return (
{/* Top bar */}
{/* Identity (banner removed — it covered ~half the screen) */}
{m.tier} · {m.city}
{m.name.toUpperCase()}
@{m.handle.toUpperCase()}
{/* Bio */}
{m.bio}
{/* Genres */}
{m.genres.map(g => {g} )}
{/* CTA row */}
onToggleFollow(m.handle)}
style={{
flex: 1,
background: isFollowing ? 'transparent' : theme.text,
color: isFollowing ? theme.text : theme.bg,
border: isFollowing ? `1px solid ${theme.border}` : 'none',
fontFamily: theme.fontDisplay, fontSize: 14, letterSpacing: 1,
padding: '13px 18px', borderRadius: 12, cursor: 'pointer',
textTransform: 'uppercase',
}}>
{isFollowing ? '✓ FOLLOWING' : '+ FOLLOW'}
onMessage(m.handle)}
style={{
flex: 1,
background: theme.accent, color: theme.onAccent,
border: 'none',
fontFamily: theme.fontDisplay, fontSize: 14, letterSpacing: 1,
padding: '13px 18px', borderRadius: 12, cursor: 'pointer',
textTransform: 'uppercase',
}}>
MESSAGE ↗
{/* Stats */}
{[
{ n: m.followers.toLocaleString(), l: 'FOLLOWERS' },
{ n: m.following, l: 'FOLLOWING' },
{ n: m.releases, l: 'RELEASES' },
{ n: m.streak, l: 'STREAK' },
].map((s, i) => (
))}
{/* Releases preview */}
RECENT RELEASES
{[
{ lbl: 'NH', t: 'NEON HOURS' },
{ lbl: 'BS', t: 'BLACK SAND' },
{ lbl: 'GH', t: 'GHOSTLINE' },
].map((r, i) => (
))}
{/* Mutuals */}
FOLLOWED BY · 3 OF YOUR SQUAD
{['SV', 'TM', 'IL'].map((lbl, i) => (
0 ? -10 : 0,
border: `2px solid ${theme.surface}`,
}}>{lbl}
))}
sasha.vee , theo.m and 1 other
);
}
window.LAUNCH_SCREENS_5 = { MessagesScreen, ThreadScreen, MemberProfileScreen, FollowButton, MEMBERS, THREADS };