Add settings
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Jack Hadrill 2022-03-22 00:42:53 +00:00
parent 561e983ae6
commit 2df8b9a241
25 changed files with 520 additions and 195 deletions

168
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "mercury",
"version": "0.0.0",
"dependencies": {
"@vueuse/core": "^8.1.2",
"animate.css": "^4.1.1",
"bennc-js": "git+https://git.jacknet.io/TerribleCodeClub/bennc-js.git",
"bootstrap": "^5.1.3",
@ -22,6 +23,7 @@
"vue-router": "^4.0.14"
},
"devDependencies": {
"@types/bootstrap": "^5.1.9",
"@vitejs/plugin-vue": "^2.2.0",
"vite": "^2.8.0"
}
@ -41,7 +43,6 @@
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.4.tgz",
"integrity": "sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg==",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
@ -52,6 +53,16 @@
"resolved": "https://registry.npmjs.org/@sphinxxxx/color-conversion/-/color-conversion-2.2.2.tgz",
"integrity": "sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw=="
},
"node_modules/@types/bootstrap": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.9.tgz",
"integrity": "sha512-Tembe6lt7819EUzV5LSG9uuwULm4hdEGV9LZ8QBYpWc0J+a+9DdmJEwZ4FMaXGVJWwumTPSkJ8JQF0/KDAmXYg==",
"dev": true,
"dependencies": {
"@popperjs/core": "^2.9.2",
"@types/jquery": "*"
}
},
"node_modules/@types/color": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.3.tgz",
@ -73,6 +84,21 @@
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
},
"node_modules/@types/jquery": {
"version": "3.5.14",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.14.tgz",
"integrity": "sha512-X1gtMRMbziVQkErhTQmSe2jFwwENA/Zr+PprCkF63vFq+Yt5PZ4AlKqgmeNlwgn7dhsXEK888eIW2520EpC+xg==",
"dev": true,
"dependencies": {
"@types/sizzle": "*"
}
},
"node_modules/@types/sizzle": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
"dev": true
},
"node_modules/@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
@ -198,6 +224,87 @@
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.31.tgz",
"integrity": "sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ=="
},
"node_modules/@vueuse/core": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-8.1.2.tgz",
"integrity": "sha512-prI2GzigBUtJNTcwRjJPzUPLFoRZM1RZFR464DFdwgU8TxRFf7dRvuvWFDNbCATzLExHFnGI3zTp9GkXTTZxgQ==",
"dependencies": {
"@vueuse/metadata": "8.1.2",
"@vueuse/shared": "8.1.2",
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.1.0",
"vue": "^2.6.0 || ^3.2.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
},
"vue": {
"optional": true
}
}
},
"node_modules/@vueuse/core/node_modules/@vueuse/shared": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-8.1.2.tgz",
"integrity": "sha512-4Hb9iPUhAz7ghO4hgvB2GV2FOy12qQGdhmQ+9HC6QN/J66DELhmxAvkZAtK5FBqZOSwzKszPqNqoyhRKQrrWGQ==",
"dependencies": {
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.1.0",
"vue": "^2.6.0 || ^3.2.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
},
"vue": {
"optional": true
}
}
},
"node_modules/@vueuse/core/node_modules/vue-demi": {
"version": "0.12.4",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.12.4.tgz",
"integrity": "sha512-ztPDkFt0TSUdoq1ZI6oD730vgztBkiByhUW7L1cOTebiSBqSYfSQgnhYakYigBkyAybqCTH7h44yZuDJf2xILQ==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vueuse/metadata": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-8.1.2.tgz",
"integrity": "sha512-LrPtdiYMleygnGmz8mEmYI9h4Eyo+/igxZWNrwuPnqvL9pIO+8eUpBgPLH5GowKv3Nu0LPZSXSIuaWVJBSU1Cg==",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/animate.css": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz",
@ -965,14 +1072,23 @@
"@popperjs/core": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.4.tgz",
"integrity": "sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg==",
"peer": true
"integrity": "sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg=="
},
"@sphinxxxx/color-conversion": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@sphinxxxx/color-conversion/-/color-conversion-2.2.2.tgz",
"integrity": "sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw=="
},
"@types/bootstrap": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.9.tgz",
"integrity": "sha512-Tembe6lt7819EUzV5LSG9uuwULm4hdEGV9LZ8QBYpWc0J+a+9DdmJEwZ4FMaXGVJWwumTPSkJ8JQF0/KDAmXYg==",
"dev": true,
"requires": {
"@popperjs/core": "^2.9.2",
"@types/jquery": "*"
}
},
"@types/color": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.3.tgz",
@ -994,6 +1110,21 @@
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
},
"@types/jquery": {
"version": "3.5.14",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.14.tgz",
"integrity": "sha512-X1gtMRMbziVQkErhTQmSe2jFwwENA/Zr+PprCkF63vFq+Yt5PZ4AlKqgmeNlwgn7dhsXEK888eIW2520EpC+xg==",
"dev": true,
"requires": {
"@types/sizzle": "*"
}
},
"@types/sizzle": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
"dev": true
},
"@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
@ -1110,6 +1241,37 @@
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.31.tgz",
"integrity": "sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ=="
},
"@vueuse/core": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-8.1.2.tgz",
"integrity": "sha512-prI2GzigBUtJNTcwRjJPzUPLFoRZM1RZFR464DFdwgU8TxRFf7dRvuvWFDNbCATzLExHFnGI3zTp9GkXTTZxgQ==",
"requires": {
"@vueuse/metadata": "8.1.2",
"@vueuse/shared": "8.1.2",
"vue-demi": "*"
},
"dependencies": {
"@vueuse/shared": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-8.1.2.tgz",
"integrity": "sha512-4Hb9iPUhAz7ghO4hgvB2GV2FOy12qQGdhmQ+9HC6QN/J66DELhmxAvkZAtK5FBqZOSwzKszPqNqoyhRKQrrWGQ==",
"requires": {
"vue-demi": "*"
}
},
"vue-demi": {
"version": "0.12.4",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.12.4.tgz",
"integrity": "sha512-ztPDkFt0TSUdoq1ZI6oD730vgztBkiByhUW7L1cOTebiSBqSYfSQgnhYakYigBkyAybqCTH7h44yZuDJf2xILQ==",
"requires": {}
}
}
},
"@vueuse/metadata": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-8.1.2.tgz",
"integrity": "sha512-LrPtdiYMleygnGmz8mEmYI9h4Eyo+/igxZWNrwuPnqvL9pIO+8eUpBgPLH5GowKv3Nu0LPZSXSIuaWVJBSU1Cg=="
},
"animate.css": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz",

