diff --git a/account/src/middleware/setup-passport.ts b/account/src/middleware/setup-passport.ts index 79b0a68..4b05f94 100644 --- a/account/src/middleware/setup-passport.ts +++ b/account/src/middleware/setup-passport.ts @@ -150,8 +150,14 @@ function setupPassport(passport: PassportStatic) { cb(null, false); return; } - const newUser = await updateLingdocsUser(userId, { lastActive: getTimestamp() }); - cb(null, newUser); + try { + // skip if there's an update conflict + const newUser = await updateLingdocsUser(userId, { lastActive: getTimestamp() }); + cb(null, newUser); + } catch (e) { + console.error(e); + cb(null, user); + } } catch (err) { cb(err, null); } diff --git a/functions/package-lock.json b/functions/package-lock.json index 3a53205..de57a0f 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -200,9 +200,9 @@ } }, "@lingdocs/pashto-inflector": { - "version": "0.9.0", - "resolved": "https://npm.lingdocs.com/@lingdocs%2fpashto-inflector/-/pashto-inflector-0.9.0.tgz", - "integrity": "sha512-kiWVshiMGp/eT0vPTheujD9hJrUXAqLKG48a6iqX8RBURlv5hjk9t+CymKvLGYDO0Zrog0kAYcSo/PqkV0UKIw==", + "version": "0.9.2", + "resolved": "https://npm.lingdocs.com/@lingdocs%2fpashto-inflector/-/pashto-inflector-0.9.2.tgz", + "integrity": "sha512-9tmPPezEvPFR/tPkBuF/bj79dCAviFTGOmCVDbRCymXmv4gWWhH4lCueYOYhyWZ4vXcihMflVpRSmLtUDXmdjQ==", "requires": { "classnames": "^2.2.6", "pbf": "^3.2.1", @@ -1697,9 +1697,9 @@ } }, "protocol-buffers-schema": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.5.1.tgz", - "integrity": "sha512-YVCvdhxWNDP8/nJDyXLuM+UFsuPk4+1PB7WGPVDzm3HTHbzFLxQYeW2iZpS4mmnXrQJGBzt230t/BbEb7PrQaw==" + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.5.2.tgz", + "integrity": "sha512-LPzSaBYp/TcbuSlpGwqT5jR9kvJ3Zp5ic2N5c2ybx6XB/lSfEHq2D7ja8AgoxHoMD91wXFALJoXsvshKPuXyew==" }, "proxy-addr": { "version": "2.0.6", @@ -1745,9 +1745,9 @@ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, "rambda": { - "version": "6.8.2", - "resolved": "https://registry.npmjs.org/rambda/-/rambda-6.8.2.tgz", - "integrity": "sha512-fIVr6nuqfHfJTguthGsWF930DkNq/ENraeSpYBj1kYvO/ieacux7rj2NW27JwwFrllFJwXkLpGyFMz99EwCJ3Q==" + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/rambda/-/rambda-6.9.0.tgz", + "integrity": "sha512-yosVdGg1hNGkXPzqGiOYNEpXKjEOxzUCg2rB0l+NKdyCaSf4z+i5ojbN0IqDSezMMf71YEglI+ZUTgTffn5afw==" }, "range-parser": { "version": "1.2.1", diff --git a/functions/package.json b/functions/package.json index 443b24d..52f4e65 100644 --- a/functions/package.json +++ b/functions/package.json @@ -14,7 +14,7 @@ "main": "lib/functions/src/index.js", "dependencies": { "@google-cloud/storage": "^5.8.1", - "@lingdocs/pashto-inflector": "^0.9.0", + "@lingdocs/pashto-inflector": "^0.9.2", "@types/cors": "^2.8.10", "@types/google-spreadsheet": "^3.0.2", "cors": "^2.8.5", diff --git a/website/package.json b/website/package.json index 1b29bdd..d51fcf4 100644 --- a/website/package.json +++ b/website/package.json @@ -6,7 +6,7 @@ "private": true, "dependencies": { "@fortawesome/fontawesome-free": "^5.15.2", - "@lingdocs/pashto-inflector": "^0.9.0", + "@lingdocs/pashto-inflector": "^0.9.2", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", @@ -32,7 +32,6 @@ "react-bootstrap": "^1.5.1", "react-dom": "^17.0.1", "react-dropzone": "^11.3.2", - "react-firebaseui": "^4.1.0", "react-ga": "^3.3.0", "react-helmet": "^6.1.0", "react-image-crop": "^8.6.9", @@ -87,14 +86,14 @@ "@types/lokijs": "^1.5.3", "@types/mousetrap": "^1.6.8", "@types/papaparse": "^5.2.5", + "@types/passport-github2": "^1.2.5", + "@types/passport-google-oauth": "^1.0.42", + "@types/passport-twitter": "^1.0.37", "@types/pouchdb": "^6.4.0", "@types/react-canvas-draw": "^1.1.0", "@types/react-helmet": "^6.1.0", "@types/react-image-crop": "^8.1.2", "@types/react-router-dom": "^5.1.7", - "@types/passport-github2": "^1.2.5", - "@types/passport-google-oauth": "^1.0.42", - "@types/passport-twitter": "^1.0.37", "fake-indexeddb": "^3.1.2", "history": "4", "jest-fetch-mock": "^3.0.3", diff --git a/website/src/App.tsx b/website/src/App.tsx index 9018ad6..d1f7bdf 100644 --- a/website/src/App.tsx +++ b/website/src/App.tsx @@ -73,6 +73,7 @@ import "./App.css"; import classNames from "classnames"; import { getTextOptions } from "./lib/get-text-options"; import { getTextFromShareTarget } from "./lib/share-target"; +import { objIsEqual } from "./lib/misc-helpers"; // to allow Moustrap key combos even when input fields are in focus Mousetrap.prototype.stopCallback = function () { @@ -290,8 +291,10 @@ class App extends Component { ...userOnServer, userTextOptionsRecord, }; - this.setState({ user }); - saveUser(user); + if (!objIsEqual(prevUser, user)) { + this.setState({ user }); + saveUser(user); + } const textOptionsRecord: TextOptionsRecord = { lastModified: userTextOptionsRecord.lastModified, textOptions: { @@ -345,7 +348,7 @@ class App extends Component { this.setState({ options }); } } else { - this.setState({ options }); + !objIsEqual(this.state.options, options) && this.setState({ options }); } } @@ -514,7 +517,8 @@ class App extends Component { {wordlistEnabled(this.state.user) && diff --git a/website/src/lib/backend-calls.ts b/website/src/lib/backend-calls.ts index 38df3a2..6170b32 100644 --- a/website/src/lib/backend-calls.ts +++ b/website/src/lib/backend-calls.ts @@ -8,6 +8,9 @@ const baseUrl: Record = { functions: "https://functions.lingdocs.com/", }; +// @ts-ignore +const sampleAdminUser: AT.LingdocsUser = {"_id":"5e8dd381-950f-4641-922d-c63c6bf0f8e9","_rev":"1713-1a299e0d66da62fe4c8d059f0f068cb7","userId":"5e8dd381-950f-4641-922d-c63c6bf0f8e9","email":"clay@mailbox.org","admin":true,"emailVerified":true,"name":"Adam D","password":"$2a$10$JR4AHXXGbFP6sKQrqGO9UuMa3tdzNhbdqBvkjn2MVBuIOHkA/Xkf.","level":"editor","tests":[],"lastLogin":1629893763810,"lastActive":1630414108552,"userTextOptionsRecord":{"lastModified":1629983812750,"userTextOptions":{"spelling":"Afghan","diacritics":false,"dialect":"standard","phonetics":"lingdocs"}},"github":{"id":"71590811","nodeId":"MDQ6VXNlcjcxNTkwODEx","displayName":"LingDocs","username":"lingdocs","profileUrl":"https://github.com/lingdocs","photos":[{"value":"https://avatars.githubusercontent.com/u/71590811?v=4"}],"provider":"github","accessToken":"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"},"twitter":{"id":"1307635660451385344","username":"lingdocs","displayName":"LingDocs","photos":[{"value":"https://pbs.twimg.com/profile_images/1315656283928899584/EJIqjI-I_normal.jpg"}],"provider":"twitter","_accessLevel":"read","token":"1307635660451385344-MhpGAaJ6hFmfJtV97N6gy11FgbamVX","tokenSecret":"QjouJ33sWb2MpCkcUqqRxbfC9bqDyRaIODO9cejDvdusN"},"wordlistDbName":"userdb-35653864643338312d393530662d343634312d393232642d633633633662663066386539","couchDbPassword":"oevewi8iptdqyxax3v1x7t2ngi4uloir53g2y4659ada","google":{"id":"111202697753203366308","displayName":"Adam Dueck","name":{"familyName":"Dueck","givenName":"Adam"},"emails":[{"value":"lingdocsdev@gmail.com","verified":true}],"photos":[{"value":"https://lh3.googleusercontent.com/a/AATXAJx3cxPPpUoosqVzyGR-LcJ48EjpVcOBInkW21E=s96-c"}],"provider":"google","accessToken":"ya29.a0ARrdaM8BOOmaKxAPe_tKuVq0VgBncURPeEUUqV85RKksibmdx-DSY0IPOGXLs7UB8vh0yIpULsCQHiBKKg_l1DtuEmfwkN7F1h5YDyn2Zwd_ytVTE4W93YvdVGHVnX8rTdlEVcXLO2apwhJrBi3PuzTzxa6BTw"}}; + // FUNCTIONS CALLS - MUST BE RE-ROUTED THROUGH FIREBASE HOSTING IN ../../../firebase.json export async function publishDictionary(): Promise { return await myFetch("functions", "publishDictionary") as FT.PublishDictionaryResponse | FT.FunctionError; @@ -41,6 +44,9 @@ export async function signOut() { } export async function getUser(): Promise { + if (process.env.REACT_APP_ENV === "dev") { + return sampleAdminUser; + } try { const response = await myFetch("account", "user"); if ("user" in response) { diff --git a/website/src/lib/misc-helpers.ts b/website/src/lib/misc-helpers.ts new file mode 100644 index 0000000..96d5e2e --- /dev/null +++ b/website/src/lib/misc-helpers.ts @@ -0,0 +1,4 @@ +export function objIsEqual(obj1: any, obj2: any): boolean { + if (!obj1 || !obj2) return false; + return JSON.stringify(obj1) === JSON.stringify(obj2); +} \ No newline at end of file diff --git a/website/src/screens/Wordlist.tsx b/website/src/screens/Wordlist.tsx index 09d441e..2b1474d 100644 --- a/website/src/screens/Wordlist.tsx +++ b/website/src/screens/Wordlist.tsx @@ -44,7 +44,6 @@ import AudioPlayButton from "../components/AudioPlayButton"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime.js"; import hitBottom from "../lib/hitBottom"; -import { getTextOptions } from "../lib/get-text-options"; const cleanupIcon = "broom"; @@ -72,8 +71,9 @@ function amountOfWords(number: number): string { return `${number} word${number !== 1 ? "s" : ""}`; } -function Wordlist({ state, isolateEntry, optionsDispatch }: { - state: State, +function Wordlist({ options, wordlist, isolateEntry, optionsDispatch }: { + options: Options, + wordlist: WordlistWord[], isolateEntry: (ts: number) => void, optionsDispatch: (action: OptionsAction) => void, }) { @@ -89,17 +89,17 @@ function Wordlist({ state, isolateEntry, optionsDispatch }: { const [showingCleanup, setShowingCleanup] = useState(false); const [monthsBackToKeep, setMonthsBackToKeep] = useState(6); const [wordsToDelete, setWordsToDelete] = useState([]); - const [startedWithWordsToReview] = useState(forReview(state.wordlist).length !== 0); + const [startedWithWordsToReview] = useState(forReview(wordlist).length !== 0); useEffect(() => { window.addEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll); // eslint-disable-next-line }, []); - const toReview = forReview(state.wordlist); - const textOptions = getTextOptions(state); + const toReview = forReview(wordlist); + const textOptions = options.textOptionsRecord.textOptions; function handleScroll() { // TODO: DON'T HAVE ENDLESS PAGE INCREASING - if (hitBottom() && state.options.wordlistMode === "browse") { + if (hitBottom() && options.wordlistMode === "browse") { setPage(page => page + 1); } } @@ -119,7 +119,7 @@ function Wordlist({ state, isolateEntry, optionsDispatch }: { } function handleSearchValueChange(value: string) { setWordlistSearchValue(value); - const results = value ? searchWordlist(value, state.wordlist, textOptions) : []; + const results = value ? searchWordlist(value, wordlist, textOptions) : []; setFilteredWords(results); } async function handleGetWordlistCSV() { @@ -137,7 +137,7 @@ function Wordlist({ state, isolateEntry, optionsDispatch }: { } function handleShowCleanup() { setWordsToDelete( - calculateWordsToDelete(state.wordlist, monthsBackToKeep) + calculateWordsToDelete(wordlist, monthsBackToKeep) ); setShowingCleanup(true); } @@ -146,7 +146,7 @@ function Wordlist({ state, isolateEntry, optionsDispatch }: { setWordsToDelete([]); } function handleCleanup() { - const toDelete = calculateWordsToDelete(state.wordlist, monthsBackToKeep); + const toDelete = calculateWordsToDelete(wordlist, monthsBackToKeep); deleteWordFromWordlist(toDelete); setShowingCleanup(false); } @@ -219,13 +219,13 @@ function Wordlist({ state, isolateEntry, optionsDispatch }: {
handleWordClickReview(word._id)}>
- {state.options.wordlistReviewLanguage === "Pashto" + {options.wordlistReviewLanguage === "Pashto" ? {{ p: word.entry.p, f: word.entry.f }} : word.entry.e }
{beingQuizzed &&
- {state.options.wordlistReviewLanguage === "Pashto" + {options.wordlistReviewLanguage === "Pashto" ?
{word.entry.e}
: {{ p: word.entry.p, f: word.entry.f }} @@ -249,7 +249,7 @@ function Wordlist({ state, isolateEntry, optionsDispatch }: {

Wordlist

- {state.wordlist.length > 0 && + {wordlist.length > 0 &&
}
- {!state.wordlist.length ? + {!wordlist.length ? : <> @@ -280,16 +280,16 @@ function Wordlist({ state, isolateEntry, optionsDispatch }: { value: "review", }, ]} - value={state.options.wordlistMode || "browse"} + value={options.wordlistMode || "browse"} handleChange={(p) => { optionsDispatch({ type: "changeWordlistMode", payload: p as WordlistMode }); }} />
- {state.options.wordlistMode === "browse" + {options.wordlistMode === "browse" ?
- {paginate(wordlistSearchValue ? filteredWords : state.wordlist, page).map((word) => ( + {paginate(wordlistSearchValue ? filteredWords : wordlist, page).map((word) => ( ))}
@@ -298,7 +298,7 @@ function Wordlist({ state, isolateEntry, optionsDispatch }: {
{ optionsDispatch({ type: "changeWordlistReviewLanguage", payload: p as Language }); }} @@ -310,7 +310,7 @@ function Wordlist({ state, isolateEntry, optionsDispatch }: { ? (startedWithWordsToReview ?

All done review 🎉

: (() => { - const nextUp = nextUpForReview(state.wordlist); + const nextUp = nextUpForReview(wordlist); const { e, ...ps } = nextUp.entry; return
None to review
@@ -370,7 +370,7 @@ function Wordlist({ state, isolateEntry, optionsDispatch }: { Wordlist Cleanup -

You have {amountOfWords(state.wordlist.length)} in your wordlist.

+

You have {amountOfWords(wordlist.length)} in your wordlist.

Delete words older than:

{ const months = parseInt(p); setWordsToDelete( - calculateWordsToDelete(state.wordlist, months), + calculateWordsToDelete(wordlist, months), ); setMonthsBackToKeep(months); }} diff --git a/website/tsconfig.json b/website/tsconfig.json index a273b0c..2c59c15 100644 --- a/website/tsconfig.json +++ b/website/tsconfig.json @@ -10,6 +10,7 @@ "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, + "downlevelIteration": true, "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, diff --git a/website/yarn.lock b/website/yarn.lock index 15781a4..04e2e03 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -1483,10 +1483,10 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" -"@lingdocs/pashto-inflector@^0.9.0": - version "0.9.0" - resolved "https://npm.lingdocs.com/@lingdocs%2fpashto-inflector/-/pashto-inflector-0.9.0.tgz#d4aa0154f65b27b27cafdbbd1881d00aa1cc43cb" - integrity sha512-kiWVshiMGp/eT0vPTheujD9hJrUXAqLKG48a6iqX8RBURlv5hjk9t+CymKvLGYDO0Zrog0kAYcSo/PqkV0UKIw== +"@lingdocs/pashto-inflector@^0.9.2": + version "0.9.2" + resolved "https://npm.lingdocs.com/@lingdocs%2fpashto-inflector/-/pashto-inflector-0.9.2.tgz#7ef3b3344c3eb3e3d1db77142ef83f0ac7e8e230" + integrity sha512-9tmPPezEvPFR/tPkBuF/bj79dCAviFTGOmCVDbRCymXmv4gWWhH4lCueYOYhyWZ4vXcihMflVpRSmLtUDXmdjQ== dependencies: classnames "^2.2.6" pbf "^3.2.1" @@ -4781,11 +4781,6 @@ detect-port-alt@1.1.6: address "^1.0.1" debug "^2.6.0" -dialog-polyfill@^0.4.7: - version "0.4.10" - resolved "https://registry.yarnpkg.com/dialog-polyfill/-/dialog-polyfill-0.4.10.tgz#c4ea68a0deed4abb59a6a2a025c548b278cd532e" - integrity sha512-j5yGMkP8T00UFgyO+78OxiN5vC5dzRQF3BEio+LhNvDbyfxWBsi3sfPArDm54VloaJwy2hm3erEiDWqHRC8rzw== - diff-sequences@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" @@ -5837,14 +5832,6 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" -firebaseui@^4.7.1: - version "4.8.1" - resolved "https://registry.yarnpkg.com/firebaseui/-/firebaseui-4.8.1.tgz#29ccbc9dfd579c4453725f88e9cf81c8ea62c580" - integrity sha512-Qh8kfqGjMIiVJ2X8MUFsmlf43QFcVc8ungD+kw5T8ACuhQ68IAyUHExlItAfumrcLlqEgyo1MjH0O9fZZAMOKw== - dependencies: - dialog-polyfill "^0.4.7" - material-design-lite "^1.2.0" - flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -8201,11 +8188,6 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -material-design-lite@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/material-design-lite/-/material-design-lite-1.3.0.tgz#d004ce3fee99a1eeb74a78b8a325134a5f1171d3" - integrity sha1-0ATOP+6Zoe63Sni4oyUTSl8RcdM= - md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -10527,13 +10509,6 @@ react-fast-compare@^3.1.1: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== -react-firebaseui@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/react-firebaseui/-/react-firebaseui-4.2.0.tgz#13846381daefe31b90af98854f3e79e82d4f81ba" - integrity sha512-KRWfQn7iJMMAcB1zmMrpo5PJ7CBrLW6NXvdEJ/N+SFZUs7ANvC3g8LzgYwsBHUr1OR1MQtGosV7dyQSmYtKyOA== - dependencies: - firebaseui "^4.7.1" - react-ga@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-3.3.0.tgz#c91f407198adcb3b49e2bc5c12b3fe460039b3ca"