Add working but unstyled notify registration modal
This commit is contained in:
@@ -31,6 +31,7 @@ export default function ActionButton({
|
|||||||
title={description || title}
|
title={description || title}
|
||||||
url={url}
|
url={url}
|
||||||
visible={showModal}
|
visible={showModal}
|
||||||
|
height="80vh"
|
||||||
handleCancel={() => setShowModal(false)}
|
handleCancel={() => setShowModal(false)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
31
web/components/action-buttons/FollowButton.tsx
Normal file
31
web/components/action-buttons/FollowButton.tsx
Normal file
@@ -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 (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
className={`${s.button}`}
|
||||||
|
icon={<HeartFilled />}
|
||||||
|
onClick={buttonClicked}
|
||||||
|
>
|
||||||
|
Follow
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
title="Follow <servername>"
|
||||||
|
visible={showModal}
|
||||||
|
handleCancel={() => setShowModal(false)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
web/components/action-buttons/NotifyButton.tsx
Normal file
30
web/components/action-buttons/NotifyButton.tsx
Normal file
@@ -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 (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
className={`${s.button}`}
|
||||||
|
icon={<NotificationFilled />}
|
||||||
|
onClick={buttonClicked}
|
||||||
|
>
|
||||||
|
Notify
|
||||||
|
</Button>
|
||||||
|
<Modal title="Notify" visible={showModal} handleCancel={() => setShowModal(false)}>
|
||||||
|
<BrowserNotifyModal />
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
123
web/components/modals/BrowserNotify/BrowserNotifyModal.tsx
Normal file
123
web/components/modals/BrowserNotify/BrowserNotifyModal.tsx
Normal file
@@ -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 <div>Browser notifications are not supported in your browser.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NotificationsEnabled() {
|
||||||
|
return <div>Notifications enabled</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PermissionPopupPreviewProps {
|
||||||
|
start: () => void;
|
||||||
|
}
|
||||||
|
function PermissionPopupPreview(props: PermissionPopupPreviewProps) {
|
||||||
|
const { start } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="browser-push-preview-box" className={s.pushPreview}>
|
||||||
|
<div className={s.inner}>
|
||||||
|
<div className={s.title}>{window.location.toString()} wants to</div>
|
||||||
|
<div className={s.permissionLine}>
|
||||||
|
<svg
|
||||||
|
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 className={s.buttonRow}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
className={s.allow}
|
||||||
|
onClick={() => {
|
||||||
|
start();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Allow
|
||||||
|
</Button>
|
||||||
|
<button type="button" className={s.disabled}>
|
||||||
|
Block
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BrowserNotifyModal() {
|
||||||
|
const [error, setError] = useState<string>(null);
|
||||||
|
const accessToken = useRecoilValue(accessTokenAtom);
|
||||||
|
const config = useRecoilValue(clientConfigStateAtom);
|
||||||
|
const [browserPushPermissionsPending, setBrowserPushPermissionsPending] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
const notificationsPermitted =
|
||||||
|
isPushNotificationSupported() && Notification.permission !== 'default';
|
||||||
|
|
||||||
|
const { notifications } = config;
|
||||||
|
const { browser } = notifications;
|
||||||
|
const { publicKey } = browser;
|
||||||
|
|
||||||
|
const browserPushSupported = browser.enabled && isPushNotificationSupported();
|
||||||
|
|
||||||
|
if (notificationsPermitted) {
|
||||||
|
return <NotificationsEnabled />;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <NotificationsNotSupported />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Spin spinning={browserPushPermissionsPending}>
|
||||||
|
<Row align="top">
|
||||||
|
<Title>Browser Notifications</Title>
|
||||||
|
Get notified right in the browser each time this stream goes live. Blah blah blah more
|
||||||
|
description text goes here.
|
||||||
|
</Row>
|
||||||
|
<Row>{error}</Row>
|
||||||
|
<Row align="top">
|
||||||
|
<Col span={12}>
|
||||||
|
<PermissionPopupPreview start={() => startBrowserPushRegistration()} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Spin>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
|
||||||
<div>
|
|
||||||
<Row align="top">
|
|
||||||
<Col span={12}>
|
|
||||||
<Switch defaultChecked={enabled} checked={enabled} onChange={onSwitchToggle} />{' '}
|
|
||||||
{enabled ? 'Enabled' : 'Disabled'}
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
You'll need to allow your browser to receive notifications from Owncast Nightly, first.
|
|
||||||
Fake push notification prompt example goes here.
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row align="top">
|
|
||||||
<Title>Browser Notifications</Title>
|
|
||||||
Get notified right in the browser each time this stream goes live. Blah blah blah more
|
|
||||||
description text goes here.
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ export default function handleConnectedClientInfoMessage(
|
|||||||
message: ConnectedClientInfoEvent,
|
message: ConnectedClientInfoEvent,
|
||||||
setChatDisplayName: (string) => void,
|
setChatDisplayName: (string) => void,
|
||||||
) {
|
) {
|
||||||
console.log('connected client', message);
|
|
||||||
const { user } = message;
|
const { user } = message;
|
||||||
const { displayName } = user;
|
const { displayName } = user;
|
||||||
setChatDisplayName(displayName);
|
setChatDisplayName(displayName);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/* eslint-disable react/no-danger */
|
/* eslint-disable react/no-danger */
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { Layout, Button, Tabs, Spin } from 'antd';
|
import { Layout, Tabs, Spin } from 'antd';
|
||||||
import { NotificationFilled, HeartFilled } from '@ant-design/icons';
|
|
||||||
import {
|
import {
|
||||||
clientConfigStateAtom,
|
clientConfigStateAtom,
|
||||||
chatMessagesAtom,
|
chatMessagesAtom,
|
||||||
@@ -31,6 +30,8 @@ import ServerLogo from '../Logo/Logo';
|
|||||||
import CategoryIcon from '../CategoryIcon/CategoryIcon';
|
import CategoryIcon from '../CategoryIcon/CategoryIcon';
|
||||||
import OfflineBanner from '../OfflineBanner/OfflineBanner';
|
import OfflineBanner from '../OfflineBanner/OfflineBanner';
|
||||||
import { AppStateOptions } from '../../stores/application-state';
|
import { AppStateOptions } from '../../stores/application-state';
|
||||||
|
import FollowButton from '../../action-buttons/FollowButton';
|
||||||
|
import NotifyButton from '../../action-buttons/NotifyButton';
|
||||||
|
|
||||||
const { TabPane } = Tabs;
|
const { TabPane } = Tabs;
|
||||||
const { Content } = Layout;
|
const { Content } = Layout;
|
||||||
@@ -88,17 +89,13 @@ export default function ContentComponent() {
|
|||||||
<div className={s.buttonsLogoTitleSection}>
|
<div className={s.buttonsLogoTitleSection}>
|
||||||
<ActionButtonRow>
|
<ActionButtonRow>
|
||||||
{externalActionButtons}
|
{externalActionButtons}
|
||||||
<Button type="primary" icon={<HeartFilled />}>
|
<FollowButton />
|
||||||
Follow
|
|
||||||
</Button>
|
|
||||||
<NotifyReminderPopup
|
<NotifyReminderPopup
|
||||||
visible
|
visible
|
||||||
notificationClicked={() => {}}
|
notificationClicked={() => {}}
|
||||||
notificationClosed={() => {}}
|
notificationClosed={() => {}}
|
||||||
>
|
>
|
||||||
<Button type="primary" icon={<NotificationFilled />}>
|
<NotifyButton />
|
||||||
Notify
|
|
||||||
</Button>
|
|
||||||
</NotifyReminderPopup>
|
</NotifyReminderPopup>
|
||||||
</ActionButtonRow>
|
</ActionButtonRow>
|
||||||
|
|
||||||
|
|||||||
@@ -10,15 +10,16 @@ interface Props {
|
|||||||
handleCancel?: () => void;
|
handleCancel?: () => void;
|
||||||
afterClose?: () => void;
|
afterClose?: () => void;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
height?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Modal(props: Props) {
|
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 [loading, setLoading] = useState(!!url);
|
||||||
|
|
||||||
const modalStyle = {
|
const modalStyle = {
|
||||||
padding: '0px',
|
padding: '0px',
|
||||||
height: '80vh',
|
height: height || '40vh',
|
||||||
};
|
};
|
||||||
|
|
||||||
const iframe = url && (
|
const iframe = url && (
|
||||||
@@ -69,4 +70,5 @@ Modal.defaultProps = {
|
|||||||
handleOk: undefined,
|
handleOk: undefined,
|
||||||
handleCancel: undefined,
|
handleCancel: undefined,
|
||||||
afterClose: undefined,
|
afterClose: undefined,
|
||||||
|
height: undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Popover } from 'antd';
|
import { Popover } from 'antd';
|
||||||
import { CloseOutlined } from '@ant-design/icons';
|
import { CloseOutlined } from '@ant-design/icons';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { LOCAL_STORAGE_KEYS, getLocalStorage } from '../../../utils/localStorage';
|
||||||
import s from './NotifyReminderPopup.module.scss';
|
import s from './NotifyReminderPopup.module.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -14,9 +15,11 @@ export default function NotifyReminderPopup(props: Props) {
|
|||||||
const { children, visible, notificationClicked, notificationClosed } = props;
|
const { children, visible, notificationClicked, notificationClosed } = props;
|
||||||
const [visiblePopup, setVisiblePopup] = useState(visible);
|
const [visiblePopup, setVisiblePopup] = useState(visible);
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [shouldShowPopup, setShouldShowPopup] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
|
setShouldShowPopup(!getLocalStorage(LOCAL_STORAGE_KEYS.hasDisplayedNotificationModal));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const title = <div className={s.title}>Stay updated!</div>;
|
const title = <div className={s.title}>Stay updated!</div>;
|
||||||
@@ -52,7 +55,8 @@ export default function NotifyReminderPopup(props: Props) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
mounted && (
|
mounted &&
|
||||||
|
shouldShowPopup && (
|
||||||
<Popover
|
<Popover
|
||||||
placement="topLeft"
|
placement="topLeft"
|
||||||
defaultVisible={visiblePopup}
|
defaultVisible={visiblePopup}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||||
import BrowserNotifyModal from '../components/modals/BrowserNotifyModal';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
import BrowserNotifyModal from '../components/modals/BrowserNotify/BrowserNotifyModal';
|
||||||
import BrowserNotifyModalMock from './assets/mocks/notify-modal.png';
|
import BrowserNotifyModalMock from './assets/mocks/notify-modal.png';
|
||||||
|
|
||||||
const Example = () => (
|
const Example = () => (
|
||||||
@@ -32,7 +33,11 @@ export default {
|
|||||||
} as ComponentMeta<typeof BrowserNotifyModal>;
|
} as ComponentMeta<typeof BrowserNotifyModal>;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const Template: ComponentStory<typeof BrowserNotifyModal> = args => <Example />;
|
const Template: ComponentStory<typeof BrowserNotifyModal> = args => (
|
||||||
|
<RecoilRoot>
|
||||||
|
<Example />
|
||||||
|
</RecoilRoot>
|
||||||
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export const Basic = Template.bind({});
|
export const Basic = Template.bind({});
|
||||||
|
|||||||
3
web/utils/browserPushNotifications.ts
Normal file
3
web/utils/browserPushNotifications.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function isPushNotificationSupported() {
|
||||||
|
return 'serviceWorker' in navigator && 'PushManager' in window;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export const LOCAL_STORAGE_KEYS = {
|
export const LOCAL_STORAGE_KEYS = {
|
||||||
username: 'username',
|
username: 'username',
|
||||||
|
hasDisplayedNotificationModal: 'HAS_DISPLAYED_NOTIFICATION_MODAL',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getLocalStorage(key) {
|
export function getLocalStorage(key) {
|
||||||
|
|||||||
@@ -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('/');
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user