View File

@ -8,6 +8,7 @@
"preview": "vite preview"
},
"dependencies": {
"@vueuse/core": "^8.1.2",
"animate.css": "^4.1.1",
"bennc-js": "git+https://git.jacknet.io/TerribleCodeClub/bennc-js.git",
"bootstrap": "^5.1.3",
@ -22,6 +23,7 @@
"vue-router": "^4.0.14"
},
"devDependencies": {
"@types/bootstrap": "^5.1.9",
"@vitejs/plugin-vue": "^2.2.0",
"vite": "^2.8.0"
}

View File

@ -1,4 +1,16 @@
<template>
<div ref="settingsModal" class="modal fade" id="staticBackdrop" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="staticBackdropLabel">Configure your user</h5>
</div>
<div class="modal-body">
<UserSettings />
</div>
</div>
</div>
</div>
<div class="container-fluid vh-100">
<div class="row vh-100">
<router-view />
@ -7,6 +19,31 @@
</template>
<script setup>
import { onMounted, ref, watch } from 'vue'
import { Modal } from 'bootstrap'
import { useMercuryStore } from './stores/mercuryStore'
import UserSettings from './components/userSettings/UserSettings.vue'
const mercuryStore = useMercuryStore()
const configured = mercuryStore.configured()
const settingsModal = ref()
onMounted(() => {
const modal = new Modal(settingsModal.value)
watch(configured, () => {
switch(configured.value) {
case true:
modal.hide()
break
case false:
modal.show()
break
}
}, { immediate: true })
})
</script>
<style>

