diff --git a/backend/migrations/20220806071918-notifications.js b/backend/migrations/20220806071918-notifications.js new file mode 100644 index 0000000..f66ff81 --- /dev/null +++ b/backend/migrations/20220806071918-notifications.js @@ -0,0 +1,20 @@ +"use strict" + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.addColumn("ChatAssociations", "notifications", { + type: Sequelize.ENUM(["all", "none", "mentions"]), + defaultValue: "all", + allowNull: false + }) + }, + + async down(queryInterface, Sequelize) { + /** + * Add reverting commands here. + * + * Example: + * await queryInterface.dropTable('users'); + */ + } +} diff --git a/backend/models/chatassociations.js b/backend/models/chatassociations.js index 163d369..51418ea 100644 --- a/backend/models/chatassociations.js +++ b/backend/models/chatassociations.js @@ -45,6 +45,11 @@ module.exports = (sequelize, DataTypes) => { updatedAt: { allowNull: false, type: DataTypes.DATE + }, + notifications: { + type: DataTypes.ENUM(["all", "none", "mentions"]), + defaultValue: "all", + allowNull: false } }, { diff --git a/backend/routes/communications.js b/backend/routes/communications.js index 92a90e7..f74361b 100644 --- a/backend/routes/communications.js +++ b/backend/routes/communications.js @@ -142,7 +142,13 @@ async function createMessage(req, type, content, association, userId) { associationId: user.dataValues.id, keyId: `${ message.dataValues.id - }-${message.dataValues.updatedAt.toISOString()}` + }-${message.dataValues.updatedAt.toISOString()}`, + notify: + association.notifications === "all" || + (association.notifications === "mentions" && + message.content + .toLowerCase() + .includes(association.user.username.toLowerCase())) }) }) } @@ -771,6 +777,28 @@ router.put("/:id", auth, async (req, res, next) => { } }) +router.put("/settings/:id", auth, async (req, res, next) => { + try { + const io = req.app.get("io") + const association = await ChatAssociation.findOne({ + where: { + userId: req.user.id, + id: req.params.id + } + }) + if (association) { + await association.update({ + notifications: req.body.notifications + }) + res.sendStatus(204) + } else { + throw Errors.invalidParameter("chat association id") + } + } catch (err) { + next(err) + } +}) + router.put("/:id/message/edit", auth, async (req, res, next) => { try { const io = req.app.get("io") @@ -1017,7 +1045,6 @@ router.post( attributes: [ "username", "name", - "avatar", "id", "createdAt", @@ -1032,7 +1059,6 @@ router.post( attributes: [ "username", "name", - "avatar", "id", "createdAt", @@ -1111,7 +1137,6 @@ router.post( attributes: [ "username", "name", - "avatar", "id", "createdAt", @@ -1141,7 +1166,6 @@ router.post( attributes: [ "username", "name", - "avatar", "id", "createdAt", @@ -1156,7 +1180,6 @@ router.post( attributes: [ "username", "name", - "avatar", "id", "createdAt", @@ -1187,7 +1210,6 @@ router.post( attributes: [ "username", "name", - "avatar", "id", "createdAt", @@ -1221,7 +1243,13 @@ router.post( }) }, associationId: association.id, - keyId: `${message.id}-${message.updatedAt.toISOString()}` + keyId: `${message.id}-${message.updatedAt.toISOString()}`, + notify: + association.notifications === "all" || + (association.notifications === "mentions" && + message.content + .toLowerCase() + .includes(association.user.username.toLowerCase())) }) } res.json({ @@ -1288,12 +1316,10 @@ router.post("/:id/message", auth, limiter, async (req, res, next) => { attributes: [ "username", "name", - "avatar", "id", "createdAt", "updatedAt", - "bot" ] } @@ -1305,12 +1331,10 @@ router.post("/:id/message", auth, limiter, async (req, res, next) => { attributes: [ "username", "name", - "avatar", "id", "createdAt", "updatedAt", - "bot" ] } @@ -1376,12 +1400,10 @@ router.post("/:id/message", auth, limiter, async (req, res, next) => { attributes: [ "username", "name", - "avatar", "id", "createdAt", "updatedAt", - "bot" ], include: [ @@ -1412,12 +1434,10 @@ router.post("/:id/message", auth, limiter, async (req, res, next) => { attributes: [ "username", "name", - "avatar", "id", "createdAt", "updatedAt", - "bot" ] } @@ -1429,12 +1449,10 @@ router.post("/:id/message", auth, limiter, async (req, res, next) => { attributes: [ "username", "name", - "avatar", "id", "createdAt", "updatedAt", - "bot" ], include: [ @@ -1462,12 +1480,10 @@ router.post("/:id/message", auth, limiter, async (req, res, next) => { attributes: [ "username", "name", - "avatar", "id", "createdAt", "updatedAt", - "bot" ] } @@ -1492,7 +1508,13 @@ router.post("/:id/message", auth, limiter, async (req, res, next) => { }) }, associationId: association.id, - keyId: `${message.id}-${message.updatedAt.toISOString()}` + keyId: `${message.id}-${message.updatedAt.toISOString()}`, + notify: + association.notifications === "all" || + (association.notifications === "mentions" && + message.content + .toLowerCase() + .includes(association.user.username.toLowerCase())) }) } res.json({ diff --git a/frontend/package.json b/frontend/package.json index aef9d47..bf7237c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "colubrina", - "version": "1.0.15", + "version": "1.0.16", "private": true, "author": "Troplo ", "scripts": { diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 865ad15..5634ee8 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -547,17 +547,20 @@ export default { ) }) this.$socket.on("message", (message) => { - this.$store.state.communicationNotifications += 1 - this.$store.state.chats.find( - (chat) => chat.id === message.associationId - ).unread += 1 + if (message.notify) { + 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 && parseInt(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()) + !document.hasFocus() && + message.notify) ) { if (localStorage.getItem("messageAudio")) { if (JSON.parse(localStorage.getItem("messageAudio"))) { diff --git a/frontend/src/assets/styles.css b/frontend/src/assets/styles.css index 848e30c..ddd6c85 100644 --- a/frontend/src/assets/styles.css +++ b/frontend/src/assets/styles.css @@ -1,3 +1,6 @@ +.mentioned-message { + box-shadow: -2px 0 0 0 var(--v-primary-base); +} .offset-message { padding-left: 53px; } diff --git a/frontend/src/components/CommsInput.vue b/frontend/src/components/CommsInput.vue index 1f2562f..5bb716c 100644 --- a/frontend/src/components/CommsInput.vue +++ b/frontend/src/components/CommsInput.vue @@ -37,6 +37,11 @@ @keyup.esc="handleEsc" @click:append-outer="handleMessage()" @keydown.enter.exact.prevent="handleMessage()" + @keydown.tab.exact.prevent="tabCompletion()" + @input=" + completions = [] + completionWord = '' + " v-model="message" @paste="handlePaste" rows="1" @@ -110,10 +115,51 @@ export default { return { message: "", file: null, - blobURL: null + blobURL: null, + mentions: false, + users: [], + completions: [], + completionWord: "" } }, methods: { + tabCompletion() { + if (!this.completions.length) { + const word = this.message.split(" ").pop().toLowerCase() + const user = this.chat.chat.users.find((u) => + u.username.toLowerCase().startsWith(word) + ) + if (user) { + if (word.length) { + this.message = this.message.replace( + this.message.split(" ").pop(), + user.username + ", " + ) + } else { + this.message = this.message + user.username + ", " + } + this.$nextTick(() => { + this.completionWord = this.message.split(" ").pop() + this.completions.push(user.username) + }) + } + } else { + const user = this.chat.chat.users.find( + (u) => + u.username.toLowerCase().startsWith(this.completionWord) && + !this.completions.includes(u.username) + ) + if (user) { + this.message = this.message.replace( + this.completions[this.completions.length - 1] + ", ", + user.username + ", " + ) + this.$nextTick(() => { + this.completions.push(user.username) + }) + } + } + }, handleEditMessage() { if (!this.message?.length) { this.editLastMessage() diff --git a/frontend/src/components/Header.vue b/frontend/src/components/Header.vue index 72239bc..3bf4bc4 100644 --- a/frontend/src/components/Header.vue +++ b/frontend/src/components/Header.vue @@ -11,8 +11,58 @@ absolute offset-y class="rounded-l" + style="z-index: 20" > + + +
+ + + All messages + mdi-check + + + + Mentions only + mdi-check + + Mentions are performed when your username is sent in the + chat. + + + + None + mdi-check + + +
+
{ + this.context.user.raw.notifications = value + this.$store.dispatch("getChats") + }) + .catch((e) => { + AjaxErrorHandler(this.$store)(e) + }) + }, groupSettings(id) { this.settings.item = this.$store.state.chats.find( (chat) => chat.id === id @@ -717,14 +780,14 @@ export default { this.leave.item = this.$store.state.chats.find((chat) => chat.id === id) this.leave.dialog = true }, - show(e, context, item, id, state) { + show(e, context, item, raw, state) { if (!state) { e.preventDefault() this.context[context].value = false this.context[context].x = e.clientX this.context[context].y = e.clientY this.context[context].item = item - this.context[context].id = id + this.context[context].raw = raw this.$nextTick(() => { this.context[context].value = true }) @@ -734,7 +797,7 @@ export default { this.$store.state.context[context].x = e.clientX this.$store.state.context[context].y = e.clientY this.$store.state.context[context].item = item - this.$store.state.context[context].id = id + this.$store.state.context[context].raw = raw this.$nextTick(() => { this.$store.state.context[context].value = true }) diff --git a/frontend/src/components/Message.vue b/frontend/src/components/Message.vue index f025038..06a6523 100644 --- a/frontend/src/components/Message.vue +++ b/frontend/src/components/Message.vue @@ -39,7 +39,8 @@ :key="message.keyId" :class="{ 'message-hover': hover, - 'pa-0': $vuetify.breakpoint.mobile + 'pa-0': $vuetify.breakpoint.mobile, + 'mentioned-message': mentioned }" :id="'message-' + index" @contextmenu="show($event, 'message', message)" @@ -894,6 +895,13 @@ export default { } } }, + computed: { + mentioned() { + return this.message.content + .toLowerCase() + .includes(this.$store.state.user.username.toLowerCase()) + } + }, methods: { pinMessage() { this.axios diff --git a/frontend/vue.config.js b/frontend/vue.config.js index 642b3ac..5b6e04d 100644 --- a/frontend/vue.config.js +++ b/frontend/vue.config.js @@ -96,6 +96,7 @@ module.exports = { `dist/**/*`, `node_modules/**/*`, `package.json`, + `src/background.js`, `background.js` ], appId: "com.troplo.colubrina", @@ -103,7 +104,11 @@ module.exports = { publish: ["github"] }, linux: { - publish: ["github"] + publish: ["github"], + target: ["deb", "AppImage", "rpm", "zip", "tar.gz", "pacman"], + category: "Network", + synopsis: "Instant Messaging", + maintainer: "Troplo " }, mac: { publish: ["github"]