├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.tsx
├── AuthPage
│ ├── LogInForm.tsx
│ ├── SignUpForm.tsx
│ ├── components
│ │ ├── Button.tsx
│ │ ├── Link.tsx
│ │ ├── PhotoInput.tsx
│ │ └── TextInput.tsx
│ └── index.tsx
├── ChatsPage
│ ├── ChatCard.tsx
│ ├── ChatHeader.tsx
│ ├── MessageForm.tsx
│ ├── Sidebar.tsx
│ ├── UserSearch.tsx
│ └── index.tsx
├── app.css
├── assets
│ ├── VisbyRoundCF-DemiBold.woff
│ ├── VisbyRoundCF-Heavy.woff
│ ├── VisbyRoundCF-Regular.woff
│ ├── adam.png
│ ├── bob.png
│ ├── callum.png
│ ├── daniel.png
│ └── valley.jpeg
├── functions
│ ├── constants.tsx
│ ├── context.tsx
│ ├── dates.tsx
│ ├── getOtherUser.tsx
│ └── isMobile.tsx
├── index.tsx
├── react-app-env.d.ts
└── theme.css
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pretty Chat React/Typescript
2 |
3 | This project is a pretty full-stack chat app built with React and Typesctipt.
4 |
5 | To learn how this project works, watch the following YouTube tutorial.
6 |
7 | ## Setup
8 |
9 | Go to [chatengine.io](https://chatengine.io) and create your own project. There you will get a Project ID and Private Key which are needed for user signup and authentication.
10 |
11 | ### `.env.local`
12 |
13 | Create a `.env.local` file at the top-level of your project, and replace the UUIDs with your own Project ID and Private Key from [chatengine.io](https://chatengine.io).
14 |
15 | ```
16 | REACT_APP_PROJECT_ID=12341234-1234-1234-1234-123412341234
17 | REACT_APP_PROJECT_KEY=abcdabcd-abcd-abcd-abcd-abcdabcdabcd
18 | ```
19 |
20 | This will link your new React App to the right Chat Engine project.
21 |
22 | ### `npm install`
23 |
24 | Build out your node modules by running `npm install`. Then you cn start the app.
25 |
26 | ### `npm run start`
27 |
28 | This will start the app. By default it will run on [localhost:3000](http://localhost:3000/)
29 |
30 | ### `npm run build`
31 |
32 | Builds the app for production to the `build` folder.\
33 | It correctly bundles React in production mode and optimizes the build for the best performance.
34 |
35 | The build is minified and the filenames include the hashes.\
36 | Your app is ready to be deployed!
37 |
38 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pretty-chat-react-typescript",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.14.1",
7 | "@testing-library/react": "^13.0.0",
8 | "@testing-library/user-event": "^13.2.1",
9 | "@types/jest": "^27.0.1",
10 | "@types/node": "^16.7.13",
11 | "@types/react": "^18.0.0",
12 | "@types/react-dom": "^18.0.0",
13 | "antd": "4.21.5",
14 | "axios": "^0.27.2",
15 | "react": "^18.2.0",
16 | "react-chat-engine-advanced": "0.1.21",
17 | "react-dom": "^18.2.0",
18 | "react-scripts": "5.0.1",
19 | "typescript": "^4.4.2",
20 | "web-vitals": "^2.1.0"
21 | },
22 | "scripts": {
23 | "start": "react-scripts start",
24 | "build": "react-scripts build",
25 | "test": "react-scripts test",
26 | "eject": "react-scripts eject"
27 | },
28 | "eslintConfig": {
29 | "extends": [
30 | "react-app",
31 | "react-app/jest"
32 | ]
33 | },
34 | "browserslist": {
35 | "production": [
36 | ">0.2%",
37 | "not dead",
38 | "not op_mini all"
39 | ],
40 | "development": [
41 | "last 1 chrome version",
42 | "last 1 firefox version",
43 | "last 1 safari version"
44 | ]
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alamorre/pretty-chat-react-typescript/80ac9a2d48bff8779020e8fc87b1dd17a4994642/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alamorre/pretty-chat-react-typescript/80ac9a2d48bff8779020e8fc87b1dd17a4994642/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alamorre/pretty-chat-react-typescript/80ac9a2d48bff8779020e8fc87b1dd17a4994642/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 |
3 | import { Context } from "./functions/context";
4 |
5 | import AuthPage from "./AuthPage";
6 | import ChatsPage from "./ChatsPage";
7 |
8 | import "./app.css";
9 |
10 | function App() {
11 | const { user } = useContext(Context);
12 |
13 | if (user) {
14 | return ;
15 | } else {
16 | return ;
17 | }
18 | }
19 |
20 | export default App;
21 |
--------------------------------------------------------------------------------
/src/AuthPage/LogInForm.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useContext } from "react";
2 |
3 | import axios from "axios";
4 |
5 | import TextInput from "./components/TextInput";
6 | import Button from "./components/Button";
7 | import Link from "./components/Link";
8 |
9 | import { Context } from "../functions/context";
10 | import { projectId } from "../functions/constants";
11 | import { PersonObject } from "react-chat-engine-advanced";
12 |
13 | interface LogInFormProps {
14 | onHasNoAccount: () => void;
15 | }
16 |
17 | const LogInForm = (props: LogInFormProps) => {
18 | // State
19 | const [email, setEmail] = useState("");
20 | const [password, setPassword] = useState("");
21 | // Hooks
22 | const { setUser } = useContext(Context);
23 |
24 | const onSubmit = (event: React.FormEvent) => {
25 | event.preventDefault();
26 |
27 | const headers = {
28 | "Project-ID": projectId,
29 | "User-Name": email,
30 | "User-Secret": password,
31 | };
32 |
33 | axios
34 | .get("https://api.chatengine.io/users/me/", {
35 | headers,
36 | })
37 | .then((r) => {
38 | if (r.status === 200) {
39 | const user: PersonObject = {
40 | first_name: r.data.first_name,
41 | last_name: r.data.last_name,
42 | email: email,
43 | username: email,
44 | secret: password,
45 | avatar: r.data.avatar,
46 | custom_json: {},
47 | is_online: true,
48 | };
49 | setUser(user);
50 | }
51 | })
52 | .catch((e) => console.log("Error", e));
53 | };
54 |
55 | return (
56 |
57 |
Welcome Back
58 |
59 |
60 | New here? props.onHasNoAccount()}>Sign Up
61 |
62 |
63 |
81 |
82 | );
83 | };
84 |
85 | export default LogInForm;
86 |
--------------------------------------------------------------------------------
/src/AuthPage/SignUpForm.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useState } from "react";
2 |
3 | import axios from "axios";
4 |
5 | import { PersonObject } from "react-chat-engine-advanced";
6 |
7 | import { useIsMobile } from "../functions/isMobile";
8 | import { Context } from "../functions/context";
9 | import { privateKey } from "../functions/constants";
10 |
11 | import TextInput from "./components/TextInput";
12 | import PhotoInput from "./components/PhotoInput";
13 | import Button from "./components/Button";
14 | import Link from "./components/Link";
15 |
16 | interface SignUpFormProps {
17 | onHasAccount: () => void;
18 | }
19 |
20 | const SignUpForm = (props: SignUpFormProps) => {
21 | // State
22 | const [firstName, setFirstName] = useState("");
23 | const [lastName, setLastName] = useState("");
24 | const [email, setEmail] = useState("");
25 | const [password, setPassword] = useState("");
26 | const [avatar, setAvatar] = useState(undefined);
27 | // Hooks
28 | const { setUser } = useContext(Context);
29 | const isMobile: boolean = useIsMobile();
30 |
31 | const onSubmit = (event: React.FormEvent) => {
32 | event.preventDefault();
33 |
34 | const userJson: PersonObject = {
35 | email: email,
36 | username: email,
37 | first_name: firstName,
38 | last_name: lastName,
39 | secret: password,
40 | avatar: null,
41 | custom_json: {},
42 | is_online: true,
43 | };
44 |
45 | let formData = new FormData();
46 | formData.append("email", email);
47 | formData.append("username", email);
48 | formData.append("first_name", firstName);
49 | formData.append("last_name", lastName);
50 | formData.append("secret", password);
51 | if (avatar) {
52 | formData.append("avatar", avatar, avatar.name);
53 | }
54 |
55 | const headers = { "Private-Key": privateKey };
56 |
57 | axios
58 | .post("https://api.chatengine.io/users/", formData, {
59 | headers,
60 | })
61 | .then((r) => {
62 | if (r.status === 201) {
63 | userJson.avatar = r.data.avatar;
64 | setUser(userJson);
65 | }
66 | })
67 | .catch((e) => console.log("Error", e));
68 | };
69 |
70 | return (
71 |
72 |
Create an account
73 |
74 |
75 | Already a member?{" "}
76 | props.onHasAccount()}>Log in
77 |
78 |
79 |
141 |
142 | );
143 | };
144 |
145 | export default SignUpForm;
146 |
--------------------------------------------------------------------------------
/src/AuthPage/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import { CSSProperties, ReactNode, useState } from "react";
2 |
3 | interface ButtonProps {
4 | children?: ReactNode;
5 | style?: CSSProperties;
6 | type?: string;
7 | }
8 |
9 | const Button = (props: ButtonProps) => {
10 | const [hovered, setHovered] = useState(false);
11 |
12 | return (
13 |
24 | );
25 | };
26 |
27 | const styles = {
28 | style: {
29 | width: "100%",
30 | height: "53px",
31 | color: "white",
32 | backgroundColor: "#fa541c",
33 | border: "none",
34 | outline: "none",
35 | borderRadius: "8px",
36 | fontFamily: "VisbyRoundCF-DemiBold",
37 | cursor: "pointer",
38 | transition: "all .44s ease",
39 | WebkitTransition: "all .44s ease",
40 | MozTransition: "all .44s ease",
41 | } as CSSProperties,
42 | hoverStyle: { filter: "brightness(145%)" } as CSSProperties,
43 | };
44 |
45 | export default Button;
46 |
--------------------------------------------------------------------------------
/src/AuthPage/components/Link.tsx:
--------------------------------------------------------------------------------
1 | import { CSSProperties, ReactNode, useState } from "react";
2 |
3 | interface LinkProps {
4 | children?: ReactNode;
5 | style?: CSSProperties;
6 | type?: string;
7 | onClick?: () => void;
8 | }
9 |
10 | const Link = (props: LinkProps) => {
11 | const [hovered, setHovered] = useState(false);
12 |
13 | return (
14 | setHovered(true)}
17 | onMouseLeave={() => setHovered(false)}
18 | style={{
19 | ...styles.style,
20 | ...(hovered && styles.hoverStyle),
21 | ...props.style,
22 | }}
23 | >
24 | {props.children}
25 |
26 | );
27 | };
28 |
29 | const styles = {
30 | style: {
31 | color: "#fa541c",
32 | cursor: "pointer",
33 | transition: "all .44s ease",
34 | WebkitTransition: "all .44s ease",
35 | MozTransition: "all .44s ease",
36 | } as CSSProperties,
37 | hoverStyle: {
38 | filter: "brightness(145%)",
39 | textDecoration: "underline",
40 | } as CSSProperties,
41 | };
42 |
43 | export default Link;
44 |
--------------------------------------------------------------------------------
/src/AuthPage/components/PhotoInput.tsx:
--------------------------------------------------------------------------------
1 | import { CSSProperties, ChangeEventHandler, useState } from "react";
2 |
3 | interface PhotoInputProps {
4 | label: string;
5 | id: string;
6 | name: string;
7 | placeholder?: string;
8 | style?: CSSProperties;
9 | onChange: ChangeEventHandler;
10 | }
11 |
12 | const PhotoInput = (props: PhotoInputProps) => {
13 | const [selectedImage, setSelectedImage] = useState(
14 | undefined
15 | );
16 |
17 | const onChange: ChangeEventHandler = (event) => {
18 | if (event.target.files !== null) {
19 | const image = event.target.files[0];
20 | setSelectedImage(image);
21 | props.onChange(event);
22 | }
23 | };
24 |
25 | return (
26 |
32 |
57 |
58 |
66 |
67 | );
68 | };
69 |
70 | const styles = {
71 | style: {
72 | position: "relative",
73 | display: "inline-block",
74 | width: "100%",
75 | paddingBottom: "12px",
76 | } as CSSProperties,
77 | uploadStyle: {
78 | color: "white",
79 | backgroundColor: "rgb(62, 64, 75)",
80 | display: "inline-block",
81 | height: "52px",
82 | width: "52px",
83 | borderRadius: "8px",
84 | textAlign: "center",
85 | fontSize: "30px",
86 | } as CSSProperties,
87 | labelStyle: {
88 | display: "inline-block",
89 | color: "rgb(175, 175, 175)",
90 | fontFamily: "VisbyRoundCF-DemiBold",
91 | fontSize: "14px",
92 | cursor: "pointer",
93 | position: "absolute",
94 | top: "18px",
95 | left: "66px",
96 | } as CSSProperties,
97 | inputStyle: {
98 | backgroundColor: "#3e404b",
99 | color: "white",
100 | fontFamily: "VisbyRoundCF-DemiBold",
101 | outline: "none",
102 | border: "none",
103 | borderRadius: "8px",
104 | padding: "24px 18px 12px 18px",
105 | width: "100%", // For the padding 18px + 18px
106 | } as CSSProperties,
107 | };
108 |
109 | export default PhotoInput;
110 |
--------------------------------------------------------------------------------
/src/AuthPage/components/TextInput.tsx:
--------------------------------------------------------------------------------
1 | import { CSSProperties, ChangeEventHandler } from "react";
2 |
3 | interface TextInputProps {
4 | label: string;
5 | name: string;
6 | type?: string;
7 | placeholder?: string;
8 | style?: CSSProperties;
9 | onChange: ChangeEventHandler;
10 | }
11 |
12 | const TextInput = (props: TextInputProps) => {
13 | return (
14 |
20 |
{props.label}
21 |
22 |
29 |
30 | );
31 | };
32 |
33 | const styles = {
34 | style: {
35 | position: "relative",
36 | display: "inline-block",
37 | width: "100%",
38 | paddingBottom: "12px",
39 | } as CSSProperties,
40 | labelStyle: {
41 | position: "absolute",
42 | top: "8px",
43 | left: "18px",
44 | fontSize: "11px",
45 | color: "rgb(175, 175, 175)",
46 | fontFamily: "VisbyRoundCF-DemiBold",
47 | width: "100px",
48 | } as CSSProperties,
49 | inputStyle: {
50 | backgroundColor: "#3e404b",
51 | color: "white",
52 | fontFamily: "VisbyRoundCF-DemiBold",
53 | outline: "none",
54 | border: "none",
55 | borderRadius: "8px",
56 | padding: "24px 18px 12px 18px",
57 | width: "100%", // For the padding 18px + 18px
58 | } as CSSProperties,
59 | };
60 |
61 | export default TextInput;
62 |
--------------------------------------------------------------------------------
/src/AuthPage/index.tsx:
--------------------------------------------------------------------------------
1 | import { CSSProperties, useState } from "react";
2 |
3 | import valley from "../assets/valley.jpeg";
4 | import SignUpForm from "./SignUpForm";
5 | import LogInForm from "./LogInForm";
6 |
7 | const AuthPage = () => {
8 | const [hasAccount, setHasAccount] = useState(false);
9 |
10 | const backgroundImage = {
11 | backgroundImage: `url(${valley})`, // Here due to variable
12 | } as CSSProperties;
13 |
14 | return (
15 |
16 |
17 |
18 |
Pretty
19 |
20 | {hasAccount ? (
21 |
setHasAccount(false)} />
22 | ) : (
23 | setHasAccount(true)} />
24 | )}
25 |
26 |
27 |
28 | );
29 | };
30 |
31 | const styles = {
32 | formContainerStyle: {
33 | width: "100%",
34 | maxWidth: "650px",
35 | padding: "36px 72px",
36 | } as CSSProperties,
37 | titleStyle: {
38 | fontSize: "24px",
39 | fontFamily: "VisbyRoundCF-Heavy",
40 | letterSpacing: "0.5px",
41 | color: "white",
42 | paddingBottom: "11vw",
43 | } as CSSProperties,
44 | };
45 |
46 | export default AuthPage;
47 |
--------------------------------------------------------------------------------
/src/ChatsPage/ChatCard.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ChatCard,
3 | ChatCardProps,
4 | ChatObject,
5 | } from "react-chat-engine-advanced";
6 |
7 | import { getOtherUser } from "../functions/getOtherUser";
8 |
9 | interface CustomChatCardProps extends ChatCardProps {
10 | username: string;
11 | isActive: boolean;
12 | onChatCardClick: (chatId: number) => void;
13 | chat?: ChatObject;
14 | }
15 |
16 | const CustomChatCard = (props: CustomChatCardProps) => {
17 | if (!props.chat) return ;
18 |
19 | const otherMember = getOtherUser(props.chat, props.username);
20 | const firstName = otherMember ? otherMember.first_name : "";
21 | const lastName = otherMember ? otherMember.last_name : "";
22 | const username = otherMember ? otherMember.username : "";
23 | const messageText = props.chat.last_message.text;
24 | const hasNotification =
25 | props.chat.last_message.sender_username !== props.username;
26 |
27 | return (
28 | props.chat && props.onChatCardClick(props.chat.id)}
48 | />
49 | );
50 | };
51 |
52 | export default CustomChatCard;
53 |
--------------------------------------------------------------------------------
/src/ChatsPage/ChatHeader.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ChatHeaderProps,
3 | ChatObject,
4 | PersonObject,
5 | Avatar,
6 | } from "react-chat-engine-advanced";
7 |
8 | import {
9 | PhoneFilled,
10 | DeleteFilled,
11 | PaperClipOutlined,
12 | LoadingOutlined,
13 | } from "@ant-design/icons";
14 |
15 | import axios from "axios";
16 |
17 | import { nowTimeStamp } from "../functions/dates";
18 | import { getOtherUser } from "../functions/getOtherUser";
19 | import { useIsMobile } from "../functions/isMobile";
20 |
21 | import { privateKey, projectId } from "../functions/constants";
22 | import { useState } from "react";
23 |
24 | interface CustomChatHeaderProps extends ChatHeaderProps {
25 | chat?: ChatObject;
26 | username: string;
27 | secret: string;
28 | }
29 |
30 | const ChatHeader = (props: CustomChatHeaderProps) => {
31 | // State
32 | const [isFilePickerLoading, setFilePickerLoading] = useState(false);
33 | const [isDeleteLoading, setDeleteLoading] = useState(false);
34 | // Hooks
35 | const isMobile: boolean = useIsMobile();
36 |
37 | // TODO: Show how TS recommends props.chat &&
38 | const otherMember: PersonObject | undefined =
39 | props.chat && getOtherUser(props.chat, props.username);
40 |
41 | const onFilesSelect: React.ChangeEventHandler = (e) => {
42 | if (!props.chat) return;
43 | setFilePickerLoading(true);
44 |
45 | const headers = {
46 | "Project-ID": projectId,
47 | "User-Name": props.username,
48 | "User-Secret": props.secret,
49 | };
50 |
51 | const formdata = new FormData();
52 | const filesArr = Array.from(e.target.files !== null ? e.target.files : []);
53 | filesArr.forEach((file) => formdata.append("attachments", file, file.name));
54 | formdata.append("created", nowTimeStamp());
55 | formdata.append("sender_username", props.username);
56 | formdata.append("custom_json", JSON.stringify({}));
57 |
58 | axios
59 | .post(
60 | `https://api.chatengine.io/chats/${props.chat.id}/messages/`,
61 | formdata,
62 | { headers }
63 | )
64 | .then((r) => setFilePickerLoading(false))
65 | .catch((e) => setFilePickerLoading(false));
66 | };
67 |
68 | const onDelete = () => {
69 | if (!props.chat) return;
70 | setDeleteLoading(true);
71 |
72 | const headers = { "Private-Key": privateKey };
73 | axios
74 | .delete(`https://api.chatengine.io/chats/${props.chat.id}/`, {
75 | headers,
76 | })
77 | .then(() => setDeleteLoading(false))
78 | .catch(() => setDeleteLoading(false));
79 | };
80 |
81 | return (
82 |
83 | {otherMember && (
84 |
85 |
91 |
92 |
93 |
94 | {otherMember.first_name} {otherMember.last_name}
95 |
96 |
97 | {otherMember.is_online ? "Online" : "Offline"}
98 |
99 |
100 |
101 |
102 |
118 |
119 |
120 |
121 | {isDeleteLoading ? (
122 |
123 | ) : (
124 |
onDelete()}
126 | className="ce-custom-header-icon"
127 | />
128 | )}
129 |
130 |
131 | )}
132 |
133 |
144 |
145 | );
146 | };
147 |
148 | export default ChatHeader;
149 |
--------------------------------------------------------------------------------
/src/ChatsPage/MessageForm.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useState } from "react";
2 |
3 | import { CaretUpFilled } from "@ant-design/icons";
4 |
5 | import { MessageObject, MessageFormProps } from "react-chat-engine-advanced";
6 |
7 | import { nowTimeStamp } from "../functions/dates";
8 | import { Context } from "../functions/context";
9 |
10 | const MessageForm = (props: MessageFormProps) => {
11 | const [text, setText] = useState("");
12 | const { user } = useContext(Context);
13 |
14 | const onSubmit = (event: React.FormEvent) => {
15 | event.preventDefault();
16 |
17 | if (text.trim().length === 0) {
18 | return;
19 | }
20 | if (!user || user.email === null) {
21 | return;
22 | }
23 |
24 | setText("");
25 |
26 | const message: MessageObject = {
27 | text: text,
28 | sender_username: user.email,
29 | created: nowTimeStamp(),
30 | custom_json: {},
31 | attachments: [],
32 | };
33 |
34 | props.onSubmit && props.onSubmit(message);
35 | };
36 |
37 | return (
38 |
50 | );
51 | };
52 |
53 | export default MessageForm;
54 |
--------------------------------------------------------------------------------
/src/ChatsPage/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 |
3 | import {
4 | LogoutOutlined,
5 | HomeFilled,
6 | MessageFilled,
7 | SettingFilled,
8 | } from "@ant-design/icons";
9 |
10 | import { Avatar } from "react-chat-engine-advanced";
11 |
12 | import { Context } from "../functions/context";
13 |
14 | const Sidebar = () => {
15 | const { user, setUser } = useContext(Context);
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
setUser(undefined)}
34 | className="signout-icon"
35 | />
36 |
37 | );
38 | };
39 |
40 | export default Sidebar;
41 |
--------------------------------------------------------------------------------
/src/ChatsPage/UserSearch.tsx:
--------------------------------------------------------------------------------
1 | import { AutoComplete, Input } from "antd";
2 | import type { SelectProps } from "antd/es/select";
3 | import { useState, useEffect, useRef } from "react";
4 |
5 | import { PersonObject, Avatar } from "react-chat-engine-advanced";
6 |
7 | import axios from "axios";
8 |
9 | import { privateKey, projectId } from "../functions/constants";
10 |
11 | interface CustomChatFormProps {
12 | username: string;
13 | secret: string;
14 | onSelect: (chatId: number) => void;
15 | }
16 |
17 | const UserSearch = (props: CustomChatFormProps) => {
18 | const didMountRef = useRef(false);
19 |
20 | const [loading, setLoading] = useState(false);
21 | const [query, setQuery] = useState("");
22 | const [users, setUsers] = useState([]);
23 | const [options, setOptions] = useState["options"]>([]);
24 |
25 | useEffect(() => {
26 | if (!didMountRef.current) {
27 | didMountRef.current = true;
28 | const headers = { "Private-Key": privateKey };
29 | axios
30 | .get("https://api.chatengine.io/users/", { headers })
31 | .then((r) => setUsers(r.data))
32 | .catch();
33 | }
34 | });
35 |
36 | const searchResult = (query: string) => {
37 | const foundUsers = users.filter(
38 | (user) =>
39 | JSON.stringify(user).toLowerCase().indexOf(query.toLowerCase()) !==
40 | -1 && user.username !== props.username
41 | );
42 |
43 | return foundUsers.map((user) => {
44 | return {
45 | value: user.username,
46 | label: (
47 |
53 |
54 |
55 |
56 |
57 |
58 | {user.first_name} {user.last_name}
59 |
60 | {user.username}
61 |
62 |
63 | ),
64 | };
65 | });
66 | };
67 | const handleSearch = (query: string) => {
68 | setOptions(query ? searchResult(query) : []);
69 | };
70 |
71 | const onSelect = (value: string) => {
72 | setLoading(true);
73 |
74 | const headers = {
75 | "Project-ID": projectId,
76 | "User-Name": props.username,
77 | "User-Secret": props.secret,
78 | };
79 | const data = {
80 | usernames: [props.username, value],
81 | };
82 | axios
83 | .put("https://api.chatengine.io/chats/", data, { headers })
84 | .then((r) => {
85 | props.onSelect(r.data.id);
86 | setLoading(false);
87 | setQuery("");
88 | })
89 | .catch(() => setLoading(false));
90 | };
91 |
92 | return (
93 |
94 |
102 | setQuery(e.target.value)}
108 | />
109 |
110 |
111 | );
112 | };
113 |
114 | export default UserSearch;
115 |
--------------------------------------------------------------------------------
/src/ChatsPage/index.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, CSSProperties } from "react";
2 |
3 | import valley from "../assets/valley.jpeg";
4 |
5 | import { useIsMobile } from "../functions/isMobile";
6 | import { Context } from "../functions/context";
7 |
8 | import {
9 | MultiChatWindow,
10 | MultiChatSocket,
11 | useMultiChatLogic,
12 | MessageFormProps,
13 | ChatCardProps,
14 | ChatHeaderProps,
15 | } from "react-chat-engine-advanced";
16 |
17 | import "../theme.css";
18 |
19 | import Sidebar from "./Sidebar";
20 | import MessageForm from "./MessageForm";
21 | import UserSearch from "./UserSearch";
22 | import ChatCard from "./ChatCard";
23 | import ChatHeader from "./ChatHeader";
24 |
25 | import { projectId } from "../functions/constants";
26 |
27 | const ChatsPage = () => {
28 | // Hooks
29 | const { user } = useContext(Context);
30 | const isMobile: boolean = useIsMobile();
31 |
32 | // Chat Engine Hooks
33 | const username: string = user ? user.username : "";
34 | const secret: string = user && user.secret !== null ? user.secret : "";
35 | const chatProps = useMultiChatLogic(projectId, username, secret);
36 |
37 | const backgroundImage = {
38 | backgroundImage: `url(${valley})`, // Here due to variable
39 | } as CSSProperties;
40 |
41 | return (
42 |
43 |
44 |
54 |
64 |
65 |
66 |
67 |
76 |
77 |
78 |
(
81 |
85 | chatProps.onChatCardClick(chatId)
86 | }
87 | />
88 | )}
89 | renderChatCard={(props: ChatCardProps) => (
90 |
100 | )}
101 | renderChatHeader={(props: ChatHeaderProps) => (
102 |
108 | )}
109 | renderMessageForm={(props: MessageFormProps) => (
110 |
111 | )}
112 | renderChatSettings={() => }
113 | style={{ height: "100%" }}
114 | />
115 |
116 |
117 |
118 |
119 | );
120 | };
121 |
122 | export default ChatsPage;
123 |
--------------------------------------------------------------------------------
/src/app.css:
--------------------------------------------------------------------------------
1 | @import url("https://cdnjs.cloudflare.com/ajax/libs/antd/4.21.5/antd.min.css");
2 |
3 | @font-face {
4 | /* TODO: Font Family https://www.dfonts.org/assets/visby-round-font-family/ */
5 | font-family: 'VisbyRoundCF-Regular';
6 | src: local('VisbyRoundCF-Regular'), url(./assets/VisbyRoundCF-Regular.woff) format('woff');
7 | font-weight: normal;
8 | }
9 |
10 | @font-face {
11 | /* TODO: Font Family https://www.dfonts.org/assets/visby-round-font-family/ */
12 | font-family: 'VisbyRoundCF-DemiBold';
13 | src: local('VisbyRoundCF-DemiBold'), url(./assets/VisbyRoundCF-DemiBold.woff) format('woff');
14 | font-weight: normal;
15 | }
16 |
17 | @font-face {
18 | /* TODO: Font Family https://www.dfonts.org/assets/visby-round-font-family/ */
19 | font-family: 'VisbyRoundCF-Heavy';
20 | src: local('VisbyRoundCF-Heavy'), url(./assets/VisbyRoundCF-Heavy.woff) format('woff');
21 | font-weight: normal;
22 | }
23 |
24 | body {
25 | font-family: 'VisbyRoundCF-Regular';
26 | margin: 0px;
27 | }
28 |
29 | .form-title {
30 | font-size: 42px;
31 | font-family: 'VisbyRoundCF-Heavy';
32 | letter-spacing: 0.5px;
33 | color: #e8e8e8;
34 | padding-bottom: 12px;
35 | }
36 |
37 | .form-subtitle {
38 | font-size: 18px;
39 | font-family: 'VisbyRoundCF-Regular';
40 | letter-spacing: 0.5px;
41 | color: #afafaf;
42 | padding-bottom: 24px;
43 | }
44 |
45 | .background-image {
46 | width: 100vw;
47 | height: 100vh;
48 | background-repeat: no-repeat;
49 | background-size: cover;
50 | }
51 |
52 | .background-gradient-dark {
53 | width: 100vw;
54 | height: 100%;
55 | overflow-y: scroll;
56 | background: linear-gradient(66deg, rgb(40,43,54) 0%, rgb(40,43,54) 50%, rgba(40,43,54,0.8) 100%);
57 | /* ^ Built with https://cssgradient.io/ */
58 | }
59 |
60 | .background-gradient-light {
61 | width: 100vw;
62 | height: 100%;
63 | overflow-y: scroll;
64 | background: linear-gradient(66deg, rgba(150, 157, 166, 0.9) 0%, rgba(150, 157, 166, 0.8) 50%, rgba(150,157,166,0.7) 100%);
65 | /* ^ Built with https://cssgradient.io/ */
66 | }
67 |
--------------------------------------------------------------------------------
/src/assets/VisbyRoundCF-DemiBold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alamorre/pretty-chat-react-typescript/80ac9a2d48bff8779020e8fc87b1dd17a4994642/src/assets/VisbyRoundCF-DemiBold.woff
--------------------------------------------------------------------------------
/src/assets/VisbyRoundCF-Heavy.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alamorre/pretty-chat-react-typescript/80ac9a2d48bff8779020e8fc87b1dd17a4994642/src/assets/VisbyRoundCF-Heavy.woff
--------------------------------------------------------------------------------
/src/assets/VisbyRoundCF-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alamorre/pretty-chat-react-typescript/80ac9a2d48bff8779020e8fc87b1dd17a4994642/src/assets/VisbyRoundCF-Regular.woff
--------------------------------------------------------------------------------
/src/assets/adam.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alamorre/pretty-chat-react-typescript/80ac9a2d48bff8779020e8fc87b1dd17a4994642/src/assets/adam.png
--------------------------------------------------------------------------------
/src/assets/bob.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alamorre/pretty-chat-react-typescript/80ac9a2d48bff8779020e8fc87b1dd17a4994642/src/assets/bob.png
--------------------------------------------------------------------------------
/src/assets/callum.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alamorre/pretty-chat-react-typescript/80ac9a2d48bff8779020e8fc87b1dd17a4994642/src/assets/callum.png
--------------------------------------------------------------------------------
/src/assets/daniel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alamorre/pretty-chat-react-typescript/80ac9a2d48bff8779020e8fc87b1dd17a4994642/src/assets/daniel.png
--------------------------------------------------------------------------------
/src/assets/valley.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alamorre/pretty-chat-react-typescript/80ac9a2d48bff8779020e8fc87b1dd17a4994642/src/assets/valley.jpeg
--------------------------------------------------------------------------------
/src/functions/constants.tsx:
--------------------------------------------------------------------------------
1 | export const projectId = process.env.REACT_APP_PROJECT_ID
2 | ? process.env.REACT_APP_PROJECT_ID
3 | : "";
4 |
5 | export const privateKey: string = process.env.REACT_APP_PROJECT_KEY
6 | ? process.env.REACT_APP_PROJECT_KEY
7 | : "";
8 |
--------------------------------------------------------------------------------
/src/functions/context.tsx:
--------------------------------------------------------------------------------
1 | import { useState, createContext, ReactNode } from "react";
2 | import { PersonObject } from "react-chat-engine-advanced";
3 |
4 | export interface ContextInterface {
5 | user: PersonObject | undefined;
6 | setUser: (u: PersonObject | undefined) => void;
7 | }
8 |
9 | interface ProviderInterface {
10 | children: ReactNode;
11 | }
12 |
13 | export const Context = createContext({
14 | user: undefined,
15 | setUser: () => {},
16 | });
17 |
18 | export const ContextProvider = (props: ProviderInterface) => {
19 | const [user, setUser] = useState(undefined);
20 | const value = { user, setUser };
21 |
22 | return {props.children};
23 | };
24 |
--------------------------------------------------------------------------------
/src/functions/dates.tsx:
--------------------------------------------------------------------------------
1 | export const nowTimeStamp = () => {
2 | return new Date()
3 | .toISOString()
4 | .replace("T", " ")
5 | .replace("Z", `${Math.floor(Math.random() * 1000)}+00:00`);
6 | };
7 |
--------------------------------------------------------------------------------
/src/functions/getOtherUser.tsx:
--------------------------------------------------------------------------------
1 | import { ChatObject, PersonObject } from "react-chat-engine-advanced";
2 |
3 | export const getOtherUser = (
4 | chat: ChatObject,
5 | username: string
6 | ): PersonObject | undefined => {
7 | const otherMember = chat.people.find(
8 | (member) => member.person.username !== username
9 | );
10 | return otherMember?.person;
11 | };
12 |
--------------------------------------------------------------------------------
/src/functions/isMobile.tsx:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect, useState } from "react";
2 |
3 | // TODO: Copy paste this: https://www.codegrepper.com/code-examples/javascript/react+get+window+width+on+resize
4 | export const useIsMobile = () => {
5 | const [size, setSize] = useState([0, 0]);
6 | useLayoutEffect(() => {
7 | function updateSize() {
8 | setSize([window.innerWidth, window.innerHeight]);
9 | }
10 | window.addEventListener("resize", updateSize);
11 | updateSize();
12 | return () => window.removeEventListener("resize", updateSize);
13 | }, []);
14 | return size[0] < 820;
15 | };
16 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from "react-dom/client";
2 | import App from "./App";
3 |
4 | import "./assets/VisbyRoundCF-Regular.woff";
5 |
6 | import { ContextProvider } from "./functions/context";
7 |
8 | const root = ReactDOM.createRoot(
9 | document.getElementById("root") as HTMLElement
10 | );
11 | root.render(
12 |
13 |
14 |
15 | );
16 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/theme.css:
--------------------------------------------------------------------------------
1 | .ce-chat-list { background-color: rgb(40,43,54) !important; }
2 | .ce-chat-form { background-color: rgb(40,43,54) !important; padding-bottom: 14px !important; }
3 | .ce-chat-form-title { color: white !important; font-family: 'VisbyRoundCF-DemiBold' !important; }
4 | .ce-default-button { border: none !important; background-color: rgb(40,43,54) !important; color: white !important; }
5 | .ce-text-input { background-color: rgb(40,43,54) !important; color: white !important; font-family: 'VisbyRoundCF-DemiBold' !important; border: 2px solid #fa541c !important; border-radius: 8px !important; }
6 | .ce-text-input::placeholder { color: white !important; }
7 | .ce-chat-card { border: 1px solid #3e404b !important; background-color: #3e404b !important; margin: 10px 12px !important; height: 68px !important; }
8 | .ce-chat-card:hover { border: 1px solid #1890ff !important; box-shadow: rgb(24 144 255 / 35%) 0px 2px 7px !important; }
9 | .ce-chat-card-loading { height: 10px !important; }
10 | .ce-chat-card-title-loading { top: 16px !important; }
11 | .ce-active-chat-card { border: 1px solid #1890ff !important; background-color: #1890ff !important; box-shadow: rgb(24 144 255 / 35%) 0px 2px 7px !important; color: white !important; }
12 | .ce-chat-card-title { color: white !important; font-family: 'VisbyRoundCF-DemiBold' !important; }
13 | .ce-chat-card-subtitle { font-family: 'VisbyRoundCF-DemiBold' !important; font-size: 12px !important; bottom: 16px !important; width: calc(70% - 44px) !important; color: #c5c5c5 !important; }
14 | .ce-chat-card-time-stamp { font-family: 'VisbyRoundCF-DemiBold' !important; font-size: 12px !important; bottom: 16px !important; }
15 | .ce-chat-card-unread { top: calc((68px - 12px) / 2) !important; }
16 | .ce-avatar-status { border: 2px solid rgb(40,43,54) !important; width: 10px !important; height: 10px !important; }
17 | .ce-chat-card-avatar { top: 12px !important; }
18 | .ce-chat-feed-column { border: none !important; }
19 | .ce-chat-feed { background-color: rgb(40,43,54) !important; }
20 | .ce-message-list { margin-top: 24px !important; margin-left: 12px !important; margin-right: 12px !important; padding: 0px 3.3vw !important; background: linear-gradient(0deg, rgba(62,64,75,1) 0%, rgba(62,64,75,1) 75%, rgba(40,43,54,1) 100%); border-radius: 8px 8px 0px 0px !important; height: calc((100% - 85px) - 72px - 24px - 12px) !important; }
21 | .ce-message-date-text { font-family: 'VisbyRoundCF-DemiBold' !important; color: rgb(153, 153, 153) !important; font-size: 14px !important; letter-spacing: -1px; }
22 | .ce-my-message-body { font-family: 'VisbyRoundCF-Regular' !important; font-size: 12px !important; padding: 15px !important; }
23 | .ce-my-message-timestamp { font-family: 'VisbyRoundCF-DemiBold' !important; font-size: 12px !important; padding: 15px !important; margin-right: 0px !important; letter-spacing: -1px; }
24 |
25 | .ce-their-message-body { font-family: 'VisbyRoundCF-Regular' !important; font-size: 12px !important; padding: 15px !important; background-color: #434756 !important; color: white !important; }
26 | .ce-their-message-timestamp { font-family: 'VisbyRoundCF-DemiBold' !important; font-size: 12px !important; padding: 15px !important; margin-left: 0px !important; letter-spacing: -1px; }
27 |
28 | .ce-their-message-timestamp { color: rgb(241, 240, 240) !important; letter-spacing: -1px; }
29 | .ce-their-message-sender-username { color: #999 !important; }
30 | .ce-message-file { background-color: #434758 !important; color: #c5c5c5 !important; border-radius: 8px !important; }
31 | .ce-message-image { background-color: #434758 !important; color: #c5c5c5 !important; border-radius: 8px !important; padding: 0px !important; max-width: 124px !important; max-height: 124px !important; }
32 |
33 | .ce-mobile-chat-list-button { top: 32px !important; left: 0px !important; }
34 | .ce-mobile-chat-settings-button { display: none !important; }
35 |
36 | .ce-custom-chat-header { display: inline-block; position: relative; width: 100%; height: 86px; }
37 | .ce-custom-header-text { display: inline-block; max-width: 50%; padding-left: 14px; position: relative; top: 21px; }
38 | .ce-custom-header-title { color: white; font-size: 13px; font-family: 'VisbyRoundCF-DemiBold'; }
39 | .ce-custom-header-subtitle { color: rgb(153, 153, 153); font-size: 11px; }
40 |
41 | .ce-custom-header-icon-wrapper { display: inline-block; max-width: 50%; position: relative; top: 36px; float: right; }
42 | .ce-custom-header-icon { margin-right: 12px; cursor: pointer; color: rgb(153, 153, 153) !important; transition: all 0.66s ease; }
43 | .ce-custom-header-icon:hover { color: rgb(24, 144, 255) !important; }
44 |
45 | .ce-custom-message-form { position: relative; height: 68px; margin-left: 12px; margin-right: 12px; width: calc(100% - 12px - 12px); border-radius: 0px 0px 8px 8px; background-color: #3e404b; }
46 | .ce-custom-message-input { position: absolute; top: 12px; left: 3.3vw; width: calc(100% - 3.3vw - 3.3vw - 14px - 15px - 15px); box-shadow: rgba(24, 144, 255, 0.35) 0px 2px 7px; border: 1px solid rgb(24, 144, 255); outline: none; background-color: #434756; color: white; font-size: 12px; padding: 0px 15px; font-family: 'VisbyRoundCF-DemiBold'; height: 36px; border-radius: 8px; transition: all .44s ease; }
47 | .ce-custom-message-input:focus { box-shadow: rgba(64, 169, 255, 0.35) 0px 2px 7px; border: 1px solid #40a9ff; }
48 | .ce-custom-message-input::placeholder { color: #e1e1e1; }
49 | .ce-custom-send-button { cursor: pointer; background-color: rgb(24, 144, 255); border: 1px solid rgb(24, 144, 255); width: 36px; height: 36px; border-radius: 8px; color: white; box-shadow: rgba(24, 144, 255, 0.35) 0px 5px 15px; position: absolute; top: 12px; right: 3.3vw; transition: all .44s ease; }
50 | .ce-custom-send-button:hover { background-color: #40a9ff; }
51 |
52 | .ce-sidebar-menu { position: absolute; top: 30vh; }
53 | .ce-sidebar-icon { width: 6vw; padding-top: 12px; padding-bottom: 12px; font-size: 16px; color: rgb(153, 153, 153) !important; }
54 | .ce-sidebar-icon-active { color: rgb(24, 144, 255) !important; border-left: 2px solid rgb(24, 144, 255); }
55 | .sidebar-avatar { position: absolute !important; bottom: 66px; left: calc(50% - 22px); border: 1px solid rgb(24, 144, 255); box-shadow: rgb(24 144 255 / 35%) 0px 2px 7px; }
56 | .signout-icon { cursor: pointer; color: rgb(153, 153, 153) !important; transition: all 0.66s ease; font-size: 18px; position: absolute; bottom: 24px; left: calc(50% - 9px); }
57 | .signout-icon:hover { color: #1890ff !important; }
58 |
59 | .ce-chat-form-autocomplete { width: calc(100% - 12px - 12px) !important; margin: 0px 12px !important; padding-top: 28px !important; padding-bottom: 32px !important; }
60 | .ant-input-lg { background-color: rgb(40,43,54) !important; outline: none !important; border: 1px solid rgb(40,43,54) !important; color: white !important; border-radius: 8px 0px 0px 8px !important; }
61 | .ant-input-lg::placeholder { color: white !important; font-family: 'VisbyRoundCF-DemiBold' !important; padding-top: 12px !important; }
62 | .ant-input-search-button { background-color: rgb(40,43,54) !important; border: none !important; outline: none !important; margin-left: 3px !important; border-radius: 0px 8px 8px 0px !important; }
63 | .ant-input-search-button:hover { background-color: rgb(40,43,54) !important; }
64 | .ant-input-group-addon { background-color: rgb(40,43,54) !important; }
65 |
66 | .ce-empty-settings { background-color: #282b36 !important; width: 3vw; height: 100vh; }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------