Delete legacy code (#249)
* refactor(react): delete unused contexts * refactor(react): delete unused hooks * refactor(react): delete unused components * refactor(react): delete unused types * refactor(react): delete unused utils * feat(react): simplify Community component structure
This commit is contained in:
parent
a781ef4f7c
commit
5cd02e8d64
|
@ -1,32 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { LeftIcon } from '../Icons/LeftIcon'
|
||||
|
||||
interface BackButtonProps {
|
||||
onBtnClick: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function BackButton({ onBtnClick, className }: BackButtonProps) {
|
||||
return (
|
||||
<BackBtn onClick={onBtnClick} className={className}>
|
||||
<LeftIcon width={24} height={24} className="black" />
|
||||
</BackBtn>
|
||||
)
|
||||
}
|
||||
|
||||
const BackBtn = styled.button`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 8px;
|
||||
width: 32px;
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
|
||||
&.narrow {
|
||||
position: static;
|
||||
margin-right: 13px;
|
||||
}
|
||||
`
|
|
@ -1,85 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { buttonStyles } from './buttonStyle'
|
||||
|
||||
const userAgent = window.navigator.userAgent
|
||||
const platform = window.navigator.platform
|
||||
const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K']
|
||||
const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']
|
||||
const iosPlatforms = ['iPhone', 'iPad', 'iPod']
|
||||
|
||||
interface DownloadButtonProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const DownloadButton = ({ className }: DownloadButtonProps) => {
|
||||
const [link, setlink] = useState('https://status.im/get/')
|
||||
const [os, setOs] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (macosPlatforms.includes(platform)) {
|
||||
setlink(
|
||||
'https://status-im-files.ams3.cdn.digitaloceanspaces.com/StatusIm-Desktop-v0.3.0-beta-a8c37d.dmg'
|
||||
)
|
||||
setOs('Mac')
|
||||
} else if (iosPlatforms.includes(platform)) {
|
||||
setlink(
|
||||
'https://apps.apple.com/us/app/status-private-communication/id1178893006'
|
||||
)
|
||||
setOs('iOS')
|
||||
} else if (windowsPlatforms.includes(platform)) {
|
||||
setlink(
|
||||
'https://status-im-files.ams3.cdn.digitaloceanspaces.com/StatusIm-Desktop-v0.3.0-beta-a8c37d.exe'
|
||||
)
|
||||
setOs('Windows')
|
||||
} else if (/Android/.test(userAgent)) {
|
||||
setlink(
|
||||
'https://play.google.com/store/apps/details?id=im.status.ethereum'
|
||||
)
|
||||
setOs('Android')
|
||||
} else if (/Linux/.test(platform)) {
|
||||
setlink(
|
||||
'https://status-im-files.ams3.cdn.digitaloceanspaces.com/StatusIm-Desktop-v0.3.0-beta-a8c37d.tar.gz'
|
||||
)
|
||||
setOs('Linux')
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={className}
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{os
|
||||
? `${className === 'activity' ? 'd' : 'D'}ownload Status for ${os}`
|
||||
: `${className === 'activity' ? 'd' : 'D'}ownload Status`}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const Link = styled.a`
|
||||
margin-top: 24px;
|
||||
padding: 11px 32px;
|
||||
|
||||
${buttonStyles}
|
||||
|
||||
&.activity {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
font-style: italic;
|
||||
border-radius: 0;
|
||||
font-weight: 400;
|
||||
text-decoration: underline;
|
||||
background: inherit;
|
||||
|
||||
&:hover {
|
||||
background: inherit;
|
||||
color: ${({ theme }) => theme.tertiary};
|
||||
}
|
||||
}
|
||||
`
|
|
@ -1,59 +0,0 @@
|
|||
import styled, { css } from 'styled-components'
|
||||
|
||||
export const buttonStyles = css`
|
||||
font-family: 'Inter';
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
border-radius: 8px;
|
||||
color: ${({ theme }) => theme.tertiary};
|
||||
background: ${({ theme }) => theme.buttonBg};
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.buttonBgHover};
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: ${({ theme }) => theme.buttonBg};
|
||||
}
|
||||
`
|
||||
|
||||
export const buttonTransparentStyles = css`
|
||||
font-family: 'Inter';
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
color: ${({ theme }) => theme.tertiary};
|
||||
background: inherit;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.buttonBgHover};
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: ${({ theme }) => theme.buttonBg};
|
||||
}
|
||||
`
|
||||
|
||||
export const ButtonNo = styled.button`
|
||||
padding: 11px 24px;
|
||||
margin-right: 16px;
|
||||
|
||||
${buttonStyles}
|
||||
background: ${({ theme }) => theme.buttonNoBg};
|
||||
color: ${({ theme }) => theme.redColor};
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.buttonNoBgHover};
|
||||
}
|
||||
`
|
||||
|
||||
export const ButtonYes = styled.button`
|
||||
padding: 11px 24px;
|
||||
|
||||
${buttonStyles}
|
||||
`
|
|
@ -1,212 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useMessengerContext } from '../../contexts/messengerProvider'
|
||||
import { useNarrow } from '../../contexts/narrowProvider'
|
||||
import { ChannelMenu } from '../Form/ChannelMenu'
|
||||
import { Tooltip } from '../Form/Tooltip'
|
||||
import { GroupIcon } from '../Icons/GroupIcon'
|
||||
import { MutedIcon } from '../Icons/MutedIcon'
|
||||
import { textMediumStyles } from '../Text'
|
||||
import { ChannelIcon } from './ChannelIcon'
|
||||
|
||||
import type { ChannelData } from '../../models/ChannelData'
|
||||
|
||||
function RenderChannelName({
|
||||
channel,
|
||||
activeView,
|
||||
className,
|
||||
}: {
|
||||
channel: ChannelData
|
||||
activeView?: boolean
|
||||
className?: string
|
||||
}) {
|
||||
const { activeChannel } = useMessengerContext()
|
||||
switch (channel.type) {
|
||||
case 'group':
|
||||
return (
|
||||
<div className={className}>
|
||||
{!activeView && (
|
||||
<GroupIcon active={channel.id === activeChannel?.id} />
|
||||
)}
|
||||
{` ${channel.name}`}
|
||||
</div>
|
||||
)
|
||||
case 'channel':
|
||||
return <div className={className}>{`# ${channel.name}`}</div>
|
||||
case 'dm':
|
||||
return <div className={className}>{channel.name.slice(0, 20)}</div>
|
||||
}
|
||||
}
|
||||
|
||||
interface ChannelProps {
|
||||
channel: ChannelData
|
||||
notified?: boolean
|
||||
mention?: number
|
||||
isActive: boolean
|
||||
activeView?: boolean
|
||||
onClick?: () => void
|
||||
setEditGroup?: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
export function Channel({
|
||||
channel,
|
||||
isActive,
|
||||
activeView,
|
||||
onClick,
|
||||
notified,
|
||||
mention,
|
||||
setEditGroup,
|
||||
}: ChannelProps) {
|
||||
const narrow = useNarrow()
|
||||
const { channelsDispatch } = useMessengerContext()
|
||||
|
||||
return (
|
||||
<ChannelWrapper
|
||||
className={`${isActive && 'active'}`}
|
||||
isNarrow={narrow && activeView}
|
||||
onClick={onClick}
|
||||
id={!activeView ? `${channel.id + 'contextMenu'}` : ''}
|
||||
>
|
||||
<ChannelInfo activeView={activeView}>
|
||||
<ChannelIcon channel={channel} activeView={activeView} />
|
||||
<ChannelTextInfo activeView={activeView && !narrow}>
|
||||
<ChannelNameWrapper>
|
||||
<ChannelName
|
||||
channel={channel}
|
||||
active={isActive || activeView || narrow}
|
||||
activeView={activeView}
|
||||
muted={channel?.isMuted}
|
||||
notified={notified}
|
||||
/>
|
||||
{channel?.isMuted && activeView && !narrow && (
|
||||
<MutedBtn
|
||||
onClick={() =>
|
||||
channelsDispatch({ type: 'ToggleMuted', payload: channel.id })
|
||||
}
|
||||
>
|
||||
<MutedIcon />
|
||||
<Tooltip tip="Unmute" className="muted" />
|
||||
</MutedBtn>
|
||||
)}
|
||||
</ChannelNameWrapper>
|
||||
{activeView && (
|
||||
<ChannelDescription>{channel.description}</ChannelDescription>
|
||||
)}
|
||||
</ChannelTextInfo>
|
||||
</ChannelInfo>
|
||||
{!activeView && !!mention && !channel?.isMuted && (
|
||||
<NotificationBagde>{mention}</NotificationBagde>
|
||||
)}
|
||||
{channel?.isMuted && !activeView && <MutedIcon />}
|
||||
{!activeView && (
|
||||
<ChannelMenu
|
||||
channel={channel}
|
||||
setEditGroup={setEditGroup}
|
||||
className={narrow ? 'sideNarrow' : 'side'}
|
||||
/>
|
||||
)}
|
||||
</ChannelWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const ChannelWrapper = styled.div<{ isNarrow?: boolean }>`
|
||||
width: ${({ isNarrow }) => (isNarrow ? 'calc(100% - 162px)' : '100%')};
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&.active,
|
||||
&:active {
|
||||
background-color: ${({ theme }) => theme.activeChannelBackground};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme, isNarrow }) => isNarrow && theme.border};
|
||||
}
|
||||
`
|
||||
|
||||
export const ChannelInfo = styled.div<{ activeView?: boolean }>`
|
||||
display: flex;
|
||||
align-items: ${({ activeView }) => (activeView ? 'flex-start' : 'center')};
|
||||
overflow-x: hidden;
|
||||
`
|
||||
|
||||
const ChannelTextInfo = styled.div<{ activeView?: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
padding: ${({ activeView }) => activeView && '0 24px 24px 0'};
|
||||
`
|
||||
|
||||
const ChannelNameWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export const ChannelName = styled(RenderChannelName)<{
|
||||
muted?: boolean
|
||||
notified?: boolean
|
||||
active?: boolean
|
||||
activeView?: boolean
|
||||
}>`
|
||||
font-weight: ${({ notified, muted, active }) =>
|
||||
notified && !muted && !active ? '600' : '500'};
|
||||
opacity: ${({ notified, muted, active }) =>
|
||||
muted ? '0.4' : notified || active ? '1.0' : '0.7'};
|
||||
color: ${({ theme }) => theme.primary};
|
||||
margin-right: ${({ muted, activeView }) =>
|
||||
muted && activeView ? '8px' : ''};
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
${textMediumStyles}
|
||||
`
|
||||
|
||||
const ChannelDescription = styled.p`
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.1px;
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
const NotificationBagde = styled.div`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
background-color: ${({ theme }) => theme.notificationColor};
|
||||
color: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
`
|
||||
|
||||
const MutedBtn = styled.button`
|
||||
padding: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
position: relative;
|
||||
|
||||
&:hover > svg {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
&:hover > div {
|
||||
visibility: visible;
|
||||
}
|
||||
`
|
|
@ -1,56 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useNarrow } from '../../contexts/narrowProvider'
|
||||
|
||||
import type { ChannelData } from '../../models/ChannelData'
|
||||
|
||||
interface ChannelIconProps {
|
||||
channel: ChannelData
|
||||
activeView?: boolean
|
||||
}
|
||||
|
||||
export function ChannelIcon({ channel, activeView }: ChannelIconProps) {
|
||||
const narrow = useNarrow()
|
||||
|
||||
return (
|
||||
<ChannelLogo
|
||||
icon={channel.icon}
|
||||
className={activeView ? 'active' : narrow ? 'narrow' : ''}
|
||||
>
|
||||
{!channel.icon && channel.name.slice(0, 1).toUpperCase()}
|
||||
</ChannelLogo>
|
||||
)
|
||||
}
|
||||
|
||||
export const ChannelLogo = styled.div<{ icon?: string }>`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-right: 10px;
|
||||
border-radius: 50%;
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
background-color: ${({ theme }) => theme.iconColor};
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-image: ${({ icon }) => icon && `url(${icon}`};
|
||||
color: ${({ theme }) => theme.iconTextColor};
|
||||
|
||||
&.active {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&.narrow {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 20px;
|
||||
}
|
||||
`
|
|
@ -1,164 +0,0 @@
|
|||
import React, { useMemo } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { ChatState, useChatState } from '../../contexts/chatStateProvider'
|
||||
import { useIdentity } from '../../contexts/identityProvider'
|
||||
import { useMessengerContext } from '../../contexts/messengerProvider'
|
||||
import { CreateIcon } from '../Icons/CreateIcon'
|
||||
import { UserCreation } from '../UserCreation/UserCreation'
|
||||
import { Channel } from './Channel'
|
||||
|
||||
interface ChannelsProps {
|
||||
onCommunityClick?: () => void
|
||||
setEditGroup?: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
type GenerateChannelsProps = ChannelsProps & {
|
||||
type: string
|
||||
}
|
||||
|
||||
function GenerateChannels({
|
||||
type,
|
||||
onCommunityClick,
|
||||
setEditGroup,
|
||||
}: GenerateChannelsProps) {
|
||||
const { mentions, notifications, activeChannel, channelsDispatch, channels } =
|
||||
useMessengerContext()
|
||||
|
||||
const channelList = useMemo(() => Object.values(channels), [channels])
|
||||
|
||||
const setChatState = useChatState()[1]
|
||||
return (
|
||||
<>
|
||||
{channelList
|
||||
.filter(channel => channel.type === type)
|
||||
.map(channel => (
|
||||
<Channel
|
||||
key={channel.id}
|
||||
channel={channel}
|
||||
isActive={channel.id === activeChannel?.id}
|
||||
notified={notifications?.[channel.id] > 0}
|
||||
mention={mentions?.[channel.id]}
|
||||
onClick={() => {
|
||||
channelsDispatch({ type: 'ChangeActive', payload: channel.id })
|
||||
if (onCommunityClick) {
|
||||
onCommunityClick()
|
||||
}
|
||||
setChatState(ChatState.ChatBody)
|
||||
}}
|
||||
setEditGroup={setEditGroup}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type ChatsListProps = {
|
||||
onCommunityClick?: () => void
|
||||
setEditGroup?: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
function ChatsSideBar({ onCommunityClick, setEditGroup }: ChatsListProps) {
|
||||
const setChatState = useChatState()[1]
|
||||
return (
|
||||
<>
|
||||
<ChatsBar>
|
||||
<Heading>Messages</Heading>
|
||||
<EditBtn onClick={() => setChatState(ChatState.ChatCreation)}>
|
||||
<CreateIcon />
|
||||
</EditBtn>
|
||||
</ChatsBar>
|
||||
<ChatsList>
|
||||
<GenerateChannels
|
||||
type={'group'}
|
||||
onCommunityClick={onCommunityClick}
|
||||
setEditGroup={setEditGroup}
|
||||
/>
|
||||
<GenerateChannels type={'dm'} onCommunityClick={onCommunityClick} />
|
||||
</ChatsList>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function Channels({ onCommunityClick, setEditGroup }: ChannelsProps) {
|
||||
const identity = useIdentity()
|
||||
return (
|
||||
<ChannelList>
|
||||
<GenerateChannels type={'channel'} onCommunityClick={onCommunityClick} />
|
||||
<Chats>
|
||||
{identity ? (
|
||||
<ChatsSideBar
|
||||
onCommunityClick={onCommunityClick}
|
||||
setEditGroup={setEditGroup}
|
||||
/>
|
||||
) : (
|
||||
<UserCreation permission={true} />
|
||||
)}
|
||||
</Chats>
|
||||
</ChannelList>
|
||||
)
|
||||
}
|
||||
|
||||
export const ChannelList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
`
|
||||
|
||||
const Chats = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 16px;
|
||||
margin-top: 16px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: calc(100% - 24px);
|
||||
height: 1px;
|
||||
background-color: ${({ theme }) => theme.primary};
|
||||
opacity: 0.1;
|
||||
}
|
||||
`
|
||||
|
||||
const ChatsBar = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
`
|
||||
|
||||
const ChatsList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
const Heading = styled.p`
|
||||
font-weight: bold;
|
||||
font-size: 17px;
|
||||
line-height: 24px;
|
||||
color: ${({ theme }) => theme.primary};
|
||||
`
|
||||
|
||||
const EditBtn = styled.button`
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.inputColor};
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: ${({ theme }) => theme.sectionBackgroundColor};
|
||||
}
|
||||
`
|
|
@ -1,130 +0,0 @@
|
|||
import React, { useMemo } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useUserPublicKey } from '../../contexts/identityProvider'
|
||||
import { useMessengerContext } from '../../contexts/messengerProvider'
|
||||
import { useNarrow } from '../../contexts/narrowProvider'
|
||||
import { textMediumStyles } from '../Text'
|
||||
import { ChannelInfo, ChannelName } from './Channel'
|
||||
import { ChannelLogo } from './ChannelIcon'
|
||||
|
||||
import type { ChannelData } from '../../models/ChannelData'
|
||||
|
||||
type ChannelBeggingTextProps = {
|
||||
channel: ChannelData
|
||||
}
|
||||
|
||||
function ChannelBeggingText({ channel }: ChannelBeggingTextProps) {
|
||||
const userPK = useUserPublicKey()
|
||||
const { contacts } = useMessengerContext()
|
||||
const members = useMemo(() => {
|
||||
if (channel?.members && userPK) {
|
||||
return channel.members
|
||||
.filter(contact => contact.id !== userPK)
|
||||
.map(member => contacts?.[member.id] ?? member)
|
||||
}
|
||||
return []
|
||||
}, [channel, contacts, userPK])
|
||||
|
||||
switch (channel.type) {
|
||||
case 'dm':
|
||||
return (
|
||||
<EmptyText>
|
||||
Any messages you send here are encrypted and can only be read by you
|
||||
and <br />
|
||||
<span>{channel.name.slice(0, 10)}</span>.
|
||||
</EmptyText>
|
||||
)
|
||||
case 'group':
|
||||
return (
|
||||
<EmptyTextGroup>
|
||||
{userPK && <span>{userPK}</span>} created a group with{' '}
|
||||
{members.map((contact, idx) => (
|
||||
<span key={contact.id}>
|
||||
{contact?.customName ?? contact.trueName.slice(0, 10)}
|
||||
{idx < members.length - 1 && <> and </>}
|
||||
</span>
|
||||
))}
|
||||
</EmptyTextGroup>
|
||||
)
|
||||
case 'channel':
|
||||
return (
|
||||
<EmptyText>
|
||||
Welcome to the beginning of the <span>#{channel.name}</span> channel!
|
||||
</EmptyText>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
type EmptyChannelProps = {
|
||||
channel: ChannelData
|
||||
}
|
||||
|
||||
export function EmptyChannel({ channel }: EmptyChannelProps) {
|
||||
const narrow = useNarrow()
|
||||
|
||||
return (
|
||||
<Wrapper className={`${!narrow && 'wide'}`}>
|
||||
<ChannelInfoEmpty>
|
||||
<ChannelLogoEmpty icon={channel.icon}>
|
||||
{' '}
|
||||
{!channel.icon && channel.name.slice(0, 1).toUpperCase()}
|
||||
</ChannelLogoEmpty>
|
||||
<ChannelNameEmpty active={true} channel={channel} />
|
||||
</ChannelInfoEmpty>
|
||||
<ChannelBeggingText channel={channel} />
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
|
||||
&.wide {
|
||||
margin-top: 24px;
|
||||
}
|
||||
`
|
||||
|
||||
const ChannelInfoEmpty = styled(ChannelInfo)`
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
const ChannelLogoEmpty = styled(ChannelLogo)`
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
font-weight: bold;
|
||||
font-size: 51px;
|
||||
line-height: 62px;
|
||||
margin-bottom: 16px;
|
||||
`
|
||||
|
||||
const ChannelNameEmpty = styled(ChannelName)`
|
||||
font-weight: bold;
|
||||
font-size: 22px;
|
||||
line-height: 30px;
|
||||
margin-bottom: 16px;
|
||||
`
|
||||
|
||||
const EmptyText = styled.p`
|
||||
display: inline-block;
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
max-width: 310px;
|
||||
text-align: center;
|
||||
|
||||
& > span {
|
||||
color: ${({ theme }) => theme.primary};
|
||||
}
|
||||
|
||||
${textMediumStyles}
|
||||
`
|
||||
|
||||
const EmptyTextGroup = styled(EmptyText)`
|
||||
& > span {
|
||||
word-break: break-all;
|
||||
}
|
||||
`
|
|
@ -1,183 +0,0 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useMessengerContext } from '../../contexts/messengerProvider'
|
||||
import { useNarrow } from '../../contexts/narrowProvider'
|
||||
import { TokenRequirement } from '../Form/TokenRequirement'
|
||||
import { MessagesList } from '../Messages/MessagesList'
|
||||
import { NarrowChannels } from '../NarrowMode/NarrowChannels'
|
||||
import { NarrowMembers } from '../NarrowMode/NarrowMembers'
|
||||
import { LoadingSkeleton } from '../Skeleton/LoadingSkeleton'
|
||||
import { ChatCreation } from './ChatCreation'
|
||||
import { ChatInput } from './ChatInput'
|
||||
import { ChatTopbar, ChatTopbarLoading } from './ChatTopbar'
|
||||
|
||||
import type { Reply } from '../../hooks/useReply'
|
||||
import type { ChannelData } from '../../models/ChannelData'
|
||||
|
||||
export enum ChatBodyState {
|
||||
Chat,
|
||||
Channels,
|
||||
Members,
|
||||
}
|
||||
|
||||
function ChatBodyLoading() {
|
||||
const narrow = useNarrow()
|
||||
return (
|
||||
<Wrapper>
|
||||
<ChatBodyWrapper className={narrow ? 'narrow' : ''}>
|
||||
<ChatTopbarLoading />
|
||||
<LoadingSkeleton />
|
||||
<ChatInput reply={undefined} setReply={() => undefined} />
|
||||
</ChatBodyWrapper>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
type ChatBodyContentProps = {
|
||||
showState: ChatBodyState
|
||||
switchShowState: (state: ChatBodyState) => void
|
||||
channel: ChannelData
|
||||
}
|
||||
|
||||
function ChatBodyContent({
|
||||
showState,
|
||||
switchShowState,
|
||||
channel,
|
||||
}: ChatBodyContentProps) {
|
||||
const [reply, setReply] = useState<Reply | undefined>(undefined)
|
||||
|
||||
switch (showState) {
|
||||
case ChatBodyState.Chat:
|
||||
return (
|
||||
<>
|
||||
<MessagesList setReply={setReply} channel={channel} />
|
||||
<ChatInput reply={reply} setReply={setReply} />
|
||||
</>
|
||||
)
|
||||
case ChatBodyState.Channels:
|
||||
return (
|
||||
<NarrowChannels
|
||||
setShowChannels={() => switchShowState(ChatBodyState.Channels)}
|
||||
/>
|
||||
)
|
||||
case ChatBodyState.Members:
|
||||
return (
|
||||
<NarrowMembers
|
||||
switchShowMembersList={() => switchShowState(ChatBodyState.Members)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface ChatBodyProps {
|
||||
onClick: () => void
|
||||
showMembers: boolean
|
||||
permission: boolean
|
||||
editGroup: boolean
|
||||
setEditGroup: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
export function ChatBody({
|
||||
onClick,
|
||||
showMembers,
|
||||
permission,
|
||||
editGroup,
|
||||
setEditGroup,
|
||||
}: ChatBodyProps) {
|
||||
const { activeChannel, loadingMessenger } = useMessengerContext()
|
||||
|
||||
const narrow = useNarrow()
|
||||
const className = useMemo(() => (narrow ? 'narrow' : ''), [narrow])
|
||||
|
||||
const [showState, setShowState] = useState<ChatBodyState>(ChatBodyState.Chat)
|
||||
const switchShowState = useCallback(
|
||||
(state: ChatBodyState) => {
|
||||
if (narrow) {
|
||||
setShowState(prev => (prev === state ? ChatBodyState.Chat : state))
|
||||
}
|
||||
},
|
||||
[narrow]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!narrow) {
|
||||
setShowState(ChatBodyState.Chat)
|
||||
}
|
||||
}, [narrow])
|
||||
|
||||
if (!loadingMessenger && activeChannel) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<ChatBodyWrapper className={className}>
|
||||
{editGroup ? (
|
||||
<ChatCreation
|
||||
setEditGroup={setEditGroup}
|
||||
activeChannel={activeChannel}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ChatTopbar
|
||||
onClick={onClick}
|
||||
setEditGroup={setEditGroup}
|
||||
showMembers={showMembers}
|
||||
showState={showState}
|
||||
switchShowState={switchShowState}
|
||||
/>
|
||||
<ChatBodyContent
|
||||
showState={showState}
|
||||
switchShowState={switchShowState}
|
||||
channel={activeChannel}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ChatBodyWrapper>
|
||||
{!permission && (
|
||||
<BluredWrapper>
|
||||
<TokenRequirement />
|
||||
</BluredWrapper>
|
||||
)}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
return <ChatBodyLoading />
|
||||
}
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
width: 61%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
background: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
position: relative;
|
||||
|
||||
&.narrow {
|
||||
width: 100%;
|
||||
}
|
||||
`
|
||||
|
||||
const ChatBodyWrapper = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
background: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
`
|
||||
|
||||
const BluredWrapper = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: ${({ theme }) => theme.bodyBackgroundGradient};
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 2;
|
||||
`
|
|
@ -1,387 +0,0 @@
|
|||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { ChatState, useChatState } from '../../contexts/chatStateProvider'
|
||||
import { useUserPublicKey } from '../../contexts/identityProvider'
|
||||
import { useMessengerContext } from '../../contexts/messengerProvider'
|
||||
import { useNarrow } from '../../contexts/narrowProvider'
|
||||
import { ActivityButton } from '../ActivityCenter/ActivityButton'
|
||||
import { BackButton } from '../Buttons/BackButton'
|
||||
import { buttonStyles } from '../Buttons/buttonStyle'
|
||||
import { CrossIcon } from '../Icons/CrossIcon'
|
||||
import { Member } from '../Members/Member'
|
||||
import { SearchBlock } from '../SearchBlock'
|
||||
import { textMediumStyles } from '../Text'
|
||||
import { ChatInput } from './ChatInput'
|
||||
|
||||
import type { ChannelData } from '../../models/ChannelData'
|
||||
|
||||
interface ChatCreationProps {
|
||||
setEditGroup?: (val: boolean) => void
|
||||
activeChannel?: ChannelData
|
||||
}
|
||||
|
||||
export function ChatCreation({
|
||||
setEditGroup,
|
||||
activeChannel,
|
||||
}: ChatCreationProps) {
|
||||
const narrow = useNarrow()
|
||||
const userPK = useUserPublicKey()
|
||||
const [query, setQuery] = useState('')
|
||||
const [groupChatMembersIds, setGroupChatMembersIds] = useState<string[]>(
|
||||
activeChannel?.members?.map(member => member.id) ?? []
|
||||
)
|
||||
const { contacts, createGroupChat, addMembers } = useMessengerContext()
|
||||
|
||||
const groupChatMembers = useMemo(
|
||||
() => groupChatMembersIds.map(id => contacts[id]).filter(e => !!e),
|
||||
[groupChatMembersIds, contacts]
|
||||
)
|
||||
|
||||
const contactsList = useMemo(() => {
|
||||
return Object.values(contacts)
|
||||
.filter(
|
||||
member =>
|
||||
member.id.includes(query) ||
|
||||
member?.customName?.includes(query) ||
|
||||
member.trueName.includes(query)
|
||||
)
|
||||
.filter(member => !groupChatMembersIds.includes(member.id))
|
||||
}, [query, groupChatMembersIds, contacts])
|
||||
|
||||
const setChatState = useChatState()[1]
|
||||
|
||||
const addMember = useCallback(
|
||||
(member: string) => {
|
||||
setGroupChatMembersIds((prevMembers: string[]) => {
|
||||
if (
|
||||
prevMembers.find(mem => mem === member) ||
|
||||
prevMembers.length >= 5
|
||||
) {
|
||||
return prevMembers
|
||||
} else {
|
||||
return [...prevMembers, member]
|
||||
}
|
||||
})
|
||||
setQuery('')
|
||||
},
|
||||
[setGroupChatMembersIds]
|
||||
)
|
||||
const removeMember = useCallback(
|
||||
(member: string) => {
|
||||
setGroupChatMembersIds(prev => prev.filter(e => e != member))
|
||||
},
|
||||
[setGroupChatMembersIds]
|
||||
)
|
||||
|
||||
const createChat = useCallback(
|
||||
(group: string[]) => {
|
||||
if (userPK) {
|
||||
const newGroup = group.slice()
|
||||
newGroup.push(userPK)
|
||||
createGroupChat(newGroup)
|
||||
setChatState(ChatState.ChatBody)
|
||||
}
|
||||
},
|
||||
[userPK, createGroupChat, setChatState]
|
||||
)
|
||||
|
||||
const handleCreationClick = useCallback(() => {
|
||||
if (!activeChannel) {
|
||||
createChat(groupChatMembers.map(member => member.id))
|
||||
} else {
|
||||
addMembers(
|
||||
groupChatMembers.map(member => member.id),
|
||||
activeChannel.id
|
||||
)
|
||||
}
|
||||
setEditGroup?.(false)
|
||||
}, [activeChannel, groupChatMembers, createChat, addMembers, setEditGroup])
|
||||
|
||||
return (
|
||||
<CreationWrapper className={`${narrow && 'narrow'}`}>
|
||||
<CreationBar
|
||||
className={`${groupChatMembers.length === 5 && narrow && 'limit'}`}
|
||||
>
|
||||
{narrow && (
|
||||
<BackButton
|
||||
onBtnClick={() =>
|
||||
setEditGroup
|
||||
? setEditGroup?.(false)
|
||||
: setChatState(ChatState.ChatBody)
|
||||
}
|
||||
className="narrow"
|
||||
/>
|
||||
)}
|
||||
<Column>
|
||||
<InputBar>
|
||||
<InputText>To:</InputText>
|
||||
<StyledList>
|
||||
{groupChatMembers.map(member => (
|
||||
<StyledMember key={member.id}>
|
||||
<StyledName>
|
||||
{member?.customName?.slice(0, 10) ??
|
||||
member.trueName.slice(0, 10)}
|
||||
</StyledName>
|
||||
<CloseButton onClick={() => removeMember(member.id)}>
|
||||
<CrossIcon memberView={true} />
|
||||
</CloseButton>
|
||||
</StyledMember>
|
||||
))}
|
||||
</StyledList>
|
||||
{groupChatMembers.length < 5 && (
|
||||
<SearchMembers>
|
||||
<Input
|
||||
value={query}
|
||||
onInput={e => setQuery(e.currentTarget.value)}
|
||||
/>
|
||||
</SearchMembers>
|
||||
)}
|
||||
{!narrow && groupChatMembers.length === 5 && (
|
||||
<LimitAlert>5 user Limit reached</LimitAlert>
|
||||
)}
|
||||
</InputBar>
|
||||
{narrow && groupChatMembers.length === 5 && (
|
||||
<LimitAlert className="narrow">5 user Limit reached</LimitAlert>
|
||||
)}
|
||||
</Column>
|
||||
<CreationBtn
|
||||
disabled={groupChatMembers.length === 0}
|
||||
onClick={handleCreationClick}
|
||||
>
|
||||
Confirm
|
||||
</CreationBtn>
|
||||
{!narrow && <ActivityButton className="creation" />}
|
||||
{!narrow && (
|
||||
<SearchBlock
|
||||
query={query}
|
||||
discludeList={groupChatMembersIds}
|
||||
onClick={addMember}
|
||||
/>
|
||||
)}
|
||||
</CreationBar>
|
||||
{((!setEditGroup && groupChatMembers.length === 0) || narrow) &&
|
||||
Object.keys(contacts).length > 0 && (
|
||||
<Contacts>
|
||||
<ContactsHeading>Contacts</ContactsHeading>
|
||||
<ContactsList>
|
||||
{userPK && narrow
|
||||
? contactsList.map(contact => (
|
||||
<Contact key={contact.id}>
|
||||
<Member
|
||||
contact={contact}
|
||||
isOnline={contact.online}
|
||||
onClick={() => addMember(contact.id)}
|
||||
/>
|
||||
</Contact>
|
||||
))
|
||||
: Object.values(contacts)
|
||||
.filter(
|
||||
e => e.id != userPK && !groupChatMembersIds.includes(e.id)
|
||||
)
|
||||
.map(contact => (
|
||||
<Contact key={contact.id}>
|
||||
<Member
|
||||
contact={contact}
|
||||
isOnline={contact.online}
|
||||
onClick={() => addMember(contact.id)}
|
||||
/>
|
||||
</Contact>
|
||||
))}
|
||||
</ContactsList>
|
||||
</Contacts>
|
||||
)}
|
||||
{!setEditGroup && Object.keys(contacts).length === 0 && (
|
||||
<EmptyContacts>
|
||||
<EmptyContactsHeading>
|
||||
You only can send direct messages to your Contacts.{' '}
|
||||
</EmptyContactsHeading>
|
||||
<EmptyContactsHeading>
|
||||
{' '}
|
||||
Send a contact request to the person you would like to chat with,
|
||||
you will be able to chat with them once they have accepted your
|
||||
contact request.
|
||||
</EmptyContactsHeading>
|
||||
</EmptyContacts>
|
||||
)}
|
||||
|
||||
{!activeChannel && (
|
||||
<ChatInput
|
||||
createChat={createChat}
|
||||
group={groupChatMembers.map(member => member.id)}
|
||||
/>
|
||||
)}
|
||||
</CreationWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const CreationWrapper = styled.div`
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
background-color: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
padding: 8px 16px;
|
||||
|
||||
&.narrow {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
`
|
||||
|
||||
const CreationBar = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
|
||||
&.limit {
|
||||
align-items: flex-start;
|
||||
}
|
||||
`
|
||||
|
||||
const Column = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
margin-right: 16px;
|
||||
overflow-x: hidden;
|
||||
`
|
||||
|
||||
const InputBar = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
background-color: ${({ theme }) => theme.inputColor};
|
||||
border: 1px solid ${({ theme }) => theme.inputColor};
|
||||
border-radius: 8px;
|
||||
padding: 6px 16px;
|
||||
|
||||
${textMediumStyles}
|
||||
`
|
||||
|
||||
const Input = styled.input`
|
||||
width: 100%;
|
||||
min-width: 20px;
|
||||
background-color: ${({ theme }) => theme.inputColor};
|
||||
border: 1px solid ${({ theme }) => theme.inputColor};
|
||||
outline: none;
|
||||
resize: none;
|
||||
|
||||
${textMediumStyles}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
caret-color: ${({ theme }) => theme.notificationColor};
|
||||
}
|
||||
`
|
||||
|
||||
const InputText = styled.div`
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
margin-right: 8px;
|
||||
`
|
||||
|
||||
const CreationBtn = styled.button`
|
||||
padding: 11px 24px;
|
||||
${buttonStyles}
|
||||
|
||||
&:disabled {
|
||||
background: ${({ theme }) => theme.inputColor};
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledList = styled.div`
|
||||
display: flex;
|
||||
overflow-x: scroll;
|
||||
margin-right: 8px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMember = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 4px 4px 8px;
|
||||
background: ${({ theme }) => theme.tertiary};
|
||||
color: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
border-radius: 8px;
|
||||
|
||||
& + & {
|
||||
margin-left: 8px;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledName = styled.p`
|
||||
color: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
|
||||
${textMediumStyles}
|
||||
`
|
||||
|
||||
const CloseButton = styled.button`
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
`
|
||||
|
||||
const Contacts = styled.div`
|
||||
height: calc(100% - 44px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
`
|
||||
|
||||
const Contact = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 12px 0 16px;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.inputColor};
|
||||
}
|
||||
`
|
||||
|
||||
const ContactsHeading = styled.p`
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
|
||||
${textMediumStyles}
|
||||
`
|
||||
|
||||
export const ContactsList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
const EmptyContacts = styled(Contacts)`
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const EmptyContactsHeading = styled(ContactsHeading)`
|
||||
max-width: 550px;
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const SearchMembers = styled.div`
|
||||
position: relative;
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
const LimitAlert = styled.p`
|
||||
text-transform: uppercase;
|
||||
margin-left: auto;
|
||||
color: ${({ theme }) => theme.redColor};
|
||||
white-space: nowrap;
|
||||
|
||||
&.narrow {
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
`
|
|
@ -1,558 +0,0 @@
|
|||
import 'emoji-mart/css/emoji-mart.css'
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { ChatState, useChatState } from '../../contexts/chatStateProvider'
|
||||
import { useIdentity } from '../../contexts/identityProvider'
|
||||
import { useMessengerContext } from '../../contexts/messengerProvider'
|
||||
import { useModal } from '../../contexts/modalProvider'
|
||||
import { useNarrow } from '../../contexts/narrowProvider'
|
||||
import { useClickOutside } from '../../hooks/useClickOutside'
|
||||
import { uintToImgUrl } from '../../utils/uintToImgUrl'
|
||||
import { ClearBtn } from '../Form/inputStyles'
|
||||
import { ClearSvg } from '../Icons/ClearIcon'
|
||||
import { ClearSvgFull } from '../Icons/ClearIconFull'
|
||||
import { EmojiIcon } from '../Icons/EmojiIcon'
|
||||
import { GifIcon } from '../Icons/GifIcon'
|
||||
import { PictureIcon } from '../Icons/PictureIcon'
|
||||
import { ReplySvg } from '../Icons/ReplyIcon'
|
||||
import { StickerIcon } from '../Icons/StickerIcon'
|
||||
import { SizeLimitModal, SizeLimitModalName } from '../Modals/SizeLimitModal'
|
||||
import { UserCreationStartModalName } from '../Modals/UserCreationStartModal'
|
||||
import { SearchBlock } from '../SearchBlock'
|
||||
import { textMediumStyles, textSmallStyles } from '../Text'
|
||||
import { EmojiPicker } from './EmojiPicker'
|
||||
|
||||
import type { Reply } from '../../hooks/useReply'
|
||||
import type { EmojiData } from 'emoji-mart'
|
||||
|
||||
interface ChatInputProps {
|
||||
reply?: Reply | undefined
|
||||
setReply?: (val: Reply | undefined) => void
|
||||
createChat?: (group: string[]) => void
|
||||
group?: string[]
|
||||
}
|
||||
|
||||
export function ChatInput({
|
||||
reply,
|
||||
setReply,
|
||||
createChat,
|
||||
group,
|
||||
}: ChatInputProps) {
|
||||
const narrow = useNarrow()
|
||||
const identity = useIdentity()
|
||||
const setChatState = useChatState()[1]
|
||||
const disabled = useMemo(() => !identity, [identity])
|
||||
const { sendMessage, contacts } = useMessengerContext()
|
||||
const [content, setContent] = useState('')
|
||||
const [clearComponent, setClearComponent] = useState('')
|
||||
const [showEmoji, setShowEmoji] = useState(false)
|
||||
const [inputHeight, setInputHeight] = useState(40)
|
||||
const [imageUint, setImageUint] = useState<undefined | Uint8Array>(undefined)
|
||||
|
||||
const { setModal } = useModal(SizeLimitModalName)
|
||||
const { setModal: setCreationStartModal } = useModal(
|
||||
UserCreationStartModalName
|
||||
)
|
||||
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
const inputRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const ref = useRef(null)
|
||||
useClickOutside(ref, () => setShowEmoji(false))
|
||||
|
||||
const image = useMemo(
|
||||
() => (imageUint ? uintToImgUrl(imageUint) : ''),
|
||||
[imageUint]
|
||||
)
|
||||
|
||||
const addEmoji = useCallback(
|
||||
(e: EmojiData) => {
|
||||
if ('unified' in e) {
|
||||
const sym = e.unified.split('-')
|
||||
const codesArray: string[] = []
|
||||
sym.forEach((el: string) => codesArray.push('0x' + el))
|
||||
const emoji = String.fromCodePoint(
|
||||
...(codesArray as unknown as number[])
|
||||
)
|
||||
if (inputRef.current) {
|
||||
inputRef.current.appendChild(document.createTextNode(emoji))
|
||||
}
|
||||
setContent(p => p + emoji)
|
||||
}
|
||||
},
|
||||
[setContent]
|
||||
)
|
||||
|
||||
const resizeTextArea = useCallback((target: HTMLDivElement) => {
|
||||
target.style.height = '40px'
|
||||
target.style.height = `${Math.min(target.scrollHeight, 438)}px`
|
||||
setInputHeight(target.scrollHeight)
|
||||
}, [])
|
||||
|
||||
const rowHeight = inputHeight + (image ? 73 : 0)
|
||||
|
||||
const onInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLDivElement>) => {
|
||||
const element = document.getSelection()
|
||||
const inputElement = inputRef.current
|
||||
if (inputElement && element && element.rangeCount > 0) {
|
||||
const selection = element?.getRangeAt(0)?.startOffset
|
||||
const parentElement = element.anchorNode?.parentElement
|
||||
if (parentElement && parentElement.tagName === 'B') {
|
||||
parentElement.outerHTML = parentElement.innerText
|
||||
const range = document.createRange()
|
||||
const sel = window.getSelection()
|
||||
if (element.anchorNode.firstChild) {
|
||||
const childNumber =
|
||||
element.focusOffset === 0 ? 0 : element.focusOffset - 1
|
||||
range.setStart(
|
||||
element.anchorNode.childNodes[childNumber],
|
||||
selection
|
||||
)
|
||||
}
|
||||
range.collapse(true)
|
||||
|
||||
sel?.removeAllRanges()
|
||||
sel?.addRange(range)
|
||||
}
|
||||
}
|
||||
const target = e.target
|
||||
resizeTextArea(target)
|
||||
setContent(target.textContent ?? '')
|
||||
},
|
||||
[resizeTextArea]
|
||||
)
|
||||
|
||||
const onInputKeyPress = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key == 'Enter' && !e.getModifierState('Shift')) {
|
||||
e.preventDefault()
|
||||
;(e.target as HTMLDivElement).style.height = '40px'
|
||||
setInputHeight(40)
|
||||
sendMessage(content, imageUint, reply?.id)
|
||||
setImageUint(undefined)
|
||||
setClearComponent('')
|
||||
if (inputRef.current) {
|
||||
inputRef.current.innerHTML = ''
|
||||
}
|
||||
setContent('')
|
||||
if (setReply) setReply(undefined)
|
||||
if (createChat && group) {
|
||||
createChat(group)
|
||||
setChatState(ChatState.ChatBody)
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
content,
|
||||
imageUint,
|
||||
createChat,
|
||||
group,
|
||||
sendMessage,
|
||||
reply?.id,
|
||||
setChatState,
|
||||
setReply,
|
||||
]
|
||||
)
|
||||
|
||||
const [selectedElement, setSelectedElement] = useState<{
|
||||
element: Selection | null
|
||||
start: number
|
||||
end: number
|
||||
text: string
|
||||
node: Node | null
|
||||
}>({ element: null, start: 0, end: 0, text: '', node: null })
|
||||
|
||||
const handleCursorChange = useCallback(() => {
|
||||
const element = document.getSelection()
|
||||
if (element && element.rangeCount > 0) {
|
||||
const selection = element?.getRangeAt(0)?.startOffset
|
||||
const text = element?.anchorNode?.textContent
|
||||
if (selection && text) {
|
||||
const end = text.indexOf(' ', selection)
|
||||
const start = text.lastIndexOf(' ', selection - 1)
|
||||
setSelectedElement({
|
||||
element,
|
||||
start,
|
||||
end,
|
||||
text,
|
||||
node: element.anchorNode,
|
||||
})
|
||||
const substring = text.substring(
|
||||
start > -1 ? start + 1 : 0,
|
||||
end > -1 ? end : undefined
|
||||
)
|
||||
if (substring.startsWith('@')) {
|
||||
setQuery(substring.slice(1))
|
||||
} else {
|
||||
setQuery('')
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(handleCursorChange, [content, handleCursorChange])
|
||||
|
||||
const addMention = useCallback(
|
||||
(contact: string) => {
|
||||
if (inputRef?.current) {
|
||||
const { element, start, end, text, node } = selectedElement
|
||||
if (element && text && node) {
|
||||
const firstSlice = text.slice(0, start > -1 ? start : 0)
|
||||
const secondSlice = text.slice(end > -1 ? end : content.length)
|
||||
const replaceContent = `${firstSlice} @${contact}${secondSlice}`
|
||||
const spaceElement = document.createTextNode(' ')
|
||||
const contactElement = document.createElement('span')
|
||||
contactElement.innerText = `@${contact}`
|
||||
|
||||
if (contactElement && element.rangeCount > 0) {
|
||||
const range = element.getRangeAt(0)
|
||||
range.setStart(node, start > -1 ? start : 0)
|
||||
if (end === -1 || end > text.length) {
|
||||
range.setEnd(node, text.length)
|
||||
} else {
|
||||
range.setEnd(node, end)
|
||||
}
|
||||
range.deleteContents()
|
||||
if (end === -1) {
|
||||
range.insertNode(spaceElement.cloneNode())
|
||||
}
|
||||
range.insertNode(contactElement)
|
||||
if (start > -1) {
|
||||
range.insertNode(spaceElement.cloneNode())
|
||||
}
|
||||
range.collapse()
|
||||
}
|
||||
inputRef.current.focus()
|
||||
setQuery('')
|
||||
setContent(replaceContent)
|
||||
resizeTextArea(inputRef.current)
|
||||
}
|
||||
}
|
||||
},
|
||||
[inputRef, content, selectedElement, resizeTextArea]
|
||||
)
|
||||
|
||||
return (
|
||||
<View className={`${createChat && 'creation'}`}>
|
||||
<SizeLimitModal />
|
||||
<AddPictureInputWrapper>
|
||||
<PictureIcon />
|
||||
<AddPictureInput
|
||||
disabled={disabled}
|
||||
type="file"
|
||||
multiple={true}
|
||||
accept="image/png, image/jpeg"
|
||||
onChange={e => {
|
||||
const fileReader = new FileReader()
|
||||
fileReader.onloadend = s => {
|
||||
const arr = new Uint8Array(s.target?.result as ArrayBuffer)
|
||||
setImageUint(arr)
|
||||
}
|
||||
|
||||
if (e?.target?.files?.[0]) {
|
||||
if (e.target.files[0].size < 1024 * 1024) {
|
||||
fileReader.readAsArrayBuffer(e.target.files[0])
|
||||
} else {
|
||||
setModal(true)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</AddPictureInputWrapper>
|
||||
<InputArea>
|
||||
{reply && (
|
||||
<ReplyWrapper>
|
||||
<ReplyTo>
|
||||
{' '}
|
||||
<ReplySvg width={18} height={18} className="input" />{' '}
|
||||
{contacts[reply.sender]?.customName ??
|
||||
contacts[reply.sender].trueName}
|
||||
</ReplyTo>
|
||||
<ReplyOn>{reply.content}</ReplyOn>
|
||||
{reply.image && <ImagePreview src={reply.image} />}
|
||||
<CloseButton
|
||||
onClick={() => {
|
||||
if (setReply) setReply(undefined)
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
<ClearSvg width={20} height={20} className="input" />
|
||||
</CloseButton>
|
||||
</ReplyWrapper>
|
||||
)}
|
||||
<Row style={{ height: `${rowHeight}px` }}>
|
||||
<InputWrapper>
|
||||
{image && (
|
||||
<ImageWrapper>
|
||||
<ImagePreview src={image} />
|
||||
<ClearImgBtn onClick={() => setImageUint(undefined)}>
|
||||
<ClearSvgFull width={20} height={20} />
|
||||
</ClearImgBtn>
|
||||
</ImageWrapper>
|
||||
)}
|
||||
{narrow && !identity ? (
|
||||
<JoinBtn onClick={() => setCreationStartModal(true)}>
|
||||
Click here to join discussion
|
||||
</JoinBtn>
|
||||
) : (
|
||||
<Input
|
||||
aria-disabled={disabled}
|
||||
contentEditable={!disabled}
|
||||
onInput={onInputChange}
|
||||
onKeyDown={onInputKeyPress}
|
||||
onKeyUp={handleCursorChange}
|
||||
ref={inputRef}
|
||||
onClick={handleCursorChange}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: disabled
|
||||
? 'You need to join this community to send messages'
|
||||
: clearComponent,
|
||||
}}
|
||||
className={`${disabled && 'disabled'} `}
|
||||
/>
|
||||
)}
|
||||
{query && (
|
||||
<SearchBlock
|
||||
query={query}
|
||||
discludeList={[]}
|
||||
onClick={addMention}
|
||||
onBotttom
|
||||
/>
|
||||
)}
|
||||
</InputWrapper>
|
||||
<InputButtons>
|
||||
<EmojiWrapper ref={ref}>
|
||||
<ChatButton
|
||||
onClick={() => {
|
||||
if (!disabled) setShowEmoji(!showEmoji)
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<EmojiIcon isActive={showEmoji} />
|
||||
</ChatButton>
|
||||
<EmojiPicker
|
||||
addEmoji={addEmoji}
|
||||
showEmoji={showEmoji}
|
||||
bottom={rowHeight - 24}
|
||||
/>
|
||||
</EmojiWrapper>
|
||||
<ChatButton disabled={disabled}>
|
||||
<StickerIcon />
|
||||
</ChatButton>
|
||||
<ChatButton disabled={disabled}>
|
||||
<GifIcon />
|
||||
</ChatButton>
|
||||
</InputButtons>
|
||||
</Row>
|
||||
</InputArea>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const InputWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const EmojiWrapper = styled.div`
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const View = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: 6px 8px 6px 10px;
|
||||
position: relative;
|
||||
|
||||
&.creation {
|
||||
padding: 0;
|
||||
}
|
||||
`
|
||||
|
||||
const InputArea = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-height: 438px;
|
||||
padding: 2px;
|
||||
background: ${({ theme }) => theme.inputColor};
|
||||
border-radius: 16px 16px 4px 16px;
|
||||
`
|
||||
|
||||
const Row = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-height: 438px;
|
||||
padding-right: 6px;
|
||||
background: ${({ theme }) => theme.inputColor};
|
||||
border-radius: 16px 16px 4px 16px;
|
||||
`
|
||||
|
||||
const InputButtons = styled.div`
|
||||
display: flex;
|
||||
align-self: flex-end;
|
||||
|
||||
button + button {
|
||||
margin-left: 4px;
|
||||
}
|
||||
`
|
||||
|
||||
const ImageWrapper = styled.div`
|
||||
width: 64px;
|
||||
position: relative;
|
||||
`
|
||||
const ImagePreview = styled.img`
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 16px 16px 4px 16px;
|
||||
margin-left: 8px;
|
||||
margin-top: 9px;
|
||||
`
|
||||
|
||||
const ClearImgBtn = styled(ClearBtn)`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
top: 4px;
|
||||
right: -20px;
|
||||
transform: none;
|
||||
padding: 0;
|
||||
border: 2px solid ${({ theme }) => theme.inputColor};
|
||||
background-color: ${({ theme }) => theme.inputColor};
|
||||
`
|
||||
|
||||
const Input = styled.div`
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
max-height: 438px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
padding: 8px 0 8px 12px;
|
||||
background: ${({ theme }) => theme.inputColor};
|
||||
border: 1px solid ${({ theme }) => theme.inputColor};
|
||||
color: ${({ theme }) => theme.primary};
|
||||
border-radius: 16px 16px 4px 16px;
|
||||
outline: none;
|
||||
|
||||
${textMediumStyles};
|
||||
|
||||
&.disabled {
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
caret-color: ${({ theme }) => theme.notificationColor};
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
& > span {
|
||||
display: inline;
|
||||
color: ${({ theme }) => theme.mentionColor};
|
||||
background: ${({ theme }) => theme.mentionBg};
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
padding: 0 2px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.mentionBgHover};
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const AddPictureInputWrapper = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 4px;
|
||||
|
||||
& > input[type='file']::-webkit-file-upload-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& > input:disabled::-webkit-file-upload-button {
|
||||
cursor: default;
|
||||
}
|
||||
`
|
||||
|
||||
const AddPictureInput = styled.input`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
`
|
||||
|
||||
const ChatButton = styled.button`
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
`
|
||||
|
||||
const CloseButton = styled(ChatButton)`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
`
|
||||
|
||||
const ReplyWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
color: ${({ theme }) => theme.primary};
|
||||
border-radius: 14px 14px 4px 14px;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
export const ReplyTo = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
${textSmallStyles};
|
||||
`
|
||||
|
||||
export const ReplyOn = styled.div`
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
${textSmallStyles};
|
||||
`
|
||||
|
||||
const JoinBtn = styled.button`
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
background: ${({ theme }) => theme.inputColor};
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 0 10px;
|
||||
text-align: start;
|
||||
|
||||
${textMediumStyles};
|
||||
`
|
|
@ -1,233 +0,0 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { decode } from 'html-entities'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useUserPublicKey } from '../../contexts/identityProvider'
|
||||
import { useMessengerContext } from '../../contexts/messengerProvider'
|
||||
import { useClickOutside } from '../../hooks/useClickOutside'
|
||||
import { ContactMenu } from '../Form/ContactMenu'
|
||||
import { ImageMenu } from '../Form/ImageMenu'
|
||||
import { textMediumStyles, textSmallStyles } from '../Text'
|
||||
|
||||
import type { ChatMessage } from '../../models/ChatMessage'
|
||||
import type { Metadata } from '../../models/Metadata'
|
||||
|
||||
interface MentionProps {
|
||||
id: string
|
||||
setMentioned: (val: boolean) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function Mention({ id, setMentioned, className }: MentionProps) {
|
||||
const { contacts } = useMessengerContext()
|
||||
const contact = useMemo(() => contacts[id.slice(1)], [id, contacts])
|
||||
const [showMenu, setShowMenu] = useState(false)
|
||||
const userPK = useUserPublicKey()
|
||||
|
||||
useEffect(() => {
|
||||
if (userPK && contact) {
|
||||
if (contact.id === userPK) setMentioned(true)
|
||||
}
|
||||
}, [contact, userPK, setMentioned])
|
||||
|
||||
const ref = useRef(null)
|
||||
useClickOutside(ref, () => setShowMenu(false))
|
||||
|
||||
if (!contact) return <>{id}</>
|
||||
return (
|
||||
<MentionBLock
|
||||
onClick={() => setShowMenu(!showMenu)}
|
||||
className={className}
|
||||
ref={ref}
|
||||
>
|
||||
{`@${contact?.customName ?? contact.trueName}`}
|
||||
{showMenu && <ContactMenu id={id.slice(1)} setShowMenu={setShowMenu} />}
|
||||
</MentionBLock>
|
||||
)
|
||||
}
|
||||
|
||||
type ChatMessageContentProps = {
|
||||
message: ChatMessage
|
||||
setImage: (image: string) => void
|
||||
setLinkOpen: (link: string) => void
|
||||
setMentioned: (val: boolean) => void
|
||||
}
|
||||
|
||||
export function ChatMessageContent({
|
||||
message,
|
||||
setImage,
|
||||
setLinkOpen,
|
||||
setMentioned,
|
||||
}: ChatMessageContentProps) {
|
||||
const { content, image } = useMemo(() => message, [message])
|
||||
const [elements, setElements] = useState<(string | React.ReactElement)[]>([
|
||||
content,
|
||||
])
|
||||
const [link, setLink] = useState<string | undefined>(undefined)
|
||||
const [openGraph] = useState<Metadata | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
let link
|
||||
const split = content.split(' ')
|
||||
const newSplit = split.flatMap((element, idx) => {
|
||||
if (element.startsWith('http://') || element.startsWith('https://')) {
|
||||
link = element
|
||||
return [
|
||||
<Link key={idx} onClick={() => setLinkOpen(element)}>
|
||||
{element}
|
||||
</Link>,
|
||||
' ',
|
||||
]
|
||||
}
|
||||
if (element.startsWith('@')) {
|
||||
return [
|
||||
<Mention key={idx} id={element} setMentioned={setMentioned} />,
|
||||
' ',
|
||||
]
|
||||
}
|
||||
return [element, ' ']
|
||||
})
|
||||
newSplit.pop()
|
||||
setLink(link)
|
||||
setElements(newSplit)
|
||||
}, [content, setLink, setMentioned, setElements, setLinkOpen])
|
||||
|
||||
// useEffect(() => {
|
||||
// const updatePreview = async () => {
|
||||
// if (link && fetchMetadata) {
|
||||
// try {
|
||||
// const metadata = await fetchMetadata(link)
|
||||
// if (metadata) {
|
||||
// setOpenGraph(metadata)
|
||||
// }
|
||||
// } catch {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// updatePreview()
|
||||
// }, [link, fetchMetadata])
|
||||
|
||||
return (
|
||||
<ContentWrapper>
|
||||
<div>{elements.map(el => el)}</div>
|
||||
{image && (
|
||||
<MessageImageWrapper>
|
||||
<MessageImage
|
||||
src={image}
|
||||
id={image}
|
||||
onClick={() => {
|
||||
setImage(image)
|
||||
}}
|
||||
/>
|
||||
<ImageMenu imageId={image} />
|
||||
</MessageImageWrapper>
|
||||
)}
|
||||
{openGraph && (
|
||||
<PreviewWrapper onClick={() => setLinkOpen(link ?? '')}>
|
||||
<PreviewImage src={decodeURI(decode(openGraph['og:image']))} />
|
||||
<PreviewTitleWrapper>{openGraph['og:title']}</PreviewTitleWrapper>
|
||||
<PreviewSiteNameWrapper>
|
||||
{openGraph['og:site_name']}
|
||||
</PreviewSiteNameWrapper>
|
||||
</PreviewWrapper>
|
||||
)}
|
||||
</ContentWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const MessageImageWrapper = styled.div`
|
||||
width: 147px;
|
||||
height: 196px;
|
||||
margin-top: 8px;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const MessageImage = styled.img`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const PreviewSiteNameWrapper = styled.div`
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.1px;
|
||||
margin-top: 2px;
|
||||
color: #939ba1;
|
||||
margin-left: 12px;
|
||||
`
|
||||
|
||||
const PreviewTitleWrapper = styled.div`
|
||||
margin-top: 7px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
width: 290px;
|
||||
margin-left: 12px;
|
||||
|
||||
${textSmallStyles}
|
||||
`
|
||||
|
||||
const PreviewImage = styled.img`
|
||||
border-radius: 15px 15px 15px 4px;
|
||||
width: 305px;
|
||||
height: 170px;
|
||||
`
|
||||
|
||||
const PreviewWrapper = styled.div`
|
||||
margin-top: 9px;
|
||||
background: #ffffff;
|
||||
width: 305px;
|
||||
height: 224px;
|
||||
border: 1px solid #eef2f5;
|
||||
box-sizing: border-box;
|
||||
border-radius: 16px 16px 16px 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0px;
|
||||
`
|
||||
|
||||
const ContentWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
const MentionBLock = styled.div`
|
||||
display: inline-flex;
|
||||
color: ${({ theme }) => theme.mentionColor};
|
||||
background: ${({ theme }) => theme.mentionBgHover};
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
padding: 0 2px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.mentionHover};
|
||||
}
|
||||
|
||||
&.activity {
|
||||
max-width: 488px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
${textMediumStyles}
|
||||
`
|
||||
|
||||
const Link = styled.a`
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
color: ${({ theme }) => theme.memberNameColor};
|
||||
`
|
|
@ -1,209 +0,0 @@
|
|||
import React, { useRef, useState } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useMessengerContext } from '../../contexts/messengerProvider'
|
||||
import { useNarrow } from '../../contexts/narrowProvider'
|
||||
import { useClickOutside } from '../../hooks/useClickOutside'
|
||||
import {
|
||||
ActivityButton,
|
||||
ActivityWrapper,
|
||||
} from '../ActivityCenter/ActivityButton'
|
||||
import { Channel } from '../Channels/Channel'
|
||||
import { ChannelMenu } from '../Form/ChannelMenu'
|
||||
import { ActivityIcon } from '../Icons/ActivityIcon'
|
||||
import { MembersIcon } from '../Icons/MembersIcon'
|
||||
import { MoreIcon } from '../Icons/MoreIcon'
|
||||
import { CommunitySkeleton } from '../Skeleton/CommunitySkeleton'
|
||||
import { Loading } from '../Skeleton/Loading'
|
||||
import { ChatBodyState } from './ChatBody'
|
||||
import { CommunitySidebar } from './CommunitySidebar'
|
||||
|
||||
export function ChatTopbarLoading() {
|
||||
const narrow = useNarrow()
|
||||
|
||||
return (
|
||||
<Topbar className={narrow ? 'narrow' : ''}>
|
||||
<ChannelWrapper className={narrow ? 'narrow' : ''}>
|
||||
<SkeletonWrapper>
|
||||
<CommunitySkeleton />
|
||||
</SkeletonWrapper>
|
||||
</ChannelWrapper>
|
||||
<MenuWrapper>
|
||||
{!narrow && (
|
||||
<TopBtn>
|
||||
<MembersIcon />
|
||||
</TopBtn>
|
||||
)}
|
||||
<TopBtn>
|
||||
<MoreIcon />
|
||||
</TopBtn>
|
||||
<ActivityWrapper>
|
||||
<TopBtn disabled>
|
||||
<ActivityIcon />
|
||||
</TopBtn>
|
||||
</ActivityWrapper>
|
||||
</MenuWrapper>
|
||||
<Loading />
|
||||
</Topbar>
|
||||
)
|
||||
}
|
||||
|
||||
type ChatTopbarProps = {
|
||||
showState: ChatBodyState
|
||||
onClick: () => void
|
||||
switchShowState: (state: ChatBodyState) => void
|
||||
showMembers: boolean
|
||||
setEditGroup: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
export function ChatTopbar({
|
||||
showState,
|
||||
onClick,
|
||||
switchShowState,
|
||||
showMembers,
|
||||
setEditGroup,
|
||||
}: ChatTopbarProps) {
|
||||
const { activeChannel, loadingMessenger } = useMessengerContext()
|
||||
|
||||
const narrow = useNarrow()
|
||||
const [showChannelMenu, setShowChannelMenu] = useState(false)
|
||||
|
||||
const ref = useRef(null)
|
||||
useClickOutside(ref, () => setShowChannelMenu(false))
|
||||
|
||||
if (!activeChannel) {
|
||||
return <ChatTopbarLoading />
|
||||
}
|
||||
|
||||
return (
|
||||
<Topbar
|
||||
className={narrow && showState !== ChatBodyState.Chat ? 'narrow' : ''}
|
||||
>
|
||||
<ChannelWrapper className={narrow ? 'narrow' : ''}>
|
||||
{!loadingMessenger ? (
|
||||
<>
|
||||
{narrow && (
|
||||
<CommunityWrap className={narrow ? 'narrow' : ''}>
|
||||
<CommunitySidebar />
|
||||
</CommunityWrap>
|
||||
)}
|
||||
|
||||
<Channel
|
||||
channel={activeChannel}
|
||||
isActive={narrow ? showState === ChatBodyState.Channels : false}
|
||||
activeView={true}
|
||||
onClick={() => switchShowState(ChatBodyState.Channels)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<SkeletonWrapper>
|
||||
<CommunitySkeleton />
|
||||
</SkeletonWrapper>
|
||||
)}
|
||||
</ChannelWrapper>
|
||||
|
||||
<MenuWrapper>
|
||||
{!narrow && activeChannel.type !== 'dm' && (
|
||||
<TopBtn onClick={onClick} className={showMembers ? 'active' : ''}>
|
||||
<MembersIcon />
|
||||
</TopBtn>
|
||||
)}
|
||||
<div ref={ref}>
|
||||
<TopBtn onClick={() => setShowChannelMenu(!showChannelMenu)}>
|
||||
<MoreIcon />
|
||||
{showChannelMenu && (
|
||||
<ChannelMenu
|
||||
channel={activeChannel}
|
||||
showNarrowMembers={showState === ChatBodyState.Members}
|
||||
switchMemberList={() => switchShowState(ChatBodyState.Members)}
|
||||
setShowChannelMenu={setShowChannelMenu}
|
||||
setEditGroup={setEditGroup}
|
||||
className={`${narrow && 'narrow'}`}
|
||||
/>
|
||||
)}
|
||||
</TopBtn>
|
||||
</div>
|
||||
{!narrow && <ActivityButton />}
|
||||
</MenuWrapper>
|
||||
{loadingMessenger && <Loading />}
|
||||
</Topbar>
|
||||
)
|
||||
}
|
||||
|
||||
const Topbar = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 5px 8px;
|
||||
background: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
position: relative;
|
||||
|
||||
&.narrow {
|
||||
width: 100%;
|
||||
}
|
||||
`
|
||||
|
||||
const ChannelWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 80%;
|
||||
|
||||
&.narrow {
|
||||
width: calc(100% - 46px);
|
||||
}
|
||||
`
|
||||
|
||||
const SkeletonWrapper = styled.div`
|
||||
padding: 8px;
|
||||
`
|
||||
|
||||
const CommunityWrap = styled.div`
|
||||
padding-right: 10px;
|
||||
margin-right: 16px;
|
||||
position: relative;
|
||||
|
||||
&.narrow {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
width: 2px;
|
||||
height: 24px;
|
||||
transform: translateY(-50%);
|
||||
border-radius: 1px;
|
||||
background: ${({ theme }) => theme.primary};
|
||||
opacity: 0.1;
|
||||
}
|
||||
`
|
||||
|
||||
const MenuWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
`
|
||||
|
||||
export const TopBtn = styled.button`
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
background: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.inputColor};
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: ${({ theme }) => theme.sectionBackgroundColor};
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
`
|
|
@ -1,38 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useMessengerContext } from '../../contexts/messengerProvider'
|
||||
import { useModal } from '../../contexts/modalProvider'
|
||||
import { CommunityIdentity } from '../CommunityIdentity'
|
||||
import { CommunityModalName } from '../Modals/CommunityModal'
|
||||
import { CommunitySkeleton } from '../Skeleton/CommunitySkeleton'
|
||||
|
||||
interface CommunityProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function CommunitySidebar({ className }: CommunityProps) {
|
||||
const { communityData } = useMessengerContext()
|
||||
const { setModal } = useModal(CommunityModalName)
|
||||
|
||||
if (!communityData) {
|
||||
return (
|
||||
<SkeletonWrapper>
|
||||
<CommunitySkeleton />
|
||||
</SkeletonWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button className={className} onClick={() => setModal(true)}>
|
||||
<CommunityIdentity subtitle={`${communityData.members} members`} />
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const SkeletonWrapper = styled.div`
|
||||
margin-bottom: 16px;
|
||||
`
|
|
@ -1,45 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import { Picker } from 'emoji-mart'
|
||||
import { useTheme } from 'styled-components'
|
||||
|
||||
import { useLow } from '../../contexts/narrowProvider'
|
||||
import { lightTheme } from '../../styles/themes'
|
||||
|
||||
import type { Theme } from '~/src/types/theme'
|
||||
import type { EmojiData } from 'emoji-mart'
|
||||
|
||||
type EmojiPickerProps = {
|
||||
showEmoji: boolean
|
||||
addEmoji: (e: EmojiData) => void
|
||||
bottom: number
|
||||
}
|
||||
|
||||
export function EmojiPicker({ showEmoji, addEmoji, bottom }: EmojiPickerProps) {
|
||||
const theme = useTheme() as Theme
|
||||
const low = useLow()
|
||||
|
||||
if (showEmoji) {
|
||||
return (
|
||||
<Picker
|
||||
onSelect={addEmoji}
|
||||
theme={theme === lightTheme ? 'light' : 'dark'}
|
||||
set="twitter"
|
||||
color={theme.tertiary}
|
||||
emojiSize={26}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: `calc(100% + ${bottom}px)`,
|
||||
right: '-76px',
|
||||
color: theme.secondary,
|
||||
height: low ? '200px' : '355px',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
showPreview={false}
|
||||
showSkinTones={false}
|
||||
title={''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useMessengerContext } from '../contexts/messengerProvider'
|
||||
import { textMediumStyles } from './Text'
|
||||
|
||||
export interface CommunityIdentityProps {
|
||||
subtitle: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const CommunityIdentity = ({
|
||||
subtitle,
|
||||
className,
|
||||
}: CommunityIdentityProps) => {
|
||||
const { communityData } = useMessengerContext()
|
||||
|
||||
return (
|
||||
<Row className={className}>
|
||||
<Logo
|
||||
style={{
|
||||
backgroundImage: communityData?.icon
|
||||
? `url(${communityData?.icon}`
|
||||
: '',
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
{communityData?.icon === undefined &&
|
||||
communityData?.name.slice(0, 1).toUpperCase()}
|
||||
</Logo>
|
||||
<Column>
|
||||
<Name>{communityData?.name}</Name>
|
||||
<Subtitle>{subtitle}</Subtitle>
|
||||
</Column>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
const Row = styled.div`
|
||||
display: flex;
|
||||
`
|
||||
|
||||
export const Column = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
`
|
||||
|
||||
export const Logo = styled.div`
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
background-color: ${({ theme }) => theme.tertiary};
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
color: ${({ theme }) => theme.iconTextColor};
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
`
|
||||
|
||||
const Name = styled.p`
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
color: ${({ theme }) => theme.primary};
|
||||
white-space: nowrap;
|
||||
|
||||
${textMediumStyles}
|
||||
`
|
||||
|
||||
const Subtitle = styled.p`
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.1px;
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
`
|
|
@ -1,194 +0,0 @@
|
|||
import React, { useMemo, useRef, useState } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useMessengerContext } from '../../contexts/messengerProvider'
|
||||
import { useModal } from '../../contexts/modalProvider'
|
||||
import { useNarrow } from '../../contexts/narrowProvider'
|
||||
import { useClickOutside } from '../../hooks/useClickOutside'
|
||||
import { useContextMenu } from '../../hooks/useContextMenu'
|
||||
import { AddMemberIcon } from '../Icons/AddMemberIcon'
|
||||
import { CheckIcon } from '../Icons/CheckIcon'
|
||||
import { DeleteIcon } from '../Icons/DeleteIcon'
|
||||
import { DownloadIcon } from '../Icons/DownloadIcon'
|
||||
import { EditIcon } from '../Icons/EditIcon'
|
||||
import { LeftIcon } from '../Icons/LeftIcon'
|
||||
import { MembersSmallIcon } from '../Icons/MembersSmallIcon'
|
||||
import { MuteIcon } from '../Icons/MuteIcon'
|
||||
import { NextIcon } from '../Icons/NextIcon'
|
||||
import { ProfileIcon } from '../Icons/ProfileIcon'
|
||||
import { EditModalName } from '../Modals/EditModal'
|
||||
import { LeavingModalName } from '../Modals/LeavingModal'
|
||||
import { ProfileModalName } from '../Modals/ProfileModal'
|
||||
import { DropdownMenu, MenuItem, MenuSection, MenuText } from './DropdownMenu'
|
||||
import { MuteMenu } from './MuteMenu'
|
||||
|
||||
import type { ChannelData } from '../../models/ChannelData'
|
||||
|
||||
interface ChannelMenuProps {
|
||||
channel: ChannelData
|
||||
setShowChannelMenu?: (val: boolean) => void
|
||||
showNarrowMembers?: boolean
|
||||
switchMemberList?: () => void
|
||||
setEditGroup?: (val: boolean) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const ChannelMenu = ({
|
||||
channel,
|
||||
setShowChannelMenu,
|
||||
showNarrowMembers,
|
||||
switchMemberList,
|
||||
setEditGroup,
|
||||
className,
|
||||
}: ChannelMenuProps) => {
|
||||
const narrow = useNarrow()
|
||||
const { clearNotifications, channelsDispatch } = useMessengerContext()
|
||||
const { setModal } = useModal(EditModalName)
|
||||
const { setModal: setLeavingModal } = useModal(LeavingModalName)
|
||||
const { setModal: setProfileModal } = useModal(ProfileModalName)
|
||||
const [showSubmenu, setShowSubmenu] = useState(false)
|
||||
|
||||
const { showMenu, setShowMenu: setShowSideMenu } = useContextMenu(
|
||||
channel.id + 'contextMenu'
|
||||
)
|
||||
|
||||
const setShowMenu = useMemo(
|
||||
() => (setShowChannelMenu ? setShowChannelMenu : setShowSideMenu),
|
||||
[setShowChannelMenu, setShowSideMenu]
|
||||
)
|
||||
|
||||
const ref = useRef(null)
|
||||
useClickOutside(ref, () => setShowMenu(false))
|
||||
|
||||
if (showMenu || setShowChannelMenu) {
|
||||
return (
|
||||
<ChannelDropdown className={className} menuRef={ref}>
|
||||
{narrow && channel.type !== 'dm' && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (switchMemberList) switchMemberList()
|
||||
setShowMenu(false)
|
||||
}}
|
||||
>
|
||||
<MembersSmallIcon width={16} height={16} />
|
||||
<MenuText>{showNarrowMembers ? 'Hide' : 'View'} Members</MenuText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{channel.type === 'group' && (
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (setEditGroup) setEditGroup(true)
|
||||
setShowMenu(false)
|
||||
}}
|
||||
>
|
||||
<AddMemberIcon width={16} height={16} />
|
||||
<MenuText>Add / remove from group</MenuText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => setModal(true)}>
|
||||
<EditIcon width={16} height={16} />
|
||||
<MenuText>Edit name and image</MenuText>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
{channel.type === 'dm' && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setProfileModal({
|
||||
id: channel.name,
|
||||
renamingState: false,
|
||||
requestState: false,
|
||||
})
|
||||
setShowMenu(false)
|
||||
}}
|
||||
>
|
||||
<ProfileIcon width={16} height={16} />
|
||||
<MenuText>View Profile</MenuText>
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuSection className={`${channel.type === 'channel' && 'channel'}`}>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (channel.isMuted) {
|
||||
channelsDispatch({
|
||||
type: 'ToggleMuted',
|
||||
payload: channel.id,
|
||||
})
|
||||
setShowMenu(false)
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (!channel.isMuted) setShowSubmenu(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (!channel.isMuted) setShowSubmenu(false)
|
||||
}}
|
||||
>
|
||||
<MuteIcon width={16} height={16} />
|
||||
{!channel.isMuted && <NextIcon />}
|
||||
<MenuText>
|
||||
{(channel.isMuted ? 'Unmute' : 'Mute') +
|
||||
(channel.type === 'group' ? ' Group' : 'Chat')}
|
||||
</MenuText>
|
||||
{!channel.isMuted && showSubmenu && (
|
||||
<MuteMenu
|
||||
setIsMuted={() =>
|
||||
channelsDispatch({
|
||||
type: 'ToggleMuted',
|
||||
payload: channel.id,
|
||||
})
|
||||
}
|
||||
className={className}
|
||||
/>
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => clearNotifications(channel.id)}>
|
||||
<CheckIcon width={16} height={16} />
|
||||
<MenuText>Mark as Read</MenuText>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<DownloadIcon width={16} height={16} />
|
||||
<MenuText>Fetch Messages</MenuText>
|
||||
</MenuItem>
|
||||
</MenuSection>
|
||||
{(channel.type === 'group' || channel.type === 'dm') && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setLeavingModal(true)
|
||||
setShowMenu(false)
|
||||
}}
|
||||
>
|
||||
{channel.type === 'group' && (
|
||||
<LeftIcon width={16} height={16} className="red" />
|
||||
)}
|
||||
{channel.type === 'dm' && (
|
||||
<DeleteIcon width={16} height={16} className="red" />
|
||||
)}
|
||||
<MenuText className="red">
|
||||
{channel.type === 'group' ? 'Leave Group' : 'Delete Chat'}
|
||||
</MenuText>
|
||||
</MenuItem>
|
||||
)}
|
||||
</ChannelDropdown>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const ChannelDropdown = styled(DropdownMenu)`
|
||||
top: calc(100% + 4px);
|
||||
right: 0px;
|
||||
|
||||
&.side {
|
||||
top: 20px;
|
||||
left: calc(100% - 35px);
|
||||
right: unset;
|
||||
}
|
||||
|
||||
&.sideNarrow {
|
||||
top: 20px;
|
||||
right: 8px;
|
||||
}
|
||||
`
|
|
@ -1,163 +0,0 @@
|
|||
import React, { useMemo } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useUserPublicKey } from '../../contexts/identityProvider'
|
||||
import { useMessengerContext } from '../../contexts/messengerProvider'
|
||||
import { useModal } from '../../contexts/modalProvider'
|
||||
import { AddContactIcon } from '../Icons/AddContactIcon'
|
||||
import { BlockSvg } from '../Icons/BlockIcon'
|
||||
import { ChatSvg } from '../Icons/ChatIcon'
|
||||
import { EditIcon } from '../Icons/EditIcon'
|
||||
import { ProfileIcon } from '../Icons/ProfileIcon'
|
||||
import { UntrustworthIcon } from '../Icons/UntrustworthIcon'
|
||||
import { UserIcon } from '../Icons/UserIcon'
|
||||
import { WarningSvg } from '../Icons/WarningIcon'
|
||||
import { UserAddress } from '../Messages/Styles'
|
||||
import { ProfileModalName } from '../Modals/ProfileModal'
|
||||
import { textMediumStyles } from '../Text'
|
||||
import { DropdownMenu, MenuItem, MenuText } from './DropdownMenu'
|
||||
|
||||
type ContactMenuProps = {
|
||||
id: string
|
||||
setShowMenu: (val: boolean) => void
|
||||
}
|
||||
|
||||
export function ContactMenu({ id, setShowMenu }: ContactMenuProps) {
|
||||
const userPK = useUserPublicKey()
|
||||
const { contacts, contactsDispatch } = useMessengerContext()
|
||||
const contact = useMemo(() => contacts[id], [id, contacts])
|
||||
const isUser = useMemo(() => {
|
||||
if (userPK) {
|
||||
return id === userPK
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}, [id, userPK])
|
||||
|
||||
const { setModal } = useModal(ProfileModalName)
|
||||
|
||||
if (!contact) return null
|
||||
return (
|
||||
<ContactDropdown>
|
||||
<ContactInfo>
|
||||
<UserIcon />
|
||||
<UserNameWrapper>
|
||||
<UserName>{contact?.customName ?? contact.trueName}</UserName>
|
||||
{contact.isUntrustworthy && <UntrustworthIcon />}
|
||||
</UserNameWrapper>
|
||||
{contact?.customName && (
|
||||
<UserTrueName>({contact.trueName})</UserTrueName>
|
||||
)}
|
||||
<UserAddress>
|
||||
{id.slice(0, 10)}...{id.slice(-3)}
|
||||
</UserAddress>
|
||||
</ContactInfo>
|
||||
<MenuSection>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setModal({ id, renamingState: false, requestState: false })
|
||||
}}
|
||||
>
|
||||
<ProfileIcon width={16} height={16} />
|
||||
<MenuText>View Profile</MenuText>
|
||||
</MenuItem>
|
||||
{!contact.isFriend && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setModal({ id, requestState: true })
|
||||
}}
|
||||
>
|
||||
<AddContactIcon width={16} height={16} />
|
||||
<MenuText>Send Contact Request</MenuText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{contact.isFriend && (
|
||||
<MenuItem>
|
||||
<ChatSvg width={16} height={16} />
|
||||
<MenuText>Send Message</MenuText>
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setModal({ id, renamingState: true })
|
||||
}}
|
||||
>
|
||||
<EditIcon width={16} height={16} />
|
||||
<MenuText>Rename</MenuText>
|
||||
</MenuItem>
|
||||
</MenuSection>
|
||||
<MenuSection>
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
contactsDispatch({ type: 'toggleTrustworthy', payload: { id } })
|
||||
}
|
||||
>
|
||||
<WarningSvg
|
||||
width={16}
|
||||
height={16}
|
||||
className={contact.isUntrustworthy ? '' : 'red'}
|
||||
/>
|
||||
<MenuText className={contact.isUntrustworthy ? '' : 'red'}>
|
||||
{contact.isUntrustworthy
|
||||
? 'Remove Untrustworthy Mark'
|
||||
: 'Mark as Untrustworthy'}
|
||||
</MenuText>
|
||||
</MenuItem>
|
||||
|
||||
{!contact.isFriend && !isUser && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
contactsDispatch({ type: 'toggleBlocked', payload: { id } })
|
||||
setShowMenu(false)
|
||||
}}
|
||||
>
|
||||
<BlockSvg width={16} height={16} className="red" />
|
||||
<MenuText className="red">
|
||||
{contact.blocked ? 'Unblock User' : 'Block User'}
|
||||
</MenuText>
|
||||
</MenuItem>
|
||||
)}
|
||||
</MenuSection>
|
||||
</ContactDropdown>
|
||||
)
|
||||
}
|
||||
|
||||
const ContactDropdown = styled(DropdownMenu)`
|
||||
top: 20px;
|
||||
left: 0px;
|
||||
width: 222px;
|
||||
`
|
||||
|
||||
const ContactInfo = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const MenuSection = styled.div`
|
||||
margin-top: 5px;
|
||||
padding-top: 5px;
|
||||
border-top: 1px solid ${({ theme }) => theme.inputColor};
|
||||
`
|
||||
|
||||
const UserNameWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
`
|
||||
|
||||
const UserName = styled.p`
|
||||
color: ${({ theme }) => theme.primary};
|
||||
margin-right: 4px;
|
||||
|
||||
${textMediumStyles}
|
||||
`
|
||||
|
||||
const UserTrueName = styled.p`
|
||||
color: ${({ theme }) => theme.primary};
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.1px;
|
||||
margin-top: 4px;
|
||||
`
|
|
@ -1,29 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import { copy } from '../../utils/copy'
|
||||
import { reduceString } from '../../utils/reduceString'
|
||||
import {
|
||||
ButtonWrapper,
|
||||
InputBtn,
|
||||
InputWrapper,
|
||||
Label,
|
||||
Text,
|
||||
Wrapper,
|
||||
} from './inputStyles'
|
||||
|
||||
interface CopyInputProps {
|
||||
label?: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export const CopyInput = ({ label, value }: CopyInputProps) => (
|
||||
<InputWrapper>
|
||||
{label && <Label>{label}</Label>}
|
||||
<Wrapper>
|
||||
<Text>{reduceString(value, 15, 15)}</Text>
|
||||
<ButtonWrapper>
|
||||
<InputBtn onClick={() => copy(value)}>Copy</InputBtn>
|
||||
</ButtonWrapper>
|
||||
</Wrapper>
|
||||
</InputWrapper>
|
||||
)
|
|
@ -1,97 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { textSmallStyles } from '../Text'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
type DropdownMenuProps = {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
style?: { top: number; left: number }
|
||||
menuRef?: React.MutableRefObject<null>
|
||||
id?: string
|
||||
}
|
||||
|
||||
export function DropdownMenu({
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
menuRef,
|
||||
id,
|
||||
}: DropdownMenuProps) {
|
||||
return (
|
||||
<MenuBlock className={className} style={style} ref={menuRef} id={id}>
|
||||
<MenuList>{children}</MenuList>
|
||||
</MenuBlock>
|
||||
)
|
||||
}
|
||||
|
||||
const MenuBlock = styled.div`
|
||||
width: 207px;
|
||||
background: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
box-shadow: 0px 2px 4px rgba(0, 34, 51, 0.16),
|
||||
0px 4px 12px rgba(0, 34, 51, 0.08);
|
||||
border-radius: 8px;
|
||||
padding: 8px 0;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
`
|
||||
|
||||
const MenuList = styled.ul`
|
||||
list-style: none;
|
||||
`
|
||||
|
||||
export const MenuItem = styled.li`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 8px 8px 14px;
|
||||
cursor: pointer;
|
||||
color: ${({ theme }) => theme.primary};
|
||||
position: relative;
|
||||
|
||||
&:hover,
|
||||
&:hover > span {
|
||||
background: ${({ theme }) => theme.border};
|
||||
}
|
||||
|
||||
&.picker:hover {
|
||||
background: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
}
|
||||
|
||||
& > svg.red {
|
||||
fill: ${({ theme }) => theme.redColor};
|
||||
}
|
||||
`
|
||||
|
||||
export const MenuText = styled.span`
|
||||
margin-left: 6px;
|
||||
color: ${({ theme }) => theme.primary};
|
||||
|
||||
&.red {
|
||||
color: ${({ theme }) => theme.redColor};
|
||||
}
|
||||
|
||||
${textSmallStyles}
|
||||
`
|
||||
|
||||
export const MenuSection = styled.div`
|
||||
padding: 4px 0;
|
||||
margin: 4px 0;
|
||||
border-top: 1px solid ${({ theme }) => theme.inputColor};
|
||||
border-bottom: 1px solid ${({ theme }) => theme.inputColor};
|
||||
|
||||
&.channel {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.message {
|
||||
padding: 4px 0 0;
|
||||
margin: 4px 0 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
`
|
|
@ -1,42 +0,0 @@
|
|||
import React, { useRef } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useClickOutside } from '../../hooks/useClickOutside'
|
||||
import { useContextMenu } from '../../hooks/useContextMenu'
|
||||
import { copyImg } from '../../utils/copyImg'
|
||||
import { downloadImg } from '../../utils/downloadImg'
|
||||
import { CopyIcon } from '../Icons/CopyIcon'
|
||||
import { DownloadIcon } from '../Icons/DownloadIcon'
|
||||
import { DropdownMenu, MenuItem, MenuText } from './DropdownMenu'
|
||||
|
||||
interface ImageMenuProps {
|
||||
imageId: string
|
||||
}
|
||||
|
||||
export const ImageMenu = ({ imageId }: ImageMenuProps) => {
|
||||
const { showMenu, setShowMenu } = useContextMenu(imageId)
|
||||
|
||||
const ref = useRef(null)
|
||||
useClickOutside(ref, () => setShowMenu(false))
|
||||
|
||||
return showMenu ? (
|
||||
<ImageDropdown menuRef={ref}>
|
||||
<MenuItem onClick={() => copyImg(imageId)}>
|
||||
<CopyIcon height={16} width={16} /> <MenuText>Copy image</MenuText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => downloadImg(imageId)}>
|
||||
<DownloadIcon height={16} width={16} />
|
||||
<MenuText> Download image</MenuText>
|
||||
</MenuItem>
|
||||
</ImageDropdown>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
|
||||
const ImageDropdown = styled(DropdownMenu)`
|
||||
width: 176px;
|
||||
left: 120px;
|
||||
top: 46px;
|
||||
`
|
|
@ -1,94 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { MobileIcon } from '../Icons/MobileIcon'
|
||||
import { ProfileIcon } from '../Icons/ProfileIcon'
|
||||
import { ScanIcon } from '../Icons/ScanIcon'
|
||||
import { textMediumStyles } from '../Text'
|
||||
|
||||
interface LoginInstructionsProps {
|
||||
mobileFlow: boolean
|
||||
}
|
||||
|
||||
export function LoginInstructions({ mobileFlow }: LoginInstructionsProps) {
|
||||
return (
|
||||
<Instructions>
|
||||
<InstructionStep>
|
||||
Open Status App on your {mobileFlow ? 'mobile' : 'desktop'}
|
||||
</InstructionStep>
|
||||
<InstructionStep>
|
||||
Navigate yourself to{' '}
|
||||
<InstructionIcon>
|
||||
{' '}
|
||||
<ProfileIcon width={13} height={13} /> <span>Profile</span>
|
||||
</InstructionIcon>{' '}
|
||||
tab
|
||||
</InstructionStep>
|
||||
<InstructionStep>
|
||||
Select{' '}
|
||||
<InstructionIcon>
|
||||
<MobileIcon />
|
||||
</InstructionIcon>{' '}
|
||||
<span>Sync Settings</span>
|
||||
</InstructionStep>
|
||||
<InstructionStep>
|
||||
Tap{' '}
|
||||
<InstructionIcon>
|
||||
{' '}
|
||||
<ScanIcon />{' '}
|
||||
</InstructionIcon>{' '}
|
||||
<span>{mobileFlow ? 'Scan' : 'Display'} sync code</span>
|
||||
</InstructionStep>
|
||||
<InstructionStep>
|
||||
{mobileFlow
|
||||
? 'Scan the sync code from this screen'
|
||||
: 'Paste the sync code above'}{' '}
|
||||
↑
|
||||
</InstructionStep>
|
||||
</Instructions>
|
||||
)
|
||||
}
|
||||
|
||||
const Instructions = styled.ol`
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
margin: auto 0;
|
||||
list-style-type: decimal;
|
||||
counter-reset: ollist;
|
||||
|
||||
${textMediumStyles}
|
||||
`
|
||||
|
||||
const InstructionStep = styled.li`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& + & {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
& > span {
|
||||
color: ${({ theme }) => theme.tertiary};
|
||||
}
|
||||
|
||||
&::before {
|
||||
counter-increment: ollist;
|
||||
content: counter(ollist) '.';
|
||||
margin-right: 4px;
|
||||
}
|
||||
`
|
||||
|
||||
const InstructionIcon = styled.div`
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: ${({ theme }) => theme.buttonBg};
|
||||
color: ${({ theme }) => theme.tertiary};
|
||||
font-size: 8px;
|
||||
line-height: 10px;
|
||||
margin: 0 6px;
|
||||
`
|
|
@ -1,120 +0,0 @@
|
|||
import React, { useMemo, useRef } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useUserPublicKey } from '../../contexts/identityProvider'
|
||||
import { useMessengerContext } from '../../contexts/messengerProvider'
|
||||
import { useClickOutside } from '../../hooks/useClickOutside'
|
||||
import { useClickPosition } from '../../hooks/useClickPosition'
|
||||
import { useContextMenu } from '../../hooks/useContextMenu'
|
||||
import { DeleteIcon } from '../Icons/DeleteIcon'
|
||||
import { EditIcon } from '../Icons/EditIcon'
|
||||
import { PinIcon } from '../Icons/PinIcon'
|
||||
import { ReplySvg } from '../Icons/ReplyIcon'
|
||||
import { ReactionPicker } from '../Reactions/ReactionPicker'
|
||||
import { DropdownMenu, MenuItem, MenuSection, MenuText } from './DropdownMenu'
|
||||
|
||||
import type { Reply } from '../../hooks/useReply'
|
||||
import type { ChatMessage } from '../../models/ChatMessage'
|
||||
import type { BaseEmoji } from 'emoji-mart'
|
||||
|
||||
interface MessageMenuProps {
|
||||
message: ChatMessage
|
||||
messageReactions: BaseEmoji[]
|
||||
setMessageReactions: React.Dispatch<React.SetStateAction<BaseEmoji[]>>
|
||||
setReply: (val: Reply | undefined) => void
|
||||
messageRef: React.MutableRefObject<null>
|
||||
}
|
||||
|
||||
export const MessageMenu = ({
|
||||
message,
|
||||
messageReactions,
|
||||
setMessageReactions,
|
||||
setReply,
|
||||
messageRef,
|
||||
}: MessageMenuProps) => {
|
||||
const userPK = useUserPublicKey()
|
||||
const { activeChannel } = useMessengerContext()
|
||||
const { showMenu, setShowMenu } = useContextMenu(message.id)
|
||||
const { topPosition, leftPosition } = useClickPosition(messageRef)
|
||||
|
||||
const menuStyle = useMemo(() => {
|
||||
return {
|
||||
top: topPosition,
|
||||
left: leftPosition,
|
||||
}
|
||||
}, [topPosition, leftPosition])
|
||||
|
||||
const ref = useRef(null)
|
||||
useClickOutside(ref, () => setShowMenu(false))
|
||||
|
||||
const userMessage = useMemo(
|
||||
() => !!userPK && message.sender === userPK,
|
||||
[userPK, message]
|
||||
)
|
||||
|
||||
return userPK && showMenu ? (
|
||||
<MessageDropdown style={menuStyle} menuRef={ref} id="messageDropdown">
|
||||
<MenuItem className="picker">
|
||||
<ReactionPicker
|
||||
messageReactions={messageReactions}
|
||||
setMessageReactions={setMessageReactions}
|
||||
className="menu"
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuSection className={`${!userMessage && 'message'}`}>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setReply({
|
||||
sender: message.sender,
|
||||
content: message.content,
|
||||
image: message.image,
|
||||
id: message.id,
|
||||
})
|
||||
setShowMenu(false)
|
||||
}}
|
||||
>
|
||||
<ReplySvg width={16} height={16} className="menu" />
|
||||
<MenuText>Reply</MenuText>
|
||||
</MenuItem>
|
||||
|
||||
{userMessage && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setShowMenu(false)
|
||||
}}
|
||||
>
|
||||
<EditIcon width={16} height={16} />
|
||||
<MenuText>Edit</MenuText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{activeChannel?.type !== 'channel' && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setShowMenu(false)
|
||||
}}
|
||||
>
|
||||
<PinIcon width={16} height={16} className="menu" />
|
||||
<MenuText>Pin</MenuText>
|
||||
</MenuItem>
|
||||
)}
|
||||
</MenuSection>
|
||||
{userMessage && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setShowMenu(false)
|
||||
}}
|
||||
>
|
||||
<DeleteIcon width={16} height={16} className="red" />
|
||||
<MenuText className="red">Delete message</MenuText>
|
||||
</MenuItem>
|
||||
)}
|
||||
</MessageDropdown>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
|
||||
const MessageDropdown = styled(DropdownMenu)`
|
||||
width: 176px;
|
||||
`
|
|
@ -1,65 +0,0 @@
|
|||
import React, { useCallback } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { DropdownMenu, MenuItem, MenuText } from './DropdownMenu'
|
||||
|
||||
interface SubMenuProps {
|
||||
setIsMuted: (val: boolean) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const MuteMenu = ({ setIsMuted, className }: SubMenuProps) => {
|
||||
const muteChannel = useCallback(
|
||||
(timeout: number) => {
|
||||
setIsMuted(true)
|
||||
const timer = setTimeout(() => setIsMuted(false), timeout * 6000000)
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
},
|
||||
[setIsMuted]
|
||||
)
|
||||
|
||||
return (
|
||||
<MuteDropdown className={className}>
|
||||
<MenuItem onClick={() => muteChannel(0.25)}>
|
||||
<MenuText>For 15 min</MenuText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => muteChannel(1)}>
|
||||
<MenuText>For 1 hour</MenuText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => muteChannel(8)}>
|
||||
<MenuText>For 8 hours</MenuText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => muteChannel(24)}>
|
||||
<MenuText>For 24 hours</MenuText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => setIsMuted(true)}>
|
||||
<MenuText>Until I turn it back on</MenuText>
|
||||
</MenuItem>
|
||||
</MuteDropdown>
|
||||
)
|
||||
}
|
||||
|
||||
const MuteDropdown = styled(DropdownMenu)`
|
||||
width: 176px;
|
||||
top: 100%;
|
||||
right: -60px;
|
||||
z-index: 3;
|
||||
|
||||
&.side {
|
||||
width: 176px;
|
||||
top: -8px;
|
||||
left: 100%;
|
||||
right: unset;
|
||||
}
|
||||
|
||||
&.narrow,
|
||||
&.sideNarrow {
|
||||
width: 176px;
|
||||
top: 100%;
|
||||
right: -16px;
|
||||
z-index: 3;
|
||||
}
|
||||
`
|
|
@ -1,45 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { NameErrors } from '../../hooks/useNameError'
|
||||
import { Hint } from '../Modals/ModalStyle'
|
||||
|
||||
type NameErrorProps = {
|
||||
error: NameErrors
|
||||
}
|
||||
|
||||
export function NameError({ error }: NameErrorProps) {
|
||||
switch (error) {
|
||||
case NameErrors.NoError:
|
||||
return null
|
||||
case NameErrors.NameExists:
|
||||
return (
|
||||
<ErrorText>
|
||||
Sorry, the name you have chosen is not allowed, try picking another
|
||||
username
|
||||
</ErrorText>
|
||||
)
|
||||
case NameErrors.BadCharacters:
|
||||
return (
|
||||
<ErrorText>
|
||||
Only letters, numbers, underscores and hypens allowed
|
||||
</ErrorText>
|
||||
)
|
||||
case NameErrors.EndingWithEth:
|
||||
return (
|
||||
<ErrorText>
|
||||
Usernames ending with {'"_eth"'} or {'"-eth"'} are not allowed
|
||||
</ErrorText>
|
||||
)
|
||||
case NameErrors.TooLong:
|
||||
return <ErrorText>24 character username limit</ErrorText>
|
||||
}
|
||||
}
|
||||
|
||||
const ErrorText = styled(Hint)`
|
||||
color: ${({ theme }) => theme.redColor};
|
||||
text-align: center;
|
||||
width: 328px;
|
||||
margin: 8px 0;
|
||||
`
|
|
@ -1,40 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { paste } from '../../utils/paste'
|
||||
import {
|
||||
ButtonWrapper,
|
||||
InputBtn,
|
||||
inputStyles,
|
||||
InputWrapper,
|
||||
Label,
|
||||
Wrapper,
|
||||
} from './inputStyles'
|
||||
|
||||
interface PasteInputProps {
|
||||
label: string
|
||||
}
|
||||
|
||||
export const PasteInput = ({ label }: PasteInputProps) => (
|
||||
<InputWrapper>
|
||||
<Label>{label}</Label>
|
||||
<Wrapper>
|
||||
<Input id="pasteInput" type="text" placeholder="eg. 0x2Ef19" />
|
||||
<ButtonWrapper>
|
||||
<InputBtn onClick={() => paste('pasteInput')}>Paste</InputBtn>
|
||||
</ButtonWrapper>
|
||||
</Wrapper>
|
||||
</InputWrapper>
|
||||
)
|
||||
|
||||
const Input = styled.input`
|
||||
${inputStyles}
|
||||
width: 100%;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
border: none;
|
||||
`
|
|
@ -1,132 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useMessengerContext } from '../../contexts/messengerProvider'
|
||||
import { textMediumStyles } from '../Text'
|
||||
|
||||
const communityRequirements = {
|
||||
requirements: [
|
||||
{
|
||||
name: 'STN',
|
||||
amount: 10,
|
||||
logo: 'https://status.im/img/logo.svg',
|
||||
},
|
||||
],
|
||||
alternativeRequirements: [
|
||||
{
|
||||
name: 'ETH',
|
||||
amount: 1,
|
||||
logo: 'https://ethereum.org/static/a110735dade3f354a46fc2446cd52476/db4de/eth-home-icon.webp',
|
||||
},
|
||||
{
|
||||
name: 'MKR',
|
||||
amount: 10,
|
||||
logo: 'https://cryptologos.cc/logos/maker-mkr-logo.svg?v=017',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export function TokenRequirement() {
|
||||
const { communityData } = useMessengerContext()
|
||||
return (
|
||||
<Wrapper>
|
||||
<Text>
|
||||
To join <span>{communityData?.name}</span> community chat you need to
|
||||
hold:
|
||||
</Text>
|
||||
<Row>
|
||||
{communityRequirements.requirements.map(req => (
|
||||
<Requirement key={req.name + req.amount}>
|
||||
<Logo
|
||||
style={{
|
||||
backgroundImage: `url(${req.logo}`,
|
||||
}}
|
||||
/>
|
||||
<Amount>
|
||||
{req.amount} {req.name}{' '}
|
||||
</Amount>
|
||||
</Requirement>
|
||||
))}
|
||||
</Row>
|
||||
{communityRequirements.alternativeRequirements && <Text>or</Text>}
|
||||
<Row>
|
||||
{communityRequirements.alternativeRequirements.map(req => (
|
||||
<Requirement key={req.name + req.amount}>
|
||||
<Logo
|
||||
style={{
|
||||
backgroundImage: `url(${req.logo}`,
|
||||
}}
|
||||
/>
|
||||
<Amount>
|
||||
{req.amount} {req.name}{' '}
|
||||
</Amount>
|
||||
</Requirement>
|
||||
))}
|
||||
</Row>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 50%;
|
||||
`
|
||||
|
||||
const Text = styled.p`
|
||||
color: ${({ theme }) => theme.primary};
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
& > span {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
${textMediumStyles}
|
||||
`
|
||||
|
||||
const Requirement = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 16px;
|
||||
padding: 2px 12px 2px 2px;
|
||||
background: ${({ theme }) => theme.buttonBg};
|
||||
color: ${({ theme }) => theme.tertiary};
|
||||
|
||||
&.denial {
|
||||
background: ${({ theme }) => theme.buttonNoBgHover};
|
||||
color: ${({ theme }) => theme.redColor};
|
||||
}
|
||||
|
||||
& + & {
|
||||
margin-left: 18px;
|
||||
}
|
||||
`
|
||||
|
||||
const Amount = styled.p`
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
margin-left: 6px;
|
||||
|
||||
${textMediumStyles}
|
||||
`
|
||||
|
||||
const Row = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
`
|
||||
|
||||
const Logo = styled.div`
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
`
|
|
@ -1,52 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { TipIcon } from '../Icons/TipIcon'
|
||||
import { textSmallStyles } from '../Text'
|
||||
|
||||
type TooltipProps = {
|
||||
tip: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function Tooltip({ tip, className }: TooltipProps) {
|
||||
return (
|
||||
<TooltipWrapper className={className}>
|
||||
<TooltipBlock>
|
||||
<TooltipText>{tip}</TooltipText>
|
||||
<TipIcon className={className} />
|
||||
</TooltipBlock>
|
||||
</TooltipWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const TooltipWrapper = styled.div`
|
||||
width: max-content;
|
||||
position: absolute;
|
||||
top: calc(-100% - 12px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
visibility: hidden;
|
||||
|
||||
&.read {
|
||||
left: 18%;
|
||||
}
|
||||
|
||||
&.muted {
|
||||
top: calc(100% + 8px);
|
||||
z-index: 10;
|
||||
}
|
||||
`
|
||||
const TooltipBlock = styled.div`
|
||||
background: ${({ theme }) => theme.primary};
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
`
|
||||
|
||||
const TooltipText = styled.p`
|
||||
font-weight: 500;
|
||||
color: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
${textSmallStyles};
|
||||
`
|
|
@ -1,93 +0,0 @@
|
|||
import styled, { css } from 'styled-components'
|
||||
|
||||
import { textMediumStyles, textSmallStyles } from '../Text'
|
||||
|
||||
export const inputStyles = css`
|
||||
background: ${({ theme }) => theme.inputColor};
|
||||
border-radius: 8px;
|
||||
border: 1px solid ${({ theme }) => theme.inputColor};
|
||||
color: ${({ theme }) => theme.primary};
|
||||
outline: none;
|
||||
|
||||
${textMediumStyles}
|
||||
|
||||
&:focus {
|
||||
outline: 1px solid ${({ theme }) => theme.tertiary};
|
||||
caret-color: ${({ theme }) => theme.notificationColor};
|
||||
}
|
||||
`
|
||||
|
||||
export const Label = styled.p`
|
||||
margin-bottom: 7px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.primary};
|
||||
|
||||
${textSmallStyles}
|
||||
`
|
||||
|
||||
export const InputWrapper = styled.div`
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
position: relative;
|
||||
padding: 14px 70px 14px 8px;
|
||||
background: ${({ theme }) => theme.inputColor};
|
||||
border-radius: 8px;
|
||||
`
|
||||
|
||||
export const Text = styled.p`
|
||||
color: ${({ theme }) => theme.primary};
|
||||
|
||||
${textMediumStyles}
|
||||
`
|
||||
|
||||
export const ButtonWrapper = styled.div`
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 70px;
|
||||
transform: translateY(-50%);
|
||||
background: ${({ theme }) => theme.inputColor};
|
||||
border-radius: 8px;
|
||||
`
|
||||
|
||||
export const InputBtn = styled.button`
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.1px;
|
||||
color: ${({ theme }) => theme.tertiary};
|
||||
background: ${({ theme }) => theme.buttonBg};
|
||||
border: 1px solid ${({ theme }) => theme.tertiary};
|
||||
border-radius: 6px;
|
||||
`
|
||||
|
||||
export const NameInputWrapper = styled.div`
|
||||
position: relative;
|
||||
`
|
||||
|
||||
export const NameInput = styled.input`
|
||||
width: 328px;
|
||||
padding: 11px 16px;
|
||||
|
||||
${inputStyles}
|
||||
`
|
||||
|
||||
export const ClearBtn = styled.button`
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-radius: 50%;
|
||||
|
||||
& > svg {
|
||||
fill: ${({ theme }) => theme.secondary};
|
||||
}
|
||||
`
|
|
@ -1,149 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
type ColorChatSvgProps = {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export function ColorChatSvg({ width, height }: ColorChatSvgProps) {
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 64 64"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_1307_429352)">
|
||||
<path
|
||||
d="M43.0453 20.5165C54.7313 20.338 64.2693 29.5356 64.4955 41.2207C64.5288 42.9363 64.3573 44.6062 64.004 46.2079C63.4058 48.92 63.0501 51.6799 63.0501 54.4573V59.8258C63.0501 60.6483 62.3834 61.315 61.5609 61.315H56.1924C53.415 61.315 50.6552 61.6707 47.943 62.2689C46.3414 62.6222 44.6715 62.7936 42.9559 62.7604C31.2709 62.5343 22.0733 52.9965 22.2516 41.3108C22.4254 29.9046 31.6392 20.6907 43.0453 20.5165Z"
|
||||
fill="url(#paint0_linear_1307_429352)"
|
||||
/>
|
||||
<path
|
||||
d="M43.0453 20.5165C54.7313 20.338 64.2693 29.5356 64.4955 41.2207C64.5288 42.9363 64.3573 44.6062 64.004 46.2079C63.4058 48.92 63.0501 51.6799 63.0501 54.4573V59.8258C63.0501 60.6483 62.3834 61.315 61.5609 61.315H56.1924C53.415 61.315 50.6552 61.6707 47.943 62.2689C46.3414 62.6222 44.6715 62.7936 42.9559 62.7604C31.2709 62.5343 22.0733 52.9965 22.2516 41.3108C22.4254 29.9046 31.6392 20.6907 43.0453 20.5165Z"
|
||||
fill="url(#paint1_linear_1307_429352)"
|
||||
/>
|
||||
<path
|
||||
d="M26.6372 1.23773C12.4005 1.02023 0.780669 12.2253 0.50492 26.4611C0.464545 28.5509 0.67342 30.5856 1.10392 32.5368C1.83267 35.8408 2.26592 39.2032 2.26592 42.5868V49.127C2.26592 50.129 3.07816 50.9414 4.08029 50.9414H10.6205C14.004 50.9414 17.3664 51.3746 20.6705 52.1034C22.6218 52.5338 24.6562 52.7428 26.7461 52.7023C40.9816 52.4266 52.1867 40.8072 51.9697 26.5707C51.7577 12.6748 40.533 1.44986 26.6372 1.23773Z"
|
||||
fill="url(#paint2_linear_1307_429352)"
|
||||
/>
|
||||
<path
|
||||
d="M17.0238 25.5916C16.2968 24.8191 15.2678 24.334 14.1231 24.334C11.9215 24.334 10.1367 26.1187 10.1367 28.3204C10.1367 29.465 10.6217 30.494 11.3943 31.2211L19.9936 39.8203C20.7207 40.5928 21.7497 41.0779 22.8943 41.0779C25.0959 41.0779 26.8807 39.2932 26.8807 37.0916C26.8807 35.947 26.3956 34.918 25.6231 34.1908L17.0238 25.5916Z"
|
||||
fill="url(#paint3_linear_1307_429352)"
|
||||
/>
|
||||
<path
|
||||
d="M14.1231 32.3075C16.3247 32.3075 18.1095 30.5227 18.1095 28.3211C18.1095 26.1195 16.3247 24.3347 14.1231 24.3347C11.9215 24.3347 10.1367 26.1195 10.1367 28.3211C10.1367 30.5227 11.9215 32.3075 14.1231 32.3075Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M28.2973 25.5916C27.5703 24.8191 26.5413 24.334 25.3965 24.334C23.1949 24.334 21.4102 26.1187 21.4102 28.3204C21.4102 29.465 21.8952 30.494 22.6678 31.2211L31.267 39.8203C31.9941 40.5928 33.0231 41.0779 34.1678 41.0779C36.3694 41.0779 38.1541 39.2932 38.1541 37.0916C38.1541 35.947 37.669 34.918 36.8965 34.1908L28.2973 25.5916Z"
|
||||
fill="url(#paint4_linear_1307_429352)"
|
||||
/>
|
||||
<path
|
||||
d="M25.3975 32.3075C27.5991 32.3075 29.3839 30.5227 29.3839 28.3211C29.3839 26.1195 27.5991 24.3347 25.3975 24.3347C23.1959 24.3347 21.4111 26.1195 21.4111 28.3211C21.4111 30.5227 23.1959 32.3075 25.3975 32.3075Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M39.5717 25.5916C38.8447 24.8191 37.8157 24.334 36.6709 24.334C34.4693 24.334 32.6846 26.1187 32.6846 28.3204C32.6846 29.465 33.1697 30.494 33.9422 31.2211L42.5414 39.8203C43.2686 40.5928 44.2976 41.0779 45.4422 41.0779C47.6438 41.0779 49.4285 39.2932 49.4285 37.0916C49.4285 35.947 48.9434 34.918 48.1709 34.1908L39.5717 25.5916Z"
|
||||
fill="url(#paint5_linear_1307_429352)"
|
||||
/>
|
||||
<path
|
||||
d="M36.6719 32.3075C38.8735 32.3075 40.6583 30.5227 40.6583 28.3211C40.6583 26.1195 38.8735 24.3347 36.6719 24.3347C34.4703 24.3347 32.6855 26.1195 32.6855 28.3211C32.6855 30.5227 34.4703 32.3075 36.6719 32.3075Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_1307_429352"
|
||||
x1="39.1099"
|
||||
y1="37.3761"
|
||||
x2="70.1253"
|
||||
y2="68.3915"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#A7F3CE" />
|
||||
<stop offset="1" stopColor="#61DB99" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_1307_429352"
|
||||
x1="49.2627"
|
||||
y1="47.5284"
|
||||
x2="36.0891"
|
||||
y2="34.356"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#61DB99" stopOpacity="0" />
|
||||
<stop offset="1" stopColor="#009E74" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_1307_429352"
|
||||
x1="16.8336"
|
||||
y1="22.8102"
|
||||
x2="49.5796"
|
||||
y2="55.5561"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#62E1FB" />
|
||||
<stop offset="1" stopColor="#00A2F3" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint3_linear_1307_429352"
|
||||
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_1307_429352"
|
||||
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_1307_429352"
|
||||
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_1307_429352">
|
||||
<rect
|
||||
width="64"
|
||||
height="64"
|
||||
fill="white"
|
||||
transform="translate(0.5)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export const ColorChatIcon = () => {
|
||||
return <Icon width={64} height={64} />
|
||||
}
|
||||
|
||||
const Icon = styled(ColorChatSvg)`
|
||||
& > path {
|
||||
fill: ${({ theme }) => theme.tertiary};
|
||||
}
|
||||
|
||||
&:hover > path {
|
||||
fill: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
}
|
||||
`
|
|
@ -1,126 +0,0 @@
|
|||
import React, { useRef, useState } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useIdentity } from '../../contexts/identityProvider'
|
||||
import { useClickOutside } from '../../hooks/useClickOutside'
|
||||
import { ContactMenu } from '../Form/ContactMenu'
|
||||
import { IconBtn, UserAddress } from '../Messages/Styles'
|
||||
import { UserLogo } from './UserLogo'
|
||||
|
||||
import type { Contact } from '../../models/Contact'
|
||||
|
||||
interface MemberProps {
|
||||
contact: Contact
|
||||
isOnline?: boolean
|
||||
isYou?: boolean
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
export function Member({ contact, isOnline, isYou, onClick }: MemberProps) {
|
||||
const identity = useIdentity()
|
||||
|
||||
const [showMenu, setShowMenu] = useState(false)
|
||||
|
||||
const ref = useRef(null)
|
||||
useClickOutside(ref, () => setShowMenu(false))
|
||||
|
||||
return (
|
||||
<MemberData onClick={onClick} className={`${isYou && 'you'}`}>
|
||||
<MemberIcon
|
||||
style={{
|
||||
backgroundImage: 'unset',
|
||||
}}
|
||||
className={
|
||||
!isYou && isOnline ? 'online' : !isYou && !isOnline ? 'offline' : ''
|
||||
}
|
||||
onClick={() => {
|
||||
if (identity) setShowMenu(e => !e)
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
{showMenu && <ContactMenu id={contact.id} setShowMenu={setShowMenu} />}
|
||||
<UserLogo
|
||||
contact={contact}
|
||||
radius={30}
|
||||
colorWheel={[
|
||||
['red', 150],
|
||||
['blue', 250],
|
||||
['green', 360],
|
||||
]}
|
||||
/>
|
||||
</MemberIcon>
|
||||
<Column>
|
||||
<MemberName>{contact?.customName ?? contact.trueName}</MemberName>
|
||||
<UserAddress>
|
||||
{contact.id.slice(0, 5)}...{contact.id.slice(-3)}
|
||||
</UserAddress>
|
||||
</Column>
|
||||
</MemberData>
|
||||
)
|
||||
}
|
||||
|
||||
export const MemberData = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&.you {
|
||||
margin-bottom: 0;
|
||||
cursor: default;
|
||||
}
|
||||
`
|
||||
|
||||
export const MemberName = styled.p`
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
color: ${({ theme }) => theme.primary};
|
||||
opacity: 0.7;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
export const MemberIcon = styled(IconBtn)`
|
||||
width: 29px;
|
||||
height: 29px;
|
||||
|
||||
&.offline {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
bottom: -2px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background-color: ${({ theme }) => theme.secondary};
|
||||
border: 2px solid ${({ theme }) => theme.bodyBackgroundColor};
|
||||
}
|
||||
}
|
||||
|
||||
&.online {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
bottom: -2px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background-color: #4ebc60;
|
||||
border: 2px solid ${({ theme }) => theme.bodyBackgroundColor};
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const Column = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 8px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
`
|
|
@ -1,43 +0,0 @@
|
|||
import React, { useMemo } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useMessengerContext } from '../../contexts/messengerProvider'
|
||||
import { MembersList } from './MembersList'
|
||||
|
||||
export function Members() {
|
||||
const { activeChannel } = useMessengerContext()
|
||||
const heading = useMemo(
|
||||
() =>
|
||||
activeChannel && activeChannel?.type === 'group'
|
||||
? 'Group members'
|
||||
: 'Members',
|
||||
[activeChannel]
|
||||
)
|
||||
|
||||
return (
|
||||
<MembersWrapper>
|
||||
<MemberHeading>{heading}</MemberHeading>
|
||||
<MembersList />
|
||||
</MembersWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const MembersWrapper = styled.div`
|
||||
width: 18%;
|
||||
height: 100%;
|
||||
min-width: 164px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: ${({ theme }) => theme.sectionBackgroundColor};
|
||||
padding: 16px;
|
||||
overflow-y: scroll;
|
||||
`
|
||||
|
||||
const MemberHeading = styled.h2`
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
color: ${({ theme }) => theme.primary};
|
||||
margin-bottom: 16px;
|
||||
`
|
|
@ -1,130 +0,0 @@
|
|||
import React, { useMemo } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useUserPublicKey } from '../../contexts/identityProvider'
|
||||
import { useMessengerContext } from '../../contexts/messengerProvider'
|
||||
import { useModal } from '../../contexts/modalProvider'
|
||||
import { buttonStyles } from '../Buttons/buttonStyle'
|
||||
import { LogoutIcon } from '../Icons/LogoutIcon'
|
||||
import { LogoutModalName } from '../Modals/LogoutModal'
|
||||
import { Member } from './Member'
|
||||
|
||||
import type { Contact } from '../../models/Contact'
|
||||
|
||||
export function MembersList() {
|
||||
const { contacts, nickname, activeChannel } = useMessengerContext()
|
||||
const userPK = useUserPublicKey()
|
||||
const { setModal } = useModal(LogoutModalName)
|
||||
|
||||
const members = useMemo(() => {
|
||||
const contactsArray = Object.values(contacts)
|
||||
if (userPK) {
|
||||
if (
|
||||
activeChannel &&
|
||||
activeChannel.type === 'group' &&
|
||||
activeChannel.members
|
||||
) {
|
||||
const returnContacts: Contact[] = []
|
||||
activeChannel.members.forEach(member => {
|
||||
if (contacts[member.id] && member.id != userPK) {
|
||||
returnContacts.push(contacts[member.id])
|
||||
}
|
||||
})
|
||||
return returnContacts
|
||||
}
|
||||
return contactsArray.filter(e => e.id !== userPK)
|
||||
}
|
||||
return contactsArray
|
||||
}, [activeChannel, contacts, userPK])
|
||||
|
||||
const onlineContacts = useMemo(() => members.filter(e => e.online), [members])
|
||||
const offlineContacts = useMemo(
|
||||
() => members.filter(e => !e.online),
|
||||
[members]
|
||||
)
|
||||
|
||||
return (
|
||||
<MembersListWrap>
|
||||
{userPK && (
|
||||
<MemberCategory>
|
||||
<MemberCategoryName>You</MemberCategoryName>
|
||||
<Row>
|
||||
<Member
|
||||
contact={{
|
||||
id: userPK,
|
||||
customName: nickname,
|
||||
trueName: userPK,
|
||||
}}
|
||||
isYou={true}
|
||||
/>
|
||||
<LogoutBtn onClick={() => setModal(true)}>
|
||||
<LogoutIcon />
|
||||
</LogoutBtn>
|
||||
</Row>
|
||||
</MemberCategory>
|
||||
)}
|
||||
{onlineContacts.length > 0 && (
|
||||
<MemberCategory>
|
||||
<MemberCategoryName>Online</MemberCategoryName>
|
||||
{onlineContacts.map(contact => (
|
||||
<Member
|
||||
key={contact.id}
|
||||
contact={contact}
|
||||
isOnline={contact.online}
|
||||
/>
|
||||
))}
|
||||
</MemberCategory>
|
||||
)}
|
||||
{offlineContacts.length > 0 && (
|
||||
<MemberCategory>
|
||||
<MemberCategoryName>Offline</MemberCategoryName>
|
||||
{offlineContacts.map(contact => (
|
||||
<Member
|
||||
key={contact.id}
|
||||
contact={contact}
|
||||
isOnline={contact.online}
|
||||
/>
|
||||
))}
|
||||
</MemberCategory>
|
||||
)}
|
||||
</MembersListWrap>
|
||||
)
|
||||
}
|
||||
|
||||
const MembersListWrap = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
`
|
||||
|
||||
const MemberCategory = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 16px;
|
||||
`
|
||||
|
||||
const MemberCategoryName = styled.h3`
|
||||
font-weight: normal;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
margin-bottom: 8px;
|
||||
`
|
||||
|
||||
const Row = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const LogoutBtn = styled.button`
|
||||
${buttonStyles}
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
padding: 0;
|
||||
`
|
|
@ -1,133 +0,0 @@
|
|||
import React, { useMemo } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import type { Contact } from '../../models/Contact'
|
||||
|
||||
type UserLogoProps = {
|
||||
radius: number
|
||||
colorWheel: [string, number][]
|
||||
contact?: Contact
|
||||
showOnlineStatus?: boolean
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export function UserLogo({
|
||||
icon,
|
||||
contact,
|
||||
radius,
|
||||
colorWheel,
|
||||
showOnlineStatus,
|
||||
}: UserLogoProps) {
|
||||
const conicGradient = useMemo(() => {
|
||||
const colors = colorWheel
|
||||
.map((color, idx) => {
|
||||
const prevDeg = idx === 0 ? '0deg' : `${colorWheel[idx - 1][1]}deg`
|
||||
return `${color[0]} ${prevDeg} ${color[1]}deg`
|
||||
})
|
||||
.join(',')
|
||||
return `conic-gradient(${colors})`
|
||||
}, [colorWheel])
|
||||
|
||||
const letters = useMemo(() => {
|
||||
if (contact && contact?.customName) {
|
||||
return contact.customName.slice(0, 2)
|
||||
}
|
||||
if (contact && contact.trueName) {
|
||||
return contact.trueName.slice(0, 2)
|
||||
}
|
||||
}, [contact])
|
||||
|
||||
const logoClassnName = useMemo(() => {
|
||||
if (showOnlineStatus) {
|
||||
if (contact && contact.online) {
|
||||
return 'online'
|
||||
}
|
||||
return 'offline'
|
||||
}
|
||||
return ''
|
||||
}, [contact, showOnlineStatus])
|
||||
|
||||
return (
|
||||
<Wrapper radius={radius} conicGradient={conicGradient}>
|
||||
<Logo
|
||||
icon={icon}
|
||||
radius={radius}
|
||||
className={contact ? logoClassnName : 'empty'}
|
||||
>
|
||||
{!icon && <TextWrapper radius={radius}>{letters}</TextWrapper>}
|
||||
</Logo>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const TextWrapper = styled.div<{ radius: number }>`
|
||||
font-weight: bold;
|
||||
font-size: calc(${({ radius }) => radius}px / 2.5);
|
||||
line-height: calc(${({ radius }) => radius}px / 2.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
letter-spacing: -0.4px;
|
||||
color: ${({ theme }) => theme.iconTextColor};
|
||||
`
|
||||
|
||||
const Logo = styled.div<{ radius: number; icon?: string }>`
|
||||
width: calc(${({ radius }) => radius}px - 6px);
|
||||
height: calc(${({ radius }) => radius}px - 6px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
background-color: ${({ theme }) => theme.logoColor};
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-image: ${({ icon }) => icon && `url(${icon}`};
|
||||
|
||||
&.offline {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
bottom: -2px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background-color: ${({ theme }) => theme.secondary};
|
||||
border: 2px solid ${({ theme }) => theme.bodyBackgroundColor};
|
||||
}
|
||||
}
|
||||
|
||||
&.online {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
bottom: -2px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background-color: ${({ theme }) => theme.greenColor};
|
||||
border: 2px solid ${({ theme }) => theme.bodyBackgroundColor};
|
||||
}
|
||||
}
|
||||
|
||||
&.empty {
|
||||
background-color: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
background-image: none;
|
||||
}
|
||||
`
|
||||
|
||||
export const Wrapper = styled.div<{ radius: number; conicGradient: string }>`
|
||||
width: ${({ radius }) => radius}px;
|
||||
height: ${({ radius }) => radius}px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: ${({ conicGradient }) => conicGradient};
|
||||
`
|
|
@ -1,69 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useMessengerContext } from '../../contexts/messengerProvider'
|
||||
import { useScrollToMessage } from '../../contexts/scrollProvider'
|
||||
import { ReplyOn, ReplyTo } from '../Chat-legacy/ChatInput'
|
||||
import { QuoteSvg } from '../Icons/QuoteIcon'
|
||||
import { UserIcon } from '../Icons/UserIcon'
|
||||
|
||||
import type { ChatMessage } from '../../models/ChatMessage'
|
||||
|
||||
function calcHeight(quote: ChatMessage) {
|
||||
if (quote.image && quote.content) {
|
||||
return 88
|
||||
} else if (quote.image && !quote.content) {
|
||||
return 68
|
||||
} else {
|
||||
return 25
|
||||
}
|
||||
}
|
||||
|
||||
type MessageQuoteProps = {
|
||||
quote: ChatMessage | undefined
|
||||
}
|
||||
|
||||
export function MessageQuote({ quote }: MessageQuoteProps) {
|
||||
const { contacts } = useMessengerContext()
|
||||
const scroll = useScrollToMessage()
|
||||
|
||||
if (quote && quote.sender) {
|
||||
return (
|
||||
<QuoteWrapper onClick={() => scroll(quote)}>
|
||||
<QuoteSvg width={22} height={calcHeight(quote)} />
|
||||
<QuoteSender>
|
||||
{' '}
|
||||
<UserIcon memberView={true} />{' '}
|
||||
{contacts[quote.sender]?.customName ??
|
||||
contacts[quote.sender].trueName}
|
||||
</QuoteSender>
|
||||
<Quote>{quote.content}</Quote>
|
||||
{quote.image && <QuoteImage src={quote.image} />}
|
||||
</QuoteWrapper>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const QuoteWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: 48px;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const QuoteSender = styled(ReplyTo)`
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
`
|
||||
|
||||
const Quote = styled(ReplyOn)`
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
`
|
||||
|
||||
const QuoteImage = styled.img`
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 4px;
|
||||
margin-top: 4px;
|
||||
`
|
|
@ -1,86 +0,0 @@
|
|||
import React, { useCallback } from 'react'
|
||||
|
||||
import { Emoji } from 'emoji-mart'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { ReactionButton } from '../Reactions/ReactionButton'
|
||||
|
||||
import type { BaseEmoji } from 'emoji-mart'
|
||||
|
||||
interface MessageReactionsProps {
|
||||
messageReactions: BaseEmoji[]
|
||||
setMessageReactions: React.Dispatch<React.SetStateAction<BaseEmoji[]>>
|
||||
}
|
||||
|
||||
export function MessageReactions({
|
||||
messageReactions,
|
||||
setMessageReactions,
|
||||
}: MessageReactionsProps) {
|
||||
const isMyReactionIncluded = (emoji: BaseEmoji) =>
|
||||
messageReactions.includes(emoji) // temporary function while message reactions are not added to waku
|
||||
|
||||
const handleReaction = useCallback(
|
||||
(emoji: BaseEmoji) => {
|
||||
messageReactions.find(e => e === emoji)
|
||||
? setMessageReactions(prev => prev.filter(e => e != emoji))
|
||||
: setMessageReactions(prev => [...prev, emoji])
|
||||
},
|
||||
[messageReactions, setMessageReactions]
|
||||
)
|
||||
|
||||
return (
|
||||
<Reactions>
|
||||
{messageReactions.map(reaction => (
|
||||
<EmojiReaction
|
||||
className={`${isMyReactionIncluded(reaction) && 'chosen'}`}
|
||||
key={reaction.id}
|
||||
onClick={() => handleReaction(reaction)}
|
||||
>
|
||||
<Emoji emoji={reaction} set={'twitter'} size={16} />
|
||||
<p>1</p>
|
||||
</EmojiReaction>
|
||||
))}
|
||||
<ReactionButton
|
||||
messageReactions={messageReactions}
|
||||
setMessageReactions={setMessageReactions}
|
||||
className="small"
|
||||
/>
|
||||
</Reactions>
|
||||
)
|
||||
}
|
||||
|
||||
export const Reactions = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 6px;
|
||||
`
|
||||
|
||||
const EmojiReaction = styled.button`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 2px 10px 10px 10px;
|
||||
color: ${({ theme }) => theme.primary};
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
padding: 2px 8px 2px 2px;
|
||||
margin-right: 4px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
& > p {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
& > span {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&.chosen {
|
||||
background: ${({ theme }) => theme.blueBg};
|
||||
border: 1px solid ${({ theme }) => theme.tertiary};
|
||||
color: ${({ theme }) => theme.tertiary};
|
||||
}
|
||||
`
|
|
@ -1,106 +0,0 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useMessengerContext } from '../../contexts/messengerProvider'
|
||||
import { useModal } from '../../contexts/modalProvider'
|
||||
import { useNarrow } from '../../contexts/narrowProvider'
|
||||
import { useChatScrollHandle } from '../../hooks/useChatScrollHandle'
|
||||
import { EmptyChannel } from '../Channels/EmptyChannel'
|
||||
import { LoadingIcon } from '../Icons/LoadingIcon'
|
||||
import { LinkModal, LinkModalName } from '../Modals/LinkModal'
|
||||
import { PictureModal, PictureModalName } from '../Modals/PictureModal'
|
||||
import { UiMessage } from './UiMessage'
|
||||
|
||||
import type { Reply } from '../../hooks/useReply'
|
||||
import type { ChannelData } from '../../models/ChannelData'
|
||||
|
||||
interface MessagesListProps {
|
||||
setReply: (val: Reply | undefined) => void
|
||||
channel: ChannelData
|
||||
}
|
||||
|
||||
export function MessagesList({ setReply, channel }: MessagesListProps) {
|
||||
const narrow = useNarrow()
|
||||
const { messages, contacts } = useMessengerContext()
|
||||
const ref = useRef<HTMLHeadingElement>(null)
|
||||
const loadingMessages = useChatScrollHandle(messages, ref)
|
||||
|
||||
const shownMessages = useMemo(
|
||||
() =>
|
||||
messages.filter(message => !contacts?.[message.sender]?.blocked ?? true),
|
||||
[contacts, messages]
|
||||
)
|
||||
|
||||
const [image, setImage] = useState('')
|
||||
const [link, setLink] = useState('')
|
||||
|
||||
const { setModal: setPictureModal, isVisible: showPictureModal } =
|
||||
useModal(PictureModalName)
|
||||
const { setModal: setLinkModal, isVisible: showLinkModal } =
|
||||
useModal(LinkModalName)
|
||||
|
||||
useEffect(
|
||||
() => (!image ? undefined : setPictureModal(true)),
|
||||
[image, setPictureModal]
|
||||
)
|
||||
useEffect(
|
||||
() => (!link ? undefined : setLinkModal(true)),
|
||||
[link, setLinkModal]
|
||||
)
|
||||
|
||||
useEffect(
|
||||
() => (!showPictureModal ? setImage('') : undefined),
|
||||
[showPictureModal]
|
||||
)
|
||||
useEffect(() => (!showLinkModal ? setLink('') : undefined), [showLinkModal])
|
||||
|
||||
return (
|
||||
<MessagesWrapper ref={ref} className={`${!narrow && 'wide'}`}>
|
||||
<PictureModal image={image} />
|
||||
<LinkModal link={link} />
|
||||
<EmptyChannel channel={channel} />
|
||||
{loadingMessages && (
|
||||
<LoadingWrapper>
|
||||
<LoadingIcon className="message" />
|
||||
</LoadingWrapper>
|
||||
)}
|
||||
{shownMessages.map((message, idx) => (
|
||||
<UiMessage
|
||||
key={message.id}
|
||||
message={message}
|
||||
idx={idx}
|
||||
prevMessage={shownMessages[idx - 1]}
|
||||
setLink={setLink}
|
||||
setImage={setImage}
|
||||
setReply={setReply}
|
||||
/>
|
||||
))}
|
||||
</MessagesWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const LoadingWrapper = styled.div`
|
||||
display: flex;
|
||||
align-self: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const MessagesWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 44px);
|
||||
overflow: auto;
|
||||
padding: 8px 0;
|
||||
|
||||
&.wide {
|
||||
margin-top: -24px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
`
|
|
@ -1,156 +0,0 @@
|
|||
import styled from 'styled-components'
|
||||
|
||||
import { textMediumStyles, textSmallStyles } from '../Text'
|
||||
|
||||
export const MessageWrapper = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px 16px;
|
||||
border-left: 2px solid ${({ theme }) => theme.bodyBackgroundColor};
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.inputColor};
|
||||
border-color: ${({ theme }) => theme.inputColor};
|
||||
}
|
||||
|
||||
&:hover > div {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&.mention {
|
||||
background: ${({ theme }) => theme.mentionBg};
|
||||
border-color: ${({ theme }) => theme.mentionColor};
|
||||
}
|
||||
|
||||
&.mention:hover {
|
||||
background: ${({ theme }) => theme.mentionBgHover};
|
||||
border-color: ${({ theme }) => theme.mentionColor};
|
||||
}
|
||||
`
|
||||
|
||||
export const MessageOuterWrapper = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
export const DateSeparator = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
color: #939ba1;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
${textSmallStyles}
|
||||
`
|
||||
|
||||
export const ContentWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 8px;
|
||||
`
|
||||
|
||||
export const MessageHeaderWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export const UserNameWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export const UserName = styled.p`
|
||||
font-weight: 500;
|
||||
color: ${({ theme }) => theme.tertiary};
|
||||
margin-right: 4px;
|
||||
|
||||
${textMediumStyles}
|
||||
`
|
||||
|
||||
export const UserNameBtn = styled.button`
|
||||
padding: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
position: relative;
|
||||
color: ${({ theme }) => theme.tertiary};
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
export const UserAddress = styled.p`
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
letter-spacing: 0.2px;
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
position: relative;
|
||||
padding-right: 8px;
|
||||
|
||||
&.chat:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: ${({ theme }) => theme.secondary};
|
||||
}
|
||||
`
|
||||
|
||||
export const TimeWrapper = styled.div`
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
letter-spacing: 0.2px;
|
||||
text-transform: uppercase;
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
margin-left: 4px;
|
||||
`
|
||||
|
||||
export const MessageText = styled.div`
|
||||
overflow-wrap: anywhere;
|
||||
width: 100%;
|
||||
white-space: pre-wrap;
|
||||
color: ${({ theme }) => theme.primary};
|
||||
`
|
||||
|
||||
export const IconBtn = styled.button`
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: end;
|
||||
flex-shrink: 0;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background-color: #bcbdff;
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
`
|
|
@ -1,164 +0,0 @@
|
|||
import React, { useMemo, useRef, useState } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useIdentity } from '../../contexts/identityProvider'
|
||||
import { useMessengerContext } from '../../contexts/messengerProvider'
|
||||
import { useClickOutside } from '../../hooks/useClickOutside'
|
||||
import { equalDate } from '../../utils'
|
||||
import { ChatMessageContent } from '../Chat-legacy/ChatMessageContent'
|
||||
import { ContactMenu } from '../Form/ContactMenu'
|
||||
import { MessageMenu } from '../Form/MessageMenu'
|
||||
import { UntrustworthIcon } from '../Icons/UntrustworthIcon'
|
||||
import { UserLogo } from '../Members/UserLogo'
|
||||
import { Reactions } from '../Reactions/Reactions'
|
||||
import { MessageQuote } from './MessageQuote'
|
||||
import { MessageReactions } from './MessageReactions'
|
||||
import {
|
||||
ContentWrapper,
|
||||
DateSeparator,
|
||||
IconBtn,
|
||||
MessageHeaderWrapper,
|
||||
MessageOuterWrapper,
|
||||
MessageText,
|
||||
MessageWrapper,
|
||||
TimeWrapper,
|
||||
UserAddress,
|
||||
UserName,
|
||||
UserNameBtn,
|
||||
UserNameWrapper,
|
||||
} from './Styles'
|
||||
|
||||
import type { Reply } from '../../hooks/useReply'
|
||||
import type { ChatMessage } from '../../models/ChatMessage'
|
||||
import type { BaseEmoji } from 'emoji-mart'
|
||||
|
||||
type UiMessageProps = {
|
||||
idx: number
|
||||
message: ChatMessage
|
||||
prevMessage: ChatMessage
|
||||
setImage: (img: string) => void
|
||||
setLink: (link: string) => void
|
||||
setReply: (val: Reply | undefined) => void
|
||||
}
|
||||
|
||||
export function UiMessage({
|
||||
message,
|
||||
idx,
|
||||
prevMessage,
|
||||
setImage,
|
||||
setLink,
|
||||
setReply,
|
||||
}: UiMessageProps) {
|
||||
const today = new Date()
|
||||
const { contacts } = useMessengerContext()
|
||||
const identity = useIdentity()
|
||||
|
||||
const contact = useMemo(
|
||||
() => contacts[message.sender],
|
||||
[message.sender, contacts]
|
||||
)
|
||||
const [showMenu, setShowMenu] = useState(false)
|
||||
|
||||
const [mentioned, setMentioned] = useState(false)
|
||||
const [messageReactions, setMessageReactions] = useState<BaseEmoji[]>([])
|
||||
|
||||
const ref = useRef(null)
|
||||
useClickOutside(ref, () => setShowMenu(false))
|
||||
|
||||
const messageRef = useRef(null)
|
||||
|
||||
return (
|
||||
<MessageOuterWrapper>
|
||||
{(idx === 0 || !equalDate(prevMessage.date, message.date)) && (
|
||||
<DateSeparator>
|
||||
{equalDate(message.date, today)
|
||||
? 'Today'
|
||||
: message.date.toLocaleDateString()}
|
||||
</DateSeparator>
|
||||
)}
|
||||
<MessageWrapper className={`${mentioned && 'mention'}`} id={message.id}>
|
||||
<MessageQuote quote={message.quote} />
|
||||
<UserMessageWrapper ref={messageRef}>
|
||||
<IconBtn
|
||||
onClick={() => {
|
||||
if (identity) setShowMenu(e => !e)
|
||||
}}
|
||||
disabled={!identity}
|
||||
ref={ref}
|
||||
>
|
||||
{showMenu && (
|
||||
<ContactMenu id={message.sender} setShowMenu={setShowMenu} />
|
||||
)}
|
||||
<UserLogo
|
||||
contact={contact}
|
||||
radius={40}
|
||||
colorWheel={[
|
||||
['red', 150],
|
||||
['blue', 250],
|
||||
['green', 360],
|
||||
]}
|
||||
/>
|
||||
</IconBtn>
|
||||
<ContentWrapper>
|
||||
<MessageHeaderWrapper>
|
||||
<UserNameWrapper>
|
||||
<UserNameBtn
|
||||
onClick={() => {
|
||||
if (identity) setShowMenu(e => !e)
|
||||
}}
|
||||
disabled={!identity}
|
||||
>
|
||||
<UserName>
|
||||
{' '}
|
||||
{contact?.customName ?? contact.trueName}
|
||||
</UserName>
|
||||
</UserNameBtn>
|
||||
<UserAddress className="chat">
|
||||
{message.sender.slice(0, 5)}...{message.sender.slice(-3)}
|
||||
</UserAddress>
|
||||
{contact.isUntrustworthy && <UntrustworthIcon />}
|
||||
</UserNameWrapper>
|
||||
<TimeWrapper>{message.date.toLocaleString()}</TimeWrapper>
|
||||
</MessageHeaderWrapper>
|
||||
<MessageText>
|
||||
<ChatMessageContent
|
||||
message={message}
|
||||
setImage={setImage}
|
||||
setLinkOpen={setLink}
|
||||
setMentioned={setMentioned}
|
||||
/>
|
||||
</MessageText>
|
||||
{messageReactions.length > 0 && (
|
||||
<MessageReactions
|
||||
messageReactions={messageReactions}
|
||||
setMessageReactions={setMessageReactions}
|
||||
/>
|
||||
)}
|
||||
</ContentWrapper>
|
||||
<MessageMenu
|
||||
message={message}
|
||||
setReply={setReply}
|
||||
messageReactions={messageReactions}
|
||||
setMessageReactions={setMessageReactions}
|
||||
messageRef={messageRef}
|
||||
/>
|
||||
</UserMessageWrapper>
|
||||
{identity && (
|
||||
<Reactions
|
||||
message={message}
|
||||
setReply={setReply}
|
||||
messageReactions={messageReactions}
|
||||
setMessageReactions={setMessageReactions}
|
||||
/>
|
||||
)}
|
||||
</MessageWrapper>
|
||||
</MessageOuterWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const UserMessageWrapper = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
`
|
|
@ -1,17 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import { Channels } from '../Channels/Channels'
|
||||
import { ListWrapper, NarrowTopbar } from './NarrowTopbar'
|
||||
|
||||
interface NarrowChannelsProps {
|
||||
setShowChannels: (val: boolean) => void
|
||||
}
|
||||
|
||||
export function NarrowChannels({ setShowChannels }: NarrowChannelsProps) {
|
||||
return (
|
||||
<ListWrapper>
|
||||
<NarrowTopbar list="Channels" onBtnClick={() => setShowChannels(false)} />
|
||||
<Channels onCommunityClick={() => setShowChannels(false)} />
|
||||
</ListWrapper>
|
||||
)
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
import React, { useMemo } from 'react'
|
||||
|
||||
import { useMessengerContext } from '../../contexts/messengerProvider'
|
||||
import { MembersList } from '../Members/MembersList'
|
||||
import { ListWrapper, NarrowTopbar } from './NarrowTopbar'
|
||||
|
||||
interface NarrowMembersProps {
|
||||
switchShowMembersList: () => void
|
||||
}
|
||||
|
||||
export function NarrowMembers({ switchShowMembersList }: NarrowMembersProps) {
|
||||
const { activeChannel } = useMessengerContext()
|
||||
const listName = useMemo(
|
||||
() =>
|
||||
activeChannel && activeChannel?.type === 'group'
|
||||
? 'Group members'
|
||||
: 'Community members',
|
||||
[activeChannel]
|
||||
)
|
||||
|
||||
return (
|
||||
<ListWrapper>
|
||||
<NarrowTopbar list={listName} onBtnClick={switchShowMembersList} />
|
||||
<MembersList />
|
||||
</ListWrapper>
|
||||
)
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useMessengerContext } from '../../contexts/messengerProvider'
|
||||
import { BackButton } from '../Buttons/BackButton'
|
||||
|
||||
interface NarrowTopbarProps {
|
||||
list: string
|
||||
onBtnClick: () => void
|
||||
}
|
||||
|
||||
export function NarrowTopbar({ list, onBtnClick }: NarrowTopbarProps) {
|
||||
const { communityData, activeChannel } = useMessengerContext()
|
||||
return (
|
||||
<TopbarWrapper>
|
||||
<BackButton onBtnClick={onBtnClick} />
|
||||
<HeadingWrapper>
|
||||
<Heading>{list}</Heading>
|
||||
<SubHeading>
|
||||
{activeChannel?.type === 'group'
|
||||
? activeChannel.name
|
||||
: communityData?.name}
|
||||
</SubHeading>
|
||||
</HeadingWrapper>
|
||||
</TopbarWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const TopbarWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
margin-bottom: 16px;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const HeadingWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const Heading = styled.p`
|
||||
font-weight: 500;
|
||||
color: ${({ theme }) => theme.primary};
|
||||
`
|
||||
|
||||
const SubHeading = styled.p`
|
||||
font-weight: 500;
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
`
|
||||
|
||||
export const ListWrapper = styled.div`
|
||||
padding: 16px;
|
||||
background: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
`
|
|
@ -1,91 +0,0 @@
|
|||
import React, { useRef, useState } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useClickOutside } from '../../hooks/useClickOutside'
|
||||
import { Tooltip } from '../Form/Tooltip'
|
||||
import { ReactionSvg } from '../Icons/ReactionIcon'
|
||||
import { ReactionPicker } from './ReactionPicker'
|
||||
|
||||
import type { BaseEmoji } from 'emoji-mart'
|
||||
|
||||
interface ReactionButtonProps {
|
||||
className?: string
|
||||
messageReactions: BaseEmoji[]
|
||||
setMessageReactions: React.Dispatch<React.SetStateAction<BaseEmoji[]>>
|
||||
}
|
||||
|
||||
export function ReactionButton({
|
||||
className,
|
||||
messageReactions,
|
||||
setMessageReactions,
|
||||
}: ReactionButtonProps) {
|
||||
const ref = useRef(null)
|
||||
useClickOutside(ref, () => setShowReactions(false))
|
||||
|
||||
const [showReactions, setShowReactions] = useState(false)
|
||||
|
||||
return (
|
||||
<Wrapper ref={ref}>
|
||||
{showReactions && (
|
||||
<ReactionPicker
|
||||
messageReactions={messageReactions}
|
||||
setMessageReactions={setMessageReactions}
|
||||
className={className}
|
||||
/>
|
||||
)}
|
||||
<ReactionBtn
|
||||
onClick={() => setShowReactions(!showReactions)}
|
||||
className={className}
|
||||
>
|
||||
<ReactionSvg className={className} />
|
||||
{!className && !showReactions && <Tooltip tip="Add reaction" />}
|
||||
</ReactionBtn>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: relative;
|
||||
`
|
||||
|
||||
export const ReactionBtn = styled.button`
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
align-self: center;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.buttonBgHover};
|
||||
}
|
||||
|
||||
&.red:hover {
|
||||
background: ${({ theme }) => theme.buttonNoBgHover};
|
||||
}
|
||||
|
||||
&:hover > svg {
|
||||
fill: ${({ theme }) => theme.tertiary};
|
||||
}
|
||||
|
||||
&.red:hover > svg {
|
||||
fill: ${({ theme }) => theme.redColor};
|
||||
}
|
||||
|
||||
&:hover > div {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&.small {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
background: inherit;
|
||||
}
|
||||
}
|
||||
`
|
|
@ -1,111 +0,0 @@
|
|||
import React, { useCallback } from 'react'
|
||||
|
||||
import { Emoji, getEmojiDataFromNative } from 'emoji-mart'
|
||||
import data from 'emoji-mart/data/all.json'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import type { BaseEmoji } from 'emoji-mart'
|
||||
|
||||
const emojiHeart = getEmojiDataFromNative('❤️', 'twitter', data)
|
||||
const emojiLike = getEmojiDataFromNative('👍', 'twitter', data)
|
||||
const emojiDislike = getEmojiDataFromNative('👎', 'twitter', data)
|
||||
const emojiLaughing = getEmojiDataFromNative('😆', 'twitter', data)
|
||||
const emojiDisappointed = getEmojiDataFromNative('😥', 'twitter', data)
|
||||
const emojiRage = getEmojiDataFromNative('😡', 'twitter', data)
|
||||
|
||||
export const emojiArr = [
|
||||
emojiHeart,
|
||||
emojiLike,
|
||||
emojiDislike,
|
||||
emojiLaughing,
|
||||
emojiDisappointed,
|
||||
emojiRage,
|
||||
]
|
||||
|
||||
interface ReactionPickerProps {
|
||||
className?: string
|
||||
messageReactions: BaseEmoji[]
|
||||
setMessageReactions: React.Dispatch<React.SetStateAction<BaseEmoji[]>>
|
||||
}
|
||||
|
||||
export function ReactionPicker({
|
||||
className,
|
||||
messageReactions,
|
||||
setMessageReactions,
|
||||
}: ReactionPickerProps) {
|
||||
const handleReaction = useCallback(
|
||||
(emoji: BaseEmoji) => {
|
||||
messageReactions.find(e => e === emoji)
|
||||
? setMessageReactions(prev => prev.filter(e => e != emoji))
|
||||
: setMessageReactions(prev => [...prev, emoji])
|
||||
},
|
||||
[messageReactions, setMessageReactions]
|
||||
)
|
||||
|
||||
return (
|
||||
<Wrapper className={className}>
|
||||
{emojiArr.map(emoji => (
|
||||
<EmojiBtn
|
||||
key={emoji.id}
|
||||
onClick={() => handleReaction(emoji)}
|
||||
className={`${messageReactions.includes(emoji) && 'chosen'}`}
|
||||
menuMode={className === 'menu'}
|
||||
>
|
||||
{' '}
|
||||
<Emoji
|
||||
emoji={emoji}
|
||||
set={'twitter'}
|
||||
skin={emoji.skin || 1}
|
||||
size={className === 'menu' ? 20 : 32}
|
||||
/>
|
||||
</EmojiBtn>
|
||||
))}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 266px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: absolute;
|
||||
right: -34px;
|
||||
top: -60px;
|
||||
background: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.08);
|
||||
border-radius: 16px 16px 4px 16px;
|
||||
padding: 8px;
|
||||
|
||||
&.small {
|
||||
right: unset;
|
||||
left: -100px;
|
||||
transform: none;
|
||||
border-radius: 16px 16px 16px 4px;
|
||||
}
|
||||
|
||||
&.menu {
|
||||
width: 100%;
|
||||
position: static;
|
||||
box-shadow: unset;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
`
|
||||
|
||||
export const EmojiBtn = styled.button<{ menuMode: boolean }>`
|
||||
width: ${({ menuMode }) => (menuMode ? '24px' : '40px')};
|
||||
height: ${({ menuMode }) => (menuMode ? '24px' : '40px')};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.inputColor};
|
||||
}
|
||||
|
||||
&.chosen {
|
||||
background: ${({ theme }) => theme.reactionBg};
|
||||
border: 1px solid ${({ theme }) => theme.tertiary};
|
||||
}
|
||||
`
|
|
@ -1,90 +0,0 @@
|
|||
import React, { useMemo } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useUserPublicKey } from '../../contexts/identityProvider'
|
||||
import { useMessengerContext } from '../../contexts/messengerProvider'
|
||||
import { Tooltip } from '../Form/Tooltip'
|
||||
import { DeleteIcon } from '../Icons/DeleteIcon'
|
||||
import { EditIcon } from '../Icons/EditIcon'
|
||||
import { PinIcon } from '../Icons/PinIcon'
|
||||
import { ReplySvg } from '../Icons/ReplyIcon'
|
||||
import { ReactionBtn, ReactionButton } from './ReactionButton'
|
||||
|
||||
import type { Reply } from '../../hooks/useReply'
|
||||
import type { ChatMessage } from '../../models/ChatMessage'
|
||||
import type { BaseEmoji } from 'emoji-mart'
|
||||
|
||||
interface ReactionsProps {
|
||||
message: ChatMessage
|
||||
setReply: (val: Reply | undefined) => void
|
||||
messageReactions: BaseEmoji[]
|
||||
setMessageReactions: React.Dispatch<React.SetStateAction<BaseEmoji[]>>
|
||||
}
|
||||
|
||||
export function Reactions({
|
||||
message,
|
||||
setReply,
|
||||
messageReactions,
|
||||
setMessageReactions,
|
||||
}: ReactionsProps) {
|
||||
const userPK = useUserPublicKey()
|
||||
const { activeChannel } = useMessengerContext()
|
||||
|
||||
const userMessage = useMemo(
|
||||
() => !!userPK && message.sender === userPK,
|
||||
[userPK, message]
|
||||
)
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<ReactionButton
|
||||
messageReactions={messageReactions}
|
||||
setMessageReactions={setMessageReactions}
|
||||
/>
|
||||
<ReactionBtn
|
||||
onClick={() =>
|
||||
setReply({
|
||||
sender: message.sender,
|
||||
content: message.content,
|
||||
image: message.image,
|
||||
id: message.id,
|
||||
})
|
||||
}
|
||||
>
|
||||
<ReplySvg width={22} height={22} />
|
||||
<Tooltip tip="Reply" />
|
||||
</ReactionBtn>
|
||||
{userMessage && (
|
||||
<ReactionBtn>
|
||||
<EditIcon width={22} height={22} className="grey" />
|
||||
<Tooltip tip="Edit" />
|
||||
</ReactionBtn>
|
||||
)}
|
||||
{activeChannel?.type !== 'channel' && (
|
||||
<ReactionBtn>
|
||||
<PinIcon width={22} height={22} />
|
||||
<Tooltip tip="Pin" />
|
||||
</ReactionBtn>
|
||||
)}
|
||||
{userMessage && (
|
||||
<ReactionBtn className="red">
|
||||
<DeleteIcon width={22} height={22} className="grey" />
|
||||
<Tooltip tip="Delete" />
|
||||
</ReactionBtn>
|
||||
)}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: -18px;
|
||||
box-shadow: 0px 4px 12px rgba(0, 34, 51, 0.08);
|
||||
border-radius: 8px;
|
||||
background: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
padding: 2px;
|
||||
visibility: hidden;
|
||||
`
|
|
@ -1,78 +0,0 @@
|
|||
import React, { useMemo } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useMessengerContext } from '../contexts/messengerProvider'
|
||||
import { ContactsList } from './Chat-legacy/ChatCreation'
|
||||
import { Member } from './Members/Member'
|
||||
|
||||
interface SearchBlockProps {
|
||||
query: string
|
||||
discludeList: string[]
|
||||
onClick: (member: string) => void
|
||||
onBotttom?: boolean
|
||||
}
|
||||
|
||||
export const SearchBlock = ({
|
||||
query,
|
||||
discludeList,
|
||||
onClick,
|
||||
onBotttom,
|
||||
}: SearchBlockProps) => {
|
||||
const { contacts } = useMessengerContext()
|
||||
|
||||
const searchList = useMemo(() => {
|
||||
return Object.values(contacts)
|
||||
.filter(
|
||||
member =>
|
||||
member.id.includes(query) ||
|
||||
member?.customName?.includes(query) ||
|
||||
member.trueName.includes(query)
|
||||
)
|
||||
.filter(member => !discludeList.includes(member.id))
|
||||
}, [query, discludeList, contacts])
|
||||
|
||||
if (searchList.length === 0 || !query) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<SearchContacts
|
||||
style={{ [onBotttom ? 'bottom' : 'top']: 'calc(100% + 24px)' }}
|
||||
>
|
||||
<ContactsList>
|
||||
{searchList.map(member => (
|
||||
<SearchContact key={member.id}>
|
||||
<Member contact={member} onClick={() => onClick(member.id)} />
|
||||
</SearchContact>
|
||||
))}
|
||||
</ContactsList>
|
||||
</SearchContacts>
|
||||
)
|
||||
}
|
||||
|
||||
const SearchContacts = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 360px;
|
||||
padding: 8px;
|
||||
background-color: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
box-shadow: 0px 2px 4px rgba(0, 34, 51, 0.16),
|
||||
0px 4px 12px rgba(0, 34, 51, 0.08);
|
||||
border-radius: 8px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
`
|
||||
|
||||
const SearchContact = styled.div`
|
||||
width: 340px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 12px 0 16px;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.inputColor};
|
||||
}
|
||||
`
|
|
@ -1,29 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { Column } from '../CommunityIdentity'
|
||||
import { Skeleton } from './Skeleton'
|
||||
|
||||
export const CommunitySkeleton = () => {
|
||||
return (
|
||||
<Loading>
|
||||
<LogoSkeleton width="40px" height="40px" borderRadius="50%" />
|
||||
<Column>
|
||||
<Skeleton width="140px" height="16px" />
|
||||
<Skeleton width="65px" height="16px" />
|
||||
</Column>
|
||||
</Loading>
|
||||
)
|
||||
}
|
||||
|
||||
const LogoSkeleton = styled(Skeleton)`
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
`
|
||||
|
||||
const Loading = styled.div`
|
||||
display: flex;
|
||||
padding: 0 0 0 10px;
|
||||
`
|
|
@ -1,38 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { LoadingIcon } from '../Icons/LoadingIcon'
|
||||
import { textSmallStyles } from '../Text'
|
||||
|
||||
export const Loading = () => {
|
||||
return (
|
||||
<LoadingBlock>
|
||||
<LoadingIcon />
|
||||
<LoadingText>Loading messages...</LoadingText>
|
||||
</LoadingBlock>
|
||||
)
|
||||
}
|
||||
|
||||
const LoadingBlock = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -35px;
|
||||
transform: translateX(-50%);
|
||||
padding: 4px 5px 4px 7px;
|
||||
background: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
color: ${({ theme }) => theme.primary};
|
||||
box-shadow: ${({ theme }) => theme.shadow};
|
||||
border-radius: 8px;
|
||||
z-index: 2;
|
||||
`
|
||||
|
||||
const LoadingText = styled.p`
|
||||
color: ${({ theme }) => theme.primary};
|
||||
margin-left: 8px;
|
||||
font-weight: 500;
|
||||
|
||||
${textSmallStyles}
|
||||
`
|
|
@ -1,56 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { MessageSkeleton } from './MessageSkeleton'
|
||||
import { Skeleton } from './Skeleton'
|
||||
|
||||
export const LoadingSkeleton = () => {
|
||||
return (
|
||||
<Loading>
|
||||
<MessageSkeleton>
|
||||
<Skeleton />
|
||||
</MessageSkeleton>
|
||||
<MessageSkeleton>
|
||||
<Skeleton />
|
||||
</MessageSkeleton>
|
||||
<MessageSkeleton>
|
||||
<Skeleton />
|
||||
</MessageSkeleton>
|
||||
<MessageSkeleton>
|
||||
<Skeleton />
|
||||
</MessageSkeleton>
|
||||
<MessageSkeleton>
|
||||
<Skeleton />
|
||||
<Skeleton width="30%" />
|
||||
</MessageSkeleton>
|
||||
<MessageSkeleton>
|
||||
<Skeleton width="70%" />
|
||||
</MessageSkeleton>
|
||||
<MessageSkeleton>
|
||||
<Skeleton width="40%" />
|
||||
<Skeleton width="25%" />
|
||||
<Skeleton width="30%" />
|
||||
</MessageSkeleton>
|
||||
<MessageSkeleton>
|
||||
<Skeleton width="50%" />
|
||||
<Skeleton width="147px" height="196px" borderRadius="16px" />
|
||||
</MessageSkeleton>
|
||||
<MessageSkeleton>
|
||||
<Skeleton width="50%" />
|
||||
</MessageSkeleton>
|
||||
<MessageSkeleton>
|
||||
<Skeleton width="70%" />
|
||||
</MessageSkeleton>
|
||||
</Loading>
|
||||
)
|
||||
}
|
||||
|
||||
const Loading = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
height: calc(100% - 44px);
|
||||
padding: 8px 16px 0;
|
||||
overflow: auto;
|
||||
`
|
|
@ -1,61 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { Skeleton } from './Skeleton'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
interface MessageSkeletonProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const MessageSkeleton = ({ children }: MessageSkeletonProps) => {
|
||||
return (
|
||||
<MessageWrapper>
|
||||
<AvatarSkeleton width="40px" height="40px" borderRadius="50%" />
|
||||
<ContentWrapper>
|
||||
<MessageHeaderWrapper>
|
||||
<UserNameSkeleton width="132px" />
|
||||
<TimeSkeleton width="47px" height="14px" />
|
||||
</MessageHeaderWrapper>
|
||||
<MessageBodyWrapper>{children}</MessageBodyWrapper>
|
||||
</ContentWrapper>
|
||||
</MessageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const MessageWrapper = styled.div`
|
||||
display: flex;
|
||||
padding: 8px 0;
|
||||
margin-bottom: 8px;
|
||||
`
|
||||
|
||||
const ContentWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
const MessageHeaderWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const MessageBodyWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
const AvatarSkeleton = styled(Skeleton)`
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
`
|
||||
|
||||
const UserNameSkeleton = styled(Skeleton)`
|
||||
margin-right: 4px;
|
||||
`
|
||||
|
||||
const TimeSkeleton = styled(Skeleton)`
|
||||
margin-right: 4px;
|
||||
`
|
|
@ -1,46 +0,0 @@
|
|||
import styled, { keyframes } from 'styled-components'
|
||||
|
||||
interface SkeletonProps {
|
||||
width?: string
|
||||
height?: string
|
||||
borderRadius?: string
|
||||
}
|
||||
|
||||
const waveKeyframe = keyframes`
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
`
|
||||
|
||||
export const Skeleton = styled.div<SkeletonProps>`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: ${({ width }) => width || '100%'};
|
||||
height: ${({ height }) => height || '22px'};
|
||||
background: ${({ theme }) => theme.skeletonDark};
|
||||
border-radius: ${({ borderRadius }) => borderRadius || '8px'};
|
||||
margin-bottom: 5px;
|
||||
overflow: hidden;
|
||||
|
||||
&::after {
|
||||
animation: ${waveKeyframe} 1.6s linear 0.5s infinite;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
${({ theme }) => theme.skeletonLight} 0%,
|
||||
${({ theme }) => theme.skeletonDark} 100%
|
||||
);
|
||||
content: '';
|
||||
position: absolute;
|
||||
transform: translateX(-100%);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
`
|
|
@ -1,11 +0,0 @@
|
|||
import { css } from 'styled-components'
|
||||
|
||||
export const textSmallStyles = css`
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
`
|
||||
|
||||
export const textMediumStyles = css`
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
`
|
|
@ -1,130 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import styled, { keyframes } from 'styled-components'
|
||||
|
||||
import { useToasts } from '../../contexts/toastProvider'
|
||||
import { Column } from '../CommunityIdentity'
|
||||
import { CheckIcon } from '../Icons/CheckIcon'
|
||||
import { CommunityIcon } from '../Icons/CommunityIcon'
|
||||
import { CrossIcon } from '../Icons/CrossIcon'
|
||||
import { ProfileIcon } from '../Icons/ProfileIcon'
|
||||
import { textSmallStyles } from '../Text'
|
||||
|
||||
import type { Toast } from '../../models/Toast'
|
||||
|
||||
export function AnimationToastMessage() {
|
||||
return keyframes`
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-100%); }
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0); }
|
||||
`
|
||||
}
|
||||
|
||||
type ToastMessageProps = {
|
||||
toast: Toast
|
||||
}
|
||||
|
||||
export function ToastMessage({ toast }: ToastMessageProps) {
|
||||
const { setToasts } = useToasts()
|
||||
|
||||
const closeToast = () => {
|
||||
setToasts(prev => prev.filter(e => e != toast))
|
||||
}
|
||||
|
||||
return (
|
||||
<ToastWrapper>
|
||||
<ToastBlock>
|
||||
{toast.type === 'confirmation' && (
|
||||
<IconWrapper className="green">
|
||||
<CheckIcon width={20} height={20} className="green" />
|
||||
</IconWrapper>
|
||||
)}
|
||||
{toast.type === 'incoming' && (
|
||||
<IconWrapper className="blue">
|
||||
<ProfileIcon width={20} height={20} />
|
||||
</IconWrapper>
|
||||
)}
|
||||
{(toast.type === 'approvement' || toast.type === 'rejection') && (
|
||||
<IconWrapper
|
||||
className={toast.type === 'approvement' ? 'green' : 'red'}
|
||||
>
|
||||
<CommunityIcon
|
||||
width={20}
|
||||
height={19}
|
||||
className={toast.type === 'approvement' ? 'green' : 'red'}
|
||||
/>
|
||||
</IconWrapper>
|
||||
)}
|
||||
<Column>
|
||||
<ToastText>{toast.text}</ToastText>
|
||||
{toast.request && <ToastRequest>{toast.request}</ToastRequest>}
|
||||
</Column>
|
||||
</ToastBlock>
|
||||
<CloseButton onClick={closeToast}>
|
||||
<CrossIcon />
|
||||
</CloseButton>
|
||||
</ToastWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const ToastWrapper = styled.div`
|
||||
width: 343px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
margin-top: 8px;
|
||||
background: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
box-shadow: ${({ theme }) => theme.shadow};
|
||||
border-radius: 8px;
|
||||
animation: ${AnimationToastMessage} 2s ease;
|
||||
`
|
||||
|
||||
const ToastBlock = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.primary};
|
||||
`
|
||||
|
||||
const ToastText = styled.p`
|
||||
font-weight: 500;
|
||||
${textSmallStyles};
|
||||
`
|
||||
|
||||
const ToastRequest = styled(ToastText)`
|
||||
width: 243px;
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
const IconWrapper = styled.div`
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
|
||||
&.green {
|
||||
background: ${({ theme }) => theme.greenBg};
|
||||
}
|
||||
|
||||
&.blue {
|
||||
background: ${({ theme }) => theme.blueBg};
|
||||
}
|
||||
|
||||
&.red {
|
||||
background: ${({ theme }) => theme.buttonNoBg};
|
||||
}
|
||||
`
|
||||
|
||||
const CloseButton = styled.button`
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
`
|
|
@ -1,29 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useToasts } from '../../contexts/toastProvider'
|
||||
import { ToastMessage } from './ToastMessage'
|
||||
|
||||
export function ToastMessageList() {
|
||||
const { toasts } = useToasts()
|
||||
|
||||
return (
|
||||
<ToastsWrapper>
|
||||
{toasts.map(toast => (
|
||||
<ToastMessage key={toast.id} toast={toast} />
|
||||
))}
|
||||
</ToastsWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const ToastsWrapper = styled.div`
|
||||
position: absolute;
|
||||
bottom: 56px;
|
||||
right: 16px;
|
||||
width: 343px;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
align-items: center;
|
||||
z-index: 999;
|
||||
`
|
|
@ -1,45 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useNarrow } from '../../contexts/narrowProvider'
|
||||
import { ColorChatIcon } from '../Icons/ColorChatIcon'
|
||||
import { UserCreationButtons } from './UserCreationButtons'
|
||||
|
||||
interface UserCreationProps {
|
||||
permission: boolean
|
||||
}
|
||||
|
||||
export function UserCreation({ permission }: UserCreationProps) {
|
||||
const narrow = useNarrow()
|
||||
|
||||
if (!narrow) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<ColorChatIcon />
|
||||
<TitleWrapper>Want to jump into the discussion?</TitleWrapper>
|
||||
<UserCreationButtons permission={permission} />
|
||||
</Wrapper>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
background-color: ${({ theme }) => theme.sectionBackgroundColor};
|
||||
`
|
||||
|
||||
const TitleWrapper = styled.div`
|
||||
font-weight: bold;
|
||||
font-size: 17px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
margin: 24px 0;
|
||||
color: ${({ theme }) => theme.primary};
|
||||
`
|
|
@ -1,86 +0,0 @@
|
|||
import React, { useMemo } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useModal } from '../../contexts/modalProvider'
|
||||
import { useNarrow } from '../../contexts/narrowProvider'
|
||||
import { loadEncryptedIdentity } from '../../utils'
|
||||
import { buttonStyles, buttonTransparentStyles } from '../Buttons/buttonStyle'
|
||||
import { ProfileFoundModalName } from '../Modals/ProfileFoundModal'
|
||||
import { StatusModalName } from '../Modals/StatusModal'
|
||||
import { UserCreationModalName } from '../Modals/UserCreationModal'
|
||||
import { UserCreationStartModalName } from '../Modals/UserCreationStartModal'
|
||||
import { WalletModalName } from '../Modals/WalletModal'
|
||||
import { textSmallStyles } from '../Text'
|
||||
|
||||
interface UserCreationProps {
|
||||
permission: boolean
|
||||
}
|
||||
|
||||
export function UserCreationButtons({ permission }: UserCreationProps) {
|
||||
const narrow = useNarrow()
|
||||
const { setModal } = useModal(UserCreationModalName)
|
||||
const { setModal: setStatusModal } = useModal(StatusModalName)
|
||||
const { setModal: setWalletModal } = useModal(WalletModalName)
|
||||
const { setModal: setProfileFoundModal } = useModal(ProfileFoundModalName)
|
||||
const { setModal: setCreationStartModal } = useModal(
|
||||
UserCreationStartModalName
|
||||
)
|
||||
|
||||
const encryptedIdentity = useMemo(() => loadEncryptedIdentity(), [])
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<LoginBtn
|
||||
onClick={() => {
|
||||
setStatusModal(true)
|
||||
setCreationStartModal(false)
|
||||
}}
|
||||
className={`${narrow && 'narrow'}`}
|
||||
>
|
||||
Sync with Status profile
|
||||
</LoginBtn>
|
||||
<LoginBtn
|
||||
onClick={() => {
|
||||
setWalletModal(true)
|
||||
setCreationStartModal(false)
|
||||
}}
|
||||
className={`${narrow && 'narrow'}`}
|
||||
>
|
||||
Connect Ethereum Wallet
|
||||
</LoginBtn>
|
||||
{permission && (
|
||||
<ThrowAwayButton
|
||||
onClick={() => {
|
||||
encryptedIdentity ? setProfileFoundModal(true) : setModal(true)
|
||||
setCreationStartModal(false)
|
||||
}}
|
||||
>
|
||||
Use a throwaway profile
|
||||
</ThrowAwayButton>
|
||||
)}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
const LoginBtn = styled.button`
|
||||
${buttonStyles}
|
||||
${textSmallStyles}
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&.narrow {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
`
|
||||
|
||||
const ThrowAwayButton = styled.button`
|
||||
${buttonTransparentStyles}
|
||||
`
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react'
|
||||
|
||||
import { useAppState } from '~/src/contexts/app-context'
|
||||
import { styled } from '~/src/styles/config'
|
||||
import { Separator } from '~/src/system'
|
||||
|
||||
|
@ -9,6 +10,12 @@ import { GetStarted } from './components/get-started'
|
|||
import { Messages } from './components/messages'
|
||||
|
||||
export const MainSidebar = () => {
|
||||
const { options } = useAppState()
|
||||
|
||||
if (options.enableSidebar === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<CommunityInfo />
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
import React, { createContext, useContext, useState } from 'react'
|
||||
|
||||
export enum ChatState {
|
||||
ChatCreation,
|
||||
ChatBody,
|
||||
}
|
||||
|
||||
type ChatStateContextType = [
|
||||
ChatState,
|
||||
React.Dispatch<React.SetStateAction<ChatState>>
|
||||
]
|
||||
|
||||
const ChatStateContext = createContext<ChatStateContextType>([
|
||||
ChatState.ChatBody,
|
||||
() => undefined,
|
||||
])
|
||||
|
||||
export function useChatState() {
|
||||
return useContext(ChatStateContext)
|
||||
}
|
||||
|
||||
export function ChatStateProvider({ children }: { children: React.ReactNode }) {
|
||||
const state = useState(ChatState.ChatBody)
|
||||
return (
|
||||
<ChatStateContext.Provider value={state}>
|
||||
{children}
|
||||
</ChatStateContext.Provider>
|
||||
)
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
import React, { createContext, useContext, useMemo, useState } from 'react'
|
||||
|
||||
import { bufToHex } from '@status-im/core'
|
||||
|
||||
import type { Identity } from '@status-im/core'
|
||||
|
||||
const IdentityContext = createContext<{
|
||||
identity: Identity | undefined
|
||||
setIdentity: React.Dispatch<React.SetStateAction<Identity | undefined>>
|
||||
publicKey: string | undefined
|
||||
walletIdentity: Identity | undefined
|
||||
setWalletIdentity: React.Dispatch<React.SetStateAction<Identity | undefined>>
|
||||
nickname: string | undefined
|
||||
setNickname: React.Dispatch<React.SetStateAction<string | undefined>>
|
||||
}>({
|
||||
identity: undefined,
|
||||
setIdentity: () => undefined,
|
||||
publicKey: undefined,
|
||||
walletIdentity: undefined,
|
||||
setWalletIdentity: () => undefined,
|
||||
nickname: undefined,
|
||||
setNickname: () => undefined,
|
||||
})
|
||||
|
||||
export function useIdentity() {
|
||||
return useContext(IdentityContext).identity
|
||||
}
|
||||
|
||||
export function useUserPublicKey() {
|
||||
return useContext(IdentityContext).publicKey
|
||||
}
|
||||
|
||||
export function useSetIdentity() {
|
||||
return useContext(IdentityContext).setIdentity
|
||||
}
|
||||
|
||||
export function useWalletIdentity() {
|
||||
return useContext(IdentityContext).walletIdentity
|
||||
}
|
||||
|
||||
export function useSetWalletIdentity() {
|
||||
return useContext(IdentityContext).setWalletIdentity
|
||||
}
|
||||
|
||||
export function useNickname() {
|
||||
return useContext(IdentityContext).nickname
|
||||
}
|
||||
|
||||
export function useSetNikcname() {
|
||||
return useContext(IdentityContext).setNickname
|
||||
}
|
||||
|
||||
interface IdentityProviderProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function IdentityProvider({ children }: IdentityProviderProps) {
|
||||
const [identity, setIdentity] = useState<Identity | undefined>(undefined)
|
||||
const publicKey = useMemo(
|
||||
() => (identity ? bufToHex(identity.publicKey) : undefined),
|
||||
[identity]
|
||||
)
|
||||
const [walletIdentity, setWalletIdentity] = useState<Identity | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [nickname, setNickname] = useState<string | undefined>(undefined)
|
||||
|
||||
return (
|
||||
<IdentityContext.Provider
|
||||
value={{
|
||||
identity,
|
||||
setIdentity,
|
||||
publicKey,
|
||||
nickname,
|
||||
setNickname,
|
||||
walletIdentity,
|
||||
setWalletIdentity,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</IdentityContext.Provider>
|
||||
)
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
import React, { createContext, useContext } from 'react'
|
||||
|
||||
import { useMessenger } from '../hooks/messenger/useMessenger'
|
||||
import { useIdentity, useNickname } from './identityProvider'
|
||||
|
||||
import type { MessengerType } from '../hooks/messenger/useMessenger'
|
||||
import type { Environment } from '~/src/types/config'
|
||||
|
||||
const MessengerContext = createContext<MessengerType>({
|
||||
messenger: undefined,
|
||||
messages: [],
|
||||
sendMessage: async () => undefined,
|
||||
notifications: {},
|
||||
clearNotifications: () => undefined,
|
||||
mentions: {},
|
||||
clearMentions: () => undefined,
|
||||
loadPrevDay: async () => undefined,
|
||||
loadingMessages: false,
|
||||
loadingMessenger: true,
|
||||
communityData: undefined,
|
||||
contacts: {},
|
||||
contactsDispatch: () => undefined,
|
||||
addContact: () => undefined,
|
||||
activeChannel: undefined,
|
||||
channels: {},
|
||||
channelsDispatch: () => undefined,
|
||||
removeChannel: () => undefined,
|
||||
createGroupChat: () => undefined,
|
||||
changeGroupChatName: () => undefined,
|
||||
addMembers: () => undefined,
|
||||
nickname: undefined,
|
||||
subscriptionsDispatch: () => undefined,
|
||||
})
|
||||
|
||||
export function useMessengerContext() {
|
||||
return useContext(MessengerContext)
|
||||
}
|
||||
|
||||
interface Props {
|
||||
publicKey: string
|
||||
environment?: Environment
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function MessengerProvider(props: Props) {
|
||||
const { publicKey, environment, children } = props
|
||||
|
||||
const identity = useIdentity()
|
||||
const nickname = useNickname()
|
||||
const messenger = useMessenger(publicKey, environment, identity, nickname)
|
||||
|
||||
return (
|
||||
<MessengerContext.Provider value={messenger}>
|
||||
{children}
|
||||
</MessengerContext.Provider>
|
||||
)
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
import type {
|
||||
ProfileModalName,
|
||||
ProfileModalProps,
|
||||
} from '../components/Modals/ProfileModal'
|
||||
|
||||
type TypeMap = {
|
||||
[ProfileModalName]?: ProfileModalProps
|
||||
}
|
||||
|
||||
type ModalsState = TypeMap & {
|
||||
[name: string]: boolean | undefined
|
||||
}
|
||||
|
||||
type ModalContextType = [
|
||||
state: ModalsState,
|
||||
setState: React.Dispatch<React.SetStateAction<ModalsState>>
|
||||
]
|
||||
|
||||
const ModalContext = createContext<ModalContextType>([{}, () => undefined])
|
||||
|
||||
export function useModal<T extends string>(name: T) {
|
||||
const [modals, setModals] = useContext(ModalContext)
|
||||
|
||||
const setModal = useCallback(
|
||||
(state: T extends keyof TypeMap ? TypeMap[T] | false : boolean) => {
|
||||
setModals(prev => {
|
||||
if (!state) {
|
||||
return {
|
||||
...prev,
|
||||
[name]: undefined,
|
||||
}
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
[name]: state,
|
||||
}
|
||||
})
|
||||
},
|
||||
[name, setModals]
|
||||
)
|
||||
const isVisible = useMemo(() => !!modals?.[name], [modals, name])
|
||||
|
||||
const props = useMemo(() => modals?.[name], [modals, name])
|
||||
|
||||
return { isVisible, setModal, props }
|
||||
}
|
||||
|
||||
interface IdentityProviderProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function ModalProvider({ children }: IdentityProviderProps) {
|
||||
const modalState = useState<ModalsState>({})
|
||||
return (
|
||||
<ModalContext.Provider value={modalState}>{children}</ModalContext.Provider>
|
||||
)
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
import React, { createContext, useContext } from 'react'
|
||||
|
||||
import { useRefBreak } from '../hooks/useRefBreak'
|
||||
|
||||
const NarrowContext = createContext<{ narrow: boolean; low: boolean }>({
|
||||
narrow: false,
|
||||
low: false,
|
||||
})
|
||||
|
||||
export function useNarrow() {
|
||||
const { narrow } = useContext(NarrowContext)
|
||||
return narrow
|
||||
}
|
||||
|
||||
export function useLow() {
|
||||
const { low } = useContext(NarrowContext)
|
||||
return low
|
||||
}
|
||||
|
||||
interface NarrowProviderProps {
|
||||
children: React.ReactNode
|
||||
myRef: React.RefObject<HTMLHeadingElement>
|
||||
}
|
||||
|
||||
export function NarrowProvider({ children, myRef }: NarrowProviderProps) {
|
||||
const narrow = useRefBreak(myRef?.current?.offsetWidth ?? 0, 736)
|
||||
const low = useRefBreak(myRef?.current?.offsetHeight ?? 0, 465)
|
||||
return (
|
||||
<NarrowContext.Provider value={{ narrow, low }}>
|
||||
{children}
|
||||
</NarrowContext.Provider>
|
||||
)
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
import { useMessengerContext } from '../contexts/messengerProvider'
|
||||
|
||||
import type { ChatMessage } from '../models/ChatMessage'
|
||||
|
||||
const ScrollContext = createContext<
|
||||
(msg: ChatMessage, channelId?: string) => void
|
||||
>(() => undefined)
|
||||
|
||||
export function useScrollToMessage() {
|
||||
return useContext(ScrollContext)
|
||||
}
|
||||
|
||||
interface ScrollProviderProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function ScrollProvider({ children }: ScrollProviderProps) {
|
||||
const scrollToDivId = useCallback((id: string) => {
|
||||
const quoteDiv = document.getElementById(id)
|
||||
if (quoteDiv) {
|
||||
quoteDiv.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'center',
|
||||
})
|
||||
quoteDiv.style.background = 'lightblue'
|
||||
quoteDiv.style.transition = 'background-color 1000ms linear'
|
||||
window.setTimeout(() => {
|
||||
quoteDiv.style.background = ''
|
||||
window.setTimeout(() => {
|
||||
quoteDiv.style.transition = ''
|
||||
}, 1000)
|
||||
}, 1000)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const { activeChannel, channelsDispatch } = useMessengerContext()
|
||||
const [scrollToMessage, setScrollToMessage] = useState('')
|
||||
const [messageChannel, setMessageChannel] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollToMessage && messageChannel) {
|
||||
if (activeChannel?.id === messageChannel) {
|
||||
scrollToDivId(scrollToMessage)
|
||||
setScrollToMessage('')
|
||||
setMessageChannel('')
|
||||
}
|
||||
}
|
||||
}, [activeChannel, scrollToMessage, messageChannel, scrollToDivId])
|
||||
const scroll = useCallback(
|
||||
(msg: ChatMessage, channelId?: string) => {
|
||||
if (!channelId || activeChannel?.id === channelId) {
|
||||
scrollToDivId(msg.id)
|
||||
} else {
|
||||
setMessageChannel(channelId)
|
||||
setScrollToMessage(msg.id)
|
||||
channelsDispatch({ type: 'ChangeActive', payload: channelId })
|
||||
}
|
||||
},
|
||||
[scrollToDivId, channelsDispatch, activeChannel]
|
||||
)
|
||||
return (
|
||||
<ScrollContext.Provider value={scroll}>{children}</ScrollContext.Provider>
|
||||
)
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
import React, { createContext, useContext, useState } from 'react'
|
||||
|
||||
import type { Toast } from '../models/Toast'
|
||||
|
||||
const ToastContext = createContext<{
|
||||
toasts: Toast[]
|
||||
setToasts: React.Dispatch<React.SetStateAction<Toast[]>>
|
||||
}>({
|
||||
toasts: [],
|
||||
setToasts: () => undefined,
|
||||
})
|
||||
|
||||
export function useToasts() {
|
||||
return useContext(ToastContext)
|
||||
}
|
||||
|
||||
interface ToastProviderProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function ToastProvider({ children }: ToastProviderProps) {
|
||||
const [toasts, setToasts] = useState<Toast[]>([])
|
||||
return (
|
||||
<ToastContext.Provider value={{ toasts, setToasts }}>
|
||||
{children}
|
||||
</ToastContext.Provider>
|
||||
)
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
import { useReducer } from 'react'
|
||||
|
||||
import type { ChannelData, ChannelsData } from '../../models/ChannelData'
|
||||
|
||||
export type ChannelsState = {
|
||||
channels: ChannelsData
|
||||
activeChannel: ChannelData
|
||||
}
|
||||
|
||||
export type ChannelAction =
|
||||
| { type: 'AddChannel'; payload: ChannelData }
|
||||
| { type: 'UpdateActive'; payload: ChannelData }
|
||||
| { type: 'ChangeActive'; payload: string }
|
||||
| { type: 'ToggleMuted'; payload: string }
|
||||
| { type: 'RemoveChannel'; payload: string }
|
||||
|
||||
function channelReducer(
|
||||
state: ChannelsState,
|
||||
action: ChannelAction
|
||||
): ChannelsState {
|
||||
switch (action.type) {
|
||||
case 'AddChannel': {
|
||||
const channels = {
|
||||
...state.channels,
|
||||
[action.payload.id]: action.payload,
|
||||
}
|
||||
return { channels, activeChannel: action.payload }
|
||||
}
|
||||
case 'UpdateActive': {
|
||||
const activeChannel = state.activeChannel
|
||||
if (activeChannel) {
|
||||
return {
|
||||
channels: { ...state.channels, [activeChannel.id]: action.payload },
|
||||
activeChannel: action.payload,
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
case 'ChangeActive': {
|
||||
const newActive = state.channels[action.payload]
|
||||
if (newActive) {
|
||||
return { ...state, activeChannel: newActive }
|
||||
}
|
||||
return state
|
||||
}
|
||||
case 'ToggleMuted': {
|
||||
const channel = state.channels[action.payload]
|
||||
if (channel) {
|
||||
const updatedChannel: ChannelData = {
|
||||
...channel,
|
||||
isMuted: !channel.isMuted,
|
||||
}
|
||||
return {
|
||||
channels: { ...state.channels, [channel.id]: updatedChannel },
|
||||
activeChannel: updatedChannel,
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
case 'RemoveChannel': {
|
||||
const channelsCopy = { ...state.channels }
|
||||
delete channelsCopy[action.payload]
|
||||
let newActive = { id: '', name: '', type: 'channel' } as ChannelData
|
||||
if (Object.values(channelsCopy).length > 0) {
|
||||
newActive = Object.values(channelsCopy)[0]
|
||||
}
|
||||
return { channels: channelsCopy, activeChannel: newActive }
|
||||
}
|
||||
default:
|
||||
throw new Error()
|
||||
}
|
||||
}
|
||||
|
||||
export function useChannelsReducer() {
|
||||
return useReducer(channelReducer, {
|
||||
channels: {},
|
||||
activeChannel: { id: '', name: '', type: 'channel' },
|
||||
} as ChannelsState)
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
import { useMemo, useReducer, useState } from 'react'
|
||||
|
||||
import { bufToHex, Contacts as ContactsClass } from '@status-im/core'
|
||||
|
||||
import type { Contacts } from '../../models/Contact'
|
||||
import type { Identity, Messenger } from '@status-im/core'
|
||||
|
||||
export type ContactsAction =
|
||||
| { type: 'updateOnline'; payload: { id: string; clock: number } }
|
||||
| { type: 'setTrueName'; payload: { id: string; trueName: string } }
|
||||
| {
|
||||
type: 'setCustomName'
|
||||
payload: { id: string; customName: string | undefined }
|
||||
}
|
||||
| {
|
||||
type: 'setIsUntrustworthy'
|
||||
payload: { id: string; isUntrustworthy: boolean }
|
||||
}
|
||||
| { type: 'setIsFriend'; payload: { id: string; isFriend: boolean } }
|
||||
| { type: 'setBlocked'; payload: { id: string; blocked: boolean } }
|
||||
| { type: 'toggleBlocked'; payload: { id: string } }
|
||||
| { type: 'toggleTrustworthy'; payload: { id: string } }
|
||||
|
||||
function contactsReducer(state: Contacts, action: ContactsAction): Contacts {
|
||||
const id = action.payload.id
|
||||
const prev = state[id]
|
||||
|
||||
switch (action.type) {
|
||||
case 'updateOnline': {
|
||||
const now = Date.now()
|
||||
const clock = action.payload.clock
|
||||
if (prev) {
|
||||
return { ...state, [id]: { ...prev, online: clock > now - 301000 } }
|
||||
}
|
||||
return { ...state, [id]: { id, trueName: id.slice(0, 10) } }
|
||||
}
|
||||
case 'setTrueName': {
|
||||
const trueName = action.payload.trueName
|
||||
if (prev) {
|
||||
return { ...state, [id]: { ...prev, trueName } }
|
||||
}
|
||||
return { ...state, [id]: { id, trueName } }
|
||||
}
|
||||
case 'setCustomName': {
|
||||
const customName = action.payload.customName
|
||||
if (prev) {
|
||||
return { ...state, [id]: { ...prev, customName } }
|
||||
}
|
||||
return state
|
||||
}
|
||||
case 'setIsUntrustworthy': {
|
||||
const isUntrustworthy = action.payload.isUntrustworthy
|
||||
if (prev) {
|
||||
return { ...state, [id]: { ...prev, isUntrustworthy } }
|
||||
}
|
||||
return state
|
||||
}
|
||||
case 'setIsFriend': {
|
||||
const isFriend = action.payload.isFriend
|
||||
if (prev) {
|
||||
return { ...state, [id]: { ...prev, isFriend } }
|
||||
}
|
||||
return state
|
||||
}
|
||||
case 'setBlocked': {
|
||||
const blocked = action.payload.blocked
|
||||
if (prev) {
|
||||
return { ...state, [id]: { ...prev, blocked } }
|
||||
}
|
||||
return state
|
||||
}
|
||||
case 'toggleBlocked': {
|
||||
if (prev) {
|
||||
return { ...state, [id]: { ...prev, blocked: !prev.blocked } }
|
||||
}
|
||||
return state
|
||||
}
|
||||
case 'toggleTrustworthy': {
|
||||
if (prev) {
|
||||
return {
|
||||
...state,
|
||||
[id]: { ...prev, isUntrustworthy: !prev.isUntrustworthy },
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
default:
|
||||
throw new Error()
|
||||
}
|
||||
}
|
||||
|
||||
export function useContacts(
|
||||
messenger: Messenger | undefined,
|
||||
identity: Identity | undefined,
|
||||
newNickname: string | undefined
|
||||
) {
|
||||
const [nickname, setNickname] = useState<string | undefined>(undefined)
|
||||
const [contacts, contactsDispatch] = useReducer(contactsReducer, {})
|
||||
|
||||
const contactsClass = useMemo(() => {
|
||||
if (messenger && messenger.identity === identity) {
|
||||
const newContacts = new ContactsClass(
|
||||
identity,
|
||||
messenger.waku,
|
||||
(id, clock) =>
|
||||
contactsDispatch({ type: 'updateOnline', payload: { id, clock } }),
|
||||
(id, nickname) => {
|
||||
if (identity?.publicKey && id === bufToHex(identity.publicKey)) {
|
||||
setNickname(nickname)
|
||||
}
|
||||
contactsDispatch({
|
||||
type: 'setTrueName',
|
||||
payload: { id, trueName: nickname },
|
||||
})
|
||||
},
|
||||
newNickname
|
||||
)
|
||||
return newContacts
|
||||
}
|
||||
}, [messenger, identity, newNickname])
|
||||
|
||||
return { contacts, contactsDispatch, contactsClass, nickname }
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
import { useCallback, useMemo } from 'react'
|
||||
|
||||
import { GroupChats } from '@status-im/core'
|
||||
|
||||
import { ChatMessage } from '../../models/ChatMessage'
|
||||
import { uintToImgUrl } from '../../utils'
|
||||
|
||||
import type { ChannelData } from '../../models/ChannelData'
|
||||
import type { Contact } from '../../models/Contact'
|
||||
import type { ChannelAction } from './useChannelsReducer'
|
||||
import type {
|
||||
ChatMessage as StatusChatMessage,
|
||||
Contacts as ContactsClass,
|
||||
GroupChat,
|
||||
Identity,
|
||||
Messenger,
|
||||
} from '@status-im/core'
|
||||
|
||||
const contactFromId = (member: string): Contact => {
|
||||
return {
|
||||
blocked: false,
|
||||
id: member,
|
||||
isUntrustworthy: false,
|
||||
online: false,
|
||||
trueName: member,
|
||||
}
|
||||
}
|
||||
|
||||
export function useGroupChats(
|
||||
messenger: Messenger | undefined,
|
||||
identity: Identity | undefined,
|
||||
dispatch: (action: ChannelAction) => void,
|
||||
addChatMessage: (newMessage: ChatMessage | undefined, id: string) => void,
|
||||
contactsClass: ContactsClass | undefined
|
||||
) {
|
||||
const groupChat = useMemo(() => {
|
||||
if (messenger && identity && contactsClass) {
|
||||
const addChat = (chat: GroupChat) => {
|
||||
const members = chat.members.map(member => member.id).map(contactFromId)
|
||||
const channel: ChannelData =
|
||||
chat.members.length > 2
|
||||
? {
|
||||
id: chat.chatId,
|
||||
name: chat.name ?? chat.chatId.slice(0, 10),
|
||||
type: 'group',
|
||||
description: `${chat.members.length} members`,
|
||||
members,
|
||||
}
|
||||
: {
|
||||
id: chat.chatId,
|
||||
name: chat.members[0].id,
|
||||
type: 'dm',
|
||||
description: `Chatkey: ${chat.members[0].id}`,
|
||||
members,
|
||||
}
|
||||
chat.members.forEach(member => contactsClass.addContact(member.id))
|
||||
dispatch({ type: 'AddChannel', payload: channel })
|
||||
}
|
||||
const removeChat = (chat: GroupChat) => {
|
||||
dispatch({ type: 'RemoveChannel', payload: chat.chatId })
|
||||
}
|
||||
const handleMessage = (msg: StatusChatMessage, sender: string) => {
|
||||
let image: string | undefined = undefined
|
||||
if (msg.image) {
|
||||
image = uintToImgUrl(msg.image.payload)
|
||||
}
|
||||
addChatMessage(
|
||||
new ChatMessage(
|
||||
msg.text ?? '',
|
||||
new Date(msg.clock ?? 0),
|
||||
sender,
|
||||
image,
|
||||
msg.responseTo
|
||||
),
|
||||
msg.chatId
|
||||
)
|
||||
}
|
||||
return new GroupChats(
|
||||
identity,
|
||||
messenger.waku,
|
||||
addChat,
|
||||
removeChat,
|
||||
handleMessage
|
||||
)
|
||||
}
|
||||
}, [messenger, identity, contactsClass, addChatMessage, dispatch])
|
||||
|
||||
const createGroupChat = useCallback(
|
||||
(members: string[]) => {
|
||||
if (groupChat) {
|
||||
groupChat.createGroupChat(members)
|
||||
}
|
||||
},
|
||||
[groupChat]
|
||||
)
|
||||
|
||||
const changeGroupChatName = useCallback(
|
||||
(name: string, chatId: string) => {
|
||||
if (groupChat) {
|
||||
groupChat.changeChatName(chatId, name)
|
||||
}
|
||||
},
|
||||
[groupChat]
|
||||
)
|
||||
|
||||
const removeChannel = useCallback(
|
||||
(channelId: string) => {
|
||||
if (groupChat) {
|
||||
groupChat.quitChat(channelId)
|
||||
}
|
||||
},
|
||||
[groupChat]
|
||||
)
|
||||
|
||||
const addMembers = useCallback(
|
||||
(members: string[], chatId: string) => {
|
||||
if (groupChat) {
|
||||
groupChat.addMembers(chatId, members)
|
||||
}
|
||||
},
|
||||
[groupChat]
|
||||
)
|
||||
|
||||
return {
|
||||
createGroupChat,
|
||||
removeChannel,
|
||||
groupChat,
|
||||
changeGroupChatName,
|
||||
addMembers,
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import type { GroupChats, Messenger } from '@status-im/core'
|
||||
|
||||
const _MS_PER_DAY = 1000 * 60 * 60 * 24
|
||||
|
||||
export function useLoadPrevDay(
|
||||
chatId: string,
|
||||
messenger: Messenger | undefined,
|
||||
groupChats?: GroupChats
|
||||
) {
|
||||
const loadingPreviousMessages = useRef<{
|
||||
[chatId: string]: boolean
|
||||
}>({})
|
||||
const lastLoadTime = useRef<{
|
||||
[chatId: string]: Date
|
||||
}>({})
|
||||
const [loadingMessages, setLoadingMessages] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (chatId) {
|
||||
setLoadingMessages(loadingPreviousMessages.current[chatId])
|
||||
}
|
||||
}, [chatId])
|
||||
|
||||
const loadPrevDay = useCallback(
|
||||
async (id: string, groupChat?: boolean) => {
|
||||
if (messenger && id) {
|
||||
const endTime = lastLoadTime.current[id] ?? new Date()
|
||||
const startTime = new Date(endTime.getTime() - _MS_PER_DAY * 5)
|
||||
const timeDiff = Math.floor(
|
||||
(new Date().getTime() - endTime.getTime()) / _MS_PER_DAY
|
||||
)
|
||||
if (timeDiff < 28) {
|
||||
if (!loadingPreviousMessages.current[id]) {
|
||||
loadingPreviousMessages.current[id] = true
|
||||
setLoadingMessages(true)
|
||||
let amountOfMessages = 0
|
||||
let failed = true
|
||||
try {
|
||||
if (groupChat && groupChats) {
|
||||
amountOfMessages = await groupChats.retrievePreviousMessages(
|
||||
id,
|
||||
startTime,
|
||||
endTime
|
||||
)
|
||||
} else {
|
||||
amountOfMessages = await messenger.retrievePreviousMessages(
|
||||
id,
|
||||
startTime,
|
||||
endTime
|
||||
)
|
||||
}
|
||||
lastLoadTime.current[id] = startTime
|
||||
failed = false
|
||||
} catch {
|
||||
failed = true
|
||||
}
|
||||
loadingPreviousMessages.current[id] = false
|
||||
setLoadingMessages(false)
|
||||
if (amountOfMessages === 0 && !failed) {
|
||||
loadPrevDay(id, groupChat)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[messenger, groupChats]
|
||||
)
|
||||
return { loadingMessages, loadPrevDay }
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
import { useCallback, useMemo, useState } from 'react'
|
||||
|
||||
import { bufToHex } from '@status-im/core'
|
||||
|
||||
import { ChatMessage } from '../../models/ChatMessage'
|
||||
import { binarySetInsert } from '../../utils'
|
||||
import { useNotifications } from './useNotifications'
|
||||
|
||||
import type {
|
||||
ApplicationMetadataMessage,
|
||||
Contacts,
|
||||
Identity,
|
||||
} from '@status-im/core'
|
||||
|
||||
export function useMessages(
|
||||
chatId: string,
|
||||
identity: Identity | undefined,
|
||||
subscriptions: React.MutableRefObject<
|
||||
((msg: ChatMessage, id: string) => void)[]
|
||||
>,
|
||||
contacts?: Contacts
|
||||
) {
|
||||
const [messages, setMessages] = useState<{ [chatId: string]: ChatMessage[] }>(
|
||||
{}
|
||||
)
|
||||
const { notifications, incNotification, clearNotifications } =
|
||||
useNotifications()
|
||||
|
||||
const {
|
||||
notifications: mentions,
|
||||
incNotification: incMentions,
|
||||
clearNotifications: clearMentions,
|
||||
} = useNotifications()
|
||||
|
||||
const addChatMessage = useCallback(
|
||||
(newMessage: ChatMessage | undefined, id: string) => {
|
||||
if (newMessage) {
|
||||
contacts?.addContact(newMessage.sender)
|
||||
setMessages(prev => {
|
||||
if (newMessage.responseTo && prev[id]) {
|
||||
newMessage.quote = prev[id].find(
|
||||
msg => msg.id === newMessage.responseTo
|
||||
)
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
[id]: binarySetInsert(
|
||||
prev?.[id] ?? [],
|
||||
newMessage,
|
||||
(a, b) => a.date < b.date,
|
||||
(a, b) => a.date.getTime() === b.date.getTime()
|
||||
),
|
||||
}
|
||||
})
|
||||
subscriptions.current.forEach(subscription =>
|
||||
subscription(newMessage, id)
|
||||
)
|
||||
incNotification(id)
|
||||
if (
|
||||
identity &&
|
||||
newMessage.content.includes(`@${bufToHex(identity.publicKey)}`)
|
||||
) {
|
||||
incMentions(id)
|
||||
}
|
||||
}
|
||||
},
|
||||
[contacts, identity, subscriptions, incMentions, incNotification]
|
||||
)
|
||||
|
||||
const addMessage = useCallback(
|
||||
(msg: ApplicationMetadataMessage, id: string, date: Date) => {
|
||||
const newMessage = ChatMessage.fromMetadataMessage(msg, date)
|
||||
addChatMessage(newMessage, id)
|
||||
},
|
||||
[addChatMessage]
|
||||
)
|
||||
|
||||
const activeMessages = useMemo(() => {
|
||||
if (messages?.[chatId]) {
|
||||
return [...messages[chatId]]
|
||||
}
|
||||
return []
|
||||
}, [messages, chatId])
|
||||
|
||||
return {
|
||||
messages: activeMessages,
|
||||
addMessage,
|
||||
notifications,
|
||||
clearNotifications,
|
||||
mentions,
|
||||
clearMentions,
|
||||
addChatMessage,
|
||||
}
|
||||
}
|
|
@ -1,361 +0,0 @@
|
|||
// import { StoreCodec } from "js-waku";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
import { createCommunity } from '../../utils/createCommunity'
|
||||
import { createMessenger } from '../../utils/createMessenger'
|
||||
import { uintToImgUrl } from '../../utils/uintToImgUrl'
|
||||
import { useChannelsReducer } from './useChannelsReducer'
|
||||
import { useContacts } from './useContacts'
|
||||
import { useGroupChats } from './useGroupChats'
|
||||
import { useLoadPrevDay } from './useLoadPrevDay'
|
||||
import { useMessages } from './useMessages'
|
||||
|
||||
import type { ChannelData, ChannelsData } from '../../models/ChannelData'
|
||||
import type { ChatMessage } from '../../models/ChatMessage'
|
||||
import type { CommunityData } from '../../models/CommunityData'
|
||||
import type { Contacts } from '../../models/Contact'
|
||||
import type { ChannelAction } from './useChannelsReducer'
|
||||
import type { ContactsAction } from './useContacts'
|
||||
import type {
|
||||
ApplicationMetadataMessage,
|
||||
Community,
|
||||
Contacts as ContactsClass,
|
||||
Identity,
|
||||
Messenger,
|
||||
} from '@status-im/core'
|
||||
import type { Environment } from '~/src/types/config'
|
||||
|
||||
export type MessengerType = {
|
||||
messenger: Messenger | undefined
|
||||
messages: ChatMessage[]
|
||||
sendMessage: (
|
||||
messageText?: string | undefined,
|
||||
image?: Uint8Array | undefined,
|
||||
responseTo?: string
|
||||
) => Promise<void>
|
||||
notifications: { [chatId: string]: number }
|
||||
clearNotifications: (id: string) => void
|
||||
mentions: { [chatId: string]: number }
|
||||
clearMentions: (id: string) => void
|
||||
loadPrevDay: (id: string, groupChat?: boolean) => Promise<void>
|
||||
loadingMessages: boolean
|
||||
loadingMessenger: boolean
|
||||
communityData: CommunityData | undefined
|
||||
contacts: Contacts
|
||||
contactsDispatch: (action: ContactsAction) => void
|
||||
addContact: (publicKey: string) => void
|
||||
channels: ChannelsData
|
||||
channelsDispatch: (action: ChannelAction) => void
|
||||
removeChannel: (channelId: string) => void
|
||||
activeChannel: ChannelData | undefined
|
||||
createGroupChat: (members: string[]) => void
|
||||
changeGroupChatName: (name: string, chatId: string) => void
|
||||
addMembers: (members: string[], chatId: string) => void
|
||||
nickname: string | undefined
|
||||
subscriptionsDispatch: (action: SubscriptionAction) => void
|
||||
}
|
||||
|
||||
function useCreateMessenger(
|
||||
environment: Environment,
|
||||
identity: Identity | undefined
|
||||
) {
|
||||
const [messenger, setMessenger] = useState<Messenger | undefined>(undefined)
|
||||
useEffect(() => {
|
||||
createMessenger(identity, environment).then(e => {
|
||||
setMessenger(e)
|
||||
})
|
||||
}, [identity, environment])
|
||||
|
||||
return messenger
|
||||
}
|
||||
|
||||
function useCreateCommunity(
|
||||
messenger: Messenger | undefined,
|
||||
identity: Identity | undefined,
|
||||
communityKey: string | undefined,
|
||||
addMessage: (msg: ApplicationMetadataMessage, id: string, date: Date) => void,
|
||||
contactsClass: ContactsClass | undefined
|
||||
) {
|
||||
const [community, setCommunity] = useState<Community | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
messenger &&
|
||||
communityKey &&
|
||||
contactsClass &&
|
||||
addMessage &&
|
||||
messenger.identity === identity
|
||||
) {
|
||||
createCommunity(communityKey, addMessage, messenger).then(comm => {
|
||||
setCommunity(comm)
|
||||
})
|
||||
}
|
||||
}, [messenger, communityKey, addMessage, contactsClass, identity])
|
||||
|
||||
const communityData = useMemo(() => {
|
||||
if (community?.description) {
|
||||
const membersList = Object.keys(community.description.proto.members)
|
||||
|
||||
if (contactsClass) {
|
||||
membersList.forEach(contactsClass.addContact, contactsClass)
|
||||
}
|
||||
|
||||
return {
|
||||
id: community.publicKeyStr,
|
||||
name: community.description.identity?.displayName ?? '',
|
||||
icon: uintToImgUrl(
|
||||
community.description?.identity?.images?.thumbnail?.payload ??
|
||||
new Uint8Array()
|
||||
),
|
||||
members: membersList.length,
|
||||
membersList,
|
||||
description: community.description.identity?.description ?? '',
|
||||
}
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}, [community, contactsClass])
|
||||
|
||||
return { community, communityData }
|
||||
}
|
||||
|
||||
type Subscriptions = {
|
||||
[id: string]: (msg: ChatMessage, id: string) => void
|
||||
}
|
||||
|
||||
type SubscriptionAction =
|
||||
| {
|
||||
type: 'addSubscription'
|
||||
payload: {
|
||||
name: string
|
||||
subFunction: (msg: ChatMessage, id: string) => void
|
||||
}
|
||||
}
|
||||
| { type: 'removeSubscription'; payload: { name: string } }
|
||||
|
||||
function subscriptionReducer(
|
||||
state: Subscriptions,
|
||||
action: SubscriptionAction
|
||||
): Subscriptions {
|
||||
switch (action.type) {
|
||||
case 'addSubscription': {
|
||||
if (state[action.payload.name]) {
|
||||
throw new Error('Subscription already exists')
|
||||
}
|
||||
return { ...state, [action.payload.name]: action.payload.subFunction }
|
||||
}
|
||||
case 'removeSubscription': {
|
||||
if (state[action.payload.name]) {
|
||||
const newState = { ...state }
|
||||
delete newState[action.payload.name]
|
||||
return newState
|
||||
}
|
||||
return state
|
||||
}
|
||||
default:
|
||||
throw new Error('Wrong subscription action type')
|
||||
}
|
||||
}
|
||||
|
||||
export function useMessenger(
|
||||
communityKey: string,
|
||||
environment: Environment | undefined = 'production',
|
||||
identity: Identity | undefined,
|
||||
newNickname: string | undefined
|
||||
) {
|
||||
const [subscriptions, subscriptionsDispatch] = useReducer(
|
||||
subscriptionReducer,
|
||||
{}
|
||||
)
|
||||
const subList = useRef<((msg: ChatMessage, id: string) => void)[]>([])
|
||||
useEffect(() => {
|
||||
subList.current = Object.values(subscriptions)
|
||||
}, [subscriptions])
|
||||
|
||||
const [channelsState, channelsDispatch] = useChannelsReducer()
|
||||
const messenger = useCreateMessenger(environment, identity)
|
||||
const { contacts, contactsDispatch, contactsClass, nickname } = useContacts(
|
||||
messenger,
|
||||
identity,
|
||||
newNickname
|
||||
)
|
||||
|
||||
const addContact = useCallback(
|
||||
(publicKey: string) => {
|
||||
if (contactsClass) {
|
||||
contactsClass.addContact(publicKey)
|
||||
}
|
||||
},
|
||||
[contactsClass]
|
||||
)
|
||||
|
||||
const {
|
||||
addChatMessage,
|
||||
addMessage,
|
||||
clearNotifications,
|
||||
notifications,
|
||||
messages,
|
||||
mentions,
|
||||
clearMentions,
|
||||
} = useMessages(
|
||||
channelsState?.activeChannel?.id,
|
||||
identity,
|
||||
subList,
|
||||
contactsClass
|
||||
)
|
||||
|
||||
const { community, communityData } = useCreateCommunity(
|
||||
messenger,
|
||||
identity,
|
||||
communityKey,
|
||||
addMessage,
|
||||
contactsClass
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (community?.chats) {
|
||||
for (const chat of community.chats.values()) {
|
||||
channelsDispatch({
|
||||
type: 'AddChannel',
|
||||
payload: {
|
||||
id: chat.id,
|
||||
name: chat.communityChat?.identity?.displayName ?? '',
|
||||
description: chat.communityChat?.identity?.description ?? '',
|
||||
type: 'channel',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [community, channelsDispatch])
|
||||
|
||||
useEffect(() => {
|
||||
Object.values(channelsState.channels)
|
||||
.filter(channel => channel.type === 'dm')
|
||||
.forEach(channel => {
|
||||
const contact = contacts?.[channel?.members?.[1]?.id ?? '']
|
||||
if (
|
||||
contact &&
|
||||
channel.name !== (contact?.customName ?? contact.trueName)
|
||||
) {
|
||||
channelsDispatch({
|
||||
type: 'AddChannel',
|
||||
payload: {
|
||||
...channel,
|
||||
name: contact?.customName ?? contact.trueName,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
}, [contacts, channelsState.channels, channelsDispatch])
|
||||
|
||||
const {
|
||||
groupChat,
|
||||
removeChannel,
|
||||
createGroupChat,
|
||||
changeGroupChatName,
|
||||
addMembers,
|
||||
} = useGroupChats(
|
||||
messenger,
|
||||
identity,
|
||||
channelsDispatch,
|
||||
addChatMessage,
|
||||
contactsClass
|
||||
)
|
||||
|
||||
const { loadPrevDay, loadingMessages } = useLoadPrevDay(
|
||||
channelsState.activeChannel.id,
|
||||
messenger,
|
||||
groupChat
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (messenger && community?.chats) {
|
||||
Array.from(community?.chats.values()).forEach(({ id }) => loadPrevDay(id))
|
||||
}
|
||||
}, [messenger, community, loadPrevDay])
|
||||
|
||||
const sendMessage = useCallback(
|
||||
async (messageText?: string, image?: Uint8Array, responseTo?: string) => {
|
||||
let content
|
||||
if (messageText) {
|
||||
content = {
|
||||
text: messageText,
|
||||
contentType: 0,
|
||||
}
|
||||
}
|
||||
if (image) {
|
||||
content = {
|
||||
image,
|
||||
imageType: 1,
|
||||
contentType: 2,
|
||||
}
|
||||
}
|
||||
if (content) {
|
||||
if (channelsState.activeChannel.type !== 'channel') {
|
||||
await groupChat?.sendMessage(
|
||||
channelsState.activeChannel.id,
|
||||
content,
|
||||
responseTo
|
||||
)
|
||||
} else {
|
||||
await messenger?.sendMessage(
|
||||
channelsState.activeChannel.id,
|
||||
content,
|
||||
responseTo
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
[messenger, groupChat, channelsState.activeChannel]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (channelsState.activeChannel) {
|
||||
if (notifications[channelsState.activeChannel.id] > 0) {
|
||||
clearNotifications(channelsState.activeChannel.id)
|
||||
clearMentions(channelsState.activeChannel.id)
|
||||
}
|
||||
}
|
||||
}, [notifications, channelsState, clearNotifications, clearMentions])
|
||||
|
||||
const loadingMessenger = useMemo(() => {
|
||||
return Boolean(
|
||||
(communityKey && !communityData) ||
|
||||
!messenger ||
|
||||
(communityKey && !channelsState.activeChannel.id)
|
||||
)
|
||||
}, [communityData, messenger, channelsState, communityKey])
|
||||
|
||||
return {
|
||||
messenger,
|
||||
messages,
|
||||
sendMessage,
|
||||
notifications,
|
||||
clearNotifications,
|
||||
loadPrevDay,
|
||||
loadingMessages,
|
||||
loadingMessenger,
|
||||
communityData,
|
||||
contacts,
|
||||
contactsDispatch,
|
||||
addContact,
|
||||
channels: channelsState.channels,
|
||||
channelsDispatch,
|
||||
removeChannel,
|
||||
activeChannel: channelsState.activeChannel,
|
||||
mentions,
|
||||
clearMentions,
|
||||
createGroupChat,
|
||||
changeGroupChatName,
|
||||
addMembers,
|
||||
nickname,
|
||||
subscriptionsDispatch,
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
import { useCallback, useState } from 'react'
|
||||
|
||||
export function useNotifications() {
|
||||
const [notifications, setNotifications] = useState<{
|
||||
[chatId: string]: number
|
||||
}>({})
|
||||
const incNotification = useCallback((id: string) => {
|
||||
setNotifications(prevNotifications => {
|
||||
return {
|
||||
...prevNotifications,
|
||||
[id]: (prevNotifications?.[id] ?? 0) + 1,
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
const clearNotifications = useCallback((id: string) => {
|
||||
setNotifications(prevNotifications => {
|
||||
return {
|
||||
...prevNotifications,
|
||||
[id]: 0,
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
return { notifications, incNotification, clearNotifications }
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
import { useCallback, useEffect } from 'react'
|
||||
|
||||
import type { RefObject } from 'react'
|
||||
|
||||
export const useClickOutside = (
|
||||
ref: RefObject<HTMLDivElement>,
|
||||
callback: () => void
|
||||
) => {
|
||||
const handleClick = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as HTMLInputElement)) {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
[ref, callback]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClick)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClick)
|
||||
}
|
||||
}, [handleClick])
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import type { RefObject } from 'react'
|
||||
|
||||
export const useClickPosition = (ref: RefObject<HTMLDivElement>) => {
|
||||
const [topPosition, setTopPosition] = useState(0)
|
||||
const [leftPosition, setLeftPosition] = useState(0)
|
||||
|
||||
const getPosition = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (ref.current) {
|
||||
const target = e.target as HTMLImageElement
|
||||
const imgTarget = target.tagName === 'IMG'
|
||||
const rect = ref.current.getBoundingClientRect()
|
||||
const x = ref.current.clientWidth - e.clientX < 180 ? 180 : 0
|
||||
setLeftPosition(imgTarget ? -200 : e.clientX - rect.left - x)
|
||||
setTopPosition(imgTarget ? 0 : e.clientY - rect.top)
|
||||
}
|
||||
},
|
||||
[setTopPosition, setLeftPosition, ref]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('contextmenu', getPosition)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('contextmenu', getPosition)
|
||||
}
|
||||
})
|
||||
|
||||
return { topPosition, leftPosition }
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
export const useContextMenu = (elementId: string) => {
|
||||
const [showMenu, setShowMenu] = useState(false)
|
||||
|
||||
const handleContextMenu = useCallback(
|
||||
event => {
|
||||
event.preventDefault()
|
||||
setShowMenu(true)
|
||||
},
|
||||
[setShowMenu]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const element = document.getElementById(elementId) || document
|
||||
|
||||
element.addEventListener('contextmenu', handleContextMenu)
|
||||
document.addEventListener('click', () => setShowMenu(false))
|
||||
return () => {
|
||||
element.removeEventListener('contextmenu', handleContextMenu)
|
||||
document.removeEventListener('click', () => setShowMenu(false))
|
||||
setShowMenu(false)
|
||||
}
|
||||
}, [elementId, handleContextMenu])
|
||||
|
||||
return { showMenu, setShowMenu }
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
export type Reply = {
|
||||
sender: string
|
||||
content: string
|
||||
image?: string
|
||||
id: string
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
export type { CommunityProps } from './modules/community'
|
||||
export { Community } from './modules/community'
|
||||
export type { CommunityProps } from './routes'
|
||||
export { Community } from './routes'
|
||||
export { darkTheme, theme as lightTheme } from './styles/config'
|
||||
export type { Config } from './types/config'
|
||||
export { HashRouter, MemoryRouter } from 'react-router-dom'
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
import type { ChannelData } from './ChannelData'
|
||||
import type { ChatMessage } from './ChatMessage'
|
||||
import type { CommunityData } from './CommunityData'
|
||||
|
||||
export type ActivityStatus = 'sent' | 'accepted' | 'declined' | 'blocked'
|
||||
|
||||
export type Activity =
|
||||
| {
|
||||
id: string
|
||||
type: 'mention'
|
||||
date: Date
|
||||
user: string
|
||||
message: ChatMessage
|
||||
channel: ChannelData
|
||||
isRead?: boolean
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: 'reply'
|
||||
date: Date
|
||||
user: string
|
||||
message: ChatMessage
|
||||
channel: ChannelData
|
||||
quote: ChatMessage
|
||||
isRead?: boolean
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: 'request'
|
||||
date: Date
|
||||
user: string
|
||||
isRead?: boolean
|
||||
request: string
|
||||
requestType: 'outcome' | 'income'
|
||||
status: ActivityStatus
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: 'invitation'
|
||||
isRead?: boolean
|
||||
date: Date
|
||||
user: string
|
||||
status: ActivityStatus
|
||||
invitation?: CommunityData
|
||||
}
|
||||
|
||||
export type Activities = {
|
||||
[id: string]: Activity
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import type { Contact } from './Contact'
|
||||
|
||||
export type ChannelData = {
|
||||
id: string
|
||||
name: string
|
||||
type: 'channel' | 'dm' | 'group'
|
||||
description?: string
|
||||
icon?: string
|
||||
isMuted?: boolean
|
||||
members?: Contact[]
|
||||
}
|
||||
|
||||
export type ChannelsData = {
|
||||
[id: string]: ChannelData
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
import { bufToHex } from '@status-im/core'
|
||||
import { keccak256 } from 'js-sha3'
|
||||
|
||||
import { uintToImgUrl } from '../utils'
|
||||
|
||||
import type { ApplicationMetadataMessage } from '@status-im/core'
|
||||
|
||||
export class ChatMessage {
|
||||
content: string
|
||||
date: Date
|
||||
sender: string
|
||||
image?: string
|
||||
responseTo?: string
|
||||
quote?: ChatMessage
|
||||
id: string
|
||||
|
||||
constructor(
|
||||
content: string,
|
||||
date: Date,
|
||||
sender: string,
|
||||
image?: string,
|
||||
responseTo?: string
|
||||
) {
|
||||
this.content = content
|
||||
this.date = date
|
||||
this.sender = sender
|
||||
this.image = image
|
||||
this.responseTo = responseTo
|
||||
this.id = keccak256(date.getTime().toString() + content)
|
||||
}
|
||||
|
||||
public static fromMetadataMessage(
|
||||
msg: ApplicationMetadataMessage,
|
||||
date: Date
|
||||
) {
|
||||
if (
|
||||
msg.signer &&
|
||||
(msg.chatMessage?.text || msg.chatMessage?.image) &&
|
||||
msg.chatMessage.clock
|
||||
) {
|
||||
const content = msg.chatMessage.text ?? ''
|
||||
let image: string | undefined = undefined
|
||||
if (msg.chatMessage?.image) {
|
||||
image = uintToImgUrl(msg.chatMessage?.image.payload)
|
||||
}
|
||||
const sender = bufToHex(msg.signer)
|
||||
return new ChatMessage(
|
||||
content,
|
||||
date,
|
||||
sender,
|
||||
image,
|
||||
msg.chatMessage.responseTo
|
||||
)
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
export type CommunityData = {
|
||||
id: string
|
||||
name: string
|
||||
icon: string
|
||||
members: number
|
||||
membersList: string[]
|
||||
description: string
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
export type Contact = {
|
||||
id: string
|
||||
online?: boolean
|
||||
trueName: string
|
||||
customName?: string
|
||||
isUntrustworthy?: boolean
|
||||
blocked?: boolean
|
||||
isFriend?: boolean
|
||||
}
|
||||
|
||||
export type Contacts = {
|
||||
[id: string]: Contact
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export interface Metadata {
|
||||
'og:site_name': string
|
||||
'og:title': string
|
||||
'og:image': string
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
export type Toast = {
|
||||
id: string
|
||||
type: 'confirmation' | 'incoming' | 'approvement' | 'rejection'
|
||||
text: string
|
||||
request?: string
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
import React, { useRef } from 'react'
|
||||
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
|
||||
import { AppProvider } from '~/src/contexts/app-context'
|
||||
import { ChatStateProvider } from '~/src/contexts/chatStateProvider'
|
||||
import { DialogProvider } from '~/src/contexts/dialog-context'
|
||||
import { IdentityProvider } from '~/src/contexts/identityProvider'
|
||||
import { MessengerProvider } from '~/src/contexts/messengerProvider'
|
||||
import { ModalProvider } from '~/src/contexts/modalProvider'
|
||||
import { NarrowProvider } from '~/src/contexts/narrowProvider'
|
||||
import { ScrollProvider } from '~/src/contexts/scrollProvider'
|
||||
import { ThemeProvider } from '~/src/contexts/theme-context'
|
||||
import { ToastProvider } from '~/src/contexts/toastProvider'
|
||||
import { styled } from '~/src/styles/config'
|
||||
import { GlobalStyle } from '~/src/styles/GlobalStyle'
|
||||
|
||||
import { Messenger } from './messenger'
|
||||
|
||||
import type { Config } from '~/src/types/config'
|
||||
|
||||
type Props = Config
|
||||
|
||||
export const Community = (props: Props) => {
|
||||
const {
|
||||
theme,
|
||||
environment,
|
||||
publicKey,
|
||||
router: Router = BrowserRouter,
|
||||
} = props
|
||||
|
||||
const ref = useRef<HTMLHeadingElement>(null)
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<AppProvider config={props}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<DialogProvider>
|
||||
<NarrowProvider myRef={ref}>
|
||||
<ModalProvider>
|
||||
<ToastProvider>
|
||||
<Wrapper ref={ref}>
|
||||
<GlobalStyle />
|
||||
<IdentityProvider>
|
||||
<MessengerProvider
|
||||
publicKey={publicKey}
|
||||
environment={environment}
|
||||
>
|
||||
<ChatStateProvider>
|
||||
<ScrollProvider>
|
||||
<Messenger />
|
||||
<div id="portal" data-radix-portal />
|
||||
</ScrollProvider>
|
||||
</ChatStateProvider>
|
||||
</MessengerProvider>
|
||||
</IdentityProvider>
|
||||
</Wrapper>
|
||||
</ToastProvider>
|
||||
</ModalProvider>
|
||||
</NarrowProvider>
|
||||
</DialogProvider>
|
||||
</ThemeProvider>
|
||||
</AppProvider>
|
||||
</Router>
|
||||
)
|
||||
}
|
||||
|
||||
export type { Props as CommunityProps }
|
||||
|
||||
const Wrapper = styled('div', {
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
})
|
|
@ -1,32 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import { Route, Routes } from 'react-router-dom'
|
||||
|
||||
import { MainSidebar } from '~/src/components/main-sidebar'
|
||||
import { useAppState } from '~/src/contexts/app-context'
|
||||
import { Chat } from '~/src/routes/chat'
|
||||
import { NewChat } from '~/src/routes/new-chat'
|
||||
import { styled } from '~/src/styles/config'
|
||||
|
||||
export function Messenger() {
|
||||
const { options } = useAppState()
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
{options.enableMembers && <MainSidebar />}
|
||||
<Routes>
|
||||
<Route path="/:id" element={<Chat />} />
|
||||
<Route path="/new" element={<NewChat />} />
|
||||
</Routes>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const Wrapper = styled('div', {
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'stretch',
|
||||
background: '$background',
|
||||
})
|
|
@ -2,43 +2,37 @@ import React from 'react'
|
|||
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom'
|
||||
|
||||
import { MainSidebar } from '~/src/components/main-sidebar'
|
||||
import { AppProvider } from '~/src/contexts/app-context'
|
||||
import { DialogProvider } from '~/src/contexts/dialog-context'
|
||||
import { ThemeProvider } from '~/src/contexts/theme-context'
|
||||
import { Chat } from '~/src/routes/chat'
|
||||
import { NewChat } from '~/src/routes/new-chat'
|
||||
import { styled } from '~/src/styles/config'
|
||||
|
||||
import { MainSidebar } from '../components/main-sidebar'
|
||||
import { Box } from '../system'
|
||||
import { Chat } from './chat'
|
||||
import { NewChat } from './new-chat'
|
||||
import { GlobalStyle } from '~/src/styles/GlobalStyle'
|
||||
|
||||
import type { Config } from '~/src/types/config'
|
||||
|
||||
type Props = Config
|
||||
|
||||
export const Community = (props: Props) => {
|
||||
const {
|
||||
// theme,
|
||||
// environment,
|
||||
// publicKey,
|
||||
router: Router = BrowserRouter,
|
||||
} = props
|
||||
|
||||
const { options } = props
|
||||
const { theme, router: Router = BrowserRouter } = props
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<AppProvider config={props}>
|
||||
<DialogProvider>
|
||||
<Box css={{ flex: '1 0 100%' }}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<DialogProvider>
|
||||
<GlobalStyle />
|
||||
<Wrapper>
|
||||
{options.enableMembers && <MainSidebar />}
|
||||
<MainSidebar />
|
||||
<Routes>
|
||||
<Route path="/:id" element={<Chat />} />
|
||||
<Route path="/new" element={<NewChat />} />
|
||||
</Routes>
|
||||
</Wrapper>
|
||||
</Box>
|
||||
</DialogProvider>
|
||||
</DialogProvider>
|
||||
</ThemeProvider>
|
||||
</AppProvider>
|
||||
</Router>
|
||||
)
|
||||
|
@ -47,6 +41,7 @@ export const Community = (props: Props) => {
|
|||
export type { Props as CommunityProps }
|
||||
|
||||
const Wrapper = styled('div', {
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
|
||||
import { BellIcon } from '~/src/icons/bell-icon'
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
export const copy = (text: string) => {
|
||||
navigator.clipboard.writeText(text).catch(error => {
|
||||
console.log(error)
|
||||
})
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
import { Community } from '@status-im/core'
|
||||
|
||||
import type { ApplicationMetadataMessage, Messenger } from '@status-im/core'
|
||||
|
||||
export async function createCommunity(
|
||||
communityKey: string,
|
||||
addMessage: (msg: ApplicationMetadataMessage, id: string, date: Date) => void,
|
||||
messenger: Messenger
|
||||
) {
|
||||
const community = await Community.instantiateCommunity(
|
||||
communityKey,
|
||||
messenger.waku
|
||||
)
|
||||
await Promise.all(
|
||||
Array.from(community.chats.values()).map(async chat => {
|
||||
await messenger.joinChat(chat)
|
||||
messenger.addObserver(
|
||||
(msg, date) => addMessage(msg, chat.id, date),
|
||||
chat.id
|
||||
)
|
||||
})
|
||||
)
|
||||
return community
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
import { Messenger } from '@status-im/core'
|
||||
import { getPredefinedBootstrapNodes } from 'js-waku'
|
||||
import { Fleet } from 'js-waku/build/main/lib/discovery/predefined'
|
||||
import { Protocols } from 'js-waku/build/main/lib/waku'
|
||||
|
||||
import type { Environment } from '../types/config'
|
||||
import type { Identity } from '@status-im/core'
|
||||
import type { CreateOptions } from 'js-waku/build/main/lib/waku'
|
||||
|
||||
function createWakuOptions(env: Environment): CreateOptions {
|
||||
let bootstrap: CreateOptions['bootstrap'] = {
|
||||
default: true,
|
||||
}
|
||||
|
||||
if (env === 'test') {
|
||||
bootstrap = {
|
||||
peers: getPredefinedBootstrapNodes(Fleet.Test).map(a => a.toString()),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
bootstrap,
|
||||
libp2p: {
|
||||
config: {
|
||||
pubsub: {
|
||||
enabled: true,
|
||||
emitSelf: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function createMessenger(
|
||||
identity: Identity | undefined,
|
||||
env: Environment
|
||||
) {
|
||||
const wakuOptions = createWakuOptions(env)
|
||||
const messenger = await Messenger.create(identity, wakuOptions)
|
||||
await messenger.waku.waitForRemotePeer([Protocols.Store])
|
||||
|
||||
return messenger
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
export const downloadImg = async (image: string) => {
|
||||
try {
|
||||
const a = document.createElement('a')
|
||||
a.download = `${image.split('/').pop()}.png`
|
||||
a.href = image
|
||||
a.click()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
export { binarySetInsert } from './binarySetInsert'
|
||||
export { copy } from './copy'
|
||||
export { copyImg } from './copyImg'
|
||||
export { downloadImg } from './downloadImg'
|
||||
export { equalDate } from './equalDate'
|
||||
export {
|
||||
decryptIdentity,
|
||||
|
|
Loading…
Reference in New Issue