1. notification via SW 2.manifest json for home screen app 3. icons for mobile/desktop shortcut app

This commit is contained in:
10G Meow 2023-07-17 15:00:40 +03:00
parent 44dd4e9562
commit 2087c2897f
19 changed files with 217 additions and 51 deletions

1
.gitignore vendored
View file

@ -1,6 +1,7 @@
.DS_Store .DS_Store
node_modules node_modules
/dist /dist
*.pem
# local env files # local env files

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

BIN
public/icons/icon-72x72.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

BIN
public/icons/icon-96x96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View file

@ -6,6 +6,16 @@
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" id="favicon" href="<%= BASE_URL %>favicon.ico"> <link rel="icon" id="favicon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title> <title><%= htmlWebpackPlugin.options.title %></title>
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-72x72.png" sizes="72x72">
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-96x96.png" sizes="96x96">
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-128x128.png" sizes="128x128">
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-144x144.png" sizes="144x144">
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-152x152.png" sizes="152x152">
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-192x192.png" sizes="192x192">
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-384x384.png" sizes="384x384">
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-512x512.png" sizes="512x512">
<link rel="manifest" href="<%= BASE_URL %>manifest.json">
</head> </head>
<body> <body>
<noscript> <noscript>

52
public/manifest.json Normal file
View file

@ -0,0 +1,52 @@
{
"id": "/",
"start_url": "/",
"name": "Convene - Chat for everyone ",
"short_name": "Convene",
"theme_color": "#FFFFFF",
"background_color": "#FFFFFF",
"display": "standalone",
"icons": [
{
"src": "/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"splash_pages": null
}

0
public/sw.js Normal file
View file

View file

@ -31,7 +31,7 @@
<script> <script>
import stickers from "./plugins/stickers"; import stickers from "./plugins/stickers";
import logoMixin from "./components/logoMixin"; import { notificationCount } from "./plugins/notificationAndServiceWorker.js"
export default { export default {
name: "App", name: "App",
@ -42,7 +42,6 @@ export default {
availableJsonTranslation: null availableJsonTranslation: null
} }
}, },
mixins: [logoMixin],
beforeMount() { beforeMount() {
this.setDefaultLanguage(); this.setDefaultLanguage();
}, },
@ -113,43 +112,10 @@ export default {
// Set language // Set language
this.$i18n.locale = this.$store.state.language || "en"; this.$i18n.locale = this.$store.state.language || "en";
},
showNotification() {
if(document.visibilityState === "visible") {
return;
}
const title = this.$t('notification.title');
const notification = new Notification(title, {icon: this.logotype});
notification.onclick = () => {
notification.close();
window.parent.focus();
}
},
requestAndShowPermission(notificationCount) {
Notification.requestPermission(function (permission) {
if(notificationCount > 0 && permission === "granted") {
this.showNotification();
}
});
},
requestNotificationPermission(notificationCount) {
if ('Notification' in window) {
Notification.requestPermission().then((permission) => {
if(notificationCount > 0 && permission === 'granted') {
this.showNotification();
} else if(permission === "default") {
this.requestAndShowPermission(notificationCount);
} else {
this.requestAndShowPermission(notificationCount);
}
});
}
} }
}, },
computed: { computed: {
notificationCount() { notificationCount,
return this.$matrix.notificationCount
},
currentUser() { currentUser() {
return this.$store.state.auth.user; return this.$store.state.auth.user;
}, },
@ -217,13 +183,8 @@ export default {
document.getElementById("favicon").setAttribute('href', favicon); document.getElementById("favicon").setAttribute('href', favicon);
}, },
immediate: true, immediate: true,
},
notificationCount: {
handler(nCount) {
this.requestNotificationPermission(nCount)
} }
} }
},
}; };
</script> </script>

View file

