Initial Commit

This commit is contained in:
supertiger1234 2019-01-29 19:04:08 +00:00
parent dbd5c5e328
commit d2bd1b13c6
65 changed files with 15427 additions and 0 deletions

9
.eslintrc Normal file
View file

@ -0,0 +1,9 @@
{
"plugins": ["@vue"],
"rules": {
"no-console": "off"
},
"parserOptions": {
"sourceType": "module"
}
}

21
.gitignore vendored Normal file
View file

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

5
babel.config.js Normal file
View file

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

11260
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

59
package.json Normal file
View file

@ -0,0 +1,59 @@
{
"name": "chatlol",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@vue/eslint-plugin": "^4.2.0",
"axios": "^0.18.0",
"jquery": "^3.3.1",
"socket.io": "^2.2.0",
"socket.io-client": "^2.2.0",
"uws": "^10.148.1",
"vue": "^2.5.17",
"vue-headful": "^2.0.1",
"vue-mq": "^1.0.1",
"vue-recaptcha": "^1.1.1",
"vue-router": "^3.0.2",
"vue-socket.io": "^3.0.4",
"vue-socket.io-extended": "^3.2.0",
"vuex": "^3.0.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.0.5",
"@vue/cli-plugin-eslint": "^3.0.5",
"@vue/cli-service": "^3.0.5",
"babel-eslint": "^10.0.1",
"eslint": "^5.8.0",
"eslint-plugin-vue": "^5.0.0-0",
"vue-template-compiler": "^2.5.17"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {},
"parserOptions": {
"parser": "babel-eslint"
}
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

30
public/index.html Normal file
View file

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<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">
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<title>Nertivia</title>
<!-- Google recaptcha -->
<script src="https://www.google.com/recaptcha/api.js?onload=vueRecaptchaApiLoaded&render=explicit" async defer>
</script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-131765299-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-131765299-1');
</script>
</head>
<body>
<noscript>
<strong>We're sorry but Nertivia 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>

5
src/Main.vue Normal file
View file

@ -0,0 +1,5 @@
<template>
<div id="app">
<router-view></router-view>
</div>
</template>

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
src/assets/background.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
src/assets/loading.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

2
src/assets/spinner.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

16
src/assets/status/0.svg Normal file
View file

@ -0,0 +1,16 @@
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
<!-- Created with Method Draw - http://github.com/duopixel/Method-Draw/ -->
<g>
<title>background</title>
<rect fill="none" id="canvas_background" height="402" width="402" y="-1" x="-1"/>
<g display="none" overflow="visible" y="0" x="0" height="100%" width="100%" id="canvasGrid">
<rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
</g>
</g>
<g>
<title>Layer 1</title>
<ellipse stroke="#919191" ry="171" rx="171" id="svg_1" cy="199" cx="201" stroke-width="33" fill="none"/>
<line stroke="#919191" transform="rotate(45 200.0000000000001,199.99999999999997) " stroke-linecap="null" stroke-linejoin="null" id="svg_12" y2="310.075198" x2="199.999992" y1="89.924784" x1="199.999992" fill-opacity="null" stroke-opacity="null" stroke-width="33" fill="none"/>
<line stroke="#919191" transform="rotate(-45 199.99999999999991,199.99999999999994) " stroke-linecap="null" stroke-linejoin="null" id="svg_13" y2="310.075198" x2="199.999992" y1="89.924784" x1="199.999992" fill-opacity="null" stroke-opacity="null" stroke-width="33" fill="none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

16
src/assets/status/1.svg Normal file
View file

@ -0,0 +1,16 @@
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
<!-- Created with Method Draw - http://github.com/duopixel/Method-Draw/ -->
<g>
<title>background</title>
<rect fill="none" id="canvas_background" height="402" width="402" y="-1" x="-1"/>
<g display="none" overflow="visible" y="0" x="0" height="100%" width="100%" id="canvasGrid">
<rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
</g>
</g>
<g>
<title>Layer 1</title>
<ellipse stroke="#26ff4a" ry="171" rx="171" id="svg_1" cy="198.69697" cx="200.69697" stroke-width="33" fill="none"/>
<line stroke="#26ff4a" transform="rotate(45 225.0000000000001,212.29728698730466) " stroke-linecap="null" stroke-linejoin="null" id="svg_13" y2="320.251181" x2="224.999985" y1="104.343413" x1="224.999985" fill-opacity="null" stroke-width="33" fill="none"/>
<line stroke="#26ff4a" transform="rotate(-45 130.6943359374999,247.77433776855463) " stroke-linecap="null" stroke-linejoin="null" id="svg_14" y2="281.545712" x2="130.694293" y1="213.423268" x1="130.694293" fill-opacity="null" stroke-width="33" fill="none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

16
src/assets/status/2.svg Normal file
View file

@ -0,0 +1,16 @@
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
<!-- Created with Method Draw - http://github.com/duopixel/Method-Draw/ -->
<g>
<title>background</title>
<rect fill="none" id="canvas_background" height="402" width="402" y="-1" x="-1"/>
<g display="none" overflow="visible" y="0" x="0" height="100%" width="100%" id="canvasGrid">
<rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
</g>
</g>
<g>
<title>Layer 1</title>
<ellipse ry="171" rx="171" id="svg_1" cy="199" cx="200" stroke-width="33" stroke="#ffdd1e" fill="none"/>
<text xml:space="preserve" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="105" id="svg_2" y="279.4375" x="140.5" stroke-width="0" stroke="#ffdd1e" fill="#ffdd1e">Z</text>
<text xml:space="preserve" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="105" id="svg_3" y="193.4375" x="210.5" stroke-width="0" stroke="#ffdd1e" fill="#ffdd1e">Z</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1,009 B

15
src/assets/status/3.svg Normal file
View file

@ -0,0 +1,15 @@
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
<!-- Created with Method Draw - http://github.com/duopixel/Method-Draw/ -->
<g>
<title>background</title>
<rect fill="none" id="canvas_background" height="402" width="402" y="-1" x="-1"/>
<g display="none" overflow="visible" y="0" x="0" height="100%" width="100%" id="canvasGrid">
<rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
</g>
</g>
<g>
<title>Layer 1</title>
<ellipse ry="171" rx="171" id="svg_1" cy="199" cx="200" stroke-width="33" stroke="#ea0b1e" fill="none"/>
<rect stroke="#ea0b1e" id="svg_4" height="6" width="60.999999" y="197" x="169.5" stroke-width="33" fill="#ea0b1e"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 727 B

15
src/assets/status/4.svg Normal file
View file

@ -0,0 +1,15 @@
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
<!-- Created with Method Draw - http://github.com/duopixel/Method-Draw/ -->
<g>
<title>background</title>
<rect fill="none" id="canvas_background" height="402" width="402" y="-1" x="-1"/>
<g display="none" overflow="visible" y="0" x="0" height="100%" width="100%" id="canvasGrid">
<rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
</g>
</g>
<g>
<title>Layer 1</title>
<ellipse ry="171" rx="171" id="svg_1" cy="199" cx="200" stroke-width="33" stroke="#9a3dd3" fill="none"/>
<text xml:space="preserve" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="227" id="svg_3" y="278.5" x="168.46875" stroke-width="0" stroke="#ffdd1e" fill="#9a3dd3">!</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 810 B

97
src/changelog.js Normal file
View file

@ -0,0 +1,97 @@
const config = [
{
title: 'Avatar',
shortTitle: 'Avatar',
date: '29/01/2019',
new: [
'Settings page has been added.',
'You can now set your own profile picture from the settings page.',
'You can now logout within the app.'
],
next: [
'Typing status.',
]
},
{
title: 'Message limit, Chat message changes',
shortTitle: 'Message limit',
date: '25/01/2019',
new: [
'Changed the design of messages slightly and changed the font size.'
],
fix: [
'Messages now have a limit of 5000 characters.',
],
next: [
'I have decided to add profile pictures in the next update.',
]
},
{
title: 'ReeeeCaptcha :D',
shortTitle: 'ReCaptcha',
date: '23/01/2019',
new: [
'Added reCaptcha to our login and register so our website is safe from any spam accounts that could be created by bots.',
],
next: [
'Typing status or maybe profile pictures (haven\'t decided yet)',
]
},
{
title: 'Online status and fixes',
shortTitle: 'Online status and fixes',
date: '22/01/2019',
new: [
'See if your friends are online, away, busy, looking to play or offline.',
'Planned features and the latest change now shows in app.'
],
fix: [
'Messages will no longer show twice when sending.',
'Adjusted padding on some places.',
'Message font is now much smaller.'
]
},
{
title: 'Send and receive messages (experimental)',
shortTitle: 'Send and receive messages',
date: '14/01/2019',
new: [
'You can now send messages to your friends!',
],
next: [
'improving the new messaging functionality and adding typing indicators, online statuses.',
]
},
{
title: 'Public change log, Accept friends',
shortTitle: 'Public change log, Accept friends',
date: '09/01/2019',
new: [
'Added a change log so you can see how much progress is being made to Nertivia.',
'Adding friends, denying requests, accepting requests is now possible.'
],
next: [
'Ability to send messages.',
]
},
{
title: 'Issues fixed',
shortTitle: 'Issues fixed',
date: '04/01/2019',
msg: 'Tweaks have been made to the website here and there.'
},
{
title: 'Compatibility',
date: '31/12/2018',
msg: 'Website is now compatible for viewing on mobile, tablet and desktop devices.'
}
]
export default config;

18
src/clickOutside.js Normal file
View file

@ -0,0 +1,18 @@
import Vue from 'vue'
// to close popout menus when clicking outside.
Vue.directive('click-outside', {
bind: function (el, binding, vnode) {
el.clickOutsideEvent = function (event) {
// here I check that click was outside the el and his childrens
if (!(el == event.target || el.contains(event.target))) {
// and if it did, call method provided in attribute value
vnode.context[binding.expression](event);
}
};
document.body.addEventListener('click', el.clickOutsideEvent)
},
unbind: function (el) {
document.body.removeEventListener('click', el.clickOutsideEvent)
},
});

47
src/components/Button.vue Normal file
View file

@ -0,0 +1,47 @@
<template>
<button type="submit" :disabled="$props.loading">
<div class="loading-icon" v-if="$props.loading" ></div>
<div class="text">{{$props.message}}</div>
</button>
</template>
<script>
export default {
props: [
"loading",
"message"
]
}
</script>
<style scoped>
button {
display: flex;
border: none;
outline: none;
background: rgba(0, 0, 0, 0.212);
padding: 10px;
color: white;
justify-content: center;
align-items: center;
margin: auto;
margin-bottom: 10px;
transition: background-color 0.3s;
height: 40px;
}
button:hover {
background-color: rgba(0, 0, 0, 0.541);
}
button:focus {
background-color: rgba(0, 0, 0, 0.541);
}
.loading-icon {
height: 20px;
width: 20px;
background-size: 100%;
background-image: url(../assets/spinner.svg);
margin-right: 5px;
}
</style>

View file

@ -0,0 +1,154 @@
<template>
<div class="change-log">
<div class="inner">
<div class="close-button" @click="close">
<i class="material-icons">
close
</i>
</div>
<div class="change-title">Change Log <div class="changelog-icon"><i class="material-icons ">update</i></div></div>
<div class="change-list">
<div class="change" v-for="change in changelog" :key="change.title">
<div class="date">{{change.date}}</div>
<div class="changes-title">{{change.title}}</div>
<div class="information">
<div v-if="change.new">
<strong>What's new?</strong><br>
<ul>
<li v-for="(wnew, index) in change.new" :key="index">{{wnew}}</li>
</ul>
</div>
<div v-if="change.fix">
<strong>Issues fixed</strong><br>
<ul>
<li v-for="(wfix, index) in change.fix" :key="index">{{wfix}}</li>
</ul>
</div>
<div v-if="change.next">
<strong>Up next</strong><br>
<ul>
<li v-for="(wnext, index) in change.next" :key="index">{{wnext}}</li>
</ul>
</div>
<div v-if="change.msg">
{{change.msg}}
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import {bus} from './../main.js'
import changelog from '@/changelog.js'
export default {
data() {
return {
changelog
}
},
methods: {
close() {
bus.$emit('closeChangeLog')
}
}
}
</script>
<style scoped>
.change-log{
position: absolute;
background: rgba(0, 0, 0, 0.342);
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
color: white;
}
.inner{
background-color: rgba(0, 0, 0, 0.664);
margin: auto;
width: 600px;
height: 700px;
display: flex;
flex-direction: column;
}
.close-button{
margin: auto;
margin-top: 10px;
margin-right: 10px;
user-select: none;
cursor: default;
color: grey;
transition: .3s;
}
.close-button:hover{
color: white;
}
.close-button .material-icons {
font-size: 30px;
}
.change-title{
color: white;
font-size: 30px;
margin: auto;
margin-top: 10px;
user-select: none;
display: flex;
padding-bottom: 15px;
flex-shrink: 0;
}
.changelog-icon{
margin-top: 2px;
margin-left: 10px
}
.changelog-icon .material-icons {
font-size: 40px;
}
.change-list {
height: 100%;
width: 100%;
overflow: auto;
border-top: 1px solid white;
}
.change{
margin: 5px;
min-height: 100px;
border-bottom: solid 1px white;
padding-bottom: 15px;
}
.change:last-child{
border-bottom: none;
}
.date{
text-align: right;
padding-top: 10px;
padding-right: 10px;
color: grey;
}
.changes-title {
font-size: 25px;
padding-left: 15px;
color: rgba(255, 255, 255, 0.795);
}
.information {
margin: 10px;
padding-left: 20px;
}
@media (max-height: 700px) {
.inner {
height: 100%;
}
}
</style>

View file

@ -0,0 +1,25 @@
<template>
<vue-recaptcha ref="recaptcha" :sitekey="sitekey" theme="dark" @verify="submit"></vue-recaptcha>
</template>
<script>
import VueRecaptcha from 'vue-recaptcha';
import config from '@/config.js'
export default {
components: { VueRecaptcha },
data () {
return {
sitekey: config.recaptcha,
}
},
methods: {
submit(response) {
this.$emit('verify', response)
},
resetRecaptcha () {
this.$refs.recaptcha.reset() // Direct call reset method
}
}
}
</script>

View file

@ -0,0 +1,36 @@
<template>
<div class="loading-screen">
<div class="loading-animation"></div>
<div class="title">{{$props.msg}}</div>
</div>
</template>
<script>
export default {
props: [
"msg"
]
}
</script>
<style scoped>
.loading-screen{
margin: 50px;
}
.loading-animation{
height: 100px;
width: 100px;
background-size: 100%;
background-image: url(../assets/spinner.svg);
display: table;
margin: auto;
}
.title {
display: table;
margin: auto;
color: white;
font-size: 20px;
text-align: center;
}
</style>

View file

@ -0,0 +1,100 @@
<template>
<div class="connecting-screen">
<div class="center-box">
<div class="animation">
<div class="map"></div>
<div class="flash-message"></div>
</div>
<div class="message">Connecting...</div>
</div>
</div>
</template>
<style scoped>
.connecting-screen {
display: flex;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.76);
color: white;
}
.center-box{
margin: auto;
}
.animation{
height: 300px;
width: 300px;
background-color: #2CB4FF;
border-radius: 50%;
box-shadow: 0px 0px 96px -4px rgba(69,212,255,1);
overflow: hidden;
}
.map {
height: 300px;
width: 300px;
background-position: -490px center;
background-size: 170%;
background-repeat: no-repeat;
background-image: url(./../../assets/LogoAnimation/map.png);
animation: rotateGlobe;
animation-timing-function: linear;
animation-duration: 4s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
}
@keyframes rotateGlobe {
from {
background-position: -490px center;
}
to {
background-position: 300px center;
}
}
.flash-message {
height: 300px;
width: 300px;
margin-top: -300px;
background-position: center;
background-size: 50%;
background-repeat: no-repeat;
background-image: url(./../../assets/LogoAnimation/message.png);
animation: flashMessage;
animation-timing-function: linear;
animation-duration: 1s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
opacity: 0.5;
}
@keyframes flashMessage {
0% {
opacity: 0;
}
10% {
background-size: 55%;
}
30% {
background-size: 50%;
}
50% {
opacity: 0.5;
}
100% {
opacity: 0;
}
}
.message{
text-align: center;
margin-top: 20px;
font-size: 20px;
user-select: none;
}
</style>

