//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.7";} 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 = { "fixed":[["Non Friends","Fixed issue where previously configured friends that are no longer in your friend list could not be configured via non-friend list"]] }; } 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"); } } for (let id in friends) if (!friendIDs.includes(id)) { let user = BDFDB.LibraryModules.UserStore.getUser(id); if (user) { nonfriends[id] = Object.assign({}, friends[id]); delete friends[id]; settingshtml += this.createHoverCard(user, friends[id] || (friends[id] = this.createDefaultConfig()), "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", id)) BDFDB.NotificationUtils.toast("User is already being observed as a 'Non-Friend'.", {type:"error"}); 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(() => { try {return this.initialize();} catch (err) {console.error(`%c[${this.getName()}]%c`, "color: #3a71c1; font-weight: 700;", "", "Fatal Error: Could not initiate plugin! " + err);} }, 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; BDFDB.TimeUtils.clear(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")}`); }); } } } onSettingsClosed () { 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.UserUtils.getActivitiy(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 () { BDFDB.TimeUtils.clear(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 = BDFDB.TimeUtils.interval(() => { 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().focus(); } }; 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); } }