<repo_name>Zertz/sherby-messenger<file_sep>/client/pages/index.js
import App from "../components/App";
import Head from "../components/Head";
import Messenger from "../components/Messenger";

export default () => (
  <App>
    <Head>
      <title>Sherby Messenger</title>
    </Head>
    <Messenger />
  </App>
);
<file_sep>/server/index.js
if (process.env.NODE_ENV !== "production") {
  require("dotenv-safe").config();
}

const crypto = require("crypto");
const http = require("http");
const { MongoClient, ObjectId } = require("mongodb");

const db = process.env.SHERBY_MESSENGER_MONGODB_DB;

let client;
let watcher;

(async () => {
  try {
    client = await MongoClient.connect(
      process.env.SHERBY_MESSENGER_MONGODB_URI,
      {
        useNewUrlParser: true
      }
    );
  } catch (e) {
    console.error(e);
  }
})();

const server = http.createServer();

const io = require("socket.io")(server);

let connectedUsers = 0;

io.on("connection", socket => {
  if (!watcher) {
    watcher = client
      .db(db)
      .collection("Messages")
      .watch({ fullDocument: "updateLookup" })
      .on("change", ({ documentKey, fullDocument, operationType }) => {
        switch (operationType) {
          case "delete": {
            socket.broadcast.emit("message:delete", documentKey._id);

            break;
          }
          case "insert": {
            socket.broadcast.emit("message:create", fullDocument);

            break;
          }
          case "update": {
            socket.broadcast.emit("message:update", fullDocument);

            break;
          }
        }
      });
  }

  socket.emit("user:connect", ++connectedUsers);
  socket.emit("user:token", crypto.randomBytes(32).toString("hex"));

  socket.broadcast.emit("user:connect", connectedUsers);

  socket.once("disconnect", () => {
    socket.broadcast.emit("user:connect", --connectedUsers);
  });

  socket.on("message:create", async data => {
    try {
      const { message, token } = JSON.parse(data);

      if (!token) {
        return;
      }

      const trimmedMessage = message.trim();

      if (trimmedMessage.length === 0) {
        return;
      }

      await client
        .db(db)
        .collection("Messages")
        .insertOne({ message: trimmedMessage, token, createdAt: new Date() });
    } catch (e) {
      console.error(e);
    }
  });

  socket.on("message:read", async () => {
    try {
      const messages = await client
        .db(db)
        .collection("Messages")
        .find()
        .sort({
          createdAt: 1
        })
        .limit(50)
        .toArray();

      socket.emit("message:read", JSON.stringify(messages));
    } catch (e) {
      console.error(e);
    }
  });

  socket.on("message:update", async data => {
    try {
      const { _id, message, token } = JSON.parse(data);

      if (!ObjectId.isValid(_id) || !message || !token) {
        return;
      }

      await client
        .db(db)
        .collection("Messages")
        .updateOne(
          { _id: ObjectId(_id), token },
          {
            $set: {
              message,
              updatedAt: new Date()
            }
          }
        );
    } catch (e) {
      console.error(e);
    }
  });

  socket.on("message:delete", async data => {
    try {
      const { _id, token } = JSON.parse(data);

      if (!ObjectId.isValid(_id) || !token) {
        return;
      }

      await client
        .db(db)
        .collection("Messages")
        .removeOne({ _id: ObjectId(_id), token });
    } catch (e) {
      console.error(e);
    }
  });
});

server.listen(3300);
<file_sep>/mongodb/index.js
const ReplSet = require("mongodb-topology-manager").ReplSet;

(async () => {
  const bind_ip = "localhost";
  const replSet = new ReplSet(
    "mongod",
    [
      {
        options: { port: 31000, dbpath: `${__dirname}/data/db/31000`, bind_ip }
      },
      {
        options: { port: 31001, dbpath: `${__dirname}/data/db/31001`, bind_ip }
      },
      {
        options: { port: 31002, dbpath: `${__dirname}/data/db/31002`, bind_ip }
      }
    ],
    { replSet: "rs0" }
  );

  await replSet.purge();
  await replSet.start();

  console.info("MongoDB started!");
})();
<file_sep>/client/components/Messenger.js
import React, { Fragment, PureComponent } from "react";
import io from "socket.io-client";

import MessageList from "./MessageList";

class Messenger extends PureComponent {
  state = { connectedUsers: 0, message: "", socket: null, token: null };

  componentDidMount() {
    const socket = io(process.env.SHERBY_MESSENGER_WS_URL);
    const token = window.localStorage.getItem("user:token");

    socket.on("user:connect", this.handleConnect);

    this.setState(
      {
        socket
      },
      () => {
        if (token) {
          this.handleToken(token);
        } else {
          socket.on("user:token", this.handleToken);
        }
      }
    );
  }

  componentWillUnmount() {
    const { socket } = this.state;

    socket.off("user:connect", this.handleConnect);

    socket.close();
  }

