From 17228d20be104632466ea95e8ffa2bd32cabd59f Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Fri, 4 Apr 2025 22:35:01 +0200 Subject: [PATCH] feat: settings to toggle notifications --- apps/web/package.json | 1 + .../settings/follows/notifyToggle.tsx | 27 ++++ .../app/(protected)/settings/follows/page.tsx | 73 +++++++++++ apps/web/src/components/app/NavBar/NavBar.tsx | 7 +- apps/web/src/components/ui/switch.tsx | 29 +++++ apps/web/src/components/ui/table.tsx | 117 ++++++++++++++++++ apps/web/src/lib/auth/resolve.ts | 16 +++ apps/web/src/lib/form/actions.ts | 24 ++++ .../web/src/lib/instrumentation/streamInfo.ts | 2 +- yarn.lock | 13 ++ 10 files changed, 307 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/app/(protected)/settings/follows/notifyToggle.tsx create mode 100644 apps/web/src/app/(protected)/settings/follows/page.tsx create mode 100644 apps/web/src/components/ui/switch.tsx create mode 100644 apps/web/src/components/ui/table.tsx diff --git a/apps/web/package.json b/apps/web/package.json index a606cdc..cba7519 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -28,6 +28,7 @@ "@radix-ui/react-select": "^2.1.5", "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tooltip": "^1.1.6", "@slack/web-api": "^7.9.1", "@uidotdev/usehooks": "^2.4.1", diff --git a/apps/web/src/app/(protected)/settings/follows/notifyToggle.tsx b/apps/web/src/app/(protected)/settings/follows/notifyToggle.tsx new file mode 100644 index 0000000..d5fd6c3 --- /dev/null +++ b/apps/web/src/app/(protected)/settings/follows/notifyToggle.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { useState } from 'react'; +import { Switch } from '@/components/ui/switch'; +import { notifyStreamToggle } from '@/lib/form/actions'; + +export default function NotifyToggle(props: Props) { + const [toggled, setToggled] = useState(props.toggled); + const [isLoading, setIsLoading] = useState(false); + + const handleToggle = async () => { + setIsLoading(true); + notifyStreamToggle(props.channel).then((res) => { + if (res.success) { + setToggled(res.toggle!); + } + }); + setIsLoading(false); + }; + + return ; +} + +interface Props { + channel: string; + toggled: boolean; +} diff --git a/apps/web/src/app/(protected)/settings/follows/page.tsx b/apps/web/src/app/(protected)/settings/follows/page.tsx new file mode 100644 index 0000000..e8ada82 --- /dev/null +++ b/apps/web/src/app/(protected)/settings/follows/page.tsx @@ -0,0 +1,73 @@ +import { validateRequest } from '@/lib/auth/validate'; +import { prisma } from '@hctv/db'; +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import Link from 'next/link'; +import { Button } from '@/components/ui/button'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import NotifyToggle from './notifyToggle'; + +export default async function Page() { + const { user } = await validateRequest(); + const following = await prisma.follow.findMany({ + where: { + userId: user!.id, + }, + include: { + channel: true, + }, + }); + + if (!following.length) { + return ( +
+

No channels followed

+

Go follow some first?

+ + + +
+ ); + } + + return ( +
+

Followed Channels

+ + + + Channel + Notifications + + + + {following.map((channel) => ( + + +
+ + + {channel.channel.name.charAt(0)} + + + {channel.channel.name} + +
+
+ + + +
+ ))} +
+
+
+ ); +} diff --git a/apps/web/src/components/app/NavBar/NavBar.tsx b/apps/web/src/components/app/NavBar/NavBar.tsx index d5c7e7e..33d27ff 100644 --- a/apps/web/src/components/app/NavBar/NavBar.tsx +++ b/apps/web/src/components/app/NavBar/NavBar.tsx @@ -14,7 +14,6 @@ import { import { logout } from '@/lib/auth/actions'; import { useSession } from '@/lib/providers/SessionProvider'; import Link from 'next/link'; -import MobileNavbarLinks from '../MobileNavbarLinks/MobileNavbarLinks'; import { ThemeSwitcher } from '../ThemeSwitcher/ThemeSwitcher'; import { Slack } from 'lucide-react'; import { SidebarTrigger } from '@/components/ui/sidebar'; @@ -66,6 +65,12 @@ export default function Navbar(props: Props) { My Account + + + Follows + + + , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/apps/web/src/components/ui/table.tsx b/apps/web/src/components/ui/table.tsx new file mode 100644 index 0000000..7f3502f --- /dev/null +++ b/apps/web/src/components/ui/table.tsx @@ -0,0 +1,117 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/apps/web/src/lib/auth/resolve.ts b/apps/web/src/lib/auth/resolve.ts index 330ea30..6b99907 100644 --- a/apps/web/src/lib/auth/resolve.ts +++ b/apps/web/src/lib/auth/resolve.ts @@ -27,4 +27,20 @@ export async function resolveOwnedChannels(id?: string) { })), ]; return channels; +} + +export async function resolveFollowedChannels(id?: string) { + const { user } = await validateRequest(); + const db = await prisma.follow.findMany({ + where: { + userId: id ?? user?.id, + }, + include: { + channel: true, + }, + }); + if (!db) { + return null; + } + return db; } \ No newline at end of file diff --git a/apps/web/src/lib/form/actions.ts b/apps/web/src/lib/form/actions.ts index ac41d89..ff29677 100644 --- a/apps/web/src/lib/form/actions.ts +++ b/apps/web/src/lib/form/actions.ts @@ -6,6 +6,7 @@ import { prisma } from '@hctv/db'; import zodVerify from '../zodVerify'; import { onboardSchema, streamInfoEditSchema } from './zod'; import { initializeStreamInfo } from '../instrumentation/streamInfo'; +import { resolveFollowedChannels } from '../auth/resolve'; export async function editStreamInfo(prev: any, formData: FormData) { const { user } = await validateRequest(); @@ -97,4 +98,27 @@ export async function onboard(prev: any, formData: FormData) { }) return { success: true }; +} + +export async function notifyStreamToggle(channelName: string) { + const { user } = await validateRequest(); + if (!user) { + return { success: false, error: 'Unauthorized' }; + } + + const followed = await resolveFollowedChannels(); + if (!followed) { + return { success: false, error: 'No followed channels' }; + } + const channel = followed.find((f) => f.channel.name === channelName); + if (!channel) { + return { success: false, error: 'Channel not found' }; + } + + await prisma.follow.update({ + where: { id: channel.id }, + data: { notifyStream: !channel.notifyStream }, + }); + + return { success: true, toggle: !channel.notifyStream }; } \ No newline at end of file diff --git a/apps/web/src/lib/instrumentation/streamInfo.ts b/apps/web/src/lib/instrumentation/streamInfo.ts index ecf3018..c7173bb 100644 --- a/apps/web/src/lib/instrumentation/streamInfo.ts +++ b/apps/web/src/lib/instrumentation/streamInfo.ts @@ -133,7 +133,7 @@ export async function syncStream() { for (const follower of subscribedFollowers) { queue.add(`streamStartDm:${follower.user.id}`, { - text: `${existingStream.username} is now *live*, streaming *${existingStream.title}* (${existingStream.category})!\n\n_Stream notifications are enabled for this user. If you want to disable them, you can do so in \`Profile > Notifications\`._`, + text: `${existingStream.username} is now *live*, streaming *${existingStream.title}* (${existingStream.category})!\n\n_Stream notifications are enabled for this user. If you want to disable them, you can do so in \`Profile > Follows\`._`, channel: follower.user.slack_id, unfurl_links: true, }); diff --git a/yarn.lock b/yarn.lock index 37c34d8..925cd01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1371,6 +1371,19 @@ dependencies: "@radix-ui/react-compose-refs" "1.1.1" +"@radix-ui/react-switch@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.3.tgz#cb6386909d1d3f65a2b81a3b15da8c91d18f49b0" + integrity sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-primitive" "2.0.2" + "@radix-ui/react-use-controllable-state" "1.1.0" + "@radix-ui/react-use-previous" "1.1.0" + "@radix-ui/react-use-size" "1.1.0" + "@radix-ui/react-tooltip@^1.1.6": version "1.1.8" resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz#1aa2a575630fca2b2845b62f85056bb826bec456"