summaryrefslogtreecommitdiff
path: root/source/helpers
diff options
context:
space:
mode:
authorGravatar Florent <cheap.glitch@gmail.com> 2021-04-08 10:45:57 +0200
committerGravatar GitHub <noreply@github.com> 2021-04-08 15:45:57 +0700
commit0ac8f8ceec6127788a233cbb41c8ee7c459ffaa7 (patch)
treef850d33f3ea19bb2effee27a9456746b34031b6e /source/helpers
parent7882e292ee0eec130b542e017d918ab7f06555be (diff)
downloadrefined-github-0ac8f8ceec6127788a233cbb41c8ee7c459ffaa7.tar.gz
refined-github-0ac8f8ceec6127788a233cbb41c8ee7c459ffaa7.tar.zst
refined-github-0ac8f8ceec6127788a233cbb41c8ee7c459ffaa7.zip
Help users find a feature with an interactive binary search (#4119)
Co-authored-by: Federico <me@fregante.com>
Diffstat (limited to 'source/helpers')
-rw-r--r--source/helpers/bisect.tsx105
1 files changed, 105 insertions, 0 deletions
diff --git a/source/helpers/bisect.tsx b/source/helpers/bisect.tsx
new file mode 100644
index 00000000..7668e688
--- /dev/null
+++ b/source/helpers/bisect.tsx
@@ -0,0 +1,105 @@
+import React from 'dom-chef';
+import cache from 'webext-storage-cache';
+import select from 'select-dom';
+
+import features from '../features';
+import pluralize from './pluralize';
+
+// Split current list of features in half and create an options-like object to be applied on load
+// Bisecting 4 features: enable 2
+// Bisecting 3 features: enable 1
+// Bisecting 2 features: enable 1
+// Bisecting 1 feature: enable 0 // This is the last step, if the user says Yes, it's not caused by a JS feature
+const getMiddleStep = (list: any[]): number => Math.floor(list.length / 2);
+
+async function onChoiceButtonClick({currentTarget: button}: React.MouseEvent<HTMLButtonElement>): Promise<void> {
+ const answer = button.value;
+ const bisectedFeatures = (await cache.get<FeatureID[]>('bisect'))!;
+
+ if (bisectedFeatures.length > 1) {
+ await cache.set('bisect', answer === 'yes' ?
+ bisectedFeatures.slice(0, getMiddleStep(bisectedFeatures)) :
+ bisectedFeatures.slice(getMiddleStep(bisectedFeatures))
+ );
+
+ button.parentElement!.replaceWith(<div className="btn" aria-disabled="true">Reloading…</div>);
+ location.reload();
+ return;
+ }
+
+ // Last step, no JS feature was enabled
+ if (answer === 'yes') {
+ createMessageBox('No features were enabled on this page. Try disabling Refined GitHub to see if it belongs to it at all.');
+ } else {
+ const feature = (
+ <a href={'https://github.com/sindresorhus/refined-github/blob/main/source/features/' + bisectedFeatures[0] + '.tsx'}>
+ <code>{bisectedFeatures[0]}</code>
+ </a>
+ );
+
+ createMessageBox(<>The change or issue is caused by {feature}.</>);
+ }
+
+ await cache.delete('bisect');
+}
+
+async function onEndButtonClick(): Promise<void> {
+ await cache.delete('bisect');
+ location.reload();
+}
+
+function createMessageBox(message: Element | string, extraButtons?: Element): void {
+ select('#rgh-bisect-dialog')?.remove();
+ document.body.append(
+ <div id="rgh-bisect-dialog" className="Box p-3">
+ <p>{message}</p>
+ <div className="d-flex flex-justify-between">
+ <button type="button" className="btn" onClick={onEndButtonClick}>Exit</button>
+ {extraButtons}
+ </div>
+ </div>
+ );
+}
+
+export default async function bisectFeatures(): Promise<Record<string, boolean> | void> {
+ // `bisect` stores the list of features to be split in half
+ const bisectedFeatures = await cache.get<FeatureID[]>('bisect');
+ if (!bisectedFeatures) {
+ return;
+ }
+
+ console.log(`Bisecting ${bisectedFeatures.length} features:\n${bisectedFeatures.join('\n')}`);
+
+ const steps = Math.ceil(Math.log2(Math.max(bisectedFeatures.length))) + 1;
+ createMessageBox(
+ `Do you see the change or issue? (${pluralize(steps, 'last step', '$$ steps remaining')})`,
+ <div>
+ <button type="button" className="btn btn-danger mr-2" value="no" aria-disabled="true" onClick={onChoiceButtonClick}>No</button>
+ <button type="button" className="btn btn-primary" value="yes" aria-disabled="true" onClick={onChoiceButtonClick}>Yes</button>
+ </div>
+ );
+
+ // Enable "Yes"/"No" buttons once the page is done loading
+ window.addEventListener('load', () => {
+ for (const button of select.all('#rgh-bisect-dialog [aria-disabled]')) {
+ button.removeAttribute('aria-disabled');
+ }
+ });
+
+ // Hide message when the process is done elsewhere
+ window.addEventListener('visibilitychange', async () => {
+ if (!await cache.get<FeatureID[]>('bisect')) {
+ createMessageBox('Process completed in another tab');
+ }
+ });
+
+ const half = getMiddleStep(bisectedFeatures);
+ const temporaryOptions: Record<string, boolean> = {};
+ for (const feature of features.list) {
+ const index = bisectedFeatures.indexOf(feature);
+ temporaryOptions[`feature:${feature}`] = index > -1 && index < half;
+ }
+
+ console.log(temporaryOptions);
+ return temporaryOptions;
+}