12
src/common/base64.js Normal file
View File

@ -0,0 +1,12 @@
export function fromBase64(base64) {
return Uint8Array.from(window.atob(base64), c => c.charCodeAt(0))
}
export function toBase64(arr) {
return window.btoa(String.fromCharCode(...arr))
}
export function validateBase64(base64) {
const base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/
return base64Regex.test(base64)
}

6
src/common/colors.js Normal file
View File

@ -0,0 +1,6 @@
export default {
Primary: getComputedStyle(document.documentElement).getPropertyValue('--bs-primary'),
Secondary: getComputedStyle(document.documentElement).getPropertyValue('--bs-secondary'),
Random: () => { return '#' + Math.floor(Math.random()*16777215).toString(16) }
}

3
src/common/constants.js Normal file
View File

@ -0,0 +1,3 @@
export default {
SelfId: -1
}

View File

@ -0,0 +1,58 @@
<template>
<h3>Add channel</h3>
<div class="row">
<div class="col">
<input v-model="name" type="text" class="form-control" placeholder="Channel name" aria-label="Channel name">
</div>
<div class="col">
<input v-model="key" type="text" class="form-control" placeholder="Channel key (base64)" aria-label="Channel key">
</div>
</div>
<div class="d-flex flex-row mt-3">
<button @click="addChannel" type="submit" class="btn btn-primary ms-auto">Add channel</button>
</div>
</template>
<script setup>
import { inject, ref } from 'vue';
import { useChannelStore } from '../../stores/channelStore'
import { fromBase64, toBase64, validateBase64 } from '../../common/base64'
const channelStore = useChannelStore()
const requestUserData = inject('requestUserData')
const name = ref('')
const key = ref('')
function padKey(key) {
const decodedKey = fromBase64(key)
const paddedKey = new Uint8Array(16)
paddedKey.set(decodedKey)
return toBase64(paddedKey)
}
function addChannel() {
const channelNames = channelStore.channels.map(channel => channel.name)
if (name.value.trim() === '' || key.value.trim() === '') {
alert('You must specify a name and key.')
return
}
if (channelNames.includes(name.value.trim())) {
alert(`A channel with name '${name.value}' already exists.`)
return
}
if (!validateBase64(key.value.trim())) {
alert(`You must specify the key in base64 format.`)
return
}
if (((4 * key.value.trim().length / 3) + 3) & ~3 > 16) {
alert('The maximum key length is 16 bytes.')
return
}
const paddedKey = padKey(key.value.trim())
channelStore.addChannel(name.value.trim(), paddedKey)
requestUserData()
name.value = ''
key.value = ''
}
</script>

View File

@ -0,0 +1,36 @@
<template>
<h3>Channels</h3>
<table class="table mb-5">
<thead>
<th scope="col">Name</th>
<th scope="col">Key</th>
<th scope="col">Remember</th>
<th scope="col" class="text-end">Delete</th>
</thead>
<tbody>
<tr v-for="channel in channelStore.channels" :key="channel.id">
<td class="w-25">{{ channel.name }}</td>
<td class="w-25"><Key :channel="channel" /></td>
<td class="w-25">
<div class="form-check form-switch">
<input v-if="channel.name === 'Default'" class="form-check-input" type="checkbox" checked disabled>
</div>
</td>
<td class="w-25 text-end"><i v-if="channel.name !== 'Default'" class="text-danger text-end bi bi-trash-fill"></i></td>
</tr>
</tbody>
</table>
</template>
<script setup>
import { useChannelStore } from '../../stores/channelStore'
import Key from '../common/Key.vue'
const channelStore = useChannelStore()
</script>
<style scope>
i {
cursor: pointer;
}
</style>

View File

@ -8,7 +8,7 @@ import { computed, ref } from 'vue'
import { onBeforeRouteUpdate } from 'vue-router'
const props = defineProps(['channel'])
const key = computed(() => { return props.channel.key.base64 })
const key = computed(() => { return props.channel.key })
const locked = ref(true)

