|
|
|
|
@@ -42,6 +42,7 @@ export default function ChatPage() {
|
|
|
|
|
const [selectedGuildId, setSelectedGuildId] = useState('')
|
|
|
|
|
const [selectedChannelId, setSelectedChannelId] = useState('')
|
|
|
|
|
const [channelMemberIds, setChannelMemberIds] = useState<string[]>([])
|
|
|
|
|
const [bypassMemberIds, setBypassMemberIds] = useState<string[]>([])
|
|
|
|
|
const [channels, setChannels] = useState<GuildChannel[]>([])
|
|
|
|
|
const [guildDbId, setGuildDbId] = useState('')
|
|
|
|
|
const [members, setMembers] = useState<MemberItem[]>([])
|
|
|
|
|
@@ -54,6 +55,7 @@ export default function ChatPage() {
|
|
|
|
|
const [newChannelXType, setNewChannelXType] = useState<XType>('general')
|
|
|
|
|
const [newChannelOnDuty, setNewChannelOnDuty] = useState('')
|
|
|
|
|
const [newChannelListeners, setNewChannelListeners] = useState<string[]>([])
|
|
|
|
|
const [newChannelBypass, setNewChannelBypass] = useState<string[]>([])
|
|
|
|
|
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 (
|
|
|
|
|
<div key={m.userId} className="member-row">
|
|
|
|
|
<div className="avatar dot">{initials(label)}</div>
|
|
|
|
|
<div style={{ minWidth: 0 }}>
|
|
|
|
|
<div style={{ minWidth: 0, flex: 1 }}>
|
|
|
|
|
<span className="nm">{label}</span>
|
|
|
|
|
{m.userId === session?.user.id ? <span className="you">you</span> : null}
|
|
|
|
|
{showBypassUi && isBypass ? <span className="chan-tag">bypass</span> : null}
|
|
|
|
|
</div>
|
|
|
|
|
{showBypassUi && !isBypass ? (
|
|
|
|
|
<button
|
|
|
|
|
className="btn btn-secondary btn-xs"
|
|
|
|
|
title="Exclude from speaking rotation (only woken when @-mentioned)"
|
|
|
|
|
onClick={() => moveToBypass(m.userId)}
|
|
|
|
|
>
|
|
|
|
|
→ bypass
|
|
|
|
|
</button>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
@@ -531,18 +575,18 @@ export default function ChatPage() {
|
|
|
|
|
<div className="dc-section-label">
|
|
|
|
|
<span>In channel — {channelMembers.length}</span>
|
|
|
|
|
</div>
|
|
|
|
|
{channelMembers.map(renderMember)}
|
|
|
|
|
{channelMembers.map((m) => renderMember(m, true))}
|
|
|
|
|
<div className="dc-section-label" style={{ marginTop: 14 }}>
|
|
|
|
|
<span>Guild — {members.length}</span>
|
|
|
|
|
</div>
|
|
|
|
|
{members.map(renderMember)}
|
|
|
|
|
{members.map((m) => renderMember(m))}
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<div className="dc-section-label">
|
|
|
|
|
<span>Members — {members.length}</span>
|
|
|
|
|
</div>
|
|
|
|
|
{members.map(renderMember)}
|
|
|
|
|
{members.map((m) => renderMember(m))}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</aside>
|
|
|
|
|
@@ -637,6 +681,33 @@ export default function ChatPage() {
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
{newChannelXType === 'discuss' || newChannelXType === 'work' ? (
|
|
|
|
|
<div className="field" style={{ marginTop: 10 }}>
|
|
|
|
|
<label>Bypass list (optional)</label>
|
|
|
|
|
<div className="modal-list">
|
|
|
|
|
{members.map((m) => (
|
|
|
|
|
<label key={m.userId} className="check-row">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={newChannelBypass.includes(m.userId)}
|
|
|
|
|
onChange={() => toggleBypass(m.userId)}
|
|
|
|
|
/>
|
|
|
|
|
<span>
|
|
|
|
|
{m.name || m.email}
|
|
|
|
|
{m.userId === session?.user.id ? ' (you)' : ''}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
))}
|
|
|
|
|
{members.length === 0 ? <div className="dc-empty">No members in this guild.</div> : null}
|
|
|
|
|
</div>
|
|
|
|
|
<p className="muted" style={{ fontSize: 12 }}>
|
|
|
|
|
Bypass members are excluded from the speaking rotation and are
|
|
|
|
|
only woken when @-mentioned. Order and bypass partition the
|
|
|
|
|
channel members.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
<label className="check-row" style={{ marginTop: 10 }}>
|
|
|
|
|
<input type="checkbox" checked={newChannelPublic} onChange={(e) => setNewChannelPublic(e.target.checked)} />
|
|
|
|
|
<span>Public — visible to all guild members</span>
|
|
|
|
|
|