Skip to content

Commit 9e42cf9

Browse files
authored
Merge pull request #58 from FrostCord/TabNavigation
Add Role Creation and Management
2 parents f3a5b6f + 97e60c7 commit 9e42cf9

24 files changed

Lines changed: 928 additions & 102 deletions

components/forms/RoleEditForm.tsx

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { useUserHighestRolePosition, useUserServerPerms } from '@/lib/store';
2+
import { decrementRolePosition, deleteRole, incrementRolePosition, updateRole } from '@/services/roles.service';
3+
import { Role } from '@/types/dbtypes';
4+
import { ServerPermissions } from '@/types/permissions';
5+
import { useSupabaseClient } from '@supabase/auth-helpers-react';
6+
import { useForm } from 'react-hook-form';
7+
import { toast } from 'react-toastify';
8+
9+
const permissionEnumToInfoMap = new Map([
10+
[ServerPermissions.OWNER, ['Owner', '']],
11+
[ServerPermissions.ADMINISTRATOR, ['Administrator', 'Grants all permissions in the server (assign this with caution!)']],
12+
[ServerPermissions.MANAGE_CHANNELS, ['Manage Channels', 'Allows the user to create, edit, and delete channels']],
13+
[ServerPermissions.MANAGE_ROLES, ['Manage Roles', 'Allows the user to create, edit, and delete roles']],
14+
[ServerPermissions.MANAGE_SERVER, ['Manage Server', 'Allows the user to edit server settings like name, descripion, and icon']],
15+
[ServerPermissions.MANAGE_USERS, ['Manage Users', 'Allows the user to kick and ban users']],
16+
[ServerPermissions.MANAGE_MESSAGES, ['Manage Messages', 'Allows the user to delete and pin messages']],
17+
[ServerPermissions.MANAGE_INVITES, ['Manage Invites', 'Allows the user to edit, and delete invites']],
18+
[ServerPermissions.CREATE_INVITES, ['Create Invites', 'Allows the user to create invites']],
19+
]);
20+
21+
type RoleEditFormResult = {
22+
name: string;
23+
position: number;
24+
permissions: string | string[];
25+
color: string;
26+
}
27+
28+
export function RoleEditForm({role, roles_length, max_role_position }: {role: Role, roles_length: number, max_role_position: number }) {
29+
const { register, handleSubmit, formState: { errors } } = useForm<RoleEditFormResult>();
30+
const supabase = useSupabaseClient();
31+
const userPerms = useUserServerPerms();
32+
const userHighestRolePosition = max_role_position; // useUserHighestRolePosition();
33+
34+
const isFormDisabled = role.is_system || role.position <= userHighestRolePosition;
35+
const serverPermissions = Object.entries(ServerPermissions).filter(
36+
([key, value]) => typeof value === 'number' && key !== 'NONE' && key !== 'OWNER'
37+
);
38+
39+
const onSubmit = async (formData: RoleEditFormResult) => {
40+
if (typeof formData.permissions === 'string') {
41+
formData.permissions = [ formData.permissions ];
42+
}
43+
44+
else if (typeof formData.permissions === 'boolean') {
45+
formData.permissions = [ '0' ];
46+
}
47+
48+
const perms = formData.permissions.reduce((acc, cur) => acc | parseInt(cur), 0);
49+
50+
// If a role is being edited, update it
51+
const { error } = await updateRole(
52+
supabase,
53+
role.id,
54+
formData.name,
55+
role.position,
56+
perms,
57+
formData.color.replace('#', '')
58+
);
59+
60+
if (error) {
61+
console.error(error);
62+
toast.error('Failed to update role');
63+
return;
64+
}
65+
66+
toast.success('Role saved');
67+
};
68+
69+
return (
70+
<form onSubmit={handleSubmit(onSubmit)}>
71+
<p className="text-base text-gray-400 text-left w-full">Display</p>
72+
<div className="w-full space-x-2 mb-2 flex flex-row">
73+
<input
74+
type="text"
75+
className="text-xl rounded-md h-7 p-2 bg-grey-900 w-full"
76+
disabled={isFormDisabled}
77+
defaultValue={role.name}
78+
placeholder='Role Name'
79+
{...register('name', { required: true })}
80+
/>
81+
82+
<input
83+
type="color"
84+
className="rounded-md h-7"
85+
disabled={isFormDisabled}
86+
defaultValue={`#${(role.color || 'a9aaab')}`}
87+
{...register('color', { required: true })}
88+
/>
89+
</div>
90+
<p className="text-base text-gray-400 text-left w-full">Heirarchy position</p>
91+
<div className="flex flex-row space-x-2">
92+
<button
93+
className="border-2 border-gray-400 rounded-md h-6 w-full mt-2 disabled:border-grey-800"
94+
type="button"
95+
onClick={async () => {
96+
console.log('up');
97+
await incrementRolePosition(supabase, role.id);
98+
}}
99+
disabled={isFormDisabled || role.position === 1 || role.position - 1 <= userHighestRolePosition}
100+
>
101+
Up
102+
</button>
103+
<button
104+
className="border-2 border-gray-400 rounded-md h-6 w-full mt-2 disabled:border-grey-800"
105+
type="button"
106+
onClick={async () => {
107+
console.log('down');
108+
await decrementRolePosition(supabase, role.id);
109+
}}
110+
disabled={isFormDisabled || role.position === roles_length - 2}
111+
>
112+
Down
113+
</button>
114+
</div>
115+
<p className="text-base text-gray-400 text-left w-full">Permissions</p>
116+
{serverPermissions.map(([permission, value]) => (
117+
<div key={permission} className="flex flex-row w-full">
118+
<input
119+
type="checkbox"
120+
className="w-5 h-5 mr-2 checked:accent-green-600 disabled:accent-green-600/50"
121+
value={value}
122+
defaultChecked={(
123+
!role
124+
// @ts-expect-error permission is always a keyof ServerPermissions. This error will never be thrown
125+
|| (role.permissions & value as number) === ServerPermissions[permission]
126+
)}
127+
disabled={
128+
isFormDisabled
129+
|| ((userPerms & value as number) === 0)
130+
|| (permission === 'OWNER')
131+
|| (permission === 'ADMINISTRATOR' && (userPerms & 2) !== 2)
132+
}
133+
{...register('permissions')}
134+
/>
135+
<div>
136+
{/* @ts-expect-error permission is always a keyof ServerPermissions. This error will never be thrown */}
137+
<label> {permissionEnumToInfoMap.get(ServerPermissions[permission])[0]}</label>
138+
{/* @ts-expect-error permission is always a keyof ServerPermissions. This error will never be thrown */}
139+
<p className="text-sm text-gray-400 text-left w-full">{permissionEnumToInfoMap.get(ServerPermissions[permission])[1]}</p>
140+
</div>
141+
142+
</div>
143+
))}
144+
<div className="flex flex-row space-x-2 w-full">
145+
<button
146+
className="bg-red-500 hover:bg-red-700 disabled:bg-grey-600 rounded-md h-6 mt-2 w-full"
147+
type="button"
148+
disabled={isFormDisabled}
149+
onClick={async () => {
150+
console.log('delete role');
151+
const { error } = await deleteRole(supabase, role.id);
152+
153+
if (error) {
154+
console.error(error);
155+
toast.error('Failed to delete role');
156+
return;
157+
}
158+
159+
toast.success('Role deleted');
160+
}}
161+
>
162+
Delete
163+
</button>
164+
<button
165+
className="bg-green-500 hover:bg-green-700 disabled:bg-grey-600 rounded-md h-6 mt-2 w-full"
166+
disabled={isFormDisabled}
167+
type="submit"
168+
>
169+
Save
170+
</button>
171+
</div>
172+
</form>
173+
);
174+
}

