Skip to content

Commit

Permalink
Merge pull request #147 from jbrunton/private-rooms
Browse files Browse the repository at this point in the history
feature: request join policy
  • Loading branch information
jbrunton authored Aug 24, 2024
2 parents 95b1623 + bed27ec commit 9fda5fe
Show file tree
Hide file tree
Showing 21 changed files with 438 additions and 24 deletions.
6 changes: 6 additions & 0 deletions client/src/data/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@ export type Room = {
joinPolicy: string
}

export type Membership = {
roomId: string
status: string
}

export type RoomResponse = {
room: Room
roles: string[]
membership?: Membership
}

const getRoom = async (roomId?: string): Promise<RoomResponse> => {
Expand Down
31 changes: 24 additions & 7 deletions client/src/features/room/organisms/ChatBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@ import { Button, Icon, Textarea, Spinner, VStack, Alert, AlertIcon, Spacer } fro
import React, { useState, KeyboardEventHandler, useRef, useEffect, ReactElement } from 'react'
import { AiOutlineArrowRight } from 'react-icons/ai'
import { usePostMessage } from '../../../data/messages'
import { useJoinRoom } from '../../../data/rooms'
import { RoomResponse, useJoinRoom } from '../../../data/rooms'
import { useUserDetails } from '../../../data/users'
import { can } from '../../../data/lib'

export type ChatBoxProps = {
roomId: string
canJoin: boolean
roomResponse: RoomResponse
}

const JoinAlert = ({ roomId, canJoin }: ChatBoxProps) => {
const JoinAlert = ({ roomResponse }: ChatBoxProps): ReactElement => {
const roomId = roomResponse.room.id

const canJoin = can('join', roomResponse)
const requiresApproval = roomResponse.room.joinPolicy === 'request'
const awaitingApproval = roomResponse.membership?.status === 'PendingApproval'

const { mutate: joinRoom, isLoading, isSuccess: isJoined } = useJoinRoom(roomId)

useEffect(() => {
Expand All @@ -20,13 +26,22 @@ const JoinAlert = ({ roomId, canJoin }: ChatBoxProps) => {
}, [isJoined])

if (canJoin) {
if (requiresApproval && awaitingApproval) {
return (
<Alert status='info' variant='top-accent'>
<AlertIcon />
Awaiting approval from owner.
<Spacer />
</Alert>
)
}
return (
<Alert status='info' variant='top-accent'>
<AlertIcon />
Join this room to chat.
<Spacer />
<Button rightIcon={isLoading ? <Spinner /> : undefined} onClick={() => joinRoom()}>
Join
{requiresApproval ? 'Request to Join' : 'Join'}
</Button>
</Alert>
)
Expand All @@ -40,7 +55,9 @@ const JoinAlert = ({ roomId, canJoin }: ChatBoxProps) => {
)
}

export const ChatBox: React.FC<ChatBoxProps> = ({ roomId, canJoin }: ChatBoxProps): ReactElement => {
export const ChatBox: React.FC<ChatBoxProps> = ({ roomResponse }: ChatBoxProps): ReactElement => {
const roomId = roomResponse.room.id

const [content, setContent] = useState<string>('')
const { data: user, isLoading } = useUserDetails()
const joined = user?.rooms.some((room) => room.id === roomId)
Expand All @@ -67,7 +84,7 @@ export const ChatBox: React.FC<ChatBoxProps> = ({ roomId, canJoin }: ChatBoxProp
}

if (!isLoading && !joined) {
return <JoinAlert roomId={roomId} canJoin={canJoin} />
return <JoinAlert roomResponse={roomResponse} />
}

return (
Expand Down
3 changes: 1 addition & 2 deletions client/src/features/room/pages/Room.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export const RoomPage = () => {
const { data: roomResponse } = useRoom(roomId)

const canRead = can('read', roomResponse)
const canJoin = can('join', roomResponse)

const { data: messages, isLoading: isLoadingMessages } = useMessages(roomId, { enabled: canRead })
useMessagesSubscription(roomId, { enabled: canRead })
Expand All @@ -27,7 +26,7 @@ export const RoomPage = () => {
return (
<Box display='flex' flexFlow='column' height='100%' flex='1'>
{<MessagesList messages={messages ?? [restrictedMessage(roomId)]} />}
<ChatBox roomId={roomId} canJoin={canJoin} />
<ChatBox roomResponse={roomResponse} />
</Box>
)
}
Expand Down
2 changes: 1 addition & 1 deletion services/api/src/app/auth/permissions/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const defineRolesForUser = (user: User, memberships: Membership[]) => {
});

can(Role.Join, 'Room', {
joinPolicy: JoinPolicy.Anyone,
joinPolicy: { $in: [JoinPolicy.Anyone, JoinPolicy.Request] },
});
can(Role.Join, 'Room', {
id: { $in: pendingInviteRoomIds },
Expand Down
9 changes: 9 additions & 0 deletions services/api/src/app/messages/command.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { P, match } from 'ts-pattern';
import { InviteUseCase } from '@usecases/rooms/invite';
import { LeaveRoomUseCase } from '@usecases/rooms/leave';
import { AboutRoomUseCase } from '@usecases/rooms/about-room';
import { ApproveRequestUseCase } from '@usecases/rooms/approve-request';

@Injectable()
export class CommandService {
Expand All @@ -22,6 +23,7 @@ export class CommandService {
private readonly help: HelpCommandUseCase,
private readonly leave: LeaveRoomUseCase,
private readonly parse: ParseCommandUseCase,
private readonly approveRequest: ApproveRequestUseCase,
private readonly changeRoomJoinPolicy: ChangeRoomJoinPolicyUseCase,
private readonly aboutRoom: AboutRoomUseCase,
private readonly invite: InviteUseCase,
Expand Down Expand Up @@ -57,6 +59,13 @@ export class CommandService {
authenticatedUser,
}),
)
.with({ tag: 'approveRequest', params: P.select() }, (params) =>
this.approveRequest.exec({
...params,
roomId,
authenticatedUser,
}),
)
.with({ tag: 'aboutRoom' }, () =>
this.aboutRoom.exec({
roomId,
Expand Down
2 changes: 2 additions & 0 deletions services/api/src/app/messages/messages.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ChangeRoomJoinPolicyUseCase } from '@usecases/rooms/change-room-join-po
import { InviteUseCase } from '@usecases/rooms/invite';
import { LeaveRoomUseCase } from '@usecases/rooms/leave';
import { AboutRoomUseCase } from '@usecases/rooms/about-room';
import { ApproveRequestUseCase } from '@usecases/rooms/approve-request';

@Module({
imports: [AuthModule, DispatcherModule],
Expand All @@ -37,6 +38,7 @@ import { AboutRoomUseCase } from '@usecases/rooms/about-room';
LeaveRoomUseCase,
LoremCommandUseCase,
AboutRoomUseCase,
ApproveRequestUseCase,
HelpCommandUseCase,
],
exports: [MessagesService],
Expand Down
12 changes: 11 additions & 1 deletion services/api/src/domain/entities/membership.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,18 @@ export const isPendingInvite = allPass([
withStatus(MembershipStatus.PendingInvite),
]);

export const isPendingApproval = allPass([
isCurrent,
withStatus(MembershipStatus.PendingApproval),
]);

export const isMemberOf = (roomId: string, memberships: Membership[]) =>
memberships.some(allPass([isActive, forRoom(roomId)]));

export const hasInviteTo = (roomId: string, memberships: Membership[]) =>
export const hasPendingInviteTo = (roomId: string, memberships: Membership[]) =>
memberships.some(allPass([isPendingInvite, forRoom(roomId)]));

export const hasPendingRequestTo = (
roomId: string,
memberships: Membership[],
) => memberships.some(allPass([isPendingApproval, forRoom(roomId)]));
3 changes: 2 additions & 1 deletion services/api/src/domain/usecases/commands/help.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ describe('HelpCommandUseCase', () => {
'* `/rename room {name}`: change the room name',
'* `/rename user {name}`: change your display name',
'* `/leave`: leave room',
"* `/set room join policy {'anyone', 'invite'}`: set the room join policy",
"* `/set room join policy {'anyone', 'request', 'invite'}`: set the room join policy",
'* `/about room`: about room (including policies)',
'* `/invite {email}`: invite a user to the room',
'* `/approve request {email}`: approve pending request to join the room',
].join('\n'),
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type ParsedCommand =
| { tag: 'renameRoom'; params: { newName: string } }
| { tag: 'renameUser'; params: { newName: string } }
| { tag: 'inviteUser'; params: { email: string } }
| { tag: 'approveRequest'; params: { email: string } }
| { tag: 'leave'; params: null }
| { tag: 'aboutRoom'; params: null }
| { tag: 'changeRoomJoinPolicy'; params: { newJoinPolicy: JoinPolicy } }
Expand Down
27 changes: 26 additions & 1 deletion services/api/src/domain/usecases/commands/parse/parse.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ describe('ParseCommandUseCase', () => {
it('validates the number of arguments', () => {
withMessage('/set room join policy').expectError(
'Error in command `/set room join policy`:',
`* Received too few arguments. Expected: \`/set room join policy {'anyone', 'invite'}\``,
`* Received too few arguments. Expected: \`/set room join policy {'anyone', 'request', 'invite'}\``,
);
});
});
Expand Down Expand Up @@ -189,6 +189,31 @@ describe('ParseCommandUseCase', () => {
});
});

describe('/approve request', () => {
it('parses valid commands', () => {
withMessage('/approve request [email protected]').expectCommand({
tag: 'approveRequest',
params: {
email: '[email protected]',
},
});
});

it('validates the number of arguments', () => {
withMessage('/approve request').expectError(
'Error in command `/approve request`:',
`* Received too few arguments. Expected: \`/approve request {email}\``,
);
});

it('validates the email', () => {
withMessage('/approve request not-an-email').expectError(
'Error in command `/approve request not-an-email`:',
`* Argument 2 (\`not-an-email\`): Invalid email`,
);
});
});

it('responds if the command is unrecognised', () => {
const command: Command = {
roomId: 'my-room',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { z } from 'zod';
import { CommandParser, ParsedCommand } from '../command.parser';

const schema = z
.tuple([z.literal('approve'), z.literal('request'), z.string().email()])
.transform<ParsedCommand>(([, , email]) => ({
tag: 'approveRequest',
params: { email },
}));

export const approveRequestParser = new CommandParser({
matchTokens: ['approve', 'request'],
schema,
signature: `/approve request {email}`,
summary: 'approve pending request to join the room',
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const schema = z
z.literal('room'),
z.literal('join'),
z.literal('policy'),
z.enum([JoinPolicy.Anyone, JoinPolicy.Invite]),
z.enum([JoinPolicy.Anyone, JoinPolicy.Invite, JoinPolicy.Request]),
])
.rest(z.string())
.transform<ParsedCommand>(([, , , , joinPolicy]) => ({
Expand All @@ -19,6 +19,6 @@ const schema = z
export const changeRoomJoinPolicyParser = new CommandParser({
matchTokens: ['set', 'room', 'join', 'policy'],
schema,
signature: `/set room join policy {'anyone', 'invite'}`,
signature: `/set room join policy {'anyone', 'request', 'invite'}`,
summary: 'set the room join policy',
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CommandParser } from '../command.parser';
import { aboutRoomParser } from './about-room-parser';
import { approveRequestParser } from './approve-request-parser';
import { changeRoomJoinPolicyParser } from './change-room-join-policy-parser';
import { helpParser } from './help.parser';
import { inviteParser } from './invite-parser';
Expand All @@ -17,4 +18,5 @@ export const parsers: CommandParser[] = [
changeRoomJoinPolicyParser,
aboutRoomParser,
inviteParser,
approveRequestParser,
];
Loading

0 comments on commit 9fda5fe

Please sign in to comment.