docs(examples): 1st part of the "private messaging" example

This commit is contained in:
Damien Arrachequesne
2021-02-08 01:06:20 +01:00
parent 12221f296d
commit 8b404f424b
17 changed files with 631 additions and 0 deletions

24
examples/private-messaging/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
package-lock.json

View File

@@ -0,0 +1,23 @@
# Private messaging with Socket.IO
Please read the related guide:
- [Part I](https://socket.io/get-started/private-messaging-part-1/): initial implementation
- [Part II](https://socket.io/get-started/private-messaging-part-2/): persistent user ID
- [Part III](https://socket.io/get-started/private-messaging-part-3/): persistent messages
- [Part IV](https://socket.io/get-started/private-messaging-part-4/): scaling up
## Running the frontend
```
npm install
npm run serve
```
### Running the server
```
cd server
npm install
npm start
```

View File

@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

View File

@@ -0,0 +1,43 @@
{
"name": "private-messaging",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.6.5",
"socket.io-client": "^3.1.1",
"vue": "^2.6.11"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"vue-template-compiler": "^2.6.11"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>Private messaging with Socket.IO</title>
</head>
<body>
<noscript>
<strong>We're sorry but this application doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@@ -0,0 +1,52 @@
const httpServer = require("http").createServer();
const io = require("socket.io")(httpServer, {
cors: {
origin: "http://localhost:8080",
},
});
io.use((socket, next) => {
const username = socket.handshake.auth.username;
if (!username) {
return next(new Error("invalid username"));
}
socket.username = username;
next();
});
io.on("connection", (socket) => {
// fetch existing users
const users = [];
for (let [id, socket] of io.of("/").sockets) {
users.push({
userID: id,
username: socket.username,
});
}
socket.emit("users", users);
// notify existing users
socket.broadcast.emit("user connected", {
userID: socket.id,
username: socket.username,
});
// forward the private message to the right recipient
socket.on("private message", ({ content, to }) => {
socket.to(to).emit("private message", {
content,
from: socket.id,
});
});
// notify users upon disconnection
socket.on("disconnect", () => {
socket.broadcast.emit("user disconnected", socket.id);
});
});
const PORT = process.env.PORT || 3000;
httpServer.listen(PORT, () =>
console.log(`server listening at http://localhost:${PORT}`)
);

View File

@@ -0,0 +1,14 @@
{
"name": "server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"author": "Damien Arrachequesne <damien.arrachequesne@gmail.com>",
"license": "MIT",
"dependencies": {
"socket.io": "^3.1.1"
}
}

View File

@@ -0,0 +1,61 @@
<template>
<div id="app">
<select-username
v-if="!usernameAlreadySelected"
@input="onUsernameSelection"
/>
<chat v-else />
</div>
</template>
<script>
import SelectUsername from "./components/SelectUsername";
import Chat from "./components/Chat";
import socket from "./socket";
export default {
name: "App",
components: {
Chat,
SelectUsername,
},
data() {
return {
usernameAlreadySelected: false,
};
},
methods: {
onUsernameSelection(username) {
this.usernameAlreadySelected = true;
socket.auth = { username };
socket.connect();
},
},
created() {
socket.on("connect_error", (err) => {
if (err.message === "invalid username") {
this.usernameAlreadySelected = false;
}
});
},
destroyed() {
socket.off("connect_error");
},
};
</script>
<style>
body {
margin: 0;
}
@font-face {
font-family: Lato;
src: url("/fonts/Lato-Regular.ttf");
}
#app {
font-family: Lato, Arial, sans-serif;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,147 @@
<template>
<div>
<div class="left-panel">
<user
v-for="user in users"
:key="user.userID"
:user="user"
:selected="selectedUser === user"
@select="onSelectUser(user)"
/>
</div>
<message-panel
v-if="selectedUser"
:user="selectedUser"
@input="onMessage"
class="right-panel"
/>
</div>
</template>
<script>
import socket from "../socket";
import User from "./User";
import MessagePanel from "./MessagePanel";
export default {
name: "Chat",
components: { User, MessagePanel },
data() {
return {
selectedUser: null,
users: [],
};
},
methods: {
onMessage(content) {
if (this.selectedUser) {
socket.emit("private message", {
content,
to: this.selectedUser.userID,
});
this.selectedUser.messages.push({
content,
fromSelf: true,
});
}
},
onSelectUser(user) {
this.selectedUser = user;
user.hasNewMessages = false;
},
},
created() {
socket.on("connect", () => {
this.users.forEach((user) => {
if (user.self) {
user.connected = true;
}
});
});
socket.on("disconnect", () => {
this.users.forEach((user) => {
if (user.self) {
user.connected = false;
}
});
});
const initReactiveProperties = (user) => {
user.connected = true;
user.messages = [];
user.hasNewMessages = false;
};
socket.on("users", (users) => {
users.forEach((user) => {
user.self = user.userID === socket.id;
initReactiveProperties(user);
});
// put the current user first, and sort by username
this.users = users.sort((a, b) => {
if (a.self) return -1;
if (b.self) return 1;
if (a.username < b.username) return -1;
return a.username > b.username ? 1 : 0;
});
});
socket.on("user connected", (user) => {
initReactiveProperties(user);
this.users.push(user);
});
socket.on("user disconnected", (id) => {
for (let i = 0; i < this.users.length; i++) {
const user = this.users[i];
if (user.userID === id) {
user.connected = false;
break;
}
}
});
socket.on("private message", ({ content, from }) => {
for (let i = 0; i < this.users.length; i++) {
const user = this.users[i];
if (user.userID === from) {
user.messages.push({
content,
fromSelf: false,
});
if (user !== this.selectedUser) {
user.hasNewMessages = true;
}
break;
}
}
});
},
destroyed() {
socket.off("connect");
socket.off("disconnect");
socket.off("users");
socket.off("user connected");
socket.off("user disconnected");
socket.off("private message");
},
};
</script>
<style scoped>
.left-panel {
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: 260px;
overflow-x: hidden;
background-color: #3f0e40;
color: white;
}
.right-panel {
margin-left: 260px;
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<div>
<div class="header">
<status-icon :connected="user.connected" />{{ user.username }}
</div>
<ul class="messages">
<li
v-for="(message, index) in user.messages"
:key="index"
class="message"
>
<div v-if="displaySender(message, index)" class="sender">
{{ message.fromSelf ? "(yourself)" : user.username }}
</div>
{{ message.content }}
</li>
</ul>
<form @submit.prevent="onSubmit" class="form">
<textarea v-model="input" placeholder="Your message..." class="input" />
<button :disabled="!isValid" class="send-button">Send</button>
</form>
</div>
</template>
<script>
import StatusIcon from "./StatusIcon";
export default {
name: "MessagePanel",
components: {
StatusIcon,
},
props: {
user: Object,
},
data() {
return {
input: "",
};
},
methods: {
onSubmit() {
this.$emit("input", this.input);
this.input = "";
},
displaySender(message, index) {
return (
index === 0 ||
this.user.messages[index - 1].fromSelf !==
this.user.messages[index].fromSelf
);
},
},
computed: {
isValid() {
return this.input.length > 0;
},
},
};
</script>
<style scoped>
.header {
line-height: 40px;
padding: 10px 20px;
border-bottom: 1px solid #dddddd;
}
.messages {
margin: 0;
padding: 20px;
}
.message {
list-style: none;
}
.sender {
font-weight: bold;
margin-top: 5px;
}
.form {
padding: 10px;
}
.input {
width: 80%;
resize: none;
padding: 10px;
line-height: 1.5;
border-radius: 5px;
border: 1px solid #000;
}
.send-button {
vertical-align: top;
}
</style>

View File

@@ -0,0 +1,36 @@
<template>
<div class="select-username">
<form @submit.prevent="onSubmit">
<input v-model="username" placeholder="Your username..." />
<button :disabled="!isValid">Send</button>
</form>
</div>
</template>
<script>
export default {
name: "SelectUsername",
data() {
return {
username: "",
};
},
computed: {
isValid() {
return this.username.length > 2;
},
},
methods: {
onSubmit() {
this.$emit("input", this.username);
},
},
};
</script>
<style scoped>
.select-username {
width: 300px;
margin: 200px auto 0;
}
</style>

View File

@@ -0,0 +1,27 @@
<template>
<i class="icon" :class="{ connected: connected }"></i>
</template>
<script>
export default {
name: "StatusIcon",
props: {
connected: Boolean,
},
};
</script>
<style scoped>
.icon {
height: 8px;
width: 8px;
border-radius: 50%;
display: inline-block;
background-color: #e38968;
margin-right: 6px;
}
.icon.connected {
background-color: #86bb71;
}
</style>

View File

@@ -0,0 +1,63 @@
<template>
<div class="user" @click="onClick" :class="{ selected: selected }">
<div class="description">
<div class="name">
{{ user.username }} {{ user.self ? " (yourself)" : "" }}
</div>
<div class="status">
<status-icon :connected="user.connected" />{{ status }}
</div>
</div>
<div v-if="user.hasNewMessages" class="new-messages">!</div>
</div>
</template>
<script>
import StatusIcon from "./StatusIcon";
export default {
name: "User",
components: { StatusIcon },
props: {
user: Object,
selected: Boolean,
},
methods: {
onClick() {
this.$emit("select");
},
},
computed: {
status() {
return this.user.connected ? "online" : "offline";
},
},
};
</script>
<style scoped>
.selected {
background-color: #1164a3;
}
.user {
padding: 10px;
}
.description {
display: inline-block;
}
.status {
color: #92959e;
}
.new-messages {
color: white;
background-color: red;
width: 20px;
border-radius: 5px;
text-align: center;
float: right;
margin-top: 10px;
}
</style>

View File

@@ -0,0 +1,8 @@
import Vue from "vue";
import App from "./App.vue";
Vue.config.productionTip = false;
new Vue({
render: (h) => h(App),
}).$mount("#app");

View File

@@ -0,0 +1,10 @@
import { io } from "socket.io-client";
const URL = "http://localhost:3000";
const socket = io(URL, { autoConnect: false });
socket.onAny((event, ...args) => {
console.log(event, args);
});
export default socket;