diff --git a/source/content-scripts.ts b/source/content-scripts.ts
index a4c8b62..4ecfc06 100644
--- a/source/content-scripts.ts
+++ b/source/content-scripts.ts
@@ -11,6 +11,7 @@ import {
runAnonymizeUsernamesFeature,
runHideVotesFeature,
runMarkdownToolbarFeature,
+ runUsernameColorsFeature,
} from './scripts/exports.js';
import Settings from './settings.js';
import {extractGroups, initializeGlobals, log} from './utilities/exports.js';
@@ -70,6 +71,12 @@ async function initialize() {
});
}
+ if (settings.features.usernameColors) {
+ observerFeatures.push(() => {
+ runUsernameColorsFeature(settings);
+ });
+ }
+
// Initialize all the observer-dependent features first.
for (const feature of observerFeatures) {
feature();
diff --git a/source/options/components/anonymize-usernames.ts b/source/options/components/anonymize-usernames.ts
index bb44c58..111a874 100644
--- a/source/options/components/anonymize-usernames.ts
+++ b/source/options/components/anonymize-usernames.ts
@@ -8,7 +8,8 @@ export function AnonymizeUsernamesSetting(props: SettingProps): TRXComponent {
Anonymizes usernames by replacing them with "Anonymous #".
- Note that User Labels will still be applied to any usernames as normal.
+ Note that User Labels and Username Colors will still be applied to any
+ usernames as normal.
/>
`;
diff --git a/source/options/components/exports.ts b/source/options/components/exports.ts
index 7444aaf..93c830e 100644
--- a/source/options/components/exports.ts
+++ b/source/options/components/exports.ts
@@ -6,3 +6,4 @@ export {HideVotesSetting} from './hide-votes.js';
export {JumpToNewCommentSetting} from './jump-to-new-comment.js';
export {MarkdownToolbarSetting} from './markdown-toolbar.js';
export {UserLabelsSetting} from './user-labels.js';
+export {UsernameColorsSetting} from './username-colors.js';
diff --git a/source/options/components/username-colors.ts b/source/options/components/username-colors.ts
new file mode 100644
index 0000000..806bbc8
--- /dev/null
+++ b/source/options/components/username-colors.ts
@@ -0,0 +1,153 @@
+import {html} from 'htm/preact';
+import {Component} from 'preact';
+
+import Settings from '../../settings.js';
+import {log} from '../../utilities/exports.js';
+import {Setting, SettingProps} from './index.js';
+
+type State = {
+ previewChecked: 'off' | 'foreground' | 'background';
+ usernameColors: UsernameColor[];
+};
+
+export class UsernameColorsSetting extends Component {
+ constructor(props: SettingProps) {
+ super(props);
+
+ this.state = {
+ previewChecked: 'off',
+ usernameColors: [],
+ };
+ }
+
+ async componentDidMount() {
+ const settings = await Settings.fromSyncStorage();
+ this.setState({usernameColors: settings.data.usernameColors});
+ }
+
+ addNewColor = () => {
+ let id = 1;
+ if (this.state.usernameColors.length > 0) {
+ id = this.state.usernameColors.sort((a, b) => b.id - a.id)[0].id + 1;
+ }
+
+ const newColor: UsernameColor = {
+ color: '',
+ id,
+ username: '',
+ };
+
+ this.setState({
+ usernameColors: [...this.state.usernameColors, newColor],
+ });
+ };
+
+ removeColor = (targetId: number) => {
+ const usernameColors = this.state.usernameColors.filter(
+ ({id}) => id !== targetId,
+ );
+ this.setState({usernameColors});
+ };
+
+ saveChanges = async () => {
+ const settings = await Settings.fromSyncStorage();
+ settings.data.usernameColors = this.state.usernameColors;
+ await settings.save();
+ };
+
+ togglePreview = async () => {
+ let {previewChecked} = this.state;
+
+ // eslint-disable-next-line default-case
+ switch (previewChecked) {
+ case 'off':
+ previewChecked = 'foreground';
+ break;
+ case 'foreground':
+ previewChecked = 'background';
+ break;
+ case 'background':
+ previewChecked = 'off';
+ break;
+ }
+
+ this.setState({previewChecked});
+ };
+
+ onInput = (event: Event, id: number, key: 'color' | 'username') => {
+ const colorIndex = this.state.usernameColors.findIndex(
+ (color) => color.id === id,
+ );
+ if (colorIndex === -1) {
+ log(`Tried to edit unknown UsernameColor ID: ${id}`);
+ return;
+ }
+
+ const newValue = (event.target as HTMLInputElement).value;
+ this.state.usernameColors[colorIndex][key] = newValue;
+ this.setState({usernameColors: this.state.usernameColors});
+ };
+
+ render() {
+ const {previewChecked, usernameColors} = this.state;
+ usernameColors.sort((a, b) => a.id - b.id);
+
+ const editors = usernameColors.map(({color, id, username}) => {
+ const style: Record = {};
+ if (previewChecked === 'background') {
+ style.backgroundColor = color;
+ } else if (previewChecked === 'foreground') {
+ style.color = color;
+ }
+
+ const usernameHandler = (event: Event) => {
+ this.onInput(event, id, 'username');
+ };
+
+ const colorHandler = (event: Event) => {
+ this.onInput(event, id, 'color');
+ };
+
+ const removeHandler = () => {
+ this.removeColor(id);
+ };
+
+ return html`
+
+
+
+
+ Remove
+
+
+ `;
+ });
+
+ return html`
+ <${Setting} ...${this.props}>
+
+ Assign custom colors to usernames.
+
+ You can enter multiple usernames separated by a comma if you want them
+ to use the same color.
+
+
+
+
+ Add New Color
+
+
+
+ Toggle Preview
+
+
+
+ Save Changes
+
+
+
+ ${editors}
+ />
+ `;
+ }
+}
diff --git a/source/options/features.ts b/source/options/features.ts
index c577dc7..eba7d73 100644
--- a/source/options/features.ts
+++ b/source/options/features.ts
@@ -7,6 +7,7 @@ import {
JumpToNewCommentSetting,
MarkdownToolbarSetting,
UserLabelsSetting,
+ UsernameColorsSetting,
} from './components/exports.js';
/**
@@ -59,6 +60,12 @@ export const features = [
value: 'User Labels',
component: () => UserLabelsSetting,
},
+ {
+ index: 0,
+ key: 'usernameColors',
+ value: 'Username Colors',
+ component: () => UsernameColorsSetting,
+ },
{
index: 1,
key: 'debug',
diff --git a/source/scripts/exports.ts b/source/scripts/exports.ts
index 05381ac..bb2d568 100644
--- a/source/scripts/exports.ts
+++ b/source/scripts/exports.ts
@@ -5,3 +5,4 @@ export * from './hide-votes.js';
export * from './jump-to-new-comment.js';
export * from './markdown-toolbar.js';
export * from './user-labels.js';
+export * from './username-colors.js';
diff --git a/source/scripts/username-colors.ts b/source/scripts/username-colors.ts
new file mode 100644
index 0000000..20e4d89
--- /dev/null
+++ b/source/scripts/username-colors.ts
@@ -0,0 +1,45 @@
+import Settings from '../settings.js';
+import {log, querySelectorAll} from '../utilities/exports.js';
+
+export function runUsernameColorsFeature(settings: Settings) {
+ const count = usernameColors(settings);
+ log(`Username Colors: Applied ${count} colors.`);
+}
+
+function usernameColors(settings: Settings): number {
+ const usernameColors = new Map();
+ for (const {color, username: usernames} of settings.data.usernameColors) {
+ for (const username of usernames.split(',')) {
+ usernameColors.set(username.trim().toLowerCase(), color);
+ }
+ }
+
+ let count = 0;
+ const usernameElements = querySelectorAll(
+ '.link-user:not(.trx-username-colors)',
+ );
+
+ for (const element of usernameElements) {
+ if (element.classList.contains('trx-username-colors')) {
+ continue;
+ }
+
+ let target =
+ element.textContent?.replace(/@/g, '').trim().toLowerCase() ??
+ '';
+ if (settings.features.anonymizeUsernames) {
+ target = element.dataset.trxUsername?.toLowerCase() ?? target;
+ }
+
+ element.classList.add('trx-username-colors');
+ const color = usernameColors.get(target);
+ if (color === undefined) {
+ continue;
+ }
+
+ element.style.color = color;
+ count += 1;
+ }
+
+ return count;
+}
diff --git a/source/scss/_settings.scss b/source/scss/_settings.scss
index 8387b5b..8becd88 100644
--- a/source/scss/_settings.scss
+++ b/source/scss/_settings.scss
@@ -124,4 +124,28 @@
list-style: square;
padding: 8px 8px 8px 24px;
}
+
+ .username-colors-controls {
+ display: grid;
+ gap: 8px;
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ .username-colors-editor {
+ display: grid;
+ gap: 8px;
+ grid-template-columns: auto auto min-content;
+ margin-top: 8px;
+
+ input {
+ background-color: var(--background-primary);
+ border: 1px solid var(--blue);
+ color: var(--foreground);
+ padding: 8px;
+ }
+
+ .button {
+ min-width: 10rem;
+ }
+ }
}
diff --git a/source/settings.ts b/source/settings.ts
index a8e26c1..83dbb40 100644
--- a/source/settings.ts
+++ b/source/settings.ts
@@ -56,6 +56,7 @@ export default class Settings {
knownGroups: string[];
latestActiveFeatureTab: string;
userLabels: UserLabel[];
+ usernameColors: UsernameColor[];
};
public features: {
@@ -68,6 +69,7 @@ export default class Settings {
jumpToNewComment: boolean;
markdownToolbar: boolean;
userLabels: boolean;
+ usernameColors: boolean;
};
private constructor() {
@@ -118,6 +120,7 @@ export default class Settings {
],
latestActiveFeatureTab: 'debug',
userLabels: [],
+ usernameColors: [],
};
this.features = {
@@ -129,6 +132,7 @@ export default class Settings {
jumpToNewComment: true,
markdownToolbar: true,
userLabels: true,
+ usernameColors: false,
};
}
diff --git a/source/types.d.ts b/source/types.d.ts
index f1ae50d..a6b391f 100644
--- a/source/types.d.ts
+++ b/source/types.d.ts
@@ -27,4 +27,10 @@ declare global {
text: string;
username: string;
};
+
+ type UsernameColor = {
+ color: string;
+ id: number;
+ username: string;
+ };
}