import React, { useCallback, useRef } from 'react'; import { sprintf } from 'sprintf-js'; import styled from 'styled-components'; import { colors } from '../../config.json'; import { compareRelayLocation, RelayLocation } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; import Accordion from './Accordion'; import * as Cell from './cell'; import ChevronButton from './ChevronButton'; import RelayStatusIndicator from './RelayStatusIndicator'; interface IContainerProps { selected: boolean; disabled: boolean; location: RelayLocation; } const Container = styled(Cell.Container)((props: IContainerProps) => { const background = 'hostname' in props.location ? colors.blue20 : 'city' in props.location ? colors.blue40 : colors.blue; const backgroundHover = 'country' in props.location ? colors.blue80 : colors.blue80; return { display: 'flex', // The actual padding is 22px except for the tick icon which has 18. paddingLeft: '18px', marginBottom: '1px', backgroundColor: props.selected ? colors.green : background, ':not(:disabled):hover': { backgroundColor: props.selected ? colors.green : props.disabled ? background : backgroundHover, }, }; }); const Button = styled.button((props: { location: RelayLocation }) => { const paddingLeft = 'hostname' in props.location ? 32 : 'city' in props.location ? 16 : 0; return { display: 'flex', alignItems: 'center', flex: 1, border: 'none', background: 'none', padding: `0 0 0 ${paddingLeft}px`, margin: 0, }; }); const StyledChevronButton = styled(ChevronButton)({ marginLeft: '18px', }); const Label = styled(Cell.Label)({ fontFamily: 'Open Sans', fontWeight: 'normal', fontSize: '16px', }); interface IProps { name: string; active: boolean; location: RelayLocation; selected: boolean; expanded?: boolean; onSelect?: (location: RelayLocation) => void; onExpand?: (location: RelayLocation, value: boolean) => void; onWillExpand?: (locationRect: DOMRect, expandedContentHeight: number) => void; onTransitionEnd?: () => void; children?: React.ReactElement[]; } function LocationRow(props: IProps, ref: React.Ref) { const hasChildren = props.children !== undefined; const buttonRef = useRef() as React.RefObject; const toggleCollapse = useCallback(() => { props.onExpand?.(props.location, !props.expanded); }, [props.onExpand, props.expanded, props.location]); const handleClick = useCallback(() => props.onSelect?.(props.location), [ props.onSelect, props.location, ]); const onWillExpand = useCallback( (nextHeight: number) => { const buttonRect = buttonRef.current?.getBoundingClientRect(); if (buttonRect) { props.onWillExpand?.(buttonRect, nextHeight); } }, [props.onWillExpand], ); return ( <> {hasChildren ? ( ) : null} {hasChildren && ( {props.children} )} ); } export default React.memo(React.forwardRef(LocationRow), compareProps); function compareProps(oldProps: IProps, nextProps: IProps): boolean { return ( React.Children.count(oldProps.children) === React.Children.count(nextProps.children) && oldProps.name === nextProps.name && oldProps.active === nextProps.active && oldProps.selected === nextProps.selected && oldProps.expanded === nextProps.expanded && oldProps.onSelect === nextProps.onSelect && oldProps.onExpand === nextProps.onExpand && oldProps.onWillExpand === nextProps.onWillExpand && oldProps.onTransitionEnd === nextProps.onTransitionEnd && compareRelayLocation(oldProps.location, nextProps.location) && compareChildren(oldProps.children, nextProps.children) ); } function compareChildren( oldChildren?: React.ReactElement[], nextChildren?: React.ReactElement[], ) { if (oldChildren === undefined || nextChildren === undefined) { return oldChildren === nextChildren; } return ( oldChildren.length === nextChildren.length && oldChildren.every((oldChild, i) => compareProps(oldChild.props, nextChildren[i].props)) ); }