├── src ├── vite-env.d.ts ├── assets │ ├── logo.png │ └── user.png ├── main.tsx ├── components │ ├── filled-button.tsx │ ├── outlined-button.tsx │ ├── modal │ │ └── modal.tsx │ ├── big-icon-button.tsx │ └── sidebar │ │ ├── channel-tile.tsx │ │ ├── message-tile.tsx │ │ └── sidebar.tsx ├── App.tsx ├── index.css ├── pages │ ├── home │ │ ├── search-modal.tsx │ │ └── home.tsx │ ├── direct-message │ │ └── direct-message.tsx │ └── channel │ │ └── channel.tsx └── static-data │ └── index.ts ├── postcss.config.js ├── vite.config.ts ├── tsconfig.node.json ├── .gitignore ├── index.html ├── .eslintrc.cjs ├── tsconfig.json ├── tailwind.config.js ├── package.json ├── public └── vite.svg └── README.md /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wakhiwemathuthu/threadsocket/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wakhiwemathuthu/threadsocket/HEAD/src/assets/user.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react-swc' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | base:"/threadsocket/", 7 | plugins: [react()], 8 | }) 9 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.tsx"; 4 | import "./index.css"; 5 | import { HashRouter } from "react-router-dom"; 6 | 7 | ReactDOM.createRoot(document.getElementById("root")!).render( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Thread Socket 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /src/components/filled-button.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | title: string; 3 | onClick?: any; 4 | bgColor?: string; 5 | textColor?: string; 6 | className?: string; 7 | }; 8 | 9 | function FilledButton({ 10 | title, 11 | onClick, 12 | className, 13 | bgColor = "bg-green-200", 14 | textColor = "text-white", 15 | }: Props){ 16 | return ( 17 | 23 | ); 24 | } 25 | 26 | export default FilledButton; 27 | -------------------------------------------------------------------------------- /src/components/outlined-button.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | title: string; 3 | onClick?: any; 4 | borderColor?: string; 5 | textColor?: string; 6 | className?: string; 7 | }; 8 | 9 | function OutlinedButton({ 10 | title, 11 | onClick, 12 | borderColor = "border border-white", 13 | className, 14 | textColor = "text-white", 15 | }: Props) { 16 | return ( 17 | 23 | ); 24 | } 25 | 26 | export default OutlinedButton; 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html","./src/**/*.{ts,tsx}"], 4 | theme: { 5 | extend: { 6 | colors: { 7 | "purple-900": "#19171d", 8 | "black-800": "#1a1d21", 9 | "blue-400": "#1164a3", 10 | "blue-50": "#27242c", 11 | "black-backdrop": "rgba(0,0,0,0.5)", 12 | "black-50": "#2b2a2f", 13 | "black-200": "#222529", 14 | "green-100": "#273435", 15 | "green-200": "#148567", 16 | "gray-300": "#908f93", 17 | "gray-200": "#d1d2d3", 18 | "black-100": "#35373b", 19 | "gray-100": "#3e3d42", 20 | }, 21 | }, 22 | }, 23 | plugins: [], 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/components/modal/modal.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | type Props = { 5 | state: string; 6 | children?: ReactNode; 7 | className?: string; 8 | backdrop?: string; 9 | backdropClick?: any; 10 | }; 11 | 12 | function Modal({ 13 | state, 14 | children, 15 | backdropClick, 16 | backdrop = "bg-black-backdrop", 17 | className = "p-3 w-96 mx-auto mt-24 rounded-md bg-black-800", 18 | }: Props) { 19 | const isOpen = state === "visible"; 20 | return ReactDOM.createPortal( 21 |
27 |
{children}
28 |
, 29 | document.body 30 | ); 31 | } 32 | 33 | export default Modal; 34 | -------------------------------------------------------------------------------- /src/components/big-icon-button.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | type Props = { 4 | icon: ReactNode; 5 | title?: string; 6 | subtitle?: string; 7 | onClick?: any; 8 | ref?: any; 9 | }; 10 | 11 | function BigIconButton({ 12 | icon, 13 | title, 14 | subtitle, 15 | onClick, 16 | ref, 17 | }: Props) { 18 | return ( 19 | 32 | ); 33 | } 34 | 35 | export default BigIconButton; 36 | -------------------------------------------------------------------------------- /src/components/sidebar/channel-tile.tsx: -------------------------------------------------------------------------------- 1 | import { BsHash } from "react-icons/bs"; 2 | import { NavLink } from "react-router-dom"; 3 | 4 | type Props = { 5 | id: string; 6 | title: string; 7 | }; 8 | 9 | function ChannelTile({ id, title }: Props) { 10 | return ( 11 | { 13 | return isActive 14 | ? "flex flex-row items-center gap-2 p-1 bg-blue-400 rounded " 15 | : "flex flex-row items-center gap-2 p-1 rounded hover:bg-black-50"; 16 | }} 17 | to={`/channel/${id}`} 18 | > 19 | {({ isActive }) => { 20 | return ( 21 | <> 22 | 23 |

24 | {title} 25 |

26 | 27 | ); 28 | }} 29 |
30 | ); 31 | } 32 | 33 | export default ChannelTile; 34 | -------------------------------------------------------------------------------- /src/components/sidebar/message-tile.tsx: -------------------------------------------------------------------------------- 1 | import { NavLink } from "react-router-dom"; 2 | import { FiAtSign } from "react-icons/fi"; 3 | 4 | type Props = { 5 | id: string; 6 | title: string; 7 | }; 8 | 9 | function MessageTile({ id, title }: Props) { 10 | return ( 11 | { 13 | return isActive 14 | ? "flex flex-row items-center gap-2 p-1 bg-blue-400 rounded " 15 | : "flex flex-row items-center gap-2 p-1 rounded hover:bg-black-50"; 16 | }} 17 | to={`/message/${id}`} 18 | > 19 | {({ isActive }) => { 20 | return ( 21 | <> 22 | 23 |

24 | {title} 25 |

26 | 27 | ); 28 | }} 29 |
30 | ); 31 | } 32 | 33 | export default MessageTile; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "http://wakhiwemathuthu.github.io/threadsocket", 3 | "name": "threadsocket", 4 | "private": true, 5 | "version": "0.0.0", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vite", 9 | "predeploy": "npm run build", 10 | "deploy": "gh-pages -d dist", 11 | "build": "tsc && vite build", 12 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 13 | "preview": "vite preview" 14 | }, 15 | "dependencies": { 16 | "emoji-picker-react": "^4.5.3", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "react-icons": "^4.11.0", 20 | "react-router-dom": "^6.17.0" 21 | }, 22 | "devDependencies": { 23 | "@types/react": "^18.2.15", 24 | "@types/react-dom": "^18.2.7", 25 | "@typescript-eslint/eslint-plugin": "^6.0.0", 26 | "@typescript-eslint/parser": "^6.0.0", 27 | "@vitejs/plugin-react-swc": "^3.3.2", 28 | "autoprefixer": "^10.4.16", 29 | "eslint": "^8.45.0", 30 | "eslint-plugin-react-hooks": "^4.6.0", 31 | "eslint-plugin-react-refresh": "^0.4.3", 32 | "gh-pages": "^6.0.0", 33 | "postcss": "^8.4.31", 34 | "tailwindcss": "^3.3.3", 35 | "typescript": "^5.0.2", 36 | "vite": "^4.4.5" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import SideBar from "./components/sidebar/sidebar"; 3 | import { Routes, Route } from "react-router-dom"; 4 | import Home from "./pages/home/home"; 5 | import DirectMessage from "./pages/direct-message/direct-message"; 6 | import Channel from "./pages/channel/channel"; 7 | 8 | function App() { 9 | const [sideBar, setSideBar] = useState<"visible" | "hidden">("visible"); 10 | 11 | const toggleSideBar = () => { 12 | setSideBar((value) => { 13 | return value === "visible" ? "hidden" : "visible"; 14 | }); 15 | }; 16 | 17 | return ( 18 |
19 | 20 |
25 | 26 | } 29 | /> 30 | 34 | } 35 | /> 36 | } 39 | /> 40 | 41 |
42 |
43 | ); 44 | } 45 | 46 | export default App; 47 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | user-select: none; 3 | } 4 | /* Webkit (Chrome, Safari) Scrollbars */ 5 | ::-webkit-scrollbar { 6 | width: 8px; 7 | } 8 | ::-webkit-scrollbar-track { 9 | background: transparent; 10 | } 11 | ::-webkit-scrollbar-thumb { 12 | background: #6c6c6f; 13 | border-radius: 6px; 14 | } 15 | /* Firefox Scrollbars */ 16 | /* These styles will only affect the track in Firefox. */ 17 | scrollbar { 18 | width: 8px; 19 | } 20 | scrollbar-track { 21 | background: #6c6c6f; 22 | } 23 | /* Internet Explorer Scrollbars */ 24 | /* Note: Internet Explorer does not support custom scrollbar styling. */ 25 | /* These styles will only affect the scrollbar in Edge and IE. */ 26 | scrollbar { 27 | width: 8px; 28 | } 29 | scrollbar-thumb { 30 | background: #6c6c6f; 31 | border-radius: 6px; 32 | } 33 | /* Generic Scrollbars (for compatibility) */ 34 | /* These styles will be applied to all browsers as a fallback. */ 35 | .scrollbar { 36 | width: 8px; 37 | } 38 | .scrollbar-track { 39 | background: transparent; 40 | } 41 | .scrollbar-thumb { 42 | background: #6c6c6f; 43 | border-radius: 6px; 44 | } 45 | .EmojiPickerReact { 46 | --epr-bg-color: #1a1d21 !important; 47 | --epr-category-label-bg-color: #1a1d21 !important; 48 | --epr-border-color: red !important; 49 | --epr-text-color: #908f93 !important; 50 | --epr-search-input-bg-color: transparent !important; 51 | --epr-search-input-text-color: white !important; 52 | --epr-search-border-color: #148567 !important; 53 | --epr-preview-border-color: #2b2a2f !important; 54 | --epr-picker-border-color: #2b2a2f !important; 55 | } 56 | 57 | @tailwind base; 58 | @tailwind utilities; 59 | @tailwind components; 60 | -------------------------------------------------------------------------------- /src/pages/home/search-modal.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom"; 2 | import { BiSearch } from "react-icons/bi"; 3 | import { IoMdClose } from "react-icons/io"; 4 | 5 | type Props = { 6 | setSearchModal: any; 7 | state: string; 8 | }; 9 | 10 | function SearchModal({ setSearchModal, state }: Props) { 11 | const isVisible = state === "visible"; 12 | 13 | const CloseSearchModal = (e: any) => { 14 | if (e.target === e.currentTarget) { 15 | setSearchModal("hidden"); 16 | } 17 | }; 18 | 19 | return ReactDOM.createPortal( 20 |
26 |
27 |
28 |
29 |
30 |
31 | 32 | 38 |
39 |
setSearchModal("hidden")} 41 | className="p-2 cursor-pointer" 42 | > 43 | 44 |
45 |
46 |
47 |
48 |
49 |
, 50 | document.body 51 | ); 52 | } 53 | 54 | export default SearchModal; 55 | -------------------------------------------------------------------------------- /src/static-data/index.ts: -------------------------------------------------------------------------------- 1 | type User = { 2 | fullName: string; 3 | email: string; 4 | gender: "male" | "female"; 5 | id: string; 6 | }; 7 | 8 | const users: User[] = [ 9 | { 10 | fullName: "John Doe", 11 | email: "john@gmail.com", 12 | gender: "male", 13 | id: "v4j142jgh45g1251", 14 | }, 15 | { 16 | fullName: "Mary Williams", 17 | email: "mary@gmail.com", 18 | gender: "female", 19 | id: "v4j142jgh4nd1251", 20 | }, 21 | { 22 | fullName: "Stephen Hawking", 23 | email: "stephen@gmail.com", 24 | gender: "male", 25 | id: "v4j142jgh45gisxc1", 26 | }, 27 | { 28 | fullName: "James Bond", 29 | email: "james@gmail.com", 30 | gender: "male", 31 | id: "v4j142jgh45i4x51", 32 | }, 33 | { 34 | fullName: "Daniel Samuel", 35 | email: "daniel@gmail.com", 36 | gender: "male", 37 | id: "v4j142jgh45nr821", 38 | }, 39 | { 40 | fullName: "Charlotte Williams", 41 | email: "charlotte@gmail.com", 42 | gender: "female", 43 | id: "v4j142jghnng1251", 44 | }, 45 | { 46 | fullName: "Emma Watson", 47 | email: "emma@gmail.com", 48 | gender: "female", 49 | id: "v4j142jg00nd1251", 50 | }, 51 | { 52 | fullName: "Elizabeth Hawking", 53 | email: "stephen@gmail.com", 54 | gender: "male", 55 | id: "v4j1466gh45gisxc1", 56 | }, 57 | { 58 | fullName: "Anna Bond", 59 | email: "james@gmail.com", 60 | gender: "female", 61 | id: "v4j142jghv35i4x51", 62 | }, 63 | { 64 | fullName: "Amelia Samuel", 65 | email: "daniel@gmail.com", 66 | gender: "male", 67 | id: "v4j142jja45nr821", 68 | }, 69 | { 70 | fullName: "Ava Bond", 71 | email: "james@gmail.com", 72 | gender: "female", 73 | id: "v4j142jghv35i47s1", 74 | }, 75 | { 76 | fullName: "Felicity Samuel", 77 | email: "daniel@gmail.com", 78 | gender: "male", 79 | id: "v4j142jja45nr8md", 80 | }, 81 | ]; 82 | export default users; 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ThreadSocket Chat App 2 | 3 | ThreadSocket is a real-time chat application powered by React, designed to facilitate seamless communication among users. Whether you need to engage in group discussions or have private one-on-one conversations, ThreadSocket has you covered. 4 | 5 | **View live version** : https://wakhiwemathuthu.github.io/threadsocket/ 6 | 7 | ## Tech Stack Used 8 | ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) 9 | ![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white) 10 | ![Vite](https://img.shields.io/badge/vite-%23646CFF.svg?style=for-the-badge&logo=vite&logoColor=white) 11 | ![React Router](https://img.shields.io/badge/React_Router-CA4245?style=for-the-badge&logo=react-router&logoColor=white) 12 | ![TailwindCSS](https://img.shields.io/badge/tailwindcss-%2338B2AC.svg?style=for-the-badge&logo=tailwind-css&logoColor=white) 13 | 14 | 15 | ## Features 16 | 17 | - **Channels**: Create and join discussion channels based on your interests or teams. 18 | - **Direct Messages**: Send private messages to individuals for more personalized conversations. 19 | - **User Authentication**: Securely register and log in to your account. 20 | - **Responsive Design**: Enjoy a smooth chat experience on various devices and screen sizes. 21 | 22 | ## Getting Started 23 | 24 | Follow these steps to get the ThreadSocket app up and running on your local machine: 25 | 26 | 1. Clone this repository: 27 | ``` 28 | https://github.com/wakhiwemathuthu/threadsocket.git 29 | ``` 30 | 2. Navigate to the project directory: 31 | ``` 32 | cd threadsocket 33 | ``` 34 | 3. Install dependencies: 35 | ``` 36 | npm install 37 | ``` 38 | 4. Start the development server: 39 | ``` 40 | npm start 41 | ``` 42 | 43 | ## How to Contribute 44 | 45 | If you're interested in collaborating on ThreadSocket, we welcome your contributions. 46 | 47 | Here's how you can get involved: 48 | 49 | - Give It a Star: If you find this project useful or interesting, please consider giving it a star on GitHub. 50 | - Collaborate: If you have ideas for improvements or new features, feel free to open an issue or submit a pull request. We'd love to work together to make ThreadSocket even better. 51 | - Contact Us: To discuss collaboration or share your thoughts, you can reach out to us via email at wakhiwemathuthu6@gmail.com. 52 | -------------------------------------------------------------------------------- /src/pages/direct-message/direct-message.tsx: -------------------------------------------------------------------------------- 1 | import { FiAtSign } from "react-icons/fi"; 2 | import { FaAngleDown } from "react-icons/fa"; 3 | import { AiOutlineInfoCircle } from "react-icons/ai"; 4 | import FilledButton from "../../components/filled-button"; 5 | import EmojiPicker from "emoji-picker-react"; 6 | import { useEffect, useRef, useState } from "react"; 7 | import { Theme } from "emoji-picker-react"; 8 | import { GoSidebarCollapse, GoSidebarExpand, GoSmiley } from "react-icons/go"; 9 | import { IoSend } from "react-icons/io5"; 10 | 11 | type Props = { 12 | state: string; 13 | toggleSideBar: any; 14 | }; 15 | 16 | function DirectMessage({ state, toggleSideBar }: Props){ 17 | const [emojiModal, setEmojiModal] = useState<"visible" | "hidden">("hidden"); 18 | const [input, setInput] = useState(""); 19 | const messageInputRef = useRef(null); 20 | const emojiButtonRef = useRef(null); 21 | 22 | const message = input.replaceAll(" ", ""); 23 | const isMessageEmpty = message === ""; 24 | const isEmojiModalOpen = emojiModal === "visible"; 25 | const isSideBarOpen = state === "visible"; 26 | 27 | //A function to toggle the emoji modal. 28 | const toggleEmojiModal = () => { 29 | setEmojiModal((value) => { 30 | const previousValue = value; 31 | if (previousValue === "hidden") { 32 | return "visible"; 33 | } else { 34 | return "hidden"; 35 | } 36 | }); 37 | }; 38 | 39 | //Side effect that runs on the initial render 40 | // and when changes are detected on the emojiModal value. 41 | //This side effect auto focuses the message input. 42 | useEffect(() => { 43 | if (messageInputRef.current) { 44 | messageInputRef.current.focus(); 45 | //get the input value. 46 | const inputValue = messageInputRef.current.value; 47 | //Place the cursor at the end of any text inside the input. 48 | messageInputRef.current.selectionStart = inputValue.length; 49 | messageInputRef.current.selectionEnd = inputValue.length; 50 | } 51 | }, [emojiModal]); 52 | 53 | //Side effect that adds a keyboard event listener to the current window 54 | //and listens for the shortcut `ctrl + e` to open the emojiModal. 55 | //This side effect only runs on the initial render of the component. 56 | useEffect(() => { 57 | const handleEmojiShortcut = (event: KeyboardEvent) => { 58 | if ((event.ctrlKey || event.metaKey) && event.key === "e") { 59 | if (emojiButtonRef.current) { 60 | event.preventDefault(); 61 | emojiButtonRef.current.click(); 62 | } 63 | } 64 | }; 65 | window.addEventListener("keydown", handleEmojiShortcut); 66 | 67 | return () => { 68 | window.removeEventListener("keydown", handleEmojiShortcut); 69 | }; 70 | }, []); 71 | 72 | return ( 73 |
74 |
75 |
79 | {isSideBarOpen ? ( 80 | 81 | ) : ( 82 | 83 | )} 84 |
85 |
86 | 87 |