View file

@ -0,0 +1,63 @@
<template>
<div class="left-panel">
<MyMiniInformation />
<div class="list">
<PendingFriends />
<Friends />
</div>
<AddFriendPanel/>
</div>
</template>
<script>
import MyMiniInformation from '../../components/app/MyMiniInformation.vue'
import PendingFriends from './relationships/PendingFriends.vue'
import AddFriendPanel from './relationships/AddFriendPanel.vue'
import Friends from './relationships/Friends.vue'
export default {
components: {
MyMiniInformation,
PendingFriends,
AddFriendPanel,
Friends
}
}
</script>
<style scoped>
.left-panel {
height: 100%;
background-color: rgba(0, 0, 0, 0.671);
width: 300px;
flex-shrink: 0;
display: flex;
flex-direction: column
}
.list{
margin: 10px;
flex: 1;
overflow: auto;
}
/* ------- SCROLL BAR -------*/
/* width */
.list::-webkit-scrollbar {
width: 3px;
}
/* Track */
.list::-webkit-scrollbar-track {
background: #8080806b;
}
/* Handle */
.list::-webkit-scrollbar-thumb {
background: #f5f5f559;
}
/* Handle on hover */
.list::-webkit-scrollbar-thumb:hover {
background: #f5f5f59e;
}
</style>

View file

@ -0,0 +1,135 @@
<template>
<div class="message">
<div class="profile-picture" :style="`background-image: url(${userAvatar})`"></div>
<div class="triangle">
<div class="triangle-inner"></div>
</div>
<div class="content">
<div class="username">{{this.$props.username}}</div>
<div class="content-message">{{this.$props.message}}</div>
</div>
<div class="sending-status">{{statusMessage}}</div>
</div>
</template>
<script>
import config from '@/config.js'
export default {
props: ['message', 'status', 'username', 'avatar'],
data() {
return{
userAvatar: config.domain + "/avatars/" + this.$props.avatar
}
},
computed: {
statusMessage(){
let status = this.$props.status;
if (status == 0) {
return "Sending"
} else if (status == 1) {
return "Sent"
} else if (status == 2) {
return "Failed"
} else {
return ""
}
}
}
}
</script>
<style scoped>
.message{
margin: 10px;
margin-top: 10px;
margin-bottom: 10px;
display: flex;
animation: showMessage .3s ease-in-out;
}
@keyframes showMessage {
from {
transform: translate(0px, 9px);
opacity: 0;
}
}
.profile-picture{
height: 50px;
width: 50px;
background-color: rgba(0, 0, 0, 0.281);
margin: auto;
margin-bottom: 0;
border-radius: 50%;
margin-right: 5px;
margin-left: 0;
flex-shrink: 0;
background-position: center;
background-size: 100%;
background-repeat: no-repeat;
}
.triangle{
display: flex;
justify-content: bottom;
flex-direction: column;
margin: auto;
margin-left: 0;
margin-right: 0px;
margin-bottom: 8.7px;
}
.triangle-inner{
width: 0;
height: 0;
border-top: 1px solid transparent;
border-bottom: 7px solid transparent;
border-right: 7px solid rgba(0, 0, 0, 0.301);
}
.content{
background: rgba(0, 0, 0, 0.301);
padding: 10px;
display: flex;
justify-content: center;
flex-direction: column;
border-radius: 10px;
color: rgb(231, 231, 231);
margin: auto;
margin-left: 0;
margin-right: 0;
transition: 1s;
}
.username {
color: rgb(189, 189, 189);
font-size: 14px;
}
.content-message {
word-wrap: break-word;
word-break: break-word;
white-space: pre-wrap;
font-size: 14px;
}
.message .sending-status {
display: flex;
justify-content: flex-end;
flex-direction: column;
padding-bottom: 5px;
margin-left: 10px;
font-size: 15px;
color: white;
align-self: normal;
user-select: none;
transition: 0.5;
}
</style>

View file

@ -0,0 +1,187 @@
<template>
<div class="my-mini-information">
<div class="profile-picture" :style="`background-image: url(${avatar})`"></div>
<div class="information">
<div class="name">{{user.username}}</div>
<div class="tag">@{{user.tag}}</div>
<div class="status" v-on:click="status.isPoppedOut = !status.isPoppedOut">
<img class="current-status" :src="getStatus"/>
<i class="material-icons expand-status-icon">expand_more</i>
<transition name="show-status-list">
<statusList v-if="status.isPoppedOut" v-click-outside="closeMenus" class="status-popout" />
</transition>
</div>
</div>
<div class="setting-icon" @click="openSettings">
<i class="material-icons">settings</i>
</div>
</div>
</template>
<script>
import {bus} from '../../main';
import config from '@/config.js'
import statusList from '../../components/app/statusList.vue'
import settingsService from '@/services/settingsService'
export default {
components: {
statusList
},
data() {
return {
status: {
isPoppedOut: false,
}
}
},
methods: {
closeMenus() {
this.status.isPoppedOut = false;
},
async changeStatus (status){
// emit to server to change their status.
console.log(status)
const {ok, error, result} = await settingsService.setStatus(status);
if (ok && result.data.status == true) {
this.$store.dispatch('changeStatus', result.data.set)
}
},
openSettings() {
bus.$emit('openSettings');
}
},
created() {
//When user changes their own status (statusList.vue)
bus.$on('status-change', this.changeStatus)
},
beforeDestroy(){
bus.$off('status-change', this.changeStatus)
},
computed: {
user() {
return this.$store.getters.user
},
avatar() {
return config.domain + "/avatars/" +this.$store.getters.user.avatar
},
getStatus() {
return require(`./../../assets/status/${this.$store.getters.user.status || 0}.svg`)
}
}
}
</script>
<style scoped>
.show-status-list-enter-active, .show-status-list-leave-active {
transition: .1s;
}
.show-status-list-enter, .show-status-list-leave-to {
opacity: 0;
transform: translateY(10px);
}
.fade-enter-active, .fade-leave-active {
transition: opacity .2s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
.my-mini-information {
border-bottom: solid 1px rgb(218, 218, 218);
width: 100%;
height: 80px;
display: flex;
align-items: center;
}
.profile-picture{
height: 50px;
width: 50px;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.377);
margin-left: 10px;
margin-right: 10px;
background-position: center;
background-size: 100%;
background-repeat: no-repeat;
}
.information{
color: white;
margin-top: -7px;
flex: 1;
}
.setting-icon{
color: white;
margin: auto;
margin-right: 15px;
padding: 5px;
border-radius: 50%;
cursor: default;
user-select: none;
transition: 0.3s;
}
.setting-icon:hover {
background: rgba(0, 0, 0, 0.294);
}
.setting-icon .material-icons{
display: block;
margin: auto;
}
.status {
display: inline-block;
padding-top: 1px;
padding-left: 5px;
margin-left: 10px;
transition: 0.3s;
user-select: none;
border-radius: 10px;
}
.status:hover {
background: rgba(26, 25, 25, 0.349);
}
.expand-status-icon{
opacity: 0;
transition: 0.3s;
}
.status:hover .expand-status-icon{
opacity: 1;
}
.status .current-status {
width: 20px;
height: 20px;
background-size: 100%;
background-position: center;
}
.name {
margin-top: 10px;
}
.tag {
color: rgb(199, 199, 199);
font-size: 13px;
display: inline-block;
vertical-align: top;
margin-top: 5px;
}
</style>

