From 18d26d8e6c780696c551aa309c8d113e80773c84 Mon Sep 17 00:00:00 2001 From: Galin Chung Nguyen Date: Tue, 31 Dec 2024 23:16:32 +0700 Subject: [PATCH] block custom url --- packages/shared/lib/constants/apps.ts | 56 --------- packages/storage/lib/constants/apps.ts | 157 ++++++++++++++++++++++++ packages/storage/lib/impl/zenStorage.ts | 28 +++-- packages/storage/tsconfig.json | 5 +- pages/content-ui/src/App.tsx | 27 +++- pages/popup/src/Popup.tsx | 95 ++++++++++---- 6 files changed, 274 insertions(+), 94 deletions(-) delete mode 100644 packages/shared/lib/constants/apps.ts create mode 100644 packages/storage/lib/constants/apps.ts diff --git a/packages/shared/lib/constants/apps.ts b/packages/shared/lib/constants/apps.ts deleted file mode 100644 index 0d59bad..0000000 --- a/packages/shared/lib/constants/apps.ts +++ /dev/null @@ -1,56 +0,0 @@ -export const availableApps = [ - { name: '9GAG', icon: 'https://www.9gag.com/favicon.ico', url: 'https://www.9gag.com' }, - { name: 'Alibaba', icon: 'https://www.alibaba.com/favicon.ico', url: 'https://www.alibaba.com' }, - { name: 'Amazon', icon: 'https://www.amazon.com/favicon.ico', url: 'https://www.amazon.com' }, - { name: 'BBC', icon: 'https://www.bbc.com/favicon.ico', url: 'https://www.bbc.com' }, - { name: 'BuzzFeed', icon: 'https://www.buzzfeed.com/favicon.ico', url: 'https://www.buzzfeed.com' }, - { name: 'CNN', icon: 'https://www.cnn.com/favicon.ico', url: 'https://www.cnn.com' }, - { name: 'Disney+', icon: 'https://www.disneyplus.com/favicon.ico', url: 'https://www.disneyplus.com' }, - { - name: 'Discord', - icon: 'https://cdn.prod.website-files.com/6257adef93867e50d84d30e2/6266bc493fb42d4e27bb8393_847541504914fd33810e70a0ea73177e.ico', - url: 'https://www.discord.com', - }, - { - name: 'DoorDash', - icon: 'https://cdn.doordash.com/static/img/favicon@2x.ico?dd-nonce', - url: 'https://www.doordash.com', - }, - { name: 'Epic Games', icon: 'https://www.epicgames.com/favicon.ico', url: 'https://www.epicgames.com' }, - { name: 'Etsy', icon: 'https://www.etsy.com/favicon.ico', url: 'https://www.etsy.com' }, - { - name: 'Facebook', - icon: 'https://static.xx.fbcdn.net/rsrc.php/yT/r/aGT3gskzWBf.ico', - url: 'https://www.facebook.com', - }, - { name: 'HuffPost', icon: 'https://www.huffpost.com/favicon.ico', url: 'https://www.huffpost.com' }, - { name: 'Hulu', icon: 'https://www.hulu.com/favicon.ico', url: 'https://www.hulu.com' }, - { name: 'Imgur', icon: 'https://www.imgur.com/favicon.ico', url: 'https://www.imgur.com' }, - { - name: 'Instagram', - icon: 'https://static.cdninstagram.com/rsrc.php/v4/yI/r/VsNE-OHk_8a.png', - url: 'https://www.instagram.com', - }, - { name: 'Kongregate', icon: 'https://www.kongregate.com/favicon.ico', url: 'https://www.kongregate.com' }, - { name: 'LinkedIn', icon: 'https://www.linkedin.com/favicon.ico', url: 'https://www.linkedin.com' }, - { name: 'Miniclip', icon: 'https://www.miniclip.com/favicon.ico', url: 'https://www.miniclip.com' }, - { name: 'Netflix', icon: 'https://www.netflix.com/favicon.ico', url: 'https://www.netflix.com' }, - { name: 'Pinterest', icon: 'https://www.pinterest.com/favicon.ico', url: 'https://www.pinterest.com' }, - { name: 'Quora', icon: 'https://www.quora.com/favicon.ico', url: 'https://www.quora.com' }, - { name: 'Reddit', icon: 'https://www.reddit.com/favicon.ico', url: 'https://www.reddit.com' }, - { name: 'Slack', icon: 'https://slack.com/favicon.ico', url: 'https://www.slack.com' }, - { name: 'Snapchat', icon: 'https://www.snapchat.com/favicon.ico', url: 'https://www.snapchat.com' }, - { name: 'Spotify', icon: 'https://www.spotify.com/favicon.ico', url: 'https://www.spotify.com' }, - { name: 'Steam', icon: 'https://store.steampowered.com/favicon.ico', url: 'https://store.steampowered.com' }, - { name: 'Telegram', icon: 'https://telegram.org/favicon.ico', url: 'https://www.telegram.org' }, - { name: 'TikTok', icon: 'https://www.tiktok.com/favicon.ico', url: 'https://www.tiktok.com' }, - { name: 'Tumblr', icon: 'https://www.tumblr.com/favicon.ico', url: 'https://www.tumblr.com' }, - { name: 'Twitch', icon: 'https://www.twitch.tv/favicon.ico', url: 'https://www.twitch.tv' }, - { name: 'Twitter', icon: 'https://twitter.com/favicon.ico', url: 'https://www.twitter.com' }, - { name: 'Uber Eats', icon: 'https://www.ubereats.com/_static/d526ae562360062f.ico', url: 'https://www.ubereats.com' }, - { name: 'Walmart', icon: 'https://www.walmart.com/favicon.ico', url: 'https://www.walmart.com' }, - { name: 'WhatsApp', icon: 'https://web.whatsapp.com/favicon.ico', url: 'https://www.whatsapp.com' }, - { name: 'Youtube', icon: 'https://www.youtube.com/favicon.ico', url: 'https://www.youtube.com' }, -] as const; - -export type App = (typeof availableApps)[number]; diff --git a/packages/storage/lib/constants/apps.ts b/packages/storage/lib/constants/apps.ts new file mode 100644 index 0000000..f309b75 --- /dev/null +++ b/packages/storage/lib/constants/apps.ts @@ -0,0 +1,157 @@ +export interface App { + name: string; + icon: string; + url: string; +} + +export const availableApps: Array = [ + { name: '9GAG', icon: 'https://www.google.com/s2/favicons?domain=9gag.com&size=32', url: 'https://www.9gag.com' }, + { + name: 'Alibaba', + icon: 'https://www.google.com/s2/favicons?domain=alibaba.com&size=32', + url: 'https://www.alibaba.com', + }, + { + name: 'Amazon', + icon: 'https://www.google.com/s2/favicons?domain=amazon.com&size=32', + url: 'https://www.amazon.com', + }, + { name: 'BBC', icon: 'https://www.google.com/s2/favicons?domain=bbc.com&size=32', url: 'https://www.bbc.com' }, + { + name: 'BuzzFeed', + icon: 'https://www.google.com/s2/favicons?domain=buzzfeed.com&size=32', + url: 'https://www.buzzfeed.com', + }, + { name: 'CNN', icon: 'https://www.google.com/s2/favicons?domain=cnn.com&size=32', url: 'https://www.cnn.com' }, + { + name: 'Disney+', + icon: 'https://www.google.com/s2/favicons?domain=disneyplus.com&size=32', + url: 'https://www.disneyplus.com', + }, + { + name: 'Discord', + icon: 'https://www.google.com/s2/favicons?domain=discord.com&size=32', + url: 'https://www.discord.com', + }, + { + name: 'DoorDash', + icon: 'https://www.google.com/s2/favicons?domain=doordash.com&size=32', + url: 'https://www.doordash.com', + }, + { + name: 'Epic Games', + icon: 'https://www.google.com/s2/favicons?domain=epicgames.com&size=32', + url: 'https://www.epicgames.com', + }, + { name: 'Etsy', icon: 'https://www.google.com/s2/favicons?domain=etsy.com&size=32', url: 'https://www.etsy.com' }, + { + name: 'Facebook', + icon: 'https://www.google.com/s2/favicons?domain=facebook.com&size=32', + url: 'https://www.facebook.com', + }, + { + name: 'Messenger', + icon: 'https://www.google.com/s2/favicons?domain=messenger.com&size=32', + url: 'https://www.messenger.com', + }, + { + name: 'HuffPost', + icon: 'https://www.google.com/s2/favicons?domain=huffpost.com&size=32', + url: 'https://www.huffpost.com', + }, + { name: 'Hulu', icon: 'https://www.google.com/s2/favicons?domain=hulu.com&size=32', url: 'https://www.hulu.com' }, + { name: 'Imgur', icon: 'https://www.google.com/s2/favicons?domain=imgur.com&size=32', url: 'https://www.imgur.com' }, + { + name: 'Instagram', + icon: 'https://www.google.com/s2/favicons?domain=instagram.com&size=32', + url: 'https://www.instagram.com', + }, + { + name: 'Kongregate', + icon: 'https://www.google.com/s2/favicons?domain=kongregate.com&size=32', + url: 'https://www.kongregate.com', + }, + { + name: 'LinkedIn', + icon: 'https://www.google.com/s2/favicons?domain=linkedin.com&size=32', + url: 'https://www.linkedin.com', + }, + { + name: 'Miniclip', + icon: 'https://www.google.com/s2/favicons?domain=miniclip.com&size=32', + url: 'https://www.miniclip.com', + }, + { + name: 'Netflix', + icon: 'https://www.google.com/s2/favicons?domain=netflix.com&size=32', + url: 'https://www.netflix.com', + }, + { + name: 'Pinterest', + icon: 'https://www.google.com/s2/favicons?domain=pinterest.com&size=32', + url: 'https://www.pinterest.com', + }, + { name: 'Quora', icon: 'https://www.google.com/s2/favicons?domain=quora.com&size=32', url: 'https://www.quora.com' }, + { + name: 'Reddit', + icon: 'https://www.google.com/s2/favicons?domain=reddit.com&size=32', + url: 'https://www.reddit.com', + }, + { name: 'Slack', icon: 'https://www.google.com/s2/favicons?domain=slack.com&size=32', url: 'https://www.slack.com' }, + { + name: 'Snapchat', + icon: 'https://www.google.com/s2/favicons?domain=snapchat.com&size=32', + url: 'https://www.snapchat.com', + }, + { + name: 'Spotify', + icon: 'https://www.google.com/s2/favicons?domain=spotify.com&size=32', + url: 'https://www.spotify.com', + }, + { + name: 'Steam', + icon: 'https://www.google.com/s2/favicons?domain=steampowered.com&size=32', + url: 'https://store.steampowered.com', + }, + { + name: 'Telegram', + icon: 'https://www.google.com/s2/favicons?domain=telegram.org&size=32', + url: 'https://www.telegram.org', + }, + { + name: 'TikTok', + icon: 'https://www.google.com/s2/favicons?domain=tiktok.com&size=32', + url: 'https://www.tiktok.com', + }, + { + name: 'Tumblr', + icon: 'https://www.google.com/s2/favicons?domain=tumblr.com&size=32', + url: 'https://www.tumblr.com', + }, + { name: 'Twitch', icon: 'https://www.google.com/s2/favicons?domain=twitch.tv&size=32', url: 'https://www.twitch.tv' }, + { + name: 'Twitter', + icon: 'https://www.google.com/s2/favicons?domain=twitter.com&size=32', + url: 'https://www.twitter.com', + }, + { + name: 'Uber Eats', + icon: 'https://www.google.com/s2/favicons?domain=ubereats.com&size=32', + url: 'https://www.ubereats.com', + }, + { + name: 'Walmart', + icon: 'https://www.google.com/s2/favicons?domain=walmart.com&size=32', + url: 'https://www.walmart.com', + }, + { + name: 'WhatsApp', + icon: 'https://www.google.com/s2/favicons?domain=whatsapp.com&size=32', + url: 'https://www.whatsapp.com', + }, + { + name: 'Youtube', + icon: 'https://www.google.com/s2/favicons?domain=youtube.com&size=32', + url: 'https://www.youtube.com', + }, +] as const; diff --git a/packages/storage/lib/impl/zenStorage.ts b/packages/storage/lib/impl/zenStorage.ts index 47cadf2..f421cd4 100644 --- a/packages/storage/lib/impl/zenStorage.ts +++ b/packages/storage/lib/impl/zenStorage.ts @@ -1,6 +1,7 @@ import { StorageEnum } from '../base/enums'; import { createStorage } from '../base/base'; import type { BaseStorage } from '../base/types'; +import { type App } from '../constants/apps'; export enum ZenTimerState { Focus = 'focus', @@ -17,7 +18,7 @@ export interface ZenSettings { sessions: number; focusMinutes: number; breakMinutes: number; - blockedApps: string[]; + blockedApps: App[]; timerActive: boolean; currentSession: number; timerState: ZenTimerState; @@ -29,8 +30,8 @@ export type ZenStorage = BaseStorage & { updateSessions: (sessions: number) => Promise; updateFocusMinutes: (minutes: number) => Promise; updateBreakMinutes: (minutes: number) => Promise; - addBlockedApp: (appName: string) => Promise; - removeBlockedApp: (appName: string) => Promise; + addBlockedApp: (app: App) => Promise; + removeBlockedApp: (appUrl: string) => Promise; updateTimerState: ( timerState: Partial>, ) => Promise; @@ -73,16 +74,23 @@ export const zenStorage: ZenStorage = { breakMinutes: minutes, })); }, - addBlockedApp: async (appName: string) => { - await storage.set(current => ({ - ...current, - blockedApps: [...current.blockedApps, appName], - })); + addBlockedApp: async (app: App) => { + // only add if not in blockedApps + const alreadyExists = (await storage.get()).blockedApps.some(blockedApp => blockedApp.url === app.url); + if (!alreadyExists) { + await storage.set(current => ({ + ...current, + blockedApps: [...current.blockedApps, app], + })); + return true; + } else { + return false; + } }, - removeBlockedApp: async (appName: string) => { + removeBlockedApp: async (appUrl: string) => { await storage.set(current => ({ ...current, - blockedApps: current.blockedApps.filter(app => app !== appName), + blockedApps: current.blockedApps.filter(app => app.url !== appUrl), })); }, updateTimerState: async ( diff --git a/packages/storage/tsconfig.json b/packages/storage/tsconfig.json index f6877b2..52b92bb 100644 --- a/packages/storage/tsconfig.json +++ b/packages/storage/tsconfig.json @@ -3,7 +3,10 @@ "compilerOptions": { "baseUrl": ".", "outDir": "dist", - "types": ["chrome"] + "types": ["chrome"], + "paths": { + "@extension/shared/*": ["../shared/*"] + } }, "include": ["index.ts", "lib"] } diff --git a/pages/content-ui/src/App.tsx b/pages/content-ui/src/App.tsx index a68f553..95b1a73 100644 --- a/pages/content-ui/src/App.tsx +++ b/pages/content-ui/src/App.tsx @@ -1,4 +1,3 @@ -import { availableApps } from '@extension/shared/lib/constants/apps'; import { useStorage } from '@extension/shared'; import { zenStorage, ZenTimerState } from '@extension/storage'; import { useEffect, useMemo } from 'react'; @@ -11,12 +10,28 @@ export default function App() { console.log('content-ui: 🎭 App component loaded '); }, []); + const normalizeUrl = (url: string) => { + return url + .replace(/^https?:\/\//, '') + .replace(/^www\./, '') + .toLowerCase(); + }; + const shouldBlock = useMemo(() => { - const currentAppName = availableApps.find(app => app.url === window.location.origin)?.name; - if (!currentAppName) return false; - console.log('content-ui: 🎭 current App Name is ', currentAppName); - console.log('content-ui: 🎭 blockedApps = ', zenSettings?.blockedApps); - return zenSettings?.timerState === ZenTimerState.Focus && zenSettings?.blockedApps.includes(currentAppName); + const currentOrigin = window.location.origin; + const rootOrigin = new URL(currentOrigin).hostname.split('.').slice(-2).join('.'); + const origins = [rootOrigin, currentOrigin]; + + return origins.some(origin => { + const normalizedOrigin = normalizeUrl(origin); + return ( + zenSettings?.timerState === ZenTimerState.Focus && + zenSettings?.blockedApps.some(app => { + const normalizedAppUrl = normalizeUrl(app.url); + return normalizedAppUrl === normalizedOrigin; + }) + ); + }); }, [zenSettings]); useEffect(() => { diff --git a/pages/popup/src/Popup.tsx b/pages/popup/src/Popup.tsx index ef1cad4..58a2939 100644 --- a/pages/popup/src/Popup.tsx +++ b/pages/popup/src/Popup.tsx @@ -3,7 +3,7 @@ import { useStorage, withErrorBoundary, withSuspense } from '@extension/shared'; import type { ZenSettings } from '@extension/storage'; import { themeStorage, zenStorage } from '@extension/storage'; import { useState } from 'react'; -import { availableApps, type App } from '@extension/shared/lib/constants/apps'; +import { availableApps, type App } from '@extension/storage/lib/constants/apps'; import Timer from './Timer'; import ThemeSwitcher from './components/ThemeSwitcher'; @@ -21,7 +21,32 @@ const Popup = ({ zenSettings }: { zenSettings: ZenSettings }) => { const [searchResults, setSearchResults] = useState([]); const shouldShowTimer = zenSettings.timerActive || zenSettings.currentSession === zenSettings.sessions + 1; // Show timer when timer is active or when the last session is done - const handleSearch = (e: React.ChangeEvent) => { + // Add new function to format and validate URLs + const formatUrl = (input: string): string | null => { + try { + // Add protocol if missing + if (!input.match(/^https?:\/\//)) { + input = 'https://' + input; + } + const url = new URL(input); + return url.origin; + } catch { + return null; + } + }; + + // Add function to fetch favicon + const getFavicon = async (url: string): Promise => { + try { + const domain = new URL(url).origin; + // Fallback to Google's favicon service if none of the above work + return `https://www.google.com/s2/favicons?domain=${domain}&size=32`; + } catch { + return 'default-favicon.ico'; // You should add a default favicon + } + }; + + const handleSearch = async (e: React.ChangeEvent) => { const searchTerm = e.target.value; setNewBlockedApp(searchTerm); @@ -30,24 +55,53 @@ const Popup = ({ zenSettings }: { zenSettings: ZenSettings }) => { return; } - const filtered = availableApps.filter( - app => app.name.toLowerCase().includes(searchTerm.toLowerCase()) && !blockedApps.includes(app.name), + // First, check predefined apps + const filteredApps = availableApps.filter( + app => + app.name.toLowerCase().includes(searchTerm.toLowerCase()) && + !blockedApps.some(blockedApp => blockedApp.url === app.url), ); - setSearchResults(filtered); + + // Then, check if input might be a URL + const formattedUrl = formatUrl(searchTerm); // TODO: this check is not working + if (formattedUrl && !filteredApps.some(app => app.url === formattedUrl)) { + const favicon = await getFavicon(formattedUrl); + const customApp: App = { + name: new URL(formattedUrl).hostname, + icon: favicon, + url: formattedUrl, + }; + setSearchResults([...filteredApps, customApp]); + } else { + setSearchResults(filteredApps); + } }; - const handleAddApp = (appName: string) => { - if (!blockedApps.includes(appName)) { - zenStorage.addBlockedApp(appName); - setBlockedApps([...blockedApps, appName]); - setNewBlockedApp(''); - setSearchResults([]); + const handleAddApp = async (url: string) => { + const defaultApp = availableApps.find(app => app.url === url); + if (defaultApp) { + const formattedUrl = formatUrl(defaultApp.url); + if (formattedUrl && !blockedApps.some(app => app.url === formattedUrl)) { + zenStorage.addBlockedApp({ ...defaultApp }); + setBlockedApps([...blockedApps, defaultApp]); + } + } else { + const formattedUrl = formatUrl(newBlockedApp); + if (formattedUrl) { + const favicon = await getFavicon(formattedUrl); + const isAdded = await zenStorage.addBlockedApp({ name: formattedUrl, icon: favicon, url: formattedUrl }); + if (isAdded) { + setBlockedApps([...blockedApps, { name: formattedUrl, icon: favicon, url: formattedUrl }]); + } + } } + setNewBlockedApp(''); + setSearchResults([]); }; - const handleRemoveBlockedApp = (appToRemove: string) => { - zenStorage.removeBlockedApp(appToRemove); - setBlockedApps(blockedApps.filter(app => app !== appToRemove)); + const handleRemoveBlockedApp = (appUrl: string) => { + zenStorage.removeBlockedApp(appUrl); + setBlockedApps(blockedApps.filter(app => app.url !== appUrl)); }; // Show timer view if timer is active or explicitly set @@ -175,8 +229,8 @@ const Popup = ({ zenSettings }: { zenSettings: ZenSettings }) => { // TODO: state.blockApps should contain the urls