Skip to content

Add 'Copy with Redirect' context menu feature#443

Closed
SmartManoj wants to merge 2 commits intoeinaregilsson:masterfrom
SmartManoj:rmb
Closed

Add 'Copy with Redirect' context menu feature#443
SmartManoj wants to merge 2 commits intoeinaregilsson:masterfrom
SmartManoj:rmb

Conversation

@SmartManoj
Copy link

Introduces a context menu option to copy redirected URLs directly from links, applying user-defined redirect rules. Updates background script to handle context menu creation, click events, and clipboard operations, and updates permissions in manifest.json. Documentation in README.md is expanded to describe the new feature.

Resolves #414

Introduces a context menu option to copy redirected URLs directly from links, applying user-defined redirect rules. Updates background script to handle context menu creation, click events, and clipboard operations, and updates permissions in manifest.json. Documentation in README.md is expanded to describe the new feature.
Copy link
Collaborator

@Gitoffthelawn Gitoffthelawn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks nice. Thank you. ❤️

I have mixed emotions about a menuitem that will copy the original link when there is no applicable redirect rule, but copies a modified URL when there is one.

Here are 3 other options. I'm interested in your thoughts about these 3 alternative UX designs (they are all mutually exclusive; only the original UX design from the PR or one of these will be used):

  1. Dim the new context menuitem when there is no applicable redirect rule.
  2. Do not show the new context menuitem when there is no applicable redirect rule.
  3. Change the menuitem's label to reflect whether or not there is an applicable redirect rule.

@SmartManoj
Copy link
Author

Will this item pollute the context menu?

@Gitoffthelawn
Copy link
Collaborator

Will this item pollute the context menu?

Somewhat. That's one reason I like option 2:

  1. Do not show the new context menuitem when there is no applicable redirect rule.

Added logic to update the context menu dynamically depending on whether there are matching redirect rules for a given URL. The context menu is now only shown when relevant, and is removed when the extension is disabled. This improves user experience by preventing unnecessary context menu items.
@SmartManoj SmartManoj requested a review from Gitoffthelawn July 23, 2025 11:28
@tillcash
Copy link

Will this item pollute the context menu?

Nanba, also allow users to disable the context menu completely.

Copy link

@brian6932 brian6932 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on first glance, I'll do another review:

  1. Currently, Copy with Redirect always shows.
  2. Currently, pressing Copy with Redirect, when the link has no redirect, results in a noop (nothing being copied to your clipboard).
  3. You're using var unnecessarily over consts and lets.

Comment on lines +412 to +517
function hasMatchingRedirects(url) {
if (!partitionedRedirects || Object.keys(partitionedRedirects).length === 0) {
return false;
}

// Check if there are any enabled redirect rules that match this URL
for (var requestType in partitionedRedirects) {
var list = partitionedRedirects[requestType];
if (list) {
for (var i = 0; i < list.length; i++) {
var r = list[i];
if (!r.disabled) {
var result = r.getMatch(url);
if (result.isMatch) {
return true;
}
}
}
}
}
return false;
}

// Setup context menu for "Copy with Redirect" feature
function setupContextMenu() {
chrome.contextMenus.removeAll(function() {
chrome.contextMenus.create({
id: "copyWithRedirect",
title: "Copy with Redirect",
contexts: ["link"],
documentUrlPatterns: ["http://*/*", "https://*/*"]
});
});
}

// Handle context menu clicks
chrome.contextMenus.onClicked.addListener(function(info, tab) {
if (info.menuItemId === "copyWithRedirect") {
// Only proceed if there are matching redirects for this URL
if (hasMatchingRedirects(info.linkUrl)) {
handleCopyWithRedirect(info.linkUrl, tab);
}
}
});

