Skip to content

Commit 5972102

Browse files
committed
Merge branch 'master' into feat/platform-observers
2 parents 7468ab8 + d054fc4 commit 5972102

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1701
-1480
lines changed

.eslintrc.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
"no-implied-eval": "error",
3333
"no-return-await": "error",
3434
"no-sequences": "error",
35+
"no-unused-vars": ["error", {
36+
"destructuredArrayIgnorePattern": "^_"
37+
}],
3538
"no-var": "error",
3639
// these comment markers fail CI (notes to self in code that need to be
3740
// resolved before a PR can be merged)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Handling for writing Toolbox settings into extension storage.
2+
//
3+
// The entire settings object is stored as a single JSON value, so in order to
4+
// update a single setting, we need to read the entire object, update the
5+
// setting in our copy of the object, and then write back the entire object.
6+
// However, because reading and writing extension storage are asynchronous
7+
// operations, doing this naively would result in conflicts if two things wanted
8+
// to change settings at the same time - two consumers could both read the state
9+
// before the other has time to commit an update, leading to one update being
10+
// "forgotten" by the other. We get around this by introducing a mutex for
11+
// writing to setting storage. Content scripts request writes by messaging the
12+
// background page, so writes from all sources are governed by a single mutex.
13+
14+
import {Mutex} from 'async-mutex';
15+
import browser from 'webextension-polyfill';
16+
17+
// these are safe to import because they're implemented purely in terms of
18+
// `browser.storage`, with no reliance on state held in the content script
19+
import {messageHandlers} from '../messageHandling';
20+
21+
/** Mutex governing writes to the `tbsettings` key of `browser.storage.local` */
22+
const settingsWriteMutex = new Mutex();
23+
24+
/** Reads settings from storage */
25+
const getSettings = async () => (await browser.storage.local.get('tbsettings')).tbsettings ?? {};
26+
27+
/** Writes a full settings object into storage */
28+
const writeSettings = newSettings => browser.storage.local.set({tbsettings: newSettings});
29+
30+
// Updates the value(s) of one or more settings.
31+
//
32+
// NOTE: Messages are serialized to JSON, so it's not possible for a key to be
33+
// present in a message object but have its value be `undefined` (which is
34+
// how we typically represent the operation of removing a setting's key
35+
// from storage). I don't want to use `null` for that instead, because
36+
// there's lots of places in the storage code that freak out if they see
37+
// `null`s as values and I don't want to cause issues with whatever's
38+
// going on there. So we do it the slightly janky way: two message fields,
39+
// one object mapping keys to new values, and one array of deleted keys.
40+
messageHandlers.set('tb-update-settings', async ({
41+
updatedSettings,
42+
deletedSettings,
43+
}) => {
44+
await settingsWriteMutex.runExclusive(async () => {
45+
const settings = await getSettings();
46+
for (const [key, value] of Object.entries(updatedSettings ?? {})) {
47+
if (value == null) {
48+
continue;
49+
}
50+
settings[key] = value;
51+
}
52+
for (const key of deletedSettings ?? []) {
53+
delete settings[key];
54+
}
55+
await writeSettings(settings);
56+
});
57+
});
58+
59+
// Overwrites the entire settings object.
60+
messageHandlers.set('tb-overwrite-all-settings', async ({newSettings}) => {
61+
await settingsWriteMutex.runExclusive(async () => {
62+
await writeSettings(newSettings);
63+
});
64+
});

extension/data/background/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ import './handlers/globalmessage.js';
33
import './handlers/modqueue.js';
44
import './handlers/notifications.js';
55
import './handlers/reload.js';
6+
import './handlers/settings.js';
67
import './handlers/url_changed.js';
78
import './handlers/webrequest.js';

extension/data/components/controls/Icon.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {icons} from '../../tbconstants';
1+
import {icons} from '../../util/icons';
22
import {classes} from '../../util/ui_interop';
33
import css from './Icon.module.css';
44

extension/data/hooks.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {useEffect, useState} from 'react';
2-
import {getSettingAsync} from './tbstorage';
2+
import {useSelector} from 'react-redux';
3+
import {RootState} from './store';
34

