1. notification via SW 2.manifest json for home screen app 3. icons for mobile/desktop shortcut app
1
.gitignore
vendored
|
|
@ -1,6 +1,7 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
*.pem
|
||||
|
||||
|
||||
# local env files
|
||||
|
|
|
|||
BIN
public/icons/icon-128x128.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/icons/icon-144x144.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/icons/icon-152x152.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/icons/icon-192x192.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
public/icons/icon-384x384.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
public/icons/icon-512x512.png
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
public/icons/icon-72x72.png
Normal file
|
After Width: | Height: | Size: 6 KiB |
BIN
public/icons/icon-96x96.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
|
|
@ -6,6 +6,16 @@
|
|||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" id="favicon" href="<%= BASE_URL %>favicon.ico">
|
||||
<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>
|
||||
<body>
|
||||
<noscript>
|
||||
|
|
|
|||
52
public/manifest.json
Normal 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
43
src/App.vue
|
|
@ -31,7 +31,7 @@
|
|||
|
||||
<script>
|
||||
import stickers from "./plugins/stickers";
|
||||
import logoMixin from "./components/logoMixin";
|
||||
import { notificationCount } from "./plugins/notificationAndServiceWorker.js"
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
|
|
@ -42,7 +42,6 @@ export default {
|
|||
availableJsonTranslation: null
|
||||
}
|
||||
},
|
||||
mixins: [logoMixin],
|
||||
beforeMount() {
|
||||
this.setDefaultLanguage();
|
||||
},
|
||||
|
|
@ -113,43 +112,10 @@ export default {
|
|||
|
||||
// Set language
|
||||
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: {
|
||||
notificationCount() {
|
||||
return this.$matrix.notificationCount
|
||||
},
|
||||
notificationCount,
|
||||
currentUser() {
|
||||
return this.$store.state.auth.user;
|
||||
},
|
||||
|
|
@ -217,13 +183,8 @@ export default {
|
|||
document.getElementById("favicon").setAttribute('href', favicon);
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
notificationCount: {
|
||||
handler(nCount) {
|
||||
this.requestNotificationPermission(nCount)
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@
|
|||
"minutes": "1 minute ago | {n} minutes ago",
|
||||
"hours": "1 hour ago | {n} hours ago",
|
||||
"days": "1 day ago | {n} days ago"
|
||||
}
|
||||
},
|
||||
"close": "close",
|
||||
"notify": "Notify"
|
||||
},
|
||||
"menu": {
|
||||
"start_private_chat": "Private chat with this user",
|
||||
|
|
@ -353,7 +355,13 @@
|
|||
"export_filename": "Exported chat {date}"
|
||||
},
|
||||
"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": {
|
||||
"search": "Search...",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
no-gutters
|
||||
align-content="center"
|
||||
v-on="$listeners"
|
||||
v-show="icon === 'notifications_active' ? this.windowNotificationPermission() !== 'granted' : true"
|
||||
>
|
||||
<v-col cols="auto" class="me-2">
|
||||
<v-icon :size="iconSize">{{ icon }}</v-icon>
|
||||
|
|
@ -13,6 +14,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { windowNotificationPermission } from "../plugins/notificationAndServiceWorker.js"
|
||||
|
||||
export default {
|
||||
name: "ActionRow",
|
||||
props: {
|
||||
|
|
@ -35,6 +38,9 @@ export default {
|
|||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
windowNotificationPermission
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
<template>
|
||||
<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"
|
||||
:events="events" :autoplay="!showRecorder"
|
||||
:timelineSet="timelineSet"
|
||||
|
|
@ -282,6 +286,46 @@
|
|||
</v-dialog>
|
||||
|
||||
<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>
|
||||
</template>
|
||||
|
||||
|
|
@ -304,6 +348,8 @@ import CreatePollDialog from "./CreatePollDialog.vue";
|
|||
import chatMixin from "./chatMixin";
|
||||
import AudioLayout from "./AudioLayout.vue";
|
||||
import FileDropLayout from "./file_mode/FileDropLayout";
|
||||
import { requestNotificationAndServiceWorker, windowNotificationPermission, notificationCount } from "../plugins/notificationAndServiceWorker.js"
|
||||
import logoMixin from "./logoMixin";
|
||||
|
||||
const sizeOf = require("image-size");
|
||||
const dataUriToBuffer = require("data-uri-to-buffer");
|
||||
|
|
@ -339,7 +385,7 @@ ScrollPosition.prototype.prepareFor = function (direction) {
|
|||
|
||||
export default {
|
||||
name: "Chat",
|
||||
mixins: [chatMixin],
|
||||
mixins: [chatMixin, logoMixin],
|
||||
components: {
|
||||
ChatHeader,
|
||||
MessageOperations,
|
||||
|
|
@ -433,7 +479,8 @@ export default {
|
|||
Symbols: this.$t("emoji.categories.symbols"),
|
||||
Places: this.$t("emoji.categories.places")
|
||||
}
|
||||
}
|
||||
},
|
||||
notificationDialog: false
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -470,6 +517,7 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
notificationCount,
|
||||
nonImageFiles() {
|
||||
return this.isCurrentFileInputsAnArray && this.currentFileInputs.filter(file => !file.type.includes("image/"))
|
||||
},
|
||||
|
|
@ -636,6 +684,13 @@ export default {
|
|||
},
|
||||
|
||||
watch: {
|
||||
notificationCount: {
|
||||
handler(nCount) {
|
||||
if (nCount > 0 && this.windowNotificationPermission() === "granted") {
|
||||
this.showNotification()
|
||||
}
|
||||
}
|
||||
},
|
||||
initialLoadDone: {
|
||||
immediate: true,
|
||||
handler(value, oldValue) {
|
||||
|
|
@ -726,6 +781,34 @@ export default {
|
|||
},
|
||||
|
||||
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) {
|
||||
// Was this room just created (by you)? Show a small info header in
|
||||
// that case!
|
||||
|
|
|
|||
|
|
@ -177,6 +177,11 @@ export default {
|
|||
this.$emit("view-room-details", { event: this.event });
|
||||
}
|
||||
});
|
||||
items.push({
|
||||
icon: 'notifications_active', text: this.$t('global.notify'), handler: () => {
|
||||
this.$emit("notify");
|
||||
}
|
||||
});
|
||||
items.push({
|
||||
icon: '$vuetify.icons.ic_member-leave', text: this.$t('leave.leave'), handler: () => {
|
||||
this.leaveRoom();
|
||||
|
|
|
|||
29
src/plugins/notificationAndServiceWorker.js
Normal 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
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const webpack = require("webpack");
|
||||
//const fs = require('fs')
|
||||
|
||||
module.exports = {
|
||||
transpileDependencies: ["vuetify"],
|
||||
|
|
@ -47,5 +48,15 @@ module.exports = {
|
|||
|
||||
devServer: {
|
||||
//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'),
|
||||
// }
|
||||
}
|
||||
};
|
||||
|
|
|
|||