// Handle the "Copy with Redirect" functionality
function handleCopyWithRedirect(originalUrl, tab) {
// Check if there are any redirects that would apply to this URL
var hasRedirect = false;
var redirectedUrl = originalUrl;

// Check all redirect types for a match
for (var requestType in partitionedRedirects) {
var list = partitionedRedirects[requestType];
if (list) {
for (var i = 0; i < list.length; i++) {
var r = list[i];
var result = r.getMatch(originalUrl);

if (result.isMatch) {
hasRedirect = true;
redirectedUrl = result.redirectTo;
break;
}
}
if (hasRedirect) break;
}
}

// Copy the appropriate URL to clipboard
var urlToCopy = hasRedirect ? redirectedUrl : originalUrl;

// Use the clipboard API to copy the URL
navigator.clipboard.writeText(urlToCopy).then(function() {
// Show a notification that the URL was copied
if (enableNotifications) {
let icon = isDarkMode() ? "images/icon-dark-theme-48.png": "images/icon-light-theme-48.png";

if(navigator.userAgent.toLowerCase().indexOf("chrome") > -1 && navigator.userAgent.toLowerCase().indexOf("opr")<0){
var items = [{title:"Original URL: ", message: originalUrl},{title:"Copied URL: ",message: urlToCopy}];
var head = "Redirector - Copied URL";
chrome.notifications.create({
type : "list",
items : items,
title : head,
message : head,
iconUrl : icon
});
} else {
var message = hasRedirect ?
"Copied redirected URL: " + urlToCopy + " (original: " + originalUrl + ")" :
"Copied URL: " + urlToCopy;

chrome.notifications.create({
type : "basic",
title : "Redirector",
message : message,
iconUrl : icon
});
}
}
}).catch(function(err) {
log('Failed to copy URL to clipboard: ' + err);
});
}

Copy link

