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:
Pavel 2022-04-18 16:20:31 +02:00 committed by GitHub
parent a781ef4f7c
commit 5cd02e8d64
No known key found for this signature in database
GPG Key ID: 0EB8D75C775AB6F1
91 changed files with 23 additions and 7512 deletions

View File

@ -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;
}
`

View File

@ -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};
}
}
`

View File

@ -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}
`

View File

@ -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;
}
`

View File

@ -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;
}
`

View File

@ -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};
}
`

View File

@ -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;
}
`

View File

@ -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;
`

View File

@ -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;
}
`

View File

@ -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};
`

View File

@ -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};
`

View File

@ -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;
}
`

View File

@ -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;
`

View File

@ -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
}

View File

@ -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};
`

View File

@ -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;
}
`

View File

@ -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;
`

View File

@ -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>
)

View File

@ -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;
}
`

View File

@ -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;
`

View File

@ -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;
`

View File

@ -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;
`

View File

@ -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;
}
`

View File

@ -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;
`

View File

@ -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;
`

View File

@ -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;
`

View File

@ -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};
`

View File

@ -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};
}
`

View File

@ -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};
}
`

View File

@ -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;
`

View File

@ -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;
`

View File

@ -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;
`

View File

@ -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};
`

View File

@ -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;
`

View File

@ -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};
}
`

View File

@ -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;
}
`

View File

@ -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;
}
`

View File

@ -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;
`

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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;
`

View File

@ -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;
}
}
`

View File

@ -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};
}
`

View File

@ -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;
`

View File

@ -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};
}
`

View File

@ -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;
`

View File

@ -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}
`

View File

@ -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;
`

View File

@ -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;
`

View File

@ -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;
}
`

View File

@ -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;
`

View File

@ -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;
`

View File

@ -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;
`

View File

@ -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};
`

View File

@ -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}
`

View File

@ -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 />

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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)
}

View File

@ -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 }
}

View File

@ -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,
}
}

View File

@ -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 }
}

View File

@ -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,
}
}

View File

@ -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,
}
}

View File

@ -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 }
}

View File

@ -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])
}

View File

@ -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 }
}

View File

@ -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 }
}

View File

@ -1,6 +0,0 @@
export type Reply = {
sender: string
content: string
image?: string
id: string
}

View File

@ -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'

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}
}

View File

@ -1,8 +0,0 @@
export type CommunityData = {
id: string
name: string
icon: string
members: number
membersList: string[]
description: string
}

View File

@ -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
}

View File

@ -1,5 +0,0 @@
export interface Metadata {
'og:site_name': string
'og:title': string
'og:image': string
}

View File

@ -1,6 +0,0 @@
export type Toast = {
id: string
type: 'confirmation' | 'incoming' | 'approvement' | 'rejection'
text: string
request?: string
}

View File

@ -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',
})

View File

@ -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',
})

View File

@ -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%',

View File

@ -1,3 +1,4 @@
// @ts-nocheck
import React, { useCallback, useMemo, useState } from 'react'
import { BellIcon } from '~/src/icons/bell-icon'

View File

@ -1,5 +0,0 @@
export const copy = (text: string) => {
navigator.clipboard.writeText(text).catch(error => {
console.log(error)
})
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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,