diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f12dafb..8d830e6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -27,7 +27,7 @@ jobs: args: zip -qq -r release.zip dist - name: Upload zip with production-ready files - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: production-files path: ./release.zip @@ -40,7 +40,7 @@ jobs: steps: - name: Download production-ready files - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: production-files diff --git a/README.md b/README.md index c6d9743..47942c7 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,11 @@ The script available here swaps usernames with preferred names from profiles ex. ## Instalation 1. Install https://www.tampermonkey.net/ - It is a very popular browser extension that allows you to add custom scripts to selected domains. - - In our case, you will add a script to github.com. - - You can check the code if you are worried about security: it doesn't touch tokens at all. - - Manifest3 requires enabling development mode (as described on the page). Alternatively, you can use the Manifest2 version, which will work faster (M3 only trusts extensions with predefined scripts, but TM by design, allows any type of scripts to be added) + +- In our case, you will add a script to github.com. +- You can check the code if you are worried about security: it doesn't touch tokens at all. +- Manifest3 requires enabling development mode (as described on the page). Alternatively, you can use the Manifest2 version, which will work faster (M3 only trusts extensions with predefined scripts, but TM by design, allows any type of scripts to be added) + 2. Go to https://deykun.github.io/github-usernames/github-usernames.user.js ![Instalation demo](docs/demo-install.gif) diff --git a/package.json b/package.json index 364df5b..832725a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "github-usernames", "private": true, - "version": "1.0.0", + "version": "1.1.0", "type": "module", "author": "Szymon Tondowski", "license": "MIT", diff --git a/processes/dev-script.ts b/processes/dev-script.ts index e31c8c2..8ec0052 100644 --- a/processes/dev-script.ts +++ b/processes/dev-script.ts @@ -10,6 +10,8 @@ console.log(''); console.log(chalk.green('UserScript endpoints are live!')); console.log(' - http://localhost:1234/server.user-script.js'); console.log(''); +console.log('(An updated version of the script will be downloaded when you check for updates in Tampermonkey)'); +console.log(''); userScriptApp.get('/server.user-script.js', (req, res) => { const devScript = fs.readFileSync('src/user-script/dev.user-srcipt.js', 'utf-8'); diff --git a/public/github-usernames.user.js b/public/github-usernames.user.js index 6cc8e66..78931d8 100644 --- a/public/github-usernames.user.js +++ b/public/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 = () => { diff --git a/src/user-script/parts/db.js b/src/user-script/parts/db.js index 499047a..6b69915 100644 --- a/src/user-script/parts/db.js +++ b/src/user-script/parts/db.js @@ -130,3 +130,7 @@ const resetUsers = () => { text: "The users' data were removed.", }); }; + +const getIsSavedUser = (username) => { + return Boolean(username && window.U2N.usersByUsernames?.[username]); +}; diff --git a/src/user-script/parts/helpers.js b/src/user-script/parts/helpers.js index 36ad1d8..6335eb8 100644 --- a/src/user-script/parts/helpers.js +++ b/src/user-script/parts/helpers.js @@ -11,6 +11,22 @@ const debounce = (fn, time) => { export const upperCaseFirstLetter = (text) => (typeof text === 'string' ? text.charAt(0).toUpperCase() + text.slice(1) : ''); +export 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}`; +}; + export const getShouldUseUsernameAsDisplayname = (username) => { const { shouldFilterBySubstring, diff --git a/src/user-script/parts/render-users.js b/src/user-script/parts/render-users.js index 809cecc..a2bf161 100644 --- a/src/user-script/parts/render-users.js +++ b/src/user-script/parts/render-users.js @@ -1,5 +1,7 @@ +const dataU2NSource = 'data-u2n-source'; + const getUserElements = () => { - const links = Array.from(document.querySelectorAll('[data-hovercard-url^="/users/"]')).map((el) => { + 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)) { @@ -12,7 +14,64 @@ const getUserElements = () => { 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(` @@ -45,10 +104,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; @@ -106,6 +171,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 @@ -117,7 +183,6 @@ appendCSS(` `, { sourceName: 'render-users' }); export const renderUsers = () => { - const elements = getUserElements(); const { color, shouldShowAvatars, @@ -128,19 +193,30 @@ export 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'); @@ -163,4 +239,31 @@ export 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; + }); };