Skip to content

Commit 2248529

Browse files
authored
Merge pull request #62 from FrostCord/FROS-Invite-Management
FROS Invite management
2 parents dd7957a + 3163b24 commit 2248529

14 files changed

Lines changed: 411 additions & 95 deletions
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Input } from './Styles';
2+
import { UseFormRegister } from 'react-hook-form';
3+
import styles from '@/styles/Livekit.module.css';
4+
import { CreateInviteFormInput } from '@/types/client/forms/createInvite';
5+
6+
export function CreateInviteform({
7+
register,
8+
}: {
9+
register: UseFormRegister<CreateInviteFormInput>;
10+
}) {
11+
return (
12+
<form className="flex flex-col space-y-4">
13+
<div className="flex flex-col">
14+
<label htmlFor="numUses">Max number of uses</label>
15+
<select
16+
id="numUses"
17+
{...(register('numUses'), { valueAsNumber: true })}
18+
className={`${Input('bg-grey-700')} mt-2 ${styles.input}`}
19+
>
20+
<option value="-1">Unlimited</option>
21+
<option value="1">1</option>
22+
<option value="5">5</option>
23+
<option value="10">10</option>
24+
<option value="25">25</option>
25+
<option value="50">50</option>
26+
<option value="100">100</option>
27+
</select>
28+
</div>
29+
30+
<div className="flex flex-col">
31+
<label htmlFor="expiresAt">Expires in...</label>
32+
<select
33+
id="expiresAt"
34+
{...register('expiresAt')}
35+
className={`${Input('bg-grey-700')} mt-2 ${styles.input}`}
36+
>
37+
<option value="1 week">1 week</option>
38+
<option value="1 day">1 day</option>
39+
<option value="1 hour">1 hour</option>
40+
<option value="30 minutes">30 minutes</option>
41+
<option value="null">Never</option>
42+
</select>
43+
</div>
44+
</form>
45+
);
46+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import ChannelName from '@/components/home/ChannelName';
2+
import { ChannelMediaIcon } from '@/components/icons/ChannelMediaIcon';
3+
import ChannelMessageIcon from '@/components/icons/ChannelMessageIcon';
4+
import { useChannel, useServerUserProfilePermissions, useSetChannel } from '@/lib/store';
5+
import { Channel } from '@/types/dbtypes';
6+
import { SyntheticEvent, useState } from 'react';
7+
import * as ContextMenu from '@radix-ui/react-context-menu';
8+
import { useUser } from '@supabase/auth-helpers-react';
9+
import { ServerPermissions } from '@/types/permissions';
10+
import CreateInviteModal from '@/components/home/modals/CreateInviteModal';
11+
12+
export function ChannelListItem({ channel, idx }: { channel: Channel, idx: number }) {
13+
const currentUser = useUser();
14+
const currentChannel = useChannel();
15+
const setChannel = useSetChannel();
16+
const currentUserPermissions = useServerUserProfilePermissions(channel.server_id, currentUser?.id!);
17+
const [showCreateInviteModal, setShowCreateInviteModal] = useState(false);
18+
19+
function joinTextChannel(e: SyntheticEvent, channel: Channel) {
20+
e.stopPropagation();
21+
setChannel(channel);
22+
}
23+
24+
function joinVideoChannel(e: SyntheticEvent, channel: Channel) {
25+
e.stopPropagation();
26+
setChannel(channel);
27+
}
28+
29+
return (
30+
<div
31+
className={`${
32+
currentChannel?.channel_id == channel.channel_id
33+
? 'bg-grey-600'
34+
: 'hover:bg-grey-600'
35+
} flex whitespace-nowrap items-center pt-2 pb-1 px-4 mt-1 hover:cursor-pointer rounded-lg max-w-[192px] ${
36+
idx === 0 ? 'mt-2' : ''
37+
}`}
38+
onClick={(e) => {
39+
if (channel.is_media) {
40+
joinVideoChannel(e, channel);
41+
}
42+
43+
else {
44+
joinTextChannel(e, channel);
45+
}
46+
}}
47+
key={channel.channel_id}
48+
>
49+
<CreateInviteModal
50+
showModal={showCreateInviteModal}
51+
setShowModal={setShowCreateInviteModal}
52+
channel={channel}
53+
/>
54+
<div className="w-auto">
55+
{channel && channel.is_media ? (
56+
<div className="flex flex-col">
57+
<div className="flex flex-row items-center">
58+
<ChannelMediaIcon />
59+
<ChannelName {...channel} />
60+
</div>
61+
</div>
62+
) : (
63+
<ContextMenu.Root>
64+
<ContextMenu.Trigger
65+
disabled={
66+
(currentUserPermissions & ServerPermissions.CREATE_INVITES) === 0
67+
}
68+
asChild
69+
>
70+
<div className="flex flex-row items-center">
71+
<ChannelMessageIcon />
72+
<ChannelName {...channel} />
73+
</div>
74+
</ContextMenu.Trigger>
75+
<ContextMenu.Content className='ContextMenuContent'>
76+
<ContextMenu.Item
77+
className="ContextMenuItem"
78+
onSelect={() => {
79+
console.log('Open invite modal using channel id', channel.channel_id);
80+
setShowCreateInviteModal(true);
81+
}}
82+
>
83+
Create invite
84+
</ContextMenu.Item>
85+
</ContextMenu.Content>
86+
</ContextMenu.Root>
87+
)}
88+
</div>
89+
</div>
90+
);
91+
}

components/home/Message.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import DeleteMsgModal from '@/components/home/DeleteMsgModal';
1111
import { Message as MessageType, MessageWithServerProfile } from '@/types/dbtypes';
1212
import { MessageHeader } from './MessageHeader';
1313
import { useChannel, useServerUserProfile } from '@/lib/store';
14+
import { formatDateStr } from '@/lib/dateManagement';
1415

1516
export default function Message({
1617
message,
@@ -21,14 +22,7 @@ export default function Message({
2122
collapse_user: boolean;
2223
hasDeletePerms?: boolean;
2324
}) {
24-
const pastDate = moment(message.sent_time).format('MM/DD/YYYY h:mm A');
25-
const todayDate = moment(message.sent_time).format('h:mm A');
26-
const displayTime =
27-
moment(moment(message.sent_time)).isSame(moment(), 'day') &&
28-
moment(moment(message.sent_time)).isSame(moment(), 'year') &&
29-
moment(moment(message.sent_time)).isSame(moment(), 'month')
30-
? `Today at ${todayDate}`
31-
: pastDate;
25+
const displayTime = formatDateStr(message.sent_time);
3226

3327
const supabase = useSupabaseClient();
3428
const user = useUser();

components/home/Server.tsx

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { OverflowMarquee } from './OverflowMarquee';
1717
import { useChannel, useServerUserProfilePermissions, useSetChannel } from '@/lib/store';
1818
import { ChannelMediaIcon } from '../icons/ChannelMediaIcon';
1919
import ChannelName from './ChannelName';
20+
import { ChannelListItem } from '@/components/home/ChannelListItem';
2021
import ServerSettingsModal from '@/components/home/modals/ServerSettingsModal';
2122
import AddChannelModal from '@/components/home/modals/AddChannelModal';
2223
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
@@ -38,7 +39,6 @@ export default function Server({
3839
const expand = expanded == server.id;
3940
const supabase = useSupabaseClient();
4041
const [isSettingsHovered, setIsSettingsHovered] = useState(false);
41-
const setChannel = useSetChannel();
4242
const [channels, setChannels] = useState<Channel[]>([]);
4343
const currentChannel = useChannel();
4444

@@ -57,16 +57,6 @@ export default function Server({
5757
handleAsync();
5858
}, [server, supabase]);
5959

60-
function joinTextChannel(e: SyntheticEvent, channel: Channel) {
61-
e.stopPropagation();
62-
setChannel(channel);
63-
}
64-
65-
function joinVideoChannel(e: SyntheticEvent, channel: Channel) {
66-
e.stopPropagation();
67-
setChannel(channel);
68-
}
69-
7060
if (expand) {
7161
return (
7262
<div className="relative overflow-x-visible">
@@ -143,41 +133,7 @@ export default function Server({
143133
</div>
144134
<div className="channels bg-grey-700 rounded-lg relative -top-3 py-4 px-7 ">
145135
{channels.map((channel: Channel, idx: number) => (
146-
<div
147-
className={`${
148-
currentChannel?.channel_id == channel.channel_id
149-
? 'bg-grey-600'
150-
: 'hover:bg-grey-600'
151-
} flex whitespace-nowrap items-center pt-2 pb-1 px-4 mt-1 hover:cursor-pointer rounded-lg max-w-[192px] ${
152-
idx === 0 ? 'mt-2' : ''
153-
}`}
154-
onClick={(e) => {
155-
if (channel.is_media) {
156-
joinVideoChannel(e, channel);
157-
}
158-
159-
else {
160-
joinTextChannel(e, channel);
161-
}
162-
}}
163-
key={channel.channel_id}
164-
>
165-
<div className="w-auto">
166-
{channel && channel.is_media ? (
167-
<div className="flex flex-col">
168-
<div className="flex flex-row items-center">
169-
<ChannelMediaIcon />
170-
<ChannelName {...channel} />
171-
</div>
172-
</div>
173-
) : (
174-
<div className="flex flex-row items-center">
175-
<ChannelMessageIcon />
176-
<ChannelName {...channel} />
177-
</div>
178-
)}
179-
</div>
180-
</div>
136+
<ChannelListItem channel={channel} idx={idx} key={channel.channel_id}/>
181137
))}
182138
</div>
183139
</div>
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { CopyLinkIcon } from '@/components/icons/CopyLinkIcon';
2+
import TrashIcon from '@/components/icons/TrashIcon';
3+
import { formatDateStr } from '@/lib/dateManagement';
4+
import { deleteInvite, getInvitesForServer } from '@/services/invites.service';
5+
import { Invite, Server } from '@/types/dbtypes';
6+
import { useSupabaseClient } from '@supabase/auth-helpers-react';
7+
import { supabase } from '@supabase/auth-ui-react/dist/esm/common/theming';
8+
import { useEffect, useState } from 'react';
9+
import { toast } from 'react-toastify';
10+
11+
export function ServerInviteList({ server }: { server: Server }) {
12+
const supabase = useSupabaseClient();
13+
const [ serverInvites, setServerInvites ] = useState<Invite[]>([]);
14+
15+
useEffect(() => {
16+
async function handleAsync() {
17+
const { data, error } = await getInvitesForServer(supabase, server.id);
18+
19+
if (error) {
20+
console.error(error);
21+
return;
22+
}
23+
24+
setServerInvites(data);
25+
}
26+
27+
handleAsync();
28+
}, [server, supabase]);
29+
30+
return (
31+
<table className="text-center font-light">
32+
<thead>
33+
<tr className="text-lg">
34+
<th className="font-light">Invite Code</th>
35+
<th className="font-light">Created</th>
36+
<th className="font-light">Expires</th>
37+
<th className="font-light">Uses Remaining</th>
38+
<th className="font-light">Actions</th>
39+
</tr>
40+
</thead>
41+
<tbody>
42+
{serverInvites.map((invite) => (
43+
<tr key={invite.id} className="border-b border-gray-600 space-y-2 p-2">
44+
<td><code className="p-1 bg-slate-800 rounded-md text-sm">{invite.url_id}</code></td>
45+
<td>{formatDateStr(invite.created_at!)}</td>
46+
<td>{!!invite.expires_at ? formatDateStr(invite.expires_at!) : 'Never'}</td>
47+
<td>{invite.uses_remaining}</td>
48+
<td className="flex flex-row space-x-2">
49+
<button
50+
className="p-1 hover:bg-slate-600 transition-colors rounded-md"
51+
onClick={() => {
52+
navigator.clipboard.writeText(`${location.origin}/invite/${invite.url_id}`);
53+
}}
54+
>
55+
<CopyLinkIcon className="w-5 h-5"/>
56+
</button>
57+
<button
58+
className="p-1 hover:bg-slate-600 transition-colors rounded-md"
59+
onClick={async () => {
60+
const { error } = await deleteInvite(supabase, invite.id);
61+
62+
if (error) {
63+
console.error(error);
64+
toast.error('Failed to delete invite');
65+
return;
66+
}
67+
68+
setServerInvites((invites) => invites.filter((i) => i.id !== invite.id));
69+
toast.success('Invite deleted');
70+
}}
71+
>
72+
<TrashIcon styles="stroke-red-500"/>
73+
</button>
74+
</td>
75+
</tr>
76+
))}
77+
</tbody>
78+
</table>
79+
);
80+
}

components/home/ServerList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import SidebarCallControl from '@/components/home/SidebarCallControl';
2121
import { ConnectionState } from 'livekit-client';
2222
import { useConnectionState } from '@livekit/components-react';
2323
import MobileCallControls from './mobile/MobileCallControls';
24-
import { ServerSettingsTooltip } from '@/components/Tooltips/ServerSettingsTooltip';
24+
2525
export default function ServerList() {
2626
//TODO: Display default page (when user belongs to and has no servers)
2727

0 commit comments

Comments
 (0)