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
SITE_NAME=Colubrina
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,
latestVersion: require("../frontend/package.json").version,
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) => {
try {
const users = await User.findAll({
attributes: [
"id",
"username",
"name",
"avatar",
"createdAt",
"updatedAt",
"status",
"admin"
],
where: {
banned: false
}
})
res.json(users)
if (process.env.PUBLIC_USERS === "true") {
const users = await User.findAll({
attributes: [
"id",
"username",
"name",
"avatar",
"createdAt",
"updatedAt",
"status",
"admin"
],
where: {
banned: false
}
})
res.json(users)
} else {
res.json([])
}
} catch (err) {
next(err)
}
@ -1361,9 +1365,26 @@ router.get("/:id/messages", auth, async (req, res, next) => {
]
})
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({
where: {
chatId: chat.chat.id
chatId: chat.chat.id,
...or
},
include: [
{
@ -1422,7 +1443,6 @@ router.get("/:id/messages", auth, async (req, res, next) => {
]
}
],
offset: parseInt(req.query.offset) || 0,
order: [["id", "DESC"]],
limit: 50
})

View file

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

View file

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

View file

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

View file

@ -386,8 +386,9 @@
{{ $store.state.site.name }} QuickSwitcher
</v-toolbar-title>
</v-toolbar>
<v-container>
<v-container v-if="$store.state.modals.search">
<v-autocomplete
@keydown.enter="handleEnter"
auto-select-first
v-model="search"
:items="$store.state.quickSwitchCache"
@ -405,7 +406,10 @@
<v-main>
<Header></Header>
<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"
>
<v-alert class="mx-4 rounded-xl" type="info" text dismissible>
@ -506,6 +510,16 @@ export default {
}
},
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() {
if (!this.$store.state.wsRegistered) {
this.$store.state.wsRegistered = true
@ -797,8 +811,9 @@ export default {
},
deep: true
},
$route(to) {
$route(to, from) {
document.title = to.name + " - " + this.$store.state.site.name
this.$store.state.lastRoute = from.path
},
search() {
if (this.search) {

View file

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

View file

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

View file

@ -92,6 +92,10 @@ Vue.directive("markdown", {
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({
router,
store,

View file

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

View file

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

View file

@ -16,7 +16,7 @@
<v-toolbar-title> Users </v-toolbar-title>
</v-toolbar>
<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-avatar
@click="userProfile(user)"
@ -68,6 +68,9 @@
</v-list-item-action>
</v-list-item>
</v-list>
<v-card-title v-else
>Public users are not enabled on this instance.</v-card-title
>
</v-card>
</v-card>
</v-tab-item>