diff --git a/src/background.ts b/src/background.ts index 75adc661..68432e87 100644 --- a/src/background.ts +++ b/src/background.ts @@ -60,14 +60,6 @@ browser.tabs.onUpdated.addListener(async (tabId, changeInfo) => { } }); -/** - * Creates IndexedDB - * https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API - */ -self.addEventListener('activate', async () => { - await WakaTimeCore.db(); -}); - browser.runtime.onMessage.addListener(async (request: { task: string }, sender) => { if (request.task === 'handleActivity') { if (!sender.tab?.id) return; diff --git a/src/components/MainList.test.tsx b/src/components/MainList.test.tsx index 11bb98a3..90db6e8c 100644 --- a/src/components/MainList.test.tsx +++ b/src/components/MainList.test.tsx @@ -27,6 +27,27 @@ describe('MainList', () => { expect(container).toMatchInlineSnapshot(`
+
+ +
+
+ +
+
+ +
@@ -39,17 +60,6 @@ describe('MainList', () => { /> Options - -
diff --git a/src/components/MainList.tsx b/src/components/MainList.tsx index 0b902f23..3a84e607 100644 --- a/src/components/MainList.tsx +++ b/src/components/MainList.tsx @@ -1,5 +1,6 @@ -import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; + +import React from 'react'; import { configLogout, setLoggingEnabled } from '../reducers/configReducer'; import { userLogout } from '../reducers/currentUser'; import { ReduxSelector } from '../types/store'; @@ -23,6 +24,9 @@ export default function MainList({ const user: User | undefined = useSelector( (selector: ReduxSelector) => selector.currentUser.user, ); + const isLoading: boolean = useSelector( + (selector: ReduxSelector) => selector.currentUser.pending ?? true, + ); const logoutUser = async (): Promise => { await browser.storage.sync.set({ apiKey: '' }); @@ -43,6 +47,12 @@ export default function MainList({ await changeExtensionState('trackingDisabled'); }; + const loading = isLoading ? ( +
+ +
+ ) : null; + return (
{user ? ( @@ -56,7 +66,9 @@ export default function MainList({
- ) : null} + ) : ( + loading + )} {loggingEnabled && user ? (
@@ -71,7 +83,9 @@ export default function MainList({

- ) : null} + ) : ( + loading + )} {!loggingEnabled && user ? (
@@ -86,21 +100,22 @@ export default function MainList({

- ) : null} + ) : ( + loading + )}
Options - {user ? ( + {isLoading ? null : user ? (
Logout
- ) : null} - {user ? null : ( + ) : ( { if (state.loading) return; setState((oldState) => ({ ...oldState, loading: true })); - if (state.apiUrl.endsWith('/')) { - state.apiUrl = state.apiUrl.slice(0, -1); - } await saveSettings({ allowList: state.allowList.filter((item) => !!item.trim()), apiKey: state.apiKey, diff --git a/src/config/config.test.ts b/src/config/config.test.ts index 07300757..821ed4c7 100644 --- a/src/config/config.test.ts +++ b/src/config/config.test.ts @@ -58,7 +58,7 @@ describe('wakatime config', () => { "chrome://", "about:", ], - "queueName": "heartbeatQueue", + "queueName": "heartbeatsQueue", "socialMediaSites": [ "facebook.com", "instagram.com", diff --git a/src/config/config.ts b/src/config/config.ts index 9a861c5e..6b4fe4eb 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -164,7 +164,7 @@ const config: Config = { nonTrackableSites: ['chrome://', 'about:'], - queueName: 'heartbeatQueue', + queueName: 'heartbeatsQueue', socialMediaSites: [ 'facebook.com', diff --git a/src/core/WakaTimeCore.ts b/src/core/WakaTimeCore.ts index 1a6218e7..7c4d66c5 100644 --- a/src/core/WakaTimeCore.ts +++ b/src/core/WakaTimeCore.ts @@ -1,4 +1,4 @@ -import { IDBPDatabase, openDB } from 'idb'; +import { openDB } from 'idb'; import browser, { Tabs } from 'webextension-polyfill'; /* eslint-disable no-fallthrough */ /* eslint-disable default-case */ @@ -9,6 +9,7 @@ import { changeExtensionStatus } from '../utils/changeExtensionStatus'; import getDomainFromUrl, { getDomain } from '../utils/getDomainFromUrl'; import { getOperatingSystem, IS_EDGE, IS_FIREFOX } from '../utils/operatingSystem'; import { getSettings, Settings } from '../utils/settings'; +import { getApiUrl } from '../utils/user'; import config, { ExtensionStatus } from '../config/config'; import { EntityType, Heartbeat, HeartbeatsBulkResponse } from '../types/heartbeats'; @@ -18,7 +19,6 @@ class WakaTimeCore { lastHeartbeat: Heartbeat | undefined; lastHeartbeatSentAt = 0; lastExtensionState: ExtensionStatus = 'allGood'; - _db: IDBPDatabase | undefined; constructor() { this.tabsWithDevtoolsOpen = []; } @@ -28,17 +28,13 @@ class WakaTimeCore { * a library that adds promises to IndexedDB and makes it easy to use */ async db() { - if (!this._db) { - const dbConnection = await openDB('wakatime', 1, { - upgrade(db) { - db.createObjectStore(config.queueName, { - keyPath: 'id', - }); - }, - }); - this._db = dbConnection; - } - return this._db; + return openDB('wakatime', 2, { + upgrade(db) { + db.createObjectStore(config.queueName, { + keyPath: 'id', + }); + }, + }); } shouldSendHeartbeat(heartbeat: Heartbeat): boolean { @@ -171,7 +167,6 @@ class WakaTimeCore { async sendHeartbeats(): Promise { const settings = await browser.storage.sync.get({ apiKey: config.apiKey, - apiUrl: config.apiUrl, heartbeatApiEndPoint: config.heartbeatApiEndPoint, hostname: '', }); @@ -180,16 +175,8 @@ class WakaTimeCore { return; } - const heartbeats = (await (await this.db()).getAll(config.queueName, undefined, 50)) as - | Heartbeat[] - | undefined; - if (!heartbeats || heartbeats.length === 0) return; - - await Promise.all( - heartbeats.map(async (heartbeat) => { - return (await this.db()).delete(config.queueName, heartbeat.id); - }), - ); + const heartbeats = await this.getHeartbeatsFromQueue(); + if (heartbeats.length === 0) return; const userAgent = await this.getUserAgent(); @@ -209,7 +196,8 @@ class WakaTimeCore { }; } - const url = `${settings.apiUrl}${settings.heartbeatApiEndPoint}?api_key=${settings.apiKey}`; + const apiUrl = await getApiUrl(); + const url = `${apiUrl}${settings.heartbeatApiEndPoint}?api_key=${settings.apiKey}`; const response = await fetch(url, request); if (response.status === 401) { await this.putHeartbeatsBackInQueue(heartbeats); @@ -228,7 +216,7 @@ class WakaTimeCore { if (resp[0].error) { await this.putHeartbeatsBackInQueue(heartbeats.filter((h, i) => i === respNumber)); console.error(resp[0].error); - } else if ((resp[1] === 201 || resp[1] === 202) && resp[0].data?.id) { + } else if (resp[1] === 201 || resp[1] === 202) { await changeExtensionStatus('allGood'); } else { if (resp[1] !== 400) { @@ -251,10 +239,38 @@ class WakaTimeCore { } } - async putHeartbeatsBackInQueue(heartbeats: Heartbeat[]): Promise { + async getHeartbeatsFromQueue(): Promise { + const tx = (await this.db()).transaction(config.queueName, 'readwrite'); + + const heartbeats = (await tx.store.getAll(undefined, 25)) as Heartbeat[] | undefined; + if (!heartbeats || heartbeats.length === 0) return []; + await Promise.all( - heartbeats.map(async (heartbeat) => (await this.db()).add(config.queueName, heartbeat)), + heartbeats.map(async (heartbeat) => { + return tx.store.delete(heartbeat.id); + }), ); + + await tx.done; + + return heartbeats; + } + + async putHeartbeatsBackInQueue(heartbeats: Heartbeat[]): Promise { + await Promise.all(heartbeats.map(async (heartbeat) => this.putHeartbeatBackInQueue(heartbeat))); + } + + async putHeartbeatBackInQueue(heartbeat: Heartbeat, tries = 0): Promise { + try { + await (await this.db()).add(config.queueName, heartbeat); + } catch (err: unknown) { + if (tries < 10) { + return await this.putHeartbeatBackInQueue(heartbeat, tries + 1); + } + console.error(err); + console.error(`Unable to add heartbeat back into queue: ${heartbeat.id}`); + console.error(JSON.stringify(heartbeat)); + } } async getUserAgent(): Promise { diff --git a/src/manifests/chrome.json b/src/manifests/chrome.json index 8c0305b5..8988b2f2 100644 --- a/src/manifests/chrome.json +++ b/src/manifests/chrome.json @@ -33,5 +33,5 @@ "page": "options.html" }, "permissions": ["alarms", "tabs", "storage", "activeTab"], - "version": "4.0.2" + "version": "4.0.6" } diff --git a/src/manifests/edge.json b/src/manifests/edge.json index 48b18184..ed5f09ae 100644 --- a/src/manifests/edge.json +++ b/src/manifests/edge.json @@ -33,5 +33,5 @@ "page": "options.html" }, "permissions": ["alarms", "tabs", "storage", "activeTab"], - "version": "4.0.2" + "version": "4.0.6" } diff --git a/src/manifests/firefox.json b/src/manifests/firefox.json index 689e654f..cb811535 100644 --- a/src/manifests/firefox.json +++ b/src/manifests/firefox.json @@ -39,5 +39,5 @@ "page": "options.html" }, "permissions": ["alarms", "tabs", "storage", "activeTab"], - "version": "4.0.2" + "version": "4.0.6" } diff --git a/src/reducers/currentUser.ts b/src/reducers/currentUser.ts index 9ae4e186..de3072d5 100644 --- a/src/reducers/currentUser.ts +++ b/src/reducers/currentUser.ts @@ -3,6 +3,7 @@ import axios, { AxiosResponse } from 'axios'; import browser from 'webextension-polyfill'; import config from '../config/config'; import { CurrentUser, User, UserPayload } from '../types/user'; +import { getApiUrl } from '../utils/user'; interface setUserAction { payload: User | undefined; @@ -16,11 +17,11 @@ export const fetchCurrentUser = createAsyncThunk( `[${name}]`, async (api_key = '') => { const items = await browser.storage.sync.get({ - apiUrl: config.apiUrl, currentUserApiEndPoint: config.currentUserApiEndPoint, }); + const apiUrl = await getApiUrl(); const userPayload: AxiosResponse = await axios.get( - `${items.apiUrl}${items.currentUserApiEndPoint}`, + `${apiUrl}${items.currentUserApiEndPoint}`, { params: { api_key }, }, @@ -35,10 +36,12 @@ const currentUser = createSlice({ extraReducers: (builder) => { builder.addCase(fetchCurrentUser.fulfilled, (state, { payload }) => { state.user = payload; + state.pending = false; }); builder.addCase(fetchCurrentUser.rejected, (state, { error }) => { state.user = undefined; state.error = error; + state.pending = false; }); }, initialState, diff --git a/src/utils/user.ts b/src/utils/user.ts index 277ae4da..0212cdb5 100644 --- a/src/utils/user.ts +++ b/src/utils/user.ts @@ -1,5 +1,6 @@ import { AnyAction, Dispatch } from '@reduxjs/toolkit'; import axios, { AxiosResponse } from 'axios'; +import browser from 'webextension-polyfill'; import moment from 'moment'; import config from '../config/config'; @@ -9,6 +10,20 @@ import { GrandTotal, Summaries } from '../types/summaries'; import { ApiKeyPayload, AxiosUserResponse, User } from '../types/user'; import changeExtensionState from './changeExtensionStatus'; +export const getApiUrl = async () => { + const settings = await browser.storage.sync.get({ + apiUrl: config.apiUrl, + }); + let apiUrl = (settings.apiUrl as string) || config.apiUrl; + const suffixes = ['/', '.bulk', '/users/current/heartbeats', '/heartbeats', '/heartbeat']; + for (const suffix of suffixes) { + if (apiUrl.endsWith(suffix)) { + apiUrl = apiUrl.slice(0, -suffix.length); + } + } + return apiUrl; +}; + /** * Checks if the user is logged in. * @@ -16,11 +31,11 @@ import changeExtensionState from './changeExtensionStatus'; */ const checkAuth = async (api_key = ''): Promise => { const items = await browser.storage.sync.get({ - apiUrl: config.apiUrl, currentUserApiEndPoint: config.currentUserApiEndPoint, }); + const apiUrl = await getApiUrl(); const userPayload: AxiosResponse = await axios.get( - `${items.apiUrl}${items.currentUserApiEndPoint}`, + `${apiUrl}${items.currentUserApiEndPoint}`, { params: { api_key } }, ); return userPayload.data.data; @@ -54,12 +69,11 @@ export const logUserIn = async (apiKey: string): Promise => { const fetchApiKey = async (): Promise => { try { const items = await browser.storage.sync.get({ - apiUrl: config.apiUrl, currentUserApiEndPoint: config.currentUserApiEndPoint, }); - + const apiUrl = await getApiUrl(); const apiKeyResponse: AxiosResponse = await axios.post( - `${items.apiUrl}${items.currentUserApiEndPoint}/get_api_key`, + `${apiUrl}${items.currentUserApiEndPoint}/get_api_key`, ); return apiKeyResponse.data.data.api_key; } catch (err: unknown) { @@ -69,13 +83,12 @@ const fetchApiKey = async (): Promise => { const getTotalTimeLoggedToday = async (api_key = ''): Promise => { const items = await browser.storage.sync.get({ - apiUrl: config.apiUrl, summariesApiEndPoint: config.summariesApiEndPoint, }); - + const apiUrl = await getApiUrl(); const today = moment().format('YYYY-MM-DD'); const summariesAxiosPayload: AxiosResponse = await axios.get( - `${items.apiUrl}${items.summariesApiEndPoint}`, + `${apiUrl}${items.summariesApiEndPoint}`, { params: { api_key,