View File

@ -1,7 +1,7 @@
<template>
<div>
<span class="text-muted">{{ message.received.toLocaleTimeString() }}&nbsp;</span>
<span class="fw-bold" :style="{ color: user.color.hex() }">{{ user.name }}&nbsp;</span>
<span class="fw-bold" :style="{ color: user.color }">{{ user.name }}&nbsp;</span>
<span>{{ message.content }}</span>
</div>
</template>
@ -9,10 +9,14 @@
<script setup>
import { computed } from 'vue'
import { useUserStore } from '../../stores/userStore'
import Constants from '../../common/constants'
import { useMercuryStore } from '../../stores/mercuryStore';
const props = defineProps(['message'])
const message = props.message
const mercuryStore = useMercuryStore()
const userStore = useUserStore()
const user = computed(() => { return userStore.getUserById(message.userId)})
const user = computed(() => { return message.userId === Constants.SelfId ? mercuryStore.user : userStore.getUserById(message.userId) })
</script>

View File

@ -27,7 +27,7 @@ const sendMessage = () => {
if (message.value.trim() === '') {
animateCSS(input.value, 'headShake')
} else {
ws.send(packers[MessageTypes.Basic](encoder.encode(message.value), channel.value.key.raw))
ws.send(packers[MessageTypes.Basic](encoder.encode(message.value), channel.value.rawKey))
messageStore.addMessage(channel.value.id, -1, message.value)
message.value = ''
}

View File

@ -1,62 +0,0 @@
<template>
<div ref="picker" id="picker" class="btn"><div class="label">Set username color</div></div>
</template>
<script setup>
import Color from 'color';
import Picker from 'vanilla-picker'
import { inject, onMounted, ref } from 'vue';
import { useUserStore } from '../../stores/userStore';
const requestUserData = inject('requestUserData')
const userStore = useUserStore()
const self = userStore.getSelf()
const picker = ref(null)
const usernameColor = ref(Color())
const bootstrapPrimaryColor = getComputedStyle(document.documentElement).getPropertyValue('--bs-primary')
onMounted(() => {
const p = new Picker(picker.value)
p.setOptions({
alpha: false,
color: self.color.hex() ?? bootstrapPrimaryColor
})
picker.value.style.backgroundColor = self.color.hex() ?? bootstrapPrimaryColor
p.onChange = function(color) {
picker.value.style.backgroundColor = color.rgbaString
}
p.onOpen = function(color) {
usernameColor.value = Color(color.hex)
}
p.onDone = function(color) {
usernameColor.value = Color(color.hex)
picker.value.style.backgroundColor = color.rgbaString
userStore.setColor(-1, Color(color.hex))
requestUserData()
}
p.onClose = function(color) {
picker.value.style.backgroundColor = usernameColor.value.hex()
}
})
</script>
<style>
#picker .label {
background: inherit;
background-clip: text;
color: transparent;
filter: invert(1) grayscale(1) contrast(9);
}
</style>

View File

@ -2,10 +2,10 @@
<div class="col-2 bg-dark">
<div class="d-flex flex-column vh-100">
<div class="d-flex flex-row logo align-items-center py-2">
<img src="../assets/logo-dark.png" class="me-2" />
<img src="../../assets/logo-dark.png" class="me-2" />
<span class="fs-3 text-light">Mercury</span>
</div>
<div>
<div v-if="configured">
<h5 class="text-muted">Channels</h5>
<Channels />
<slot name="top"></slot>
@ -21,10 +21,13 @@
</template>
<script setup>
import { computed } from 'vue'
import { useMercuryStore } from '../stores/mercuryStore'
import Channels from './sidebar/Channels.vue'
import { storeToRefs } from 'pinia';
import { watch } from 'vue';
import { useMercuryStore } from '../../stores/mercuryStore'
import Channels from './Channels.vue'
const mercuryStore = useMercuryStore()
const connected = computed (() => { return mercuryStore.connected })
const { connected } = storeToRefs(mercuryStore)
const configured = mercuryStore.configured()
</script>

View File

