Repository: refined-github/refined-github
Pull request #8189 (2025-01-07 06:59:33)

`easy-toggle-files` - Support new commit view
#7808


## Test URLs

https://github.com/refined-github/refined-github/commit/7b12c475a54ee32531ea4f9a548f08aea1081b83

## Screenshot

https://github.com/user-attachments/assets/4cf494ca-a1a8-4f8b-8bce-33816c132623




Patch:
diff --git a/build/__snapshots__/features-meta.json b/build/__snapshots__/features-meta.json
index 57e41ff8..625d0abe 100644
--- a/build/__snapshots__/features-meta.json
+++ b/build/__snapshots__/features-meta.json
@@ -807,7 +807,6 @@
 	{
 		"id": "show-names",
 		"description": "Adds the real name of users by their usernames.",
-		"css": true,
 		"screenshot": "https://github-production-user-asset-6210df.s3.amazonaws.com/83146190/252756294-94785dc2-423e-498c-939a-359a012036e0.png"
 	},
 	{
diff --git a/package-lock.json b/package-lock.json
index 32110c80..c0e46288 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -45,7 +45,7 @@
 				"webext-dynamic-content-scripts": "^10.0.4",
 				"webext-msg": "^1.0.0",
 				"webext-options-sync": "^4.3.0",
-				"webext-options-sync-per-domain": "^4.2.3",
+				"webext-options-sync-per-domain": "^4.3.1",
 				"webext-permission-toggle": "^5.1.0",
 				"webext-storage": "^1.2.2",
 				"webext-storage-cache": "^6.0.3",
@@ -11797,9 +11797,9 @@
 			}
 		},
 		"node_modules/webext-detect": {
-			"version": "5.3.1",
-			"resolved": "https://registry.npmjs.org/webext-detect/-/webext-detect-5.3.1.tgz",
-			"integrity": "sha512-vmVd7haZ+CW/n037KUCi7t5Xn+oMxxScsgTPXirnHxXhHLJsR+4je9glh6QrsptgUfkZN4dGcFTrBrZUah1RAg==",
+			"version": "5.3.2",
+			"resolved": "https://registry.npmjs.org/webext-detect/-/webext-detect-5.3.2.tgz",
+			"integrity": "sha512-RU/e8OLiPwU0efWs/cHp3sRCY+vakaTnbOS/x8VM/FkTtv6cfW/Ofed0i7WnkAKw2jbp4gki3WcpAdii+2QVMw==",
 			"license": "MIT",
 			"engines": {
 				"node": ">=18"
@@ -11878,14 +11878,14 @@
 			}
 		},
 		"node_modules/webext-options-sync-per-domain": {
-			"version": "4.2.3",
-			"resolved": "https://registry.npmjs.org/webext-options-sync-per-domain/-/webext-options-sync-per-domain-4.2.3.tgz",
-			"integrity": "sha512-WebwQ+ah8oqccLrzN965Bw+BzuKGSjiYbBpGrFAkRaD3WkWeyqPth9alw3Wlq1UEaC5Ul5TkB2qxQbCuoYHuTQ==",
+			"version": "4.3.1",
+			"resolved": "https://registry.npmjs.org/webext-options-sync-per-domain/-/webext-options-sync-per-domain-4.3.1.tgz",
+			"integrity": "sha512-7X7hr5opY4o3l3GWRawHrsQQv3NDreRzbqAYEo29OTFKe8ihlRydaD2nfZj3RDB0QBjf1Fb1H7OTgM1HQFSvuA==",
 			"license": "MIT",
 			"dependencies": {
 				"memoize": "^10.0.0",
-				"webext-detect": "^5.0.2",
-				"webext-options-sync": "^4.2.3",
+				"webext-detect": "^5.3.2",
+				"webext-options-sync": "^4.3.0",
 				"webext-patterns": "^1.5.0",
 				"webext-permissions": "^3.1.3"
 			},
diff --git a/package.json b/package.json
index 071513b9..8e899087 100644
--- a/package.json
+++ b/package.json
@@ -109,7 +109,7 @@
 		"webext-dynamic-content-scripts": "^10.0.4",
 		"webext-msg": "^1.0.0",
 		"webext-options-sync": "^4.3.0",
-		"webext-options-sync-per-domain": "^4.2.3",
+		"webext-options-sync-per-domain": "^4.3.1",
 		"webext-permission-toggle": "^5.1.0",
 		"webext-storage": "^1.2.2",
 		"webext-storage-cache": "^6.0.3",
diff --git a/safari/app-store-description.txt b/safari/app-store-description.txt
index ddd1c364..be8c62b8 100644
--- a/safari/app-store-description.txt
+++ b/safari/app-store-description.txt
@@ -18,7 +18,7 @@ Refined GitHub is a Safari extension that simplifies the GitHub.com interface an
 - Adds possible related pages and alternatives on 404 pages.
 - Displays the age of the repository in the sidebar.
 
-And so much more! 200+ features. See the website for a full list of features.
+And so much more! 200+ features. See the Developer Website for a full list of features.
 
 
 The GITHUB and REFINED GITHUB trademarks are owned by GitHub, Inc. and used under license.
diff --git a/source/features/conversation-activity-filter.tsx b/source/features/conversation-activity-filter.tsx
index 0823395a..a6835b21 100644
--- a/source/features/conversation-activity-filter.tsx
+++ b/source/features/conversation-activity-filter.tsx
@@ -161,10 +161,10 @@ async function addWidget(state: State, anchor: HTMLElement): Promise<void> {
 			className={`details-reset details-overlay d-inline-block ml-2 position-relative ${dropdownClass}`}
 			id="rgh-conversation-activity-filter-select-menu"
 		>
-			<summary>
-				<EyeIcon className="color-fg-muted" />
+			<summary className="height-full color-fg-muted">
+				<EyeIcon />
 				<EyeClosedIcon className="color-fg-danger" />
-				<span className="text-small color-fg-danger v-align-text-bottom rgh-conversation-events-label"> events</span>
+				<span className="text-small color-fg-danger v-align-text-bottom rgh-conversation-events-label ml-1">events</span>
 				<div className="dropdown-caret ml-1" />
 			</summary>
 			<details-menu
diff --git a/source/features/easy-toggle-files.tsx b/source/features/easy-toggle-files.tsx
index 46e03740..b99a7343 100644
--- a/source/features/easy-toggle-files.tsx
+++ b/source/features/easy-toggle-files.tsx
@@ -9,9 +9,23 @@ function toggleFile(event: DelegateEvent<MouseEvent>): void {
 	const elementClicked = event.target as HTMLElement;
 	const headerBar = event.delegateTarget;
 
-	// The clicked element is either the bar itself or one of its 2 children
-	if (elementClicked === headerBar || elementClicked.parentElement === headerBar) {
-		$('[aria-label="Toggle diff contents"]', headerBar)
+	if (
+		// The clicked element is either the bar itself or one of its 2 children
+		elementClicked === headerBar
+		|| elementClicked.parentElement === headerBar
+		// React
+		// TODO: Replace with `|| !elementClicked.closest('a, button')` if it works
+		|| elementClicked.matches([
+			'[class^="DiffFileHeader-module__diff-file-header"] > div',
+			'[class^="DiffFileHeader-module__diff-file-header"] > div > div',
+		])
+	) {
+		$([
+			'[aria-label="Toggle diff contents"]',
+			// React
+			'[aria-label^="collapse file"]',
+			'[aria-label^="expand file"]',
+		], headerBar)
 			.dispatchEvent(new MouseEvent('click', {bubbles: true, altKey: event.altKey}));
 	}
 }
@@ -28,7 +42,11 @@ function toggleCodeSearchFile(event: DelegateEvent<MouseEvent>): void {
 }
 
 function init(signal: AbortSignal): void {
-	delegate('.file-header', 'click', toggleFile, {signal});
+	delegate([
+		'.file-header',
+		// React
+		'[class^="Diff-module__diffHeaderWrapper"]',
+	], 'click', toggleFile, {signal});
 }
 
 function initSearchPage(signal: AbortSignal): void {
diff --git a/source/features/esc-to-deselect-line.tsx b/source/features/esc-to-deselect-line.tsx
index eaf208b2..fb912095 100644
--- a/source/features/esc-to-deselect-line.tsx
+++ b/source/features/esc-to-deselect-line.tsx
@@ -1,3 +1,4 @@
+import {$optional} from 'select-dom/strict.js';
 import * as pageDetect from 'github-url-detection';
 
 import features from '../feature-manager.js';
@@ -13,7 +14,23 @@ function isLineSelected(): boolean {
 
 function listener({key, target}: KeyboardEvent): void {
 	if (key === 'Escape' && isLineSelected() && !isEditable(target)) {
-		location.hash = '#no-line'; // Update UI, without `scroll-to-top` behavior
+		const selectedLineNumber = $optional('.react-line-number.highlighted-line');
+
+		if (selectedLineNumber) {
+			// Save and remove line number
+			const {lineNumber} = selectedLineNumber.dataset;
+			selectedLineNumber.dataset.lineNumber = '';
+			// Trigger click to deselect
+			selectedLineNumber.dispatchEvent(new MouseEvent('mousedown', {bubbles: true}));
+			// Restore line number
+			selectedLineNumber.dataset.lineNumber = lineNumber;
+			// Un-focus code block
+			(document.activeElement as HTMLElement).blur();
+		} else {
+			// TODO: Review in December 2025 if old UI is gone. Currently only applies to PRs
+			location.hash = '#no-line'; // Update UI, without `scroll-to-top` behavior
+		}
+
 		history.replaceState(undefined, document.title, location.pathname); // Drop remaining # from url
 	}
 }
diff --git a/source/features/github-bugs.css b/source/features/github-bugs.css
index 544c84b7..ceb6ad50 100644
--- a/source/features/github-bugs.css
+++ b/source/features/github-bugs.css
@@ -102,6 +102,21 @@ svg.octicon-package + a.Truncate[href*='/releases/download/'] > .Truncate-text {
 	white-space: wrap;
 }
 
+/* Notifications "Group by" button unnecessary space */
+/* Info: https://github.com/refined-github/refined-github/pull/8208 */
+/* Test: https://github.com/notifications */
+/* Matches .d-md-block */
+@media (width >= 768px) {
+	/* Pulls the alert container to the right when empty */
+	.gap-4.pl-3:not(:has(a.h6)):has(
+			> .d-md-block
+				> .js-socket-channel[data-url^='/notifications/beta/recent_notifications_alert']
+		) {
+		/* Substract 16px spacing from both .gap-3 and .ml-3 */
+		margin-right: -32px;
+	}
+}
+
 /*
 
 Test URLs:
diff --git a/source/features/linkify-user-location.tsx b/source/features/linkify-user-location.tsx
index 785167d5..129a7f88 100644
--- a/source/features/linkify-user-location.tsx
+++ b/source/features/linkify-user-location.tsx
@@ -28,7 +28,9 @@ function addLocation({nextElementSibling, nextSibling}: SVGElement): Element {
 function initOnce(): void {
 	observe([
 		'[itemprop="homeLocation"] svg.octicon-location', // `isUserProfile`
-		'[aria-label="User location"] svg.octicon-location', // Hover cards
+		'.pagehead .has-location svg.octicon-location', // `isOrganizationProfile`
+		'[aria-label="User location"] svg.octicon-location', // User hover cards
+		'[aria-label="Organization Hovercard"] svg.octicon-location', // Organization hover cards
 	], addLocation);
 }
 
@@ -41,7 +43,8 @@ void features.add(import.meta.url, {
 
 Test URLs
 
-https://github.com/docubot
-https://github.com/
+- isUserProfile: https://github.com/mysticatea
+- isOrganizationProfile: https://github.com/github
+- Hover cards: https://github.com/
 
 */
diff --git a/source/features/quick-mention.css b/source/features/quick-mention.css
index cc0ecdd6..127ded13 100644
--- a/source/features/quick-mention.css
+++ b/source/features/quick-mention.css
@@ -1,6 +1,6 @@
 /* Show button when the avatar or the button itself are hovered/focused */
 :not(:hover, :focus) + .rgh-quick-mention:not(:hover, :focus) {
-	opacity: 10%;
+	opacity: 20%;
 }
 
 .rgh-quick-mention {
@@ -14,6 +14,11 @@
 	padding: 4px 12px !important;
 }
 
+.rgh-quick-mention.react-view {
+	position: static !important;
+	translate: none;
+}
+
 /* Show icon in black if it's not hovered/focused, instead of the blue color caused by .btn-link */
 .rgh-quick-mention:not(:hover, :focus) {
 	color: inherit;
diff --git a/source/features/quick-mention.tsx b/source/features/quick-mention.tsx
index 7c20602d..c63a604f 100644
--- a/source/features/quick-mention.tsx
+++ b/source/features/quick-mention.tsx
@@ -13,7 +13,10 @@ import features from '../feature-manager.js';
 import {getUsername, isArchivedRepoAsync} from '../github-helpers/index.js';
 import observe from '../helpers/selector-observer.js';
 
-const fieldSelector = 'textarea#new_comment_field';
+const fieldSelector = [
+	'textarea#new_comment_field',
+	'#react-issue-comment-composer textarea',
+] as const;
 
 function prefixUserMention(userMention: string): string {
 	// The alt may or may not have it #4859
@@ -50,16 +53,26 @@ function add(avatar: HTMLElement): void {
 		// Reviews
 		'.js-comment',
 	])!;
-	if (debug) {
-		timelineItem.style.border = 'solid 5px red';
-	}
 
-	if (
-		// TODO: Rewrite with :has()
-		// Exclude events that aren't tall enough, like hidden comments or reviews without comments
-		!elementExists('.unminimized-comment, .js-comment-container', timelineItem)
-	) {
-		return;
+	const isOldView = Boolean(timelineItem);
+
+	if (isOldView) {
+		if (debug) {
+			timelineItem.style.border = 'solid 5px red';
+		}
+
+		if (
+			// Exclude events that aren't tall enough, like hidden comments or reviews without comments
+			!elementExists('.unminimized-comment, .js-comment-container', timelineItem)
+		)
+			return;
+	} else {
+		// Make sure the comment isn't hidden
+		const contentItem = avatar.parentElement!.querySelector('[data-testid="comment-header"] + div')!;
+
+		if (!contentItem) {
+			return;
+		}
 	}
 
 	if (debug) {
@@ -72,12 +85,17 @@ function add(avatar: HTMLElement): void {
 		wrap(avatar, <div className="avatar-parent-child TimelineItem-avatar d-none d-md-block" />);
 	}
 
-	const userMention = $('img', avatar).alt;
+	if (!isOldView) {
+		wrap(avatar, <div className="avatar-parent-child d-none d-md-block" />);
+	}
+
+	const userMention = isOldView ? $('img', avatar).alt : avatar.getAttribute('alt');
+
 	avatar.after(
 		<button
 			type="button"
-			className="rgh-quick-mention tooltipped tooltipped-e btn-link"
-			aria-label={`Mention ${prefixUserMention(userMention)} in a new comment`}
+			className={['rgh-quick-mention tooltipped tooltipped-e btn-link', isOldView ? '' : 'react-view'].join(' ')}
+			aria-label={`Mention ${prefixUserMention(userMention!)} in a new comment`}
 		>
 			<ReplyIcon />
 		</button>,
@@ -96,13 +114,17 @@ async function init(signal: AbortSignal): Promise<void> {
 	// Avatars next to review events aren't wrapped in a <div> #4844
 	// :has(fieldSelector) enables the feature only when/after the "mention" button can actually work
 	// .js-quote-selection-container selects the closest parent that contains both the new comment field and the avatar #7378
-	observe(`
-		.js-quote-selection-container:has(${fieldSelector})
+	observe([
+		// TODO: Drop after June 2025
+		`
+		.js-quote-selection-container:has(${fieldSelector[0]})
 		:is(
 			div.TimelineItem-avatar > [data-hovercard-type="user"]:first-child,
 			a.TimelineItem-avatar
 		):not([href="/${getUsername()!}"])
-	`, add, {signal});
+	`,
+		`[data-testid="issue-viewer-container"]:has(${fieldSelector[1]}) [class^="LayoutHelpers-module__timelineElement"] > img[data-component="Avatar"]:not([alt="${getUsername()!}"])`,
+	], add, {signal});
 }
 
 void features.add(import.meta.url, {
diff --git a/source/features/show-names.css b/source/features/show-names.css
deleted file mode 100644
index 5765041e..00000000
--- a/source/features/show-names.css
+++ /dev/null
@@ -1,4 +0,0 @@
-/* Prevent text reversal Unicode hacks in full names */
-.rgh-show-names .timeline-comment-header-text bdo {
-	display: inline-block;
-}
diff --git a/source/features/show-names.tsx b/source/features/show-names.tsx
index dc26d3f1..51376d4d 100644
--- a/source/features/show-names.tsx
+++ b/source/features/show-names.tsx
@@ -1,5 +1,3 @@
-import './show-names.css';
-
 import React from 'dom-chef';
 import * as pageDetect from 'github-url-detection';
 import batchedFunction from 'batched-function';
@@ -27,7 +25,8 @@ function appendName(element: HTMLAnchorElement, fullName: string): void {
 	const insertionPoint = parentElement!.tagName === 'STRONG' ? parentElement! : element;
 	const nameElement = (
 		<span className="color-fg-muted css-truncate d-inline-block ml-1">
-			(<bdo className="css-truncate-target" style={{maxWidth: '200px'}}>{fullName}</bdo>)
+			{/* .css-truncate-target sets display: inline-block and confines bidi overrides #8191 */}
+			(<span className="css-truncate-target" style={{maxWidth: '200px'}}>{fullName}</span>)
 		</span>
 	);
 
@@ -106,4 +105,10 @@ Test URLs:
 - mannequins: https://togithub.com/python/cpython/issues/67591
 - newsfeed: https://github.com
 
+Special cases:
+
+- RTL: https://github.com/refined-github/refined-github/issues/8191
+- Bidi override case 1: https://togithub.com/FortAwesome/Font-Awesome/issues/2465
+- Bidi override case 2: https://togithub.com/w3c/webdriver/issues/385#issuecomment-598407238
+
 */
diff --git a/source/features/sticky-sidebar.css b/source/features/sticky-sidebar.css
index abcbf4d2..74f4d135 100644
--- a/source/features/sticky-sidebar.css
+++ b/source/features/sticky-sidebar.css
@@ -45,6 +45,10 @@ Exclusively use simple sums and use `0px` instead of `0`
 	z-index: 1;
 }
 
+#discussion_bucket #partial-discussion-sidebar.rgh-sticky-sidebar-container {
+	height: min-content;
+}
+
 /* Hides the last divider (on pull requests) to avoid https://user-images.githubusercontent.com/10238474/62282128-af6fb980-b457-11e9-8b18-c29687b88da1.gif */
 .rgh-sticky-sidebar + .border-top {
 	display: none;
diff --git a/source/features/sticky-sidebar.tsx b/source/features/sticky-sidebar.tsx
index af22fd0b..14c1fc1d 100644
--- a/source/features/sticky-sidebar.tsx
+++ b/source/features/sticky-sidebar.tsx
@@ -14,6 +14,7 @@ const sidebarSelector = [
 	'.Layout-sidebar .BorderGrid', // `isRepoRoot`
 	'.Layout-sidebar #partial-discussion-sidebar', // Old `isConversation`
 	'div[data-testid="issue-viewer-metadata-pane"]', // `isConversation`
+	'#discussion_bucket #partial-discussion-sidebar', // `isDiscussion`
 ];
 
 let sidebar: HTMLElement | undefined;
@@ -39,7 +40,8 @@ function trackSidebar(signal: AbortSignal, foundSidebar: HTMLElement): void {
 		sidebar = undefined;
 	});
 
-	sidebar.parentElement?.classList.add('rgh-sticky-sidebar-container');
+	const container = sidebar.parentElement!.id === 'discussion_bucket' ? sidebar : sidebar.parentElement!;
+	container.classList.add('rgh-sticky-sidebar-container');
 
 	sidebar.addEventListener('mouseenter', toggleHoverState, {signal});
 	sidebar.addEventListener('mouseleave', toggleHoverState, {signal});
@@ -73,6 +75,7 @@ void features.add(import.meta.url, {
 	include: [
 		pageDetect.isRepoRoot,
 		pageDetect.isConversation,
+		pageDetect.isDiscussion,
 	],
 	exclude: [
 		() => screen.availWidth < minimumViewportWidthForSidebar,
diff --git a/source/features/update-pr-from-base-branch.tsx b/source/features/update-pr-from-base-branch.tsx
index a432aa8c..4e72cfd8 100644
--- a/source/features/update-pr-from-base-branch.tsx
+++ b/source/features/update-pr-from-base-branch.tsx
@@ -44,7 +44,7 @@ function createButton(): JSX.Element {
 	return (
 		<button
 			type="button"
-			className="btn btn-sm rgh-update-pr-from-base-branch tooltipped tooltipped-sw"
+			className="btn btn-sm rgh-update-pr-from-base-branch tooltipped tooltipped-w"
 			aria-label="Use Refined GitHub to update the PR from the base branch"
 		>
 			Update branch
@@ -59,40 +59,57 @@ async function addButton(mergeBar: Element): Promise<void> {
 
 	const {base} = getBranches();
 	const prInfo = await getPrInfo(base.relative);
-	if (!prInfo.needsUpdate || !(prInfo.viewerCanUpdate || prInfo.viewerCanEditFiles) || prInfo.mergeable === 'CONFLICTING') {
+	if (
+		!prInfo.needsUpdate
+		|| prInfo.mergeable === 'CONFLICTING'
+		|| !(
+			prInfo.viewerCanUpdate
+			|| prInfo.viewerCanEditFiles
+		)
+	) {
 		return;
 	}
 
-	const mergeabilityRow = $optional('.branch-action-item:has(.merging-body)');
+	const mergeabilityRow = $optional([
+		'.branch-action-item:has(.merging-body)', // TODO: Drop after June 2025
+		'[aria-label="Conflicts"] [class^="MergeBoxSectionHeader-module__wrapper"]',
+	]);
+
 	if (mergeabilityRow) {
-		// The PR is not a draft
-		mergeabilityRow.prepend(
+		const isOldView = mergeBar.parentElement?.classList.contains('mergeability-details');
+		const positionClass = isOldView
+			? 'float-right'
+			: 'flex-order-2 flex-self-center';
 
+		mergeabilityRow.prepend(
 			<div
-				className="branch-action-btn float-right js-immediate-updates js-needs-timeline-marker-header"
+				className={['branch-action-btn js-immediate-updates js-needs-timeline-marker-header', positionClass].join(' ')}
 			>
 				{createButton()}
 			</div>,
 		);
-		return;
+	} else {
+		// Old view draft PRs require a new row to display the button
+		// https://github.com/refined-github/refined-github/pull/8193#discussion_r1908581612
+		mergeBar.before(createMergeabilityRow({
+			className: 'rgh-update-pr-from-base-branch-row',
+			action: createButton(),
+			icon: <CheckIcon />,
+			iconClass: 'completeness-indicator-success',
+			heading: 'This branch has no conflicts with the base branch',
+			meta: 'Merging can be performed automatically.',
+		}));
 	}
-
-	// The PR is still a draft
-	mergeBar.before(createMergeabilityRow({
-		className: 'rgh-update-pr-from-base-branch-row',
-		action: createButton(),
-		icon: <CheckIcon />,
-		iconClass: 'completeness-indicator-success',
-		heading: 'This branch has no conflicts with the base branch',
-		meta: 'Merging can be performed automatically.',
-	}));
 }
 
 async function init(signal: AbortSignal): Promise<false | void> {
 	await expectToken();
 
 	delegate('.rgh-update-pr-from-base-branch', 'click', handler, {signal});
-	observe('.mergeability-details > *:last-child', addButton, {signal});
+	observe([
+		'.mergeability-details > *:last-child', // Old view - TODO: Drop after June 2025
+		'[class^="MergeBox-module__mergePartialContainer"]',
+	], addButton, {signal});
 }
 
 void features.add(import.meta.url, {
diff --git a/source/github-helpers/index.ts b/source/github-helpers/index.ts
index dc38bf79..8b8e5ae3 100644
--- a/source/github-helpers/index.ts
+++ b/source/github-helpers/index.ts
@@ -49,7 +49,10 @@ export function isUsernameAlreadyFullName(username: string, realname: string): b
 		.toLowerCase();
 	realname = realname
 		.normalize('NFD')
-		.replaceAll(/\W/g, '')
+		// Remove diacritics, punctuation and spaces
+		// https://stackoverflow.com/a/37511463/288906
+		// https://www.freecodecamp.org/news/what-is-punct-in-regex-how-to-match-all-punctuation-marks-in-regular-expressions/
+		.replaceAll(/[\p{Diacritic}\p{P}\s]/gu, '')
 		.toLowerCase();
 
 	return username === realname;
diff --git a/source/options.html b/source/options.html
index 87aa5fb6..df5170e8 100644
--- a/source/options.html
+++ b/source/options.html
@@ -24,7 +24,8 @@
 	<details id="token">
 		<summary><strong>🔑 Personal token</strong></summary>
 		<div>
-			<p>You should <a id="personal-token-link" href="https://github.com/settings/tokens/new?description=Refined%20GitHub&scopes=repo,read:project">generate a token</a> to ensure that every feature works correctly. You can read more about the token on <a href="https://github.com/refined-github/refined-github/wiki/Security">the wiki.</a></p>
+			<!-- Keep this URL in sync with welcome.html -->
+			<p>You should <a id="personal-token-link" href="https://github.com/settings/tokens/new?description=Refined%20GitHub&scopes=repo,read:project&default_expires_at=none">generate a token</a> to ensure that every feature works correctly. You can read more about the token on <a href="https://github.com/refined-github/refined-github/wiki/Security">the wiki.</a></p>
 			<p><strong>Token-less usage is not officially supported.</strong></p>
 			<p>
 				<input
diff --git a/source/options.tsx b/source/options.tsx
index 46eb600e..599e5aaa 100644
--- a/source/options.tsx
+++ b/source/options.tsx
@@ -147,8 +147,7 @@ function addEventListeners(): void {
 			element.setAttribute('item', domain === 'default' ? 'options' : 'options:' + domain);
 		}
 
-		// Delay to let options load first
-		setTimeout(updateListDom, 100);
+		updateListDom();
 	});
 
 	// Refresh page when permissions are changed (because the dropdown selector needs to be regenerated)
diff --git a/source/options/token-validation.tsx b/source/options/token-validation.tsx
index aea26f1c..140e9bd0 100644
--- a/source/options/token-validation.tsx
+++ b/source/options/token-validation.tsx
@@ -85,8 +85,5 @@ export default async function initTokenValidation(syncedForm: SyncedForm | undef
 	});
 
 	// Update domain-dependent page content when the domain is changed
-	syncedForm?.onChange(async () => {
-		// Delay to let options load first
-		setTimeout(validateToken, 100);
-	});
+	syncedForm?.onChange(validateToken);
 }
diff --git a/source/welcome.svelte b/source/welcome.svelte
index b459a0ba..a1fc0065 100644
--- a/source/welcome.svelte
+++ b/source/welcome.svelte
@@ -98,6 +98,7 @@
 				class="will-show"
 				onclick={showThirdStep}
 			>
+				<!-- Keep this URL in sync with options.html -->
 				<a
 					href="https://github.com/settings/tokens/new?description=Refined%20GitHub&scopes=repo,read:project&default_expires_at=none"
 					onclick={markSecondStep}


Test patch:
diff --git a/source/github-helpers/index.test.ts b/source/github-helpers/index.test.ts
index e3a7e40c..091a8ce4 100644
--- a/source/github-helpers/index.test.ts
+++ b/source/github-helpers/index.test.ts
@@ -91,8 +91,11 @@ test('isUsernameAlreadyFullName', () => {
 	assert.isTrue(isUsernameAlreadyFullName('john-wdoe', 'John W. Doe'));
 	assert.isTrue(isUsernameAlreadyFullName('john-doe-jr', 'John Doe Jr.'));
 	assert.isTrue(isUsernameAlreadyFullName('nicolo', 'Nicolò'));
+
+	assert.isFalse(isUsernameAlreadyFullName('wonderful', 'wonder'));
 	assert.isFalse(isUsernameAlreadyFullName('dotconnor', 'Connor Love'));
-	assert.isFalse(isUsernameAlreadyFullName('fregante ', 'Federico Brigante'));
+	assert.isFalse(isUsernameAlreadyFullName('fregante', 'Federico Brigante'));
+	assert.isFalse(isUsernameAlreadyFullName('chipwolf', 'Chip Wolf ‮ '));
 });
 
 test('getLatestVersionTag', () => {