From 16849791876ed1a57e8e84c559a4130fa9dcebe4 Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Sun, 29 May 2022 21:52:38 -0700 Subject: [PATCH] Add working but unstyled notify registration modal --- .../action-buttons/ActionButton.tsx | 1 + .../action-buttons/FollowButton.tsx | 31 +++++ .../action-buttons/NotifyButton.tsx | 30 +++++ .../BrowserNotifyModal.module.scss | 48 +++++++ .../BrowserNotify/BrowserNotifyModal.tsx | 123 ++++++++++++++++++ web/components/modals/BrowserNotifyModal.tsx | 36 ----- .../connected-client-info-handler.ts | 1 - web/components/ui/Content/Content.tsx | 13 +- web/components/ui/Modal/Modal.tsx | 6 +- .../NotifyReminderPopup.tsx | 6 +- web/stories/BrowserNotifyModal.stories.tsx | 9 +- web/utils/browserPushNotifications.ts | 3 + web/utils/localStorage.ts | 1 + webroot/serviceWorker.js | 24 ---- 14 files changed, 258 insertions(+), 74 deletions(-) create mode 100644 web/components/action-buttons/FollowButton.tsx create mode 100644 web/components/action-buttons/NotifyButton.tsx create mode 100644 web/components/modals/BrowserNotify/BrowserNotifyModal.module.scss create mode 100644 web/components/modals/BrowserNotify/BrowserNotifyModal.tsx delete mode 100644 web/components/modals/BrowserNotifyModal.tsx create mode 100644 web/utils/browserPushNotifications.ts delete mode 100644 webroot/serviceWorker.js diff --git a/web/components/action-buttons/ActionButton.tsx b/web/components/action-buttons/ActionButton.tsx index 336fe68cd..47565360e 100644 --- a/web/components/action-buttons/ActionButton.tsx +++ b/web/components/action-buttons/ActionButton.tsx @@ -31,6 +31,7 @@ export default function ActionButton({ title={description || title} url={url} visible={showModal} + height="80vh" handleCancel={() => setShowModal(false)} /> diff --git a/web/components/action-buttons/FollowButton.tsx b/web/components/action-buttons/FollowButton.tsx new file mode 100644 index 000000000..710a584c5 --- /dev/null +++ b/web/components/action-buttons/FollowButton.tsx @@ -0,0 +1,31 @@ +import { Button } from 'antd'; +import { HeartFilled } from '@ant-design/icons'; +import { useState } from 'react'; +import Modal from '../ui/Modal/Modal'; +import s from './ActionButton.module.scss'; + +export default function FollowButton() { + const [showModal, setShowModal] = useState(false); + + const buttonClicked = () => { + setShowModal(true); + }; + + return ( + <> + + setShowModal(false)} + /> + + ); +} diff --git a/web/components/action-buttons/NotifyButton.tsx b/web/components/action-buttons/NotifyButton.tsx new file mode 100644 index 000000000..5e0b709d4 --- /dev/null +++ b/web/components/action-buttons/NotifyButton.tsx @@ -0,0 +1,30 @@ +import { Button } from 'antd'; +import { NotificationFilled } from '@ant-design/icons'; +import { useState } from 'react'; +import Modal from '../ui/Modal/Modal'; +import s from './ActionButton.module.scss'; +import BrowserNotifyModal from '../modals/BrowserNotify/BrowserNotifyModal'; + +export default function NotifyButton() { + const [showModal, setShowModal] = useState(false); + + const buttonClicked = () => { + setShowModal(true); + }; + + return ( + <> + + setShowModal(false)}> + + + + ); +} diff --git a/web/components/modals/BrowserNotify/BrowserNotifyModal.module.scss b/web/components/modals/BrowserNotify/BrowserNotifyModal.module.scss new file mode 100644 index 000000000..16544618e --- /dev/null +++ b/web/components/modals/BrowserNotify/BrowserNotifyModal.module.scss @@ -0,0 +1,48 @@ +.pushPreview { + border-style: dashed; + border-width: 2px; + width: 30vw; + + .inner { + margin: 10px; + padding: 15px; + background-color: white; + box-shadow: 2px 6px 7px 0px #87898d; + + .title { + font-weight: bold; + } + + .permissionLine { + margin-top: 5px; + } + + .buttonRow { + display: flex; + flex-direction: row; + justify-content: flex-end; + + .disabled { + cursor: not-allowed; + outline-width: 1; + outline-color: '#e2e8f0'; + outline-style: 'solid'; + background-color: white; + } + + .allow { + background-color: var(--theme-primary-color); + } + + button { + margin-left: 10px; + padding-left: 15px; + padding-right: 15px; + padding-top: 4px; + padding-bottom: 4px; + border-radius: 3px; + border-style: solid; + } + } + } +} diff --git a/web/components/modals/BrowserNotify/BrowserNotifyModal.tsx b/web/components/modals/BrowserNotify/BrowserNotifyModal.tsx new file mode 100644 index 000000000..6299c0081 --- /dev/null +++ b/web/components/modals/BrowserNotify/BrowserNotifyModal.tsx @@ -0,0 +1,123 @@ +import { Row, Col, Spin, Typography, Button } from 'antd'; +import React, { useState } from 'react'; +import { useRecoilValue } from 'recoil'; +import { accessTokenAtom, clientConfigStateAtom } from '../../stores/ClientConfigStore'; +import { + registerWebPushNotifications, + saveNotificationRegistration, +} from '../../../services/notifications-service'; +import s from './BrowserNotifyModal.module.scss'; +import isPushNotificationSupported from '../../../utils/browserPushNotifications'; + +const { Title } = Typography; + +function NotificationsNotSupported() { + return
Browser notifications are not supported in your browser.
; +} + +function NotificationsEnabled() { + return
Notifications enabled
; +} + +interface PermissionPopupPreviewProps { + start: () => void; +} +function PermissionPopupPreview(props: PermissionPopupPreviewProps) { + const { start } = props; + + return ( +
+
+
{window.location.toString()} wants to
+
+ + + + Show notifications +
+
+ + +
+
+
+ ); +} + +export default function BrowserNotifyModal() { + const [error, setError] = useState(null); + const accessToken = useRecoilValue(accessTokenAtom); + const config = useRecoilValue(clientConfigStateAtom); + const [browserPushPermissionsPending, setBrowserPushPermissionsPending] = + useState(false); + const notificationsPermitted = + isPushNotificationSupported() && Notification.permission !== 'default'; + + const { notifications } = config; + const { browser } = notifications; + const { publicKey } = browser; + + const browserPushSupported = browser.enabled && isPushNotificationSupported(); + + if (notificationsPermitted) { + return ; + } + + const startBrowserPushRegistration = async () => { + // If it's already denied or granted, don't do anything. + if (isPushNotificationSupported() && Notification.permission !== 'default') { + return; + } + + setBrowserPushPermissionsPending(true); + try { + const subscription = await registerWebPushNotifications(publicKey); + saveNotificationRegistration('BROWSER_PUSH_NOTIFICATION', subscription, accessToken); + 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); + }; + + if (!browserPushSupported) { + return ; + } + + return ( + + + Browser Notifications + Get notified right in the browser each time this stream goes live. Blah blah blah more + description text goes here. + + {error} + + + startBrowserPushRegistration()} /> + + + + ); +} diff --git a/web/components/modals/BrowserNotifyModal.tsx b/web/components/modals/BrowserNotifyModal.tsx deleted file mode 100644 index e487858fd..000000000 --- a/web/components/modals/BrowserNotifyModal.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint-disable react/no-unescaped-entities */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { Row, Col, Switch, Typography } from 'antd'; -import { useState } from 'react'; - -const { Title } = Typography; - -// interface Props {} - -export default function BrowserNotifyModal() { - const [enabled, setEnabled] = useState(false); - - const onSwitchToggle = (checked: Boolean) => { - setEnabled(true); - }; - - return ( -
- - - {' '} - {enabled ? 'Enabled' : 'Disabled'} - - - You'll need to allow your browser to receive notifications from Owncast Nightly, first. - Fake push notification prompt example goes here. - - - - Browser Notifications - Get notified right in the browser each time this stream goes live. Blah blah blah more - description text goes here. - -
- ); -} diff --git a/web/components/stores/eventhandlers/connected-client-info-handler.ts b/web/components/stores/eventhandlers/connected-client-info-handler.ts index 221c68ae9..9cce6719d 100644 --- a/web/components/stores/eventhandlers/connected-client-info-handler.ts +++ b/web/components/stores/eventhandlers/connected-client-info-handler.ts @@ -4,7 +4,6 @@ export default function handleConnectedClientInfoMessage( message: ConnectedClientInfoEvent, setChatDisplayName: (string) => void, ) { - console.log('connected client', message); const { user } = message; const { displayName } = user; setChatDisplayName(displayName); diff --git a/web/components/ui/Content/Content.tsx b/web/components/ui/Content/Content.tsx index 34d4e1f9c..07be48316 100644 --- a/web/components/ui/Content/Content.tsx +++ b/web/components/ui/Content/Content.tsx @@ -1,7 +1,6 @@ /* eslint-disable react/no-danger */ import { useRecoilValue } from 'recoil'; -import { Layout, Button, Tabs, Spin } from 'antd'; -import { NotificationFilled, HeartFilled } from '@ant-design/icons'; +import { Layout, Tabs, Spin } from 'antd'; import { clientConfigStateAtom, chatMessagesAtom, @@ -31,6 +30,8 @@ import ServerLogo from '../Logo/Logo'; import CategoryIcon from '../CategoryIcon/CategoryIcon'; import OfflineBanner from '../OfflineBanner/OfflineBanner'; import { AppStateOptions } from '../../stores/application-state'; +import FollowButton from '../../action-buttons/FollowButton'; +import NotifyButton from '../../action-buttons/NotifyButton'; const { TabPane } = Tabs; const { Content } = Layout; @@ -88,17 +89,13 @@ export default function ContentComponent() {
{externalActionButtons} - + {}} notificationClosed={() => {}} > - + diff --git a/web/components/ui/Modal/Modal.tsx b/web/components/ui/Modal/Modal.tsx index 6e43a0b6e..6149260d7 100644 --- a/web/components/ui/Modal/Modal.tsx +++ b/web/components/ui/Modal/Modal.tsx @@ -10,15 +10,16 @@ interface Props { handleCancel?: () => void; afterClose?: () => void; children?: ReactNode; + height?: string; } export default function Modal(props: Props) { - const { title, url, visible, handleOk, handleCancel, afterClose, children } = props; + const { title, url, visible, handleOk, handleCancel, afterClose, height, children } = props; const [loading, setLoading] = useState(!!url); const modalStyle = { padding: '0px', - height: '80vh', + height: height || '40vh', }; const iframe = url && ( @@ -69,4 +70,5 @@ Modal.defaultProps = { handleOk: undefined, handleCancel: undefined, afterClose: undefined, + height: undefined, }; diff --git a/web/components/ui/NotifyReminderPopup/NotifyReminderPopup.tsx b/web/components/ui/NotifyReminderPopup/NotifyReminderPopup.tsx index 074b0b500..f6d9e356d 100644 --- a/web/components/ui/NotifyReminderPopup/NotifyReminderPopup.tsx +++ b/web/components/ui/NotifyReminderPopup/NotifyReminderPopup.tsx @@ -1,6 +1,7 @@ import { Popover } from 'antd'; import { CloseOutlined } from '@ant-design/icons'; import React, { useState, useEffect } from 'react'; +import { LOCAL_STORAGE_KEYS, getLocalStorage } from '../../../utils/localStorage'; import s from './NotifyReminderPopup.module.scss'; interface Props { @@ -14,9 +15,11 @@ export default function NotifyReminderPopup(props: Props) { const { children, visible, notificationClicked, notificationClosed } = props; const [visiblePopup, setVisiblePopup] = useState(visible); const [mounted, setMounted] = useState(false); + const [shouldShowPopup, setShouldShowPopup] = useState(false); useEffect(() => { setMounted(true); + setShouldShowPopup(!getLocalStorage(LOCAL_STORAGE_KEYS.hasDisplayedNotificationModal)); }, []); const title =
Stay updated!
; @@ -52,7 +55,8 @@ export default function NotifyReminderPopup(props: Props) {
); return ( - mounted && ( + mounted && + shouldShowPopup && ( ( @@ -32,7 +33,11 @@ export default { } as ComponentMeta; // eslint-disable-next-line @typescript-eslint/no-unused-vars -const Template: ComponentStory = args => ; +const Template: ComponentStory = args => ( + + + +); // eslint-disable-next-line @typescript-eslint/no-unused-vars export const Basic = Template.bind({}); diff --git a/web/utils/browserPushNotifications.ts b/web/utils/browserPushNotifications.ts new file mode 100644 index 000000000..20f22804a --- /dev/null +++ b/web/utils/browserPushNotifications.ts @@ -0,0 +1,3 @@ +export default function isPushNotificationSupported() { + return 'serviceWorker' in navigator && 'PushManager' in window; +} diff --git a/web/utils/localStorage.ts b/web/utils/localStorage.ts index 5d4c6baa0..bbf075e9d 100644 --- a/web/utils/localStorage.ts +++ b/web/utils/localStorage.ts @@ -1,5 +1,6 @@ export const LOCAL_STORAGE_KEYS = { username: 'username', + hasDisplayedNotificationModal: 'HAS_DISPLAYED_NOTIFICATION_MODAL', }; export function getLocalStorage(key) { diff --git a/webroot/serviceWorker.js b/webroot/serviceWorker.js deleted file mode 100644 index 8a6f3320c..000000000 --- a/webroot/serviceWorker.js +++ /dev/null @@ -1,24 +0,0 @@ -self.addEventListener('activate', (event) => { - console.log('Owncast service worker activated', event); -}); - -self.addEventListener('install', (event) => { - console.log('installing Owncast service worker...', event); -}); - -self.addEventListener('push', (event) => { - const data = JSON.parse(event.data.text()); - const { title, body, icon, tag } = data; - const options = { - title: title || 'Live!', - body: body || 'This live stream has started.', - icon: icon || '/logo/external', - tag: tag, - }; - - event.waitUntil(self.registration.showNotification(options.title, options)); -}); - -self.addEventListener('notificationclick', (event) => { - clients.openWindow('/'); -});