@ -0,0 +1,54 @@
<template>
<div ref="picker" class="btn w-100"><div class="label">Set username color</div></div>
</template>
<script setup>
import Color from 'color';
import Picker from 'vanilla-picker'
import { onMounted, ref } from 'vue'
const props = defineProps({
modelValue: String
})
const emit = defineEmits(['update:modelValue'])
const picker = ref()
onMounted(() => {
var selectedColor = ref(props.modelValue)
picker.value.style.backgroundColor = selectedColor.value
const p = new Picker(picker.value)
p.setOptions({
alpha: false,
color: props.modelValue ?? bootstrapPrimaryColor,
popup: 'bottom'
})
p.onChange = function(color) {
picker.value.style.backgroundColor = Color(color.rgbString).hex()
}
p.onDone = function(color) {
selectedColor.value = Color(color.rgbString).hex()
picker.value.style.backgroundColor = Color(color.rgbString).hex()
emit('update:modelValue', selectedColor.value)
}
p.onClose = function(color) {
picker.value.style.backgroundColor = selectedColor.value
}
})
</script>
<style>
.label {
background: inherit;
background-clip: text;
color: transparent;
filter: invert(1) grayscale(1) contrast(9);
}
</style>

View File

@ -0,0 +1,40 @@
<template>
<input v-model="name" type="text" class="mb-3 form-control" placeholder="Name" aria-label="name" />
<ColorPicker v-model="color" />
<hr>
<div class="d-flex flex-row align-items-center">
<label v-if="name">Preview:</label>
<span class="fw-bold ms-1" :style="{ color: color ?? '#000000' }">{{ name }}</span>
<button @click="save" class="btn btn-primary ms-auto">Save</button>
</div>
</template>
<script setup>
import { inject, ref } from 'vue'
import { useMercuryStore } from '../../stores/mercuryStore'
import Colors from '../../common/colors'
import ColorPicker from './ColorPicker.vue'
const requestUserData = inject('requestUserData')
const mercuryStore = useMercuryStore()
const name = ref()
const color = ref(Colors.Random())
// Set initial username.
if (mercuryStore.user?.name) {
name.value = mercuryStore.user.name
}
// Set initial color.
if (mercuryStore.user?.color) {
color.value = mercuryStore.user.color
}
function save() {
mercuryStore.setUserName(name.value)
mercuryStore.setUserColor(color.value)
requestUserData()
}
</script>

View File

@ -4,13 +4,12 @@
<script setup>
import { computed } from 'vue'
import Colors from '../../common/colors'
const props = defineProps(['user'])
const user = props.user
const bootstrapSecondaryColor = getComputedStyle(document.documentElement).getPropertyValue('--bs-secondary')
const color = computed(() => { return user.color?.hex() ?? bootstrapSecondaryColor })
const color = computed(() => { return user.color ?? Colors.Secondary })
</script>
<style scoped>

View File

@ -1,6 +1,6 @@
<template>
<div class="d-flex flex-column">
<User :user="self" />
<User :user="mercuryStore.user" />
<div v-for="user in users" :key="user.id" class="d-flex flex-row align-items-center">
<User :user="user" />
</div>
@ -9,6 +9,7 @@
<script setup>
import { computed } from 'vue'
import { useMercuryStore } from '../../stores/mercuryStore'
import { useUserStore } from '../../stores/userStore'
import User from './User.vue'
@ -16,7 +17,6 @@ const props = defineProps(['channel'])
const channelId = computed(() => { return props.channel.id })
const userStore = useUserStore()
const mercuryStore = useMercuryStore()
const users = computed(() => { return userStore.getUsersByChannelId(channelId.value) })
const self = userStore.getSelf()
</script>

View File

@ -16,8 +16,8 @@ import websocket from './plugins/websocket'
const app = createApp(App)
const pinia = createPinia()
app.use(router)
app.use(pinia)
app.use(router)
app.use(websocket)
app.mount('#app')

View File

