diff options
Diffstat (limited to 'gui/src')
| -rw-r--r-- | gui/src/renderer/components/select-location/CustomListDialogs.tsx | 201 |
1 files changed, 201 insertions, 0 deletions
diff --git a/gui/src/renderer/components/select-location/CustomListDialogs.tsx b/gui/src/renderer/components/select-location/CustomListDialogs.tsx new file mode 100644 index 0000000000..e68c87e3a4 --- /dev/null +++ b/gui/src/renderer/components/select-location/CustomListDialogs.tsx @@ -0,0 +1,201 @@ +import { useCallback, useState } from 'react'; +import { sprintf } from 'sprintf-js'; +import styled from 'styled-components'; + +import { colors } from '../../../config.json'; +import { + compareRelayLocationGeographical, + ICustomList, + RelayLocation, + RelayLocationGeographical, +} from '../../../shared/daemon-rpc-types'; +import { messages } from '../../../shared/gettext'; +import log from '../../../shared/logging'; +import { useAppContext } from '../../context'; +import { formatHtml } from '../../lib/html-formatter'; +import { useBoolean } from '../../lib/utilityHooks'; +import { useSelector } from '../../redux/store'; +import * as AppButton from '../AppButton'; +import * as Cell from '../cell'; +import { normalText, tinyText } from '../common-styles'; +import { ModalAlert, ModalMessage } from '../Modal'; +import SimpleInput from '../SimpleInput'; + +const StyledModalMessage = styled(ModalMessage)({ + marginTop: '8px', + marginBottom: '8px', +}); + +interface AddToListDialogProps { + location: RelayLocationGeographical; + isOpen: boolean; + hide: () => void; +} + +// Dialog that displays list of custom lists when adding location to custom list. +export function AddToListDialog(props: AddToListDialogProps) { + const { updateCustomList } = useAppContext(); + const customLists = useSelector((state) => state.settings.customLists); + + const add = useCallback( + async (list: ICustomList) => { + // Update the list with the new location. + const updatedList = { + ...list, + locations: [...list.locations, props.location], + }; + try { + await updateCustomList(updatedList); + } catch (e) { + const error = e as Error; + log.error(`Failed to edit custom list ${list.id}: ${error.message}`); + } + + props.hide(); + }, + [location, updateCustomList], + ); + + let locationType: string; + if ('hostname' in props.location) { + // TRANSLATORS: This refers to our VPN relays/servers + locationType = messages.pgettext('select-location-view', 'Relay'); + } else if ('city' in props.location) { + locationType = messages.pgettext('select-location-view', 'City'); + } else { + locationType = messages.pgettext('select-location-view', 'Country'); + } + + const lists = customLists.map((list) => ( + <SelectList key={list.id} list={list} location={props.location} add={add} /> + )); + + return ( + <ModalAlert + isOpen={props.isOpen} + buttons={[ + <AppButton.BlueButton key="cancel" onClick={props.hide}> + {messages.gettext('Cancel')} + </AppButton.BlueButton>, + ]} + close={props.hide}> + <StyledModalMessage> + {formatHtml( + sprintf( + // TRANSLATORS: This is a label shown above a list of options. + // TRANSLATORS: Available placeholder: + // TRANSLATORS: %(locationType) - Could be either "Country", "City" and "Relay" + messages.pgettext('select-location-view', 'Add <b>%(locationType)s</b> to list'), + { + locationType, + }, + ), + )} + </StyledModalMessage> + {lists} + </ModalAlert> + ); +} + +const StyledSelectListItemLabel = styled(Cell.Label)(normalText, { + fontWeight: 'normal', +}); + +const StyledSelectListItemIcon = styled(Cell.Icon)({ + [`${Cell.CellButton}:not(:disabled):hover &&`]: { + backgroundColor: colors.white80, + }, +}); + +interface SelectListProps { + list: ICustomList; + location: RelayLocation; + add: (list: ICustomList) => void; +} + +function SelectList(props: SelectListProps) { + const onAdd = useCallback(() => props.add(props.list), [props.list]); + + // List should be disabled if location already is in list. + const disabled = props.list.locations.some((location) => + compareRelayLocationGeographical(location, props.location), + ); + + return ( + <Cell.CellButton onClick={onAdd} disabled={disabled}> + <StyledSelectListItemLabel>{props.list.name}</StyledSelectListItemLabel> + <StyledSelectListItemIcon source="icon-add" width={18} /> + </Cell.CellButton> + ); +} + +const StyledInputErrorText = styled.span(tinyText, { + marginTop: '6px', + color: colors.red, +}); + +interface EditListProps { + list: ICustomList; + isOpen: boolean; + hide: () => void; +} + +// Dialog for changing the name of a custom list. +export function EditListDialog(props: EditListProps) { + const { updateCustomList } = useAppContext(); + + const [newName, setNewName] = useState(props.list.name); + const [error, setError, unsetError] = useBoolean(); + + // Update name in list and save it. + const save = useCallback(async () => { + try { + const updatedList = { ...props.list, name: newName }; + const result = await updateCustomList(updatedList); + if (result && result.type === 'name already exists') { + setError(); + } else { + props.hide(); + } + } catch (e) { + const error = e as Error; + log.error(`Failed to edit custom list ${props.list.id}: ${error.message}`); + } + }, [props.list, newName, props.hide]); + + // Errors should be reset when editing the value + const onChange = useCallback((value: string) => { + setNewName(value); + unsetError(); + }, []); + + return ( + <ModalAlert + isOpen={props.isOpen} + buttons={[ + <AppButton.BlueButton key="save" onClick={save}> + {messages.gettext('Save')} + </AppButton.BlueButton>, + <AppButton.BlueButton key="cancel" onClick={props.hide}> + {messages.gettext('Cancel')} + </AppButton.BlueButton>, + ]} + close={props.hide}> + <StyledModalMessage> + {messages.pgettext('select-location-view', 'Edit list name')} + </StyledModalMessage> + <SimpleInput + value={newName} + onChangeValue={onChange} + onSubmitValue={save} + maxLength={30} + autoFocus + /> + {error && ( + <StyledInputErrorText> + {messages.pgettext('select-location-view', 'Name is already taken.')} + </StyledInputErrorText> + )} + </ModalAlert> + ); +} |
