community: update sample realtime-chat dependencies

GITHUB_PR_NUMBER: 7844
GITHUB_PR_URL: https://github.com/hasura/graphql-engine/pull/7844

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/2964
Co-authored-by: arjunyel <11153289+arjunyel@users.noreply.github.com>
GitOrigin-RevId: 34c4054e24fc1fa6c44c6c8a84db5b0d887faddb
This commit is contained in:
hasura-bot 2021-12-07 18:44:01 +05:30
parent 23e1cb218a
commit c5cedbd84b
22 changed files with 49677 additions and 18093 deletions

View File

@ -1,4 +1,4 @@
FROM node:carbon as builder
FROM node:16 as builder
ENV NODE_ENV=PRODUCTION
WORKDIR /app
COPY package.json ./
@ -6,9 +6,9 @@ RUN npm install
COPY . .
RUN npm run build
FROM node:8-alpine
FROM node:16-alpine
RUN npm -g install serve
WORKDIR /app
COPY --from=builder /app/build .
CMD ["serve", "-s", "-p", "8080"]
CMD ["serve", "-s", "-p", "3000"]

View File

@ -2,10 +2,218 @@
This is the source code for a fully working group chat app that uses subscriptions in Hasura GraphQL Engine. It is built using React and Apollo.
Run this example with Docker: `docker compose up -d --build`
[![Edit chat-app](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/hasura/graphql-engine/tree/master/community/sample-apps/realtime-chat?fontsize=14)
- [Fully working app](https://realtime-chat.demo.hasura.app/)
- [Backend](https://realtime-chat.demo.hasura.app/console)
For a complete tutorial about data modelling, [check out this blog](https://hasura.io/blog/building-a-realtime-chat-app-with-graphql-subscriptions-d68cd33e73f).
Adapted from the [original blogpost by Rishichandra Wawhal](https://hasura.io/blog/building-a-realtime-chat-app-with-graphql-subscriptions-d68cd33e73f).
[![Deploy to Hasura Cloud](https://graphql-engine-cdn.hasura.io/img/deploy_to_hasura.png)](https://cloud.hasura.io/signup)
## TLDR
- Hasura allows us to build a realtime GraphQL API without writing any backend code.
- To save bandwith we use a subscription to listen for message events, then run a query to fetch only the new messages.
- Every two seconds our frontend runs a user_online mutation to populate an online users list.
- A user_typing mutation is run whenever we type few characters and a subscription is used to fetch the last typing user.
## Introduction
Hasura allows us to instantly create a realtime GraphQL API from our data. In this tutorial we walk through creating a group chat application, without needing to write any backend code, using React and Apollo. The focus is on data models that we store in Postgres rather than full chat functionality.
## Data Modelling
### Users
When a user signs up we insert their chosen username and generate their ID. We also track when they last typed and were seen.
```sql
user (
id SERIAL PRIMARY KEY
username TEXT UNIQUE
last_seen timestamp with time zone
last_typed timestamp with time zone
)
```
### Messages
For our tutorial we will just be inserting messages, not editing or deleting, but if we wanted to in the future Hasura will autogenerate the mutations for us. We could also extend this by adding features such as multiple different chatrooms.
```sql
message (
id SERIAL NOT NULL PRIMARY KEY
"text" TEXT NOT NULL
username INT FOREIGN KEY REFERENCES user(username) NOT NULL
"timestamp" timestamp with time zone DEFAULT now() NOT NULL
)
```
### Online users
To query the users online we create a Postgres view that fetches all users with last_seen less than 10 seconds ago.
```sql
CREATE OR REPLACE VIEW public."user_online" AS
SELECT "user".id,
"user".username,
"user".last_typed,
"user".last_seen
FROM "user"
WHERE ("user".last_seen > (now() - '00:00:10'::interval));
```
### Typing Indicator
To query the last person typing we create a similar view with last_typed within the past 2 seconds.
```sql
CREATE OR REPLACE VIEW public."user_typing" AS
SELECT "user".id,
"user".username,
"user".last_typed,
"user".last_seen
FROM "user"
WHERE ("user".last_typed > (now() - '00:00:02'::interval));
```
## Frontend
### Creating a user
At user signup we insert a single row into the users table
```gql
mutation ($username: String!) {
insert_user_one(object: { username: $username }) {
id
username
}
}
```
We take the returned id and username and store it in our app's state management.
### User online events
Every two seconds we run a mutation to update our user's last_seen value. For example, with React using UseEffect or componentDidMount we could call the mutation inside a setInterval. [Be mindful when using setInterval inside a React hook.](https://overreacted.io/making-setinterval-declarative-with-react-hooks/)
```gql
mutation ($userId: Int!) {
update_user_by_pk(pk_columns: { id: $userId }, _set: { last_seen: "now()" }) {
id
}
}
```
### Subscribing to new messages
We can use Graphql subscriptions in two ways:
1. Subscribe to a query and render all the data that comes back.
1. Use the subscription as an event notification then run our own custom fetching logic.
Option two is best for our chatroom use case because we can do some work to just fetch new messages. Otherwise every new message we would also come with all previous ones.
1. On load we fetch all existing messages by setting $last_received_id to -1 and $last_received_ts to a timestamp well into the past such as '2018-08-21T19:58:46.987552+00:00'
```gql
query ($last_received_id: Int, $last_received_ts: timestamptz) {
message(
order_by: { timestamp: asc }
where: {
_and: {
id: { _neq: $last_received_id }
timestamp: { _gte: $last_received_ts }
}
}
) {
id
text
username
timestamp
}
}
```
1. Now we subscribe to new message events
```gql
subscription {
message(order_by: { id: desc }, limit: 1) {
id
username
text
timestamp
}
}
```
1. Once we detect a new message we call our message query with our last known id and timestamp from our client state.
With a bit of magic from realtime subscriptions we saved our users a ton of data by only fetching new messages.
### Sending messages
A user sends a message by inserting a row into the messages table. In the future if we setup [JWT based authentication in the future](https://hasura.io/docs/latest/graphql/core/auth/authentication/jwt.html) we can automatically [set the username from the JWT custom claims](https://hasura.io/docs/latest/graphql/core/auth/authorization/roles-variables.html#dynamic-session-variables).
```gql
mutation insert_message($message: message_insert_input!) {
insert_message_one(object: $message) {
id
timestamp
text
username
}
}
```
### UI Typing Indicator
1. Similar to the user online events, when a user types a few characters we run a mutation on their last_typing timestamp.
```gql
mutation ($userId: Int!) {
update_user_by_pk(
pk_columns: { id: $userId }
_set: { last_typed: "now()" }
) {
id
}
}
```
1. Then we subscribe to our Postgres view of the last user typing, making sure to exclude ourselves.
```gql
subscription ($selfId: Int) {
user_typing(
where: { id: { _neq: $selfId } }
limit: 1
order_by: { last_typed: desc }
) {
last_typed
username
}
}
```
### Online Users list
We can easily get a list of all users online by subscribing to the user_online view we created.
```gql
subscription {
user_online(order_by: { username: asc }) {
id
username
}
}
```

View File

@ -0,0 +1,37 @@
services:
postgres:
image: postgres:14
restart: always
volumes:
- db_data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: postgrespassword
graphql-engine:
image: hasura/graphql-engine:v2.0.10.cli-migrations-v3
ports:
- "8080:8080"
depends_on:
- "postgres"
restart: always
environment:
## postgres database to store Hasura metadata
HASURA_GRAPHQL_METADATA_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres
## this env var can be used to add the above postgres database to Hasura as a data source. this can be removed/updated based on your needs
PG_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres
HASURA_GRAPHQL_ENABLE_CONSOLE: "true" # set to "false" to disable console
## enable debugging mode. It is recommended to disable this in production
HASURA_GRAPHQL_DEV_MODE: "true"
volumes:
- ./hasura/metadata:/hasura-metadata
- ./hasura/migrations:/hasura-migrations
frontend:
build:
dockerfile: ./Dockerfile
context: .
ports:
- '3000:3000'
depends_on:
- 'graphql'
volumes:
db_data:

View File

@ -3,7 +3,7 @@
configuration:
connection_info:
database_url:
from_env: SAMPLE_APPS_DATABASE_URL
from_env: PG_DATABASE_URL
pool_settings:
idle_timeout: 180
max_connections: 50

File diff suppressed because it is too large Load Diff

View File

@ -3,31 +3,42 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"apollo-boost": "^0.1.10",
"apollo-client-preset": "^1.0.8",
"apollo-link-schema": "^1.1.0",
"apollo-link-ws": "^1.0.8",
"apollo-utilities": "^1.0.16",
"graphql": "^0.13.2",
"graphql-tag": "^2.9.2",
"graphql-tools": "^3.0.5",
"moment": "^2.22.2",
"react": "^16.4.1",
"react-apollo": "^2.1.9",
"react-dom": "^16.4.1",
"react-router-dom": "^4.3.1",
"react-scripts": "^1.1.1",
"subscriptions-transport-ws": "^0.9.12"
"@apollo/client": "^3.5.5",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"moment": "^2.29.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3",
"graphql-ws": "^5.5.5",
"web-vitals": "^2.1.2"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"devDependencies": {
"eslint-plugin-graphql": "^1.5.0",
"eslint": "^5.1.0",
"eslint-plugin-react": "^7.10.0"
"eslint-plugin-graphql": "^4.0.0"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

@ -555,7 +555,7 @@ body {
.headerWrapper {
display: block !important;
}
.000scription {
.description {
text-align: center;
padding-bottom: 10px;
}

View File

@ -1,4 +1,3 @@
import React from 'react';
import '../App.css';
const Banner = (props) => {
@ -9,4 +8,4 @@ const Banner = (props) => {
);
};
export default Banner;
export default Banner;

View File

@ -1,121 +1,73 @@
import React from 'react';
import { Subscription } from 'react-apollo';
import gql from 'graphql-tag';
import { gql } from '@apollo/client';
import ChatWrapper from './ChatWrapper';
import '../App.css';
const subscribeToNewMessages = gql`
subscription {
message ( order_by: {id:desc} limit: 1) {
id
username
text
timestamp
} }
`;
import { useInterval } from '../hooks/useInterval';
const emitOnlineEvent = gql`
mutation ($userId:Int!){
update_user (
_set: {
last_seen: "now()"
}
where: {
id: {
_eq: $userId
}
}
mutation ($userId: Int!) {
update_user_by_pk(
pk_columns: { id: $userId }
_set: { last_seen: "now()" }
) {
affected_rows
id
}
}
`;
class Chat extends React.Component {
constructor (props) {
super(props);
this.state = {
username: props.username,
refetch: null
};
}
// set refetch function (coming from child <Query> component) using callback
setRefetch = (refetch) => {
this.setState({
refetch
})
}
async componentDidMount() {
// Emit and event saying the user is online every 5 seconds
setInterval(
async () => {
await this.props.client.mutate({
mutation: emitOnlineEvent,
variables: {
userId: this.props.userId
}
});
function Chat(props) {
/**
* Every 3 seconds emit an online event
*/
useInterval(async () => {
await props.client.mutate({
mutation: emitOnlineEvent,
variables: {
userId: props.userId,
},
3000
);
}
});
}, 3000);
/*
Subscription is used only for event notification
No data is bound to the subscription component
As soon as an event occurs, the refetch() of the child component is called
*/
render () {
const { refetch, username } = this.state;
return (
<div>
<Subscription
subscription={subscribeToNewMessages}
>
{
({data, error, loading}) => {
if (error || (data && data.message === null)) {
console.error(error || `Unexpected response: ${data}`);
return "Error";
}
if (refetch) {
refetch();
}
return null;
}
}
</Subscription>
<ChatWrapper
refetch={refetch}
setRefetch={this.setRefetch}
userId={this.props.userId}
username={username}
/>
<footer className="App-footer">
<div className="hasura-logo">
<img src="https://graphql-engine-cdn.hasura.io/img/powered_by_hasura_black.svg" onClick={() => window.open("https://hasura.io")} alt="Powered by Hasura"/>
&nbsp; | &nbsp;
<a href="https://realtime-chat.hasura.app/console" target="_blank" rel="noopener noreferrer">
Backend
</a>
&nbsp; | &nbsp;
<a href="https://github.com/hasura/graphql-engine/tree/master/community/sample-apps/realtime-chat" target="_blank" rel="noopener noreferrer">
Source
</a>
&nbsp; | &nbsp;
<a href="https://hasura.io/blog/building-a-realtime-chat-app-with-graphql-subscriptions-d68cd33e73f" target="_blank" rel="noopener noreferrer">
Blogpost
</a>
</div>
<div className="footer-small-text"><span>(The database resets every 24 hours)</span></div>
</footer>
</div>
);
}
};
return (
<div>
<ChatWrapper userId={props.userId} username={props.username} />
<footer className="App-footer">
<div className="hasura-logo">
<img
src="https://graphql-engine-cdn.hasura.io/img/powered_by_hasura_black.svg"
onClick={() => window.open('https://hasura.io')}
alt="Powered by Hasura"
/>
&nbsp; | &nbsp;
<a
href="https://realtime-chat.hasura.app/console"
target="_blank"
rel="noopener noreferrer"
>
Backend
</a>
&nbsp; | &nbsp;
<a
href="https://github.com/hasura/graphql-engine/tree/master/community/sample-apps/realtime-chat"
target="_blank"
rel="noopener noreferrer"
>
Source
</a>
&nbsp; | &nbsp;
<a
href="https://hasura.io/blog/building-a-realtime-chat-app-with-graphql-subscriptions-d68cd33e73f"
target="_blank"
rel="noopener noreferrer"
>
Blogpost
</a>
</div>
<div className="footer-small-text">
<span>(The database resets every 24 hours)</span>
</div>
</footer>
</div>
);
}
export default Chat;

View File

@ -1,55 +1,32 @@
import React from 'react';
import { useState } from 'react';
import RenderMessages from './RenderMessages';
import Textbox from './Textbox'
import Textbox from './Textbox';
import OnlineUsers from './OnlineUsers';
import "../App.css";
import '../App.css';
export default class RenderMessagesProxy extends React.Component {
constructor(props) {
super(props);
this.state = {
refetch: null
}
}
export default function RenderMessagesProxy(props) {
const [mutationCallback, setMutationCallback] = useState(null);
// Set mutation callback. For instantly adding messages to state after mutation
setMutationCallback = (mutationCallback) => {
this.setState({
mutationCallback
})
}
render() {
return (
<div className="chatWrapper">
<div className="wd25 hidden-xs">
<OnlineUsers
userId={this.props.userId}
username={this.props.username}
/>
</div>
<div className="mobileview visible-xs">
<OnlineUsers
userId={this.props.userId}
username={this.props.username}
/>
</div>
<div className="wd75">
<RenderMessages
refetch={this.props.refetch}
setRefetch={this.props.setRefetch}
setMutationCallback={this.setMutationCallback}
username={this.props.username}
userId={this.props.userId}
/>
<Textbox
username={this.props.username}
mutationCallback={this.state.mutationCallback}
userId={this.props.userId}
/>
</div>
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>
</div>
);
}

View File

@ -1,205 +1,196 @@
import React from "react";
import PropTypes from "prop-types";
import gql from 'graphql-tag';
import { Mutation } from 'react-apollo';
import "../App.css";
import PropTypes from 'prop-types';
import { gql, useMutation } from '@apollo/client';
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 apolloLogo from '../images/apollo.png';
import rightImg from '../images/chat-app.png';
const addUser = gql`
mutation ($username: String!) {
insert_user (
objects: [{
username: $username
}]
) {
returning {
id
username
}
insert_user_one(object: { username: $username }) {
id
username
}
}
`;
const LandingPage = (props) => {
const reactLogo = require("../images/React-logo.png");
const graphql = require("../images/graphql.png");
const hasuraLogo = require("../images/green-logo-white.svg");
const apolloLogo = require("../images/apollo.png");
const rightImg = require("../images/chat-app.png");
const [addUserHandler, { loading }] = useMutation(addUser, {
variables: {
username: props.username,
},
onCompleted: (data) => {
props.login(data.insert_user_one.id);
},
onError: () => {
alert('Please try again with a different username.');
props.setUsername('');
},
});
const handleKeyPress = (key, mutate, loading) => {
if (!loading && key.charCode === 13) {
mutate();
}
};
return (
<Mutation
mutation={addUser}
variables={{
username: props.username
}}
onCompleted={(data) => {
props.login(data.insert_user.returning[0].id);
}}
onError={() => {
alert('Please try again with a different username.')
props.setUsername('');
}}
>
{
(insert_user, { data, loading, error}) => {
return (
<div className="container-fluid minHeight">
<div className="bgImage">
</div>
<div>
<div className="headerWrapper">
<div className="headerDescription">
Realtime Chat App
<div className="container-fluid minHeight">
<div className="bgImage"></div>
<div>
<div className="headerWrapper">
<div className="headerDescription">Realtime Chat App</div>
</div>
<div className="mainWrapper">
<div className="col-md-5 col-sm-6 col-xs-12 noPadd">
<div className="appstackWrapper">
<div className="appStack">
<div className="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth">
<i className="em em---1" />
</div>
<div className="col-md-11 col-sm-11 col-xs-10 noPadd">
<div className="description">
Try out a realtime app that uses
</div>
<div className="appStackIconWrapper">
<div className="col-md-4 col-sm-4 col-xs-4 noPadd">
<div className="appStackIcon">
<img
className="img-responsive"
src={reactLogo}
alt="React logo"
/>
</div>
</div>
<div className="col-md-8 col-sm-8 col-xs-8 noPadd">
<div className="appStackIcon">
<img
className="img-responsive"
src={graphql}
alt="GraphQL logo"
/>
</div>
</div>
</div>
</div>
<div className="mainWrapper">
<div className="col-md-5 col-sm-6 col-xs-12 noPadd">
<div className="appstackWrapper">
<div className="appStack">
<div className="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth">
<i className="em em---1" />
</div>
<div className="col-md-11 col-sm-11 col-xs-10 noPadd">
<div className="description">
Try out a realtime app that uses
</div>
<div className="appStackIconWrapper">
<div className="col-md-4 col-sm-4 col-xs-4 noPadd">
<div className="appStackIcon">
<img
className="img-responsive"
src={reactLogo}
alt="React logo"
/>
</div>
</div>
<div className="col-md-8 col-sm-8 col-xs-8 noPadd">
<div className="appStackIcon">
<img
className="img-responsive"
src={graphql}
alt="GraphQL logo"
/>
</div>
</div>
</div>
</div>
</div>
<div className="appStack">
<div className="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth">
<i className="em em-rocket" />
</div>
<div className="col-md-11 col-sm-11 col-xs-10 noPadd">
<div className="description">Powered by</div>
<div className="appStackIconWrapper">
<div className="col-md-4 col-sm-4 col-xs-4 noPadd">
<div className="appStackIcon">
<img
className="img-responsive"
src={apolloLogo}
alt="apollo logo"
/>
</div>
</div>
<div className="col-md-4 col-sm-4 col-xs-4 noPadd">
<div className="appStackIcon">
<img
className="img-responsive"
src={hasuraLogo}
alt="Hasura logo"
/>
</div>
</div>
</div>
</div>
</div>
<div className="appStack">
<div className="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth">
<i className="em em-sunglasses" />
</div>
<div className="col-md-11 col-sm-11 col-xs-10 noPadd">
<div className="description removePaddBottom">
Explore the Hasura GraphQL backend and try out some queries & mutations
</div>
</div>
</div>
<div className="appStack removePaddBottom">
<div className="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth">
<i className="fas fa-check-square checkBox"></i>
</div>
<div className="col-md-11 col-sm-11 col-xs-10 noPadd">
<div className="description removePaddBottom">
What you get...
</div>
<div className="addPaddTop">
<div className="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth">
<i className="em em-hammer_and_wrench"></i>
</div>
<div className="col-md-11 col-sm-11 col-xs-10 noPadd">
<div className="description removePaddBottom">
Source code
</div>
</div>
</div>
<div className="addPaddTop">
<div className="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth">
<i className="em em-closed_lock_with_key"></i>
</div>
<div className="col-md-11 col-sm-11 col-xs-10 noPadd">
<div className="description removePaddBottom">
Access to GraphQL Backend
</div>
</div>
</div>
<div className="addPaddTop">
<div className="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth">
<i className="em em-zap" />
</div>
<div className="col-md-11 col-sm-11 col-xs-10 noPadd">
<div className="description removePaddBottom">
Full Tutorial
</div>
</div>
</div>
</div>
</div>
</div>
<div className="formGroupWrapper">
<div className="input-group inputGroup">
<input
type="text"
className="form-control"
placeholder="Enter your username"
value={props.username}
onChange = {(e) => props.setUsername(e.target.value)}
onKeyPress={(key) => handleKeyPress(key, insert_user, loading)}
disabled={loading}
</div>
<div className="appStack">
<div className="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth">
<i className="em em-rocket" />
</div>
<div className="col-md-11 col-sm-11 col-xs-10 noPadd">
<div className="description">Powered by</div>
<div className="appStackIconWrapper">
<div className="col-md-4 col-sm-4 col-xs-4 noPadd">
<div className="appStackIcon">
<img
className="img-responsive"
src={apolloLogo}
alt="apollo logo"
/>
<div className="input-group-append groupAppend">
<button
className="btn btn-outline-secondary"
type="submit"
onClick={(e) => {
e.preventDefault();
if (props.username.match(/^[a-z0-9_-]{3,15}$/g)) {
insert_user();
} else {
alert("Invalid username. Spaces and special characters not allowed. Please try again");
props.setUsername('');
}
}}
disabled={loading || props.username === ''}
>
{ loading ? 'Please wait ...' : 'Get Started'}
</button>
</div>
</div>
</div>
{/*
<div className="col-md-4 col-sm-4 col-xs-4 noPadd">
<div className="appStackIcon">
<img
className="img-responsive"
src={hasuraLogo}
alt="Hasura logo"
/>
</div>
</div>
</div>
</div>
</div>
<div className="appStack">
<div className="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth">
<i className="em em-sunglasses" />
</div>
<div className="col-md-11 col-sm-11 col-xs-10 noPadd">
<div className="description removePaddBottom">
Explore the Hasura GraphQL backend and try out some queries
& mutations
</div>
</div>
</div>
<div className="appStack removePaddBottom">
<div className="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth">
<i className="fas fa-check-square checkBox"></i>
</div>
<div className="col-md-11 col-sm-11 col-xs-10 noPadd">
<div className="description removePaddBottom">
What you get...
</div>
<div className="addPaddTop">
<div className="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth">
<i className="em em-hammer_and_wrench"></i>
</div>
<div className="col-md-11 col-sm-11 col-xs-10 noPadd">
<div className="description removePaddBottom">
Source code
</div>
</div>
</div>
<div className="addPaddTop">
<div className="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth">
<i className="em em-closed_lock_with_key"></i>
</div>
<div className="col-md-11 col-sm-11 col-xs-10 noPadd">
<div className="description removePaddBottom">
Access to GraphQL Backend
</div>
</div>
</div>
<div className="addPaddTop">
<div className="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth">
<i className="em em-zap" />
</div>
<div className="col-md-11 col-sm-11 col-xs-10 noPadd">
<div className="description removePaddBottom">
Full Tutorial
</div>
</div>
</div>
</div>
</div>
</div>
<div className="formGroupWrapper">
<div className="input-group inputGroup">
<input
type="text"
className="form-control"
placeholder="Enter your username"
value={props.username}
onChange={(e) => props.setUsername(e.target.value)}
onKeyPress={(key) =>
handleKeyPress(key, addUserHandler, loading)
}
disabled={loading}
/>
<div className="input-group-append groupAppend">
<button
className="btn btn-outline-secondary"
type="submit"
onClick={(e) => {
e.preventDefault();
if (props.username.match(/^[a-z0-9_-]{3,15}$/g)) {
addUserHandler();
} else {
alert(
'Invalid username. Spaces and special characters not allowed. Please try again'
);
props.setUsername('');
}
}}
disabled={loading || props.username === ''}
>
{loading ? 'Please wait ...' : 'Get Started'}
</button>
</div>
</div>
</div>
{/*
<div className="footer">
Built with
<i className="fas fa-heart" />
@ -213,23 +204,19 @@ const LandingPage = (props) => {
</a>
</div>
*/}
</div>
<div className="tutorialImg col-md-6 col-sm-6 col-xs-12 hidden-xs noPadd">
<img className="img-responsive" src={rightImg} alt="View" />
</div>
</div>
</div>
</div>
);
}
}
</Mutation>
</div>
<div className="tutorialImg col-md-6 col-sm-6 col-xs-12 hidden-xs noPadd">
<img className="img-responsive" src={rightImg} alt="View" />
</div>
</div>
</div>
</div>
);
}
};
LandingPage.propTypes = {
auth: PropTypes.object,
isAuthenticated: PropTypes.bool
isAuthenticated: PropTypes.bool,
};
export default LandingPage;

View File

@ -1,66 +1,35 @@
import { ApolloConsumer } from 'react-apollo';
import React from 'react';
import { useState } from 'react';
import { ApolloConsumer } from '@apollo/client';
import Chat from './Chat';
// import Login from './Login';
import LandingPage from './LandingPage';
import '../App.css';
export default class Main extends React.Component {
constructor() {
super();
this.state = {
isLoggedIn: false,
username:"",
userId: null
};
}
// set username
setUsername = (username) => {
this.setState({
username
})
}
export default function Main() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [username, setUsername] = useState('');
const [userId, setUserId] = useState(null);
// check usernme and perform login
login = (id) => {
this.setState({
isLoggedIn: true,
userId: id
})
}
const login = (id) => {
setIsLoggedIn(true);
setUserId(id);
};
render() {
const { username, isLoggedIn, userId } = this.state;
// Login if not logged in and head to chat
return (
<div className="app">
{
!isLoggedIn ? (
<LandingPage
setUsername={this.setUsername}
login={this.login}
username={username}
/>
) : (
<ApolloConsumer>
{
(client) => {
return (
<Chat
userId={userId}
username={username}
client={client}
/>
);
}
}
</ApolloConsumer>
)
}
</div>
)
}
};
return (
<div className="app">
{!isLoggedIn ? (
<LandingPage
setUsername={setUsername}
login={login}
username={username}
/>
) : (
<ApolloConsumer>
{(client) => {
return <Chat userId={userId} username={username} client={client} />;
}}
</ApolloConsumer>
)}
</div>
);
}

View File

@ -1,39 +1,26 @@
import React from 'react';
import '../App.js';
import '../App.css';
import moment from 'moment';
export default class MessageList extends React.Component {
render() {
const { isNew } = this.props;
return (
<div className={isNew ? "messageWrapperNew" : "messageWrapper"}>
{
this.props.messages.map((m, i) => {
return (
<div key={m.id} className="message">
<div className="messageNameTime">
<div className="messageName">
<b>{m.username}</b>
</div>
<div className="messsageTime">
<i>{moment(m.timestamp).fromNow()} </i>
</div>
</div>
<div className="messageText">
{m.text }
</div>
export default function MessageList(props) {
return (
<div className={props.isNew ? 'messageWrapperNew' : 'messageWrapper'}>
{props.messages.map((m) => {
return (
<div key={m.id} className="message">
<div className="messageNameTime">
<div className="messageName">
<b>{m.username}</b>
</div>
);
})
}
<div
style={{ "height": 0 }}
id="lastMessage"
>
</div>
</div>
);
}
};
<div className="messsageTime">
<i>{moment(m.timestamp).fromNow()} </i>
</div>
</div>
<div className="messageText">{m.text}</div>
</div>
);
})}
<div style={{ height: 0 }} id="lastMessage"></div>
</div>
);
}

View File

@ -1,87 +1,51 @@
import React from 'react';
import { Subscription } from 'react-apollo';
import moment from 'moment';
import gql from 'graphql-tag';
import { useState } from 'react';
import { gql, useSubscription } from '@apollo/client';
const fetchOnlineUsersSubscription = gql`
subscription {
user_online (
order_by: {username:asc}
) {
user_online(order_by: { username: asc }) {
id
username
}
}
`;
class OnlineUsers extends React.Component {
function OnlineUsers() {
const [showMobileView, setMobileView] = useState(false);
constructor(props) {
super(props);
this.state = {
time: moment().subtract(10, 'seconds').format(),
refetch: null,
showMobileView: false
}
}
const { data } = useSubscription(fetchOnlineUsersSubscription);
toggleMobileView = () => {
this.setState({
showMobileView: !this.state.showMobileView
});
}
const toggleMobileView = () => {
setMobileView(!showMobileView);
};
render() {
const subscriptionData = (isMobileView) => (
<Subscription
subscription={fetchOnlineUsersSubscription}
const subscriptionData = (isMobileView) => (
<div>
<p
className={isMobileView ? 'mobileuserListHeading' : 'userListHeading'}
onClick={toggleMobileView}
>
{
({data, error, loading }) => {
if (loading) {
return null;
}
if (error) { return "Error loading online users"; }
return (
<div>
<p
className={ isMobileView ? "mobileuserListHeading" : "userListHeading"}
onClick={this.toggleMobileView}
>
Online Users ({!data.user_online ? 0 : data.user_online.length}) { isMobileView && (<i className="fa fa-angle-up"></i>)}
</p>
{
((isMobileView && this.state.showMobileView) || !isMobileView) &&
(
<ul className={isMobileView ? "mobileUserList" : "userList"}>
{
data.user_online.map((u) => {
return <li key={u.id}>{u.username}</li>
})
}
</ul>
)
}
</div>
);
}
}
</Subscription>
);
Online Users ({!data?.user_online ? 0 : data?.user_online.length}){' '}
{isMobileView && <i className="fa fa-angle-up"></i>}
</p>
{((isMobileView && showMobileView) || !isMobileView) && (
<ul className={isMobileView ? 'mobileUserList' : 'userList'}>
{data?.user_online.map((u) => {
return <li key={u.id}>{u.username}</li>;
})}
</ul>
)}
</div>
);
return (
<div>
<div className="onlineUsers hidden-xs">
{subscriptionData(false)}
</div>
<div className="mobileonlineUsers visible-xs">
{subscriptionData(true)}
</div>
return (
<div>
<div className="onlineUsers hidden-xs">{subscriptionData(false)}</div>
<div className="mobileonlineUsers visible-xs">
{subscriptionData(true)}
</div>
);
}
};
</div>
);
}
export default OnlineUsers;

View File

@ -1,24 +1,18 @@
import React from 'react';
import { Query } from 'react-apollo';
import gql from 'graphql-tag';
import { useCallback, useEffect, useState } from 'react';
import { gql, useQuery, useSubscription } from '@apollo/client';
import '../App.js';
import Banner from './Banner';
import MessageList from './MessageList';
const fetchMessages = gql`
query ($last_received_id: Int, $last_received_ts: timestamptz){
message (
order_by: {timestamp:asc}
query ($last_received_id: Int, $last_received_ts: timestamptz) {
message(
order_by: { timestamp: asc }
where: {
_and: {
id: {
_neq: $last_received_id
},
timestamp: {
_gte: $last_received_ts
}
id: { _neq: $last_received_id }
timestamp: { _gte: $last_received_ts }
}
}
) {
id
@ -29,236 +23,203 @@ const fetchMessages = gql`
}
`;
export default class RenderMessages extends React.Component {
constructor() {
super();
this.state = {
messages: [],
newMessages: [],
error: null,
const subscribeToNewMessages = gql`
subscription {
message(order_by: { id: desc }, limit: 1) {
id
username
text
timestamp
}
}
`;
async componentWillMount() {
// set mutation callback to update messages in state after mutation
this.props.setMutationCallback(this.mutationCallback);
}
export default function RenderMessages({
setMutationCallback,
username,
userId,
}) {
const [messages, setMessages] = useState([]);
const [newMessages, setNewMessages] = useState([]);
const [bottom, setBottom] = useState(true);
componentDidMount() {
// add scroll listener on mount
window.addEventListener("scroll", this.handleScroll);
}
componentDidUpdate() {
if (this.state.newMessages.length === 0) {
this.scrollToBottom();
}
}
componentWillUnmount() {
// remove scroll listener on unmount
window.removeEventListener("scroll", this.handleScroll);
}
// add old (read) messages to state
const addOldMessages = (newMessages) => {
const oldMessages = [...messages, ...newMessages];
setMessages(oldMessages);
setNewMessages([]);
};
// get appropriate query variables
getLastReceivedVars = () => {
const { messages, newMessages } = this.state;
const getLastReceivedVars = () => {
if (newMessages.length === 0) {
if (messages.length !== 0) {
return {
last_received_id: messages[messages.length - 1].id,
last_received_ts: messages[messages.length - 1].timestamp
}
last_received_ts: messages[messages.length - 1].timestamp,
};
} else {
return {
last_received_id: -1,
last_received_ts: "2018-08-21T19:58:46.987552+00:00"
}
last_received_ts: '2018-08-21T19:58:46.987552+00:00',
};
}
} else {
return {
last_received_id: newMessages[newMessages.length - 1].id,
last_received_ts: newMessages[newMessages.length - 1].timestamp
}
last_received_ts: newMessages[newMessages.length - 1].timestamp,
};
}
}
};
// add new (unread) messages to state
addNewMessages = (messages) => {
const newMessages = [...this.state.newMessages];
messages.forEach((m) => {
// do not add new messages from self
if (m.username !== this.props.username) {
newMessages.push(m);
const { loading, refetch } = useQuery(fetchMessages, {
variables: getLastReceivedVars(),
onCompleted: (data) => {
const receivedMessages = data.message;
// load all messages to state in the beginning
if (receivedMessages.length !== 0) {
if (messages.length === 0) {
addOldMessages(receivedMessages);
}
}
});
this.setState({
newMessages
})
}
},
});
// add old (read) messages to state
addOldMessages = (messages) => {
const oldMessages = [ ...this.state.messages, ...messages];
this.setState({
messages: oldMessages,
newMessages: []
})
}
// add message to state when text is entered
mutationCallback = (message) => {
const messages = [ ...this.state.messages, ...this.state.newMessages ];
messages.push(message);
this.setState({
messages,
newMessages: []
});
}
// custom refetch to be passed to parent for refetching on event occurance
refetch = async() => {
if (!this.state.loading) {
const resp = await this.state.refetch(this.getLastReceivedVars());
if (resp.data) {
if (!this.isViewScrollable()) {
this.addOldMessages(resp.data.message);
} else {
if (this.state.bottom) {
this.addOldMessages(resp.data.message);
useSubscription(subscribeToNewMessages, {
onSubscriptionData: async () => {
if (!loading) {
const resp = await refetch(getLastReceivedVars());
if (resp.data) {
if (!isViewScrollable()) {
addOldMessages(resp.data.message);
} else {
this.addNewMessages(resp.data.message);
if (bottom) {
addOldMessages(resp.data.message);
} else {
addNewMessages(resp.data.message);
}
}
}
}
}
}
},
});
// scroll to bottom
scrollToBottom = () => {
document.getElementById('lastMessage').scrollIntoView({ behavior: "instant" });
}
const scrollToBottom = () => {
document
?.getElementById('lastMessage')
?.scrollIntoView({ behavior: 'instant' });
};
// scroll to the new message
scrollToNewMessage = () => {
document.getElementById('newMessage').scrollIntoView({ behavior: "instant" });
const scrollToNewMessage = () => {
document
?.getElementById('newMessage')
?.scrollIntoView({ behavior: 'instant' });
};
if (newMessages.length === 0) {
scrollToBottom();
}
// add message to state when text is entered
const mutationCallback = useCallback(() => {
return (newMessage) => {
const allMessages = [...messages, ...newMessages];
allMessages.push(newMessage);
setMessages(messages);
setNewMessages([]);
};
}, [messages, newMessages]);
// scroll handler
handleScroll = (e) => {
const windowHeight = "innerHeight" in window ? window.innerHeight : document.documentElement.offsetHeight;
const body = document.getElementById("chatbox");
const html = document.documentElement;
const docHeight = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight);
const windowBottom = windowHeight + window.pageYOffset;
if (windowBottom >= docHeight) {
this.setState({
bottom: true
})
} else {
if (this.state.bottom) {
this.setState({
bottom: false
});
const handleScroll = useCallback(() => {
return (e) => {
const windowHeight =
'innerHeight' in window
? window.innerHeight
: document.documentElement.offsetHeight;
const body = document.getElementById('chatbox');
const html = document.documentElement;
const docHeight = Math.max(
body.scrollHeight,
body.offsetHeight,
html.clientHeight,
html.scrollHeight,
html.offsetHeight
);
const windowBottom = windowHeight + window.pageYOffset;
if (windowBottom >= docHeight) {
setBottom(true);
} else {
if (bottom) {
setBottom(false);
}
}
}
}
};
}, [bottom]);
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [handleScroll]);
useEffect(() => {
setMutationCallback(mutationCallback);
}, [setMutationCallback, mutationCallback]);
// add new (unread) messages to state
const addNewMessages = (incomingMessages) => {
const allNewMessages = [...newMessages];
incomingMessages.forEach((m) => {
// do not add new messages from self
if (m.username !== username) {
allNewMessages.push(m);
}
});
setNewMessages(newMessages);
};
// check if the view is scrollable
isViewScrollable = () => {
const isViewScrollable = () => {
const isInViewport = (elem) => {
const bounding = elem.getBoundingClientRect();
return (
bounding.top >= 0 &&
bounding.left >= 0 &&
bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
bounding.right <= (window.innerWidth || document.documentElement.clientWidth)
bounding.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
bounding.right <=
(window.innerWidth || document.documentElement.clientWidth)
);
};
if (document.getElementById('lastMessage')) {
return !isInViewport(document.getElementById('lastMessage'));
}
return false;
}
};
render() {
const { messages, newMessages, bottom } = this.state;
// set refetch in parent component for refetching data whenever an event occurs
if (!this.props.refetch && this.state.refetch) {
this.props.setRefetch(this.refetch);
}
return (
<div id="chatbox">
<Query
query={fetchMessages}
variables={this.getLastReceivedVars()}
>
{
({ data, loading, error, refetch}) => {
if (loading) {
return null;
}
if (error) {
return "Error: " + error;
}
// set refetch in local state to make a custom refetch
if (!this.state.refetch) {
this.setState({
refetch
});
}
const receivedMessages = data.message;
// load all messages to state in the beginning
if (receivedMessages.length !== 0) {
if (messages.length === 0) {
this.addOldMessages(receivedMessages);
}
}
// return null; real rendering happens below
return null;
}
}
</Query>
{ /* show "unread messages" banner if not at bottom */}
{
(!bottom && newMessages.length > 0 && this.isViewScrollable()) ?
<Banner
scrollToNewMessage={this.scrollToNewMessage}
numOfNewMessages={newMessages.length}
/> : null
}
{ /* Render old messages */}
<MessageList
messages={messages}
isNew={false}
username={this.props.username}
return (
<div id="chatbox">
{/* show "unread messages" banner if not at bottom */}
{!bottom && newMessages.length > 0 && isViewScrollable() ? (
<Banner
scrollToNewMessage={scrollToNewMessage}
numOfNewMessages={newMessages.length}
/>
{ /* Show old/new message separation */}
<div
id="newMessage"
className="oldNewSeparator"
>
{
newMessages.length !== 0 ?
"New messages" :
null
}
) : null}
</div>
{ /* render new messages */}
<MessageList
messages={newMessages}
isNew={true}
username={this.props.username}
/>
{ /* Bottom div to scroll to */}
{/* 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}
</div>
);
}
{/* render new messages */}
<MessageList messages={newMessages} isNew={true} username={username} />
{/* Bottom div to scroll to */}
</div>
);
}

View File

@ -1,132 +1,99 @@
import React from 'react';
import { Mutation } from 'react-apollo';
import gql from 'graphql-tag';
import { useState } from 'react';
import { gql, useMutation } from '@apollo/client';
import TypingIndicator from './TypingIndicator';
import '../App.css';
const insertMessage = gql`
mutation insert_message ($message: message_insert_input! ){
insert_message (
objects: [$message]
) {
returning {
id
timestamp
text
username
}
mutation insert_message($message: message_insert_input!) {
insert_message_one(object: $message) {
id
timestamp
text
username
}
}
`;
const emitTypingEvent = gql`
mutation ($userId: Int) {
update_user (
_set: {
last_typed: "now()"
}
where: {
id: {
_eq: $userId
}
}
const emitTypingEventGql = gql`
mutation ($userId: Int!) {
update_user_by_pk(
pk_columns: { id: $userId }
_set: { last_typed: "now()" }
) {
affected_rows
id
}
}
`;
export default class Textbox extends React.Component {
export default function Textbox(props) {
const [text, setText] = useState('');
constructor(props) {
super()
this.state = {
text: ""
}
}
const [insertMessageHandler, { client }] = useMutation(insertMessage, {
variables: {
message: {
username: props.username,
text: text,
},
update: (cache, { data: { insert_message_one } }) => {
props.mutationCallback({
id: insert_message_one.id,
timestamp: insert_message_one.timestamp,
username: insert_message_one.username,
text: insert_message_one.text,
});
},
},
});
handleTyping = (text, mutate) => {
const handleTyping = (text, mutate) => {
const textLength = text.length;
if ((textLength !== 0 && textLength % 5 === 0) || textLength === 1) {
this.emitTypingEvent(mutate);
emitTypingEvent(mutate);
}
this.setState({ text });
}
setText(text);
};
emitTypingEvent = async (mutate) => {
if (this.props.userId) {
await mutate({
mutation: emitTypingEvent,
variables: {
userId: this.props.userId
}
});
}
}
render() {
// Mutation component. Add message to the state of <RenderMessages> after mutation.
return (
<Mutation
mutation={insertMessage}
variables={{
message: {
username: this.props.username,
text: this.state.text
}
}}
update={(cache, { data: { insert_message }}) => {
this.props.mutationCallback(
{
id: insert_message.returning[0].id,
timestamp: insert_message.returning[0].timestamp,
username: insert_message.returning[0].username,
text: insert_message.returning[0].text,
}
);
}}
>
{
(insert_message, { data, loading, error, client}) => {
const sendMessage = (e) => {
e.preventDefault();
if (this.state.text === '') {
return;
}
insert_message();
this.setState({
text: ""
});
}
return this.form(sendMessage, client);
}
}
</Mutation>
)
}
form = (sendMessage, client) => {
const form = (sendMessage, client) => {
return (
<form onSubmit={sendMessage}>
<div className="textboxWrapper">
<TypingIndicator userId={this.props.userId} />
<TypingIndicator userId={props.userId} />
<input
id="textbox"
className="textbox typoTextbox"
value={this.state.text}
value={text}
autoFocus={true}
onChange={(e) => {
this.handleTyping(e.target.value, client.mutate);
handleTyping(e.target.value, client.mutate);
}}
autoComplete="off"
/>
<button
className="sendButton typoButton"
onClick={sendMessage}
> Send </button>
<button className="sendButton typoButton" onClick={sendMessage}>
{' '}
Send{' '}
</button>
</div>
</form>
);
}
};
const emitTypingEvent = async (mutate) => {
if (props.userId) {
await mutate({
mutation: emitTypingEventGql,
variables: {
userId: props.userId,
},
});
}
};
const sendMessage = (e) => {
e.preventDefault();
if (text === '') {
return;
}
insertMessageHandler();
setText('');
};
return form(sendMessage, client);
}

View File

@ -1,51 +1,39 @@
import React from 'react';
import { Subscription } from 'react-apollo';
import gql from 'graphql-tag';
import { gql, useSubscription } from '@apollo/client';
import '../App.css';
const getUserTyping = gql`
subscription ($selfId: Int ) {
user_typing (
where: {
id: {
_neq: $selfId
}
},
subscription ($selfId: Int) {
user_typing(
where: { id: { _neq: $selfId } }
limit: 1
order_by: {last_typed:desc}
){
order_by: { last_typed: desc }
) {
last_typed
username
}
}
`;
class TypingIndicator extends React.Component {
render() {
return (
<div className="typingIndicator">
<Subscription
subscription={getUserTyping}
variables={{
selfId: this.props.userId
}}
>
{
({ data, loading, error}) => {
if (loading) { return ""; }
if (error) { return ""; }
if (data.user_typing.length === 0) {
return "";
} else {
return `${data.user_typing[0].username} is typing ...`;
}
}
}
</Subscription>
</div>
)
function TypingIndicator(props) {
const { data, loading, error } = useSubscription(getUserTyping, {
variables: {
selfId: props.userId,
},
});
if (loading) {
return '';
}
};
if (error) {
return '';
}
return (
<div className="typingIndicator">
{data?.user_typing?.length === 0
? ''
: `${data.user_typing[0].username} is typing ...`}
</div>
);
}
export default TypingIndicator;

View File

@ -0,0 +1,22 @@
// Credit to Dan Abramov https://overreacted.io/making-setinterval-declarative-with-react-hooks/
import { useEffect, useRef } from 'react';
export function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}

View File

@ -1,31 +1,77 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloProvider } from 'react-apollo';
import App from './App';
import ApolloClient from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { WebSocketLink } from 'apollo-link-ws';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { split } from 'apollo-link';
import { getMainDefinition } from 'apollo-utilities';
import {
ApolloClient,
InMemoryCache,
ApolloProvider,
split,
HttpLink,
} from '@apollo/client';
import { getMainDefinition } from '@apollo/client/utilities';
import { ApolloLink, Observable } from '@apollo/client/core';
import { print } from 'graphql';
import { createClient } from 'graphql-ws';
import reportWebVitals from './reportWebVitals';
// Create graphql-ws Apollo adapter
class WebSocketLink extends ApolloLink {
constructor(options) {
super();
this.client = createClient(options);
}
request(operation) {
return new Observable((sink) => {
return this.client.subscribe(
{ ...operation, query: print(operation.query) },
{
next: sink.next.bind(sink),
complete: sink.complete.bind(sink),
error: (err) => {
if (Array.isArray(err))
// GraphQLError[]
return sink.error(
new Error(err.map(({ message }) => message).join(', '))
);
if (err instanceof CloseEvent)
return sink.error(
new Error(
`Socket closed with event ${err.code} ${err.reason || ''}` // reason will be available on clean closes only
)
);
return sink.error(err);
},
}
);
});
}
}
const scheme = (proto) => {
return window.location.protocol === 'https:' ? `${proto}s` : proto;
}
const HASURA_GRAPHQL_ENGINE_HOSTNAME = 'realtime-chat.hasura.app';
export const GRAPHQL_ENDPOINT = `${scheme('http')}://${HASURA_GRAPHQL_ENGINE_HOSTNAME}/v1/graphql`;
export const WEBSOCKET_ENDPOINT = `${scheme('ws')}://${HASURA_GRAPHQL_ENGINE_HOSTNAME}/v1/graphql`;
};
const HASURA_GRAPHQL_ENGINE_HOSTNAME = 'localhost:8080';
export const GRAPHQL_ENDPOINT = `${scheme(
'http'
)}://${HASURA_GRAPHQL_ENGINE_HOSTNAME}/v1/graphql`;
export const WEBSOCKET_ENDPOINT = `${scheme(
'ws'
)}://${HASURA_GRAPHQL_ENGINE_HOSTNAME}/v1/graphql`;
// Make WebSocketLink with appropriate url
const mkWsLink = (uri) => {
const splitUri = uri.split('//');
const subClient = new SubscriptionClient(
WEBSOCKET_ENDPOINT,
{ reconnect: true }
);
const subClient = {
url: WEBSOCKET_ENDPOINT,
options: {
reconnect: true,
},
};
return new WebSocketLink(subClient);
}
};
// Make HttpLink
const httpLink = new HttpLink({ uri: GRAPHQL_ENDPOINT });
@ -44,13 +90,20 @@ const link = split(
const client = new ApolloClient({
link,
cache: new InMemoryCache({
addTypename: false
})
})
addTypename: false,
}),
});
ReactDOM.render(
(<ApolloProvider client={client}>
<App />
</ApolloProvider>),
<React.StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@ -1,117 +0,0 @@
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://goo.gl/SC7cgQ'
);
});
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl);
}
});
}
}
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}

View File

@ -0,0 +1,13 @@
const reportWebVitals = (onPerfEntry) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

File diff suppressed because it is too large Load Diff