128
src/components/app/News.vue Normal file
View file

@ -0,0 +1,128 @@
<template>
<div class="news">
<div class="change-log">
<span class="news-title">Changes in this release</span>
<div class="change">
<div class="date">{{changelog.date}}</div>
<div class="changes-title">{{changelog.title}}</div>
<div class="information">
<div v-if="changelog.new">
<strong>What's new?</strong><br>
<ul>
<li v-for="(wnew, index) in changelog.new" :key="index">{{wnew}}</li>
</ul>
</div>
<div v-if="changelog.fix">
<strong>Issues fixed</strong><br>
<ul>
<li v-for="(wfix, index) in changelog.fix" :key="index">{{wfix}}</li>
</ul>
</div>
<div v-if="changelog.next">
<strong>Up next</strong><br>
<ul>
<li v-for="(wnext, index) in changelog.next" :key="index">{{wnext}}</li>
</ul>
</div>
<div v-if="changelog.msg">
{{changelog.msg}}
</div>
</div>
</div>
</div>
<div class="todo-list">
<span class="news-title">Planned Features</span>
<p>Features that are coming soon:</p>
<ul class="plan-list">
<li>Online, Offline status (Done)</li>
<li>Profile picture</li>
<li>Typing indicator</li>
<li>Sending files</li>
<li>Custom emojis</li>
<li>Guilds</li>
</ul>
</div>
</div>
</template>
<script>
import Spinner from '@/components/Spinner.vue'
import ChangeLog from '@/components/ChangeLog.vue'
import changelog from '@/changelog.js'
export default {
components: {
Spinner,
ChangeLog
},
data() {
return {
changelog: changelog[0]
}
}
}
</script>
<style scoped>
.news {
display: flex;
flex: 1;
margin:20px;
color: white;
overflow: auto;
}
.news-title {
display: inline-block;
margin-bottom: 10px;
font-size: 20px;
color: white;
font-weight: bold;
padding-bottom: 10px;
border-bottom: solid 1px white;
}
.todo-list{
flex: 1;
margin-left: 10px;
background: rgba(0, 0, 0, 0.137);
padding: 20px;
}
.change-log{
background: rgba(0, 0, 0, 0.137);
padding: 20px;
flex: 1;
}
.plan-list{
color: white;
}
.date{
text-align: left;
margin-right: 50px;
color: rgba(255, 255, 255, 0.692);
}
.changes-title {
font-size: 20px;
color: rgba(255, 255, 255, 0.979);
margin-bottom: 10px;
margin-top: 10px;
font-weight: bold;
}
@media (max-width: 840px) {
.news {
flex-direction: column;
}
.todo-list{
margin-left: 0;
}
.change-log {
margin-bottom: 20px;
}
}
</style>

View file

@ -0,0 +1,265 @@
<template>
<div class="right-panel">
<div class="heading">
<div class="show-menu-button" cli>
<i class="material-icons" @click="toggleLeftMenu">
menu
</i>
</div>
<div class="current-channel"><span v-if="!selectedChannelID">Welcome back!</span><span v-else>{{channelName}}</span></div>
</div>
<div class="loading" v-if="selectedChannelID && !messages[selectedChannelID]">
<spinner />
</div>
<div v-else-if="selectedChannelID" class="message-logs">
<message v-for="(msg, index) in messages[selectedChannelID]" :key="index" :username="msg.creator.username" :avatar="msg.creator.avatar" :message="msg.message" :status="msg.status" />
</div>
<news v-else />
<div class="chat-input-area" v-if="selectedChannelID">
<div class="message-area">
<textarea class="chat-input" ref="input-box" placeholder="Message" @keydown="chatInput" @keyup="messageKeyUp" v-model="message"></textarea>
<button :class="{'send-button': true, 'error-send-button': messageLength > 5000}" @click="sendMessage">Send</button>
</div>
<div class="info">
<div :class="{'message-count': true, 'error-info': messageLength > 5000 }">{{messageLength}}/5000</div>
</div>
</div>
</div>
</template>
<script>
import messagesService from '@/services/messagesService'
import {bus} from '../../main'
import JQuery from 'jquery'
let $ = JQuery
import News from '../../components/app/News.vue'
import Message from '../../components/app/MessageTemplate.vue'
import Spinner from '@/components/Spinner.vue'
export default {
components: {
Message,
Spinner,
News
},
data() {
return {
message: "",
messageLength: 0
}
},
methods:{
toggleLeftMenu(){
bus.$emit('toggleLeftMenu')
},
generateNum(n) {
var add = 1, max = 12 - add; // 12 is the min safe number Math.random() can generate without it starting to pad the end with zeros.
if ( n > max ) {
return this.generateNum(max) + this.generateNum(n - max);
}
max = Math.pow(10, n+add);
var min = max/10; // Math.pow(10, n) basically
var number = Math.floor( Math.random() * (max - min + 1) ) + min;
return ("" + number).substring(add);
},
async sendMessage(){
this.$refs["input-box"].focus()
this.message = this.message.trim();
if(this.message == "")return;
if (this.message.length > 5000) return;
const msg = this.message;
const tempID = this.generateNum(25);
this.$store.dispatch('addMessage', {
sender: true,
channelID: this.selectedChannelID,
message: {
tempID,
message: this.message,
channelID: this.selectedChannelID
}
})
this.message = ""
const { ok, error, result } = await messagesService.post(this.selectedChannelID, {
message: msg,
socketID: this.$socket.id,
tempID
})
if ( ok ) {
const message = result.data.messageCreated
message.status = 1;
this.$store.dispatch('replaceMessage', {
tempID: result.data.tempID,
message
});
} else {
// TODO: Error handling
console.log(error)
}
},
messageKeyUp(event){
this.messageLength = this.message.length;
},
chatInput(event) {
if (event.keyCode == 13) {
event.preventDefault();
this.sendMessage();
}
},
scrollDown(){
//Scroll to bottom
$(".message-logs").stop(true).animate({
scrollTop: $(".message-logs")[0].scrollHeight
}, 300);
}
},
mounted() {
bus.$on('scrollDown', this.scrollDown);
},
beforeDestroy() {
bus.$off('status-scrollDown', this.scrollDown)
},
computed: {
messages() {
return this.$store.getters.messages;
},
selectedChannelID() {
return this.$store.getters.selectedChannelID
},
channelName() {
return this.$store.getters.channelName;
},
}
}
</script>
<style scoped>
.error-info {
color: red;
}
.heading{
border-bottom: solid 2px white;
margin: 5px;
height: 40px;
padding-bottom: 2spx;
display: flex;
flex-shrink: 0;
}
.show-menu-button{
display: inline-block;
margin: auto;
color: white;
margin-left: 10px;
margin-right: 5px;
margin-top: 8px;
user-select: none;
display: none;
}
.heading .current-channel{
color: white;
font-size: 20px;
margin: auto;
margin-left: 5px;
flex: 1;
padding: 5px;
}
.right-panel {
height: 100%;
background-color: rgba(0, 0, 0, 0.507);
flex: 1;
display: flex;
flex-direction: column;
}
.message-logs{
overflow: auto;
flex: 1;
}
.loading{
overflow: auto;
flex: 1;
}
.chat-input-area{
height: 60px;
display: flex;
flex-direction: column;
padding-top: 10px;
}
.chat-input-area .info{
color: white;
font-size: 12px;
margin-left: 25px;
}
.message-area{
display: flex;
width: 100%;
}
.chat-input{
font-family: 'Roboto', sans-serif;
background: rgba(0, 0, 0, 0.158);
color: white;
flex: 1;
height: 20px;
padding: 10px;
margin: auto;
margin-left: 20px;
font-size: 15px;
resize: none;
border: none;
outline: none;
padding-left: 10px;
transition: 0.3s;
}
.chat-input:hover{
background: rgba(0, 0, 0, 0.288);
}
.chat-input:focus{
background: rgba(0, 0, 0, 0.466);
}
.send-button{
font-size: 20px;
color:white;
background: rgba(0, 0, 0, 0.274);
border: none;
outline: none;
margin: auto;
margin-left: 2px;
margin-right: 20px;
height: 40px;
transition: 0.3s;
}
.send-button:hover{
background: rgba(0, 0, 0, 0.514);
}
.error-send-button {
background-color: rgba(255, 0, 0, 0.294);
}
.error-send-button:hover {
background-color: rgba(255, 0, 0, 0.294);
}
@media (max-width: 600px) {
.show-menu-button{
display: block;
}
}
</style>

View file