components/home/DMessageList.tsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,39 @@
11
import { Channel, DMChannelWithRecipient } from '@/types/dbtypes';
22
import UserIcon from '../icons/UserIcon';
33
import styles from '@/styles/Chat.module.css';
4-
import { useConnectionRef, useDMChannels, useSetChannel } from '@/lib/store';
4+
import { useChannel, useConnectionRef, useDMChannels, useSetChannel } from '@/lib/store';
55
import SidebarCallControl from '@/components/home/SidebarCallControl';
66
import { SearchBar } from '@/components/forms/Styles';
77
import { useState } from 'react';
88

99
function mapToComponentArray(
1010
_map: Map<string, DMChannelWithRecipient>,
11-
setChannel: (channel: Channel) => void
11+
setChannel: (channel: Channel) => void,
12+
channel: Channel | null
1213
) {
1314
const rv = [];
1415

1516
for (const [_, value] of _map) {
1617
rv.push(
1718
<div
1819
key={value.channel_id}
19-
className="flex items-center p-2 w-full hover:bg-grey-700 rounded-md transition-colors"
20+
className={`${
21+
channel?.channel_id == value.channel_id
22+
? 'bg-grey-700'
23+
: 'hover:bg-grey-700'
24+
} flex items-center p-2 w-full rounded-md transition-colors hover:cursor-pointer mt-2`}
2025
onClick={() => {
2126
setChannel({
2227
channel_id: value.channel_id,
2328
server_id: value.server_id,
2429
name: value.recipient.username,
2530
is_media: false,
2631
description: null,
27-
created_at: null
32+
created_at: null,
2833
});
2934
}}
3035
>
31-
<UserIcon user={value.recipient} className="!w-6 !h-6"/>
36+
<UserIcon user={value.recipient} className="!w-6 !h-6" />
3237
<div>{value.recipient.username}</div>
3338
</div>
3439
);
@@ -39,16 +44,15 @@ function mapToComponentArray(
3944
export default function DMessageList() {
4045
const setChannel = useSetChannel();
4146
const dmChannels = useDMChannels();
47+
const channel = useChannel();
4248
const isInVoice = useConnectionRef();
4349
const [ filteredDMs, setFilteredDMs ] = useState(dmChannels);
4450

4551
return (
4652
<div className="flex flex-col h-full">
4753
<div className={`${styles.chatHeader} px-5 pt-5 mb-3`}>
4854
<div className="flex flex-row items-center space-x-3">
49-
<h1 className="text-3xl font-semibold tracking-wide">
50-
Directs
51-
</h1>
55+
<h1 className="text-3xl font-semibold tracking-wide">Directs</h1>
5256
</div>
5357
</div>
5458
<div className="border-t-2 mx-5 border-grey-700 flex-grow pt-3">
@@ -74,7 +78,7 @@ export default function DMessageList() {
7478
}}
7579
/>
7680
</div>
77-
{ mapToComponentArray(filteredDMs, setChannel) }
81+
{ mapToComponentArray(dmChannels, setChannel, channel) }
7882
</div>
7983
{ isInVoice && (
8084
<div className="w-full self-end p-4 mb-7">

components/home/DeleteMsgModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import MessageContent from '@/components/home/MessageContent';
55
import { useSupabaseClient } from '@supabase/auth-helpers-react';
66
import styles from '@/styles/Components.module.css';
77
import { useEffect } from 'react';
8-
import Modal from '@/components/home/Modal';
8+
import Modal from '@/components/home/modals/Modal';
99
import { MessageWithServerProfile } from '@/types/dbtypes';
1010

1111
export default function DeleteMsgModal({

components/home/MediaChat.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { Channel, User } from '@/types/dbtypes';
2222
import { FloatingCallControl } from './FloatingCallControl';
2323
import { MediaDispTrack } from './MediaDispTrack';
2424
import { TrackBundle } from '@livekit/components-core';
25-
import Modal from '@/components/home/Modal';
25+
import Modal from '@/components/home/modals/Modal';
2626
import { useEffect, useRef, useState } from 'react';
2727
import LoadingIcon from '../icons/LoadingIcon';
2828
import { MediaPlaceholderTrack } from './MediaPlaceholderTrack';

components/home/Server.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import styles from '@/styles/Servers.module.css';
1414
import { Channel, Server as ServerType } from '@/types/dbtypes';
1515
import { ServerMemberStats } from './ServerMemberStats';
1616
import { OverflowMarquee } from './OverflowMarquee';
17-
import { useSetChannel } from '@/lib/store';
17+
import { useChannel, useSetChannel } from '@/lib/store';
1818
import { ChannelMediaIcon } from '../icons/ChannelMediaIcon';
1919
import { Tooltip } from 'react-tooltip';
2020
import ChannelName from './ChannelName';
@@ -35,6 +35,7 @@ export default function Server({
3535
const [isSettingsHovered, setIsSettingsHovered] = useState(false);
3636
const setChannel = useSetChannel();
3737
const [channels, setChannels] = useState<Channel[]>([]);
38+
const currentChannel = useChannel();
3839

3940
useEffect(() => {
4041
const handleAsync = async () => {
@@ -87,7 +88,11 @@ export default function Server({
8788
<div className="channels bg-grey-700 rounded-lg relative -top-3 py-4 px-7 ">
8889
{channels.map((channel: Channel, idx: number) => (
8990
<div
90-
className={`channel flex whitespace-nowrap items-center pt-2 pb-1 px-4 hover:bg-grey-600 hover:cursor-pointer rounded-lg max-w-[192px] ${
91+
className={`${
92+
currentChannel?.channel_id == channel.channel_id
93+
? 'bg-grey-600'
94+
: 'hover:bg-grey-600'
95+
} flex whitespace-nowrap items-center pt-2 pb-1 px-4 mt-1 hover:cursor-pointer rounded-lg max-w-[192px] ${
9196
idx === 0 ? 'mt-2' : ''
9297
}`}
9398
onClick={(e) => {
@@ -128,7 +133,11 @@ export default function Server({
128133
<div
129134
className={`${
130135
!isLast ? 'border-b-2 border-grey-700' : ''
131-
} py-2 px-3 flex justify-between hover:bg-grey-700 hover:rounded-xl items-center`}
136+
} py-2 px-3 flex justify-between ${
137+
currentChannel?.server_id == server.id
138+
? 'bg-grey-700 rounded-xl'
139+
: 'hover:bg-grey-700 hover:rounded-xl'
140+
} items-center`}
132141
>
133142
<div className="flex items-center">
134143
<div className={`${styles.serverIcon} p-[6px] rounded-xl`}>

0 commit comments

Comments
 (0)