@ -1,11 +1,12 @@
import { watch } from 'vue'
import Color from 'color'
import ReconnectingWebSocket from 'reconnecting-websocket'
import { MessageTypes, numberToUint16BE, packers, unpackers, unpackIncomingPacket } from 'bennc-js'
import { decrypt } from 'romulus-js'
import { useMercuryStore } from '../stores/mercuryStore'
import { useUserStore } from '../stores/userStore'
import { useMessageStore } from '../stores/messageStore'
import { useChannelStore } from '../stores/channelStore'
import { decrypt } from 'romulus-js'
import { provide } from 'vue'
const CLIENT_ID = 'Mercury'
const SUBSCRIBED_MESSAGE_TYPES = [MessageTypes.Basic, MessageTypes.UserDataRequest, MessageTypes.UserDataResponse]
@ -26,12 +27,13 @@ export default {
const messageStore = useMessageStore()
const userStore = useUserStore()
const configured = mercuryStore.configured()
function handleBasicMessage(incomingPacket) {
// Try decryption with each encryption key.
for (let channel of channelStore.channels) {
// Attempt decryption of incoming message.
const result = decrypt(incomingPacket.data, numberToUint16BE(MessageTypes.Basic), channel.key.raw)
const result = decrypt(incomingPacket.data, numberToUint16BE(MessageTypes.Basic), channel.rawKey)
// If encryption key is found...
if (result.success) {
@ -53,7 +55,7 @@ export default {
// Try decryption with each encryption key.
for (let channel of channelStore.channels) {
// Attempt decryption of incoming message.
const result = decrypt(incomingPacket.data, numberToUint16BE(MessageTypes.UserDataRequest), channel.key.raw)
const result = decrypt(incomingPacket.data, numberToUint16BE(MessageTypes.UserDataRequest), channel.rawKey)
// If encryption key is found...
if (result.success) {
@ -62,12 +64,11 @@ export default {
// Update user in user store.
userStore.setName(incomingPacket.senderId, userDataRequest.username)
userStore.setColor(incomingPacket.senderId, userDataRequest.colour)
userStore.setColor(incomingPacket.senderId, userDataRequest.colour.hex())
userStore.addChannel(incomingPacket.senderId, channel.id)
// Respond to user data request with a user data response.
const self = userStore.getSelf()
const userDataResponse = packers[MessageTypes.UserDataResponse]({ username: self.name, colour: self.color, clientId: CLIENT_ID }, channel.key.raw)
const userDataResponse = packers[MessageTypes.UserDataResponse]({ username: mercuryStore.user.name, colour: Color(mercuryStore.user.color), clientId: CLIENT_ID }, channel.rawKey)
ws.send(userDataResponse)
// Don't process other keys.
@ -80,7 +81,7 @@ export default {
// Try decryption with each encryption key.
for (let channel of channelStore.channels) {
// Attempt decryption of incoming message.
const result = decrypt(incomingPacket.data, numberToUint16BE(MessageTypes.UserDataResponse), channel.key.raw)
const result = decrypt(incomingPacket.data, numberToUint16BE(MessageTypes.UserDataResponse), channel.rawKey)
// If encryption key is found...
if (result.success) {
@ -89,7 +90,7 @@ export default {
// Update user in user store.
userStore.setName(incomingPacket.senderId, userDataResponse.username)
userStore.setColor(incomingPacket.senderId, userDataResponse.colour)
userStore.setColor(incomingPacket.senderId, userDataResponse.colour.hex())
userStore.addChannel(incomingPacket.senderId, channel.id)
// Don't process other keys.
@ -99,9 +100,8 @@ export default {
}
function requestUserData() {
const self = userStore.getSelf()
channelStore.channels.forEach(channel => {
const userDataRequest = packers[MessageTypes.UserDataRequest]({ username: self.name, colour: self.color, clientId: CLIENT_ID }, channel.key.raw)
const userDataRequest = packers[MessageTypes.UserDataRequest]({ username: mercuryStore.user.name, colour: Color(mercuryStore.user.color), clientId: CLIENT_ID }, channel.rawKey)
ws.send(userDataRequest)
})
}
@ -114,7 +114,9 @@ export default {
}, 30000)
setInterval(() => {
requestUserData()
if (configured.value) {
requestUserData()
}
}, 30000)
ws.addEventListener('open', () => {
@ -127,7 +129,9 @@ export default {
})
// Make a user data request for each channel.
requestUserData()
if (configured.value) {
requestUserData()
}
})
ws.addEventListener('close', () => {
@ -159,7 +163,6 @@ export default {
// Restart keepalive.
clearInterval(timeout)
timeout = setInterval(() => {
console.log("Keepalive message sent!")
ws.send(packers[MessageTypes.Keepalive]())
}, 30000)
})

View File

@ -1,11 +1,6 @@
import { defineStore } from 'pinia'
class Key {
constructor(base64) {
this.base64 = base64
this.raw = Uint8Array.from(window.atob(base64), c => c.charCodeAt(0))
}
}
import { v4 as uuidv4 } from 'uuid'
import { fromBase64 } from '../common/base64'
export const useChannelStore = defineStore({
id: 'channelStore',
@ -14,12 +9,8 @@ export const useChannelStore = defineStore({
{
id: 'a798423c-3fbf-434d-9a00-57d7642bff65',
name: 'Default',
key: new Key('')
},
{
id: '47198ee7-8bf4-41fe-8b42-bb001bd5f140',
name: 'HackerNews',
key: new Key('HACKERNEWQAAAAAAAAAAAA==')
key: 'AAAAAAAAAAAAAAAAAAAAAA==',
rawKey: new Uint8Array(16)
}
]
}),
@ -29,8 +20,13 @@ export const useChannelStore = defineStore({
}
},
actions: {
addChannel(channel) {
this.channels.push(channel)
addChannel(name, key) {
this.channels.push({
id: uuidv4(),
name: name,
key: key,
rawKey: fromBase64(key)
})
}
}
})

View File

@ -1,14 +1,32 @@
import { computed } from 'vue'
import { defineStore } from 'pinia'
import { useLocalStorage } from '@vueuse/core'
export const useMercuryStore = defineStore({
id: 'mercuryStore',
state: () => ({
user: useLocalStorage('user', {
name: '',
color: null
}),
connected: false
}),
getters: {},
getters: {
configured: (state) => {
return () => computed(() => {
return state.user?.name.trim() !== '' && state.user?.color !== undefined
})
}
},
actions: {
setConnectionState(state) {
this.connected = state
},
setUserName(name) {
this.user.name = name
},
setUserColor(color) {
this.user.color = color
}
}
})

View File

@ -1,23 +1,11 @@
import Color from 'color'
import { defineStore } from 'pinia'
export const useUserStore = defineStore({
id: 'userStore',
state: () => ({
users: [
{
id: -1,
name: 'Mercury',
color: Color('#ff4000'),
channels: [],
self: true
}
]
users: []
}),
getters: {
getSelf: (state) => {
return (self) => state.users.find(user => user.self === true)
},
getUserById: (state) => {
return (id) => state.users.find(user => user.id === id)
},
@ -32,7 +20,7 @@ export const useUserStore = defineStore({
user = {
id: newUser.id,
name: String(newUser.id),
color: Color('#000000'),
color: '#000000',
channels: []
}
this.users.push(user)

View File

@ -1,4 +1,5 @@
<template>
<template v-if="channel">
<Sidebar>
<template v-slot:top>
<h5 class="text-muted mt-3">Users</h5>
@ -18,16 +19,30 @@
</template>
</Content>
</template>
<template v-else>
<Sidebar />
<Content>
<template v-slot:title>Missing channel</template>
<template v-slot:content>
<div class="d-flex flex-column text-center">
<i class="error text-warning bi bi-cone-striped"></i>
<h2>That channel does not exist!</h2>
</div>
</template>
</Content>
</template>
</template>
<script setup>
import { computed, ref } from 'vue'
import { onBeforeRouteUpdate, useRoute } from 'vue-router'
import { useChannelStore } from '../stores/channelStore'
import { useMessageStore } from '../stores/messageStore'
import Sidebar from '../components/Sidebar.vue'
import Sidebar from '../components/sidebar/Sidebar.vue'
import Content from '../components/Content.vue'
import Key from '../components/common/Key.vue'
import UserList from '../components/userlist/UserList.vue'
import UserList from '../components/userList/UserList.vue'
import Message from '../components/conversation/Message.vue'
import MessageInput from '../components/conversation/MessageInput.vue'
@ -43,3 +58,9 @@ const locked = ref(false)
onBeforeRouteUpdate(async (to, from) => { locked.value = false })
const abcdef = ref(null)
</script>
<style scoped>
i.error {
font-size: 10rem;
}
</style>

View File

@ -1,6 +1,5 @@
<template>
<Sidebar>
</Sidebar>
<Sidebar />
<Content>
<template v-slot:title>Welcome to Mercury!</template>
<template v-slot:content>
@ -9,16 +8,16 @@
<h1 class="animate__animated animate__zoomIn">
Mercury is a web-based BENNC client.
</h1>
<h2 class="animate__animated animate__zoomIn">
<h3 class="animate__animated animate__zoomIn">
Go to <router-link :to="{ name: 'Settings' }" class="text-muted"><i class="bi bi-gear-fill"></i> Settings</router-link> to configure your client.
</h2>
</h3>
</div>
</template>
</Content>
</template>
<script setup>
import Sidebar from '../components/Sidebar.vue'
import Sidebar from '../components/sidebar/Sidebar.vue'
import Content from '../components/Content.vue'
</script>
@ -26,10 +25,4 @@ import Content from '../components/Content.vue'
#logo {
filter: invert(80%);
}
/* h1 {
--animate-delay: 0.5s;
}
h2 {
--animate-delay: 0.5s;
} */
</style>

View File

@ -5,68 +5,20 @@
<template v-slot:content>
<h3>User</h3>
<div class="mb-5">
<div class="input-group mb-3">
<input @keyup.enter="saveUsername" v-model="username" type="text" class="form-control" placeholder="Username" aria-label="Username">
<button @click="saveUsername" class="btn btn-primary" type="button">Save</button>
</div>
<ColorPicker></ColorPicker>
<UserSettings />
</div>
<h3>Channels</h3>
<table class="table mb-5">
<thead>
<th scope="col">Name</th>
<th scope="col">Key</th>
<th scope="col" class="text-end">Delete</th>
</thead>
<tbody>
<tr v-for="channel in channelStore.channels" :key="channel.id">
<td class="w-25">{{ channel.name }}</td>
<td class="w-25"><Key :channel="channel" /></td>
<td class="w-25 text-end"><i class="deleteChannel bi bi-trash-fill text-danger text-end"></i></td>
</tr>
</tbody>
</table>
<h3>Add channel</h3>
<div class="row mb-3">
<div class="col">
<input type="text" class="form-control" placeholder="Channel name" aria-label="Channel name">
</div>
<div class="col">
<input type="password" class="form-control" placeholder="Channel key" aria-label="Channel key">
</div>
</div>
<button type="submit" class="btn btn-primary float-end">Add channel</button>
<ConfiguredChannels />
<AddChannel />
</template>
</Content>
</template>
<script setup>
import { useChannelStore } from '../stores/channelStore'
import Key from '../components/common/Key.vue'
import Sidebar from '../components/Sidebar.vue'
import Sidebar from '../components/sidebar/Sidebar.vue'
import Content from '../components/Content.vue'
import ColorPicker from '../components/settings/ColorPicker.vue';
import { inject, ref } from 'vue';
import { useUserStore } from '../stores/userStore';
import UserSettings from '../components/userSettings/UserSettings.vue'
import ConfiguredChannels from '../components/channelSettings/ConfiguredChannels.vue'
import AddChannel from '../components/channelSettings/AddChannel.vue'
const channelStore = useChannelStore()
const userStore = useUserStore()
const requestUserData = inject('requestUserData')
const self = userStore.getSelf()
const username = ref('')
username.value = self.name
function saveUsername() {
userStore.setName(-1, username.value)
requestUserData()
}
</script>
<style scope>
.deleteChannel {
cursor: pointer;
}
</style>