Skip to content

Commit

Permalink
feat(ui): add SelectRecipients component
Browse files Browse the repository at this point in the history
  • Loading branch information
guanbinrui authored and Jack-Works committed Dec 17, 2019
1 parent 21cf662 commit 0e624f5
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 47 deletions.
2 changes: 1 addition & 1 deletion src/components/InjectedComponents/AdditionalPostBox.tsx
Expand Up @@ -169,8 +169,8 @@ export function AdditionalPostBox(props: AdditionalPostBoxProps) {
const ui = (
<AdditionalPostBoxUI
currentIdentity={currentIdentity}
availableShareTarget={availableShareTarget}
currentShareTarget={currentShareTarget}
availableShareTarget={availableShareTarget}
onShareTargetChanged={onShareTargetChanged}
postBoxText={postText}
postBoxPlaceholder={geti18nString(
Expand Down
164 changes: 144 additions & 20 deletions src/components/InjectedComponents/PostModal.tsx
@@ -1,4 +1,4 @@
import * as React from 'react'
import { memo, useState, useMemo, useCallback, useEffect, useRef } from 'react'
import {
Card,
Modal,
Expand All @@ -13,8 +13,20 @@ import {
} from '@material-ui/core'
import CloseIcon from '@material-ui/icons/Close'
import { geti18nString } from '../../utils/i18n'
import { MessageCenter } from '../../utils/messages'
import { useCapturedInput } from '../../utils/hooks/useCapturedEvents'
import { useStylesExtends } from '../custom-ui-helper'
import { useStylesExtends, or } from '../custom-ui-helper'
import { SelectPeopleAndGroupsUIProps } from '../shared/SelectPeopleAndGroups'
import { Person, Group } from '../../database'
import { useFriendsList, useGroupsList, useMyIdentities, useCurrentIdentity } from '../DataSource/useActivatedUI'
import { NotSetupYetPromptProps, NotSetupYetPrompt } from '../shared/NotSetupYetPrompt'
import { steganographyModeSetting } from '../shared-settings/settings'
import { useValueRef } from '../../utils/hooks/useValueRef'
import { useAsync } from '../../utils/components/AsyncComponent'
import { getActivatedUI } from '../../social-network/ui'
import { ChooseIdentity, ChooseIdentityProps } from '../shared/ChooseIdentity'
import Services from '../../extension/service'
import { SelectRecipientsUI, SelectRecipientsProps } from '../shared/SelectRecipients/SelectRecipients'

const useStyles = makeStyles(theme => ({
MUIInputRoot: {
Expand Down Expand Up @@ -48,17 +60,24 @@ const useStyles = makeStyles(theme => ({

export interface PostModalUIProps extends withClasses<KeysInferFromUseStyles<typeof useStyles>> {
open: boolean
availableShareTarget: Array<Person | Group>
currentShareTarget: Array<Person | Group>
currentIdentity: Person | null
postBoxText: string
postBoxButtonDisabled: boolean
onPostTextChange: (nextString: string) => void
onCloseButtonClicked: () => void
onFinishButtonClicked: () => void
onShareTargetChanged: SelectRecipientsProps['onSetSelected']
ChooseIdentityProps?: Partial<ChooseIdentityProps>
SelectPeopleAndGroupsProps?: Partial<SelectRecipientsProps>
}
export const PostModalUI = React.memo(function PostModalUI(props: PostModalUIProps) {
export const PostModalUI = memo(function PostModalUI(props: PostModalUIProps) {
const classes = useStylesExtends(useStyles(), props)
const rootRef = React.useRef<HTMLDivElement>(null)
const inputRef = React.useRef<HTMLInputElement>(null)
const rootRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(null)
useCapturedInput(inputRef, props.onPostTextChange)

return (
<div ref={rootRef}>
<Modal
Expand All @@ -81,9 +100,9 @@ export const PostModalUI = React.memo(function PostModalUI(props: PostModalUIPro
title={
<>
<IconButton
classes={{ root: classes.close }}
aria-label={geti18nString('post_modal__dismiss_aria')}
onClick={props.onCloseButtonClicked}
classes={{ root: classes.close }}>
onClick={props.onCloseButtonClicked}>
<CloseIcon />
</IconButton>
<Typography className={classes.title} display="inline" variant="h6">
Expand All @@ -103,14 +122,24 @@ export const PostModalUI = React.memo(function PostModalUI(props: PostModalUIPro
multiline
placeholder={geti18nString('post_modal__placeholder')}
/>
<Typography>{geti18nString('post_modal__select_recipients_title')}</Typography>
<Typography style={{ marginBottom: 10 }}>
{geti18nString('post_modal__select_recipients_title')}
</Typography>
<SelectRecipientsUI
ignoreMyself
items={props.availableShareTarget}
selected={props.currentShareTarget}
onSetSelected={props.onShareTargetChanged}
{...props.SelectPeopleAndGroupsProps}
/>
</CardContent>
<CardActions>
<Button
className={classes.button}
style={{ marginLeft: 'auto' }}
variant="contained"
color="primary"
variant="contained"
disabled={props.postBoxButtonDisabled}
onClick={props.onFinishButtonClicked}>
{geti18nString('post_modal__button')}
</Button>
Expand All @@ -121,17 +150,112 @@ export const PostModalUI = React.memo(function PostModalUI(props: PostModalUIPro
)
})

export interface PostModalProps
extends PartialRequired<PostModalUIProps, 'open' | 'postBoxText' | 'onPostTextChange'> {}
export interface PostModalProps extends Partial<PostModalUIProps> {
identities?: Person[]
onRequestPost?: (target: (Person | Group)[], text: string) => void
onRequestReset?: () => void
NotSetupYetPromptProps?: Partial<NotSetupYetPromptProps>
}
export function PostModal(props: PostModalProps) {
return (
<>
<PostModalUI
postBoxButtonDisabled={false}
onCloseButtonClicked={() => {}}
onFinishButtonClicked={() => {}}
{...props}
/>
</>
const people = useFriendsList()
const groups = useGroupsList()
const availableShareTarget = or(
props.availableShareTarget,
useMemo(() => [...groups, ...people], [people, groups]),
)
const identities = or(props.identities, useMyIdentities())
const currentIdentity = or(props.currentIdentity, useCurrentIdentity())
const isSteganography = useValueRef(steganographyModeSetting)

const onRequestPost = or(
props.onRequestPost,
useCallback(
async (target: (Person | Group)[], text: string) => {
const [encrypted, token] = await Services.Crypto.encryptTo(
text,
target.map(x => x.identifier),
currentIdentity!.identifier,
)
const activeUI = getActivatedUI()
if (isSteganography) {
activeUI.taskPasteIntoPostBox(geti18nString('additional_post_box__steganography_post_pre'), {
warningText: geti18nString('additional_post_box__encrypted_failed'),
shouldOpenPostDialog: false,
})
activeUI.taskUploadToPostBox(encrypted, {
warningText: geti18nString('additional_post_box__steganography_post_failed'),
})
} else {
activeUI.taskPasteIntoPostBox(geti18nString('additional_post_box__encrypted_post_pre', encrypted), {
warningText: geti18nString('additional_post_box__encrypted_failed'),
shouldOpenPostDialog: false,
})
}
Services.Crypto.publishPostAESKey(token)
},
[currentIdentity, isSteganography],
),
)
const onRequestReset = or(
props.onRequestReset,
useCallback(() => {
setOpen(false)
setPostBoxText('')
onShareTargetChanged([])
}, []),
)

const [open, setOpen] = useState(false)
const onStartCompose = useCallback(() => setOpen(true), [])
const onCancelCompose = useCallback(() => setOpen(false), [])
useEffect(() => {
MessageCenter.on('startCompose', onStartCompose)
MessageCenter.on('cancelCompose', onCancelCompose)
return () => {
MessageCenter.off('startCompose', onStartCompose)
MessageCenter.off('cancelCompose', onCancelCompose)
}
}, [onStartCompose, onCancelCompose])

const [postBoxText, setPostBoxText] = useState('')
const [currentShareTarget, onShareTargetChanged] = useState(availableShareTarget)
const onFinishButtonClicked = useCallback(() => {
onRequestPost(currentShareTarget, postBoxText)
onRequestReset()
}, [currentShareTarget, onRequestPost, onRequestReset, postBoxText])
const onCloseButtonClicked = useCallback(() => {
setOpen(false)
}, [])

const [showWelcome, setShowWelcome] = useState(false)
useAsync(getActivatedUI().shouldDisplayWelcome, []).then(x => setShowWelcome(x))
// TODO: ??? should we do this without including `ui` ???
if (showWelcome) {
return <NotSetupYetPrompt {...props.NotSetupYetPromptProps} />
}

const ui = (
<PostModalUI
open={open}
availableShareTarget={availableShareTarget}
currentIdentity={currentIdentity}
currentShareTarget={currentShareTarget}
postBoxText={postBoxText}
postBoxButtonDisabled={!(currentShareTarget.length && postBoxText)}
onPostTextChange={setPostBoxText}
onFinishButtonClicked={onFinishButtonClicked}
onCloseButtonClicked={onCloseButtonClicked}
onShareTargetChanged={onShareTargetChanged}
{...props}
/>
)

if (identities.length > 1)
return (
<>
<ChooseIdentity {...props.ChooseIdentityProps} />
{ui}
</>
)
return ui
}
20 changes: 20 additions & 0 deletions src/components/shared/SelectRecipients/ClickableChip.tsx
@@ -0,0 +1,20 @@
import { makeStyles } from '@material-ui/core'
import Chip, { ChipProps } from '@material-ui/core/Chip'

export interface ClickableChipProps {
ChipProps?: ChipProps
}

const useStyles = makeStyles({
root: {
marginRight: 6,
marginBottom: 6,
cursor: 'pointer',
},
})

export function ClickableChip(props: ClickableChipProps) {
const classes = useStyles()

return <Chip className={classes.root} {...props.ChipProps} />
}
38 changes: 38 additions & 0 deletions src/components/shared/SelectRecipients/GroupInChip.tsx
@@ -0,0 +1,38 @@
import Chip, { ChipProps } from '@material-ui/core/Chip'
import { Group } from '../../../database'
import DoneIcon from '@material-ui/icons/Done'
import { useResolveSpecialGroupName } from '../SelectPeopleAndGroups/resolveSpecialGroupName'
import { makeStyles } from '@material-ui/styles'

export interface GroupInChipProps {
item: Group
selected?: boolean
disabled?: boolean
onClick?(): void
ChipProps?: ChipProps
}

const useStyles = makeStyles({
root: {
marginRight: 6,
marginBottom: 6,
cursor: 'pointer',
},
})

export function GroupInChip(props: GroupInChipProps) {
const classes = useStyles()
const { selected, onClick } = props
const group = props.item

return (
<Chip
className={classes.root}
avatar={selected ? <DoneIcon /> : undefined}
color={selected ? 'primary' : 'default'}
onClick={onClick}
label={useResolveSpecialGroupName(group)}
{...props.ChipProps}
/>
)
}
65 changes: 65 additions & 0 deletions src/components/shared/SelectRecipients/SelectRecipients.tsx
@@ -0,0 +1,65 @@
import { Group, Person } from '../../../database'
import { makeStyles, Box } from '@material-ui/core'
import { PersonOrGroupInListProps } from '../SelectPeopleAndGroups'
import { GroupInChipProps, GroupInChip } from './GroupInChip'
import { PersonIdentifier, GroupIdentifier } from '../../../database/type'
import AddIcon from '@material-ui/icons/Add'
import { ClickableChip } from './ClickableChip'

export interface SelectRecipientsProps<T extends Group | Person = Group | Person>
extends withClasses<KeysInferFromUseStyles<typeof useStyles> | 'root'> {
/** Omit myself in the UI and the selected result */
ignoreMyself?: boolean
items: T[]
selected: T[]
// frozenSelected: T[]
disabled?: boolean
hideSelectAll?: boolean
hideSelectNone?: boolean
showAtNetwork?: boolean
maxSelection?: number
onSetSelected(selected: T[]): void
GroupInChipProps?: Partial<GroupInChipProps>
PersonOrGroupInListProps?: Partial<PersonOrGroupInListProps>
}
const useStyles = makeStyles({
root: {},
selectArea: {
display: 'flex',
flexWrap: 'wrap',
},
})
export function SelectRecipientsUI<T extends Group | Person = Group | Person>(props: SelectRecipientsProps) {
const classes = useStyles()
const { items, onSetSelected } = props
const groupItems = items.filter(item => isGroup(item)) as Group[]
return (
<div className={classes.root}>
<Box className={classes.selectArea} display="flex">
{groupItems.map(item => (
<GroupInChip
key={item.identifier.toText()}
item={item}
disabled={false}
onClick={() => {}}
{...props.GroupInChipProps}
/>
))}
<ClickableChip
ChipProps={{
label: 'Specific Friends (12 selected)',
avatar: <AddIcon />,
onClick() {},
}}
/>
</Box>
</div>
)
}

export function isPerson(x: Person | Group): x is Person {
return x.identifier instanceof PersonIdentifier
}
export function isGroup(x: Person | Group): x is Group {
return x.identifier instanceof GroupIdentifier
}
@@ -0,0 +1,3 @@
export interface SelectSpecificFriendsUIProps {}

export function SelectSpecificFriendsUI() {}

0 comments on commit 0e624f5

Please sign in to comment.