14 changed files with 258 additions and 74 deletions
@ -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)} |
||||||
|
/> |
||||||
|
</> |
||||||
|
); |
||||||
|
} |
||||||
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -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> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -0,0 +1,3 @@ |
|||||||
|
export default function isPushNotificationSupported() { |
||||||
|
return 'serviceWorker' in navigator && 'PushManager' in window; |
||||||
|
} |
||||||
@ -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('/'); |
|
||||||
}); |
|
||||||
Loading…
Reference in new issue