@brian6932 brian6932 Jul 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function hasMatchingRedirects(url) {
if (!partitionedRedirects || Object.keys(partitionedRedirects).length === 0) {
return false;
}
// Check if there are any enabled redirect rules that match this URL
for (var requestType in partitionedRedirects) {
var list = partitionedRedirects[requestType];
if (list) {
for (var i = 0; i < list.length; i++) {
var r = list[i];
if (!r.disabled) {
var result = r.getMatch(url);
if (result.isMatch) {
return true;
}
}
}
}
}
return false;
}
// Setup context menu for "Copy with Redirect" feature
function setupContextMenu() {
chrome.contextMenus.removeAll(function() {
chrome.contextMenus.create({
id: "copyWithRedirect",
title: "Copy with Redirect",
contexts: ["link"],
documentUrlPatterns: ["http://*/*", "https://*/*"]
});
});
}
// Handle context menu clicks
chrome.contextMenus.onClicked.addListener(function(info, tab) {
if (info.menuItemId === "copyWithRedirect") {
// Only proceed if there are matching redirects for this URL
if (hasMatchingRedirects(info.linkUrl)) {
handleCopyWithRedirect(info.linkUrl, tab);
}
}
});
// Handle the "Copy with Redirect" functionality
function handleCopyWithRedirect(originalUrl, tab) {
// Check if there are any redirects that would apply to this URL
var hasRedirect = false;
var redirectedUrl = originalUrl;
// Check all redirect types for a match
for (var requestType in partitionedRedirects) {
var list = partitionedRedirects[requestType];
if (list) {
for (var i = 0; i < list.length; i++) {
var r = list[i];
var result = r.getMatch(originalUrl);
if (result.isMatch) {
hasRedirect = true;
redirectedUrl = result.redirectTo;
break;
}
}
if (hasRedirect) break;
}
}
// Copy the appropriate URL to clipboard
var urlToCopy = hasRedirect ? redirectedUrl : originalUrl;
// Use the clipboard API to copy the URL
navigator.clipboard.writeText(urlToCopy).then(function() {
// Show a notification that the URL was copied
if (enableNotifications) {
let icon = isDarkMode() ? "images/icon-dark-theme-48.png": "images/icon-light-theme-48.png";
if(navigator.userAgent.toLowerCase().indexOf("chrome") > -1 && navigator.userAgent.toLowerCase().indexOf("opr")<0){
var items = [{title:"Original URL: ", message: originalUrl},{title:"Copied URL: ",message: urlToCopy}];
var head = "Redirector - Copied URL";
chrome.notifications.create({
type : "list",
items : items,
title : head,
message : head,
iconUrl : icon
});
} else {
var message = hasRedirect ?
"Copied redirected URL: " + urlToCopy + " (original: " + originalUrl + ")" :
"Copied URL: " + urlToCopy;
chrome.notifications.create({
type : "basic",
title : "Redirector",
message : message,
iconUrl : icon
});
}
}
}).catch(function(err) {
log('Failed to copy URL to clipboard: ' + err);
});
}
function setupContextMenu() {
chrome.contextMenus.removeAll(function() {
chrome.contextMenus.create({
id: "copyWithRedirect",
title: "Copy with Redirect",
contexts: ["link"],
documentUrlPatterns: ["http://*/*", "https://*/*"]
});
});
}
// Handle the "Copy with Redirect" functionality
const handleCopyWithRedirect = async url => {
let hasRedirect = false;
// Check all redirect types for a match
partitionedLoop:
for (const requestType in partitionedRedirects) {
for (let r of partitionedRedirects[requestType]) {
if (!r.disabled && (r = r.getMatch(url)).isMatch) {
hasRedirect = true;
// Copy the appropriate URL to clipboard
url = r.redirectTo;
break partitionedLoop;
}
}
}
// Use the clipboard API to copy the URL
try {
await navigator.clipboard.writeText(url)
// Show a notification that the URL was copied
if (enableNotifications) {
const iconUrl = `images/icon-${isDarkMode() ? "dark" : "light"}-theme-48.png`;
if (navigator.userAgent.includes(" Chrome/")) {
const
items = [
{ title: "Original URL: ", message: url },
{ title: "Copied URL: ", message: url }
],
head = "Redirector - Copied URL";
chrome.notifications.create({
type : "list",
items,
title : head,
message : head,
iconUrl
});
} else {
const message = hasRedirect
? "Copied redirected URL: " + url
: "Copied URL: " + url;
chrome.notifications.create({
type : "basic",
title : "Redirector",
message,
iconUrl
});
}
}
}
catch (err) {
log('Failed to copy URL to clipboard: ' + err.message);
};
}
// Handle context menu clicks
chrome.contextMenus.onClicked.addListener(info => {
// Only proceed if there are matching redirects for this URL
if (info.menuItemId === "copyWithRedirect") {
handleCopyWithRedirect(info.linkUrl);
}
});
  • Remove duplicated logic.
  • Reduce unnecessary branches.
  • Use async/await.
  • Use loop labels.
  • Use the Array.prototype.values built in iterator.
  • Remove spaces on empty lines.
  • Remove unused function params.
  • Use const/let instead of var.
  • Name variables the same as object properties (DRY).
  • Prefer chronological arrow functions.
  • Use the correct DOMException property.
  • Your code loops through partitionedRedirects a whole 3 TIMES, you only need to once. Object.keys (completely unnecessary), hasMatchingRedirects (completely duplicated logic btw), and handleCopyWithRedirect. Not only this, but you didn't break the loop properly.

Copy link
Author

@SmartManoj SmartManoj Jul 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

for chrome, Original URL and Copied URL belongs to same now.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image Now it will show for all links. The function should be `getMatchingRedirects`.

Copy link

@brian6932 brian6932 Jul 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now it will show for all links. The function should be getMatchingRedirects.

That's just not true, you don't understand your own code, there's a link context.

Original URL and Copied URL belongs to same now.

I chose to remove the Original URL, because there was no point in keeping that. It's already in the bottom left corner when hovering.

brian6932 added a commit to brian6932/Redirector that referenced this pull request Jul 24, 2025
@SmartManoj
Copy link
Author

Closing in favor of #448

@SmartManoj SmartManoj closed this Jul 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Copy with Redirect RMB Context Menu Action

4 participants