* Add community dialog

* feat(react): add welcome dialog

* feat(react): add connect wallet dialog

* feat(react): add disconnect dialog

* feat(react): add create profile dialog

* feat(react): add welcome dialog

* feat(react): add load throwaway profile dialog

* feat(react): add sync status profile dialog

* fix(react): disconnect dialog spacing

* feat(react): add get started section to main sidebar

* feat(react): add user profile dialog

* feat(react): add edit group dialog

* feat(react): support opening dialogs programmatically

* feat(react): delete legacy components
This commit is contained in:
Pavel 2022-04-12 15:51:49 +02:00 committed by GitHub
parent d9a0fb49ae
commit 9c74cf4685
No known key found for this signature in database
GPG Key ID: 0EB8D75C775AB6F1
40 changed files with 838 additions and 2088 deletions

View File

@ -1,158 +0,0 @@
import React, { useState } from 'react'
import HCaptcha from '@hcaptcha/react-hcaptcha'
import styled, { useTheme } from 'styled-components'
import { useMessengerContext } from '../../contexts/messengerProvider'
import { useModal } from '../../contexts/modalProvider'
import { lightTheme } from '../../styles/themes'
import { Logo } from '../CommunityIdentity'
import { textMediumStyles } from '../Text'
import { Modal } from './Modal'
import { Btn, ButtonSection, Heading, Section, Text } from './ModalStyle'
import type { Theme } from '../../styles/themes'
export const AgreementModalName = 'AgreementModal'
export function AgreementModal() {
const theme = useTheme() as Theme
const { communityData } = useMessengerContext()
const { setModal } = useModal(AgreementModalName)
const [checked, setChecked] = useState(false)
const [token, setToken] = useState('')
return (
<Modal name={AgreementModalName} className="wide">
<Section>
<Heading>Welcome to {communityData?.name}</Heading>
</Section>
<Section>
<LogoWrapper>
<CommunityLogo
style={{
backgroundImage: communityData?.icon
? `url(${communityData?.icon}`
: '',
}}
>
{' '}
{communityData?.icon === undefined &&
communityData?.name.slice(0, 1).toUpperCase()}
</CommunityLogo>
</LogoWrapper>
<AgreementSection>
<Text>{communityData?.description}</Text>
</AgreementSection>
<Agreements>
<Agreement>
<AgreementInput
type="checkbox"
name="agreement"
value="user agreement"
checked={checked}
onChange={e => setChecked(e.target.checked)}
required
/>
<Checkmark />I agree with the above
</Agreement>
<form>
<HCaptcha
sitekey="64702fa3-7f57-41bb-bd43-7afeae54227e"
theme={theme === lightTheme ? 'light' : 'dark'}
onVerify={setToken}
/>
</form>
</Agreements>
</Section>
<ButtonSection>
<Btn
onClick={() => {
setModal(false)
}}
disabled={!token || !checked}
>
Join {communityData?.name}
</Btn>
</ButtonSection>
</Modal>
)
}
const LogoWrapper = styled.div`
display: flex;
justify-content: center;
margin-bottom: 24px;
`
const CommunityLogo = styled(Logo)`
width: 64px;
height: 64px;
`
const AgreementSection = styled.div`
margin-bottom: 24px;
`
const Agreements = styled.div`
display: flex;
justify-content: center;
align-items: center;
`
const Agreement = styled.label`
display: flex;
align-items: center;
position: relative;
color: ${({ theme }) => theme.primary};
padding-left: 26px;
margin-right: 48px;
${textMediumStyles}
& input:checked ~ span {
background-color: ${({ theme }) => theme.tertiary};
border: 1px solid ${({ theme }) => theme.tertiary};
border-radius: 2px;
}
& input:checked ~ span:after {
display: block;
}
`
const AgreementInput = styled.input`
position: absolute;
opacity: 0;
height: 0;
width: 0;
`
const Checkmark = styled.span`
position: absolute;
top: 2px;
left: 0;
width: 18px;
height: 18px;
background-color: ${({ theme }) => theme.inputColor};
border: 1px solid ${({ theme }) => theme.inputColor};
border-radius: 2px;
margin: 0 8px 0 0;
&:after {
content: '';
position: absolute;
display: none;
left: 5px;
top: 1px;
width: 4px;
height: 9px;
border: solid ${({ theme }) => theme.bodyBackgroundColor};
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
`

View File

@ -1,18 +0,0 @@
import React from 'react'
import { ConnectModal } from './ConnectModal'
import { Modal } from './Modal'
export const CoinbaseModalName = 'CoinbaseModal'
export function CoinbaseModal() {
return (
<Modal name={CoinbaseModalName}>
<ConnectModal
name="Coinbase Wallet"
text="Scan QR code or copy and pase it in your Coinbase Wallet."
address="https://www.coinbase.com/wallet"
/>
</Modal>
)
}

View File

@ -1,75 +0,0 @@
import React from 'react'
import styled from 'styled-components'
import { useMessengerContext } from '../../contexts/messengerProvider'
import { useNarrow } from '../../contexts/narrowProvider'
import { DownloadButton } from '../Buttons/DownloadButton'
import { CommunityIdentity } from '../CommunityIdentity'
import { CopyInput } from '../Form/CopyInput'
import { StatusLogo } from '../Icons/StatusLogo'
import { textSmallStyles } from '../Text'
import { Modal } from './Modal'
import { Section, Text } from './ModalStyle'
import type { CommunityIdentityProps } from '../CommunityIdentity'
export const CommunityModalName = 'CommunityModal'
type CommunityModalProps = CommunityIdentityProps
export const CommunityModal = ({ subtitle }: CommunityModalProps) => {
const narrow = useNarrow()
const { communityData } = useMessengerContext()
return (
<Modal name={CommunityModalName}>
<Section>
<CommunityIdentity subtitle={subtitle} />
</Section>
<Section>
<Text>{communityData?.description}</Text>
</Section>
<Section>
<CopyInput
value={communityData?.id ?? ''}
label="Community public key"
/>
<Hint>
To access this community, paste community public key in Status desktop
or mobile app.
{narrow && <StyledDownloadButton />}
</Hint>
</Section>
{!narrow && (
<BottomSection>
<StatusLogo />
<DownloadButton />
</BottomSection>
)}
</Modal>
)
}
const BottomSection = styled(Section)`
display: flex;
flex-direction: column;
align-items: center;
`
const StyledDownloadButton = styled(DownloadButton)`
display: inline;
padding: 0;
margin-left: 4px;
background: none;
font-size: 13px;
line-height: 18px;
text-decoration: underline;
color: ${({ theme }) => theme.secondary};
`
const Hint = styled.p`
margin-top: 16px;
color: ${({ theme }) => theme.secondary};
${textSmallStyles}
`

View File

@ -1,32 +0,0 @@
import React from 'react'
import QRCode from 'qrcode.react'
import { CopyInput } from '../Form/CopyInput'
import { Heading, MiddleSection, QRWrapper, Section, Text } from './ModalStyle'
export const ConnectModalName = 'ConnectModal'
interface ConnectModalProps {
name: string
address: string
text: string
}
export function ConnectModal({ name, address, text }: ConnectModalProps) {
return (
<>
<Section>
<Heading>Connect with {name}</Heading>
</Section>
<MiddleSection>
<Text>{text}</Text>
<QRWrapper>
{' '}
<QRCode value={address} size={224} />
</QRWrapper>
<CopyInput value="2Ef1907d50926...6dt4cEbd975aC5E0Ba" />
</MiddleSection>
</>
)
}

View File

@ -1,158 +0,0 @@
import React, { useState } from 'react'
import styled from 'styled-components'
import { useMessengerContext } from '../../contexts/messengerProvider'
import { useModal } from '../../contexts/modalProvider'
import { buttonStyles } from '../Buttons/buttonStyle'
import { ChannelLogo } from '../Channels/ChannelIcon'
import { inputStyles } from '../Form/inputStyles'
import { AddIcon } from '../Icons/AddIcon'
import { textMediumStyles } from '../Text'
import { Modal } from './Modal'
import { AddWrapper, ButtonSection, Heading, Hint, Section } from './ModalStyle'
export const EditModalName = 'editModal'
export const EditModal = () => {
const { activeChannel, changeGroupChatName } = useMessengerContext()
const { setModal } = useModal(EditModalName)
const [groupName, setGroupName] = useState('')
const [image, setImage] = useState('')
const handleChange = (e: React.FormEvent<HTMLInputElement>) => {
if (e.currentTarget?.files?.length) {
setImage(URL.createObjectURL(e.currentTarget.files[0]))
}
}
const handleUpload = () => {
if (activeChannel) {
if (image) {
activeChannel.icon = image // Need function to send image to waku
setImage('')
}
if (groupName) {
changeGroupChatName(groupName, activeChannel.id)
setGroupName('')
}
setModal(false)
}
}
return (
<Modal name={EditModalName}>
<Section>
<Heading>Edit group name and image</Heading>
</Section>
<Section>
<NameSection>
<LabelGroup>
<Label>Name the group</Label>
<Hint>{groupName.length}/30</Hint>
</LabelGroup>
<NameInput
value={groupName}
type="text"
placeholder="A catchy name"
maxLength={30}
onInput={e => setGroupName(e.currentTarget.value)}
/>
</NameSection>
<LogoSection>
<Label>Group image</Label>
<GroupLogo icon={activeChannel?.icon}>
{!activeChannel?.icon &&
!image &&
activeChannel?.name?.slice(0, 1)?.toUpperCase()}
{image && <LogoPreview src={image} />}
<AddPictureInputWrapper>
<AddIcon />
<AddPictureInput
type="file"
accept="image/png, image/jpeg"
onChange={handleChange}
/>
</AddPictureInputWrapper>
</GroupLogo>
</LogoSection>
</Section>
<ButtonSection>
<SaveBtn onClick={handleUpload}>Save changes</SaveBtn>
</ButtonSection>
</Modal>
)
}
const NameSection = styled.div`
display: flex;
flex-direction: column;
margin-bottom: 16px;
`
const LabelGroup = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`
const Label = styled.p`
color: ${({ theme }) => theme.primary};
padding: 10px 0;
${textMediumStyles}
`
const NameInput = styled.input`
padding: 14px 70px 14px 8px;
${inputStyles}
`
const LogoSection = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
margin-bottom: 8px;
`
const GroupLogo = styled(ChannelLogo)`
width: 128px;
height: 128px;
font-weight: bold;
font-size: 80px;
position: relative;
align-self: center;
margin-right: 0;
`
const LogoPreview = styled.img`
width: 128px;
height: 128px;
border-radius: 50%;
`
const AddPictureInputWrapper = styled(AddWrapper)`
top: 0;
right: 8px;
`
const AddPictureInput = styled.input`
position: absolute;
top: 0;
right: 0;
width: 40px;
height: 40px;
opacity: 0;
z-index: 2;
cursor: pointer;
`
const SaveBtn = styled.button`
padding: 11px 24px;
${buttonStyles}
`

