@material-ui/core#ClickAwayListener TypeScript Examples

The following examples show how to use @material-ui/core#ClickAwayListener. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: TooltipIconCircle.tsx    From anchor-web-app with Apache License 2.0 6 votes vote down vote up
export function TouchTooltip({
  children,
  title,
  className,
  style,
  placement = 'top',
  ...tooltipProps
}: TooltipIconCircleProps) {
  const [open, setOpen] = useState<boolean>(false);

  const tooltipOpen = useCallback(() => {
    setOpen(true);
  }, []);

  const tooltipClose = useCallback(() => {
    setOpen(false);
  }, []);

  return (
    <ClickAwayListener onClickAway={tooltipClose}>
      <IconCircle style={style} className={className} onClick={tooltipOpen}>
        <Tooltip
          {...tooltipProps}
          open={open}
          onClose={tooltipClose}
          disableFocusListener
          disableHoverListener
          disableTouchListener
          title={title}
          placement={placement}
        >
          <span>{children}</span>
        </Tooltip>
      </IconCircle>
    </ClickAwayListener>
  );
}
Example #2
Source File: TooltipLabel.tsx    From anchor-web-app with Apache License 2.0 6 votes vote down vote up
export function TouchTooltip({
  children,
  title,
  style,
  className,
  placement = 'top',
  ...tooltipProps
}: TooltipLabelProps) {
  const [open, setOpen] = useState<boolean>(false);

  const tooltipOpen = useCallback(() => {
    setOpen(true);
  }, []);

  const tooltipClose = useCallback(() => {
    setOpen(false);
  }, []);

  return (
    <ClickAwayListener onClickAway={tooltipClose}>
      <Label style={style} className={className} onClick={tooltipOpen}>
        <Tooltip
          {...tooltipProps}
          open={open}
          onClose={tooltipClose}
          disableFocusListener
          disableHoverListener
          disableTouchListener
          title={title}
          placement={placement}
        >
          <span>{children}</span>
        </Tooltip>
      </Label>
    </ClickAwayListener>
  );
}
Example #3
Source File: ChainSelector.tsx    From anchor-web-app with Apache License 2.0 6 votes vote down vote up
ChainSelectorBase = (props: UIElementProps) => {
  const { className } = props;

  const [open, setOpen] = useState(false);

  return (
    <ClickAwayListener onClickAway={() => setOpen(false)}>
      <div className={className}>
        <ChainButton onClick={() => setOpen((v) => !v)} />
        {open && (
          <DropdownContainer>
            <DropdownBox>
              <ChainList onClose={() => setOpen((v) => !v)} />
            </DropdownBox>
          </DropdownContainer>
        )}
      </div>
    </ClickAwayListener>
  );
}
Example #4
Source File: DesktopNotification.tsx    From anchor-web-app with Apache License 2.0 6 votes vote down vote up
function DesktopNotificationBase({ className }: DesktopNotificationProps) {
  const { terraWalletAddress } = useAccount();
  const { permission } = useNotification();
  const { liquidationAlert } = useJobs();

  const [openDropdown, setOpenDropdown] = useState<boolean>(false);

  const toggleOpen = useCallback(() => {
    setOpenDropdown((prev) => !prev);
  }, []);

  const onClickAway = useCallback(() => {
    setOpenDropdown(false);
  }, []);

  const visible = useMemo(() => {
    return terraWalletAddress && permission === 'granted';
  }, [permission, terraWalletAddress]);

  return visible ? (
    <ClickAwayListener onClickAway={onClickAway}>
      <div className={className} data-enabled={liquidationAlert.enabled}>
        <div onClick={toggleOpen} className="notification-icon">
          {liquidationAlert.enabled ? <NotificationOn /> : <NotificationOff />}
        </div>

        {openDropdown && (
          <DropdownContainer>
            <DropdownBox className="notification-dropdown-box">
              <NotificationContent />
            </DropdownBox>
          </DropdownContainer>
        )}
      </div>
    </ClickAwayListener>
  ) : null;
}
Example #5
Source File: InfoTooltip.tsx    From anchor-web-app with Apache License 2.0 6 votes vote down vote up
export function TouchTooltip({
  children,
  placement = 'top',
  ...tooltipProps
}: InfoTooltipProps) {
  const [open, setOpen] = useState<boolean>(false);

  const tooltipOpen = useCallback(() => {
    setOpen(true);
  }, []);

  const tooltipClose = useCallback(() => {
    setOpen(false);
  }, []);

  return (
    <ClickAwayListener onClickAway={tooltipClose}>
      <sup onClick={tooltipOpen}>
        <Tooltip
          {...tooltipProps}
          open={open}
          onClose={tooltipClose}
          disableFocusListener
          disableHoverListener
          disableTouchListener
          title={children}
          placement={placement}
        >
          <InfoOutlined />
        </Tooltip>
      </sup>
    </ClickAwayListener>
  );
}
Example #6
Source File: WalletSelector.tsx    From anchor-web-app with Apache License 2.0 5 votes vote down vote up
function WalletSelectorBase(props: WalletSelectorProps) {
  const { walletAddress, initializing, className, onClick, onClose, children } =
    props;

  const { uUST } = useBalances();

  // ---------------------------------------------
  // presentation
  // ---------------------------------------------

  if (initializing) {
    return (
      <div className={className}>
        <ConnectWalletButton initializing={true} onClick={onClick} />
      </div>
    );
  }

  if (!walletAddress) {
    return (
      <ClickAwayListener onClickAway={onClose}>
        <div className={className}>
          <ConnectWalletButton onClick={onClick} />
          {children}
        </div>
      </ClickAwayListener>
    );
  }

  return (
    <ClickAwayListener onClickAway={onClose}>
      <div className={className}>
        <ConnectWalletButton
          walletAddress={walletAddress}
          totalUST={uUST}
          onClick={onClick}
        />
        {children}
      </div>
    </ClickAwayListener>
  );
}
Example #7
Source File: TransactionWidget.tsx    From anchor-web-app with Apache License 2.0 5 votes vote down vote up
TransactionWidgetBase = (props: UIElementProps & { color?: string }) => {
  const theme = useTheme();
  const { className, color = theme.header.textColor } = props;

  const [open, setOpen] = useState(false);
  const { backgroundTransactions } = useBackgroundTransactions();

  const {
    target: { chain },
  } = useDeploymentTarget();

  const navigate = useNavigate();
  const restoreTx = useCallback(() => {
    setOpen(false);
    navigate('/bridge/restore');
  }, [navigate, setOpen]);

  if (backgroundTransactions.length === 0 || chain === Chain.Terra) {
    return null;
  }

  return (
    <ClickAwayListener onClickAway={() => setOpen(false)}>
      <div className={className}>
        <TransactionButton
          color={color}
          backgroundTransactions={backgroundTransactions}
          onClick={() => setOpen((v) => !v)}
          closeWidget={() => setOpen(false)}
        />
        {open && (
          <DropdownContainer className="transaction-dropdown">
            <DropdownBox>
              <TransactionList
                backgroundTransactions={backgroundTransactions}
                onClose={() => setOpen((v) => !v)}
                footer={
                  <div className="restore-tx">
                    <div>Having transaction issues?</div>
                    <BorderButton onClick={restoreTx}>
                      Restore transaction
                    </BorderButton>
                  </div>
                }
              />
            </DropdownBox>
          </DropdownContainer>
        )}
      </div>
    </ClickAwayListener>
  );
}
Example #8
Source File: SQFormDateTimePicker.tsx    From SQForm with MIT License 5 votes vote down vote up
function SQFormDateTimePicker({
  name,
  label,
  size = 'auto',
  isDisabled = false,
  placeholder = '',
  onBlur,
  onChange,
  muiFieldProps = {},
}: SQFormDateTimePickerProps): JSX.Element {
  const {
    formikField: {field, helpers},
    fieldState: {isFieldError, isFieldRequired},
    fieldHelpers: {handleBlur, HelperTextComponent},
  } = useForm<Moment | null, unknown>({
    name,
    onBlur,
  });

  const [isCalendarOpen, setIsCalendarOpen] = React.useState(false);
  const handleClose = () => setIsCalendarOpen(false);
  const toggleCalendar = () => setIsCalendarOpen(!isCalendarOpen);
  const handleClickAway = () => {
    if (isCalendarOpen) {
      setIsCalendarOpen(false);
    }
  };

  const classes = useStyles();

  // An empty string will not reset the DatePicker so we have to pass null
  const value: ParsableDate<Moment> | null =
    (field.value as ParsableDate<Moment>) ?? null;

  const handleChange = (date: Moment | null): void => {
    helpers.setValue(date);
    onChange && onChange(date);
  };

  return (
    <ClickAwayListener onClickAway={handleClickAway}>
      <Grid item sm={size}>
        <DateTimePicker
          label={label}
          disabled={isDisabled}
          value={value}
          onChange={handleChange}
          onClose={handleClose}
          onOpen={toggleCalendar}
          open={isCalendarOpen}
          renderInput={(inputProps) => {
            return (
              <TextField
                {...inputProps}
                name={name}
                color="primary"
                disabled={isDisabled}
                error={isFieldError}
                fullWidth={true}
                InputLabelProps={{shrink: true}}
                FormHelperTextProps={{error: isFieldError}}
                helperText={!isDisabled && HelperTextComponent}
                placeholder={placeholder}
                onBlur={handleBlur}
                onClick={handleClickAway}
                required={isFieldRequired}
                classes={classes}
              />
            );
          }}
          {...muiFieldProps}
        />
      </Grid>
    </ClickAwayListener>
  );
}
Example #9
Source File: index.tsx    From multisig-react with MIT License 5 votes vote down vote up
EllipsisTransactionDetails = ({
  address,
  knownAddress,
  sendModalOpenHandler,
}: EllipsisTransactionDetailsProps): React.ReactElement => {
  const classes = useStyles()
  const [anchorEl, setAnchorEl] = React.useState(null)

  const dispatch = useDispatch()
  const currentSafeAddress = useSelector(safeParamAddressFromStateSelector)

  const handleClick = (event) => setAnchorEl(event.currentTarget)

  const closeMenuHandler = () => setAnchorEl(null)

  const addOrEditEntryHandler = () => {
    dispatch(push(`${SAFELIST_ADDRESS}/${currentSafeAddress}/address-book?entryAddress=${address}`))
    closeMenuHandler()
  }

  return (
    <ClickAwayListener onClickAway={closeMenuHandler}>
      <div className={classes.container} role="menu" tabIndex={0}>
        <MoreHorizIcon onClick={handleClick} onKeyDown={handleClick} />
        <Menu anchorEl={anchorEl} id="simple-menu" keepMounted onClose={closeMenuHandler} open={Boolean(anchorEl)}>
          {sendModalOpenHandler
            ? [
                <MenuItem key="send-again-button" onClick={sendModalOpenHandler}>
                  Send Again
                </MenuItem>,
                <Divider key="divider" />,
              ]
            : null}
          {knownAddress ? (
            <MenuItem onClick={addOrEditEntryHandler}>Edit Address book Entry</MenuItem>
          ) : (
            <MenuItem onClick={addOrEditEntryHandler}>Add to address book</MenuItem>
          )}
        </Menu>
      </div>
    </ClickAwayListener>
  )
}
Example #10
Source File: Menu.tsx    From glific-frontend with GNU Affero General Public License v3.0 5 votes vote down vote up
Menu: React.SFC<MenuProps> = ({
  menus,
  children,
  eventType = 'Click',
  placement = 'top',
}) => {
  const [open, setOpen] = useState(false);
  const anchorRef = useRef<HTMLDivElement>(null);

  const handleOpen = () => {
    setOpen(true);
  };

  const handleClose = () => {
    setOpen(false);
  };

  const menuList = menus.map((menu: any) => (
    <div key={menu.title}>
      <MenuItem
        onClickHandler={() => {
          if (menu.onClick) {
            menu.onClick();
          } else {
            handleClose();
          }
        }}
        {...menu}
      />
    </div>
  ));

  return (
    <div data-testid="Menu">
      <div
        onClick={eventType === 'Click' ? handleOpen : undefined}
        onKeyDown={eventType === 'Click' ? handleOpen : undefined}
        onMouseEnter={eventType === 'MouseEnter' ? handleOpen : undefined}
        onMouseLeave={eventType === 'MouseEnter' ? handleClose : undefined}
        aria-hidden="true"
        ref={anchorRef}
        aria-controls={open ? 'menu-list-grow' : undefined}
        aria-haspopup="true"
      >
        {children}
      </div>

      <Popper
        open={open}
        anchorEl={anchorRef.current}
        role={undefined}
        transition
        disablePortal={placement === 'top'}
        placement={placement}
      >
        {({ TransitionProps }) => (
          <Grow {...TransitionProps}>
            <Paper>
              <ClickAwayListener onClickAway={handleClose}>
                <div
                  onMouseEnter={eventType === 'MouseEnter' ? handleOpen : undefined}
                  onMouseLeave={eventType === 'MouseEnter' ? handleClose : undefined}
                >
                  <MenuList autoFocusItem={open}>{menuList}</MenuList>
                </div>
              </ClickAwayListener>
            </Paper>
          </Grow>
        )}
      </Popper>
    </div>
  );
}
Example #11
Source File: global-search-field.tsx    From mtcute with GNU Lesser General Public License v3.0 4 votes vote down vote up
export function GlobalSearchField({ isMobile }: { isMobile: boolean }): React.ReactElement {
    const classes = useStyles()
    const allObjects: {
        allTlObject: GraphqlAllResponse<ExtendedTlObject>
    } = useStaticQuery(graphql`
        query {
            allTlObject {
                edges {
                    node {
                        id
                        prefix
                        type
                        name
                    }
                }
            }
        }
    `)

    const [includeMtproto, setIncludeMtproto] = useLocalState('mtproto', false)
    const { hits, query, onSearch } = useFuse(
        allObjects.allTlObject.edges,
        {
            keys: ['node.name'],
            includeMatches: true,
            threshold: 0.3,
        },
        { limit: 25 },
        includeMtproto ? undefined : (it) => it.node.prefix !== 'mtproto/'
    )

    const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null)
    const [open, setOpen] = useState(false)

    const notFound = () => (
        <>
            <ErrorOutlineIcon className={classes.popupEmptyIcon} />
            Nothing found
            {!includeMtproto && (
                <Button
                    variant="text"
                    size="small"
                    style={{
                        margin: '4px auto',
                    }}
                    onClick={() => {
                        setIncludeMtproto(true)
                    }}
                >
                    Retry including MTProto objects
                </Button>
            )}
        </>
    )

    const emptyField = () => (
        <>
            <SearchIcon className={classes.popupEmptyIcon} />
            Start typing...
        </>
    )

    const renderSearchItem = (
        node: ExtendedTlObject,
        matches: ReadonlyArray<Fuse.FuseResultMatch>
    ) => (
        <ListItem
            button
            divider
            component={Link}
            to={`/${node.prefix}${node.type}/${node.name}`}
            className={classes.popupListItem}
            onClick={() => setOpen(false)}
            key={node.id}
        >
            <ListItemAvatar>
                <Avatar
                    style={{
                        backgroundColor:
                            node.type === 'class'
                                ? blue[600]
                                : node.type === 'method'
                                ? red[600]
                                : yellow[700],
                    }}
                >
                    {node.type === 'class' ? (
                        <ClassIcon />
                    ) : node.type === 'method' ? (
                        <FunctionsIcon />
                    ) : (
                        <UnionIcon />
                    )}
                </Avatar>
            </ListItemAvatar>
            <ListItemText
                primary={
                    <>
                        {node.prefix}
                        <FuseHighlight
                            matches={matches}
                            value={node.name}
                            className={classes.searchItemMatch}
                        />
                    </>
                }
                secondary={node.type}
            />
        </ListItem>
    )

    const popupContent = (
        <Paper className={classes.popup}>
            {query.length <= 1 || !hits.length ? (
                <div className={classes.popupEmpty}>
                    {query.length <= 1 ? emptyField() : notFound()}
                </div>
            ) : (
                <List disablePadding dense className={classes.popupList}>
                    {hits.map(({ item: { node }, matches }) =>
                        renderSearchItem(node, matches!)
                    )}
                    <div style={{ textAlign: 'center' }}>
                        <Button
                            variant="text"
                            size="small"
                            style={{
                                margin: '4px auto',
                            }}
                            onClick={() => {
                                setIncludeMtproto(!includeMtproto)
                            }}
                        >
                            {includeMtproto ? 'Hide' : 'Include'} MTProto
                            objects
                        </Button>
                    </div>
                </List>
            )}
        </Paper>
    )

    return (
        <ClickAwayListener onClickAway={() => setOpen(false)}>
            <>
                <ActionBarSearchField
                    inputRef={setAnchorEl}
                    autoComplete="off"
                    onFocus={() => setOpen(true)}
                    onBlur={() => setOpen(false)}
                    onChange={onSearch}
                />
                <Popper
                    open={open}
                    anchorEl={anchorEl}
                    placement="bottom"
                    transition
                    style={{
                        width: isMobile ? '100%' : anchorEl?.clientWidth,
                        zIndex: 9999,
                    }}
                >
                    {({ TransitionProps }) => (
                        <Fade {...TransitionProps} timeout={350}>
                            {popupContent}
                        </Fade>
                    )}
                </Popper>
            </>
        </ClickAwayListener>
    )
}
Example #12
Source File: Planet.tsx    From react-planet with MIT License 4 votes vote down vote up
export function Planet(props: Props) {
  const {
    centerContent,
    children,
    open,
    onClick,
    mass,
    tension,
    friction,
    orbitRadius,
    rotation,
    orbitStyle,
    hideOrbit,
    onClose,
    autoClose,
    dragablePlanet,
    dragRadiusPlanet,
    dragableSatellites,
    dragRadiusSatellites,
    bounceRadius,
    bounce,
    bounceOnOpen,
    bounceOnClose,
    bounceDirection,
    satelliteOrientation,
  } = props;
  const classes = useStyles(props);
  const { ref, height = 0, width = 0 } = useResizeObserver();
  const [_open, setOpen] = React.useState(!!open);

  React.useEffect(() => {
    setOpen(!!open);
  }, [open]);

  var satellites: ReactElement<any>[] = [];
  var satelliteCount = React.Children.count(children);
  React.Children.forEach(children, (c, i) => {
    satellites[i] = (
      <Satellite
        key={i}
        index={i}
        open={_open}
        satelliteCount={satelliteCount}
        planetHeight={height}
        planetWidth={width}
        mass={mass ? mass : DEFAULT_MASS}
        friction={friction ? friction : DEFAULT_FRICTION}
        tension={tension ? tension : DEFAULT_TENSTION}
        orbitRadius={orbitRadius ? orbitRadius : DEFAULT_RADIUS}
        rotation={rotation ? rotation : DEFAULT_ROTATION}
        dragable={!!dragableSatellites}
        dragRadius={dragRadiusSatellites}
        orientation={satelliteOrientation}
      >
        {c}
      </Satellite>
    );
  });

  const onPlanet = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
    if (onClick) {
      onClick(e);
    } else {
      if (_open && autoClose) {
        setOpen(false);
        if (onClose) {
          onClose(e);
        }
      } else {
        setOpen(true);
      }
    }
  };

  const onClickAway = (e: React.MouseEvent<Document, MouseEvent>) => {
    if (autoClose) {
      setOpen(false);
    }

    if (onClose && _open) {
      onClose(e);
    }
  };

  return (
    <ClickAwayListener onClickAway={onClickAway}>
      <div className={classes.root}>
        {!hideOrbit && (
          <Orbit
            open={_open}
            orbitStyle={orbitStyle}
            planetHeight={height}
            planetWidth={width}
            mass={mass ? mass : DEFAULT_MASS}
            friction={friction ? friction : DEFAULT_FRICTION}
            tension={tension ? tension : DEFAULT_TENSTION}
            orbitRadius={orbitRadius ? orbitRadius : DEFAULT_RADIUS}
          />
        )}
        <>{satellites}</>
        <div className={classes.planetContent} onClick={onPlanet}>
          <DragableContainer
            on={
              !!dragablePlanet || !!bounce || !!bounceOnOpen || !!bounceOnClose
            }
            dragable={!!dragablePlanet}
            dragRadius={dragRadiusPlanet}
            open={_open}
            bounceRadius={bounceRadius}
            bounceOnOpen={(bounce && !!!bounceOnClose) || bounceOnOpen}
            bounceOnClose={(bounce && !!!bounceOnOpen) || bounceOnClose}
            bounceDirection={bounceDirection}
          >
            <div ref={ref as any}>{centerContent}</div>
          </DragableContainer>
        </div>
      </div>
    </ClickAwayListener>
  );
}
Example #13
Source File: index.tsx    From uno-game with MIT License 4 votes vote down vote up
CardDeck: React.FC<CardDeckProps> = (props) => {
	const { cards, player } = props

	const { gameId } = useParams<{ gameId: string }>()

	const {
		isDraggingAnyCard,
	} = useDragLayer((monitor) => ({
		isDraggingAnyCard: monitor.isDragging(),
	}))

	const cardStore = useCardStore()
	const socketStore = useSocketStore()
	const socket = useSocket()
	const classes = useStyles()
	const customClasses = useCustomStyles({
		limitedNameWidth: 70,
		avatarTimerRemainingPercentage: buildPercentage(
			socketStore.gameRoundRemainingTimeInSeconds as number,
			socketStore.game?.maxRoundDurationInSeconds as number,
		),
	})

	const getCardInclination = (index: number) => {
		const isMiddleCard = Math.round(cards.length / 2) === index
		const isBeforeMiddleCard = index < Math.round(cards.length / 2)

		let inclination: number

		if (isMiddleCard) {
			inclination = 0
		} else if (isBeforeMiddleCard) {
			inclination = -Math.abs(index - Math.round(cards.length / 2))
		} else {
			inclination = Math.abs(Math.round(cards.length / 2) - index)
		}

		const delta = Device.isMobile ? 4 : 3

		return inclination * delta
	}

	const getCardElevation = (index: number) => {
		const isMiddleCard = Math.round(cards.length / 2) === index

		let elevation: number

		if (isMiddleCard) {
			elevation = 0
		} else {
			elevation = -Math.abs(index - Math.round(cards.length / 2))
		}

		const delta = Device.isMobile ? 3 : 7

		return elevation * delta
	}

	const onDragEnd = () => {
		cardStore.setSelectedCards([])
	}

	const isCardSelected = (cardId: string) => !!cardStore?.selectedCards?.some(card => card.id === cardId)

	const canBePartOfCurrentCombo = (cardType: CardTypes) => !!cardStore?.selectedCards?.some(card => card.type === cardType)

	const toggleSelectedCard = (cardId: string) => {
		const lastSelectedCards = cardStore.selectedCards
		const selectedCard = cards.find(card => card.id === cardId)
		const cardOnTopOfCardStack = (socketStore.game as Game).usedCards[0]
		const selectedCardTypes = lastSelectedCards?.map(card => card.type)

		const isAlreadySelected = isCardSelected(cardId)

		if (isAlreadySelected) {
			const cardsWithoutAlreadySelected = lastSelectedCards?.filter(card => card.id !== cardId)

			if (cardOnTopOfCardStack.color === selectedCard?.color) {
				if (cardsWithoutAlreadySelected[0] && cardsWithoutAlreadySelected[0].type === cardOnTopOfCardStack.type) {
					cardStore.setSelectedCards(cardsWithoutAlreadySelected)
				} else {
					cardStore.setSelectedCards([])
				}
			} else {
				cardStore.setSelectedCards(cardsWithoutAlreadySelected)
			}
		} else if ((selectedCard && selectedCardTypes?.includes(selectedCard.type)) || !selectedCardTypes?.length) {
			cardStore.setSelectedCards([
				...(lastSelectedCards || []),
				selectedCard as CardData,
			])
		}
	}

	const unselectAllCards = () => {
		cardStore.setSelectedCards([])
	}

	const handleClickOutsideCardDeck = () => {
		if (cardStore.selectedCards.length > 0) {
			unselectAllCards()
		}
	}

	const toggleOnlineStatus = () => {
		socket.toggleOnlineStatus(gameId)
	}

	return (
		<ClickAwayListener
			onClickAway={handleClickOutsideCardDeck}
		>
			<Grid
				container
				alignItems="flex-end"
				justify="center"
				className={classes.cardContainer}
			>
				<PlayerEffect
					playerId={player?.id}
				/>

				<Zoom in={player?.status === "afk"}>
					<Grid
						container
						className={classes.afkContainer}
					>
						<Grid
							container
							alignItems="center"
							justify="center"
							direction="column"
							className={classes.afkContent}
						>
							<Typography
								variant="body1"
								className={classes.afkInfo}
							>
								We noticed you are afk, so we are making random plays
								{" "}
								automatically for you. In case you want to keep playing by
								{" "}
								yourself, click on the button below.
							</Typography>

							<Divider orientation="horizontal" size={2} />

							<Button
								variant="contained"
								className={classes.afkButton}
								onClick={toggleOnlineStatus}
							>
								I'M HERE
							</Button>
						</Grid>
					</Grid>
				</Zoom>

				<Grid
					container
					className={classes.cardContent}
					style={{
						width: (cards?.length * CARD_WIDTH) + CARD_WIDTH,
					}}
				>
					{cards?.map((card, index) => (
						<DraggableCard
							key={card.id}
							card={card}
							className={classes.card}
							index={index}
							style={{
								transform: `rotate(${getCardInclination(index)}deg)`,
								bottom: getCardElevation(index),
								zIndex: (index + 2),
								left: index * CARD_WIDTH,
							}}
							onClick={() => toggleSelectedCard(card.id)}
							selected={isCardSelected(card.id)}
							isDraggingAnyCard={isDraggingAnyCard}
							isMoreThanOneCardBeingDragged={cardStore?.selectedCards?.length > 1}
							onDragEnd={onDragEnd}
							canBePartOfCurrentCombo={canBePartOfCurrentCombo(card.type)}
						/>
					))}
				</Grid>

				<Grid
					container
					justify="center"
					alignItems="center"
					className={classes.avatarContainer}
					style={{
						opacity: player?.isCurrentRoundPlayer ? 1 : 0.5,
					}}
				>
					<Avatar
						name={player?.name}
						size="small"
						className={player?.isCurrentRoundPlayer ? customClasses.avatarTimer : ""}
					/>

					<Divider orientation="vertical" size={2} />

					<Grid item>
						<Typography
							variant="h3"
							className={`${classes.title} ${customClasses.limitedName}`}
						>
							{player?.name}
						</Typography>

						{player?.id && (
							<Typography
								variant="h2"
								className={classes.description}
							>
								(You)
							</Typography>
						)}
					</Grid>
				</Grid>
			</Grid>
		</ClickAwayListener>
	)
}
Example #14
Source File: SavedSearchToolbar.tsx    From glific-frontend with GNU Affero General Public License v3.0 4 votes vote down vote up
SavedSearchToolbar: React.SFC<SavedSearchToolbarProps> = (props) => {
  const { searchMode, refetchData, savedSearchCriteriaCallback, onSelect } = props;
  const [selectedSavedSearch, setSelectedSavedSearch] = useState<number | null>(null);
  const [optionsSelected, setOptionsSelected] = useState(false);
  const [fixedSearches, setFixedSearches] = useState<any>([]);
  const [searchesCount, setSearchesCount] = useState<any>({});
  const [anchorEl, setAnchorEl] = useState(null);
  const Ref = useRef(null);
  const open = Boolean(anchorEl);
  const variables = { organizationId: getUserSession('organizationId') };

  const { data: collectionCount } = useSubscription(COLLECTION_COUNT_SUBSCRIPTION, { variables });

  const { data: countData } = useQuery<any>(SEARCHES_COUNT, {
    variables,
  });

  useEffect(() => {
    if (countData) {
      const collectionStats = JSON.parse(countData.collectionStats);
      if (collectionStats[variables.organizationId]) {
        setSearchesCount(collectionStats[variables.organizationId]);
      }
    }
  }, [countData]);

  useEffect(() => {
    if (collectionCount) {
      const countDataSubscription = JSON.parse(collectionCount.collectionCount);
      setSearchesCount(countDataSubscription.collection);
    }
  }, [collectionCount]);

  // default query variables
  const queryVariables = {
    filter: { isReserved: true },
    opts: {},
  };

  // remove selected searches on search
  if (searchMode && selectedSavedSearch) {
    setSelectedSavedSearch(null);
  }

  const { loading, error, refetch } = useQuery<any>(SAVED_SEARCH_QUERY, {
    variables: queryVariables,
    onCompleted: (data) => {
      setFixedSearches(data.savedSearches);
    },
  });

  const handlerSavedSearchCriteria = (
    savedSearchCriteria: string | null,
    savedSearchId: number | null
  ) => {
    savedSearchCriteriaCallback(savedSearchCriteria, savedSearchId);
    setSelectedSavedSearch(savedSearchId);
  };

  const handleAdditionalSavedSearch = (search: any) => {
    const replaceSearchIndex = fixedSearches
      .map((savedSearch: any) => savedSearch.id)
      .indexOf(search.id);
    const fixedSearchesCopy = JSON.parse(JSON.stringify(fixedSearches));
    if (replaceSearchIndex !== -1) {
      [fixedSearchesCopy[replaceSearchIndex], fixedSearchesCopy[2]] = [
        fixedSearches[2],
        fixedSearches[replaceSearchIndex],
      ];
      setFixedSearches(fixedSearchesCopy);
    }
    handlerSavedSearchCriteria(search.args, search.id);
  };

  useEffect(() => {
    // display created searches
    if (refetchData.savedSearches) {
      refetch();
      handleAdditionalSavedSearch(refetchData.savedSearches);
    }
  }, [refetchData.savedSearches]);

  if (loading) return <Loading />;
  if (error) {
    setErrorMessage(error);
    return <div>error</div>;
  }

  const savedSearchList = fixedSearches.slice(0, 3).map((savedSearch: any) => {
    // set the selected class if the button is clicked
    const labelClass = [styles.SavedSearchItemLabel];
    const countClass = [styles.SavedSearchCount];
    if (savedSearch.id === selectedSavedSearch) {
      labelClass.push(styles.SavedSearchItemSelected);
      countClass.push(styles.SavedSearchSelectedCount);
    }

    const count = searchesCount[savedSearch.shortcode] ? searchesCount[savedSearch.shortcode] : 0;
    return (
      <div
        data-testid="savedSearchDiv"
        className={styles.SavedSearchItem}
        key={savedSearch.id}
        onClick={() => {
          handlerSavedSearchCriteria(savedSearch.args, savedSearch.id);
          onSelect();
        }}
        onKeyDown={() => {
          handlerSavedSearchCriteria(savedSearch.args, savedSearch.id);
          onSelect();
        }}
        aria-hidden="true"
      >
        <div className={labelClass.join(' ')}>{savedSearch.shortcode}</div>
        <Tooltip title={count} placement="right">
          <div className={countClass.join(' ')}>{numberToAbbreviation(count)}</div>
        </Tooltip>
      </div>
    );
  });

  const handleClickAway = () => {
    setAnchorEl(null);
    setOptionsSelected(false);
  };

  const additionalOptions = (
    <Popper
      open={open}
      anchorEl={anchorEl}
      placement="bottom"
      transition
      className={styles.PopperContainer}
    >
      {({ TransitionProps }) => (
        <Fade {...TransitionProps} timeout={350}>
          <Paper elevation={3} className={styles.Popper}>
            {fixedSearches.slice(3, 6).map((search: any) => {
              const count = searchesCount[search.shortcode] ? searchesCount[search.shortcode] : 0;
              return (
                <div
                  key={search.id}
                  className={styles.LabelContainer}
                  onClick={() => handleAdditionalSavedSearch(search)}
                  aria-hidden="true"
                >
                  <span className={styles.Label}>{search.shortcode}</span>
                  <span className={styles.Count}>{numberToAbbreviation(count)}</span>
                </div>
              );
            })}
          </Paper>
        </Fade>
      )}
    </Popper>
  );

  return (
    <div className={styles.SavedSearchToolbar}>
      <div className={styles.SaveSearchContainer}>{savedSearchList}</div>
      <div className={styles.MoreLink}>
        <ClickAwayListener onClickAway={handleClickAway}>
          <IconButton
            onClick={() => {
              setAnchorEl(Ref.current);
              setOptionsSelected(true);
            }}
            aria-label="more"
            aria-controls="long-menu"
            aria-haspopup="true"
            size="small"
            ref={Ref}
          >
            {optionsSelected ? (
              <OptionsIconSelected className={styles.OptionsIcon} />
            ) : (
              <OptionsIcon className={styles.OptionsIcon} />
            )}
          </IconButton>
        </ClickAwayListener>
        {additionalOptions}
      </div>
    </div>
  );
}
Example #15
Source File: ContactBar.tsx    From glific-frontend with GNU Affero General Public License v3.0 4 votes vote down vote up
ContactBar: React.SFC<ContactBarProps> = (props) => {
  const {
    contactId,
    collectionId,
    contactBspStatus,
    lastMessageTime,
    contactStatus,
    displayName,
    handleAction,
    isSimulator,
  } = props;

  const [anchorEl, setAnchorEl] = useState(null);
  const open = Boolean(anchorEl);
  const history = useHistory();
  const [showCollectionDialog, setShowCollectionDialog] = useState(false);
  const [showFlowDialog, setShowFlowDialog] = useState(false);
  const [showBlockDialog, setShowBlockDialog] = useState(false);
  const [showClearChatDialog, setClearChatDialog] = useState(false);
  const [addContactsDialogShow, setAddContactsDialogShow] = useState(false);
  const [showTerminateDialog, setShowTerminateDialog] = useState(false);
  const { t } = useTranslation();

  // get collection list
  const [getCollections, { data: collectionsData }] = useLazyQuery(GET_COLLECTIONS, {
    variables: setVariables(),
  });

  // get the published flow list
  const [getFlows, { data: flowsData }] = useLazyQuery(GET_FLOWS, {
    variables: setVariables({
      status: FLOW_STATUS_PUBLISHED,
    }),
    fetchPolicy: 'network-only', // set for now, need to check cache issue
  });

  // get contact collections
  const [getContactCollections, { data }] = useLazyQuery(GET_CONTACT_COLLECTIONS, {
    variables: { id: contactId },
    fetchPolicy: 'cache-and-network',
  });

  useEffect(() => {
    if (contactId) {
      getContactCollections();
    }
  }, [contactId]);

  // mutation to update the contact collections
  const [updateContactCollections] = useMutation(UPDATE_CONTACT_COLLECTIONS, {
    onCompleted: (result: any) => {
      const { numberDeleted, contactGroups } = result.updateContactGroups;
      const numberAdded = contactGroups.length;
      let notification = `Added to ${numberAdded} collection${numberAdded === 1 ? '' : 's'}`;
      if (numberDeleted > 0 && numberAdded > 0) {
        notification = `Added to ${numberDeleted} collection${
          numberDeleted === 1 ? '' : 's  and'
        } removed from ${numberAdded} collection${numberAdded === 1 ? '' : 's '}`;
      } else if (numberDeleted > 0) {
        notification = `Removed from ${numberDeleted} collection${numberDeleted === 1 ? '' : 's'}`;
      }
      setNotification(notification);
    },
    refetchQueries: [{ query: GET_CONTACT_COLLECTIONS, variables: { id: contactId } }],
  });

  const [blockContact] = useMutation(UPDATE_CONTACT, {
    onCompleted: () => {
      setNotification(t('Contact blocked successfully.'));
    },
    refetchQueries: [{ query: SEARCH_QUERY, variables: SEARCH_QUERY_VARIABLES }],
  });

  const [addFlow] = useMutation(ADD_FLOW_TO_CONTACT, {
    onCompleted: () => {
      setNotification(t('Flow started successfully.'));
    },
    onError: (error) => {
      setErrorMessage(error);
    },
  });

  const [addFlowToCollection] = useMutation(ADD_FLOW_TO_COLLECTION, {
    onCompleted: () => {
      setNotification(t('Your flow will start in a couple of minutes.'));
    },
  });

  // mutation to clear the chat messages of the contact
  const [clearMessages] = useMutation(CLEAR_MESSAGES, {
    variables: { contactId },
    onCompleted: () => {
      setClearChatDialog(false);
      setNotification(t('Conversation cleared for this contact.'), 'warning');
    },
  });

  let collectionOptions = [];
  let flowOptions = [];
  let initialSelectedCollectionIds: Array<any> = [];
  let selectedCollectionsName;
  let selectedCollections: any = [];
  let assignedToCollection: any = [];

  if (data) {
    const { groups } = data.contact.contact;
    initialSelectedCollectionIds = groups.map((group: any) => group.id);

    selectedCollections = groups.map((group: any) => group.label);
    selectedCollectionsName = shortenMultipleItems(selectedCollections);

    assignedToCollection = groups.map((group: any) => group.users.map((user: any) => user.name));
    assignedToCollection = Array.from(new Set([].concat(...assignedToCollection)));
    assignedToCollection = shortenMultipleItems(assignedToCollection);
  }

  if (collectionsData) {
    collectionOptions = collectionsData.groups;
  }

  if (flowsData) {
    flowOptions = flowsData.flows;
  }

  let dialogBox = null;

  const handleCollectionDialogOk = (selectedCollectionIds: any) => {
    const finalSelectedCollections = selectedCollectionIds.filter(
      (selectedCollectionId: any) => !initialSelectedCollectionIds.includes(selectedCollectionId)
    );
    const finalRemovedCollections = initialSelectedCollectionIds.filter(
      (gId: any) => !selectedCollectionIds.includes(gId)
    );

    if (finalSelectedCollections.length > 0 || finalRemovedCollections.length > 0) {
      updateContactCollections({
        variables: {
          input: {
            contactId,
            addGroupIds: finalSelectedCollections,
            deleteGroupIds: finalRemovedCollections,
          },
        },
      });
    }

    setShowCollectionDialog(false);
  };

  const handleCollectionDialogCancel = () => {
    setShowCollectionDialog(false);
  };

  if (showCollectionDialog) {
    dialogBox = (
      <SearchDialogBox
        selectedOptions={initialSelectedCollectionIds}
        title={t('Add contact to collection')}
        handleOk={handleCollectionDialogOk}
        handleCancel={handleCollectionDialogCancel}
        options={collectionOptions}
      />
    );
  }

  const handleFlowSubmit = (flowId: any) => {
    const flowVariables: any = {
      flowId,
    };

    if (contactId) {
      flowVariables.contactId = contactId;
      addFlow({
        variables: flowVariables,
      });
    }

    if (collectionId) {
      flowVariables.groupId = collectionId;
      addFlowToCollection({
        variables: flowVariables,
      });
    }

    setShowFlowDialog(false);
  };

  const closeFlowDialogBox = () => {
    setShowFlowDialog(false);
  };

  if (showFlowDialog) {
    dialogBox = (
      <DropdownDialog
        title={t('Select flow')}
        handleOk={handleFlowSubmit}
        handleCancel={closeFlowDialogBox}
        options={flowOptions}
        placeholder={t('Select flow')}
        description={t('The contact will be responded as per the messages planned in the flow.')}
      />
    );
  }

  const handleClearChatSubmit = () => {
    clearMessages();
    setClearChatDialog(false);
    handleAction();
  };

  if (showClearChatDialog) {
    const bodyContext =
      'All the conversation data for this contact will be deleted permanently from Glific. This action cannot be undone. However, you should be able to access it in reports if you have backup configuration enabled.';
    dialogBox = (
      <DialogBox
        title="Are you sure you want to clear all conversation for this contact?"
        handleOk={handleClearChatSubmit}
        handleCancel={() => setClearChatDialog(false)}
        alignButtons="center"
        buttonOk="YES, CLEAR"
        colorOk="secondary"
        buttonCancel="MAYBE LATER"
      >
        <p className={styles.DialogText}>{bodyContext}</p>
      </DialogBox>
    );
  }

  const handleBlock = () => {
    blockContact({
      variables: {
        id: contactId,
        input: {
          status: 'BLOCKED',
        },
      },
    });
  };

  if (showBlockDialog) {
    dialogBox = (
      <DialogBox
        title="Do you want to block this contact"
        handleOk={handleBlock}
        handleCancel={() => setShowBlockDialog(false)}
        alignButtons="center"
        colorOk="secondary"
      >
        <p className={styles.DialogText}>
          You will not be able to view their chats and interact with them again
        </p>
      </DialogBox>
    );
  }

  if (showTerminateDialog) {
    dialogBox = <TerminateFlow contactId={contactId} setDialog={setShowTerminateDialog} />;
  }

  let flowButton: any;

  const blockContactButton = contactId ? (
    <Button
      data-testid="blockButton"
      className={styles.ListButtonDanger}
      color="secondary"
      disabled={isSimulator}
      onClick={() => setShowBlockDialog(true)}
    >
      {isSimulator ? (
        <BlockDisabledIcon className={styles.Icon} />
      ) : (
        <BlockIcon className={styles.Icon} />
      )}
      Block Contact
    </Button>
  ) : null;

  if (collectionId) {
    flowButton = (
      <Button
        data-testid="flowButton"
        className={styles.ListButtonPrimary}
        onClick={() => {
          getFlows();
          setShowFlowDialog(true);
        }}
      >
        <FlowIcon className={styles.Icon} />
        Start a flow
      </Button>
    );
  } else if (
    contactBspStatus &&
    status.includes(contactBspStatus) &&
    !is24HourWindowOver(lastMessageTime)
  ) {
    flowButton = (
      <Button
        data-testid="flowButton"
        className={styles.ListButtonPrimary}
        onClick={() => {
          getFlows();
          setShowFlowDialog(true);
        }}
      >
        <FlowIcon className={styles.Icon} />
        Start a flow
      </Button>
    );
  } else {
    let toolTip = 'Option disabled because the 24hr window expired';
    let disabled = true;
    // if 24hr window expired & contact type HSM. we can start flow with template msg .
    if (contactBspStatus === 'HSM') {
      toolTip =
        'Since the 24-hour window has passed, the contact will only receive a template message.';
      disabled = false;
    }
    flowButton = (
      <Tooltip title={toolTip} placement="right">
        <span>
          <Button
            data-testid="disabledFlowButton"
            className={styles.ListButtonPrimary}
            disabled={disabled}
            onClick={() => {
              getFlows();
              setShowFlowDialog(true);
            }}
          >
            {disabled ? (
              <FlowUnselectedIcon className={styles.Icon} />
            ) : (
              <FlowIcon className={styles.Icon} />
            )}
            Start a flow
          </Button>
        </span>
      </Tooltip>
    );
  }

  const terminateFLows = contactId ? (
    <Button
      data-testid="terminateButton"
      className={styles.ListButtonPrimary}
      onClick={() => {
        setShowTerminateDialog(!showTerminateDialog);
      }}
    >
      <TerminateFlowIcon className={styles.Icon} />
      Terminate flows
    </Button>
  ) : null;

  const viewDetails = contactId ? (
    <Button
      className={styles.ListButtonPrimary}
      disabled={isSimulator}
      data-testid="viewProfile"
      onClick={() => {
        history.push(`/contact-profile/${contactId}`);
      }}
    >
      {isSimulator ? (
        <ProfileDisabledIcon className={styles.Icon} />
      ) : (
        <ProfileIcon className={styles.Icon} />
      )}
      View contact profile
    </Button>
  ) : (
    <Button
      className={styles.ListButtonPrimary}
      data-testid="viewContacts"
      onClick={() => {
        history.push(`/collection/${collectionId}/contacts`);
      }}
    >
      <ProfileIcon className={styles.Icon} />
      View details
    </Button>
  );

  const addMember = contactId ? (
    <>
      <Button
        data-testid="collectionButton"
        className={styles.ListButtonPrimary}
        onClick={() => {
          getCollections();
          setShowCollectionDialog(true);
        }}
      >
        <AddContactIcon className={styles.Icon} />
        Add to collection
      </Button>
      <Button
        className={styles.ListButtonPrimary}
        data-testid="clearChatButton"
        onClick={() => setClearChatDialog(true)}
      >
        <ClearConversation className={styles.Icon} />
        Clear conversation
      </Button>
    </>
  ) : (
    <Button
      data-testid="collectionButton"
      className={styles.ListButtonPrimary}
      onClick={() => {
        setAddContactsDialogShow(true);
      }}
    >
      <AddContactIcon className={styles.Icon} />
      Add contact
    </Button>
  );

  if (addContactsDialogShow) {
    dialogBox = (
      <AddContactsToCollection collectionId={collectionId} setDialog={setAddContactsDialogShow} />
    );
  }

  const popper = (
    <Popper
      open={open}
      anchorEl={anchorEl}
      placement="bottom-start"
      transition
      className={styles.Popper}
    >
      {({ TransitionProps }) => (
        <Fade {...TransitionProps} timeout={350}>
          <Paper elevation={3} className={styles.Container}>
            {viewDetails}
            {flowButton}

            {addMember}
            {terminateFLows}
            {blockContactButton}
          </Paper>
        </Fade>
      )}
    </Popper>
  );

  const handleConfigureIconClick = (event: any) => {
    setAnchorEl(anchorEl ? null : event.currentTarget);
  };

  let contactCollections: any;
  if (selectedCollections.length > 0) {
    contactCollections = (
      <div className={styles.ContactCollections}>
        <span className={styles.CollectionHeading}>Collections</span>
        <span className={styles.CollectionsName} data-testid="collectionNames">
          {selectedCollectionsName}
        </span>
      </div>
    );
  }

  const getTitleAndIconForSmallScreen = (() => {
    const { location } = history;

    if (location.pathname.includes('collection')) {
      return CollectionIcon;
    }

    if (location.pathname.includes('saved-searches')) {
      return SavedSearchIcon;
    }

    return ChatIcon;
  })();

  // CONTACT: display session timer & Assigned to
  const IconComponent = getTitleAndIconForSmallScreen;
  const sessionAndCollectionAssignedTo = (
    <>
      {contactId ? (
        <div className={styles.SessionTimerContainer}>
          <div className={styles.SessionTimer} data-testid="sessionTimer">
            <span>Session Timer</span>
            <Timer
              time={lastMessageTime}
              contactStatus={contactStatus}
              contactBspStatus={contactBspStatus}
            />
          </div>
          <div>
            {assignedToCollection ? (
              <>
                <span className={styles.CollectionHeading}>Assigned to</span>
                <span className={styles.CollectionsName}>{assignedToCollection}</span>
              </>
            ) : null}
          </div>
        </div>
      ) : null}
      <div className={styles.Chat} onClick={() => showChats()} aria-hidden="true">
        <IconButton className={styles.MobileIcon}>
          <IconComponent />
        </IconButton>
      </div>
    </>
  );

  // COLLECTION: display contact info & Assigned to
  let collectionStatus: any;
  if (collectionId) {
    collectionStatus = <CollectionInformation collectionId={collectionId} />;
  }

  return (
    <Toolbar className={styles.ContactBar} color="primary">
      <div className={styles.ContactBarWrapper}>
        <div className={styles.ContactInfoContainer}>
          <div className={styles.ContactInfoWrapper}>
            <div className={styles.InfoWrapperRight}>
              <div className={styles.ContactDetails}>
                <ClickAwayListener onClickAway={() => setAnchorEl(null)}>
                  <div
                    className={styles.Configure}
                    data-testid="dropdownIcon"
                    onClick={handleConfigureIconClick}
                    onKeyPress={handleConfigureIconClick}
                    aria-hidden
                  >
                    <DropdownIcon />
                  </div>
                </ClickAwayListener>
                <Typography
                  className={styles.Title}
                  variant="h6"
                  noWrap
                  data-testid="beneficiaryName"
                >
                  {displayName}
                </Typography>
              </div>
              {contactCollections}
            </div>
            {collectionStatus}
            {sessionAndCollectionAssignedTo}
          </div>
        </div>
      </div>
      {popper}
      {dialogBox}
    </Toolbar>
  );
}
Example #16
Source File: ChatInput.tsx    From glific-frontend with GNU Affero General Public License v3.0 4 votes vote down vote up
ChatInput: React.SFC<ChatInputProps> = (props) => {
  const {
    onSendMessage,
    contactBspStatus,
    contactStatus,
    additionalStyle,
    handleHeightChange,
    isCollection,
    lastMessageTime,
  } = props;
  const [editorState, setEditorState] = useState(EditorState.createEmpty());
  const [selectedTab, setSelectedTab] = useState('');
  const [open, setOpen] = React.useState(false);
  const [searchVal, setSearchVal] = useState('');
  const [attachment, setAttachment] = useState(false);
  const [attachmentAdded, setAttachmentAdded] = useState(false);
  const [interactiveMessageContent, setInteractiveMessageContent] = useState<any>({});
  const [attachmentType, setAttachmentType] = useState<any>();
  const [attachmentURL, setAttachmentURL] = useState('');
  const [variable, setVariable] = useState(false);
  const [updatedEditorState, setUpdatedEditorState] = useState<any>();
  const [selectedTemplate, setSelectedTemplate] = useState<any>();
  const [variableParam, setVariableParam] = useState<any>([]);
  const [recordedAudio, setRecordedAudio] = useState<any>('');
  const [clearAudio, setClearAudio] = useState<any>(false);
  const [uploading, setUploading] = useState(false);

  const { t } = useTranslation();
  const speedSends = 'Speed sends';
  const templates = 'Templates';
  const interactiveMsg = 'Interactive msg';
  let uploadPermission = false;

  let dialog;

  const resetVariable = () => {
    setUpdatedEditorState(undefined);
    setEditorState(EditorState.createEmpty());
    setSelectedTemplate(undefined);
    setInteractiveMessageContent({});
    setVariableParam([]);
  };

  const { data: permission } = useQuery(GET_ATTACHMENT_PERMISSION);

  const [createMediaMessage] = useMutation(CREATE_MEDIA_MESSAGE, {
    onCompleted: (data: any) => {
      if (data) {
        onSendMessage(
          '',
          data.createMessageMedia.messageMedia.id,
          attachmentType,
          selectedTemplate,
          variableParam
        );
        setAttachmentAdded(false);
        setAttachmentURL('');
        setAttachmentType('');
        resetVariable();
      }
    },
  });
  const [uploadMediaBlob] = useMutation(UPLOAD_MEDIA_BLOB, {
    onCompleted: (data: any) => {
      if (data) {
        setAttachmentType('AUDIO');
        createMediaMessage({
          variables: {
            input: {
              caption: '',
              sourceUrl: data.uploadBlob,
              url: data.uploadBlob,
            },
          },
        });

        setClearAudio(true);
        setRecordedAudio('');
        setUploading(false);
      }
    },
    onError: () => {
      setNotification(t('Sorry, unable to upload audio.'), 'warning');
      setUploading(false);
    },
  });

  const submitMessage = async (message: string) => {
    // let's check if we are sending voice recording
    if (recordedAudio) {
      // converting blob into base64 format as needed by backend
      const reader = new FileReader();
      reader.readAsDataURL(recordedAudio);
      reader.onloadend = () => {
        const base64String: any = reader.result;
        // get the part without the tags
        const media = base64String.split(',')[1];
        // save media that will return an URL
        uploadMediaBlob({
          variables: {
            media,
            extension: 'mp3',
          },
        });
      };
      setUploading(true);
    }

    // check for an empty message or message with just spaces
    if ((!message || /^\s*$/.test(message)) && !attachmentAdded) return;

    // check if there is any attachment
    if (attachmentAdded) {
      createMediaMessage({
        variables: {
          input: {
            caption: message,
            sourceUrl: attachmentURL,
            url: attachmentURL,
          },
        },
      });
      // check if type is list or quick replies
    } else if (interactiveMessageContent && interactiveMessageContent.type) {
      onSendMessage(
        null,
        null,
        interactiveMessageContent.type.toUpperCase(),
        null,
        null,
        Number(selectedTemplate.id)
      );
      // else the type will by default be text
    } else {
      onSendMessage(message, null, 'TEXT', selectedTemplate, variableParam);
    }
    resetVariable();

    // Resetting the EditorState
    setEditorState(
      EditorState.moveFocusToEnd(
        EditorState.push(editorState, ContentState.createFromText(''), 'remove-range')
      )
    );
  };

  const handleClick = (title: string) => {
    // clear the search when tab is opened again
    setSearchVal('');
    if (selectedTab === title) {
      setSelectedTab('');
    } else {
      setSelectedTab(title);
    }
    setOpen(selectedTab !== title);
  };

  const handleClickAway = () => {
    setOpen(false);
    setSelectedTab('');
  };

  const resetAttachment = () => {
    setAttachmentAdded(false);
    setAttachmentURL('');
    setAttachmentType('');
  };

  const handleSelectText = (obj: any, isInteractiveMsg: boolean = false) => {
    resetVariable();

    // set selected template

    let messageBody = obj.body;
    if (isInteractiveMsg) {
      const interactiveContent = JSON.parse(obj.interactiveContent);
      messageBody = getInteractiveMessageBody(interactiveContent);
      setInteractiveMessageContent(interactiveContent);
    }

    setSelectedTemplate(obj);
    // Conversion from HTML text to EditorState
    setEditorState(getEditorFromContent(messageBody));

    // Add attachment if present
    if (Object.prototype.hasOwnProperty.call(obj, 'MessageMedia') && obj.MessageMedia) {
      const type = obj.type ? obj.type : obj.MessageMedia.type;
      setAttachmentAdded(true);
      setAttachmentURL(obj.MessageMedia.sourceUrl);
      setAttachmentType(type);
    } else {
      resetAttachment();
    }

    // check if variable present
    const isVariable = obj.body?.match(pattern);
    if (isVariable) {
      setVariable(true);
    }
    handleClickAway();
  };

  const handleCancel = () => {
    resetAttachment();
    resetVariable();
  };

  const updateEditorState = (body: string) => {
    setUpdatedEditorState(body);
  };

  const variableParams = (params: Array<any>) => {
    setVariableParam(params);
  };

  if (variable) {
    const dialogProps = {
      template: selectedTemplate,
      setVariable,
      handleCancel,
      updateEditorState,
      variableParams,
      variableParam,
    };
    dialog = <AddVariables {...dialogProps} />;
  }

  const handleAudioRecording = (blob: any) => {
    setRecordedAudio(blob);
    setClearAudio(false);
  };

  const handleSearch = (e: any) => {
    setSearchVal(e.target.value);
  };

  const quickSendButtons = (quickSendTypes: any) => {
    const buttons = quickSendTypes.map((type: string) => (
      <div
        key={type}
        data-testid="shortcutButton"
        onClick={() => handleClick(type)}
        aria-hidden="true"
        className={clsx(styles.QuickSend, {
          [styles.QuickSendSelected]: selectedTab === type,
        })}
      >
        {type}
      </div>
    ));
    return <div className={styles.QuickSendButtons}>{buttons}</div>;
  };

  // determine what kind of messages we should display
  let quickSendTypes: any = [];
  if (contactBspStatus) {
    switch (contactBspStatus) {
      case 'SESSION':
        quickSendTypes = [speedSends, interactiveMsg];
        break;
      case 'SESSION_AND_HSM':
        quickSendTypes = [speedSends, templates, interactiveMsg];
        break;
      case 'HSM':
        quickSendTypes = [templates];
        break;
      default:
        break;
    }
    if (is24HourWindowOver(lastMessageTime)) {
      quickSendTypes = [templates];
    }
  }

  if ((contactStatus && contactStatus === 'INVALID') || contactBspStatus === 'NONE') {
    return (
      <div className={styles.ContactOptOutMessage}>
        {t(
          'Sorry, chat is unavailable with this contact at this moment because they aren’t opted in to your number.'
        )}
      </div>
    );
  }

  if (isCollection) {
    quickSendTypes = [speedSends, templates];
  }
  let audioOption: any;
  // enable audio only if GCS is configured
  if (permission && permission.attachmentsEnabled) {
    if (!selectedTemplate && Object.keys(interactiveMessageContent).length === 0) {
      audioOption = (
        <VoiceRecorder
          handleAudioRecording={handleAudioRecording}
          clearAudio={clearAudio}
          uploading={uploading}
          isMicActive={recordedAudio !== ''}
        />
      );
    }
    uploadPermission = true;
  }

  if (attachment) {
    const dialogProps = {
      attachmentType,
      attachmentURL,
      setAttachment,
      setAttachmentAdded,
      setAttachmentType,
      setAttachmentURL,
      uploadPermission,
    };
    dialog = <AddAttachment {...dialogProps} />;
  }
  return (
    <Container
      className={`${styles.ChatInput} ${additionalStyle}`}
      data-testid="message-input-container"
    >
      {dialog}
      <ClickAwayListener onClickAway={handleClickAway}>
        <div className={styles.SendsContainer}>
          {open ? (
            <Fade in={open} timeout={200}>
              <div className={styles.Popup}>
                <ChatTemplates
                  isTemplate={selectedTab === templates}
                  isInteractiveMsg={selectedTab === interactiveMsg}
                  searchVal={searchVal}
                  handleSelectText={handleSelectText}
                />
                <SearchBar
                  className={styles.ChatSearchBar}
                  handleChange={handleSearch}
                  onReset={() => setSearchVal('')}
                  searchMode
                />
              </div>
            </Fade>
          ) : null}
          {quickSendButtons(quickSendTypes)}
        </div>
      </ClickAwayListener>

      <div
        className={clsx(styles.ChatInputElements, {
          [styles.Unrounded]: selectedTab !== '',
          [styles.Rounded]: selectedTab === '',
        })}
      >
        <WhatsAppEditor
          editorState={updatedEditorState ? getEditorFromContent(updatedEditorState) : editorState}
          setEditorState={setEditorState}
          sendMessage={submitMessage}
          handleHeightChange={handleHeightChange}
          readOnly={
            (selectedTemplate !== undefined && selectedTemplate.isHsm) ||
            Object.keys(interactiveMessageContent).length !== 0
          }
        />

        {selectedTemplate || Object.keys(interactiveMessageContent).length > 0 ? (
          <Tooltip title={t('Remove message')} placement="top">
            <IconButton
              className={updatedEditorState ? styles.CrossIcon : styles.CrossIconWithVariable}
              onClick={() => {
                resetVariable();
                resetAttachment();
              }}
            >
              <CrossIcon />
            </IconButton>
          </Tooltip>
        ) : null}
        {updatedEditorState ? (
          <IconButton
            className={styles.VariableIcon}
            onClick={() => {
              setVariable(!variable);
            }}
          >
            <VariableIcon />
          </IconButton>
        ) : null}
        {audioOption}
        <IconButton
          className={styles.AttachmentIcon}
          onClick={() => {
            setAttachment(!attachment);
          }}
        >
          {attachment || attachmentAdded ? <AttachmentIconSelected /> : <AttachmentIcon />}
        </IconButton>
        <div className={styles.SendButtonContainer}>
          <Button
            className={styles.SendButton}
            data-testid="sendButton"
            variant="contained"
            color="primary"
            disableElevation
            onClick={() => {
              if (updatedEditorState) {
                submitMessage(updatedEditorState);
              } else {
                submitMessage(getPlainTextFromEditor(editorState));
              }
            }}
            disabled={
              (!editorState.getCurrentContent().hasText() && !attachmentAdded && !recordedAudio) ||
              uploading
            }
          >
            <SendMessageIcon className={styles.SendIcon} />
          </Button>
        </div>
      </div>
    </Container>
  );
}
Example #17
Source File: Simulator.tsx    From glific-frontend with GNU Affero General Public License v3.0 4 votes vote down vote up
Simulator: React.FC<SimulatorProps> = ({
  showSimulator,
  setSimulatorId,
  simulatorIcon = true,
  message,
  flowSimulator,
  isPreviewMessage,
  resetMessage,
  getFlowKeyword,
  interactiveMessage,
  showHeader = true,
  hasResetButton = false,
}: SimulatorProps) => {
  const [inputMessage, setInputMessage] = useState('');
  const [simulatedMessages, setSimulatedMessage] = useState<any>();
  const [isOpen, setIsOpen] = useState(false);

  // Template listing
  const [isDrawerOpen, setIsDrawerOpen] = useState<Boolean>(false);
  const [selectedListTemplate, setSelectedListTemplate] = useState<any>(null);

  const variables = { organizationId: getUserSession('organizationId') };

  let messages: any[] = [];
  let simulatorId = '';
  const sender = {
    name: '',
    phone: '',
  };
  // chat messages will be shown on simulator
  const isSimulatedMessage = true;

  const sendMessage = (senderDetails: any, interactivePayload?: any, templateValue?: any) => {
    const sendMessageText = inputMessage === '' && message ? message : inputMessage;

    // check if send message text is not empty
    if (!sendMessageText && !interactivePayload && !templateValue) return;

    let type = 'text';

    let payload: any = {};

    let context: any = {};

    if (interactivePayload) {
      type = interactivePayload.payload.type;
      payload = interactivePayload.payload;
      delete payload.type;
      context = interactivePayload.context;
    } else if (templateValue) {
      payload.text = templateValue;
    } else {
      payload.text = sendMessageText;
    }

    axios
      .post(GUPSHUP_CALLBACK_URL, {
        type: 'message',
        payload: {
          id: uuidv4(),
          type,
          payload,
          sender: senderDetails,
          context,
        },
      })
      .catch((error) => {
        // add log's
        setLogs(
          `sendMessageText:${sendMessageText} GUPSHUP_CALLBACK_URL:${GUPSHUP_CALLBACK_URL}`,
          'info'
        );
        setLogs(error, 'error');
      });
    setInputMessage('');
    // reset the message from floweditor for the next time
    if (resetMessage) {
      resetMessage();
    }
  };

  const [loadSimulator, { data: allConversations, subscribeToMore }] = useLazyQuery(
    SIMULATOR_SEARCH_QUERY,
    {
      fetchPolicy: 'network-only',
      nextFetchPolicy: 'cache-only',
      onCompleted: ({ search }) => {
        if (subscribeToMore) {
          const subscriptionVariables = { organizationId: getUserSession('organizationId') };
          // message received subscription
          subscribeToMore({
            document: SIMULATOR_MESSAGE_RECEIVED_SUBSCRIPTION,
            variables: subscriptionVariables,
            updateQuery: (prev, { subscriptionData }) =>
              updateSimulatorConversations(prev, subscriptionData, 'RECEIVED'),
          });

          // message sent subscription
          subscribeToMore({
            document: SIMULATOR_MESSAGE_SENT_SUBSCRIPTION,
            variables: subscriptionVariables,
            updateQuery: (prev, { subscriptionData }) =>
              updateSimulatorConversations(prev, subscriptionData, 'SENT'),
          });

          if (search.length > 0) {
            sendMessage({ name: search[0].contact.name, phone: search[0].contact.phone });
          }
        }
      },
    }
  );

  const { data: simulatorSubscribe }: any = useSubscription(SIMULATOR_RELEASE_SUBSCRIPTION, {
    variables,
  });

  useEffect(() => {
    if (simulatorSubscribe) {
      try {
        const userId = JSON.parse(simulatorSubscribe.simulatorRelease).simulator_release.user_id;
        if (userId.toString() === getUserSession('id')) {
          setSimulatorId(0);
        }
      } catch (error) {
        setLogs('simulator release error', 'error');
      }
    }
  }, [simulatorSubscribe]);

  const [getSimulator, { data }] = useLazyQuery(GET_SIMULATOR, {
    fetchPolicy: 'network-only',
    onCompleted: (simulatorData) => {
      if (simulatorData.simulatorGet) {
        loadSimulator({ variables: getSimulatorVariables(simulatorData.simulatorGet.id) });
        setSimulatorId(simulatorData.simulatorGet.id);
      } else {
        setNotification(
          'Sorry! Simulators are in use by other staff members right now. Please wait for it to be idle',
          'warning'
        );
      }
    },
  });

  const [releaseSimulator]: any = useLazyQuery(RELEASE_SIMULATOR, {
    fetchPolicy: 'network-only',
  });

  if (allConversations && data && data.simulatorGet) {
    // currently setting the simulated contact as the default receiver
    const simulatedContact = allConversations.search.filter(
      (item: any) => item.contact.id === data.simulatorGet.id
    );
    if (simulatedContact.length > 0) {
      messages = simulatedContact[0].messages;
      simulatorId = simulatedContact[0].contact.id;
      sender.name = simulatedContact[0].contact.name;
      sender.phone = simulatedContact[0].contact.phone;
    }
  }

  const getStyleForDirection = (direction: string, isInteractive: boolean): string => {
    const simulatorClasses = [styles.ReceivedMessage, styles.InteractiveReceivedMessage];
    if (isInteractive && direction === 'received') {
      return simulatorClasses.join(' ');
    }

    if (direction === 'send') {
      return styles.SendMessage;
    }

    return styles.ReceivedMessage;
  };

  const releaseUserSimulator = () => {
    releaseSimulator();
    setSimulatorId(0);
  };

  const handleOpenListReplyDrawer = (items: any) => {
    setSelectedListTemplate(items);
    setIsDrawerOpen(true);
  };

  const sendMediaMessage = (type: string, payload: any) => {
    axios
      .post(GUPSHUP_CALLBACK_URL, {
        type: 'message',
        payload: {
          id: uuidv4(),
          type,
          payload,
          sender: {
            // this number will be the simulated contact number
            phone: data ? data.simulatorGet?.phone : '',
            name: data ? data.simulatorGet?.name : '',
          },
        },
      })
      .catch((error) => {
        // add log's
        setLogs(`sendMediaMessage:${type} GUPSHUP_CALLBACK_URL:${GUPSHUP_CALLBACK_URL}`, 'info');
        setLogs(error, 'error');
      });
  };

  const renderMessage = (
    messageObject: any,
    direction: string,
    index: number,
    isInteractive: boolean = false
  ) => {
    const { insertedAt, type, media, location, interactiveContent, bspMessageId, templateType } =
      messageObject;

    const messageType = isInteractive ? templateType : type;
    const { body, buttons } = WhatsAppTemplateButton(isInteractive ? '' : messageObject.body);

    // Checking if interactive content is present then only parse to JSON
    const content = interactiveContent && JSON.parse(interactiveContent);
    let isInteractiveContentPresent = false;
    let template;

    if (content) {
      isInteractiveContentPresent = !!Object.entries(content).length;

      if (isInteractiveContentPresent && messageType === INTERACTIVE_LIST) {
        template = (
          <>
            <ListReplyTemplate
              {...content}
              bspMessageId={bspMessageId}
              showHeader={showHeader}
              component={SimulatorTemplate}
              onGlobalButtonClick={handleOpenListReplyDrawer}
            />
            <TimeComponent direction={direction} insertedAt={insertedAt} />
          </>
        );
      }

      if (isInteractiveContentPresent && messageType === INTERACTIVE_QUICK_REPLY) {
        template = (
          <QuickReplyTemplate
            {...content}
            isSimulator
            showHeader={showHeader}
            disabled={isInteractive}
            bspMessageId={bspMessageId}
            onQuickReplyClick={(value: any) => sendMessage(sender, value)}
          />
        );
      }
    }

    return (
      <div key={index}>
        <div className={getStyleForDirection(direction, isInteractiveContentPresent)}>
          {isInteractiveContentPresent && direction !== 'send' ? (
            template
          ) : (
            <>
              <ChatMessageType
                type={messageType}
                media={media}
                body={body}
                location={location}
                isSimulatedMessage={isSimulatedMessage}
              />
              <TimeComponent direction={direction} insertedAt={insertedAt} />
            </>
          )}
        </div>
        <div className={styles.TemplateButtons}>
          <TemplateButtons
            template={buttons}
            callbackTemplateButtonClick={(value: string) => sendMessage(sender, null, value)}
            isSimulator
          />
        </div>
      </div>
    );
  };

  const getChatMessage = () => {
    const chatMessage = messages
      .map((simulatorMessage: any, index: number) => {
        if (simulatorMessage.receiver.id === simulatorId) {
          return renderMessage(simulatorMessage, 'received', index);
        }
        return renderMessage(simulatorMessage, 'send', index);
      })
      .reverse();
    setSimulatedMessage(chatMessage);
  };

  const getPreviewMessage = () => {
    if (message && message.type) {
      const previewMessage = renderMessage(message, 'received', 0);
      if (['STICKER', 'AUDIO'].includes(message.type)) {
        setSimulatedMessage(previewMessage);
      } else if (message.body || message.media?.caption) {
        setSimulatedMessage(previewMessage);
      } else {
        // To get rid of empty body and media caption for preview HSM
        setSimulatedMessage('');
      }
    }

    if (interactiveMessage) {
      const { templateType, interactiveContent } = interactiveMessage;
      const previewMessage = renderMessage(interactiveMessage, 'received', 0, true);
      setSimulatedMessage(previewMessage);
      if (templateType === INTERACTIVE_LIST) {
        const { items } = JSON.parse(interactiveContent);
        setSelectedListTemplate(items);
      } else {
        setIsDrawerOpen(false);
      }
    }
  };

  // to display only preview for template
  useEffect(() => {
    if (isPreviewMessage) {
      getPreviewMessage();
    }
  }, [message]);

  // for loading conversation
  useEffect(() => {
    if (allConversations && data) {
      getChatMessage();
    }
  }, [data, allConversations]);

  // for sending message to Gupshup
  useEffect(() => {
    if (!isPreviewMessage && message && data) {
      sendMessage(sender);
    }
  }, [message]);

  useEffect(() => {
    if (isPreviewMessage && interactiveMessage) {
      getPreviewMessage();
    }

    // Cleaning up simulator when switching between templates
    if (!interactiveMessage) {
      setSimulatedMessage(null);
      setIsDrawerOpen(false);
    }
  }, [interactiveMessage]);

  const messageRef = useCallback(
    (node: any) => {
      if (node) {
        const nodeCopy = node;
        setTimeout(() => {
          nodeCopy.scrollTop = node.scrollHeight;
        }, 100);
      }
    },
    [messages]
  );

  const handleAttachmentClick = (media: any) => {
    const { name: type, payload } = media;

    const mediaUrl = document.querySelector('#media');
    if (mediaUrl) {
      const url = mediaUrl.getAttribute('data-url');
      if (url) {
        payload.url = url;
      }
    }
    sendMediaMessage(type, payload);
    setIsOpen(false);
  };

  const handleListReplyDrawerClose = () => {
    setIsDrawerOpen(false);
    setSelectedListTemplate(null);
  };

  const handleListDrawerItemClick = (payloadObject: any) => {
    sendMessage(sender, payloadObject);
    handleListReplyDrawerClose();
  };

  const dropdown = (
    <ClickAwayListener onClickAway={() => setIsOpen(false)}>
      <div className={styles.Dropdown} id="media">
        {SAMPLE_MEDIA_FOR_SIMULATOR.map((media: any) => (
          <Button
            onClick={() => handleAttachmentClick(media)}
            key={media.id}
            className={styles.AttachmentOptions}
          >
            <MessageType type={media.id} color="dark" />
          </Button>
        ))}
      </div>
    </ClickAwayListener>
  );

  const simulator = (
    <Draggable>
      <div className={styles.SimContainer}>
        <div>
          <div id="simulator" className={styles.Simulator}>
            {!isPreviewMessage && (
              <>
                <ClearIcon
                  className={styles.ClearIcon}
                  onClick={() => {
                    releaseUserSimulator();
                  }}
                  data-testid="clearIcon"
                />
                {hasResetButton && (
                  <ResetIcon
                    data-testid="resetIcon"
                    className={styles.ResetIcon}
                    onClick={() => {
                      if (getFlowKeyword) {
                        getFlowKeyword();
                      }
                    }}
                  />
                )}
              </>
            )}

            <div className={styles.Screen}>
              <div className={styles.Header}>
                <ArrowBackIcon />
                <img src={DefaultWhatsappImage} alt="default" />
                <span data-testid="beneficiaryName">Beneficiary</span>
                <div>
                  <VideocamIcon />
                  <CallIcon />
                  <MoreVertIcon />
                </div>
              </div>
              <div className={styles.Messages} ref={messageRef} data-testid="simulatedMessages">
                {simulatedMessages}
              </div>
              {isDrawerOpen && <div className={styles.BackgroundTint} />}
              <div className={styles.Controls}>
                <div>
                  <InsertEmoticonIcon className={styles.Icon} />
                  <input
                    type="text"
                    data-testid="simulatorInput"
                    onKeyPress={(event: any) => {
                      if (event.key === 'Enter') {
                        sendMessage(sender);
                      }
                    }}
                    value={inputMessage}
                    placeholder="Type a message"
                    disabled={isPreviewMessage}
                    onChange={(event) => setInputMessage(event.target.value)}
                  />
                  <AttachFileIcon
                    data-testid="attachment"
                    className={styles.AttachFileIcon}
                    onClick={() => setIsOpen(!isOpen)}
                  />
                  {isOpen ? dropdown : null}
                  <CameraAltIcon className={styles.Icon} />
                </div>

                <Button
                  variant="contained"
                  color="primary"
                  className={styles.SendButton}
                  disabled={isPreviewMessage}
                  onClick={() => sendMessage(sender)}
                >
                  <MicIcon />
                </Button>
              </div>
              {isDrawerOpen && (
                <ListReplyTemplateDrawer
                  drawerTitle="Items"
                  items={selectedListTemplate}
                  disableSend={!!interactiveMessage}
                  onItemClick={handleListDrawerItemClick}
                  onDrawerClose={handleListReplyDrawerClose}
                />
              )}
            </div>
          </div>
        </div>
      </div>
    </Draggable>
  );

  const handleSimulator = () => {
    // check for the flowkeyword from floweditor
    if (getFlowKeyword) {
      getFlowKeyword();
    }
    getSimulator();
  };
  return (
    <>
      {showSimulator && simulator}
      {simulatorIcon && (
        <SimulatorIcon
          data-testid="simulatorIcon"
          className={showSimulator ? styles.SimulatorIconClicked : styles.SimulatorIconNormal}
          onClick={() => {
            if (showSimulator) {
              releaseUserSimulator();
            } else {
              handleSimulator();
            }
          }}
        />
      )}

      {flowSimulator && (
        <div className={styles.PreviewButton}>
          <FormButton
            variant="outlined"
            color="primary"
            data-testid="previewButton"
            className={styles.Button}
            onClick={() => {
              if (showSimulator) {
                releaseUserSimulator();
              } else {
                handleSimulator();
              }
            }}
          >
            Preview
            {showSimulator && <CancelOutlinedIcon className={styles.CrossIcon} />}
          </FormButton>
        </div>
      )}
    </>
  );
}
Example #18
Source File: WhatsAppEditor.tsx    From glific-frontend with GNU Affero General Public License v3.0 4 votes vote down vote up
WhatsAppEditor: React.SFC<WhatsAppEditorProps> = (props) => {
  const { setEditorState, sendMessage, editorState, handleHeightChange, readOnly = false } = props;
  const [showEmojiPicker, setShowEmojiPicker] = useState(false);
  const { t } = useTranslation();

  const handleChange = (editorStateChange: any) => {
    setEditorState(editorStateChange);
  };

  const updateValue = (input: any, isEmoji: boolean = false) => {
    const editorContentState = editorState.getCurrentContent();
    const editorSelectionState: any = editorState.getSelection();
    const ModifiedContent = Modifier.replaceText(
      editorContentState,
      editorSelectionState,
      isEmoji ? input.native : input
    );
    let updatedEditorState = EditorState.push(editorState, ModifiedContent, 'insert-characters');
    if (!isEmoji) {
      const editorSelectionStateMod = updatedEditorState.getSelection();
      const updatedSelection = editorSelectionStateMod.merge({
        anchorOffset: editorSelectionStateMod.getAnchorOffset() - 1,
        focusOffset: editorSelectionStateMod.getFocusOffset() - 1,
      });
      updatedEditorState = EditorState.forceSelection(updatedEditorState, updatedSelection);
    }
    setEditorState(updatedEditorState);
  };

  const handleKeyCommand = (command: string, editorStateChange: any) => {
    // On enter, submit. Otherwise, deal with commands like normal.
    if (command === 'enter') {
      // Convert Draft.js to WhatsApp
      sendMessage(getPlainTextFromEditor(editorStateChange));
      return 'handled';
    }

    if (command === 'underline') {
      return 'handled';
    }

    if (command === 'bold') {
      updateValue('**');
    } else if (command === 'italic') {
      updateValue('__');
    } else {
      const newState = RichUtils.handleKeyCommand(editorStateChange, command);
      if (newState) {
        setEditorState(newState);
        return 'handled';
      }
    }
    return 'not-handled';
  };

  const keyBindingFn = (e: any) => {
    // Shift-enter is by default supported. Only 'enter' needs to be changed.
    if (e.keyCode === 13 && !e.nativeEvent.shiftKey) {
      return 'enter';
    }
    return getDefaultKeyBinding(e);
  };

  const handleClickAway = () => {
    setShowEmojiPicker(false);
  };

  const emojiStyles: any = {
    position: 'absolute',
    bottom: '60px',
    right: '-150px',
    zIndex: 100,
  };

  if (window.innerWidth <= 768) {
    emojiStyles.right = '5%';
  }

  return (
    <>
      <ReactResizeDetector
        data-testid="resizer"
        handleHeight
        onResize={(width: any, height: any) => handleHeightChange(height - 40)} // 40 is the initial height
      >
        <div className={styles.Editor}>
          <Editor
            data-testid="editor"
            editorState={editorState}
            onChange={handleChange}
            handleKeyCommand={handleKeyCommand}
            keyBindingFn={keyBindingFn}
            placeholder={t('Type a message...')}
            readOnly={readOnly}
          />
        </div>
      </ReactResizeDetector>
      <ClickAwayListener onClickAway={handleClickAway}>
        <div>
          <div className={styles.EmojiButton}>
            <IconButton
              data-testid="emoji-picker"
              color="primary"
              aria-label="pick emoji"
              component="span"
              onClick={() => setShowEmojiPicker(!showEmojiPicker)}
            >
              <span role="img" aria-label="pick emoji">
                ?
              </span>
            </IconButton>
          </div>
          {showEmojiPicker ? (
            <Picker
              data-testid="emoji-popup"
              title={t('Pick your emoji…')}
              emoji="point_up"
              style={emojiStyles}
              onSelect={(emoji) => updateValue(emoji, true)}
            />
          ) : null}
        </div>
      </ClickAwayListener>
    </>
  );
}
Example #19
Source File: EmojiInput.tsx    From glific-frontend with GNU Affero General Public License v3.0 4 votes vote down vote up
EmojiInput: React.FC<EmojiInputProps> = ({
  field: { value, name, onBlur },
  handleChange,
  getEditorValue,
  handleBlur,
  ...props
}: EmojiInputProps) => {
  const [showEmojiPicker, setShowEmojiPicker] = useState(false);
  const { t } = useTranslation();

  const updateValue = (input: any, isEmoji = false) => {
    const editorContentState = value.getCurrentContent();
    const editorSelectionState: any = value.getSelection();
    const ModifiedContent = Modifier.replaceText(
      editorContentState,
      editorSelectionState,
      isEmoji ? input.native : input
    );
    let updatedEditorState = EditorState.push(value, ModifiedContent, 'insert-characters');
    if (!isEmoji) {
      const editorSelectionStateMod = updatedEditorState.getSelection();
      const updatedSelection = editorSelectionStateMod.merge({
        anchorOffset: editorSelectionStateMod.getAnchorOffset() - 1,
        focusOffset: editorSelectionStateMod.getFocusOffset() - 1,
      });
      updatedEditorState = EditorState.forceSelection(updatedEditorState, updatedSelection);
    }
    props.form.setFieldValue(name, updatedEditorState);
  };

  const handleKeyCommand = (command: any, editorState: any) => {
    if (command === 'underline') {
      return 'handled';
    }
    if (command === 'bold') {
      updateValue('**');
    } else if (command === 'italic') {
      updateValue('__');
    } else {
      const newState = RichUtils.handleKeyCommand(editorState, command);
      if (newState) {
        props.form.setFieldValue(name, newState);
        return 'handled';
      }
    }
    return 'not-handled';
  };

  const draftJsChange = (editorState: any) => {
    if (handleChange) {
      handleChange(getPlainTextFromEditor(props.form.values.example));
    }
    if (getEditorValue) {
      getEditorValue(editorState);
    } else {
      props.form.setFieldValue(name, editorState);
    }
  };

  const mentions = props.inputProp?.suggestions || [];

  const [open, setOpen] = useState(false);
  const [suggestions, setSuggestions] = useState(mentions);

  const onOpenChange = (_open: boolean) => {
    setOpen(_open);
  };

  const getSuggestions = useCallback(customSuggestionsFilter, []);

  const onSearchChange = ({ value: searchValue }: { value: string }) => {
    setSuggestions(getSuggestions(searchValue, mentions));
  };

  const inputProps = {
    component: Editor,
    editorState: value,
    open,
    readOnly: props.disabled,
    suggestions,
    onOpenChange,
    onSearchChange,
    handlePastedText: (text: string, html: string, editorState: EditorState) => {
      const pastedBlocks = ContentState.createFromText(text).getBlockMap();
      const newState = Modifier.replaceWithFragment(
        editorState.getCurrentContent(),
        editorState.getSelection(),
        pastedBlocks
      );
      const newEditorState = EditorState.push(editorState, newState, 'insert-fragment');
      draftJsChange(newEditorState);
      return 'handled';
    },
    handleKeyCommand,
    onBlur: handleBlur,
    onChange: draftJsChange,
  };

  const editor = { inputComponent: DraftField, inputProps };

  const emojiPicker = showEmojiPicker ? (
    <Picker
      data-testid="emoji-container"
      title={t('Pick your emoji…')}
      emoji="point_up"
      style={{ position: 'absolute', top: '10px', right: '0px', zIndex: 2 }}
      onSelect={(emojiValue) => updateValue(emojiValue, true)}
    />
  ) : (
    ''
  );

  const handleClickAway = () => {
    setShowEmojiPicker(false);
  };

  const picker = (
    <ClickAwayListener onClickAway={handleClickAway}>
      <InputAdornment position="end" className={Styles.EmojiPosition}>
        <IconButton
          color="primary"
          data-testid="emoji-picker"
          aria-label="pick emoji"
          component="span"
          className={Styles.Emoji}
          onClick={() => setShowEmojiPicker(!showEmojiPicker)}
        >
          <span role="img" aria-label="pick emoji">
            ?
          </span>
        </IconButton>

        {emojiPicker}
      </InputAdornment>
    </ClickAwayListener>
  );

  const input = (
    <Input field={{ name, value, onBlur }} {...props} editor={editor} emojiPicker={picker} />
  );

  return input;
}
Example #20
Source File: Dropdown.tsx    From halstack-react with Apache License 2.0 4 votes vote down vote up
DxcDropdown = ({
  options,
  optionsIconPosition = "before",
  icon,
  iconPosition = "before",
  label = "",
  caretHidden = false,
  onSelectOption,
  expandOnHover = false,
  margin,
  size = "fitContent",
  tabIndex = 0,
  disabled = false,
}: DropdownPropsType): JSX.Element => {
  const [width, setWidth] = useState();
  const colorsTheme = useTheme();

  const ref = useRef(null);
  const handleResize = () => {
    if (ref.current) setWidth(ref.current.offsetWidth);
  };

  useEffect(() => {
    if (ref.current) {
      ref.current.addEventListener("resize", handleResize);
      handleResize();
    }

    return () => {
      if (ref.current) ref.current.removeEventListener("resize", handleResize);
    };
  }, []);

  const [anchorEl, setAnchorEl] = useState(null);

  function handleClickListItem(event) {
    setAnchorEl(event.currentTarget);
  }

  function handleMenuItemClick(option) {
    setAnchorEl(null);
    onSelectOption(option.value);
  }

  function handleClose() {
    setAnchorEl(null);
  }

  const handleCloseOver = expandOnHover ? handleClose : undefined;

  const UpArrowIcon = () => (
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
      <path d="M7 14l5-5 5 5z" />
      <path d="M0 0h24v24H0z" fill="none" />
    </svg>
  );

  const DownArrowIcon = () => (
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
      <path d="M7 10l5 5 5-5z" />
      <path d="M0 0h24v24H0z" fill="none" />
    </svg>
  );

  const labelComponent = (
    <DropdownTriggerLabel iconPosition={iconPosition} label={label}>
      {label}
    </DropdownTriggerLabel>
  );

  return (
    <ThemeProvider theme={colorsTheme.dropdown}>
      <DXCDropdownContainer margin={margin} size={size} disabled={disabled}>
        <div
          onMouseOver={expandOnHover && !disabled ? handleClickListItem : undefined}
          onMouseOut={handleCloseOver}
          onFocus={handleCloseOver}
          onBlur={handleCloseOver}
        >
          <DropdownTrigger
            opened={anchorEl === null ? false : true}
            onClick={handleClickListItem}
            disabled={disabled}
            label={label}
            caretHidden={caretHidden}
            margin={margin}
            size={size}
            ref={ref}
            tabIndex={tabIndex}
          >
            <DropdownTriggerContainer caretHidden={caretHidden}>
              {iconPosition === "after" && labelComponent}
              {icon && (
                <ButtonIconContainer label={label} iconPosition={iconPosition} disabled={disabled}>
                  {typeof icon === "string" ? <ButtonIcon src={icon} /> : icon}
                </ButtonIconContainer>
              )}
              {iconPosition === "before" && labelComponent}
            </DropdownTriggerContainer>
            <CaretIconContainer caretHidden={caretHidden} disabled={disabled}>
              {!caretHidden && (anchorEl === null ? <DownArrowIcon /> : <UpArrowIcon />)}
            </CaretIconContainer>
          </DropdownTrigger>
          <DXCMenu
            anchorEl={anchorEl}
            open={Boolean(anchorEl)}
            onClose={handleClose}
            getContentAnchorEl={null}
            anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
            transformOrigin={{ vertical: "top", horizontal: "left" }}
            size={size}
            width={width}
            role={undefined}
            transition
            disablePortal
            placement="bottom-start"
          >
            {({ TransitionProps }) => (
              <Grow {...TransitionProps}>
                <Paper>
                  <ClickAwayListener onClickAway={handleClose}>
                    <MenuList autoFocusItem={Boolean(anchorEl)} id="menu-list-grow">
                      {options.map((option) => (
                        <MenuItem
                          key={option.value}
                          value={option.value}
                          disableRipple={true}
                          onClick={(event) => handleMenuItemClick(option)}
                        >
                          {optionsIconPosition === "after" && <span className="optionLabel">{option.label}</span>}
                          {option.icon && (
                            <ListIconContainer label={option.label} iconPosition={optionsIconPosition}>
                              {typeof option.icon === "string" ? <ListIcon src={option.icon} /> : option.icon}
                            </ListIconContainer>
                          )}
                          {optionsIconPosition === "before" && <span className="optionLabel">{option.label}</span>}
                        </MenuItem>
                      ))}
                    </MenuList>
                  </ClickAwayListener>
                </Paper>
              </Grow>
            )}
          </DXCMenu>
        </div>
      </DXCDropdownContainer>
    </ThemeProvider>
  );
}
Example #21
Source File: ClosablePopper.tsx    From clearflask with Apache License 2.0 4 votes vote down vote up
render() {
    const {
      children,
      classes,
      theme,
      paperClassName,
      zIndex,
      onClose,
      anchorType,
      closeButtonPosition,
      clickAway,
      clickAwayProps,
      arrow,
      transitionCmpt,
      transitionProps,
      placement,
      useBackdrop,
      ...popperProps
    } = this.props;

    const TransitionCmpt = transitionCmpt || Fade;

    var anchorEl: PopperProps['anchorEl'];
    if (this.props.anchorType === 'native') {
      anchorEl = this.props.anchor;
    } else {
      // Overly complicated way to ensure popper.js
      // always gets some kind of coordinates
      anchorEl = () => {
        var el: ReferenceObject | undefined | null;
        if (!el && this.props.anchorType === 'ref') {
          el = this.props.anchor.current;
        }
        if (!el && this.props.anchorType === 'element') {
          el = (typeof this.props.anchor === 'function')
            ? this.props.anchor()
            : this.props.anchor;
        }
        if (!el && this.props.anchorType === 'virtual') {
          const virtualAnchor = this.props.anchor;
          const bounds = virtualAnchor?.() || this.boundsLast;
          if (!!bounds) {
            this.boundsLast = bounds;
          }
          if (bounds) {
            el = {
              clientHeight: bounds.height,
              clientWidth: bounds.width,
              getBoundingClientRect: () => {
                const boundsInner = virtualAnchor?.() || this.boundsLast || bounds;
                this.boundsLast = boundsInner;
                const domRect: MyDomRect = {
                  height: boundsInner.height,
                  width: boundsInner.width,
                  top: boundsInner.top,
                  bottom: boundsInner.top + boundsInner.height,
                  left: boundsInner.left,
                  right: boundsInner.left + boundsInner.width,
                  x: boundsInner.width >= 0 ? boundsInner.left : (boundsInner.left - boundsInner.width),
                  y: boundsInner.height >= 0 ? boundsInner.top : (boundsInner.top - boundsInner.height),
                  toJSON: () => domRect,
                };
                return domRect;
              }
            }
          }
        }
        if (!el) {
          el = this.anchorRef.current;
        }
        if (!el) {
          const domRect: MyDomRect = {
            height: 0,
            width: 0,
            top: 0,
            bottom: 0,
            left: 0,
            right: 0,
            x: 0,
            y: 0,
            toJSON: () => domRect,
          };
          el = {
            clientHeight: 0,
            clientWidth: 0,
            getBoundingClientRect: () => domRect,
          };
        }
        return el;
      };
    }

    return (
      <>
        <div ref={this.anchorRef} />
        <Popper
          placement={placement || 'right-start'}
          transition
          {...popperProps}
          anchorEl={anchorEl}
          className={classNames(
            classes.popper,
            popperProps.className,
            !popperProps.open && classes.hidden,
          )}
          modifiers={{
            ...(arrow ? {
              arrow: {
                enabled: true,
                element: this.arrowRef.current || '[x-arrow]',
              },
            } : {}),
            ...(popperProps.modifiers || {}),
          }}
        >
          {props => (
            <ClickAwayListener
              mouseEvent='onMouseDown'
              onClickAway={() => clickAway && onClose()}
              {...clickAwayProps}
            >
              <TransitionCmpt
                {...props.TransitionProps}
                {...transitionProps}
              >
                <Paper className={classNames(paperClassName, classes.paper)}>
                  {arrow && (
                    <span x-arrow='true' className={classes.arrow} ref={this.arrowRef} />
                  )}
                  {closeButtonPosition !== 'disable' && (
                    <IconButton
                      classes={{
                        label: classes.closeButtonLabel,
                        root: classNames(
                          classes.closeButton,
                          (closeButtonPosition === 'top-right' || !closeButtonPosition) && classes.closeButtonTopRight,
                          closeButtonPosition === 'top-left' && classes.closeButtonTopLeft,
                        ),
                      }}
                      aria-label="Close"
                      onClick={() => onClose()}
                    >
                      <CloseIcon className={classes.closeIcon} fontSize='inherit' />
                    </IconButton>
                  )}
                  {children}
                </Paper>
              </TransitionCmpt>
            </ClickAwayListener>
          )}
        </Popper>
      </>
    );
  }
