Fetch history (#292)
This commit is contained in:
parent
c39b84a1e5
commit
190011d97e
|
@ -240,9 +240,10 @@ export class Chat {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.#messages.size) {
|
// fixme?: to stop the loading we need to let the listeners know even if there are no messages
|
||||||
return
|
// if (!this.#messages.size) {
|
||||||
}
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
const messages = this.getMessages()
|
const messages = this.getMessages()
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ import { containsOnlyEmoji } from './contains-only-emoji'
|
||||||
test('should be truthy', () => {
|
test('should be truthy', () => {
|
||||||
expect(containsOnlyEmoji('💩')).toBeTruthy()
|
expect(containsOnlyEmoji('💩')).toBeTruthy()
|
||||||
expect(containsOnlyEmoji('💩💩💩💩💩💩')).toBeTruthy()
|
expect(containsOnlyEmoji('💩💩💩💩💩💩')).toBeTruthy()
|
||||||
|
// expect(containsOnlyEmoji('1️⃣')).toBeTruthy()
|
||||||
|
// expect(containsOnlyEmoji('👨👩👧')).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should be falsy', () => {
|
test('should be falsy', () => {
|
||||||
|
@ -14,4 +16,7 @@ test('should be falsy', () => {
|
||||||
expect(containsOnlyEmoji('💩 ')).toBeFalsy()
|
expect(containsOnlyEmoji('💩 ')).toBeFalsy()
|
||||||
expect(containsOnlyEmoji('text 💩')).toBeFalsy()
|
expect(containsOnlyEmoji('text 💩')).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()
|
// todo?: should ignore whitespaces with replace(/\s+/g, '').trim()
|
||||||
|
/**
|
||||||
|
* https://www.unicode.org/reports/tr51/#def_emoji_presentation
|
||||||
|
*/
|
||||||
export function containsOnlyEmoji(text: string): boolean {
|
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-label": "^0.1.5",
|
||||||
"@radix-ui/react-popover": "^0.1.6",
|
"@radix-ui/react-popover": "^0.1.6",
|
||||||
"@radix-ui/react-separator": "^0.1.4",
|
"@radix-ui/react-separator": "^0.1.4",
|
||||||
|
"@radix-ui/react-toast": "^0.1.1",
|
||||||
"@radix-ui/react-tabs": "^1.0.0",
|
"@radix-ui/react-tabs": "^1.0.0",
|
||||||
"@radix-ui/react-toggle-group": "^0.1.5",
|
"@radix-ui/react-toggle-group": "^0.1.5",
|
||||||
"@radix-ui/react-tooltip": "^1.0.0",
|
"@radix-ui/react-tooltip": "^1.0.0",
|
||||||
|
|
|
@ -56,17 +56,7 @@ export const CommunityDialog = () => {
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<Button
|
<Button href="https://status.im/get">Download Status</Button>
|
||||||
onClick={() =>
|
|
||||||
window.open(
|
|
||||||
'https://status.im/get',
|
|
||||||
'_blank',
|
|
||||||
'noopener,noreferrer'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Download Status
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Dialog.Body>
|
</Dialog.Body>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export { ProtocolProvider, useProtocol } from './provider'
|
export { ProtocolProvider } from './provider'
|
||||||
export type { Account } from './use-account'
|
export type { Account } from './use-account'
|
||||||
export { useAccount } from './use-account'
|
export { useAccount } from './use-account'
|
||||||
export { useActivityCenter } from './use-activity-center'
|
export { useActivityCenter } from './use-activity-center'
|
||||||
|
@ -8,4 +8,5 @@ export type { Member } from './use-members'
|
||||||
export { useMembers } from './use-members'
|
export { useMembers } from './use-members'
|
||||||
export type { Message, Reaction, Reactions } from './use-messages'
|
export type { Message, Reaction, Reactions } from './use-messages'
|
||||||
export { useMessages } from './use-messages'
|
export { useMessages } from './use-messages'
|
||||||
|
export { useProtocol } from './use-protocol'
|
||||||
export { useSortedChats } from './use-sorted-chats'
|
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'
|
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'
|
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
|
loading: boolean
|
||||||
client: Client | undefined
|
client: Client | undefined
|
||||||
community: Community['description'] | undefined
|
community: Community['description'] | undefined
|
||||||
|
@ -16,7 +16,7 @@ type State = {
|
||||||
dispatch?: React.Dispatch<Action>
|
dispatch?: React.Dispatch<Action>
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action =
|
export type Action =
|
||||||
| { type: 'INIT'; client: Client }
|
| { type: 'INIT'; client: Client }
|
||||||
| { type: 'UPDATE_COMMUNITY'; community: Community['description'] }
|
| { type: 'UPDATE_COMMUNITY'; community: Community['description'] }
|
||||||
| { type: 'SET_ACCOUNT'; account: Account | undefined }
|
| { type: 'SET_ACCOUNT'; account: Account | undefined }
|
||||||
|
@ -99,18 +99,3 @@ export const ProtocolProvider = (props: Props) => {
|
||||||
</Context.Provider>
|
</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'
|
import type { Account } from '@status-im/js'
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useMatch } from 'react-router-dom'
|
import { useMatch } from 'react-router-dom'
|
||||||
|
|
||||||
import { useProtocol } from './provider'
|
import { useProtocol } from './use-protocol'
|
||||||
|
|
||||||
export const useActiveChat = () => {
|
export const useActiveChat = () => {
|
||||||
const { client } = useProtocol()
|
const { client } = useProtocol()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { useProtocol } from './provider'
|
import { useProtocol } from './use-protocol'
|
||||||
|
|
||||||
import type { ActivityCenterLatest } from '@status-im/js'
|
import type { ActivityCenterLatest } from '@status-im/js'
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
import { useProtocol } from './provider'
|
import { useProtocol } from './use-protocol'
|
||||||
|
|
||||||
import type { Community } from '@status-im/js'
|
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'
|
import type { Member } from '@status-im/js'
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { useEffect, useState } from 'react'
|
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'
|
import type { Message, Reactions } from '@status-im/js'
|
||||||
|
|
||||||
|
@ -9,21 +11,20 @@ type Reaction = keyof Reactions
|
||||||
interface Result {
|
interface Result {
|
||||||
data: Message[]
|
data: Message[]
|
||||||
loading: boolean
|
loading: boolean
|
||||||
// error?: Error
|
|
||||||
// fetchMore: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useMessages = (channelId: string): Result => {
|
export const useMessages = (chatId: string): Result => {
|
||||||
const { client } = useProtocol()
|
const { client } = useProtocol()
|
||||||
|
|
||||||
const chat = client.community.chats.get(channelId)!
|
const chat = client.community.chats.get(chatId)!
|
||||||
// const [state, dispatch] = useReducer<Result>((state,action) => {}, {})
|
|
||||||
|
|
||||||
const [data, setData] = useState<Message[]>(() => chat.getMessages())
|
const [data, setData] = useState<Message[]>(() => chat.getMessages())
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
// const [error, setError] = useState<Error>()
|
// const [error, setError] = useState<Error>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const messages = chat.getMessages()
|
||||||
|
|
||||||
setData(chat.getMessages())
|
setData(chat.getMessages())
|
||||||
|
|
||||||
const handleUpdate = (messages: Message[]) => {
|
const handleUpdate = (messages: Message[]) => {
|
||||||
|
@ -31,15 +32,21 @@ export const useMessages = (channelId: string): Result => {
|
||||||
setData(messages)
|
setData(messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (messages.length === 0) {
|
||||||
|
setLoading(true)
|
||||||
|
chat.fetchMessages({ start: sub(new Date(), { days: 30 }) })
|
||||||
|
}
|
||||||
|
|
||||||
return chat.onMessage(handleUpdate)
|
return chat.onMessage(handleUpdate)
|
||||||
}, [chat])
|
}, [chat])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
loading,
|
loading,
|
||||||
|
// fetchMore,
|
||||||
|
// fetching,
|
||||||
// error,
|
// error,
|
||||||
// hasMore
|
// hasMore
|
||||||
// fetchMore,
|
|
||||||
// refetch
|
// 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 { useMemo } from 'react'
|
||||||
|
|
||||||
import { useProtocol } from './provider'
|
import { useProtocol } from './use-protocol'
|
||||||
|
|
||||||
import type { Community } from '@status-im/js'
|
import type { Community } from '@status-im/js'
|
||||||
|
|
||||||
|
|
|
@ -82,7 +82,8 @@ const Wrapper = styled('div', {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
alignItems: 'flex-end',
|
alignItems: 'flex-end',
|
||||||
padding: '12px 8px 12px 4px',
|
// padding: '12px 8px 12px 4px',
|
||||||
|
padding: '12px 8px 12px 8px',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ import type { Message, Reaction } from '../../../../protocol'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: Message
|
message: Message
|
||||||
prevMessage?: Message
|
collapse: boolean
|
||||||
highlight?: boolean
|
highlight?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,16 +57,17 @@ interface Props {
|
||||||
// })
|
// })
|
||||||
|
|
||||||
export const ChatMessage = (props: Props) => {
|
export const ChatMessage = (props: Props) => {
|
||||||
|
const { message, collapse, highlight } = props
|
||||||
|
|
||||||
const { client, account } = useProtocol()
|
const { client, account } = useProtocol()
|
||||||
const { params } = useMatch(':id')!
|
const { params } = useMatch(':id')!
|
||||||
|
|
||||||
const chatId = params.id!
|
const chatId = params.id!
|
||||||
const { message, highlight } = props
|
|
||||||
|
|
||||||
const mention = false
|
const mention = false
|
||||||
const pinned = false
|
const pinned = false
|
||||||
|
|
||||||
const { messageId, contentType, clock, reactions, signer, responseTo } =
|
const { messageId, contentType, timestamp, reactions, signer, responseTo } =
|
||||||
message
|
message
|
||||||
|
|
||||||
// TODO: remove usage of 0x prefix
|
// TODO: remove usage of 0x prefix
|
||||||
|
@ -74,6 +75,7 @@ export const ChatMessage = (props: Props) => {
|
||||||
const chat = client.community.getChat(chatId)!
|
const chat = client.community.getChat(chatId)!
|
||||||
|
|
||||||
const member = client.community.getMember(signer)!
|
const member = client.community.getMember(signer)!
|
||||||
|
const response = client.community.getChat(params.id!)!.getMessage(responseTo)
|
||||||
|
|
||||||
const [editing, setEditing] = useState(false)
|
const [editing, setEditing] = useState(false)
|
||||||
const [reacting, setReacting] = useState(false)
|
const [reacting, setReacting] = useState(false)
|
||||||
|
@ -82,6 +84,7 @@ export const ChatMessage = (props: Props) => {
|
||||||
|
|
||||||
// const userProfileDialog = useDialog(UserProfileDialog)
|
// const userProfileDialog = useDialog(UserProfileDialog)
|
||||||
|
|
||||||
|
// TODO: fix saving of edited message
|
||||||
const handleMessageSubmit = (message: string) => {
|
const handleMessageSubmit = (message: string) => {
|
||||||
chat.sendTextMessage(message)
|
chat.sendTextMessage(message)
|
||||||
}
|
}
|
||||||
|
@ -107,7 +110,7 @@ export const ChatMessage = (props: Props) => {
|
||||||
// TODO: pin message
|
// TODO: pin message
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderMessage = () => {
|
const renderContent = () => {
|
||||||
if (editing) {
|
if (editing) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<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 (
|
return (
|
||||||
<>
|
|
||||||
{/* <ContextMenuTrigger> */}
|
|
||||||
<Wrapper
|
|
||||||
mention={mention}
|
|
||||||
pinned={pinned}
|
|
||||||
data-active={reacting}
|
|
||||||
highlight={highlight}
|
|
||||||
>
|
|
||||||
{responseTo && <MessageReply messageId={responseTo} />}
|
|
||||||
<Flex gap={2}>
|
<Flex gap={2}>
|
||||||
<Box>
|
<Box>
|
||||||
{/* <DropdownMenuTrigger>
|
|
||||||
<button type="button"> */}
|
|
||||||
<Avatar
|
<Avatar
|
||||||
size={44}
|
size={44}
|
||||||
name={member!.username}
|
name={member!.username}
|
||||||
colorHash={member!.colorHash}
|
colorHash={member!.colorHash}
|
||||||
/>
|
/>
|
||||||
{/* </button> */}
|
</Box>
|
||||||
{/* <DropdownMenu>
|
|
||||||
<Flex direction="column" align="center" gap="1">
|
<Box css={{ flex: 1 }}>
|
||||||
<Avatar size="36" />
|
{/* {pinned && (
|
||||||
<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>
|
||||||
|
|
||||||
<Box css={{ flex: 1 }}>
|
<Box css={{ flex: 1 }}>
|
||||||
|
@ -233,19 +210,34 @@ export const ChatMessage = (props: Props) => {
|
||||||
{member!.username}
|
{member!.username}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="10" color="gray">
|
<Text size="10" color="gray">
|
||||||
{new Date(Number(clock)).toLocaleTimeString([], {
|
{new Date(Number(timestamp)).toLocaleTimeString([], {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
})}
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{renderMessage()}
|
{renderContent()}
|
||||||
|
|
||||||
<MessageReactions reactions={reactions} onClick={handleReaction} />
|
<MessageReactions reactions={reactions} onClick={handleReaction} />
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* <ContextMenuTrigger> */}
|
||||||
|
<Wrapper
|
||||||
|
mention={mention}
|
||||||
|
pinned={pinned}
|
||||||
|
highlight={highlight}
|
||||||
|
data-active={reacting}
|
||||||
|
>
|
||||||
|
{response && <MessageReply message={response} />}
|
||||||
|
{renderMessage()}
|
||||||
|
|
||||||
|
{account && (
|
||||||
<Actions
|
<Actions
|
||||||
owner={owner}
|
owner={owner}
|
||||||
pinned={pinned}
|
pinned={pinned}
|
||||||
|
@ -258,12 +250,8 @@ export const ChatMessage = (props: Props) => {
|
||||||
onReactingChange={setReacting}
|
onReactingChange={setReacting}
|
||||||
reactions={reactions}
|
reactions={reactions}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</Wrapper>
|
</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
|
// TODO: Use compound variants https://stitches.dev/docs/variants#compound-variants
|
||||||
const Wrapper = styled('div', {
|
const Wrapper = styled('div', {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
padding: '10px 16px',
|
padding: '2px 16px',
|
||||||
|
marginTop: 14,
|
||||||
gap: '$2',
|
gap: '$2',
|
||||||
|
|
||||||
transitionProperty: 'background-color, border-color, color, fill, stroke',
|
transitionProperty: 'background-color, border-color, color, fill, stroke',
|
||||||
|
|
|
@ -1,47 +1,37 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { useMatch } from 'react-router-dom'
|
|
||||||
|
|
||||||
import { useProtocol } from '../../../../protocol'
|
import { useProtocol } from '../../../../protocol'
|
||||||
import { styled } from '../../../../styles/config'
|
import { styled } from '../../../../styles/config'
|
||||||
import { Avatar, Box, Flex, Image, Text } from '../../../../system'
|
import { Avatar, Box, Flex, Image, Text } from '../../../../system'
|
||||||
|
|
||||||
|
import type { Message } from '../../../../protocol'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
messageId: string
|
message: Message
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MessageReply = (props: Props) => {
|
export const MessageReply = (props: Props) => {
|
||||||
const { messageId } = props
|
const { message } = props
|
||||||
|
|
||||||
const { client } = useProtocol()
|
const { client } = useProtocol()
|
||||||
|
|
||||||
// TODO: use protocol hook
|
// if (!message) {
|
||||||
const { params } = useMatch(':id')! // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
// return (
|
||||||
const message = client.community.getChat(params.id!)!.getMessage(messageId)
|
// <Wrapper>
|
||||||
|
// <Text color="gray" size="13" truncate>
|
||||||
if (!message) {
|
// Message not available.
|
||||||
return (
|
// </Text>
|
||||||
<Wrapper>
|
// </Wrapper>
|
||||||
<Text color="gray" size="13" truncate>
|
// )
|
||||||
Message not available.
|
// }
|
||||||
</Text>
|
|
||||||
</Wrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { contentType, text, signer } = message
|
const { contentType, text, signer } = message
|
||||||
|
const member = client.community.getMember(signer)!
|
||||||
// TODO: can this happen?
|
|
||||||
const member = client.community.getMember(signer)
|
|
||||||
|
|
||||||
if (!member) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Flex gap="1" align="center">
|
<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">
|
<Text color="gray" size="13" weight="500">
|
||||||
{member.username}
|
{member.username}
|
||||||
</Text>
|
</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 { useLocation, useMatch } from 'react-router-dom'
|
||||||
|
|
||||||
import { MemberSidebar } from '../../components/member-sidebar'
|
import { MemberSidebar } from '../../components/member-sidebar'
|
||||||
|
@ -10,6 +11,9 @@ import { styled } from '../../styles/config'
|
||||||
import { Avatar, Flex, Heading, Text } from '../../system'
|
import { Avatar, Flex, Heading, Text } from '../../system'
|
||||||
import { ChatInput } from './components/chat-input'
|
import { ChatInput } from './components/chat-input'
|
||||||
import { ChatMessage } from './components/chat-message'
|
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'
|
import { Navbar } from './components/navbar'
|
||||||
|
|
||||||
interface ChatStartProps {
|
interface ChatStartProps {
|
||||||
|
@ -75,20 +79,52 @@ const Body = () => {
|
||||||
chat.sendTextMessage(message, state.reply?.message.messageId)
|
chat.sendTextMessage(message, state.reply?.message.messageId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
if (messages.loading) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ContentWrapper ref={contentRef}>
|
<LoadingToast label="last 30 days" />
|
||||||
<ChatStart chatId={chatId} />
|
|
||||||
{messages.data.map(message => {
|
<MessageLoader />
|
||||||
return (
|
<MessageLoader />
|
||||||
<ChatMessage
|
<MessageLoader />
|
||||||
key={message.messageId}
|
</>
|
||||||
message={message}
|
|
||||||
highlight={message.messageId === selectedMessageId}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
})}
|
}
|
||||||
</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} />}
|
{account && <ChatInput onSubmit={handleMessageSubmit} />}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -130,7 +166,7 @@ const ContentWrapper = styled('div', {
|
||||||
overscrollBehavior: 'contain',
|
overscrollBehavior: 'contain',
|
||||||
|
|
||||||
// scrollSnapType: 'y proximity',
|
// scrollSnapType: 'y proximity',
|
||||||
|
paddingBottom: 16,
|
||||||
// '& > div:last-child': {
|
// '& > div:last-child': {
|
||||||
// scrollSnapAlign: 'end',
|
// scrollSnapAlign: 'end',
|
||||||
// scrollMarginBlockEnd: '1px',
|
// scrollMarginBlockEnd: '1px',
|
||||||
|
|
|
@ -5,45 +5,51 @@ import { Base } from './styles'
|
||||||
import type { Variants } from './styles'
|
import type { Variants } from './styles'
|
||||||
import type { Ref } from 'react'
|
import type { Ref } from 'react'
|
||||||
|
|
||||||
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>
|
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children: string
|
|
||||||
disabled?: boolean
|
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
active?: boolean
|
}
|
||||||
type?: ButtonProps['type']
|
type AnchorProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
|
||||||
onClick?: ButtonProps['onClick']
|
href: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = (AnchorProps | ButtonProps) & {
|
||||||
|
children: string
|
||||||
variant?: Variants['variant']
|
variant?: Variants['variant']
|
||||||
size?: Variants['size']
|
size?: Variants['size']
|
||||||
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = (props: Props, ref: Ref<HTMLButtonElement>) => {
|
const Button = (props: Props, ref: Ref<HTMLButtonElement>) => {
|
||||||
const {
|
const { children } = props
|
||||||
type = 'button',
|
|
||||||
children,
|
if ('href' in props) {
|
||||||
disabled,
|
const { href, ...linkProps } = props
|
||||||
loading,
|
const external = href.startsWith('http')
|
||||||
onClick,
|
|
||||||
variant = 'default',
|
|
||||||
...buttonProps
|
|
||||||
} = props
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Base
|
<Base
|
||||||
{...buttonProps}
|
{...linkProps}
|
||||||
type={type}
|
as="a"
|
||||||
ref={ref}
|
href={props.href}
|
||||||
disabled={disabled}
|
{...(external && {
|
||||||
loading={loading}
|
target: '_blank',
|
||||||
onClick={onClick}
|
rel: 'noopener noreferrer',
|
||||||
variant={variant}
|
})}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Base>
|
</Base>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { type = 'button', loading, ...buttonProps } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Base {...buttonProps} type={type} ref={ref} loading={loading}>
|
||||||
|
{children}
|
||||||
|
</Base>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const _Button = forwardRef(Button)
|
const _Button = forwardRef(Button)
|
||||||
|
|
||||||
export { _Button as 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-roving-focus" "1.0.0"
|
||||||
"@radix-ui/react-use-controllable-state" "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":
|
"@radix-ui/react-toggle-group@^0.1.5":
|
||||||
version "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"
|
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:
|
node-fetch@^2.x.x:
|
||||||
version "2.6.7"
|
version "2.6.7"
|
||||||
|
uid "1b5d62978f2ed07b99444f64f0df39f960a6d34d"
|
||||||
resolved "https://registry.npmjs.org/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz#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:
|
node-forge@^1.1.0, node-forge@^1.3.1:
|
||||||
|
|
Loading…
Reference in New Issue