import debounce from "debounce"; import {Component, render} from "preact"; import { type UserLabelsData, createValueUserLabel, saveUserLabels, } from "../../storage/exports.js"; import { createElementFromString, isColorBright, log, querySelectorAll, themeColors, } from "../../utilities/exports.js"; type Props = { anonymizeUsernamesEnabled: boolean; userLabels: UserLabelsData; }; type State = { color: string; selectedColor: string; hidden: boolean; id: number | undefined; priority: number; target: HTMLElement | undefined; text: string; username: string; }; export class UserLabelsFeature extends Component { constructor(props: Props) { super(props); const selectedColor = window .getComputedStyle(document.body) .getPropertyValue(themeColors[1].value) .trim(); this.state = { color: selectedColor, hidden: true, id: undefined, text: "", priority: 0, selectedColor, target: undefined, username: "", }; const count = this.addLabelsToUsernames(querySelectorAll(".link-user")); log(`User Labels: Initialized for ${count} user links.`); } hide = () => { this.setState({hidden: true}); }; addLabelsToUsernames = (elements: HTMLElement[], onlyID?: number): number => { const {userLabels} = this.props; const inTopicListing = document.querySelector(".topic-listing") !== null; // Sort the labels by priority or alphabetically, so 2 labels with the same // priority will be sorted alphabetically. const sortedLabels = userLabels.sort((a, b): number => { if (inTopicListing) { // If we're in the topic listing sort with highest priority first. if (a.value.priority !== b.value.priority) { return b.value.priority - a.value.priority; } } else if (a.value.priority !== b.value.priority) { // If we're not in the topic listing, sort with lowest priority first. // We will add elements backwards, so the first label will be // behind all the other labels. return a.value.priority - b.value.priority; } return b.value.text.localeCompare(a.value.text); }); for (const element of elements) { let username: string = element.textContent!.replace(/@/g, ""); if (this.props.anonymizeUsernamesEnabled) { username = element.dataset.trxUsername ?? username; } const userLabels = sortedLabels.filter( ({value}) => value.username.toLowerCase() === username.toLowerCase() && (onlyID === undefined ? true : value.id === onlyID), ); const addLabel = ( { this.addLabelHandler(event, username); }} > [+] ); if (!inTopicListing && onlyID === undefined) { const addLabelPlaceholder = document.createElement("span"); element.after(addLabelPlaceholder); render(addLabel, element.parentElement!, addLabelPlaceholder); } if (userLabels.length === 0 && onlyID === undefined) { if ( inTopicListing && (element.nextElementSibling === null || !element.nextElementSibling.className.includes("trx-user-label")) ) { const addLabelPlaceholder = document.createElement("span"); element.after(addLabelPlaceholder); render(addLabel, element.parentElement!, addLabelPlaceholder); } continue; } for (const userLabel of userLabels) { const bright = isColorBright(userLabel.value.color.trim()) ? "trx-bright" : ""; const label = createElementFromString(` ${userLabel.value.text} `); label.addEventListener("click", (event: MouseEvent) => { this.editLabelHandler(event, userLabel.value.id); }); element.after(label); label.setAttribute( "style", `background-color: ${userLabel.value.color};`, ); // If we're in the topic listing, stop after adding 1 label. if (inTopicListing) { break; } } } return elements.length; }; addLabelHandler = (event: MouseEvent, username: string) => { event.preventDefault(); const target = event.target as HTMLElement; if (this.state.target === target && !this.state.hidden) { this.hide(); } else { const selectedColor = window .getComputedStyle(document.body) .getPropertyValue(themeColors[1].value) .trim(); this.setState({ hidden: false, target, username, color: selectedColor, id: undefined, text: "", priority: 0, selectedColor, }); } }; editLabelHandler = (event: MouseEvent, id: number) => { event.preventDefault(); const target = event.target as HTMLElement; if (this.state.target === target && !this.state.hidden) { this.hide(); } else { const label = this.props.userLabels.find(({value}) => value.id === id); if (label === undefined) { log( "User Labels: Tried to edit label with ID that could not be found.", true, ); return; } this.setState({ hidden: false, target, color: label.value.color, id: label.value.id, priority: label.value.priority, text: label.value.text, username: label.value.username, }); } }; colorChange = (event: Event) => { const color = (event.target as HTMLInputElement).value.toLowerCase(); // If the color was changed through the preset values, also change the // selected color state. if ((event.target as HTMLElement).tagName === "SELECT") { this.setState({color, selectedColor: color}); } else { this.setState({color}); } }; labelChange = (event: Event) => { this.setState({text: (event.target as HTMLInputElement).value}); }; priorityChange = (event: Event) => { this.setState({ priority: Number((event.target as HTMLInputElement).value), }); }; save = async (event: MouseEvent) => { event.preventDefault(); const {color, id, text, priority, username} = this.state; if (color === "" || username === "") { log("Cannot save user label without all values present."); return; } const {userLabels} = this.props; // If no ID is present then save a new label otherwise edit the existing one. if (id === undefined) { let newId = 1; if (userLabels.length > 0) { newId = userLabels.sort((a, b) => b.value.id - a.value.id)[0].value.id + 1; } userLabels.push( await createValueUserLabel({ color, id: newId, priority, text, username, }), ); this.addLabelsToUsernames(querySelectorAll(".link-user"), newId); } else { const index = userLabels.findIndex(({value}) => value.id === id); await userLabels.splice(index, 1)[0].remove(); userLabels.push( await createValueUserLabel({ id, color, priority, text, username, }), ); const elements = querySelectorAll(`[data-trx-label-id="${id}"]`); const bright = isColorBright(color); for (const element of elements) { element.textContent = text; element.setAttribute("style", `background-color: ${color};`); if (bright) { element.classList.add("trx-bright"); } else { element.classList.remove("trx-bright"); } } } await saveUserLabels(userLabels); this.props.userLabels = userLabels; this.hide(); }; remove = async (event: MouseEvent) => { event.preventDefault(); const {id} = this.state; if (id === undefined) { log("User Labels: Tried remove label when ID was undefined."); return; } const {userLabels} = this.props; const index = userLabels.findIndex(({value}) => value.id === id); if (index === undefined) { log( `User Labels: Tried to remove label with ID ${id} that could not be found.`, true, ); return; } for (const value of querySelectorAll(`[data-trx-label-id="${id}"]`)) { value.remove(); } for (const userLabel of userLabels.splice(index, 1)) { await userLabel.remove(); } this.props.userLabels = userLabels; this.hide(); }; render() { const bodyStyle = window.getComputedStyle(document.body); const themeSelectOptions = themeColors.map(({name, value}) => ( )); const bright = isColorBright(this.state.color) ? "trx-bright" : ""; const hidden = this.state.hidden ? "trx-hidden" : ""; const {color, text: label, priority, selectedColor, username} = this.state; let top = 0; let left = 0; const target = this.state.target; if (target !== undefined) { const bounds = target.getBoundingClientRect(); top = bounds.y + bounds.height + 4 + window.scrollY; left = bounds.x + window.scrollX; } const position = `left: ${left}px; top: ${top}px;`; const previewStyle = `background-color: ${color}`; return (

{label}

); } }