Skip to content

Commit

Permalink
Count meetings and associate feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
third774 committed Sep 20, 2024
1 parent 50c0ddf commit 0bbd0b5
Show file tree
Hide file tree
Showing 10 changed files with 374 additions and 27 deletions.
8 changes: 7 additions & 1 deletion app/components/LeaveRoomButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,24 @@ import { Tooltip } from './Tooltip'

interface LeaveRoomButtonProps {
navigateToFeedbackPage: boolean
meetingId?: string
}

export const LeaveRoomButton: FC<LeaveRoomButtonProps> = ({
navigateToFeedbackPage,
meetingId,
}) => {
const navigate = useNavigate()
return (
<Tooltip content="Leave">
<Button
displayType="danger"
onClick={() => {
navigate(navigateToFeedbackPage ? '/call-quality-feedback' : '/')
const params = new URLSearchParams()
if (meetingId) params.set('meetingId', meetingId)
navigate(
navigateToFeedbackPage ? `/call-quality-feedback?${params}` : '/'
)
}}
>
<VisuallyHidden>Leave</VisuallyHidden>
Expand Down
109 changes: 98 additions & 11 deletions app/durableObjects/ChatRoom.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import assertNever from '~/utils/assertNever'
import { assertNonNullable } from '~/utils/assertNonNullable'
import getUsername from '~/utils/getUsername.server'

import { eq, sql } from 'drizzle-orm'
import type { DrizzleD1Database } from 'drizzle-orm/d1'
import {
Server,
type Connection,
type ConnectionContext,
type WSMessage,
} from 'partyserver'
import { getDb, Meetings } from 'schema'

/**
* The ChatRoom Durable Object Class
Expand All @@ -21,6 +24,15 @@ import {
* to all others.
*/
export class ChatRoom extends Server<Env> {
env: Env
db: DrizzleD1Database<Record<string, never>> | null

constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env)
this.env = env
this.db = getDb(this)
}

static options = {
hibernate: true,
}
Expand All @@ -31,6 +43,7 @@ export class ChatRoom extends Server<Env> {
}

async onStart(): Promise<void> {
this.db = getDb(this)
// TODO: make this a part of partyserver
this.ctx.setWebSocketAutoResponse(
new WebSocketRequestResponsePair(
Expand All @@ -43,8 +56,8 @@ export class ChatRoom extends Server<Env> {
await this.ctx.storage.delete('sessions').catch(() => {
console.warn('Failed to delete old sessions storage')
})
// We can remove this line later
}

async onConnect(
connection: Connection<User>,
ctx: ConnectionContext
Expand Down Expand Up @@ -76,22 +89,68 @@ export class ChatRoom extends Server<Env> {

// store the user's data in storage
await this.ctx.storage.put(`session-${connection.id}`, user)

await this.trackPeakUserCount()
await this.broadcastRoomState()
}

async trackPeakUserCount() {
if (this.db) {
const meetingId = await this.getMeetingId()
const meeting = await this.getMeeting(meetingId)
if (!meeting) return
if (meeting.ended !== null) {
await this.db
.update(Meetings)
.set({ ended: null })
.where(eq(Meetings.id, meeting.id))
}

const previousCount = meeting.peakUserCount
const userCount = [...this.getConnections()].length
if (userCount > previousCount) {
await this.db
.update(Meetings)
.set({
peakUserCount: userCount,
})
.where(eq(Meetings.id, meeting.id))
}
}
}

async getMeetingId() {
let meetingId = await this.ctx.storage.get<string>('meetingId')
if (meetingId) return meetingId
meetingId = crypto.randomUUID()
await this.ctx.storage.put('meetingId', meetingId)
if (this.db) {
await this.db.insert(Meetings).values({
id: meetingId,
peakUserCount: 1,
})
}

return meetingId
}

async getMeeting(meetingId: string) {
if (!this.db) return null
const [meeting] = await this.db
.select()
.from(Meetings)
.where(eq(Meetings.id, meetingId))

return meeting ?? null
}

async broadcastRoomState() {
let didSomeoneQuit = false
const meetingId = await this.getMeetingId()
const roomState = {
type: 'roomState',
state: {
users: [
...(
await this.ctx.storage.list<User>({
prefix: 'session-',
})
).values(),
],
meetingId,
users: [...(await this.getUsers()).values()],
},
} satisfies ServerMessage

Expand Down Expand Up @@ -234,15 +293,43 @@ export class ChatRoom extends Server<Env> {
this.broadcastRoomState()
}

getUsers() {
return this.ctx.storage.list<User>({
prefix: 'session-',
})
}

async endMeeting(meetingId: string) {
await this.ctx.storage.delete('meetingId')
if (this.db) {
// stamp meeting as ended
await this.db
.update(Meetings)
.set({
ended: sql`CURRENT_TIMESTAMP`,
})
.where(eq(Meetings.id, meetingId))
}
}

async cleanupOldConnections() {
const connections = [...this.getConnections()]
const sessionsToCleanUp = await this.ctx.storage.list<User>()
const sessionsToCleanUp = await this.getUsers()

connections.forEach((connection) =>
sessionsToCleanUp.delete(`session-${connection.id}`)
)
return this.ctx.storage

await this.ctx.storage
.delete([...sessionsToCleanUp.keys()])
.catch(() => console.warn('Failed to clean up orphaned sessions'))

const activeUserCount = (await this.getUsers()).size
const meetingId = await this.ctx.storage.get<string>('meetingId')

if (meetingId && activeUserCount === 0) {
this.endMeeting(meetingId)
}
}

async alarm(): Promise<void> {
Expand Down
12 changes: 10 additions & 2 deletions app/routes/_room.$roomName.room.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,12 @@ function JoinedRoom({ bugReportsEnabled }: { bugReportsEnabled: boolean }) {
peer,
dataSaverMode,
pushedTracks,
room: { otherUsers, websocket, identity },
room: {
otherUsers,
websocket,
identity,
roomState: { meetingId },
},
} = useRoomContext()

const debugEnabled = useDebugEnabled()
Expand Down Expand Up @@ -326,7 +331,10 @@ function JoinedRoom({ bugReportsEnabled }: { bugReportsEnabled: boolean }) {
className="hidden md:block"
></ParticipantsButton>
<OverflowMenu bugReportsEnabled={bugReportsEnabled} />
<LeaveRoomButton navigateToFeedbackPage={hasDb} />
<LeaveRoomButton
navigateToFeedbackPage={hasDb}
meetingId={meetingId}
/>
</div>
</div>
<HighPacketLossWarningsToast />
Expand Down
41 changes: 31 additions & 10 deletions app/routes/call-quality-feedback.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type ActionFunctionArgs } from '@remix-run/cloudflare'
import { Form } from '@remix-run/react'
import { Form, useSearchParams } from '@remix-run/react'
import { AnalyticsSimpleCallFeedback, getDb } from 'schema'
import invariant from 'tiny-invariant'
import { Button } from '~/components/Button'
import { RELEASE } from '~/utils/constants'

Expand All @@ -17,26 +18,46 @@ export const action = async ({ request, context }: ActionFunctionArgs) => {

const formData = await request.formData()
const experiencedIssues = formData.get('experiencedIssues') === 'true'
const meetingId = formData.get('meetingId')
invariant(typeof meetingId === 'string')
await db.insert(AnalyticsSimpleCallFeedback).values({
experiencedIssues: Number(experiencedIssues),
version: RELEASE ?? 'dev',
meetingId,
})

return redirectToHome
}

export default function SetUsername() {
const [params] = useSearchParams()
const meetingId = params.get('meetingId')
return (
<div className="grid h-full gap-4 place-content-center">
<h1 className="text-3xl font-bold">Experience any issues?</h1>
<Form className="flex items-end gap-4" method="post">
<Button displayType="secondary" value="true" name="experiencedIssues">
Yes
</Button>
<Button displayType="secondary" value="false" name="experiencedIssues">
No
</Button>
</Form>
{meetingId ? (
<>
<h1 className="text-3xl font-bold">Experience any issues?</h1>
<Form className="flex items-end gap-4" method="post">
<Button
displayType="secondary"
value="true"
name="experiencedIssues"
>
Yes
</Button>
<Button
displayType="secondary"
value="false"
name="experiencedIssues"
>
No
</Button>
<input type="hidden" name="meetingId" value={meetingId} />
</Form>
</>
) : (
<h1>Missing meetingId</h1>
)}
</div>
)
}
1 change: 1 addition & 0 deletions app/types/Messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type User = {
}

export type RoomState = {
meetingId?: string
users: User[]
}

Expand Down
17 changes: 17 additions & 0 deletions migrations/0001_daily_blink.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
CREATE TABLE `Meetings` (
`id` text PRIMARY KEY NOT NULL,
`created` text DEFAULT CURRENT_TIMESTAMP NOT NULL,
`modified` text DEFAULT CURRENT_TIMESTAMP NOT NULL,
`deleted` text,
`userCount` integer NOT NULL,
`ended` text
);
--> statement-breakpoint
ALTER TABLE `AnalyticsSimpleCallFeedback` ADD `meetingId` text NOT NULL REFERENCES Meetings(id);--> statement-breakpoint
/*
SQLite does not support "Creating foreign key on existing column" out of the box, we do not generate automatic migration for that, so it has to be done manually
Please refer to: https://www.techonthenet.com/sqlite/tables/alter_table.php
https://www.sqlite.org/lang_altertable.html
Due to that we don't generate migration automatically and it has to be done manually
*/
Loading

0 comments on commit 0bbd0b5

Please sign in to comment.