diff --git a/core/language/us.json b/core/language/us.json index cbf85a4f..7fcfc8a1 100644 --- a/core/language/us.json +++ b/core/language/us.json @@ -719,7 +719,8 @@ "bchange43": "Do you give this application permission to add to this list?", "bchange44": "Do you give this application permission to delete from this list?", "bchange45": "Encrypt", - "bchange46": "Do you give this application permission to save the following file" + "bchange46": "Do you give this application permission to save the following file", + "bchange47": "Do you give this application permission to send you notifications" }, "datapage": { "dchange1": "Data Management", diff --git a/core/src/components/show-plugin.js b/core/src/components/show-plugin.js index c1ba4afc..a5c1153b 100644 --- a/core/src/components/show-plugin.js +++ b/core/src/components/show-plugin.js @@ -396,14 +396,15 @@ class ShowPlugin extends connect(store)(LitElement) { icon = 'tab' } - if (tab.myPlugObj && (tab.myPlugObj.url === 'websites' || tab.myPlugObj.url === 'qapps') && this.tabInfo[tab.id]) { - title = this.tabInfo[tab.id].name + if (tab.myPlugObj && (tab.myPlugObj.url === 'myapp') && this.tabInfo[tab.id]) { + title = this.tabInfo[tab.id].name } - if (tab.myPlugObj && (tab.myPlugObj.url === 'websites' || tab.myPlugObj.url === 'qapps') && this.tabInfo[tab.id]) { - count = this.tabInfo[tab.id].count + if (tab.myPlugObj && (tab.myPlugObj.url === 'myapp') && this.tabInfo[tab.id]) { + count = this.tabInfo[tab.id].count } + if (tab.myPlugObj && tab.myPlugObj.url === 'q-chat') { for (const chat of this.chatHeads) { diff --git a/core/src/notifications/dispatcher.js b/core/src/notifications/dispatcher.js index b74e11e1..b0c7713e 100644 --- a/core/src/notifications/dispatcher.js +++ b/core/src/notifications/dispatcher.js @@ -1,5 +1,5 @@ -import { NEW_MESSAGE, NEW_MESSAGE_NOTIFICATION_QAPP } from './types' -import { newMessage, newMessageNotificationQapp } from './notification-actions' +import { NEW_MESSAGE, NEW_MESSAGE_NOTIFICATION_QAPP, NEW_MESSAGE_NOTIFICATION_QAPP_LOCAL } from './types' +import { newMessage, newMessageNotificationQapp, newMessageNotificationQappLocal } from './notification-actions' export const dispatcher = function (notificationState) { @@ -8,6 +8,8 @@ export const dispatcher = function (notificationState) { return newMessage(notificationState.data) case NEW_MESSAGE_NOTIFICATION_QAPP: return newMessageNotificationQapp(notificationState.data) + case NEW_MESSAGE_NOTIFICATION_QAPP_LOCAL: + return newMessageNotificationQappLocal(notificationState.data) default: } } diff --git a/core/src/notifications/notification-actions/index.js b/core/src/notifications/notification-actions/index.js index 02faea66..cc2f8937 100644 --- a/core/src/notifications/notification-actions/index.js +++ b/core/src/notifications/notification-actions/index.js @@ -1 +1 @@ -export { newMessage, newMessageNotificationQapp } from './new-message' +export { newMessage, newMessageNotificationQapp, newMessageNotificationQappLocal } from './new-message' diff --git a/core/src/notifications/notification-actions/new-message.js b/core/src/notifications/notification-actions/new-message.js index 26803216..932da937 100644 --- a/core/src/notifications/notification-actions/new-message.js +++ b/core/src/notifications/notification-actions/new-message.js @@ -1,6 +1,9 @@ import { store } from '../../store.js' import { doPageUrl, setNewTab } from '../../redux/app/app-actions.js' import isElectron from 'is-electron' +import ShortUniqueId from 'short-unique-id'; + +const uid = new ShortUniqueId() export const newMessage = (data) => { const alert = playSound(data.sound) @@ -101,6 +104,157 @@ export const newMessageNotificationQapp = (data) => { } } +} + + const extractComponents= async (url)=> { + if (!url.startsWith("qortal://")) { + return null; + } + + url = url.replace(/^(qortal\:\/\/)/, ""); + if (url.includes("/")) { + let parts = url.split("/"); + const service = parts[0].toUpperCase(); + parts.shift(); + const name = parts[0]; + parts.shift(); + let identifier; + + if (parts.length > 0) { + identifier = parts[0]; // Do not shift yet + // Check if a resource exists with this service, name and identifier combination + let responseObj = await parentEpml.request('apiCall', { + url: `/arbitrary/resource/status/${service}/${name}/${identifier}?apiKey=${this.getApiKey()}` + }) + + if (responseObj.totalChunkCount > 0) { + // Identifier exists, so don't include it in the path + parts.shift(); + } + else { + identifier = null; + } + } + + const path = parts.join("/"); + + const components = {}; + components["service"] = service; + components["name"] = name; + components["identifier"] = identifier; + components["path"] = path; + return components; + } + + return null; +} + +export const newMessageNotificationQappLocal = (data) => { + const alert = playSound(data.sound) + + // Should I show notification ? + if (store.getState().user.notifications.q_chat.showNotification) { + + // Yes, I can but should I play sound ? + if (store.getState().user.notifications.q_chat.playSound) { + + const notify = new Notification(data.title, data.options) + + notify.onshow = (e) => { + alert.play() + } + + notify.onclick = async(e) => { + const url = data?.url + const value = url + let newQuery = value; + if (newQuery.endsWith('/')) { + newQuery = newQuery.slice(0, -1); + } + const res = await extractComponents(newQuery) + if (!res) return + const { service, name, identifier, path } = res + let query = `?service=${service}` + if (name) { + query = query + `&name=${name}` + } + if (identifier) { + query = query + `&identifier=${identifier}` + } + if (path) { + query = query + `&path=${path}` + } + const tab = { + url: `qdn/browser/index.html${query}`, + id: uid(), + myPlugObj: { + "url": service === 'WEBSITE' ? "websites" : "qapps", + "domain": "core", + "page": `qdn/browser/index.html${query}`, + "title": name, + "icon": service === 'WEBSITE' ? 'vaadin:desktop' : 'vaadin:external-browser', + "mwcicon": service === 'WEBSITE' ? 'desktop_mac' : 'open_in_browser', + "menus": [], + "parent": false + } + } + store.dispatch(setNewTab(tab)) + if (!isElectron()) { + window.focus(); + } else { + window.electronAPI.focusApp() + } + + } + } else { + + const notify = new Notification(data.title, data.options) + + notify.onclick = async(e) => { + const url = data?.url + const value = url + let newQuery = value; + if (newQuery.endsWith('/')) { + newQuery = newQuery.slice(0, -1); + } + const res = await extractComponents(newQuery) + if (!res) return + const { service, name, identifier, path } = res + let query = `?service=${service}` + if (name) { + query = query + `&name=${name}` + } + if (identifier) { + query = query + `&identifier=${identifier}` + } + if (path) { + query = query + `&path=${path}` + } + const tab = { + url: `qdn/browser/index.html${query}`, + id: uid(), + myPlugObj: { + "url": service === 'WEBSITE' ? "websites" : "qapps", + "domain": "core", + "page": `qdn/browser/index.html${query}`, + "title": name, + "icon": service === 'WEBSITE' ? 'vaadin:desktop' : 'vaadin:external-browser', + "mwcicon": service === 'WEBSITE' ? 'desktop_mac' : 'open_in_browser', + "menus": [], + "parent": false + } + } + store.dispatch(setNewTab(tab)) + if (!isElectron()) { + window.focus(); + } else { + window.electronAPI.focusApp() + } + + } + } + } + } const playSound = (soundUrl) => { diff --git a/core/src/notifications/types.js b/core/src/notifications/types.js index 0f8b1c05..f50b3ec7 100644 --- a/core/src/notifications/types.js +++ b/core/src/notifications/types.js @@ -1,2 +1,3 @@ export const NEW_MESSAGE = 'NEW_MESSAGE' export const NEW_MESSAGE_NOTIFICATION_QAPP = 'NEW_MESSAGE_NOTIFICATION_QAPP' +export const NEW_MESSAGE_NOTIFICATION_QAPP_LOCAL = 'NEW_MESSAGE_NOTIFICATION_QAPP_LOCAL' diff --git a/core/src/redux/app/version.js b/core/src/redux/app/version.js index 91ebdadc..51e897a7 100644 --- a/core/src/redux/app/version.js +++ b/core/src/redux/app/version.js @@ -1 +1 @@ -export const UI_VERSION = "4.0.4"; +export const UI_VERSION = "4.3.0"; diff --git a/plugins/plugins/core/components/qdn-action-types.js b/plugins/plugins/core/components/qdn-action-types.js index fd337ad1..59385690 100644 --- a/plugins/plugins/core/components/qdn-action-types.js +++ b/plugins/plugins/core/components/qdn-action-types.js @@ -50,4 +50,13 @@ export const DECRYPT_DATA_GROUP = 'DECRYPT_DATA_GROUP' export const SAVE_FILE = 'SAVE_FILE' //SET_TAB_NOTIFICATIONS -export const SET_TAB_NOTIFICATIONS = 'SET_TAB_NOTIFICATIONS' \ No newline at end of file +export const SET_TAB_NOTIFICATIONS = 'SET_TAB_NOTIFICATIONS' + +//OPEN_NEW_TAB +export const OPEN_NEW_TAB = 'OPEN_NEW_TAB' + +//NOTIFICATIONS_PERMISSION +export const NOTIFICATIONS_PERMISSION = 'NOTIFICATIONS_PERMISSION' + +//SEND_LOCAL_NOTIFICATION +export const SEND_LOCAL_NOTIFICATION = 'SEND_LOCAL_NOTIFICATION' \ No newline at end of file diff --git a/plugins/plugins/core/qdn/browser/browser.src.js b/plugins/plugins/core/qdn/browser/browser.src.js index db59e4fd..fd3d30de 100644 --- a/plugins/plugins/core/qdn/browser/browser.src.js +++ b/plugins/plugins/core/qdn/browser/browser.src.js @@ -3,6 +3,7 @@ import { render } from 'lit/html.js' import { Epml } from '../../../../epml' import isElectron from 'is-electron' import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate' +import ShortUniqueId from 'short-unique-id'; registerTranslateConfig({ loader: (lang) => fetch(`/language/${lang}.json`).then((res) => res.json()) @@ -131,6 +132,7 @@ class WebBrowser extends LitElement { constructor() { super(); this.url = 'about:blank'; + this.uid = new ShortUniqueId() this.myAddress = window.parent.reduxStore.getState().app.selectedAddress this._publicKey = { key: '', hasPubKey: false } const urlParams = new URLSearchParams(window.location.search) @@ -318,6 +320,44 @@ class WebBrowser extends LitElement { } } + async linkOpenNewTab(link) { + + const value = link + let newQuery = value; + if (newQuery.endsWith('/')) { + newQuery = newQuery.slice(0, -1); + } + const res = await this.extractComponents(newQuery) + if (!res) return + const { service, name, identifier, path } = res + let query = `?service=${service}` + if (name) { + query = query + `&name=${name}` + } + if (identifier) { + query = query + `&identifier=${identifier}` + } + if (path) { + query = query + `&path=${path}` + } + + window.parent.reduxStore.dispatch(window.parent.reduxAction.setNewTab({ + url: `qdn/browser/index.html${query}`, + id: this.uid(), + myPlugObj: { + "url": service === 'WEBSITE' ? "websites" : "qapps", + "domain": "core", + "page": `qdn/browser/index.html${query}`, + "title": name, + "icon": service === 'WEBSITE' ? 'vaadin:desktop' : 'vaadin:external-browser', + "mwcicon": service === 'WEBSITE' ? 'desktop_mac' : 'open_in_browser', + "menus": [], + "parent": false + } + })) + + } + render() { return html` @@ -1253,8 +1293,91 @@ class WebBrowser extends LitElement { // If they decline, send back JSON that includes an `error` key, such as `{"error": "User declined request"}` break; } + case actions.OPEN_NEW_TAB: { + if(!data.qortalLink){ + const obj = {}; + const errorMsg = 'Please enter a qortal link - qortal://...'; + obj['error'] = errorMsg; + response = JSON.stringify(obj); + break + } + try { + await this.linkOpenNewTab(data.qortalLink) + response = true + } catch (error) { + console.log('error', error) + const obj = {}; + const errorMsg = "Invalid qortal link"; + obj['error'] = errorMsg; + response = JSON.stringify(obj); + break; + } + + } + case actions.NOTIFICATIONS_PERMISSION: { + try { + const res = await showModalAndWait( + actions.NOTIFICATIONS_PERMISSION, + { + name: this.name + } + ); + if (res.action === 'accept'){ + this.addAppToNotificationList(this.name) + response = true + break; + } + + } catch (error) { + break; + } + + } + case actions.SEND_LOCAL_NOTIFICATION: { + const {title, url, icon, message} = data + try { + const id = `appNotificationList-${this.selectedAddress.address}` + const checkData = localStorage.getItem(id) ? JSON.parse(localStorage.getItem(id)) : null; + if(!checkData || !checkData[this.name]) throw new Error('App not on permission list') + const appInfo = checkData[this.name] + const lastNotification = appInfo.lastNotification + const interval = appInfo.interval + if (lastNotification && interval) { + const timeDifference = Date.now() - lastNotification; + + if (timeDifference > interval) { + parentEpml.request('showNotification', { + title, type: "qapp-local-notification", sound: '', url, options: { body: message, icon, badge: icon } + }) + response = true + this.updateLastNotification(id, this.name) + break; + } else { + throw new Error(`duration until another notification can be sent: ${interval - timeDifference}`) + } + } else if(!lastNotification){ + parentEpml.request('showNotification', { + title, type: "qapp-local-notification", sound: '', url, options: { body: message, icon, badge: icon } + }) + response = true + this.updateLastNotification(id) + break; + } else { + throw new Error(`invalid data`) + } + + } catch (error) { + const obj = {}; + const errorMsg = error.message || "error in pushing notification"; + obj['error'] = errorMsg; + response = JSON.stringify(obj); + break; + + } + + } case actions.SEND_CHAT_MESSAGE: { const message = data.message; const recipient = data.destinationAddress; @@ -2707,6 +2830,45 @@ class WebBrowser extends LitElement { use(checkLanguage); } } + addAppToNotificationList(appName) { + const id = `appNotificationList-${this.selectedAddress.address}`; + const checkData = localStorage.getItem(id) ? JSON.parse(localStorage.getItem(id)) : null; + + if (!checkData) { + const newData = { + [appName]: { + interval: 900000, // 15mins in milliseconds + lastNotification: null, + }, + }; + localStorage.setItem(id, JSON.stringify(newData)); + } else { + const copyData = { ...checkData }; + copyData[appName] = { + interval: 900000, // 15mins in milliseconds + lastNotification: null, + }; + localStorage.setItem(id, JSON.stringify(copyData)); + } + } + + updateLastNotification(id, appName) { + const checkData = localStorage.getItem(id) ? JSON.parse(localStorage.getItem(id)) : null; + + if (checkData) { + const copyData = { ...checkData }; + if (copyData[appName]) { + copyData[appName].lastNotification = Date.now(); // Make sure to use Date.now(), not date.now() + } else { + copyData[appName] = { + interval: 900000, // 15mins in milliseconds + lastNotification: Date.now(), + }; + } + localStorage.setItem(id, JSON.stringify(copyData)); + } + } + renderFollowUnfollowButton() { // Only show the follow/unfollow button if we have permission to modify the list on this node @@ -3064,7 +3226,12 @@ async function showModalAndWait(type, data) { ` : ''} - + ${type === actions.NOTIFICATIONS_PERMISSION ? ` + + ` : ''} + ${type === actions.DELETE_LIST_ITEM ? `