Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions discord-mini-tabs-extension/MANUAL_TESTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Manual Tests

## Load Unpacked

1. Open `chrome://extensions`.
2. Enable Developer mode.
3. Load the `C:\Users\RGB\rtk\.worktrees\discord-mini-tabs-extension\discord-mini-tabs-extension` directory.
4. Confirm Chrome shows no manifest or service worker registration errors.

## Shortcut Management

1. Add a text shortcut with `https://discord.com/channels/123456789012345678/987654321098765432`.
2. Confirm the shortcut appears in the Text list.
3. Edit the shortcut name.
4. Search for the new name.
5. Delete the shortcut.
6. Confirm the list updates without reopening the popup.

## Save Current Discord Channel

1. Open a real Discord channel in a normal Chrome tab.
2. Open the extension popup.
3. Click Save current.
4. Confirm the URL and suggested name populate the form.
5. Save the shortcut.

## Mini Window

1. Open a saved text shortcut.
2. Confirm one Chrome popup window opens.
3. Send a Discord chat message in that window.
4. Open another shortcut.
5. Confirm the same popup window is reused.
6. Resize the popup window.
7. Close and reopen a shortcut.
8. Confirm the remembered size is used.

## Voice URL

1. Save a Discord voice channel URL as type Voice.
2. Open it from the Voice list.
3. Confirm Discord web displays the voice channel UI.
4. Join or leave voice manually through Discord web.

## Window Controls

1. Click Focus and confirm the mini window comes forward.
2. Click Close and confirm the mini window closes.
3. Click Reset position and confirm the next open lets Chrome choose a valid position with the saved size.

## Settings

1. Set width to `420`, height to `900`, and zoom to `90`.
2. Open a shortcut.
3. Confirm the mini window size and Discord tab zoom are applied.
4. Change width to `500` and zoom to `100`.
5. Confirm the existing mini window updates.

## Stability Smoke Test

1. Switch between several text and voice shortcuts repeatedly.
2. Confirm only one Discord mini window remains open.
3. Close the mini window manually.
4. Open a shortcut again.
5. Confirm the extension recovers without errors.

## Service Worker Errors

1. Open `chrome://extensions`.
2. Open the service worker inspection link for Discord Mini Tabs.
3. Confirm there are no uncaught exceptions after opening the popup, adding a shortcut, opening a shortcut, focusing, closing, and changing settings.
22 changes: 22 additions & 0 deletions discord-mini-tabs-extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Discord Mini Tabs

A Chrome Manifest V3 extension that opens saved Discord text and voice channel URLs in one reusable Chrome popup window.

## Development

Run pure logic tests:

```bash
npm test
```

Load in Chrome:

1. Open `chrome://extensions`.
2. Enable Developer mode.
3. Click Load unpacked.
4. Select the `discord-mini-tabs-extension` directory.

## Scope

Discord runs as the official Discord web app in a real Chrome popup window. This extension does not embed Discord, inject content scripts, read Discord messages, or inspect Discord voice internals.
16 changes: 16 additions & 0 deletions discord-mini-tabs-extension/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"manifest_version": 3,
"name": "Discord Mini Tabs",
"version": "0.1.0",
"description": "Open saved Discord text and voice channels in one reusable mini Chrome popup window.",
"permissions": ["storage", "tabs", "windows", "activeTab"],
"host_permissions": ["https://discord.com/*"],
"background": {
"service_worker": "src/background/service-worker.js",
"type": "module"
},
"action": {
"default_title": "Discord Mini Tabs",
"default_popup": "src/popup/popup.html"
}
}
12 changes: 12 additions & 0 deletions discord-mini-tabs-extension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "discord-mini-tabs-extension",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"test": "node --test"
},
"engines": {
"node": ">=20"
}
}
183 changes: 183 additions & 0 deletions discord-mini-tabs-extension/src/background/service-worker-core.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { MESSAGE_TYPES } from "../shared/constants.js";

function getSuggestedName(title) {
const cleanedTitle = String(title ?? "")
.replace(/\s*(?:-|[|])\s*Discord$/i, "")
.trim();
return cleanedTitle || "Discord channel";
}

