/* * Wire * Copyright (C) 2018 Wire Swiss GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * */ import React, {useEffect, useState} from 'react'; import {RegisteredClient} from '@wireapp/api-client/lib/client/index'; import {TabIndex} from '@wireapp/react-ui-kit/lib/types/enums'; import { COLOR, ContainerXS, DeviceIcon, FlexBox, Form, IconButton, Input, Line, Small, Text, TrashIcon, } from '@wireapp/react-ui-kit'; import {isEnterKey} from 'Util/KeyboardUtil'; import {t} from 'Util/LocalizerUtil'; import {splitFingerprint} from 'Util/StringUtil'; import {ValidationError} from '../module/action/ValidationError'; import {parseError, parseValidationErrors} from '../util/errorUtil'; export interface Props extends React.HTMLProps { client: RegisteredClient; onClick: (event: React.MouseEvent | React.KeyboardEvent) => void; onClientRemoval: (password?: string) => void; requirePassword: boolean; selected: boolean; clientError?: Error; } const ClientItem = ({selected, onClientRemoval, onClick, client, clientError, requirePassword}: Props) => { const passwordInput = React.useRef(null); const CONFIG = { animationSteps: 8, }; const [animationStep, setAnimationStep] = useState(selected ? CONFIG.animationSteps : 0); const [isSelected, setIsSelected] = useState(selected); const [isAnimating, setIsAnimating] = useState(false); const [password, setPassword] = useState(''); const [isValidPassword, setIsValidPassword] = useState(true); const [validationError, setValidationError] = useState(null); const [isOpen, setIsOpen] = useState(requirePassword && (isSelected || isAnimating)); useEffect(() => { if (!selected && isSelected) { setIsAnimating(true); setIsSelected(false); setIsOpen(false); requestAnimationFrame(() => executeAnimateOut()); } else if (selected && !isSelected) { setIsAnimating(true); setIsSelected(true); setIsOpen(true); requestAnimationFrame(() => executeAnimateIn()); } else if (selected && isSelected) { setIsOpen(true); } else { setAnimationStep(0); setIsOpen(false); } }, [selected]); const formatId = (id = '?') => splitFingerprint(id).join(' '); const executeAnimateIn = (): void => { setAnimationStep(step => { if (step < CONFIG.animationSteps) { window.requestAnimationFrame(executeAnimateIn); return step + 1; } setIsAnimating(false); return step; }); }; const executeAnimateOut = (): void => { setAnimationStep(step => { if (step > 0) { window.requestAnimationFrame(executeAnimateOut); return step - 1; } setIsAnimating(false); return step; }); }; const formatDate = (dateString: string): string => dateString ? new Date(dateString).toLocaleString('en-US', { day: 'numeric', hour: 'numeric', hour12: false, minute: 'numeric', month: 'short', weekday: 'short', year: 'numeric', }) : '?'; const formatName = (model: string, clazz: string): string | JSX.Element => model || ( {clazz} ) || '?'; const resetState = () => { setAnimationStep(selected ? CONFIG.animationSteps : 0); setIsAnimating(false); }; const handleWrapperClick = (event: React.MouseEvent | React.KeyboardEvent): void => { resetState(); onClick(event); }; const onWrapperEnter = (event: React.KeyboardEvent) => { if (!isEnterKey(event)) { return; } handleWrapperClick(event); }; const handlePasswordlessClientDeletion = (event: React.FormEvent): Promise => { event.preventDefault(); return Promise.resolve() .then(() => onClientRemoval()) .catch(error => { if (!error.label) { throw error; } }); }; const handleSubmit = (event: React.FormEvent): Promise => { event.preventDefault(); let localValidationError = null; if (passwordInput.current) { if (!passwordInput.current.checkValidity()) { localValidationError = ValidationError.handleValidationState( passwordInput.current.name, passwordInput.current.validity, ); } setIsValidPassword(passwordInput.current.validity.valid); } setValidationError(localValidationError); return Promise.resolve(localValidationError) .then(error => { if (error) { throw error; } }) .then(() => onClientRemoval(password)) .catch(error => { if (error.label) { switch (error.label) { default: { const isValidationError = Object.values(ValidationError.ERROR).some(errorType => error.label.endsWith(errorType), ); if (!isValidationError) { throw error; } } } } else { throw error; } }); }; const onPasswordChange = (event: React.ChangeEvent) => { setPassword(event.target.value); setIsValidPassword(true); }; const animatedCardSpacing = { l: 32, m: 16, s: 12, xl: 48, xs: 8, xxs: 4, }; const inputContainerHeight = 104; const animationPosition = animationStep / CONFIG.animationSteps; const smoothHeight = animationPosition * inputContainerHeight; const smoothMarginTop = animationPosition * animatedCardSpacing.m; return ( ) => requirePassword && handleWrapperClick(event)} onKeyDown={(event: React.KeyboardEvent) => requirePassword && onWrapperEnter(event)} css={{ ['&:focus-visible']: { outline: `none`, }, cursor: requirePassword ? 'pointer' : 'auto', margin: `${smoothMarginTop}px 0 0 0`, padding: `0 ${animatedCardSpacing.m}px`, }} data-uie-name="go-remove-device" tabIndex={TabIndex.FOCUSABLE} >
{client.model && ( {formatName(client.model, client.class)} )} {`ID: ${formatId(client.id)}`} {formatDate(client.time)}
{!requirePassword && ( )}
{isOpen && (
{/* eslint jsx-a11y/no-autofocus : "off" */}
)}
{validationError && selected ? (
{parseValidationErrors(validationError)}
) : clientError && selected ? (
{parseError(clientError)}
) : null}
); }; export {ClientItem};