This commit is contained in:
Troplo 2022-07-31 23:57:47 +10:00
parent 7d0141d2bf
commit 970c0021c3
12 changed files with 1831 additions and 83 deletions

2
frontend/.gitignore vendored
View file

@ -21,3 +21,5 @@ pnpm-debug.log*
backend/node_modules backend/node_modules
backend/config/config.json backend/config/config.json
backend/.env backend/.env
#Electron-builder output
/dist_electron

View file

@ -1,15 +1,18 @@
{ {
"name": "colubrina-chat", "name": "colubrina-chat",
"version": "1.0.10", "version": "1.0.11",
"private": true, "private": true,
"author": "Troplo <troplo@troplo.com>", "author": "Troplo <troplo@troplo.com>",
"license": "GPL-3.0",
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "vue-cli-service build", "build": "vue-cli-service build",
"lint": "vue-cli-service lint", "lint": "vue-cli-service lint",
"postinstall": "patch-package" "electron:build": "vue-cli-service electron:build",
"electron:serve": "vue-cli-service electron:serve",
"postinstall": "patch-package && electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps"
}, },
"main": "background.js",
"dependencies": { "dependencies": {
"@babel/preset-env": "^7.17.10", "@babel/preset-env": "^7.17.10",
"@mdi/js": "^6.6.95", "@mdi/js": "^6.6.95",
@ -62,11 +65,14 @@
"@vue/cli-service": "~4.5.15", "@vue/cli-service": "~4.5.15",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"dotenv-webpack": "^7.1.0", "dotenv-webpack": "^7.1.0",
"electron": "^13.0.0",
"electron-devtools-installer": "^3.1.0",
"eslint": "^6.7.2", "eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2", "eslint-plugin-vue": "^6.2.2",
"material-design-icons-iconfont": "^6.5.0", "material-design-icons-iconfont": "^6.5.0",
"sass": "~1.32.0", "sass": "~1.32.0",
"sass-loader": "^10.0.0", "sass-loader": "^10.0.0",
"vue-cli-plugin-electron-builder": "~2.1.1",
"vue-cli-plugin-vuetify": "~2.4.7", "vue-cli-plugin-vuetify": "~2.4.7",
"vue-template-babel-compiler": "^1.1.3", "vue-template-babel-compiler": "^1.1.3",
"vue-template-compiler": "^2.6.11", "vue-template-compiler": "^2.6.11",
@ -74,6 +80,7 @@
"webpack-auto-inject-version-next": "1.2.4", "webpack-auto-inject-version-next": "1.2.4",
"webpack-bundle-analyzer": "^4.5.0" "webpack-bundle-analyzer": "^4.5.0"
}, },
"license": "GPL-3.0",
"resolutions": { "resolutions": {
"node-ipc": "git+https://git.troplo.com/Troplo/node-ipc", "node-ipc": "git+https://git.troplo.com/Troplo/node-ipc",
"selfsigned": "^2.0.1", "selfsigned": "^2.0.1",

View file

@ -729,8 +729,22 @@ export default {
} }
}, },
mounted() { mounted() {
Vue.axios.defaults.headers.common["X-Colubrina"] = true
Vue.axios.defaults.headers.common["Authorization"] = Vue.axios.defaults.headers.common["Authorization"] =
localStorage.getItem("session") localStorage.getItem("session")
console.log(localStorage.getItem("instance"))
if (process.env.IS_ELECTRON) {
this.axios.defaults.baseURL = localStorage.getItem("instance")
this.$store.state.baseURL = localStorage.getItem("instance")
}
if (localStorage.getItem("customHeaders")) {
console.log(JSON.parse(localStorage.getItem("customHeaders")))
for (let header in JSON.parse(localStorage.getItem("customHeaders"))) {
Vue.axios.defaults.headers[header] = JSON.parse(
localStorage.getItem("customHeaders")
)[header]
}
}
if (this.$vuetify.breakpoint.mobile) { if (this.$vuetify.breakpoint.mobile) {
this.$store.state.drawer = false this.$store.state.drawer = false
} }

View file

@ -0,0 +1,82 @@
"use strict"
import { app, protocol, BrowserWindow } from "electron"
import { createProtocol } from "vue-cli-plugin-electron-builder/lib"
import installExtension, { VUEJS_DEVTOOLS } from "electron-devtools-installer"
const isDevelopment = process.env.NODE_ENV !== "production"
// Scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([
{ scheme: "app", privileges: { secure: true, standard: true } }
])
async function createWindow() {
// Create the browser window.
const win = new BrowserWindow({
width: 1400,
height: 1000,
webPreferences: {
// Use pluginOptions.nodeIntegration, leave this alone
// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,
preload: "window-preload.js",
webSecurity: false
}
})
if (process.env.WEBPACK_DEV_SERVER_URL) {
// Load the url of the dev server if in development mode
await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
if (!process.env.IS_TEST) win.webContents.openDevTools()
} else {
createProtocol("app")
// Load the index.html when not in development
win.loadURL("app://./index.html")
}
}
// Quit when all windows are closed.
app.on("window-all-closed", () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== "darwin") {
app.quit()
}
})
app.on("activate", () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on("ready", async () => {
if (isDevelopment && !process.env.IS_TEST) {
// Install Vue Devtools
try {
await installExtension(VUEJS_DEVTOOLS)
} catch (e) {
console.error("Vue Devtools failed to install:", e.toString())
}
}
createWindow()
})
// Exit cleanly on request from parent process in development mode.
if (isDevelopment) {
if (process.platform === "win32") {
process.on("message", (data) => {
if (data === "graceful-exit") {
app.quit()
}
})
} else {
process.on("SIGTERM", () => {
app.quit()
})
}
}

View file

@ -67,14 +67,22 @@
@click:close="remove(data.item)" @click:close="remove(data.item)"
> >
<v-avatar left v-if="data.item.avatar"> <v-avatar left v-if="data.item.avatar">
<v-img :src="'/usercontent/' + data.item.avatar"></v-img> <v-img
:src="
$store.state.baseURL + '/usercontent/' + data.item.avatar
"
></v-img>
</v-avatar> </v-avatar>
@{{ data.item.username }} @{{ data.item.username }}
</v-chip> </v-chip>
</template> </template>
<template v-slot:item="data"> <template v-slot:item="data">
<v-avatar left v-if="data.item.avatar" class="mr-3 mb-2 mt-2"> <v-avatar left v-if="data.item.avatar" class="mr-3 mb-2 mt-2">
<v-img :src="'/usercontent/' + data.item.avatar"></v-img> <v-img
:src="
$store.state.baseURL + '/usercontent/' + data.item.avatar
"
></v-img>
</v-avatar> </v-avatar>
<v-avatar left v-else class="mr-3 mb-2 mt-2"> <v-avatar left v-else class="mr-3 mb-2 mt-2">
<v-icon>mdi-account</v-icon> <v-icon>mdi-account</v-icon>
@ -142,7 +150,9 @@
> >
<v-list-item-avatar :color="$vuetify.theme.themes.dark.primary"> <v-list-item-avatar :color="$vuetify.theme.themes.dark.primary">
<v-img <v-img
:src="'/usercontent/' + user.user.avatar" :src="
$store.state.baseURL + '/usercontent/' + user.user.avatar
"
v-if="user.user.avatar" v-if="user.user.avatar"
/> />
<v-icon v-else> mdi-account </v-icon> <v-icon v-else> mdi-account </v-icon>
@ -249,14 +259,22 @@
@click:close="remove(data.item)" @click:close="remove(data.item)"
> >
<v-avatar left v-if="data.item.avatar"> <v-avatar left v-if="data.item.avatar">
<v-img :src="'/usercontent/' + data.item.avatar"></v-img> <v-img
:src="
$store.state.baseURL + '/usercontent/' + data.item.avatar
"
></v-img>
</v-avatar> </v-avatar>
@{{ data.item.username }} @{{ data.item.username }}
</v-chip> </v-chip>
</template> </template>
<template v-slot:item="data"> <template v-slot:item="data">
<v-avatar left v-if="data.item.avatar" class="mr-3 mb-2 mt-2"> <v-avatar left v-if="data.item.avatar" class="mr-3 mb-2 mt-2">
<v-img :src="'/usercontent/' + data.item.avatar"></v-img> <v-img
:src="
$store.state.baseURL + '/usercontent/' + data.item.avatar
"
></v-img>
</v-avatar> </v-avatar>
<v-avatar left v-else class="mr-3 mb-2 mt-2"> <v-avatar left v-else class="mr-3 mb-2 mt-2">
<v-icon>mdi-account</v-icon> <v-icon>mdi-account</v-icon>
@ -527,7 +545,11 @@
> >
<v-img <v-img
v-if="$store.state.user.avatar" v-if="$store.state.user.avatar"
:src="'/usercontent/' + $store.state.user.avatar" :src="
$store.state.baseURL +
'/usercontent/' +
$store.state.user.avatar
"
/> />
<v-icon v-else> mdi-account </v-icon> <v-icon v-else> mdi-account </v-icon>
</v-list-item-avatar> </v-list-item-avatar>

View file

@ -164,7 +164,7 @@
@click="setImagePreview(embed)" @click="setImagePreview(embed)"
contain contain
:aspect-ratio="16 / 9" :aspect-ratio="16 / 9"
:src="embed.mediaProxyLink" :src="$store.state.baseURL + embed.mediaProxyLink"
> >
<template v-slot:placeholder> <template v-slot:placeholder>
<v-row <v-row

View file

@ -175,7 +175,7 @@ const routes = [
] ]
const router = new VueRouter({ const router = new VueRouter({
mode: "history", mode: process.env.IS_ELECTRON ? "hash" : "history",
base: process.env.BASE_URL, base: process.env.BASE_URL,
routes routes
}) })

