Skip to content

Commit 118ed13

Browse files
committed
implement guild bans + ban page
1 parent 058e737 commit 118ed13

9 files changed

Lines changed: 338 additions & 65 deletions

File tree

src/Entrypoint.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const GuildSettingsRoleMembers = lazy(() => import('./pages/guilds/settings/Role
6565
const GuildSettingsEmojis = lazy(() => import('./pages/guilds/settings/EmojiSettings'))
6666
const GuildSettingsMembers = lazy(() => import('./pages/guilds/settings/MemberSettings'))
6767
const GuildSettingsInvites = lazy(() => import('./pages/guilds/settings/InviteSettings'))
68+
const GuildSettingsBans = lazy(() => import('./pages/guilds/settings/BanSettings'))
6869

6970
// Guild Channel Settings
7071
const GuildChannelSettingsOverview = lazy(() => import('./pages/channels/settings/Overview'))
@@ -192,6 +193,7 @@ const Entrypoint: Component = () => {
192193
<Route path="/emojis" component={GuildSettingsEmojis} />
193194
<Route path="/members" component={GuildSettingsMembers} />
194195
<Route path="/invites" component={GuildSettingsInvites} />
196+
<Route path="/bans" component={GuildSettingsBans} />
195197
</Route>
196198
<Route path="/guilds/:guildId/settings" component={GuildSettingsRoot} />
197199
<Route path="/guilds/:guildId/:channelId/settings" component={GuildChannelSettings}>

src/components/guilds/GuildMemberList.tsx

Lines changed: 41 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import {useNavigate, useParams} from "@solidjs/router";
33
import {Accessor, createEffect, createMemo, createSignal, For, Index, onMount, Show} from "solid-js";
44
import StatusIndicator from "../users/StatusIndicator";
55
import SidebarSection from "../ui/SidebarSection";
6-
import {ReactiveSet} from "@solid-primitives/set";
7-
import {displayName, extendedColor, maxIterator, setDifference} from "../../utils";
6+
import {displayName, extendedColor, maxIterator} from "../../utils";
87
import useContextMenu from "../../hooks/useContextMenu";
98
import ContextMenu, {ContextMenuButton, DangerContextMenuButton} from "../ui/ContextMenu";
109
import UserPlus from "../icons/svg/UserPlus";
@@ -23,6 +22,7 @@ import Crown from "../icons/svg/Crown";
2322
import Robot from "../icons/svg/Robot";
2423
import UserMinus from "../icons/svg/UserMinus";
2524
import {t} from "../../i18n";
25+
import Gavel from "../icons/svg/Gavel";
2626

2727
export function GuildMemberGroup(props: { members: Iterable<User | bigint>, offline?: boolean }) {
2828
const api = getApi()!
@@ -41,6 +41,11 @@ export function GuildMemberGroup(props: { members: Iterable<User | bigint>, offl
4141
if (!response.ok) throw new Error(response.errorJsonOrThrow().message)
4242
}
4343

44+
const banMember = async (id: bigint) => {
45+
const response = await api.request('PUT', `/guilds/${guildId()}/bans/${id}`)
46+
if (!response.ok) throw new Error(response.errorJsonOrThrow().message)
47+
}
48+
4449
return (
4550
<For each={[...props.members]}>
4651
{(userOrId) => {
@@ -101,6 +106,20 @@ export function GuildMemberGroup(props: { members: Iterable<User | bigint>, offl
101106
)}
102107
/>
103108
</Show>
109+
<Show when={!isSelf() && canManage() && permissions().has('BAN_MEMBERS')}>
110+
<DangerContextMenuButton
111+
icon={Gavel}
112+
label="Ban Member"
113+
onClick={() => toast.promise(
114+
banMember(user_id),
115+
{
116+
loading: 'Banning user...',
117+
success: 'User banned.',
118+
error: (err) => err.message,
119+
}
120+
)}
121+
/>
122+
</Show>
104123
</ContextMenu>
105124
)}
106125
>
@@ -180,56 +199,37 @@ export default function GuildMemberList() {
180199
const params = useParams()
181200
const guildId = () => BigInt(params.guildId!)
182201
const guildMemo = createMemo(() => api.cache!.guilds.get(guildId()))
183-
if (!guildMemo()) return
184202

185203
const isLoaded = createMemo(() => api.cache!.isGuildLoaded(guildId()))
186204

187205
onMount(() => {
188206
api.ws?.ensureGuildLoaded(guildId())
189207
})
190208

191-
const online = new ReactiveSet<bigint>()
192-
const offline = new ReactiveSet<bigint>()
193-
194-
const membersMemo = createMemo(() => api.cache!.memberReactor.get(guildMemo()!.id))
195-
createEffect<Set<bigint> | undefined>((tracked) => {
196-
const members = membersMemo()
197-
if (members == null) return
198-
199-
for (const member of members) {
200-
if (tracked?.has(member)) continue
201-
tracked?.add(member)
202-
203-
createEffect((prev) => {
204-
const status = api.cache!.presences.get(member)?.status
205-
if (prev != null && (prev === 'offline') === (status === 'offline'))
206-
return status
209+
const membersMemo = createMemo(() => {
210+
const guild = guildMemo()
211+
return guild ? (api.cache!.memberReactor.get(guild.id) ?? []) : []
212+
})
207213

208-
if (status === 'offline' || !status) {
209-
online.delete(member);
210-
offline.add(member)
211-
} else {
212-
offline.delete(member);
213-
online.add(member)
214-
}
215-
return status
216-
})
214+
const onlineOffline = createMemo(() => {
215+
const online: bigint[] = []
216+
const offline: bigint[] = []
217+
for (const member of membersMemo()) {
218+
const status = api.cache!.presences.get(member)?.status
219+
if (status && status !== 'offline') online.push(member)
220+
else offline.push(member)
217221
}
218-
const updated = new Set(members)
219-
if (tracked) for (const removed of setDifference(tracked, updated)) {
220-
online.delete(removed)
221-
offline.delete(removed)
222-
}
223-
return updated
224-
}, new Set<bigint>())
222+
return { online, offline }
223+
})
224+
const online = () => onlineOffline().online
225+
const offline = () => onlineOffline().offline
225226

226-
// TODO: probably inefficient to have this reevaluate every time member presence changes
227227
const roleGroups = createMemo(() => {
228228
const roles = api.cache!.roles
229229
const groups = new Map<bigint, { name: string, position: number, members: bigint[] }>()
230230
const noRoles = [] as bigint[]
231231

232-
for (const member of online) {
232+
for (const member of online()) {
233233
const memberRoles = api.cache!.members.get(memberKey(guildId(), member))?.roles?.map(BigInt) ?? []
234234
const resolved = memberRoles
235235
.map(r => roles.get(r))
@@ -314,17 +314,17 @@ export default function GuildMemberList() {
314314
</Show>
315315
)}
316316
</For>
317-
<Show when={noRoles().length} keyed={false}>
317+
<Show when={noRoles().length}>
318318
<SidebarSection badge={() => noRoles().length}>
319319
{t('status.online')}
320320
</SidebarSection>
321321
<GuildMemberGroup members={noRoles()} />
322322
</Show>
323-
<Show when={offline.size} keyed={false}>
324-
<SidebarSection badge={() => offline.size}>
323+
<Show when={offline().length}>
324+
<SidebarSection badge={() => offline().length}>
325325
{t('status.offline')}
326326
</SidebarSection>
327-
<GuildMemberGroup members={offline} offline />
327+
<GuildMemberGroup members={offline()} offline />
328328
</Show>
329329
</>
330330
}>

src/components/guilds/GuildSideSelect.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@ export default function GuildSideSelect() {
105105
}}
106106
</For>
107107
<Separator />
108-
<For each={Array.from(cache.guildList.map(g => api.cache!.guilds.get(g)!))}>
109-
{(guild: Guild) => guild && (
108+
<For each={cache.guildList.map(g => api.cache!.guilds.get(g)).filter(g => !!g)}>
109+
{(guild: Guild) => (
110110
<A href={`/guilds/${guild.id}`} class="flex" onContextMenu={contextMenu.getHandler(
111111
<GuildContextMenu guild={guild} />
112112
)}>

src/components/guilds/GuildSidebar.tsx

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {A, useNavigate, useParams} from "@solidjs/router";
2-
import {createMemo, createSignal, For, Match, Show, Switch, onMount, onCleanup, Index} from "solid-js";
2+
import {createEffect, createMemo, createSignal, For, Match, Show, Switch, onMount, onCleanup, Index} from "solid-js";
33
import {getApi} from "../../api/Api";
44
import {GuildChannel} from "../../types/channel";
55
import {ModalId, useModal} from "../ui/Modal";
@@ -196,16 +196,14 @@ export default function GuildSidebar() {
196196
const {showModal} = useModal()
197197

198198
const api = getApi()!
199-
const guild = createMemo(() => api.cache!.guilds.get(guildId())!)
200-
if (!guild()) return
201-
202-
// Trigger on-demand loading if guild channels aren't available yet
203-
onMount(() => {
204-
api.ws?.ensureGuildLoaded(guildId())
199+
const guild = createMemo(() => api.cache!.guilds.get(guildId()))
200+
createEffect(() => {
201+
if (!guild()) navigate('/')
205202
})
203+
onMount(() => api.ws?.ensureGuildLoaded(guildId()))
206204

207205
const [dropdownExpanded, setDropdownExpanded] = createSignal(false)
208-
const isOwner = createMemo(() => guild().owner_id === api.cache?.clientUser?.id)
206+
const isOwner = createMemo(() => guild()?.owner_id === api.cache?.clientUser?.id)
209207

210208
let dropdownRef: HTMLUListElement | undefined
211209
let toggleRef: HTMLDivElement | undefined
@@ -231,7 +229,7 @@ export default function GuildSidebar() {
231229
icon={UserPlus}
232230
label="Invite People"
233231
buttonClass="hover:bg-accent"
234-
onClick={() => showModal(ModalId.CreateInvite, guild())}
232+
onClick={() => showModal(ModalId.CreateInvite, guild()!)}
235233
/>
236234
</Show>
237235
)
@@ -332,6 +330,7 @@ export default function GuildSidebar() {
332330
}
333331

334332
return (
333+
<Show when={guild()}>
335334
<div
336335
class="flex flex-col items-center flex-grow"
337336
onContextMenu={contextMenu.getHandler(
@@ -356,14 +355,14 @@ export default function GuildSidebar() {
356355
ref={toggleRef}
357356
class="box-border flex flex-col justify-end border-b-[1px] border-fg/5
358357
group hover:bg-2 transition-all duration-200 cursor-pointer relative w-full"
359-
classList={{ 'min-h-[150px]': !!guild().banner }}
358+
classList={{ 'min-h-[150px]': !!guild()!.banner }}
360359
onClick={() => setDropdownExpanded(prev => !prev)}
361360
>
362-
<Show when={guild().banner}>
361+
<Show when={guild()!.banner}>
363362
<figure
364363
class="absolute inset-0 z-0"
365364
style={{
366-
"background-image": `url(${guild().banner})`,
365+
"background-image": `url(${guild()!.banner})`,
367366
"background-size": "cover",
368367
"background-position": "center",
369368
"mask-image": "linear-gradient(to bottom, rgba(0,0,0,1), rgba(0,0,0,0))",
@@ -372,10 +371,10 @@ export default function GuildSidebar() {
372371
</Show>
373372
<div classList={{
374373
"flex justify-between items-center px-3 mt-3": true,
375-
"pb-3": !guild().description,
374+
"pb-3": !guild()!.description,
376375
}}>
377376
<span class="inline-block font-title font-bold text-base truncate min-w-0 pr-2">
378-
{guild().name}
377+
{guild()!.name}
379378
</span>
380379
<label tabIndex={0} classList={{
381380
"cursor-pointer transition-transform transform": true,
@@ -389,9 +388,9 @@ export default function GuildSidebar() {
389388
/>
390389
</label>
391390
</div>
392-
{guild().description && (
391+
{guild()!.description && (
393392
<div class="card-body px-3 pt-1 pb-3">
394-
<p class="text-xs text-fg/50 truncate min-w-0">{guild().description}</p>
393+
<p class="text-xs text-fg/50 truncate min-w-0">{guild()!.description}</p>
395394
</div>
396395
)}
397396
<Show when={dropdownExpanded()}>
@@ -405,7 +404,7 @@ export default function GuildSidebar() {
405404
icon={UserPlus}
406405
label="Invite People"
407406
svgClass="fill-fg"
408-
onClick={() => showModal(ModalId.CreateInvite, guild())}
407+
onClick={() => showModal(ModalId.CreateInvite, guild()!)}
409408
/>
410409
</Show>
411410
<Show when={guildPermissions()?.has('MANAGE_CHANNELS')}>
@@ -438,7 +437,7 @@ export default function GuildSidebar() {
438437
groupHoverColor="danger"
439438
svgClass="fill-danger group-hover/gdb:fill-fg"
440439
labelClass="text-danger group-hover/gdb:text-fg"
441-
onClick={() => showModal(ModalId.LeaveGuild, guild())}
440+
onClick={() => showModal(ModalId.LeaveGuild, guild()!)}
442441
/>
443442
</Show>
444443
</ul>
@@ -453,5 +452,6 @@ export default function GuildSidebar() {
453452
</Show>
454453
</div>
455454
</div>
455+
</Show>
456456
)
457457
}

src/components/icons/svg/Gavel.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {JSX} from "solid-js";
2+
3+
export default function Gavel(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
4+
return (
5+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" {...props}>
6+
{/* Font Awesome Pro 6.0.0-alpha2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) */}
7+
<path d="M512 216.329C512 210.204 509.656 204.08 504.969 199.395L482.346 176.77C477.658 172.084 471.502 169.741 465.347 169.741S453.035 172.084 448.348 176.77L442.723 182.395L329.605 69.277L335.23 63.652C339.917 58.965 342.26 52.81 342.26 46.654S339.917 34.344 335.23 29.656L312.605 7.031C307.919 2.344 301.794 0 295.67 0S283.42 2.344 278.732 7.031L154.24 131.523C149.553 136.211 147.209 142.336 147.209 148.461S149.553 160.711 154.24 165.398L176.863 188.02C181.551 192.707 187.707 195.051 193.863 195.051S206.175 192.707 210.861 188.02L216.486 182.395L329.605 295.516L323.98 301.137C319.293 305.824 316.949 311.98 316.949 318.136S319.293 330.447 323.98 335.133L346.604 357.758C351.291 362.445 357.416 364.789 363.54 364.789S375.789 362.445 380.477 357.758L504.969 233.266C509.656 228.578 512 222.453 512 216.329ZM227.793 238.957L169.363 297.387C163.113 291.139 154.927 288.015 146.74 288.015S130.367 291.139 124.117 297.387L9.375 412.133C3.125 418.381 0 426.567 0 434.754S3.125 451.127 9.375 457.375L54.621 502.625C60.871 508.875 69.058 512 77.245 512S93.619 508.875 99.869 502.625L214.611 387.883C220.862 381.632 223.987 373.446 223.987 365.259C223.987 357.074 220.863 348.888 214.613 342.637L273.043 284.207L227.793 238.957Z "></path>
8+
</svg>
9+
)
10+
}

0 commit comments

Comments
 (0)