//META{"name":"FriendNotifications","website":"https://github.com/mwittrien/BetterDiscordAddons/tree/master/Plugins/FriendNotifications","source":"https://raw.githubusercontent.com/mwittrien/BetterDiscordAddons/master/Plugins/FriendNotifications/FriendNotifications.plugin.js"}*// class FriendNotifications { getName () {return "FriendNotifications";} getVersion () {return "1.3.3";} getAuthor () {return "DevilBro";} getDescription () {return "Notifies you when a Friend or a User your choose to observe changing his online status, can be configured individually in the settings.";} constructor () { this.changelog = { "added":[["Toast/Desktop Time","The amount of seconds a toast or desktop notification is on screen can now be configured in the settings"]], "fixed":[["Double notifications","It now should be impossible for the plugin to trigger double notifications, if this problem still occurs to you, then something is wrong on your end (double plugin file or two different versions at the same time)"],["Listening/Playing/Streaming","Fixed notifications not showing for those types"]] }; this.patchModules = { "StandardSidebarView":"componentWillUnmount" }; } initConstructor () { this.userStatusStore = {}; this.checkInterval = null; this.timeLog = []; this.timeLogModalMarkup = `

Friends LogIn/-Out Timelog

`; this.logEntryMarkup = `

`; this.css = ` .${this.name}-modal .log-time { width: 110px; } .${this.name}-modal .log-avatar { width: 35px; height: 35px; background-size: cover; background-position: center; border-radius: 50%; } .${this.name}-settings .type-toast, .${this.name}-settings .type-desktop { border-radius: 3px; padding: 0 3px; } .${this.name}-settings .type-toast { background-color: #7289DA; } .${this.name}-settings .type-desktop { background-color: #43B581; } .${this.name}-settings .settings-avatar.desktop { border-color: #43B581; } .${this.name}-settings .settings-avatar { margin: 5px; width: 35px; height: 35px; background-size: cover; background-position: center; border: 3px solid #7289DA; border-radius: 50%; box-sizing: border-box; cursor: pointer; } .${this.name}-settings .settings-avatar.desktop { border-color: #43B581; } .${this.name}-settings .settings-avatar.disabled { border-color: #36393F; filter: grayscale(100%) brightness(50%); } .${this.name}-settings .settings-avatar.disabled ~ * { filter: grayscale(100%) brightness(50%); }`; this.defaults = { settings: { disableForNew: {value:false, description:"Disable Notifications for newly added Friends:"}, muteOnDND: {value:false, description:"Do not notify me when I am DnD:"}, openOnClick: {value:false, description:"Open the DM when you click a Notification:"} }, notificationstrings: { online: {value:"$user changed status to '$status'", libstring:"STATUS_ONLINE", init:true}, mobile: {value:"$user changed status to '$status'", libstring:"STATUS_ONLINE_MOBILE", init:true}, idle: {value:"$user changed status to '$status'", libstring:"STATUS_IDLE", init:false}, dnd: {value:"$user changed status to '$status'", libstring:"STATUS_DND", init:false}, playing: {value:"$user started playing '$game'", statusname:"Playing", init:false}, listening: {value:"$user started listening to '$song'", statusname:"Listening", init:false}, streaming: {value:"$user started streaming '$game'", libstring:"STATUS_STREAMING", init:false}, offline: {value:"$user changed status to '$status'", libstring:"STATUS_OFFLINE", init:true} }, notificationsounds: {}, amounts: { toastTime: {value:5, min:1, description:"Amount of seconds a toast notification stays on screen:"}, desktopTime: {value:5, min:1, description:"Amount of seconds a desktop notification stays on screen:"}, checkInterval: {value:10, min:5, description:"Check Users every X seconds:"} } }; for (let type in this.defaults.notificationstrings) { this.defaults.notificationsounds["toast" + type] = {value:{url:null,song:null,mute:false}}; this.defaults.notificationsounds["desktop" + type] = {value:{url:null,song:null,mute:false}}; } this.activityTypes = {}; for (let type in BDFDB.DiscordConstants.ActivityTypes) this.activityTypes[BDFDB.DiscordConstants.ActivityTypes[type]] = type; } getSettingsPanel () { if (!global.BDFDB || typeof BDFDB != "object" || !BDFDB.loaded || !this.started) return; let settings = BDFDB.DataUtils.get(this, "settings"); let notificationstrings = BDFDB.DataUtils.get(this, "notificationstrings"); let notificationsounds = BDFDB.DataUtils.get(this, "notificationsounds"); let amounts = BDFDB.DataUtils.get(this, "amounts"); let friendIDs = BDFDB.LibraryModules.FriendUtils.getFriendIDs(); let friends = BDFDB.DataUtils.load(this, "friends"); let nonfriends = BDFDB.DataUtils.load(this, "nonfriends"); let settingshtml = `
${this.name}
`; settingshtml += `
General Settings
`; for (let key in settings) settingshtml += `

${this.defaults.settings[key].description}

`; for (let key in amounts) if (key.indexOf("desktop") == -1 || "Notification" in window) settingshtml += `

${this.defaults.amounts[key].description}

`; settingshtml += `
`; settingshtml += `
Friend-List
`; settingshtml += `

Click on an Icon to toggle Notifications for that User:

`; if ("Notification" in window) settingshtml += `

Rightclick on an Icon to toggle Notifications for that User:

`; settingshtml += `

Click/Rightclick on the table headers to batch set all Friends

`; settingshtml += `

TYPE
DISABLE

`; for (let config in this.defaults.notificationstrings) settingshtml += `
${config.toUpperCase()}
`; settingshtml += `
`; for (let id of friendIDs) { let user = BDFDB.LibraryModules.UserStore.getUser(id); if (user) { let friend = null; if (friends[id]) {} else if (nonfriends[id]) { friends[id] = Object.assign({}, nonfriends[id]); delete nonfriends[id]; } else friends[id] = this.createDefaultConfig(); settingshtml += this.createHoverCard(user, friends[id], "friends"); } } settingshtml += `
`; settingshtml += `
`; settingshtml += `
Non-Friend-List
`; settingshtml += `

Click on a Icon to toggle Notifications for that User:

`; if ("Notification" in window) settingshtml += `

Rightclick on a Icon to toggle Notifications for that User:

`; settingshtml += `

Click/Rightclick on the table headers to batch set all Non-Friends

`; settingshtml += `

Add Non-Friend:

`; settingshtml += `

TYPE
DISABLE

`; for (let config in this.defaults.notificationstrings) settingshtml += `
${config.toUpperCase()}
`; settingshtml += `
`; for (let id in nonfriends) if (!friendIDs.includes(id)) { let user = BDFDB.LibraryModules.UserStore.getUser(id); if (user) { delete friends[id]; settingshtml += this.createHoverCard(user, nonfriends[id] || (nonfriends[id] = this.createDefaultConfig()), "nonfriends"); } } settingshtml += `
`; settingshtml += `
`; settingshtml += `
Timelog
`; settingshtml += `

Timelog of LogIns/-Outs:

`; settingshtml += `
`; settingshtml += `
Notification Message Settings
`; settingshtml += `

Allows you to configure your own message strings for the different statuses. $user is the placeholder for the username, $status for the statusname, $game for the gamename, $song for the songname and $artist for the songartist.

`; for (let config in notificationstrings) { settingshtml += `

${config.charAt(0).toUpperCase() + config.slice(1)} Message:

`; } settingshtml += `
`; settingshtml += `
Notification Sound Settings
`; for (let config in notificationsounds) if (config.indexOf("desktop") == -1 || "Notification" in window) settingshtml += `
${config} notification sound:
Mute:
`; settingshtml += `
`; settingshtml += `
`; BDFDB.DataUtils.save(friends, this, "friends"); BDFDB.DataUtils.save(nonfriends, this, "nonfriends"); let settingspanel = BDFDB.DOMUtils.create(settingshtml); BDFDB.initElements(settingspanel, this); BDFDB.ListenerUtils.add(this, settingspanel, "keyup", ".input-notificationstring", e => {this.saveNotificationString(e.currentTarget);}); BDFDB.ListenerUtils.add(this, settingspanel, "click", ".btn-savesong", e => {this.saveNotificationSound(e.currentTarget.parentElement.querySelector(BDFDB.dotCN.input));}); BDFDB.ListenerUtils.add(this, settingspanel, "click", ".mute-checkbox", e => { let config = e.currentTarget.getAttribute("config"); if (config) { let notificationsound = BDFDB.DataUtils.get(this, "notificationsounds", config); notificationsound.mute = e.currentTarget.checked; BDFDB.DataUtils.save(notificationsound, this, "notificationsounds", config); } }); BDFDB.ListenerUtils.add(this, settingspanel, "click", ".settings-avatar", e => { this.changeNotificationType(e.currentTarget, false, !BDFDB.DOMUtils.containsClass(e.currentTarget, "disabled", "desktop", false)); }); BDFDB.ListenerUtils.add(this, settingspanel, "contextmenu", ".settings-avatar", e => { if (!("Notification" in window)) return; this.changeNotificationType(e.currentTarget, true, !(BDFDB.DOMUtils.containsClass(e.currentTarget, "disabled") || !BDFDB.DOMUtils.containsClass(e.currentTarget, "desktop"))); }); BDFDB.ListenerUtils.add(this, settingspanel, "click", ".btn-batch", e => { this.changeAllNotificationTypes(settingspanel, e.currentTarget, true); }); BDFDB.ListenerUtils.add(this, settingspanel, "contextmenu", ".btn-batch", e => { this.changeAllNotificationTypes(settingspanel, e.currentTarget, false); }); BDFDB.ListenerUtils.add(this, settingspanel, "click", BDFDB.dotCN.checkboxinput, e => { if (BDFDB.DOMUtils.containsClass(e.target, "remove-user")) return; this.changeNotificationConfig(e.currentTarget); }); BDFDB.ListenerUtils.add(this, settingspanel, "click", ".BDFDB-tableheadercolumn", e => { this.changeAllNotificationConfigs(settingspanel, e.currentTarget, true); }); BDFDB.ListenerUtils.add(this, settingspanel, "contextmenu", ".BDFDB-tableheadercolumn", e => { this.changeAllNotificationConfigs(settingspanel, e.currentTarget, false); }); BDFDB.ListenerUtils.add(this, settingspanel, "click", ".remove-user", e => { let id = e.currentTarget.getAttribute("user-id"); let group = e.currentTarget.getAttribute("group"); if (id && group) { BDFDB.DataUtils.remove(this, group, id); BDFDB.DOMUtils.remove(BDFDB.DOMUtils.getParent(BDFDB.dotCN.hovercard, e.currentTarget)); this.SettingsUpdated = true; } }); BDFDB.ListenerUtils.add(this, settingspanel, "click", ".btn-adduser", e => { let idinput = settingspanel.querySelector("#input-userid"); let id = idinput.value; idinput.value = ""; if (friendIDs.includes(id)) BDFDB.NotificationUtils.toast("User is already a friend of yours. Please use the 'Friends' area to configure him/her.", {type:"error"}); else if (BDFDB.DataUtils.load(this, "nonfriends")) BDFDB.NotificationUtils.toast("User is already being observed as a 'Non-Friend'.", {type:"error"}, id); else { let user = BDFDB.LibraryModules.UserStore.getUser(id); if (user) { let data = this.createDefaultConfig(); BDFDB.DataUtils.save(data, this, "nonfriends", user.id); let hovercard = BDFDB.DOMUtils.create(this.createHoverCard(user, data, "nonfriends")); settingspanel.querySelector(".nonfriend-list").appendChild(hovercard); BDFDB.initElements(hovercard); this.SettingsUpdated = true; } else if (/.+#[0-9]{4}/.test(id)) BDFDB.NotificationUtils.toast("A UserID does not consist of the username and discriminator.", {type:"error"}); else BDFDB.NotificationUtils.toast("Please enter a valid UserID of a user that has been loaded in your client.", {type:"error"}); } }); BDFDB.ListenerUtils.add(this, settingspanel, "click", ".btn-timelog", () => {this.showTimeLog();}); return settingspanel; } //legacy load () {} start () { if (!global.BDFDB) global.BDFDB = {myPlugins:{}}; if (global.BDFDB && global.BDFDB.myPlugins && typeof global.BDFDB.myPlugins == "object") global.BDFDB.myPlugins[this.getName()] = this; var libraryScript = document.querySelector('head script#BDFDBLibraryScript'); if (!libraryScript || (performance.now() - libraryScript.getAttribute("date")) > 600000) { if (libraryScript) libraryScript.remove(); libraryScript = document.createElement("script"); libraryScript.setAttribute("id", "BDFDBLibraryScript"); libraryScript.setAttribute("type", "text/javascript"); libraryScript.setAttribute("src", "https://mwittrien.github.io/BetterDiscordAddons/Plugins/BDFDB.min.js"); libraryScript.setAttribute("date", performance.now()); libraryScript.addEventListener("load", () => {this.initialize();}); document.head.appendChild(libraryScript); } else if (global.BDFDB && typeof BDFDB === "object" && BDFDB.loaded) this.initialize(); this.startTimeout = setTimeout(() => {this.initialize();}, 30000); } initialize () { if (global.BDFDB && typeof BDFDB === "object" && BDFDB.loaded) { if (this.started) return; BDFDB.PluginUtils.init(this); this.startInterval(); BDFDB.ModuleUtils.forceAllUpdates(this); } else console.error(`%c[${this.getName()}]%c`, 'color: #3a71c1; font-weight: 700;', '', 'Fatal Error: Could not load BD functions!'); } stop () { if (global.BDFDB && typeof BDFDB === "object" && BDFDB.loaded) { this.stopping = true; clearInterval(this.checkInterval); BDFDB.PluginUtils.clear(this); } } // begin of own functions createHoverCard (user, data, group) { let EUdata = BDFDB.DataUtils.load("EditUsers", "users", user.id) || {}; var hovercardhtml = `
${BDFDB.StringUtils.htmlEscape(EUdata.name || user.username)}
`; for (let config in this.defaults.notificationstrings) { hovercardhtml += `
`; } return hovercardhtml + `
${group == "nonfriends" ? `
` : ''}
` } changeNotificationType (avatar, desktopon, disableon) { let id = avatar.getAttribute("user-id"); let group = avatar.getAttribute("group"); if (id && group) { let data = BDFDB.DataUtils.load(this, group, id) || this.createDefaultConfig(); data.desktop = desktopon; data.disabled = disableon; BDFDB.DOMUtils.toggleClass(avatar, "desktop", desktopon); BDFDB.DOMUtils.toggleClass(avatar, "disabled", disableon); BDFDB.DataUtils.save(data, this, group, id); this.SettingsUpdated = true; } } changeAllNotificationTypes (settingspanel, tableheader, enable) { let config = tableheader.getAttribute("config"); let group = tableheader.getAttribute("group"); if (config && group) { let data = BDFDB.DataUtils.load(this, group); if (config == "desktop") { enable = !enable; for (let id in data) data[id].disabled = false; for (let avatar of settingspanel.querySelectorAll(`.settings-avatar[group="${group}"]`)) BDFDB.DOMUtils.removeClass(avatar, "disabled"); } for (let id in data) data[id][config] = enable; for (let avatar of settingspanel.querySelectorAll(`.settings-avatar[group="${group}"]`)) BDFDB.DOMUtils.toggleClass(avatar, config, enable); BDFDB.DataUtils.save(data, this, group); this.SettingsUpdated = true; } } changeNotificationConfig (checkbox) { let id = checkbox.getAttribute("user-id"); let config = checkbox.getAttribute("config"); let group = checkbox.getAttribute("group"); if (id && config && group) { let data = BDFDB.DataUtils.load(this, group, id) || this.createDefaultConfig(); data[config] = checkbox.checked; BDFDB.DataUtils.save(data, this, group, id); this.SettingsUpdated = true; } } changeAllNotificationConfigs (settingspanel, tableheader, enable) { let config = tableheader.getAttribute("config"); let group = tableheader.getAttribute("group"); if (config && group) { let data = BDFDB.DataUtils.load(this, group); for (let id in data) data[id][config] = enable; BDFDB.DataUtils.save(data, this, group); for (let checkbox of settingspanel.querySelectorAll(`${BDFDB.dotCN.checkboxinput}[config="${config}"][group="${group}"]`)) { checkbox.checked = enable; if (typeof checkbox.BDFDBupdateElement == "function") checkbox.BDFDBupdateElement(); } this.SettingsUpdated = true; } } createDefaultConfig () { return Object.assign({desktop: false, disabled: BDFDB.DataUtils.get(this, "settings", "disableForNew")}, BDFDB.ObjectUtils.map(this.defaults.notificationstrings, "init")); } saveNotificationString (input) { let config = input.getAttribute("config"); if (config) { BDFDB.DataUtils.save(input.value, this, "notificationstrings", config); this.SettingsUpdated = true; } } saveNotificationSound (input) { let config = input.getAttribute("config"); if (config) { let successSavedAudio = (parsedurl, parseddata) => { if (parsedurl && parseddata) BDFDB.NotificationUtils.toast(`Sound was saved successfully.`, {type:"success"}); let notificationsound = BDFDB.DataUtils.get(this, "notificationsounds", config); notificationsound.url = parsedurl; notificationsound.song = parseddata; BDFDB.DataUtils.save(notificationsound, this, "notificationsounds", config); this.SettingsUpdated = true; }; let url = input.value; if (url.length == 0) { BDFDB.NotificationUtils.toast(`Sound file was removed.`, {type:"warn"}); successSavedAudio(url, url); } else if (url.indexOf("http") == 0) { BDFDB.LibraryRequires.request(url, (error, response, result) => { if (response) { let type = response.headers["content-type"]; if (type && (type.indexOf("octet-stream") > -1 || type.indexOf("audio") > -1 || type.indexOf("video") > -1)) { successSavedAudio(url, url); return; } } BDFDB.NotificationUtils.toast("Use a valid direct link to a video or audio source. They usually end on something like .mp3, .mp4 or .wav.", {type:"danger"}); }); } else { BDFDB.LibraryRequires.fs.readFile(url, (error, response) => { if (error) BDFDB.NotificationUtils.toast("Could not fetch file. Please make sure the file exists.", {type:"danger"}); else successSavedAudio(url, `data:audio/mpeg;base64,${response.toString("base64")}`); }); } } } processStandardSidebarView (instance, wrapper, returnvalue) { if (this.SettingsUpdated) { delete this.SettingsUpdated; this.startInterval(); } } getStatusWithMobileAndActivity (id, config) { let statusname = BDFDB.UserUtils.getStatus(id); let status = {statusname, isactivity:false}; let activity = BDFDB.LibraryModules.StatusMetaUtils.getPrimaryActivity(id); if (activity && this.activityTypes[activity.type]) { let activityname = this.activityTypes[activity.type].toLowerCase(); if (this.defaults.notificationstrings[activityname] && config[activityname]) { status = Object.assign({statusname:activityname, isactivity:true}, activity); if (activityname == "listening" || activityname == "streaming") delete status.name; } } if (status.statusname == "online" && BDFDB.LibraryModules.StatusMetaUtils.isMobileOnline(id)) status.statusname = "mobile"; return status; } startInterval () { clearInterval(this.checkInterval); let settings = BDFDB.DataUtils.get(this, "settings"); let amounts = BDFDB.DataUtils.get(this, "amounts"); let notificationstrings = BDFDB.DataUtils.get(this, "notificationstrings"); let notificationsounds = BDFDB.DataUtils.get(this, "notificationsounds"); let users = Object.assign({}, BDFDB.DataUtils.load(this, "nonfriends"), BDFDB.DataUtils.load(this, "friends")); for (let id in users) this.userStatusStore[id] = this.getStatusWithMobileAndActivity(id, users[id]).statusname; let toasttime = (amounts.toastTime > amounts.checkInterval ? amounts.checkInterval : amounts.toastTime) * 1000; let desktoptime = (amounts.desktopTime > amounts.checkInterval ? amounts.checkInterval : amounts.desktopTime) * 1000; this.checkInterval = setInterval(() => { for (let id in users) if (!users[id].disabled) { let user = BDFDB.LibraryModules.UserStore.getUser(id); let status = this.getStatusWithMobileAndActivity(id, users[id]); if (user && this.userStatusStore[id] != status.statusname && users[id][status.statusname]) { let EUdata = BDFDB.DataUtils.load("EditUsers", "users", user.id) || {}; let libstring = (this.defaults.notificationstrings[status.statusname].libstring ? BDFDB.LanguageUtils.LanguageStrings[this.defaults.notificationstrings[status.statusname].libstring] : (this.defaults.notificationstrings[status.statusname].statusname || "")).toLowerCase(); let string = notificationstrings[status.statusname] || "$user changed status to $status"; let toaststring = BDFDB.StringUtils.htmlEscape(string).replace(/'{0,1}\$user'{0,1}/g, `${BDFDB.StringUtils.htmlEscape(EUdata.name || user.username)}`).replace(/'{0,1}\$status'{0,1}/g, `${libstring}`); if (status.isactivity) toaststring = toaststring.replace(/'{0,1}\$song'{0,1}|'{0,1}\$game'{0,1}/g, `${status.name || status.details}`).replace(/'{0,1}\$artist'{0,1}/g, `${status.state}`); let avatar = EUdata.removeIcon ? "" : (EUdata.url ? EUdata.url : BDFDB.UserUtils.getAvatar(user.id)); this.timeLog.push({string:toaststring, avatar, time: new Date()}); if (!(settings.muteOnDND && BDFDB.UserUtils.getStatus() == "dnd")) { let openChannel = () => { if (settings.openOnClick) { let DMid = BDFDB.LibraryModules.ChannelStore.getDMFromUserId(user.id) if (DMid) BDFDB.LibraryModules.SelectChannelUtils.selectPrivateChannel(DMid); else BDFDB.LibraryModules.DirectMessageUtils.openPrivateChannel(BDFDB.UserUtils.me.id, user.id); BDFDB.LibraryRequires.electron.remote.getCurrentWindow().maximize(); } }; if (!users[id].desktop) { if (!document.querySelector(`.friendnotifications-${id}-toast`)) { let toast = BDFDB.NotificationUtils.toast(`
${toaststring}
`, {html:true, timeout:toasttime, color:BDFDB.UserUtils.getStatusColor(status.statusname), icon:false, selector:`friendnotifications-${status.statusname}-toast friendnotifications-${id}-toast`}); toast.addEventListener("click", openChannel); let notificationsound = notificationsounds["toast" + status.statusname] || {}; if (!notificationsound.mute && notificationsound.song) { let audio = new Audio(); audio.src = notificationsound.song; audio.play(); } } } else { let desktopstring = string.replace(/\$user/g, EUdata.name || user.username).replace(/\$status/g, libstring); if (status.isactivity) desktopstring = desktopstring.replace(/\$song|\$game/g, status.name || status.details).replace(/\$artist/g, status.state); let notificationsound = notificationsounds["desktop" + status.statusname] || {}; BDFDB.NotificationUtils.desktop(desktopstring, {icon:avatar, timeout:desktoptime, click:openChannel, silent:notificationsound.mute, sound:notificationsound.song}); } } } this.userStatusStore[id] = status.statusname; } }, amounts.checkInterval * 1000); } showTimeLog () { let timeLogModal = BDFDB.DOMUtils.create(this.timeLogModalMarkup); let container = timeLogModal.querySelector(".entries"); if (!container) return; let logs = this.timeLog.slice(0).reverse(); for (let log of logs) { if (container.childElementCount) container.appendChild(BDFDB.DOMUtils.create(`
`)); let entry = BDFDB.DOMUtils.create(this.logEntryMarkup); entry.querySelector(".log-time").innerText = `[${log.time.toLocaleTimeString()}]`; entry.querySelector(".log-avatar").style.setProperty("background-image", `url(${log.avatar})`); entry.querySelector(".log-description").innerHTML = log.string; container.appendChild(entry) } BDFDB.appendModal(timeLogModal); } }