finish refactor
This commit is contained in:
@@ -10,7 +10,7 @@ browser.alarms.onAlarm.addListener(async (alarm) => {
|
|||||||
// Checks if the user is online and if there are cached heartbeats requests,
|
// Checks if the user is online and if there are cached heartbeats requests,
|
||||||
// if so then procedd to send these payload to wakatime api
|
// if so then procedd to send these payload to wakatime api
|
||||||
if (navigator.onLine) {
|
if (navigator.onLine) {
|
||||||
await WakaTimeCore.sendCachedHeartbeatsRequest();
|
await WakaTimeCore.sendHeartbeats();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -30,7 +30,6 @@ browser.tabs.onActivated.addListener(async (activeInfo) => {
|
|||||||
*/
|
*/
|
||||||
browser.windows.onFocusChanged.addListener(async (windowId) => {
|
browser.windows.onFocusChanged.addListener(async (windowId) => {
|
||||||
if (windowId != browser.windows.WINDOW_ID_NONE) {
|
if (windowId != browser.windows.WINDOW_ID_NONE) {
|
||||||
console.log('recording a heartbeat - active window changed');
|
|
||||||
const tabs: browser.Tabs.Tab[] = await browser.tabs.query({
|
const tabs: browser.Tabs.Tab[] = await browser.tabs.query({
|
||||||
active: true,
|
active: true,
|
||||||
currentWindow: true,
|
currentWindow: true,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import config, { SuccessOrFailType } from '../config/config';
|
import config, { SuccessOrFailType } from '../config/config';
|
||||||
import { IS_CHROME } from '../utils';
|
|
||||||
import apiKeyInvalid from '../utils/apiKey';
|
import apiKeyInvalid from '../utils/apiKey';
|
||||||
|
import { IS_CHROME } from '../utils/operatingSystem';
|
||||||
import { getSettings, saveSettings, Settings } from '../utils/settings';
|
import { getSettings, saveSettings, Settings } from '../utils/settings';
|
||||||
import { logUserIn } from '../utils/user';
|
import { logUserIn } from '../utils/user';
|
||||||
import SitesList from './SitesList';
|
import SitesList from './SitesList';
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import browser, { Tabs } from 'webextension-polyfill';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { v4 as uuid4 } from 'uuid';
|
import { v4 as uuid4 } from 'uuid';
|
||||||
import { OptionalHeartbeat } from '../types/sites';
|
import { OptionalHeartbeat } from '../types/sites';
|
||||||
import { getOperatingSystem, IS_EDGE, IS_FIREFOX } from '../utils';
|
|
||||||
import { changeExtensionStatus } from '../utils/changeExtensionStatus';
|
import { changeExtensionStatus } from '../utils/changeExtensionStatus';
|
||||||
import getDomainFromUrl, { getDomain } from '../utils/getDomainFromUrl';
|
import getDomainFromUrl, { getDomain } from '../utils/getDomainFromUrl';
|
||||||
|
import { getOperatingSystem, IS_EDGE, IS_FIREFOX } from '../utils/operatingSystem';
|
||||||
import { getSettings, Settings } from '../utils/settings';
|
import { getSettings, Settings } from '../utils/settings';
|
||||||
|
|
||||||
import config, { ExtensionStatus } from '../config/config';
|
import config, { ExtensionStatus } from '../config/config';
|
||||||
@@ -149,6 +149,7 @@ class WakaTimeCore {
|
|||||||
entity: heartbeat?.entity ?? entity,
|
entity: heartbeat?.entity ?? entity,
|
||||||
id: uuid4(),
|
id: uuid4(),
|
||||||
language: heartbeat?.language,
|
language: heartbeat?.language,
|
||||||
|
plugin: heartbeat?.plugin,
|
||||||
project: heartbeat?.project ?? '<<LAST_PROJECT>>',
|
project: heartbeat?.project ?? '<<LAST_PROJECT>>',
|
||||||
time: this.getCurrentTime(),
|
time: this.getCurrentTime(),
|
||||||
type: heartbeat?.entityType ?? (settings.loggingType as EntityType),
|
type: heartbeat?.entityType ?? (settings.loggingType as EntityType),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export enum Category {
|
|||||||
browsing = 'browsing',
|
browsing = 'browsing',
|
||||||
code_reviewing = 'code reviewing',
|
code_reviewing = 'code reviewing',
|
||||||
coding = 'coding',
|
coding = 'coding',
|
||||||
|
communicating = 'communicating',
|
||||||
debugging = 'debugging',
|
debugging = 'debugging',
|
||||||
designing = 'designing',
|
designing = 'designing',
|
||||||
meeting = 'meeting',
|
meeting = 'meeting',
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface OptionalHeartbeat {
|
|||||||
entity?: string;
|
entity?: string;
|
||||||
entityType?: EntityType;
|
entityType?: EntityType;
|
||||||
language?: string | null;
|
language?: string | null;
|
||||||
|
plugin?: string;
|
||||||
project?: string | null;
|
project?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import browser from 'webextension-polyfill';
|
import browser from 'webextension-polyfill';
|
||||||
import config, { ExtensionStatus } from '../config/config';
|
import config, { ExtensionStatus } from '../config/config';
|
||||||
import { IS_FIREFOX } from '.';
|
import { IS_FIREFOX } from './operatingSystem';
|
||||||
|
|
||||||
type ColorIconTypes = 'gray' | 'red' | 'white' | '';
|
type ColorIconTypes = 'gray' | 'red' | 'white' | '';
|
||||||
|
|
||||||
|
|||||||
@@ -1,232 +0,0 @@
|
|||||||
import { HeartbeatParser, KnownSite, SiteParser, StackExchangeSite } from '../types/sites';
|
|
||||||
import { STACKEXCHANGE_SITES } from './stackexchange-sites';
|
|
||||||
|
|
||||||
const GitHub: HeartbeatParser = (url: string) => {
|
|
||||||
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: HeartbeatParser = (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: HeartbeatParser = (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: HeartbeatParser = (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: HeartbeatParser = (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: HeartbeatParser = (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 StackOverflow: HeartbeatParser = (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 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<string> => {
|
|
||||||
const response = (await browser.tabs.sendMessage(tabId, { message: 'get_html' })) as {
|
|
||||||
html: string;
|
|
||||||
};
|
|
||||||
return response.html;
|
|
||||||
};
|
|
||||||
|
|
||||||
const _normalizeUrl = (url?: string | null) => {
|
|
||||||
if (!url) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
if (url.startsWith('http://')) {
|
|
||||||
url = url.substring('http://'.length);
|
|
||||||
}
|
|
||||||
if (url.startsWith('https://')) {
|
|
||||||
url = url.substring('https://'.length);
|
|
||||||
}
|
|
||||||
if (url.startsWith('www.')) {
|
|
||||||
url = url.substring('www.'.length);
|
|
||||||
}
|
|
||||||
if (url.endsWith('/')) {
|
|
||||||
url = url.substring(0, url.length - 1);
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
};
|
|
||||||
|
|
||||||
const stackExchangeDomains = (STACKEXCHANGE_SITES as StackExchangeSite[]).map((site) => {
|
|
||||||
return _normalizeUrl(site.site_url);
|
|
||||||
});
|
|
||||||
|
|
||||||
const SITES: Record<KnownSite, SiteParser> = {
|
|
||||||
bitbucket: {
|
|
||||||
parser: BitBucket,
|
|
||||||
urls: [/^https?:\/\/(.+\.)?bitbucket.org\//],
|
|
||||||
},
|
|
||||||
circleci: {
|
|
||||||
parser: CircleCI,
|
|
||||||
urls: [/^https?:\/\/(.+\.)?circleci.com\//],
|
|
||||||
},
|
|
||||||
github: {
|
|
||||||
parser: GitHub,
|
|
||||||
urls: [
|
|
||||||
/^https?:\/\/(.+\.)?github.com\//,
|
|
||||||
/^https?:\/\/(.+\.)?github.dev\//,
|
|
||||||
/^https?:\/\/(.+\.)?github.blog\//,
|
|
||||||
/^https?:\/\/(.+\.)?github.io\//,
|
|
||||||
/^https?:\/\/(.+\.)?github.community\//,
|
|
||||||
// /^https?:\/\/(.+\.)?ghcr.io\//,
|
|
||||||
// /^https?:\/\/(.+\.)?githubapp.com\//,
|
|
||||||
// /^https?:\/\/(.+\.)?githubassets.com\//,
|
|
||||||
// /^https?:\/\/(.+\.)?githubusercontent.com\//,
|
|
||||||
// /^https?:\/\/(.+\.)?githubnext.com\//,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
gitlab: {
|
|
||||||
parser: GitLab,
|
|
||||||
urls: [/^https?:\/\/(.+\.)?gitlab.com\//],
|
|
||||||
},
|
|
||||||
stackoverflow: {
|
|
||||||
parser: StackOverflow,
|
|
||||||
urls: stackExchangeDomains,
|
|
||||||
},
|
|
||||||
travisci: {
|
|
||||||
parser: TravisCI,
|
|
||||||
urls: [/^https?:\/\/(.+\.)?travis-ci.com\//],
|
|
||||||
},
|
|
||||||
vercel: {
|
|
||||||
parser: Vercel,
|
|
||||||
urls: [/^https?:\/\/(.+\.)?vercel.com\//],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const match = (url: string, pattern: RegExp | string): boolean => {
|
|
||||||
if (typeof pattern === 'string') {
|
|
||||||
return _normalizeUrl(url).startsWith(_normalizeUrl(pattern));
|
|
||||||
}
|
|
||||||
return pattern.test(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSite = (url: string): SiteParser | undefined => {
|
|
||||||
return Object.values(SITES).find((site) => {
|
|
||||||
return site.urls.some((re) => match(url, re));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -2,17 +2,6 @@ export const IS_EDGE = navigator.userAgent.includes('Edg');
|
|||||||
export const IS_FIREFOX = navigator.userAgent.includes('Firefox');
|
export const IS_FIREFOX = navigator.userAgent.includes('Firefox');
|
||||||
export const IS_CHROME = IS_EDGE === false && IS_FIREFOX === false;
|
export const IS_CHROME = IS_EDGE === false && IS_FIREFOX === false;
|
||||||
|
|
||||||
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 getOperatingSystem = (): Promise<string> => {
|
export const getOperatingSystem = (): Promise<string> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
chrome.runtime.getPlatformInfo(function (info) {
|
chrome.runtime.getPlatformInfo(function (info) {
|
||||||
320
src/utils/sites.ts
Normal file
320
src/utils/sites.ts
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
import { Category, EntityType } from '../types/heartbeats';
|
||||||
|
import {
|
||||||
|
HeartbeatParser,
|
||||||
|
KnownSite,
|
||||||
|
OptionalHeartbeat,
|
||||||
|
SiteParser,
|
||||||
|
StackExchangeSite,
|
||||||
|
} from '../types/sites';
|
||||||
|
import { STACKEXCHANGE_SITES } from './stackexchange-sites';
|
||||||
|
|
||||||
|
const GitHub: HeartbeatParser = (url: string) => {
|
||||||
|
const { hostname } = new URL(url);
|
||||||
|
const match = url.match(/(?<=github\.(?:com|dev)\/[^/]+\/)([^/?#]+)/);
|
||||||
|
if (!match) return;
|
||||||
|
|
||||||
|
if (hostname.endsWith('.dev')) {
|
||||||
|
return {
|
||||||
|
project: match[0],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = document.getElementsByTagName('body').item(0);
|
||||||
|
if (!body) return;
|
||||||
|
|
||||||
|
const repo = body
|
||||||
|
.querySelector('meta[name=octolytics-dimension-repository_nwo]')
|
||||||
|
?.getAttribute('content');
|
||||||
|
if (repo?.split('/')[1] !== match[0]) return;
|
||||||
|
|
||||||
|
// TODO: parse language associated with this repo from the DOM
|
||||||
|
// TODO: parse branch associated with the PR url from the DOM
|
||||||
|
|
||||||
|
const re = new RegExp(/github.com\/[^/]+\/[^/]+\/pull\/\d+\/files/);
|
||||||
|
const category: Category | undefined = re.test(url) ? Category.code_reviewing : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
category,
|
||||||
|
project: match[0],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const GitLab: HeartbeatParser = (url: string) => {
|
||||||
|
const match = url.match(/(?<=gitlab\.com\/[^/]+\/)([^/?#]+)/);
|
||||||
|
if (!match) return;
|
||||||
|
|
||||||
|
const repoName = document.querySelector('body')?.getAttribute('data-project-full-path');
|
||||||
|
if (!repoName || repoName.split('/')[1] !== match[0]) return;
|
||||||
|
|
||||||
|
return {
|
||||||
|
project: match[0],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const BitBucket: HeartbeatParser = (url: string) => {
|
||||||
|
const match = url.match(/(?<=bitbucket\.org\/[^/]+\/)([^/?#]+)/);
|
||||||
|
if (!match) return;
|
||||||
|
|
||||||
|
// this regex extracts the project name from the title
|
||||||
|
// eg. title: jhondoe / my-test-repo — Bitbucket
|
||||||
|
const match2 = document.querySelector('title')?.textContent?.match(/(?<=\/\s)([^/\s]+)(?=\s—)/);
|
||||||
|
if (!match2 || match2[0] !== match[0]) return;
|
||||||
|
|
||||||
|
return {
|
||||||
|
project: match[0],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const TravisCI: HeartbeatParser = (url: string) => {
|
||||||
|
const match = url.match(/(?<=app\.travis-ci\.com\/[^/]+\/[^/]+\/)([^/?#]+)/);
|
||||||
|
if (!match) return;
|
||||||
|
|
||||||
|
const projectName = document.querySelector('#ember737')?.textContent;
|
||||||
|
if (projectName !== match[0]) return;
|
||||||
|
|
||||||
|
return {
|
||||||
|
project: match[0],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const CircleCI: HeartbeatParser = (url: string) => {
|
||||||
|
const projectPageMatch = url.match(
|
||||||
|
/(?<=app\.circleci\.com\/projects\/[^/]+\/[^/]+\/[^/]+\/)([^/?#]+)/,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (projectPageMatch) {
|
||||||
|
const seconndBreadcrumbLabel = document.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 = document.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 { project: projectPageMatch[0] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsPageMatch = url.match(
|
||||||
|
/(?<=app\.circleci\.com\/settings\/project\/[^/]+\/[^/]+\/)([^/?#]+)/,
|
||||||
|
);
|
||||||
|
if (settingsPageMatch) {
|
||||||
|
const pageTitle = document.querySelector(
|
||||||
|
'#__next > div > div:nth-child(1) > header > div > div:nth-child(2) > h1',
|
||||||
|
)?.textContent;
|
||||||
|
const pageSubtitle = document.querySelector(
|
||||||
|
'#__next > div > div:nth-child(1) > header > div > div:nth-child(2) > div',
|
||||||
|
)?.textContent;
|
||||||
|
if (pageTitle === 'Project Settings' && pageSubtitle === settingsPageMatch[0]) {
|
||||||
|
return { project: settingsPageMatch[0] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Vercel: HeartbeatParser = (url: string) => {
|
||||||
|
const match = url.match(/(?<=vercel\.com\/[^/]+\/)([^/?#]+)/);
|
||||||
|
if (!match) return;
|
||||||
|
|
||||||
|
// this regex extracts the project name from the title
|
||||||
|
// eg. title: test-website - Overview – Vercel
|
||||||
|
const match2 = document.querySelector('title')?.textContent?.match(/^[^\s]+(?=\s-\s)/);
|
||||||
|
if (!match2 || match2[0] !== match[0]) return;
|
||||||
|
|
||||||
|
return { project: match[0] };
|
||||||
|
};
|
||||||
|
|
||||||
|
const StackOverflow: HeartbeatParser = (_url: string) => {
|
||||||
|
const tags = Array.from(document.querySelectorAll('.post-tag').values())
|
||||||
|
.map((el) => el.textContent)
|
||||||
|
.filter(Boolean) as string[];
|
||||||
|
if (tags.length === 0) return;
|
||||||
|
|
||||||
|
const languages = Array.from(document.querySelectorAll('code[data-highlighted="yes"]'))
|
||||||
|
.map((code) => {
|
||||||
|
const cls = Array.from(code.classList.values()).find((c) => c.startsWith('language-'));
|
||||||
|
return cls?.substring('language-'.length);
|
||||||
|
})
|
||||||
|
.filter(Boolean) as string[];
|
||||||
|
|
||||||
|
for (const lang of languages) {
|
||||||
|
if (tags.includes(lang)) {
|
||||||
|
return { language: lang };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Canva: HeartbeatParser = (_url: string): OptionalHeartbeat | undefined => {
|
||||||
|
const projectName = (document.head.querySelector('meta[property="og:title"]') as HTMLMetaElement)
|
||||||
|
.content;
|
||||||
|
if (!projectName) return;
|
||||||
|
|
||||||
|
// make sure the page title matches the design input element's value, meaning this is a design file
|
||||||
|
const canvaProjectInput = Array.from(
|
||||||
|
document.querySelector('nav')?.querySelectorAll('input') ?? [],
|
||||||
|
).find((inp) => inp.value === projectName);
|
||||||
|
if (!canvaProjectInput) return;
|
||||||
|
|
||||||
|
return {
|
||||||
|
category: Category.designing,
|
||||||
|
language: 'Image (svg)',
|
||||||
|
plugin: 'Canva',
|
||||||
|
project: projectName,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const Figma: HeartbeatParser = (_url: string): OptionalHeartbeat | undefined => {
|
||||||
|
const figmaProject = document.getElementsByClassName('gpu-view-content');
|
||||||
|
if (figmaProject.length === 0) return;
|
||||||
|
|
||||||
|
const project = (document.querySelector('span[data-testid="filename"]') as HTMLElement).innerText;
|
||||||
|
return {
|
||||||
|
category: Category.designing,
|
||||||
|
language: 'Image (svg)',
|
||||||
|
plugin: 'Figma',
|
||||||
|
project,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const GoogleMeet: HeartbeatParser = (_url: string): OptionalHeartbeat | undefined => {
|
||||||
|
const meetId = document.querySelector('[data-meeting-title]')?.getAttribute('data-meeting-title');
|
||||||
|
if (!meetId) return;
|
||||||
|
|
||||||
|
return {
|
||||||
|
category: Category.meeting,
|
||||||
|
plugin: 'Google Meet',
|
||||||
|
project: meetId,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const Slack: HeartbeatParser = (_url: string): OptionalHeartbeat | undefined => {
|
||||||
|
const title = document.querySelector('title')?.textContent?.split(' - ');
|
||||||
|
if (!title || title.length < 3 || title[-1] !== 'Slack') {
|
||||||
|
return {
|
||||||
|
category: Category.communicating,
|
||||||
|
plugin: 'Slack',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const entity = title[0];
|
||||||
|
const project = title[1];
|
||||||
|
|
||||||
|
return {
|
||||||
|
category: Category.communicating,
|
||||||
|
entity,
|
||||||
|
entityType: EntityType.app,
|
||||||
|
plugin: 'Slack',
|
||||||
|
project,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const Zoom: HeartbeatParser = (_url: string): OptionalHeartbeat | undefined => {
|
||||||
|
const entity = document.querySelector('title')?.textContent;
|
||||||
|
|
||||||
|
return {
|
||||||
|
category: Category.communicating,
|
||||||
|
entity: entity ?? undefined,
|
||||||
|
entityType: entity ? EntityType.app : undefined,
|
||||||
|
plugin: 'Zoom',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const _normalizeUrl = (url?: string | null) => {
|
||||||
|
if (!url) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (url.startsWith('http://')) {
|
||||||
|
url = url.substring('http://'.length);
|
||||||
|
}
|
||||||
|
if (url.startsWith('https://')) {
|
||||||
|
url = url.substring('https://'.length);
|
||||||
|
}
|
||||||
|
if (url.startsWith('www.')) {
|
||||||
|
url = url.substring('www.'.length);
|
||||||
|
}
|
||||||
|
if (url.endsWith('/')) {
|
||||||
|
url = url.substring(0, url.length - 1);
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stackExchangeDomains = (STACKEXCHANGE_SITES as StackExchangeSite[]).map((site) => {
|
||||||
|
return _normalizeUrl(site.site_url);
|
||||||
|
});
|
||||||
|
|
||||||
|
const SITES: Record<KnownSite, SiteParser> = {
|
||||||
|
bitbucket: {
|
||||||
|
parser: BitBucket,
|
||||||
|
urls: [/^https?:\/\/(.+\.)?bitbucket.org\//],
|
||||||
|
},
|
||||||
|
canva: {
|
||||||
|
parser: Canva,
|
||||||
|
urls: ['canva.com'],
|
||||||
|
},
|
||||||
|
circleci: {
|
||||||
|
parser: CircleCI,
|
||||||
|
urls: [/^https?:\/\/(.+\.)?circleci.com\//],
|
||||||
|
},
|
||||||
|
figma: {
|
||||||
|
parser: Figma,
|
||||||
|
urls: ['figma.com'],
|
||||||
|
},
|
||||||
|
github: {
|
||||||
|
parser: GitHub,
|
||||||
|
urls: [
|
||||||
|
/^https?:\/\/(.+\.)?github.com\//,
|
||||||
|
/^https?:\/\/(.+\.)?github.dev\//,
|
||||||
|
/^https?:\/\/(.+\.)?github.blog\//,
|
||||||
|
/^https?:\/\/(.+\.)?github.io\//,
|
||||||
|
/^https?:\/\/(.+\.)?github.community\//,
|
||||||
|
// /^https?:\/\/(.+\.)?ghcr.io\//,
|
||||||
|
// /^https?:\/\/(.+\.)?githubapp.com\//,
|
||||||
|
// /^https?:\/\/(.+\.)?githubassets.com\//,
|
||||||
|
// /^https?:\/\/(.+\.)?githubusercontent.com\//,
|
||||||
|
// /^https?:\/\/(.+\.)?githubnext.com\//,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
gitlab: {
|
||||||
|
parser: GitLab,
|
||||||
|
urls: [/^https?:\/\/(.+\.)?gitlab.com\//],
|
||||||
|
},
|
||||||
|
googlemeet: {
|
||||||
|
parser: GoogleMeet,
|
||||||
|
urls: [/^https?:\/\/meet.google.com\//],
|
||||||
|
},
|
||||||
|
slack: {
|
||||||
|
parser: Slack,
|
||||||
|
urls: [/^https:\/\/app.slack.com\/client\//],
|
||||||
|
},
|
||||||
|
stackoverflow: {
|
||||||
|
parser: StackOverflow,
|
||||||
|
urls: stackExchangeDomains,
|
||||||
|
},
|
||||||
|
travisci: {
|
||||||
|
parser: TravisCI,
|
||||||
|
urls: [/^https?:\/\/(.+\.)?travis-ci.com\//],
|
||||||
|
},
|
||||||
|
vercel: {
|
||||||
|
parser: Vercel,
|
||||||
|
urls: [/^https?:\/\/(.+\.)?vercel.com\//],
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
parser: Zoom,
|
||||||
|
urls: [/^https:\/\/(.+\.)?zoom.us\/[^?]+\/join/],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const match = (url: string, pattern: RegExp | string): boolean => {
|
||||||
|
if (typeof pattern === 'string') {
|
||||||
|
return _normalizeUrl(url).startsWith(_normalizeUrl(pattern));
|
||||||
|
}
|
||||||
|
return pattern.test(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSite = (url: string): SiteParser | undefined => {
|
||||||
|
return Object.values(SITES).find((site) => {
|
||||||
|
return site.urls.some((re) => match(url, re));
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,61 +1,8 @@
|
|||||||
import { getSite } from './utils/heartbeat';
|
import { getSite } from './utils/sites';
|
||||||
|
|
||||||
const oneMinute = 60000;
|
const oneMinute = 60000;
|
||||||
const fiveMinutes = 300000;
|
const fiveMinutes = 300000;
|
||||||
|
|
||||||
interface DesignProject {
|
|
||||||
category: string;
|
|
||||||
editor: string;
|
|
||||||
language: string;
|
|
||||||
project: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseCanva = (): DesignProject | undefined => {
|
|
||||||
const projectName = (document.head.querySelector('meta[property="og:title"]') as HTMLMetaElement)
|
|
||||||
.content;
|
|
||||||
if (!projectName) return;
|
|
||||||
|
|
||||||
// make sure the page title matches the design input element's value, meaning this is a design file
|
|
||||||
const canvaProjectInput = Array.from(
|
|
||||||
document.querySelector('nav')?.querySelectorAll('input') ?? [],
|
|
||||||
).find((inp) => inp.value === projectName);
|
|
||||||
if (!canvaProjectInput) return;
|
|
||||||
|
|
||||||
return {
|
|
||||||
category: 'designing',
|
|
||||||
editor: 'Canva',
|
|
||||||
language: 'Canva Design',
|
|
||||||
project: projectName,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseFigma = (): DesignProject | undefined => {
|
|
||||||
const figmaProject = document.getElementsByClassName('gpu-view-content');
|
|
||||||
if (figmaProject.length === 0) return;
|
|
||||||
|
|
||||||
const projectName = (document.querySelector('span[data-testid="filename"]') as HTMLElement)
|
|
||||||
.innerText;
|
|
||||||
return {
|
|
||||||
category: 'designing',
|
|
||||||
editor: 'Figma',
|
|
||||||
language: 'Figma Design',
|
|
||||||
project: projectName,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseMeet = (): DesignProject | undefined => {
|
|
||||||
const meetId = document.querySelector('[data-meeting-title]')?.getAttribute('data-meeting-title');
|
|
||||||
if (!meetId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
category: 'meeting',
|
|
||||||
editor: 'Meet',
|
|
||||||
language: 'Google Meet',
|
|
||||||
project: meetId,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debounces the execution of a function.
|
* Debounces the execution of a function.
|
||||||
*
|
*
|
||||||
@@ -112,6 +59,6 @@ const checkIfInAMeeting = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Google Meet
|
// Google Meet
|
||||||
if (window.location.href.startsWith('https://meet.google.com')) {
|
if (window.location.href.startsWith('https://meet.google.com/')) {
|
||||||
checkIfInAMeeting();
|
checkIfInAMeeting();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user