owncast/webroot/js/components/notification.js

393 lines
13 KiB
JavaScript
Raw Normal View History

import { h } from '/js/web_modules/preact.js';
import { useState, useEffect } from '/js/web_modules/preact/hooks.js';
import htm from '/js/web_modules/htm.js';
import { ExternalActionButton } from './external-action-modal.js';
import {
registerWebPushNotifications,
isPushNotificationSupported,
} from '../notification/registerWeb.js';
import {
URL_REGISTER_NOTIFICATION,
URL_REGISTER_EMAIL_NOTIFICATION,
HAS_DISPLAYED_NOTIFICATION_MODAL_KEY,
USER_VISIT_COUNT_KEY,
USER_DISMISSED_ANNOYING_NOTIFICATION_POPUP_KEY,
} from '../utils/constants.js';
import { setLocalStorage, getLocalStorage } from '../utils/helpers.js';
const html = htm.bind(h);
export function NotifyModal({ notifications, streamName, accessToken }) {
const [error, setError] = useState(null);
const [loaderStyle, setLoaderStyle] = useState('none');
const [emailNotificationsButtonEnabled, setEmailNotificationsButtonEnabled] =
useState(false);
const [emailAddress, setEmailAddress] = useState(null);
const emailNotificationButtonState = emailNotificationsButtonEnabled
? ''
: 'cursor-not-allowed opacity-50';
const [browserPushPermissionsPending, setBrowserPushPermissionsPending] =
useState(false);
const { browser } = notifications;
const { publicKey } = browser;
const browserPushEnabled = browser.enabled && isPushNotificationSupported();
let emailEnabled = false;
// Store that the user has opened the notifications modal at least once
// so we don't ever need to remind them to do it again.
useEffect(() => {
setLocalStorage(HAS_DISPLAYED_NOTIFICATION_MODAL_KEY, true);
}, []);
async function saveNotificationRegistration(channel, destination) {
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ channel: channel, destination: destination }),
};
try {
await fetch(
URL_REGISTER_NOTIFICATION + `?accessToken=${accessToken}`,
options
);
} catch (e) {
console.error(e);
}
}
async function startBrowserPushRegistration() {
// If it's already denied or granted, don't do anything.
if (Notification.permission !== 'default') {
return;
}
setBrowserPushPermissionsPending(true);
try {
const subscription = await registerWebPushNotifications(publicKey);
saveNotificationRegistration('BROWSER_PUSH_NOTIFICATION', subscription);
setError(null);
} catch (e) {
setError(
`Error registering for live notifications: ${e.message}. Make sure you're not inside a private browser environment or have previously disabled notifications for this stream.`
);
}
setBrowserPushPermissionsPending(false);
}
async function handlePushToggleChange() {
// Nothing can be done if they already denied access.
if (Notification.permission === 'denied') {
return;
}
if (!pushEnabled) {
startBrowserPushRegistration();
}
}
async function registerForEmailButtonPressed() {
try {
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ emailAddress: emailAddress }),
};
try {
await fetch(
URL_REGISTER_EMAIL_NOTIFICATION + `?accessToken=${accessToken}`,
options
);
} catch (e) {
console.error(e);
}
} catch (e) {
setError(`Error registering for email notifications: ${e.message}.`);
}
}
function onEmailInput(e) {
const { value } = e.target;
// TODO: Add validation for email
const valid = true;
setEmailAddress(value);
setEmailNotificationsButtonEnabled(valid);
}
function getBrowserPushButtonText() {
let pushNotificationButtonText = html`<span id="push-notification-arrow"
></span
>
CLICK TO ENABLE`;
if (browserPushPermissionsPending) {
pushNotificationButtonText = '↑ ACCEPT THE BROWSER PERMISSIONS';
} else if (Notification.permission === 'granted') {
pushNotificationButtonText = 'ENABLED';
} else if (Notification.permission === 'denied') {
pushNotificationButtonText = 'DENIED. PLEASE FIX BROWSER PERMISSIONS.';
}
return pushNotificationButtonText;
}
const pushEnabled = Notification.permission === 'granted';
return html`
<div class="bg-gray-100 bg-center bg-no-repeat p-6">
<div
style=${{ display: emailEnabled ? 'grid' : 'none' }}
class="grid grid-cols-2 gap-10 px-5 py-8"
>
<div>
<h2 class="text-slate-600 text-2xl mb-2 font-semibold">
Email Notifications
</h2>
<h2>
Get notified directly to your email when this stream goes live.
</h2>
</div>
<div>
<div class="font-semibold">Enter your email address:</div>
<input
class="border bg-white rounded-l w-8/12 mt-2 mb-1 mr-1 py-2 px-3 text-indigo-700 leading-tight focus:outline-none focus:shadow-outline"
value=${emailAddress}
onInput=${onEmailInput}
placeholder="streamlover42@gmail.com"
/>
<button
class="rounded-sm inline px-3 py-2 text-base text-white bg-indigo-700 ${emailNotificationButtonState}"
onClick=${registerForEmailButtonPressed}
>
Save
</button>
<div class="text-sm mt-3 text-gray-700">
Stop receiving emails any time by clicking the unsubscribe link in
the email. <a href="">Learn more.</a>
</div>
</div>
</div>
<hr
style=${{ display: pushEnabled && emailEnabled ? 'block' : 'none' }}
/>
<div
class="grid grid-cols-2 gap-10 px-5 py-8"
style=${{ display: browserPushEnabled ? 'grid' : 'none' }}
>
<div>
<div>
<div
class="text-sm border-2 p-4 border-red-300"
style=${{
display:
Notification.permission === 'denied' ? 'block' : 'none',
}}
>
Browser notification permissions were denied. Please visit your
browser settings to re-enable in order to get notifications.
</div>
<div
class="form-check form-switch"
style=${{
display:
Notification.permission === 'denied' ? 'none' : 'block',
}}
>
<div
class="relative inline-block w-10 align-middle select-none transition duration-200 ease-in"
>
<input
checked=${pushEnabled || browserPushPermissionsPending}
disabled=${pushEnabled}
type="checkbox"
name="toggle"
id="toggle"
onchange=${handlePushToggleChange}
class="toggle-checkbox absolute block w-8 h-8 rounded-full bg-white border-4 appearance-none cursor-pointer"
/>
<label
for="toggle"
style=${{ width: '50px' }}
class="toggle-label block overflow-hidden h-8 rounded-full bg-gray-300 cursor-pointer"
></label>
</div>
<div class="ml-8 text-xs inline-block text-gray-700">
${getBrowserPushButtonText()}
</div>
</div>
</div>
<h2 class="text-slate-600 text-2xl mt-4 mb-2 font-semibold">
Browser Notifications
</h2>
<h2>
Get notified right in the browser each time this stream goes live.
</h2>
</div>
<div>
<div
class="text-sm mt-3"
style=${{ display: !pushEnabled ? 'none' : 'block' }}
>
To disable push notifications from ${window.location.hostname}
${' '} access your browser permissions for this site and turn off
notifications.
<div style=${{ 'margin-top': '5px' }}>
<a href="">Learn more.</a>
</div>
</div>
<div
id="browser-push-preview-box"
class="w-full bg-white p-4 m-2 mt-4"
style=${{ display: pushEnabled ? 'none' : 'block' }}
>
<div class="text-lg text-gray-700 ml-2 my-2">
${window.location.toString()} wants to
</div>
<div class="text-sm text-gray-700 my-2">
<svg
class="mr-3"
style=${{ display: 'inline-block' }}
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14 12.3333V13H2V12.3333L3.33333 11V7C3.33333 4.93333 4.68667 3.11333 6.66667 2.52667C6.66667 2.46 6.66667 2.4 6.66667 2.33333C6.66667 1.97971 6.80714 1.64057 7.05719 1.39052C7.30724 1.14048 7.64638 1 8 1C8.35362 1 8.69276 1.14048 8.94281 1.39052C9.19286 1.64057 9.33333 1.97971 9.33333 2.33333C9.33333 2.4 9.33333 2.46 9.33333 2.52667C11.3133 3.11333 12.6667 4.93333 12.6667 7V11L14 12.3333ZM9.33333 13.6667C9.33333 14.0203 9.19286 14.3594 8.94281 14.6095C8.69276 14.8595 8.35362 15 8 15C7.64638 15 7.30724 14.8595 7.05719 14.6095C6.80714 14.3594 6.66667 14.0203 6.66667 13.6667"
fill="#676670"
/>
</svg>
Show notifications
</div>
<div class="flex flex-row justify-end">
<button
class="bg-blue-500 py-1 px-4 mr-4 rounded-sm text-white"
onClick=${startBrowserPushRegistration}
>
Allow
</button>
<button
class="bg-slate-200 py-1 px-4 rounded-sm text-gray-500 cursor-not-allowed"
style=${{
'outline-width': 1,
'outline-color': '#e2e8f0',
'outline-style': 'solid',
}}
>
Block
</button>
</div>
</div>
<p
class="text-gray-700 text-sm mt-6"
style=${{ display: pushEnabled ? 'none' : 'block' }}
>
You'll need to allow your browser to receive notifications from
${' '} ${streamName}, first.
</p>
</div>
</div>
<div
id="follow-loading-spinner-container"
style="display: ${loaderStyle}"
>
<img id="follow-loading-spinner" src="/img/loading.gif" />
<p class="text-gray-700 text-lg">Contacting your server.</p>
<p class="text-gray-600 text-lg">Please wait...</p>
</div>
</div>
`;
}
export function NotifyButton({ serverName, onClick }) {
const hasDisplayedNotificationModal = getLocalStorage(
HAS_DISPLAYED_NOTIFICATION_MODAL_KEY
);
const hasPreviouslyDismissedAnnoyingPopup = getLocalStorage(
USER_DISMISSED_ANNOYING_NOTIFICATION_POPUP_KEY
);
let visits = parseInt(getLocalStorage(USER_VISIT_COUNT_KEY));
if (isNaN(visits)) {
visits = 0;
}
// Only show the annoying popup if the user has never opened the notification
// modal previously _and_ they've visited more than 3 times.
const [showPopup, setShowPopup] = useState(
!hasPreviouslyDismissedAnnoyingPopup &&
!hasDisplayedNotificationModal &&
visits > 3
);
const notifyAction = {
color: 'rgba(219, 223, 231, 1)',
description: `Never miss a stream! Get notified when ${serverName} goes live.`,
icon: '/img/notification-bell.svg',
openExternally: false,
};
const buttonClicked = (e) => {
onClick(e);
setShowPopup(false);
};
const notifyPopupDismissedClicked = (e) => {
e.stopPropagation();
setShowPopup(false);
setLocalStorage(USER_DISMISSED_ANNOYING_NOTIFICATION_POPUP_KEY, true);
};
return html`
<span id="notify-button-container" class="relative">
<div
id="notify-button-popup"
class="text-gray-200 p-4 rounded-md cursor-pointer"
style=${{ display: showPopup ? 'block' : 'none' }}
onClick=${buttonClicked}
>
2022-04-17 21:59:42 +03:00
<div class="flex justify-between items-center mb-2">
<div class="font-bold">Stay updated!</div>
<button
class="popout-close-button rounded-md p-1 color-gray-500"
onClick=${notifyPopupDismissedClicked}
>
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</button>
</div>
<div>Click and never miss future streams!</div>
</div>
<${ExternalActionButton}
onClick=${buttonClicked}
action=${notifyAction}
/>
</span>
`;
}