@ -17,7 +17,9 @@
"minutes": "1 minute ago | {n} minutes ago", "minutes": "1 minute ago | {n} minutes ago",
"hours": "1 hour ago | {n} hours ago", "hours": "1 hour ago | {n} hours ago",
"days": "1 day ago | {n} days ago" "days": "1 day ago | {n} days ago"
} },
"close": "close",
"notify": "Notify"
}, },
"menu": { "menu": {
"start_private_chat": "Private chat with this user", "start_private_chat": "Private chat with this user",
@ -353,7 +355,13 @@
"export_filename": "Exported chat {date}" "export_filename": "Exported chat {date}"
}, },
"notification": { "notification": {
"title": "New message received" "title": "New message received",
"dialog" : {
"title": "Stay Connected with Chat Notifications!",
"body": "Never miss a message or important conversation again! Be notified whenever someone sends you a message or replies to your chat.",
"enable": "Enable"
},
"blocked_message": "Notifications blocked. Please reset the permissions"
}, },
"emoji": { "emoji": {
"search": "Search...", "search": "Search...",

View file

@ -4,6 +4,7 @@
no-gutters no-gutters
align-content="center" align-content="center"
v-on="$listeners" v-on="$listeners"
v-show="icon === 'notifications_active' ? this.windowNotificationPermission() !== 'granted' : true"
> >
<v-col cols="auto" class="me-2"> <v-col cols="auto" class="me-2">
<v-icon :size="iconSize">{{ icon }}</v-icon> <v-icon :size="iconSize">{{ icon }}</v-icon>
@ -13,6 +14,8 @@
</template> </template>
<script> <script>
import { windowNotificationPermission } from "../plugins/notificationAndServiceWorker.js"
export default { export default {
name: "ActionRow", name: "ActionRow",
props: { props: {
@ -35,6 +38,9 @@ export default {
}, },
}, },
}, },
methods: {
windowNotificationPermission
}
}; };
</script> </script>

View file

