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"