mirror of
https://github.com/Troplo/Colubrina.git
synced 2024-11-22 19:27:55 +11:00
major update
This commit is contained in:
parent
be90a91e52
commit
f17a9d3027
37 changed files with 3510 additions and 1987 deletions
2
NOTICE
2
NOTICE
|
@ -1,2 +1,2 @@
|
||||||
Colubrina or Troplo/Colubrina is a simple chatting platform that allows you to communicate with people over websockets.
|
Colubrina or Troplo/Colubrina is a simple chatting platform that allows you to communicate with people over websockets.
|
||||||
Licensed under GNU GPLv3, and based off of the original BetterCompass Communications by @Troplo.
|
Licensed under GNU GPLv3.
|
|
@ -1,6 +1,13 @@
|
||||||
# Colubrina
|
# Colubrina
|
||||||
|
|
||||||
![Wakatime](https://wakatime.troplo.com/api/badge/Troplo/interval:any/project:Colubrina?label=wakatime)
|
![Wakatime](https://wakatime.troplo.com/api/badge/Troplo/interval:any/project:Colubrina?label=wakatime)
|
||||||
|
|
||||||
|
<img src="https://i.troplo.com/i/d608273e066c.png" alt="Chat" width="45%"></img>
|
||||||
|
<img src="https://i.troplo.com/i/e8e2c9d6e349.png" alt="Friends" width="45%"></img>
|
||||||
|
<img src="https://i.troplo.com/i/e958b8e58c5e.png" alt="Chat with AMOLED theme" width="45%"></img>
|
||||||
|
<img src="https://i.troplo.com/i/279376da3f1d.png" alt="Chat with profile card and light theme" width="45%"></img>
|
||||||
|
<img src="https://i.troplo.com/i/59b63d5aa167.png" alt="QuickSwitcher" width="45%"></img>
|
||||||
|
<img src="https://i.troplo.com/i/b2d6dd14c6b6.png" alt="QuickSwitcher with AMOLED theme" width="45%"></img>
|
||||||
## Project setup
|
## Project setup
|
||||||
|
|
||||||
Rename .env.example to .env and fill it out with your own information.
|
Rename .env.example to .env and fill it out with your own information.
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
HOSTNAME=localhost
|
HOSTNAME=localhost
|
||||||
RELEASE=dev
|
CORS_HOSTNAME=http://localhost:8080
|
||||||
|
RELEASE=stable
|
||||||
NOTIFICATION=
|
NOTIFICATION=
|
||||||
WEATHER_API_KEY=OpenWeatherMap API Key
|
|
||||||
PARENT_LINK=[{"intendedFor": [0], "instance": "compass-vic", "userId": 1, "token": "aaaaaaaa-bbbbbbbb-cccccccc-dddddddd", "whitelistedSenderIds": []}]
|
|
||||||
NOTIFICATION_TYPE=info
|
NOTIFICATION_TYPE=info
|
||||||
|
SITE_NAME=Colubrina
|
||||||
|
ALLOW_REGISTRATIONS=true
|
|
@ -1,4 +1,2 @@
|
||||||
BetterCompass is created and maintained by GitHub user @Troplo, and Gitea user https://git.troplo.com/Troplo, and is licensed under the GNU GENERAL PUBLIC LICENSE Version 3, which can be found in the LICENSE file at the root of this project.
|
Colubrina is created and maintained by GitHub user @Troplo, and Gitea user https://git.troplo.com/Troplo, and is licensed under the GNU GENERAL PUBLIC LICENSE Version 3, which can be found in the LICENSE file at the root of this project.
|
||||||
The source code is available on both Gitea, and GitHub respectively, https://git.troplo.com/Troplo/compass-vue, and https://github.com/Troplo/BetterCompass
|
The source code is available on GitHub, https://github.com/Troplo/Colubrina
|
||||||
|
|
||||||
BetterCompass Proxy is provided with the BetterCompass software, and has the same license.
|
|
322
backend/cli/index.js
Normal file
322
backend/cli/index.js
Normal file
|
@ -0,0 +1,322 @@
|
||||||
|
const input = require("input")
|
||||||
|
const fs = require("fs")
|
||||||
|
const path = require("path")
|
||||||
|
const { Umzug, SequelizeStorage } = require("umzug")
|
||||||
|
const { Sequelize } = require("sequelize")
|
||||||
|
const argon2 = require("argon2")
|
||||||
|
const { production: config } = require("../config/config.json")
|
||||||
|
const { User } = require("../models")
|
||||||
|
const axios = require("axios")
|
||||||
|
const os = require("os")
|
||||||
|
|
||||||
|
console.log("Troplo/Colubrina CLI")
|
||||||
|
console.log("Colubrina version", require("../../package.json").version)
|
||||||
|
async function checkForUpdates() {
|
||||||
|
await axios
|
||||||
|
.get("https://services.troplo.com/api/v1/state", {
|
||||||
|
headers: {
|
||||||
|
"X-Troplo-Project": "colubrina"
|
||||||
|
},
|
||||||
|
timeout: 800
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (require("../../package.json").version !== res.data.latestVersion) {
|
||||||
|
console.log("A new version of Colubrina is available!")
|
||||||
|
console.log("Latest version:", res.data.latestVersion)
|
||||||
|
} else {
|
||||||
|
console.log("Colubrina is up to date.")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.log(
|
||||||
|
"Failed to check for updates, ensure you are connected to the internet, and services.troplo.com is whitelisted behind any potential firewalls."
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let state = {
|
||||||
|
db: {
|
||||||
|
host: "localhost",
|
||||||
|
port: 3306,
|
||||||
|
username: "colubrina",
|
||||||
|
password: null,
|
||||||
|
database: "colubrina",
|
||||||
|
storage: "./storage.db"
|
||||||
|
},
|
||||||
|
dbConfig: {}
|
||||||
|
}
|
||||||
|
async function doSetupDB() {
|
||||||
|
const dialect = await input.select(
|
||||||
|
"What database dialect do you want to use? (MariaDB tested, recommended)",
|
||||||
|
["mariadb", "postgres", "sqlite"]
|
||||||
|
)
|
||||||
|
const host = await input.text("What is the host?", {
|
||||||
|
default: state.db.host || "localhost"
|
||||||
|
})
|
||||||
|
const port = await input.text("What is the port?", {
|
||||||
|
default: state.db.port || 3306
|
||||||
|
})
|
||||||
|
const username = await input.text("What is the username?", {
|
||||||
|
default: state.db.username || "colubrina"
|
||||||
|
})
|
||||||
|
const password = await input.text("What is the password?", {
|
||||||
|
default: state.db.password ? "Enter for cached password" : "Please specify"
|
||||||
|
})
|
||||||
|
const database = await input.text("What is the database name?", {
|
||||||
|
default: state.db.database || "colubrina"
|
||||||
|
})
|
||||||
|
let storage
|
||||||
|
if (dialect === "sqlite") {
|
||||||
|
storage = await input.text(
|
||||||
|
"What is the path to the storage file (SQLite only)?",
|
||||||
|
{
|
||||||
|
default: state.db.storage || "./storage.db"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
state.db = {
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
database: database,
|
||||||
|
host: host,
|
||||||
|
dialect: dialect,
|
||||||
|
port: port,
|
||||||
|
logging: false
|
||||||
|
}
|
||||||
|
state.dbConfig = {
|
||||||
|
development: {
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
database: database,
|
||||||
|
host: host,
|
||||||
|
dialect: dialect,
|
||||||
|
port: port,
|
||||||
|
storage: dialect === "sqlite" ? storage : null,
|
||||||
|
logging: false
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
database: database,
|
||||||
|
host: host,
|
||||||
|
dialect: dialect,
|
||||||
|
port: port,
|
||||||
|
logging: false
|
||||||
|
},
|
||||||
|
production: {
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
database: database,
|
||||||
|
host: host,
|
||||||
|
dialect: dialect,
|
||||||
|
port: port,
|
||||||
|
logging: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await testDB()
|
||||||
|
}
|
||||||
|
async function testDB() {
|
||||||
|
try {
|
||||||
|
const sequelize = new Sequelize(state.db)
|
||||||
|
await sequelize.authenticate()
|
||||||
|
console.log("Connection to database has been established successfully.")
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Unable to connect to the database:", error)
|
||||||
|
await doSetupDB()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function dbSetup() {
|
||||||
|
await doSetupDB()
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(__dirname, "../config/config.json"),
|
||||||
|
JSON.stringify(state.dbConfig)
|
||||||
|
)
|
||||||
|
console.log("config/config.json overwritten")
|
||||||
|
}
|
||||||
|
async function runMigrations() {
|
||||||
|
console.log("Running migrations")
|
||||||
|
const config = require("../config/config.json").production
|
||||||
|
const sequelize = new Sequelize(config)
|
||||||
|
|
||||||
|
const umzug = new Umzug({
|
||||||
|
migrations: { glob: "../migrations/*.js" },
|
||||||
|
context: sequelize.getQueryInterface(),
|
||||||
|
storage: new SequelizeStorage({ sequelize }),
|
||||||
|
logger: console,
|
||||||
|
logging: true
|
||||||
|
})
|
||||||
|
|
||||||
|
await (async () => {
|
||||||
|
await umzug.up()
|
||||||
|
})()
|
||||||
|
console.log("Migrations applied")
|
||||||
|
}
|
||||||
|
async function createUser() {
|
||||||
|
const user = {
|
||||||
|
username: await input.text("Username", {
|
||||||
|
default: "admin"
|
||||||
|
}),
|
||||||
|
password: await argon2.hash(await input.text("Password", {})),
|
||||||
|
email: await input.text("Email", {
|
||||||
|
default: "troplo@troplo.com"
|
||||||
|
}),
|
||||||
|
admin: JSON.parse(
|
||||||
|
await input.confirm("Admin (true/false)", {
|
||||||
|
default: false
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
await User.create(user)
|
||||||
|
console.log("User created")
|
||||||
|
}
|
||||||
|
async function configureDotEnv() {
|
||||||
|
function setEnvValue(key, value) {
|
||||||
|
const ENV_VARS = fs.readFileSync("../.env", "utf8").split(os.EOL)
|
||||||
|
|
||||||
|
// find the env we want based on the key
|
||||||
|
const target = ENV_VARS.indexOf(
|
||||||
|
ENV_VARS.find((line) => {
|
||||||
|
// (?<!#\s*) Negative lookbehind to avoid matching comments (lines that starts with #).
|
||||||
|
// There is a double slash in the RegExp constructor to escape it.
|
||||||
|
// (?==) Positive lookahead to check if there is an equal sign right after the key.
|
||||||
|
// This is to prevent matching keys prefixed with the key of the env var to update.
|
||||||
|
const keyValRegex = new RegExp(`(?<!#\\s*)${key}(?==)`)
|
||||||
|
|
||||||
|
return line.match(keyValRegex)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// if key-value pair exists in the .env file,
|
||||||
|
if (target !== -1) {
|
||||||
|
// replace the key/value with the new value
|
||||||
|
ENV_VARS.splice(target, 1, `${key}=${value}`)
|
||||||
|
} else {
|
||||||
|
// if it doesn't exist, add it instead
|
||||||
|
ENV_VARS.push(`${key}=${value}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write everything back to the file system
|
||||||
|
fs.writeFileSync("../.env", ENV_VARS.join(os.EOL))
|
||||||
|
}
|
||||||
|
if (!fs.existsSync("../.env")) {
|
||||||
|
fs.writeFileSync("../.env", "")
|
||||||
|
}
|
||||||
|
setEnvValue(
|
||||||
|
"HOSTNAME",
|
||||||
|
await input.text("Public Domain", {
|
||||||
|
default: "localhost"
|
||||||
|
})
|
||||||
|
)
|
||||||
|
setEnvValue(
|
||||||
|
"CORS_HOSTNAME",
|
||||||
|
await input.text("Public Hostname", {
|
||||||
|
default: "http://localhost:8080"
|
||||||
|
})
|
||||||
|
)
|
||||||
|
setEnvValue(
|
||||||
|
"SITE_NAME",
|
||||||
|
await input.text("Site Name", {
|
||||||
|
default: "Colubrina"
|
||||||
|
})
|
||||||
|
)
|
||||||
|
setEnvValue(
|
||||||
|
"ALLOW_REGISTRATIONS",
|
||||||
|
await input.text("Permit Public Registrations", {
|
||||||
|
default: false
|
||||||
|
})
|
||||||
|
)
|
||||||
|
setEnvValue("NOTIFICATION", "")
|
||||||
|
setEnvValue("NOTIFICATION_TYPE", "info")
|
||||||
|
setEnvValue("RELEASE", "stable")
|
||||||
|
}
|
||||||
|
async function init() {
|
||||||
|
while (true) {
|
||||||
|
const option = await input.select(`Please select an option`, [
|
||||||
|
"Setup",
|
||||||
|
"Create user",
|
||||||
|
"Run migrations",
|
||||||
|
"Update/create config file",
|
||||||
|
"Check for updates",
|
||||||
|
"Exit"
|
||||||
|
])
|
||||||
|
|
||||||
|
if (option === "Setup") {
|
||||||
|
if (fs.existsSync(path.join(__dirname, "../.env"))) {
|
||||||
|
const option = await input.confirm(".env already exists, overwrite?", {
|
||||||
|
default: false
|
||||||
|
})
|
||||||
|
if (option) {
|
||||||
|
await configureDotEnv()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await configureDotEnv()
|
||||||
|
}
|
||||||
|
if (fs.existsSync(path.join(__dirname, "../config/config.json"))) {
|
||||||
|
const option = await input.select(
|
||||||
|
`config/config.json already exists. Do you want to overwrite it?`,
|
||||||
|
["Yes", "No"]
|
||||||
|
)
|
||||||
|
if (option === "Yes") {
|
||||||
|
await dbSetup()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await dbSetup()
|
||||||
|
}
|
||||||
|
await runMigrations()
|
||||||
|
const { User, Theme } = require("../models")
|
||||||
|
try {
|
||||||
|
await Theme.bulkCreate(
|
||||||
|
JSON.parse(
|
||||||
|
fs.readFileSync(path.join(__dirname, "./templates/themes.json"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
console.log("Themes already exist.")
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await User.create({
|
||||||
|
username: "Colubrina",
|
||||||
|
id: 0,
|
||||||
|
bot: true,
|
||||||
|
email: "colubrina@troplo.com"
|
||||||
|
})
|
||||||
|
await User.update(
|
||||||
|
{
|
||||||
|
id: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
where: {
|
||||||
|
username: "Colubrina"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
console.log("Users already exist.")
|
||||||
|
}
|
||||||
|
console.log("DB templates applied")
|
||||||
|
console.log("Admin user creation")
|
||||||
|
await createUser()
|
||||||
|
console.log("Colubrina has been setup.")
|
||||||
|
console.log(
|
||||||
|
"Colubrina can be started with `yarn serve` or `node .` in the backend directory."
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
"The Colubrina frontend can be built with `yarn build` in the root project directory, and is recommended to be served via NGINX, with a proxy_pass to the backend on /api and /socket.io."
|
||||||
|
)
|
||||||
|
} else if (option === "Update/create config file") {
|
||||||
|
await dbSetup()
|
||||||
|
console.log("config/config.json overwritten or created")
|
||||||
|
} else if (option === "Create user") {
|
||||||
|
await createUser()
|
||||||
|
} else if (option === "Run migrations") {
|
||||||
|
await runMigrations()
|
||||||
|
} else if (option === "Check for updates") {
|
||||||
|
await checkForUpdates()
|
||||||
|
} else if (option === "Exit") {
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkForUpdates().finally(() => {
|
||||||
|
init()
|
||||||
|
})
|
133
backend/cli/templates/themes.json
Normal file
133
backend/cli/templates/themes.json
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Colubrina Classic",
|
||||||
|
"userId": 0,
|
||||||
|
"public": true,
|
||||||
|
"theme": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "Colubrina Classic",
|
||||||
|
"primaryType": "all",
|
||||||
|
"dark": {
|
||||||
|
"primary": "#0190ea",
|
||||||
|
"secondary": "#757575",
|
||||||
|
"accent": "#000000",
|
||||||
|
"error": "#ff1744",
|
||||||
|
"info": "#2196F3",
|
||||||
|
"success": "#4CAF50",
|
||||||
|
"warning": "#ff9800",
|
||||||
|
"card": "#151515",
|
||||||
|
"toolbar": "#191919",
|
||||||
|
"sheet": "#181818",
|
||||||
|
"text": "#000000",
|
||||||
|
"dark": "#151515",
|
||||||
|
"bg": "#151515"
|
||||||
|
},
|
||||||
|
"light": {
|
||||||
|
"primary": "#0190ea",
|
||||||
|
"secondary": "#757575",
|
||||||
|
"accent": "#000000",
|
||||||
|
"error": "#ff1744",
|
||||||
|
"info": "#2196F3",
|
||||||
|
"success": "#4CAF50",
|
||||||
|
"warning": "#ff9800",
|
||||||
|
"card": "#f8f8f8",
|
||||||
|
"toolbar": "#f8f8f8",
|
||||||
|
"sheet": "#f8f8f8",
|
||||||
|
"text": "#000000",
|
||||||
|
"dark": "#f8f8f8",
|
||||||
|
"bg": "#f8f8f8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"createdAt": "2022-03-26 23:23:29",
|
||||||
|
"updatedAt": "2022-03-26 23:23:29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Colubrina Gray",
|
||||||
|
"userId": 0,
|
||||||
|
"public": true,
|
||||||
|
"theme": {
|
||||||
|
"id": 2,
|
||||||
|
"name": "Colubrina Gray",
|
||||||
|
"primaryType": "all",
|
||||||
|
"user": {
|
||||||
|
"sussiId": "asdasd"
|
||||||
|
},
|
||||||
|
"dark": {
|
||||||
|
"primary": "#0190ea",
|
||||||
|
"secondary": "#757575",
|
||||||
|
"accent": "#000000",
|
||||||
|
"error": "#ff1744",
|
||||||
|
"info": "#2196F3",
|
||||||
|
"success": "#4CAF50",
|
||||||
|
"warning": "#ff9800",
|
||||||
|
"card": "#262626",
|
||||||
|
"toolbar": "#262626",
|
||||||
|
"sheet": "#262626",
|
||||||
|
"text": "#000000",
|
||||||
|
"dark": "#262626",
|
||||||
|
"bg": "#191919"
|
||||||
|
},
|
||||||
|
"light": {
|
||||||
|
"primary": "#0190ea",
|
||||||
|
"secondary": "#757575",
|
||||||
|
"accent": "#000000",
|
||||||
|
"error": "#ff1744",
|
||||||
|
"info": "#2196F3",
|
||||||
|
"success": "#4CAF50",
|
||||||
|
"warning": "#ff9800",
|
||||||
|
"card": "#dedede",
|
||||||
|
"toolbar": "#dedede",
|
||||||
|
"sheet": "#dedede",
|
||||||
|
"text": "#000000",
|
||||||
|
"dark": "#dedede",
|
||||||
|
"bg": "#e7e7e7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"createdAt": "2022-03-26 23:25:12",
|
||||||
|
"updatedAt": "2022-03-26 23:25:12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "Colubrina AMOLED (Black)",
|
||||||
|
"userId": 0,
|
||||||
|
"public": true,
|
||||||
|
"theme": {
|
||||||
|
"id": 3,
|
||||||
|
"name": "Colubrina AMOLED",
|
||||||
|
"primaryType": "dark",
|
||||||
|
"dark": {
|
||||||
|
"primary": "#0190ea",
|
||||||
|
"secondary": "#757575",
|
||||||
|
"accent": "#000000",
|
||||||
|
"error": "#ff1744",
|
||||||
|
"info": "#2196F3",
|
||||||
|
"success": "#4CAF50",
|
||||||
|
"warning": "#ff9800",
|
||||||
|
"card": "#000000",
|
||||||
|
"toolbar": "#121212",
|
||||||
|
"sheet": "#000000",
|
||||||
|
"text": "#000000",
|
||||||
|
"dark": "#000000",
|
||||||
|
"bg": "#000000"
|
||||||
|
},
|
||||||
|
"light": {
|
||||||
|
"primary": "#0190ea",
|
||||||
|
"secondary": "#757575",
|
||||||
|
"accent": "#000000",
|
||||||
|
"error": "#ff1744",
|
||||||
|
"info": "#2196F3",
|
||||||
|
"success": "#4CAF50",
|
||||||
|
"warning": "#ff9800",
|
||||||
|
"card": "#f8f8f8",
|
||||||
|
"toolbar": "#f8f8f8",
|
||||||
|
"sheet": "#f8f8f8",
|
||||||
|
"text": "#000000",
|
||||||
|
"dark": "#f8f8f8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"createdAt": "2022-03-26 23:25:12",
|
||||||
|
"updatedAt": "2022-03-26 23:25:12"
|
||||||
|
}
|
||||||
|
]
|
9
backend/cli/templates/users.json
Normal file
9
backend/cli/templates/users.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
[{
|
||||||
|
"username": "Colubrina",
|
||||||
|
"id": 0,
|
||||||
|
"password": null,
|
||||||
|
"email": null,
|
||||||
|
"storedStatus": "invisible",
|
||||||
|
"status": "offline",
|
||||||
|
"bot": true
|
||||||
|
}]
|
|
@ -27,7 +27,8 @@ app.get("/api/v1/state", async (req, res) => {
|
||||||
notification: process.env.NOTIFICATION,
|
notification: process.env.NOTIFICATION,
|
||||||
notificationType: process.env.NOTIFICATION_TYPE,
|
notificationType: process.env.NOTIFICATION_TYPE,
|
||||||
latestVersion: require("../package.json").version,
|
latestVersion: require("../package.json").version,
|
||||||
name: "Colubrina"
|
name: process.env.SITE_NAME,
|
||||||
|
allowRegistrations: JSON.parse(process.env.ALLOW_REGISTRATIONS)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -2,25 +2,16 @@ let Errors = {
|
||||||
unknown: ["Something went wrong.", 500],
|
unknown: ["Something went wrong.", 500],
|
||||||
unauthorized: ["You don't have permission to do that.", 401],
|
unauthorized: ["You don't have permission to do that.", 401],
|
||||||
notAuthenticated: ["You have to login to do that", 401],
|
notAuthenticated: ["You have to login to do that", 401],
|
||||||
userNotOptedIn: [
|
|
||||||
"You have to opt in to BetterCompass Accounts to do that.",
|
|
||||||
401
|
|
||||||
],
|
|
||||||
invalidUserOrPassword: ["Invalid username or password.", 401],
|
invalidUserOrPassword: ["Invalid username or password.", 401],
|
||||||
parentLinkIneligible: ["Your school does not support ParentLink.", 401],
|
|
||||||
invalidTotp: ["Invalid 2FA code.", 401],
|
invalidTotp: ["Invalid 2FA code.", 401],
|
||||||
invalidCredentials: ["Invalid username or password.", 401],
|
invalidCredentials: ["Invalid username or password.", 401],
|
||||||
bcSessionsForced: [
|
|
||||||
"You are attempting to login as a user that enforces BetterCompass Sessions.\nBetterCompass extended functionality is disabled for this session instance.",
|
|
||||||
401
|
|
||||||
],
|
|
||||||
rateLimit: [
|
rateLimit: [
|
||||||
"You are being rate-limited. Please try again in a few minutes.",
|
"You are being rate-limited. Please try again in a few minutes.",
|
||||||
429
|
429
|
||||||
],
|
],
|
||||||
communicationsUserNotFound: ["This user does not exist.", 400],
|
communicationsUserNotFound: ["This user does not exist.", 400],
|
||||||
communicationsUserNotOptedIn: [
|
communicationsUserNotOptedIn: [
|
||||||
"This user has not opted in to BetterCompass Communications.",
|
"This user does not have chatting enabled.",
|
||||||
400
|
400
|
||||||
],
|
],
|
||||||
friendAlreadyFriends: ["You are already friends with this user.", 400],
|
friendAlreadyFriends: ["You are already friends with this user.", 400],
|
||||||
|
@ -39,20 +30,20 @@ let Errors = {
|
||||||
400
|
400
|
||||||
],
|
],
|
||||||
fileTooLarge: ["The file you are trying to upload is too large.", 400],
|
fileTooLarge: ["The file you are trying to upload is too large.", 400],
|
||||||
unauthorizedInstance: [
|
|
||||||
"You are not authorized to access BetterCompass.",
|
|
||||||
401
|
|
||||||
],
|
|
||||||
invalidPassword: [
|
invalidPassword: [
|
||||||
"Your password must be at least 8 characters in length.",
|
"Your password must be at least 8 characters in length.",
|
||||||
400
|
400
|
||||||
|
],
|
||||||
|
registrationsDisabled: [
|
||||||
|
"Registrations are currently disabled on this instance. Please try again later.",
|
||||||
|
400
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function processErrors(errorName) {
|
function processErrors(errorName) {
|
||||||
let arr = Errors[errorName]
|
let arr = Errors[errorName]
|
||||||
|
|
||||||
temp = {}
|
let temp = {}
|
||||||
temp.name = errorName
|
temp.name = errorName
|
||||||
temp.message = arr[0]
|
temp.message = arr[0]
|
||||||
temp.status = arr[1]
|
temp.status = arr[1]
|
||||||
|
|
|
@ -4,7 +4,7 @@ module.exports = {
|
||||||
up: async (queryInterface, Sequelize) => {
|
up: async (queryInterface, Sequelize) => {
|
||||||
await queryInterface.addColumn("Users", "font", {
|
await queryInterface.addColumn("Users", "font", {
|
||||||
type: Sequelize.STRING,
|
type: Sequelize.STRING,
|
||||||
defaultValue: "Roboto",
|
defaultValue: "Inter",
|
||||||
allowNull: false
|
allowNull: false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -24,6 +24,11 @@ module.exports = (sequelize, DataTypes) => {
|
||||||
}
|
}
|
||||||
User.init(
|
User.init(
|
||||||
{
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.BIGINT,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
username: {
|
username: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
@ -112,7 +117,7 @@ module.exports = (sequelize, DataTypes) => {
|
||||||
},
|
},
|
||||||
font: {
|
font: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: "Roboto",
|
defaultValue: "Inter",
|
||||||
allowNull: false
|
allowNull: false
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
|
@ -144,6 +149,10 @@ module.exports = (sequelize, DataTypes) => {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: false
|
defaultValue: false
|
||||||
|
},
|
||||||
|
lastSeenAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
"serve": "nodemon"
|
"serve": "nodemon"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argon2": "^0.28.5",
|
"argon2": "^0.28.7",
|
||||||
"axios": "^0.26.1",
|
"axios": "^0.27.2",
|
||||||
"clean-css": "^4.1.11",
|
"clean-css": "^4.1.11",
|
||||||
"constantinople": "3.1.1",
|
"constantinople": "3.1.1",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
|
@ -20,20 +20,26 @@
|
||||||
"express-rate-limit": "^6.4.0",
|
"express-rate-limit": "^6.4.0",
|
||||||
"file-type": "16.5.3",
|
"file-type": "16.5.3",
|
||||||
"http-errors": "~1.6.3",
|
"http-errors": "~1.6.3",
|
||||||
|
"input": "^1.0.1",
|
||||||
"jade": "~1.11.0",
|
"jade": "~1.11.0",
|
||||||
"jw-paginate": "^1.0.4",
|
"jw-paginate": "^1.0.4",
|
||||||
"local-cors-proxy": "^1.1.0",
|
"local-cors-proxy": "^1.1.0",
|
||||||
"mariadb": "^3.0.0",
|
"mariadb": "^3.0.1",
|
||||||
"multer": "^1.4.4",
|
"multer": "^1.4.4",
|
||||||
"node-xwhois": "^2.0.10",
|
"node-xwhois": "^2.0.10",
|
||||||
"open-graph-scraper": "^4.11.0",
|
"open-graph-scraper": "^4.11.0",
|
||||||
"patch-package": "^6.4.7",
|
"patch-package": "^6.4.7",
|
||||||
|
"pg": "^8.7.3",
|
||||||
|
"pg-hstore": "^2.3.4",
|
||||||
"semver": "^7.3.7",
|
"semver": "^7.3.7",
|
||||||
"sequelize": "^6.17.0",
|
"sequelize": "^6.21.3",
|
||||||
|
"sequelize-cli": "^6.4.1",
|
||||||
"socket.io": "^4.5.1",
|
"socket.io": "^4.5.1",
|
||||||
"speakeasy": "^2.0.0",
|
"speakeasy": "^2.0.0",
|
||||||
|
"sqlite3": "^5.0.10",
|
||||||
"ua-parser-js": "^1.0.2",
|
"ua-parser-js": "^1.0.2",
|
||||||
"uglify-js": "^2.6.0"
|
"uglify-js": "^2.6.0",
|
||||||
|
"umzug": "^3.1.1"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"constantinople": "^3.1.1",
|
"constantinople": "^3.1.1",
|
||||||
|
|
|
@ -2,7 +2,7 @@ const express = require("express")
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
const Errors = require("../lib/errors.js")
|
const Errors = require("../lib/errors.js")
|
||||||
const auth = require("../lib/authorize.js")
|
const auth = require("../lib/authorize.js")
|
||||||
const { User, Theme, Feedback, Message } = require("../models")
|
const { User, Theme, Message } = require("../models")
|
||||||
const { Op } = require("sequelize")
|
const { Op } = require("sequelize")
|
||||||
const dayjs = require("dayjs")
|
const dayjs = require("dayjs")
|
||||||
const fs = require("fs")
|
const fs = require("fs")
|
||||||
|
@ -25,7 +25,6 @@ router.get("/", auth, async (req, res, next) => {
|
||||||
res.json({
|
res.json({
|
||||||
users: await User.count(),
|
users: await User.count(),
|
||||||
themes: await Theme.count(),
|
themes: await Theme.count(),
|
||||||
feedback: await Feedback.count(),
|
|
||||||
messages: await Message.count(),
|
messages: await Message.count(),
|
||||||
usersToday: await User.count({
|
usersToday: await User.count({
|
||||||
where: {
|
where: {
|
||||||
|
@ -62,6 +61,14 @@ router.get("/metrics", auth, async (req, res, next) => {
|
||||||
exclude: ["totp", "compassSession", "password"]
|
exclude: ["totp", "compassSession", "password"]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const messages = await Message.findAll({
|
||||||
|
where: {
|
||||||
|
createdAt: createdAt
|
||||||
|
},
|
||||||
|
attributes: {
|
||||||
|
exclude: ["totp", "compassSession", "password"]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const registrationGraphInterim = registrationStats.reduce(function (
|
const registrationGraphInterim = registrationStats.reduce(function (
|
||||||
result,
|
result,
|
||||||
|
@ -76,18 +83,14 @@ router.get("/metrics", auth, async (req, res, next) => {
|
||||||
},
|
},
|
||||||
{})
|
{})
|
||||||
|
|
||||||
const activeUsersGraphInterim = registrationStats.reduce(function (
|
const messagesGraphInterim = messages.reduce(function (result, message) {
|
||||||
result,
|
let day = dayjs(message.createdAt).format("YYYY-MM-DD")
|
||||||
user
|
|
||||||
) {
|
|
||||||
let day = dayjs(user.lastSeenAt).format("YYYY-MM-DD")
|
|
||||||
if (!result[day]) {
|
if (!result[day]) {
|
||||||
result[day] = 0
|
result[day] = 0
|
||||||
}
|
}
|
||||||
result[day]++
|
result[day]++
|
||||||
return result
|
return result
|
||||||
},
|
}, {})
|
||||||
{})
|
|
||||||
|
|
||||||
const usersGraph = {
|
const usersGraph = {
|
||||||
labels: Object.keys(registrationGraphInterim),
|
labels: Object.keys(registrationGraphInterim),
|
||||||
|
@ -102,12 +105,12 @@ router.get("/metrics", auth, async (req, res, next) => {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeUsersGraph = {
|
const messagesGraph = {
|
||||||
labels: Object.keys(activeUsersGraphInterim),
|
labels: Object.keys(messagesGraphInterim),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
data: Object.values(activeUsersGraphInterim),
|
data: Object.values(messagesGraphInterim),
|
||||||
label: "Active Users",
|
label: "Messages",
|
||||||
borderColor: "#3e95cd",
|
borderColor: "#3e95cd",
|
||||||
pointBackgroundColor: "#FFFFFF",
|
pointBackgroundColor: "#FFFFFF",
|
||||||
backgroundColor: "transparent"
|
backgroundColor: "transparent"
|
||||||
|
@ -117,7 +120,7 @@ router.get("/metrics", auth, async (req, res, next) => {
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
users: usersGraph,
|
users: usersGraph,
|
||||||
activeUsers: activeUsersGraph
|
activeUsers: messagesGraph
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return next(err)
|
return next(err)
|
||||||
|
@ -184,23 +187,6 @@ router.put("/themes/apply", auth, async (req, res, next) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get("/feedback", auth, async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const feedback = await Feedback.findAndCountAll({
|
|
||||||
order: [["createdAt", "DESC"]],
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: "user"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
res.json(feedback)
|
|
||||||
} catch (err) {
|
|
||||||
return next(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
router.put("/state", auth, async (req, res, next) => {
|
router.put("/state", auth, async (req, res, next) => {
|
||||||
function setEnvValue(key, value) {
|
function setEnvValue(key, value) {
|
||||||
// read file from hdd & split if from a linebreak to a array
|
// read file from hdd & split if from a linebreak to a array
|
||||||
|
@ -233,6 +219,7 @@ router.put("/state", auth, async (req, res, next) => {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const io = req.app.get("io")
|
const io = req.app.get("io")
|
||||||
|
setEnvValue("ALLOW_REGISTRATIONS", req.body.allowRegistrations)
|
||||||
if (req.body.broadcastType === "permanent") {
|
if (req.body.broadcastType === "permanent") {
|
||||||
setEnvValue("NOTIFICATION", req.body.notification)
|
setEnvValue("NOTIFICATION", req.body.notification)
|
||||||
setEnvValue("NOTIFICATION_TYPE", req.body.notificationType)
|
setEnvValue("NOTIFICATION_TYPE", req.body.notificationType)
|
||||||
|
@ -242,7 +229,8 @@ router.put("/state", auth, async (req, res, next) => {
|
||||||
io.emit("siteState", {
|
io.emit("siteState", {
|
||||||
notification: req.body.notification,
|
notification: req.body.notification,
|
||||||
notificationType: req.body.notificationType,
|
notificationType: req.body.notificationType,
|
||||||
latestVersion: require("../../package.json").version
|
latestVersion: require("../../package.json").version,
|
||||||
|
allowRegistrations: req.body.allowRegistrations
|
||||||
})
|
})
|
||||||
res.sendStatus(204)
|
res.sendStatus(204)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -47,7 +47,92 @@ const upload = multer({
|
||||||
|
|
||||||
const resolveEmbeds = require("../lib/resolveEmbeds.js")
|
const resolveEmbeds = require("../lib/resolveEmbeds.js")
|
||||||
const paginate = require("jw-paginate")
|
const paginate = require("jw-paginate")
|
||||||
|
async function createMessage(req, type, content, association, userId) {
|
||||||
|
const io = req.app.get("io")
|
||||||
|
const message = await Message.create({
|
||||||
|
userId: 0,
|
||||||
|
chatId: association.chatId,
|
||||||
|
content: content,
|
||||||
|
type: type || "system"
|
||||||
|
})
|
||||||
|
const associations = await ChatAssociation.findAll({
|
||||||
|
where: {
|
||||||
|
chatId: association.chatId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const messageLookup = await Message.findOne({
|
||||||
|
where: {
|
||||||
|
id: message.id
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Attachment,
|
||||||
|
as: "attachments"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Message,
|
||||||
|
as: "reply",
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "user",
|
||||||
|
attributes: [
|
||||||
|
"username",
|
||||||
|
"name",
|
||||||
|
|
||||||
|
"avatar",
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Chat,
|
||||||
|
as: "chat",
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "users",
|
||||||
|
attributes: [
|
||||||
|
"username",
|
||||||
|
"name",
|
||||||
|
|
||||||
|
"avatar",
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "user",
|
||||||
|
attributes: [
|
||||||
|
"username",
|
||||||
|
"name",
|
||||||
|
|
||||||
|
"avatar",
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
associations.forEach((user) => {
|
||||||
|
console.log(user)
|
||||||
|
io.to(user.dataValues.userId).emit("message", {
|
||||||
|
...messageLookup.dataValues,
|
||||||
|
associationId: user.dataValues.id,
|
||||||
|
keyId: `${
|
||||||
|
message.dataValues.id
|
||||||
|
}-${message.dataValues.updatedAt.toISOString()}`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
router.get("/", auth, async (req, res, next) => {
|
router.get("/", auth, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
let chats = await ChatAssociation.findAll({
|
let chats = await ChatAssociation.findAll({
|
||||||
|
@ -746,6 +831,8 @@ router.post("/friends", auth, async (req, res, next) => {
|
||||||
friendId: req.user.id,
|
friendId: req.user.id,
|
||||||
status: "pendingCanAccept"
|
status: "pendingCanAccept"
|
||||||
})
|
})
|
||||||
|
io.to(user.id).emit("friendUpdate", {})
|
||||||
|
io.to(req.user.id).emit("friendUpdate", {})
|
||||||
io.to(user.id).emit("friendRequest", {
|
io.to(user.id).emit("friendRequest", {
|
||||||
...remoteFriend.dataValues,
|
...remoteFriend.dataValues,
|
||||||
user: {
|
user: {
|
||||||
|
@ -768,6 +855,7 @@ router.post("/friends", auth, async (req, res, next) => {
|
||||||
|
|
||||||
router.delete("/friends/:id", auth, async (req, res, next) => {
|
router.delete("/friends/:id", auth, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
const io = req.app.get("io")
|
||||||
const friend = await Friend.findOne({
|
const friend = await Friend.findOne({
|
||||||
where: {
|
where: {
|
||||||
userId: req.user.id,
|
userId: req.user.id,
|
||||||
|
@ -782,6 +870,8 @@ router.delete("/friends/:id", auth, async (req, res, next) => {
|
||||||
friendId: req.user.id
|
friendId: req.user.id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
io.to(friend.friendId).emit("friendUpdate", {})
|
||||||
|
io.to(req.user.id).emit("friendUpdate", {})
|
||||||
res.sendStatus(204)
|
res.sendStatus(204)
|
||||||
} else {
|
} else {
|
||||||
throw Errors.friendNotFound
|
throw Errors.friendNotFound
|
||||||
|
@ -826,9 +916,8 @@ router.put("/friends/:id", auth, async (req, res, next) => {
|
||||||
await remoteFriend.update({
|
await remoteFriend.update({
|
||||||
status: "accepted"
|
status: "accepted"
|
||||||
})
|
})
|
||||||
io.to(req.user.id).emit("friendAccepted", {
|
io.to(friend.userId).emit("friendUpdate", {})
|
||||||
...friend.dataValues
|
io.to(remoteFriend.userId).emit("friendUpdate", {})
|
||||||
})
|
|
||||||
io.to(remoteFriend.userId).emit("friendAccepted", {
|
io.to(remoteFriend.userId).emit("friendAccepted", {
|
||||||
...remoteFriend.dataValues
|
...remoteFriend.dataValues
|
||||||
})
|
})
|
||||||
|
@ -1004,6 +1093,13 @@ router.put("/:id", auth, async (req, res, next) => {
|
||||||
name: req.body.name
|
name: req.body.name
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
await createMessage(
|
||||||
|
req,
|
||||||
|
"rename",
|
||||||
|
`${req.user.username} renamed the chat to ${req.body.name}`,
|
||||||
|
association,
|
||||||
|
req.user.id
|
||||||
|
)
|
||||||
res.sendStatus(204)
|
res.sendStatus(204)
|
||||||
} else {
|
} else {
|
||||||
throw Errors.chatNotFoundOrNotAdmin
|
throw Errors.chatNotFoundOrNotAdmin
|
||||||
|
|
|
@ -199,6 +199,9 @@ router.post("/register", async (req, res, next) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
if (!JSON.parse(process.env.ALLOW_REGISTRATIONS)) {
|
||||||
|
throw Errors.registrationsDisabled
|
||||||
|
}
|
||||||
if (req.body.password.length < 8) {
|
if (req.body.password.length < 8) {
|
||||||
throw Errors.invalidPassword
|
throw Errors.invalidPassword
|
||||||
}
|
}
|
||||||
|
@ -211,7 +214,7 @@ router.post("/register", async (req, res, next) => {
|
||||||
name: req.body.username,
|
name: req.body.username,
|
||||||
admin: false,
|
admin: false,
|
||||||
email: req.body.email,
|
email: req.body.email,
|
||||||
font: "Roboto",
|
font: "Inter",
|
||||||
status: "offline",
|
status: "offline",
|
||||||
storedStatus: "online",
|
storedStatus: "online",
|
||||||
experiments: [],
|
experiments: [],
|
||||||
|
@ -230,9 +233,7 @@ router.post("/register", async (req, res, next) => {
|
||||||
router.get("/", auth, (req, res, next) => {
|
router.get("/", auth, (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
res.json(req.user)
|
res.json(req.user)
|
||||||
} catch (e) {
|
} catch {}
|
||||||
console.log(1)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get("/sessions", auth, async (req, res, next) => {
|
router.get("/sessions", auth, async (req, res, next) => {
|
||||||
|
|
1426
backend/yarn.lock
1426
backend/yarn.lock
File diff suppressed because it is too large
Load diff
201
src/App.vue
201
src/App.vue
|
@ -506,6 +506,108 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
registerSocket() {
|
||||||
|
if (!this.$store.state.wsRegistered) {
|
||||||
|
this.$store.state.wsRegistered = true
|
||||||
|
this.$store.dispatch("getCommunicationsUnread")
|
||||||
|
this.$socket.on("friendAccepted", (message) => {
|
||||||
|
this.$notification.show(
|
||||||
|
message.user.username,
|
||||||
|
{
|
||||||
|
body:
|
||||||
|
message.user2.username + " has accepted your friend request",
|
||||||
|
icon: message.user2.avatar
|
||||||
|
? "/usercontent/" + message.user2.avatar
|
||||||
|
: null
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
new Audio(require("@/assets/audio/message.wav")).play()
|
||||||
|
this.$toast.success(
|
||||||
|
"Friend request accepted by " + message.user2.username
|
||||||
|
)
|
||||||
|
})
|
||||||
|
this.$socket.on("message", (message) => {
|
||||||
|
this.$store.state.communicationNotifications += 1
|
||||||
|
this.$store.state.chats.find(
|
||||||
|
(chat) => chat.id === message.associationId
|
||||||
|
).unread += 1
|
||||||
|
if (
|
||||||
|
(message.userId !== this.$store.state.user.id &&
|
||||||
|
this.$route.params?.id !== message.associationId &&
|
||||||
|
this.$store.state.user?.storedStatus !== "busy") ||
|
||||||
|
(message.userId !== this.$store.state.user.id &&
|
||||||
|
this.$store.state.user?.storedStatus !== "busy" &&
|
||||||
|
!document.hasFocus())
|
||||||
|
) {
|
||||||
|
if (localStorage.getItem("messageAudio")) {
|
||||||
|
if (JSON.parse(localStorage.getItem("messageAudio"))) {
|
||||||
|
new Audio(require("@/assets/audio/message.wav")).play()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
new Audio(require("@/assets/audio/message.wav")).play()
|
||||||
|
}
|
||||||
|
this.$notification.show(
|
||||||
|
message.user.username + " (" + message.chat.name + ")",
|
||||||
|
{
|
||||||
|
body: message.content,
|
||||||
|
icon: message.user.avatar
|
||||||
|
? "/usercontent/" + message.user.avatar
|
||||||
|
: null
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
this.$toast.info(
|
||||||
|
"Message: " +
|
||||||
|
message.content +
|
||||||
|
"\n\n" +
|
||||||
|
"From: " +
|
||||||
|
message.user.username +
|
||||||
|
"\n" +
|
||||||
|
"Sent in: " +
|
||||||
|
message.chat.name,
|
||||||
|
{
|
||||||
|
onClick: () => {
|
||||||
|
this.$router.push("/communications/" + message.associationId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (this.$store.state.user.storedStatus !== "busy") {
|
||||||
|
this.$socket.on("friendRequest", (message) => {
|
||||||
|
this.$notification.show(
|
||||||
|
message.user.username,
|
||||||
|
{
|
||||||
|
body: message.user.username + " has sent a friend request",
|
||||||
|
icon: message.user.avatar
|
||||||
|
? "/usercontent/" + message.user.avatar
|
||||||
|
: null
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
new Audio(require("@/assets/audio/message.wav")).play()
|
||||||
|
this.$toast.info("Friend request sent by " + message.user.username)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.$store.commit("setWSConnected", true)
|
||||||
|
this.$socket.on("disconnect", () => {
|
||||||
|
this.$store.commit("setWSConnected", false)
|
||||||
|
})
|
||||||
|
this.$socket.on("connect", () => {
|
||||||
|
this.$store.commit("setWSConnected", true)
|
||||||
|
})
|
||||||
|
this.$socket.on("siteState", (state) => {
|
||||||
|
this.$store.state.site.latestVersion = state.latestVersion
|
||||||
|
this.$store.state.site.notification = state.notification
|
||||||
|
this.$store.state.site.notificationType = state.notificationType
|
||||||
|
})
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
this.$store.dispatch("updateQuickSwitch")
|
||||||
|
} else {
|
||||||
|
console.info("Socket already registered.")
|
||||||
|
}
|
||||||
|
},
|
||||||
communicationsIdleCheck() {
|
communicationsIdleCheck() {
|
||||||
let time
|
let time
|
||||||
let idle = false
|
let idle = false
|
||||||
|
@ -652,103 +754,18 @@ export default {
|
||||||
this.$socket.connect()
|
this.$socket.connect()
|
||||||
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 || "Colubrina"
|
||||||
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.$store.dispatch("getState")
|
this.$store.dispatch("getState")
|
||||||
this.$store.dispatch("checkAuth").catch(() => {
|
|
||||||
this.$store.dispatch("logout")
|
|
||||||
this.$router.push("/login")
|
|
||||||
})
|
|
||||||
this.getThemes()
|
this.getThemes()
|
||||||
this.$store
|
this.communicationsIdleCheck()
|
||||||
.dispatch("getUserInfo")
|
this.$store.dispatch("getUserInfo").catch(() => {
|
||||||
.then(() => {
|
if (!["/login", "/register"].includes(this.$route.path)) {
|
||||||
this.communicationsIdleCheck()
|
|
||||||
this.$store.dispatch("getCommunicationsUnread")
|
|
||||||
this.$socket.on("message", (message) => {
|
|
||||||
this.$store.state.communicationNotifications += 1
|
|
||||||
if (
|
|
||||||
(this.$route.name !== "Communications" &&
|
|
||||||
this.$store.state.user.storedStatus !== "busy") ||
|
|
||||||
(this.$route.name === "Communications" &&
|
|
||||||
!document.hasFocus() &&
|
|
||||||
this.$store.state.user.storedStatus !== "busy")
|
|
||||||
) {
|
|
||||||
if (localStorage.getItem("messageAudio")) {
|
|
||||||
if (JSON.parse(localStorage.getItem("messageAudio"))) {
|
|
||||||
new Audio(require("@/assets/audio/message.wav")).play()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
new Audio(require("@/assets/audio/message.wav")).play()
|
|
||||||
}
|
|
||||||
this.$notification.show(
|
|
||||||
message.user.username + " (" + message.chat.name + ")",
|
|
||||||
{
|
|
||||||
body: message.content,
|
|
||||||
icon: message.user.avatar
|
|
||||||
? "/usercontent/" + message.user.avatar
|
|
||||||
: null
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
this.$toast.info(
|
|
||||||
"Message: " +
|
|
||||||
message.content +
|
|
||||||
"\n\n" +
|
|
||||||
"From: " +
|
|
||||||
message.user.username +
|
|
||||||
"\n" +
|
|
||||||
"Sent in: " +
|
|
||||||
message.chat.name,
|
|
||||||
{
|
|
||||||
onClick: () => {
|
|
||||||
this.$router.push("/communications/" + message.associationId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (this.$store.state.user.storedStatus !== "busy") {
|
|
||||||
this.$socket.on("friendRequest", (message) => {
|
|
||||||
this.$notification.show(
|
|
||||||
message.user.username,
|
|
||||||
{
|
|
||||||
body: message.user.username + " has sent a friend request",
|
|
||||||
icon: message.user.avatar
|
|
||||||
? "/usercontent/" + message.user.avatar
|
|
||||||
: null
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
new Audio(require("@/assets/audio/message.wav")).play()
|
|
||||||
this.$toast.info("Friend request sent by " + message.user.username)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.$store.commit("setWSConnected", true)
|
|
||||||
this.$socket.on("disconnect", () => {
|
|
||||||
this.$store.commit("setWSConnected", false)
|
|
||||||
})
|
|
||||||
this.$socket.on("connect", () => {
|
|
||||||
this.$store.commit("setWSConnected", true)
|
|
||||||
})
|
|
||||||
this.$socket.on("siteState", (state) => {
|
|
||||||
this.$store.state.site.latestVersion = state.latestVersion
|
|
||||||
this.$store.state.site.notification = state.notification
|
|
||||||
this.$store.state.site.notificationType = state.notificationType
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
if (JSON.parse(process.env.VUE_APP_MATOMO_ENABLED)) {
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
_paq.push(["setUserId", this.$store.state.user.id])
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
_paq.push(["trackPageView"])
|
|
||||||
}
|
|
||||||
this.$store.dispatch("updateQuickSwitch")
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
this.$router.push("/login")
|
this.$router.push("/login")
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
this.registerSocket()
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
"$store.state.userPanel"(val) {
|
"$store.state.userPanel"(val) {
|
||||||
|
|
1
src/assets/audio/message.wav.LICENSE.txt
Normal file
1
src/assets/audio/message.wav.LICENSE.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Message audio sound from jitsi-meet project, https://github.com/jitsi/jitsi-meet/blob/master/LICENSE
|
|
@ -45,7 +45,7 @@ img.emoji {
|
||||||
}
|
}
|
||||||
|
|
||||||
.troplo-nav {
|
.troplo-nav {
|
||||||
font-family: "Roboto", sans-serif;
|
font-family: "Inter", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.troplo-header {
|
.troplo-header {
|
||||||
|
|
|
@ -165,7 +165,7 @@ export default {
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
},
|
},
|
||||||
handleChange() {
|
handleChange() {
|
||||||
if (this.$store.state.user.bcUser.storedStatus !== "invisible") {
|
if (this.$store.state.user.storedStatus !== "invisible") {
|
||||||
if (this.typingDate) {
|
if (this.typingDate) {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
if (now - this.typingDate > 5000) {
|
if (now - this.typingDate > 5000) {
|
||||||
|
|
|
@ -405,10 +405,11 @@
|
||||||
background-color="toolbar"
|
background-color="toolbar"
|
||||||
style="margin-bottom: -18px"
|
style="margin-bottom: -18px"
|
||||||
elevation="2"
|
elevation="2"
|
||||||
|
v-model="search"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
<v-toolbar color="toolbar" class="rounded-xl mb-3" elevation="2">
|
<v-toolbar color="toolbar" class="rounded-xl mb-3" elevation="2">
|
||||||
<v-toolbar-title class="subtitle-1">
|
<v-toolbar-title class="subtitle-1">
|
||||||
CHATS ({{ $store.state.chats.length }})
|
CHATS ({{ chats.length }})
|
||||||
</v-toolbar-title>
|
</v-toolbar-title>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn icon @click="dialogs.new = true">
|
<v-btn icon @click="dialogs.new = true">
|
||||||
|
@ -416,7 +417,7 @@
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-toolbar></template
|
</v-toolbar></template
|
||||||
>
|
>
|
||||||
<v-list v-for="item in $store.state.chats" :key="item.id">
|
<v-list v-for="item in chats" :key="item.id">
|
||||||
<template>
|
<template>
|
||||||
<v-list-item
|
<v-list-item
|
||||||
:to="'/communications/' + item.id"
|
:to="'/communications/' + item.id"
|
||||||
|
@ -556,7 +557,7 @@
|
||||||
<v-list-item-content
|
<v-list-item-content
|
||||||
@click="copyUsername"
|
@click="copyUsername"
|
||||||
v-bind="attrs"
|
v-bind="attrs"
|
||||||
style="cursor: pointer"
|
style="cursor: pointer; min-width: 100px"
|
||||||
>
|
>
|
||||||
<v-list-item-title>
|
<v-list-item-title>
|
||||||
{{ $store.state.user.username }}
|
{{ $store.state.user.username }}
|
||||||
|
@ -569,6 +570,15 @@
|
||||||
<span>Copied!</span>
|
<span>Copied!</span>
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
text
|
||||||
|
to="/admin"
|
||||||
|
v-if="$store.state.user.admin"
|
||||||
|
style="margin-right: 0; padding-right: 0"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-gavel</v-icon>
|
||||||
|
</v-btn>
|
||||||
<v-btn icon text to="/settings">
|
<v-btn icon text to="/settings">
|
||||||
<v-icon>mdi-cog</v-icon>
|
<v-icon>mdi-cog</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
@ -589,6 +599,7 @@ export default {
|
||||||
components: { NicknameDialog },
|
components: { NicknameDialog },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
search: "",
|
||||||
nickname: {
|
nickname: {
|
||||||
dialog: false,
|
dialog: false,
|
||||||
nickname: "",
|
nickname: "",
|
||||||
|
@ -644,6 +655,23 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
chats() {
|
||||||
|
if (!this.search?.length) {
|
||||||
|
return this.$store.state.chats
|
||||||
|
}
|
||||||
|
return this.$store.state.chats.filter((item) => {
|
||||||
|
return (
|
||||||
|
(item.chat.type === "group" &&
|
||||||
|
item.chat.name.toLowerCase().includes(this.search.toLowerCase())) ||
|
||||||
|
(item.chat.type === "direct" &&
|
||||||
|
this.getDirectRecipient(item)
|
||||||
|
.name.toLowerCase()
|
||||||
|
.includes(this.search.toLowerCase()))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
groupSettings(id) {
|
groupSettings(id) {
|
||||||
this.settings.item = this.$store.state.chats.find(
|
this.settings.item = this.$store.state.chats.find(
|
||||||
|
@ -875,14 +903,15 @@ export default {
|
||||||
createConversation() {
|
createConversation() {
|
||||||
this.newConversation.loading = true
|
this.newConversation.loading = true
|
||||||
this.axios
|
this.axios
|
||||||
.post("/api/v1/communications/create", {
|
.post(process.env.VUE_APP_BASE_URL + "/api/v1/communications/create", {
|
||||||
users: this.newConversation.users
|
users: this.newConversation.users
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then((res) => {
|
||||||
this.newConversation.name = ""
|
this.newConversation.name = ""
|
||||||
this.newConversation.users = []
|
this.newConversation.users = []
|
||||||
this.newConversation.loading = false
|
this.newConversation.loading = false
|
||||||
this.newConversation.results = []
|
this.dialogs.new = false
|
||||||
|
this.$router.push("/communications/" + res.data.id)
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
this.newConversation.loading = false
|
this.newConversation.loading = false
|
||||||
|
@ -927,6 +956,10 @@ export default {
|
||||||
this.searchUsers()
|
this.searchUsers()
|
||||||
this.searchUsersForGroupAdmin()
|
this.searchUsersForGroupAdmin()
|
||||||
this.$store.dispatch("getChats")
|
this.$store.dispatch("getChats")
|
||||||
|
this.$socket.on("friendUpdate", () => {
|
||||||
|
this.searchUsers()
|
||||||
|
this.searchUsersForGroupAdmin()
|
||||||
|
})
|
||||||
this.$socket.on("userSettings", () => {
|
this.$socket.on("userSettings", () => {
|
||||||
this.$store.dispatch("getChats")
|
this.$store.dispatch("getChats")
|
||||||
})
|
})
|
||||||
|
|
197
src/components/UserDialog.vue
Normal file
197
src/components/UserDialog.vue
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
<template>
|
||||||
|
<v-dialog v-model="user.value" max-width="650px">
|
||||||
|
<v-card v-if="user.item" class="user-popout" height="600px">
|
||||||
|
<v-toolbar color="toolbar" height="100">
|
||||||
|
<v-avatar size="64" class="mr-3 mb-2 mt-2">
|
||||||
|
<v-img
|
||||||
|
:src="$store.state.baseURL + '/usercontent/' + user.item.avatar"
|
||||||
|
v-if="user.item.avatar"
|
||||||
|
class="elevation-1"
|
||||||
|
/>
|
||||||
|
<v-icon v-else class="elevation-1"> mdi-account </v-icon>
|
||||||
|
</v-avatar>
|
||||||
|
<v-toolbar-title>
|
||||||
|
{{ getName(user.item) }}
|
||||||
|
<v-tooltip top v-if="user.item.admin">
|
||||||
|
<template v-slot:activator="{ on }">
|
||||||
|
<v-btn icon v-on="on" small>
|
||||||
|
<v-icon> mdi-crown </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<span>Colubrina Instance Administrator</span>
|
||||||
|
</v-tooltip>
|
||||||
|
<v-tooltip top v-if="user.item.id < 35">
|
||||||
|
<template v-slot:activator="{ on }">
|
||||||
|
<v-btn icon v-on="on" small>
|
||||||
|
<v-icon> mdi-alpha-a-circle </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<span>Early User</span>
|
||||||
|
</v-tooltip>
|
||||||
|
<v-tooltip top v-if="user.item.bot">
|
||||||
|
<template v-slot:activator="{ on }">
|
||||||
|
<v-btn icon v-on="on" small>
|
||||||
|
<v-icon> mdi-robot </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<span>Bot</span>
|
||||||
|
</v-tooltip>
|
||||||
|
<div class="subheading subtitle-1 text--lighten-2">
|
||||||
|
<template v-if="user.item.nickname"
|
||||||
|
>{{ user.item.username }}:</template
|
||||||
|
>{{ user.item.instance }}
|
||||||
|
</div>
|
||||||
|
</v-toolbar-title>
|
||||||
|
</v-toolbar>
|
||||||
|
<v-tabs :show-arrows="false" fixed-tabs background-color="card">
|
||||||
|
<v-tab>
|
||||||
|
<v-icon>mdi-account-multiple</v-icon> Mutual Friends
|
||||||
|
</v-tab>
|
||||||
|
<v-tab> <v-icon>mdi-account-group</v-icon> Mutual Groups </v-tab>
|
||||||
|
<v-tab-item
|
||||||
|
:style="
|
||||||
|
'background-color: ' +
|
||||||
|
$vuetify.theme.themes[$vuetify.theme.dark ? 'dark' : 'light'].card
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-list
|
||||||
|
:height="400"
|
||||||
|
:style="
|
||||||
|
'background-color: ' +
|
||||||
|
$vuetify.theme.themes[$vuetify.theme.dark ? 'dark' : 'light'].card
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-overlay :value="loading.mutualFriends" absolute>
|
||||||
|
<v-progress-circular
|
||||||
|
indeterminate
|
||||||
|
size="64"
|
||||||
|
></v-progress-circular>
|
||||||
|
</v-overlay>
|
||||||
|
<v-list-item
|
||||||
|
v-for="item in mutualFriends"
|
||||||
|
:key="item.id"
|
||||||
|
@click="openUserPanel(item)"
|
||||||
|
>
|
||||||
|
<v-list-item-title>
|
||||||
|
<v-avatar size="24">
|
||||||
|
<v-img
|
||||||
|
:src="$store.state.baseURL + '/usercontent/' + item.avatar"
|
||||||
|
v-if="item.avatar"
|
||||||
|
class="elevation-1"
|
||||||
|
/>
|
||||||
|
<v-icon v-else class="elevation-1"> mdi-account </v-icon>
|
||||||
|
</v-avatar>
|
||||||
|
{{ getName(item) }}
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-tab-item>
|
||||||
|
<v-tab-item
|
||||||
|
:style="
|
||||||
|
'background-color: ' +
|
||||||
|
$vuetify.theme.themes[$vuetify.theme.dark ? 'dark' : 'light'].card
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-list
|
||||||
|
:height="400"
|
||||||
|
:style="
|
||||||
|
'background-color: ' +
|
||||||
|
$vuetify.theme.themes[$vuetify.theme.dark ? 'dark' : 'light'].card
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-overlay :value="loading.mutualFriends" absolute>
|
||||||
|
<v-progress-circular
|
||||||
|
indeterminate
|
||||||
|
size="64"
|
||||||
|
></v-progress-circular>
|
||||||
|
</v-overlay>
|
||||||
|
<v-list-item
|
||||||
|
v-for="item in mutualGroups"
|
||||||
|
:key="item.id"
|
||||||
|
@click="$router.push('/communications/' + item.associationId)"
|
||||||
|
>
|
||||||
|
<v-list-item-title>
|
||||||
|
{{ item.name }}
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-tab-item>
|
||||||
|
</v-tabs>
|
||||||
|
<v-card-actions
|
||||||
|
:style="
|
||||||
|
'background-color: ' +
|
||||||
|
$vuetify.theme.themes[$vuetify.theme.dark ? 'dark' : 'light'].card
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="primary" text @click="user.value = false"> Close </v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import AjaxErrorHandler from "@/lib/errorHandler"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "UserDialog",
|
||||||
|
props: ["user"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
mutualGroups: [],
|
||||||
|
mutualFriends: [],
|
||||||
|
loading: {
|
||||||
|
mutualFriends: true,
|
||||||
|
mutualGroups: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getName(user) {
|
||||||
|
if (user.nickname?.nickname) {
|
||||||
|
return user.nickname.nickname
|
||||||
|
} else {
|
||||||
|
return user.username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.mutualGroups = []
|
||||||
|
this.mutualFriends = []
|
||||||
|
this.loading = {
|
||||||
|
mutualGroups: true,
|
||||||
|
mutualFriends: true
|
||||||
|
}
|
||||||
|
this.axios
|
||||||
|
.get(
|
||||||
|
process.env.VUE_APP_BASE_URL +
|
||||||
|
"/api/v1/communications/mutual/" +
|
||||||
|
this.user.item.id +
|
||||||
|
"/groups"
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
this.mutualGroups = res.data
|
||||||
|
this.loading.mutualGroups = false
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
AjaxErrorHandler(this.$store)(e)
|
||||||
|
})
|
||||||
|
this.axios
|
||||||
|
.get(
|
||||||
|
process.env.VUE_APP_BASE_URL +
|
||||||
|
"/api/v1/communications/mutual/" +
|
||||||
|
this.user.item.id +
|
||||||
|
"/friends"
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
this.mutualFriends = res.data
|
||||||
|
this.loading.mutualFriends = false
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
AjaxErrorHandler(this.$store)(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
116
src/main.js
116
src/main.js
|
@ -13,9 +13,6 @@ import VueSanitize from "vue-sanitize"
|
||||||
import "@mdi/font/css/materialdesignicons.css"
|
import "@mdi/font/css/materialdesignicons.css"
|
||||||
import "./plugins/dayjs"
|
import "./plugins/dayjs"
|
||||||
import VueApollo from "./plugins/apollo"
|
import VueApollo from "./plugins/apollo"
|
||||||
import VueMatomo from "vue-matomo"
|
|
||||||
import * as Sentry from "@sentry/vue"
|
|
||||||
import { BrowserTracing } from "@sentry/tracing"
|
|
||||||
import SocketIO from "socket.io-client"
|
import SocketIO from "socket.io-client"
|
||||||
import twemoji from "twemoji"
|
import twemoji from "twemoji"
|
||||||
import VueNativeNotification from "vue-native-notification"
|
import VueNativeNotification from "vue-native-notification"
|
||||||
|
@ -72,25 +69,6 @@ md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
|
||||||
// pass token to default renderer.
|
// pass token to default renderer.
|
||||||
return defaultRender(tokens, idx, options, env, self)
|
return defaultRender(tokens, idx, options, env, self)
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
process.env.NODE_ENV === "production" &&
|
|
||||||
JSON.parse(process.env.VUE_APP_SENTRY_ENABLED)
|
|
||||||
) {
|
|
||||||
Sentry.init({
|
|
||||||
Vue,
|
|
||||||
dsn: process.env.VUE_APP_SENTRY_DSN,
|
|
||||||
integrations: [
|
|
||||||
new BrowserTracing({
|
|
||||||
routingInstrumentation: Sentry.vueRouterInstrumentation(router),
|
|
||||||
tracingOrigins: ["bettercompass.com.au", "compass.troplo.com", /^\//]
|
|
||||||
})
|
|
||||||
],
|
|
||||||
// Set tracesSampleRate to 1.0 to capture 100%
|
|
||||||
// of transactions for performance monitoring.
|
|
||||||
// We recommend adjusting this value in production
|
|
||||||
tracesSampleRate: 1.0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.use(VueNativeNotification, {
|
Vue.use(VueNativeNotification, {
|
||||||
requestOnNotify: false
|
requestOnNotify: false
|
||||||
|
@ -106,100 +84,6 @@ Vue.use({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (JSON.parse(process.env.VUE_APP_MATOMO_ENABLED)) {
|
|
||||||
Vue.use(VueMatomo, {
|
|
||||||
// Configure your matomo server and site by providing
|
|
||||||
host: process.env.VUE_APP_MATOMO_URL,
|
|
||||||
siteId: process.env.VUE_APP_MATOMO_SITE_ID,
|
|
||||||
|
|
||||||
// Changes the default .js and .php endpoint's filename
|
|
||||||
// Default: 'matomo'
|
|
||||||
trackerFileName: process.env.VUE_APP_MATOMO_TRACKER,
|
|
||||||
|
|
||||||
// Overrides the autogenerated tracker endpoint entirely
|
|
||||||
// Default: undefined
|
|
||||||
// trackerUrl: 'https://example.com/whatever/endpoint/you/have',
|
|
||||||
|
|
||||||
// Overrides the autogenerated tracker script path entirely
|
|
||||||
// Default: undefined
|
|
||||||
// trackerScriptUrl: 'https://example.com/whatever/script/path/you/have',
|
|
||||||
|
|
||||||
// Enables automatically registering pageviews on the router
|
|
||||||
router: router,
|
|
||||||
|
|
||||||
// Enables link tracking on regular links. Note that this won't
|
|
||||||
// work for routing links (ie. internal Vue router links)
|
|
||||||
// Default: true
|
|
||||||
enableLinkTracking: true,
|
|
||||||
|
|
||||||
// Require consent before sending tracking information to matomo
|
|
||||||
// Default: false
|
|
||||||
requireConsent: false,
|
|
||||||
|
|
||||||
// Whether to track the initial page view
|
|
||||||
// Default: true
|
|
||||||
trackInitialView: true,
|
|
||||||
|
|
||||||
// Run Matomo without cookies
|
|
||||||
// Default: false
|
|
||||||
disableCookies: false,
|
|
||||||
|
|
||||||
// Require consent before creating matomo session cookie
|
|
||||||
// Default: false
|
|
||||||
requireCookieConsent: false,
|
|
||||||
|
|
||||||
// Enable the heartbeat timer (https://developer.matomo.org/guides/tracking-javascript-guide#accurately-measure-the-time-spent-on-each-page)
|
|
||||||
// Default: false
|
|
||||||
enableHeartBeatTimer: false,
|
|
||||||
|
|
||||||
// Set the heartbeat timer interval
|
|
||||||
// Default: 15
|
|
||||||
heartBeatTimerInterval: 15,
|
|
||||||
|
|
||||||
// Whether or not to log debug information
|
|
||||||
// Default: false
|
|
||||||
debug: false,
|
|
||||||
|
|
||||||
// UserID passed to Matomo (see https://developer.matomo.org/guides/tracking-javascript-guide#user-id)
|
|
||||||
// Default: undefined
|
|
||||||
userId: undefined,
|
|
||||||
|
|
||||||
// Share the tracking cookie across subdomains (see https://developer.matomo.org/guides/tracking-javascript-guide#measuring-domains-andor-sub-domains)
|
|
||||||
// Default: undefined, example '*.example.com'
|
|
||||||
cookieDomain: undefined,
|
|
||||||
|
|
||||||
// Tell Matomo the website domain so that clicks on these domains are not tracked as 'Outlinks'
|
|
||||||
// Default: undefined, example: '*.example.com'
|
|
||||||
domains: process.env.VUE_APP_MATOMO_DOMAINS,
|
|
||||||
|
|
||||||
// A list of pre-initialization actions that run before matomo is loaded
|
|
||||||
// Default: []
|
|
||||||
// Example: [
|
|
||||||
// ['API_method_name', parameter_list],
|
|
||||||
// ['setCustomVariable','1','VisitorType','Member'],
|
|
||||||
// ['appendToTrackingUrl', 'new_visit=1'],
|
|
||||||
// etc.
|
|
||||||
// ]
|
|
||||||
preInitActions: [],
|
|
||||||
|
|
||||||
// A function to determine whether to track an interaction as a site search
|
|
||||||
// instead of as a page view. If not a function, all interactions will be
|
|
||||||
// tracked as page views. Receives the new route as an argument, and
|
|
||||||
// returns either an object of keyword, category (optional) and resultsCount
|
|
||||||
// (optional) to track as a site search, or a falsey value to track as a page
|
|
||||||
// view.
|
|
||||||
// Default: false, i.e. track all interactions as page views
|
|
||||||
// Example: (to) => {
|
|
||||||
// if (to.query.q && to.name === 'search') {
|
|
||||||
// return { keyword: to.query.q, category: to.params.category }
|
|
||||||
// } else {
|
|
||||||
// return null
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
trackSiteSearch: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.use(VueSanitize, {
|
Vue.use(VueSanitize, {
|
||||||
allowedTags: [
|
allowedTags: [
|
||||||
"address",
|
"address",
|
||||||
|
|
|
@ -15,7 +15,7 @@ const routes = [
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "friends",
|
path: "friends",
|
||||||
name: "Friends - Communications",
|
name: "Friends",
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
/* webpackChunkName: "communicationsFriends" */ "../views/Communications/CommunicationsFriends"
|
/* webpackChunkName: "communicationsFriends" */ "../views/Communications/CommunicationsFriends"
|
||||||
|
@ -85,7 +85,7 @@ const routes = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "communications",
|
path: "communications",
|
||||||
name: "Communications",
|
name: "Communications Settings",
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
/* webpackChunkName: "settingsCommunications" */ "../views/Settings/SettingsCommunications"
|
/* webpackChunkName: "settingsCommunications" */ "../views/Settings/SettingsCommunications"
|
||||||
|
@ -116,14 +116,6 @@ const routes = [
|
||||||
/* webpackChunkName: "adminUsers" */ "../views/Admin/AdminUsers.vue"
|
/* webpackChunkName: "adminUsers" */ "../views/Admin/AdminUsers.vue"
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "feedback",
|
|
||||||
name: "Feedback",
|
|
||||||
component: () =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "adminFeedback" */ "../views/Admin/AdminFeedback.vue"
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "themes",
|
path: "themes",
|
||||||
name: "Themes",
|
name: "Themes",
|
||||||
|
@ -166,4 +158,11 @@ const router = new VueRouter({
|
||||||
routes
|
routes
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const originalPush = router.push
|
||||||
|
router.push = function push(location, onResolve, onReject) {
|
||||||
|
if (onResolve || onReject)
|
||||||
|
return originalPush.call(this, location, onResolve, onReject)
|
||||||
|
return originalPush.call(this, location).catch((err) => err)
|
||||||
|
}
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
|
@ -60,6 +60,7 @@ export default new Vuex.Store({
|
||||||
},
|
},
|
||||||
communicationNotifications: 0,
|
communicationNotifications: 0,
|
||||||
wsConnected: false,
|
wsConnected: false,
|
||||||
|
wsRegistered: false,
|
||||||
lastChat: "friends",
|
lastChat: "friends",
|
||||||
searchPanel: false,
|
searchPanel: false,
|
||||||
userPanel: false
|
userPanel: false
|
||||||
|
@ -258,7 +259,7 @@ export default new Vuex.Store({
|
||||||
document.head.appendChild(style)
|
document.head.appendChild(style)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
checkAuth(context) {
|
checkAuth() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
Vue.axios.defaults.headers.common["Authorization"] =
|
Vue.axios.defaults.headers.common["Authorization"] =
|
||||||
localStorage.getItem("session")
|
localStorage.getItem("session")
|
||||||
|
@ -270,7 +271,6 @@ export default new Vuex.Store({
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
if (e?.response?.status === 401) {
|
if (e?.response?.status === 401) {
|
||||||
reject(false)
|
reject(false)
|
||||||
context.dispatch("logout")
|
|
||||||
} else {
|
} else {
|
||||||
AjaxErrorHandler(this.$store)(e)
|
AjaxErrorHandler(this.$store)(e)
|
||||||
}
|
}
|
||||||
|
@ -321,7 +321,7 @@ export default new Vuex.Store({
|
||||||
context.commit("updateQuickSwitchCache", {
|
context.commit("updateQuickSwitchCache", {
|
||||||
subjectLongName: "Home",
|
subjectLongName: "Home",
|
||||||
customType: 1,
|
customType: 1,
|
||||||
route: "/"
|
route: "/communications/friends"
|
||||||
})
|
})
|
||||||
context.state.chats.forEach((chat) => {
|
context.state.chats.forEach((chat) => {
|
||||||
context.commit("updateQuickSwitchCache", {
|
context.commit("updateQuickSwitchCache", {
|
||||||
|
@ -417,7 +417,6 @@ div {
|
||||||
resolve(res.data)
|
resolve(res.data)
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.log(e)
|
|
||||||
if (JSON.parse(localStorage.getItem("userCache"))?.id) {
|
if (JSON.parse(localStorage.getItem("userCache"))?.id) {
|
||||||
const user = JSON.parse(localStorage.getItem("userCache"))
|
const user = JSON.parse(localStorage.getItem("userCache"))
|
||||||
const name = user.themeObject.id
|
const name = user.themeObject.id
|
||||||
|
@ -475,12 +474,7 @@ div {
|
||||||
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",
|
||||||
|
@ -495,12 +489,7 @@ div {
|
||||||
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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const name = theme.id
|
const name = theme.id
|
||||||
|
|
|
@ -17,10 +17,6 @@
|
||||||
<v-icon>mdi-account-multiple</v-icon>
|
<v-icon>mdi-account-multiple</v-icon>
|
||||||
<span>Users</span>
|
<span>Users</span>
|
||||||
</v-tab>
|
</v-tab>
|
||||||
<v-tab to="/admin/feedback">
|
|
||||||
<v-icon>mdi-bug</v-icon>
|
|
||||||
<span>Feedback</span>
|
|
||||||
</v-tab>
|
|
||||||
<v-tab to="/admin/themes">
|
<v-tab to="/admin/themes">
|
||||||
<v-icon>mdi-brush</v-icon>
|
<v-icon>mdi-brush</v-icon>
|
||||||
<span>Themes</span>
|
<span>Themes</span>
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
<template>
|
|
||||||
<div id="admin-feedback">
|
|
||||||
<v-toolbar color="toolbar">
|
|
||||||
<v-toolbar-title>Feedback ({{ feedback.count }})</v-toolbar-title>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-btn @click="getFeedback" icon>
|
|
||||||
<v-icon>mdi-refresh</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-toolbar>
|
|
||||||
<v-container>
|
|
||||||
<v-card
|
|
||||||
v-for="item in feedback.rows"
|
|
||||||
:key="item.id"
|
|
||||||
class="rounded-xl mb-2"
|
|
||||||
color="card"
|
|
||||||
>
|
|
||||||
<v-toolbar color="toolbar">
|
|
||||||
<v-toolbar-title>
|
|
||||||
Feedback -
|
|
||||||
{{
|
|
||||||
item.user
|
|
||||||
? item.user?.displayCode + ` (BCID: ${item.user?.id})`
|
|
||||||
: item.userId || "Unknown"
|
|
||||||
}}
|
|
||||||
</v-toolbar-title>
|
|
||||||
</v-toolbar>
|
|
||||||
<v-container>
|
|
||||||
{{ item.feedbackText.substring(0, 1000) }}
|
|
||||||
<br />
|
|
||||||
<small v-if="item.user"> User ID: {{ item.user.id }} </small>
|
|
||||||
</v-container>
|
|
||||||
</v-card>
|
|
||||||
</v-container>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: "AdminFeedback",
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
feedback: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getFeedback() {
|
|
||||||
this.axios.get("/api/v1/admin/feedback").then((res) => {
|
|
||||||
this.feedback = res.data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.getFeedback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -26,18 +26,6 @@
|
||||||
</v-container>
|
</v-container>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col>
|
|
||||||
<v-card color="card" class="rounded-xl">
|
|
||||||
<v-toolbar color="toolbar">
|
|
||||||
<v-toolbar-title> Feedback </v-toolbar-title>
|
|
||||||
</v-toolbar>
|
|
||||||
<v-container>
|
|
||||||
<h1 class="text-center" style="font-size: 69px">
|
|
||||||
{{ admin.feedback }}
|
|
||||||
</h1>
|
|
||||||
</v-container>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
<v-col>
|
<v-col>
|
||||||
<v-card color="card" class="rounded-xl">
|
<v-card color="card" class="rounded-xl">
|
||||||
<v-toolbar color="toolbar">
|
<v-toolbar color="toolbar">
|
||||||
|
|
|
@ -26,6 +26,12 @@
|
||||||
text-value="value"
|
text-value="value"
|
||||||
>
|
>
|
||||||
</v-select>
|
</v-select>
|
||||||
|
<v-switch
|
||||||
|
inset
|
||||||
|
class="mx-3"
|
||||||
|
label="Allow registrations"
|
||||||
|
v-model="allowRegistrations"
|
||||||
|
></v-switch>
|
||||||
<v-btn text class="mx-3 mb-3" color="primary" @click="updateState"
|
<v-btn text class="mx-3 mb-3" color="primary" @click="updateState"
|
||||||
>Save</v-btn
|
>Save</v-btn
|
||||||
>
|
>
|
||||||
|
@ -40,6 +46,7 @@ export default {
|
||||||
return {
|
return {
|
||||||
notification: "",
|
notification: "",
|
||||||
notificationType: "info",
|
notificationType: "info",
|
||||||
|
allowRegistrations: true,
|
||||||
notificationTypes: [
|
notificationTypes: [
|
||||||
{ text: "Info", value: "info" },
|
{ text: "Info", value: "info" },
|
||||||
{ text: "Success", value: "success" },
|
{ text: "Success", value: "success" },
|
||||||
|
@ -65,7 +72,8 @@ export default {
|
||||||
.put("/api/v1/admin/state", {
|
.put("/api/v1/admin/state", {
|
||||||
notification: this.notification,
|
notification: this.notification,
|
||||||
notificationType: this.notificationType,
|
notificationType: this.notificationType,
|
||||||
broadcastType: this.broadcastType
|
broadcastType: this.broadcastType,
|
||||||
|
allowRegistrations: this.allowRegistrations
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success("State updated")
|
this.$toast.success("State updated")
|
||||||
|
@ -78,6 +86,7 @@ export default {
|
||||||
mounted() {
|
mounted() {
|
||||||
this.notification = this.$store.state.site.notification
|
this.notification = this.$store.state.site.notification
|
||||||
this.notificationType = this.$store.state.site.notificationType
|
this.notificationType = this.$store.state.site.notificationType
|
||||||
|
this.allowRegistrations = this.$store.state.site.allowRegistrations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -41,21 +41,13 @@ export default {
|
||||||
text: "ID",
|
text: "ID",
|
||||||
value: "id"
|
value: "id"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
text: "Compass User ID",
|
|
||||||
value: "compassUserId"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Sussi Auth ID",
|
|
||||||
value: "sussiId"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
text: "Username",
|
text: "Username",
|
||||||
value: "displayCode"
|
value: "username"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Instance",
|
text: "Email",
|
||||||
value: "instance"
|
value: "email"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Created At",
|
text: "Created At",
|
||||||
|
@ -69,10 +61,6 @@ export default {
|
||||||
text: "Base Theme",
|
text: "Base Theme",
|
||||||
value: "theme"
|
value: "theme"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
text: "Communications",
|
|
||||||
value: "privacy.communications.enabled"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
text: "Theme",
|
text: "Theme",
|
||||||
value: "themeObject.name"
|
value: "themeObject.name"
|
||||||
|
|
|
@ -33,187 +33,11 @@
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
<v-dialog v-model="context.userPopout.value" max-width="650px">
|
<UserDialog
|
||||||
<v-card v-if="context.userPopout.item" class="user-popout" height="600px">
|
:user="context.userPopout"
|
||||||
<v-toolbar color="toolbar" height="100">
|
:key="context.userPopout.item?.id || 0"
|
||||||
<v-avatar size="64" class="mr-3 mb-2 mt-2">
|
></UserDialog>
|
||||||
<v-img
|
<NicknameDialog :nickname="context.nickname" />
|
||||||
:src="
|
|
||||||
$store.state.baseURL +
|
|
||||||
'/usercontent/' +
|
|
||||||
context.userPopout.item.avatar
|
|
||||||
"
|
|
||||||
v-if="context.userPopout.item.avatar"
|
|
||||||
class="elevation-1"
|
|
||||||
/>
|
|
||||||
<v-icon v-else class="elevation-1"> mdi-account </v-icon>
|
|
||||||
</v-avatar>
|
|
||||||
<v-toolbar-title>
|
|
||||||
{{ getName(context.userPopout.item) }}
|
|
||||||
<v-tooltip top v-if="context.userPopout.item.admin">
|
|
||||||
<template v-slot:activator="{ on }">
|
|
||||||
<v-btn icon v-on="on" small>
|
|
||||||
<v-icon> mdi-crown </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
<span>Colubrina Instance Administrator</span>
|
|
||||||
</v-tooltip>
|
|
||||||
<v-tooltip top v-if="context.userPopout.item.id < 35">
|
|
||||||
<template v-slot:activator="{ on }">
|
|
||||||
<v-btn icon v-on="on" small>
|
|
||||||
<v-icon> mdi-alpha-a-circle </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
<span>Early User</span>
|
|
||||||
</v-tooltip>
|
|
||||||
<v-tooltip top v-if="context.userPopout.item.bot">
|
|
||||||
<template v-slot:activator="{ on }">
|
|
||||||
<v-btn icon v-on="on" small>
|
|
||||||
<v-icon> mdi-robot </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
<span>Bot</span>
|
|
||||||
</v-tooltip>
|
|
||||||
<div class="subheading subtitle-1 text--lighten-2">
|
|
||||||
<template v-if="context.userPopout.item.nickname"
|
|
||||||
>{{ context.userPopout.item.username }}:</template
|
|
||||||
>{{ context.userPopout.item.instance }}
|
|
||||||
</div>
|
|
||||||
</v-toolbar-title>
|
|
||||||
</v-toolbar>
|
|
||||||
<v-tabs :show-arrows="false" fixed-tabs background-color="card">
|
|
||||||
<v-tab>
|
|
||||||
<v-icon>mdi-account-multiple</v-icon> Mutual Friends
|
|
||||||
</v-tab>
|
|
||||||
<v-tab>
|
|
||||||
<v-icon>mdi-account-group</v-icon> Mutual Groups
|
|
||||||
</v-tab>
|
|
||||||
<v-tab-item
|
|
||||||
:style="
|
|
||||||
'background-color: ' +
|
|
||||||
$vuetify.theme.themes[$vuetify.theme.dark ? 'dark' : 'light'].card
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<v-list
|
|
||||||
:height="400"
|
|
||||||
:style="
|
|
||||||
'background-color: ' +
|
|
||||||
$vuetify.theme.themes[$vuetify.theme.dark ? 'dark' : 'light']
|
|
||||||
.card
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<v-overlay
|
|
||||||
:value="context.userPopout.item.loading.mutualFriends"
|
|
||||||
absolute
|
|
||||||
>
|
|
||||||
<v-progress-circular
|
|
||||||
indeterminate
|
|
||||||
size="64"
|
|
||||||
></v-progress-circular>
|
|
||||||
</v-overlay>
|
|
||||||
<v-list-item
|
|
||||||
v-for="item in context.userPopout.item.mutualFriends"
|
|
||||||
:key="item.id"
|
|
||||||
@click="openUserPanel(item)"
|
|
||||||
>
|
|
||||||
<v-list-item-title>
|
|
||||||
<v-avatar size="24">
|
|
||||||
<v-img
|
|
||||||
:src="
|
|
||||||
$store.state.baseURL + '/usercontent/' + item.avatar
|
|
||||||
"
|
|
||||||
v-if="item.avatar"
|
|
||||||
class="elevation-1"
|
|
||||||
/>
|
|
||||||
<v-icon v-else class="elevation-1"> mdi-account </v-icon>
|
|
||||||
</v-avatar>
|
|
||||||
{{ getName(item) }}
|
|
||||||
</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-tab-item>
|
|
||||||
<v-tab-item
|
|
||||||
:style="
|
|
||||||
'background-color: ' +
|
|
||||||
$vuetify.theme.themes[$vuetify.theme.dark ? 'dark' : 'light'].card
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<v-list
|
|
||||||
:height="400"
|
|
||||||
:style="
|
|
||||||
'background-color: ' +
|
|
||||||
$vuetify.theme.themes[$vuetify.theme.dark ? 'dark' : 'light']
|
|
||||||
.card
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<v-overlay
|
|
||||||
:value="context.userPopout.item.loading.mutualFriends"
|
|
||||||
absolute
|
|
||||||
>
|
|
||||||
<v-progress-circular
|
|
||||||
indeterminate
|
|
||||||
size="64"
|
|
||||||
></v-progress-circular>
|
|
||||||
</v-overlay>
|
|
||||||
<v-list-item
|
|
||||||
v-for="item in context.userPopout.item.mutualGroups"
|
|
||||||
:key="item.id"
|
|
||||||
@click="$router.push('/communications/' + item.associationId)"
|
|
||||||
>
|
|
||||||
<v-list-item-title>
|
|
||||||
{{ item.name }}
|
|
||||||
</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-tab-item>
|
|
||||||
</v-tabs>
|
|
||||||
<v-card-actions
|
|
||||||
:style="
|
|
||||||
'background-color: ' +
|
|
||||||
$vuetify.theme.themes[$vuetify.theme.dark ? 'dark' : 'light'].card
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-btn color="primary" text @click="context.userPopout.value = false">
|
|
||||||
Close
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-dialog>
|
|
||||||
<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"
|
|
||||||
autofocus
|
|
||||||
></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>
|
|
||||||
<v-dialog
|
<v-dialog
|
||||||
v-model="preview.dialog"
|
v-model="preview.dialog"
|
||||||
elevation="0"
|
elevation="0"
|
||||||
|
@ -906,6 +730,128 @@
|
||||||
</v-list-item-action>
|
</v-list-item-action>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="message.type === 'rename'">
|
||||||
|
<v-list-item :key="message.keyId" :id="'message-' + index">
|
||||||
|
<v-icon color="grey" class="mr-2 ml-1"> mdi-pencil </v-icon>
|
||||||
|
<v-list-item-content>
|
||||||
|
{{ message.content }}
|
||||||
|
</v-list-item-content>
|
||||||
|
<v-list-item-action>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
{{
|
||||||
|
$date(message.createdAt).format("DD/MM/YYYY hh:mm A")
|
||||||
|
}}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
v-if="message.userId === $store.state.user.id"
|
||||||
|
@click="deleteMessage(message)"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-delete </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="
|
||||||
|
edit.content = message.content
|
||||||
|
edit.editing = true
|
||||||
|
edit.id = message.id
|
||||||
|
"
|
||||||
|
v-if="
|
||||||
|
message.userId === $store.state.user.id &&
|
||||||
|
edit.id !== message.id
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-pencil </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="
|
||||||
|
edit.content = ''
|
||||||
|
edit.editing = false
|
||||||
|
edit.id = null
|
||||||
|
"
|
||||||
|
v-if="
|
||||||
|
message.userId === $store.state.user.id &&
|
||||||
|
edit.id === message.id
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-close </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="
|
||||||
|
replying = message
|
||||||
|
focusInput()
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-reply </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item-action>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="message.type === 'system'">
|
||||||
|
<v-list-item :key="message.keyId" :id="'message-' + index">
|
||||||
|
<v-icon color="grey" class="mr-2 ml-1"> mdi-pencil </v-icon>
|
||||||
|
<v-list-item-content>
|
||||||
|
{{ message.content }}
|
||||||
|
</v-list-item-content>
|
||||||
|
<v-list-item-action>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
{{
|
||||||
|
$date(message.createdAt).format("DD/MM/YYYY hh:mm A")
|
||||||
|
}}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
v-if="message.userId === $store.state.user.id"
|
||||||
|
@click="deleteMessage(message)"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-delete </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="
|
||||||
|
edit.content = message.content
|
||||||
|
edit.editing = true
|
||||||
|
edit.id = message.id
|
||||||
|
"
|
||||||
|
v-if="
|
||||||
|
message.userId === $store.state.user.id &&
|
||||||
|
edit.id !== message.id
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-pencil </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="
|
||||||
|
edit.content = ''
|
||||||
|
edit.editing = false
|
||||||
|
edit.id = null
|
||||||
|
"
|
||||||
|
v-if="
|
||||||
|
message.userId === $store.state.user.id &&
|
||||||
|
edit.id === message.id
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-close </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="
|
||||||
|
replying = message
|
||||||
|
focusInput()
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-reply </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item-action>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
@ -965,23 +911,32 @@
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
<v-divider
|
||||||
|
vertical
|
||||||
|
style="z-index: 2; padding-right: 3px; padding-left: 3px"
|
||||||
|
v-if="
|
||||||
|
($store.state.userPanel && !$vuetify.breakpoint.mobile) ||
|
||||||
|
($store.state.searchPanel && !$vuetify.breakpoint.mobile)
|
||||||
|
"
|
||||||
|
></v-divider>
|
||||||
<v-col
|
<v-col
|
||||||
cols="3"
|
cols="3"
|
||||||
class=""
|
class=""
|
||||||
id="search-col"
|
id="search-col"
|
||||||
v-if="searchPanel && !$vuetify.breakpoint.mobile"
|
v-if="$store.state.searchPanel && !$vuetify.breakpoint.mobile"
|
||||||
>
|
>
|
||||||
<v-card
|
<v-card
|
||||||
class="d-flex flex-column fill-height rounded-xl"
|
class="d-flex flex-column fill-height"
|
||||||
style="overflow: scroll; height: calc(100vh - 24px - 40px - 40px)"
|
style="overflow: scroll; height: calc(100vh - 24px - 40px - 40px)"
|
||||||
color="card"
|
color="card"
|
||||||
|
elevation="0"
|
||||||
>
|
>
|
||||||
<v-toolbar color="transparent" class="flex-grow-0 flex-shrink-0">
|
<v-toolbar color="toolbar" class="flex-grow-0 flex-shrink-0">
|
||||||
<v-toolbar-title>
|
<v-toolbar-title>
|
||||||
Search ({{ search.pager.totalItems || 0 }})
|
Search ({{ search.pager.totalItems || 0 }})
|
||||||
</v-toolbar-title>
|
</v-toolbar-title>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn icon @click="searchPanel = false">
|
<v-btn icon @click="$store.state.searchPanel = false">
|
||||||
<v-icon>mdi-close</v-icon>
|
<v-icon>mdi-close</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
|
@ -1330,10 +1285,14 @@
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col
|
<v-col
|
||||||
cols="3"
|
:cols="$vuetify.breakpoint.xl ? 2 : 3"
|
||||||
class="ml-2"
|
class="ml-2"
|
||||||
id="user-col"
|
id="user-col"
|
||||||
v-if="$store.state.userPanel && !$vuetify.breakpoint.mobile"
|
v-if="
|
||||||
|
$store.state.userPanel &&
|
||||||
|
!$vuetify.breakpoint.mobile &&
|
||||||
|
!$store.state.searchPanel
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<v-card
|
<v-card
|
||||||
class="d-flex flex-column fill-height rounded-xl"
|
class="d-flex flex-column fill-height rounded-xl"
|
||||||
|
@ -1363,17 +1322,6 @@
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
<v-toolbar
|
|
||||||
color="transparent"
|
|
||||||
class="flex-grow-0 flex-shrink-0"
|
|
||||||
elevation="2"
|
|
||||||
>
|
|
||||||
<v-toolbar-title> Members </v-toolbar-title>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-btn icon @click="$store.state.userPanel = false">
|
|
||||||
<v-icon>mdi-close</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-toolbar>
|
|
||||||
<v-list two-line color="card">
|
<v-list two-line color="card">
|
||||||
<v-list-item-group class="rounded-xl">
|
<v-list-item-group class="rounded-xl">
|
||||||
<template v-for="item in associations">
|
<template v-for="item in associations">
|
||||||
|
@ -1460,6 +1408,8 @@ import {
|
||||||
PointElement,
|
PointElement,
|
||||||
LineElement
|
LineElement
|
||||||
} from "chart.js"
|
} from "chart.js"
|
||||||
|
import NicknameDialog from "@/components/NicknameDialog"
|
||||||
|
import UserDialog from "@/components/UserDialog"
|
||||||
|
|
||||||
ChartJS.register(
|
ChartJS.register(
|
||||||
Title,
|
Title,
|
||||||
|
@ -1472,7 +1422,7 @@ ChartJS.register(
|
||||||
)
|
)
|
||||||
export default {
|
export default {
|
||||||
name: "CommunicationsChat",
|
name: "CommunicationsChat",
|
||||||
components: { CommsInput, Chart },
|
components: { UserDialog, NicknameDialog, CommsInput, Chart },
|
||||||
props: ["chat", "loading", "items"],
|
props: ["chat", "loading", "items"],
|
||||||
data: () => ({
|
data: () => ({
|
||||||
graphOptions: {
|
graphOptions: {
|
||||||
|
@ -1615,41 +1565,7 @@ export default {
|
||||||
},
|
},
|
||||||
openUserPanel(user) {
|
openUserPanel(user) {
|
||||||
this.context.userPopout.item = user
|
this.context.userPopout.item = user
|
||||||
this.context.userPopout.item.mutualGroups = []
|
|
||||||
this.context.userPopout.item.mutualFriends = []
|
|
||||||
this.context.userPopout.item.loading = {
|
|
||||||
mutualGroups: true,
|
|
||||||
mutualFriends: true
|
|
||||||
}
|
|
||||||
this.context.userPopout.value = true
|
this.context.userPopout.value = true
|
||||||
this.axios
|
|
||||||
.get(
|
|
||||||
process.env.VUE_APP_BASE_URL +
|
|
||||||
"/api/v1/communications/mutual/" +
|
|
||||||
user.id +
|
|
||||||
"/groups"
|
|
||||||
)
|
|
||||||
.then((res) => {
|
|
||||||
this.context.userPopout.item.mutualGroups = res.data
|
|
||||||
this.context.userPopout.item.loading.mutualGroups = false
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
AjaxErrorHandler(this.$store)(e)
|
|
||||||
})
|
|
||||||
this.axios
|
|
||||||
.get(
|
|
||||||
process.env.VUE_APP_BASE_URL +
|
|
||||||
"/api/v1/communications/mutual/" +
|
|
||||||
user.id +
|
|
||||||
"/friends"
|
|
||||||
)
|
|
||||||
.then((res) => {
|
|
||||||
this.context.userPopout.item.mutualFriends = res.data
|
|
||||||
this.context.userPopout.item.loading.mutualFriends = false
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
AjaxErrorHandler(this.$store)(e)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
getName(user) {
|
getName(user) {
|
||||||
if (user.nickname?.nickname) {
|
if (user.nickname?.nickname) {
|
||||||
|
@ -1757,7 +1673,6 @@ export default {
|
||||||
img.src = link
|
img.src = link
|
||||||
},
|
},
|
||||||
handleDrag(e) {
|
handleDrag(e) {
|
||||||
console.log(1)
|
|
||||||
if (e.dataTransfer.files.length) {
|
if (e.dataTransfer.files.length) {
|
||||||
this.file = e.dataTransfer.files[0]
|
this.file = e.dataTransfer.files[0]
|
||||||
}
|
}
|
||||||
|
@ -1919,7 +1834,6 @@ export default {
|
||||||
this.message = drafts[this.$route.params.id]
|
this.message = drafts[this.$route.params.id]
|
||||||
}
|
}
|
||||||
this.$socket.on("message", (message) => {
|
this.$socket.on("message", (message) => {
|
||||||
console.log(this.chat)
|
|
||||||
if (message.chatId === this.chat.chatId) {
|
if (message.chatId === this.chat.chatId) {
|
||||||
this.messages.push(message)
|
this.messages.push(message)
|
||||||
this.autoScroll()
|
this.autoScroll()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="communications-friends">
|
<div id="communications-friends">
|
||||||
<v-card color="card" class="rounded-xl" :height="viewport()">
|
<v-card color="card" class="rounded-xl" :height="viewport()" elevation="0">
|
||||||
<v-tabs centered background-color="card" v-model="tab">
|
<v-tabs centered background-color="card" v-model="tab">
|
||||||
<v-tab :key="0">Users</v-tab>
|
<v-tab :key="0">Users</v-tab>
|
||||||
<v-tab :key="1"> Friends </v-tab>
|
<v-tab :key="1"> Friends </v-tab>
|
||||||
|
@ -341,6 +341,12 @@ export default {
|
||||||
this.$socket.on("friendRequest", () => {
|
this.$socket.on("friendRequest", () => {
|
||||||
this.getFriends()
|
this.getFriends()
|
||||||
})
|
})
|
||||||
|
this.$socket.on("friendUpdate", () => {
|
||||||
|
this.getFriends()
|
||||||
|
})
|
||||||
|
this.$socket.on("friendAccepted", () => {
|
||||||
|
this.getFriends()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -30,7 +30,8 @@
|
||||||
<v-form ref="form" class="pa-4 pt-6">
|
<v-form ref="form" class="pa-4 pt-6">
|
||||||
<p class="text-center text-h4">
|
<p class="text-center text-h4">
|
||||||
Login to
|
Login to
|
||||||
<span class="troplo-title">{{ $store.state.site.name }}</span>
|
<span class="troplo-title">{{ $store.state.site.name }}</span
|
||||||
|
><small style="font-size: 15px">beta</small>
|
||||||
</p>
|
</p>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
@keyup.enter="doLogin()"
|
@keyup.enter="doLogin()"
|
||||||
|
@ -61,6 +62,7 @@
|
||||||
color="primary"
|
color="primary"
|
||||||
text
|
text
|
||||||
@click="$router.push('/register')"
|
@click="$router.push('/register')"
|
||||||
|
v-if="$store.state.site.allowRegistrations"
|
||||||
>
|
>
|
||||||
Register
|
Register
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
|
@ -8,10 +8,11 @@
|
||||||
<v-form ref="form" class="pa-4 pt-6">
|
<v-form ref="form" class="pa-4 pt-6">
|
||||||
<p class="text-center text-h4">
|
<p class="text-center text-h4">
|
||||||
Login to
|
Login to
|
||||||
<span class="troplo-title">{{ $store.state.site.name }}</span>
|
<span class="troplo-title">{{ $store.state.site.name }}</span
|
||||||
|
><small style="font-size: 15px">beta</small>
|
||||||
</p>
|
</p>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
@keyup.enter="doLogin()"
|
@keyup.enter="doRegister()"
|
||||||
class="rounded-xl"
|
class="rounded-xl"
|
||||||
v-model="username"
|
v-model="username"
|
||||||
label="Username"
|
label="Username"
|
||||||
|
@ -19,7 +20,7 @@
|
||||||
type="email"
|
type="email"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
@keyup.enter="doLogin()"
|
@keyup.enter="doRegister()"
|
||||||
class="rounded-xl"
|
class="rounded-xl"
|
||||||
v-model="email"
|
v-model="email"
|
||||||
label="Email"
|
label="Email"
|
||||||
|
@ -27,7 +28,7 @@
|
||||||
type="email"
|
type="email"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
@keyup.enter="doLogin()"
|
@keyup.enter="doRegister()"
|
||||||
class="rounded-xl"
|
class="rounded-xl"
|
||||||
v-model="password"
|
v-model="password"
|
||||||
color="blue accent-7"
|
color="blue accent-7"
|
||||||
|
|
|
@ -150,8 +150,8 @@ export default {
|
||||||
css: "",
|
css: "",
|
||||||
autoCSS: false,
|
autoCSS: false,
|
||||||
fonts: [
|
fonts: [
|
||||||
"Roboto",
|
|
||||||
"Inter",
|
"Inter",
|
||||||
|
"Roboto",
|
||||||
"JetBrains Mono",
|
"JetBrains Mono",
|
||||||
"Montserrat",
|
"Montserrat",
|
||||||
"Ubuntu",
|
"Ubuntu",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="settings-site">
|
<div id="settings-site" v-if="$store.state.user?.id">
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<v-hover v-slot="{ hover }">
|
<v-hover v-slot="{ hover }">
|
||||||
|
@ -65,6 +65,15 @@
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="red"
|
||||||
|
text
|
||||||
|
@click="
|
||||||
|
$store.dispatch('logout')
|
||||||
|
$router.push('/login')
|
||||||
|
"
|
||||||
|
>Logout</v-btn
|
||||||
|
>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
Loading…
Reference in a new issue