Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/** @jsx jsx */
import { jsx, css } from '@emotion/core';

import { NotificationContainer } from '../NotificationContainer';
import { NotificationContainer } from '../Notifications/NotificationContainer';

import { SideBar } from './SideBar';
import { RightPanel } from './RightPanel';
Expand Down
73 changes: 44 additions & 29 deletions Composer/packages/client/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,24 @@ import {
import composerIcon from '../images/composerIcon.svg';
import { AppUpdaterStatus } from '../constants';

import { NotificationButton } from './Notifications/NotificationButton';

// -------------------- Styles -------------------- //

const headerContainer = css`
position: relative;
background: ${SharedColors.cyanBlue10};
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding-right: 20px;
margin: auto;
`;

const logo = css`
display: flex;
`;

const title = css`
margin-left: 20px;
font-weight: ${FontWeights.semibold};
font-size: 16px;
color: #fff;
Expand All @@ -50,17 +55,20 @@ const divider = css`
margin: 0px 0px 0px 20px;
`;

const updateAvailableIcon = {
const controls = css`
display: flex;
align-items: center;
`;

const buttonStyles: IButtonStyles = {
icon: {
color: '#FFF',
fontSize: '20px',
},
root: {
position: 'absolute',
height: '20px',
width: '20px',
top: 'calc(50% - 10px)',
right: '20px',
marginLeft: '16px',
},
rootHovered: {
backgroundColor: 'transparent',
Expand All @@ -74,6 +82,8 @@ const headerTextContainer = css`
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
margin-left: 20px;
`;

// -------------------- Header -------------------- //
Expand All @@ -94,29 +104,34 @@ export const Header = () => {

return (
<div css={headerContainer} role="banner">
<img
alt={formatMessage('Composer Logo')}
aria-label={formatMessage('Composer Logo')}
src={composerIcon}
style={{ marginLeft: '9px' }}
/>
<div css={headerTextContainer}>
<div css={title}>{formatMessage('Bot Framework Composer')}</div>
{projectName && (
<Fragment>
<div css={divider} />
<span css={botName}>{`${projectName} (${locale})`}</span>
</Fragment>
<div css={logo}>
<img
alt={formatMessage('Composer Logo')}
aria-label={formatMessage('Composer Logo')}
src={composerIcon}
style={{ marginLeft: '9px' }}
/>
<div css={headerTextContainer}>
<div css={title}>{formatMessage('Bot Framework Composer')}</div>
{projectName && (
<Fragment>
<div css={divider} />
<span css={botName}>{`${projectName} (${locale})`}</span>
</Fragment>
)}
</div>
</div>
<div css={controls}>
{showUpdateAvailableIcon && (
<IconButton
iconProps={{ iconName: 'History' }}
styles={buttonStyles}
title={formatMessage('Update available')}
onClick={onUpdateAvailableClick}
/>
)}
<NotificationButton buttonStyles={buttonStyles} />
</div>
{showUpdateAvailableIcon && (
<IconButton
iconProps={{ iconName: 'History' }}
styles={updateAvailableIcon as IButtonStyles}
title={formatMessage('Update available')}
onClick={onUpdateAvailableClick}
/>
)}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/** @jsx jsx */
import { css, jsx } from '@emotion/core';
import React, { useState } from 'react';
import { FontWeights } from '@uifabric/styling';
import { IButtonStyles, IconButton } from 'office-ui-fabric-react/lib/Button';
import { NeutralColors, SharedColors } from '@uifabric/fluent-theme';
import { useRecoilValue } from 'recoil';
import formatMessage from 'format-message';

import { notificationsSelector } from '../../recoilModel/selectors/notificationsSelector';
import { dispatcherState } from '../../recoilModel';

import { NotificationPanel } from './NotificationPanel';

const styles = {
container: css`
position: relative;
`,
count: (visible?: boolean) => css`
background-color: ${NeutralColors.white};
border: 2px solid ${SharedColors.cyanBlue10};
border-radius: 100%;
color: ${SharedColors.cyanBlue10};
font-size: 8px;
font-weight: ${FontWeights.bold};
height: 12px;
right: -4px;
position: absolute;
text-align: center;
visibility: ${visible ? 'visible' : 'hidden'};
width: 12px;
`,
};

type NotificationButtonProps = {
buttonStyles?: IButtonStyles;
};

const NotificationButton: React.FC<NotificationButtonProps> = ({ buttonStyles }) => {
const [isOpen, setIsOpen] = useState(false);
const { deleteNotification, markNotificationAsRead } = useRecoilValue(dispatcherState);
const notifications = useRecoilValue(notificationsSelector);
const unreadNotification = notifications.filter(({ read }) => !read);

const toggleIsOpen = () => {
if (!isOpen) {
notifications.map(({ id }) => markNotificationAsRead(id));
}
setIsOpen(!isOpen);
};

return (
<div aria-label={formatMessage('Open notification panel')}>
<IconButton iconProps={{ iconName: 'Ringer' }} styles={buttonStyles} onClick={toggleIsOpen}>
<div css={styles.container}>
<div aria-hidden css={styles.count(!isOpen && !!unreadNotification.length)}>
{unreadNotification.length}
</div>
</div>
</IconButton>
<NotificationPanel
isOpen={isOpen}
notifications={notifications}
onDeleteNotification={deleteNotification}
onDismiss={toggleIsOpen}
/>
</div>
);
};

export { NotificationButton };
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@

/** @jsx jsx */
import { jsx, css, keyframes } from '@emotion/core';
import React from 'react';
import React, { useState } from 'react';
import { IconButton, ActionButton } from 'office-ui-fabric-react/lib/Button';
import { useEffect, useRef, useState } from 'react';
import { useRef } from 'react';
import { FontSizes } from '@uifabric/fluent-theme';
import { Shimmer, ShimmerElementType } from 'office-ui-fabric-react/lib/Shimmer';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import formatMessage from 'format-message';

import Timer from '../utils/timer';
import { useInterval } from '../../utils/hooks';

// -------------------- Styles -------------------- //

Expand Down Expand Up @@ -129,13 +129,16 @@ export type CardProps = {
description?: string;
retentionTime?: number;
link?: Link;
read?: boolean;
hidden?: boolean;
onRenderCardContent?: (props: CardProps) => JSX.Element;
};

export type NotificationProps = {
id: string;
cardProps: CardProps;
onDismiss: (id: string) => void;
onHide?: (id: string) => void;
};

const defaultCardContentRenderer = (props: CardProps) => {
Expand All @@ -161,50 +164,38 @@ const defaultCardContentRenderer = (props: CardProps) => {
};

export const NotificationCard = React.memo((props: NotificationProps) => {
const { cardProps, id, onDismiss } = props;
const [show, setShow] = useState(true);
const { cardProps, id, onDismiss, onHide } = props;
const { hidden, retentionTime = null } = cardProps;

const [delay, setDelay] = useState(retentionTime || null);
const containerRef = useRef<HTMLDivElement>(null);

const removeNotification = () => {
setShow(false);
typeof onHide === 'function' && onHide(id);
setDelay(null);
};

// notification will disappear in 5 secs
const timer = useRef(cardProps.retentionTime ? new Timer(removeNotification, cardProps.retentionTime) : null).current;

useEffect(() => {
return () => {
if (timer) {
timer.clear();
}
};
}, []);
useInterval(removeNotification, delay);

const handleMouseOver = () => {
// if mouse over stop the time and record the remaining time
if (timer) {
timer.pause();
if (retentionTime) {
setDelay(null);
}
};

const handleMouseLeave = () => {
if (timer) {
timer.resume();
if (retentionTime) {
setDelay(retentionTime);
}
};

const handleAnimationEnd = () => {
if (!show) onDismiss(id);
};

const renderCard = cardProps.onRenderCardContent || defaultCardContentRenderer;

return (
<div
ref={containerRef}
css={cardContainer(show, containerRef.current)}
css={cardContainer(!hidden, containerRef.current)}
role="presentation"
onAnimationEnd={handleAnimationEnd}
onFocus={() => void 0}
onMouseLeave={handleMouseLeave}
onMouseOver={handleMouseOver}
Expand All @@ -213,7 +204,7 @@ export const NotificationCard = React.memo((props: NotificationProps) => {
ariaLabel={formatMessage('Close')}
css={cancelButton}
iconProps={{ iconName: 'Cancel', styles: { root: { fontSize: '12px' } } }}
onClick={removeNotification}
onClick={() => onDismiss(id)}
/>
{renderCard(cardProps)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
/** @jsx jsx */
import { jsx, css } from '@emotion/core';
import { useRecoilValue } from 'recoil';
import React from 'react';

import { dispatcherState } from '../recoilModel';
import { notificationsSelector } from '../recoilModel/selectors/notificationsSelector';
import { dispatcherState } from '../../recoilModel';
import { notificationsSelector } from '../../recoilModel/selectors/notificationsSelector';

import { NotificationCard } from './NotificationCard';

Expand All @@ -22,15 +21,23 @@ const container = css`

// -------------------- NotificationContainer -------------------- //

export const NotificationContainer = React.memo(() => {
export const NotificationContainer = () => {
const notifications = useRecoilValue(notificationsSelector);
const { deleteNotification } = useRecoilValue(dispatcherState);
const { deleteNotification, hideNotification } = useRecoilValue(dispatcherState);

return (
<div css={container} role="presentation">
{notifications.map((item) => {
return <NotificationCard key={item.id} cardProps={item} id={item.id} onDismiss={deleteNotification} />;
return (
<NotificationCard
key={item.id}
cardProps={item}
id={item.id}
onDismiss={deleteNotification}
onHide={hideNotification}
/>
);
})}
</div>
);
});
};
Loading