45
/** Hook to get the return value of a promise. */
56
export const useFetched = <T>(promise: Promise<T>) => {
@@ -23,5 +24,15 @@ export const useFetched = <T>(promise: Promise<T>) => {
2324

2425
/** Hook to get a Toolbox setting. */
2526
export const useSetting = (moduleName: string, settingName: string, defaultValue: any) => {
26-
return useFetched(getSettingAsync(moduleName, settingName, defaultValue));
27+
const savedValue = useSelector((state: RootState) => state.settings.values[`Toolbox.${moduleName}.${settingName}`]);
28+
29+
// Return the given default value if the setting doesn't have a value (i.e.
30+
// is `undefined`) *or* if the setting's value is `null` (mirroring the old
31+
// implementation of `getSetting` from the old `tbstorage.js`, which says
32+
// that `null` is never a valid value for any setting)
33+
if (savedValue == null) {
34+
return defaultValue;
35+
}
36+
37+
return savedValue;
2738
};

extension/data/init.ts

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,15 @@ import * as TBApi from './tbapi';
3232
import * as TBCore from './tbcore.js';
3333
import {delay} from './tbhelpers.js';
3434
import TBListener from './tblistener.js';
35-
import TBLog from './tblog';
3635
import TBModule from './tbmodule.jsx';
37-
import * as TBStorage from './tbstorage.js';
3836

3937
import AppRoot from './AppRoot';
4038
import {initializeObserver} from './frontends';
39+
import {getCache, setCache} from './util/cache';
4140
import {documentInteractive} from './util/dom';
41+
import createLogger from './util/logging';
4242
import {isUserLoggedInQuick} from './util/platform';
43+
import {getSettingAsync, setSettingAsync, updateSettings} from './util/settings';
4344
import {reactRenderer} from './util/ui_interop';
4445

4546
import Achievements from './modules/achievements.js';
@@ -162,12 +163,12 @@ async function checkLoadConditions (tries = 3) {
162163

163164
// Write a setting and read back its value, if this fails something is wrong
164165
let echoValue = Math.random();
165-
let echoResult: number;
166166
try {
167-
echoResult = await TBStorage.setSettingAsync('Utils', 'echoTest', echoValue);
167+
await setSettingAsync('Utils', 'echoTest', echoValue);
168168
} catch (error) {
169169
throw new Error('Failed to write to settings', {cause: error});
170170
}
171+
const echoResult = await getSettingAsync('Utils', 'echoTest');
171172
if (echoResult !== echoValue) {
172173
throw new Error(`Settings read/write inconsistent: expected ${echoValue}, received ${echoResult}`);
173174
}
@@ -185,11 +186,11 @@ async function doSettingsUpdates () {
185186
const currentUser = await TBApi.getCurrentUser();
186187
let lastVersion = await TBCore.getLastVersion();
187188

188-
const cacheName = await TBStorage.getCache('Utils', 'cacheName', '');
189+
const cacheName = await getCache('Utils', 'cacheName', '');
189190

190191
// Update cache if we're logged in as someone else
191192
if (cacheName !== currentUser) {
192-
await TBStorage.setCache(SETTINGS_NAME, 'cacheName', currentUser);
193+
await setCache(SETTINGS_NAME, 'cacheName', currentUser);
193194

194195
// Force refresh of timed cache
195196
browser.runtime.sendMessage({
@@ -200,46 +201,46 @@ async function doSettingsUpdates () {
200201
// Extra checks on old faults
201202
if (typeof lastVersion !== 'number') {
202203
lastVersion = parseInt(lastVersion);
203-
await TBStorage.setSettingAsync(SETTINGS_NAME, 'lastVersion', lastVersion);
204+
await setSettingAsync(SETTINGS_NAME, 'lastVersion', lastVersion);
204205
}
205206

206-
let shortLength = await TBStorage.getSettingAsync(SETTINGS_NAME, 'shortLength', 15);
207-
let longLength = await TBStorage.getSettingAsync(SETTINGS_NAME, 'longLength', 45);
207+
let shortLength = await getSettingAsync(SETTINGS_NAME, 'shortLength', 15);
208+
let longLength = await getSettingAsync(SETTINGS_NAME, 'longLength', 45);
208209

209210
if (typeof shortLength !== 'number') {
210211
shortLength = parseInt(shortLength);
211-
await TBStorage.setSettingAsync(SETTINGS_NAME, 'shortLength', shortLength);
212+
await setSettingAsync(SETTINGS_NAME, 'shortLength', shortLength);
212213
}
213214

214215
if (typeof longLength !== 'number') {
215216
longLength = parseInt(longLength);
216-
await TBStorage.setSettingAsync(SETTINGS_NAME, 'longLength', longLength);
217+
await setSettingAsync(SETTINGS_NAME, 'longLength', longLength);
217218
}
218219

219220
// First run changes for all releases.
220221
if (TBCore.shortVersion > lastVersion) {
221222
// These need to happen for every version change
222-
await TBStorage.setSettingAsync(SETTINGS_NAME, 'lastVersion', TBCore.shortVersion); // set last version to this version.
223+
await setSettingAsync(SETTINGS_NAME, 'lastVersion', TBCore.shortVersion); // set last version to this version.
223224
TBCore.getToolboxDevs(); // always repopulate tb devs for each version change
224225

225226
// This should be a per-release section of stuff we want to change in each update. Like setting/converting data/etc. It should always be removed before the next release.
226227

227228
// Start: version changes.
228229
// reportsThreshold should be 0 by default
229230
if (lastVersion < 50101) {
230-
await TBStorage.setSettingAsync('QueueTools', 'reportsThreshold', 0);
231+
await setSettingAsync('QueueTools', 'reportsThreshold', 0);
231232
}
232233

233234
// Clean up removed settings - it doesn't really matter what version
234235
// we're coming from, we just want to make sure these removed settings
235236
// aren't cluttering up storage
236-
await Promise.all([
237+
const keysToDelete = [
237238
// Some new modmail settings were removed in 5.7.0
238-
TBStorage.setSettingAsync('NewModMail', 'searchhelp', undefined),
239-
TBStorage.setSettingAsync('NewModMail', 'checkForNewMessages', undefined),
239+
'Toolbox.NewModMail.searchhelp',
240+
'Toolbox.NewModMail.checkForNewMessages',
240241

241242
// Beta mode setting removed in favor of dedicated beta builds #917
242-
TBStorage.setSettingAsync(SETTINGS_NAME, 'betaMode', undefined),
243+
'Toolbox.Utils.betaMode',
243244

244245
// (old) modmail pro removed in v7, RIP old modmail
245246
...[
@@ -265,26 +266,27 @@ async function doSettingsUpdates () {
265266
'entryProcessRate',
266267
'chunkProcessSize',
267268
'twoPhaseProcessing',
268-
].map(setting => TBStorage.setSettingAsync('ModMail', setting, undefined)),
269+
].map(setting => `Toolbox.ModMail.${setting}`),
269270

270271
// new reddit is dead, long live shreddit i guess. the setting to
271272
// skip the new reddit lightbox when viewing comments no longer
272273
// applies to anything, remove it
273-
TBStorage.setSettingAsync('Comments', 'commentsAsFullPage', undefined),
274-
]);
274+
'Toolbox.Comments.commentsAsFullPage',
275+
];
276+
await updateSettings(Object.fromEntries(keysToDelete.map(key => [key, undefined])));
275277

276278
// End: version changes.
277279

278280
// This is a super extra check to make sure the wiki page for settings export really is private.
279-
const settingSubEnabled = await TBStorage.getSettingAsync('Utils', 'settingSub', '');
281+
const settingSubEnabled = await getSettingAsync('Utils', 'settingSub', '');
280282
if (settingSubEnabled) {
281283
TBCore.setWikiPrivate('tbsettings', settingSubEnabled, false);
282284
}
283285

284286
// These two should be left for every new release. If there is a new beta feature people want, it should be opt-in, not left to old settings.
285-
// TBStorage.setSetting('Notifier', 'lastSeenModmail', now); // don't spam 100 new mod mails on first install.
286-
// TBStorage.setSetting('Notifier', 'modmailCount', 0);
287-
await TBStorage.setSettingAsync(SETTINGS_NAME, 'debugMode', false);
287+
// await setSettingAsync('Notifier', 'lastSeenModmail', now); // don't spam 100 new mod mails on first install.
288+
// await setSettingAsync('Notifier', 'modmailCount', 0);
289+
await setSettingAsync(SETTINGS_NAME, 'debugMode', false);
288290
}
289291
}
290292

@@ -295,13 +297,13 @@ async function doSettingsUpdates () {
295297
}
296298

297299
// Create a logger
298-
const logger = TBLog('Init');
300+
const log = createLogger('Init');
299301

300302
// Ensure that other conditions are met, and return early if not
301303
try {
302304
await checkLoadConditions();
303305
} catch (error) {
304-
logger.error('Load condition not met:', (error as Error).message);
306+
log.error('Load condition not met:', error);
305307
return;
306308
}
307309

@@ -391,7 +393,7 @@ async function doSettingsUpdates () {
391393
OldReddit,
392394
]
393395
) {
394-
logger.debug('Registering module', m);
396+
log.debug('Registering module', m);
395397
TBModule.register_module(m);
396398
}
397399

extension/data/modules/achievements.js

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import $ from 'jquery';
33
import * as TBCore from '../tbcore.js';
44
import * as TBHelpers from '../tbhelpers.js';
55
import {Module} from '../tbmodule.jsx';
6-
import {getSettingAsync} from '../tbstorage.js';
6+
import createLogger from '../util/logging.ts';
7+
import {getSettingAsync} from '../util/settings.ts';
8+
9+
const log = createLogger('Achievements');
710

811
const self = new Module({
912
name: 'Achievements',
@@ -65,12 +68,12 @@ function Manager () {
6568
const title = titles[i];
6669
const maxValue = maxValues[i];
6770

68-
self.log('Registering Achievement');
71+
log.debug('Registering Achievement');
6972
if (debugMode) {
70-
self.log(` name=${title}`);
73+
log.debug(` name=${title}`);
7174
} // spoilers
72-
self.log(` maxValue=${maxValue}`);
73-
self.log(` saveIndex=${saveIndex}`);
75+
log.debug(` maxValue=${maxValue}`);
76+
log.debug(` saveIndex=${saveIndex}`);
7477

7578
achievementsBlock.push({
7679
title,
@@ -89,19 +92,19 @@ function Manager () {
8992
if (value === undefined) {
9093
value = 1;
9194
}
92-
self.log(`Unlocking achievement block: index=${saveIndex}, value=${value}`);
95+
log.debug(`Unlocking achievement block: index=${saveIndex}, value=${value}`);
9396

9497
const old = saves[saveIndex];
95-
self.log(` Old value: ${saves[saveIndex]}`);
98+
log.debug(` Old value: ${saves[saveIndex]}`);
9699
saves[saveIndex] += value;
97-
self.log(` New value: ${saves[saveIndex]}`);
100+
log.debug(` New value: ${saves[saveIndex]}`);
98101

99102
const achievementsBlock = achievements[saveIndex];
100103
let achievement;
101104
for (let index = 0; index < achievementsBlock.length; index++) {
102-
self.log(` Checking achievement ${index}`);
105+
log.debug(` Checking achievement ${index}`);
103106
achievement = achievementsBlock[index];
104-
self.log(` Comparing to max value: ${achievement.maxValue}`);
107+
log.debug(` Comparing to max value: ${achievement.maxValue}`);
105108
if (saves[saveIndex] >= achievement.maxValue && old < achievement.maxValue) {
106109
let title = achievement.title;
107110

@@ -111,10 +114,10 @@ function Manager () {
111114
try {
112115
title = $(achievement.title).text() || achievement.title;
113116
} catch (e) {
114-
self.log(`error: ${e}`);
117+
log.debug(`error: ${e}`);
115118
}
116119

117-
self.log(`${title} Unlocked!`);
120+
log.debug(`${title} Unlocked!`);
118121
TBCore.notification(
119122
'Mod achievement unlocked!',
120123
title,
@@ -206,7 +209,7 @@ function init ({lastSeen}) {
206209
const $body = $('body');
207210

208211
// Achievement definitions
209-
self.log('Registering achievements');
212+
log.debug('Registering achievements');
210213

211214
// Random awesome
212215
self.manager.register(
@@ -216,7 +219,7 @@ function init ({lastSeen}) {
216219
const awesome = 7;
217220
const chanceOfBeingAwesome = TBHelpers.getRandomNumber(10000);
218221

219-
self.log(`You rolled a: ${chanceOfBeingAwesome}`);
222+
log.debug(`You rolled a: ${chanceOfBeingAwesome}`);
220223
if (awesome === chanceOfBeingAwesome) {
221224
self.manager.unlock(saveIndex);
222225
}
@@ -233,10 +236,10 @@ function init ({lastSeen}) {
233236
const now = TBHelpers.getTime();
234237
const timeSince = now - lastSeen;
235238
const daysSince = TBHelpers.millisecondsToDays(timeSince);
236-
self.log(`daysSince: ${daysSince}`);
239+
log.debug(`daysSince: ${daysSince}`);
237240

238241
if (daysSince >= 7) {
239-
// self.log("you've got an award!");
242+
// log.debug("you've got an award!");
240243
self.manager.unlock(saveIndex);
241244
}
242245

@@ -278,7 +281,7 @@ function init ({lastSeen}) {
278281
}
279282
// TODO: wait for 'yes' click.
280283
// $body.on('click', '.yes', function(){
281-
// self.log('yes clicked');
284+
// log.debug('yes clicked');
282285
// });
283286
});
284287
});

0 commit comments

Comments
 (0)