@ -0,0 +1,156 @@
<template>
<div class="settings-darken-background">
<div class="settings-box">
<div class="tabs">
<div
:class="{tab: true, selected: currentTab == 'my-profile'}"
@click="tabClicked('my-profile','account_circle', 'My Profile')">
<div class="material-icons">account_circle</div>
<div>My Profile</div>
</div>
<div
:class="{tab: true, selected: currentTab == 'ddddd'}"
@click="tabClicked('ddddd','palette', 'Coming soon!')">
<div class="material-icons">palette</div>
<div>Message Themes</div>
</div>
<div
:class="{tab: true, selected: currentTab == 'eee'}"
@click="tabClicked('eee','error', 'Spoopi')">
<div class="material-icons">error</div>
<div>Another Tab</div>
</div>
</div>
<div class="panel">
<div class="title">
<div class="material-icons">{{icon}}</div>
<div class="in-title">{{title}}</div>
<div class="close-button" @click="close">
<div class="material-icons">close</div>
</div>
</div>
<component :is="currentTab"></component>
</div>
</div>
</div>
</template>
<script>
import {bus} from '../../main'
import MyProfile from './SettingsPanels/MyProfile.vue'
export default {
components: {
MyProfile
},
data() {
return {
currentTab: "my-profile",
icon: "account_circle",
title: "My Profile"
}
},
methods: {
tabClicked(currentTab, icon, title) {
this.currentTab = currentTab;
this.icon = icon;
this.title = title;
},
close() {
bus.$emit('closeSettings');
}
}
}
</script>
<style scoped>
.settings-darken-background{
position: absolute;
background: rgba(0, 0, 0, 0.541);
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 99;
display: flex;
color: white;
}
.settings-box{
display: flex;
margin: auto;
box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.507);
}
.tabs{
background: rgba(24, 24, 24, 0.938);
height: 600px;
width: 200px;
}
.panel {
background: rgba(31, 31, 31, 0.924);
height: 600px;
width: 600px;
}
.tab {
display: flex;
padding: 10px;
background: rgba(26, 26, 26, 0.233);
margin-top: 5px;
margin-bottom: 5px;
cursor: default;
user-select: none;
transition: 0.3s;
}
.tab.selected {
background: rgb(61, 61, 61) !important;
}
.tab div {
margin: 5px;
}
.tab:hover {
background: rgba(61, 61, 61, 0.616);
}
.title{
display: flex;
padding: 10px;
font-size: 25px;
background: rgb(20, 20, 20);
margin-bottom: 20px;
}
.title .material-icons{
font-size: 40px;
}
.title div {
margin: auto;
margin-left: 5px;
margin-right: 5px;
}
.in-title {
flex:1;
}
.close-button{
display: flex;
border-radius: 50%;
padding: 5px;
cursor: default;
user-select: none;
transition: 0.3s;
}
.close-button:hover{
background: rgba(37, 37, 37, 0.692);
}
.close-button .material-icons {
margin: auto;
font-size: 30px;
}
@media (max-width: 815px) {
.settings-box{
width:100%
}
}
</style>

View file

@ -0,0 +1,209 @@
<template>
<div class="my-profile-panel">
<div class="profile-picture" :style="`background-image: url(${avatar})`"></div>
<div class="information">
<div class="username"><strong>Username: </strong>{{user.username}}</div>
<div class="tag"><strong>Tag: </strong>@{{user.tag}}</div>
</div>
<div class="options">
<input type="file" accept="image/*" ref="avatarBrowser" @change="avatarBrowse" class="hidden">
<div class="option" @click="$refs.avatarBrowser.click()">Edit Avatar</div>
<div class="option" @click="changePassword">Change Password</div>
<div class="option red" @click="logout">Logout</div>
</div>
<div class="alert-outer" v-if="alert.show">
<div class="alert" >
<div class="alert-title">Error</div>
<div class="alert-content">
{{alert.content}}
</div>
<div class="alert-buttons">
<div class="alert-button" @click="alert.show = false">Okay</div>
</div>
</div>
</div>
</div>
</template>
<script>
import UploadService from '@/services/UploadService.js'
import config from '@/config.js'
import path from 'path'
export default {
data(){
return {
alert: {
content: 'Image size must be less than 10mb!',
show: false
},
}
},
methods: {
onProgress(percent){
//update vue
console.log("Avatar upload progress: ", percent)
},
async avatarBrowse(event) {
const file = event.target.files[0];
const allowedFormats = ['.png', '.jpeg', '.gif', '.jpg' ];
if (!allowedFormats.includes(path.extname(file.name).toLowerCase())){
this.alert.content = 'Unsupported image file.';
return this.alert.show = true;
} else if (file.size >= 10490000){
// 10490000 = 10mb
this.alert.content = 'Image size must be less than 10 megabytes.';
return this.alert.show = true;
}
const formData = new FormData();
formData.append('avatar', file);
const {ok, error, result} = await UploadService.uploadAvatar(formData, this.onProgress);
if (!ok) {
this.alert.content = 'Something went wrong. Try again later.';
return this.alert.show = true;
}
},
logout() {
this.$store.dispatch('logout')
window.location.href = "/"
},
changePassword() {
this.alert.content = 'Not implemented yet.';
return this.alert.show = true;
}
},
computed: {
user() {
return this.$store.getters.user
},
avatar() {
return config.domain + "/avatars/" +this.$store.getters.user.avatar
}
}
}
</script>
<style scoped>
.hidden {
display: none;
}
.my-profile-panel{
display: flex;
width: 100%;
height: 100px;
margin-top: 10px;
}
.profile-picture {
width: 100px;
height: 100px;
background: rgb(63, 63, 63);
border-radius: 50%;
margin-left: 20px;
flex-shrink: 0;
background-position: center;
background-size: 100%;
background-repeat: no-repeat;
}
.information {
margin: auto;
margin-left: 20px;
margin-right: 0;
font-size: 18px;
flex: 1;
}
.username{
margin-bottom: 10px;
}
.options{
margin: auto;
margin-right: 30px;
border-left: solid 1px rgb(204, 204, 204);
padding-left: 10px;
}
.option {
color: rgb(218, 218, 218);
cursor: default;
user-select: none;
transition: 0.3s;
}
.option:hover {
color: rgb(255, 255, 255);
}
.option.red {
color: rgba(255, 0, 0, 0.678);
}
.option.red:hover {
color: red;
}
.alert-title{
background: rgb(34, 34, 34);
font-size: 20px;
color: white;
padding: 10px;
}
.alert-outer {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
display: flex;
background: rgba(0, 0, 0, 0.267)
}
.alert {
margin: auto;
background: rgb(49, 49, 49);
width: 500px;
box-shadow: 0px 0px 30px #000000;
display: flex;
flex-direction: column;
user-select: none;
cursor: default;
}
.alert-content{
margin: auto;
font-size: 16px;
color: white;
padding: 10px;
padding-top: 30px;
padding-bottom: 40px;
}
.alert-buttons {
margin-left: auto;
margin-right: auto;
margin-bottom: 20px;
}
.alert-button {
color: white;
margin: auto;
background: rgba(73, 53, 53, 0.712);
padding: 10px;
transition: 0.3s;
}
.alert-button:hover {
background: rgb(83, 53, 53);
}
@media (max-width: 815px) {
.my-profile-panel{
flex-direction: column;
}
.profile-picture {
margin: auto;
margin-bottom: 10px;
}
.information {
margin: auto;
text-align: center;
}
.options {
margin: auto;
margin-top: 20px;
text-align: center;
border: none;
}
}
</style>

View file

@ -0,0 +1,175 @@
<template>
<div class="add-friend-panel">
<div class="panel-title" @click="expanded = !expanded">
<div>
<i class="material-icons" v-if="!expanded">
person_add
</i>
<span>{{expanded ? "Hide" : "Add friend"}}</span>
</div>
</div>
<transition name="slide" appear>
<div class="add-friend" v-if="expanded">
<div class="title">Add friend</div>
<div class="info">Type in your friends username and tag. eg: someone@jt4g</div>
<div class="infoC">Creators tag: Fishie@azK0</div>
<form action="#" @submit.prevent="addFriend">
<input type="text" placeholder="username@tag" v-model="input">
<loadingButton :loading="currentButtonMessage == 1" :message="buttonMessages[currentButtonMessage]" />
</form>
<div :class="{message: true, warning: errorMessage.isError}">
{{errorMessage.message}}
</div>
</div>
</transition>
</div>
</template>
<script>
import RelationshipService from '@/services/RelationshipService.js'
import loadingButton from './../../Button.vue'
export default {
components: {
loadingButton
},
data() {
return {
expanded: false,
buttonMessages: [
"Add Friend",
"Adding..."
],
currentButtonMessage: 0,
input: "",
errorMessage:{
message: "",
isError: false
}
}
},
methods: {
async addFriend() {
this.$set(this.errorMessage, 'message', "")
this.currentButtonMessage = 1;
const split = this.input.trim().split("@");
// validation
if ( split.length <2 || split.length >2 || split[1] === "" || split[1].length !== 4){
this.$set(this.errorMessage, 'message', "Invalid username or tag.")
this.$set(this.errorMessage, 'isError', true)
this.currentButtonMessage = 0;
return;
}
const username = split[0];
const tag = split[1];
const {ok, error, result} = await RelationshipService.post({username, tag})
this.currentButtonMessage = 0;
if ( ok ) {
this.$set(this.errorMessage, 'message', result.data.message)
this.$set(this.errorMessage, 'isError', false)
} else {
if (error.response === undefined) {
this.$set(this.errorMessage, 'message', "Can't connect to server.")
this.$set(this.errorMessage, 'isError', true)
return
}
this.$set(this.errorMessage, 'message', error.response.data.errors[0].msg)
this.$set(this.errorMessage, 'isError', true)
}
}
}
}
</script>
<style scoped>
.slide-enter-active, .slide-leave-active {
transition: .3s;
}
.slide-enter, .slide-leave-to /* .fade-leave-active below version 2.1.8 */ {
margin-bottom: -270px;
opacity: 0;
}
.add-friend-panel{
width: 100%;
background: rgba(0, 0, 0, 0.123);
display: flex;
flex-direction: column;
}
.add-friend{
background: rgba(0, 0, 0, 0.13);
flex: 1;
height: 250px;
display: flex;
flex-direction: column;
color: white;
padding: 10px;
}
.title{
margin: auto;
margin-top: 10px;
margin-bottom: 0px;
font-size: 20px;
color: white;
user-select: none;
cursor: default;
}
.info{
text-align: center;
color: rgb(182, 182, 182);
font-size: 15px;
user-select: none;
cursor: default;
}
.infoC{
text-align: center;
color: rgb(255, 79, 79);
font-size: 15px;
}
form {
margin: auto;
margin-top: 5px;
margin-bottom: 0px;
}
.message{
margin: auto;
margin-top: 2px;
font-size: 15px;
color: green;
}
.warning {
color: red;
}
.panel-title{
background: rgba(0, 0, 0, 0.274);
width: 100%;
text-align: center;
padding-top: 15px;
padding-bottom: 15px;
color: white;
cursor: pointer;
user-select: none;
display: flex;
transition: 0.3s;
}
.panel-title:hover{
background: rgba(0, 0, 0, 0.445);
}
.panel-title .material-icons{
vertical-align: top;
margin-top: -2px;
}
.panel-title span{
margin-left: 5px;
}
.panel-title div{
margin: auto;
}
</style>

View file

@ -0,0 +1,64 @@
<template>
<div class="friends" >
<div class="tab" @click="expanded = !expanded">
<Tab :expanded="expanded" tabname="Friends" />
</div>
<transition name="list" appear>
<div class="list" v-if="expanded">
<FriendsTemplate v-for="(friend, key) of friends" :key="key" :channelID="friend.channelID" :uniqueID="friends[key].recipient.uniqueID" :username="friend.recipient.username" :tag="friend.recipient.tag"/>
</div>
</transition>
</div>
</template>
<script>
import Tab from './Tab.vue'
import FriendsTemplate from './FriendsTemplate.vue'
export default {
components: {
Tab,
FriendsTemplate
},
data() {
return {
expanded: true
}
},
computed: {
friends() {
const allFriend = this.$store.getters.user.friends;
const result = Object.keys(allFriend).map(function(key) {
return allFriend[key];
});
return result.filter(friend => friend.status == 2);
}
}
}
</script>
<style scoped>
.list-enter-active, .list-leave-active {
transition: .3s;
}
.list-enter, .list-leave-to /* .fade-leave-active below version 2.1.8 */ {
transform: translateY(-20px);
opacity: 0;
}
.friends{
background-color: rgba(0, 0, 0, 0);
margin: 5px;
user-select: none;
padding-bottom: 3px;
border-radius: 5px;
transition: 0.3s;
}
.tab{
border-radius: 5px;
transition: 0.3s;
}
.tab:hover{
background-color: rgba(0, 0, 0, 0.123);
}
</style>

