Skip to content

Commit

Permalink
feat(settings): user can disabled plausible analytics
Browse files Browse the repository at this point in the history
  • Loading branch information
Stéphane committed Nov 2, 2023
1 parent f6ddf23 commit b18df0e
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 2 deletions.
3 changes: 2 additions & 1 deletion .env.dist
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
REACT_APP_API_URL=http://localhost:3001
REACT_APP_API_URL=https://holoplay-serverless.vercel.app
REACT_APP_PLAUSIBLE_ANALYTICS=true
1 change: 0 additions & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,5 @@
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<script defer data-domain="holoplay.io" src="https://plausible.holoplay.io/js/script.js"></script>
</body>
</html>
2 changes: 2 additions & 0 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { userAgent } from "../utils/userAgent";
import "./App.css";
import { AppUpdate } from "./AppUpdate";
import { Main } from "./Main";
import { Scripts } from "./Script";

export const App = () => {
return (
Expand Down Expand Up @@ -66,6 +67,7 @@ export const App = () => {
<DrawerPlayerContainer />
<PlayerContainer />
<MobileNavigationContainer />
<Scripts />
</Flex>
</AppShell>
</MantineProvider>
Expand Down
12 changes: 12 additions & 0 deletions src/components/Script.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { memo, useMemo } from "react";
import { useScript } from "../hooks/useScript";
import { useSettings } from "../providers/Settings";

const PLAUSIBLE_INSTANCE_SCRIPT_URL = "https://plausible.holoplay.io/js/script.js";

export const Scripts = memo(() => {
const settings = useSettings();
const analyticsEnabled = useMemo(() => process.env.REACT_APP_PLAUSIBLE_ANALYTICS === "true" && settings.analytics, [settings.analytics]);
useScript(analyticsEnabled ? PLAUSIBLE_INSTANCE_SCRIPT_URL : null);
return null
});
38 changes: 38 additions & 0 deletions src/components/SwitchPlausibleAnalytics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { memo } from "react"
import { useSetSettings, useSettings } from "../providers/Settings";
import { Alert, Switch } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { db } from "../database";
import type { Settings } from "../types/interfaces/Settings";

export const SwitchPlausibleAnalytics = memo(() => {
const settings = useSettings();
const setSettings = useSetSettings();
const { t } = useTranslation();

const handleChange = () => {
const analytics = !settings.analytics;
db.update("settings", { ID: 1 }, (data: Settings) => ({
analytics
}));
db.commit();
setSettings((previousState) => ({
...previousState,
analytics,
}));
}

return (
<>
<Alert mb="lg">
{t('settings.general.analytics.info')} <a href="https://plausible.holoplay.io/holoplay.io" target="_blank" rel="noreferrer">{t('settings.general.analytics.link')}</a>
</Alert>
<Switch
size="md"
checked={settings.analytics}
label={t("settings.general.analytics.label")}
onChange={handleChange}
/>
</>
)
});
10 changes: 10 additions & 0 deletions src/database/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ const initDb = () => {
db.commit();
}

if (!db.columnExists("settings", "analytics")) {
db.alterTable("settings", "analytics", true);
db.commit();

db.update("settings", { ID: 1 }, (data: Settings) => ({
analytics: true
}));
db.commit();
}

if (!db.tableExists("migrations")) {
db.createTable("migrations", ["createdAt", "name"]);
}
Expand Down
112 changes: 112 additions & 0 deletions src/hooks/useScript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { useEffect, useState } from 'react'

// Source: https://usehooks-ts.com/react-hook/use-script

export type UseScriptStatus = 'idle' | 'loading' | 'ready' | 'error'
export interface UseScriptOptions {
shouldPreventLoad?: boolean
removeOnUnmount?: boolean
}

const cachedScriptStatuses: Record<string, UseScriptStatus | undefined> = {}

const getScriptNode = (src: string) => {
const node: HTMLScriptElement | null = document.querySelector(
`script[src="${src}"]`,
)
const status = node?.getAttribute('data-status') as
| UseScriptStatus
| undefined

return {
node,
status,
}
}

export const useScript = (
src: string | null,
options?: UseScriptOptions,
): UseScriptStatus => {
const [status, setStatus] = useState<UseScriptStatus>(() => {
if (!src || options?.shouldPreventLoad) {
return 'idle'
}

if (typeof window === 'undefined') {
// SSR Handling - always return 'loading'
return 'loading'
}

return cachedScriptStatuses[src] ?? 'loading'
})

useEffect(() => {
if (!src || options?.shouldPreventLoad) {
return
}

const cachedScriptStatus = cachedScriptStatuses[src]
if (cachedScriptStatus === 'ready' || cachedScriptStatus === 'error') {
// If the script is already cached, set its status immediately
setStatus(cachedScriptStatus)
return
}

// Fetch existing script element by src
// It may have been added by another instance of this hook
const script = getScriptNode(src)
let scriptNode = script.node

if (!scriptNode) {
// Create script element and add it to document body
scriptNode = document.createElement('script')
scriptNode.src = src
scriptNode.async = true
scriptNode.setAttribute('data-status', 'loading')
document.body.appendChild(scriptNode)

// Store status in attribute on script
// This can be read by other instances of this hook
const setAttributeFromEvent = (event: Event) => {
const scriptStatus: UseScriptStatus =
event.type === 'load' ? 'ready' : 'error'

scriptNode?.setAttribute('data-status', scriptStatus)
}

scriptNode.addEventListener('load', setAttributeFromEvent)
scriptNode.addEventListener('error', setAttributeFromEvent)
} else {
// Grab existing script status from attribute and set to state.
setStatus(script.status ?? cachedScriptStatus ?? 'loading')
}

// Script event handler to update status in state
// Note: Even if the script already exists we still need to add
// event handlers to update the state for *this* hook instance.
const setStateFromEvent = (event: Event) => {
const newStatus = event.type === 'load' ? 'ready' : 'error'
setStatus(newStatus)
cachedScriptStatuses[src] = newStatus
}

// Add event listeners
scriptNode.addEventListener('load', setStateFromEvent)
scriptNode.addEventListener('error', setStateFromEvent)

// Remove event listeners on cleanup
return () => {
if (scriptNode) {
scriptNode.removeEventListener('load', setStateFromEvent)
scriptNode.removeEventListener('error', setStateFromEvent)
}

if (scriptNode && options?.removeOnUnmount) {
scriptNode.remove()
}
}
}, [src, options?.shouldPreventLoad, options?.removeOnUnmount])

return status
}
15 changes: 15 additions & 0 deletions src/pages/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { SelectInvidiousInstance } from "../components/SelectInvidiousInstance";
import { SponsorBlockSettings } from "../components/SponsorBlockSettings";
import { SwitchVideoMode } from "../components/SwitchVideoMode";
import { useStorage } from "../hooks/useStorage";
import { SwitchPlausibleAnalytics } from "../components/SwitchPlausibleAnalytics";

export const SettingsPage = memo(() => {
const { t } = useTranslation();
Expand Down Expand Up @@ -59,13 +60,27 @@ const GeneralItem = memo(() => {
<ChangeLanguage />
<Divider mt="md" mb="lg" />
<SwitchColorScheme />
<AnalyticsItem />
<Divider mt="md" mb="lg" />
<StorageEstimate />
</Accordion.Panel>
</Accordion.Item>
);
});

const AnalyticsItem = memo(() => {
if (process.env.REACT_APP_PLAUSIBLE_ANALYTICS !== "true") {
return null;
}

return (
<>
<Divider mt="md" mb="lg" />
<SwitchPlausibleAnalytics />
</>
)
})

const StorageEstimate = memo(() => {
const storage = useStorage();
const hasUsage = useMemo(
Expand Down
3 changes: 3 additions & 0 deletions src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
"settings.general.darkmode": "Use dark mode",
"settings.general.storage.available.storage": "Available storage",
"settings.general.storage.usage": "You've used {{percentage}} of the available storage",
"settings.general.analytics.label": "Plausible Analytics",
"settings.general.analytics.info": "Plausible is intuitive and open source web analytics. No cookies and fully compliant with GDPR, CCPA and PECR.",
"settings.general.analytics.link": "See HoloPlay Plausible instance",
"settings.player.title": "Player",
"settings.player.description": "Player settings",
"settings.player.video.mode.title": "Video mode",
Expand Down
3 changes: 3 additions & 0 deletions src/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
"settings.general.darkmode": "Mode sombre",
"settings.general.storage.available.storage": "Stockage disponible",
"settings.general.storage.usage": "Vous utilisez {{percentage}} du stockage disponible",
"settings.general.analytics.label": "Plausible Analytics",
"settings.general.analytics.info": "Plausible est une analyse Web intuitive et open source. Pas de cookies et entièrement conforme au RGPD, CCPA et PECR.",
"settings.general.analytics.link": "Voir l'instance Plausible de HoloPlay",
"settings.player.title": "Player",
"settings.player.description": "Paramètrage du player",
"settings.player.video.mode.title": "Mode vidéo",
Expand Down
1 change: 1 addition & 0 deletions src/types/interfaces/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export interface Settings {
deviceId: string;
sponsorBlock: boolean;
sponsorBlockCategories: string[];
analytics: boolean;
}

0 comments on commit b18df0e

Please sign in to comment.