diff --git a/backend/.env.example b/backend/.env.example index 45bf30f..ecd5e9d 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -4,4 +4,5 @@ RELEASE=stable NOTIFICATION= NOTIFICATION_TYPE=info SITE_NAME=Colubrina -ALLOW_REGISTRATIONS=true \ No newline at end of file +ALLOW_REGISTRATIONS=true +PUBLIC_USERS=false \ No newline at end of file diff --git a/backend/index.js b/backend/index.js index 203678d..dcb9a74 100644 --- a/backend/index.js +++ b/backend/index.js @@ -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" }) }) diff --git a/backend/routes/communications.js b/backend/routes/communications.js index b89c521..97e7a2f 100644 --- a/backend/routes/communications.js +++ b/backend/routes/communications.js @@ -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 }) diff --git a/backend/routes/user.js b/backend/routes/user.js index 7306653..80a0e10 100644 --- a/backend/routes/user.js +++ b/backend/routes/user.js @@ -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) { diff --git a/cli/index.js b/cli/index.js index 6f5b1f4..0f4921b 100644 --- a/cli/index.js +++ b/cli/index.js @@ -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) } diff --git a/frontend/package.json b/frontend/package.json index 0b9b587..bb19687 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "colubrina-chat", - "version": "1.0.5", + "version": "1.0.6", "private": true, "author": "Troplo ", "license": "GPL-3.0", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 588ee31..13e6160 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -386,8 +386,9 @@ {{ $store.state.site.name }} QuickSwitcher - +
@@ -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) { diff --git a/frontend/src/components/CommsInput.vue b/frontend/src/components/CommsInput.vue index 636920e..023fbd2 100644 --- a/frontend/src/components/CommsInput.vue +++ b/frontend/src/components/CommsInput.vue @@ -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() + } } } diff --git a/frontend/src/components/Message.vue b/frontend/src/components/Message.vue index 0241a6b..5c3004f 100644 --- a/frontend/src/components/Message.vue +++ b/frontend/src/components/Message.vue @@ -669,7 +669,8 @@ export default { "endEdit", "autoScroll", "index", - "show" + "show", + "setImagePreview" ], components: { CommsInput diff --git a/frontend/src/main.js b/frontend/src/main.js index 6e80720..34f4506 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -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, diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index 14db108..1c0bbd3 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -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, diff --git a/frontend/src/views/Communications/CommunicationsChat.vue b/frontend/src/views/Communications/CommunicationsChat.vue index 47aad3b..b621043 100644 --- a/frontend/src/views/Communications/CommunicationsChat.vue +++ b/frontend/src/views/Communications/CommunicationsChat.vue @@ -126,38 +126,37 @@ elevation="0" > - - - Welcome to the start of - {{ - $store.state.selectedChat?.chat?.type === "direct" - ? getDirectRecipient($store.state.selectedChat).username - : $store.state.selectedChat?.chat?.name - }} - - - - + + Welcome to the start of + {{ + $store.state.selectedChat?.chat?.type === "direct" + ? getDirectRecipient($store.state.selectedChat).username + : $store.state.selectedChat?.chat?.name + }} + + + mdi-reply @@ -194,17 +192,29 @@ mdi-close - -

- {{ usersTyping.map((user) => getName(user)).join(", ") }} - {{ usersTyping.length > 1 ? " are" : " is" }} typing... -

-
+ + +
+ mdi-arrow-down + Jump to bottom... +
+
+
+ + +
+ {{ usersTyping.map((user) => getName(user)).join(", ") }} + {{ usersTyping.length > 1 ? " are" : " is" }} typing... +
+
+
@@ -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) } } diff --git a/frontend/src/views/Communications/CommunicationsFriends.vue b/frontend/src/views/Communications/CommunicationsFriends.vue index 02eb80c..92f255d 100644 --- a/frontend/src/views/Communications/CommunicationsFriends.vue +++ b/frontend/src/views/Communications/CommunicationsFriends.vue @@ -16,7 +16,7 @@ Users - + + Public users are not enabled on this instance.