Person's Name

88 | 89 |
90 |
91 |
92 |
93 | 94 |
95 |

96 | Messages and files older than 60 days are concealed. 97 |

98 |

99 | Join Thread Socket Premium for extended file retention, ensuring 100 | your data remains accessible for an extended duration plus all the 101 | premium features of the Pro plan. 102 |

103 | 104 |
105 |
106 |
107 |
108 | 115 | { 119 | setInput(e.target.value); 120 | }} 121 | value={input} 122 | className="p-2 resize-none flex-1 bg-transparent caret-white text-white border-none outline-none" 123 | /> 124 | 133 |
134 |
{ 136 | if (e.target === e.currentTarget) { 137 | setEmojiModal("hidden"); 138 | } 139 | }} 140 | className={`${ 141 | isEmojiModalOpen ? "block" : "hidden" 142 | } absolute top-0 left-0 bottom-0 right-0`} 143 | > 144 |
145 | { 148 | setInput((value) => { 149 | return value + emoji.emoji; 150 | }); 151 | }} 152 | /> 153 |
154 |
155 |
156 | ); 157 | } 158 | 159 | export default DirectMessage; 160 | -------------------------------------------------------------------------------- /src/components/sidebar/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { LiaRocketSolid } from "react-icons/lia"; 2 | import { IoMdArrowDropdown, IoMdArrowDropright } from "react-icons/io"; 3 | import { AiOutlinePlus } from "react-icons/ai"; 4 | import { useState } from "react"; 5 | import MessageTile from "./message-tile"; 6 | import { NavLink } from "react-router-dom"; 7 | import { MdLogout } from "react-icons/md"; 8 | import { FiEdit } from "react-icons/fi"; 9 | import { BiHome } from "react-icons/bi"; 10 | import FilledButton from "../filled-button"; 11 | import Modal from "../modal/modal"; 12 | import ChannelTile from "./channel-tile"; 13 | 14 | type Props = { 15 | state: string; 16 | }; 17 | 18 | function SideBar({ state }: Props) { 19 | const [directMessagesVisible, setDirectMessagesVisible] = useState< 20 | "visible" | "invisible" 21 | >("invisible"); 22 | const [channelsVisible, setChannelsVisible] = useState< 23 | "visible" | "invisible" 24 | >("invisible"); 25 | const [modal, setModal] = useState<"hidden" | "visible">("hidden"); 26 | 27 | const toggleModal = () => { 28 | setModal((val) => { 29 | return val === "hidden" ? "visible" : "hidden"; 30 | }); 31 | }; 32 | 33 | const messages = [ 34 | { name: "Karl", id: "1" }, 35 | { name: "Tony", id: "2" }, 36 | { name: "Wakhiwe", id: "3" }, 37 | { name: "John", id: "4" }, 38 | { name: "Daniel", id: "5" }, 39 | { name: "Wakhiwe", id: "6" }, 40 | { name: "John", id: "7" }, 41 | { name: "Daniel", id: "8" }, 42 | { name: "Wakhiwe", id: "9" }, 43 | { name: "John", id: "10" }, 44 | { name: "Daniel", id: "11" }, 45 | { name: "Wakhiwe", id: "12" }, 46 | { name: "John", id: "13" }, 47 | { name: "Daniel", id: "14" }, 48 | { name: "Wakhiwe", id: "15" }, 49 | { name: "John", id: "16" }, 50 | { name: "Daniel", id: "17" }, 51 | ]; 52 | const channels = [ 53 | { name: "Channel 1", id: "12" }, 54 | { name: "Channel 2", id: "13" }, 55 | { name: "Channel 3", id: "14" }, 56 | { name: "Channel 4", id: "15" }, 57 | { name: "Channel 5", id: "16" }, 58 | { name: "Channel 6", id: "17" }, 59 | { name: "Channel 1", id: "18" }, 60 | { name: "Channel 2", id: "19" }, 61 | { name: "Channel 3", id: "20" }, 62 | { name: "Channel 4", id: "21" }, 63 | { name: "Channel 5", id: "22" }, 64 | { name: "Channel 6", id: "23" }, 65 | { name: "Channel 7", id: "24" }, 66 | { name: "Channel 8", id: "25" }, 67 | { name: "Channel 9", id: "26" }, 68 | { name: "Channel 10", id: "27" }, 69 | { name: "Channel 1", id: "28" }, 70 | { name: "Channel 2", id: "29" }, 71 | { name: "Channel 3", id: "30" }, 72 | { name: "Channel 4", id: "31" }, 73 | { name: "Channel 5", id: "32" }, 74 | { name: "Channel 6", id: "33" }, 75 | { name: "Channel 7", id: "34" }, 76 | { name: "Channel 8", id: "35" }, 77 | { name: "Channel 9", id: "36" }, 78 | { name: "Channel 101", id: "37" }, 79 | ]; 80 | const isOpen = state === "visible"; 81 | const isMessagesVisible = directMessagesVisible === "visible"; 82 | const isChannelsVisible = channelsVisible === "visible"; 83 | 84 | const toggleDirectMessagesVisibility = () => { 85 | setDirectMessagesVisible((value) => { 86 | return value === "visible" ? "invisible" : "visible"; 87 | }); 88 | }; 89 | const toggleChannelsVisibility = () => { 90 | setChannelsVisible((value) => { 91 | return value === "visible" ? "invisible" : "visible"; 92 | }); 93 | }; 94 | 95 | return ( 96 | <> 97 | 228 | 229 |

Logout

230 |

231 | Are you certain that you wish to log out? 232 |

233 |
234 | 235 | 236 |
237 |
238 | 239 | ); 240 | } 241 | 242 | export default SideBar; 243 | -------------------------------------------------------------------------------- /src/pages/channel/channel.tsx: -------------------------------------------------------------------------------- 1 | import { FaAngleDown } from "react-icons/fa"; 2 | import { AiOutlineClose, AiOutlineInfoCircle } from "react-icons/ai"; 3 | import FilledButton from "../../components/filled-button"; 4 | import EmojiPicker from "emoji-picker-react"; 5 | import { useEffect, useRef, useState } from "react"; 6 | import { Theme } from "emoji-picker-react"; 7 | import { GoSidebarCollapse, GoSidebarExpand, GoSmiley } from "react-icons/go"; 8 | import { IoSend } from "react-icons/io5"; 9 | import { BsHash } from "react-icons/bs"; 10 | import Modal from "../../components/modal/modal"; 11 | 12 | type Props = { 13 | state: string; 14 | toggleSideBar: any; 15 | }; 16 | 17 | function Channel({ state, toggleSideBar }: Props) { 18 | const [emojiModal, setEmojiModal] = useState<"visible" | "hidden">("hidden"); 19 | const [channelInfoModal, setChannelInfoModal] = useState< 20 | "visible" | "hidden" 21 | >("hidden"); 22 | const [input, setInput] = useState(""); 23 | const [searchInputFocus, setSearchInputFocus] = useState(false); 24 | const messageInputRef = useRef(null); 25 | const emojiButtonRef = useRef(null); 26 | const searchInputRef = useRef(null); 27 | 28 | const message = input.replaceAll(" ", ""); 29 | const isMessageEmpty = message === ""; 30 | const isEmojiModalOpen = emojiModal === "visible"; 31 | const isSideBarOpen = state === "visible"; 32 | 33 | //A function to toggle the emoji modal. 34 | const toggleEmojiModal = () => { 35 | setEmojiModal((value) => { 36 | const previousValue = value; 37 | if (previousValue === "hidden") { 38 | return "visible"; 39 | } else { 40 | return "hidden"; 41 | } 42 | }); 43 | }; 44 | 45 | useEffect(() => { 46 | const handleFocus = () => { 47 | setSearchInputFocus(true); 48 | }; 49 | const handleBlur = () => { 50 | setSearchInputFocus(false); 51 | }; 52 | 53 | if (searchInputRef.current) { 54 | searchInputRef.current.addEventListener("focus", handleFocus); 55 | searchInputRef.current.addEventListener("blur", handleBlur); 56 | } 57 | 58 | return () => { 59 | if (searchInputRef.current) { 60 | searchInputRef.current.removeEventListener("focus", handleFocus); 61 | searchInputRef.current.removeEventListener("blur", handleBlur); 62 | } 63 | }; 64 | }, []); 65 | 66 | // and when changes are detected on the emojiModal value. 67 | //This side effect auto focuses the message input. 68 | useEffect(() => { 69 | if (messageInputRef.current) { 70 | messageInputRef.current.focus(); 71 | //get the input value. 72 | const inputValue = messageInputRef.current.value; 73 | //Place the cursor at the end of any text inside the input. 74 | messageInputRef.current.selectionStart = inputValue.length; 75 | messageInputRef.current.selectionEnd = inputValue.length; 76 | } 77 | }, [emojiModal]); 78 | 79 | //Side effect that adds a keyboard event listener to the current window 80 | //and listens for the shortcut `ctrl + e` to open the emojiModal. 81 | //This side effect only runs on the initial render of the component. 82 | useEffect(() => { 83 | const handleEmojiShortcut = (event: KeyboardEvent) => { 84 | if ((event.ctrlKey || event.metaKey) && event.key === "e") { 85 | if (emojiButtonRef.current) { 86 | event.preventDefault(); 87 | emojiButtonRef.current.click(); 88 | } 89 | } 90 | }; 91 | window.addEventListener("keydown", handleEmojiShortcut); 92 | 93 | return () => { 94 | window.removeEventListener("keydown", handleEmojiShortcut); 95 | }; 96 | }, []); 97 | 98 | return ( 99 |
100 |
101 | 111 |
112 | 118 |
123 |
124 | 132 |
133 |
134 |
135 | 136 |
137 |

138 | Channel messages and files older than 60 days are concealed. 139 |

140 |

141 | Join Thread Socket Premium for extended file retention, ensuring 142 | your data remains accessible for an extended duration plus all the 143 | premium features of the Pro plan. 144 |

145 | 146 |
147 |
148 |
149 | 150 |
151 | 158 | { 162 | setInput(e.target.value); 163 | }} 164 | value={input} 165 | className="p-2 resize-none flex-1 bg-transparent caret-white text-white border-none outline-none" 166 | /> 167 | 176 |
177 |
{ 179 | if (e.target === e.currentTarget) { 180 | setEmojiModal("hidden"); 181 | } 182 | }} 183 | className={`${ 184 | isEmojiModalOpen ? "block" : "hidden" 185 | } absolute top-0 left-0 bottom-0 right-0`} 186 | > 187 |
188 | { 191 | setInput((value) => { 192 | return value + emoji.emoji; 193 | }); 194 | }} 195 | /> 196 |
197 |
198 | 199 |
200 |
201 | 202 |

Channel Name

203 |
204 | 210 |
211 |
212 |
213 |
214 |

Created By :

215 |

John Doe

216 |
217 |
218 |

Date Created :

219 |

2023-10-18 17:00

220 |
221 |
222 |

Total members :

223 |

167

224 |
225 |
226 |
227 | 230 | 236 |
237 |
238 | 241 |