View File

@ -1,47 +0,0 @@
import React from 'react'
import { useMessengerContext } from '../../contexts/messengerProvider'
import { useModal } from '../../contexts/modalProvider'
import { ButtonNo } from '../Buttons/buttonStyle'
import { Modal } from './Modal'
import { ButtonSection, Heading, Section, Text } from './ModalStyle'
export const LeavingModalName = 'LeavingModal'
export const LeavingModal = () => {
const { setModal } = useModal(LeavingModalName)
const { activeChannel, removeChannel } = useMessengerContext()
if (activeChannel)
return (
<Modal name={LeavingModalName}>
<Section>
<Heading>
{activeChannel.type === 'dm' ? 'Delete chat' : 'Leave group'}
</Heading>
</Section>
<Section>
<Text>
Are you sure you want to{' '}
{activeChannel.type === 'dm'
? 'delete this chat'
: 'leave this group'}
?
</Text>
</Section>
<ButtonSection>
<ButtonNo
onClick={() => {
removeChannel(activeChannel.id)
setModal(false)
}}
>
{activeChannel.type === 'dm' ? 'Delete' : 'Leave'}
</ButtonNo>
</ButtonSection>
</Modal>
)
else {
return null
}
}

View File

@ -1,48 +0,0 @@
import React from 'react'
import styled from 'styled-components'
import { useModal } from '../../contexts/modalProvider'
import { ButtonNo, ButtonYes } from '../Buttons/buttonStyle'
import { textMediumStyles } from '../Text'
import { Modal } from './Modal'
import { ButtonSection, Heading, Section } from './ModalStyle'
export const LinkModalName = 'LinkModal'
interface LinkModalProps {
link: string
}
export const LinkModal = ({ link }: LinkModalProps) => {
const { setModal } = useModal(LinkModalName)
return (
<Modal name={LinkModalName}>
<Section>
<Heading>Are you sure you want to visit this website?</Heading>
</Section>
<Section>
<Link>{link}</Link>
</Section>
<ButtonSection>
<ButtonNo onClick={() => setModal(false)}>No</ButtonNo>
<ButtonYes
onClick={() => {
window?.open(link, '_blank', 'noopener')?.focus()
setModal(false)
}}
>
Yes, take me there
</ButtonYes>
</ButtonSection>
</Modal>
)
}
const Link = styled.a`
text-decoration: none;
word-break: break-all;
color: ${({ theme }) => theme.primary};
${textMediumStyles}
`

View File

@ -1,100 +0,0 @@
import React from 'react'
import styled from 'styled-components'
import {
useSetIdentity,
useSetNikcname,
useUserPublicKey,
} from '../../contexts/identityProvider'
import { useMessengerContext } from '../../contexts/messengerProvider'
import { useModal } from '../../contexts/modalProvider'
import { ButtonNo, ButtonYes } from '../Buttons/buttonStyle'
import { UserLogo } from '../Members/UserLogo'
import { Modal } from './Modal'
import { ButtonSection, Heading, Section, Text } from './ModalStyle'
import {
EmojiKey,
UserAddress,
UserAddressWrapper,
UserName,
UserNameWrapper,
} from './ProfileModal'
export const LogoutModalName = 'LogoutModal'
export const LogoutModal = () => {
const { setModal } = useModal(LogoutModalName)
const logout = useSetIdentity()
const setNickname = useSetNikcname()
const userPK = useUserPublicKey()
const { nickname } = useMessengerContext()
if (userPK) {
return (
<Modal name={LogoutModalName}>
<Section>
<Heading>Disconnect</Heading>
</Section>
<Section>
<Text>Do you want to disconnect your profile?</Text>
<UserSection>
<UserLogo
contact={{
id: userPK,
customName: nickname,
trueName: userPK,
}}
radius={80}
colorWheel={[
['red', 150],
['blue', 250],
['green', 360],
]}
/>
<UserNameWrapper className="logout">
{' '}
<UserName className="small">{nickname}</UserName>
</UserNameWrapper>
<UserAddressWrapper className="small">
<UserAddress className="small">
{' '}
Chatkey: {userPK.slice(0, 10)}...
{userPK.slice(-3)}{' '}
</UserAddress>
</UserAddressWrapper>
<EmojiKey className="small">🎩🍞🥑🦍🌈📡💅🏻🔔👵🅱</EmojiKey>
</UserSection>
</Section>
<ButtonSection>
<ButtonNo
onClick={() => {
setModal(false)
logout(undefined)
setNickname(undefined)
}}
>
Disconnect
</ButtonNo>
<ButtonYes
onClick={() => {
setModal(false)
}}
>
Stay Connected
</ButtonYes>
</ButtonSection>
</Modal>
)
}
return null
}
const UserSection = styled.div`
display: flex;
flex-direction: column;
align-items: center;
margin: 8px 0;
`

View File

@ -1,115 +0,0 @@
import React, { useCallback, useEffect } from 'react'
import { createPortal } from 'react-dom'
import styled from 'styled-components'
import { useModal } from '../../contexts/modalProvider'
import { CrossIcon } from '../Icons/CrossIcon'
import type { ReactNode } from 'react'
export interface BasicModalProps {
name: string
className?: string
}
export interface ModalProps extends BasicModalProps {
children: ReactNode
}
export const Modal = ({ name, children, className }: ModalProps) => {
const { isVisible, setModal } = useModal(name)
const listenKeyboard = useCallback(
(event: KeyboardEvent) => {
if (event.key === 'Escape' || event.keyCode === 27) {
setModal(false)
}
},
[setModal]
)
useEffect(() => {
if (isVisible) {
window.addEventListener('keydown', listenKeyboard, true)
return () => {
window.removeEventListener('keydown', listenKeyboard, true)
}
}
}, [isVisible, listenKeyboard])
if (!isVisible) return null
const element = document.getElementById('modal-root')
if (element) {
return createPortal(
<ModalView>
<ModalOverlay onClick={() => setModal(false)} />
<ModalBody className={className}>
<CloseButton onClick={() => setModal(false)} className={className}>
<CrossIcon />
</CloseButton>
{children}
</ModalBody>
</ModalView>,
element
)
}
return null
}
const ModalView = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 9999;
`
const ModalBody = styled.div`
position: absolute;
top: 50%;
left: 50%;
max-width: 480px;
width: 100%;
transform: translate(-50%, -50%);
background: ${({ theme }) => theme.bodyBackgroundColor};
border-radius: 8px;
overflow-y: auto;
&.picture {
max-width: 820px;
border-radius: 0;
}
&.wide {
max-width: 640px;
}
`
const ModalOverlay = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
background: ${({ theme }) => theme.primary};
opacity: 0.4;
`
const CloseButton = styled.button`
position: absolute;
top: 12px;
right: 12px;
padding: 10px;
&.picture {
display: none;
}
`

View File

