├── 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 |
21 | {title}
22 |
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 |
21 | {title}
22 |
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 | ,
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 |
24 |
25 | {icon}
26 |
27 |
28 |
{title}
29 |
{subtitle}
30 |
31 |
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 | 
9 | 
10 | 
11 | 
12 | 
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 |
113 |
114 |
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 | alert("Hello World")}
126 | disabled={isMessageEmpty}
127 | className={`p-2 ${
128 | isMessageEmpty ? "bg-black-50" : "bg-green-200"
129 | } w-fit rounded`}
130 | >
131 |
132 |
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 |
102 |
103 |
104 |
105 |
Thread Socket
106 |
107 |
108 |
109 |
110 |
111 |
112 | Upgrade Plan
113 |
114 |
115 |
116 |
117 |
118 |
119 |
{
121 | return isActive
122 | ? "flex flex-row items-center gap-2 p-1 bg-blue-400 rounded"
123 | : "flex flex-row items-center gap-2 p-1 rounded hover:bg-black-50";
124 | }}
125 | to="/"
126 | >
127 | {({ isActive }) => {
128 | return (
129 | <>
130 |
134 |
139 | Home
140 |
141 | >
142 | );
143 | }}
144 |
145 |
146 |
147 |
148 |
149 |
153 | {isMessagesVisible ? (
154 |
155 | ) : (
156 |
157 | )}
158 |
159 |
Direct Messages
160 |
161 |
164 |
165 |
170 | {messages.map((message, index) => {
171 | return (
172 |
177 | );
178 | })}
179 |
180 |
181 |
182 |
183 |
184 |
188 | {isChannelsVisible ? (
189 |
190 | ) : (
191 |
192 | )}
193 |
194 |
Channels
195 |
196 |
199 |
200 |
205 | {channels.map((channel, index) => {
206 | return (
207 |
212 | );
213 | })}
214 |
215 |
216 |
217 |
218 |
222 |
223 | Logout
224 |
225 |
226 |
227 |
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 |
105 | {isSideBarOpen ? (
106 |
107 | ) : (
108 |
109 | )}
110 |
111 |
124 |
setChannelInfoModal("visible")}
126 | className="flex items-center gap-1 p-1 rounded cursor-pointer hover:bg-black-50"
127 | >
128 |
129 | Channel name
130 |
131 |
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 |
156 |
157 |
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 | alert("Hello World")}
169 | disabled={isMessageEmpty}
170 | className={`p-2 ${
171 | isMessageEmpty ? "bg-black-50" : "bg-green-200"
172 | } w-fit rounded`}
173 | >
174 |
175 |
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 |
setChannelInfoModal("hidden")}
206 | className="rounded cursor-pointer hover:bg-black-50 p-1"
207 | >
208 |
209 |
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 |
228 | Channel Topic
229 |
230 |
236 |
237 |
238 |
239 | Channel Description
240 |
241 |
247 |
248 |
249 | Save Changes
250 |
251 |
252 |
253 |
254 | );
255 | }
256 |
257 | export default Channel;
258 |
--------------------------------------------------------------------------------
/src/pages/home/home.tsx:
--------------------------------------------------------------------------------
1 | import { FiEdit } from "react-icons/fi";
2 | import { LuKeyboard } from "react-icons/lu";
3 | import { IoIosHelpCircleOutline, IoMdAdd } from "react-icons/io";
4 | import { GoSidebarCollapse, GoSidebarExpand } from "react-icons/go";
5 | import { BiSearch } from "react-icons/bi";
6 | import { ChangeEvent, useEffect, useRef, useState } from "react";
7 | import SearchModal from "./search-modal";
8 | import { useNavigate } from "react-router-dom";
9 | import Modal from "../../components/modal/modal";
10 | import { AiOutlineClose } from "react-icons/ai";
11 | import users from "../../static-data";
12 | import { BsHash } from "react-icons/bs";
13 |
14 | import ChatIcon from "../../assets/svg/chat.svg";
15 | import ClockIcon from "../../assets/svg/clock.svg";
16 | import GiftIcon from "../../assets/svg/gift.svg";
17 | import AnnouncementIcon from "../../assets/svg/announce.svg";
18 |
19 | type Props = {
20 | state: string;
21 | toggleSideBar: any;
22 | };
23 |
24 | function Home({ state, toggleSideBar }: Props) {
25 | const navigate = useNavigate();
26 | const [searchModal, setSearchModal] = useState<"visible" | "hidden">(
27 | "hidden"
28 | );
29 | const [directMessageModal, setDirectMessageModal] = useState<
30 | "visible" | "hidden"
31 | >("hidden");
32 | const [createChannelModal, setCreateChannelModal] = useState<
33 | "visible" | "hidden"
34 | >("hidden");
35 | const [createChannelPage, setCreateChannelPage] = useState<
36 | "first" | "second"
37 | >("first");
38 | const [supportModal, setSupportModal] = useState<"visible" | "hidden">(
39 | "hidden"
40 | );
41 | const [directMessageSearch, setDirectMessageSearch] = useState("");
42 | const [channelName, setChannelName] = useState("");
43 | const [channelDescription, setChannelDescription] = useState("");
44 | const [channelExposure, setChannelExposure] = useState("");
45 |
46 | const name = channelName.trim().replaceAll(" ", "");
47 | const description = channelDescription.trim().replaceAll(" ", "");
48 |
49 | const isSideBarOpen = state === "visible";
50 | const isSearchModalOpen = searchModal === "visible";
51 | const isDirectMessageModalOpen = directMessageModal === "visible";
52 | const isCreateChannelModalOpen = createChannelModal === "visible";
53 | const isSupportModalOpen = supportModal === "visible";
54 |
55 | const helpButtonRef = useRef(null);
56 | const searchButtonRef = useRef(null);
57 | const sidebarButtonRef = useRef(null);
58 | const directMessageButtonRef = useRef(null);
59 | const createChannelButtonRef = useRef(null);
60 |
61 | const toggleSupportModal = () => {
62 | setSupportModal((value) => {
63 | const previousValue = value;
64 | if (previousValue === "hidden") {
65 | return "visible";
66 | } else {
67 | return "hidden";
68 | }
69 | });
70 | };
71 | const toggleSearchModal = () => {
72 | setSearchModal((value) => {
73 | const previousValue = value;
74 | if (previousValue === "hidden") {
75 | return "visible";
76 | } else {
77 | return "hidden";
78 | }
79 | });
80 | };
81 |
82 | //Side effect that adds `ctrl + h` shortcut to the current window.
83 | //This shortcut is for opening and closing Help and Support Modal.
84 | useEffect(() => {
85 | const handleSupportShortcut = (e: KeyboardEvent) => {
86 | if (
87 | !isSearchModalOpen &&
88 | !isDirectMessageModalOpen &&
89 | !isCreateChannelModalOpen
90 | ) {
91 | if ((e.ctrlKey || e.metaKey) && e.key === "h") {
92 | if (helpButtonRef.current) {
93 | e.preventDefault();
94 | helpButtonRef.current.click();
95 | }
96 | }
97 | }
98 | };
99 | window.addEventListener("keydown", handleSupportShortcut);
100 |
101 | return () => {
102 | window.removeEventListener("keydown", handleSupportShortcut);
103 | };
104 | }, [searchModal, directMessageModal, createChannelModal]);
105 |
106 | //Side effect that adds `ctrl + s` shortcut to the current window.
107 | //This shortcut is for opening and closing Search people and channels Modal.
108 | useEffect(() => {
109 | const handleSearchShortcut = (e: KeyboardEvent) => {
110 | if (
111 | !isSupportModalOpen &&
112 | !isCreateChannelModalOpen &&
113 | !isDirectMessageModalOpen
114 | ) {
115 | if ((e.ctrlKey || e.metaKey) && e.key === "s") {
116 | if (searchButtonRef.current) {
117 | e.preventDefault();
118 | searchButtonRef.current.click();
119 | }
120 | }
121 | }
122 | };
123 | window.addEventListener("keydown", handleSearchShortcut);
124 |
125 | return () => {
126 | window.removeEventListener("keydown", handleSearchShortcut);
127 | };
128 | }, [supportModal, createChannelModal, directMessageModal]);
129 |
130 | //Side effect that adds `ctrl + alt + d` shortcut to the current window.
131 | //This shortcut is for opening and closing Sidebar.
132 | useEffect(() => {
133 | const handleSideBarShortcut = (e: KeyboardEvent) => {
134 | if ((e.ctrlKey || e.metaKey) && e.altKey && e.key === "d") {
135 | if (sidebarButtonRef.current) {
136 | e.preventDefault();
137 | sidebarButtonRef.current.click();
138 | }
139 | }
140 | };
141 | window.addEventListener("keydown", handleSideBarShortcut);
142 |
143 | return () => {
144 | window.removeEventListener("keydown", handleSideBarShortcut);
145 | };
146 | }, []);
147 |
148 | //Side effect that adds `ctrl + alt + m` shortcut to the current window.
149 | //This shortcut is for opening Start a direct message modal.
150 | useEffect(() => {
151 | const handleDirectMessageShortcut = (e: KeyboardEvent) => {
152 | if (
153 | !isSupportModalOpen &&
154 | !isSearchModalOpen &&
155 | !isCreateChannelModalOpen
156 | ) {
157 | if (e.altKey && (e.ctrlKey || e.metaKey) && e.key === "m") {
158 | if (directMessageButtonRef.current) {
159 | e.preventDefault();
160 | directMessageButtonRef.current.click();
161 | }
162 | }
163 | }
164 | };
165 | window.addEventListener("keydown", handleDirectMessageShortcut);
166 |
167 | return () => {
168 | window.removeEventListener("keydown", handleDirectMessageShortcut);
169 | };
170 | }, [supportModal, searchModal, createChannelModal]);
171 |
172 | //Side effect that adds `ctrl + alt + c` shortcut to the current window.
173 | //This shortcut is for opening Start a direct message modal.
174 | useEffect(() => {
175 | const handleCreateChannelShortcut = (e: KeyboardEvent) => {
176 | if (
177 | !isSupportModalOpen &&
178 | !isSearchModalOpen &&
179 | !isDirectMessageModalOpen
180 | ) {
181 | if (e.altKey && (e.ctrlKey || e.metaKey) && e.key === "c") {
182 | if (createChannelButtonRef.current) {
183 | e.preventDefault();
184 | createChannelButtonRef.current.click();
185 | }
186 | }
187 | }
188 | };
189 | window.addEventListener("keydown", handleCreateChannelShortcut);
190 |
191 | return () => {
192 | window.removeEventListener("keydown", handleCreateChannelShortcut);
193 | };
194 | }, [supportModal, searchModal, directMessageModal]);
195 |
196 | return (
197 |
198 |
199 |
204 | {isSideBarOpen ? (
205 |
206 | ) : (
207 |
208 | )}
209 |
210 |
215 | Search people, channels
216 |
217 |
218 |
223 |
224 |
225 |
226 |
227 |
228 |
229 | Welcome to ThreadSocket: Where lightning-fast, secure, and real-time
230 | communication comes together in one powerful app. Stay connected
231 | like never before.
232 |
233 |
234 |
235 |
236 |
setDirectMessageModal("visible")}
239 | className="bg-black-800 w-96 flex flex-row items-center gap-2 p-3 rounded border border-gray-300 hover:bg-black-50"
240 | >
241 |
242 |
243 |
244 |
245 |
246 | Start a direct message
247 |
248 |
249 | Talk one-on-one with anyone
250 |
251 |
252 |
253 |
setCreateChannelModal("visible")}
256 | className="bg-black-800 w-96 flex flex-row items-center gap-2 p-3 rounded border border-gray-300 hover:bg-black-50"
257 | >
258 |
259 |
260 |
261 |
262 |
263 | Create a channel
264 |
265 |
266 | Connect Globally, Conversations Locally
267 |
268 |
269 |
270 |
271 |
272 | {
273 | //Direct Message Modal
274 | }
275 |
276 |
277 |
Direct Message
278 |
setDirectMessageModal("hidden")}
280 | className="rounded cursor-pointer hover:bg-black-50 p-1"
281 | >
282 |
283 |
284 |
285 | ) => {
287 | setDirectMessageSearch(e.target.value);
288 | }}
289 | placeholder="Search people"
290 | type="text"
291 | className="flex-1 placeholder:text-gray-300 caret-white text-white p-1 mt-3 bg-transparent border border-gray-300 rounded w-full focus:outline-2 focus:outline-green-200"
292 | />
293 |
314 |
315 | {
316 | //Create Channel Modal
317 | }
318 |
319 |
320 |
Create a Channel
321 |
{
323 | setChannelName("");
324 | setChannelDescription("");
325 | setChannelExposure("");
326 | setCreateChannelPage("first");
327 | setCreateChannelModal("hidden");
328 | }}
329 | className="rounded cursor-pointer hover:bg-black-50 p-1"
330 | >
331 |
332 |
333 |
334 | {createChannelPage === "first" ? (
335 |
336 |
337 |
) => {
340 | setChannelName(e.target.value);
341 | }}
342 | placeholder="Channel name. e.g. my-team"
343 | type="text"
344 | className="flex-1 caret-white text-white p-1 mt-3 bg-transparent border border-gray-300 rounded w-full placeholder:text-gray-300 focus:outline-2 focus:outline-green-200"
345 | />
346 |
347 | Channels are where conversations happen around a topic. Use a
348 | name that is easy to find and understand.
349 |
350 |
351 |
367 |
368 |
Step 1 of 2
369 |
setCreateChannelPage("second")}
371 | disabled={name === "" || description === ""}
372 | className={`${
373 | name !== "" && description !== ""
374 | ? "text-white"
375 | : "text-gray-300 "
376 | } ${
377 | name !== "" && description !== ""
378 | ? "bg-green-200"
379 | : "bg-black-50"
380 | } py-2 px-4 rounded font-bold`}
381 | >
382 | Next
383 |
384 |
385 |
386 | ) : createChannelPage === "second" ? (
387 |
388 |
389 |
390 |
{channelName}
391 |
392 |
393 |
Exposure
394 |
395 | {
397 | setChannelExposure("public");
398 | }}
399 | name="visibility"
400 | id="public"
401 | type="radio"
402 | className="accent-green-200"
403 | />
404 |
405 | Public - Open to anyone in the ThreadSocket.
406 |
407 |
408 |
409 | {
411 | setChannelExposure("private");
412 | }}
413 | name="visibility"
414 | className="accent-green-200"
415 | id="private"
416 | type="radio"
417 | />
418 |
419 | Private - Accessible only to specific individuals.
420 |
421 |
422 |
423 |
Step 2 of 2
424 |
425 | {
427 | setChannelExposure("");
428 | setCreateChannelPage("first");
429 | }}
430 | className="py-2 px-4 font-bold text-white border border-gray-300 rounded hover:bg-black-50"
431 | >
432 | Back
433 |
434 | alert("Channel Created")}
436 | disabled={channelExposure === ""}
437 | className={`${
438 | channelExposure !== "" ? "text-white" : "text-gray-300 "
439 | } ${
440 | channelExposure !== "" ? "bg-green-200" : "bg-black-50"
441 | } py-2 px-4 rounded font-bold`}
442 | >
443 | Create
444 |
445 |
446 |
447 |
448 |
449 | ) : null}
450 |
451 | {
452 | //Support Modal
453 | }
454 |
{
456 | if (e.target === e.currentTarget) {
457 | setSupportModal("hidden");
458 | }
459 | }}
460 | backdrop="transparent"
461 | className="absolute bottom-3 right-3 rounded-md w-96 h-1/2 bg-black-800 overflow-y-auto"
462 | state={supportModal}
463 | >
464 |
465 |
Help and Support
466 |
{
468 | setSupportModal("hidden");
469 | }}
470 | className="rounded cursor-pointer hover:bg-black-50 p-1"
471 | >
472 |
473 |
474 |
475 |
476 |
477 |
478 |
Explore help topics
479 |
480 |
481 |
482 | Understand Direct
483 | messages.
484 |
485 |
486 |
487 | Set a reminder.
488 |
489 |
490 |
491 | Special Announcements
492 |
493 |
494 |
495 | What's new in Thread Socket
496 |
497 |
498 |
499 |
500 |
Keyboard shortcuts
501 |
502 |
503 |
504 |
Toggle Help an Support
505 |
506 |
507 | Ctrl
508 |
509 |
510 | H
511 |
512 |
513 |
514 |
515 |
Search people , channels
516 |
517 |
518 | Ctrl
519 |
520 |
521 | S
522 |
523 |
524 |
525 |
526 |
Toggle SideBar
527 |
528 |
529 | Ctrl
530 |
531 |
532 | Alt
533 |
534 |
535 | D
536 |
537 |
538 |
539 |
540 |
Direct Message
541 |
542 |
543 | Ctrl
544 |
545 |
546 | Alt
547 |
548 |
549 | M
550 |
551 |
552 |
553 |
554 |
Create a Channel
555 |
556 |
557 | Ctrl
558 |
559 |
560 | Alt
561 |
562 |
563 | C
564 |
565 |
566 |
567 |
568 |
569 |
570 |
571 | );
572 | }
573 |
574 | export default Home;
575 |
--------------------------------------------------------------------------------