From dcf0deac39b65343a3f31f9bc1da0e79bffd3f20 Mon Sep 17 00:00:00 2001 From: Deykun Date: Mon, 29 Jul 2024 17:37:35 +0000 Subject: [PATCH 1/7] deploy: ca8d17f3cef772867287a7d06116571f5da87a49 --- .nojekyll | 0 assets/index-BLmGfr_b.js | 5 + assets/index-DozumpDh.css | 1 + github-usernames.user-srcipt.js | 1386 +++++++++++++++++++++++++++++++ index.html | 14 + 5 files changed, 1406 insertions(+) create mode 100644 .nojekyll create mode 100644 assets/index-BLmGfr_b.js create mode 100644 assets/index-DozumpDh.css create mode 100644 github-usernames.user-srcipt.js create mode 100644 index.html diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/assets/index-BLmGfr_b.js b/assets/index-BLmGfr_b.js new file mode 100644 index 0000000..cb8400b --- /dev/null +++ b/assets/index-BLmGfr_b.js @@ -0,0 +1,5 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))i(e);new MutationObserver(e=>{for(const r of e)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function n(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?r.credentials="include":e.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(e){if(e.ep)return;e.ep=!0;const r=n(e);fetch(e.href,r)}})();document.querySelector("#app").innerHTML=` +
+ Placeholder +
+`; diff --git a/assets/index-DozumpDh.css b/assets/index-DozumpDh.css new file mode 100644 index 0000000..1c14e3f --- /dev/null +++ b/assets/index-DozumpDh.css @@ -0,0 +1 @@ +*,*:before,*:after{box-sizing:border-box} diff --git a/github-usernames.user-srcipt.js b/github-usernames.user-srcipt.js new file mode 100644 index 0000000..37177cd --- /dev/null +++ b/github-usernames.user-srcipt.js @@ -0,0 +1,1386 @@ +// ==UserScript== +// @namespace deykun +// @name Usernames to names - GitHub +// @description Replace ambiguous usernames with actual names from user profiles. +// @author deykun +// @version 1.0.0 +// @include https://github.com* +// @grant none +// @run-at document-start +// @updateURL https://raw.githubusercontent.com/Deykun/github-usernames/main/public/github-usernames.user-srcipt.js +// @downloadURL https://raw.githubusercontent.com/Deykun/github-usernames/main/public/github-usernames.user-srcipt.js +// ==/UserScript== + +'use strict'; + +const getFromLocalStorage = (key, defaultValues = {}) => (localStorage.getItem(key) + ? { ...defaultValues, ...JSON.parse(localStorage.getItem(key)) } + : { ...defaultValues }); + +const defaultSettings = { + color: 'light', + name: 'name-s', + shouldShowUsernameWhenBetter: true, + shouldShowAvatars: true, + shouldFilterBySubstring: false, + filterSubstring: '', +}; + +const getSettingsFromLS = () => getFromLocalStorage('u2n-settings', defaultSettings); +const getUsersByUsernamesFromLS = () => getFromLocalStorage('u2n-users'); +const getCustomNamesByUsernamesFromLS = () => getFromLocalStorage('u2n-users-names'); + +window.U2N = { + version: '1.0.0', + isDevMode: false, + cache: { + HTML: {}, + CSS: {}, + inited: false, + status: null, + location: location.href, + }, + settings: getSettingsFromLS(), + usersByUsernames: getUsersByUsernamesFromLS(), + customNamesByUsernames: getCustomNamesByUsernamesFromLS(), + actions: {}, +}; + +window.U2N.ui = { + status: { + type: '', + text: '', + }, + openedContent: '', + eventsSubscribers: {}, +}; + + +const userScriptLogger = (params) => { + if (params.isError) { + const { isCritical = false, message = '', error } = params; + + if (isCritical) { + // eslint-disable-next-line no-console + console.error('A User2Names error (from Tampermonkey) has occurred. You can ignore it, or describe the error and create an issue here: https://github.com/Deykun/github-usernames/issues'); + // eslint-disable-next-line no-console + console.error(`U2N error: ${message}`); + // eslint-disable-next-line no-console + console.error(error); + } + + if (window.U2N.isDevMode && error) { + // eslint-disable-next-line no-console + console.error(error); + } + } +}; + +const domReady = (fn) => { + document.addEventListener('DOMContentLoaded', fn); + if (document.readyState === 'interactive' || document.readyState === 'complete') { + fn(); + } +}; + +const initU2N = async () => { + if (window.U2N.cache.inited) { + return; + } + + window.U2N.cache.inited = true; + + try { + const updateStatus = ({ type = '', text = '', durationInSeconds = 4 }) => { + if (window.U2N.cache.status) { + clearTimeout(window.U2N.cache.status); + } + + window.U2N.ui.status = { + type, + text, + }; + + renderStatus(); + + window.U2N.cache.status = setTimeout(() => { + window.U2N.ui.status = { + type: '', + text: '', + }; + + renderStatus(); + }, durationInSeconds * 1000); +}; + +const saveNewUsers = (usersByNumber = {}, params = {}) => { + const oldUserByUsernames = getUsersByUsernamesFromLS(); + + const newUserByUsernames = Object.entries(usersByNumber).reduce((stack, [username, value]) => { + const isValidUsername = username && !username.includes(' '); + if (isValidUsername) { + stack[username] = value; + } + + return stack; + }, JSON.parse(JSON.stringify(oldUserByUsernames))); + + const didChange = JSON.stringify(oldUserByUsernames) !== JSON.stringify(newUserByUsernames); + + if (!didChange) { + return false; + } + + window.U2N.usersByUsernames = newUserByUsernames; + localStorage.setItem('u2n-users', JSON.stringify(window.U2N.usersByUsernames)); + + renderUsers(); + updateStatus({ + type: 'users-update', + text: params.customStatusText || "The users' data were updated.", + }); + + if (window.U2N.ui.openedContent === 'settings') { + renderApp(); + } + + return true; +}; + +const saveNewUser = (newUser) => { + if (newUser.username) { + const wasUpdated = JSON.stringify(window.U2N.usersByUsernames?.[newUser.username]) + !== JSON.stringify(newUser); + + if (wasUpdated) { + return saveNewUsers({ + [newUser.username]: newUser, + }, { customStatusText: `${newUser.username}'s data was updated.` }); + } + } + + return false; +}; + +const saveDisplayNameForUsername = (username, name) => { + if (!username) { + return false; + } + + const customNamesByUsernames = getCustomNamesByUsernamesFromLS(); + + if (name) { + customNamesByUsernames[username] = name; + } else { + delete customNamesByUsernames[username]; + } + + window.U2N.customNamesByUsernames = customNamesByUsernames; + + localStorage.setItem('u2n-users-names', JSON.stringify(customNamesByUsernames)); + + renderUsers(); + updateStatus({ + type: 'users-update', + text: `${username}'s display name was updated.`, + }); + + return true; +}; + +const saveSetting = (settingName, value, params) => { + const acceptedSettingsNames = Object.keys(defaultSettings); + if (!acceptedSettingsNames.includes(settingName)) { + return false; + } + + const settings = getSettingsFromLS(); + + settings[settingName] = value; + + window.U2N.settings = settings; + localStorage.setItem('u2n-settings', JSON.stringify(settings)); + + renderApp(); + renderUsers(); + updateStatus({ + type: 'settings-update', + text: params?.customStatusText || 'A setting was updated.', + }); + + return true; +}; + +const resetUsers = () => { + localStorage.removeItem('u2n-users'); + localStorage.removeItem('u2n-users-names'); + window.U2N.usersByUsernames = {}; + window.U2N.customNamesByUsernames = {}; + renderUsers(); + renderApp(); + updateStatus({ + type: 'users-reset', + text: "The users' data were removed.", + }); +}; + + const appendCSS = (styles, { sourceName = '' } = {}) => { + const appendOnceSelector = sourceName ? `g-u2n-css-${sourceName}`.trim() : undefined; + if (appendOnceSelector) { + /* Already appended */ + if (document.getElementById(appendOnceSelector)) { + return; + } + } + + const style = document.createElement('style'); + if (sourceName) { + style.setAttribute('id', appendOnceSelector); + } + + style.innerHTML = styles; + document.head.append(style); +}; + +// eslint-disable-next-line default-param-last +const render = (HTML = '', source) => { + const id = `g-u2n-html-${source}`; + + if (HTML === window.U2N.cache.HTML[id]) { + /* Don't rerender if HTML is the same */ + return; + } + + window.U2N.cache.HTML[id] = HTML; + + const wrapperEl = document.getElementById(id); + + if (!HTML) { + if (wrapperEl) { + wrapperEl.remove(); + } + + return; + } + + if (wrapperEl) { + wrapperEl.innerHTML = HTML; + + return; + } + + const el = document.createElement('div'); + el.id = id; + el.setAttribute('data-testid', id); + el.innerHTML = HTML; + + document.body.appendChild(el); +}; + +const nestedSelectors = (selectors, subcontents) => { + return subcontents.map(([subselector, content]) => { + return `${selectors.map((selector) => `${selector} ${subselector}`).join(', ')} { + ${content} + }`; + }).join(' '); +}; + + const debounce = (fn, time) => { + let timeoutHandler; + + return (...args) => { + clearTimeout(timeoutHandler); + timeoutHandler = setTimeout(() => { + fn(...args); + }, time); + }; +}; + +const upperCaseFirstLetter = (text) => (typeof text === 'string' ? text.charAt(0).toUpperCase() + text.slice(1) : ''); + +const getShouldUseUsernameAsDisplayname = (username) => { + const { + shouldFilterBySubstring, + filterSubstring, + } = window.U2N.settings; + + if (!shouldFilterBySubstring) { + return false; + } + + const lowerCasedUsername = username?.toLowerCase(); + + const hasAtleastOneSubstringIncludedInUsername = filterSubstring.replaceAll(' ', '').split(',').some( + (substring) => lowerCasedUsername.includes(substring.toLowerCase()), + ); + + return !hasAtleastOneSubstringIncludedInUsername; +}; + +const getDisplayNameByUsername = (username) => { + if (getShouldUseUsernameAsDisplayname(username)) { + return username; + } + + const user = window.U2N.usersByUsernames?.[username]; + const customDisplayName = window.U2N.customNamesByUsernames?.[username]; + + if (customDisplayName) { + return customDisplayName; + } + + if (!user?.name) { + return username; + } + + const { + name: nameSetting, + shouldShowUsernameWhenBetter, + } = window.U2N.settings; + + if (nameSetting === 'username') { + return username; + } + + const subnames = user.name.split(' ').filter(Boolean).map((subname) => upperCaseFirstLetter(subname)); + + if (shouldShowUsernameWhenBetter) { + const nameToCompare = subnames.join(' '); + const totalNamesLetters = nameToCompare.match(/[a-zA-Z]/gi).length; + const totalUsernamesLetters = username.match(/[a-zA-Z]/gi).length; + + const isUsernameBetter = totalNamesLetters < totalUsernamesLetters && totalNamesLetters < 7; + + if (isUsernameBetter) { + return username; + } + } + + if (nameSetting === 'name-surname') { + return subnames.join(' '); + } + + const [firstName, ...restOfNames] = subnames; + + if (nameSetting === 'name-s') { + return [firstName, ...restOfNames.map((subname) => `${subname.at(0)}.`)].join(' '); + } + + if (nameSetting === 'name') { + return firstName; + } + + const [lastName, ...firstNamesReversed] = subnames.reverse(); + const firstNames = firstNamesReversed.reverse(); + + // n-surname + return [firstNames.map((subname) => `${subname.at(0)}.`), lastName].join(' '); +}; + + /* + https://iconmonstr.com +*/ + +const IconCog = ` + +`; +const IconGithub = ` + +`; +const IconNewUser = ` + +`; +const IconSave = ` + +`; +const IconThemes = ` + +`; +const IconWrench = ` + +`; +const IconUser = ` + +`; +const IconRemoveUsers = ` + +`; + + appendCSS(` +.u2n-text-input-wrapper { + display: flex; + gap: 5px; + position: relative; +} + +.u2n-text-input-wrapper input { + width: 100%; + padding-left: 10px; +} + +.u2n-text-input-wrapper label { + position: absolute; + top: 0; + left: 5px; + transform: translateY(-50%); + background-color: var(--u2n-nav-item-bg); + padding: 2px 5px; + border-radius: 2px; + font-size: 9px; +} +`, { sourceName: 'interface-text-input' }); + +const getTextInput = ({ + idInput, idButton, label, name, value = '', placeholder, isDisabled = false, +}) => { + return `
+ + ${label ? `` : ''} + +
`; +}; + +appendCSS(` +.u2n-checkbox-wrapper { + display: flex; + align-items: center; + gap: 5px; + font-weight: 400; +} + +.u2n-checkbox-wrapper input { + margin-left: 5px; + margin-right: 5px; +} +`, { sourceName: 'interface-value' }); + +const getCheckbox = ({ + idInput, classNameInput, label, name, value, isChecked = false, type = 'checkbox', +}) => { + return ``; +}; + +const getRadiobox = (params) => { + return getCheckbox({ ...params, type: 'radio' }); +}; + + appendCSS(` + .u2n-nav-popup-button.u2n-nav-popup-button--github { + color: var(--u2n-nav-item-bg); + background-color: var(--u2n-nav-item-text-strong); + } + + .u2n-nav-remove-all { + color: var(--fgColor-danger); + background: transparent; + border: none; + borer-bottom: 1px solid var(--fgColor-danger); + padding: 0; + font-size: 12px; + } + + .u2n-nav-popup-footer { + margin-top: -10px; + font-size: 10px; + color: var(--u2n-nav-item-text); + text-align: right; + } +`, { sourceName: 'render-app-settings' }); + +const getAppSettings = ({ isActive = false }) => { + const { settings } = window.U2N; + const totalSavedUsers = Object.values(window.U2N.usersByUsernames).length; + + return `
+ ${!isActive + ? `` + : ` +
+
+

${IconCog} Settings

+
+ Users saved: ${totalSavedUsers} + ${totalSavedUsers === 0 ? '' : ``} +
+
+ ${getCheckbox({ + idInput: 'settings-should-use-substring', + label: 'only use names from profiles when their username contains the specified string (use a comma for multiple)', + isChecked: settings.shouldFilterBySubstring, + })} + ${getTextInput({ + label: 'Edit substring', + placeholder: 'ex. company_', + idButton: 'settings-save-substring', + idInput: 'settings-value-substring', + value: settings.filterSubstring, + })} +
+
+ You can learn more or report an issue here: +
+ + ${IconGithub} deykun / github-usernames + + Version ${window.U2N.version} +
+
`} +
`; +}; + +window.U2N.ui.eventsSubscribers.removeAllUsers = { + selector: '#u2n-remove-all-users', + handleClick: resetUsers, +}; + +window.U2N.ui.eventsSubscribers.shouldFilterBySubstring = { + selector: '#settings-should-use-substring', + handleClick: (_, calledByElement) => { + saveSetting('shouldFilterBySubstring', calledByElement.checked); + }, +}; + +window.U2N.ui.eventsSubscribers.filterSubstring = { + selector: '#settings-save-substring', + handleClick: () => { + const value = document.getElementById('settings-value-substring')?.value || ''; + + saveSetting('filterSubstring', value); + }, +}; + + /* import @/render-app-status.js */ + const themeSettings = { + colors: [{ + label: 'Light', + value: 'light', + }, + { + label: 'Dark', + value: 'dark', + }, + { + label: 'Sky', + value: 'sky', + }, + { + label: 'Grass', + value: 'grass', + }], + names: [ + { + label: 'Dwight Schrute', + value: 'name-surname', + }, + { + label: 'Dwight S.', + value: 'name-s', + }, + { + label: 'Dwight', + value: 'name', + }, + { + label: 'D. Schrute', + value: 'n-surname', + }, + { + label: 'DSchrute911 (github\'s default)', + value: 'username', + }], +}; + +appendCSS(` + .u2u-names-list li:last-child { + grid-column: 1 / 3; + } +`, { sourceName: 'render-app-theme' }); + +const getAppTheme = ({ isActive = false }) => { + const { settings } = window.U2N; + + return `
+ ${!isActive + ? `` + : ` +
+
+

${IconThemes} Theme

+
+

Color

+
    + ${themeSettings.colors.map(({ label, value }) => `
  • + ${getRadiobox({ + name: 'color', + classNameInput: 'u2n-theme-color', + label, + value, + isChecked: settings.color === value, + })}
  • `).join('')} +
+
+
+

Display name

+
    + ${themeSettings.names.map(({ label, value }, index) => `
  • + ${getRadiobox({ + name: 'names', + classNameInput: 'u2n-theme-name', + label, + value, + isChecked: settings.name === value, + })}
  • `).join('')} +
+
+
+

Other

+ ${getCheckbox({ + idInput: 'settings-should-show-username-when-better', + label: 'should show the username when it fits better', + isChecked: settings.shouldShowUsernameWhenBetter, + })} + ${getCheckbox({ + idInput: 'settings-should-show-avatar', + label: 'should show avatars', + isChecked: settings.shouldShowAvatars, + })} +
+
+
`} +
`; +}; + +window.U2N.ui.eventsSubscribers.color = { + selector: '.u2n-theme-color', + handleClick: (_, calledByElement) => { + saveSetting('color', calledByElement.value); + }, +}; + +window.U2N.ui.eventsSubscribers.name = { + selector: '.u2n-theme-name', + handleClick: (_, calledByElement) => { + saveSetting('name', calledByElement.value); + }, +}; + +window.U2N.ui.eventsSubscribers.shouldShowAvatars = { + selector: '#settings-should-show-avatar', + handleClick: (_, calledByElement) => { + saveSetting('shouldShowAvatars', calledByElement.checked); + }, +}; + +window.U2N.ui.eventsSubscribers.shouldShowUsernameWhenBetter = { + selector: '#settings-should-show-username-when-better', + handleClick: (_, calledByElement) => { + saveSetting('shouldShowUsernameWhenBetter', calledByElement.checked); + }, +}; + + appendCSS(` + .u2n-nav-user-preview { + display: flex; + align-items: center; + gap: 10px; + height: 20px; + font-size: 10px; + } +`, { sourceName: 'render-app-user' }); + +const getAppUser = ({ isActive = false }) => { + const isProfilPage = Boolean(document.querySelector('.page-profile')); + const username = location.pathname.replace('/', ''); + + const shouldRender = Boolean(isProfilPage && username); + if (!shouldRender) { + return ''; + } + + const user = window.U2N.usersByUsernames?.[username] || {}; + const displayName = getDisplayNameByUsername(username); + + return `
+ ${!isActive + ? `` + : ` +
+
+

${IconUser} User

+
+ ${user.username} +
+
    +
  • + ID: ${user.id} +
  • +
  • + Username: ${user.username} +
  • +
  • + Name: ${user.name} +
  • +
+
+ ${getTextInput({ + label: 'Edit display name', + placeholder: displayName, + value: displayName, + name: username, + idButton: 'user-save-name', + idInput: 'user-value-name', + isDisabled: getShouldUseUsernameAsDisplayname(username), + })} + ${getShouldUseUsernameAsDisplayname(username) + ? 'This user is excluded by a string in the Settings tab.' + : ''} +
+
`} +
`; +}; + +window.U2N.ui.eventsSubscribers.displayNameUpdate = { + selector: '#user-save-name', + handleClick: () => { + const inputElement = document.getElementById('user-value-name'); + const username = inputElement.getAttribute('name'); + const displayName = inputElement.value; + + saveDisplayNameForUsername(username, displayName); + }, +}; + + appendCSS(` + :root { + --u2n-nav-item-size: 35px; + --u2n-nav-item-bg: var(--bgColor-muted); + --u2n-nav-item-bg: var(--bgColor-default); + --u2n-nav-item-text-strong: var(--fgColor-default); + --u2n-nav-item-text: var(--fgColor-muted); + --u2n-nav-item-text-hover: var(--fgColor-accent); + --u2n-nav-item-border: var(--borderColor-muted); + --u2n-nav-item-radius: 5px; + } + + .u2n-nav { + display: flex; + position: fixed; + bottom: 0; + right: 30px; + height: var(--u2n-nav-item-size); + filter: drop-shadow(0 0 10px rgba(0, 0, 0, 0.08)); + } + + .u2n-nav > * + * { + margin-left: -1px; + } + + .u2n-nav > :first-child { + border-top-left-radius: var(--u2n-nav-item-radius); + } + + .u2n-nav > :last-child { + border-top-right-radius: var(--u2n-nav-item-radius); + } + + .u2n-nav-status, + .u2n-nav-button-wrapper { + height: var(--u2n-nav-item-size); + min-width: var(--u2n-nav-item-size); + line-height: var(--u2n-nav-item-size); + border: 1px solid var(--u2n-nav-item-border); + border-bottom-width: 0px; + background: var(--u2n-nav-item-bg); + } + + .u2n-nav-button-wrapper { + position: relative; + } + + .u2n-nav-button { + background: transparent; + border: none; + padding: 0; + color: var(--u2n-nav-item-text); + width: var(--u2n-nav-item-size); + transition: 0.3s ease-in-out; + } + + .u2n-nav-button:hover { + color: var(--u2n-nav-item-text-hover); + } + + .u2n-nav-button--active { + color: var(--u2n-nav-item-text-strong); + } + + .u2n-nav-button svg { + fill: currentColor; + padding: 25%; + height: var(--u2n-nav-item-size); + width: var(--u2n-nav-item-size); + line-height: var(--u2n-nav-item-size); + } + + .u2n-nav-popup { + position: absolute; + right: 0; + bottom: calc(100% + 10px); + width: 300px; + color: var(--u2n-nav-item-text-strong); + border: 1px solid var(--u2n-nav-item-border); + border-radius: var(--u2n-nav-item-radius); + border-bottom-right-radius: 0; + background-color: var(--u2n-nav-item-bg); + } + + .u2n-nav-popup-content { + display: flex; + flex-flow: column; + gap: 18px; + max-height: calc(100vh - 60px); + overflow: auto; + padding: 10px; + padding-top: 0; + font-size: 12px; + line-height: 1.3; + text-align: left; + } + + .u2n-nav-popup-title { + position: sticky; + top: 0px; + display: flex; + align-items: center; + gap: 8px; + padding-top: 10px; + padding-bottom: 5px; + font-size: 16px; + background-color: var(--u2n-nav-item-bg); + } + + .u2n-nav-popup-title svg { + fill: currentColor; + height: 16px; + width: 16px; + } + + .u2n-nav-popup h3 { + font-size: 13px; + margin-bottom: 8px; + } + + .u2n-nav-popup ul { + display: flex; + flex-flow: column; + gap: 8px; + list-style: none; + } + + .u2n-nav-popup .grid-2 { + display: grid; + grid-template-columns: 1fr 1fr; + } + + .u2n-nav-popup::after { + content: ''; + position: absolute; + bottom: -10px; + right: calc((var(--u2n-nav-item-size) / 2) - 5px); + width: 0; + height: 0; + border: 5px solid transparent; + border-top-color: var(--u2n-nav-item-border); + } + + .u2n-nav-popup-button { + display: flex; + gap: 10px; + justify-content: center; + align-items: center; + padding: 8px; + border-radius: 3px; + font-size: 14px; + letter-spacing: 0.04em; + text-decoration: none; + background: none; + border: none; + color: var(--bgColor-default); + background-color: var(--fgColor-success); + } + + .u2n-nav-popup-button:hover { + text-decoration: none; + } + + .u2n-nav-popup-button svg { + fill: currentColor; + width: 18px; + height: 18px; + } +`, { sourceName: 'render-app' }); + +const renderApp = () => { + const content = window.U2N.ui.openedContent; + + render(``, 'u2n-app'); +}; + +window.U2N.ui.eventsSubscribers.content = { + selector: '.u2n-nav-button', + handleClick: (_, calledByElement) => { + if (calledByElement) { + const content = calledByElement.getAttribute('data-content'); + const isClose = !content || content === window.U2N.ui.openedContent; + + if (isClose) { + window.U2N.ui.openedContent = ''; + } else { + window.U2N.ui.openedContent = content; + } + } + + renderApp(); + }, +}; + + appendCSS(` + .u2n-nav-status { + display: flex; + position: fixed; + bottom: 0; + left: 50%; + height: var(--u2n-nav-item-size); + filter: drop-shadow(0 0 10px rgba(0, 0, 0, 0.08)); + display: inline-flex; + align-items: center; + justify-content: center; + gap: 10px; + padding: 0 10px; + margin-right: 10px; + border-top-left-radius: var(--u2n-nav-item-radius); + border-top-right-radius: var(--u2n-nav-item-radius); + color: var(--fgColor-default); + font-size: 12px; + transform: translateY(60px) translateX(-50%); + animation: U2NSlideInFromTop 0.4s cubic-bezier(0.1, 0.7, 1, 0.1) forwards; + } + + @keyframes U2NSlideInFromTop { + 0% { + transform: translateY(60px) translateX(-50%); + } + 100% { + transform: translateY(0) translateX(-50%); + } + } + + .u2n-nav-status svg { + fill: currentColor; + color: var(--fgColor-success); + height: 14px; + width: 14px; + } + + .u2n-nav-status--danger svg { + color: var(--fgColor-danger); + } +`, { sourceName: 'render-app-status' }); + +const StatusIconByType = { + 'users-update': IconNewUser, + 'users-reset': IconRemoveUsers, + 'settings-update': IconWrench, +}; + +const renderStatus = () => { + const { + type, + text: statusText = '', + } = window.U2N.ui.status; + + if (!statusText) { + render('', 'u2n-status'); + + return; + } + + const Icon = StatusIconByType[type] || ''; + const isNegative = ['users-reset'].includes(type); + + render(` + ${Icon} ${statusText} +`, 'u2n-status'); +}; + + const getUserElements = () => { + const links = Array.from(document.querySelectorAll('[data-hovercard-url^="/users/"]')).map((el) => { + const username = el.getAttribute('data-hovercard-url').match(/users\/([A-Za-z0-9_-]+)\//)[1]; + + if (username && el.textContent.includes(username)) { + return { + el, + username, + }; + } + + return undefined; + }).filter(Boolean); + + return links; +}; + +appendCSS(` + :root { + --u2n-user-text: #00293e; + --u2n-user-bg: #f2f2f2; + --u2n-user-text--hover: #0054ae; + --u2n-user-bg--hover: #dbedff; + } + + body[data-u2n-color="dark"] { + --u2n-user-text: white; + --u2n-user-bg: #26292e; + --u2n-user-text--hover: #dbedff; + --u2n-user-bg--hover: #142a42; + } + + body[data-u2n-color="sky"] { + --u2n-user-text: #03113c; + --u2n-user-bg: #def3fa; + --u2n-user-text--hover: #000; + --u2n-user-bg--hover: #beedfc; + } + + body[data-u2n-color="grass"] { + --u2n-user-text: #fff; + --u2n-user-bg: #163b13; + --u2n-user-text--hover: #b8ffb3; + --u2n-user-bg--hover: #30582d; + } + + [data-u2n-cache-user] { + display: inline-block; + font-size: 0; + text-overflow: unset !important; + } + + .user-mention[data-u2n-cache-user] { + background-color: transparent !important; + } + + .u2n-tag { + align-self: center; + display: inline-flex; + align-items: center; + gap: 5px; + margin-left: 3px; + padding: 0 6px; + border-radius: 4px; + font-size: 12px; + letter-spacing: 0.05em; + font-weight: 600; + font-style: normal; + text-decoration: none !important; + line-height: 19px; + height: 18px; + white-space: nowrap; + color: var(--u2n-user-text) !important; + background-color: var(--u2n-user-bg) !important; + transition: 0.15s ease-in-out; + position: relative; + } + + .u2n-tag svg { + display: inline-block; + vertical-align: middle; + fill: currentColor; + height: 10px; + width: 10px; + } + + .u2n-tag img { + position: absolute; + left: 0; + top: 0; + border-radius: 4px; + height: 100%; + aspect-ratio: 1 / 1; + } + + .u2n-tag:hover { + color: var(--u2n-user-text--hover) !important; + background-color: var(--u2n-user-bg--hover) !important; + } + + /* We hide them and show them only in verified locations */ + .u2n-tag-avatar { + display: none; + } + + ${nestedSelectors([ + '.gh-header', // pr header on pr site + '.u2n-nav-user-preview', // preview in user tab + '[data-issue-and-pr-hovercards-enabled] [id*="issue_"]', // prs in repo + '[data-issue-and-pr-hovercards-enabled] [id*="check_"]', // actions in repo + '.timeline-comment-header', // comments headers + '.comment-body', // comments body + ], [ + ['.u2n-tag-avatar', 'display: inline-block;'], + ['.u2n-tag-avatar + *', 'margin-left: 1.5em;'], + ])} +`, { sourceName: 'render-users' }); + +const renderUsers = () => { + const elements = getUserElements(); + const { + color, + shouldShowAvatars, + } = window.U2N.settings; + + const shouldUpdateTheme = document.body.getAttribute('data-u2n-color') !== color; + if (shouldUpdateTheme) { + document.body.setAttribute('data-u2n-color', color); + } + + elements.forEach(({ el, username }) => { + const user = window.U2N.usersByUsernames?.[username]; + const displayName = getDisplayNameByUsername(username); + + const cacheValue = `${displayName}${user ? '+u' : '-u'}${shouldShowAvatars ? '+a' : '-a'}`; + + const isAlreadySet = el.getAttribute('data-u2n-cache-user') === cacheValue; + if (isAlreadySet) { + return; + } + + el.setAttribute('data-u2n-cache-user', cacheValue); + + el.querySelector('.u2n-tags-holder')?.remove(); + + const tagsHolderEl = document.createElement('span'); + + let holderClassNames = 'u2n-tags-holder u2n-tags--user'; + if (!user) { + holderClassNames += ' u2n-tags--no-data'; + } + + tagsHolderEl.setAttribute('class', holderClassNames); + + const tagEl = document.createElement('span'); + tagEl.setAttribute('class', 'u2n-tag'); + + const avatarSrc = user?.avatarSrc || ''; + + tagEl.innerHTML = `${shouldShowAvatars && avatarSrc ? `` : ''}${displayName}`; + + tagsHolderEl.append(tagEl); + + el.append(tagsHolderEl); + }); +}; + + const getUserFromUserPageIfPossible = () => { + const elProfile = document.querySelector('.page-profile .js-profile-editable-replace'); + + if (elProfile) { + try { + const avatarEl = elProfile.querySelector('.avatar-user'); + const avatarSrc = avatarEl?.getAttribute('src')?.split('?')[0] || ''; + const id = avatarSrc ? avatarSrc.match(/u\/([0-9]+)?/)[1] : ''; + const username = elProfile.querySelector('.vcard-username')?.textContent?.trim() || ''; + const name = elProfile.querySelector('.vcard-fullname')?.textContent?.trim() || ''; + + return { + id, + username, + avatarSrc, + name, + }; + } catch (error) { + userScriptLogger({ + isError: true, message: 'getUserFromUserPageIfPossible() failed while parsing the profile', error, + }); + } + } + + return undefined; +}; + +const getUserFromHovercardIfPossible = () => { + const elHovercard = document.querySelector('.user-hovercard-avatar'); + + if (elHovercard) { + try { + const avatarEl = elHovercard.querySelector('.avatar-user'); + const avatarSrc = avatarEl?.getAttribute('src')?.split('?')[0] || ''; + const id = avatarSrc ? avatarSrc.match(/u\/([0-9]+)?/)[1] : ''; + const username = avatarEl?.getAttribute('alt')?.replace('@', '').trim(); + const name = elHovercard.parentNode.parentNode.querySelector(`.Link--secondary[href="/${username}"]`)?.textContent?.trim() || ''; + + return { + id, + username, + avatarSrc, + name, + }; + } catch (error) { + userScriptLogger({ + isError: true, message: 'getUserFromHovercardIfPossible() failed while parsing the card', error, + }); + } + } + + return undefined; +}; + +const getUsersFromPeopleListIfPossible = () => { + if (!location.pathname.includes('/people')) { + return []; + } + + try { + const usersEls = Array.from(document.querySelectorAll('li[data-bulk-actions-id]')); + + const users = usersEls.map((el) => { + const avatarEl = el.querySelector('.avatar-user'); + const avatarSrc = avatarEl?.getAttribute('src')?.split('?')[0] || ''; + const id = avatarSrc ? avatarSrc.match(/u\/([0-9]+)?/)[1] : ''; + const username = avatarEl?.getAttribute('alt')?.replace('@', '').trim(); + const name = Array.from( + el.querySelector('[data-test-selector="linked-name-is-full-if-exists"]').childNodes, + ).find((child) => child.nodeType === Node.TEXT_NODE).textContent.trim(); + + return { + id, + username, + avatarSrc, + name, + }; + }); + + return users; + } catch (error) { + userScriptLogger({ + isError: true, message: 'getUsersFromPeopleListIfPossible() failed while parsing the people list', error, + }); + } + + return []; +}; + +const saveNewUsersIfPossible = () => { + const userFromProfile = getUserFromUserPageIfPossible(); + if (userFromProfile) { + saveNewUser(userFromProfile); + } + + const userFromHoverCard = getUserFromHovercardIfPossible(); + if (userFromHoverCard) { + saveNewUser(userFromHoverCard); + } + + const usersFromPeopleList = getUsersFromPeopleListIfPossible(); + if (usersFromPeopleList.length > 0) { + saveNewUsers(usersFromPeopleList.reduce((stack, user) => { + stack[user.username] = user; + + return stack; + }, {}), { customStatusText: `${usersFromPeopleList.length} users' data were updated` }); + } +}; + + + saveNewUsersIfPossible(); + renderUsers(); + renderStatus(); + renderApp(); + + try { + document.body.addEventListener('click', (event) => { + const handlerData = Object.values(window.U2N.ui.eventsSubscribers).find(({ selector }) => { + /* It checks max 4 nodes, while .closest() would look for all the nodes to body */ + const matchedHandlerData = [ + event.target, + event.target?.parentElement, + event.target?.parentElement?.parentElement, + event.target?.parentElement?.parentElement?.parentElement, + ].filter(Boolean).find((el) => el.matches(selector)); + + return Boolean(matchedHandlerData); + }); + + if (handlerData) { + const { selector, handleClick, shouldPreventDefault = true } = handlerData; + + if (shouldPreventDefault) { + event.preventDefault(); + } + + const calledByElement = event.target.closest(selector); + + handleClick(event, calledByElement); + } + }); +} catch (error) { + userScriptLogger({ + isError: true, isCritical: true, message: 'Click detect failed', error, + }); +} + + + const debouncedRefresh = debounce(() => { + saveNewUsersIfPossible(); + renderUsers(); + + const didLocationChange = location.href !== window.U2N.cache.location; + if (didLocationChange) { + window.U2N.cache.location = location.href; + + renderApp(); + } + }, 500); + + const observer = new MutationObserver(debouncedRefresh); + const config = { + childList: true, + subtree: true, + }; + observer.observe(document.body, config); + } catch (error) { + userScriptLogger({ + isError: true, isCritical: true, message: 'initU2N() failed', error, + }); + + throw error; + } +}; + +domReady(initU2N); diff --git a/index.html b/index.html new file mode 100644 index 0000000..f8d6df9 --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + + Usernames to names - GitHub + + + + +
+ + From 960fd5e19b15b1d9374465d64c2c56b27bfe5026 Mon Sep 17 00:00:00 2001 From: Deykun Date: Mon, 29 Jul 2024 17:47:09 +0000 Subject: [PATCH 2/7] deploy: 6e8fb090acf85e623a33c87b4e86a72fb82c6d8f --- github-usernames.user.js | 1386 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 1386 insertions(+) create mode 100644 github-usernames.user.js diff --git a/github-usernames.user.js b/github-usernames.user.js new file mode 100644 index 0000000..ad4238a --- /dev/null +++ b/github-usernames.user.js @@ -0,0 +1,1386 @@ +// ==UserScript== +// @name Usernames to names - GitHub +// @description Replace ambiguous usernames with actual names from user profiles. +// @namespace deykun +// @author deykun +// @version 1.0.0 +// @include https://github.com* +// @grant none +// @run-at document-start +// @updateURL https://deykun.github.io/github-usernames/github-usernames.user-srcipt.js +// @downloadURL https://deykun.github.io/github-usernames/github-usernames.user-srcipt.js +// ==/UserScript== + +'use strict'; + +const getFromLocalStorage = (key, defaultValues = {}) => (localStorage.getItem(key) + ? { ...defaultValues, ...JSON.parse(localStorage.getItem(key)) } + : { ...defaultValues }); + +const defaultSettings = { + color: 'light', + name: 'name-s', + shouldShowUsernameWhenBetter: true, + shouldShowAvatars: true, + shouldFilterBySubstring: false, + filterSubstring: '', +}; + +const getSettingsFromLS = () => getFromLocalStorage('u2n-settings', defaultSettings); +const getUsersByUsernamesFromLS = () => getFromLocalStorage('u2n-users'); +const getCustomNamesByUsernamesFromLS = () => getFromLocalStorage('u2n-users-names'); + +window.U2N = { + version: '1.0.0', + isDevMode: false, + cache: { + HTML: {}, + CSS: {}, + inited: false, + status: null, + location: location.href, + }, + settings: getSettingsFromLS(), + usersByUsernames: getUsersByUsernamesFromLS(), + customNamesByUsernames: getCustomNamesByUsernamesFromLS(), + actions: {}, +}; + +window.U2N.ui = { + status: { + type: '', + text: '', + }, + openedContent: '', + eventsSubscribers: {}, +}; + + +const userScriptLogger = (params) => { + if (params.isError) { + const { isCritical = false, message = '', error } = params; + + if (isCritical) { + // eslint-disable-next-line no-console + console.error('A User2Names error (from Tampermonkey) has occurred. You can ignore it, or describe the error and create an issue here: https://github.com/Deykun/github-usernames/issues'); + // eslint-disable-next-line no-console + console.error(`U2N error: ${message}`); + // eslint-disable-next-line no-console + console.error(error); + } + + if (window.U2N.isDevMode && error) { + // eslint-disable-next-line no-console + console.error(error); + } + } +}; + +const domReady = (fn) => { + document.addEventListener('DOMContentLoaded', fn); + if (document.readyState === 'interactive' || document.readyState === 'complete') { + fn(); + } +}; + +const initU2N = async () => { + if (window.U2N.cache.inited) { + return; + } + + window.U2N.cache.inited = true; + + try { + const updateStatus = ({ type = '', text = '', durationInSeconds = 4 }) => { + if (window.U2N.cache.status) { + clearTimeout(window.U2N.cache.status); + } + + window.U2N.ui.status = { + type, + text, + }; + + renderStatus(); + + window.U2N.cache.status = setTimeout(() => { + window.U2N.ui.status = { + type: '', + text: '', + }; + + renderStatus(); + }, durationInSeconds * 1000); +}; + +const saveNewUsers = (usersByNumber = {}, params = {}) => { + const oldUserByUsernames = getUsersByUsernamesFromLS(); + + const newUserByUsernames = Object.entries(usersByNumber).reduce((stack, [username, value]) => { + const isValidUsername = username && !username.includes(' '); + if (isValidUsername) { + stack[username] = value; + } + + return stack; + }, JSON.parse(JSON.stringify(oldUserByUsernames))); + + const didChange = JSON.stringify(oldUserByUsernames) !== JSON.stringify(newUserByUsernames); + + if (!didChange) { + return false; + } + + window.U2N.usersByUsernames = newUserByUsernames; + localStorage.setItem('u2n-users', JSON.stringify(window.U2N.usersByUsernames)); + + renderUsers(); + updateStatus({ + type: 'users-update', + text: params.customStatusText || "The users' data were updated.", + }); + + if (window.U2N.ui.openedContent === 'settings') { + renderApp(); + } + + return true; +}; + +const saveNewUser = (newUser) => { + if (newUser.username) { + const wasUpdated = JSON.stringify(window.U2N.usersByUsernames?.[newUser.username]) + !== JSON.stringify(newUser); + + if (wasUpdated) { + return saveNewUsers({ + [newUser.username]: newUser, + }, { customStatusText: `${newUser.username}'s data was updated.` }); + } + } + + return false; +}; + +const saveDisplayNameForUsername = (username, name) => { + if (!username) { + return false; + } + + const customNamesByUsernames = getCustomNamesByUsernamesFromLS(); + + if (name) { + customNamesByUsernames[username] = name; + } else { + delete customNamesByUsernames[username]; + } + + window.U2N.customNamesByUsernames = customNamesByUsernames; + + localStorage.setItem('u2n-users-names', JSON.stringify(customNamesByUsernames)); + + renderUsers(); + updateStatus({ + type: 'users-update', + text: `${username}'s display name was updated.`, + }); + + return true; +}; + +const saveSetting = (settingName, value, params) => { + const acceptedSettingsNames = Object.keys(defaultSettings); + if (!acceptedSettingsNames.includes(settingName)) { + return false; + } + + const settings = getSettingsFromLS(); + + settings[settingName] = value; + + window.U2N.settings = settings; + localStorage.setItem('u2n-settings', JSON.stringify(settings)); + + renderApp(); + renderUsers(); + updateStatus({ + type: 'settings-update', + text: params?.customStatusText || 'A setting was updated.', + }); + + return true; +}; + +const resetUsers = () => { + localStorage.removeItem('u2n-users'); + localStorage.removeItem('u2n-users-names'); + window.U2N.usersByUsernames = {}; + window.U2N.customNamesByUsernames = {}; + renderUsers(); + renderApp(); + updateStatus({ + type: 'users-reset', + text: "The users' data were removed.", + }); +}; + + const appendCSS = (styles, { sourceName = '' } = {}) => { + const appendOnceSelector = sourceName ? `g-u2n-css-${sourceName}`.trim() : undefined; + if (appendOnceSelector) { + /* Already appended */ + if (document.getElementById(appendOnceSelector)) { + return; + } + } + + const style = document.createElement('style'); + if (sourceName) { + style.setAttribute('id', appendOnceSelector); + } + + style.innerHTML = styles; + document.head.append(style); +}; + +// eslint-disable-next-line default-param-last +const render = (HTML = '', source) => { + const id = `g-u2n-html-${source}`; + + if (HTML === window.U2N.cache.HTML[id]) { + /* Don't rerender if HTML is the same */ + return; + } + + window.U2N.cache.HTML[id] = HTML; + + const wrapperEl = document.getElementById(id); + + if (!HTML) { + if (wrapperEl) { + wrapperEl.remove(); + } + + return; + } + + if (wrapperEl) { + wrapperEl.innerHTML = HTML; + + return; + } + + const el = document.createElement('div'); + el.id = id; + el.setAttribute('data-testid', id); + el.innerHTML = HTML; + + document.body.appendChild(el); +}; + +const nestedSelectors = (selectors, subcontents) => { + return subcontents.map(([subselector, content]) => { + return `${selectors.map((selector) => `${selector} ${subselector}`).join(', ')} { + ${content} + }`; + }).join(' '); +}; + + const debounce = (fn, time) => { + let timeoutHandler; + + return (...args) => { + clearTimeout(timeoutHandler); + timeoutHandler = setTimeout(() => { + fn(...args); + }, time); + }; +}; + +const upperCaseFirstLetter = (text) => (typeof text === 'string' ? text.charAt(0).toUpperCase() + text.slice(1) : ''); + +const getShouldUseUsernameAsDisplayname = (username) => { + const { + shouldFilterBySubstring, + filterSubstring, + } = window.U2N.settings; + + if (!shouldFilterBySubstring) { + return false; + } + + const lowerCasedUsername = username?.toLowerCase(); + + const hasAtleastOneSubstringIncludedInUsername = filterSubstring.replaceAll(' ', '').split(',').some( + (substring) => lowerCasedUsername.includes(substring.toLowerCase()), + ); + + return !hasAtleastOneSubstringIncludedInUsername; +}; + +const getDisplayNameByUsername = (username) => { + if (getShouldUseUsernameAsDisplayname(username)) { + return username; + } + + const user = window.U2N.usersByUsernames?.[username]; + const customDisplayName = window.U2N.customNamesByUsernames?.[username]; + + if (customDisplayName) { + return customDisplayName; + } + + if (!user?.name) { + return username; + } + + const { + name: nameSetting, + shouldShowUsernameWhenBetter, + } = window.U2N.settings; + + if (nameSetting === 'username') { + return username; + } + + const subnames = user.name.split(' ').filter(Boolean).map((subname) => upperCaseFirstLetter(subname)); + + if (shouldShowUsernameWhenBetter) { + const nameToCompare = subnames.join(' '); + const totalNamesLetters = nameToCompare.match(/[a-zA-Z]/gi).length; + const totalUsernamesLetters = username.match(/[a-zA-Z]/gi).length; + + const isUsernameBetter = totalNamesLetters < totalUsernamesLetters && totalNamesLetters < 7; + + if (isUsernameBetter) { + return username; + } + } + + if (nameSetting === 'name-surname') { + return subnames.join(' '); + } + + const [firstName, ...restOfNames] = subnames; + + if (nameSetting === 'name-s') { + return [firstName, ...restOfNames.map((subname) => `${subname.at(0)}.`)].join(' '); + } + + if (nameSetting === 'name') { + return firstName; + } + + const [lastName, ...firstNamesReversed] = subnames.reverse(); + const firstNames = firstNamesReversed.reverse(); + + // n-surname + return [firstNames.map((subname) => `${subname.at(0)}.`), lastName].join(' '); +}; + + /* + https://iconmonstr.com +*/ + +const IconCog = ` + +`; +const IconGithub = ` + +`; +const IconNewUser = ` + +`; +const IconSave = ` + +`; +const IconThemes = ` + +`; +const IconWrench = ` + +`; +const IconUser = ` + +`; +const IconRemoveUsers = ` + +`; + + appendCSS(` +.u2n-text-input-wrapper { + display: flex; + gap: 5px; + position: relative; +} + +.u2n-text-input-wrapper input { + width: 100%; + padding-left: 10px; +} + +.u2n-text-input-wrapper label { + position: absolute; + top: 0; + left: 5px; + transform: translateY(-50%); + background-color: var(--u2n-nav-item-bg); + padding: 2px 5px; + border-radius: 2px; + font-size: 9px; +} +`, { sourceName: 'interface-text-input' }); + +const getTextInput = ({ + idInput, idButton, label, name, value = '', placeholder, isDisabled = false, +}) => { + return `
+ + ${label ? `` : ''} + +
`; +}; + +appendCSS(` +.u2n-checkbox-wrapper { + display: flex; + align-items: center; + gap: 5px; + font-weight: 400; +} + +.u2n-checkbox-wrapper input { + margin-left: 5px; + margin-right: 5px; +} +`, { sourceName: 'interface-value' }); + +const getCheckbox = ({ + idInput, classNameInput, label, name, value, isChecked = false, type = 'checkbox', +}) => { + return ``; +}; + +const getRadiobox = (params) => { + return getCheckbox({ ...params, type: 'radio' }); +}; + + appendCSS(` + .u2n-nav-popup-button.u2n-nav-popup-button--github { + color: var(--u2n-nav-item-bg); + background-color: var(--u2n-nav-item-text-strong); + } + + .u2n-nav-remove-all { + color: var(--fgColor-danger); + background: transparent; + border: none; + borer-bottom: 1px solid var(--fgColor-danger); + padding: 0; + font-size: 12px; + } + + .u2n-nav-popup-footer { + margin-top: -10px; + font-size: 10px; + color: var(--u2n-nav-item-text); + text-align: right; + } +`, { sourceName: 'render-app-settings' }); + +const getAppSettings = ({ isActive = false }) => { + const { settings } = window.U2N; + const totalSavedUsers = Object.values(window.U2N.usersByUsernames).length; + + return `
+ ${!isActive + ? `` + : ` +
+
+

${IconCog} Settings

+
+ Users saved: ${totalSavedUsers} + ${totalSavedUsers === 0 ? '' : ``} +
+
+ ${getCheckbox({ + idInput: 'settings-should-use-substring', + label: 'only use names from profiles when their username contains the specified string (use a comma for multiple)', + isChecked: settings.shouldFilterBySubstring, + })} + ${getTextInput({ + label: 'Edit substring', + placeholder: 'ex. company_', + idButton: 'settings-save-substring', + idInput: 'settings-value-substring', + value: settings.filterSubstring, + })} +
+
+ You can learn more or report an issue here: +
+ + ${IconGithub} deykun / github-usernames + + Version ${window.U2N.version} +
+
`} +
`; +}; + +window.U2N.ui.eventsSubscribers.removeAllUsers = { + selector: '#u2n-remove-all-users', + handleClick: resetUsers, +}; + +window.U2N.ui.eventsSubscribers.shouldFilterBySubstring = { + selector: '#settings-should-use-substring', + handleClick: (_, calledByElement) => { + saveSetting('shouldFilterBySubstring', calledByElement.checked); + }, +}; + +window.U2N.ui.eventsSubscribers.filterSubstring = { + selector: '#settings-save-substring', + handleClick: () => { + const value = document.getElementById('settings-value-substring')?.value || ''; + + saveSetting('filterSubstring', value); + }, +}; + + /* import @/render-app-status.js */ + const themeSettings = { + colors: [{ + label: 'Light', + value: 'light', + }, + { + label: 'Dark', + value: 'dark', + }, + { + label: 'Sky', + value: 'sky', + }, + { + label: 'Grass', + value: 'grass', + }], + names: [ + { + label: 'Dwight Schrute', + value: 'name-surname', + }, + { + label: 'Dwight S.', + value: 'name-s', + }, + { + label: 'Dwight', + value: 'name', + }, + { + label: 'D. Schrute', + value: 'n-surname', + }, + { + label: 'DSchrute911 (github\'s default)', + value: 'username', + }], +}; + +appendCSS(` + .u2u-names-list li:last-child { + grid-column: 1 / 3; + } +`, { sourceName: 'render-app-theme' }); + +const getAppTheme = ({ isActive = false }) => { + const { settings } = window.U2N; + + return `
+ ${!isActive + ? `` + : ` +
+
+

${IconThemes} Theme

+
+

Color

+
    + ${themeSettings.colors.map(({ label, value }) => `
  • + ${getRadiobox({ + name: 'color', + classNameInput: 'u2n-theme-color', + label, + value, + isChecked: settings.color === value, + })}
  • `).join('')} +
+
+
+

Display name

+
    + ${themeSettings.names.map(({ label, value }, index) => `
  • + ${getRadiobox({ + name: 'names', + classNameInput: 'u2n-theme-name', + label, + value, + isChecked: settings.name === value, + })}
  • `).join('')} +
+
+
+

Other

+ ${getCheckbox({ + idInput: 'settings-should-show-username-when-better', + label: 'should show the username when it fits better', + isChecked: settings.shouldShowUsernameWhenBetter, + })} + ${getCheckbox({ + idInput: 'settings-should-show-avatar', + label: 'should show avatars', + isChecked: settings.shouldShowAvatars, + })} +
+
+
`} +
`; +}; + +window.U2N.ui.eventsSubscribers.color = { + selector: '.u2n-theme-color', + handleClick: (_, calledByElement) => { + saveSetting('color', calledByElement.value); + }, +}; + +window.U2N.ui.eventsSubscribers.name = { + selector: '.u2n-theme-name', + handleClick: (_, calledByElement) => { + saveSetting('name', calledByElement.value); + }, +}; + +window.U2N.ui.eventsSubscribers.shouldShowAvatars = { + selector: '#settings-should-show-avatar', + handleClick: (_, calledByElement) => { + saveSetting('shouldShowAvatars', calledByElement.checked); + }, +}; + +window.U2N.ui.eventsSubscribers.shouldShowUsernameWhenBetter = { + selector: '#settings-should-show-username-when-better', + handleClick: (_, calledByElement) => { + saveSetting('shouldShowUsernameWhenBetter', calledByElement.checked); + }, +}; + + appendCSS(` + .u2n-nav-user-preview { + display: flex; + align-items: center; + gap: 10px; + height: 20px; + font-size: 10px; + } +`, { sourceName: 'render-app-user' }); + +const getAppUser = ({ isActive = false }) => { + const isProfilPage = Boolean(document.querySelector('.page-profile')); + const username = location.pathname.replace('/', ''); + + const shouldRender = Boolean(isProfilPage && username); + if (!shouldRender) { + return ''; + } + + const user = window.U2N.usersByUsernames?.[username] || {}; + const displayName = getDisplayNameByUsername(username); + + return `
+ ${!isActive + ? `` + : ` +
+
+

${IconUser} User

+
+ ${user.username} +
+
    +
  • + ID: ${user.id} +
  • +
  • + Username: ${user.username} +
  • +
  • + Name: ${user.name} +
  • +
+
+ ${getTextInput({ + label: 'Edit display name', + placeholder: displayName, + value: displayName, + name: username, + idButton: 'user-save-name', + idInput: 'user-value-name', + isDisabled: getShouldUseUsernameAsDisplayname(username), + })} + ${getShouldUseUsernameAsDisplayname(username) + ? 'This user is excluded by a string in the Settings tab.' + : ''} +
+
`} +
`; +}; + +window.U2N.ui.eventsSubscribers.displayNameUpdate = { + selector: '#user-save-name', + handleClick: () => { + const inputElement = document.getElementById('user-value-name'); + const username = inputElement.getAttribute('name'); + const displayName = inputElement.value; + + saveDisplayNameForUsername(username, displayName); + }, +}; + + appendCSS(` + :root { + --u2n-nav-item-size: 35px; + --u2n-nav-item-bg: var(--bgColor-muted); + --u2n-nav-item-bg: var(--bgColor-default); + --u2n-nav-item-text-strong: var(--fgColor-default); + --u2n-nav-item-text: var(--fgColor-muted); + --u2n-nav-item-text-hover: var(--fgColor-accent); + --u2n-nav-item-border: var(--borderColor-muted); + --u2n-nav-item-radius: 5px; + } + + .u2n-nav { + display: flex; + position: fixed; + bottom: 0; + right: 30px; + height: var(--u2n-nav-item-size); + filter: drop-shadow(0 0 10px rgba(0, 0, 0, 0.08)); + } + + .u2n-nav > * + * { + margin-left: -1px; + } + + .u2n-nav > :first-child { + border-top-left-radius: var(--u2n-nav-item-radius); + } + + .u2n-nav > :last-child { + border-top-right-radius: var(--u2n-nav-item-radius); + } + + .u2n-nav-status, + .u2n-nav-button-wrapper { + height: var(--u2n-nav-item-size); + min-width: var(--u2n-nav-item-size); + line-height: var(--u2n-nav-item-size); + border: 1px solid var(--u2n-nav-item-border); + border-bottom-width: 0px; + background: var(--u2n-nav-item-bg); + } + + .u2n-nav-button-wrapper { + position: relative; + } + + .u2n-nav-button { + background: transparent; + border: none; + padding: 0; + color: var(--u2n-nav-item-text); + width: var(--u2n-nav-item-size); + transition: 0.3s ease-in-out; + } + + .u2n-nav-button:hover { + color: var(--u2n-nav-item-text-hover); + } + + .u2n-nav-button--active { + color: var(--u2n-nav-item-text-strong); + } + + .u2n-nav-button svg { + fill: currentColor; + padding: 25%; + height: var(--u2n-nav-item-size); + width: var(--u2n-nav-item-size); + line-height: var(--u2n-nav-item-size); + } + + .u2n-nav-popup { + position: absolute; + right: 0; + bottom: calc(100% + 10px); + width: 300px; + color: var(--u2n-nav-item-text-strong); + border: 1px solid var(--u2n-nav-item-border); + border-radius: var(--u2n-nav-item-radius); + border-bottom-right-radius: 0; + background-color: var(--u2n-nav-item-bg); + } + + .u2n-nav-popup-content { + display: flex; + flex-flow: column; + gap: 18px; + max-height: calc(100vh - 60px); + overflow: auto; + padding: 10px; + padding-top: 0; + font-size: 12px; + line-height: 1.3; + text-align: left; + } + + .u2n-nav-popup-title { + position: sticky; + top: 0px; + display: flex; + align-items: center; + gap: 8px; + padding-top: 10px; + padding-bottom: 5px; + font-size: 16px; + background-color: var(--u2n-nav-item-bg); + } + + .u2n-nav-popup-title svg { + fill: currentColor; + height: 16px; + width: 16px; + } + + .u2n-nav-popup h3 { + font-size: 13px; + margin-bottom: 8px; + } + + .u2n-nav-popup ul { + display: flex; + flex-flow: column; + gap: 8px; + list-style: none; + } + + .u2n-nav-popup .grid-2 { + display: grid; + grid-template-columns: 1fr 1fr; + } + + .u2n-nav-popup::after { + content: ''; + position: absolute; + bottom: -10px; + right: calc((var(--u2n-nav-item-size) / 2) - 5px); + width: 0; + height: 0; + border: 5px solid transparent; + border-top-color: var(--u2n-nav-item-border); + } + + .u2n-nav-popup-button { + display: flex; + gap: 10px; + justify-content: center; + align-items: center; + padding: 8px; + border-radius: 3px; + font-size: 14px; + letter-spacing: 0.04em; + text-decoration: none; + background: none; + border: none; + color: var(--bgColor-default); + background-color: var(--fgColor-success); + } + + .u2n-nav-popup-button:hover { + text-decoration: none; + } + + .u2n-nav-popup-button svg { + fill: currentColor; + width: 18px; + height: 18px; + } +`, { sourceName: 'render-app' }); + +const renderApp = () => { + const content = window.U2N.ui.openedContent; + + render(``, 'u2n-app'); +}; + +window.U2N.ui.eventsSubscribers.content = { + selector: '.u2n-nav-button', + handleClick: (_, calledByElement) => { + if (calledByElement) { + const content = calledByElement.getAttribute('data-content'); + const isClose = !content || content === window.U2N.ui.openedContent; + + if (isClose) { + window.U2N.ui.openedContent = ''; + } else { + window.U2N.ui.openedContent = content; + } + } + + renderApp(); + }, +}; + + appendCSS(` + .u2n-nav-status { + display: flex; + position: fixed; + bottom: 0; + left: 50%; + height: var(--u2n-nav-item-size); + filter: drop-shadow(0 0 10px rgba(0, 0, 0, 0.08)); + display: inline-flex; + align-items: center; + justify-content: center; + gap: 10px; + padding: 0 10px; + margin-right: 10px; + border-top-left-radius: var(--u2n-nav-item-radius); + border-top-right-radius: var(--u2n-nav-item-radius); + color: var(--fgColor-default); + font-size: 12px; + transform: translateY(60px) translateX(-50%); + animation: U2NSlideInFromTop 0.4s cubic-bezier(0.1, 0.7, 1, 0.1) forwards; + } + + @keyframes U2NSlideInFromTop { + 0% { + transform: translateY(60px) translateX(-50%); + } + 100% { + transform: translateY(0) translateX(-50%); + } + } + + .u2n-nav-status svg { + fill: currentColor; + color: var(--fgColor-success); + height: 14px; + width: 14px; + } + + .u2n-nav-status--danger svg { + color: var(--fgColor-danger); + } +`, { sourceName: 'render-app-status' }); + +const StatusIconByType = { + 'users-update': IconNewUser, + 'users-reset': IconRemoveUsers, + 'settings-update': IconWrench, +}; + +const renderStatus = () => { + const { + type, + text: statusText = '', + } = window.U2N.ui.status; + + if (!statusText) { + render('', 'u2n-status'); + + return; + } + + const Icon = StatusIconByType[type] || ''; + const isNegative = ['users-reset'].includes(type); + + render(` + ${Icon} ${statusText} +`, 'u2n-status'); +}; + + const getUserElements = () => { + const links = Array.from(document.querySelectorAll('[data-hovercard-url^="/users/"]')).map((el) => { + const username = el.getAttribute('data-hovercard-url').match(/users\/([A-Za-z0-9_-]+)\//)[1]; + + if (username && el.textContent.includes(username)) { + return { + el, + username, + }; + } + + return undefined; + }).filter(Boolean); + + return links; +}; + +appendCSS(` + :root { + --u2n-user-text: #00293e; + --u2n-user-bg: #f2f2f2; + --u2n-user-text--hover: #0054ae; + --u2n-user-bg--hover: #dbedff; + } + + body[data-u2n-color="dark"] { + --u2n-user-text: white; + --u2n-user-bg: #26292e; + --u2n-user-text--hover: #dbedff; + --u2n-user-bg--hover: #142a42; + } + + body[data-u2n-color="sky"] { + --u2n-user-text: #03113c; + --u2n-user-bg: #def3fa; + --u2n-user-text--hover: #000; + --u2n-user-bg--hover: #beedfc; + } + + body[data-u2n-color="grass"] { + --u2n-user-text: #fff; + --u2n-user-bg: #163b13; + --u2n-user-text--hover: #b8ffb3; + --u2n-user-bg--hover: #30582d; + } + + [data-u2n-cache-user] { + display: inline-block; + font-size: 0; + text-overflow: unset !important; + } + + .user-mention[data-u2n-cache-user] { + background-color: transparent !important; + } + + .u2n-tag { + align-self: center; + display: inline-flex; + align-items: center; + gap: 5px; + margin-left: 3px; + padding: 0 6px; + border-radius: 4px; + font-size: 12px; + letter-spacing: 0.05em; + font-weight: 600; + font-style: normal; + text-decoration: none !important; + line-height: 19px; + height: 18px; + white-space: nowrap; + color: var(--u2n-user-text) !important; + background-color: var(--u2n-user-bg) !important; + transition: 0.15s ease-in-out; + position: relative; + } + + .u2n-tag svg { + display: inline-block; + vertical-align: middle; + fill: currentColor; + height: 10px; + width: 10px; + } + + .u2n-tag img { + position: absolute; + left: 0; + top: 0; + border-radius: 4px; + height: 100%; + aspect-ratio: 1 / 1; + } + + .u2n-tag:hover { + color: var(--u2n-user-text--hover) !important; + background-color: var(--u2n-user-bg--hover) !important; + } + + /* We hide them and show them only in verified locations */ + .u2n-tag-avatar { + display: none; + } + + ${nestedSelectors([ + '.gh-header', // pr header on pr site + '.u2n-nav-user-preview', // preview in user tab + '[data-issue-and-pr-hovercards-enabled] [id*="issue_"]', // prs in repo + '[data-issue-and-pr-hovercards-enabled] [id*="check_"]', // actions in repo + '.timeline-comment-header', // comments headers + '.comment-body', // comments body + ], [ + ['.u2n-tag-avatar', 'display: inline-block;'], + ['.u2n-tag-avatar + *', 'margin-left: 1.5em;'], + ])} +`, { sourceName: 'render-users' }); + +const renderUsers = () => { + const elements = getUserElements(); + const { + color, + shouldShowAvatars, + } = window.U2N.settings; + + const shouldUpdateTheme = document.body.getAttribute('data-u2n-color') !== color; + if (shouldUpdateTheme) { + document.body.setAttribute('data-u2n-color', color); + } + + elements.forEach(({ el, username }) => { + const user = window.U2N.usersByUsernames?.[username]; + const displayName = getDisplayNameByUsername(username); + + const cacheValue = `${displayName}${user ? '+u' : '-u'}${shouldShowAvatars ? '+a' : '-a'}`; + + const isAlreadySet = el.getAttribute('data-u2n-cache-user') === cacheValue; + if (isAlreadySet) { + return; + } + + el.setAttribute('data-u2n-cache-user', cacheValue); + + el.querySelector('.u2n-tags-holder')?.remove(); + + const tagsHolderEl = document.createElement('span'); + + let holderClassNames = 'u2n-tags-holder u2n-tags--user'; + if (!user) { + holderClassNames += ' u2n-tags--no-data'; + } + + tagsHolderEl.setAttribute('class', holderClassNames); + + const tagEl = document.createElement('span'); + tagEl.setAttribute('class', 'u2n-tag'); + + const avatarSrc = user?.avatarSrc || ''; + + tagEl.innerHTML = `${shouldShowAvatars && avatarSrc ? `` : ''}${displayName}`; + + tagsHolderEl.append(tagEl); + + el.append(tagsHolderEl); + }); +}; + + const getUserFromUserPageIfPossible = () => { + const elProfile = document.querySelector('.page-profile .js-profile-editable-replace'); + + if (elProfile) { + try { + const avatarEl = elProfile.querySelector('.avatar-user'); + const avatarSrc = avatarEl?.getAttribute('src')?.split('?')[0] || ''; + const id = avatarSrc ? avatarSrc.match(/u\/([0-9]+)?/)[1] : ''; + const username = elProfile.querySelector('.vcard-username')?.textContent?.trim() || ''; + const name = elProfile.querySelector('.vcard-fullname')?.textContent?.trim() || ''; + + return { + id, + username, + avatarSrc, + name, + }; + } catch (error) { + userScriptLogger({ + isError: true, message: 'getUserFromUserPageIfPossible() failed while parsing the profile', error, + }); + } + } + + return undefined; +}; + +const getUserFromHovercardIfPossible = () => { + const elHovercard = document.querySelector('.user-hovercard-avatar'); + + if (elHovercard) { + try { + const avatarEl = elHovercard.querySelector('.avatar-user'); + const avatarSrc = avatarEl?.getAttribute('src')?.split('?')[0] || ''; + const id = avatarSrc ? avatarSrc.match(/u\/([0-9]+)?/)[1] : ''; + const username = avatarEl?.getAttribute('alt')?.replace('@', '').trim(); + const name = elHovercard.parentNode.parentNode.querySelector(`.Link--secondary[href="/${username}"]`)?.textContent?.trim() || ''; + + return { + id, + username, + avatarSrc, + name, + }; + } catch (error) { + userScriptLogger({ + isError: true, message: 'getUserFromHovercardIfPossible() failed while parsing the card', error, + }); + } + } + + return undefined; +}; + +const getUsersFromPeopleListIfPossible = () => { + if (!location.pathname.includes('/people')) { + return []; + } + + try { + const usersEls = Array.from(document.querySelectorAll('li[data-bulk-actions-id]')); + + const users = usersEls.map((el) => { + const avatarEl = el.querySelector('.avatar-user'); + const avatarSrc = avatarEl?.getAttribute('src')?.split('?')[0] || ''; + const id = avatarSrc ? avatarSrc.match(/u\/([0-9]+)?/)[1] : ''; + const username = avatarEl?.getAttribute('alt')?.replace('@', '').trim(); + const name = Array.from( + el.querySelector('[data-test-selector="linked-name-is-full-if-exists"]').childNodes, + ).find((child) => child.nodeType === Node.TEXT_NODE).textContent.trim(); + + return { + id, + username, + avatarSrc, + name, + }; + }); + + return users; + } catch (error) { + userScriptLogger({ + isError: true, message: 'getUsersFromPeopleListIfPossible() failed while parsing the people list', error, + }); + } + + return []; +}; + +const saveNewUsersIfPossible = () => { + const userFromProfile = getUserFromUserPageIfPossible(); + if (userFromProfile) { + saveNewUser(userFromProfile); + } + + const userFromHoverCard = getUserFromHovercardIfPossible(); + if (userFromHoverCard) { + saveNewUser(userFromHoverCard); + } + + const usersFromPeopleList = getUsersFromPeopleListIfPossible(); + if (usersFromPeopleList.length > 0) { + saveNewUsers(usersFromPeopleList.reduce((stack, user) => { + stack[user.username] = user; + + return stack; + }, {}), { customStatusText: `${usersFromPeopleList.length} users' data were updated` }); + } +}; + + + saveNewUsersIfPossible(); + renderUsers(); + renderStatus(); + renderApp(); + + try { + document.body.addEventListener('click', (event) => { + const handlerData = Object.values(window.U2N.ui.eventsSubscribers).find(({ selector }) => { + /* It checks max 4 nodes, while .closest() would look for all the nodes to body */ + const matchedHandlerData = [ + event.target, + event.target?.parentElement, + event.target?.parentElement?.parentElement, + event.target?.parentElement?.parentElement?.parentElement, + ].filter(Boolean).find((el) => el.matches(selector)); + + return Boolean(matchedHandlerData); + }); + + if (handlerData) { + const { selector, handleClick, shouldPreventDefault = true } = handlerData; + + if (shouldPreventDefault) { + event.preventDefault(); + } + + const calledByElement = event.target.closest(selector); + + handleClick(event, calledByElement); + } + }); +} catch (error) { + userScriptLogger({ + isError: true, isCritical: true, message: 'Click detect failed', error, + }); +} + + + const debouncedRefresh = debounce(() => { + saveNewUsersIfPossible(); + renderUsers(); + + const didLocationChange = location.href !== window.U2N.cache.location; + if (didLocationChange) { + window.U2N.cache.location = location.href; + + renderApp(); + } + }, 500); + + const observer = new MutationObserver(debouncedRefresh); + const config = { + childList: true, + subtree: true, + }; + observer.observe(document.body, config); + } catch (error) { + userScriptLogger({ + isError: true, isCritical: true, message: 'initU2N() failed', error, + }); + + throw error; + } +}; + +domReady(initU2N); From b9280ca0b3b40d39edc49beb8f97b9062914e65c Mon Sep 17 00:00:00 2001 From: Deykun Date: Mon, 29 Jul 2024 17:52:17 +0000 Subject: [PATCH 3/7] deploy: e553cea8b7708b947687be3a85838ff2acc5f0ca --- github-usernames.user-srcipt.js | 1386 ------------------------------- github-usernames.user.js | 4 +- 2 files changed, 2 insertions(+), 1388 deletions(-) delete mode 100644 github-usernames.user-srcipt.js diff --git a/github-usernames.user-srcipt.js b/github-usernames.user-srcipt.js deleted file mode 100644 index 37177cd..0000000 --- a/github-usernames.user-srcipt.js +++ /dev/null @@ -1,1386 +0,0 @@ -// ==UserScript== -// @namespace deykun -// @name Usernames to names - GitHub -// @description Replace ambiguous usernames with actual names from user profiles. -// @author deykun -// @version 1.0.0 -// @include https://github.com* -// @grant none -// @run-at document-start -// @updateURL https://raw.githubusercontent.com/Deykun/github-usernames/main/public/github-usernames.user-srcipt.js -// @downloadURL https://raw.githubusercontent.com/Deykun/github-usernames/main/public/github-usernames.user-srcipt.js -// ==/UserScript== - -'use strict'; - -const getFromLocalStorage = (key, defaultValues = {}) => (localStorage.getItem(key) - ? { ...defaultValues, ...JSON.parse(localStorage.getItem(key)) } - : { ...defaultValues }); - -const defaultSettings = { - color: 'light', - name: 'name-s', - shouldShowUsernameWhenBetter: true, - shouldShowAvatars: true, - shouldFilterBySubstring: false, - filterSubstring: '', -}; - -const getSettingsFromLS = () => getFromLocalStorage('u2n-settings', defaultSettings); -const getUsersByUsernamesFromLS = () => getFromLocalStorage('u2n-users'); -const getCustomNamesByUsernamesFromLS = () => getFromLocalStorage('u2n-users-names'); - -window.U2N = { - version: '1.0.0', - isDevMode: false, - cache: { - HTML: {}, - CSS: {}, - inited: false, - status: null, - location: location.href, - }, - settings: getSettingsFromLS(), - usersByUsernames: getUsersByUsernamesFromLS(), - customNamesByUsernames: getCustomNamesByUsernamesFromLS(), - actions: {}, -}; - -window.U2N.ui = { - status: { - type: '', - text: '', - }, - openedContent: '', - eventsSubscribers: {}, -}; - - -const userScriptLogger = (params) => { - if (params.isError) { - const { isCritical = false, message = '', error } = params; - - if (isCritical) { - // eslint-disable-next-line no-console - console.error('A User2Names error (from Tampermonkey) has occurred. You can ignore it, or describe the error and create an issue here: https://github.com/Deykun/github-usernames/issues'); - // eslint-disable-next-line no-console - console.error(`U2N error: ${message}`); - // eslint-disable-next-line no-console - console.error(error); - } - - if (window.U2N.isDevMode && error) { - // eslint-disable-next-line no-console - console.error(error); - } - } -}; - -const domReady = (fn) => { - document.addEventListener('DOMContentLoaded', fn); - if (document.readyState === 'interactive' || document.readyState === 'complete') { - fn(); - } -}; - -const initU2N = async () => { - if (window.U2N.cache.inited) { - return; - } - - window.U2N.cache.inited = true; - - try { - const updateStatus = ({ type = '', text = '', durationInSeconds = 4 }) => { - if (window.U2N.cache.status) { - clearTimeout(window.U2N.cache.status); - } - - window.U2N.ui.status = { - type, - text, - }; - - renderStatus(); - - window.U2N.cache.status = setTimeout(() => { - window.U2N.ui.status = { - type: '', - text: '', - }; - - renderStatus(); - }, durationInSeconds * 1000); -}; - -const saveNewUsers = (usersByNumber = {}, params = {}) => { - const oldUserByUsernames = getUsersByUsernamesFromLS(); - - const newUserByUsernames = Object.entries(usersByNumber).reduce((stack, [username, value]) => { - const isValidUsername = username && !username.includes(' '); - if (isValidUsername) { - stack[username] = value; - } - - return stack; - }, JSON.parse(JSON.stringify(oldUserByUsernames))); - - const didChange = JSON.stringify(oldUserByUsernames) !== JSON.stringify(newUserByUsernames); - - if (!didChange) { - return false; - } - - window.U2N.usersByUsernames = newUserByUsernames; - localStorage.setItem('u2n-users', JSON.stringify(window.U2N.usersByUsernames)); - - renderUsers(); - updateStatus({ - type: 'users-update', - text: params.customStatusText || "The users' data were updated.", - }); - - if (window.U2N.ui.openedContent === 'settings') { - renderApp(); - } - - return true; -}; - -const saveNewUser = (newUser) => { - if (newUser.username) { - const wasUpdated = JSON.stringify(window.U2N.usersByUsernames?.[newUser.username]) - !== JSON.stringify(newUser); - - if (wasUpdated) { - return saveNewUsers({ - [newUser.username]: newUser, - }, { customStatusText: `${newUser.username}'s data was updated.` }); - } - } - - return false; -}; - -const saveDisplayNameForUsername = (username, name) => { - if (!username) { - return false; - } - - const customNamesByUsernames = getCustomNamesByUsernamesFromLS(); - - if (name) { - customNamesByUsernames[username] = name; - } else { - delete customNamesByUsernames[username]; - } - - window.U2N.customNamesByUsernames = customNamesByUsernames; - - localStorage.setItem('u2n-users-names', JSON.stringify(customNamesByUsernames)); - - renderUsers(); - updateStatus({ - type: 'users-update', - text: `${username}'s display name was updated.`, - }); - - return true; -}; - -const saveSetting = (settingName, value, params) => { - const acceptedSettingsNames = Object.keys(defaultSettings); - if (!acceptedSettingsNames.includes(settingName)) { - return false; - } - - const settings = getSettingsFromLS(); - - settings[settingName] = value; - - window.U2N.settings = settings; - localStorage.setItem('u2n-settings', JSON.stringify(settings)); - - renderApp(); - renderUsers(); - updateStatus({ - type: 'settings-update', - text: params?.customStatusText || 'A setting was updated.', - }); - - return true; -}; - -const resetUsers = () => { - localStorage.removeItem('u2n-users'); - localStorage.removeItem('u2n-users-names'); - window.U2N.usersByUsernames = {}; - window.U2N.customNamesByUsernames = {}; - renderUsers(); - renderApp(); - updateStatus({ - type: 'users-reset', - text: "The users' data were removed.", - }); -}; - - const appendCSS = (styles, { sourceName = '' } = {}) => { - const appendOnceSelector = sourceName ? `g-u2n-css-${sourceName}`.trim() : undefined; - if (appendOnceSelector) { - /* Already appended */ - if (document.getElementById(appendOnceSelector)) { - return; - } - } - - const style = document.createElement('style'); - if (sourceName) { - style.setAttribute('id', appendOnceSelector); - } - - style.innerHTML = styles; - document.head.append(style); -}; - -// eslint-disable-next-line default-param-last -const render = (HTML = '', source) => { - const id = `g-u2n-html-${source}`; - - if (HTML === window.U2N.cache.HTML[id]) { - /* Don't rerender if HTML is the same */ - return; - } - - window.U2N.cache.HTML[id] = HTML; - - const wrapperEl = document.getElementById(id); - - if (!HTML) { - if (wrapperEl) { - wrapperEl.remove(); - } - - return; - } - - if (wrapperEl) { - wrapperEl.innerHTML = HTML; - - return; - } - - const el = document.createElement('div'); - el.id = id; - el.setAttribute('data-testid', id); - el.innerHTML = HTML; - - document.body.appendChild(el); -}; - -const nestedSelectors = (selectors, subcontents) => { - return subcontents.map(([subselector, content]) => { - return `${selectors.map((selector) => `${selector} ${subselector}`).join(', ')} { - ${content} - }`; - }).join(' '); -}; - - const debounce = (fn, time) => { - let timeoutHandler; - - return (...args) => { - clearTimeout(timeoutHandler); - timeoutHandler = setTimeout(() => { - fn(...args); - }, time); - }; -}; - -const upperCaseFirstLetter = (text) => (typeof text === 'string' ? text.charAt(0).toUpperCase() + text.slice(1) : ''); - -const getShouldUseUsernameAsDisplayname = (username) => { - const { - shouldFilterBySubstring, - filterSubstring, - } = window.U2N.settings; - - if (!shouldFilterBySubstring) { - return false; - } - - const lowerCasedUsername = username?.toLowerCase(); - - const hasAtleastOneSubstringIncludedInUsername = filterSubstring.replaceAll(' ', '').split(',').some( - (substring) => lowerCasedUsername.includes(substring.toLowerCase()), - ); - - return !hasAtleastOneSubstringIncludedInUsername; -}; - -const getDisplayNameByUsername = (username) => { - if (getShouldUseUsernameAsDisplayname(username)) { - return username; - } - - const user = window.U2N.usersByUsernames?.[username]; - const customDisplayName = window.U2N.customNamesByUsernames?.[username]; - - if (customDisplayName) { - return customDisplayName; - } - - if (!user?.name) { - return username; - } - - const { - name: nameSetting, - shouldShowUsernameWhenBetter, - } = window.U2N.settings; - - if (nameSetting === 'username') { - return username; - } - - const subnames = user.name.split(' ').filter(Boolean).map((subname) => upperCaseFirstLetter(subname)); - - if (shouldShowUsernameWhenBetter) { - const nameToCompare = subnames.join(' '); - const totalNamesLetters = nameToCompare.match(/[a-zA-Z]/gi).length; - const totalUsernamesLetters = username.match(/[a-zA-Z]/gi).length; - - const isUsernameBetter = totalNamesLetters < totalUsernamesLetters && totalNamesLetters < 7; - - if (isUsernameBetter) { - return username; - } - } - - if (nameSetting === 'name-surname') { - return subnames.join(' '); - } - - const [firstName, ...restOfNames] = subnames; - - if (nameSetting === 'name-s') { - return [firstName, ...restOfNames.map((subname) => `${subname.at(0)}.`)].join(' '); - } - - if (nameSetting === 'name') { - return firstName; - } - - const [lastName, ...firstNamesReversed] = subnames.reverse(); - const firstNames = firstNamesReversed.reverse(); - - // n-surname - return [firstNames.map((subname) => `${subname.at(0)}.`), lastName].join(' '); -}; - - /* - https://iconmonstr.com -*/ - -const IconCog = ` - -`; -const IconGithub = ` - -`; -const IconNewUser = ` - -`; -const IconSave = ` - -`; -const IconThemes = ` - -`; -const IconWrench = ` - -`; -const IconUser = ` - -`; -const IconRemoveUsers = ` - -`; - - appendCSS(` -.u2n-text-input-wrapper { - display: flex; - gap: 5px; - position: relative; -} - -.u2n-text-input-wrapper input { - width: 100%; - padding-left: 10px; -} - -.u2n-text-input-wrapper label { - position: absolute; - top: 0; - left: 5px; - transform: translateY(-50%); - background-color: var(--u2n-nav-item-bg); - padding: 2px 5px; - border-radius: 2px; - font-size: 9px; -} -`, { sourceName: 'interface-text-input' }); - -const getTextInput = ({ - idInput, idButton, label, name, value = '', placeholder, isDisabled = false, -}) => { - return `
- - ${label ? `` : ''} - -
`; -}; - -appendCSS(` -.u2n-checkbox-wrapper { - display: flex; - align-items: center; - gap: 5px; - font-weight: 400; -} - -.u2n-checkbox-wrapper input { - margin-left: 5px; - margin-right: 5px; -} -`, { sourceName: 'interface-value' }); - -const getCheckbox = ({ - idInput, classNameInput, label, name, value, isChecked = false, type = 'checkbox', -}) => { - return ``; -}; - -const getRadiobox = (params) => { - return getCheckbox({ ...params, type: 'radio' }); -}; - - appendCSS(` - .u2n-nav-popup-button.u2n-nav-popup-button--github { - color: var(--u2n-nav-item-bg); - background-color: var(--u2n-nav-item-text-strong); - } - - .u2n-nav-remove-all { - color: var(--fgColor-danger); - background: transparent; - border: none; - borer-bottom: 1px solid var(--fgColor-danger); - padding: 0; - font-size: 12px; - } - - .u2n-nav-popup-footer { - margin-top: -10px; - font-size: 10px; - color: var(--u2n-nav-item-text); - text-align: right; - } -`, { sourceName: 'render-app-settings' }); - -const getAppSettings = ({ isActive = false }) => { - const { settings } = window.U2N; - const totalSavedUsers = Object.values(window.U2N.usersByUsernames).length; - - return `
- ${!isActive - ? `` - : ` -
-
-

${IconCog} Settings

-
- Users saved: ${totalSavedUsers} - ${totalSavedUsers === 0 ? '' : ``} -
-
- ${getCheckbox({ - idInput: 'settings-should-use-substring', - label: 'only use names from profiles when their username contains the specified string (use a comma for multiple)', - isChecked: settings.shouldFilterBySubstring, - })} - ${getTextInput({ - label: 'Edit substring', - placeholder: 'ex. company_', - idButton: 'settings-save-substring', - idInput: 'settings-value-substring', - value: settings.filterSubstring, - })} -
-
- You can learn more or report an issue here: -
- - ${IconGithub} deykun / github-usernames - - Version ${window.U2N.version} -
-
`} -
`; -}; - -window.U2N.ui.eventsSubscribers.removeAllUsers = { - selector: '#u2n-remove-all-users', - handleClick: resetUsers, -}; - -window.U2N.ui.eventsSubscribers.shouldFilterBySubstring = { - selector: '#settings-should-use-substring', - handleClick: (_, calledByElement) => { - saveSetting('shouldFilterBySubstring', calledByElement.checked); - }, -}; - -window.U2N.ui.eventsSubscribers.filterSubstring = { - selector: '#settings-save-substring', - handleClick: () => { - const value = document.getElementById('settings-value-substring')?.value || ''; - - saveSetting('filterSubstring', value); - }, -}; - - /* import @/render-app-status.js */ - const themeSettings = { - colors: [{ - label: 'Light', - value: 'light', - }, - { - label: 'Dark', - value: 'dark', - }, - { - label: 'Sky', - value: 'sky', - }, - { - label: 'Grass', - value: 'grass', - }], - names: [ - { - label: 'Dwight Schrute', - value: 'name-surname', - }, - { - label: 'Dwight S.', - value: 'name-s', - }, - { - label: 'Dwight', - value: 'name', - }, - { - label: 'D. Schrute', - value: 'n-surname', - }, - { - label: 'DSchrute911 (github\'s default)', - value: 'username', - }], -}; - -appendCSS(` - .u2u-names-list li:last-child { - grid-column: 1 / 3; - } -`, { sourceName: 'render-app-theme' }); - -const getAppTheme = ({ isActive = false }) => { - const { settings } = window.U2N; - - return `
- ${!isActive - ? `` - : ` -
-
-

${IconThemes} Theme

-
-

Color

-
    - ${themeSettings.colors.map(({ label, value }) => `
  • - ${getRadiobox({ - name: 'color', - classNameInput: 'u2n-theme-color', - label, - value, - isChecked: settings.color === value, - })}
  • `).join('')} -
-
-
-

Display name

-
    - ${themeSettings.names.map(({ label, value }, index) => `
  • - ${getRadiobox({ - name: 'names', - classNameInput: 'u2n-theme-name', - label, - value, - isChecked: settings.name === value, - })}
  • `).join('')} -
-
-
-

Other

- ${getCheckbox({ - idInput: 'settings-should-show-username-when-better', - label: 'should show the username when it fits better', - isChecked: settings.shouldShowUsernameWhenBetter, - })} - ${getCheckbox({ - idInput: 'settings-should-show-avatar', - label: 'should show avatars', - isChecked: settings.shouldShowAvatars, - })} -
-
-
`} -
`; -}; - -window.U2N.ui.eventsSubscribers.color = { - selector: '.u2n-theme-color', - handleClick: (_, calledByElement) => { - saveSetting('color', calledByElement.value); - }, -}; - -window.U2N.ui.eventsSubscribers.name = { - selector: '.u2n-theme-name', - handleClick: (_, calledByElement) => { - saveSetting('name', calledByElement.value); - }, -}; - -window.U2N.ui.eventsSubscribers.shouldShowAvatars = { - selector: '#settings-should-show-avatar', - handleClick: (_, calledByElement) => { - saveSetting('shouldShowAvatars', calledByElement.checked); - }, -}; - -window.U2N.ui.eventsSubscribers.shouldShowUsernameWhenBetter = { - selector: '#settings-should-show-username-when-better', - handleClick: (_, calledByElement) => { - saveSetting('shouldShowUsernameWhenBetter', calledByElement.checked); - }, -}; - - appendCSS(` - .u2n-nav-user-preview { - display: flex; - align-items: center; - gap: 10px; - height: 20px; - font-size: 10px; - } -`, { sourceName: 'render-app-user' }); - -const getAppUser = ({ isActive = false }) => { - const isProfilPage = Boolean(document.querySelector('.page-profile')); - const username = location.pathname.replace('/', ''); - - const shouldRender = Boolean(isProfilPage && username); - if (!shouldRender) { - return ''; - } - - const user = window.U2N.usersByUsernames?.[username] || {}; - const displayName = getDisplayNameByUsername(username); - - return `
- ${!isActive - ? `` - : ` -
-
-

${IconUser} User

-
- ${user.username} -
-
    -
  • - ID: ${user.id} -
  • -
  • - Username: ${user.username} -
  • -
  • - Name: ${user.name} -
  • -
-
- ${getTextInput({ - label: 'Edit display name', - placeholder: displayName, - value: displayName, - name: username, - idButton: 'user-save-name', - idInput: 'user-value-name', - isDisabled: getShouldUseUsernameAsDisplayname(username), - })} - ${getShouldUseUsernameAsDisplayname(username) - ? 'This user is excluded by a string in the Settings tab.' - : ''} -
-
`} -
`; -}; - -window.U2N.ui.eventsSubscribers.displayNameUpdate = { - selector: '#user-save-name', - handleClick: () => { - const inputElement = document.getElementById('user-value-name'); - const username = inputElement.getAttribute('name'); - const displayName = inputElement.value; - - saveDisplayNameForUsername(username, displayName); - }, -}; - - appendCSS(` - :root { - --u2n-nav-item-size: 35px; - --u2n-nav-item-bg: var(--bgColor-muted); - --u2n-nav-item-bg: var(--bgColor-default); - --u2n-nav-item-text-strong: var(--fgColor-default); - --u2n-nav-item-text: var(--fgColor-muted); - --u2n-nav-item-text-hover: var(--fgColor-accent); - --u2n-nav-item-border: var(--borderColor-muted); - --u2n-nav-item-radius: 5px; - } - - .u2n-nav { - display: flex; - position: fixed; - bottom: 0; - right: 30px; - height: var(--u2n-nav-item-size); - filter: drop-shadow(0 0 10px rgba(0, 0, 0, 0.08)); - } - - .u2n-nav > * + * { - margin-left: -1px; - } - - .u2n-nav > :first-child { - border-top-left-radius: var(--u2n-nav-item-radius); - } - - .u2n-nav > :last-child { - border-top-right-radius: var(--u2n-nav-item-radius); - } - - .u2n-nav-status, - .u2n-nav-button-wrapper { - height: var(--u2n-nav-item-size); - min-width: var(--u2n-nav-item-size); - line-height: var(--u2n-nav-item-size); - border: 1px solid var(--u2n-nav-item-border); - border-bottom-width: 0px; - background: var(--u2n-nav-item-bg); - } - - .u2n-nav-button-wrapper { - position: relative; - } - - .u2n-nav-button { - background: transparent; - border: none; - padding: 0; - color: var(--u2n-nav-item-text); - width: var(--u2n-nav-item-size); - transition: 0.3s ease-in-out; - } - - .u2n-nav-button:hover { - color: var(--u2n-nav-item-text-hover); - } - - .u2n-nav-button--active { - color: var(--u2n-nav-item-text-strong); - } - - .u2n-nav-button svg { - fill: currentColor; - padding: 25%; - height: var(--u2n-nav-item-size); - width: var(--u2n-nav-item-size); - line-height: var(--u2n-nav-item-size); - } - - .u2n-nav-popup { - position: absolute; - right: 0; - bottom: calc(100% + 10px); - width: 300px; - color: var(--u2n-nav-item-text-strong); - border: 1px solid var(--u2n-nav-item-border); - border-radius: var(--u2n-nav-item-radius); - border-bottom-right-radius: 0; - background-color: var(--u2n-nav-item-bg); - } - - .u2n-nav-popup-content { - display: flex; - flex-flow: column; - gap: 18px; - max-height: calc(100vh - 60px); - overflow: auto; - padding: 10px; - padding-top: 0; - font-size: 12px; - line-height: 1.3; - text-align: left; - } - - .u2n-nav-popup-title { - position: sticky; - top: 0px; - display: flex; - align-items: center; - gap: 8px; - padding-top: 10px; - padding-bottom: 5px; - font-size: 16px; - background-color: var(--u2n-nav-item-bg); - } - - .u2n-nav-popup-title svg { - fill: currentColor; - height: 16px; - width: 16px; - } - - .u2n-nav-popup h3 { - font-size: 13px; - margin-bottom: 8px; - } - - .u2n-nav-popup ul { - display: flex; - flex-flow: column; - gap: 8px; - list-style: none; - } - - .u2n-nav-popup .grid-2 { - display: grid; - grid-template-columns: 1fr 1fr; - } - - .u2n-nav-popup::after { - content: ''; - position: absolute; - bottom: -10px; - right: calc((var(--u2n-nav-item-size) / 2) - 5px); - width: 0; - height: 0; - border: 5px solid transparent; - border-top-color: var(--u2n-nav-item-border); - } - - .u2n-nav-popup-button { - display: flex; - gap: 10px; - justify-content: center; - align-items: center; - padding: 8px; - border-radius: 3px; - font-size: 14px; - letter-spacing: 0.04em; - text-decoration: none; - background: none; - border: none; - color: var(--bgColor-default); - background-color: var(--fgColor-success); - } - - .u2n-nav-popup-button:hover { - text-decoration: none; - } - - .u2n-nav-popup-button svg { - fill: currentColor; - width: 18px; - height: 18px; - } -`, { sourceName: 'render-app' }); - -const renderApp = () => { - const content = window.U2N.ui.openedContent; - - render(``, 'u2n-app'); -}; - -window.U2N.ui.eventsSubscribers.content = { - selector: '.u2n-nav-button', - handleClick: (_, calledByElement) => { - if (calledByElement) { - const content = calledByElement.getAttribute('data-content'); - const isClose = !content || content === window.U2N.ui.openedContent; - - if (isClose) { - window.U2N.ui.openedContent = ''; - } else { - window.U2N.ui.openedContent = content; - } - } - - renderApp(); - }, -}; - - appendCSS(` - .u2n-nav-status { - display: flex; - position: fixed; - bottom: 0; - left: 50%; - height: var(--u2n-nav-item-size); - filter: drop-shadow(0 0 10px rgba(0, 0, 0, 0.08)); - display: inline-flex; - align-items: center; - justify-content: center; - gap: 10px; - padding: 0 10px; - margin-right: 10px; - border-top-left-radius: var(--u2n-nav-item-radius); - border-top-right-radius: var(--u2n-nav-item-radius); - color: var(--fgColor-default); - font-size: 12px; - transform: translateY(60px) translateX(-50%); - animation: U2NSlideInFromTop 0.4s cubic-bezier(0.1, 0.7, 1, 0.1) forwards; - } - - @keyframes U2NSlideInFromTop { - 0% { - transform: translateY(60px) translateX(-50%); - } - 100% { - transform: translateY(0) translateX(-50%); - } - } - - .u2n-nav-status svg { - fill: currentColor; - color: var(--fgColor-success); - height: 14px; - width: 14px; - } - - .u2n-nav-status--danger svg { - color: var(--fgColor-danger); - } -`, { sourceName: 'render-app-status' }); - -const StatusIconByType = { - 'users-update': IconNewUser, - 'users-reset': IconRemoveUsers, - 'settings-update': IconWrench, -}; - -const renderStatus = () => { - const { - type, - text: statusText = '', - } = window.U2N.ui.status; - - if (!statusText) { - render('', 'u2n-status'); - - return; - } - - const Icon = StatusIconByType[type] || ''; - const isNegative = ['users-reset'].includes(type); - - render(` - ${Icon} ${statusText} -`, 'u2n-status'); -}; - - const getUserElements = () => { - const links = Array.from(document.querySelectorAll('[data-hovercard-url^="/users/"]')).map((el) => { - const username = el.getAttribute('data-hovercard-url').match(/users\/([A-Za-z0-9_-]+)\//)[1]; - - if (username && el.textContent.includes(username)) { - return { - el, - username, - }; - } - - return undefined; - }).filter(Boolean); - - return links; -}; - -appendCSS(` - :root { - --u2n-user-text: #00293e; - --u2n-user-bg: #f2f2f2; - --u2n-user-text--hover: #0054ae; - --u2n-user-bg--hover: #dbedff; - } - - body[data-u2n-color="dark"] { - --u2n-user-text: white; - --u2n-user-bg: #26292e; - --u2n-user-text--hover: #dbedff; - --u2n-user-bg--hover: #142a42; - } - - body[data-u2n-color="sky"] { - --u2n-user-text: #03113c; - --u2n-user-bg: #def3fa; - --u2n-user-text--hover: #000; - --u2n-user-bg--hover: #beedfc; - } - - body[data-u2n-color="grass"] { - --u2n-user-text: #fff; - --u2n-user-bg: #163b13; - --u2n-user-text--hover: #b8ffb3; - --u2n-user-bg--hover: #30582d; - } - - [data-u2n-cache-user] { - display: inline-block; - font-size: 0; - text-overflow: unset !important; - } - - .user-mention[data-u2n-cache-user] { - background-color: transparent !important; - } - - .u2n-tag { - align-self: center; - display: inline-flex; - align-items: center; - gap: 5px; - margin-left: 3px; - padding: 0 6px; - border-radius: 4px; - font-size: 12px; - letter-spacing: 0.05em; - font-weight: 600; - font-style: normal; - text-decoration: none !important; - line-height: 19px; - height: 18px; - white-space: nowrap; - color: var(--u2n-user-text) !important; - background-color: var(--u2n-user-bg) !important; - transition: 0.15s ease-in-out; - position: relative; - } - - .u2n-tag svg { - display: inline-block; - vertical-align: middle; - fill: currentColor; - height: 10px; - width: 10px; - } - - .u2n-tag img { - position: absolute; - left: 0; - top: 0; - border-radius: 4px; - height: 100%; - aspect-ratio: 1 / 1; - } - - .u2n-tag:hover { - color: var(--u2n-user-text--hover) !important; - background-color: var(--u2n-user-bg--hover) !important; - } - - /* We hide them and show them only in verified locations */ - .u2n-tag-avatar { - display: none; - } - - ${nestedSelectors([ - '.gh-header', // pr header on pr site - '.u2n-nav-user-preview', // preview in user tab - '[data-issue-and-pr-hovercards-enabled] [id*="issue_"]', // prs in repo - '[data-issue-and-pr-hovercards-enabled] [id*="check_"]', // actions in repo - '.timeline-comment-header', // comments headers - '.comment-body', // comments body - ], [ - ['.u2n-tag-avatar', 'display: inline-block;'], - ['.u2n-tag-avatar + *', 'margin-left: 1.5em;'], - ])} -`, { sourceName: 'render-users' }); - -const renderUsers = () => { - const elements = getUserElements(); - const { - color, - shouldShowAvatars, - } = window.U2N.settings; - - const shouldUpdateTheme = document.body.getAttribute('data-u2n-color') !== color; - if (shouldUpdateTheme) { - document.body.setAttribute('data-u2n-color', color); - } - - elements.forEach(({ el, username }) => { - const user = window.U2N.usersByUsernames?.[username]; - const displayName = getDisplayNameByUsername(username); - - const cacheValue = `${displayName}${user ? '+u' : '-u'}${shouldShowAvatars ? '+a' : '-a'}`; - - const isAlreadySet = el.getAttribute('data-u2n-cache-user') === cacheValue; - if (isAlreadySet) { - return; - } - - el.setAttribute('data-u2n-cache-user', cacheValue); - - el.querySelector('.u2n-tags-holder')?.remove(); - - const tagsHolderEl = document.createElement('span'); - - let holderClassNames = 'u2n-tags-holder u2n-tags--user'; - if (!user) { - holderClassNames += ' u2n-tags--no-data'; - } - - tagsHolderEl.setAttribute('class', holderClassNames); - - const tagEl = document.createElement('span'); - tagEl.setAttribute('class', 'u2n-tag'); - - const avatarSrc = user?.avatarSrc || ''; - - tagEl.innerHTML = `${shouldShowAvatars && avatarSrc ? `` : ''}${displayName}`; - - tagsHolderEl.append(tagEl); - - el.append(tagsHolderEl); - }); -}; - - const getUserFromUserPageIfPossible = () => { - const elProfile = document.querySelector('.page-profile .js-profile-editable-replace'); - - if (elProfile) { - try { - const avatarEl = elProfile.querySelector('.avatar-user'); - const avatarSrc = avatarEl?.getAttribute('src')?.split('?')[0] || ''; - const id = avatarSrc ? avatarSrc.match(/u\/([0-9]+)?/)[1] : ''; - const username = elProfile.querySelector('.vcard-username')?.textContent?.trim() || ''; - const name = elProfile.querySelector('.vcard-fullname')?.textContent?.trim() || ''; - - return { - id, - username, - avatarSrc, - name, - }; - } catch (error) { - userScriptLogger({ - isError: true, message: 'getUserFromUserPageIfPossible() failed while parsing the profile', error, - }); - } - } - - return undefined; -}; - -const getUserFromHovercardIfPossible = () => { - const elHovercard = document.querySelector('.user-hovercard-avatar'); - - if (elHovercard) { - try { - const avatarEl = elHovercard.querySelector('.avatar-user'); - const avatarSrc = avatarEl?.getAttribute('src')?.split('?')[0] || ''; - const id = avatarSrc ? avatarSrc.match(/u\/([0-9]+)?/)[1] : ''; - const username = avatarEl?.getAttribute('alt')?.replace('@', '').trim(); - const name = elHovercard.parentNode.parentNode.querySelector(`.Link--secondary[href="/${username}"]`)?.textContent?.trim() || ''; - - return { - id, - username, - avatarSrc, - name, - }; - } catch (error) { - userScriptLogger({ - isError: true, message: 'getUserFromHovercardIfPossible() failed while parsing the card', error, - }); - } - } - - return undefined; -}; - -const getUsersFromPeopleListIfPossible = () => { - if (!location.pathname.includes('/people')) { - return []; - } - - try { - const usersEls = Array.from(document.querySelectorAll('li[data-bulk-actions-id]')); - - const users = usersEls.map((el) => { - const avatarEl = el.querySelector('.avatar-user'); - const avatarSrc = avatarEl?.getAttribute('src')?.split('?')[0] || ''; - const id = avatarSrc ? avatarSrc.match(/u\/([0-9]+)?/)[1] : ''; - const username = avatarEl?.getAttribute('alt')?.replace('@', '').trim(); - const name = Array.from( - el.querySelector('[data-test-selector="linked-name-is-full-if-exists"]').childNodes, - ).find((child) => child.nodeType === Node.TEXT_NODE).textContent.trim(); - - return { - id, - username, - avatarSrc, - name, - }; - }); - - return users; - } catch (error) { - userScriptLogger({ - isError: true, message: 'getUsersFromPeopleListIfPossible() failed while parsing the people list', error, - }); - } - - return []; -}; - -const saveNewUsersIfPossible = () => { - const userFromProfile = getUserFromUserPageIfPossible(); - if (userFromProfile) { - saveNewUser(userFromProfile); - } - - const userFromHoverCard = getUserFromHovercardIfPossible(); - if (userFromHoverCard) { - saveNewUser(userFromHoverCard); - } - - const usersFromPeopleList = getUsersFromPeopleListIfPossible(); - if (usersFromPeopleList.length > 0) { - saveNewUsers(usersFromPeopleList.reduce((stack, user) => { - stack[user.username] = user; - - return stack; - }, {}), { customStatusText: `${usersFromPeopleList.length} users' data were updated` }); - } -}; - - - saveNewUsersIfPossible(); - renderUsers(); - renderStatus(); - renderApp(); - - try { - document.body.addEventListener('click', (event) => { - const handlerData = Object.values(window.U2N.ui.eventsSubscribers).find(({ selector }) => { - /* It checks max 4 nodes, while .closest() would look for all the nodes to body */ - const matchedHandlerData = [ - event.target, - event.target?.parentElement, - event.target?.parentElement?.parentElement, - event.target?.parentElement?.parentElement?.parentElement, - ].filter(Boolean).find((el) => el.matches(selector)); - - return Boolean(matchedHandlerData); - }); - - if (handlerData) { - const { selector, handleClick, shouldPreventDefault = true } = handlerData; - - if (shouldPreventDefault) { - event.preventDefault(); - } - - const calledByElement = event.target.closest(selector); - - handleClick(event, calledByElement); - } - }); -} catch (error) { - userScriptLogger({ - isError: true, isCritical: true, message: 'Click detect failed', error, - }); -} - - - const debouncedRefresh = debounce(() => { - saveNewUsersIfPossible(); - renderUsers(); - - const didLocationChange = location.href !== window.U2N.cache.location; - if (didLocationChange) { - window.U2N.cache.location = location.href; - - renderApp(); - } - }, 500); - - const observer = new MutationObserver(debouncedRefresh); - const config = { - childList: true, - subtree: true, - }; - observer.observe(document.body, config); - } catch (error) { - userScriptLogger({ - isError: true, isCritical: true, message: 'initU2N() failed', error, - }); - - throw error; - } -}; - -domReady(initU2N); diff --git a/github-usernames.user.js b/github-usernames.user.js index ad4238a..6cc8e66 100644 --- a/github-usernames.user.js +++ b/github-usernames.user.js @@ -7,8 +7,8 @@ // @include https://github.com* // @grant none // @run-at document-start -// @updateURL https://deykun.github.io/github-usernames/github-usernames.user-srcipt.js -// @downloadURL https://deykun.github.io/github-usernames/github-usernames.user-srcipt.js +// @updateURL https://deykun.github.io/github-usernames/github-usernames.user.js +// @downloadURL https://deykun.github.io/github-usernames/github-usernames.user.js // ==/UserScript== 'use strict'; From 28094efe52a4b8b0c3d39d2f82eb9489edc7f7c9 Mon Sep 17 00:00:00 2001 From: Deykun Date: Sat, 19 Apr 2025 19:53:27 +0000 Subject: [PATCH 4/7] deploy: 2575a9acca8cc10a6d22d1e52dbf8bf870dc2437 --- github-usernames.user.js | 143 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 133 insertions(+), 10 deletions(-) diff --git a/github-usernames.user.js b/github-usernames.user.js index 6cc8e66..78931d8 100644 --- a/github-usernames.user.js +++ b/github-usernames.user.js @@ -3,7 +3,7 @@ // @description Replace ambiguous usernames with actual names from user profiles. // @namespace deykun // @author deykun -// @version 1.0.0 +// @version 1.1.0 // @include https://github.com* // @grant none // @run-at document-start @@ -31,7 +31,7 @@ const getUsersByUsernamesFromLS = () => getFromLocalStorage('u2n-users'); const getCustomNamesByUsernamesFromLS = () => getFromLocalStorage('u2n-users-names'); window.U2N = { - version: '1.0.0', + version: '1.1.0', isDevMode: false, cache: { HTML: {}, @@ -224,6 +224,10 @@ const resetUsers = () => { }); }; +const getIsSavedUser = (username) => { + return Boolean(username && window.U2N.usersByUsernames?.[username]); +}; + const appendCSS = (styles, { sourceName = '' } = {}) => { const appendOnceSelector = sourceName ? `g-u2n-css-${sourceName}`.trim() : undefined; if (appendOnceSelector) { @@ -298,6 +302,22 @@ const nestedSelectors = (selectors, subcontents) => { const upperCaseFirstLetter = (text) => (typeof text === 'string' ? text.charAt(0).toUpperCase() + text.slice(1) : ''); +const joinWithAnd = (items) => { + if (items.length === 0) { + return ''; + } + if (items.length === 1) { + return items[0]; + } + if (items.length === 2) { + return `${items[0]} and ${items[1]}`; + } + + const allButLast = items.slice(0, -1).join(', '); + const last = items[items.length - 1]; + return `${allButLast} and ${last}`; +}; + const getShouldUseUsernameAsDisplayname = (username) => { const { shouldFilterBySubstring, @@ -1040,8 +1060,10 @@ const renderStatus = () => { `, 'u2n-status'); }; - const getUserElements = () => { - const links = Array.from(document.querySelectorAll('[data-hovercard-url^="/users/"]')).map((el) => { + const dataU2NSource = 'data-u2n-source'; + +const getUserElements = () => { + const hovercardUrls = Array.from(document.querySelectorAll('[data-hovercard-url^="/users/"]')).map((el) => { const username = el.getAttribute('data-hovercard-url').match(/users\/([A-Za-z0-9_-]+)\//)[1]; if (username && el.textContent.includes(username)) { @@ -1054,7 +1076,64 @@ const renderStatus = () => { return undefined; }).filter(Boolean); - return links; + const kanbanListItems = Array.from(document.querySelectorAll('[class*="slicer-items-module__title"]')).map((el) => { + const username = el.getAttribute(dataU2NSource) || el.textContent.trim(); + + const isSavedUser = getIsSavedUser(username); + if (isSavedUser) { + return { + el, + username, + }; + } + + return undefined; + }).filter(Boolean); + + const tooltipsItems = Array.from(document.querySelectorAll('[data-visible-text]')).map((el) => { + const username = el.getAttribute(dataU2NSource) || el.getAttribute('data-visible-text').trim(); + + const isSavedUser = getIsSavedUser(username); + if (isSavedUser) { + return { + el, + username, + updateAttributeInstead: 'data-visible-text', + }; + } + + return undefined; + }).filter(Boolean); + + return [ + ...hovercardUrls, + ...kanbanListItems, + ...tooltipsItems, + ]; +}; + +const getGroupedUserElements = () => { + /* Example page https://github.com/orgs/input-output-hk/projects/102/ */ + const projectsCellItems = Array.from(document.querySelectorAll('[role="gridcell"]:has([data-component="Avatar"] + span, [data-avatar-count] + span)')).map((el) => { + const source = el.getAttribute(dataU2NSource) || el.textContent.trim() || ''; + const usernames = source.replace(' and ', ', ').split(', ').filter(Boolean); + + const hasSavedUsername = (usernames?.length || 0) > 0 && usernames.some(getIsSavedUser); + + if (hasSavedUsername) { + return { + el, + usernames, + source, + }; + } + + return undefined; + }).filter(Boolean); + + return [ + ...projectsCellItems, + ]; }; appendCSS(` @@ -1087,10 +1166,16 @@ appendCSS(` } [data-u2n-cache-user] { - display: inline-block; + display: inline-flex; + justify-content: start; + vertical-align: middle; font-size: 0; text-overflow: unset !important; } + + [data-u2n-cache-user] [class*="ActionList-ActionListSubContent"] { + display: none; + } .user-mention[data-u2n-cache-user] { background-color: transparent !important; @@ -1148,6 +1233,7 @@ appendCSS(` ${nestedSelectors([ '.gh-header', // pr header on pr site '.u2n-nav-user-preview', // preview in user tab + '[data-testid="list-row-repo-name-and-number"]', // prs in repo '[data-issue-and-pr-hovercards-enabled] [id*="issue_"]', // prs in repo '[data-issue-and-pr-hovercards-enabled] [id*="check_"]', // actions in repo '.timeline-comment-header', // comments headers @@ -1159,7 +1245,6 @@ appendCSS(` `, { sourceName: 'render-users' }); const renderUsers = () => { - const elements = getUserElements(); const { color, shouldShowAvatars, @@ -1170,19 +1255,30 @@ const renderUsers = () => { document.body.setAttribute('data-u2n-color', color); } - elements.forEach(({ el, username }) => { + const userElements = getUserElements(); + + userElements.forEach(({ el, username: usernameFromElement, updateAttributeInstead }) => { + const username = usernameFromElement; const user = window.U2N.usersByUsernames?.[username]; const displayName = getDisplayNameByUsername(username); + const previousCacheValue = el.getAttribute('data-u2n-cache-user') || ''; - const cacheValue = `${displayName}${user ? '+u' : '-u'}${shouldShowAvatars ? '+a' : '-a'}`; + const cacheValue = `${username}|${displayName}${user ? '+u' : '-u'}${shouldShowAvatars ? '+a' : '-a'}`; - const isAlreadySet = el.getAttribute('data-u2n-cache-user') === cacheValue; + const isAlreadySet = previousCacheValue === cacheValue; if (isAlreadySet) { return; } + el.setAttribute(dataU2NSource, username); el.setAttribute('data-u2n-cache-user', cacheValue); + if (updateAttributeInstead) { + el.setAttribute(updateAttributeInstead, displayName); + + return; + } + el.querySelector('.u2n-tags-holder')?.remove(); const tagsHolderEl = document.createElement('span'); @@ -1205,6 +1301,33 @@ const renderUsers = () => { el.append(tagsHolderEl); }); + + const groupedUsersElements = getGroupedUserElements(); + + groupedUsersElements.forEach(({ el, usernames: usernamesFromElement, source }) => { + const hasSavedUsername = usernamesFromElement.some(getIsSavedUser); + + if (!hasSavedUsername) { + return; + } + + const displayNames = usernamesFromElement.map((username) => getDisplayNameByUsername(username)); + const displayNamesString = joinWithAnd(displayNames); + + const previousCacheValue = el.getAttribute('data-u2n-cache-user') || ''; + + const cacheValue = `${source}|${displayNamesString}${shouldShowAvatars ? '+a' : '-a'}`; + + const isAlreadySet = previousCacheValue === cacheValue; + if (isAlreadySet) { + return; + } + + el.setAttribute(dataU2NSource, source); + el.setAttribute('data-u2n-cache-user', cacheValue); + + Array.from(el.querySelectorAll('span')).at(-1).textContent = displayNamesString; + }); }; const getUserFromUserPageIfPossible = () => { From 2f72a5a9bd075a0041a642b0c733e31b55fff59e Mon Sep 17 00:00:00 2001 From: Deykun Date: Sat, 19 Apr 2025 20:00:39 +0000 Subject: [PATCH 5/7] deploy: 30d1855975512e1456ed3edc78fb081e3d92b3fd --- github-usernames.user.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/github-usernames.user.js b/github-usernames.user.js index 78931d8..25eddf6 100644 --- a/github-usernames.user.js +++ b/github-usernames.user.js @@ -3,7 +3,7 @@ // @description Replace ambiguous usernames with actual names from user profiles. // @namespace deykun // @author deykun -// @version 1.1.0 +// @version 1.1.1 // @include https://github.com* // @grant none // @run-at document-start @@ -31,7 +31,7 @@ const getUsersByUsernamesFromLS = () => getFromLocalStorage('u2n-users'); const getCustomNamesByUsernamesFromLS = () => getFromLocalStorage('u2n-users-names'); window.U2N = { - version: '1.1.0', + version: '1.1.1', isDevMode: false, cache: { HTML: {}, @@ -1165,7 +1165,7 @@ appendCSS(` --u2n-user-bg--hover: #30582d; } - [data-u2n-cache-user] { + [data-u2n-cache-user][data-u2n-cache-user][data-u2n-cache-user] { display: inline-flex; justify-content: start; vertical-align: middle; From 21fe84a035c8d63e8f12b3e41bdb4f71310d38e8 Mon Sep 17 00:00:00 2001 From: Deykun Date: Sat, 19 Apr 2025 20:17:30 +0000 Subject: [PATCH 6/7] deploy: e75d11dcfd0c1f182f2cff693afa63b7f69432ef --- github-usernames.user.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/github-usernames.user.js b/github-usernames.user.js index 25eddf6..cf5a4d2 100644 --- a/github-usernames.user.js +++ b/github-usernames.user.js @@ -3,7 +3,7 @@ // @description Replace ambiguous usernames with actual names from user profiles. // @namespace deykun // @author deykun -// @version 1.1.1 +// @version 1.1.2 // @include https://github.com* // @grant none // @run-at document-start @@ -31,7 +31,7 @@ const getUsersByUsernamesFromLS = () => getFromLocalStorage('u2n-users'); const getCustomNamesByUsernamesFromLS = () => getFromLocalStorage('u2n-users-names'); window.U2N = { - version: '1.1.1', + version: '1.1.2', isDevMode: false, cache: { HTML: {}, @@ -1165,6 +1165,10 @@ appendCSS(` --u2n-user-bg--hover: #30582d; } + *:has(> [data-u2n-cache-user]) { + vertical-align: middle; + } + [data-u2n-cache-user][data-u2n-cache-user][data-u2n-cache-user] { display: inline-flex; justify-content: start; From 96138e9d555335531ec5723f187f09f2193bd7f6 Mon Sep 17 00:00:00 2001 From: Deykun Date: Tue, 2 Dec 2025 21:03:46 +0000 Subject: [PATCH 7/7] deploy: fd764b817ef58265591a2199cb0f1ff5b4e9e7eb --- github-usernames.user.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/github-usernames.user.js b/github-usernames.user.js index cf5a4d2..be36ee4 100644 --- a/github-usernames.user.js +++ b/github-usernames.user.js @@ -3,7 +3,7 @@ // @description Replace ambiguous usernames with actual names from user profiles. // @namespace deykun // @author deykun -// @version 1.1.2 +// @version 1.1.3 // @include https://github.com* // @grant none // @run-at document-start @@ -31,12 +31,12 @@ const getUsersByUsernamesFromLS = () => getFromLocalStorage('u2n-users'); const getCustomNamesByUsernamesFromLS = () => getFromLocalStorage('u2n-users-names'); window.U2N = { - version: '1.1.2', + version: '1.1.3', isDevMode: false, cache: { HTML: {}, CSS: {}, - inited: false, + wasInited: false, status: null, location: location.href, }, @@ -84,11 +84,11 @@ const domReady = (fn) => { }; const initU2N = async () => { - if (window.U2N.cache.inited) { + if (window.U2N.cache.wasInited) { return; } - window.U2N.cache.inited = true; + window.U2N.cache.wasInited = true; try { const updateStatus = ({ type = '', text = '', durationInSeconds = 4 }) => {