@ -1,85 +0,0 @@
import styled from 'styled-components'
import { buttonStyles } from '../Buttons/buttonStyle'
import { textMediumStyles } from '../Text'
export const Section = styled.div`
padding: 16px;
& + & {
border-top: 1px solid ${({ theme }) => theme.border};
}
`
export const MiddleSection = styled(Section)`
display: flex;
flex-direction: column;
align-items: center;
`
export const Heading = styled.p`
color: ${({ theme }) => theme.primary};
font-weight: bold;
font-size: 17px;
line-height: 24px;
`
export const Text = styled.p`
color: ${({ theme }) => theme.primary};
${textMediumStyles}
`
export const Btn = styled.button`
padding: 11px 24px;
margin-left: 8px;
${buttonStyles}
&:disabled {
background: ${({ theme }) => theme.border};
color: ${({ theme }) => theme.secondary};
}
`
export const BackBtn = styled(Btn)`
position: absolute;
left: 16px;
top: 16px;
width: 44px;
height: 44px;
border-radius: 50%;
padding: 8px;
margin-left: 0;
& > svg {
fill: ${({ theme }) => theme.tertiary};
}
`
export const ButtonSection = styled(Section)`
display: flex;
justify-content: flex-end;
align-items: center;
position: relative;
`
export const Hint = styled.p`
color: ${({ theme }) => theme.secondary};
font-size: 12px;
line-height: 16px;
`
export const AddWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
position: absolute;
width: 40px;
height: 40px;
background: ${({ theme }) => theme.tertiary};
border-radius: 50%;
`
export const QRWrapper = styled.div`
margin: 30px 0;
`

View File

@ -1,32 +0,0 @@
import React from 'react'
import styled from 'styled-components'
import { Modal } from './Modal'
export const PictureModalName = 'PictureModal' as const
export interface PictureModalProps {
image: string
}
export const PictureModal = ({ image }: PictureModalProps) => {
return (
<Modal name={PictureModalName} className="picture">
<ModalImageWrapper>
<ModalImage src={image}></ModalImage>
</ModalImageWrapper>
</Modal>
)
}
const ModalImageWrapper = styled.div`
display: flex;
max-width: 820px;
max-height: 820px;
`
const ModalImage = styled.img`
width: 100%;
height: 100%;
`

View File

@ -1,131 +0,0 @@
import React, { useEffect, useMemo, useState } from 'react'
import { bufToHex } from '@status-im/core'
import styled from 'styled-components'
import { useNickname, useSetIdentity } from '../../contexts/identityProvider'
import { useModal } from '../../contexts/modalProvider'
import { decryptIdentity, loadEncryptedIdentity } from '../../utils'
import { buttonTransparentStyles } from '../Buttons/buttonStyle'
import { UserLogo } from '../Members/UserLogo'
import { textMediumStyles } from '../Text'
import { Modal } from './Modal'
import {
Btn,
ButtonSection,
Heading,
MiddleSection,
Section,
Text,
} from './ModalStyle'
import {
EmojiKey,
UserAddress,
UserAddressWrapper,
UserName,
} from './ProfileModal'
import { UserCreationModalName } from './UserCreationModal'
import type { Identity } from '@status-im/core'
export const ProfileFoundModalName = 'ProfileFoundModal'
export function ProfileFoundModal() {
const { setModal } = useModal(ProfileFoundModalName)
const { setModal: setCreationModal } = useModal(UserCreationModalName)
const setIdentity = useSetIdentity()
const encryptedIdentity = useMemo(() => loadEncryptedIdentity(), [])
const nickname = useNickname()
const [decryptedIdentity, setDecryptedIdentity] = useState<
Identity | undefined
>(undefined)
useEffect(() => {
if (encryptedIdentity)
(async () => {
setDecryptedIdentity(
await decryptIdentity(encryptedIdentity, 'noPassword')
)
})()
}, [encryptedIdentity])
if (decryptedIdentity) {
return (
<Modal name={ProfileFoundModalName}>
<Section>
<Heading>Throwaway Profile found</Heading>
</Section>
<MiddleSection>
<Logo
contact={{
id: bufToHex(decryptedIdentity.publicKey),
customName: nickname,
trueName: bufToHex(decryptedIdentity.publicKey),
}}
radius={80}
colorWheel={[
['red', 150],
['blue', 250],
['green', 360],
]}
/>
<Name className="small">{nickname}</Name>
<UserAddressWrapper>
<UserAddress className="small">
{' '}
Chatkey: {decryptedIdentity.privateKey.slice(0, 10)}...
{decryptedIdentity.privateKey.slice(-3)}{' '}
</UserAddress>
</UserAddressWrapper>
<EmojiKeyBlock>🎩🍞🥑🦍🌈📡💅🏻🔔👵🅱</EmojiKeyBlock>
<Text>
Throwaway Profile is found in your local browsers cache. Would you
like to load it and use it?{' '}
</Text>
</MiddleSection>
<ButtonSection>
<SkipBtn
onClick={() => {
setCreationModal(true)
setModal(false)
}}
>
Skip
</SkipBtn>
<Btn
onClick={() => {
setIdentity(decryptedIdentity)
setModal(false)
}}
>
Load Throwaway Profile
</Btn>
</ButtonSection>
</Modal>
)
} else {
return null
}
}
const Logo = styled(UserLogo)`
margin-bottom: 8px;
`
const Name = styled(UserName)`
margin-bottom: 8px;
`
const EmojiKeyBlock = styled(EmojiKey)`
margin-bottom: 24px;
`
const SkipBtn = styled.button`
${buttonTransparentStyles}
${textMediumStyles}
`

View File

@ -1,415 +0,0 @@
import React, { useEffect, useMemo, useState } from 'react'
import styled from 'styled-components'
import { useUserPublicKey } from '../../contexts/identityProvider'
import { useMessengerContext } from '../../contexts/messengerProvider'
import { useModal } from '../../contexts/modalProvider'
import { useToasts } from '../../contexts/toastProvider'
import { copy } from '../../utils'
import { buttonStyles } from '../Buttons/buttonStyle'
import {
ClearBtn,
inputStyles,
NameInput,
NameInputWrapper,
} from '../Form/inputStyles'
import { ClearSvgFull } from '../Icons/ClearIconFull'
import { CopyIcon } from '../Icons/CopyIcon'
import { EditIcon } from '../Icons/EditIcon'
import { LeftIcon } from '../Icons/LeftIcon'
import { UntrustworthIcon } from '../Icons/UntrustworthIcon'
import { UserIcon } from '../Icons/UserIcon'
import { textMediumStyles, textSmallStyles } from '../Text'
import { Modal } from './Modal'
import {
BackBtn,
Btn,
ButtonSection,
Heading,
Hint,
Section,
} from './ModalStyle'
export const ProfileModalName = 'profileModal' as const
export type ProfileModalProps = {
id: string
image?: string
renamingState?: boolean
requestState?: boolean
}
export const ProfileModal = () => {
const { props } = useModal(ProfileModalName)
const { id, image, renamingState, requestState } = useMemo(
() => (props ? props : { id: '' }),
[props]
)
const { setToasts } = useToasts()
const { setModal } = useModal(ProfileModalName)
const userPK = useUserPublicKey()
const isUser = useMemo(() => {
if (userPK) {
return id === userPK
} else {
return false
}
}, [id, userPK])
const [renaming, setRenaming] = useState(renamingState ?? false)
useEffect(() => {
setRenaming(renamingState ?? false)
}, [renamingState])
const [request, setRequest] = useState('')
const [requestCreation, setRequestCreation] = useState(requestState ?? false)
useEffect(() => {
setRequestCreation(requestState ?? false)
}, [requestState])
const { contacts, contactsDispatch } = useMessengerContext()
const contact = useMemo(() => contacts[id], [id, contacts])
const [customNameInput, setCustomNameInput] = useState('')
if (!contact) return null
return (
<Modal name={ProfileModalName} className={`${!requestCreation && 'wide'}`}>
<Section>
<Heading>{contact.trueName}s Profile</Heading>
</Section>
<ProfileSection>
<NameSection className={`${requestCreation && 'small'}`}>
{image ? (
<ProfileIcon
style={{
backgroundImage: `url(${image}`,
}}
className={`${requestCreation && 'small'}`}
/>
) : (
<UserIcon modalView={!requestCreation} />
)}
<UserNameWrapper>
<UserName className={`${requestCreation && 'small'}`}>
{contact?.customName ?? contact.trueName}
</UserName>
{contact.isUntrustworthy && <UntrustworthIcon />}
{!renaming && (
<button onClick={() => setRenaming(true)}>
{' '}
{!requestCreation && <EditIcon width={24} height={24} />}
</button>
)}
</UserNameWrapper>
{contact?.customName && (
<UserTrueName>{contact.trueName}</UserTrueName>
)}
</NameSection>
{renaming ? (
<NameInputWrapper>
<NameInput
placeholder="Only you will see this nickname"
value={customNameInput}
onChange={e => setCustomNameInput(e.currentTarget.value)}
/>
{customNameInput && (
<ClearBtn
onClick={() => {
contactsDispatch({
type: 'setCustomName',
payload: { id, customName: undefined },
})
setCustomNameInput('')
}}
>
<ClearSvgFull width={16} height={16} />
</ClearBtn>
)}
</NameInputWrapper>
) : (
<>
<UserAddressWrapper className={`${requestCreation && 'small'}`}>
{requestCreation ? (
<UserAddress>
{id.slice(0, 10)}...{id.slice(-3)}
</UserAddress>
) : (
<>
<UserAddress className={`${requestCreation && 'small'}`}>
Chatkey: {id.slice(0, 30)}
</UserAddress>
<CopyButton onClick={() => copy(id)}>
<CopyIcon width={24} height={24} />
</CopyButton>
</>
)}
</UserAddressWrapper>
<EmojiKey className={`${requestCreation && 'small'}`}>
🎩🍞🥑🦍🌈📡💅🏻🔔👵🅱
</EmojiKey>{' '}
</>
)}
{requestCreation && (
<RequestSection>
<Hint>{request.length}/280</Hint>
<RequestInput
value={request}
placeholder="Say who you are / why you want to became a contact..."
maxLength={280}
onInput={e => setRequest(e.currentTarget.value)}
required
/>
</RequestSection>
)}
</ProfileSection>
<ButtonSection>
{renaming ? (
<>
<BackBtn onClick={() => setRenaming(false)}>
<LeftIcon width={28} height={28} />
</BackBtn>
<Btn
disabled={!customNameInput}
onClick={() => {
contactsDispatch({
type: 'setCustomName',
payload: { id, customName: customNameInput },
})
setRenaming(false)
}}
>
Apply nickname
</Btn>
</>
) : requestCreation ? (
<>
<BackBtn onClick={() => setRequestCreation(false)}>
<LeftIcon width={28} height={28} />
</BackBtn>
<Btn
disabled={!request}
onClick={() => {
setToasts(prev => [
...prev,
{
id: id + request,
type: 'confirmation',
text: 'Contact Request Sent',
},
]),
setRequestCreation(false),
setModal(false),
setRequest('')
}}
>
Send Contact Request
</Btn>
</>
) : (
<>
{!contact.isFriend && !isUser && (
<ProfileBtn
className={contact.blocked ? '' : 'red'}
onClick={() => {
contactsDispatch({ type: 'toggleBlocked', payload: { id } })
}}
>
{contact.blocked ? 'Unblock' : 'Block'}
</ProfileBtn>
)}
{contact.isFriend && (
<ProfileBtn
className="red"
onClick={() =>
contactsDispatch({
type: 'setIsFriend',
payload: { id, isFriend: false },
})
}
>
Remove Contact
</ProfileBtn>
)}
<ProfileBtn
className={contact.isUntrustworthy ? '' : 'red'}
onClick={() =>
contactsDispatch({ type: 'toggleTrustworthy', payload: { id } })
}
>
{contact.isUntrustworthy
? 'Remove Untrustworthy Mark'
: 'Mark as Untrustworthy'}
</ProfileBtn>
{!contact.isFriend && (
<Btn onClick={() => setRequestCreation(true)}>
Send Contact Request
</Btn>
)}
</>
)}
</ButtonSection>
</Modal>
)
}
const ProfileSection = styled(Section)`
display: flex;
flex-direction: column;
align-items: center;
`
const NameSection = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-bottom: 16px;
&.small {
margin-bottom: 0;
}
`
const ProfileIcon = styled.div`
width: 80px;
height: 80px;
display: flex;
justify-content: center;
align-items: end;
border-radius: 50%;
background-color: #bcbdff;
background-size: contain;
background-position: center;
flex-shrink: 0;
position: relative;
cursor: pointer;
&.small {
width: 64px;
height: 64px;
}
`
export const UserNameWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
margin-top: 4px;
& > svg {
fill: ${({ theme }) => theme.tertiary};
}
&.logout {
margin: 8px 0;
}
`
export const UserName = styled.p`
color: ${({ theme }) => theme.primary};
font-weight: bold;
font-size: 22px;
line-height: 30px;
letter-spacing: -0.2px;
margin-right: 8px;
&.small {
font-size: 17px;
line-height: 24px;
margin-right: 0;
}
`
const UserTrueName = styled.p`
color: ${({ theme }) => theme.primary};
font-size: 12px;
line-height: 16px;
letter-spacing: 0.1px;
margin-top: 8px;
`
export const UserAddressWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 24px;
&.small {
margin-bottom: 8px;
}
`
export const UserAddress = styled.p`
display: flex;
letter-spacing: 1px;
margin-right: 8px;
color: ${({ theme }) => theme.secondary};
${textMediumStyles}
&.small {
margin-right: 0;
${textSmallStyles}
}
`
export const EmojiKey = styled.div`
width: 116px;
gap: 8px;
font-size: 15px;
line-height: 14px;
letter-spacing: 0.2px;
&.small {
width: 83px;
${textSmallStyles}
}
`
const ProfileBtn = styled.button`
padding: 11px 24px;
${buttonStyles}
background: ${({ theme }) => theme.bodyBackgroundColor};
border: 1px solid ${({ theme }) => theme.border};
margin-left: 8px;
&.red {
color: ${({ theme }) => theme.redColor};
}
&.red:hover {
background: ${({ theme }) => theme.buttonNoBgHover};
}
`
const CopyButton = styled.button`
& > svg {
fill: ${({ theme }) => theme.tertiary};
}
`
const RequestSection = styled.div`
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-end;
margin: 16px 0;
`
const RequestInput = styled.textarea`
width: 100%;
height: 152px;
padding: 10px 16px;
resize: none;
margin-top: 16px;
font-family: 'Inter';
${inputStyles}
`

View File

@ -1,21 +0,0 @@
import React from 'react'
import { useModal } from '../../contexts/modalProvider'
import { Modal } from './Modal'
export const SizeLimitModalName = 'SizeLimitModal'
export function SizeLimitModal() {
const { setModal } = useModal(SizeLimitModalName)
return (
<Modal name={SizeLimitModalName}>
<button
onClick={() => setModal(false)}
style={{ padding: '20px', display: 'block' }}
>
File size must be less than 1MB
</button>
</Modal>
)
}

View File

@ -1,89 +0,0 @@
import React, { useState } from 'react'
import QRCode from 'qrcode.react'
import styled from 'styled-components'
import { buttonStyles } from '../Buttons/buttonStyle'
import { LoginInstructions } from '../Form/LoginInstructions'
import { PasteInput } from '../Form/PasteInput'
import { Modal } from './Modal'
import { Heading, MiddleSection, Section } from './ModalStyle'
export const StatusModalName = 'StatusModal'
export enum StatusModalState {
Mobile,
Desktop,
}
export function StatusModal() {
const [modalState, setModalState] = useState<StatusModalState>(
StatusModalState.Mobile
)
const mobileFlow = modalState === StatusModalState.Mobile
const desktopFlow = modalState === StatusModalState.Desktop
const switchModalState = (state: StatusModalState) => {
setModalState(prev => (prev === state ? StatusModalState.Mobile : state))
}
return (
<Modal name={StatusModalName}>
<Section>
<Heading>Sync with Status profile</Heading>
</Section>
<MiddleSectionStatus>
<Switch>
<SwitchBtn
className={`${modalState === StatusModalState.Mobile && 'active'}`}
onClick={() => switchModalState(StatusModalState.Mobile)}
>
From mobile
</SwitchBtn>
<SwitchBtnMobile
className={`${modalState === StatusModalState.Desktop && 'active'}`}
onClick={() => switchModalState(StatusModalState.Desktop)}
>
From desktop
</SwitchBtnMobile>
</Switch>
{mobileFlow && <QRCode value="https://status.im/get/" size={158} />}
{desktopFlow && <PasteInput label="Paste sync code" />}
<LoginInstructions mobileFlow={mobileFlow} />
</MiddleSectionStatus>
</Modal>
)
}
const MiddleSectionStatus = styled(MiddleSection)`
height: 514px;
`
const Switch = styled.div`
display: flex;
align-items: center;
margin-bottom: 32px;
`
const SwitchBtn = styled.button`
${buttonStyles}
width: 159px;
padding: 7px 0;
text-align: center;
color: ${({ theme }) => theme.tertiary};
background: ${({ theme }) => theme.buttonBg};
position: relative;
&.active {
background: ${({ theme }) => theme.tertiary};
color: ${({ theme }) => theme.bodyBackgroundColor};
z-index: 10000;
}
`
const SwitchBtnMobile = styled(SwitchBtn)`
margin-left: -8px;
`

View File

@ -1,214 +0,0 @@
import React, { useState } from 'react'
import { Identity } from '@status-im/core'
import styled from 'styled-components'
import {
useSetIdentity,
useSetNikcname,
useUserPublicKey,
useWalletIdentity,
} from '../../contexts/identityProvider'
import { useModal } from '../../contexts/modalProvider'
import { useNameError } from '../../hooks/useNameError'
import { saveIdentity } from '../../utils'
import { ClearBtn, NameInput, NameInputWrapper } from '../Form/inputStyles'
import { NameError } from '../Form/NameError'
import { AddIcon } from '../Icons/AddIcon'
import { ChainIcon } from '../Icons/ChainIcon'
import { ClearSvgFull } from '../Icons/ClearIconFull'
import { LeftIcon } from '../Icons/LeftIcon'
import { UserLogo } from '../Members/UserLogo'
import { AgreementModalName } from './AgreementModal'
import { Modal } from './Modal'
import {
AddWrapper,
BackBtn,
Btn,
ButtonSection,
Heading,
Hint,
Section,
Text,
} from './ModalStyle'
import { EmojiKey, UserAddress } from './ProfileModal'
import type { Contact } from '../../models/Contact'
export const UserCreationModalName = 'UserCreationModal'
export function UserCreationModal() {
const walletIdentity = useWalletIdentity()
const userPK = useUserPublicKey()
const setIdentity = useSetIdentity()
const setNickname = useSetNikcname()
const [customNameInput, setCustomNameInput] = useState('')
const error = useNameError(customNameInput)
const [nextStep, setNextStep] = useState(false)
const { setModal } = useModal(UserCreationModalName)
const { setModal: setAgreementModal } = useModal(AgreementModalName)
return (
<Modal name={UserCreationModalName}>
<Section>
<Heading>Create a Status Profile</Heading>
</Section>
<MiddleSection className={`${!nextStep && 'initial'}`}>
{nextStep ? (
<Title>Your emojihash and identicon ring</Title>
) : (
<Title>Your profile</Title>
)}
{nextStep ? (
<StyledHint>
{' '}
This set of emojis and coloured ring around your avatar are unique
and represent your chat key, so your friends can easily distinguish
you from potential impersonators.
</StyledHint>
) : (
<StyledHint>
Longer and unusual names are better as they are <br /> less likely
to be used by someone else.
</StyledHint>
)}
<LogoWrapper>
<UserLogo
contact={{ trueName: customNameInput } as Contact}
radius={80}
colorWheel={[
['red', 150],
['blue', 250],
['green', 360],
]}
/>
{!nextStep && (
<AddIconWrapper>
<AddIcon />
</AddIconWrapper>
)}
</LogoWrapper>
{!nextStep && (
<NameInputWrapper>
<NameInput
placeholder="Display name"
value={customNameInput}
onChange={e => setCustomNameInput(e.currentTarget.value)}
maxLength={24}
/>
{customNameInput && (
<ClearBtn onClick={() => setCustomNameInput('')}>
<ClearSvgFull width={16} height={16} />
</ClearBtn>
)}
</NameInputWrapper>
)}
<NameError error={error} />
{nextStep && userPK && (
<>
<UserAddress>
{' '}
Chatkey: {userPK.slice(0, 10)}...
{userPK.slice(-3)}{' '}
</UserAddress>
<ChainIcons>
<ChainIcon className="transformed" />
<ChainIcon />
</ChainIcons>
<UserAttributes>
<EmojiKey>🎩🍞🥑🦍🌈📡💅🏻🔔👵🅱</EmojiKey>
<UserLogo
radius={40}
colorWheel={[
['red', 150],
['blue', 250],
['green', 360],
]}
/>
</UserAttributes>
</>
)}
</MiddleSection>
<ButtonSection>
<BackBtn onClick={() => setModal(false)}>
<LeftIcon width={28} height={28} />
</BackBtn>
<Btn
onClick={() => {
if (nextStep) {
setModal(false)
setAgreementModal(true)
} else {
const identity = walletIdentity || Identity.generate()
setNickname(customNameInput)
setIdentity(identity)
!walletIdentity && saveIdentity(identity, 'noPassword')
setNextStep(true)
}
}}
disabled={!customNameInput || error !== 0}
>
Next
</Btn>
</ButtonSection>
</Modal>
)
}
const MiddleSection = styled(Section)`
height: 420px;
display: flex;
flex-direction: column;
align-items: center;
&.initial {
padding: 32px;
}
`
const Title = styled(Text)`
font-weight: bold;
font-size: 22px;
line-height: 30px;
letter-spacing: -0.2px;
margin-bottom: 16px;
`
const StyledHint = styled(Hint)`
font-size: 15px;
line-height: 22px;
margin-bottom: 32px;
text-align: center;
`
const LogoWrapper = styled.div`
position: relative;
display: flex;
margin-bottom: 32px;
`
const AddIconWrapper = styled(AddWrapper)`
top: 0;
right: -50%;
transform: translateX(-50%);
`
const ChainIcons = styled.div`
width: 104px;
display: flex;
justify-content: space-between;
align-items: center;
margin: 16px 0;
`
const UserAttributes = styled.div`
width: 200px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
`

View File

@ -1,26 +0,0 @@
import React from 'react'
import styled from 'styled-components'
import { UserCreationButtons } from '../UserCreation/UserCreationButtons'
import { Modal } from './Modal'
import { Heading, Section } from './ModalStyle'
export const UserCreationStartModalName = 'UserCreationStartModal'
export const UserCreationStartModal = () => {
return (
<Modal name={UserCreationStartModalName}>
<Section>
<Heading>Jump into the discussion</Heading>
</Section>
<MiddleSection>
<UserCreationButtons permission={true} />
</MiddleSection>
</Modal>
)
}
const MiddleSection = styled(Section)`
padding: 48px 0;
`

View File

@ -1,19 +0,0 @@
import React from 'react'
import { ConnectModal } from './ConnectModal'
import { Modal } from './Modal'
export const WalletConnectModalName = 'WalletConnectModal'
export function WalletConnectModal() {
return (
<Modal name={WalletConnectModalName}>
<ConnectModal
name="WalletConnect"
text="Scan QR code with a WallectConnect-compatible wallet or copy code and
paste it in your hardware wallet."
address="https://walletconnect.com/"
/>
</Modal>
)
}

View File

@ -1,165 +0,0 @@
import React, { useCallback } from 'react'
import { genPrivateKeyWithEntropy, Identity } from '@status-im/core'
import styled from 'styled-components'
import {
useSetIdentity,
useSetWalletIdentity,
} from '../../contexts/identityProvider'
import { useMessengerContext } from '../../contexts/messengerProvider'
import { useModal } from '../../contexts/modalProvider'
import { CoinbaseLogo } from '../Icons/CoinbaseLogo'
import { MetamaskLogo } from '../Icons/MetamaskLogo'
import { WalletConnectLogo } from '../Icons/WalletConnectLogo'
import { CoinbaseModalName } from './CoinbaseModal'
import { Modal } from './Modal'
import { Heading, MiddleSection, Section, Text } from './ModalStyle'
import { UserCreationModalName } from './UserCreationModal'
import { WalletConnectModalName } from './WalletConnectModal'
export const WalletModalName = 'WalletModal'
export function WalletModal() {
const { setModal } = useModal(WalletModalName)
const setIdentity = useSetIdentity()
const setWalletIdentity = useSetWalletIdentity()
const { setModal: setUserCreationModal } = useModal(UserCreationModalName)
const { setModal: setWalleConnectModal } = useModal(WalletConnectModalName)
const { setModal: setCoinbaseModal } = useModal(CoinbaseModalName)
const { messenger } = useMessengerContext()
const handleMetamaskClick = useCallback(async () => {
// TODO: Add types for global Ethereum object
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dappUrl = window.location.hostname
const ethereum = (window as any)?.ethereum as any | undefined
if (document.location.origin !== dappUrl) {
alert('You are not signing in from correct url!')
return
}
if (ethereum && messenger) {
try {
if (ethereum?.isMetaMask) {
const [account] = await ethereum.request({
method: 'eth_requestAccounts',
})
const msgParams = JSON.stringify({
domain: {
chainId: 1,
name: window.location.origin,
version: '1',
},
message: {
action: 'Status CommunityChatRoom Key',
onlySignOn: dappUrl,
message:
"This signature will be used to decrypt chat communications; check that the 'onlySignOn' property of this message matches the current website address.",
},
primaryType: 'Mail',
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
],
Mail: [
{ name: 'action', type: 'string' },
{ name: 'onlySignOn', type: 'string' },
{ name: 'message', type: 'string' },
],
},
})
const params = [account, msgParams]
const method = 'eth_signTypedData_v4'
const signature = await ethereum.request({
method,
params,
from: account,
})
const privateKey = genPrivateKeyWithEntropy(signature)
const loadedIdentity = new Identity(privateKey)
const userInNetwork = await messenger.checkIfUserInWakuNetwork(
loadedIdentity.publicKey
)
if (userInNetwork) {
setIdentity(loadedIdentity)
} else {
setWalletIdentity(loadedIdentity)
setUserCreationModal(true)
}
setModal(false)
return
}
} catch {
alert('Error')
}
}
alert('Metamask not found')
}, [
messenger,
setIdentity,
setModal,
setWalletIdentity,
setUserCreationModal,
])
return (
<Modal name={WalletModalName}>
<Section>
<Heading>Connect an Ethereum Wallet</Heading>
</Section>
<MiddleSectionWallet>
<Text>Choose a way to chat using your Ethereum address.</Text>
<Wallets>
<Wallet onClick={() => (setModal(false), setWalleConnectModal(true))}>
<Heading>WalletConnect</Heading>
<WalletConnectLogo />
</Wallet>
<Wallet onClick={() => (setModal(false), setCoinbaseModal(true))}>
<Heading>Coinbase Wallet</Heading>
<CoinbaseLogo />
</Wallet>
<Wallet onClick={handleMetamaskClick}>
<Heading>MetaMask</Heading>
<MetamaskLogo />
</Wallet>
</Wallets>
</MiddleSectionWallet>
</Modal>
)
}
const MiddleSectionWallet = styled(MiddleSection)`
align-items: stretch;
`
const Wallets = styled.div`
display: flex;
flex-direction: column;
align-items: center;
margin-top: 16px;
`
const Wallet = styled.div`
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding: 12px 16px;
border: 1px solid ${({ theme }) => theme.skeletonDark};
border-radius: 8px;
cursor: pointer;
&:hover {
background: ${({ theme }) => theme.buttonBgHover};
}
`