  handleChange = e => {
    const {
      target: { name, value }
    } = e;

    this.setState({ [name]: value });
  };

  handleConnect = connectedUsers => {
    this.setState({
      connectedUsers
    });
  };

  handleSubmit = e => {
    const { message, socket, token } = this.state;

    e.preventDefault();

    socket.emit("message:create", JSON.stringify({ message, token }));

    this.setState({
      message: ""
    });
  };

  handleToken = token => {
    const { socket } = this.state;

    socket.off("user:token", this.handleToken);

    localStorage.setItem("user:token", token);

    this.setState({
      token
    });
  };

  render() {
    const { connectedUsers, message, socket, token } = this.state;

    return (
      <section>
        <style jsx>{`
          section {
            display: flex;
            flex-direction: column;
            overflow: hidden;
            position: absolute;
            top: 0.5em;
            right: 0.5em;
            bottom: 0.5em;
            left: 0.5em;
          }

          .User {
            font-size: 1rem;
            margin-bottom: 0.5em;
            text-align: center;
            word-wrap: break-word;
          }

          form {
            display: flex;
            flex-shrink: 0;
          }

          input,
          button {
            border: 1px solid #ccc;
          }

          input {
            flex-grow: 1;
            font-size: 1rem;
            margin-right: 0.5em;
            padding: 1em;
          }

          button[type="submit"] {
            flex-shrink: 0;
            cursor: pointer;
            font-size: 1rem;
            padding: 1em;
          }
        `}</style>
        <div className="User">
          <p>{token ? token : "Connecting..."}</p>
          <p>{`Utilisateurs: ${connectedUsers}`}</p>
        </div>
        <MessageList socket={socket} token={token} />
        <form onSubmit={this.handleSubmit}>
          <input
            autoComplete="off"
            autoFocus
            type="text"
            name="message"
            value={message}
            onChange={this.handleChange}
          />
          <button type="submit">Envoyer</button>
        </form>
      </section>
    );
  }
}

export default Messenger;
<file_sep>/client/components/MessageList.js
import React, { PureComponent } from "react";
import io from "socket.io-client";

import Message from "./Message";

class MessageList extends PureComponent {
  state = { messages: [] };

  componentWillReceiveProps(nextProps) {
    const { socket, token } = this.props;

    if (!token && nextProps.token) {
      socket.on("message:read", this.handleRead);
      socket.emit("message:read");
    }
  }

  componentWillUnmount() {
    const { socket } = this.props;

    socket.off("message:create", this.handleCreate);
    socket.off("message:update", this.handleUpdate);
    socket.off("message:delete", this.handleDelete);
  }

  handleCreate = message => {
    this.setState(
      prevState => ({
        messages: prevState.messages.concat(message)
      }),
      () => {
        this.ref.scrollTop = this.ref.scrollHeight;
      }
    );
  };

  handleRead = messages => {
    const { socket } = this.props;

    socket.off("message:read", this.handleRead);

    this.setState(
      {
        messages: JSON.parse(messages)
      },
      () => {
        this.ref.scrollTop = this.ref.scrollHeight;

        socket.on("message:create", this.handleCreate);
        socket.on("message:update", this.handleUpdate);
        socket.on("message:delete", this.handleDelete);
      }
    );
  };

  handleUpdate = message => {
    this.setState(prevState => ({
      messages: prevState.messages.map(m => {
        if (m._id === message._id) {
          return {
            ...m,
            ...message
          };
        }

        return m;
      })
    }));
  };

  handleDelete = _id => {
    this.setState(prevState => ({
      messages: prevState.messages.filter(m => m._id !== _id)
    }));
  };

  render() {
    const { socket, token } = this.props;
    const { messages } = this.state;

    return (
      <div className="MessageList" ref={ref => (this.ref = ref)}>
        <style jsx>{`
          .MessageList {
            display: flex;
            flex-direction: column;
            align-items: flex-start;
            flex-grow: 1;
            background-color: #fefefe;
            font-size: 1rem;
            margin-bottom: 0.5em;
            overflow-x: hidden;
            overflow-y: scroll;
            padding: 0.5em;
          }

          hr {
            margin: 0.5em 0;
            padding: 0;
          }
        `}</style>
        {messages.map(message => (
          <Message
            key={message._id}
            message={message}
            socket={socket}
            token={token}
          />
        ))}
      </div>
    );
  }
}

export default MessageList;
<file_sep>/README.md
# sherby-messenger

## Getting started

1.  Install [Node LTS or Current](https://nodejs.org/en/)
1.  Install [Yarn](https://yarnpkg.com/en/docs/install)
1.  Run `yarn install`
1.  Run `yarn mongodb`
1.  Wait until it says "MongoDB started!"
1.  In another terminal, run `yarn server`
1.  In another terminal, run `yarn client`
1.  Open a browser at http://localhost:3500
<file_sep>/server/.env.example
SHERBY_MESSENGER_MONGODB_DB=
SHERBY_MESSENGER_MONGODB_URI=