View file

@ -0,0 +1,131 @@
<template>
<div class="friend" @click="openChat">
<div class="profile-picture" :style="`border-color: ${status.statusColor}; background-image: url(${userAvatar})`">
<div class="status" :style="`background-image: url(${status.statusURL})`" ></div>
</div>
<div class="information">
<div class="username">{{$props.username}}</div>
<div class="status-name" :style="`color: ${status.statusColor}`">{{status.statusName}}</div>
</div>
</div>
</template>
<script>
import channelService from '@/services/channelService';
import messagesService from '@/services/messagesService';
import config from '@/config.js'
import statuses from '@/statuses';
export default {
props: ['username', 'tag', 'channelID', 'uniqueID'],
methods: {
async getMessages() {
const {ok, error, result} = await messagesService.get(this.$props.channelID);
if ( ok ) {
this.$store.dispatch('messages', {channelID: result.data.channelID, messages: result.data.messages});
} else {
// TODO handle this
console.log (error.response)
}
},
async openChat() {
this.$store.dispatch('selectedChannelID', this.$props.channelID);
this.$store.dispatch('setName', this.$props.username);
if (this.$store.getters.channels[this.$props.channelID]) return
const {ok, error, result} = await channelService.post(this.$props.channelID);
if ( ok ) {
this.$store.dispatch('channel', result.data.channel);
this.getMessages();
} else {
// TODO handle this
console.log(error)
}
}
},
computed: {
user() {
return this.$store.getters.user.friends[this.$props.uniqueID].recipient;
},
userAvatar() {
return config.domain + "/avatars/" + this.user.avatar
},
status() {
const status = this.$store.getters.user.friends[this.$props.uniqueID].recipient.status || 0
return {
statusName: statuses[parseInt(status)].name,
statusURL: statuses[parseInt(status)].url,
statusColor: statuses[parseInt(status)].color
}
}
}
}
</script>
<style scoped>
.friend {
color: white;
background-color: rgba(0, 0, 0, 0.137);
margin: 5px;
padding: 10px;
display: flex;
transition: 0.3s;
border-radius: 3px;
}
.friend:hover {
background-color: rgba(0, 0, 0, 0.246);
}
.profile-picture{
height: 40px;
width: 40px;
background-color: rgba(0, 0, 0, 0.425);
border-radius: 50%;
margin: auto;
margin-left: 2px;
margin-right: 5px;
border: solid 3px;
position: relative;
background-position: center;
background-size: 100%;
background-repeat: no-repeat;
}
.information{
margin: auto;
margin-left: 5px;
margin-right: 5px;
flex: 1;
}
.status {
position: absolute;
height: 20px;
width: 20px;
background-color: black;
border-radius: 50%;
background-size: calc(100% + 2px);
background-position: center;
bottom: -10px;
right: -5px;
opacity: 0;
transition: 0.3s;
}
.friend:hover .status {
opacity: 1;
bottom: -5px;
}
.status-name{
opacity: 0;
font-size: 13px;
transition: 0.3s;
height: 0;
}
.friend:hover .status-name {
opacity: 0.8;
height: 19px;
}
</style>

View file

@ -0,0 +1,64 @@
<template>
<div class="pending-friends" >
<div class="tab" @click="expanded = !expanded">
<Tab :expanded="expanded" tabname="Pending requests" />
</div>
<transition name="list" appear>
<div class="list" v-if="expanded">
<PendingTemplate v-for="(friend, key) of friends" :key="key" :uniqueID="friend.recipient.uniqueID" :status="friend.status" :username="friend.recipient.username" :tag="friend.recipient.tag"/>
</div>
</transition>
</div>
</template>
<script>
import Tab from './Tab.vue'
import PendingTemplate from './PendingTemplate.vue'
export default {
components: {
Tab,
PendingTemplate
},
data() {
return {
expanded: true
}
},
computed: {
friends() {
const allFriend = this.$store.getters.user.friends;
const result = Object.keys(allFriend).map(function(key) {
return allFriend[key];
});
return result.filter(friend => friend.status < 2);
}
}
}
</script>
<style scoped>
.list-enter-active, .list-leave-active {
transition: .3s;
}
.list-enter, .list-leave-to /* .fade-leave-active below version 2.1.8 */ {
transform: translateY(-20px);
opacity: 0;
}
.pending-friends{
background-color: rgba(0, 0, 0, 0);
margin: 5px;
user-select: none;
padding-bottom: 3px;
border-radius: 5px;
transition: 0.3s;
}
.tab{
border-radius: 5px;
transition: 0.3s;
}
.tab:hover{
background-color: rgba(0, 0, 0, 0.123);
}
</style>

View file

@ -0,0 +1,116 @@
<template>
<div class="pending-friend">
<div class="profile-picture" :style="`background-image: url(${userAvatar})`"></div>
<div class="information">
<div class="username">{{$props.username}}</div>
<div class="tag">@{{$props.tag}}</div>
</div>
<div class="buttons">
<div :class="{button: true, accept: true, hide: $props.status == 0}" @click="accept" >
<i class="material-icons">
check
</i>
</div>
<div class="button decline" @click="deny">
<i class="material-icons">
not_interested
</i>
</div>
</div>
</div>
</template>
<script>
import RelationshipService from '@/services/RelationshipService.js'
import config from '@/config.js'
export default {
props: ['username', 'tag', 'status', 'uniqueID'],
methods: {
deny() {
RelationshipService.delete(this.$props.uniqueID)
},
accept() {
RelationshipService.put(this.$props.uniqueID)
}
},
computed: {
user() {
return this.$store.getters.user.friends[this.$props.uniqueID].recipient;
},
userAvatar() {
return config.domain + "/avatars/" + this.user.avatar
},
}
}
</script>
<style scoped>
.pending-friend {
color: white;
background-color: rgba(0, 0, 0, 0.137);
margin: 5px;
padding: 10px;
display: flex;
transition: 0.3s;
border-radius: 3px;
}
.pending-friend:hover {
background-color: rgba(0, 0, 0, 0.246);
}
.profile-picture{
height: 40px;
width: 40px;
background-color: rgba(0, 0, 0, 0.425);
border-radius: 50%;
margin: auto;
margin-left: 2px;
margin-right: 5px;
background-position: center;
background-size: 100%;
background-repeat: no-repeat;
}
.information{
margin: auto;
margin-left: 5px;
margin-right: 5px;
flex: 1;
}
.tag{
color: rgb(173, 173, 173);
font-size: 15px;
}
.buttons{
display: flex;
margin: auto;
margin-right: 5px;
}
.button {
background-color: rgba(65, 65, 65, 0.438);
width: 30px;
height: 30px;
margin: 5px;
display: flex;
transition: 0.3s;
border-radius: 3px;
}
.hide {
display: none;
}
.button:hover{
background-color: rgba(0, 255, 0, 0.281);
}
.button .material-icons{
margin: auto;
color: rgba(255, 255, 255, 0.747);
}
.button.decline:hover{
background-color: rgba(255, 0, 0, 0.281);
}
</style>

View file

@ -0,0 +1,31 @@
<template>
<div class="tab">
<i :class="{'material-icons': true, closed: !$props.expanded}">
expand_more
</i>
<div class="tab-name">
{{$props.tabname}}
</div>
</div>
</template>
<script>
export default {
props: ['tabname', 'expanded']
}
</script>
<style scoped>
.tab{
display: flex;
color: white;
}
.material-icons{
transition: 0.3s;
}
.closed{
transform: rotate(180deg);
}
</style>

View file

@ -0,0 +1,65 @@
<template>
<div class="status-popout">
<div class="status-list" @click="changeStatus(1)"><span class="status-icon"><img class="icon" :src="getStatusLogo(1)" /></span><span class="text">Online</span></div>
<div class="status-list" @click="changeStatus(2)"><span class="status-icon"><img class="icon" :src="getStatusLogo(2)" /></span><span class="text">Away</span></div>
<div class="status-list" @click="changeStatus(3)"><span class="status-icon"><img class="icon" :src="getStatusLogo(3)" /></span><span class="text">Busy</span></div>
<div class="status-list" @click="changeStatus(4)"><span class="status-icon"><img class="icon" :src="getStatusLogo(4)" /></span><span class="text">Looking to play</span></div>
<div class="status-list" @click="changeStatus(0)"><span class="status-icon"><img class="icon" :src="getStatusLogo(0)" /></span><span class="text">Offline</span></div>
</div>
</template>
<script>
import {bus} from '../../main';
export default {
methods: {
getStatusLogo(status){
return require(`./../../assets/status/${status}.svg`)
},
changeStatus(status) {
bus.$emit('status-change', status)
}
}
}
</script>
<style scoped>
.status-popout{
position: absolute;
background-color: rgba(44, 44, 44, 0.671);
border-radius: 10px;
padding: 5px;
width: 180px;
}
.status-list {
padding: 5px;
transition: 0.3s;
border-radius: 5px;
margin: 5px;
}
.status-list:hover {
background: rgba(0, 0, 0, 0.349);
}
.status-icon{
display: inline-block;
}
.icon{
height: 30px;
width: 30px;
margin-top: 3px;
}
.text{
display: inline-block;
vertical-align: top;
margin-top: 9px;
margin-left: 10px;
padding-right: 5px;
}
</style>

View file

@ -0,0 +1,113 @@
<template>
<div class="logged-in">
<div class="title">Welcome!</div>
<div class="card">
<div class="avatar-outer">
<div class="avatar" :style="`background-image: url(${avatar})`"></div>
</div>
<div class="info">
<div class="username">{{user.username}}<span class="tag">@{{user.tag}}</span></div>
<div class="buttons">
<button class="button" @click="openChat">Enter</button>
<button class="button logout" @click="logout">Logout</button>
</div>
</div>
</div>
</div>
</template>
<script>
import config from '@/config.js'
export default {
methods: {
logout() {
this.$store.commit('logout')
},
openChat() {
window.location.href = "/app"
}
},
computed: {
user() {
return this.$store.getters.user;
},
avatar() {
return config.domain + "/avatars/" +this.$store.getters.user.avatar
}
}
}
</script>
<style scoped>
.logged-in {
margin-top: 50px;
color: white;
}
.card{
background-color: rgba(0, 0, 0, 0.26);
padding: 10px;
margin: 20px;
margin-bottom: 30px;
display: flex;
}
.title {
display: table;
margin: auto;
font-size: 30px;
}
.avatar {
width: 80px;
height: 80px;
background-color: rgba(0, 0, 0, 0.459);
border-radius: 50%;
background-position: center;
background-size: 100%;
background-repeat: no-repeat;
}
.info {
display: flex;
flex-direction: column;
margin-left: 10px;
margin-top: 5px;
}
.username {
font-size: 20px;
user-select: all;
}
.tag{
font-size: 13px;
color: rgb(194, 194, 194);
}
.buttons{
display: flex;
}
.button {
color: white;
background: rgba(0, 0, 0, 0.308);
padding: 10px;
border: none;
outline: none;
margin-right: 5px;
margin-top: 10px;
transition: 0.3s;
}
.button:hover {
background: rgba(0, 0, 0, 0.582);
}
.button.logout{
background: rgba(149, 0, 0, 0.404);
}
.button.logout:hover {
background: rgba(255, 0, 0, 0.582);
}
</style>

