community: update streaming-subscriptions-chat ui

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6082
Co-authored-by: arjunyel <11153289+arjunyel@users.noreply.github.com>
Co-authored-by: Praveen Durairaju <14110316+praveenweb@users.noreply.github.com>
GitOrigin-RevId: 48a02077900571686529c52e835569ad9dbc6b1c
This commit is contained in:
Jigyasu Arya 2022-10-04 19:45:32 +05:30 committed by hasura-bot
parent 31c94f3180
commit bf991be955
20 changed files with 1521 additions and 1644 deletions

View File

@ -7,7 +7,7 @@ services:
environment:
POSTGRES_PASSWORD: postgrespassword
graphql-engine:
image: hasura/graphql-engine:v2.10.1.cli-migrations-v3
image: hasura/graphql-engine:v2.12.0.cli-migrations-v3
ports:
- "8080:8080"
depends_on:

File diff suppressed because it is too large Load Diff

View File

@ -8,10 +8,13 @@
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.4.3",
"date-fns": "^2.29.2",
"graphql": "^16.6.0",
"graphql-ws": "^5.10.0",
"project-name-generator": "^2.1.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"styled-components": "^5.3.5",
"web-vitals": "^2.1.4"
},
"scripts": {
@ -20,9 +23,6 @@
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"devDependencies": {
"eslint-plugin-graphql": "^4.0.0"
},
"eslintConfig": {
"extends": [
"react-app",

View File

@ -1,20 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000" />
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link href="https://afeld.github.io/emoji-css/emoji.css" rel="stylesheet">
<link href="https://use.fontawesome.com/releases/v5.0.7/css/all.css" rel="stylesheet" crossorigin="anonymous">
<link
rel="stylesheet"
href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
crossorigin="anonymous"
/>
<link href="https://afeld.github.io/emoji-css/emoji.css" rel="stylesheet" />
<link
href="https://use.fontawesome.com/releases/v5.0.7/css/all.css"
rel="stylesheet"
crossorigin="anonymous"
/>
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.png">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<!--
Notice the use of %PUBLIC_URL% in the tags above.
@ -27,19 +46,22 @@
-->
<title>Realtime group chat | Powered by Hasura</title>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-129818961-1"></script>
<script
async
src="https://www.googletagmanager.com/gtag/js?id=UA-129818961-1"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'UA-129818961-1');
</script>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<noscript> You need to enable JavaScript to run this app. </noscript>
<div id="root"></div>
<!--
This HTML file is a template.

View File

@ -1,21 +1,19 @@
@import url('https://fonts.googleapis.com/css?family=Raleway:400,600');
@import url('https://fonts.googleapis.com/css?family=Open+Sans:300,400');
body {
font-family: 'Open Sans';
font-family: 'IBM Plex Sans';
font-size: 16px;
margin: 0;
box-sizing: border-box;
font-display: swap;
}
.noPadd
{
.noPadd {
padding-left: 0;
padding-right: 0;
}
.removePaddLeft {
padding-left: 0;
padding-left: 0;
}
.addPaddTop
{
.addPaddTop {
padding-top: 10px;
clear: both;
}
@ -50,8 +48,7 @@ body {
/* Opera 11.10+ */
background: -o-linear-gradient(top, #a0b4cc, #c2a899);
}
.bgImage
{
.bgImage {
position: fixed;
left: 0;
width: 100%;
@ -62,16 +59,15 @@ body {
background-position: 0 0;
background-repeat: no-repeat;
}
.bgImage::before
{
.bgImage::before {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-image: linear-gradient(to bottom right,#000,#a92101);
opacity: .9;
background-image: linear-gradient(to bottom right, #000, #a92101);
opacity: 0.9;
}
.minHeight {
width: 100%;
@ -80,12 +76,12 @@ body {
.headerWrapper {
padding: 30px 0;
padding-left: 75px;
min-height: 15vh
min-height: 15vh;
}
.headerDescription {
font-size: 20px;
color: #fff;
font-family: "Raleway";
font-family: 'Raleway';
font-weight: 700;
z-index: 100;
position: relative;
@ -106,7 +102,7 @@ body {
padding: 10px 30px;
border: 0;
color: #fff;
font-family: "raleway";
font-family: 'raleway';
font-size: 16px;
font-weight: 700;
letter-spacing: 1px;
@ -161,8 +157,7 @@ body {
.appStack i {
font-size: 16px;
}
.checkBox
{
.checkBox {
color: #00bc00;
font-size: 22px !important;
}
@ -175,29 +170,24 @@ body {
.appStackIcon img {
width: 70%;
}
.formGroupWrapper
{
.formGroupWrapper {
padding-top: 20px;
width: 100%;
float: left;
}
.inputGroup
{
.inputGroup {
width: 100%;
}
.inputGroup input
{
.inputGroup input {
width: 68% !important;
display: inline-block;
height: 40px;
}
.inputGroup .groupAppend
{
.inputGroup .groupAppend {
width: 32% !important;
display: inline-block;
}
.groupAppend button
{
.groupAppend button {
width: 100%;
border-bottom-left-radius: 0;
border-top-left-radius: 0;
@ -206,19 +196,17 @@ body {
text-align: center;
background-color: #f93c18;
color: #fff;
font-family: "raleway";
font-family: 'raleway';
font-size: 14px;
font-weight: 700;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.groupAppend button:hover
{
.groupAppend button:hover {
color: #fff;
background-color: #e0270e;
}
.groupAppend button:focus
{
.groupAppend button:focus {
outline: none;
}
.footer {
@ -258,16 +246,6 @@ body {
padding-left: 5px;
}
.message {
font-size: 16px;
background-color: #fff;
padding-left: 5px;
margin: 20px;
border-radius: 5px;
width: 50%;
padding: 5px;
}
.selfMessage {
font-size: 16px;
background-color: #eee;
@ -281,33 +259,18 @@ body {
.newMessageEven {
font-size: 18px;
background-color: #98FB98;
background-color: #98fb98;
padding-left: 5px;
}
.newMessageOdd {
font-size: 18px;
background-color: #8FBC8F;
background-color: #8fbc8f;
padding-left: 5px;
}
.messageWrapperNew {
padding-bottom: 75px;
}
.banner {
position: -webkit-sticky;
position: sticky;
top: 0;
align-self: flex-start;
background-color: #20c40f;
font-size: 18px;
cursor: pointer;
font-weight: 400;
padding: 10px 0;
text-align: center;
font-family: 'raleway';
color: #fff;
padding-bottom: 35px;
}
.oldNewSeparator {
@ -316,26 +279,9 @@ body {
margin-bottom: 15px;
}
#chatbox {
overflow: auto;
height: calc(100vh - 90px);
background-color: #f8f9f9;
}
.textboxWrapper {
text-align: center;
position: fixed;
bottom: 87px;
width: 75%;
background-color: #fff;
padding: 1%;
}
.textbox {}
.sendButton {
width: 20%;
background-color:'green';
background-color: 'green';
}
.login {
@ -353,7 +299,6 @@ body {
padding: 0;
padding-left: 10px;
display: inline-block;
}
.typoTextbox {
@ -370,7 +315,8 @@ body {
background-color: #f6f6f7;
}
.loginTextbox:focus, .typoTextbox:focus {
.loginTextbox:focus,
.typoTextbox:focus {
outline: none;
border-color: #016d95;
}
@ -408,7 +354,8 @@ body {
background-color: #dba203;
}
.loginButton:focus, .typoButton:focus {
.loginButton:focus,
.typoButton:focus {
outline: none;
}
@ -452,13 +399,18 @@ body {
display: inline-block;
}
.onlineUsers {
background-color: #4f5050;
height: 100vh;
overflow: auto;
.wd45 {
width: 45%;
display: inline-block;
}
.messageName, .messsageTime {
.onlineUsers {
/* background-color: #4f5050; */
/* height: 100vh; */
}
.messageName,
.messsageTime {
width: 49%;
display: inline-block;
}
@ -482,32 +434,17 @@ body {
.userList li {
padding: 10px;
border-bottom: 1px solid #444;
color: #fff;
}
.chatWrapper {
display: flex;
width: 100%;
justify-content: space-between;
height: 100vh;
}
.userListHeading {
font-weight: 600;
padding: 15px 10px;
margin-top: 0;
margin-bottom: 0;
background-color: #222;
color: #fff;
}
.typingIndicator {
text-align: left;
padding-bottom: 10px;
padding-left: 1%;
}
.displayFlex
{
.displayFlex {
display: flex;
align-items: center;
}
@ -533,7 +470,7 @@ body {
color: white;
text-align: left;
}
.App-footer{
.App-footer {
position: fixed;
bottom: 0;
text-align: center;
@ -544,12 +481,11 @@ body {
border-top: 1px solid #ccc;
}
.footer-small-text{
.footer-small-text {
font-size: 12px;
}
@media (max-width: 767px) {
.headerDescription
{
.headerDescription {
text-align: center;
}
.headerWrapper {
@ -563,27 +499,17 @@ body {
display: flex;
justify-content: center;
}
.wd75
{
.wd75 {
width: 100%;
}
.message
{
width: 90%;
}
.textboxWrapper
{
width: 100%;
}
.mobileview
{
.mobileview {
position: absolute;
right: 0px;
bottom: 152px;
width: 50%;
}
.mobileuserListHeading
{
.mobileuserListHeading {
font-size: 14px;
background-color: #222;
color: #fff;
@ -591,12 +517,10 @@ body {
margin-bottom: 0;
padding: 5px;
}
.mobileuserListHeading i
{
.mobileuserListHeading i {
margin-left: 10px;
}
.mobileUserList
{
.mobileUserList {
background-color: #4f5050;
padding-inline-start: 0px;
-webkit-padding-start: 0px;
@ -604,19 +528,19 @@ body {
-o-padding-start: 0px;
margin-bottom: 0;
}
.mobileUserList li
{
.mobileUserList li {
list-style-type: none;
color: #fff;
padding: 5px;
font-size: 12px;
border-bottom: 1px solid #444;
}
.hasura-logo a {
padding: 0 0px;
font-size: 12px;
}
}
@media (max-width: 991px) {
.headerWrapper {
display: flex;
@ -629,8 +553,7 @@ body {
height: auto;
}
.minHeight {
height: auto;
min-height: 100vh;
/* min-height: 100vh; */
}
.loginBtn {
padding-right: 0;
@ -650,17 +573,24 @@ body {
.appStackIconWrapper {
padding-left: 10px;
}
.inputGroup input
{
.inputGroup input {
width: 63% !important;
}
.inputGroup .groupAppend
{
.inputGroup .groupAppend {
width: 37% !important;
}
.groupAppend button {
font-size: 13px;
letter-spacing: 0;
}
}
@media (max-width: 600px) {
.chatWrapper {
display: flex;
width: 100%;
flex-direction: column;
justify-content: flex-start;
height: 100vh;
}
}

View File

@ -1,9 +1,10 @@
import React from 'react';
import Main from './components/Main';
import './App.css' ;
import './App.css';
const App = () => {
return <div className="app"> <Main/> </div>;
}
return <Main />;
};
export default App;

View File

@ -1,10 +1,13 @@
import React from 'react';
import { StyledBanner } from '../styles/StyledChatApp';
import '../App.css';
const Banner = (props) => {
return (
<div className="banner" onClick={props.scrollToNewMessage}>
<StyledBanner onClick={props.scrollToNewMessage}>
You have {props.numOfNewMessages} new message(s)
</div>
</StyledBanner>
);
};

View File

@ -0,0 +1,54 @@
import React, { useEffect } from 'react';
import { StyledBetaAccessForm } from '../styles/StyledChatApp';
import { ExternalLinkIcon } from './ExternalLinkIcon';
export const BetaAccessForm = (props) => {
useEffect(() => {
const script = document.createElement('script');
script.src = 'https://paperform.co/__embed.min.js';
document.body.appendChild(script);
}, []);
return (
<StyledBetaAccessForm>
<div className="flex header-div">
<img
loading="lazy"
alt="Hasura"
src={
props?.isDarkThemeActive
? 'https://graphql-engine-cdn.hasura.io/assets/main-site/logo_primary_light.svg'
: 'https://graphql-engine-cdn.hasura.io/assets/main-site/logo_primary_dark.svg'
}
className="hasura-logo-img"
/>
<a
href="https://github.com/hasura/graphql-engine/tree/master/community/sample-apps/streaming-subscriptions-chat"
target="_blank"
rel="noreferrer"
>
View Source
<ExternalLinkIcon
color={props.isDarkThemeActive ? '#fff' : '#2C64F4'}
/>
</a>
</div>
<h2>Streaming Subscriptions</h2>
<p>
Hasura now allows you to instantly create a secure API for clients to fetch
large amounts of data in Postgres as a continuous stream. Subscribe to Hasura Newsletter
for updates about new features.
</p>
<div
data-paperform-id="hf-streaming-chat-app"
data-spinner="1"
className="paperform"
/>
{/* <form>
<input placeholder="Your email address" type="email" />
<button>Get updates</button>
</form> */}
</StyledBetaAccessForm>
);
};

View File

@ -29,8 +29,8 @@ function Chat(props) {
return (
<div>
<ChatWrapper userId={props.userId} username={props.username} />
<footer className="App-footer">
<ChatWrapper userId={props.userId} username={props.username} {...props} />
{/* <footer className="App-footer">
<div className="hasura-logo">
<img
src="https://hasura.io/brand-assets/powered-by-hasura-primary-dark.svg"
@ -57,7 +57,7 @@ function Chat(props) {
<div className="footer-small-text">
<span>(The database resets every 24 hours)</span>
</div>
</footer>
</footer> */}
</div>
);
}

View File

@ -1,32 +1,80 @@
import { useState } from 'react';
import styled from 'styled-components';
import {
ThemeSwitch,
StyledChatBox,
StyledChatBoxFormDiv,
} from '../styles/StyledChatApp';
import { BetaAccessForm } from './BetaAccessForm';
import RenderMessages from './RenderMessages';
import Textbox from './Textbox';
import OnlineUsers from './OnlineUsers';
import '../App.css';
const StyledRightSection = styled.div`
display: flex;
flex-direction: column;
width: 30%;
height: 100%;
padding: 24px 0;
justify-content: space-between;
@media (max-width: 1010px) {
display: none;
}
`;
const RightSection = (props) => {
const handleOnChange = () => {
props.toggleDarkTheme();
};
return (
<StyledRightSection>
<ThemeSwitch>
<label id="switch" className="switch">
<input
type="checkbox"
id="slider"
onChange={handleOnChange}
checked={props?.isDarkThemeActive ? false : true}
/>
<span className="slider round"></span>
</label>
</ThemeSwitch>
<BetaAccessForm {...props} />
</StyledRightSection>
);
};
export default function RenderMessagesProxy(props) {
const [mutationCallback, setMutationCallback] = useState(null);
const [dataStream, setDataStream] = useState(null);
return (
<div className="chatWrapper">
<div className="wd25 hidden-xs">
<OnlineUsers userId={props.userId} username={props.username} />
</div>
<div className="mobileview visible-xs">
<OnlineUsers userId={props.userId} username={props.username} />
</div>
<div className="wd75">
<RenderMessages
setMutationCallback={setMutationCallback}
username={props.username}
userId={props.userId}
/>
<Textbox
username={props.username}
mutationCallback={mutationCallback}
userId={props.userId}
/>
</div>
<OnlineUsers
userId={props.userId}
username={props.username}
dataStream={dataStream}
/>
<StyledChatBox className="wd45">
<StyledChatBoxFormDiv>
<RenderMessages
setMutationCallback={setMutationCallback}
username={props.username}
userId={props.userId}
setDataStream={setDataStream}
/>
<Textbox
username={props.username}
mutationCallback={mutationCallback}
userId={props.userId}
/>
</StyledChatBoxFormDiv>
</StyledChatBox>
<RightSection {...props} />
</div>
);
}

View File

@ -0,0 +1,20 @@
import React from "react";
export const ExternalLinkIcon = (props) => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.22032 14.78C5.36094 14.9205 5.55157 14.9993 5.75032 14.9993C5.94907 14.9993 6.13969 14.9205 6.28032 14.78L13.5003 7.56V13.25C13.5003 13.4489 13.5793 13.6397 13.72 13.7803C13.8606 13.921 14.0514 14 14.2503 14C14.4492 14 14.64 13.921 14.7806 13.7803C14.9213 13.6397 15.0003 13.4489 15.0003 13.25V5.75C15.0003 5.55109 14.9213 5.36032 14.7806 5.21967C14.64 5.07902 14.4492 5 14.2503 5H6.75032C6.5514 5 6.36064 5.07902 6.21999 5.21967C6.07933 5.36032 6.00032 5.55109 6.00032 5.75C6.00032 5.94891 6.07933 6.13968 6.21999 6.28033C6.36064 6.42098 6.5514 6.5 6.75032 6.5H12.4403L5.22032 13.72C5.07987 13.8606 5.00098 14.0512 5.00098 14.25C5.00098 14.4488 5.07987 14.6394 5.22032 14.78Z"
fill={props.color}
/>
</svg>
);
};

View File

@ -1,12 +1,12 @@
import PropTypes from "prop-types";
import { gql, useMutation } from "@apollo/client";
import { useRef } from "react";
import "../App.css";
import PropTypes from 'prop-types';
import { gql, useMutation } from '@apollo/client';
import { useRef } from 'react';
import '../App.css';
import reactLogo from "../images/React-logo.png";
import graphql from "../images/graphql.png";
import hasuraLogo from "../images/green-logo-white.svg";
import rightImg from "../images/chat-app.png";
import reactLogo from '../images/React-logo.png';
import graphql from '../images/graphql.png';
import hasuraLogo from '../images/green-logo-white.svg';
import rightImg from '../images/chat-app.png';
const addUser = gql`
mutation ($username: String!) {
@ -29,17 +29,19 @@ const LandingPage = (props) => {
},
onError: (err) => {
console.log(err);
alert("Please try again with a different username.");
props.setUsername("");
alert('Please try again with a different username.');
props.setUsername('');
},
});
const handleKeyPress = (key, mutate, loading) => {
if (!loading && key.charCode === 13) {
mutate();
}
};
return (
<div className="container-fluid minHeight">
<div className="container-fluid">
<div className="bgImage"></div>
<div>
<div className="headerWrapper">
@ -167,7 +169,7 @@ const LandingPage = (props) => {
disabled={loading}
minLength={3}
maxLength={15}
pattern={"^[a-z0-9_-]{3,15}$"}
pattern={'^[a-z0-9_-]{3,15}$'}
ref={usernameInput}
/>
<div className="input-group-append groupAppend">
@ -180,18 +182,18 @@ const LandingPage = (props) => {
addUserHandler();
} else {
alert(
"Invalid username. Spaces and special characters not allowed. Please try again"
'Invalid username. Spaces and special characters not allowed. Please try again'
);
props.setUsername("");
props.setUsername('');
}
}}
disabled={
loading ||
props.username === "" ||
props.username === '' ||
!usernameInput.current.validity.valid
}
>
{loading ? "Please wait ..." : "Get Started"}
{loading ? 'Please wait ...' : 'Get Started'}
</button>
</div>
</div>

View File

@ -1,35 +1,142 @@
import { useState } from 'react';
import { ApolloConsumer } from '@apollo/client';
import React, { useEffect, useState } from 'react';
import { ThemeProvider } from 'styled-components';
import { ApolloConsumer, gql, useMutation } from '@apollo/client';
import generateUsername from 'project-name-generator';
import styled from 'styled-components';
import { darkTheme, lightTheme } from '../styles/theme';
import Chat from './Chat';
import LandingPage from './LandingPage';
// import LandingPage from './LandingPage';
import '../App.css';
export default function Main() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [username, setUsername] = useState('');
const StyledApp = styled.div`
background: ${({ theme }) => theme.colors.background};
min-height: 100vh;
height: 100vh;
width: 100%;
padding: 0 24px;
overflow-y: auto;
::-webkit-scrollbar {
background: ${({ theme }) =>
theme.name === 'dark' ? '#1c262f' : '#FAFAFA'};
width: 12px;
}
::-webkit-scrollbar-thumb {
background: ${({ theme }) =>
theme.name === 'dark' ? '#394e60' : '#A6B6C4'};
border-radius: 80px;
width: 12px;
}
@media (min-width: 1250px) {
background-image: url('https://graphql-engine-cdn.hasura.io/assets/main-site/hasura_sf_illus.png');
background-repeat: no-repeat;
background-position: 95% 10%;
}
@media (min-width: 1250px) and (max-width: 1450px) {
background-position: 92% 10%;
background-size: 250px;
}
@media (max-width: 590px) {
padding: 0;
}
`;
const addUser = gql`
mutation ($username: String!) {
insert_user_one(
object: { username: $username }
on_conflict: { constraint: user_username_key, update_columns: [] }
) {
id
username
}
}
`;
export default function Main(props) {
const [isLoggedIn, setIsLoggedIn] = useState(true);
const [username, setUsername] = useState(generateUsername().dashed);
const [userId, setUserId] = useState(null);
const [isDarkThemeActive, toggleActiveTheme] = useState(true);
const [newUserHandler] = useMutation(addUser, {
variables: {
username,
},
onCompleted: (data) => {
if (data.insert_user_one?.id) {
setUserId(data.insert_user_one.id);
}
},
});
// check usernme and perform login
const login = (id) => {
setIsLoggedIn(true);
setUserId(id);
};
const toggleDarkTheme = () => {
toggleActiveTheme(!isDarkThemeActive);
window.localStorage.setItem(
'isDarkThemeActive',
JSON.stringify(!isDarkThemeActive)
);
};
// const retrieveActiveTheme = () => {
// const isDarkThemeActive = JSON.parse(
// window.localStorage.getItem('isDarkThemeActive')
// );
// // toggleDarkTheme({ isDarkThemeActive });
// };
// useEffect(() => {
// retrieveActiveTheme();
// }, []);
const currentActiveTheme = isDarkThemeActive ? darkTheme : lightTheme;
useEffect(() => {
if (!userId) {
newUserHandler();
}
}, [newUserHandler, userId]);
return (
<div className="app">
{!isLoggedIn ? (
<LandingPage
setUsername={setUsername}
login={login}
username={username}
/>
) : (
<ApolloConsumer>
{(client) => {
return <Chat userId={userId} username={username} client={client} />;
}}
</ApolloConsumer>
)}
</div>
<ThemeProvider theme={currentActiveTheme}>
<StyledApp className="app">
{!userId ? (
<div>Loading</div>
) : (
// <LandingPage
// setUsername={setUsername}
// login={login}
// username={username}
// />
<ApolloConsumer>
{(client) => {
return (
<Chat
userId={userId}
username={username}
client={client}
isDarkThemeActive={isDarkThemeActive}
toggleDarkTheme={toggleDarkTheme}
/>
);
}}
</ApolloConsumer>
)}
</StyledApp>
</ThemeProvider>
);
}

View File

@ -1,27 +1,88 @@
import "../App.js";
import "../App.css";
import formatDistanceToNow from "date-fns/formatDistanceToNow";
import '../App.js';
import '../App.css';
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
import { StyledMessage } from '../styles/StyledChatApp.js';
const alphabetsArr = [
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
];
export const getUserBgColor = (username) => {
if (username && username?.constructor?.name === 'String') {
const firstChar = username.charAt(0).toLowerCase();
const charIndex = alphabetsArr.indexOf(firstChar);
if (charIndex <= 4) {
return '#638FFF';
}
if (charIndex <= 8) {
return '#F669A1';
}
if (charIndex <= 12) {
return '#A36FF8';
}
if (charIndex <= 16) {
return '#FFC960';
}
if (charIndex <= 20) {
return '#39DAAA';
}
if (charIndex <= 27) {
return '#45D7F6';
}
}
return '#4F6C86';
};
export default function MessageList(props) {
return (
<div className={props.isNew ? "messageWrapperNew" : "messageWrapper"}>
<div className={props.isNew ? 'messageWrapperNew' : 'messageWrapper'}>
{props.messages.map((m) => {
return (
<div key={m.id} className="message">
<StyledMessage key={m.id} bgColor={getUserBgColor(m.username)}>
<div className="messageNameTime">
<div className="messageName">
<b>{m.username}</b>
</div>
<div className="messsageTime">
<i>
{formatDistanceToNow(new Date(m.timestamp), {
addSuffix: true,
})}{" "}
</i>
</div>
<div className="messageName">{m.username.substring(0, 2)}</div>
<div className="messageText">{m.text}</div>
</div>
<div className="messageText">{m.text}</div>
</div>
<div className="time_stamp">
{formatDistanceToNow(new Date(m.timestamp), {
addSuffix: true,
})}{' '}
</div>
</StyledMessage>
);
})}
<div style={{ height: 0 }} id="lastMessage"></div>

View File

@ -1,6 +1,13 @@
import { useState } from 'react';
import React, { useState } from 'react';
import { gql, useSubscription } from '@apollo/client';
import {
StyledLeftSection,
StyledOnlineUsers,
StyledOnlineUserCircle,
} from '../styles/StyledChatApp';
import { getUserBgColor } from './MessageList';
const fetchOnlineUsersSubscription = gql`
subscription {
user_online(order_by: { username: asc }) {
@ -10,17 +17,42 @@ const fetchOnlineUsersSubscription = gql`
}
`;
function OnlineUsers() {
function OnlineUsers({ dataStream }) {
const [showMobileView, setMobileView] = useState(false);
const [showMobileMenu, toggleMobileMenu] = useState(false);
const { data } = useSubscription(fetchOnlineUsersSubscription);
const toggleMobileView = () => {
setMobileView(!showMobileView);
};
const subscriptionStreamData = (isMobileView) => (
<StyledOnlineUsers>
<p
className={isMobileView ? 'mobileuserListHeading' : 'userListHeading'}
onClick={toggleMobileView}
>
Subscription Stream
{isMobileView && <i className="fa fa-angle-up"></i>}
</p>
{((isMobileView && showMobileView) || !isMobileView) && (
<ul
className={
isMobileView ? 'mobileUserList' : 'userList subscription-stream'
}
>
<p className="subscription-stream">
{JSON.stringify(dataStream, undefined, 4)}
</p>
</ul>
)}
</StyledOnlineUsers>
);
const subscriptionData = (isMobileView) => (
<div>
<StyledOnlineUsers>
<p
className={isMobileView ? 'mobileuserListHeading' : 'userListHeading'}
onClick={toggleMobileView}
@ -31,20 +63,59 @@ function OnlineUsers() {
{((isMobileView && showMobileView) || !isMobileView) && (
<ul className={isMobileView ? 'mobileUserList' : 'userList'}>
{data?.user_online.map((u) => {
return <li key={u.id}>{u.username}</li>;
return (
<StyledOnlineUserCircle
bgColor={getUserBgColor(u.username)}
key={u.id}
>
{u.username.substring(0, 2)}
</StyledOnlineUserCircle>
);
})}
</ul>
)}
</div>
</StyledOnlineUsers>
);
return (
<div>
<div className="onlineUsers hidden-xs">{subscriptionData(false)}</div>
<div className="mobileonlineUsers visible-xs">
{subscriptionData(true)}
<StyledLeftSection showMobileMenu={showMobileMenu}>
{/* Mobile View */}
<div className="mobile-header hide-on-desk">
<a href="https://hasura.io/">
<img
loading="lazy"
alt="Hasura"
src="https://graphql-engine-cdn.hasura.io/assets/main-site/logo_primary_light.svg"
className="hasura-logo-img"
/>
</a>
{!showMobileMenu && (
<div className="flex-div">
<p onClick={() => toggleMobileMenu(true)}>
Online Users&nbsp;(
{!data?.user_online ? 0 : data?.user_online.length}){' '}
</p>
<i
className={showMobileMenu ? 'fa fa-angle-down' : 'fa fa-angle-up'}
></i>
</div>
)}
{showMobileMenu && (
<div className="mobile-data-wrapper">
<p className="close-btn" onClick={() => toggleMobileMenu(false)}>
<i className="fa fa-times"></i>
</p>
<div className="onlineUsers">{subscriptionData(false)}</div>
<div className="onlineUsers">{subscriptionStreamData(false)}</div>
</div>
)}
</div>
</div>
{/* ************ */}
<div className="onlineUsers hideOnMobile">
{subscriptionStreamData(false)}
</div>
<div className="onlineUsers hideOnMobile">{subscriptionData(false)}</div>
</StyledLeftSection>
);
}

View File

@ -1,8 +1,10 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { gql, useLazyQuery, useQuery, useSubscription } from "@apollo/client";
import "../App.js";
import Banner from "./Banner";
import MessageList from "./MessageList";
import { useCallback, useEffect, useRef, useState } from 'react';
import { gql, useLazyQuery, useQuery, useSubscription } from '@apollo/client';
import '../App.js';
import Banner from './Banner';
import MessageList from './MessageList';
import { StyledMessagesList } from '../styles/StyledChatApp.js';
const fetchOldMessages = gql`
query ($last_received_ts: timestamptz) {
@ -48,13 +50,14 @@ export default function RenderMessages({
setMutationCallback,
username,
userId,
setDataStream,
}) {
const [messages, setMessages] = useState([]);
const [newMessages, setNewMessages] = useState([]);
const [bottom, setBottom] = useState(true);
const [initialLoad, setInitialLoad] = useState(false);
const [initialTimestamp, setInitialTimestamp] = useState(
"2018-08-21T19:58:46.987552+00:00"
'2018-08-21T19:58:46.987552+00:00'
);
const listInnerRef = useRef();
@ -71,7 +74,7 @@ export default function RenderMessages({
addOldMessages(data.message);
setInitialLoad(true);
setInitialTimestamp(
data.message[0]?.timestamp || "2018-08-21T19:58:46.987552+00:00"
data.message[0]?.timestamp || '2018-08-21T19:58:46.987552+00:00'
);
},
});
@ -93,6 +96,7 @@ export default function RenderMessages({
onSubscriptionData: ({ subscriptionData }) => {
if (!loading) {
if (subscriptionData.data) {
setDataStream(subscriptionData.data);
if (!isViewScrollable()) {
addOldMessages(subscriptionData.data.message_stream);
} else {
@ -110,15 +114,15 @@ export default function RenderMessages({
// scroll to bottom
const scrollToBottom = () => {
document
?.getElementById("lastMessage")
?.scrollIntoView({ behavior: "instant" });
?.getElementById('lastMessage')
?.scrollIntoView({ behavior: 'instant' });
};
// scroll to the new message
const scrollToNewMessage = () => {
document
?.getElementById("newMessage")
?.scrollIntoView({ behavior: "instant" });
?.getElementById('newMessage')
?.scrollIntoView({ behavior: 'instant' });
};
if (newMessages.length === 0 && bottom) {
@ -177,26 +181,26 @@ export default function RenderMessages({
(window.innerWidth || document.documentElement.clientWidth)
);
};
if (document.getElementById("lastMessage")) {
return !isInViewport(document.getElementById("lastMessage"));
if (document.getElementById('lastMessage')) {
return !isInViewport(document.getElementById('lastMessage'));
}
return false;
};
return (
<div id="chatbox" onScroll={handleScroll} ref={listInnerRef}>
<StyledMessagesList onScroll={handleScroll} ref={listInnerRef}>
{/* show "unread messages" banner if not at bottom */}
{!bottom && newMessages.length > 0 && isViewScrollable() ? (
{!bottom && newMessages.length > 0 && isViewScrollable() && (
<Banner
scrollToNewMessage={scrollToNewMessage}
numOfNewMessages={newMessages.length}
/>
) : null}
)}
<div
{/* <div
style={{
margin: "auto",
textAlign: "center",
margin: 'auto',
textAlign: 'center',
}}
>
<button
@ -207,20 +211,20 @@ export default function RenderMessages({
}
disabled={loadingOldMessages}
>
{loadingOldMessages === true ? "Loading..." : "Load More"}{" "}
{loadingOldMessages === true ? 'Loading...' : 'Load More'}{' '}
</button>
</div>
</div> */}
{/* Render old messages */}
<MessageList messages={messages} isNew={false} username={username} />
{/* Show old/new message separation */}
<div id="newMessage" className="oldNewSeparator">
{newMessages.length !== 0 ? "New messages" : null}
{newMessages.length !== 0 ? 'New messages' : null}
</div>
{/* render new messages */}
<MessageList messages={newMessages} isNew={true} username={username} />
{/* Bottom div to scroll to */}
</div>
</StyledMessagesList>
);
}

View File

@ -57,22 +57,21 @@ export default function Textbox(props) {
return (
<form onSubmit={sendMessage}>
<div className="textboxWrapper">
<TypingIndicator userId={props.userId} />
<input
id="textbox"
className="textbox typoTextbox"
value={text}
autoFocus={true}
placeholder="Message your friends here.."
onChange={(e) => {
handleTyping(e.target.value, client.mutate);
}}
autoComplete="off"
/>
<button className="sendButton typoButton" onClick={sendMessage}>
{' '}
Send{' '}
</button>
<div className="form-btn-div">
<button onClick={sendMessage}> Send </button>
</div>
</div>
<TypingIndicator userId={props.userId} />
</form>
);
};
@ -95,5 +94,6 @@ export default function Textbox(props) {
insertMessageHandler();
setText('');
};
return form(sendMessage, client);
}

View File

@ -1,4 +1,6 @@
import React from 'react';
import { gql, useSubscription } from '@apollo/client';
import { StyledTypingIndicator } from '../styles/StyledChatApp';
import '../App.css';
const getUserTyping = gql`
@ -28,11 +30,11 @@ function TypingIndicator(props) {
}
return (
<div className="typingIndicator">
<StyledTypingIndicator>
{data?.user_typing?.length === 0
? ''
? ``
: `${data.user_typing[0].username} is typing ...`}
</div>
</StyledTypingIndicator>
);
}

View File

@ -0,0 +1,588 @@
import styled from 'styled-components';
export const ThemeSwitch = styled.div`
display: flex;
justify-content: flex-end;
width: 100%;
margin-right: 10%;
margin-bottom: 10px;
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 72px;
height: 36px;
padding: 2.5px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #1c262f;
-webkit-transition: 0.4s;
background: #1c262f;
/* box-shadow: 3.21429px 1.28571px 8.35714px -1.28571px rgba(0, 0, 0, 0.3),
0px 2.57143px 3.85714px -0.642857px rgba(0, 0, 0, 0.1); */
transition: 0.4s;
}
.slider:before {
position: absolute;
content: '';
height: 28px;
width: 28px;
left: 3px;
top: 0;
bottom: 0;
margin: auto 0;
-webkit-transition: 0.4s;
transition: 0.4s;
box-shadow: 3.21429px 1.28571px 8.35714px -1.28571px rgba(0, 0, 0, 0.3),
0px 2.57143px 3.85714px -0.642857px rgba(0, 0, 0, 0.1);
background: #344658
url('https://graphql-engine-cdn.hasura.io/assets/main-site/bulb_night.svg');
background-repeat: no-repeat;
background-position: center;
}
input:checked + .slider {
box-shadow: inset 0px 0px 1.28571px rgba(0, 0, 0, 0.15);
background: #e7eef3;
}
input:checked + .slider:before {
-webkit-transform: translateX(36px);
-ms-transform: translateX(36px);
transform: translateX(36px);
background: #fff
url('https://graphql-engine-cdn.hasura.io/assets/main-site/bulb_day.svg');
background-repeat: no-repeat;
background-position: center;
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
`;
export const StyledBetaAccessForm = styled.div`
border-radius: 8px;
padding: 7%;
margin-left: 24px;
background: ${({ theme }) => theme.colors.sectionBg};
box-shadow: ${({ theme }) => theme.boxShadow};
.header-div {
min-height: 45px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
a {
display: flex;
align-items: center;
color: ${({ theme }) => theme.colors.anchor};
margin: 0;
font-size: 16px;
font-weight: 500;
svg {
min-width: 20px;
margin-left: 8px;
}
}
}
form {
margin-top: 31px;
display: flex;
flex-direction: column;
input {
border: 1px solid;
outline: none;
height: 48px;
border-radius: 4px;
background: ${({ theme }) => theme.colors.sectionBg};
border-color: ${({ theme }) => theme.colors.border};
color: ${({ theme }) => theme.colors.text};
padding: 8px 10px;
}
button {
width: 100%;
height: 48px;
background: #1eb4d4;
border: none;
font-weight: 500;
outline: none;
margin-top: 16px;
color: #fff;
border-radius: 4px;
}
}
.hasura-logo-img {
min-width: 137px;
width: 137px;
max-width: 137px;
}
h2 {
margin-top: 40px;
font-size: 24px;
line-height: 1.25;
font-weight: 700;
color: ${({ theme }) => theme.colors.heading};
/* margin-bottom: 8px; */
}
p {
color: ${({ theme }) => theme.colors.text};
font-size: 16px;
line-height: 1.75;
font-weight: 400;
}
@media (max-width: 1200px) {
.hasura-logo-img {
min-width: 100px;
width: 100px;
max-width: 100px;
}
.header-div {
a {
font-size: 12px;
}
svg {
min-width: 15px;
width: 15px;
margin-left: 6px;
}
}
}
.paperform {
color: white !important;
input {
color: white !important;
}
}
.Paperform__Container {
.LiveField__answer {
color: white !important;
}
input {
color: white !important;
}
}
`;
export const StyledChatBox = styled.div`
height: 100vh;
padding: 24px 0;
overflow: hidden;
@media (min-width: 600px) and (max-width: 1010px) {
width: 70%;
}
@media (max-width: 590px) {
width: 100% !important;
padding: 0;
}
`;
export const StyledChatBoxFormDiv = styled.div`
height: 100%;
border-radius: 8px;
/* padding: 32px; */
box-shadow: ${({ theme }) => theme.boxShadow};
display: flex;
overflow-y: auto;
flex-direction: column;
justify-content: space-between;
background: ${({ theme }) => theme.colors.sectionBg};
.textboxWrapper {
display: flex;
border: 1px solid;
border-color: ${({ theme }) => theme.colors.border};
border-radius: 4px;
}
form {
background: ${({ theme }) => theme.colors.headerBg};
min-height: 128px;
display: flex;
align-items: center;
padding: 24px 24px 32px 24px;
input {
height: 72px;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
background: ${({ theme }) => theme.colors.formInput};
width: 100%;
border: none;
outline: none;
padding: 8px 12px;
color: ${({ theme }) => theme.colors.text};
}
.form-btn-div {
min-width: 120px;
height: 72px;
display: flex;
justify-content: center;
align-items: center;
}
button {
min-width: 88px;
height: 40px;
line-height: 40px;
text-align: center;
border-radius: 4px;
outline: none;
border: none;
font-size: 16px;
font-weight: 500;
background: #344658;
color: #fff;
padding: 0;
}
}
.textboxWrapper {
width: 100%;
}
@media (max-width: 590px) {
form {
padding: 5px 10px;
}
}
`;
export const StyledLeftSection = styled.div`
width: 25%;
padding-top: 24px;
height: 100vh;
/* overflow-y: auto; */
margin-right: 24px;
display: flex;
flex-direction: column;
justify-content: space-between;
@media (min-width: 591px) {
.hide-on-desk {
display: none;
}
}
@media (max-width: 590px) {
width: 100%;
height: 80px;
position: fixed;
z-index: 1;
.hideOnMobile {
display: none;
}
.mobile-header {
width: 100%;
position: absolute;
top: 0;
/* background: ${({ theme }) => theme.colors.background}; */
background: #23303d;
z-index: 100;
display: flex;
align-items: flex-start;
flex-direction: ${(props) => (props?.showMobileMenu ? 'column' : '')};
justify-content: space-between;
padding: 24px 24px 0;
min-height: ${(props) => (props?.showMobileMenu ? '100vh' : '80px')};
height: ${(props) => (props?.showMobileMenu ? '100%' : '80px')};
overflow-y: scroll;
.mobile-data-wrapper {
width: 100%;
margin-top: 35px;
.close-btn {
position: absolute;
top: 24px;
right: 35px;
color: #fff;
font-size: 20px;
}
.userList {
min-height: 30vh;
max-height: 30vh;
}
}
.hasura-logo-img {
width: 100px;
}
p {
font-size: 14px;
color: #fff;
margin: 0;
}
.flex-div {
display: flex;
align-items: center;
margin-top: 8px;
p {
margin-top: -2px;
font-size: 14px;
font-weight: 600;
}
i {
color: #fff;
font-size: 18px;
margin-left: 6px;
display: block;
}
}
}
}
`;
export const StyledOnlineUsers = styled.div`
border-top-left-radius: 12px;
border-top-right-radius: 12px;
margin-bottom: 7%;
.userListHeading {
font-weight: 500;
padding: 20px 13px;
margin-top: 0;
margin-bottom: 0;
background: ${({ theme }) => theme.colors.headerBg};
border-radius: 12px 12px 0px 0px;
color: ${({ theme }) => theme.colors.heading};
width: 100%;
height: 56px;
}
.userList {
padding: 25px 20px;
display: flex;
height: 40%;
min-height: 38.2vh;
max-height: 38.2vh;
overflow-y: auto;
flex: 1;
flex-wrap: wrap;
background: ${({ theme }) => theme.colors.sectionBg};
border-radius: 0px 0px 12px 12px;
li {
list-style-type: none;
min-width: 48px;
width: 48px;
min-height: 48px;
height: 48px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
text-transform: uppercase;
font-weight: 700;
margin: 12px;
}
::-webkit-scrollbar {
background: ${({ theme }) =>
theme.name === 'dark' ? '#1c262f' : '#FAFAFA'};
width: 12px;
}
::-webkit-scrollbar-thumb {
background: ${({ theme }) =>
theme.name === 'dark' ? '#394e60' : '#A6B6C4'};
border-radius: 80px;
width: 12px;
}
}
.subscription-stream {
color: ${({ theme }) => theme.colors.text};
font-size: 14px;
font-weight: 500;
}
@media (min-width: 1560px) {
.userList {
min-height: 40vh;
max-height: 40vh;
}
}
@media (max-width: 1400px) {
.userList {
min-height: 35vh;
max-height: 35vh;
}
}
`;
export const StyledMessage = styled.div`
width: 92%;
display: flex;
justify-content: space-between;
min-height: 48px;
height: auto;
align-items: center;
font-size: 16px;
margin: 20px;
border-radius: 5px;
.time_stamp {
font-size: 14px;
font-weight: 400;
color: ${({ theme }) => theme.colors.timeStamp};
font-style: normal;
/* line-height: 1.7; */
}
.messageNameTime {
display: flex;
align-items: center;
flex: 1;
}
.messageName {
min-width: 48px;
width: 48px;
min-height: 48px;
height: 48px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
background: #f669a1;
background: ${(props) => props?.bgColor};
color: #fff;
text-transform: uppercase;
font-weight: 700;
margin-right: 16px;
}
.messageText {
color: ${({ theme }) => theme.colors.text};
font-size: 14px;
line-height: 1.7;
}
`;
export const StyledMessagesList = styled.div`
height: calc(100% - 120px);
overflow-y: auto;
::-webkit-scrollbar {
background: ${({ theme }) =>
theme.name === 'dark' ? '#1c262f' : '#FAFAFA'};
width: 12px;
}
::-webkit-scrollbar-thumb {
background: ${({ theme }) =>
theme.name === 'dark' ? '#394e60' : '#A6B6C4'};
border-radius: 80px;
width: 12px;
}
#newMessage {
color: ${({ theme }) => theme.colors.text};
}
`;
export const StyledTypingIndicator = styled.div`
font-style: italic;
color: #4f6c86;
font-size: 12px;
font-weight: 400;
line-height: 32px;
white-space: nowrap;
position: absolute;
bottom: 25px;
@media (min-width: 1450px) {
bottom: 30px;
}
`;
export const StyledBanner = styled.div`
position: -webkit-sticky;
position: sticky;
top: 24px;
background-color: #0c9a70;
cursor: pointer;
font-weight: 400;
padding: 10px 0;
text-align: center;
color: #fff;
min-width: 213px;
width: 40%;
height: 36px;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
margin: 24px auto 0;
box-shadow: 0px 8px 10px -6px rgba(0, 0, 0, 0.1),
0px 20px 25px -5px rgba(0, 0, 0, 0.1);
border-radius: 68px;
`;
export const StyledOnlineUserCircle = styled.li`
background: ${(props) => props?.bgColor};
`;

View File

@ -0,0 +1,38 @@
// Light Theme
export const lightTheme = {
colors: {
background: '#F3F5F7',
sectionBg: '#fff',
heading: '#141C22',
text: '#141C22',
border: '#C4CED7',
anchor: '#2C64F4',
headerBg: '#E7EEF3',
formInput: '#fff',
timeStamp: '#A6B6C4',
},
name: 'light',
boxShadow:
'0px 1px 2px -1px rgba(0, 0, 0, 0.1), 0px 1px 3px rgba(0, 0, 0, 0.1)',
};
// **************************************** //
// Dark Theme
export const darkTheme = {
colors: {
background: '#0C1015',
sectionBg: '#141C22',
headerBg: '#1C262F',
heading: '#fff',
text: '#A6B6C4',
border: '#344658',
anchor: '#fff',
formInput: '#23303D',
timeStamp: '#4F6C86',
},
name: 'dark',
boxShadow: '',
};