//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.0";} 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":[["Playing/Listening/Streaming","You can now listen for substatus like playing and listening, also added new placeholders like $game and $song to custom notifications"],["Default disable","Option to disable notification for newly added friends"]], "fixed":[["Startup spam","Fixed the spam of toasts on plugin start"],["Missing sounds","Fixed the missing sounds for playing/listening"]] }; 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: { 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.getAllData(this, "settings"); let notificationstrings = BDFDB.getAllData(this, "notificationstrings"); let notificationsounds = BDFDB.getAllData(this, "notificationsounds"); let amounts = BDFDB.getAllData(this, "amounts"); let friendIDs = BDFDB.LibraryModules.FriendUtils.getFriendIDs(); let friends = BDFDB.loadAllData(this, "friends"); let nonfriends = BDFDB.loadAllData(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) 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.saveAllData(friends, this, "friends"); BDFDB.saveAllData(nonfriends, this, "nonfriends"); let settingspanel = BDFDB.htmlToElement(settingshtml); BDFDB.initElements(settingspanel, this); BDFDB.addEventListener(this, settingspanel, "keyup", ".input-notificationstring", e => {this.saveNotificationString(e.currentTarget);}); BDFDB.addEventListener(this, settingspanel, "click", ".btn-savesong", e => {this.saveNotificationSound(e.currentTarget.parentElement.querySelector(BDFDB.dotCN.input));}); BDFDB.addEventListener(this, settingspanel, "click", ".mute-checkbox", e => { let config = e.currentTarget.getAttribute("config"); if (config) { let notificationsound = BDFDB.getData(config, this, "notificationsounds"); notificationsound.mute = e.currentTarget.checked; BDFDB.saveData(config, notificationsound, this, "notificationsounds"); } }); BDFDB.addEventListener(this, settingspanel, "click", ".settings-avatar", e => { this.changeNotificationType(e.currentTarget, false, !BDFDB.containsClass(e.currentTarget, "disabled", "desktop", false)); }); BDFDB.addEventListener(this, settingspanel, "contextmenu", ".settings-avatar", e => { if (!("Notification" in window)) return; this.changeNotificationType(e.currentTarget, true, !(BDFDB.containsClass(e.currentTarget, "disabled") || !BDFDB.containsClass(e.currentTarget, "desktop"))); }); BDFDB.addEventListener(this, settingspanel, "click", ".btn-batch", e => { this.changeAllNotificationTypes(settingspanel, e.currentTarget, true); }); BDFDB.addEventListener(this, settingspanel, "contextmenu", ".btn-batch", e => { this.changeAllNotificationTypes(settingspanel, e.currentTarget, false); }); BDFDB.addEventListener(this, settingspanel, "click", BDFDB.dotCN.checkboxinput, e => { if (BDFDB.containsClass(e.target, "remove-user")) return; this.changeNotificationConfig(e.currentTarget); }); BDFDB.addEventListener(this, settingspanel, "click", ".BDFDB-tableheadercolumn", e => { this.changeAllNotificationConfigs(settingspanel, e.currentTarget, true); }); BDFDB.addEventListener(this, settingspanel, "contextmenu", ".BDFDB-tableheadercolumn", e => { this.changeAllNotificationConfigs(settingspanel, e.currentTarget, false); }); BDFDB.addEventListener(this, settingspanel, "click", ".remove-user", e => { let id = e.currentTarget.getAttribute("user-id"); let group = e.currentTarget.getAttribute("group"); if (id && group) { BDFDB.removeData(id, this, group); BDFDB.removeEles(BDFDB.getParentEle(BDFDB.dotCN.hovercard, e.currentTarget)); this.SettingsUpdated = true; } }); BDFDB.addEventListener(this, settingspanel, "click", ".btn-adduser", e => { let idinput = settingspanel.querySelector("#input-userid"); let id = idinput.value; idinput.value = ""; if (friendIDs.includes(id)) BDFDB.showToast("User is already a friend of yours. Please use the 'Friends' area to configure him/her.", {type:"error"}); else if (BDFDB.loadData(id, this, "nonfriends")) BDFDB.showToast("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.saveData(user.id, data, this, "nonfriends"); let hovercard = BDFDB.htmlToElement(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.showToast("A UserID does not consist of the username and discriminator.", {type:"error"}); else BDFDB.showToast("Please enter a valid UserID of a user that has been loaded in your client.", {type:"error"}); } }); BDFDB.addEventListener(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.js"); libraryScript.setAttribute("date", performance.now()); libraryScript.addEventListener("load", () => {this.initialize();}); document.head.appendChild(libraryScript); this.libLoadTimeout = setTimeout(() => { libraryScript.remove(); BDFDB.LibraryRequires.request("https://mwittrien.github.io/BetterDiscordAddons/Plugins/BDFDB.js", (error, response, body) => { if (body) { libraryScript = document.createElement("script"); libraryScript.setAttribute("id", "BDFDBLibraryScript"); libraryScript.setAttribute("type", "text/javascript"); libraryScript.setAttribute("date", performance.now()); libraryScript.innerText = body; document.head.appendChild(libraryScript); } this.initialize(); }); }, 15000); } 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.loadMessage(this); this.startInterval(); BDFDB.WebModules.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) { clearInterval(this.checkInterval); BDFDB.unloadMessage(this); } } // begin of own functions createHoverCard (user, data, group) { let EUdata = BDFDB.loadData(user.id, "EditUsers", "users") || {}; var hovercardhtml = `
${BDFDB.encodeToHTML(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.loadData(id, this, group) || this.createDefaultConfig(); data.desktop = desktopon; data.disabled = disableon; BDFDB.toggleClass(avatar, "desktop", desktopon); BDFDB.toggleClass(avatar, "disabled", disableon); BDFDB.saveData(id, data, this, group); this.SettingsUpdated = true; } } changeAllNotificationTypes (settingspanel, tableheader, enable) { let config = tableheader.getAttribute("config"); let group = tableheader.getAttribute("group"); if (config && group) { let data = BDFDB.loadAllData(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.removeClass(avatar, "disabled"); } for (let id in data) data[id][config] = enable; for (let avatar of settingspanel.querySelectorAll(`.settings-avatar[group="${group}"]`)) BDFDB.toggleClass(avatar, config, enable); BDFDB.saveAllData(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.loadData(id, this, group) || this.createDefaultConfig(); data[config] = checkbox.checked; BDFDB.saveData(id, data, this, group); this.SettingsUpdated = true; } } changeAllNotificationConfigs (settingspanel, tableheader, enable) { let config = tableheader.getAttribute("config"); let group = tableheader.getAttribute("group"); if (config && group) { let data = BDFDB.loadAllData(this, group); for (let id in data) data[id][config] = enable; BDFDB.saveAllData(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.getData("disableForNew", this, "settings")}, BDFDB.mapObject(this.defaults.notificationstrings, "init")); } saveNotificationString (input) { let config = input.getAttribute("config"); if (config) { BDFDB.saveData(config, input.value, this, "notificationstrings"); this.SettingsUpdated = true; } } saveNotificationSound (input) { let config = input.getAttribute("config"); if (config) { let successSavedAudio = (parsedurl, parseddata) => { if (parsedurl && parseddata) BDFDB.showToast(`Sound was saved successfully.`, {type:"success"}); let notificationsound = BDFDB.getData(config, this, "notificationsounds"); notificationsound.url = parsedurl; notificationsound.song = parseddata; BDFDB.saveData(config, notificationsound, this, "notificationsounds"); this.SettingsUpdated = true; }; let url = input.value; if (url.length == 0) { BDFDB.showToast(`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.showToast("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.showToast("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.getUserStatus(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.statusname; } } if (status.statusname == "online" && BDFDB.LibraryModules.StatusMetaUtils.isMobileOnline(id)) status.statusname = "mobile"; return status; } startInterval () { clearInterval(this.checkInterval); let settings = BDFDB.getAllData(this, "settings"); let notificationstrings = BDFDB.getAllData(this, "notificationstrings"); let notificationsounds = BDFDB.getAllData(this, "notificationsounds"); let users = Object.assign({}, BDFDB.loadAllData(this, "nonfriends"), BDFDB.loadAllData(this, "friends")); for (let id in users) this.userStatusStore[id] = this.getStatusWithMobileAndActivity(id, users[id]).statusname; 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.loadData(user.id, "EditUsers", "users") || {}; let libstring = (this.defaults.notificationstrings[status.statusname].libstring ? BDFDB.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.encodeToHTML(string).replace(/\$user/g, `${BDFDB.encodeToHTML(EUdata.name || user.username)}`).replace(/\$status/g, `${libstring}`); if (status.isactivity) toaststring = toaststring.replace(/\$song|\$game/g, `${status.name || status.details}`).replace(/\$artist/g, `${status.state}`); let avatar = EUdata.removeIcon ? "" : (EUdata.url ? EUdata.url : BDFDB.getUserAvatar(user.id)); this.timeLog.push({string:toaststring, avatar, time: new Date()}); if (!(settings.muteOnDND && BDFDB.getUserStatus() == "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.myData.id, user.id); BDFDB.LibraryRequires.electron.remote.getCurrentWindow().maximize(); } }; if (!users[id].desktop) { let toast = BDFDB.showToast(`
${toaststring}
`, {html:true, timeout:5000, color:BDFDB.getUserStatusColor(status.statusname), icon:false, selector:`friendnotifications-${status.statusname}-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.showDesktopNotification(desktopstring, {icon:avatar, timeout:5000, click:openChannel, silent:notificationsound.mute, sound:notificationsound.song}); } } } this.userStatusStore[id] = status.statusname; } },BDFDB.getData("checkInterval", this, "amounts") * 1000); } showTimeLog () { let timeLogModal = BDFDB.htmlToElement(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.htmlToElement(`
`)); let entry = BDFDB.htmlToElement(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); } }