diff --git a/src/components/MainList.tsx b/src/components/MainList.tsx
index 2f447d8..92ba5e4 100644
--- a/src/components/MainList.tsx
+++ b/src/components/MainList.tsx
@@ -4,7 +4,7 @@ import { configLogout, setLoggingEnabled } from '../reducers/configReducer';
import { userLogout } from '../reducers/currentUser';
import { ReduxSelector } from '../types/store';
import { User } from '../types/user';
-import changeExtensionState from '../utils/changeExtensionState';
+import changeExtensionState from '../utils/changeExtensionStatus';
export interface MainListProps {
loggingEnabled: boolean;
diff --git a/src/components/Options.tsx b/src/components/Options.tsx
index 414c32e..15de2ca 100644
--- a/src/components/Options.tsx
+++ b/src/components/Options.tsx
@@ -20,8 +20,10 @@ export default function Options(): JSX.Element {
apiKey: '',
apiUrl: config.apiUrl,
denyList: [],
+ extensionStatus: 'allGood',
hostname: '',
loading: false,
+ loggingEnabled: true,
loggingStyle: config.loggingStyle,
loggingType: config.loggingType,
socialMediaSites: config.socialMediaSites,
@@ -54,7 +56,9 @@ export default function Options(): JSX.Element {
apiKey: state.apiKey,
apiUrl: state.apiUrl,
denyList: state.denyList,
+ extensionStatus: state.extensionStatus,
hostname: state.hostname,
+ loggingEnabled: state.loggingEnabled,
loggingStyle: state.loggingStyle,
loggingType: state.loggingType,
socialMediaSites: state.socialMediaSites,
diff --git a/src/components/WakaTime.tsx b/src/components/WakaTime.tsx
index 23010e7..ff30c79 100644
--- a/src/components/WakaTime.tsx
+++ b/src/components/WakaTime.tsx
@@ -10,7 +10,7 @@ import NavBar from './NavBar';
export default function WakaTime(): JSX.Element {
const dispatch = useDispatch();
- const [extensionState, setExtensionState] = useState('');
+ const [extensionStatus, setExtensionStatus] = useState('');
const {
apiKey: apiKeyFromRedux,
@@ -21,8 +21,8 @@ export default function WakaTime(): JSX.Element {
useEffect(() => {
const fetchData = async () => {
await fetchUserData(apiKeyFromRedux, dispatch);
- const items = await browser.storage.sync.get({ extensionState: '' });
- setExtensionState(items.extensionState as string);
+ const items = await browser.storage.sync.get({ extensionStatus: '' });
+ setExtensionStatus(items.extensionStatus as string);
};
void fetchData();
}, []);
@@ -32,7 +32,7 @@ export default function WakaTime(): JSX.Element {
return (
- {isApiKeyValid && extensionState === 'notSignedIn' && (
+ {isApiKeyValid && extensionStatus === 'notSignedIn' && (
{
+ const re = new RegExp(pattern.replace(/\*/g, '.*'));
+ return re.test(domain);
+ }) !== undefined
+ ) {
+ return false;
+ }
+ }
+
+ if (settings.loggingStyle === 'deny') {
+ return (
+ settings.denyList.find((pattern) => {
+ const re = new RegExp(pattern.replace(/\*/g, '.*'));
+ return re.test(url);
+ }) == undefined
+ );
+ }
+ return (
+ settings.allowList.find((pattern) => {
+ const re = new RegExp(pattern.replace(/\*/g, '.*'));
+ return re.test(url);
+ }) !== undefined
+ );
+ }
+
+ async handleActivity(url: string, heartbeat: Heartbeat) {
+ const settings = await getSettings();
+ if (!settings.loggingEnabled) {
+ await changeExtensionStatus('notLogging');
+ return;
+ }
if (!this.shouldSendHeartbeat(heartbeat)) return;
- if (items.loggingStyle == 'deny') {
- if (!contains(url, items.denyList as string)) {
- await this.sendHeartbeat(
- {
- branch: null,
- hostname: items.hostname as string,
- project,
- url,
- },
- apiKey,
- payload,
- );
- } else {
- await changeExtensionState('ignored');
- console.log(`${url} is on denyList.`);
+ if (!this.canSendHeartbeat(url, settings)) {
+ await changeExtensionStatus('ignored');
+ return;
+ }
+
+ if (this.db) {
+ // append heartbeat to queue
+ await this.db.add('cacheHeartbeats', heartbeat);
+
+ if (settings.extensionStatus !== 'notSignedIn') {
+ await changeExtensionStatus('allGood');
}
- } else if (items.loggingStyle == 'allow') {
- const heartbeat = this.getHeartbeat(url, items.allowList as string);
- if (heartbeat.url) {
- await this.sendHeartbeat(
- {
- ...heartbeat,
- branch: null,
- hostname: items.hostname as string,
- project: heartbeat.project ?? project,
- },
- apiKey,
- payload,
- );
- } else {
- await changeExtensionState('ignored');
- console.log(`${url} is not on allowList.`);
- }
- } else {
- throw Error(`Unknown logging styel: ${items.loggingStyle}`);
}
}
@@ -106,7 +117,7 @@ class WakaTimeCore {
async recordHeartbeat(html: string, payload: Record = {}): Promise {
const apiKey = await getApiKey();
if (!apiKey) {
- return changeExtensionState('notLogging');
+ return changeExtensionStatus('notLogging');
}
const items = await browser.storage.sync.get({
allowList: '',
@@ -118,14 +129,14 @@ class WakaTimeCore {
trackSocialMedia: config.trackSocialMedia,
});
if (items.loggingEnabled === true) {
- await changeExtensionState('allGood');
+ await changeExtensionStatus('allGood');
let newState = '';
// Detects we are running this code in the extension scope
if (browser.idle as browser.Idle.Static | undefined) {
newState = await browser.idle.queryState(config.detectionIntervalInSeconds);
if (newState !== 'active') {
- return changeExtensionState('notLogging');
+ return changeExtensionStatus('notLogging');
}
}
@@ -150,7 +161,7 @@ class WakaTimeCore {
const hostname = getDomain(url);
if (!items.trackSocialMedia) {
if ((items.socialMediaSites as string[]).includes(hostname)) {
- return changeExtensionState('ignored');
+ return changeExtensionStatus('ignored');
}
}
@@ -176,7 +187,7 @@ class WakaTimeCore {
payload,
);
} else {
- await changeExtensionState('ignored');
+ await changeExtensionStatus('ignored');
console.log(`${url} is on denyList.`);
}
} else if (items.loggingStyle == 'allow') {
@@ -193,7 +204,7 @@ class WakaTimeCore {
payload,
);
} else {
- await changeExtensionState('ignored');
+ await changeExtensionStatus('ignored');
console.log(`${url} is not on allowList.`);
}
} else {
@@ -374,7 +385,7 @@ class WakaTimeCore {
await this.db.add('cacheHeartbeats', payload);
}
- await changeExtensionState('notSignedIn');
+ await changeExtensionStatus('notSignedIn');
}
}
@@ -385,7 +396,7 @@ class WakaTimeCore {
async sendCachedHeartbeatsRequest(): Promise {
const apiKey = await getApiKey();
if (!apiKey) {
- return changeExtensionState('notLogging');
+ return changeExtensionStatus('notLogging');
}
if (this.db) {
diff --git a/src/utils/changeExtensionIcon.ts b/src/utils/changeExtensionIcon.ts
deleted file mode 100644
index 2821908..0000000
--- a/src/utils/changeExtensionIcon.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import browser from 'webextension-polyfill';
-import config from '../config/config';
-import { IS_FIREFOX } from '.';
-
-type ColorIconTypes = 'gray' | 'red' | 'white' | '';
-
-/**
- * It changes the extension icon color.
- */
-export default async function changeExtensionIcon(color?: ColorIconTypes): Promise {
- let path;
- if (color) {
- path = `./graphics/wakatime-logo-38-${color}.png`;
- } else {
- const { theme } = await browser.storage.sync.get({
- theme: config.theme,
- });
- path =
- theme === config.theme
- ? './graphics/wakatime-logo-38.png'
- : './graphics/wakatime-logo-38-white.png';
- }
-
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- if (IS_FIREFOX && browser.browserAction) {
- await browser.browserAction.setIcon({ path: path }); // Support for FF with manifest V2
- } else if ((browser.action as browser.Action.Static | undefined) !== undefined) {
- await browser.action.setIcon({ path: path }); // Support for Chrome with manifest V3
- }
-}
diff --git a/src/utils/changeExtensionState.ts b/src/utils/changeExtensionState.ts
deleted file mode 100644
index 19c4aaa..0000000
--- a/src/utils/changeExtensionState.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import browser from 'webextension-polyfill';
-import config, { ApiStates } from '../config/config';
-import changeExtensionIcon from './changeExtensionIcon';
-import changeExtensionTooltip from './changeExtensionTooltip';
-
-/**
- * Sets the current state of the extension.
- */
-export default async function changeExtensionState(state: ApiStates): Promise {
- switch (state) {
- case 'allGood':
- await changeExtensionIcon(config.colors.allGood);
- await changeExtensionTooltip(config.tooltips.allGood);
- break;
- case 'notLogging':
- await changeExtensionIcon(config.colors.notLogging);
- await changeExtensionTooltip(config.tooltips.notLogging);
- break;
- case 'notSignedIn':
- await changeExtensionIcon(config.colors.notSignedIn);
- await changeExtensionTooltip(config.tooltips.notSignedIn);
- break;
- case 'ignored':
- await changeExtensionIcon(config.colors.notLogging);
- await changeExtensionTooltip(config.tooltips.ignored);
- break;
- default:
- break;
- }
- await browser.storage.sync.set({ extensionState: state });
-}
diff --git a/src/utils/changeExtensionStatus.ts b/src/utils/changeExtensionStatus.ts
new file mode 100644
index 0000000..8ffd9e1
--- /dev/null
+++ b/src/utils/changeExtensionStatus.ts
@@ -0,0 +1,76 @@
+import browser from 'webextension-polyfill';
+import config, { ExtensionStatus } from '../config/config';
+import { IS_FIREFOX } from '.';
+
+type ColorIconTypes = 'gray' | 'red' | 'white' | '';
+
+/**
+ * Sets status of the extension.
+ */
+export async function changeExtensionStatus(status: ExtensionStatus): Promise {
+ switch (status) {
+ case 'allGood':
+ await changeExtensionIcon(config.colors.allGood);
+ await changeExtensionTooltip(config.tooltips.allGood);
+ break;
+ case 'notLogging':
+ await changeExtensionIcon(config.colors.notLogging);
+ await changeExtensionTooltip(config.tooltips.notLogging);
+ break;
+ case 'notSignedIn':
+ await changeExtensionIcon(config.colors.notSignedIn);
+ await changeExtensionTooltip(config.tooltips.notSignedIn);
+ break;
+ case 'ignored':
+ await changeExtensionIcon(config.colors.notLogging);
+ await changeExtensionTooltip(config.tooltips.ignored);
+ break;
+ default:
+ break;
+ }
+ await browser.storage.sync.set({ extensionStatus: status });
+}
+
+/**
+ * Changes the extension icon color.
+ */
+export async function changeExtensionIcon(color?: ColorIconTypes): Promise {
+ let path;
+ if (color) {
+ path = `./graphics/wakatime-logo-38-${color}.png`;
+ } else {
+ const { theme } = await browser.storage.sync.get({
+ theme: config.theme,
+ });
+ path =
+ theme === config.theme
+ ? './graphics/wakatime-logo-38.png'
+ : './graphics/wakatime-logo-38-white.png';
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (IS_FIREFOX && browser.browserAction) {
+ await browser.browserAction.setIcon({ path: path }); // Support for FF with manifest V2
+ } else if ((browser.action as browser.Action.Static | undefined) !== undefined) {
+ await browser.action.setIcon({ path: path }); // Support for Chrome with manifest V3
+ }
+}
+
+/**
+ * It changes the extension title
+ *
+ */
+export default async function changeExtensionTooltip(text: string): Promise {
+ if (text === '') {
+ text = config.name;
+ } else {
+ text = `${config.name} - ${text}`;
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (IS_FIREFOX && browser.browserAction) {
+ await browser.browserAction.setTitle({ title: text }); // Support for FF with manifest V2
+ } else if ((browser.action as browser.Action.Static | undefined) !== undefined) {
+ await browser.action.setTitle({ title: text }); // Support for Chrome with manifest V3
+ }
+}
diff --git a/src/utils/changeExtensionTooltip.ts b/src/utils/changeExtensionTooltip.ts
deleted file mode 100644
index e9e3187..0000000
--- a/src/utils/changeExtensionTooltip.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import browser from 'webextension-polyfill';
-import config from '../config/config';
-import { IS_FIREFOX } from '.';
-
-/**
- * It changes the extension title
- *
- */
-export default async function changeExtensionTooltip(text: string): Promise {
- if (text === '') {
- text = config.name;
- } else {
- text = `${config.name} - ${text}`;
- }
-
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- if (IS_FIREFOX && browser.browserAction) {
- await browser.browserAction.setTitle({ title: text }); // Support for FF with manifest V2
- } else if ((browser.action as browser.Action.Static | undefined) !== undefined) {
- await browser.action.setTitle({ title: text }); // Support for Chrome with manifest V3
- }
-}
diff --git a/src/utils/settings.ts b/src/utils/settings.ts
index 95a0ea2..ccef201 100644
--- a/src/utils/settings.ts
+++ b/src/utils/settings.ts
@@ -1,12 +1,14 @@
import browser from 'webextension-polyfill';
-import config, { LoggingStyle, LoggingType, Theme } from '../config/config';
+import config, { ExtensionStatus, LoggingStyle, LoggingType, Theme } from '../config/config';
export interface Settings {
allowList: string[];
apiKey: string;
apiUrl: string;
denyList: string[];
+ extensionStatus: ExtensionStatus;
hostname: string;
+ loggingEnabled: boolean;
loggingStyle: LoggingStyle;
loggingType: LoggingType;
socialMediaSites: string[];
@@ -57,7 +59,9 @@ export const getSettings = async (): Promise => {
apiKey: settings.apiKey,
apiUrl: settings.apiUrl,
denyList: settings.denyList,
+ extensionStatus: settings.extensionStatus,
hostname: settings.hostname,
+ loggingEnabled: settings.loggingEnabled,
loggingStyle: settings.loggingStyle,
loggingType: settings.loggingType,
socialMediaSites: settings.socialMediaSites,
diff --git a/src/utils/sites.ts b/src/utils/sites.ts
new file mode 100644
index 0000000..d383015
--- /dev/null
+++ b/src/utils/sites.ts
@@ -0,0 +1,158 @@
+import { parse } from 'node-html-parser';
+
+type ProjectNameExtractor = (url: string, html: string) => string | null;
+
+const GitHub: ProjectNameExtractor = (url: string, html: string): string | null => {
+ const { hostname } = new URL(url);
+ const match = url.match(/(?<=github\.(?:com|dev)\/[^/]+\/)([^/?#]+)/);
+
+ if (match) {
+ if (hostname.endsWith('.com')) {
+ const root = parse(html);
+ const repoName = root
+ .querySelector('meta[name=octolytics-dimension-repository_nwo]')
+ ?.getAttribute('content');
+ if (!repoName || repoName.split('/')[1] !== match[0]) {
+ return null;
+ }
+ }
+ return match[0];
+ }
+
+ return null;
+};
+
+const GitLab: ProjectNameExtractor = (url: string, html: string): string | null => {
+ const match = url.match(/(?<=gitlab\.com\/[^/]+\/)([^/?#]+)/);
+
+ if (match) {
+ const root = parse(html);
+ const repoName = root.querySelector('body')?.getAttribute('data-project-full-path');
+ if (!repoName || repoName.split('/')[1] !== match[0]) {
+ return null;
+ }
+ return match[0];
+ }
+
+ return null;
+};
+
+const BitBucket: ProjectNameExtractor = (url: string, html: string): string | null => {
+ const match = url.match(/(?<=bitbucket\.org\/[^/]+\/)([^/?#]+)/);
+
+ if (match) {
+ const root = parse(html);
+ // this regex extracts the project name from the title
+ // eg. title: jhondoe / my-test-repo ā Bitbucket
+ const match2 = root.querySelector('title')?.textContent.match(/(?<=\/\s)([^/\s]+)(?=\sā)/);
+ if (match2 && match2[0] === match[0]) {
+ return match[0];
+ }
+ }
+
+ return null;
+};
+
+const TravisCI: ProjectNameExtractor = (url: string, html: string): string | null => {
+ const match = url.match(/(?<=app\.travis-ci\.com\/[^/]+\/[^/]+\/)([^/?#]+)/);
+
+ if (match) {
+ const root = parse(html);
+ const projectName = root.querySelector('#ember737')?.textContent;
+ if (projectName === match[0]) {
+ return match[0];
+ }
+ }
+
+ return null;
+};
+
+const CircleCI: ProjectNameExtractor = (url: string, html: string): string | null => {
+ const projectPageMatch = url.match(
+ /(?<=app\.circleci\.com\/projects\/[^/]+\/[^/]+\/[^/]+\/)([^/?#]+)/,
+ );
+
+ if (projectPageMatch) {
+ const root = parse(html);
+ const seconndBreadcrumbLabel = root.querySelector(
+ '#__next > div:nth-child(2) > div > div > main > div > header > div:nth-child(1) > ol > li:nth-child(2) > div > div > span',
+ )?.textContent;
+ const seconndBreadcrumbValue = root.querySelector(
+ '#__next > div:nth-child(2) > div > div > main > div > header > div:nth-child(1) > ol > li:nth-child(2) > div > span',
+ )?.textContent;
+ if (seconndBreadcrumbLabel === 'Project' && seconndBreadcrumbValue === projectPageMatch[0]) {
+ return projectPageMatch[0];
+ }
+ }
+
+ const settingsPageMatch = url.match(
+ /(?<=app\.circleci\.com\/settings\/project\/[^/]+\/[^/]+\/)([^/?#]+)/,
+ );
+ if (settingsPageMatch) {
+ const root = parse(html);
+ const pageTitle = root.querySelector(
+ '#__next > div > div:nth-child(1) > header > div > div:nth-child(2) > h1',
+ )?.textContent;
+ const pageSubtitle = root.querySelector(
+ '#__next > div > div:nth-child(1) > header > div > div:nth-child(2) > div',
+ )?.textContent;
+ if (pageTitle === 'Project Settings' && pageSubtitle === settingsPageMatch[0]) {
+ return settingsPageMatch[0];
+ }
+ }
+
+ return null;
+};
+
+const Vercel: ProjectNameExtractor = (url: string, html: string): string | null => {
+ const match = url.match(/(?<=vercel\.com\/[^/]+\/)([^/?#]+)/);
+
+ if (match) {
+ const root = parse(html);
+ // this regex extracts the project name from the title
+ // eg. title: test-website - Overview ā Vercel
+ const match2 = root.querySelector('title')?.textContent.match(/^[^\s]+(?=\s-\s)/);
+ if (match2 && match2[0] === match[0]) {
+ return match[0];
+ }
+ }
+
+ return null;
+};
+
+const ProjectNameExtractors: ProjectNameExtractor[] = [
+ GitHub,
+ GitLab,
+ BitBucket,
+ TravisCI,
+ CircleCI,
+ Vercel,
+];
+
+export const getHeartbeatFromPage = (): string | null => {
+ for (const projectNameExtractor of ProjectNameExtractors) {
+ const projectName = projectNameExtractor(url, html);
+ if (projectName) {
+ return projectName;
+ }
+ }
+ return null;
+};
+
+const CODE_REVIEW_URL_REG_LIST = [/github.com\/[^/]+\/[^/]+\/pull\/\d+\/files/];
+
+export const isCodeReviewing = (url: string): boolean => {
+ for (const reg of CODE_REVIEW_URL_REG_LIST) {
+ if (url.match(reg)) {
+ return true;
+ }
+ }
+ return false;
+};
+
+export const getHtmlContentByTabId = async (tabId: number): Promise => {
+ const response = (await browser.tabs.sendMessage(tabId, { message: 'get_html' })) as {
+ html: string;
+ };
+ return response.html;
+};
diff --git a/src/utils/user.ts b/src/utils/user.ts
index c95c681..29857aa 100644
--- a/src/utils/user.ts
+++ b/src/utils/user.ts
@@ -7,7 +7,7 @@ import { setApiKey, setLoggingEnabled, setTotalTimeLoggedToday } from '../reduce
import { setUser } from '../reducers/currentUser';
import { GrandTotal, Summaries } from '../types/summaries';
import { ApiKeyPayload, AxiosUserResponse, User } from '../types/user';
-import changeExtensionState from './changeExtensionState';
+import changeExtensionState from './changeExtensionStatus';
/**
* Checks if the user is logged in.
diff --git a/tests/utils/changeExtensionState.spec.ts b/tests/utils/changeExtensionState.spec.ts
index 17b3340..d829143 100644
--- a/tests/utils/changeExtensionState.spec.ts
+++ b/tests/utils/changeExtensionState.spec.ts
@@ -1,5 +1,5 @@
import chai from 'chai';
-import changeExtensionState from '../../src/utils/changeExtensionState';
+import changeExtensionState from '../../src/utils/changeExtensionStatus';
const expect = chai.expect;