Fetch history (#292)
This commit is contained in:
parent
213ca26877
commit
95dc03b99f
|
@ -240,9 +240,10 @@ export class Chat {
|
|||
return
|
||||
}
|
||||
|
||||
if (!this.#messages.size) {
|
||||
return
|
||||
}
|
||||
// fixme?: to stop the loading we need to let the listeners know even if there are no messages
|
||||
// if (!this.#messages.size) {
|
||||
// return
|
||||
// }
|
||||
|
||||
const messages = this.getMessages()
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ import { containsOnlyEmoji } from './contains-only-emoji'
|
|||
test('should be truthy', () => {
|
||||
expect(containsOnlyEmoji('💩')).toBeTruthy()
|
||||
expect(containsOnlyEmoji('💩💩💩💩💩💩')).toBeTruthy()
|
||||
// expect(containsOnlyEmoji('1️⃣')).toBeTruthy()
|
||||
// expect(containsOnlyEmoji('👨👩👧')).toBeTruthy()
|
||||
})
|
||||
|
||||
test('should be falsy', () => {
|
||||
|
@ -14,4 +16,7 @@ test('should be falsy', () => {
|
|||
expect(containsOnlyEmoji('💩 ')).toBeFalsy()
|
||||
expect(containsOnlyEmoji('text 💩')).toBeFalsy()
|
||||
expect(containsOnlyEmoji('💩 text')).toBeFalsy()
|
||||
expect(containsOnlyEmoji('123')).toBeFalsy()
|
||||
expect(containsOnlyEmoji('💩 123')).toBeFalsy()
|
||||
expect(containsOnlyEmoji('123 💩💩💩 ')).toBeFalsy()
|
||||
})
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
// todo?: should ignore whitespaces with replace(/\s+/g, '').trim()
|
||||
/**
|
||||
* https://www.unicode.org/reports/tr51/#def_emoji_presentation
|
||||
*/
|
||||
export function containsOnlyEmoji(text: string): boolean {
|
||||
return /^\p{Emoji}+$/gu.test(text)
|
||||
return /^\p{Emoji_Presentation}+$/gu.test(text)
|
||||
}
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
"@radix-ui/react-label": "^0.1.5",
|
||||
"@radix-ui/react-popover": "^0.1.6",
|
||||
"@radix-ui/react-separator": "^0.1.4",
|
||||
"@radix-ui/react-toast": "^0.1.1",
|
||||
"@radix-ui/react-tabs": "^1.0.0",
|
||||
"@radix-ui/react-toggle-group": "^0.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.0.0",
|
||||
|
|
|
@ -56,17 +56,7 @@ export const CommunityDialog = () => {
|
|||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<Button
|
||||
onClick={() =>
|
||||
window.open(
|
||||
'https://status.im/get',
|
||||
'_blank',
|
||||
'noopener,noreferrer'
|
||||
)
|
||||
}
|
||||
>
|
||||
Download Status
|
||||
</Button>
|
||||
<Button href="https://status.im/get">Download Status</Button>
|
||||
</Flex>
|
||||
</Dialog.Body>
|
||||
</Dialog>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export { ProtocolProvider, useProtocol } from './provider'
|
||||
export { ProtocolProvider } from './provider'
|
||||
export type { Account } from './use-account'
|
||||
export { useAccount } from './use-account'
|
||||
export { useActivityCenter } from './use-activity-center'
|
||||
|
@ -8,4 +8,5 @@ export type { Member } from './use-members'
|
|||
export { useMembers } from './use-members'
|
||||
export type { Message, Reaction, Reactions } from './use-messages'
|
||||
export { useMessages } from './use-messages'
|
||||
export { useProtocol } from './use-protocol'
|
||||
export { useSortedChats } from './use-sorted-chats'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { createContext, useContext, useEffect, useReducer } from 'react'
|
||||
import React, { createContext, useEffect, useReducer } from 'react'
|
||||
|
||||
import { createClient } from '@status-im/js'
|
||||
|
||||
|
@ -6,9 +6,9 @@ import { Loading } from '../components/loading'
|
|||
|
||||
import type { Account, Client, ClientOptions, Community } from '@status-im/js'
|
||||
|
||||
const Context = createContext<State | undefined>(undefined)
|
||||
export const Context = createContext<State | undefined>(undefined)
|
||||
|
||||
type State = {
|
||||
export type State = {
|
||||
loading: boolean
|
||||
client: Client | undefined
|
||||
community: Community['description'] | undefined
|
||||
|
@ -16,7 +16,7 @@ type State = {
|
|||
dispatch?: React.Dispatch<Action>
|
||||
}
|
||||
|
||||
type Action =
|
||||
export type Action =
|
||||
| { type: 'INIT'; client: Client }
|
||||
| { type: 'UPDATE_COMMUNITY'; community: Community['description'] }
|
||||
| { type: 'SET_ACCOUNT'; account: Account | undefined }
|
||||
|
@ -99,18 +99,3 @@ export const ProtocolProvider = (props: Props) => {
|
|||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useProtocol() {
|
||||
const context = useContext(Context)
|
||||
|
||||
if (!context) {
|
||||
throw new Error(`useProtocol must be used within a ProtocolProvider`)
|
||||
}
|
||||
|
||||
// we enforce initialization of client before rendering children
|
||||
return context as State & {
|
||||
client: Client
|
||||
community: Community['description']
|
||||
dispatch: React.Dispatch<Action>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useProtocol } from './provider'
|
||||
import { useProtocol } from './use-protocol'
|
||||
|
||||
import type { Account } from '@status-im/js'
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useMatch } from 'react-router-dom'
|
||||
|
||||
import { useProtocol } from './provider'
|
||||
import { useProtocol } from './use-protocol'
|
||||
|
||||
export const useActiveChat = () => {
|
||||
const { client } = useProtocol()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { useProtocol } from './provider'
|
||||
import { useProtocol } from './use-protocol'
|
||||
|
||||
import type { ActivityCenterLatest } from '@status-im/js'
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useMemo } from 'react'
|
||||
|
||||
import { useProtocol } from './provider'
|
||||
import { useProtocol } from './use-protocol'
|
||||
|
||||
import type { Community } from '@status-im/js'
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useProtocol } from './provider'
|
||||
import { useProtocol } from './use-protocol'
|
||||
|
||||
import type { Member } from '@status-im/js'
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { useProtocol } from './provider'
|
||||
import sub from 'date-fns/sub'
|
||||
|
||||
import { useProtocol } from './use-protocol'
|
||||
|
||||
import type { Message, Reactions } from '@status-im/js'
|
||||
|
||||
|
@ -9,21 +11,20 @@ type Reaction = keyof Reactions
|
|||
interface Result {
|
||||
data: Message[]
|
||||
loading: boolean
|
||||
// error?: Error
|
||||
// fetchMore: () => void
|
||||
}
|
||||
|
||||
export const useMessages = (channelId: string): Result => {
|
||||
export const useMessages = (chatId: string): Result => {
|
||||
const { client } = useProtocol()
|
||||
|
||||
const chat = client.community.chats.get(channelId)!
|
||||
// const [state, dispatch] = useReducer<Result>((state,action) => {}, {})
|
||||
const chat = client.community.chats.get(chatId)!
|
||||
|
||||
const [data, setData] = useState<Message[]>(() => chat.getMessages())
|
||||
const [loading, setLoading] = useState(true)
|
||||
// const [error, setError] = useState<Error>()
|
||||
|
||||
useEffect(() => {
|
||||
const messages = chat.getMessages()
|
||||
|
||||
setData(chat.getMessages())
|
||||
|
||||
const handleUpdate = (messages: Message[]) => {
|
||||
|
@ -31,15 +32,21 @@ export const useMessages = (channelId: string): Result => {
|
|||
setData(messages)
|
||||
}
|
||||
|
||||
if (messages.length === 0) {
|
||||
setLoading(true)
|
||||
chat.fetchMessages({ start: sub(new Date(), { days: 30 }) })
|
||||
}
|
||||
|
||||
return chat.onMessage(handleUpdate)
|
||||
}, [chat])
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
// fetchMore,
|
||||
// fetching,
|
||||
// error,
|
||||
// hasMore
|
||||
// fetchMore,
|
||||
// refetch
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { useContext } from 'react'
|
||||
|
||||
import { Context } from './provider'
|
||||
|
||||
import type { Action, State } from './provider'
|
||||
import type { Client, Community } from '@status-im/js'
|
||||
import type React from 'react'
|
||||
|
||||
export function useProtocol() {
|
||||
const context = useContext(Context)
|
||||
|
||||
if (!context) {
|
||||
throw new Error(`useProtocol must be used within a ProtocolProvider`)
|
||||
}
|
||||
|
||||
// we enforce initialization of client before rendering children
|
||||
return context as State & {
|
||||
client: Client
|
||||
community: Community['description']
|
||||
dispatch: React.Dispatch<Action>
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { useMemo } from 'react'
|
||||
|
||||
import { useProtocol } from './provider'
|
||||
import { useProtocol } from './use-protocol'
|
||||
|
||||
import type { Community } from '@status-im/js'
|
||||
|
||||
|
|
|
@ -82,7 +82,8 @@ const Wrapper = styled('div', {
|
|||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
alignItems: 'flex-end',
|
||||
padding: '12px 8px 12px 4px',
|
||||
// padding: '12px 8px 12px 4px',
|
||||
padding: '12px 8px 12px 8px',
|
||||
gap: 4,
|
||||
})
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ import type { Message, Reaction } from '../../../../protocol'
|
|||
|
||||
interface Props {
|
||||
message: Message
|
||||
prevMessage?: Message
|
||||
collapse: boolean
|
||||
highlight?: boolean
|
||||
}
|
||||
|
||||
|
@ -57,16 +57,17 @@ interface Props {
|
|||
// })
|
||||
|
||||
export const ChatMessage = (props: Props) => {
|
||||
const { message, collapse, highlight } = props
|
||||
|
||||
const { client, account } = useProtocol()
|
||||
const { params } = useMatch(':id')!
|
||||
|
||||
const chatId = params.id!
|
||||
const { message, highlight } = props
|
||||
|
||||
const mention = false
|
||||
const pinned = false
|
||||
|
||||
const { messageId, contentType, clock, reactions, signer, responseTo } =
|
||||
const { messageId, contentType, timestamp, reactions, signer, responseTo } =
|
||||
message
|
||||
|
||||
// TODO: remove usage of 0x prefix
|
||||
|
@ -74,6 +75,7 @@ export const ChatMessage = (props: Props) => {
|
|||
const chat = client.community.getChat(chatId)!
|
||||
|
||||
const member = client.community.getMember(signer)!
|
||||
const response = client.community.getChat(params.id!)!.getMessage(responseTo)
|
||||
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [reacting, setReacting] = useState(false)
|
||||
|
@ -82,6 +84,7 @@ export const ChatMessage = (props: Props) => {
|
|||
|
||||
// const userProfileDialog = useDialog(UserProfileDialog)
|
||||
|
||||
// TODO: fix saving of edited message
|
||||
const handleMessageSubmit = (message: string) => {
|
||||
chat.sendTextMessage(message)
|
||||
}
|
||||
|
@ -107,7 +110,7 @@ export const ChatMessage = (props: Props) => {
|
|||
// TODO: pin message
|
||||
}
|
||||
|
||||
const renderMessage = () => {
|
||||
const renderContent = () => {
|
||||
if (editing) {
|
||||
return (
|
||||
<Box>
|
||||
|
@ -170,54 +173,28 @@ export const ChatMessage = (props: Props) => {
|
|||
}
|
||||
}
|
||||
|
||||
const renderMessage = () => {
|
||||
if (collapse) {
|
||||
return (
|
||||
<Box css={{ flex: 1, paddingLeft: 52 }}>
|
||||
{renderContent()}
|
||||
<MessageReactions reactions={reactions} onClick={handleReaction} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <ContextMenuTrigger> */}
|
||||
<Wrapper
|
||||
mention={mention}
|
||||
pinned={pinned}
|
||||
data-active={reacting}
|
||||
highlight={highlight}
|
||||
>
|
||||
{responseTo && <MessageReply messageId={responseTo} />}
|
||||
<Flex gap={2}>
|
||||
<Box>
|
||||
{/* <DropdownMenuTrigger>
|
||||
<button type="button"> */}
|
||||
<Avatar
|
||||
size={44}
|
||||
name={member!.username}
|
||||
colorHash={member!.colorHash}
|
||||
/>
|
||||
{/* </button> */}
|
||||
{/* <DropdownMenu>
|
||||
<Flex direction="column" align="center" gap="1">
|
||||
<Avatar size="36" />
|
||||
<Text>{member!.username}</Text>
|
||||
<EmojiHash />
|
||||
</Flex>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
icon={<BellIcon />}
|
||||
onSelect={() => userProfileDialog.open({ member })}
|
||||
>
|
||||
View Profile
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item icon={<BellIcon />}>
|
||||
Send Message
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item icon={<BellIcon />}>
|
||||
Verify Identity
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item icon={<BellIcon />}>
|
||||
Send Contact Request
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item icon={<BellIcon />} danger>
|
||||
Mark as Untrustworthy
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu>
|
||||
</DropdownMenuTrigger> */}
|
||||
</Box>
|
||||
|
||||
<Box css={{ flex: 1 }}>
|
||||
{/* {pinned && (
|
||||
</Box>
|
||||
|
||||
<Box css={{ flex: 1 }}>
|
||||
|
@ -233,19 +210,34 @@ export const ChatMessage = (props: Props) => {
|
|||
{member!.username}
|
||||
</Text>
|
||||
<Text size="10" color="gray">
|
||||
{new Date(Number(clock)).toLocaleTimeString([], {
|
||||
{new Date(Number(timestamp)).toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
{renderMessage()}
|
||||
{renderContent()}
|
||||
|
||||
<MessageReactions reactions={reactions} onClick={handleReaction} />
|
||||
</Box>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <ContextMenuTrigger> */}
|
||||
<Wrapper
|
||||
mention={mention}
|
||||
pinned={pinned}
|
||||
highlight={highlight}
|
||||
data-active={reacting}
|
||||
>
|
||||
{response && <MessageReply message={response} />}
|
||||
{renderMessage()}
|
||||
|
||||
{account && (
|
||||
<Actions
|
||||
owner={owner}
|
||||
pinned={pinned}
|
||||
|
@ -258,12 +250,8 @@ export const ChatMessage = (props: Props) => {
|
|||
onReactingChange={setReacting}
|
||||
reactions={reactions}
|
||||
/>
|
||||
)}
|
||||
</Wrapper>
|
||||
{/* <ContextMenu>
|
||||
<ContextMenu.Item onSelect={handleReplyClick}>Reply</ContextMenu.Item>
|
||||
<ContextMenu.Item onSelect={handlePinClick}>Pin</ContextMenu.Item>
|
||||
</ContextMenu> */}
|
||||
{/* </ContextMenuTrigger> */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -280,7 +268,8 @@ const backgroundAnimation = keyframes({
|
|||
// TODO: Use compound variants https://stitches.dev/docs/variants#compound-variants
|
||||
const Wrapper = styled('div', {
|
||||
position: 'relative',
|
||||
padding: '10px 16px',
|
||||
padding: '2px 16px',
|
||||
marginTop: 14,
|
||||
gap: '$2',
|
||||
|
||||
transitionProperty: 'background-color, border-color, color, fill, stroke',
|
||||
|
|
|
@ -1,47 +1,37 @@
|
|||
import React from 'react'
|
||||
|
||||
import { useMatch } from 'react-router-dom'
|
||||
|
||||
import { useProtocol } from '../../../../protocol'
|
||||
import { styled } from '../../../../styles/config'
|
||||
import { Avatar, Box, Flex, Image, Text } from '../../../../system'
|
||||
|
||||
import type { Message } from '../../../../protocol'
|
||||
|
||||
interface Props {
|
||||
messageId: string
|
||||
message: Message
|
||||
}
|
||||
|
||||
export const MessageReply = (props: Props) => {
|
||||
const { messageId } = props
|
||||
const { message } = props
|
||||
|
||||
const { client } = useProtocol()
|
||||
|
||||
// TODO: use protocol hook
|
||||
const { params } = useMatch(':id')! // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
||||
const message = client.community.getChat(params.id!)!.getMessage(messageId)
|
||||
|
||||
if (!message) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<Text color="gray" size="13" truncate>
|
||||
Message not available.
|
||||
</Text>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
// if (!message) {
|
||||
// return (
|
||||
// <Wrapper>
|
||||
// <Text color="gray" size="13" truncate>
|
||||
// Message not available.
|
||||
// </Text>
|
||||
// </Wrapper>
|
||||
// )
|
||||
// }
|
||||
|
||||
const { contentType, text, signer } = message
|
||||
|
||||
// TODO: can this happen?
|
||||
const member = client.community.getMember(signer)
|
||||
|
||||
if (!member) {
|
||||
return null
|
||||
}
|
||||
const member = client.community.getMember(signer)!
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Flex gap="1" align="center">
|
||||
<Avatar size={20} name={member.username} />
|
||||
<Avatar size={20} name={member.username} colorHash={member.colorHash} />
|
||||
<Text color="gray" size="13" weight="500">
|
||||
{member.username}
|
||||
</Text>
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react'
|
||||
|
||||
import isSameDay from 'date-fns/isSameDay'
|
||||
|
||||
import { Flex, Text } from '../../../../system'
|
||||
|
||||
interface Props {
|
||||
date: Date
|
||||
}
|
||||
|
||||
export const DateDivider = (props: Props) => {
|
||||
const { date } = props
|
||||
|
||||
let label = date.toLocaleDateString([], { weekday: 'long' })
|
||||
|
||||
const today = new Date()
|
||||
const yesterday = new Date().setDate(today.getDate() - 1)
|
||||
|
||||
if (isSameDay(date, today)) {
|
||||
label = 'Today'
|
||||
} else if (isSameDay(date, yesterday)) {
|
||||
label = 'Yesterday'
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex justify="center" css={{ padding: '18px 0 8px' }}>
|
||||
<Text size="13" color="gray">
|
||||
{label}
|
||||
</Text>
|
||||
</Flex>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
import React from 'react'
|
||||
|
||||
import { keyframes } from '../../../../styles/config'
|
||||
import { Box, Text } from '../../../../system'
|
||||
|
||||
interface Props {
|
||||
label: string
|
||||
}
|
||||
const fadeIn = keyframes({
|
||||
from: { opacity: 0, top: 0 },
|
||||
to: { opacity: 1 },
|
||||
})
|
||||
|
||||
const spin = keyframes({
|
||||
to: {
|
||||
transform: 'rotate(1turn)',
|
||||
},
|
||||
})
|
||||
|
||||
export const LoadingToast = (props: Props) => {
|
||||
const { label } = props
|
||||
|
||||
return (
|
||||
<Box
|
||||
css={{
|
||||
width: 'max-content',
|
||||
position: 'sticky',
|
||||
top: 10,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
background: '$accent-11',
|
||||
color: '$accent-1',
|
||||
boxShadow:
|
||||
'0px 2px 4px rgba(0, 34, 51, 0.16), 0px 4px 12px rgba(0, 34, 51, 0.08)',
|
||||
borderRadius: 8,
|
||||
padding: '4px 8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
animation: `${fadeIn} .2s linear`,
|
||||
|
||||
svg: {
|
||||
animation: `${spin} 1s linear infinite`,
|
||||
marginBottom: -1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="13"
|
||||
height="12"
|
||||
viewBox="0 0 13 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.7692 5.07742C10.5677 4.18403 10.0787 3.37742 9.37342 2.77889C8.668 2.18025 7.78425 1.8222 6.85392 1.7598C5.92358 1.69741 4.99847 1.93417 4.21695 2.43357C3.43557 2.93289 2.84032 3.66744 2.51829 4.52621C2.1963 5.38485 2.16416 6.32294 2.42647 7.20091C2.68883 8.07899 3.23215 8.85135 3.97718 9.40157C4.72235 9.95188 5.62888 10.25 6.56139 10.25L6.56139 11.75C5.30961 11.75 4.09047 11.3499 3.08608 10.6082C2.08155 9.86633 1.34532 8.82207 0.989253 7.63032C0.63315 6.43846 0.676901 5.16459 1.11379 3.99953C1.55064 2.8346 2.35652 1.84232 3.40925 1.16961C4.46184 0.496978 5.70538 0.179402 6.9543 0.263164C8.20325 0.346928 9.39243 0.827686 10.344 1.63521C11.2957 2.44286 11.9588 3.53431 12.2324 4.74738L10.7692 5.07742Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<Text size="13" weight="500">
|
||||
Loading {label}...
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react'
|
||||
|
||||
import ContentLoader from 'react-content-loader'
|
||||
|
||||
export const MessageLoader = () => {
|
||||
return (
|
||||
<ContentLoader
|
||||
speed={2}
|
||||
width={880}
|
||||
height={64}
|
||||
viewBox="0 0 880 64"
|
||||
backgroundColor="var(--colors-accent-8)"
|
||||
foregroundColor="var(--colors-accent-5)"
|
||||
>
|
||||
<circle cx="36" cy="30" r="20" />
|
||||
<rect x="64" y="8" rx="8" ry="8" width="132" height="20" />
|
||||
<rect x="200" y="11" rx="4" ry="4" width="50" height="14" />
|
||||
<rect x="64" y="35" rx="8" ry="8" width="574" height="20" />
|
||||
</ContentLoader>
|
||||
)
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useEffect, useRef } from 'react'
|
||||
import React, { Fragment, useEffect, useRef } from 'react'
|
||||
|
||||
import isSameDay from 'date-fns/isSameDay'
|
||||
import { useLocation, useMatch } from 'react-router-dom'
|
||||
|
||||
import { MemberSidebar } from '../../components/member-sidebar'
|
||||
|
@ -10,6 +11,9 @@ import { styled } from '../../styles/config'
|
|||
import { Avatar, Flex, Heading, Text } from '../../system'
|
||||
import { ChatInput } from './components/chat-input'
|
||||
import { ChatMessage } from './components/chat-message'
|
||||
import { DateDivider } from './components/date-divider'
|
||||
import { LoadingToast } from './components/loading-toast'
|
||||
import { MessageLoader } from './components/message-loader'
|
||||
import { Navbar } from './components/navbar'
|
||||
|
||||
interface ChatStartProps {
|
||||
|
@ -75,20 +79,52 @@ const Body = () => {
|
|||
chat.sendTextMessage(message, state.reply?.message.messageId)
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
if (messages.loading) {
|
||||
return (
|
||||
<>
|
||||
<ContentWrapper ref={contentRef}>
|
||||
<ChatStart chatId={chatId} />
|
||||
{messages.data.map(message => {
|
||||
return (
|
||||
<ChatMessage
|
||||
key={message.messageId}
|
||||
message={message}
|
||||
highlight={message.messageId === selectedMessageId}
|
||||
/>
|
||||
<LoadingToast label="last 30 days" />
|
||||
|
||||
<MessageLoader />
|
||||
<MessageLoader />
|
||||
<MessageLoader />
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</ContentWrapper>
|
||||
}
|
||||
|
||||
if (messages.data.length === 0) {
|
||||
return <ChatStart chatId={chatId} />
|
||||
}
|
||||
|
||||
return messages.data.map((message, index) => {
|
||||
const sentDate = new Date(Number(message.timestamp))
|
||||
const previousMessage = messages.data[index - 1]
|
||||
|
||||
let hasDateSeparator = true
|
||||
|
||||
if (previousMessage) {
|
||||
const prevSentDate = new Date(Number(previousMessage.timestamp))
|
||||
|
||||
if (isSameDay(prevSentDate, sentDate)) {
|
||||
hasDateSeparator = false
|
||||
}
|
||||
}
|
||||
|
||||
const shouldCollapse =
|
||||
!message.responseTo && message.signer === previousMessage?.signer
|
||||
|
||||
return (
|
||||
<Fragment key={message.messageId}>
|
||||
{hasDateSeparator && <DateDivider date={sentDate} />}
|
||||
<ChatMessage message={message} collapse={shouldCollapse} />
|
||||
</Fragment>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContentWrapper ref={contentRef}>{renderContent()}</ContentWrapper>
|
||||
{account && <ChatInput onSubmit={handleMessageSubmit} />}
|
||||
</>
|
||||
)
|
||||
|
@ -130,7 +166,7 @@ const ContentWrapper = styled('div', {
|
|||
overscrollBehavior: 'contain',
|
||||
|
||||
// scrollSnapType: 'y proximity',
|
||||
|
||||
paddingBottom: 16,
|
||||
// '& > div:last-child': {
|
||||
// scrollSnapAlign: 'end',
|
||||
// scrollMarginBlockEnd: '1px',
|
||||
|
|
|
@ -5,45 +5,51 @@ import { Base } from './styles'
|
|||
import type { Variants } from './styles'
|
||||
import type { Ref } from 'react'
|
||||
|
||||
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>
|
||||
|
||||
interface Props {
|
||||
children: string
|
||||
disabled?: boolean
|
||||
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
loading?: boolean
|
||||
active?: boolean
|
||||
type?: ButtonProps['type']
|
||||
onClick?: ButtonProps['onClick']
|
||||
}
|
||||
type AnchorProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
|
||||
href: string
|
||||
}
|
||||
|
||||
type Props = (AnchorProps | ButtonProps) & {
|
||||
children: string
|
||||
variant?: Variants['variant']
|
||||
size?: Variants['size']
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const Button = (props: Props, ref: Ref<HTMLButtonElement>) => {
|
||||
const {
|
||||
type = 'button',
|
||||
children,
|
||||
disabled,
|
||||
loading,
|
||||
onClick,
|
||||
variant = 'default',
|
||||
...buttonProps
|
||||
} = props
|
||||
const { children } = props
|
||||
|
||||
if ('href' in props) {
|
||||
const { href, ...linkProps } = props
|
||||
const external = href.startsWith('http')
|
||||
|
||||
return (
|
||||
<Base
|
||||
{...buttonProps}
|
||||
type={type}
|
||||
ref={ref}
|
||||
disabled={disabled}
|
||||
loading={loading}
|
||||
onClick={onClick}
|
||||
variant={variant}
|
||||
{...linkProps}
|
||||
as="a"
|
||||
href={props.href}
|
||||
{...(external && {
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</Base>
|
||||
)
|
||||
}
|
||||
|
||||
const { type = 'button', loading, ...buttonProps } = props
|
||||
|
||||
return (
|
||||
<Base {...buttonProps} type={type} ref={ref} loading={loading}>
|
||||
{children}
|
||||
</Base>
|
||||
)
|
||||
}
|
||||
|
||||
const _Button = forwardRef(Button)
|
||||
|
||||
export { _Button as Button }
|
||||
|
|
|
@ -81,4 +81,7 @@ export const Base = styled('button', {
|
|||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
})
|
||||
|
|
19
yarn.lock
19
yarn.lock
|
@ -1916,6 +1916,24 @@
|
|||
"@radix-ui/react-roving-focus" "1.0.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.0"
|
||||
|
||||
"@radix-ui/react-toast@^0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-toast/-/react-toast-0.1.1.tgz#d544e796b307e56f1298e40f356f468680958e93"
|
||||
integrity sha512-9JWC4mPP78OE6muDrpaPf/71dIeozppdcnik1IvsjTxZpDnt9PbTtQj94DdWjlCphbv3S5faD3KL0GOpqKBpTQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "0.1.0"
|
||||
"@radix-ui/react-compose-refs" "0.1.0"
|
||||
"@radix-ui/react-context" "0.1.1"
|
||||
"@radix-ui/react-dismissable-layer" "0.1.5"
|
||||
"@radix-ui/react-portal" "0.1.4"
|
||||
"@radix-ui/react-presence" "0.1.2"
|
||||
"@radix-ui/react-primitive" "0.1.4"
|
||||
"@radix-ui/react-use-callback-ref" "0.1.0"
|
||||
"@radix-ui/react-use-controllable-state" "0.1.0"
|
||||
"@radix-ui/react-use-layout-effect" "0.1.0"
|
||||
"@radix-ui/react-visually-hidden" "0.1.4"
|
||||
|
||||
"@radix-ui/react-toggle-group@^0.1.5":
|
||||
version "0.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle-group/-/react-toggle-group-0.1.5.tgz#9e4d65e22c4fc0ba3a42fbc8d5496c430e5e9852"
|
||||
|
@ -5397,6 +5415,7 @@ netmask@^2.0.2:
|
|||
|
||||
node-fetch@^2.x.x:
|
||||
version "2.6.7"
|
||||
uid "1b5d62978f2ed07b99444f64f0df39f960a6d34d"
|
||||
resolved "https://registry.npmjs.org/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz#1b5d62978f2ed07b99444f64f0df39f960a6d34d"
|
||||
|
||||
node-forge@^1.1.0, node-forge@^1.3.1:
|
||||
|
|
Loading…
Reference in New Issue