Improve notifications UX (#337)

Improve notification UX
This commit is contained in:
Andrew Prifer 2022-11-13 14:37:24 +01:00 committed by GitHub
parent a8a9b5ef05
commit a55a34d48f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 59 additions and 39 deletions

View file

@ -11,6 +11,8 @@ import type {
import {useVal} from '@theatre/react' import {useVal} from '@theatre/react'
import getStudio from './getStudio' import getStudio from './getStudio'
import {marked} from 'marked' import {marked} from 'marked'
import useTooltip from './uiComponents/Popover/useTooltip'
import MinimalTooltip from './uiComponents/Popover/MinimalTooltip'
/** /**
* Creates a string key unique to a notification with a certain title and message. * Creates a string key unique to a notification with a certain title and message.
@ -181,9 +183,9 @@ const COLORS = {
const IndicatorDot = styled.div<{type: NotificationType}>` const IndicatorDot = styled.div<{type: NotificationType}>`
display: flex; display: flex;
align-items: center;
justify-content: center; justify-content: center;
margin-left: 12px; margin-left: 12px;
padding-top: 21px;
::before { ::before {
content: ''; content: '';
@ -308,7 +310,7 @@ const NotifierContainer = styled.div`
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
position: fixed; position: fixed;
right: 8px; right: 92px;
top: 50px; top: 50px;
width: 500px; width: 500px;
height: 85vh; height: 85vh;
@ -330,27 +332,31 @@ const NotificationScroller = styled.div`
` `
const EmptyState = styled.div` const EmptyState = styled.div`
align-self: flex-end;
width: fit-content; width: fit-content;
padding: 12px; padding: 8px;
border-radius: 4px; border-radius: 4px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
${pointerEventsAutoInNormalMode};
background-color: rgba(40, 43, 47, 0.8);
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.25), 0 2px 6px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(14px);
color: #b4b4b4; color: #b4b4b4;
font-size: 12px; font-size: 12px;
line-height: 1.4; line-height: 1.4;
@supports not (backdrop-filter: blur()) {
background: rgba(40, 43, 47, 0.95);
}
` `
//endregion //endregion
export const useEmptyNotificationsTooltip = () => {
const {hasNotifications} = useNotifications()
return useTooltip({enabled: !hasNotifications}, () => (
<MinimalTooltip>
<EmptyState>
<NotificationTitle>No notifications</NotificationTitle>
Notifications will appear here when you get them.
</EmptyState>
</MinimalTooltip>
))
}
/** /**
* The component responsible for rendering the notifications. * The component responsible for rendering the notifications.
*/ */
@ -363,26 +369,26 @@ export const Notifier = () => {
return ( return (
<NotifierContainer> <NotifierContainer>
{!pinNotifications ? null : toasts.length > 0 ? ( {!pinNotifications
<NotificationScroller onMouseEnter={startPause} onMouseLeave={endPause}> ? null
<div> : toasts.length > 0 && (
{toasts.map((toast) => { <NotificationScroller
return ( onMouseEnter={startPause}
<div key={toast.id}> onMouseLeave={endPause}
{/* message is always a function in our case */} >
{/* @ts-ignore */} <div>
{toast.message(toast)} {toasts.map((toast) => {
</div> return (
) <div key={toast.id}>
})} {/* message is always a function in our case */}
</div> {/* @ts-ignore */}
</NotificationScroller> {toast.message(toast)}
) : ( </div>
<EmptyState> )
<NotificationTitle>No notifications</NotificationTitle> })}
Notifications will appear here when you get them. </div>
</EmptyState> </NotificationScroller>
)} )}
<ButtonContainer align="side"> <ButtonContainer align="side">
{pinNotifications && toasts.length > 0 && ( {pinNotifications && toasts.length > 0 && (
<Button <Button

View file

@ -20,7 +20,10 @@ import DoubleChevronRight from '@theatre/studio/uiComponents/icons/DoubleChevron
import ToolbarIconButton from '@theatre/studio/uiComponents/toolbar/ToolbarIconButton' import ToolbarIconButton from '@theatre/studio/uiComponents/toolbar/ToolbarIconButton'
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover' import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
import MoreMenu from './MoreMenu/MoreMenu' import MoreMenu from './MoreMenu/MoreMenu'
import {useNotifications} from '@theatre/studio/notify' import {
useNotifications,
useEmptyNotificationsTooltip,
} from '@theatre/studio/notify'
const Container = styled.div` const Container = styled.div`
height: 36px; height: 36px;
@ -147,6 +150,9 @@ const GlobalToolbar: React.FC = () => {
const {hasNotifications} = useNotifications() const {hasNotifications} = useNotifications()
const [notificationsTooltip, notificationsTriggerRef] =
useEmptyNotificationsTooltip()
return ( return (
<Container> <Container>
<SubContainer> <SubContainer>
@ -174,7 +180,9 @@ const GlobalToolbar: React.FC = () => {
<ExtensionToolbar showLeftDivider toolbarId="global" /> <ExtensionToolbar showLeftDivider toolbarId="global" />
</SubContainer> </SubContainer>
<SubContainer> <SubContainer>
<ToolbarIconButton {notificationsTooltip}
<PinButton
ref={notificationsTriggerRef as $IntentionalAny}
onClick={() => { onClick={() => {
getStudio().transaction(({stateEditors, drafts}) => { getStudio().transaction(({stateEditors, drafts}) => {
stateEditors.studio.ahistoric.setPinNotifications( stateEditors.studio.ahistoric.setPinNotifications(
@ -182,10 +190,13 @@ const GlobalToolbar: React.FC = () => {
) )
}) })
}} }}
icon={<Bell />}
pinHintIcon={<Bell />}
unpinHintIcon={<Bell />}
pinned={useVal(getStudio().atomP.ahistoric.pinNotifications) ?? false}
> >
<Bell />
{hasNotifications && <HasUpdatesBadge type="warning" />} {hasNotifications && <HasUpdatesBadge type="warning" />}
</ToolbarIconButton> </PinButton>
{moreMenu.node} {moreMenu.node}
<ToolbarIconButton <ToolbarIconButton
ref={moreMenuTriggerRef} ref={moreMenuTriggerRef}

View file

@ -20,7 +20,10 @@ interface PinButtonProps extends ComponentPropsWithRef<'button'> {
} }
const PinButton = forwardRef<HTMLButtonElement, PinButtonProps>( const PinButton = forwardRef<HTMLButtonElement, PinButtonProps>(
({hint, pinned, icon, pinHintIcon, unpinHintIcon, ...props}, ref) => { (
{children, hint, pinned, icon, pinHintIcon, unpinHintIcon, ...props},
ref,
) => {
const [hovered, setHovered] = useState(false) const [hovered, setHovered] = useState(false)
const showHint = hovered || hint const showHint = hovered || hint
@ -48,6 +51,7 @@ const PinButton = forwardRef<HTMLButtonElement, PinButtonProps>(
? unpinHintIcon ? unpinHintIcon
: icon} : icon}
</div> </div>
{children}
</Container> </Container>
) )
}, },

View file

@ -12,8 +12,7 @@ function Bell(props: React.SVGProps<SVGSVGElement>) {
> >
<path <path
d="M8 1.57c-.416 0-.752.36-.752.804v.482c-1.715.372-3.006 1.994-3.006 3.938v.473c0 1.18-.407 2.32-1.14 3.205l-.173.208a.85.85 0 00-.125.864.75.75 0 00.686.475h9.019a.752.752 0 00.686-.475.845.845 0 00-.125-.864l-.174-.208a5.026 5.026 0 01-1.139-3.205v-.473c0-1.944-1.291-3.566-3.006-3.938v-.482c0-.445-.336-.804-.752-.804zm1.063 12.39c.282-.301.44-.71.44-1.138H6.496c0 .428.158.837.44 1.138.281.302.664.47 1.063.47.4 0 .783-.168 1.064-.47z" d="M8 1.57c-.416 0-.752.36-.752.804v.482c-1.715.372-3.006 1.994-3.006 3.938v.473c0 1.18-.407 2.32-1.14 3.205l-.173.208a.85.85 0 00-.125.864.75.75 0 00.686.475h9.019a.752.752 0 00.686-.475.845.845 0 00-.125-.864l-.174-.208a5.026 5.026 0 01-1.139-3.205v-.473c0-1.944-1.291-3.566-3.006-3.938v-.482c0-.445-.336-.804-.752-.804zm1.063 12.39c.282-.301.44-.71.44-1.138H6.496c0 .428.158.837.44 1.138.281.302.664.47 1.063.47.4 0 .783-.168 1.064-.47z"
fill="#fff" fill="currentColor"
fillOpacity={0.6}
/> />
</svg> </svg>
) )