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.
|
||||
Licensed under GNU GPLv3, and based off of the original BetterCompass Communications by @Troplo.
|
||||
Licensed under GNU GPLv3.
|
|
@ -1,6 +1,13 @@
|
|||
# Colubrina
|
||||
|
||||
![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
|
||||
|
||||
Rename .env.example to .env and fill it out with your own information.
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
HOSTNAME=localhost
|
||||
RELEASE=dev
|
||||
CORS_HOSTNAME=http://localhost:8080
|
||||
RELEASE=stable
|
||||
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
|
||||
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.
|
||||
The source code is available on both Gitea, and GitHub respectively, https://git.troplo.com/Troplo/compass-vue, and https://github.com/Troplo/BetterCompass
|
||||
|
||||
BetterCompass Proxy is provided with the BetterCompass software, and has the same license.
|
||||
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 GitHub, https://github.com/Troplo/Colubrina
|
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,
|
||||
notificationType: process.env.NOTIFICATION_TYPE,
|
||||
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],
|
||||
unauthorized: ["You don't have permission 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],
|
||||
parentLinkIneligible: ["Your school does not support ParentLink.", 401],
|
||||
invalidTotp: ["Invalid 2FA code.", 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: [
|
||||
"You are being rate-limited. Please try again in a few minutes.",
|
||||
429
|
||||
],
|
||||
communicationsUserNotFound: ["This user does not exist.", 400],
|
||||
communicationsUserNotOptedIn: [
|
||||
"This user has not opted in to BetterCompass Communications.",
|
||||
"This user does not have chatting enabled.",
|
||||
400
|
||||
],
|
||||
friendAlreadyFriends: ["You are already friends with this user.", 400],
|
||||
|
@ -39,20 +30,20 @@ let Errors = {
|
|||
400
|
||||
],
|
||||
fileTooLarge: ["The file you are trying to upload is too large.", 400],
|
||||
unauthorizedInstance: [
|
||||
"You are not authorized to access BetterCompass.",
|
||||
401
|
||||
],
|
||||
invalidPassword: [
|
||||
"Your password must be at least 8 characters in length.",
|
||||
400
|
||||
],
|
||||
registrationsDisabled: [
|
||||
"Registrations are currently disabled on this instance. Please try again later.",
|
||||
400
|
||||
]
|
||||
}
|
||||
|
||||
function processErrors(errorName) {
|
||||
let arr = Errors[errorName]
|
||||
|
||||
temp = {}
|
||||
let temp = {}
|
||||
temp.name = errorName
|
||||
temp.message = arr[0]
|
||||
temp.status = arr[1]
|
||||
|
|
|
@ -4,7 +4,7 @@ module.exports = {
|
|||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn("Users", "font", {
|
||||
type: Sequelize.STRING,
|
||||
defaultValue: "Roboto",
|
||||
defaultValue: "Inter",
|
||||
allowNull: false
|
||||
})
|
||||
},
|
||||
|
|
|
@ -24,6 +24,11 @@ module.exports = (sequelize, DataTypes) => {
|
|||
}
|
||||
User.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.BIGINT,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
|
@ -112,7 +117,7 @@ module.exports = (sequelize, DataTypes) => {
|
|||
},
|
||||
font: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: "Roboto",
|
||||
defaultValue: "Inter",
|
||||
allowNull: false
|
||||
},
|
||||
status: {
|
||||
|
@ -144,6 +149,10 @@ module.exports = (sequelize, DataTypes) => {
|
|||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
lastSeenAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
"serve": "nodemon"
|
||||
},
|
||||
"dependencies": {
|
||||
"argon2": "^0.28.5",
|
||||
"axios": "^0.26.1",
|
||||
"argon2": "^0.28.7",
|
||||
"axios": "^0.27.2",
|
||||
"clean-css": "^4.1.11",
|
||||
"constantinople": "3.1.1",
|
||||
"cookie-parser": "^1.4.6",
|
||||
|
@ -20,20 +20,26 @@
|
|||
"express-rate-limit": "^6.4.0",
|
||||
"file-type": "16.5.3",
|
||||
"http-errors": "~1.6.3",
|
||||
"input": "^1.0.1",
|
||||
"jade": "~1.11.0",
|
||||
"jw-paginate": "^1.0.4",
|
||||
"local-cors-proxy": "^1.1.0",
|
||||
"mariadb": "^3.0.0",
|
||||
"mariadb": "^3.0.1",
|
||||
"multer": "^1.4.4",
|
||||
"node-xwhois": "^2.0.10",
|
||||
"open-graph-scraper": "^4.11.0",
|
||||
"patch-package": "^6.4.7",
|
||||
"pg": "^8.7.3",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"semver": "^7.3.7",
|
||||
"sequelize": "^6.17.0",
|
||||
"sequelize": "^6.21.3",
|
||||
"sequelize-cli": "^6.4.1",
|
||||
"socket.io": "^4.5.1",
|
||||
"speakeasy": "^2.0.0",
|
||||
"sqlite3": "^5.0.10",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
"uglify-js": "^2.6.0"
|
||||
"uglify-js": "^2.6.0",
|
||||
"umzug": "^3.1.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"constantinople": "^3.1.1",
|
||||
|
|
|
@ -2,7 +2,7 @@ const express = require("express")
|
|||
const router = express.Router()
|
||||
const Errors = require("../lib/errors.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 dayjs = require("dayjs")
|
||||
const fs = require("fs")
|
||||
|
@ -25,7 +25,6 @@ router.get("/", auth, async (req, res, next) => {
|
|||
res.json({
|
||||
users: await User.count(),
|
||||
themes: await Theme.count(),
|
||||
feedback: await Feedback.count(),
|
||||
messages: await Message.count(),
|
||||
usersToday: await User.count({
|
||||
where: {
|
||||
|
@ -62,6 +61,14 @@ router.get("/metrics", auth, async (req, res, next) => {
|
|||
exclude: ["totp", "compassSession", "password"]
|
||||
}
|
||||
})
|
||||
const messages = await Message.findAll({
|
||||
where: {
|
||||
createdAt: createdAt
|
||||
},
|
||||
attributes: {
|
||||
exclude: ["totp", "compassSession", "password"]
|
||||
}
|
||||
})
|
||||
|
||||
const registrationGraphInterim = registrationStats.reduce(function (
|
||||
result,
|
||||
|
@ -76,18 +83,14 @@ router.get("/metrics", auth, async (req, res, next) => {
|
|||
},
|
||||
{})
|
||||
|
||||
const activeUsersGraphInterim = registrationStats.reduce(function (
|
||||
result,
|
||||
user
|
||||
) {
|
||||
let day = dayjs(user.lastSeenAt).format("YYYY-MM-DD")
|
||||
const messagesGraphInterim = messages.reduce(function (result, message) {
|
||||
let day = dayjs(message.createdAt).format("YYYY-MM-DD")
|
||||
if (!result[day]) {
|
||||
result[day] = 0
|
||||
}
|
||||
result[day]++
|
||||
return result
|
||||
},
|
||||
{})
|
||||
}, {})
|
||||
|
||||
const usersGraph = {
|
||||
labels: Object.keys(registrationGraphInterim),
|
||||
|
@ -102,12 +105,12 @@ router.get("/metrics", auth, async (req, res, next) => {
|
|||
]
|
||||
}
|
||||
|
||||
const activeUsersGraph = {
|
||||
labels: Object.keys(activeUsersGraphInterim),
|
||||
const messagesGraph = {
|
||||
labels: Object.keys(messagesGraphInterim),
|
||||
datasets: [
|
||||
{
|
||||
data: Object.values(activeUsersGraphInterim),
|
||||
label: "Active Users",
|
||||
data: Object.values(messagesGraphInterim),
|
||||
label: "Messages",
|
||||
borderColor: "#3e95cd",
|
||||
pointBackgroundColor: "#FFFFFF",
|
||||
backgroundColor: "transparent"
|
||||
|
@ -117,7 +120,7 @@ router.get("/metrics", auth, async (req, res, next) => {
|
|||
|
||||
res.json({
|
||||
users: usersGraph,
|
||||
activeUsers: activeUsersGraph
|
||||
activeUsers: messagesGraph
|
||||
})
|
||||
} catch (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) => {
|
||||
function setEnvValue(key, value) {
|
||||
// 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 {
|
||||
const io = req.app.get("io")
|
||||
setEnvValue("ALLOW_REGISTRATIONS", req.body.allowRegistrations)
|
||||
if (req.body.broadcastType === "permanent") {
|
||||
setEnvValue("NOTIFICATION", req.body.notification)
|
||||
setEnvValue("NOTIFICATION_TYPE", req.body.notificationType)
|
||||
|
@ -242,7 +229,8 @@ router.put("/state", auth, async (req, res, next) => {
|
|||
io.emit("siteState", {
|
||||
notification: req.body.notification,
|
||||
notificationType: req.body.notificationType,
|
||||
latestVersion: require("../../package.json").version
|
||||
latestVersion: require("../../package.json").version,
|
||||
allowRegistrations: req.body.allowRegistrations
|
||||
})
|
||||
res.sendStatus(204)
|
||||
} catch (err) {
|
||||
|
|
|
@ -47,7 +47,92 @@ const upload = multer({
|
|||
|
||||
const resolveEmbeds = require("../lib/resolveEmbeds.js")
|
||||
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) => {
|
||||
try {
|
||||
let chats = await ChatAssociation.findAll({
|
||||
|
@ -746,6 +831,8 @@ router.post("/friends", auth, async (req, res, next) => {
|
|||
friendId: req.user.id,
|
||||
status: "pendingCanAccept"
|
||||
})
|
||||
io.to(user.id).emit("friendUpdate", {})
|
||||
io.to(req.user.id).emit("friendUpdate", {})
|
||||
io.to(user.id).emit("friendRequest", {
|
||||
...remoteFriend.dataValues,
|
||||
user: {
|
||||
|
@ -768,6 +855,7 @@ router.post("/friends", auth, async (req, res, next) => {
|
|||
|
||||
router.delete("/friends/:id", auth, async (req, res, next) => {
|
||||
try {
|
||||
const io = req.app.get("io")
|
||||
const friend = await Friend.findOne({
|
||||
where: {
|
||||
userId: req.user.id,
|
||||
|
@ -782,6 +870,8 @@ router.delete("/friends/:id", auth, async (req, res, next) => {
|
|||
friendId: req.user.id
|
||||
}
|
||||
})
|
||||
io.to(friend.friendId).emit("friendUpdate", {})
|
||||
io.to(req.user.id).emit("friendUpdate", {})
|
||||
res.sendStatus(204)
|
||||
} else {
|
||||
throw Errors.friendNotFound
|
||||
|
@ -826,9 +916,8 @@ router.put("/friends/:id", auth, async (req, res, next) => {
|
|||
await remoteFriend.update({
|
||||
status: "accepted"
|
||||
})
|
||||
io.to(req.user.id).emit("friendAccepted", {
|
||||
...friend.dataValues
|
||||
})
|
||||
io.to(friend.userId).emit("friendUpdate", {})
|
||||
io.to(remoteFriend.userId).emit("friendUpdate", {})
|
||||
io.to(remoteFriend.userId).emit("friendAccepted", {
|
||||
...remoteFriend.dataValues
|
||||
})
|
||||
|
@ -1004,6 +1093,13 @@ router.put("/:id", auth, async (req, res, next) => {
|
|||
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)
|
||||
} else {
|
||||
throw Errors.chatNotFoundOrNotAdmin
|
||||
|
|
|
@ -199,6 +199,9 @@ router.post("/register", async (req, res, next) => {
|
|||
}
|
||||
}
|
||||
try {
|
||||
if (!JSON.parse(process.env.ALLOW_REGISTRATIONS)) {
|
||||
throw Errors.registrationsDisabled
|
||||
}
|
||||
if (req.body.password.length < 8) {
|
||||
throw Errors.invalidPassword
|
||||
}
|
||||
|
@ -211,7 +214,7 @@ router.post("/register", async (req, res, next) => {
|
|||
name: req.body.username,
|
||||
admin: false,
|
||||
email: req.body.email,
|
||||
font: "Roboto",
|
||||
font: "Inter",
|
||||
status: "offline",
|
||||
storedStatus: "online",
|
||||
experiments: [],
|
||||
|
@ -230,9 +233,7 @@ router.post("/register", async (req, res, next) => {
|
|||
router.get("/", auth, (req, res, next) => {
|
||||
try {
|
||||
res.json(req.user)
|
||||
} catch (e) {
|
||||
console.log(1)
|
||||
}
|
||||
} catch {}
|
||||
})
|
||||
|
||||
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: {
|
||||
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() {
|
||||
let time
|
||||
let idle = false
|
||||
|
@ -652,103 +754,18 @@ export default {
|
|||
this.$socket.connect()
|
||||
document.title = this.$route.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.$vuetify.theme.dark = this.$store.state.user?.theme === "dark" || true
|
||||
this.$store.dispatch("getState")
|
||||
this.$store.dispatch("checkAuth").catch(() => {
|
||||
this.$store.dispatch("logout")
|
||||
this.$router.push("/login")
|
||||
})
|
||||
this.getThemes()
|
||||
this.$store
|
||||
.dispatch("getUserInfo")
|
||||
.then(() => {
|
||||
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.communicationsIdleCheck()
|
||||
this.$store.dispatch("getUserInfo").catch(() => {
|
||||
if (!["/login", "/register"].includes(this.$route.path)) {
|
||||
this.$router.push("/login")
|
||||
})
|
||||
}
|
||||
})
|
||||
this.registerSocket()
|
||||
},
|
||||
watch: {
|
||||
"$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 {
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
.troplo-header {
|
||||
|
|
|
@ -165,7 +165,7 @@ export default {
|
|||
reader.readAsDataURL(file)
|
||||
},
|
||||
handleChange() {
|
||||
if (this.$store.state.user.bcUser.storedStatus !== "invisible") {
|
||||
if (this.$store.state.user.storedStatus !== "invisible") {
|
||||
if (this.typingDate) {
|
||||
const now = new Date()
|
||||
if (now - this.typingDate > 5000) {
|
||||
|
|
|
@ -405,10 +405,11 @@
|
|||
background-color="toolbar"
|
||||
style="margin-bottom: -18px"
|
||||
elevation="2"
|
||||
v-model="search"
|
||||
></v-text-field>
|
||||
<v-toolbar color="toolbar" class="rounded-xl mb-3" elevation="2">
|
||||
<v-toolbar-title class="subtitle-1">
|
||||
CHATS ({{ $store.state.chats.length }})
|
||||
CHATS ({{ chats.length }})
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon @click="dialogs.new = true">
|
||||
|
@ -416,7 +417,7 @@
|
|||
</v-btn>
|
||||
</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>
|
||||
<v-list-item
|
||||
:to="'/communications/' + item.id"
|
||||
|
@ -556,7 +557,7 @@
|
|||
<v-list-item-content
|
||||
@click="copyUsername"
|
||||
v-bind="attrs"
|
||||
style="cursor: pointer"
|
||||
style="cursor: pointer; min-width: 100px"
|
||||
>
|
||||
<v-list-item-title>
|
||||
{{ $store.state.user.username }}
|
||||
|
@ -569,6 +570,15 @@
|
|||
<span>Copied!</span>
|
||||
</v-tooltip>
|
||||
<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-icon>mdi-cog</v-icon>
|
||||
</v-btn>
|
||||
|
@ -589,6 +599,7 @@ export default {
|
|||
components: { NicknameDialog },
|
||||
data() {
|
||||
return {
|
||||
search: "",
|
||||
nickname: {
|
||||
dialog: false,
|
||||
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: {
|
||||
groupSettings(id) {
|
||||
this.settings.item = this.$store.state.chats.find(
|
||||
|
@ -875,14 +903,15 @@ export default {
|
|||
createConversation() {
|
||||
this.newConversation.loading = true
|
||||
this.axios
|
||||
.post("/api/v1/communications/create", {
|
||||
.post(process.env.VUE_APP_BASE_URL + "/api/v1/communications/create", {
|
||||
users: this.newConversation.users
|
||||
})
|
||||
.then(() => {
|
||||
.then((res) => {
|
||||
this.newConversation.name = ""
|
||||
this.newConversation.users = []
|
||||
this.newConversation.loading = false
|
||||
this.newConversation.results = []
|
||||
this.dialogs.new = false
|
||||
this.$router.push("/communications/" + res.data.id)
|
||||
})
|
||||
.catch((e) => {
|
||||
this.newConversation.loading = false
|
||||
|
@ -927,6 +956,10 @@ export default {
|
|||
this.searchUsers()
|
||||
this.searchUsersForGroupAdmin()
|
||||
this.$store.dispatch("getChats")
|
||||
this.$socket.on("friendUpdate", () => {
|
||||
this.searchUsers()
|
||||
this.searchUsersForGroupAdmin()
|
||||
})
|
||||
this.$socket.on("userSettings", () => {
|
||||
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 "./plugins/dayjs"
|
||||
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 twemoji from "twemoji"
|
||||
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.
|
||||
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, {
|
||||
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, {
|
||||
allowedTags: [
|
||||
"address",
|
||||
|
|
|
@ -15,7 +15,7 @@ const routes = [
|
|||
children: [
|
||||
{
|
||||
path: "friends",
|
||||
name: "Friends - Communications",
|
||||
name: "Friends",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "communicationsFriends" */ "../views/Communications/CommunicationsFriends"
|
||||
|
@ -85,7 +85,7 @@ const routes = [
|
|||
},
|
||||
{
|
||||
path: "communications",
|
||||
name: "Communications",
|
||||
name: "Communications Settings",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "settingsCommunications" */ "../views/Settings/SettingsCommunications"
|
||||
|
@ -116,14 +116,6 @@ const routes = [
|
|||
/* webpackChunkName: "adminUsers" */ "../views/Admin/AdminUsers.vue"
|
||||
)
|
||||
},
|
||||
{
|
||||
path: "feedback",
|
||||
name: "Feedback",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "adminFeedback" */ "../views/Admin/AdminFeedback.vue"
|
||||
)
|
||||
},
|
||||
{
|
||||
path: "themes",
|
||||
name: "Themes",
|
||||
|
@ -166,4 +158,11 @@ const router = new VueRouter({
|
|||
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
|
||||
|
|
|
@ -60,6 +60,7 @@ export default new Vuex.Store({
|
|||
},
|
||||
communicationNotifications: 0,
|
||||
wsConnected: false,
|
||||
wsRegistered: false,
|
||||
lastChat: "friends",
|
||||
searchPanel: false,
|
||||
userPanel: false
|
||||
|
@ -258,7 +259,7 @@ export default new Vuex.Store({
|
|||
document.head.appendChild(style)
|
||||
}
|
||||
},
|
||||
checkAuth(context) {
|
||||
checkAuth() {
|
||||
return new Promise((resolve, reject) => {
|
||||
Vue.axios.defaults.headers.common["Authorization"] =
|
||||
localStorage.getItem("session")
|
||||
|
@ -270,7 +271,6 @@ export default new Vuex.Store({
|
|||
.catch((e) => {
|
||||
if (e?.response?.status === 401) {
|
||||
reject(false)
|
||||
context.dispatch("logout")
|
||||
} else {
|
||||
AjaxErrorHandler(this.$store)(e)
|
||||
}
|
||||
|
@ -321,7 +321,7 @@ export default new Vuex.Store({
|
|||
context.commit("updateQuickSwitchCache", {
|
||||
subjectLongName: "Home",
|
||||
customType: 1,
|
||||
route: "/"
|
||||
route: "/communications/friends"
|
||||
})
|
||||
context.state.chats.forEach((chat) => {
|
||||
context.commit("updateQuickSwitchCache", {
|
||||
|
@ -417,7 +417,6 @@ div {
|
|||
resolve(res.data)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
if (JSON.parse(localStorage.getItem("userCache"))?.id) {
|
||||
const user = JSON.parse(localStorage.getItem("userCache"))
|
||||
const name = user.themeObject.id
|
||||
|
@ -475,12 +474,7 @@ div {
|
|||
sheet: "#181818",
|
||||
text: "#000000",
|
||||
dark: "#151515",
|
||||
bg: "#151515",
|
||||
calendarNormalActivity: "#3f51b5",
|
||||
calendarActivityType7: "#f44336",
|
||||
calendarActivityType8: "#4caf50",
|
||||
calendarActivityType10: "#ff9800",
|
||||
calendarExternalActivity: "#2196f3"
|
||||
bg: "#151515"
|
||||
},
|
||||
light: {
|
||||
primary: "#0190ea",
|
||||
|
@ -495,12 +489,7 @@ div {
|
|||
sheet: "#f8f8f8",
|
||||
text: "#000000",
|
||||
dark: "#f8f8f8",
|
||||
bg: "#f8f8f8",
|
||||
calendarNormalActivity: "#3f51b5",
|
||||
calendarActivityType7: "#f44336",
|
||||
calendarActivityType8: "#4caf50",
|
||||
calendarActivityType10: "#ff9800",
|
||||
calendarExternalActivity: "#2196f3"
|
||||
bg: "#f8f8f8"
|
||||
}
|
||||
}
|
||||
const name = theme.id
|
||||
|
|
|
@ -17,10 +17,6 @@
|
|||
<v-icon>mdi-account-multiple</v-icon>
|
||||
<span>Users</span>
|
||||
</v-tab>
|
||||
<v-tab to="/admin/feedback">
|
||||
<v-icon>mdi-bug</v-icon>
|
||||
<span>Feedback</span>
|
||||
</v-tab>
|
||||
<v-tab to="/admin/themes">
|
||||
<v-icon>mdi-brush</v-icon>
|
||||
<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-card>
|
||||
</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-card color="card" class="rounded-xl">
|
||||
<v-toolbar color="toolbar">
|
||||
|
|
|
@ -26,6 +26,12 @@
|
|||
text-value="value"
|
||||
>
|
||||
</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"
|
||||
>Save</v-btn
|
||||
>
|
||||
|
@ -40,6 +46,7 @@ export default {
|
|||
return {
|
||||
notification: "",
|
||||
notificationType: "info",
|
||||
allowRegistrations: true,
|
||||
notificationTypes: [
|
||||
{ text: "Info", value: "info" },
|
||||
{ text: "Success", value: "success" },
|
||||
|
@ -65,7 +72,8 @@ export default {
|
|||
.put("/api/v1/admin/state", {
|
||||
notification: this.notification,
|
||||
notificationType: this.notificationType,
|
||||
broadcastType: this.broadcastType
|
||||
broadcastType: this.broadcastType,
|
||||
allowRegistrations: this.allowRegistrations
|
||||
})
|
||||
.then(() => {
|
||||
this.$toast.success("State updated")
|
||||
|
@ -78,6 +86,7 @@ export default {
|
|||
mounted() {
|
||||
this.notification = this.$store.state.site.notification
|
||||
this.notificationType = this.$store.state.site.notificationType
|
||||
this.allowRegistrations = this.$store.state.site.allowRegistrations
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -41,21 +41,13 @@ export default {
|
|||
text: "ID",
|
||||
value: "id"
|
||||
},
|
||||
{
|
||||
text: "Compass User ID",
|
||||
value: "compassUserId"
|
||||
},
|
||||
{
|
||||
text: "Sussi Auth ID",
|
||||
value: "sussiId"
|
||||
},
|
||||
{
|
||||
text: "Username",
|
||||
value: "displayCode"
|
||||
value: "username"
|
||||
},
|
||||
{
|
||||
text: "Instance",
|
||||
value: "instance"
|
||||
text: "Email",
|
||||
value: "email"
|
||||
},
|
||||
{
|
||||
text: "Created At",
|
||||
|
@ -69,10 +61,6 @@ export default {
|
|||
text: "Base Theme",
|
||||
value: "theme"
|
||||
},
|
||||
{
|
||||
text: "Communications",
|
||||
value: "privacy.communications.enabled"
|
||||
},
|
||||
{
|
||||
text: "Theme",
|
||||
value: "themeObject.name"
|
||||
|
|
|
@ -33,187 +33,11 @@
|
|||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-dialog v-model="context.userPopout.value" max-width="650px">
|
||||
<v-card v-if="context.userPopout.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/' +
|
||||
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>
|
||||
<UserDialog
|
||||
:user="context.userPopout"
|
||||
:key="context.userPopout.item?.id || 0"
|
||||
></UserDialog>
|
||||
<NicknameDialog :nickname="context.nickname" />
|
||||
<v-dialog
|
||||
v-model="preview.dialog"
|
||||
elevation="0"
|
||||
|
@ -906,6 +730,128 @@
|
|||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</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>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
|
@ -965,23 +911,32 @@
|
|||
</v-card-text>
|
||||
</v-card>
|
||||
</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
|
||||
cols="3"
|
||||
class=""
|
||||
id="search-col"
|
||||
v-if="searchPanel && !$vuetify.breakpoint.mobile"
|
||||
v-if="$store.state.searchPanel && !$vuetify.breakpoint.mobile"
|
||||
>
|
||||
<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)"
|
||||
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>
|
||||
Search ({{ search.pager.totalItems || 0 }})
|
||||
</v-toolbar-title>
|
||||
<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-btn>
|
||||
</v-toolbar>
|
||||
|
@ -1330,10 +1285,14 @@
|
|||
</v-card>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="3"
|
||||
:cols="$vuetify.breakpoint.xl ? 2 : 3"
|
||||
class="ml-2"
|
||||
id="user-col"
|
||||
v-if="$store.state.userPanel && !$vuetify.breakpoint.mobile"
|
||||
v-if="
|
||||
$store.state.userPanel &&
|
||||
!$vuetify.breakpoint.mobile &&
|
||||
!$store.state.searchPanel
|
||||
"
|
||||
>
|
||||
<v-card
|
||||
class="d-flex flex-column fill-height rounded-xl"
|
||||
|
@ -1363,17 +1322,6 @@
|
|||
</v-list-item>
|
||||
</v-list>
|
||||
</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-item-group class="rounded-xl">
|
||||
<template v-for="item in associations">
|
||||
|
@ -1460,6 +1408,8 @@ import {
|
|||
PointElement,
|
||||
LineElement
|
||||
} from "chart.js"
|
||||
import NicknameDialog from "@/components/NicknameDialog"
|
||||
import UserDialog from "@/components/UserDialog"
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
|
@ -1472,7 +1422,7 @@ ChartJS.register(
|
|||
)
|
||||
export default {
|
||||
name: "CommunicationsChat",
|
||||
components: { CommsInput, Chart },
|
||||
components: { UserDialog, NicknameDialog, CommsInput, Chart },
|
||||
props: ["chat", "loading", "items"],
|
||||
data: () => ({
|
||||
graphOptions: {
|
||||
|
@ -1615,41 +1565,7 @@ export default {
|
|||
},
|
||||
openUserPanel(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.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) {
|
||||
if (user.nickname?.nickname) {
|
||||
|
@ -1757,7 +1673,6 @@ export default {
|
|||
img.src = link
|
||||
},
|
||||
handleDrag(e) {
|
||||
console.log(1)
|
||||
if (e.dataTransfer.files.length) {
|
||||
this.file = e.dataTransfer.files[0]
|
||||
}
|
||||
|
@ -1919,7 +1834,6 @@ export default {
|
|||
this.message = drafts[this.$route.params.id]
|
||||
}
|
||||
this.$socket.on("message", (message) => {
|
||||
console.log(this.chat)
|
||||
if (message.chatId === this.chat.chatId) {
|
||||
this.messages.push(message)
|
||||
this.autoScroll()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<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-tab :key="0">Users</v-tab>
|
||||
<v-tab :key="1"> Friends </v-tab>
|
||||
|
@ -341,6 +341,12 @@ export default {
|
|||
this.$socket.on("friendRequest", () => {
|
||||
this.getFriends()
|
||||
})
|
||||
this.$socket.on("friendUpdate", () => {
|
||||
this.getFriends()
|
||||
})
|
||||
this.$socket.on("friendAccepted", () => {
|
||||
this.getFriends()
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -30,7 +30,8 @@
|
|||
<v-form ref="form" class="pa-4 pt-6">
|
||||
<p class="text-center text-h4">
|
||||
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>
|
||||
<v-text-field
|
||||
@keyup.enter="doLogin()"
|
||||
|
@ -61,6 +62,7 @@
|
|||
color="primary"
|
||||
text
|
||||
@click="$router.push('/register')"
|
||||
v-if="$store.state.site.allowRegistrations"
|
||||
>
|
||||
Register
|
||||
</v-btn>
|
||||
|
|
|
@ -8,10 +8,11 @@
|
|||
<v-form ref="form" class="pa-4 pt-6">
|
||||
<p class="text-center text-h4">
|
||||
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>
|
||||
<v-text-field
|
||||
@keyup.enter="doLogin()"
|
||||
@keyup.enter="doRegister()"
|
||||
class="rounded-xl"
|
||||
v-model="username"
|
||||
label="Username"
|
||||
|
@ -19,7 +20,7 @@
|
|||
type="email"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
@keyup.enter="doLogin()"
|
||||
@keyup.enter="doRegister()"
|
||||
class="rounded-xl"
|
||||
v-model="email"
|
||||
label="Email"
|
||||
|
@ -27,7 +28,7 @@
|
|||
type="email"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
@keyup.enter="doLogin()"
|
||||
@keyup.enter="doRegister()"
|
||||
class="rounded-xl"
|
||||
v-model="password"
|
||||
color="blue accent-7"
|
||||
|
|
|
@ -150,8 +150,8 @@ export default {
|
|||
css: "",
|
||||
autoCSS: false,
|
||||
fonts: [
|
||||
"Roboto",
|
||||
"Inter",
|
||||
"Roboto",
|
||||
"JetBrains Mono",
|
||||
"Montserrat",
|
||||
"Ubuntu",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div id="settings-site">
|
||||
<div id="settings-site" v-if="$store.state.user?.id">
|
||||
<v-card-text>
|
||||
<div class="d-flex">
|
||||
<v-hover v-slot="{ hover }">
|
||||
|
@ -65,6 +65,15 @@
|
|||
>
|
||||
Save
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="red"
|
||||
text
|
||||
@click="
|
||||
$store.dispatch('logout')
|
||||
$router.push('/login')
|
||||
"
|
||||
>Logout</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</div>
|
||||
</template>
|
||||
|
|
Loading…
Reference in a new issue