Skip to content

Commit

Permalink
Merge pull request #7 from YubaNeupane/dev
Browse files Browse the repository at this point in the history
Added socket io and channel chatting ui
  • Loading branch information
YubaNeupane authored Sep 18, 2023
2 parents 5cd6a54 + 4e1b225 commit c3cbd82
Show file tree
Hide file tree
Showing 13 changed files with 594 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ChatHeader } from "@/components/chat/chat-header";
import { ChatInput } from "@/components/chat/chat-input";
import { currentProfile } from "@/lib/current-profile";
import { db } from "@/lib/db";
import { redirectToSignIn } from "@clerk/nextjs";
Expand Down Expand Up @@ -42,6 +43,13 @@ const channelIdPage = async ({ params }: channelIdPageProps) => {
serverId={channel.serverId}
type="channel"
/>
<div className="flex-1">Future Messages</div>
<ChatInput
name={channel.name}
type="channel"
apiUrl="/api/socket/messages"
query={{ channelId: channel.id, serverId: channel.serverId }}
/>
</div>
);
};
Expand Down
7 changes: 5 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { cn } from "@/lib/utils";

import { ThemeProvider } from "@/components/providers/theme-provider";
import { ModalProvider } from "@/components/providers/modal-provider";
import { SocketProvider } from "@/components/providers/socket-provider";

const font = Open_Sans({ subsets: ["latin"] });

Expand All @@ -29,8 +30,10 @@ export default function RootLayout({
enableSystem={false}
storageKey="discord-theme"
>
<ModalProvider />
{children}
<SocketProvider>
<ModalProvider />
{children}
</SocketProvider>
</ThemeProvider>
</body>
</html>
Expand Down
4 changes: 4 additions & 0 deletions components/chat/chat-header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Hash, Menu } from "lucide-react";
import { MobileToggle } from "@/components/mobile-toggle";
import { UserAvatar } from "@/components/user-avatar";
import { SocketIndicator } from "@/components/socket-indicator";

interface ChatHeaderProps {
serverId: string;
Expand All @@ -25,6 +26,9 @@ export const ChatHeader = ({
<UserAvatar src={imageUrl} className="w-8 h-8 mr-2 md:h-8 md:w-8" />
)}
<p className="font-semibold text-black text-md dark:text-white">{name}</p>
<div className="flex items-center ml-auto">
<SocketIndicator />
</div>
</div>
);
};
81 changes: 81 additions & 0 deletions components/chat/chat-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"use client";
import { useForm } from "react-hook-form";
import * as z from "zod";
import axios from "axios";
import qs from "query-string";
import { zodResolver } from "@hookform/resolvers/zod";

import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Plus, SmileIcon } from "lucide-react";

interface ChatInputProps {
apiUrl: string;
query: Record<string, any>;
name: string;
type: "channel" | "conversation";
}

const formSchema = z.object({
content: z.string().min(1),
});

export const ChatInput = ({ apiUrl, query, name, type }: ChatInputProps) => {
const form = useForm<z.infer<typeof formSchema>>({
defaultValues: {
content: "",
},
resolver: zodResolver(formSchema),
});

const isLoading = form.formState.isSubmitting;

const onSubmit = async (values: z.infer<typeof formSchema>) => {
try {
const url = qs.stringifyUrl({
url: apiUrl,
query,
});
await axios.post(url, values);
} catch (e) {
console.log(e);
}
};

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="relative p-4 pb-6">
<button
typeof="button"
onClick={() => {}}
className="absolute top-7 left-8 h-[24px] w-[24px] bg-zinc-500 dark:bg-zinc-400 hover:bg-zinc-600 dark:hover:bg-zinc-300 transition rounded-full p-1 flex items-center justify-center"
>
<Plus className="text-white dark:text-[#313338]" />
</button>
<Input
disabled={isLoading}
{...field}
placeholder={`Message ${
type === "conversation" ? name : `#${name}`
}`}
className="py-6 border-0 border-none px-14 bg-zinc-200/90 dark:bg-zinc-700/75 focus-visible:ring-0 focus-visible:ring-offset-0 text-zinc-600 dark:text-zinc-200"
/>
<div className="absolute top-7 right-8">
<SmileIcon />
</div>
</div>
</FormControl>
</FormItem>
)}
/>
</form>
</Form>
);
};
53 changes: 53 additions & 0 deletions components/providers/socket-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"use client";

import { createContext, useContext, useEffect, useState } from "react";
import { io as ClientIO } from "socket.io-client";

type SocketContextType = {
socket: any | null;
isConnected: boolean;
};

const SocketContext = createContext<SocketContextType>({
socket: null,
isConnected: false,
});

export const useSocket = () => {
return useContext(SocketContext);
};

export const SocketProvider = ({ children }: { children: React.ReactNode }) => {
const [socket, setSocket] = useState(null);
const [isConnected, setIsConnected] = useState(false);

useEffect(() => {
const socketInstance = new (ClientIO as any)(
process.env.NEXT_PUBLIC_SITE_URL!,
{
path: "/api/socket/io",
addTrailingSlash: false,
}
);

socketInstance.on("connect", () => {
setIsConnected(true);
});

socketInstance.on("disconnect", () => {
setIsConnected(false);
});

setSocket(socketInstance);

return () => {
socketInstance.disconnect();
};
}, []);

return (
<SocketContext.Provider value={{ socket, isConnected }}>
{children}
</SocketContext.Provider>
);
};
4 changes: 2 additions & 2 deletions components/server/server-channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,13 @@ export const ServerChannel = ({
</p>
{channel.name !== "general" && role !== MemberRole.GUEST && (
<div className="flex items-center ml-auto gap-x-2">
<ActionTooltip label="Edit">
<ActionTooltip label="Edit" align="center">
<Edit
onClick={(e) => onAction(e, "editChannel")}
className="hidden w-4 h-4 transition group-hover:block text-zinc-500 hover:text-zinc-600 dark:text-zinc-400 dark:hover:text-zinc-300"
/>
</ActionTooltip>
<ActionTooltip label="Delete">
<ActionTooltip label="Delete" align="center">
<Trash
onClick={(e) => onAction(e, "deleteChannel")}
className="hidden w-4 h-4 transition group-hover:block text-zinc-500 hover:text-zinc-600 dark:text-zinc-400 dark:hover:text-zinc-300"
Expand Down
23 changes: 23 additions & 0 deletions components/socket-indicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"use client";

import { useSocket } from "@/components/providers/socket-provider";

import { Badge } from "@/components/ui/badge";

export const SocketIndicator = () => {
const { isConnected } = useSocket();

if (!isConnected) {
return (
<Badge variant="outline" className="text-white bg-yellow-600 border-none">
Fallback: Polling every 1s
</Badge>
);
}

return (
<Badge variant="outline" className="text-white border-none bg-emerald-600">
Live: Real-time updates
</Badge>
);
};
36 changes: 36 additions & 0 deletions components/ui/badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)

export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}

function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}

export { Badge, badgeVariants }
2 changes: 0 additions & 2 deletions lib/conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ export const getOrCreateConversation = async (
memberOneId: string,
memberTwoId: string
) => {
console.log("memberOneId", memberOneId);
console.log("memberTwoId", memberTwoId);
let conversation =
(await findConversation(memberOneId, memberTwoId)) ||
(await findConversation(memberTwoId, memberOneId));
Expand Down
Loading

1 comment on commit c3cbd82

@vercel
Copy link

@vercel vercel bot commented on c3cbd82 Sep 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.