Example #22
Source File: SQFormDatePickerWithDateFNS.tsx    From SQForm with MIT License 4 votes vote down vote up
function SQFormDatePickerWithDateFNS({
  name,
  label,
  size = 'auto',
  isDisabled = false,
  placeholder = '',
  onBlur,
  onChange,
  setDisabledDate,
  muiFieldProps,
  muiTextInputProps = {},
  isCalendarOnly = false,
  InputProps,
  InputAdornmentProps,
}: SQFormDatePickerDateFNSProps): JSX.Element {
  const {
    formikField: {field, helpers},
    fieldState: {isFieldError, isFieldRequired},
    fieldHelpers: {handleBlur, HelperTextComponent},
  } = useForm<Date | null, Date | null>({
    name,
    onBlur,
  });

  const handleChange = (date: Date | null) => {
    helpers.setValue(date);
    onChange && onChange(date);
  };

  const [isCalendarOpen, setIsCalendarOpen] = React.useState(false);
  const handleClose = () => setIsCalendarOpen(false);
  const toggleCalendar = () => setIsCalendarOpen(!isCalendarOpen);
  const handleClickAway = () => {
    if (isCalendarOpen) {
      setIsCalendarOpen(false);
    }
  };

  const classes = useStyles();

  // An empty string will not reset the DatePicker so we have to pass null
  const value = field.value || null;

  return (
    <ClickAwayListener onClickAway={handleClickAway}>
      <Grid item sm={size}>
        <DatePicker
          label={label}
          disabled={isDisabled}
          shouldDisableDate={setDisabledDate}
          value={value}
          onChange={handleChange}
          onClose={handleClose}
          onOpen={toggleCalendar}
          open={isCalendarOpen}
          renderInput={(inputProps) => {
            return (
              <TextField
                {...inputProps}
                name={name}
                color="primary"
                error={isFieldError}
                fullWidth={true}
                inputProps={{...inputProps.inputProps, ...muiTextInputProps}}
                InputLabelProps={{shrink: true}}
                FormHelperTextProps={{error: isFieldError}}
                helperText={!isDisabled && HelperTextComponent}
                placeholder={placeholder}
                onBlur={handleBlur}
                required={isFieldRequired}
                onClick={
                  isCalendarOnly && !isDisabled
                    ? toggleCalendar
                    : handleClickAway
                }
                classes={classes}
              />
            );
          }}
          InputProps={InputProps}
          InputAdornmentProps={InputAdornmentProps}
          {...muiFieldProps}
        />
      </Grid>
    </ClickAwayListener>
  );
}
Example #23
Source File: SQFormDatePicker.tsx    From SQForm with MIT License 4 votes vote down vote up
function SQFormDatePicker({
  name,
  label,
  size = 'auto',
  isDisabled = false,
  placeholder = '',
  onBlur,
  onChange,
  setDisabledDate,
  muiFieldProps,
  muiTextInputProps = {},
  isCalendarOnly = false,
  InputProps,
  InputAdornmentProps,
}: SQFormDatePickerProps): JSX.Element {
  const {
    formikField: {field, helpers},
    fieldState: {isFieldError, isFieldRequired},
    fieldHelpers: {handleBlur, HelperTextComponent},
  } = useForm<Moment | null, Moment | null>({
    name,
    onBlur,
  });

  const handleChange = (date: Moment | null) => {
    helpers.setValue(date);
    onChange && onChange(date);
  };

  const [isCalendarOpen, setIsCalendarOpen] = React.useState(false);
  const handleClose = () => setIsCalendarOpen(false);
  const toggleCalendar = () => setIsCalendarOpen(!isCalendarOpen);
  const handleClickAway = () => {
    if (isCalendarOpen) {
      setIsCalendarOpen(false);
    }
  };

  const classes = useStyles();

  // An empty string will not reset the DatePicker so we have to pass null
  const value = field.value || null;

  return (
    <ClickAwayListener onClickAway={handleClickAway}>
      <Grid item sm={size}>
        <DatePicker
          label={label}
          disabled={isDisabled}
          shouldDisableDate={setDisabledDate}
          value={value}
          onChange={handleChange}
          onClose={handleClose}
          onOpen={toggleCalendar}
          open={isCalendarOpen}
          renderInput={(inputProps) => {
            return (
              <TextField
                {...inputProps}
                name={name}
                color="primary"
                error={isFieldError}
                fullWidth={true}
                inputProps={{...inputProps.inputProps, ...muiTextInputProps}}
                InputLabelProps={{shrink: true}}
                FormHelperTextProps={{error: isFieldError}}
                helperText={!isDisabled && HelperTextComponent}
                placeholder={placeholder}
                onBlur={handleBlur}
                required={isFieldRequired}
                onClick={
                  isCalendarOnly && !isDisabled
                    ? toggleCalendar
                    : handleClickAway
                }
                classes={classes}
              />
            );
          }}
          InputProps={InputProps}
          InputAdornmentProps={InputAdornmentProps}
          {...muiFieldProps}
        />
      </Grid>
    </ClickAwayListener>
  );
}