View file

@ -0,0 +1,103 @@
<template>
<div class="form">
<form action="#" @submit.prevent="login">
<div class="input">
<div class="alert other-alert">{{otherError}}</div>
<div class="input-title">Email: <div class="alert">{{email.alert}}</div></div>
<input type="email" autocomplete="on" v-model="email.value">
</div>
<div class="input">
<div class="input-title">Password: <div class="alert">{{password.alert}}</div></div>
<input type="password" autocomplete="current-password" v-model="password.value">
</div>
<div class="cap">
<recaptcha ref="recaptcha" @verify="captchaSubmit" />
</div>
<loadingButton :loading="currentMessage == 1" :message="buttonMessages[currentMessage]" />
</form>
</div>
</template>
<script>
import Recaptcha from '../Recaptcha.vue'
import {bus} from '../../main';
import loadingButton from "../../components/Button.vue"
import AuthenticationService from '@/services/AuthenticationService';
export default {
components: {
loadingButton,
Recaptcha
},
data() {
return {
email: {value: "", alert: ""},
password: {value: "", alert: ""},
otherError: "",
buttonMessages: [
"Login",
"Logging in..."
],
currentMessage: 0,
captcha: null
}
},
methods: {
resetValues() {
// Resets all of the alert values
this.email.alert = "";
this.password.alert = "";
this.otherError = "";
},
async login() {
this.currentMessage = 1
this.resetValues();
const email = this.email.value.trim();
const password = this.password.value.trim();
const captcha = this.captcha;
const {ok, error, result} = await AuthenticationService.login({email, password, token: captcha})
this.currentMessage = 0
if (ok) {
this.$store.dispatch('token', result.data.token)
this.$store.dispatch('user', result.data.user)
} else {
this.captcha = null;
this.$refs.recaptcha.resetRecaptcha();
if (error.response === undefined) {
this.otherError = "Can't connect to server."
return;
}
const errors = error.response.data.errors;
for (let index in errors) {
const message = errors[index].msg;
const param = errors[index].param;
if(this[param] === undefined) {
this.otherError = message;
} else {
this[param].alert = message;
}
}
}
},
captchaSubmit(token) {
this.captcha = token;
}
}
}
</script>
<style scoped>
.input {
display: table;
margin: auto;
}
.cap {
margin: 20px;
opacity: 0.8;
transition: 0.3s;
}
.cap:hover {
opacity: 1;
}
</style>

View file

@ -0,0 +1,121 @@
<template>
<div class="form">
<form action="#" @submit.prevent="register">
<div class="input">
<div class="alert">{{otherError}}</div>
<div class="input-title">Email: <div class="alert">{{email.alert}}</div></div>
<input type="email" autocomplete="on" v-model="email.value">
</div>
<div class="input">
<div class="input-title">Username: <div class="alert">{{username.alert}}</div></div>
<input type="username" autocomplete="off" v-model="username.value">
</div>
<div class="input">
<div class="input-title">Password: <div class="alert">{{password.alert}}</div></div>
<input type="password" autocomplete="new-password" v-model="password.value">
</div>
<div class="input">
<div class="input-title">Password confirm: <div class="alert">{{passwordConfirm.alert}}</div></div>
<input type="password" autocomplete="new-password" v-model="passwordConfirm.value">
</div>
<div class="cap">
<recaptcha ref="recaptcha" @verify="captchaSubmit" />
</div>
<loading-button :loading="currentMessage == 1" :message="buttonMessages[currentMessage]" />
</form>
</div>
</template>
<script>
import Recaptcha from '../Recaptcha.vue'
import AuthenticationService from '@/services/AuthenticationService.js'
import {bus} from '../../main';
import LoadingButton from "../../components/Button.vue"
export default {
components: {
LoadingButton,
Recaptcha
},
data() {
return {
email: {value: "", alert: ""},
username: {value: "", alert: ""},
password: {value: "", alert: ""},
passwordConfirm: {value: "", alert: ""},
otherError: "",
buttonMessages: [
"Register",
"Creating... ( Thank you c: )"
],
currentMessage: 0,
captcha: null
}
},
methods: {
resetValues() {
// Resets all of the alert values
this.email.alert = "";
this.username.alert = "";
this.password.alert = "";
this.passwordConfirm.alert = "";
this.otherError = ""
},
async register() {
this.currentMessage = 1
this.resetValues();
const email = this.email.value.trim();
const username = this.username.value.trim();
const password = this.password.value.trim();
const passwordConfirm = this.passwordConfirm.value.trim();
// check if password + password confirm matches.
if ( password != passwordConfirm ) {
this.currentMessage = 0;
return this.passwordConfirm.alert = "Passwords do not match!"
}
const {ok, error, result} = await AuthenticationService.register({email, username, password, token: this.captcha})
this.currentMessage = 0
if ( ok ) {
this.$store.dispatch('token', result.data.token)
this.$store.dispatch('user', result.data.user)
} else {
this.captcha = null;
this.$refs.recaptcha.resetRecaptcha();
if (error.response === undefined) {
this.otherError = "Can't connect to server."
return;
}
const errors = error.response.data.errors;
for (let index in errors) {
const message = errors[index].msg;
const param = errors[index].param;
if(this[param] === undefined) {
this.otherError = message;
} else {
this[param].alert = message;
}
}
}
},
captchaSubmit(token) {
this.captcha = token;
}
}
}
</script>
</script>
<style scoped>
.input {
display: table;
margin: auto;
}
.cap {
margin: 20px;
opacity: 0.8;
transition: 0.3s;
}
.cap:hover {
opacity: 1;
}
</style>

View file

@ -0,0 +1,186 @@
<template>
<div class="right-panel-home">
<div class="right-panel-inner">
<div class="logo"></div>
<div class="title">Nertivia</div>
<spinner :msg="spinnerMessage" v-if="previouslyLoggedIn && user == null && tokenExists" />
<transition name="component-fade" appear mode="out-in" v-else>
<logged-in v-if="tokenExists && user != null" />
<div class="new-member" v-if="!tokenExists">
<div class="details">
Nertivia chat is the best chat client to be made with 99% uptime, you won't miss a thing! Join now if youre new!
</div>
<div class="switch-buttons">
<div :class="{button: true, selected: loginSelected}" @click="loginSelected = true">Already a pro</div>
<div :class="{button: true, selected: !loginSelected}" @click="loginSelected = false">I'm new!</div>
</div>
<transition name="switch-selected" mode="out-in">
<login-panel v-if="loginSelected" />
<register-panel v-if="!loginSelected" />
</transition>
</div>
</transition>
</div>
</div>
</template>
<script>
import {bus} from '../../main';
import AuthenticationService from '@/services/AuthenticationService.js'
import RegisterPanel from "../../components/homePage/RegisterPanel.vue"
import LoginPanel from "../../components/homePage/LoginPanel.vue"
import LoggedIn from "../../components/homePage/LoggedIn.vue"
import Spinner from "../../components/Spinner.vue"
export default {
components: {
RegisterPanel,
LoginPanel,
LoggedIn,
Spinner
},
data() {
return {
loginSelected: true,
previouslyLoggedIn: false,
spinnerMessage: "Logging in...",
connectionRetryCount: 0
}
},
methods: {
async getUser() {
// Get details if previously logged in.
if (this.previouslyLoggedIn) {
const { ok, error, result } = await AuthenticationService.user();
if ( ok ) {
this.$store.commit( 'user', result.data.user );
} else {
if ( error.response === undefined ) {
this.connectionRetryCount++;
this.spinnerMessage = `Connection failed. Trying again (${this.connectionRetryCount})`
setTimeout(() => {
this.getUser();
}, 5000);
return;
}
this.$store.commit( 'logout' );
}
}
}
},
async mounted() {
this.previouslyLoggedIn = this.tokenExists;
this.getUser()
},
computed: {
tokenExists() {
return this.$store.getters.tokenExists;
},
user() {
return this.$store.getters.user;
}
},
}
</script>
<style scoped>
.component-fade-enter-active, .component-fade-leave-active {
transition: .3s ease;
}
.component-fade-enter, .component-fade-leave-to {
transform: translateY(20px);
opacity: 0;
}
.switch-selected-enter-active, .switch-selected-leave-active {
transition: .3s ease;
}
.switch-selected-enter, .switch-selected-leave-to {
transform: translateY(20px);
opacity: 0;
}
.right-panel-home {
width: 400px;
height: 100%;
background: rgba(0, 0, 0, 0.493);
display: flex;
flex-direction: column;
overflow: auto;
user-select: none;
}
.right-panel-inner{
display: flex;
flex-direction: column;
height: 100%;
}
.new-member{
display: flex;
flex-direction: column;
transition: .3s;
}
.logo {
height: 150px;
width: 150px;
background: url(./../../assets/logo.png);
background-size: 129%;
background-position: center;
border-radius: 50%;
box-shadow: 0px 0px 96px -4px rgba(69,212,255,1);
margin: auto;
margin-bottom: 0;
margin-top: 40px;
flex-shrink: 0;
}
.title{
color: white;
font-size: 35px;
text-align: center;
margin-top: 50px;
margin-bottom: 10px;
}
.right-panel .title {
margin: auto;
margin-top: 10px;
width: 230px;
font-size: 40px;
text-align: center;
}
.details{
color: rgb(204, 204, 204);
margin: 49px;
margin-top: 10px;
}
.switch-buttons{
display: table;
margin: auto;
}
.button{
color: white;
font-size: 18px;
display: inline-block;
padding: 10px;
margin: 5px;
user-select: none;
transition: 0.3s;
}
.button:hover{
border-bottom: solid 2px rgba(255, 255, 255, 0.493);
}
.button.selected{
border-bottom: solid 2px white;
}
</style>

32
src/config.js Normal file
View file

@ -0,0 +1,32 @@
const config = {
devMode:true,
recaptcha: "",
IP: [
{
domain: "http://api.localhost",
socketIP: "localhost"
},
{
domain: "https://api.supertiger.tk",
socketIP: "https://nertivia.supertiger.tk"
}
]
}
if (window.webpackHotUpdate) {
config.devMode = true;
} else {
config.devMode = false
}
if ( config.devMode ) {
config.recaptcha = "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI";
config['domain'] = config.IP[0].domain;
config['socketIP'] = config.IP[0].socketIP;
} else {
config.recaptcha = "6Ld0EIwUAAAAALJNTa-1s63l-w_jHyCY6dFAVwKe";
config['domain'] = config.IP[1].domain;
config['socketIP'] = config.IP[1].socketIP;
}
export default config;

