From ee289d562c75276d805e9045056d65d923a2623b Mon Sep 17 00:00:00 2001 From: Bauke Date: Fri, 25 Feb 2022 01:06:24 +0100 Subject: [PATCH] Add the Username Colors feature (#6). --- source/content-scripts.ts | 7 + .../options/components/anonymize-usernames.ts | 3 +- source/options/components/exports.ts | 1 + source/options/components/username-colors.ts | 153 ++++++++++++++++++ source/options/features.ts | 7 + source/scripts/exports.ts | 1 + source/scripts/username-colors.ts | 45 ++++++ source/scss/_settings.scss | 24 +++ source/settings.ts | 4 + source/types.d.ts | 6 + 10 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 source/options/components/username-colors.ts create mode 100644 source/scripts/username-colors.ts 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` +
+ + + +
+ `; + }); + + 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. +

+ +
+ + + + + +
+ + ${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; + }; }