@ -1,6 +1,10 @@
<template> <template>
<div class="chat-root fill-height d-flex flex-column"> <div class="chat-root fill-height d-flex flex-column">
<ChatHeader class="chat-header flex-grow-0 flex-shrink-0" v-on:header-click="onHeaderClick" v-on:view-room-details="viewRoomDetails" v-if="!useFileModeNonAdmin" /> <ChatHeader class="chat-header flex-grow-0 flex-shrink-0"
v-on:header-click="onHeaderClick"
v-on:view-room-details="viewRoomDetails"
v-on:notify="onNotificationDialog"
v-if="!useFileModeNonAdmin" />
<AudioLayout ref="chatContainer" class="auto-audio-player-root" v-if="useVoiceMode" :room="room" <AudioLayout ref="chatContainer" class="auto-audio-player-root" v-if="useVoiceMode" :room="room"
:events="events" :autoplay="!showRecorder" :events="events" :autoplay="!showRecorder"
:timelineSet="timelineSet" :timelineSet="timelineSet"
@ -282,6 +286,46 @@
</v-dialog> </v-dialog>
<CreatePollDialog :show="showCreatePollDialog" @close="showCreatePollDialog = false" /> <CreatePollDialog :show="showCreatePollDialog" @close="showCreatePollDialog = false" />
<!-- Dialog for request Notification and register service worker-->
<v-dialog
v-model="notificationDialog"
persistent
class="ma-0 pa-0"
:width="$vuetify.breakpoint.smAndUp ? '688px' : '95%'"
>
<div class="dialog-content text-center">
<v-icon size="30">notifications_active</v-icon>
<h2 class="dialog-title">
{{ $t("notification.dialog.title") }}
</h2>
<div class="dialog-text">{{ $t("notification.dialog.body") }}</div>
<v-container fluid>
<v-row cols="12">
<v-col cols="6">
<v-btn
depressed
text
block
class="text-button"
@click="notificationDialog = false"
>{{ $t("global.close") }}</v-btn
>
</v-col>
<v-col cols="6" align="center">
<v-btn
color="primary"
depressed
block
class="filled-button"
@click.stop="onRequestNotificationAndServiceWorker"
>{{ $t("notification.dialog.enable") }}</v-btn
>
</v-col>
</v-row>
</v-container>
</div>
</v-dialog>
</div> </div>
</template> </template>
@ -304,6 +348,8 @@ import CreatePollDialog from "./CreatePollDialog.vue";
import chatMixin from "./chatMixin"; import chatMixin from "./chatMixin";
import AudioLayout from "./AudioLayout.vue"; import AudioLayout from "./AudioLayout.vue";
import FileDropLayout from "./file_mode/FileDropLayout"; import FileDropLayout from "./file_mode/FileDropLayout";
import { requestNotificationAndServiceWorker, windowNotificationPermission, notificationCount } from "../plugins/notificationAndServiceWorker.js"
import logoMixin from "./logoMixin";
const sizeOf = require("image-size"); const sizeOf = require("image-size");
const dataUriToBuffer = require("data-uri-to-buffer"); const dataUriToBuffer = require("data-uri-to-buffer");
@ -339,7 +385,7 @@ ScrollPosition.prototype.prepareFor = function (direction) {
export default { export default {
name: "Chat", name: "Chat",
mixins: [chatMixin], mixins: [chatMixin, logoMixin],
components: { components: {
ChatHeader, ChatHeader,
MessageOperations, MessageOperations,
@ -433,7 +479,8 @@ export default {
Symbols: this.$t("emoji.categories.symbols"), Symbols: this.$t("emoji.categories.symbols"),
Places: this.$t("emoji.categories.places") Places: this.$t("emoji.categories.places")
} }
} },
notificationDialog: false
}; };
}, },
@ -470,6 +517,7 @@ export default {
}, },
computed: { computed: {
notificationCount,
nonImageFiles() { nonImageFiles() {
return this.isCurrentFileInputsAnArray && this.currentFileInputs.filter(file => !file.type.includes("image/")) return this.isCurrentFileInputsAnArray && this.currentFileInputs.filter(file => !file.type.includes("image/"))
}, },
@ -636,6 +684,13 @@ export default {
}, },
watch: { watch: {
notificationCount: {
handler(nCount) {
if (nCount > 0 && this.windowNotificationPermission() === "granted") {
this.showNotification()
}
}
},
initialLoadDone: { initialLoadDone: {
immediate: true, immediate: true,
handler(value, oldValue) { handler(value, oldValue) {
@ -726,6 +781,34 @@ export default {
}, },
methods: { methods: {
windowNotificationPermission,
showNotification() {
if(document.visibilityState === "visible") {
return;
}
const title = this.$t('notification.title');
const self = this;
navigator.serviceWorker.ready.then(function(registration) {
registration.showNotification(title, {
icon: self.logotype,
tag: "new-message-notification",
});
});
},
onNotificationDialog() {
if(this.windowNotificationPermission() === 'denied') {
alert(this.$t("notification.blocked_message"));
} else if(this.windowNotificationPermission() === 'default') {
this.notificationDialog = true;
}
},
onRequestNotificationAndServiceWorker() {
requestNotificationAndServiceWorker()
this.notificationDialog = false;
},
onRoomJoined(initialEventId) { onRoomJoined(initialEventId) {
// Was this room just created (by you)? Show a small info header in // Was this room just created (by you)? Show a small info header in
// that case! // that case!

View file

@ -177,6 +177,11 @@ export default {
this.$emit("view-room-details", { event: this.event }); this.$emit("view-room-details", { event: this.event });
} }
}); });
items.push({
icon: 'notifications_active', text: this.$t('global.notify'), handler: () => {
this.$emit("notify");
}
});
items.push({ items.push({
icon: '$vuetify.icons.ic_member-leave', text: this.$t('leave.leave'), handler: () => { icon: '$vuetify.icons.ic_member-leave', text: this.$t('leave.leave'), handler: () => {
this.leaveRoom(); this.leaveRoom();

View file

@ -0,0 +1,29 @@
const registerServiceWorker = async () => {
const swRegistration = await navigator.serviceWorker.register("/sw.js");
return swRegistration;
};
const requestNotificationPermission = async () => {
// return value: 'granted', 'default', 'denied'
return await window.Notification.requestPermission();
};
export async function requestNotificationAndServiceWorker() {
if (!("serviceWorker" in navigator)) {
throw new Error("No Service Worker support!");
}
if (!("PushManager" in window)) {
throw new Error("No Push API Support!");
}
const permission = await requestNotificationPermission();
if(permission==='granted') await registerServiceWorker();
return permission
}
export function windowNotificationPermission() {
return window.Notification.permission
}
export function notificationCount() {
return this.$matrix.notificationCount
}

View file

@ -1,5 +1,6 @@
const CopyWebpackPlugin = require("copy-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin");
const webpack = require("webpack"); const webpack = require("webpack");
//const fs = require('fs')
module.exports = { module.exports = {
transpileDependencies: ["vuetify"], transpileDependencies: ["vuetify"],
@ -47,5 +48,15 @@ module.exports = {
devServer: { devServer: {
//https: true, //https: true,
},
/***
* For testing notification via service worker in Mobile
* Run your site locally with secure HTTPS using mkcert
* https://web.dev/how-to-use-local-https/#running-your-site-locally-with-https-using-mkcert-recommended
*/
// https: {
// key: fs.readFileSync('./your-local-ip-address-key.pem'),
// cert: fs.readFileSync('./your-local-ip-address.pem'),
// }
}
}; };