diff --git a/backend/src/ws/ws-events/channel-create.ts b/backend/src/ws/ws-events/channel-create.ts index 5030d54c..5165fd0a 100644 --- a/backend/src/ws/ws-events/channel-create.ts +++ b/backend/src/ws/ws-events/channel-create.ts @@ -6,7 +6,22 @@ import { WSEvent } from './ws-event'; export default class implements WSEvent<'CHANNEL_CREATE'> { public on = 'CHANNEL_CREATE' as const; - public async invoke(ws: WebSocket, client: Socket, { name, guildId, type }: WS.Params.ChannelCreate) { + public async invoke(ws: WebSocket, client: Socket, params: WS.Params.ChannelCreate) { + const { name, guildId, type, userIds } = params; + + if (type === 'DM') { + if (!userIds) throw new TypeError('userIds required for DM creation'); + const channel = await deps.channels.createDM(userIds); + return [{ + emit: this.on, + to: userIds, + send: { + channel, + creatorId: ws.sessions.get(client.id), + }, + }]; + } + if (!name || !guildId || !type) throw new TypeError('Not enough options were provided'); @@ -23,4 +38,4 @@ export default class implements WSEvent<'CHANNEL_CREATE'> { }, }]; } -} +} \ No newline at end of file diff --git a/frontend/src/components/app.tsx b/frontend/src/components/app.tsx index 7dad4eae..798f2912 100644 --- a/frontend/src/components/app.tsx +++ b/frontend/src/components/app.tsx @@ -7,6 +7,7 @@ import OverviewPage from './pages/overview-page'; import LogoutPage from './pages/auth/logout-page'; import PrivateRoute from './routing/private-route'; import NotFoundPage from './pages/not-found-page'; +import DMPage from './pages/dm-page'; import { useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { ready } from '../store/auth'; @@ -38,6 +39,7 @@ export default function App() { {/* Users must be logged in to use private routes. */} + {/* This route is a catch-all for any other routes that don't exist. */} diff --git a/frontend/src/components/channel/text-based-channel.tsx b/frontend/src/components/channel/text-based-channel.tsx index f5d89e19..59455476 100644 --- a/frontend/src/components/channel/text-based-channel.tsx +++ b/frontend/src/components/channel/text-based-channel.tsx @@ -11,7 +11,7 @@ import { Util } from '@acrd/types'; const TextBasedChannel: React.FunctionComponent = () => { const dispatch = useDispatch(); const channel = useSelector((s: Store.AppState) => s.ui.activeChannel)!; - const guild = useSelector((s: Store.AppState) => s.ui.activeGuild)!; + const guild = useSelector((s: Store.AppState) => s.ui.activeGuild); const messages = useSelector(getChannelMessages(channel.id)); const perms = usePerms(); const [cachedContent, setCachedContent] = useState({}); @@ -41,7 +41,11 @@ const TextBasedChannel: React.FunctionComponent = () => { dispatch(fetchMessages(channel.id, back)); } - const canRead = perms.canInChannel('READ_MESSAGES', guild.id, channel.id); + const canRead = channel.type === 'DM' + ? true + : guild + ? perms.canInChannel('READ_MESSAGES', guild.id, channel.id) + : false; const LoadingIndicator: React.FunctionComponent = () => ( <> diff --git a/frontend/src/components/navigation/sidebar/dm-list.tsx b/frontend/src/components/navigation/sidebar/dm-list.tsx new file mode 100644 index 00000000..c5d1139e --- /dev/null +++ b/frontend/src/components/navigation/sidebar/dm-list.tsx @@ -0,0 +1,36 @@ +import { useSelector } from 'react-redux'; +import { Link } from 'react-router-dom'; +import { getDMChannels } from '../../../store/channels'; +import FoundUsername from '../../user/username'; +import { ChannelTypes } from '@acrd/types'; + +const DMList: React.FunctionComponent = () => { + const channels = useSelector(getDMChannels()); + const selfUser = useSelector((s: Store.AppState) => s.auth.user)!; + const users = useSelector((s: Store.AppState) => s.entities.users); + + + return ( +
+
+ {channels.map(c => { + const otherUserId = (c as ChannelTypes.DM).userIds + .find(id => id !== selfUser.id); + const otherUser = users.find(u => u.id === otherUserId); + if (!otherUser) return null; + + return ( + + + + ); + })} +
+
+ ); +} + +export default DMList; \ No newline at end of file diff --git a/frontend/src/components/pages/dm-page.tsx b/frontend/src/components/pages/dm-page.tsx new file mode 100644 index 00000000..c4d0921e --- /dev/null +++ b/frontend/src/components/pages/dm-page.tsx @@ -0,0 +1,58 @@ +import { useDispatch, useSelector } from 'react-redux'; +import AppNavbar from '../navigation/app-navbar'; +import Sidebar from '../navigation/sidebar/sidebar'; +import { Redirect, useParams } from 'react-router-dom'; +import { actions as uiActions } from '../../store/ui'; +import TextBasedChannel from '../channel/text-based-channel'; +import { useEffect } from 'react'; +import PageWrapper from './page-wrapper'; +import { getDMChannel, createDM } from '../../store/channels'; +import WSListener from '../ws-listener'; +import DMList from '../navigation/sidebar/dm-list'; + +const DMPage: React.FunctionComponent = () => { + const { channelId }: any = useParams(); + const dispatch = useDispatch(); + const channel = useSelector(getDMChannel(channelId)); + const users = useSelector((s: Store.AppState) => s.entities.users); + const selfUser = useSelector((s: Store.AppState) => s.auth.user)!; + const ui = useSelector((s: Store.AppState) => s.ui); + + useEffect(() => { + if (!channel && channelId) { + const otherUser = users.find(u => u.id === channelId); + if (otherUser) { + dispatch(createDM(otherUser.id)); + } + } + dispatch(uiActions.pageSwitched({ channel })); + }, [channel, channelId]); + + if (!channel && !channelId) + return ; + + const otherUser = users.find(u => + channel?.type === 'DM' && channel.userIds.includes(u.id) && u.id !== selfUser.id); + + return ( + + +
+ +
+ +
+ {channel && ui.activeChannel && { + 'DM': , + }[channel.type]} + +
+
+
+
+ ); +} + +export default DMPage; \ No newline at end of file diff --git a/frontend/src/components/pages/guild-page.tsx b/frontend/src/components/pages/guild-page.tsx index b5df7454..79d453b9 100644 --- a/frontend/src/components/pages/guild-page.tsx +++ b/frontend/src/components/pages/guild-page.tsx @@ -40,6 +40,7 @@ const GuildPage: React.FunctionComponent = () => { className="flex"> {ui.activeChannel && { 'TEXT': , + 'DM': , 'VOICE':
Add something cool here for voice channels?
, }[channel.type]} diff --git a/frontend/src/store/channels.ts b/frontend/src/store/channels.ts index 4bc5603a..9e17c7a9 100755 --- a/frontend/src/store/channels.ts +++ b/frontend/src/store/channels.ts @@ -67,6 +67,11 @@ export const getChannel = (id: string) => createSelector( channels => channels.find(c => c.id === id), ); +export const getDMChannel = (id: string) => createSelector( + state => state.entities.channels, + channels => channels.find(c => c.id === id && c.type === 'DM') +); + export const getChannelByName = (guildId: string, name: string) => createSelector( state => state.entities.channels, channels => { @@ -80,4 +85,29 @@ export const getChannelUsers = (channelId: string) => createSelector( const vc = channels.find(c => c.id === channelId) as ChannelTypes.Voice; return vc.userIds.map(id => users.find(u => u.id === id)) }, -); \ No newline at end of file +); + +export const getDMChannels = () => createSelector( + state => state.entities.channels, + channels => channels.filter(c => c.type === 'DM') +); + +export const createDM = (userId: string) => (dispatch, getState: () => Store.AppState) => { + const selfUser = getState().auth.user!; + const existingDM = getState().entities.channels + .find(c => c.type === 'DM' && + c.userIds.includes(selfUser.id) && + c.userIds.includes(userId)); + + if (existingDM) return; + + dispatch(api.wsCallBegan({ + event: 'CHANNEL_CREATE', + data: { + type: 'DM', + userIds: [selfUser.id, userId], + name: 'DM', + guildId: '0' + } as unknown as WS.Params.ChannelCreate, + })); +} \ No newline at end of file