diff --git a/src/index.css b/src/index.css index e35eb6f..0059faf 100644 --- a/src/index.css +++ b/src/index.css @@ -145,6 +145,13 @@ button { .btn-secondary:hover { background: #44474f; } +.btn-xs { + padding: 2px 8px; + font-size: 11px; + line-height: 1.4; + white-space: nowrap; + flex: 0 0 auto; +} .btn-ghost { background: transparent; color: var(--text-muted); diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 507c48e..4919439 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -42,6 +42,7 @@ export default function ChatPage() { const [selectedGuildId, setSelectedGuildId] = useState('') const [selectedChannelId, setSelectedChannelId] = useState('') const [channelMemberIds, setChannelMemberIds] = useState([]) + const [bypassMemberIds, setBypassMemberIds] = useState([]) const [channels, setChannels] = useState([]) const [guildDbId, setGuildDbId] = useState('') const [members, setMembers] = useState([]) @@ -54,6 +55,7 @@ export default function ChatPage() { const [newChannelXType, setNewChannelXType] = useState('general') const [newChannelOnDuty, setNewChannelOnDuty] = useState('') const [newChannelListeners, setNewChannelListeners] = useState([]) + const [newChannelBypass, setNewChannelBypass] = useState([]) const [showCreateChannelModal, setShowCreateChannelModal] = useState(false) const [showSettingsModal, setShowSettingsModal] = useState(false) const [showAddGuildModal, setShowAddGuildModal] = useState(false) @@ -139,14 +141,19 @@ export default function ChatPage() { async function loadChannelMembers() { if (!guild || !guildToken || !selectedChannelId) { setChannelMemberIds([]) + setBypassMemberIds([]) return } try { const res = await guildApi().get(`/channels/${selectedChannelId}/members`) - const list = Array.isArray(res.data) ? (res.data as Array<{ userId: string }>) : [] + const list = Array.isArray(res.data) + ? (res.data as Array<{ userId: string; bypass?: boolean }>) + : [] setChannelMemberIds(list.map((x) => x.userId)) + setBypassMemberIds(list.filter((x) => x.bypass).map((x) => x.userId)) } catch { setChannelMemberIds([]) + setBypassMemberIds([]) } } @@ -204,6 +211,10 @@ export default function ChatPage() { setNewChannelListeners((prev) => (prev.includes(userId) ? prev.filter((x) => x !== userId) : [...prev, userId])) } + function toggleBypass(userId: string) { + setNewChannelBypass((prev) => (prev.includes(userId) ? prev.filter((x) => x !== userId) : [...prev, userId])) + } + function openCreateChannel() { setNewChannelName('') setSelectedMemberIds([]) @@ -211,6 +222,7 @@ export default function ChatPage() { setNewChannelXType('general') setNewChannelOnDuty(session?.user.id ?? '') setNewChannelListeners([]) + setNewChannelBypass([]) setShowCreateChannelModal(true) } @@ -235,6 +247,10 @@ export default function ChatPage() { memberUserIds: selectedMemberIds, onDuty: newChannelXType === 'triage' ? newChannelOnDuty : undefined, listeners: newChannelXType === 'custom' ? newChannelListeners : [], + bypassUserIds: + newChannelXType === 'discuss' || newChannelXType === 'work' + ? newChannelBypass + : [], } const res = await guildApi().post('/channels', payload) const createdId = res.data?.id as string | undefined @@ -244,6 +260,7 @@ export default function ChatPage() { setNewChannelXType('general') setNewChannelOnDuty(session?.user.id ?? '') setNewChannelListeners([]) + setNewChannelBypass([]) setShowCreateChannelModal(false) await loadChannels() if (createdId) setSelectedChannelId(createdId) @@ -264,6 +281,16 @@ export default function ChatPage() { } } + async function moveToBypass(userId: string) { + if (!guild || !guildToken || !selectedChannelId) return + try { + await guildApi().post(`/channels/${selectedChannelId}/bypass`, { userId }) + await loadChannelMembers() + } catch { + setError('Failed to move member to bypass') + } + } + async function leaveChannel(c: GuildChannel) { if (!guild || !guildToken) return try { @@ -345,15 +372,32 @@ export default function ChatPage() { const channelMembers = channelMemberIds.map( (id) => guildById.get(id) ?? { userId: id, email: '', name: '', status: 'active' }, ) - const renderMember = (m: { userId: string; name?: string; email?: string }) => { + const rotationChannel = + currentChannel?.xType === 'discuss' || currentChannel?.xType === 'work' + const renderMember = ( + m: { userId: string; name?: string; email?: string }, + inChannel = false, + ) => { const label = m.name || m.email || m.userId.slice(0, 8) + const isBypass = bypassMemberIds.includes(m.userId) + const showBypassUi = inChannel && rotationChannel return (
{initials(label)}
-
+
{label} {m.userId === session?.user.id ? you : null} + {showBypassUi && isBypass ? bypass : null}
+ {showBypassUi && !isBypass ? ( + + ) : null}
) } @@ -531,18 +575,18 @@ export default function ChatPage() {
In channel — {channelMembers.length}
- {channelMembers.map(renderMember)} + {channelMembers.map((m) => renderMember(m, true))}
Guild — {members.length}
- {members.map(renderMember)} + {members.map((m) => renderMember(m))} ) : ( <>
Members — {members.length}
- {members.map(renderMember)} + {members.map((m) => renderMember(m))} )} @@ -637,6 +681,33 @@ export default function ChatPage() {
) : null} + {newChannelXType === 'discuss' || newChannelXType === 'work' ? ( +
+ +
+ {members.map((m) => ( + + ))} + {members.length === 0 ?
No members in this guild.
: null} +
+

+ Bypass members are excluded from the speaking rotation and are + only woken when @-mentioned. Order and bypass partition the + channel members. +

+
+ ) : null} +