diff --git a/apps/privelte/src/lib/components/Chat.svelte b/apps/privelte/src/lib/components/Chat.svelte index 073d36d..f5692d3 100644 --- a/apps/privelte/src/lib/components/Chat.svelte +++ b/apps/privelte/src/lib/components/Chat.svelte @@ -2,7 +2,7 @@ import { afterUpdate } from 'svelte' import { ProgressRadial } from '@skeletonlabs/skeleton' import Message from './Message.svelte' - import { pendingMessages } from '$lib/stores' + import { pendingMessages, unreadMessagesCount } from '$lib/stores' import { page } from '$app/stores' import type { Payload, Presence } from '$lib/types/types' import Clipboard from './Clipboard.svelte' @@ -12,6 +12,7 @@ export let subscribed: 'loading' | 'ok' | 'error' let bottom: HTMLDivElement + let scrollable: HTMLDivElement $: getStatus = (id: string) => { if (!$pendingMessages.get(id)) { @@ -28,7 +29,14 @@ }) -
+
{ + if (scrollable.scrollHeight - scrollable.scrollTop - scrollable.clientHeight < 1) + unreadMessagesCount.reset() + }} + class="absolute left-0 top-0 h-full w-full space-y-4 overflow-y-auto overflow-x-clip px-4" +>

