This commit is contained in:
Troplo 2022-07-30 17:45:29 +10:00
parent d80c7a76e6
commit 3d7df1c98a
13 changed files with 224 additions and 103 deletions

View file

@ -5,3 +5,4 @@ NOTIFICATION=
NOTIFICATION_TYPE=info NOTIFICATION_TYPE=info
SITE_NAME=Colubrina SITE_NAME=Colubrina
ALLOW_REGISTRATIONS=true ALLOW_REGISTRATIONS=true
PUBLIC_USERS=false

View file

@ -30,7 +30,8 @@ app.get("/api/v1/state", async (req, res) => {
notificationType: process.env.NOTIFICATION_TYPE, notificationType: process.env.NOTIFICATION_TYPE,
latestVersion: require("../frontend/package.json").version, latestVersion: require("../frontend/package.json").version,
name: process.env.SITE_NAME, name: process.env.SITE_NAME,
allowRegistrations: JSON.parse(process.env.ALLOW_REGISTRATIONS) allowRegistrations: process.env.ALLOW_REGISTRATIONS === "true",
publicUsers: process.env.PUBLIC_USERS === "true"
}) })
}) })

View file

@ -348,22 +348,26 @@ router.get("/mutual/:id/groups", auth, async (req, res, next) => {
router.get("/users", auth, async (req, res, next) => { router.get("/users", auth, async (req, res, next) => {
try { try {
const users = await User.findAll({ if (process.env.PUBLIC_USERS === "true") {
attributes: [ const users = await User.findAll({
"id", attributes: [
"username", "id",
"name", "username",
"avatar", "name",
"createdAt", "avatar",
"updatedAt", "createdAt",
"status", "updatedAt",
"admin" "status",
], "admin"
where: { ],
banned: false where: {
} banned: false
}) }
res.json(users) })
res.json(users)
} else {
res.json([])
}
} catch (err) { } catch (err) {
next(err) next(err)
} }
@ -1361,9 +1365,26 @@ router.get("/:id/messages", auth, async (req, res, next) => {
] ]
}) })
if (chat) { if (chat) {
let or
if (parseInt(req.query.offset)) {
or = {
[Op.or]: [
{
id: {
[Op.lt]: parseInt(req.query.offset)
? parseInt(req.query.offset)
: 0
}
}
]
}
} else {
or = {}
}
const messages = await Message.findAll({ const messages = await Message.findAll({
where: { where: {
chatId: chat.chat.id chatId: chat.chat.id,
...or
}, },
include: [ include: [
{ {
@ -1422,7 +1443,6 @@ router.get("/:id/messages", auth, async (req, res, next) => {
] ]
} }
], ],
offset: parseInt(req.query.offset) || 0,
order: [["id", "DESC"]], order: [["id", "DESC"]],
limit: 50 limit: 50
}) })

View file

@ -192,7 +192,7 @@ router.post("/register", async (req, res, next) => {
} }
} }
try { try {
if (!JSON.parse(process.env.ALLOW_REGISTRATIONS)) { if (process.env.ALLOW_REGISTRATIONS !== "true") {
throw Errors.registrationsDisabled throw Errors.registrationsDisabled
} }
if (req.body.password.length < 8) { if (req.body.password.length < 8) {

View file

@ -6,7 +6,7 @@ const { Sequelize } = require("sequelize")
const argon2 = require("argon2") const argon2 = require("argon2")
const axios = require("axios") const axios = require("axios")
const os = require("os") const os = require("os")
const { execSync } = require('child_process'); const { execSync } = require("child_process")
console.log("Troplo/Colubrina CLI") console.log("Troplo/Colubrina CLI")
console.log("Colubrina version", require("../frontend/package.json").version) console.log("Colubrina version", require("../frontend/package.json").version)
@ -15,12 +15,19 @@ async function checkForUpdates() {
await axios await axios
.get("https://services.troplo.com/api/v1/state", { .get("https://services.troplo.com/api/v1/state", {
headers: { headers: {
"X-Troplo-Project": "colubrina" "X-Troplo-Project": "colubrina",
"X-Troplo-Project-Version": require("../frontend/package.json")
.version
}, },
timeout: 1000 timeout: 1000
}) })
.then((res) => { .then((res) => {
if (require("../frontend/package.json").version !== res.data.latestVersion) { if (res.data.warning) {
console.log(res.data.warning)
}
if (
require("../frontend/package.json").version !== res.data.latestVersion
) {
console.log("A new version of Colubrina is available!") console.log("A new version of Colubrina is available!")
console.log("Latest version:", res.data.latestVersion) console.log("Latest version:", res.data.latestVersion)
} else { } else {
@ -139,7 +146,7 @@ async function dbSetup() {
} }
async function runMigrations() { async function runMigrations() {
console.log("Running migrations") console.log("Running migrations")
execSync("cd ../backend && sequelize db:migrate", () => { execSync("cd ../backend && sequelize db:migrate", () => {
console.log("Migrations applied") console.log("Migrations applied")
}) })
} }
@ -218,6 +225,12 @@ async function configureDotEnv() {
default: false default: false
}) })
) )
setEnvValue(
"PUBLIC_USERS",
await input.text("Show instance users publicly", {
default: false
})
)
setEnvValue("NOTIFICATION", "") setEnvValue("NOTIFICATION", "")
setEnvValue("NOTIFICATION_TYPE", "info") setEnvValue("NOTIFICATION_TYPE", "info")
setEnvValue("RELEASE", "stable") setEnvValue("RELEASE", "stable")
@ -240,8 +253,8 @@ async function init() {
execSync("cd ../backend && yarn install --frozen-lockfile", () => { execSync("cd ../backend && yarn install --frozen-lockfile", () => {
console.log("yarn install complete (backend)") console.log("yarn install complete (backend)")
}) })
execSync("cd ../frontend && yarn install --frozen-lockfile", () => { execSync("cd ../frontend && yarn install --frozen-lockfile", () => {
console.log("yarn install complete (frontend)") console.log("yarn install complete (frontend)")
}) })
if (fs.existsSync(path.join(__dirname, "../backend/.env"))) { if (fs.existsSync(path.join(__dirname, "../backend/.env"))) {
const option = await input.confirm(".env already exists, overwrite?", { const option = await input.confirm(".env already exists, overwrite?", {
@ -253,7 +266,9 @@ async function init() {
} else { } else {
await configureDotEnv() await configureDotEnv()
} }
if (fs.existsSync(path.join(__dirname, "../backend/config/config.json"))) { if (
fs.existsSync(path.join(__dirname, "../backend/config/config.json"))
) {
const option = await input.select( const option = await input.select(
`config/config.json already exists. Do you want to overwrite it?`, `config/config.json already exists. Do you want to overwrite it?`,
["Yes", "No"] ["Yes", "No"]
@ -315,11 +330,14 @@ async function init() {
await runMigrations() await runMigrations()
} else if (option === "Check for updates") { } else if (option === "Check for updates") {
await checkForUpdates() await checkForUpdates()
} else if(option === "Build frontend for production") { } else if (option === "Build frontend for production") {
console.log("Building...") console.log("Building...")
execSync("cd ../frontend && yarn install --frozen-lockfile && yarn build", () => { execSync(
console.log("yarn build complete") "cd ../frontend && yarn install --frozen-lockfile && yarn build",
}) () => {
console.log("yarn build complete")
}
)
} else if (option === "Exit") { } else if (option === "Exit") {
process.exit(0) process.exit(0)
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "colubrina-chat", "name": "colubrina-chat",
"version": "1.0.5", "version": "1.0.6",
"private": true, "private": true,
"author": "Troplo <troplo@troplo.com>", "author": "Troplo <troplo@troplo.com>",
"license": "GPL-3.0", "license": "GPL-3.0",

View file

@ -386,8 +386,9 @@
{{ $store.state.site.name }} QuickSwitcher {{ $store.state.site.name }} QuickSwitcher
</v-toolbar-title> </v-toolbar-title>
</v-toolbar> </v-toolbar>
<v-container> <v-container v-if="$store.state.modals.search">
<v-autocomplete <v-autocomplete
@keydown.enter="handleEnter"
auto-select-first auto-select-first
v-model="search" v-model="search"
:items="$store.state.quickSwitchCache" :items="$store.state.quickSwitchCache"
@ -405,7 +406,10 @@
<v-main> <v-main>
<Header></Header> <Header></Header>
<v-container <v-container
v-if="$store.state.site.latestVersion > $store.state.versioning.version" v-if="
$store.state.site.latestVersion > $store.state.versioning.version &&
$store.state.site.release !== 'dev'
"
id="update-notify-banner" id="update-notify-banner"
> >
<v-alert class="mx-4 rounded-xl" type="info" text dismissible> <v-alert class="mx-4 rounded-xl" type="info" text dismissible>
@ -506,6 +510,16 @@ export default {
} }
}, },
methods: { methods: {
handleEnter() {
if (
!this.searchInput &&
this.$store.state.lastRoute &&
this.$store.state.lastRoute !== this.$route.path
) {
this.$router.push(this.$store.state.lastRoute)
this.$store.state.modals.search = false
}
},
registerSocket() { registerSocket() {
if (!this.$store.state.wsRegistered) { if (!this.$store.state.wsRegistered) {
this.$store.state.wsRegistered = true this.$store.state.wsRegistered = true
@ -797,8 +811,9 @@ export default {
}, },
deep: true deep: true
}, },
$route(to) { $route(to, from) {
document.title = to.name + " - " + this.$store.state.site.name document.title = to.name + " - " + this.$store.state.site.name
this.$store.state.lastRoute = from.path
}, },
search() { search() {
if (this.search) { if (this.search) {

View file

@ -35,7 +35,6 @@
@keydown.enter.exact.prevent="handleMessage()" @keydown.enter.exact.prevent="handleMessage()"
v-model="message" v-model="message"
@paste="handlePaste" @paste="handlePaste"
@change="handleChange"
rows="1" rows="1"
single-line single-line
dense dense
@ -112,7 +111,7 @@ export default {
}, },
methods: { methods: {
handleEditMessage() { handleEditMessage() {
if(!this.message?.length) { if (!this.message?.length) {
this.editLastMessage() this.editLastMessage()
} }
}, },
@ -283,6 +282,11 @@ export default {
if (this.edit) { if (this.edit) {
this.message = this.edit.content this.message = this.edit.content
} }
},
watch: {
message() {
this.handleChange()
}
} }
} }
</script> </script>

View file

@ -669,7 +669,8 @@ export default {
"endEdit", "endEdit",
"autoScroll", "autoScroll",
"index", "index",
"show" "show",
"setImagePreview"
], ],
components: { components: {
CommsInput CommsInput

View file

@ -92,6 +92,10 @@ Vue.directive("markdown", {
el.innerHTML = md.render(el.innerHTML) el.innerHTML = md.render(el.innerHTML)
} }
}) })
if (process.env.NODE_ENV === "development") {
console.log("Colubrina is running in development mode.")
Vue.config.performance = true
}
new Vue({ new Vue({
router, router,
store, store,

View file

@ -29,6 +29,7 @@ export default new Vuex.Store({
selectedChat: null, selectedChat: null,
chats: [], chats: [],
baseURL: process.env.VUE_APP_BASE_URL, baseURL: process.env.VUE_APP_BASE_URL,
lastRoute: null,
versioning: { versioning: {
date: process.env.VUE_APP_BUILD_DATE, date: process.env.VUE_APP_BUILD_DATE,
version: process.env.VUE_APP_VERSION, version: process.env.VUE_APP_VERSION,

View file

@ -126,38 +126,37 @@
elevation="0" elevation="0"
> >
<v-card-text class="flex-grow-1 overflow-y-auto" id="message-list"> <v-card-text class="flex-grow-1 overflow-y-auto" id="message-list">
<v-list two-line color="card" ref="message-list"> <v-card-title v-if="reachedTop">
<v-card-title v-if="reachedTop"> Welcome to the start of
Welcome to the start of {{
{{ $store.state.selectedChat?.chat?.type === "direct"
$store.state.selectedChat?.chat?.type === "direct" ? getDirectRecipient($store.state.selectedChat).username
? getDirectRecipient($store.state.selectedChat).username : $store.state.selectedChat?.chat?.name
: $store.state.selectedChat?.chat?.name }}
}} </v-card-title>
</v-card-title> <v-progress-circular
<v-progress-circular v-if="loadingMessages"
v-if="loadingMessages" indeterminate
indeterminate size="64"
size="128" style="display: block; width: 100px; margin: 0 auto"
class="justify-center" ></v-progress-circular>
></v-progress-circular> <template v-for="(message, index) in messages">
<template v-for="(message, index) in messages"> <Message
<Message :key="message.keyId"
:key="message.keyId" :message="message"
:message="message" :jump-to-message="jumpToMessage"
:jump-to-message="jumpToMessage" :edit="edit"
:edit="edit" :focus-input="focusInput"
:focus-input="focusInput" :replying="setReply"
:replying="setReply" :get-name="getName"
:get-name="getName" :end-edit="endEdit"
:end-edit="endEdit" :auto-scroll="autoScroll"
:auto-scroll="autoScroll" :chat="chat"
:chat="chat" :index="index"
:index="index" :show="show"
:show="show" :set-image-preview="setImagePreview"
></Message> ></Message>
</template> </template>
</v-list>
</v-card-text> </v-card-text>
<v-card-text> <v-card-text>
<v-toolbar <v-toolbar
@ -167,7 +166,6 @@
color="card" color="card"
v-if="replying" v-if="replying"
style="cursor: pointer; overflow: hidden" style="cursor: pointer; overflow: hidden"
class="mb-2"
> >
<v-icon class="mr-2">mdi-reply</v-icon> <v-icon class="mr-2">mdi-reply</v-icon>
<v-avatar size="24" class="mr-2"> <v-avatar size="24" class="mr-2">
@ -194,17 +192,29 @@
<v-icon> mdi-close </v-icon> <v-icon> mdi-close </v-icon>
</v-btn> </v-btn>
</v-toolbar> </v-toolbar>
<v-toolbar <v-fade-transition v-model="avoidAutoScroll">
height="29" <v-toolbar
color="transparent" height="22"
elevation="0" color="toolbar"
style="margin-bottom: -12px; padding-top: 0" elevation="0"
> style="
<p v-if="usersTyping.length" style="float: left"> border-radius: 20px 20px 0 0;
{{ usersTyping.map((user) => getName(user)).join(", ") }} cursor: pointer;
{{ usersTyping.length > 1 ? " are" : " is" }} typing... z-index: 50;
</p> position: relative;
</v-toolbar> top: -30px;
margin-bottom: -27px;
"
width="100%"
@click="forceBottom"
v-if="avoidAutoScroll"
>
<div>
<v-icon size="16px"> mdi-arrow-down </v-icon>
Jump to bottom...
</div>
</v-toolbar>
</v-fade-transition>
<CommsInput <CommsInput
:chat="chat" :chat="chat"
:replying="replying" :replying="replying"
@ -212,6 +222,27 @@
:autoScroll="autoScroll" :autoScroll="autoScroll"
:endSend="endSend" :endSend="endSend"
></CommsInput> ></CommsInput>
<v-fade-transition v-model="usersTyping.length">
<v-toolbar
height="22"
elevation="0"
style="
border-radius: 0 0 20px 20px;
position: relative;
margin-bottom: -2px;
margin-top: -20px;
bottom: -14px;
"
width="100%"
color="toolbar"
v-if="usersTyping.length"
>
<div style="overflow: hidden">
{{ usersTyping.map((user) => getName(user)).join(", ") }}
{{ usersTyping.length > 1 ? " are" : " is" }} typing...
</div>
</v-toolbar>
</v-fade-transition>
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-col> </v-col>
@ -812,6 +843,10 @@ export default {
} }
}, },
methods: { methods: {
forceBottom() {
this.avoidAutoScroll = false
this.autoScroll()
},
getDirectRecipient(item) { getDirectRecipient(item) {
let user = item.chat.users.find( let user = item.chat.users.find(
(user) => user.id !== this.$store.state.user.id (user) => user.id !== this.$store.state.user.id
@ -841,7 +876,9 @@ export default {
}, },
async scrollEvent(e) { async scrollEvent(e) {
this.avoidAutoScroll = this.avoidAutoScroll =
e.target.scrollTop + e.target.offsetHeight !== e.target.scrollHeight e.target.scrollHeight -
Math.round(e.target.scrollTop + e.target.offsetHeight) >
50
if ( if (
e.target.scrollTop === 0 && e.target.scrollTop === 0 &&
!this.rateLimit && !this.rateLimit &&
@ -1077,26 +1114,32 @@ export default {
this.edit.id = "" this.edit.id = ""
this.focusInput() this.focusInput()
}, },
autoScroll() { autoScroll(smooth = false) {
if (!this.avoidAutoScroll) { if (!this.avoidAutoScroll) {
this.$nextTick(() => { try {
try { const lastIndex = this.messages.length - 1
const lastIndex = this.messages.length - 1 const lastMessage = document.querySelector(`#message-${lastIndex}`)
const lastMessage = document.querySelector(`#message-${lastIndex}`) if (smooth) {
lastMessage.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "start"
})
} else {
lastMessage.scrollIntoView() lastMessage.scrollIntoView()
this.autoScrollRetry = 0
} catch (e) {
console.log("Could not auto scroll, retrying...")
if (this.autoScrollRetry < 20) {
setTimeout(() => {
this.autoScroll()
}, 50)
this.autoScrollRetry++
} else {
console.log("Could not auto scroll, retry limit reached")
}
} }
}) this.autoScrollRetry = 0
} catch (e) {
console.log("Could not auto scroll, retrying...")
if (this.autoScrollRetry < 20) {
setTimeout(() => {
this.autoScroll()
}, 50)
this.autoScrollRetry++
} else {
console.log("Could not auto scroll, retry limit reached")
}
}
} }
}, },
getMessages() { getMessages() {
@ -1107,7 +1150,7 @@ export default {
"/api/v1/communications/" + "/api/v1/communications/" +
this.$route.params.id + this.$route.params.id +
"/messages?limit=50&offset=" + "/messages?limit=50&offset=" +
this.offset this.messages[0]?.id || 0
) )
.then((res) => { .then((res) => {
if (!res.data.length) { if (!res.data.length) {
@ -1132,9 +1175,15 @@ export default {
"/read" "/read"
) )
this.markAsRead() this.markAsRead()
},
focusKey() {
if (document.activeElement.tagName === "BODY") {
this.focusInput()
}
} }
}, },
mounted() { mounted() {
document.addEventListener("keypress", this.focusKey)
document document
.getElementById("message-list") .getElementById("message-list")
.addEventListener("scroll", this.scrollEvent) .addEventListener("scroll", this.scrollEvent)
@ -1231,6 +1280,10 @@ export default {
this.offset = 0 this.offset = 0
this.getMessages() this.getMessages()
} }
},
destroyed() {
document.removeEventListener("keypress", this.focusKey)
document.removeEventListener("scroll", this.scrollEvent)
} }
} }
</script> </script>

View file

@ -16,7 +16,7 @@
<v-toolbar-title> Users </v-toolbar-title> <v-toolbar-title> Users </v-toolbar-title>
</v-toolbar> </v-toolbar>
<v-card color="card" elevation="0"> <v-card color="card" elevation="0">
<v-list color="card"> <v-list color="card" v-if="$store.state.site.publicUsers">
<v-list-item v-for="user in users" :key="user.id"> <v-list-item v-for="user in users" :key="user.id">
<v-list-item-avatar <v-list-item-avatar
@click="userProfile(user)" @click="userProfile(user)"
@ -68,6 +68,9 @@
</v-list-item-action> </v-list-item-action>
</v-list-item> </v-list-item>
</v-list> </v-list>
<v-card-title v-else
>Public users are not enabled on this instance.</v-card-title
>
</v-card> </v-card>
</v-card> </v-card>
</v-tab-item> </v-tab-item>