export function createServiceWorkerController({
chromeApi,
getExtensionState,
setShortcuts,
setWindowState,
createShortcut,
deleteShortcut,
findShortcut,
updateShortcut,
validateDiscordChannelUrl,
closeMiniWindow,
focusMiniWindow,
openShortcutInMiniWindow,
resetMiniWindowPosition,
saveBoundsFromWindow,
updateMiniWindowSettings,
setTimeoutFn,
clearTimeoutFn,
debounceMs = 400
}) {
const boundsSaveTimers = new Map();
const boundsSaveTokens = new Map();
let shortcutMutationQueue = Promise.resolve();

function enqueueShortcutMutation(operation) {
const next = shortcutMutationQueue.catch(() => undefined).then(operation);
shortcutMutationQueue = next.catch(() => undefined);
return next;
}

async function handleCreateShortcut(payload) {
const state = await getExtensionState();
const shortcut = createShortcut(payload);
const shortcuts = await setShortcuts([...state.shortcuts, shortcut]);
return { shortcut, shortcuts };
}

async function handleUpdateShortcut(payload) {
const state = await getExtensionState();
const existing = findShortcut(state.shortcuts, payload.id);
if (!existing) {
throw new Error("Shortcut not found.");
}

const updated = updateShortcut(existing, payload);
const shortcuts = await setShortcuts(
state.shortcuts.map((shortcut) => (shortcut.id === updated.id ? updated : shortcut))
);
return { shortcut: updated, shortcuts };
}

async function handleDeleteShortcut(payload) {
const state = await getExtensionState();
const shortcuts = await setShortcuts(deleteShortcut(state.shortcuts, payload.id));
return { shortcuts };
}

async function handleOpenShortcut(payload) {
const state = await getExtensionState();
const shortcut = findShortcut(state.shortcuts, payload.id);
if (!shortcut) {
throw new Error("Shortcut not found.");
}

const windowState = await openShortcutInMiniWindow({ chromeApi, shortcut });
return { windowState };
}

async function handleReadActiveDiscordTab() {
const tabs = await chromeApi.tabs.query({ active: true, currentWindow: true });
const activeTab = tabs[0];
const result = validateDiscordChannelUrl(activeTab?.url ?? "");
if (!result.ok) {
throw new Error("The active tab is not a supported Discord channel URL.");
}

return {
url: result.url,
title: activeTab.title ?? "",
suggestedName: getSuggestedName(activeTab.title)
};
}

async function handleMessage(message) {
const payload = message?.payload ?? {};

switch (message?.type) {
case MESSAGE_TYPES.GET_STATE:
return getExtensionState();
case MESSAGE_TYPES.CREATE_SHORTCUT:
return enqueueShortcutMutation(() => handleCreateShortcut(payload));
case MESSAGE_TYPES.UPDATE_SHORTCUT:
return enqueueShortcutMutation(() => handleUpdateShortcut(payload));
case MESSAGE_TYPES.DELETE_SHORTCUT:
return enqueueShortcutMutation(() => handleDeleteShortcut(payload));
case MESSAGE_TYPES.OPEN_SHORTCUT:
return handleOpenShortcut(payload);
case MESSAGE_TYPES.READ_ACTIVE_DISCORD_TAB:
return handleReadActiveDiscordTab();
case MESSAGE_TYPES.UPDATE_WINDOW_SETTINGS:
return {
windowState: await updateMiniWindowSettings({
chromeApi,
bounds: payload.bounds,
zoom: payload.zoom
})
};
case MESSAGE_TYPES.FOCUS_WINDOW:
return { windowId: await focusMiniWindow({ chromeApi }) };
case MESSAGE_TYPES.CLOSE_WINDOW:
return { windowState: await closeMiniWindow({ chromeApi }) };
case MESSAGE_TYPES.RESET_POSITION:
return { windowState: await resetMiniWindowPosition({ chromeApi }) };
default:
throw new Error("Unsupported message type.");
}
}

async function handleWindowRemoved(windowId) {
try {
const state = await getExtensionState();
if (state.windowState.windowId === windowId) {
await setWindowState({ ...state.windowState, windowId: null, tabId: null });
}
} catch {
// Background window events must not surface unhandled storage failures.
}
}

function handleBoundsChanged(changedWindow) {
if (!changedWindow || changedWindow.type !== "popup" || !Number.isInteger(changedWindow.id)) {
return Promise.resolve();
}

const windowId = changedWindow.id;
const token = Symbol("bounds-save");
boundsSaveTokens.set(windowId, token);
clearTimeoutFn(boundsSaveTimers.get(windowId));
const timerId = setTimeoutFn(async () => {
if (boundsSaveTimers.get(windowId) === timerId) {
boundsSaveTimers.delete(windowId);
}
if (boundsSaveTokens.get(windowId) !== token) {
return;
}

try {
const state = await getExtensionState();
if (boundsSaveTokens.get(windowId) !== token) {
return;
}

await saveBoundsFromWindow(changedWindow, {
expectedWindowId: state.windowState.windowId,
shouldSave: () => boundsSaveTokens.get(windowId) === token
});
} catch {
// Background window events must not surface unhandled storage failures.
} finally {
if (boundsSaveTokens.get(windowId) === token) {
boundsSaveTokens.delete(windowId);
}
}
}, debounceMs);
boundsSaveTimers.set(windowId, timerId);
return Promise.resolve();
}

return {
handleMessage,
handleWindowRemoved,
handleBoundsChanged
};
}
53 changes: 53 additions & 0 deletions discord-mini-tabs-extension/src/background/service-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { createServiceWorkerController } from "./service-worker-core.js";
import { createShortcut, deleteShortcut, findShortcut, updateShortcut } from "../shared/shortcuts.js";
import { validateDiscordChannelUrl } from "../shared/url.js";
import { getExtensionState, setShortcuts, setWindowState } from "./storage.js";
import {
closeMiniWindow,
focusMiniWindow,
openShortcutInMiniWindow,
resetMiniWindowPosition,
saveBoundsFromWindow,
updateMiniWindowSettings
} from "./window-manager.js";

function errorMessage(error) {
return error instanceof Error ? error.message : "Unknown extension error.";
}

const controller = createServiceWorkerController({
chromeApi: chrome,
getExtensionState,
setShortcuts,
setWindowState,
createShortcut,
deleteShortcut,
findShortcut,
updateShortcut,
validateDiscordChannelUrl,
closeMiniWindow,
focusMiniWindow,
openShortcutInMiniWindow,
resetMiniWindowPosition,
saveBoundsFromWindow,
updateMiniWindowSettings,
setTimeoutFn: setTimeout,
clearTimeoutFn: clearTimeout,
debounceMs: 400
});

chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
controller
.handleMessage(message)
.then((data) => sendResponse({ ok: true, data }))
.catch((error) => sendResponse({ ok: false, error: errorMessage(error) }));
return true;
});

chrome.windows.onRemoved.addListener((windowId) => {
controller.handleWindowRemoved(windowId);
});

chrome.windows.onBoundsChanged.addListener((changedWindow) => {
controller.handleBoundsChanged(changedWindow);
});
Loading