Fetch history (#292)

This commit is contained in:
Pavel 2022-10-07 21:30:22 +02:00 committed by GitHub
parent c39b84a1e5
commit 190011d97e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 382 additions and 202 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -81,4 +81,7 @@ export const Base = styled('button', {
}, },
}, },
}, },
defaultVariants: {
variant: 'default',
},
}) })

View File

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