This commit is contained in:
Troplo 2022-07-29 01:12:29 +10:00
parent 87302afa1d
commit be90a91e52
22 changed files with 2768 additions and 1119 deletions

View file

@ -7,3 +7,4 @@ VUE_APP_MATOMO_TRACKER=flow
VUE_APP_MATOMO_DOMAINS=*.troplo.com VUE_APP_MATOMO_DOMAINS=*.troplo.com
VUE_APP_MATOMO_ENABLED=true VUE_APP_MATOMO_ENABLED=true
VUE_APP_SENTRY_ENABLED=true VUE_APP_SENTRY_ENABLED=true
VUE_APP_BASE_URL=

View file

@ -34,12 +34,36 @@ module.exports = async function (socket, next) {
next() next()
} }
} else { } else {
// socket.user = {
id: null,
username: "Not Authenticated"
}
socket.compassUser = {
id: null,
username: "BC-NOAUTH"
}
next()
} }
} else { } else {
// socket.user = {
id: null,
username: "Not Authenticated"
}
socket.compassUser = {
id: null,
username: "BC-NOAUTH"
}
next()
} }
} catch (error) { } catch (error) {
// socket.user = {
id: null,
username: "Not Authenticated"
}
socket.compassUser = {
id: null,
username: "BC-NOAUTH"
}
next()
} }
} }

View file

@ -15,88 +15,115 @@ module.exports = {
id: socket.user.id id: socket.user.id
} }
}) })
socket.join(user.id) if (user && socket.user.id) {
socket.emit("siteState", { console.log(socket.user.id)
release: process.env.RELEASE, socket.join(user.id)
notification: process.env.NOTIFICATION, socket.emit("siteState", {
notificationType: process.env.NOTIFICATION_TYPE, release: process.env.RELEASE,
latestVersion: require("../../package.json").version notification: process.env.NOTIFICATION,
}) notificationType: process.env.NOTIFICATION_TYPE,
const friends = await Friend.findAll({ latestVersion: require("../../package.json").version
where: { })
userId: user.id, const friends = await Friend.findAll({
status: "accepted" where: {
} userId: user.id,
}) status: "accepted"
await user.update({ }
status: })
user.storedStatus === "invisible" ? "offline" : user.storedStatus await user.update({
})
friends.forEach((friend) => {
io.to(friend.friendId).emit("userStatus", {
userId: user.id,
status: status:
user.storedStatus === "invisible" ? "offline" : user.storedStatus user.storedStatus === "invisible" ? "offline" : user.storedStatus
}) })
})
socket.on("ping", () => {
socket.emit("pong")
})
socket.on("idle", async () => {
const user = await User.findOne({
where: {
id: socket.user.id
}
})
if (user.storedStatus === "online") {
friends.forEach((friend) => {
io.to(friend.friendId).emit("userStatus", {
userId: user.id,
status: "away"
})
})
io.to(user.id).emit("userStatus", {
userId: user.id,
status: "away"
})
await user.update({
status: "away"
})
}
})
socket.on("online", async () => {
const user = await User.findOne({
where: {
id: socket.user.id
}
})
if (user.storedStatus === "online") {
friends.forEach((friend) => {
io.to(friend.friendId).emit("userStatus", {
userId: user.id,
status: "online"
})
})
io.to(user.id).emit("userStatus", {
userId: user.id,
status: "online"
})
await user.update({
status: "online"
})
}
})
socket.on("disconnect", async function () {
friends.forEach((friend) => { friends.forEach((friend) => {
io.to(friend.friendId).emit("userStatus", { io.to(friend.friendId).emit("userStatus", {
userId: user.id, userId: user.id,
status:
user.storedStatus === "invisible" ? "offline" : user.storedStatus
})
})
socket.on("ping", () => {
socket.emit("pong")
})
socket.on("bcBots/deleteMessage", (e) => {
if (socket.user.bot) {
socket.to(e.userId).emit("deleteMessage", e)
} else {
socket.emit("bcBots/deleteMessage", {
error: "You cannot perform this action."
})
}
})
socket.on("idle", async () => {
const user = await User.findOne({
where: {
id: socket.user.id
}
})
if (user.storedStatus === "online") {
friends.forEach((friend) => {
io.to(friend.friendId).emit("userStatus", {
userId: user.id,
status: "away"
})
})
io.to(user.id).emit("userStatus", {
userId: user.id,
status: "away"
})
await user.update({
status: "away"
})
}
})
socket.on("online", async () => {
const user = await User.findOne({
where: {
id: socket.user.id
}
})
if (user.storedStatus === "online") {
friends.forEach((friend) => {
io.to(friend.friendId).emit("userStatus", {
userId: user.id,
status: "online"
})
})
io.to(user.id).emit("userStatus", {
userId: user.id,
status: "online"
})
await user.update({
status: "online"
})
}
})
socket.on("disconnect", async function () {
friends.forEach((friend) => {
io.to(friend.friendId).emit("userStatus", {
userId: user.id,
status: "offline"
})
})
await user.update({
status: "offline" status: "offline"
}) })
}) })
await user.update({ } else {
status: "offline" socket.join(-1)
socket.emit("siteState", {
release: process.env.RELEASE,
notification: process.env.NOTIFICATION,
notificationType: process.env.NOTIFICATION_TYPE,
latestVersion: require("../../package.json").version
}) })
}) socket.emit("unauthorized", {
message: "Please reauth."
})
console.log("Unauthenticated user")
socket.on("reAuth", async () => {
socket.disconnect()
})
}
}) })
console.log("WS OK") console.log("WS OK")
app.set("io", io) app.set("io", io)

View file

@ -0,0 +1,43 @@
"use strict"
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable("Nicknames", {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.BIGINT
},
userId: {
allowNull: false,
type: Sequelize.BIGINT
},
nickname: {
allowNull: false,
type: Sequelize.STRING
},
friendId: {
allowNull: false,
type: Sequelize.BIGINT
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
})
},
async down(queryInterface, Sequelize) {
/**
* Add reverting commands here.
*
* Example:
* await queryInterface.dropTable('users');
*/
}
}

View file

@ -0,0 +1,20 @@
"use strict"
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn("Users", "bot", {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
})
},
async down(queryInterface, Sequelize) {
/**
* Add reverting commands here.
*
* Example:
* await queryInterface.dropTable('users');
*/
}
}

View file

@ -0,0 +1,55 @@
"use strict"
const { Model } = require("sequelize")
module.exports = (sequelize, DataTypes) => {
class Nickname extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
// eslint-disable-next-line no-unused-vars
static associate(models) {
Nickname.belongsTo(models.User, {
foreignKey: "userId",
as: "user"
})
Nickname.belongsTo(models.User, {
foreignKey: "friendId",
as: "userNickname"
})
}
}
Nickname.init(
{
userId: {
allowNull: false,
type: DataTypes.BIGINT
},
nickname: {
allowNull: false,
type: DataTypes.STRING,
validate: {
notEmpty: true,
len: [1, 20]
}
},
friendId: {
allowNull: false,
type: DataTypes.BIGINT
},
createdAt: {
allowNull: false,
type: DataTypes.DATE
},
updatedAt: {
allowNull: false,
type: DataTypes.DATE
}
},
{
sequelize,
modelName: "Nickname"
}
)
return Nickname
}

View file