26
src/main.js Normal file
View file

@ -0,0 +1,26 @@
import Vue from 'vue'
import {router} from './router'
import Main from '../src/Main.vue'
import {store} from './store/index';
import Axios from 'axios';
import './clickOutside';
import vueHeadful from 'vue-headful';
Vue.component('vue-headful', vueHeadful);
Vue.config.productionTip = false
const token = localStorage.getItem('hauthid');
Vue.prototype.$http = Axios;
if (token) {
Vue.prototype.$http.defaults.headers.common['authorization'] = token;
}
export const bus = new Vue();
new Vue({
store,
router,
render: h => h(Main)
}).$mount('#app')

47
src/router.js Normal file
View file

@ -0,0 +1,47 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
import {store} from './store/index';
import MainApp from '../src/views/App.vue'
import HomePage from '../src/views/HomePage.vue'
import VueSocketio from 'vue-socket.io-extended';
import io from 'socket.io-client';
import config from './config'
import VueMq from 'vue-mq'
export const router = new VueRouter({
mode: 'history',
routes: [
{
path: '/',
name: 'HomePage',
component: HomePage
},
{
path: '/app',
name: 'app',
component: MainApp,
beforeEnter (to, from, next) {
if (!localStorage.getItem('hauthid')){
return router.push({ path: '/' })
}
Vue.use(VueSocketio, io(config.socketIP, {
transportOptions: {
polling: {
extraHeaders: {
authorization: localStorage.getItem('hauthid')
}
}
}
}), {store});
Vue.use(VueMq, {
breakpoints: {
mobile: 600,
desktop: Infinity,
}
})
next()
}
}
]
})

14
src/services/Api.js Normal file
View file

@ -0,0 +1,14 @@
import axios from 'axios';
import config from '@/config';
export const instance = () => {
return axios.create({
baseURL: config.domain
})
}
export const wrapper = (promise) => {
return promise
.then(result => ({ok: true, result}))
.catch(error => Promise.resolve({ok: false, error}))
}

View file

@ -0,0 +1,13 @@
import {instance, wrapper} from './Api';
export default {
register ( credentials ) {
return wrapper(instance().post('user/register', credentials));
},
login( credentials ) {
return wrapper(instance().post('user/login', credentials));
},
user () {
return wrapper(instance().get('user/details'))
}
}

View file

@ -0,0 +1,18 @@
import {instance, wrapper} from './Api';
export default {
post ( friend ) {
return wrapper(instance().post('/user/relationship', friend));
},
put( uniqueID ) {
return wrapper(instance().put('/user/relationship', {uniqueID}));
},
delete( uniqueID ) {
return wrapper(instance().delete(
'/user/relationship',
{
data: {uniqueID}
}
));
}
}

View file

@ -0,0 +1,19 @@
import {instance, wrapper} from './Api';
export default {
uploadAvatar(data, onProgress){
const url = `/settings/avatar`;
let config = {
onUploadProgress(progressEvent) {
var percentCompleted = Math.round((progressEvent.loaded * 100) /
progressEvent.total);
// execute the callback
if (onProgress) onProgress(percentCompleted)
return percentCompleted;
},
};
return wrapper(instance().post(url, data, config));
}
}

View file

@ -0,0 +1,7 @@
import {instance, wrapper} from './Api';
export default {
post ( channelID ) {
return wrapper(instance().post(`channels/${channelID}`));
}
}

View file

@ -0,0 +1,12 @@
import {instance, wrapper} from './Api';
export default {
// TODO: add ?continue=id
get ( channelID ) {
return wrapper(instance().get(`messages/${channelID}`));
},
// TODO tempID
post (channelID, data) {
return wrapper(instance().post(`messages/${channelID}`, data))
}
}

View file

@ -0,0 +1,7 @@
import {instance, wrapper} from './Api';
export default {
setStatus ( status ) {
return wrapper(instance().post('/settings/status', { status }));
},
}

View file

@ -0,0 +1,5 @@
import {instance, wrapper} from './Api';
export default {
}

35
src/statuses.js Normal file
View file

@ -0,0 +1,35 @@
const config = [
//Offline
{
name: "Offline",
url: require("@/assets/status/0.svg"),
color: "#919191"
},
//Online
{
name: "Online",
url: require("@/assets/status/1.svg"),
color: "#27eb48"
},
//Away
{
name: "Away",
url: require("@/assets/status/2.svg"),
color: "#ffdd1e"
},
//Busy
{
name: "Busy",
url: require("@/assets/status/3.svg"),
color: "#ea0b1e"
},
//Looking to play
{
name: "Looking to play",
url: require("@/assets/status/4.svg"),
color: "#9a3dd3"
}
]
export default config;

31
src/store/index.js Normal file
View file

@ -0,0 +1,31 @@
import Vue from 'vue';
import Vuex from 'vuex';
import user from './modules/userModule';
import socketModule from './modules/socketIOModule';
import channelModule from './modules/channelModule';
import messageModule from './modules/messageModule';
import {router} from './../router'
Vue.use(Vuex);
export const store = new Vuex.Store({
modules: { user, socketModule, channelModule, messageModule },
state: {
},
getters: {
},
mutations: {
sendMessage(state, message) {
if (state.messageLogs[state.channelID] === undefined) {
state.messageLogs[state.channelID] = {};
}
state.messageLogs[state.channelID][Date.now().toString()] = {channelID: state.channelID, message: message, messageID: Date.now(), status: 0};
}
},
actions: {
}
})

View file

@ -0,0 +1,52 @@
import {bus} from '../../main'
import {router} from './../../router'
const state = {
selectedChannelID: null,
channelName: null,
channels: {}
}
const getters = {
selectedChannelID(state) {
return state.selectedChannelID;
},
channels(state) {
return state.channels;
},
channelName(state) {
return state.channelName;
}
}
const actions = {
channel(context, channel) {
context.commit('channel', channel)
},
selectedChannelID(context, channelID) {
context.commit('selectedChannelID', channelID)
},
setName(context, name) {
context.commit('setName', name)
}
}
const mutations = {
channel(state, channel) {
state.channels[channel.channelID] = channel;
},
selectedChannelID(state, channelID) {
state.selectedChannelID = channelID;
},
setName(state, name) {
state.channelName = name;
}
}
export default {
namespace: true,
state,
actions,
mutations,
getters
}

View file

@ -0,0 +1,71 @@
import {bus} from '../../main'
import {router} from '../../router'
import Vue from 'vue';
const state = {
messages: {}
}
const getters = {
messages(state) {
return state.messages;
}
}
const actions = {
messages(context, data) {
context.commit('messages', data)
},
addMessage(context, data) {
if (data.sender) {
data.message.creator = context.getters.user
data.message.status = 0;
}
context.commit('addMessage', data);
},
replaceMessage(context, data) {
context.commit('replaceMessage', data)
}
}
const mutations = {
messages(state, data) {
Vue.set(state.messages, data.channelID, data.messages.reverse())
setTimeout(() => {
bus.$emit('scrollDown');
}, 300);
},
addMessage(state, data) {
bus.$emit('scrollDown');
Vue.set(
state.messages[data.channelID],
state.messages[data.channelID].length,
data.message
);
},
replaceMessage (state, data) {
const {tempID, message} = data;
state.messages[message.channelID].find((o, i) => {
if(o.tempID === tempID) {
Vue.set(
state.messages[message.channelID],
i,
message
)
return true;
}
})
}
}
export default {
namespace: true,
state,
actions,
mutations,
getters
}

View file

@ -0,0 +1,70 @@
import {bus} from '../../main'
import {router} from './../../router'
const state = {
}
const actions = {
socket_error(context, error) {
// if the token is invalid.
if (error === "Authentication error") {
context.commit('logout')
router.push({ path: '/' })
}
},
socket_success(context, data) {
const friendsArray = data.user.friends;
const friendObject = {};
if(friendsArray !== undefined && friendsArray.length >=1) {
for (let index = 0; index < friendsArray.length; index++) {
const element = friendsArray[index];
friendObject[element.recipient.uniqueID] = element;
for (let currentFriendStatus of data.currentFriendStatus){
if(currentFriendStatus[0] == element.recipient.uniqueID){
friendObject[element.recipient.uniqueID].recipient.status = currentFriendStatus[1]
}
}
}
data.user.friends = friendObject;
}
context.commit('user', data.user)
},
socket_relationshipAdd(context, friend) {
context.commit('addFriend', friend)
},
socket_relationshipAccept(context, uniqueID) {
context.commit('acceptFriend', uniqueID)
},
socket_relationshipRemove(context, uniqueID) {
context.commit('removeFriend', uniqueID)
},
socket_receiveMessage(context, data) {
context.dispatch('addMessage', {
message: data.message,
channelID: data.message.channelID,
tempID: data.tempID
})
},
socket_userStatusChange(context, data) {
context.commit('userStatusChange', data)
},
socket_multiDeviceStatus(context, data) {
context.commit('changeStatus', data.status)
},
socket_disconnect(context) {
context.commit('user', null)
},
socket_multiDeviceUserAvatarChange(context, data) {
context.commit('changeAvatar', data.avatarID);
},
socket_userAvatarChange(context, data) {
context.commit('userAvatarChange', data)
}
}
export default {
namespace: true,
actions
}

View file

@ -0,0 +1,101 @@
import axios from 'axios'
import Vue from 'vue'
import {bus} from '../../main'
import VueRouter from 'vue-router';
const state = {
token: localStorage.getItem('hauthid') || null,
channelID: "",
user: null,
}
const getters = {
tokenExists(state) {
return state.token !== null;
},
user(state){
return state.user;
},
loggedIn(state){
return state.user !== null
}
}
const actions = {
token(context, token) {
context.commit('token', token);
},
user(context, user) {
context.commit('user', user)
},
logout(context) {
context.commit('logout');
},
changeStatus(context, status) {
context.commit('changeStatus', status)
}
}
const mutations = {
changeAvatar(state, avatar) {
//changes my avatar
Vue.set(state.user, "avatar", avatar)
},
changeStatus(state, status) {
//changes my status
Vue.set(state.user, "status", status)
},
userStatusChange(state, data) {
// changes friends status
const friends = state.user.friends;
friends[data.uniqueID].recipient.status = data.status;
state.user.friends = Object.assign({}, friends)
},
userAvatarChange(state, data) {
// changes friends status
const friends = state.user.friends;
friends[data.uniqueID].recipient.avatar = data.avatarID;
state.user.friends = Object.assign({}, friends)
},
token(state, token) {
axios.defaults.headers.common['authorization'] = token;
localStorage.setItem('hauthid', token);
state.token = token
},
logout(state) {
axios.defaults.headers.common['authorization'] = "";
localStorage.removeItem('hauthid')
state.user = null,
state.token = null;
},
user(state, user) {
Vue.set(state, 'user', user)
},
addFriend(state, friend) {
const friends = state.user.friends;
friends[friend.recipient.uniqueID] = friend;
state.user.friends = Object.assign({}, friends)
},
removeFriend(state, uniqueID) {
const friends = state.user.friends;
delete friends[uniqueID];
state.user.friends = Object.assign({}, friends)
},
acceptFriend(state, uniqueID) {
const friends = state.user.friends;
friends[uniqueID].status = 2;
state.user.friends = Object.assign({}, friends)
}
}
export default {
namespace: true,
state,
getters,
actions,
mutations
}

