diff --git a/src/background/controller/index.ts b/src/background/controller/index.ts index 2b568ee..27d57c5 100644 --- a/src/background/controller/index.ts +++ b/src/background/controller/index.ts @@ -7,7 +7,7 @@ import { } from '../../shared/db/types'; import { getSettings } from '../../shared/preferences'; import { getIsoDate, getMinutesInMs } from '../../shared/utils/dates-helper'; -import { isInvalidUrl } from '../../shared/utils/url'; +import { isInvalidUrl, isDomainAllowedByUser } from '../../shared/utils/url'; import { setActiveTabRecord } from '../tables/state'; import { ActiveTimelineRecordDao, createNewActiveRecord } from './active'; import { updateTimeOnBadge } from './badge'; @@ -49,17 +49,19 @@ const handleAndCollectDomainIgnoredInfo = async ( currentTimelineRecord: TimelineRecord | null, focusedActiveTab: chrome.tabs.Tab | null, ): Promise => { - const isDomainIgnored = preferences.ignoredHosts.includes( - currentTimelineRecord?.hostname ?? '', - ); - if (!isDomainIgnored) { + + const hostname = currentTimelineRecord?.hostname ?? ''; + + const isDomainAllowed = isDomainAllowedByUser(hostname, preferences.allowedHosts, preferences.ignoredHosts) + + if (isDomainAllowed) { await handlePageLimitExceed( preferences.limits, focusedActiveTab, currentTimelineRecord, ); } - return isDomainIgnored; + return isDomainAllowed; }; const getTabStatus = ( @@ -104,7 +106,7 @@ export const handleStateChange = async ( await activeTimeline.set(currentTimelineRecord); } - const isDomainIgnored = await handleAndCollectDomainIgnoredInfo( + const isDomainAllowed = await handleAndCollectDomainIgnoredInfo( preferences, currentTimelineRecord, focusedActiveTab, @@ -113,7 +115,7 @@ export const handleStateChange = async ( await updateTimeOnBadge( focusedActiveTab, currentTimelineRecord, - preferences.displayTimeOnBadge && !isDomainIgnored, + preferences.displayTimeOnBadge && isDomainAllowed, ); if ( @@ -121,7 +123,7 @@ export const handleStateChange = async ( isImpossiblyLongEvent || isInvalidUrl(focusedActiveTab?.url) ) { - await commitTabActivity(await activeTimeline.get()); + await commitTabActivity(await activeTimeline.get(), preferences); return; } @@ -129,7 +131,7 @@ export const handleStateChange = async ( focusedActiveTab && currentTimelineRecord?.url !== focusedActiveTab?.url ) { - await commitTabActivity(await activeTimeline.get()); + await commitTabActivity(await activeTimeline.get(), preferences); await createNewActiveRecord( timestamp, focusedActiveTab, @@ -138,11 +140,16 @@ export const handleStateChange = async ( } }; -async function commitTabActivity(currentTimelineRecord: TimelineRecord | null) { +async function commitTabActivity(currentTimelineRecord: TimelineRecord | null, preferences: Preferences) { if (!currentTimelineRecord) { return; } + const isDomainAllowed = isDomainAllowedByUser(currentTimelineRecord.hostname, preferences.allowedHosts, preferences.ignoredHosts); + if (!isDomainAllowed) { + return; + } + const currentIsoDate = getIsoDate(new Date()); await saveTimelineRecord(currentTimelineRecord, currentIsoDate); diff --git a/src/background/services/stats.ts b/src/background/services/stats.ts index 929b88c..988ff48 100644 --- a/src/background/services/stats.ts +++ b/src/background/services/stats.ts @@ -34,18 +34,24 @@ const emitSuccessSyncStats = async ( await callback({ result: 'ok', }); - const lastUpdateDateTime = preferences.lastUpdateStats?.Datetime; + + let lastUpdateDateTime = DateTime.fromJSDate(new Date()); + if (preferences.lastUpdateStats?.Datetime) { + lastUpdateDateTime = DateTime.fromISO(preferences.lastUpdateStats?.Datetime); + } + await chrome.action.setTitle({ title: "Codealike time tracker. You're authenticated to Codealike. Last bundle of stats sent " + - lastUpdateDateTime ?? DateTime.now() + '.', + lastUpdateDateTime.toLocaleString(DateTime.DATETIME_SHORT) + '.', }); await chrome.action.setBadgeText({ text: '', }); + await setSettings({ lastUpdateStats: { - Datetime: DateTime.fromJSDate(new Date()), + Datetime: new Date().toJSON(), Status: 'OK', }, }); @@ -58,18 +64,23 @@ const emitFailedSyncStats = async ( await callback({ result: 'failed', }); - const lastUpdateDateTime = preferences.lastUpdateStats?.Datetime; + + let lastUpdateDateTime = DateTime.fromJSDate(new Date()); + if (preferences.lastUpdateStats?.Datetime) { + lastUpdateDateTime = DateTime.fromISO(preferences.lastUpdateStats?.Datetime); + } await chrome.action.setTitle({ title: 'Codealike time tracker. An error happened trying to send Web Activity ' + - lastUpdateDateTime ?? DateTime.now() + '.', + lastUpdateDateTime.toLocaleString(DateTime.DATETIME_SHORT) + '.', }); await chrome.action.setBadgeText({ text: '', }); + await setSettings({ lastUpdateStats: { - Datetime: DateTime.fromJSDate(new Date()), + Datetime: new Date().toJSON(), Status: 'NOK', }, }); @@ -135,7 +146,13 @@ const sendWebActivity = async ( } const { records, states } = transformTimelineInWebActivity(timeline); - const result = await sendStats(userToken, records, states); + let result = null; + try { + result = await sendStats(userToken, records, states); + } + catch(err) { + console.log(err); + } if (result) { await Promise.all([ diff --git a/src/popup/components/IgnoredDomainsSetting/IgnoredDomainSetting.tsx b/src/popup/components/IgnoredDomainsSetting/IgnoredDomainSetting.tsx index 5ac3a0d..64a51c3 100644 --- a/src/popup/components/IgnoredDomainsSetting/IgnoredDomainSetting.tsx +++ b/src/popup/components/IgnoredDomainsSetting/IgnoredDomainSetting.tsx @@ -4,7 +4,7 @@ import { twMerge } from 'tailwind-merge'; import { Button, ButtonType } from '../../../blocks/Button'; import { Icon, IconType } from '../../../blocks/Icon'; import { Input } from '../../../blocks/Input'; -import { Panel, PanelBody, PanelHeader } from '../../../blocks/Panel'; +import { PanelBody } from '../../../blocks/Panel'; import { assertDomainIsValid } from '../../../shared/utils/domains'; import { usePopupContext } from '../../hooks/PopupContext'; @@ -65,8 +65,7 @@ export const IgnoredDomainSetting: React.FC = () => { }, [setDomainsListExpanded]); return ( - - Blacklist domains +

You can hide unwanted websites to keep dashboards clean.

@@ -111,6 +110,6 @@ export const IgnoredDomainSetting: React.FC = () => {
-
+ ); }; diff --git a/src/popup/components/WhitelistDomainsSetting/WhitelistDomainSetting.tsx b/src/popup/components/WhitelistDomainsSetting/WhitelistDomainSetting.tsx new file mode 100644 index 0000000..4892106 --- /dev/null +++ b/src/popup/components/WhitelistDomainsSetting/WhitelistDomainSetting.tsx @@ -0,0 +1,115 @@ +import * as React from 'react'; +import { twMerge } from 'tailwind-merge'; + +import { Button, ButtonType } from '../../../blocks/Button'; +import { Icon, IconType } from '../../../blocks/Icon'; +import { Input } from '../../../blocks/Input'; +import { PanelBody } from '../../../blocks/Panel'; +import { assertDomainIsValid } from '../../../shared/utils/domains'; +import { usePopupContext } from '../../hooks/PopupContext'; + +export const WhitelistDomainSetting: React.FC = () => { + const { settings, updateSettings } = usePopupContext(); + const [ allowedHosts, setAllowedHosts] = React.useState( + settings.allowedHosts ?? [] + ); + const [ newAllowedHost, setNewAllowedHost] = React.useState(''); + const [isAllowedHostsListExpanded, setAllowedHostListExpanded] = + React.useState(false); + + const handleAddWhitelistDomain = React.useCallback(() => { + try { + assertDomainIsValid(newAllowedHost); + setAllowedHosts((prev) => { + const newAllowedHostList = Array.from( + new Set([...prev, newAllowedHost]) + ); + + updateSettings({ + allowedHosts: newAllowedHostList, + }); + + return newAllowedHostList; + }); + + setNewAllowedHost(''); + } catch (_) { + // + } + }, [newAllowedHost, updateSettings]); + + const handleRemoveAllowedHost = React.useCallback( + (host: string) => { + setAllowedHosts((prev) => { + const newAllowedHostList = prev.filter((h) => h !== host); + + updateSettings({ + allowedHosts: newAllowedHostList, + }); + + return newAllowedHostList; + }); + }, + [setAllowedHosts, updateSettings] + ); + + const handleAddtoAllowedHostChange = React.useCallback( + (e: React.ChangeEvent) => { + setNewAllowedHost(e.target.value); + }, + [] + ); + + const handleAllowHostsListExpanded = React.useCallback(() => { + setAllowedHostListExpanded((prev) => !prev); + }, [setAllowedHostListExpanded]); + + return ( +
+ +

You can add only the domains you wish to track.

+
+ + +
+
+ + View all whitelisted domains + +
+ {!allowedHosts.length && ( +

No whitelisted domains

+ )} + {allowedHosts.map((domain) => ( +
+ handleRemoveAllowedHost(domain)} + /> + {domain} +
+ ))} +
+
+
+
+ ); +}; diff --git a/src/popup/hooks/PopupContext.tsx b/src/popup/hooks/PopupContext.tsx index 3076f8a..a9b2b96 100644 --- a/src/popup/hooks/PopupContext.tsx +++ b/src/popup/hooks/PopupContext.tsx @@ -31,14 +31,25 @@ export const PopupContextProvider: React.FC = ({ children }) => { const filterDomainsFromStore = React.useCallback( (store: Record) => { - const filteredStore = Object.fromEntries( + let filteredStore = Object.fromEntries( Object.entries(store).filter( ([key]) => !settings.ignoredHosts.includes(key), ), ); + + // Handling whitelisting of hosts + const allowedHosts = settings.allowedHosts ?? []; + if (allowedHosts.length > 0) { + filteredStore = Object.fromEntries( + Object.entries(filteredStore).filter( + ([key]) => allowedHosts.includes(key), + ), + ); + } + return filteredStore; }, - [settings.ignoredHosts], + [settings.ignoredHosts, settings.allowedHosts], ); const filteredStore = React.useMemo( diff --git a/src/popup/pages/PreferencesPage.tsx b/src/popup/pages/PreferencesPage.tsx index 3a81c40..4633116 100644 --- a/src/popup/pages/PreferencesPage.tsx +++ b/src/popup/pages/PreferencesPage.tsx @@ -1,14 +1,50 @@ import * as React from 'react'; import {FC} from 'react'; +import { Panel, PanelBody, PanelHeader } from './../../blocks/Panel'; +import { Input } from './../../blocks/Input'; + + import {IgnoredDomainSetting} from '../components/IgnoredDomainsSetting/IgnoredDomainSetting'; +import { WhitelistDomainSetting } from '../components/WhitelistDomainsSetting/WhitelistDomainSetting'; import {UserTokenSetting} from "../components/UserTokenSetting/UserTokenSetting"; export const PreferencesPage: FC = () => { + const [isWhitelistShown, hideWhitelist] = React.useState(true); + + const toggle = React.useCallback(() => { + hideWhitelist((prev) => !prev); + }, [hideWhitelist]); + return (
- + + Domain Management +
+
+ + +
+ +
+ + +
+
+ + + {(isWhitelistShown ? : )} + +
); }; diff --git a/src/shared/db/types.ts b/src/shared/db/types.ts index aad7186..e31518c 100644 --- a/src/shared/db/types.ts +++ b/src/shared/db/types.ts @@ -56,7 +56,8 @@ export interface LogMessage { export interface Preferences { connectionStatus: ConnectionStatus; userToken?: string; - ignoredHosts: string[]; + ignoredHosts: string[]; //urls that are be blacklisted + allowedHosts?: string[]; //urls that are to be whitelisted limits: Record; displayTimeOnBadge: boolean; lastUpdateStats?: Statistics; @@ -64,7 +65,7 @@ export interface Preferences { export interface Statistics { Status: 'OK' | 'NOK'; - Datetime: DateTime; + Datetime: string; } export enum ConnectionStatus { diff --git a/src/shared/preferences/index.ts b/src/shared/preferences/index.ts index 826ecab..62096a3 100644 --- a/src/shared/preferences/index.ts +++ b/src/shared/preferences/index.ts @@ -1,6 +1,7 @@ import { ConnectionStatus, Preferences } from '../db/types'; export const DEFAULT_PREFERENCES: Preferences = { + allowedHosts: [], //whitelisted domains connectionStatus: ConnectionStatus.Disconnected, displayTimeOnBadge: true, ignoredHosts: [], diff --git a/src/shared/utils/url.ts b/src/shared/utils/url.ts index f7daa00..f9ec3d1 100644 --- a/src/shared/utils/url.ts +++ b/src/shared/utils/url.ts @@ -8,7 +8,28 @@ export const isInvalidUrl = (url: string | undefined): url is undefined => { return ( !url || ['chrome', 'about', 'opera', 'edge', 'coccoc', 'yabro'].some((broName) => - url.startsWith(broName) + url.startsWith(broName), ) ); }; + +export const isDomainAllowedByUser = ( + currentHost: string | undefined, + allowedHosts: string[] | undefined, + ignoreHosts: string[] | undefined, +) => { + if (!currentHost) { + return false; + } + + let isAllowed = true; + if (allowedHosts && allowedHosts?.length > 0) { + isAllowed = allowedHosts.includes(currentHost); + } + + let isBlocked = false; + if (ignoreHosts && ignoreHosts?.length > 0) { + isBlocked = ignoreHosts.includes(currentHost); + } + return isAllowed && !isBlocked; +}; diff --git a/static/manifest.json b/static/manifest.json index aaea938..cd25251 100644 --- a/static/manifest.json +++ b/static/manifest.json @@ -20,7 +20,9 @@ } ], "minimum_chrome_version": "90", - "version": "2.0.4", + + "version": "2.1.0", + "description": "Track activity while coding.", "permissions": [ "idle",