View File

@ -0,0 +1,21 @@
import React from 'react'
import { Avatar, Dialog, TextInput } from '~/src/system'
export const EditGroupChatDialog = () => {
return (
<Dialog title="Edit Group">
<Dialog.Body align="center">
<TextInput
label="Group name"
placeholder="A catchy name"
maxLength={30}
/>
<Avatar size="120" />
</Dialog.Body>
<Dialog.Actions>
<Dialog.Action>Save changes</Dialog.Action>
</Dialog.Actions>
</Dialog>
)
}

View File

@ -2,6 +2,11 @@ import React from 'react'
import { BellIcon } from '~/src/icons/bell-icon' import { BellIcon } from '~/src/icons/bell-icon'
import { ContextMenu, DropdownMenu } from '~/src/system' import { ContextMenu, DropdownMenu } from '~/src/system'
import { useAlertDialog } from '~/src/system/dialog/alert-dialog'
import { useDialog } from '~/src/system/dialog/dialog'
import { UserProfileDialog } from '../user-profile-dialog'
import { EditGroupChatDialog } from './edit-group-chat-dialog'
interface Props { interface Props {
type: 'dropdown' | 'context' type: 'dropdown' | 'context'
@ -13,7 +18,24 @@ export const ChatMenu = (props: Props) => {
const Menu = type === 'dropdown' ? DropdownMenu : ContextMenu const Menu = type === 'dropdown' ? DropdownMenu : ContextMenu
const renderMenuItems = () => { const userProfileDialog = useDialog(UserProfileDialog)
const editGroupChatDialog = useDialog(EditGroupChatDialog)
const deleteChatDialog = useAlertDialog({
title: 'Delete Chat',
description: 'Are you sure you want to delete this chat?',
actionLabel: 'Delete',
actionVariant: 'danger',
cancelLabel: 'Keep',
})
const leaveGroupDialog = useAlertDialog({
title: 'Leave Group',
description: 'Are you sure you want to leave this group chat?',
actionLabel: 'Leave',
actionVariant: 'danger',
cancelLabel: 'Stay',
})
const commonMenuItems = ( const commonMenuItems = (
<> <>
<Menu.TriggerItem label="Mute Chat" icon={<BellIcon />}> <Menu.TriggerItem label="Mute Chat" icon={<BellIcon />}>
@ -34,36 +56,51 @@ export const ChatMenu = (props: Props) => {
) )
if (chatType === 'channel') { if (chatType === 'channel') {
return commonMenuItems return <Menu>{commonMenuItems}</Menu>
} }
if (chatType === 'group-chat') { if (chatType === 'group-chat') {
return ( return (
<> <Menu>
<Menu.Item icon={<BellIcon />}>Add / remove from group</Menu.Item> <Menu.Item icon={<BellIcon />}>Add / remove from group</Menu.Item>
<Menu.Item icon={<BellIcon />}>Edit name and image</Menu.Item> <Menu.Item
icon={<BellIcon />}
onSelect={() => editGroupChatDialog.open({})}
>
Edit name and image
</Menu.Item>
<Menu.Separator /> <Menu.Separator />
{commonMenuItems} {commonMenuItems}
<Menu.Separator /> <Menu.Separator />
<Menu.Item icon={<BellIcon />} danger> <Menu.Item
icon={<BellIcon />}
danger
onSelect={() => leaveGroupDialog.open()}
>
Leave Chat Leave Chat
</Menu.Item> </Menu.Item>
</> </Menu>
) )
} }
return ( return (
<> <Menu>
<Menu.Item icon={<BellIcon />}>View Profile</Menu.Item> <Menu.Item
icon={<BellIcon />}
onSelect={() => userProfileDialog.open({ contact: 'Satoshi' })}
>
View Profile
</Menu.Item>
<Menu.Separator /> <Menu.Separator />
{commonMenuItems} {commonMenuItems}
<Menu.Separator /> <Menu.Separator />
<Menu.Item icon={<BellIcon />} danger> <Menu.Item
icon={<BellIcon />}
danger
onSelect={() => deleteChatDialog.open()}
>
Delete Chat Delete Chat
</Menu.Item> </Menu.Item>
</> </Menu>
) )
} }
return <Menu>{renderMenuItems()}</Menu>
}

View File

@ -0,0 +1,63 @@
import React from 'react'
import {
Avatar,
Dialog,
// EmojiHash,
Flex,
Heading,
Text,
TextInput,
} from '~/src/system'
export const CreateProfileDialog = () => {
return (
<Dialog title="Create Status Profile">
{/* <Dialog.Body align="stretch" css={{ paddingTop: 32 }}>
<Heading
size="22"
weight="600"
align="center"
css={{ marginBottom: 16 }}
>
Your emojihash and identicon ring
</Heading>
<Text color="gray" align="center" size="15" css={{ marginBottom: 32 }}>
This set of emojis and coloured ring around your avatar are unique and
represent your chat key, so your friends can easily distinguish you
from potential impersonators.
</Text>
<Flex justify="center" css={{ marginBottom: 38 }}>
<Avatar size="80" />
</Flex>
<Text color="gray" align="center">
Chatkey: 0x63FaC920149...fae4d52fe3BD377
</Text>
<EmojiHash />
</Dialog.Body> */}
<Dialog.Body align="stretch" css={{ paddingTop: 32 }}>
<Heading
size="22"
weight="600"
align="center"
css={{ marginBottom: 16 }}
>
Your profile
</Heading>
<Text color="gray" align="center" css={{ marginBottom: 32 }}>
Longer and unusual names are better as they
<br />
are less likely to be used by someone else.
</Text>
<Flex justify="center" css={{ marginBottom: 38 }}>
<Avatar size="80" />
</Flex>
<TextInput placeholder="Display name" />
</Dialog.Body>
<Dialog.Actions>
<Dialog.Action>Next</Dialog.Action>
</Dialog.Actions>
</Dialog>
)
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,68 @@
import React, { useState } from 'react'
import { styled } from '~/src/styles/config'
import { Dialog, Grid, Text } from '~/src/system'
// TODO: Add wallet integration
export const ConnectWalletDialog = () => {
const [wallet, setWallet] = useState<'coinbase' | undefined>()
console.log(wallet)
// TODO: Add wallet logos
return (
<Dialog title="Connect Ethereum Wallet">
<Dialog.Body>
<Text css={{ marginBottom: '$3' }}>
Choose a way to chat using your Ethereum address.
</Text>
<Grid gap={2} css={{ paddingBottom: '$2' }}>
<ButtonItem>WalletConnect</ButtonItem>
<ButtonItem onClick={() => setWallet('coinbase')}>
Coinbase Wallet
</ButtonItem>
<ButtonItem>MetaMask</ButtonItem>
</Grid>
</Dialog.Body>
</Dialog>
)
}
// const CoinbaseWalletDialog = () => {
// return (
// <Dialog title="Connect with Coinbase Wallet">
// <Dialog.Body>
// <Text>Scan QR code or copy and pase it in your Coinbase Wallet.</Text>
// </Dialog.Body>
// </Dialog>
// )
// }
// const WalletConnectDialog = () => {
// return (
// <Dialog title="Connect with WalletConnect">
// <Dialog.Body>
// <Text>
// Scan QR code with a WallectConnect-compatible wallet or copy code and
// paste it in your hardware wallet.
// </Text>
// </Dialog.Body>
// </Dialog>
// )
// }
const ButtonItem = styled('button', {
width: '100%',
padding: '12px 16px',
textAlign: 'left',
border: '1px solid $gray-3',
borderRadius: '$2',
color: '$accent-1',
fontSize: 17,
lineHeight: 1.5,
fontWeight: '$600',
'&:hover': {
backgroundColor: '$primary-3',
},
})

View File

@ -0,0 +1,166 @@
import React from 'react'
import { CreateProfileDialog } from '~/src/components/create-profile-dialog'
import { useLocalStorage } from '~/src/hooks/use-local-storage'
import { Button, Flex } from '~/src/system'
import { DialogTrigger } from '~/src/system/dialog'
import { Grid } from '~/src/system/grid'
import { Heading } from '~/src/system/heading'
import { ConnectWalletDialog } from './connect-wallet-dialog'
import { SyncStatusProfileDialog } from './sync-status-profile-dialog'
import { ThrowawayProfileFoundDialog } from './throwaway-profile-found-dialog'
export const GetStarted = () => {
const [throwawayProfile] = useLocalStorage('cipherIdentityt', null)
const handleSkip = () => {
// TODO: Add skip logic
}
return (
<Flex direction="column" align="center" gap={5}>
<svg
width={65}
height={64}
viewBox="0 0 65 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_3_411236)">
<path
d="M43.045 20.516a21.054 21.054 0 0121.45 20.704 21.217 21.217 0 01-.491 4.988c-.598 2.712-.954 5.472-.954 8.249v5.368a1.49 1.49 0 01-1.49 1.49h-5.368c-2.777 0-5.537.355-8.249.954a21.225 21.225 0 01-4.987.491 21.054 21.054 0 01-20.704-21.45c.173-11.406 9.387-20.62 20.793-20.794z"
fill="url(#paint0_linear_3_411236)"
/>
<path
d="M43.045 20.516a21.054 21.054 0 0121.45 20.704 21.217 21.217 0 01-.491 4.988c-.598 2.712-.954 5.472-.954 8.249v5.368a1.49 1.49 0 01-1.49 1.49h-5.368c-2.777 0-5.537.355-8.249.954a21.225 21.225 0 01-4.987.491 21.054 21.054 0 01-20.704-21.45c.173-11.406 9.387-20.62 20.793-20.794z"
fill="url(#paint1_linear_3_411236)"
/>
<path
d="M26.637 1.237A25.65 25.65 0 00.505 26.461c-.04 2.09.168 4.124.599 6.076.729 3.304 1.162 6.666 1.162 10.05v6.54c0 1.002.812 1.814 1.814 1.814h6.54c3.384 0 6.746.433 10.05 1.162 1.952.43 3.986.64 6.076.599A25.65 25.65 0 0051.97 26.57C51.758 12.675 40.533 1.45 26.637 1.237z"
fill="url(#paint2_linear_3_411236)"
/>
<path
d="M17.024 25.592a3.971 3.971 0 00-2.9-1.258 3.986 3.986 0 00-3.987 3.986c0 1.145.485 2.174 1.257 2.901l8.6 8.6a3.972 3.972 0 002.9 1.257 3.986 3.986 0 003.987-3.986 3.972 3.972 0 00-1.258-2.901l-8.6-8.6z"
fill="url(#paint3_linear_3_411236)"
/>
<path
d="M14.123 32.308a3.986 3.986 0 100-7.973 3.986 3.986 0 000 7.973z"
fill="#fff"
/>
<path
d="M28.297 25.592a3.971 3.971 0 00-2.9-1.258 3.986 3.986 0 00-3.987 3.986c0 1.145.485 2.174 1.258 2.901l8.599 8.6a3.972 3.972 0 002.9 1.257 3.986 3.986 0 003.987-3.986 3.972 3.972 0 00-1.258-2.901l-8.599-8.6z"
fill="url(#paint4_linear_3_411236)"
/>
<path
d="M25.398 32.308a3.986 3.986 0 100-7.973 3.986 3.986 0 000 7.973z"
fill="#fff"
/>
<path
d="M39.572 25.592a3.971 3.971 0 00-2.901-1.258 3.986 3.986 0 00-3.986 3.986c0 1.145.485 2.174 1.257 2.901l8.6 8.6a3.972 3.972 0 002.9 1.257 3.986 3.986 0 003.986-3.986 3.972 3.972 0 00-1.257-2.901l-8.6-8.6z"
fill="url(#paint5_linear_3_411236)"
/>
<path
d="M36.672 32.308a3.986 3.986 0 100-7.973 3.986 3.986 0 000 7.973z"
fill="#fff"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_3_411236"
x1={39.1099}
y1={37.3759}
x2={70.1253}
y2={68.3913}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#A7F3CE" />
<stop offset={1} stopColor="#61DB99" />
</linearGradient>
<linearGradient
id="paint1_linear_3_411236"
x1={49.2627}
y1={47.5281}
x2={36.0891}
y2={34.3557}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#61DB99" stopOpacity={0} />
<stop offset={1} stopColor="#009E74" />
</linearGradient>
<linearGradient
id="paint2_linear_3_411236"
x1={16.8336}
y1={22.8099}
x2={49.5796}
y2={55.5558}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#62E1FB" />
<stop offset={1} stopColor="#00A2F3" />
</linearGradient>
<linearGradient
id="paint3_linear_3_411236"
x1={22.9908}
y1={37.1889}
x2={5.74961}
y2={19.9482}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#00A2F3" stopOpacity={0} />
<stop offset={1} stopColor="#0075CD" />
</linearGradient>
<linearGradient
id="paint4_linear_3_411236"
x1={34.2635}
y1={37.1884}
x2={17.0228}
y2={19.9478}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#00A2F3" stopOpacity={0} />
<stop offset={1} stopColor="#2A353D" />
</linearGradient>
<linearGradient
id="paint5_linear_3_411236"
x1={45.5379}
y1={37.1886}
x2={28.2972}
y2={19.9479}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#00A2F3" stopOpacity={0} />
<stop offset={1} stopColor="#0075CD" />
</linearGradient>
<clipPath id="clip0_3_411236">
<path fill="#fff" transform="translate(.5)" d="M0 0H64V64H0z" />
</clipPath>
</defs>
</svg>
<Heading align="center" size="17" weight="600">
Want to jump into the discussion?
</Heading>
<Grid gap={3} align="center" justify="center">
<DialogTrigger>
<Button>Sync with Status profile</Button>
<SyncStatusProfileDialog />
</DialogTrigger>
<DialogTrigger>
<Button>Connect Wallet</Button>
<ConnectWalletDialog />
</DialogTrigger>
<DialogTrigger>
<Button variant="outline">Use Throwaway Profile</Button>
{throwawayProfile ? (
<ThrowawayProfileFoundDialog onSkip={handleSkip} />
) : (
<CreateProfileDialog />
)}
</DialogTrigger>
</Grid>
</Flex>
)
}

View File

@ -0,0 +1,70 @@
import React, { useState } from 'react'
import { QRCodeSVG } from 'qrcode.react'
import { styled } from '~/src/styles/config'
import { Box, ButtonGroup, Dialog, Text, TextInput } from '~/src/system'
export const SyncStatusProfileDialog = () => {
const [platform, setPlatform] = useState<'mobile' | 'desktop'>('mobile')
return (
<Dialog title="Sync with Status profile">
<Dialog.Body
align="center"
gap="6"
css={{ paddingTop: 24, paddingBottom: 48 }}
>
<ButtonGroup value={platform} onChange={setPlatform}>
<ButtonGroup.Item value="mobile">From Mobile</ButtonGroup.Item>
<ButtonGroup.Item value="desktop">From Desktop</ButtonGroup.Item>
</ButtonGroup>
{platform === 'mobile' && (
<>
{/* TODO: Add mobile QR code */}
<QRCodeSVG value="https://status.im/get/" size={158} />
<Box>
<List>
{/* // TODO: Add icons to instructions */}
<ListItem>1. Open Status App on your mobile</ListItem>
<ListItem>2. Navigate yourself to tab</ListItem>
<ListItem>3. Select</ListItem>
<ListItem>4. Tap</ListItem>
<ListItem>5. Scan the sync code from this screen </ListItem>
</List>
</Box>
</>
)}
{platform === 'desktop' && (
<>
<Box css={{ width: '100%' }}>
<TextInput label="Sync Code" placeholder="0x000000" />
</Box>
<List>
{/* TODO: Add icons to instructions */}
<ListItem>1. Open Status App on your desktop</ListItem>
<ListItem>2. Navigate yourself to tab</ListItem>
<ListItem>3. Select</ListItem>
<ListItem>4. Tap</ListItem>
<ListItem>5. Scan the sync code from this screen </ListItem>
</List>
</>
)}
</Dialog.Body>
</Dialog>
)
}
const List = styled('ul', {
display: 'flex',
flexDirection: 'column',
gap: 20,
})
const ListItem = styled('li', Text, {
defaultVariants: {
color: 'gray',
},
})

View File

@ -0,0 +1,47 @@
import React from 'react'
import { useProfile } from '~/src/protocol/use-profile'
import { Avatar, Dialog, EmojiHash, Flex, Heading, Text } from '~/src/system'
interface Props {
onSkip: () => void
}
export const ThrowawayProfileFoundDialog = (props: Props) => {
const { onSkip } = props
const profile = useProfile()
const handleLoadThrowawayProfile = () => {
// TODO: load throwaway profile
}
return (
<Dialog title="Throwaway Profile Found">
<Dialog.Body gap="5">
<Flex direction="column" align="center" gap="2">
<Avatar size={64} src={profile.imageUrl} />
<Heading weight="600">{profile.name}</Heading>
<Text color="gray">
Chatkey: 0x63FaC9201494f0bd17B9892B9fae4d52fe3BD377
</Text>
<EmojiHash />
</Flex>
<Text>
Throwaway profile is found in your local {"browser's"} storage.
<br />
Would you like to load it and use it?
</Text>
</Dialog.Body>
<Dialog.Actions>
<Dialog.Action variant="outline" onClick={onSkip}>
Skip
</Dialog.Action>
<Dialog.Action onClick={handleLoadThrowawayProfile}>
Load Throwaway Profile
</Dialog.Action>
</Dialog.Actions>
</Dialog>
)
}

View File

@ -38,4 +38,5 @@ const Separator = styled('div', {
margin: '16px 0', margin: '16px 0',
height: 1, height: 1,
background: 'rgba(0, 0, 0, 0.1)', background: 'rgba(0, 0, 0, 0.1)',
flexShrink: 0,
}) })

View File

@ -0,0 +1,28 @@
import React from 'react'
import { useProfile } from '~/src/protocol/use-profile'
import { Avatar, Dialog, EmojiHash, Flex, Heading, Text } from '~/src/system'
export const DisconnectDialog = () => {
const profile = useProfile()
return (
<Dialog title="Disconnect">
<Dialog.Body gap="5">
<Text>Do you want to disconnect your profile from this browser?</Text>
<Flex direction="column" align="center" gap="2">
<Avatar size={64} src={profile.imageUrl} />
<Heading weight="600">{profile.name}</Heading>
<Text color="gray">
Chatkey: 0x63FaC9201494f0bd17B9892B9fae4d52fe3BD377
</Text>
<EmojiHash />
</Flex>
</Dialog.Body>
<Dialog.Actions>
<Dialog.Cancel>Stay Connected</Dialog.Cancel>
<Dialog.Action variant="danger">Disconnect</Dialog.Action>
</Dialog.Actions>
</Dialog>
)
}

View File

@ -1,37 +1,32 @@
import React from 'react' import React from 'react'
import { useProfile } from '~/src/protocol/use-profile'
import { styled } from '~/src/styles/config' import { styled } from '~/src/styles/config'
import { import { Avatar, DialogTrigger, EthAddress, Flex, Text } from '~/src/system'
Avatar,
Dialog, import { DisconnectDialog } from './disconnect-dialog'
DialogTrigger,
EthAddress,
Flex,
Text,
} from '~/src/system'
export const UserItem = () => { export const UserItem = () => {
const profile = useProfile()
return ( return (
<Flex align="center" justify="between"> <Flex align="center" justify="between">
<Flex gap="2" align="center" css={{ height: 56 }}> <Flex gap="2" align="center" css={{ height: 56 }}>
<Avatar <Avatar size={32} src={profile.imageUrl} />
size={32}
src="https://images.unsplash.com/photo-1546776310-eef45dd6d63c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1620&q=80"
/>
<div> <div>
<Flex align="center" gap={1}> <Flex align="center" gap={1}>
<Text size="15" color="black-70"> <Text size="15" color="accent">
Pavel {profile.name}
</Text> </Text>
</Flex> </Flex>
<EthAddress size={10} color="gray"> <EthAddress size={10} color="gray">
71C7656EC7ab88b098defB751B7401B5f6d8976F {profile.publicKey}
</EthAddress> </EthAddress>
</div> </div>
</Flex> </Flex>
<DialogTrigger> <DialogTrigger>
<LogoutButton> <DisconnectButton>
<svg <svg
width="20" width="20"
height="18" height="18"
@ -48,21 +43,14 @@ export const UserItem = () => {
fill="#4360DF" fill="#4360DF"
/> />
</svg> </svg>
</LogoutButton> </DisconnectButton>
<Dialog <DisconnectDialog />
title="Disconnect"
cancelLabel="Disconnect"
actionLabel="Stay Connected"
>
<Text>Do you want to disconnect your profile from this browser?</Text>
<Avatar size="120" />
</Dialog>
</DialogTrigger> </DialogTrigger>
</Flex> </Flex>
) )
} }
const LogoutButton = styled('button', { const DisconnectButton = styled('button', {
background: 'rgba(67, 96, 223, 0.1)', background: 'rgba(67, 96, 223, 0.1)',
borderRadius: '50%', borderRadius: '50%',
height: 32, height: 32,

View File

@ -0,0 +1,28 @@
import React from 'react'
import { Avatar, Dialog, EmojiHash, Heading, Text } from '~/src/system'
interface Props {
contact: string
}
// TODO: Add all states of contact, wait for desktop release
export const UserProfileDialog = (props: Props) => {
const { contact, ...dialogProps } = props
return (
<Dialog title={`${contact}'s Profile`} size="640" {...dialogProps}>
<Dialog.Body align="center">
<Avatar size="80" />
<Heading size="22">{contact}</Heading>
<Text>Chatkey:0x63FaC9201494f0bd17B9892B9fae4d52fe3BD377</Text>
<EmojiHash />
</Dialog.Body>
<Dialog.Actions>
<Dialog.Action variant="danger">Remove Contact</Dialog.Action>
<Dialog.Action variant="danger">Mark Untrustworthy</Dialog.Action>
<Dialog.Action>Send Contact Request</Dialog.Action>
</Dialog.Actions>
</Dialog>
)
}

View File

@ -0,0 +1,46 @@
import React, { useState } from 'react'
import { useCommunity } from '~/src/protocol/use-community'
import { Avatar, Checkbox, Dialog, Flex, Text } from '~/src/system'
export const WelcomeDialog = () => {
const { name, imageUrl, requestNeeded } = useCommunity()
const [agreed, setAgreed] = useState(false)
return (
<Dialog title={`Welcome to ${name}`} size={640}>
<Dialog.Body gap="4">
<Flex justify="center">
<Avatar size="64" src={imageUrl} />
</Flex>
<Text>
CryptoKitties sed ut perspiciatis unde omnis iste natus error sit
voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque
ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae
dicta sunt explicabo.
<br />
<br />
Ut enim ad minim veniam Excepteur sint occaecat cupidatat non proident
Duis aute irure Dolore eu fugiat nulla pariatur 🚗 consectetur
adipiscing elit.
<br />
<br />
Nemo enim 😋 ipsam voluptatem quia voluptas sit aspernatur aut odit
aut fugit, sed quia consequuntur magni dolores eos qui ratione
voluptatem sequi nesciunt.
</Text>
<Flex>
<Checkbox checked={agreed} onChange={setAgreed}>
I agree with the above
</Checkbox>
</Flex>
</Dialog.Body>
<Dialog.Actions>
<Dialog.Action disabled={agreed === false}>
{requestNeeded ? 'Request to Join' : `Join ${name}`}
</Dialog.Action>
</Dialog.Actions>
</Dialog>
)
}

View File

@ -0,0 +1,36 @@
import React, { cloneElement, createContext, useContext, useState } from 'react'
const DialogContext = createContext<
((dialog: React.ReactElement) => void) | null
>(null)
interface Props {
children: React.ReactNode
}
export const DialogProvider = (props: Props) => {
const { children } = props
const [dialog, setDialog] = useState<React.ReactElement | null>(null)
return (
<DialogContext.Provider value={setDialog}>
{children}
{dialog &&
cloneElement(dialog, {
defaultOpen: true,
onOpenChange: () => setDialog(null),
})}
</DialogContext.Provider>
)
}
export const useDialogContext = () => {
const context = useContext(DialogContext)
if (!context) {
throw new Error('useDialogContext must be used within a DialogProvider')
}
return context
}

View File

@ -5,6 +5,7 @@ import styled, { ThemeProvider } from 'styled-components'
import { AppProvider } from '~/src/contexts/app-context' import { AppProvider } from '~/src/contexts/app-context'
import { ChatStateProvider } from '~/src/contexts/chatStateProvider' import { ChatStateProvider } from '~/src/contexts/chatStateProvider'
import { DialogProvider } from '~/src/contexts/dialog-context'
import { IdentityProvider } from '~/src/contexts/identityProvider' import { IdentityProvider } from '~/src/contexts/identityProvider'
import { MessengerProvider } from '~/src/contexts/messengerProvider' import { MessengerProvider } from '~/src/contexts/messengerProvider'
import { ModalProvider } from '~/src/contexts/modalProvider' import { ModalProvider } from '~/src/contexts/modalProvider'
@ -32,6 +33,7 @@ export const Community = (props: Props) => {
return ( return (
<Router> <Router>
<AppProvider config={props}> <AppProvider config={props}>
<DialogProvider>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<NarrowProvider myRef={ref}> <NarrowProvider myRef={ref}>
<ModalProvider> <ModalProvider>
@ -56,6 +58,7 @@ export const Community = (props: Props) => {
</ModalProvider> </ModalProvider>
</NarrowProvider> </NarrowProvider>
</ThemeProvider> </ThemeProvider>
</DialogProvider>
</AppProvider> </AppProvider>
</Router> </Router>
) )

View File

@ -8,40 +8,6 @@ import { Chat } from '~/src/routes/chat'
import { NewChat } from '~/src/routes/new-chat' import { NewChat } from '~/src/routes/new-chat'
import { styled } from '~/src/styles/config' import { styled } from '~/src/styles/config'
// import { AgreementModal } from '~/src/components/Modals/AgreementModal'
// import { CoinbaseModal } from '~/src/components/Modals/CoinbaseModal'
// import { CommunityModal } from '~/src/components/Modals/CommunityModal'
// import { EditModal } from '~/src/components/Modals/EditModal'
// import { LeavingModal } from '~/src/components/Modals/LeavingModal'
// import { LogoutModal } from '~/src/components/Modals/LogoutModal'
// import { ProfileFoundModal } from '~/src/components/Modals/ProfileFoundModal'
// import { ProfileModal } from '~/src/components/Modals/ProfileModal'
// import { StatusModal } from '~/src/components/Modals/StatusModal'
// import { UserCreationModal } from '~/src/components/Modals/UserCreationModal'
// import { UserCreationStartModal } from '~/src/components/Modals/UserCreationStartModal'
// import { WalletConnectModal } from '~/src/components/Modals/WalletConnectModal'
// import { WalletModal } from '~/src/components/Modals/WalletModal'
// function Modals() {
// return (
// <>
// <CommunityModal subtitle="Public Community" />
// <UserCreationModal />
// <EditModal />
// <ProfileModal />
// <StatusModal />
// <WalletModal />
// <WalletConnectModal />
// <CoinbaseModal />
// <LogoutModal />
// <AgreementModal />
// <ProfileFoundModal />
// <UserCreationStartModal />
// <LeavingModal />
// </>
// )
// }
export function Messenger() { export function Messenger() {
const { options } = useAppState() const { options } = useAppState()

View File

@ -1,7 +1,8 @@
import React from 'react' import React, { cloneElement, useCallback, useRef } from 'react'
import * as Primitive from '@radix-ui/react-alert-dialog' import * as Primitive from '@radix-ui/react-alert-dialog'
import { useDialogContext } from '~/src/contexts/dialog-context'
import { CrossIcon } from '~/src/icons/cross-icon' import { CrossIcon } from '~/src/icons/cross-icon'
import { Button } from '../button' import { Button } from '../button'
@ -10,19 +11,25 @@ import { IconButton } from '../icon-button'
import { Text } from '../text' import { Text } from '../text'
import { Actions, Body, Content, Header, Overlay } from './styles' import { Actions, Body, Content, Header, Overlay } from './styles'
import type { ButtonProps } from '../button'
import type { DialogContentProps } from '@radix-ui/react-dialog'
interface TriggerProps { interface TriggerProps {
open?: boolean
onOpenChange?: (open: boolean) => void
children: [React.ReactElement, React.ReactElement] children: [React.ReactElement, React.ReactElement]
} }
const AlertDialogTrigger = (props: TriggerProps) => { const AlertDialogTrigger = (props: TriggerProps) => {
const { children } = props const { children, open, onOpenChange, ...triggerProps } = props
const [trigger, content] = children const [trigger, content] = children
return ( return (
<Primitive.Root> <Primitive.Root open={open} onOpenChange={onOpenChange}>
<Primitive.Trigger asChild>{trigger}</Primitive.Trigger> <Primitive.Trigger asChild>
{cloneElement(trigger, triggerProps)}
</Primitive.Trigger>
{content} {content}
</Primitive.Root> </Primitive.Root>
) )
@ -32,16 +39,26 @@ interface DialogProps {
title: string title: string
description: string description: string
actionLabel: string actionLabel: string
actionVariant?: ButtonProps['variant']
cancelLabel?: string cancelLabel?: string
onOpenAutoFocus?: DialogContentProps['onOpenAutoFocus']
onCloseAutoFocus?: DialogContentProps['onCloseAutoFocus']
} }
const AlertDialog = (props: DialogProps) => { const AlertDialog = (props: DialogProps) => {
const { title, description, cancelLabel = 'Cancel', actionLabel } = props const {
title,
description,
actionLabel,
actionVariant,
cancelLabel = 'Cancel',
...contentProps
} = props
return ( return (
<Primitive.Portal> <Primitive.Portal>
<Overlay as={Primitive.Overlay} /> <Overlay as={Primitive.Overlay} />
<Content as={Primitive.Content}> <Content as={Primitive.Content} {...contentProps}>
<Header> <Header>
<Heading as={Primitive.Title} weight="600" size="17"> <Heading as={Primitive.Title} weight="600" size="17">
{title} {title}
@ -60,7 +77,7 @@ const AlertDialog = (props: DialogProps) => {
<Button>{cancelLabel}</Button> <Button>{cancelLabel}</Button>
</Primitive.Cancel> </Primitive.Cancel>
<Primitive.Action asChild> <Primitive.Action asChild>
<Button>{actionLabel}</Button> <Button variant={actionVariant}>{actionLabel}</Button>
</Primitive.Action> </Primitive.Action>
</Actions> </Actions>
</Content> </Content>
@ -68,4 +85,23 @@ const AlertDialog = (props: DialogProps) => {
) )
} }
export { AlertDialog, AlertDialogTrigger } const useAlertDialog = (props: DialogProps) => {
const render = useDialogContext()
const triggerRef = useRef<HTMLButtonElement>(null)
const handleCloseAutoFocus = () => {
triggerRef.current?.focus()
}
const open = useCallback(() => {
render(
<Primitive.Root>
<AlertDialog {...props} onCloseAutoFocus={handleCloseAutoFocus} />
</Primitive.Root>
)
}, [props, render])
return { open, triggerRef }
}
export { AlertDialog, AlertDialogTrigger, useAlertDialog }

View File

@ -1,7 +1,8 @@
import React, { useState } from 'react' import React, { useCallback, useRef, useState } from 'react'
import * as Primitive from '@radix-ui/react-dialog' import * as Primitive from '@radix-ui/react-dialog'
import { useDialogContext } from '~/src/contexts/dialog-context'
import { CrossIcon } from '~/src/icons/cross-icon' import { CrossIcon } from '~/src/icons/cross-icon'
import { Button } from '../button' import { Button } from '../button'
@ -12,6 +13,7 @@ import { Actions, Body, Content, Header, Overlay } from './styles'
import type { ButtonProps } from '../button' import type { ButtonProps } from '../button'
import type { Variants } from './styles' import type { Variants } from './styles'
import type { DialogContentProps } from '@radix-ui/react-dialog'
interface DialogTriggerProps { interface DialogTriggerProps {
children: [React.ReactElement, React.ReactElement] children: [React.ReactElement, React.ReactElement]
@ -36,6 +38,8 @@ interface DialogProps {
title: React.ReactNode title: React.ReactNode
children: React.ReactNode children: React.ReactNode
size?: Variants['size'] size?: Variants['size']
onOpenAutoFocus?: DialogContentProps['onOpenAutoFocus']
onCloseAutoFocus?: DialogContentProps['onCloseAutoFocus']
} }
const Dialog = (props: DialogProps) => { const Dialog = (props: DialogProps) => {
@ -79,4 +83,26 @@ Dialog.Cancel = Cancel
Dialog.Action = Action Dialog.Action = Action
Dialog.Separator = Separator Dialog.Separator = Separator
export { Dialog, DialogTrigger } const useDialog = <Props,>(Component: React.ComponentType<Props>) => {
const render = useDialogContext()
const triggerRef = useRef<HTMLButtonElement>(null)
const handleCloseAutoFocus = () => {
triggerRef.current?.focus()
}
const open = useCallback(
(props: Props) => {
render(
<Primitive.Root>
<Component {...props} onCloseAutoFocus={handleCloseAutoFocus} />
</Primitive.Root>
)
},
[render, Component]
)
return { open, triggerRef }
}
export { Dialog, DialogTrigger, useDialog }

View File

@ -1,2 +1,2 @@
export { AlertDialog, AlertDialogTrigger } from './alert-dialog' export { AlertDialog, AlertDialogTrigger, useAlertDialog } from './alert-dialog'
export { Dialog, DialogTrigger } from './dialog' export { Dialog, DialogTrigger, useDialog } from './dialog'

View File

@ -1,5 +1,7 @@
import { keyframes, styled } from '~/src/styles/config' import { keyframes, styled } from '~/src/styles/config'
import { Flex } from '../flex'
import type { VariantProps } from '~/src/styles/config' import type { VariantProps } from '~/src/styles/config'
export type Variants = VariantProps<typeof Content> export type Variants = VariantProps<typeof Content>
@ -76,8 +78,12 @@ export const Header = styled('div', {
borderBottom: '1px solid #eee', borderBottom: '1px solid #eee',
}) })
export const Body = styled('div', { export const Body = styled(Flex, {
padding: '16px', padding: '16px',
defaultVariants: {
direction: 'column',
},
}) })
export const Actions = styled('div', { export const Actions = styled('div', {

View File

@ -10,6 +10,8 @@ export {
AlertDialogTrigger, AlertDialogTrigger,
Dialog, Dialog,
DialogTrigger, DialogTrigger,
useAlertDialog,
useDialog,
} from './dialog' } from './dialog'
export { DropdownMenu, DropdownMenuTrigger } from './dropdown-menu' export { DropdownMenu, DropdownMenuTrigger } from './dropdown-menu'
export { EmojiHash } from './emoji-hash' export { EmojiHash } from './emoji-hash'