174
src/views/App.vue Normal file
View file

@ -0,0 +1,174 @@
<template>
<div id="app">
<vue-headful title="Nertivia"/>
<div class="background-image"></div>
<transition name="fade-between-two" appear >
<ConnectingScreen v-if="!loggedIn" />
<div class="box" v-if="loggedIn">
<div class="panel-layout">
<transition name="slidein">
<LeftPanel class="left-panel" v-click-outside="hideLeftPanel" v-show="$mq === 'mobile' && showLeftPanel || $mq === 'desktop'"> </LeftPanel>
</transition>
<RightPanel> </RightPanel>
</div>
</div>
</transition>
<transition name="fade">
<settings v-if="showSettings && loggedIn" />
</transition>
</div>
</template>
<script>
import {bus} from '../main'
import Settings from '@/components/app/Settings.vue'
import LeftPanel from './../components/app/LeftPanel.vue'
import RightPanel from './../components/app/RightPanel.vue'
import ConnectingScreen from './../components/app/ConnectingScreen.vue'
export default {
name: 'app',
components: {
LeftPanel,
RightPanel,
ConnectingScreen,
Settings
},
data() {
return {
showLeftPanel: false,
showSettings: false
}
},
methods: {
hideLeftPanel(test) {
if (this.showLeftPanel){
if(test.target.closest('.show-menu-button') == null){
this.showLeftPanel = false;
}
}
}
},
mounted() {
bus.$on('toggleLeftMenu', () => {
this.showLeftPanel = !this.showLeftPanel;
})
bus.$on('openSettings', () => {
this.showSettings = true;
})
bus.$on('closeSettings', () => {
this.showSettings = false;
})
},
computed: {
loggedIn() {
return this.$store.getters.loggedIn
}
}
}
</script>
<style scoped>
.slidein-enter-active, .slidein-leave-active {
transition: .5s;
}
.slidein-enter, .slidein-leave-to /* .fade-leave-active below version 2.1.8 */ {
margin-left: -300px;
}
.fade-between-two-enter-active, .fade-between-two-leave-active{
transition: 0.3s;
}
.fade-between-two-enter, .fade-between-two-leave-to {
opacity: 0 !important;
}
.fade-enter-active, .fade-leave-active {
transition: opacity .2s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
@media (max-width: 600px) {
.left-panel{
position: absolute;
top: 47px;
height: calc(100% - 47px);
}
}
</style>
<style>
html{
height: 100%;
}
body{
margin: 0;
height: 100%;
overflow: hidden;
}
#app {
font-family: 'Roboto', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #383838;
height: 100%;
}
.box {
height: 100%;
width: 100%;
}
.background-image {
background: url(./../assets/background.jpg);
position: absolute;
z-index: -1;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-repeat: no-repeat;
background-position: bottom;
background-size: cover;
}
.panel-layout {
display: flex;
height: 100%;
}
</style>
<style>
/* ------- SCROLL BAR -------*/
/* width */
::-webkit-scrollbar {
width: 10px;
}
/* Track */
::-webkit-scrollbar-track {
background: #8080806b;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #f5f5f559;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: #f5f5f59e;
}
</style>

432
src/views/HomePage.vue Normal file
View file

@ -0,0 +1,432 @@
<template>
<div id="app">
<vue-headful title="Nertivia" />
<div class="background-image"></div>
<div class="layout">
<div class="small-view-nav-bar">
<div class="small-logo"></div>
<div class="small-title">Nertivia</div>
<div class="show-menu-button" @click="showMobileMenu = !showMobileMenu">
<i class="material-icons">
menu
</i>
</div>
</div>
<div class="panels">
<div class="left-panel">
<div class="title">The best chat client that wont sell your data.</div>
<img src="../assets/graphics/HomeGraphics.png" class="graphic-app" />
<div class="change-log-mini" @click="showChangeLog = true">
<div class="change-title">Change log <span style="font-size: 15px; color: rgba(211, 211, 211, 0.774);">Click for details</span></div>
<div class="change-list">
<div class="change" v-for="change in changelogFiltered" :key="change.title">
<div class="notable-changes">{{change.shortTitle}}</div>
<div class="change-date">{{change.date}}</div>
</div>
</div>
</div>
</div>
<RightPanel :class="{'show-menu-content': showMobileMenu }" />
</div>
</div>
<transition name="fade">
<ChangeLog v-if="showChangeLog"/>
</transition>
</div>
</template>
<script>
import {bus} from '../main';
import RightPanel from "./../components/homePage/RightPanel.vue"
import ChangeLog from "./../components/ChangeLog.vue"
import changelog from '@/changelog.js'
export default {
components: {
RightPanel,
ChangeLog
},
data() {
return {
loginSelected: true,
showMobileMenu: false,
showChangeLog: false,
changelog
}
},
mounted() {
bus.$on('closeChangeLog', () => {
this.showChangeLog = false;
})
},
computed: {
changelogFiltered() {
return this.changelog.slice(0, 3)
}
}
}
</script>
<style scoped>
.fade-enter-active, .fade-leave-active {
transition: opacity .2s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
#app {
font-family: 'Roboto', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #383838;
height: 100%;
}
.spinner{
margin: auto;
padding: 30px;
}
.background-image {
background: url(./../assets/background.jpg);
position: absolute;
z-index: -1;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-repeat: no-repeat;
background-position: bottom;
background-size: cover;
}
.layout{
display: flex;
height: 100%;
width:100%;
flex-direction: column;
}
.panels{
display: flex;
height: 100%;
width: 100%;
}
.left-panel {
flex: 1;
background: rgba(0, 0, 0, 0.253);
overflow: auto;
display: flex;
flex-direction: column;
}
.loader{
display: flex;
flex-direction: column;
}
.title-panel{
width: 100%;
height: 150px;
}
.graphics-panel{
flex: 1;
}
.graphic-app{
display: table;
margin: auto;
margin-top: 20px;
margin-bottom: 20px;
width: 900px;
height: auto;
user-select: none;
}
.title{
color: white;
font-size: 35px;
text-align: center;
margin-top: 120px;
}
.change-log-mini{
background: rgba(0, 0, 0, 0.322);
height: 150px;
width: 640px;
margin: auto;
margin-top: 20px;
color: white;
margin-bottom: 50px;
}
.change-title {
font-size: 18px;
margin-top: 10px;
margin-bottom: 10px;
margin-left: 10px;
user-select: none;
}
.change-list{
display: flex;
}
.change {
background: rgba(0, 0, 0, 0.335);
width: 200px;
height: 90px;
margin-left: 10px;
border-radius: 5px;
transition: 0.3s;
position: relative
}
.change:hover {
background: rgba(0, 0, 0, 0.466);
}
.notable-changes{
margin: 10px;
cursor: default;
user-select: none;
}
.change-date{
position: absolute;
bottom: 10px;
right: 10px;
color: rgba(255, 255, 255, 0.753);
cursor: default;
user-select: none;
}
.small-view-nav-bar{
width: 100%;
height: 50px;
background: rgba(0, 0, 0, 0.411);
display: none;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.small-logo {
height: 30px;
width: 30px;
background: url(./../assets/logo.png);
background-size: 125%;
background-position: center;
border-radius: 50%;
box-shadow: 0px 0px 96px -4px rgba(69,212,255,1);
margin: auto;
margin-left: 10px;
flex-shrink: 0;
}
.small-title{
color: white;
font-size: 20px;
flex: 1;
margin-left: 10px;
}
.show-menu-button{
color: rgba(255, 255, 255, 0.698);
margin-right: 20px;
margin-top: 7px;
user-select: none;
transition: 0.3s
}
.show-menu-button:hover {
color: rgb(255, 255, 255);
}
.show-menu-content {
display: flex !important;
width: 400px !important;
opacity: 1 !important;
transform: scale(1) !important;
}
@media (max-width: 1051px) {
.change:nth-child(3){
display: none;
}
.change-log-mini{
width: 430px;
}
}
@media (max-width: 906px) {
.change:nth-child(3){
display: block;
}
.change-log-mini{
width: 640px;
}
}
@media (max-width: 649px) {
.change-list{
flex-direction: column;
}
.change-log-mini{
height: initial;
width: calc(100% - 20px);
padding-bottom: 10px;
margin: auto;
}
.change{
margin-bottom: 5px;
margin-left: 5px;
margin-right: 0;
width: calc(100% - 10px);
}
}
@media (max-width: 1380px) {
.graphic-app{
width: calc(100% - 80px);
}
.title{
font-size: 30px;
margin-left: 20px;
margin-right: 20px;
}
}
@media (max-width: 906px) {
.right-panel-home {
position: absolute;
bottom: 0;
right: 0;
top: 50px;
display: flex;
margin-right: 0;
margin-top: 0;
height:calc(100% - 50px);
background-color: rgba(34, 34, 34, 0.877);
width: 0;
overflow-x: hidden;
transition: 0.5s ease;
transform: scale(0.97);
opacity: 0;
}
.right-panel-inner{
width: 400px;
}
.small-view-nav-bar{
display: flex;
}
}
@media (max-width: 401px) {
.show-menu-content {
width: 100% !important;
}
.right-panel-inner{
width: 100%;
}
}
</style>
<!-- Used for forms !-->
<style>
@media (max-width: 1380px) {
.graphic-app{
width: calc(100% - 80px);
}
}
@media (max-width: 906px) {
.right-panel-home {
position: absolute;
bottom: 0;
right: 0;
top: 50px;
display: flex;
margin-right: 0;
margin-top: 0;
height:calc(100% - 50px);
background-color: rgba(34, 34, 34, 0.877);
width: 0;
overflow-x: hidden;
transition: 0.5s ease;
transform: scale(0.97);
opacity: 0;
}
.right-panel-inner{
width: 400px;
}
.small-view-nav-bar{
display: flex;
}
}
@media (max-width: 401px) {
.show-menu-content {
width: 100% !important;
}
.right-panel-inner{
width: 100%;
}
}
.form {
color: white;
margin: auto;
padding: 10px;
}
input{
padding: 10px;
background: rgba(0, 0, 0, 0.301);
outline: none;
border: none;
color: white;
margin-top: 5px;
margin-bottom: 10px;
width: 200px;
transition: 0.3s;
}
input:hover{
background: rgba(0, 0, 0, 0.452);
}
input:focus {
background: rgba(0, 0, 0, 0.603);
}
.input-title{
margin-top: 10px;
}
.form-button{
padding: 10px;
background: rgba(0, 0, 0, 0.226);
display: table;
transition: 0.5s;
margin: auto;
color: white;
border: none;
outline: none;
}
.form-button:hover{
background: rgba(0, 0, 0, 0.534)
}
.alert{
color: red;
font-size: 15px;
width: 220px;
}
</style>

3
vue.config.js Normal file
View file

@ -0,0 +1,3 @@
module.exports = {
runtimeCompiler: true
}