@ -16,6 +16,10 @@ module.exports = (sequelize, DataTypes) => {
as: "friends", as: "friends",
foreignKey: "userId" foreignKey: "userId"
}) })
User.hasOne(models.Nickname, {
foreignKey: "friendId",
as: "nickname"
})
} }
} }
User.init( User.init(
@ -135,6 +139,11 @@ module.exports = (sequelize, DataTypes) => {
avatar: { avatar: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true
},
bot: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
} }
}, },
{ {

File diff suppressed because it is too large Load diff

View file

@ -342,40 +342,6 @@ router.put("/settings/:type", auth, async (req, res, next) => {
return false return false
} }
} }
async function checkPassword(password) {
return new Promise((resolve, reject) => {
axios
.post(
"http://localhost:23994/services/admin.svc/GetApiKey",
{
password: password,
schoolId: req.body.schoolId,
sussiId: req.user.sussiId
},
{
headers: {
compassInstance:
req.header("compassInstance") ||
req.query.compassInstance ||
"devices"
}
}
)
.then((resp) => {
if (resp.data.d) {
resolve(true)
return true
} else {
resolve(false)
return false
}
})
.catch((e) => {
reject(e)
return false
})
})
}
try { try {
const io = req.app.get("io") const io = req.app.get("io")
if (req.params.type === "full") { if (req.params.type === "full") {
@ -452,66 +418,6 @@ router.put("/settings/:type", auth, async (req, res, next) => {
} else { } else {
throw Errors.invalidParameter("Password or Code") throw Errors.invalidParameter("Password or Code")
} }
} else if (req.params.type === "totpConfirm") {
const user = await User.findOne({
where: {
id: req.user.id
}
})
if (user.totp) {
const verified = speakeasy.totp.verify({
secret: user.totp,
encoding: "base32",
token: req.body.code
})
if (verified) {
await User.update(
{
totpEnabled: true
},
{
where: {
id: req.user.id
}
}
)
res.sendStatus(204)
} else {
throw Errors.invalidTotp
}
} else {
throw Errors.unknown
}
} else if (req.params.type === "bcSessionsEnable") {
/*
const match = await checkPassword(req.body.password)
if (match) {
await User.update(
{
bcSessionsEnabled: true
},
{
where: {
id: req.user.id
}
}
)
const session = await Session.create({
session:
"BETTERCOMPASS-" +
cryptoRandomString({
length: 64
}),
compassUserId: req.user.compassUserId,
sussiId: req.user.sussiId,
instance: req.user.instance,
other: {},
userId: req.user.id
})
} else {
throw Errors.invalidCredentials
}*/
throw Errors.experimentsOptIn
} else if (req.params.type === "password") { } else if (req.params.type === "password") {
const user = await User.findOne({ const user = await User.findOne({
where: { where: {

View file

@ -15,6 +15,7 @@
"@mdi/js": "^6.6.95", "@mdi/js": "^6.6.95",
"@sentry/tracing": "^6.19.3", "@sentry/tracing": "^6.19.3",
"@sentry/vue": "^6.19.3", "@sentry/vue": "^6.19.3",
"@tauri-apps/api": "^1.0.2",
"apollo-boost": "^0.4.9", "apollo-boost": "^0.4.9",
"axios": "^0.26.1", "axios": "^0.26.1",
"brace": "^0.11.1", "brace": "^0.11.1",

View file

@ -404,27 +404,6 @@
</v-dialog> </v-dialog>
<v-main> <v-main>
<Header></Header> <Header></Header>
<v-container
v-if="
$store.state.user?.compact === 'nagPending' &&
$vuetify.breakpoint.lgAndDown &&
!$vuetify.breakpoint.mobile
"
>
<v-alert dense text color="info" dismissible v-model="compactModeNag">
Introducing <strong>Compact Mode</strong>, a better experience for
lower resolution devices.
<v-btn
to="/settings/appearance"
text
color="primary"
outlined
class="ml-1"
>
Settings
</v-btn>
</v-alert>
</v-container>
<v-container <v-container
v-if="$store.state.site.latestVersion > $store.state.versioning.version" v-if="$store.state.site.latestVersion > $store.state.versioning.version"
id="update-notify-banner" id="update-notify-banner"
@ -466,10 +445,9 @@
<style></style> <style></style>
<script> <script>
import AjaxErrorHandler from "@/lib/errorHandler" import AjaxErrorHandler from "@/lib/errorHandler"
import Vue from "vue"
import Vuetify from "@/plugins/vuetify"
import { VueFinalModal } from "vue-final-modal" import { VueFinalModal } from "vue-final-modal"
import Header from "@/components/Header" import Header from "@/components/Header"
import Vue from "vue"
export default { export default {
name: "App", name: "App",
components: { components: {
@ -580,35 +558,6 @@ export default {
return index.charAt(0).toUpperCase() + index.slice(1) return index.charAt(0).toUpperCase() + index.slice(1)
} }
}, },
computeColor(event) {
if (this.$vuetify?.theme?.themes) {
if (event.color === "#003300") {
return this.$vuetify.theme.themes[
this.$store.state.user.theme || "dark"
].calendarActivityType8
} else if (event.color === "#133897") {
return this.$vuetify.theme.themes[
this.$store.state.user.theme || "dark"
].calendarExternalActivity
} else if (event.activityType === 7 || event.color === "#f4dcdc") {
return this.$vuetify.theme.themes[
this.$store.state.user.theme || "dark"
].calendarActivityType7
} else if (event.color === "#dce6f4") {
return this.$vuetify.theme.themes[
this.$store.state.user.theme || "dark"
].calendarNormalActivity
} else if (event.activityType === 10) {
return this.$vuetify.theme.themes[
this.$store.state.user.theme || "dark"
].calendarActivityType10
} else {
return this.$vuetify.theme.themes[
this.$store.state.user.theme || "dark"
].calendarNormalActivity
}
}
},
editorInit() { editorInit() {
require("brace/ext/language_tools") require("brace/ext/language_tools")
require("brace/mode/css") require("brace/mode/css")
@ -617,19 +566,6 @@ export default {
require("brace/theme/chrome") require("brace/theme/chrome")
require("brace/snippets/css") require("brace/snippets/css")
}, },
baseRole() {
if (this.$store.state.user?.baseRole) {
return (
this.$store.state.user.baseRole
.toLowerCase()
.charAt(0)
.toUpperCase() +
this.$store.state.user.baseRole.toLowerCase().slice(1)
)
} else {
return "Not Authenticated"
}
},
saveSettings() { saveSettings() {
this.loading = true this.loading = true
this.$vuetify.theme.dark = this.$store.state.user?.theme === "dark" this.$vuetify.theme.dark = this.$store.state.user?.theme === "dark"
@ -643,15 +579,6 @@ export default {
AjaxErrorHandler(this.$store)(e) AjaxErrorHandler(this.$store)(e)
}) })
}, },
completeGuidedWizard() {
this.loadingGuidedWizard = true
this.$store.dispatch("saveOnlineSettings", {
guidedWizard: false
})
this.$store.dispatch("getUserInfo").then(() => {
this.loadingGuidedWizard = false
})
},
getThemes() { getThemes() {
this.axios.get("/api/v1/themes").then((res) => { this.axios.get("/api/v1/themes").then((res) => {
this.themes = res.data.map((theme) => { this.themes = res.data.map((theme) => {
@ -692,25 +619,27 @@ export default {
.catch((e) => { .catch((e) => {
AjaxErrorHandler(this.$store)(e) AjaxErrorHandler(this.$store)(e)
}) })
},
retryConnection() {
this.connectionLoading = true
this.$store.dispatch("getState").finally(() => {
this.connectionLoading = false
})
},
validate(value, defaultValue) {
if (value === undefined || value === null) {
return defaultValue
} else {
return value
}
} }
}, },
mounted() { mounted() {
Vue.axios.defaults.headers.common["Authorization"] =
localStorage.getItem("session")
if (this.$vuetify.breakpoint.mobile) {
this.$store.state.drawer = false
}
if (localStorage.getItem("cssTipsDismissed")) { if (localStorage.getItem("cssTipsDismissed")) {
this.cssTips = false this.cssTips = false
} }
if (localStorage.getItem("userPanel")) {
this.$store.state.userPanel = JSON.parse(
localStorage.getItem("userPanel")
)
} else {
this.$store.state.userPanel = true
}
if (this.$vuetify.breakpoint.mobile) {
this.$store.state.userPanel = false
}
window.addEventListener("offline", () => { window.addEventListener("offline", () => {
this.$store.commit("setOnline", false) this.$store.commit("setOnline", false)
this.$store.dispatch("getState") this.$store.dispatch("getState")
@ -721,56 +650,11 @@ export default {
this.$store.dispatch("getUserInfo") this.$store.dispatch("getUserInfo")
}) })
this.$socket.connect() this.$socket.connect()
Vue.axios.defaults.headers.common["CompassAPIKey"] =
localStorage.getItem("apiKey")
Vue.axios.defaults.headers.common["Authorization"] =
localStorage.getItem("bcToken")
document.title = this.$route.name document.title = this.$route.name
? this.$route.name + " - " + this.$store.state.site.name ? this.$route.name + " - " + this.$store.state.site.name
: this.$store.state.site.name : this.$store.state.site.name
this.$store.commit("setLoading", true) this.$store.commit("setLoading", true)
this.$vuetify.theme.dark = this.$store.state.user?.theme === "dark" || true this.$vuetify.theme.dark = this.$store.state.user?.theme === "dark" || true
this.axios.defaults.headers.common["compassInstance"] =
localStorage.getItem("schoolInstance")
this.axios.defaults.headers.common["compassSchoolId"] =
localStorage.getItem("schoolId")
if (JSON.parse(localStorage.getItem("userCache"))?.id) {
const user = JSON.parse(localStorage.getItem("userCache"))
const name = user.themeObject.id
const dark = user.themeObject.theme.dark
const light = user.themeObject.theme.light
if (user.accentColor) {
user.themeObject.theme.dark.primary = user.accentColor
user.themeObject.theme.light.primary = user.accentColor
}
if (JSON.parse(localStorage.getItem("subjectsCache"))) {
this.$store.commit(
"setSubjects",
JSON.parse(localStorage.getItem("subjectsCache"))
)
}
Vuetify.framework.theme.themes.dark = dark
Vuetify.framework.theme.themes.light = light
Vuetify.framework.theme.themes.name = name
Vuetify.framework.theme.themes.primaryType =
user.themeObject.theme.primaryType
this.$store.commit(
"setUser",
JSON.parse(localStorage.getItem("userCache"))
)
}
if (localStorage.getItem("calendarCache")?.length) {
this.$store.commit(
"setCalendar",
JSON.parse(localStorage.getItem("calendarCache")).map((event) => {
return {
...event,
start: new Date(event.start),
end: new Date(event.finish)
}
})
)
}
this.$store.dispatch("getState") this.$store.dispatch("getState")
this.$store.dispatch("checkAuth").catch(() => { this.$store.dispatch("checkAuth").catch(() => {
this.$store.dispatch("logout") this.$store.dispatch("logout")
@ -853,9 +737,6 @@ export default {
this.$store.state.site.notification = state.notification this.$store.state.site.notification = state.notification
this.$store.state.site.notificationType = state.notificationType this.$store.state.site.notificationType = state.notificationType
}) })
setInterval(() => {
this.$socket.emit("ping")
}, 10000)
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
if (JSON.parse(process.env.VUE_APP_MATOMO_ENABLED)) { if (JSON.parse(process.env.VUE_APP_MATOMO_ENABLED)) {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
@ -870,11 +751,8 @@ export default {
}) })
}, },
watch: { watch: {
compactModeNag(val) { "$store.state.userPanel"(val) {
if (!val) { localStorage.setItem("userPanel", val)
this.$store.state.user.compact = "disabled"
this.$store.dispatch("saveOnlineSettings")
}
}, },
cssTips(val) { cssTips(val) {
localStorage.setItem("cssTipsDismissed", !val) localStorage.setItem("cssTipsDismissed", !val)
@ -908,7 +786,7 @@ export default {
search() { search() {
if (this.search) { if (this.search) {
if (this.search.id) { if (this.search.id) {
this.$router.push("/activity/activity/" + this.search.id) this.$router.push("/communications/" + this.search.id)
this.$store.commit("setSearch", false) this.$store.commit("setSearch", false)
this.search = null this.search = null
this.$nextTick(() => { this.$nextTick(() => {

View file

@ -0,0 +1,292 @@
<template>
<div>
<v-toolbar
elevation="0"
outlined
height="40"
color="card"
v-if="file"
style="cursor: pointer; overflow: hidden"
class="mb-2"
>
<v-toolbar-title>
<v-icon> mdi-attachment </v-icon>
{{ file.name }}
</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn icon @click="file = null" small>
<v-icon> mdi-close </v-icon>
</v-btn>
</v-toolbar>
<v-textarea
style="margin-bottom: 5px"
autofocus
label="Type a message"
placeholder="Keep it civil"
type="text"
ref="message-input"
:id="edit ? 'edit-input' : 'message-input'"
outlined
append-outer-icon="mdi-send"
auto-grow
@keyup.up="editLastMessage"
@keyup.esc="handleEsc"
@click:append-outer="handleMessage()"
@keydown.enter.exact.prevent="handleMessage()"
v-model="message"
@paste="handlePaste"
@change="handleChange"
rows="1"
single-line
dense
hide-details
@keydown.enter.shift.exact.prevent="newLine($event)"
>
<template v-slot:append-outer>
<v-btn
icon
small
@click="handleMessage()"
v-if="!edit"
:retain-focus-on-click="false"
class="no-focus"
>
<v-icon> mdi-send </v-icon>
</v-btn>
</template>
<template v-slot:prepend>
<v-file-input
style="margin-top: -18px"
single-line
hide-input
v-model="file"
@change="getURLForImage"
v-if="!edit"
></v-file-input>
</template>
</v-textarea>
</div>
</template>
<script>
import AjaxErrorHandler from "@/lib/errorHandler"
export default {
name: "CommsInput",
props: {
replying: {
type: Object,
default: null
},
edit: {
type: Object,
default: null
},
autoScroll: {
type: Function,
default: () => {}
},
editLastMessage: {
type: Function,
default: () => {}
},
endEdit: {
type: Function,
default: () => {}
},
endSend: {
type: Function,
default: () => {}
},
chat: {
type: Object,
default: null
}
},
data() {
return {
message: "",
file: null,
blobURL: null
}
},
methods: {
handleMessage() {
if (this.edit) {
this.editMessage()
} else {
this.sendMessage()
}
},
editMessage() {
if (this.message.length > 0) {
this.axios
.put(
process.env.VUE_APP_BASE_URL +
"/api/v1/communications/" +
this.$route.params.id +
"/message/edit",
{
id: this.edit.id,
content: this.message
}
)
.then(() => {
this.edit.editing = false
this.edit.id = null
this.edit.content = ""
this.focusInput()
this.endEdit()
// response will be handled via WebSocket
})
.catch((e) => {
AjaxErrorHandler(this.$store)(e)
})
}
},
newLine(event) {
this.message = event.target.value + "\n"
},
handleEsc() {
if (!this.edit) {
if (this.replying) {
this.endSend()
}
} else {
this.endEdit()
}
},
getURLForImage() {
const file = this.file
const reader = new FileReader()
reader.onload = (e) => {
this.blobURL = e.target.result
}
reader.readAsDataURL(file)
},
handleChange() {
if (this.$store.state.user.bcUser.storedStatus !== "invisible") {
if (this.typingDate) {
const now = new Date()
if (now - this.typingDate > 5000) {
this.typingDate = now
this.axios.put(
process.env.VUE_APP_BASE_URL +
"/api/v1/communications/" +
this.chat.id +
"/typing"
)
}
} else {
this.typingDate = new Date()
this.axios.put(
process.env.VUE_APP_BASE_URL +
"/api/v1/communications/" +
this.chat.id +
"/typing"
)
}
}
},
handlePaste(data) {
if (data.clipboardData.items.length) {
const item = data.clipboardData.items[0]
if (item.kind === "file") {
this.file = item.getAsFile()
this.getURLForImage()
}
}
},
focusInput() {
const input = document.getElementById("message-input")
input.focus()
},
sendMessage() {
this.focusInput()
let message = this.message
this.message = ""
if (this.file || message.length > 0) {
const emojis = require("../lib/emojis.json")
message = message.replaceAll(
/:([a-zA-Z0-9_\-+]+):/g,
(match, group1) => {
const emoji = emojis.find((emoji) => {
return emoji.aliases.includes(group1)
})
if (emoji) {
return emoji.emoji
} else {
return match
}
}
)
if (!this.file) {
this.axios
.post(
process.env.VUE_APP_BASE_URL +
"/api/v1/communications/" +
this.$route.params.id +
"/message",
{
message: message,
replyId: this.replying?.id
}
)
.then(() => {
this.focusInput()
this.message = ""
this.autoScroll()
this.endSend()
})
.catch((e) => {
console.log(e)
AjaxErrorHandler(this.$store)(e)
})
} else {
const formData = new FormData()
formData.append("message", message)
if (this.replying) {
formData.append("replyId", this.replying.id)
}
formData.append("file", this.file)
this.axios
.post(
process.env.VUE_APP_BASE_URL +
"/api/v1/communications/" +
this.$route.params.id +
"/formData/message",
formData,
{
headers: {
"Content-Type": "multipart/form-data"
}
}
)
.then(() => {
this.focusInput()
this.message = ""
this.autoScroll()
this.endSend()
this.file = null
})
.catch((e) => {
console.log(e)
AjaxErrorHandler(this.$store)(e)
})
}
}
}
},
mounted() {
if (this.edit) {
this.message = this.edit.content
}
}
}
</script>
<style scoped>
.no-focus:focus::before {
opacity: 0 !important;
}
</style>

View file

@ -3,6 +3,39 @@
<v-overlay :value="!$store.state.wsConnected" absolute> <v-overlay :value="!$store.state.wsConnected" absolute>
<v-progress-circular indeterminate size="64"></v-progress-circular> <v-progress-circular indeterminate size="64"></v-progress-circular>
</v-overlay> </v-overlay>
<NicknameDialog :nickname="nickname" />
<v-menu
v-model="context.user.value"
:position-x="context.user.x"
:position-y="context.user.y"
absolute
offset-y
class="rounded-l"
>
<v-list class="rounded-l" v-if="context.user.item">
<v-list-item
v-if="context.user.item.type === 'direct'"
@click="
nickname.user = context.user.item
nickname.dialog = true
"
>
<v-list-item-title>Change Friend Nickname</v-list-item-title>
</v-list-item>
<v-list-item @click="groupSettings(context.user.id)">
<v-list-item-title>Group Settings</v-list-item-title>
</v-list-item>
<v-list-item
@click="
leave.item = context.user.item
leave.dialog = true
"
color="error"
>
<v-list-item-title>Leave Group</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-dialog <v-dialog
v-model="settings.addMembers.dialog" v-model="settings.addMembers.dialog"
max-width="400px" max-width="400px"
@ -263,6 +296,38 @@
</v-card> </v-card>
</v-dialog> </v-dialog>
<v-app-bar app color="bg"> <v-app-bar app color="bg">
<v-app-bar-nav-icon
@click.stop="$store.state.drawer = !$store.state.drawer"
v-if="$vuetify.breakpoint.mobile"
></v-app-bar-nav-icon>
<button
style="display: none"
v-shortkey="['ctrl', 'k']"
@shortkey="$store.commit('setSearch', true)"
>
Debug
</button>
<button
style="display: none"
v-shortkey="['meta', 'k']"
@shortkey="$store.commit('setSearch', true)"
>
Debug
</button>
<button
style="display: none"
v-shortkey="['ctrl', 'alt', 'd']"
@shortkey="$store.dispatch('toggleCSS')"
>
Style Toggle
</button>
<button
style="display: none"
v-shortkey="['f9']"
@shortkey="$store.dispatch('toggleCSS')"
>
Style Toggle
</button>
<template v-if="$route.name === 'Communications'"> <template v-if="$route.name === 'Communications'">
<v-toolbar-title v-if="$store.state.selectedChat?.chat?.type"> <v-toolbar-title v-if="$store.state.selectedChat?.chat?.type">
{{ {{
@ -271,7 +336,6 @@
: $store.state.selectedChat?.chat?.name : $store.state.selectedChat?.chat?.name
}} }}
</v-toolbar-title> </v-toolbar-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn <v-btn
icon icon
@ -279,11 +343,7 @@
> >
<v-icon>mdi-magnify</v-icon> <v-icon>mdi-magnify</v-icon>
</v-btn> </v-btn>
<v-btn <v-btn icon @click="$store.state.userPanel = !$store.state.userPanel">
icon
@click="$store.state.userPanel = !$store.state.userPanel"
v-if="selected?.chat?.type === 'group'"
>
<v-icon>mdi-account-group</v-icon> <v-icon>mdi-account-group</v-icon>
</v-btn> </v-btn>
</template> </template>
@ -293,148 +353,147 @@
</v-toolbar-title> </v-toolbar-title>
</template> </template>
</v-app-bar> </v-app-bar>
<v-navigation-drawer color="bg" floating app style="max-height: 100%"> <v-navigation-drawer
color="bg"
floating
app
style="max-height: 100%"
v-model="$store.state.drawer"
:width="$vuetify.breakpoint.mobile ? 270 : 320"
>
<v-container> <v-container>
<v-btn <v-list dense nav id="comms-sidebar-list">
color="toolbar" <template v-if="$vuetify.breakpoint.mobile">
to="/communications/friends" <v-btn
block color="toolbar"
class="mb-3 rounded-xl" to="/communications/friends"
elevation="2" width="48%"
> class="mb-3 mr-1 rounded-xl"
<v-icon left>mdi-account-multiple</v-icon> elevation="2"
Friends >
</v-btn> <v-icon left>mdi-account-multiple</v-icon>
<v-text-field Friends
class="rounded-xl" </v-btn>
filled <v-btn
solo color="toolbar"
label="Search..." width="48%"
append-icon="mdi-magnify" class="mb-3 ml-1 rounded-xl"
background-color="toolbar" elevation="2"
style="margin-bottom: -18px" @click="dialogs.new = true"
elevation="2" >
></v-text-field> <v-icon left>mdi-plus</v-icon>
<v-toolbar color="toolbar" class="rounded-xl mb-3" elevation="2"> New
<v-toolbar-title class="subtitle-1"> </v-btn>
CHATS ({{ $store.state.chats.length }}) </template>
</v-toolbar-title> <template v-else>
<v-spacer></v-spacer> <v-btn
<v-btn icon @click="dialogs.new = true"> color="toolbar"
<v-icon>mdi-plus</v-icon> to="/communications/friends"
</v-btn> block
</v-toolbar> class="mb-3 rounded-xl"
<v-card height="100%" color="transparent" class="mt-2" elevation="0"> elevation="2"
<v-list height="100%" two-line color="transparent" elevation="0"> >
<v-list-item-group v-model="selected"> <v-icon left>mdi-account-multiple</v-icon>
<template v-for="(item, index) in $store.state.chats"> Friends
<v-list-item </v-btn>
:key="item.title" <v-text-field
:to="'/communications/' + item.id" class="rounded-xl"
filled
solo
label="Search..."
append-icon="mdi-magnify"
background-color="toolbar"
style="margin-bottom: -18px"
elevation="2"
></v-text-field>
<v-toolbar color="toolbar" class="rounded-xl mb-3" elevation="2">
<v-toolbar-title class="subtitle-1">
CHATS ({{ $store.state.chats.length }})
</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn icon @click="dialogs.new = true">
<v-icon>mdi-plus</v-icon>
</v-btn>
</v-toolbar></template
>
<v-list v-for="item in $store.state.chats" :key="item.id">
<template>
<v-list-item
:to="'/communications/' + item.id"
@contextmenu="
show($event, 'user', getDirectRecipient(item), item.id)
"
>
<v-badge
bordered
bottom
:color="getStatus(item)"
v-if="item.chat.type === 'direct'"
dot
offset-x="24"
offset-y="20"
> >
<v-badge <v-list-item-avatar
bordered :color="$vuetify.theme.themes.dark.primary"
bottom
:color="getStatus(item)"
v-if="item.chat.type === 'direct'"
dot
offset-x="24"
offset-y="26"
> >
<v-list-item-avatar <v-icon v-if="item.chat.type === 'group'">
:color="$vuetify.theme.themes.dark.primary" mdi-account-group
> </v-icon>
<v-icon v-if="item.chat.type === 'group'"> <v-img
mdi-account-group v-else-if="
</v-icon> item.chat.type === 'direct' &&
<v-img getDirectRecipient(item).avatar
v-else-if=" "
item.chat.type === 'direct' && :src="
getDirectRecipient(item).avatar $store.state.baseURL +
" '/usercontent/' +
:src="'/usercontent/' + getDirectRecipient(item).avatar" getDirectRecipient(item).avatar
/> "
<v-icon v-else-if="item.chat.type === 'direct'"> />
mdi-account <v-icon v-else-if="item.chat.type === 'direct'">
</v-icon> mdi-account
</v-list-item-avatar> </v-icon>
</v-badge> </v-list-item-avatar>
<v-badge dot color="none" v-else> </v-badge>
<v-list-item-avatar <v-badge dot color="none" v-else>
:color="$vuetify.theme.themes.dark.primary" <v-list-item-avatar
> :color="$vuetify.theme.themes.dark.primary"
<v-icon v-if="item.chat.type === 'group'"> >
mdi-account-group <v-icon v-if="item.chat.type === 'group'">
</v-icon> mdi-account-group
</v-list-item-avatar> </v-icon>
</v-badge> </v-list-item-avatar>
<template> </v-badge>
<v-list-item-content> <template>
<v-list-item-title v-if="item.chat.type === 'direct'"> <v-list-item-content>
{{ getDirectRecipient(item).username }} <v-list-item-title v-if="item.chat.type === 'direct'">
<v-badge {{ getDirectRecipient(item).name }}
v-if=" </v-list-item-title>
getLastRead(item).count >= 1 && <v-list-item-title v-else>
$route.params.id !== item.id.toString() <span> {{ item.chat.name }} </span>
" </v-list-item-title>
color="red"
class="ml-2"
:content="getLastRead(item).count"
>
</v-badge>
</v-list-item-title>
<v-list-item-title v-else>
{{ item.chat.name }}
<v-badge
v-if="
getLastRead(item).count >= 1 &&
$route.params.id !== item.id.toString()
"
color="red"
class="ml-2"
:content="getLastRead(item).count"
>
</v-badge>
</v-list-item-title>
<v-list-item-subtitle v-if="item.chat.type === 'group'"> <v-list-item-subtitle v-if="item.chat.type === 'group'">
{{ item.chat.users.length }} Members {{ item.chat.users.length }} Members
</v-list-item-subtitle> </v-list-item-subtitle>
</v-list-item-content> </v-list-item-content>
<v-list-item-action> <v-list-item-action
<v-icon v-if="
@click=" item.unread >= 1 &&
leave.item = item $route.params.id !== item.id.toString()
leave.dialog = true "
" >
> <v-badge color="red" inline :content="item.unread">
mdi-exit-to-app </v-badge>
</v-icon> </v-list-item-action>
</v-list-item-action> </template>
<v-list-item-action> </v-list-item>
<v-icon </template>
@click="
settings.item = item
settings.dialog = true
"
>
mdi-cog
</v-icon>
</v-list-item-action>
</template>
</v-list-item>
<v-divider
v-if="index < $store.state.chats.length - 1"
:key="index"
></v-divider>
</template>
</v-list-item-group>
</v-list> </v-list>
</v-card> </v-list>
</v-container> </v-container>
<template v-slot:append> <template v-slot:append>
<v-card tile color="card" elevation="0"> <v-card tile color="bg" elevation="0">
<v-overlay :value="!$store.state.wsConnected" absolute> <v-overlay :value="!$store.state.wsConnected" absolute>
<v-progress-circular indeterminate size="48"></v-progress-circular> <v-progress-circular indeterminate size="48"></v-progress-circular>
</v-overlay> </v-overlay>
@ -522,11 +581,19 @@
<script> <script>
import AjaxErrorHandler from "@/lib/errorHandler" import AjaxErrorHandler from "@/lib/errorHandler"
import NicknameDialog from "@/components/NicknameDialog"
import Vue from "vue"
export default { export default {
name: "Header", name: "Header",
components: { NicknameDialog },
data() { data() {
return { return {
nickname: {
dialog: false,
nickname: "",
user: {}
},
copyTooltip: false, copyTooltip: false,
settings: { settings: {
dialog: false, dialog: false,
@ -538,6 +605,28 @@ export default {
}, },
item: null item: null
}, },
context: {
user: {
value: false,
x: null,
y: null,
item: null,
id: 0
},
userPopout: {
value: false,
x: null,
y: null,
item: null,
id: 0
},
message: {
value: false,
x: null,
y: null,
item: null
}
},
selected: [2], selected: [2],
loading: true, loading: true,
leave: { leave: {
@ -556,6 +645,23 @@ export default {
} }
}, },
methods: { methods: {
groupSettings(id) {
this.settings.item = this.$store.state.chats.find(
(chat) => chat.id === id
)
this.settings.dialog = true
},
show(e, context, item, id) {
e.preventDefault()
this.context[context].value = false
this.context[context].x = e.clientX
this.context[context].y = e.clientY
this.context[context].item = item
this.context[context].id = id
this.$nextTick(() => {
this.context[context].value = true
})
},
setStatus(status) { setStatus(status) {
const previousStatus = { const previousStatus = {
status: this.$store.state.user.status, status: this.$store.state.user.status,
@ -740,13 +846,30 @@ export default {
} }
}, },
getDirectRecipient(item) { getDirectRecipient(item) {
const user = item.chat.users.find( let user = item.chat.users.find(
(user) => user.id !== this.$store.state.user.id (user) => user.id !== this.$store.state.user.id
) )
if (user) { if (user) {
return user if (user.nickname?.nickname) {
user.name = user.nickname.nickname
} else {
user.name = user.username
}
return {
...user,
type: item.chat.type
}
} else { } else {
return item.chat.users[0] let user = item.chat.users[0]
if (user.nickname?.nickname) {
user.name = user.nickname.nickname
} else {
user.name = user.username
}
return {
...user,
type: item.chat.type
}
} }
}, },
createConversation() { createConversation() {
@ -756,7 +879,6 @@ export default {
users: this.newConversation.users users: this.newConversation.users
}) })
.then(() => { .then(() => {
this.getChats()
this.newConversation.name = "" this.newConversation.name = ""
this.newConversation.users = [] this.newConversation.users = []
this.newConversation.loading = false this.newConversation.loading = false
@ -774,10 +896,10 @@ export default {
searchUsers() { searchUsers() {
this.newConversation.loading = true this.newConversation.loading = true
this.axios this.axios
.get("/api/v1/communications/search?query=" + this.newConversation.name) .get("/api/v1/communications/search?query=")
.then((res) => { .then((res) => {
this.newConversation.loading = false this.newConversation.loading = false
this.newConversation.results.push(...res.data) this.newConversation.results = res.data
}) })
.catch(() => { .catch(() => {
this.newConversation.loading = false this.newConversation.loading = false
@ -785,12 +907,10 @@ export default {
}, },
searchUsersForGroupAdmin() { searchUsersForGroupAdmin() {
this.axios this.axios
.get( .get("/api/v1/communications/search?query=")
"/api/v1/communications/search?query=" + this.settings.addMembers.name
)
.then((res) => { .then((res) => {
if (this.settings.item) { if (this.settings.item) {
this.settings.addMembers.results.push(...res.data) this.settings.addMembers.results = res.data
this.settings.addMembers.results = this.settings.addMembers.results =
this.settings.addMembers.results.filter( this.settings.addMembers.results.filter(
(user) => (user) =>
@ -802,6 +922,8 @@ export default {
} }
}, },
mounted() { mounted() {
Vue.axios.defaults.headers.common["Authorization"] =
localStorage.getItem("session")
this.searchUsers() this.searchUsers()
this.searchUsersForGroupAdmin() this.searchUsersForGroupAdmin()
this.$store.dispatch("getChats") this.$store.dispatch("getChats")
@ -834,7 +956,6 @@ export default {
) )
if (chat) { if (chat) {
const index = this.$store.state.chats.indexOf(chat) const index = this.$store.state.chats.indexOf(chat)
this.$store.state.chats[index].chat.lastMessages.unshift(message)
this.$store.state.chats.splice(index, 1) this.$store.state.chats.splice(index, 1)
this.$store.state.chats.unshift(chat) this.$store.state.chats.unshift(chat)
} }

View file

@ -0,0 +1,84 @@
<template>
<v-dialog v-model="nickname.dialog" width="500px">
<v-card>
<v-card-title>
<span class="headline">{{ nickname.user.username }}</span>
</v-card-title>
<v-card-text>
<v-text-field
v-model="nickname.nickname"
label="Nickname"
required
@keyup.enter="setFriendNickname"
></v-text-field>
<small>Friend nicknames only show to you.</small>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="blue darken-1"
text
@click="
nickname.dialog = false
nickname.nickname = ''
nickname.user = {}
"
>
Cancel
</v-btn>
<v-btn color="blue darken-1" text @click="setFriendNickname">
Apply
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import AjaxErrorHandler from "@/lib/errorHandler"
export default {
name: "NicknameDialog",
props: ["nickname"],
methods: {
setFriendNickname() {
this.axios
.post(
process.env.VUE_APP_BASE_URL +
"/api/v1/communications/nickname/" +
this.nickname.user.id,
{
nickname: this.nickname.nickname
}
)
.then((res) => {
this.$store.state.chats.forEach((item) => {
item.chat.associations.forEach((a) => {
if (a.user.id === this.nickname.user.id) {
a.user.nickname = {
nickname: res.data.nickname
}
}
})
item.chat.users.forEach((u) => {
if (u.id === this.nickname.user.id) {
u.nickname = {
nickname: res.data.nickname
}
}
})
})
this.nickname.dialog = false
this.nickname.nickname = ""
this.nickname.user = {}
this.$toast.success("Nickname changed successfully.")
})
.catch((e) => {
AjaxErrorHandler(this.$store)(e)
})
}
}
}
</script>
<style scoped></style>

View file

@ -2,8 +2,14 @@ module.exports = function (vuex) {
return function (res, ignorePathErrorCb) { return function (res, ignorePathErrorCb) {
let errors = [] let errors = []
if (res.response === undefined || res.response.data.errors === undefined) { if (
if (res.response.status === 429) { res?.response === undefined ||
res?.response?.data?.errors === undefined
) {
if (vuex.state.site.release === "dev") {
console.error(res)
}
if (res?.response?.status === 429) {
vuex._vm.$toast.error( vuex._vm.$toast.error(
"You are being rate limited, retry in " + "You are being rate limited, retry in " +
res.response.headers["ratelimit-reset"] + res.response.headers["ratelimit-reset"] +

View file

@ -4,17 +4,37 @@ import Vuetify from "../plugins/vuetify"
import AjaxErrorHandler from "@/lib/errorHandler" import AjaxErrorHandler from "@/lib/errorHandler"
Vue.use(Vuex) Vue.use(Vuex)
function getDirectRecipient(context, item) {
let user = item.chat.users.find((user) => user.id !== context.state.user.id)
if (user) {
if (user.nickname?.nickname) {
user.name = user.nickname.nickname
} else {
user.name = user.username
}
return user
} else {
let user = item.chat.users[0]
if (user.nickname?.nickname) {
user.name = user.nickname.nickname
} else {
user.name = user.username
}
return user
}
}
export default new Vuex.Store({ export default new Vuex.Store({
state: { state: {
online: true, online: true,
selectedChat: null, selectedChat: null,
chats: [], chats: [],
baseURL: process.env.VUE_APP_BASE_URL,
versioning: { versioning: {
date: process.env.VUE_APP_BUILD_DATE, date: process.env.VUE_APP_BUILD_DATE,
version: process.env.VUE_APP_VERSION, version: process.env.VUE_APP_VERSION,
release: process.env.RELEASE release: process.env.RELEASE
}, },
drawer: true,
site: { site: {
release: "stable", release: "stable",
loading: true loading: true
@ -97,7 +117,7 @@ export default new Vuex.Store({
state.modals.search = value state.modals.search = value
}, },
addChat(state, chat) { addChat(state, chat) {
state.chats.push(chat) state.chats.unshift(chat)
}, },
updateQuickSwitchCache(state, value) { updateQuickSwitchCache(state, value) {
state.quickSwitchCache.push({ state.quickSwitchCache.push({
@ -119,34 +139,16 @@ export default new Vuex.Store({
}) })
}, },
getCommunicationsUnread(context) { getCommunicationsUnread(context) {
Vue.axios.defaults.headers.common["Authorization"] = Vue.axios
localStorage.getItem("session") .get(process.env.VUE_APP_BASE_URL + "/api/v1/communications")
Vue.axios.get("/api/v1/communications").then((res) => { .then((res) => {
context.state.communicationNotifications = 0 context.commit("setChats", res.data)
res.data.forEach((item) => { context.dispatch("updateQuickSwitch")
const message = item.chat.lastMessages.find( context.state.communicationNotifications = 0
(message) => message.id === item.lastRead res.data.forEach((item) => {
) context.state.communicationNotifications += item.unread
const lastMessage = item.chat.lastMessages.find( })
(message) => message.userId !== context.state.user.id
)
let count
if (message && lastMessage) {
const index = item.chat.lastMessages.indexOf(message)
const indexLast = item.chat.lastMessages.indexOf(lastMessage)
let value = index - indexLast
if (value < 0) {
value = 0
}
count = value
} else if (!message) {
count = item.chat.lastMessages.length
} else {
count = 0
}
context.state.communicationNotifications += count
}) })
})
}, },
discardTheme(context) { discardTheme(context) {
context.state.themeEngine.theme = { context.state.themeEngine.theme = {
@ -323,9 +325,12 @@ export default new Vuex.Store({
}) })
context.state.chats.forEach((chat) => { context.state.chats.forEach((chat) => {
context.commit("updateQuickSwitchCache", { context.commit("updateQuickSwitchCache", {
...chat, id: chat.id,
customType: 2, subjectLongName:
route: "/communications/" + chat.id chat.chat.type === "group"
? chat.chat.name
: getDirectRecipient(context, chat).name,
customType: 3
}) })
}) })
}, },

File diff suppressed because it is too large Load diff

View file

@ -21,16 +21,13 @@
<v-list-item-avatar <v-list-item-avatar
@click="userProfile(user)" @click="userProfile(user)"
style="cursor: pointer" style="cursor: pointer"
:color="$vuetify.theme.themes.dark.primary"
> >
<v-list-item-avatar <v-img
:color="$vuetify.theme.themes.dark.primary" :src="'/usercontent/' + user.avatar"
> v-if="user.avatar"
<v-img />
:src="'/usercontent/' + user.avatar" <v-icon v-else> mdi-account </v-icon>
v-if="user.avatar"
/>
<v-icon v-else> mdi-account </v-icon>
</v-list-item-avatar>
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content <v-list-item-content
@ -90,16 +87,15 @@
v-for="friend in computePendingIncoming" v-for="friend in computePendingIncoming"
:key="friend.id" :key="friend.id"
> >
<v-list-item-avatar @click="userProfile(friend.user2)"> <v-list-item-avatar
<v-list-item-avatar @click="userProfile(friend.user2)"
:color="$vuetify.theme.themes.dark.primary" :color="$vuetify.theme.themes.dark.primary"
> >
<img <v-img
:src="'/usercontent/' + friend.user2.avatar" :src="'/usercontent/' + friend.user2.avatar"
v-if="friend.user2.avatar" v-if="friend.user2.avatar"
/> />
<v-icon v-else> mdi-account </v-icon> <v-icon v-else> mdi-account </v-icon>
</v-list-item-avatar>
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content @click="userProfile(friend.user2)"> <v-list-item-content @click="userProfile(friend.user2)">
@ -136,16 +132,13 @@
<v-list-item-avatar <v-list-item-avatar
@click="userProfile(friend.user2)" @click="userProfile(friend.user2)"
style="cursor: pointer" style="cursor: pointer"
:color="$vuetify.theme.themes.dark.primary"
> >
<v-list-item-avatar <v-img
:color="$vuetify.theme.themes.dark.primary" :src="'/usercontent/' + friend.user2.avatar"
> v-if="friend.user2.avatar"
<img />
:src="'/usercontent/' + friend.user2.avatar" <v-icon v-else> mdi-account </v-icon>
v-if="friend.user2.avatar"
/>
<v-icon v-else> mdi-account </v-icon>
</v-list-item-avatar>
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content <v-list-item-content
@ -175,16 +168,13 @@
<v-list-item-avatar <v-list-item-avatar
@click="userProfile(friend.user2)" @click="userProfile(friend.user2)"
style="cursor: pointer" style="cursor: pointer"
:color="$vuetify.theme.themes.dark.primary"
> >
<v-list-item-avatar <v-img
:color="$vuetify.theme.themes.dark.primary" :src="'/usercontent/' + friend.user2.avatar"
> v-if="friend.user2.avatar"
<img />
:src="'/usercontent/' + friend.user2.avatar" <v-icon v-else> mdi-account </v-icon>
v-if="friend.user2.avatar"
/>
<v-icon v-else> mdi-account </v-icon>
</v-list-item-avatar>
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content> <v-list-item-content>

View file

@ -118,6 +118,7 @@ export default {
this.$store.commit("setToken", res.data.session) this.$store.commit("setToken", res.data.session)
await this.$store.dispatch("getUserInfo") await this.$store.dispatch("getUserInfo")
this.loading = false this.loading = false
this.$socket.disconnect()
this.$socket.connect() this.$socket.connect()
this.$router.push("/") this.$router.push("/")
}) })

View file

@ -1,27 +1,5 @@
<template> <template>
<div id="login" v-if="!$store.state.user?.bcUser?.id"> <div id="login" v-if="!$store.state.user?.bcUser?.id">
<v-dialog v-model="totpDialog" max-width="500px">
<v-card color="card">
<v-toolbar color="toolbar">
<v-toolbar-title> Two-Factor Authentication </v-toolbar-title>
</v-toolbar>
<v-container>
<v-card-text
>You are seeing this because you have enabled Two-Factor
Authentication on BetterCompass.<br />
Please check your phone, or authenticator app to obtain the 6 digit
code and enter it here.</v-card-text
>
<v-otp-input
length="6"
@keyup.enter="doLogin()"
@finish="doLogin()"
v-model="totp"
:disabled="loading"
></v-otp-input>
</v-container>
</v-card>
</v-dialog>
<div :class="{ outer: !$vuetify.breakpoint.mobile }"> <div :class="{ outer: !$vuetify.breakpoint.mobile }">
<div :class="{ middle: !$vuetify.breakpoint.mobile }"> <div :class="{ middle: !$vuetify.breakpoint.mobile }">
<div :class="{ innerLogin: !$vuetify.breakpoint.mobile }"> <div :class="{ innerLogin: !$vuetify.breakpoint.mobile }">
@ -121,6 +99,7 @@ export default {
this.$store.commit("setToken", res.data.session) this.$store.commit("setToken", res.data.session)
await this.$store.dispatch("getUserInfo") await this.$store.dispatch("getUserInfo")
this.loading = false this.loading = false
this.$socket.disconnect()
this.$socket.connect() this.$socket.connect()
this.$router.push("/") this.$router.push("/")
}) })

View file

@ -179,12 +179,7 @@ export default {
sheet: "#181818", sheet: "#181818",
text: "#000000", text: "#000000",
dark: "#151515", dark: "#151515",
bg: "#151515", bg: "#151515"
calendarNormalActivity: "#3f51b5",
calendarActivityType7: "#f44336",
calendarActivityType8: "#4caf50",
calendarActivityType10: "#ff9800",
calendarExternalActivity: "#2196f3"
}, },
light: { light: {
primary: "#0190ea", primary: "#0190ea",
@ -199,68 +194,14 @@ export default {
sheet: "#f8f8f8", sheet: "#f8f8f8",
text: "#000000", text: "#000000",
dark: "#f8f8f8", dark: "#f8f8f8",
bg: "#f8f8f8", bg: "#f8f8f8"
calendarNormalActivity: "#3f51b5",
calendarActivityType7: "#f44336",
calendarActivityType8: "#4caf50",
calendarActivityType10: "#ff9800",
calendarExternalActivity: "#2196f3"
} }
}, },
createTheme: false, createTheme: false,
createThemeCSS: false, createThemeCSS: false,
name: "", name: "",
creatorType: "create", creatorType: "create",
themes: [], themes: []
fakeEvents: [
{
name: "English",
content: "English",
color: "#dce6f4",
start: new Date().setHours(9, 0, 0, 0),
end: new Date().setHours(10, 0, 0, 0),
timed: true,
activityType: 1,
activityId: 0,
calendarId: 0
},
{
name: "Maths",
content: "Maths",
color: "#f4dcdc",
start: new Date().setHours(10, 0, 0, 0),
end: new Date().setHours(11, 0, 0, 0),
timed: true,
activityType: 1,
activityId: 0,
instanceId: 0,
calendarId: 0
},
{
name: "Lunch Break",
content: "Lunch Break",
color: "#dce6f4",
start: new Date().setHours(11, 0, 0, 0),
end: new Date().setHours(11, 30, 0, 0),
timed: true,
activityType: 1,
activityId: 0,
instanceId: 0,
calendarId: 0
},
{
name: "Excursion",
content: "Excursion",
color: "#dce6f4",
start: new Date().setHours(11, 0, 0, 0),
end: new Date().setHours(13, 30, 0, 0),
timed: true,
activityType: 1,
activityId: 0,
instanceId: 0,
calendarId: 0
}
]
} }
}, },
computed: { computed: {
@ -308,33 +249,6 @@ div {
document.head.appendChild(style) document.head.appendChild(style)
this.$store.dispatch("saveOnlineSettings") this.$store.dispatch("saveOnlineSettings")
}, },
computeColor(event) {
if (event.color === "#003300") {
return this.$vuetify.theme.themes[
this.$store.state.user.theme || "dark"
].calendarActivityType8
} else if (event.color === "#133897") {
return this.$vuetify.theme.themes[
this.$store.state.user.theme || "dark"
].calendarExternalActivity
} else if (event.activityType === 7 || event.color === "#f4dcdc") {
return this.$vuetify.theme.themes[
this.$store.state.user.theme || "dark"
].calendarActivityType7
} else if (event.color === "#dce6f4") {
return this.$vuetify.theme.themes[
this.$store.state.user.theme || "dark"
].calendarNormalActivity
} else if (event.activityType === 10) {
return this.$vuetify.theme.themes[
this.$store.state.user.theme || "dark"
].calendarActivityType10
} else {
return this.$vuetify.theme.themes[
this.$store.state.user.theme || "dark"
].calendarNormalActivity
}
},
editorInit() { editorInit() {
require("brace/ext/language_tools") require("brace/ext/language_tools")
require("brace/mode/css") require("brace/mode/css")
@ -357,18 +271,7 @@ div {
sheet: "#" + Math.floor(Math.random() * 16777215).toString(16), sheet: "#" + Math.floor(Math.random() * 16777215).toString(16),
text: "#" + Math.floor(Math.random() * 16777215).toString(16), text: "#" + Math.floor(Math.random() * 16777215).toString(16),
dark: "#" + Math.floor(Math.random() * 16777215).toString(16), dark: "#" + Math.floor(Math.random() * 16777215).toString(16),
bg: "#" + Math.floor(Math.random() * 16777215).toString(16), bg: "#" + Math.floor(Math.random() * 16777215).toString(16)
calendarNormalActivity:
"#" + Math.floor(Math.random() * 16777215).toString(16),
calendarActivityType7:
"#" + Math.floor(Math.random() * 16777215).toString(16),
calendarActivityType8:
"#" + Math.floor(Math.random() * 16777215).toString(16),
calendarActivityType10:
"#" + Math.floor(Math.random() * 16777215).toString(16),
calendarExternalActivity: Math.floor(Math.random() * 16777215).toString(
16
)
} }
this.creator.dark = { this.creator.dark = {
primary: "#" + Math.floor(Math.random() * 16777215).toString(16), primary: "#" + Math.floor(Math.random() * 16777215).toString(16),
@ -383,18 +286,7 @@ div {
sheet: "#" + Math.floor(Math.random() * 16777215).toString(16), sheet: "#" + Math.floor(Math.random() * 16777215).toString(16),
text: "#" + Math.floor(Math.random() * 16777215).toString(16), text: "#" + Math.floor(Math.random() * 16777215).toString(16),
dark: "#" + Math.floor(Math.random() * 16777215).toString(16), dark: "#" + Math.floor(Math.random() * 16777215).toString(16),
bg: "#" + Math.floor(Math.random() * 16777215).toString(16), bg: "#" + Math.floor(Math.random() * 16777215).toString(16)
calendarNormalActivity:
"#" + Math.floor(Math.random() * 16777215).toString(16),
calendarActivityType7:
"#" + Math.floor(Math.random() * 16777215).toString(16),
calendarActivityType8:
"#" + Math.floor(Math.random() * 16777215).toString(16),
calendarActivityType10:
"#" + Math.floor(Math.random() * 16777215).toString(16),
calendarExternalActivity: Math.floor(Math.random() * 16777215).toString(
16
)
} }
}, },
doDeleteTheme(theme) { doDeleteTheme(theme) {
@ -500,17 +392,7 @@ div {
a.dispatchEvent(e) a.dispatchEvent(e)
}, },
friendlyName(index) { friendlyName(index) {
if (index === "calendarNormalActivity") { if (index === "bg") {
return "Standard Class"
} else if (index === "calendarActivityType7") {
return "Relief Event"
} else if (index === "calendarActivityType8") {
return "Generic Type 8"
} else if (index === "calendarActivityType10") {
return "Learning Task"
} else if (index === "calendarExternalActivity") {
return "External Activity"
} else if (index === "bg") {
return "Background" return "Background"
} else if (index === "dark") { } else if (index === "dark") {
return "Sidebar & Header" return "Sidebar & Header"
@ -581,7 +463,6 @@ div {
} }
}, },
mounted() { mounted() {
console.log(document.querySelectorAll(".editor__toolbar"))
this.defineAccent = this.$store.state.user.accentColor !== null this.defineAccent = this.$store.state.user.accentColor !== null
this.accent = this.$store.state.user.accentColor this.accent = this.$store.state.user.accentColor
this.name = this.$vuetify.theme.themes.name this.name = this.$vuetify.theme.themes.name

View file

@ -1194,6 +1194,11 @@
resolved "https://registry.yarnpkg.com/@soda/get-current-script/-/get-current-script-1.0.2.tgz#a53515db25d8038374381b73af20bb4f2e508d87" resolved "https://registry.yarnpkg.com/@soda/get-current-script/-/get-current-script-1.0.2.tgz#a53515db25d8038374381b73af20bb4f2e508d87"
integrity sha512-T7VNNlYVM1SgQ+VsMYhnDkcGmWhQdL0bDyGm5TlQ3GBXnJscEClUUOKduWTmm2zCnvNLC1hc3JpuXjs/nFOc5w== integrity sha512-T7VNNlYVM1SgQ+VsMYhnDkcGmWhQdL0bDyGm5TlQ3GBXnJscEClUUOKduWTmm2zCnvNLC1hc3JpuXjs/nFOc5w==
"@tauri-apps/api@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-1.0.2.tgz#5228720e35d50fd08df87067dc29e7306c1f7a10"
integrity sha512-yuNW0oeJ1/ZA7wNF1KgxhHrSu5viPVzY/UgUczzN5ptLM8dH15Juy5rEGkoHfeXGju90Y/l22hi3BtIrp/za+w==
"@types/body-parser@*": "@types/body-parser@*":
version "1.19.2" version "1.19.2"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0"