Share the link {#if isOwnMessage}
- {payload.username} + {payload.username} {#if status === 'loading'} {:else if status === 'error'} diff --git a/apps/privelte/src/lib/stores.ts b/apps/privelte/src/lib/stores.ts index 63dd636..f15002e 100644 --- a/apps/privelte/src/lib/stores.ts +++ b/apps/privelte/src/lib/stores.ts @@ -1,4 +1,4 @@ -import { writable } from 'svelte/store' +import { writable, derived } from 'svelte/store' export const createPendingMessages = () => { const { subscribe, update } = writable>(new Map()) @@ -18,3 +18,28 @@ export const createPendingMessages = () => { } export const pendingMessages = createPendingMessages() + +export const createUnreadMessagesCount = () => { + const { subscribe, update, set } = writable(0) + + return { + subscribe, + increment: () => update((value) => (value < 100 ? value + 1 : value)), + reset: () => set(0) + } +} + +export const unreadMessagesCount = createUnreadMessagesCount() + +export const unreadMessages = derived(unreadMessagesCount, ($unreadMessagesCount) => { + const strValue = String($unreadMessagesCount) + + switch ($unreadMessagesCount) { + case 0: + return '' + case 100: + return '(99+)' + default: + return `(${strValue})` + } +}) diff --git a/apps/privelte/src/routes/+layout.svelte b/apps/privelte/src/routes/+layout.svelte index c3ae701..2a00dc8 100644 --- a/apps/privelte/src/routes/+layout.svelte +++ b/apps/privelte/src/routes/+layout.svelte @@ -3,10 +3,13 @@ import { AppShell, AppBar, LightSwitch } from '@skeletonlabs/skeleton' import logo from '$lib/images/github-mark.svg' import { page } from '$app/stores' + import { unreadMessages } from '$lib/stores' - {$page.data.title ?? 'Privelte'} + + {`${$unreadMessages} ${$page.data.title ? `${$page.data.title} - ` : ''}Privelte`} + diff --git a/apps/privelte/src/routes/new/+page.server.ts b/apps/privelte/src/routes/new/+page.server.ts index 67d36b9..37f9c5d 100644 --- a/apps/privelte/src/routes/new/+page.server.ts +++ b/apps/privelte/src/routes/new/+page.server.ts @@ -22,9 +22,7 @@ export const load: PageServerLoad = () => { currentId = id - return { - id - } + return { id, title: 'New Room' } } export const actions: Actions = { diff --git a/apps/privelte/src/routes/room/[id]/+page.server.ts b/apps/privelte/src/routes/room/[id]/+page.server.ts index 21e3e64..34a25b3 100644 --- a/apps/privelte/src/routes/room/[id]/+page.server.ts +++ b/apps/privelte/src/routes/room/[id]/+page.server.ts @@ -9,7 +9,7 @@ export const load: PageServerLoad = async ({ params, cookies }) => { const { userId, username } = await verifyUser(session, params.id) return { - title: `${params.id} - Privelte`, + title: `${params.id}`, userId, username } diff --git a/apps/privelte/src/routes/room/[id]/+page.svelte b/apps/privelte/src/routes/room/[id]/+page.svelte index e59fa75..df40266 100644 --- a/apps/privelte/src/routes/room/[id]/+page.svelte +++ b/apps/privelte/src/routes/room/[id]/+page.svelte @@ -2,7 +2,7 @@ import { onMount } from 'svelte' import { nanoid } from 'nanoid' import { supabase } from '$lib/supabaseClient' - import { pendingMessages } from '$lib/stores' + import { pendingMessages, unreadMessagesCount } from '$lib/stores' import type { Payload, Presence } from '$lib/types/types' import type { PageData } from './$types' import Chat from '$lib/components/Chat.svelte' @@ -14,7 +14,45 @@ const channel = supabase.channel(data.roomId) - const sendMessage = async (message: string, id: string) => { + onMount(() => { + ;(async () => { + await heartbeat() + })() + + const intervalId = setInterval(async () => { + await heartbeat() + }, 12000) + + channel + .on('broadcast', { event: 'presence' }, ({ payload }) => { + const { username, event } = payload as { + username: string + event: 'joined' | 'left' + } + + handlePresence(username, event) + }) + .on('broadcast', { event: 'message' }, ({ payload }: { payload: Payload }) => { + if (payload.userId !== data.userId) { + unreadMessagesCount.increment() + entries = [...entries, payload] + } + }) + .subscribe((status) => { + if (status === 'SUBSCRIBED') { + subscribed = 'ok' + } else { + subscribed = 'error' + } + }) + + return async () => { + clearInterval(intervalId) + await supabase.removeChannel(channel) + } + }) + + async function sendMessage(message: string, id: string) { pendingMessages.setStatus(id, 'loading') const response = await fetch(data.roomId, { @@ -32,13 +70,13 @@ } } - const handleRetry = async (event: CustomEvent) => { + async function handleRetry(event: CustomEvent) { const { id, message } = event.detail as Pick await sendMessage(message, id) } - const handleSubmit = async (event: CustomEvent) => { + async function handleSubmit(event: CustomEvent) { const { id, message } = event.detail as Pick const payload: Payload = { @@ -54,48 +92,23 @@ await sendMessage(message, id) } - onMount(() => { - const intervalId = setInterval(async () => { - await fetch(data.roomId, { - method: 'PATCH' - }) - }, 12000) - - channel - .on('broadcast', { event: 'presence' }, ({ payload }) => { - const { username, event } = payload as { - username: string - event: 'joined' | 'left' - } - - const id = nanoid() - const presence: Presence = { - type: 'presence', - username, - event, - id - } - - entries = [...entries, presence] - }) - .on('broadcast', { event: 'message' }, ({ payload }: { payload: Payload }) => { - if (payload.userId !== data.userId) { - entries = [...entries, payload] - } - }) - .subscribe((status) => { - if (status === 'SUBSCRIBED') { - subscribed = 'ok' - } else { - subscribed = 'error' - } - }) + async function heartbeat() { + await fetch(data.roomId, { + method: 'PATCH' + }) + } - return async () => { - clearInterval(intervalId) - await supabase.removeChannel(channel) + function handlePresence(username: string, event: 'joined' | 'left') { + const id = nanoid() + const presence: Presence = { + type: 'presence', + username, + event, + id } - }) + + entries = [...entries, presence] + }
diff --git a/apps/privelte/src/routes/room/[id]/join/+page.server.ts b/apps/privelte/src/routes/room/[id]/join/+page.server.ts index 56bbc25..3da8551 100644 --- a/apps/privelte/src/routes/room/[id]/join/+page.server.ts +++ b/apps/privelte/src/routes/room/[id]/join/+page.server.ts @@ -10,7 +10,7 @@ export const load: PageServerLoad = async ({ params, cookies }) => { try { await verifyUser(session, params.id) } catch { - return { title: 'Join - Privelte' } + return { title: 'Join Room' } } return redirect(303, `/room/${params.id}`) diff --git a/packages/eslint-config/index.js b/packages/eslint-config/index.js index cbd783d..008e218 100644 --- a/packages/eslint-config/index.js +++ b/packages/eslint-config/index.js @@ -51,6 +51,7 @@ module.exports = { '@typescript-eslint/no-explicit-any': ['error', { ignoreRestArgs: true }], // getting data from 3rd party apis which naming conventions I don't control makes this rule insufferable '@typescript-eslint/naming-convention': 'off', + '@typescript-eslint/no-floating-promises': ['error', { ignoreIIFE: true }], '@typescript-eslint/no-misused-promises': [ 'error', {