View file

@ -855,6 +855,7 @@ export default {
}, },
props: ["chat", "loading", "items"], props: ["chat", "loading", "items"],
data: () => ({ data: () => ({
interval: null,
pins: [], pins: [],
pinsLoading: true, pinsLoading: true,
reachedTop: false, reachedTop: false,
@ -1361,7 +1362,7 @@ export default {
document document
.getElementById("message-list") .getElementById("message-list")
.addEventListener("scroll", this.scrollEvent) .addEventListener("scroll", this.scrollEvent)
setInterval(() => { this.interval = setInterval(() => {
this.typing() this.typing()
if ( if (
document.hasFocus() && document.hasFocus() &&
@ -1494,6 +1495,7 @@ export default {
destroyed() { destroyed() {
document.removeEventListener("keypress", this.focusKey) document.removeEventListener("keypress", this.focusKey)
document.removeEventListener("scroll", this.scrollEvent) document.removeEventListener("scroll", this.scrollEvent)
clearInterval(this.interval)
} }
} }
</script> </script>

View file

@ -24,7 +24,9 @@
:color="$vuetify.theme.themes.dark.primary" :color="$vuetify.theme.themes.dark.primary"
> >
<v-img <v-img
:src="'/usercontent/' + user.avatar" :src="
$store.state.baseURL + '/usercontent//' + user.avatar
"
v-if="user.avatar" v-if="user.avatar"
/> />
<v-icon v-else> mdi-account </v-icon> <v-icon v-else> mdi-account </v-icon>
@ -102,7 +104,11 @@
:color="$vuetify.theme.themes.dark.primary" :color="$vuetify.theme.themes.dark.primary"
> >
<v-img <v-img
:src="'/usercontent/' + friend.user2.avatar" :src="
$store.state.baseURL +
'/usercontent//' +
friend.user2.avatar
"
v-if="friend.user2.avatar" v-if="friend.user2.avatar"
/> />
<v-icon v-else> mdi-account </v-icon> <v-icon v-else> mdi-account </v-icon>
@ -152,7 +158,11 @@
:color="$vuetify.theme.themes.dark.primary" :color="$vuetify.theme.themes.dark.primary"
> >
<v-img <v-img
:src="'/usercontent/' + friend.user2.avatar" :src="
$store.state.baseURL +
'/usercontent//' +
friend.user2.avatar
"
v-if="friend.user2.avatar" v-if="friend.user2.avatar"
/> />
<v-icon v-else> mdi-account </v-icon> <v-icon v-else> mdi-account </v-icon>
@ -195,7 +205,11 @@
:color="$vuetify.theme.themes.dark.primary" :color="$vuetify.theme.themes.dark.primary"
> >
<v-img <v-img
:src="'/usercontent/' + friend.user2.avatar" :src="
$store.state.baseURL +
'/usercontent//' +
friend.user2.avatar
"
v-if="friend.user2.avatar" v-if="friend.user2.avatar"
/> />
<v-icon v-else> mdi-account </v-icon> <v-icon v-else> mdi-account </v-icon>

View file

@ -33,6 +33,29 @@
<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> ><small style="font-size: 15px">beta</small>
</p> </p>
<v-text-field
@keyup.enter="doLogin()"
class="rounded-xl"
v-model="instance"
v-if="isElectron()"
label="Instance URL"
placeholder="https://colubrina.troplo.com"
type="email"
></v-text-field>
<small style="float: right" v-if="isElectron()">{{
instanceString
}}</small
><br v-if="isElectron()" />
<v-text-field
@keyup.enter="doLogin()"
class="rounded-xl"
v-model="customHeaders[header.name]"
v-for="header in $store.state.site.customHeaders"
:key="header.name"
:label="header.friendlyName"
:placeholder="header.placeholder"
type="email"
></v-text-field>
<v-text-field <v-text-field
@keyup.enter="doLogin()" @keyup.enter="doLogin()"
class="rounded-xl" class="rounded-xl"
@ -93,20 +116,30 @@ export default {
name: "Login", name: "Login",
data() { data() {
return { return {
instanceString: "",
rememberMe: true, rememberMe: true,
username: "", username: "",
password: "", password: "",
totp: "", totp: "",
totpDialog: false, totpDialog: false,
loading: false loading: false,
instance: "https://colubrina.troplo.com",
customHeaders: {}
} }
}, },
methods: { methods: {
isElectron() {
return process.env.IS_ELECTRON
},
viewport() { viewport() {
return window.innerHeight return window.innerHeight
}, },
doLogin() { doLogin() {
this.loading = true this.loading = true
localStorage.setItem("customHeaders", JSON.stringify(this.customHeaders))
for (let header in this.customHeaders) {
Vue.axios.defaults.headers[header] = this.customHeaders[header]
}
this.axios this.axios
.post("/api/v1/user/login", { .post("/api/v1/user/login", {
password: this.password, password: this.password,
@ -115,9 +148,12 @@ export default {
totp: this.totp totp: this.totp
}) })
.then(async (res) => { .then(async (res) => {
localStorage.setItem("session", res.data.session) const session =
Vue.axios.defaults.headers.common["Authorization"] = res.data.session res.data.session || res.data.bcToken || res.data.cookieToken
this.$store.commit("setToken", res.data.session)
localStorage.setItem("session", session)
Vue.axios.defaults.headers.common["Authorization"] = session
this.$store.commit("setToken", session)
await this.$store.dispatch("getUserInfo") await this.$store.dispatch("getUserInfo")
this.$store.dispatch("getChats") this.$store.dispatch("getChats")
this.loading = false this.loading = false
@ -144,12 +180,33 @@ export default {
this.loading = false this.loading = false
} }
}) })
},
testInstance() {
if (this.isElectron()) {
this.axios
.get(this.instance + "/api/v1/state")
.then((res) => {
this.instanceString = res.data.name + " v" + res.data.latestVersion
this.axios.defaults.baseURL = this.instance
localStorage.setItem("instance", this.instance)
this.$store.dispatch("getState")
})
.catch(() => {
this.instanceString = "Error connecting to instance"
})
}
} }
}, },
mounted() { mounted() {
if (this.$store.state.user?.id) { if (this.$store.state.user?.id) {
this.$router.push("/") this.$router.push("/")
} }
this.testInstance()
},
watch: {
instance() {
this.testInstance()
}
} }
} }
</script> </script>

View file

@ -0,0 +1 @@
Object.defineProperty(window, "isElectron", { get: () => true })

File diff suppressed because it is too large Load diff