+
+
+
+
+
diff --git a/client/src/components/message/ChatMessageText.vue b/client/src/components/message/ChatMessageText.vue
new file mode 100644
index 00000000..e881d42d
--- /dev/null
+++ b/client/src/components/message/ChatMessageText.vue
@@ -0,0 +1,38 @@
+
+
+ {{ content }}
+
+ Show more
+
+
+
+
+
+
+
diff --git a/client/src/components/message/ChatMessageVideo.vue b/client/src/components/message/ChatMessageVideo.vue
new file mode 100644
index 00000000..0ba8689f
--- /dev/null
+++ b/client/src/components/message/ChatMessageVideo.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
diff --git a/client/src/main.js b/client/src/main.js
index 4f670a37..d7e0dc34 100644
--- a/client/src/main.js
+++ b/client/src/main.js
@@ -3,8 +3,9 @@ import VueAxios from 'vue-axios';
import App from './App.vue';
import router from './router';
import store from './store';
-import axios from './plugin/axios';
-import apiClient from './plugin/api-client';
+import axios from './plugins/axios';
+import apiClient from './plugins/api-client';
+import vuetify from './plugins/vuetify';
Vue.use(apiClient, axios);
Vue.use(VueAxios, axios);
@@ -14,5 +15,6 @@ new Vue({
router,
store,
axios,
+ vuetify,
render: (h) => h(App),
}).$mount('#app');
diff --git a/client/src/plugin/api-client.js b/client/src/plugins/api-client.js
similarity index 57%
rename from client/src/plugin/api-client.js
rename to client/src/plugins/api-client.js
index 2830b567..d53c26da 100644
--- a/client/src/plugin/api-client.js
+++ b/client/src/plugins/api-client.js
@@ -1,3 +1,5 @@
+import store from '@/store';
+
class ApiClient {
constructor(axiosInstance) {
this.axiosInstance = axiosInstance;
@@ -5,9 +7,9 @@ class ApiClient {
/**
* Retrieve the list of chats that match the filter.
- * @param username
- * @param password
- * @returns {*}
+ * @param username {String} the username
+ * @param password {String} the password
+ * @returns {Promise>} The promise with the response
*/
login(username, password) {
return this.axiosInstance.post('/auth/login', { username, password });
@@ -28,6 +30,14 @@ class ApiClient {
});
}
+ /**
+ * Refresh the access token.
+ * @returns {Promise>} The promise with the response
+ */
+ refreshToken() {
+ return this.axiosInstance.post('/auth/refresh');
+ }
+
/**
* Retrieve the list of users that match the filter.
* @param filter {string} The filter to apply
@@ -48,15 +58,37 @@ class ApiClient {
/**
* Create a message in a chat.
- * @param chatID {string} The ID of the chat
+ * @param chatId {string} The ID of the chat
* @param message {Message} The message to create
* @returns {Promise>} The promise with the response
*/
- sendMessage(chatID, message) {
- return this.axiosInstance.post(`/api/chats/${chatID}/messages`, {
+ sendMessage(chatId, message) {
+ return this.axiosInstance.post(`/api/chats/${chatId}/messages`, {
message,
});
}
+
+ /**
+ * Send a file in a chat.
+ * @param chatId {string} The ID of the chat
+ * @param file {File} The file to send
+ * @param type {string} The type of the file: image / video / file
+ * @returns {Promise>} The promise with the response
+ */
+ async sendFile(chatId, file, type) {
+ // before sending the file, it's best to refresh the token
+ // to avoid the token to expire while the file is being uploaded
+ await store.dispatch('refreshToken');
+ const formData = new FormData();
+ formData.append(type, file);
+ formData.append('chatId', chatId);
+ // console.log(chatId, file, type, formData);
+ return this.axiosInstance.post(`/upload/${type}`, formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+ }
}
export default {
diff --git a/client/src/plugin/axios.js b/client/src/plugins/axios.js
similarity index 100%
rename from client/src/plugin/axios.js
rename to client/src/plugins/axios.js
diff --git a/client/src/plugins/vuetify.js b/client/src/plugins/vuetify.js
new file mode 100644
index 00000000..71386cb1
--- /dev/null
+++ b/client/src/plugins/vuetify.js
@@ -0,0 +1,13 @@
+import Vue from 'vue';
+import Vuetify from 'vuetify/lib/framework';
+
+Vue.use(Vuetify);
+
+export default new Vuetify({
+ theme: {
+ options: {
+ customProperties: true,
+ },
+ themes: {},
+ },
+});
diff --git a/client/src/router/index.js b/client/src/router/index.js
index a8860577..f4266f9c 100644
--- a/client/src/router/index.js
+++ b/client/src/router/index.js
@@ -8,12 +8,18 @@ const routes = [
{
path: '/login',
name: 'Login',
+ meta: {
+ layout: true,
+ },
component: () =>
import(/* webpackChunkName: "login" */ '../views/Login.vue'),
},
{
path: '/signup',
name: 'Signup',
+ meta: {
+ layout: true,
+ },
component: () =>
import(/* webpackChunkName: "signup" */ '../views/Signup.vue'),
},
@@ -27,6 +33,12 @@ const routes = [
component: () =>
import(/* webpackChunkName: "container" */ '../views/AppContainer.vue'),
},
+ {
+ path: '/about',
+ name: 'AboutUs',
+ component: () =>
+ import(/* webpackChunkName: "about" */ '../views/AboutUs.vue'),
+ },
];
const router = new VueRouter({
@@ -38,11 +50,14 @@ const router = new VueRouter({
/**
* Middleware to check if user is authenticated before accessing a route
*/
-router.beforeEach((to, from, next) => {
- if (
- to.matched.some((record) => record.meta.requiresAuth) &&
- !store.getters.isLoggedIn
- ) {
+router.beforeEach(async (to, from, next) => {
+ await store.restored;
+ const requiresAuth = to.matched.some((record) => record.meta.requiresAuth);
+
+ if (requiresAuth && store.getters.isLoggedIn) {
+ await store.dispatch('refreshToken');
+ }
+ if (requiresAuth && !store.getters.isLoggedIn) {
next('/login');
} else {
next();
diff --git a/client/src/store/index.js b/client/src/store/index.js
index 268a9407..c3d79514 100644
--- a/client/src/store/index.js
+++ b/client/src/store/index.js
@@ -1,5 +1,19 @@
import Vue from 'vue';
import Vuex from 'vuex';
+import VuexPersistence from 'vuex-persist';
+import localforage from 'localforage';
+import router from '../router';
+
+const vuexLocal = new VuexPersistence({
+ storage: localforage,
+ reducer: (state) => ({
+ // Only save the state of the module 'auth'
+ isLoggedIn: state.isLoggedIn,
+ user: state.user,
+ theme: state.theme,
+ }),
+ asyncStorage: true,
+});
Vue.use(Vuex);
@@ -8,11 +22,16 @@ export default new Vuex.Store({
isLoggedIn: false,
user: null,
socket: null,
+ activeChat: null,
+ theme: null,
},
getters: {
isLoggedIn: (state) => state.isLoggedIn,
user: (state) => state.user,
socket: (state) => state.socket,
+ activeChat: (state) => state.activeChat,
+ isMobile: () => window.innerWidth < 600,
+ theme: (state) => state.theme,
},
mutations: {
login(state, { user }) {
@@ -22,10 +41,19 @@ export default new Vuex.Store({
logout(state) {
state.isLoggedIn = false;
state.user = null;
+ router.push('/login').catch(() => {});
+ console.log('logout');
+ if (state.socket) state.socket.disconnect();
},
setSocket(state, { socket }) {
state.socket = socket;
},
+ setActiveChat(state, { chat }) {
+ state.activeChat = chat;
+ },
+ setTheme(state, { theme }) {
+ state.theme = theme;
+ },
},
actions: {
login({ commit }, { username, password }) {
@@ -44,6 +72,14 @@ export default new Vuex.Store({
signup({ commit }, { username, email, password }) {
return this._vm.$api.signup(email, username, password);
},
+ async refreshToken({ commit }) {
+ try {
+ await this._vm.$api.refreshToken();
+ } catch (error) {
+ commit('logout');
+ }
+ },
},
modules: {},
+ plugins: [vuexLocal.plugin],
});
diff --git a/client/src/utils.js b/client/src/utils.js
new file mode 100644
index 00000000..dc05814b
--- /dev/null
+++ b/client/src/utils.js
@@ -0,0 +1,10 @@
+export function formatTime(time) {
+ const datetime = new Date(time);
+ const hours = datetime.getHours().toString().padStart(2, '0');
+ const minutes = datetime.getMinutes().toString().padStart(2, '0');
+ return `${hours}:${minutes}`;
+}
+
+export function getFilePath(folder, filename) {
+ return `/media/${folder}/${filename}`;
+}
diff --git a/client/src/views/AboutUs.vue b/client/src/views/AboutUs.vue
new file mode 100644
index 00000000..9b8991e0
--- /dev/null
+++ b/client/src/views/AboutUs.vue
@@ -0,0 +1,229 @@
+
+
+
+
HandShake
+ About us
+
+
+
+
Hand Shake
+
Connecting the world, one Shake at a time.
+
+
+
+ HandShake is a brand-new messaging service for everyone who is tired
+ of the usual old chat apps. Scroll down to learn more about us.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The messaging app for creative people, for fun people, for people who
+ love sharing pictures of their pets.
+
+
+
+
+
+
+
+
+
+
+ Connect with people around the world and start messaging now!
+
+
+ With HandShake, keeping in contact is as easy as ever.
+
+
+ Text messages, pictures, videos, audio and documents: we got you!
+ Share the things you love with your family, friends and even with
+ your enemies (we hope you don't have any).
+ We'll make sure the texts reach them, and that no one else will ever
+ access them.
+ Voice calls and video calls will be supported in a future update.
+
+
+
+
+
+
+
+ A new generation of chat, for a new generation of users.
+
+
Texting has never been more fun.
+
+ Play games directly in the chat and challenge your friends to see
+ who the real MVP is.
+