Add pinned messages (#355)
* initial * add to app * add to app * context tag * rebase and fix changes * update context-tag, update dialog * update mocks * fix dialog show * clean up stories * fix ids * unify component definition * pr fixes * fix blur view * fix blur view * fix composer position * context tag * add icon avatar + pin announcement * fix spacing * fixes * blue background for pin --------- Co-authored-by: Pavel Prichodko <14926950+prichodko@users.noreply.github.com>
This commit is contained in:
parent
a167396062
commit
c338bf7aae
|
@ -20,7 +20,7 @@ body,
|
|||
#main {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: 56px 1fr 100px;
|
||||
grid-template-rows: 96px 1fr 100px; /* 56px 1fr 100px without pinned messages */
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
|
@ -45,7 +45,7 @@ body,
|
|||
overflow: auto;
|
||||
padding: 40px 0px 0px 0px;
|
||||
height: 100vh;
|
||||
margin-top: -56px;
|
||||
margin-top: -96px; /* -56px without pinned messages */
|
||||
}
|
||||
|
||||
#messages {
|
||||
|
|
|
@ -17,7 +17,7 @@ type UseBlurProps = {
|
|||
const useBlur = (props: UseBlurProps): UseBlurReturn => {
|
||||
const {
|
||||
marginBlurBottom = 32,
|
||||
heightTop = 56,
|
||||
heightTop = 96,
|
||||
throttle = 100,
|
||||
ref,
|
||||
} = props || {}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import { cloneElement } from 'react'
|
||||
|
||||
import { type ColorTokens, Stack, styled } from '@tamagui/core'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactElement
|
||||
backgroundColor?: ColorTokens
|
||||
color?: ColorTokens
|
||||
size?: 20 | 32 | 48
|
||||
}
|
||||
|
||||
const IconAvatar = (props: Props) => {
|
||||
const {
|
||||
children,
|
||||
color = '$blue-50',
|
||||
backgroundColor = '$blue-50-opa-20',
|
||||
size = 32,
|
||||
} = props
|
||||
return (
|
||||
<Base backgroundColor={backgroundColor} size={size}>
|
||||
{cloneElement(children, { color })}
|
||||
</Base>
|
||||
)
|
||||
}
|
||||
|
||||
const Base = styled(Stack, {
|
||||
borderRadius: 80,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
|
||||
variants: {
|
||||
size: {
|
||||
'...': (size: number) => {
|
||||
return {
|
||||
width: size,
|
||||
height: size,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export { IconAvatar }
|
||||
export type { Props as IconAvatarProps }
|
|
@ -1 +1,2 @@
|
|||
export * from './avatar'
|
||||
export * from './icon-avatar'
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import { PinIcon } from '@status-im/icons/20'
|
||||
import { Stack } from '@tamagui/core'
|
||||
|
||||
import { Banner } from './banner'
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
const meta: Meta<typeof Banner> = {
|
||||
component: Banner,
|
||||
argTypes: {
|
||||
children: {
|
||||
control: 'text',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof Banner>
|
||||
|
||||
export const Full: Story = {
|
||||
args: {
|
||||
icon: <PinIcon />,
|
||||
children: 'Banner message',
|
||||
count: 5,
|
||||
},
|
||||
}
|
||||
|
||||
export const NoIcon: Story = {
|
||||
args: {
|
||||
children: 'Banner message',
|
||||
count: 5,
|
||||
},
|
||||
}
|
||||
|
||||
export const NoCount: Story = {
|
||||
args: {
|
||||
icon: <PinIcon />,
|
||||
children: 'Banner message',
|
||||
},
|
||||
}
|
||||
|
||||
export const AllVariants: Story = {
|
||||
args: {},
|
||||
render: () => (
|
||||
<Stack space>
|
||||
<Banner icon={<PinIcon />} count={5}>
|
||||
Banner message
|
||||
</Banner>
|
||||
<Banner count={5}>Banner message</Banner>
|
||||
<Banner icon={<PinIcon />}>Banner message</Banner>
|
||||
</Stack>
|
||||
),
|
||||
}
|
||||
|
||||
export default meta
|
|
@ -0,0 +1,45 @@
|
|||
import { styled } from '@tamagui/core'
|
||||
import { View } from 'react-native'
|
||||
|
||||
import { Counter } from '../counter'
|
||||
import { Text } from '../text'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
icon?: React.ReactNode
|
||||
count?: number
|
||||
}
|
||||
|
||||
const Banner = (props: Props) => {
|
||||
const { icon, children, count } = props
|
||||
|
||||
return (
|
||||
<Base>
|
||||
<Content>
|
||||
{icon}
|
||||
<Text size={13} color="$textPrimary">
|
||||
{children}
|
||||
</Text>
|
||||
</Content>
|
||||
{count ? <Counter value={count} /> : null}
|
||||
</Base>
|
||||
)
|
||||
}
|
||||
|
||||
export { Banner }
|
||||
export type { Props as BannerProps }
|
||||
|
||||
const Base = styled(View, {
|
||||
backgroundColor: '$primary-50-opa-20',
|
||||
padding: 12,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
maxHeight: '40px',
|
||||
})
|
||||
|
||||
const Content = styled(View, {
|
||||
flexDirection: 'row',
|
||||
gap: 10,
|
||||
alignItems: 'center',
|
||||
})
|
|
@ -0,0 +1 @@
|
|||
export { Banner, type BannerProps } from './banner'
|
|
@ -0,0 +1,61 @@
|
|||
import { PendingIcon } from '@status-im/icons/12'
|
||||
import { Stack } from '@tamagui/core'
|
||||
|
||||
import { ContextTag } from './context-tag'
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
const meta: Meta<typeof ContextTag> = {
|
||||
component: ContextTag,
|
||||
argTypes: {
|
||||
label: {
|
||||
control: ['Rarible', '# channel-name'],
|
||||
},
|
||||
size: [24, 32],
|
||||
outline: [true, false],
|
||||
blur: [true, false],
|
||||
},
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof ContextTag>
|
||||
|
||||
export const Base: Story = {
|
||||
args: { label: 'Name', size: 24, outline: false, blur: false },
|
||||
}
|
||||
|
||||
export const AllVariants: Story = {
|
||||
args: {},
|
||||
render: () => (
|
||||
<Stack space flexDirection="row">
|
||||
<Stack space flexDirection="column" alignItems="flex-start">
|
||||
<ContextTag label="Name" />
|
||||
<ContextTag type="group" label="Group" />
|
||||
<ContextTag
|
||||
src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fi.seadn.io%2Fgae%2FFG0QJ00fN3c_FWuPeUr9-T__iQl63j9hn5d6svW8UqOmia5zp3lKHPkJuHcvhZ0f_Pd6P2COo9tt9zVUvdPxG_9BBw%3Fw%3D500%26auto%3Dformat&f=1&nofb=1&ipt=c177cd71d8d0114080cfc6efd3f9e098ddaeb1b347919bd3089bf0aacb003b3e&ipo=images"
|
||||
type="channel"
|
||||
label={['Rarible', '# channel']}
|
||||
/>
|
||||
<ContextTag
|
||||
src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fi.seadn.io%2Fgae%2FFG0QJ00fN3c_FWuPeUr9-T__iQl63j9hn5d6svW8UqOmia5zp3lKHPkJuHcvhZ0f_Pd6P2COo9tt9zVUvdPxG_9BBw%3Fw%3D500%26auto%3Dformat&f=1&nofb=1&ipt=c177cd71d8d0114080cfc6efd3f9e098ddaeb1b347919bd3089bf0aacb003b3e&ipo=images"
|
||||
type="community"
|
||||
label="Coinbase"
|
||||
outline={true}
|
||||
/>
|
||||
<ContextTag type="token" label="10 ETH" />
|
||||
<ContextTag type="network" label="Network" />
|
||||
<ContextTag type="account" label="Account Name" />
|
||||
<ContextTag type="collectible" label="Collectible #123" />
|
||||
<ContextTag type="address" label="0x045...1ah" />
|
||||
<ContextTag
|
||||
icon={<PendingIcon />}
|
||||
type="icon"
|
||||
label="Context"
|
||||
outline
|
||||
/>
|
||||
<ContextTag type="audio" label="00:32" />
|
||||
</Stack>
|
||||
</Stack>
|
||||
),
|
||||
}
|
||||
|
||||
export default meta
|
|
@ -0,0 +1,147 @@
|
|||
import { cloneElement, Fragment } from 'react'
|
||||
|
||||
import { ChevronRightIcon } from '@status-im/icons/16'
|
||||
import { styled } from '@tamagui/core'
|
||||
import { View } from 'react-native'
|
||||
|
||||
import { Avatar } from '../avatar'
|
||||
import { Text } from '../text'
|
||||
|
||||
import type { AvatarProps } from '../avatar'
|
||||
import type { TextProps } from '../text'
|
||||
|
||||
type ContextTagType =
|
||||
| 'default'
|
||||
| 'group'
|
||||
| 'channel'
|
||||
| 'community'
|
||||
| 'token'
|
||||
| 'network'
|
||||
| 'account'
|
||||
| 'collectible'
|
||||
| 'address'
|
||||
| 'icon'
|
||||
| 'audio'
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode
|
||||
src?: string
|
||||
icon?: React.ReactElement
|
||||
label: string | [string, string]
|
||||
type?: ContextTagType
|
||||
size?: 24 | 32
|
||||
blur?: boolean
|
||||
outline?: boolean
|
||||
}
|
||||
|
||||
const textSizes: Record<NonNullable<Props['size']>, TextProps['size']> = {
|
||||
'32': 15,
|
||||
'24': 13,
|
||||
}
|
||||
|
||||
const avatarSizes: Record<NonNullable<Props['size']>, AvatarProps['size']> = {
|
||||
'32': 28,
|
||||
'24': 20,
|
||||
}
|
||||
|
||||
const Label = ({ children, size }: { children: string; size: 24 | 32 }) => (
|
||||
<Text size={textSizes[size]} weight="medium" color="$neutral-100">
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
|
||||
const ContextTag = (props: Props) => {
|
||||
const {
|
||||
src,
|
||||
icon,
|
||||
label,
|
||||
type = 'default',
|
||||
size = 24,
|
||||
blur = false,
|
||||
outline,
|
||||
} = props
|
||||
|
||||
const hasImg = Boolean(src || icon)
|
||||
|
||||
return (
|
||||
<Base outline={outline} size={size} hasImg={hasImg} blur={blur}>
|
||||
{src && <Avatar size={avatarSizes[size]} src={src} />}
|
||||
{icon && cloneElement(icon, { color: '$neutral-50' })}
|
||||
|
||||
{Array.isArray(label) ? (
|
||||
label.map((item, i) => {
|
||||
if (i !== 0) {
|
||||
return (
|
||||
<Fragment key={item}>
|
||||
<ChevronRightIcon color="$neutral-50" />
|
||||
<Label size={size}>{item}</Label>
|
||||
</Fragment>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Label size={size} key={item}>
|
||||
{item}
|
||||
</Label>
|
||||
)
|
||||
}
|
||||
})
|
||||
) : (
|
||||
<Label size={size}>{label}</Label>
|
||||
)}
|
||||
</Base>
|
||||
)
|
||||
}
|
||||
|
||||
export { ContextTag }
|
||||
export type { Props as ContextTagProps }
|
||||
|
||||
const Base = styled(View, {
|
||||
backgroundColor: '$neutral-10',
|
||||
paddingVertical: 1,
|
||||
borderRadius: 20,
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderWidth: '1px',
|
||||
borderStyle: 'solid',
|
||||
borderColor: 'transparent',
|
||||
|
||||
variants: {
|
||||
outline: {
|
||||
true: {
|
||||
borderColor: '$primary-50',
|
||||
},
|
||||
false: {
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
},
|
||||
blur: {
|
||||
true: {
|
||||
backgroundColor: '$neutral-80-opa-5',
|
||||
},
|
||||
false: {
|
||||
backgroundColor: '$neutral-10',
|
||||
},
|
||||
},
|
||||
size: {
|
||||
24: props => {
|
||||
// there is only first param which is "size" and hasImg doesn't exist here
|
||||
return {
|
||||
space: 4,
|
||||
paddingLeft: props.hasImg ? 8 : 2,
|
||||
paddingRight: 8,
|
||||
}
|
||||
},
|
||||
32: ({ hasImg }) => ({
|
||||
// this therefore doesn't work as well
|
||||
space: 8,
|
||||
paddingLeft: hasImg ? 12 : 2,
|
||||
paddingRight: 12,
|
||||
}),
|
||||
},
|
||||
hasImg: {
|
||||
true: {},
|
||||
false: {},
|
||||
}, // to correctly infer the type of the variant
|
||||
} as const,
|
||||
})
|
|
@ -0,0 +1 @@
|
|||
export { ContextTag, type ContextTagProps } from './context-tag'
|
|
@ -0,0 +1,80 @@
|
|||
import { Stack } from '@tamagui/core'
|
||||
|
||||
import { Counter } from './counter'
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
const meta: Meta<typeof Counter> = {
|
||||
component: Counter,
|
||||
argTypes: {
|
||||
value: {
|
||||
control: {
|
||||
type: 'number',
|
||||
min: 0,
|
||||
max: 1000,
|
||||
},
|
||||
},
|
||||
type: {
|
||||
control: 'select',
|
||||
options: ['default', 'secondary', 'grey', 'outline'],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof Counter>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
value: 5,
|
||||
type: 'default',
|
||||
},
|
||||
}
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
value: 5,
|
||||
type: 'secondary',
|
||||
},
|
||||
}
|
||||
|
||||
export const Grey: Story = {
|
||||
args: {
|
||||
value: 5,
|
||||
type: 'grey',
|
||||
},
|
||||
}
|
||||
|
||||
export const Outline: Story = {
|
||||
args: {
|
||||
value: 5,
|
||||
type: 'outline',
|
||||
},
|
||||
}
|
||||
|
||||
export const AllVariants: Story = {
|
||||
args: {},
|
||||
render: () => (
|
||||
<Stack space flexDirection="row">
|
||||
<Stack space>
|
||||
<Counter type="default" value={5} />
|
||||
<Counter type="secondary" value={5} />
|
||||
<Counter type="grey" value={5} />
|
||||
<Counter type="outline" value={5} />
|
||||
</Stack>
|
||||
<Stack space>
|
||||
<Counter type="default" value={10} />
|
||||
<Counter type="secondary" value={10} />
|
||||
<Counter type="grey" value={10} />
|
||||
<Counter type="outline" value={10} />
|
||||
</Stack>
|
||||
<Stack space>
|
||||
<Counter type="default" value={100} />
|
||||
<Counter type="secondary" value={100} />
|
||||
<Counter type="grey" value={100} />
|
||||
<Counter type="outline" value={100} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
),
|
||||
}
|
||||
|
||||
export default meta
|
|
@ -0,0 +1,68 @@
|
|||
import { styled } from '@tamagui/core'
|
||||
import { View } from 'react-native'
|
||||
|
||||
import { Text } from '../text'
|
||||
|
||||
import type { ColorTokens } from '@tamagui/core'
|
||||
|
||||
export type CounterVariants = 'default' | 'grey' | 'secondary' | 'outline'
|
||||
|
||||
type Props = {
|
||||
value: number
|
||||
type?: CounterVariants
|
||||
}
|
||||
|
||||
const Counter = (props: Props) => {
|
||||
const { value, type = 'default' } = props
|
||||
|
||||
return (
|
||||
<Base type={type}>
|
||||
<Text size={11} color={textColor[type]}>
|
||||
{value > 99 ? '99+' : value}
|
||||
</Text>
|
||||
</Base>
|
||||
)
|
||||
}
|
||||
|
||||
export { Counter }
|
||||
export type { Props as CounterProps }
|
||||
|
||||
const Base = styled(View, {
|
||||
backgroundColor: '$primary-50',
|
||||
paddingHorizontal: 3,
|
||||
paddingVertical: 0,
|
||||
borderRadius: '6px', // TODO: use tokens when fixed its definition
|
||||
height: 16,
|
||||
minWidth: 16,
|
||||
maxWidth: 28,
|
||||
display: 'inline-flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexBasis: 'fit-content',
|
||||
|
||||
variants: {
|
||||
type: {
|
||||
default: {
|
||||
backgroundColor: '$primary-50',
|
||||
},
|
||||
secondary: {
|
||||
backgroundColor: '$neutral-80-opa-5',
|
||||
},
|
||||
grey: {
|
||||
backgroundColor: '$neutral-10',
|
||||
},
|
||||
outline: {
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: '$neutral-20',
|
||||
borderWidth: '1px',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const textColor: Record<NonNullable<Props['type']>, ColorTokens> = {
|
||||
default: '$white-100',
|
||||
secondary: '$neutral-100',
|
||||
outline: '$neutral-100',
|
||||
grey: '$neutral-100',
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { Counter, type CounterProps } from './counter'
|
|
@ -1,7 +1,7 @@
|
|||
import { forwardRef } from 'react'
|
||||
|
||||
import { Content, Overlay, Portal, Root, Trigger } from '@radix-ui/react-dialog'
|
||||
import { useMedia } from 'tamagui'
|
||||
import { Stack, styled, useMedia } from 'tamagui'
|
||||
|
||||
import { Sheet } from '../sheet'
|
||||
|
||||
|
@ -15,6 +15,15 @@ interface Props {
|
|||
press?: 'normal' | 'long'
|
||||
}
|
||||
|
||||
const Wrapper = styled(Stack, {
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
})
|
||||
|
||||
// const DialogTrigger = (
|
||||
// props: DialogTriggerProps & {
|
||||
// press: Props['press']
|
||||
|
@ -52,6 +61,7 @@ const Dialog = (props: Props) => {
|
|||
|
||||
{/* CONTENT */}
|
||||
<Portal>
|
||||
<Wrapper>
|
||||
<Overlay
|
||||
style={{
|
||||
position: 'fixed',
|
||||
|
@ -60,6 +70,7 @@ const Dialog = (props: Props) => {
|
|||
}}
|
||||
/>
|
||||
{content}
|
||||
</Wrapper>
|
||||
</Portal>
|
||||
</Root>
|
||||
)
|
||||
|
|
|
@ -8,6 +8,7 @@ export * from './icon-button'
|
|||
export * from './image'
|
||||
export * from './input'
|
||||
export * from './messages'
|
||||
export * from './pinned-message'
|
||||
export * from './provider'
|
||||
export * from './sidebar'
|
||||
export * from './sidebar-members'
|
||||
|
|
|
@ -25,10 +25,11 @@ interface Props {
|
|||
onReplyPress: VoidFunction
|
||||
onEditPress: VoidFunction
|
||||
// onDeletePress: VoidFunction
|
||||
pinned?: boolean
|
||||
}
|
||||
|
||||
export const Actions = (props: Props) => {
|
||||
const { reactions, onOpenChange, onReplyPress, onEditPress } = props
|
||||
const { reactions, onOpenChange, onReplyPress, onEditPress, pinned } = props
|
||||
|
||||
useEffect(() => {
|
||||
return () => onOpenChange(false)
|
||||
|
@ -76,7 +77,7 @@ export const Actions = (props: Props) => {
|
|||
{/* OPTIONS MENU */}
|
||||
<DropdownMenu modal={false} onOpenChange={onOpenChange}>
|
||||
<IconButton variant="ghost" icon={<OptionsIcon />} />
|
||||
<DropdownMenu.Content align="end" sideOffset={10}>
|
||||
<DropdownMenu.Content align="end" sideOffset={10} zIndex={101}>
|
||||
<DropdownMenu.Item
|
||||
icon={<EditIcon />}
|
||||
label="Edit message"
|
||||
|
@ -92,11 +93,19 @@ export const Actions = (props: Props) => {
|
|||
label="Copy text"
|
||||
onSelect={() => console.log('copy')}
|
||||
/>
|
||||
{pinned ? (
|
||||
<DropdownMenu.Item
|
||||
icon={<PinIcon />}
|
||||
label="Unpin message"
|
||||
onSelect={() => console.log('unpin')}
|
||||
/>
|
||||
) : (
|
||||
<DropdownMenu.Item
|
||||
icon={<PinIcon />}
|
||||
label="Pin to the channel"
|
||||
onSelect={() => console.log('pin')}
|
||||
/>
|
||||
)}
|
||||
<DropdownMenu.Item
|
||||
icon={<ForwardIcon />}
|
||||
label="Forward"
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { PinAnnouncement } from '../system-messages'
|
||||
import { Message } from './message'
|
||||
|
||||
import type { ReactionsType } from './types'
|
||||
|
@ -16,16 +17,19 @@ export const Messages = () => {
|
|||
<Message
|
||||
text="Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit. Nullam sapien sem, ornare ac, nonummy non, lobortis a, enim. Nunc tincidunt ante vitae massa. Duis ante orci, molestie vitae, vehicula venenatis, tincidunt ac, pede. Nulla accumsan, elit sit"
|
||||
reactions={{}}
|
||||
id="1234-1234"
|
||||
/>
|
||||
<Message
|
||||
text="Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit. "
|
||||
reactions={{}}
|
||||
reply
|
||||
pinned
|
||||
id="1234-1235"
|
||||
/>
|
||||
<Message
|
||||
text="Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit. "
|
||||
reactions={{}}
|
||||
id="1234-1236"
|
||||
/>
|
||||
<Message
|
||||
images={[
|
||||
|
@ -34,33 +38,50 @@ export const Messages = () => {
|
|||
},
|
||||
]}
|
||||
reactions={{}}
|
||||
id="1234-1237"
|
||||
/>
|
||||
<Message
|
||||
text="Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit. Nullam sapien sem, ornare ac, nonummy non, lobortis a, enim. Nunc tincidunt ante vitae massa. Duis ante orci, molestie vitae, vehicula venenatis, tincidunt ac, pede. Nulla accumsan, elit sit"
|
||||
reactions={reactions}
|
||||
id="1234-1238"
|
||||
/>
|
||||
<Message
|
||||
text="Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam. "
|
||||
reactions={{}}
|
||||
pinned
|
||||
id="1234-1239"
|
||||
/>
|
||||
<PinAnnouncement
|
||||
name="Steve"
|
||||
message={{
|
||||
text: 'Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit.',
|
||||
reactions: {},
|
||||
reply: true,
|
||||
pinned: true,
|
||||
id: '1234-1235',
|
||||
}}
|
||||
/>
|
||||
<Message
|
||||
text="Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam. "
|
||||
reactions={{}}
|
||||
reply
|
||||
id="1234-1240"
|
||||
/>
|
||||
<Message
|
||||
text="Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam. "
|
||||
reactions={{}}
|
||||
id="1234-1241"
|
||||
/>
|
||||
<Message
|
||||
text="Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit. Nullam sapien sem, ornare ac, nonummy non, lobortis a, enim.sit"
|
||||
reactions={reactions}
|
||||
reply
|
||||
id="1234-1242"
|
||||
/>
|
||||
<Message
|
||||
text="Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit. Nullam sapien sem, ornare ac, nonummy non, lobortis a, enim.sit"
|
||||
reactions={reactions}
|
||||
id="1234-1243"
|
||||
/>
|
||||
<Message
|
||||
images={[
|
||||
|
@ -69,6 +90,7 @@ export const Messages = () => {
|
|||
},
|
||||
]}
|
||||
reactions={{}}
|
||||
id="1234-1244"
|
||||
/>
|
||||
<Message
|
||||
images={[
|
||||
|
@ -77,6 +99,7 @@ export const Messages = () => {
|
|||
},
|
||||
]}
|
||||
reactions={{}}
|
||||
id="1234-1245"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -14,7 +14,8 @@ import { Reactions } from './components/reactions'
|
|||
|
||||
import type { ReactionsType } from './types'
|
||||
|
||||
interface Props {
|
||||
export interface MessageProps {
|
||||
id: string
|
||||
text?: React.ReactNode
|
||||
images?: Array<{ url: string }>
|
||||
reactions: ReactionsType
|
||||
|
@ -44,7 +45,7 @@ const Base = styled(Stack, {
|
|||
} as const,
|
||||
})
|
||||
|
||||
const Message = (props: Props) => {
|
||||
const Message = (props: MessageProps) => {
|
||||
const { text, images, reactions, reply, pinned } = props
|
||||
|
||||
const [hovered, setHovered] = useState(false)
|
||||
|
@ -59,6 +60,7 @@ const Message = (props: Props) => {
|
|||
return (
|
||||
<Base
|
||||
active={active}
|
||||
pinned={pinned}
|
||||
onHoverIn={() => setHovered(true)}
|
||||
onHoverOut={() => setHovered(false)}
|
||||
>
|
||||
|
@ -69,6 +71,7 @@ const Message = (props: Props) => {
|
|||
onOpenChange={setShowActions}
|
||||
onReplyPress={() => dispatch({ type: 'reply', messageId: '1' })}
|
||||
onEditPress={() => dispatch({ type: 'edit', messageId: '1' })}
|
||||
pinned={pinned}
|
||||
/>
|
||||
</Unspaced>
|
||||
)}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export { PinnedMessage, type PinnedMessageProps } from './pinned-message'
|
|
@ -0,0 +1,36 @@
|
|||
import { PinnedMessage } from './pinned-message'
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
const mockMessages = [
|
||||
{
|
||||
text: 'Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit.',
|
||||
reactions: {},
|
||||
pinned: true,
|
||||
id: '1234-1234',
|
||||
},
|
||||
{
|
||||
text: 'Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam. message',
|
||||
reactions: {},
|
||||
pinned: true,
|
||||
id: '4321-4321',
|
||||
},
|
||||
]
|
||||
|
||||
const meta: Meta<typeof PinnedMessage> = {
|
||||
component: PinnedMessage,
|
||||
argTypes: {
|
||||
messages: mockMessages,
|
||||
},
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof PinnedMessage>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
messages: mockMessages,
|
||||
// children: 'Click me',
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
|
@ -0,0 +1,93 @@
|
|||
import { useState } from 'react'
|
||||
|
||||
import { PinIcon } from '@status-im/icons/20'
|
||||
import { styled } from '@tamagui/core'
|
||||
import { Pressable, View } from 'react-native'
|
||||
|
||||
import { Banner } from '../banner'
|
||||
import { Button } from '../button'
|
||||
import { ContextTag } from '../context-tag'
|
||||
import { Dialog } from '../dialog'
|
||||
import { Message } from '../messages'
|
||||
import { Text } from '../text'
|
||||
|
||||
import type { MessageProps } from '../messages'
|
||||
|
||||
type Props = {
|
||||
messages: MessageProps[]
|
||||
}
|
||||
|
||||
const PinnedMessage = (props: Props) => {
|
||||
const { messages } = props
|
||||
const [isDetailVisible, setIsDetailVisible] = useState(false)
|
||||
|
||||
return messages.length > 0 ? (
|
||||
<Dialog open={isDetailVisible}>
|
||||
<Pressable onPress={() => setIsDetailVisible(true)}>
|
||||
<Banner count={messages.length} icon={<PinIcon />}>
|
||||
{messages[0].text}
|
||||
</Banner>
|
||||
</Pressable>
|
||||
|
||||
<Base>
|
||||
<Button variant="grey" onPress={() => setIsDetailVisible(false)}>
|
||||
×
|
||||
</Button>
|
||||
<DialogHeader>
|
||||
<Text size={27} weight="semibold">
|
||||
Pinned Messages
|
||||
</Text>
|
||||
<ContextTag
|
||||
src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fi.seadn.io%2Fgae%2FFG0QJ00fN3c_FWuPeUr9-T__iQl63j9hn5d6svW8UqOmia5zp3lKHPkJuHcvhZ0f_Pd6P2COo9tt9zVUvdPxG_9BBw%3Fw%3D500%26auto%3Dformat&f=1&nofb=1&ipt=c177cd71d8d0114080cfc6efd3f9e098ddaeb1b347919bd3089bf0aacb003b3e&ipo=images"
|
||||
label={['Rarible', '# random']}
|
||||
/>
|
||||
</DialogHeader>
|
||||
<DialogContent>
|
||||
{messages.map(message => (
|
||||
<Message key={message.id} {...message} pinned={false} />
|
||||
))}
|
||||
</DialogContent>
|
||||
</Base>
|
||||
</Dialog>
|
||||
) : null
|
||||
}
|
||||
|
||||
export { PinnedMessage }
|
||||
export type { Props as PinnedMessageProps }
|
||||
|
||||
const Base = styled(View, {
|
||||
position: 'relative',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 16,
|
||||
borderRadius: 16,
|
||||
alignSelf: 'center',
|
||||
alignItems: 'flex-start',
|
||||
maxWidth: 480,
|
||||
backgroundColor: '$neutral-5',
|
||||
zIndex: 100,
|
||||
|
||||
variants: {
|
||||
active: {
|
||||
true: {
|
||||
backgroundColor: '$neutral-5',
|
||||
},
|
||||
},
|
||||
pinned: {
|
||||
true: {
|
||||
backgroundColor: '$blue-50-opa-5',
|
||||
},
|
||||
},
|
||||
} as const,
|
||||
})
|
||||
|
||||
const DialogHeader = styled(View, {
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'flex-start',
|
||||
paddingVertical: 16,
|
||||
space: 11,
|
||||
})
|
||||
|
||||
const DialogContent = styled(View, {
|
||||
alignItems: 'stretch',
|
||||
justifyContent: 'flex-start',
|
||||
})
|
|
@ -0,0 +1 @@
|
|||
export { PinAnnouncement } from './pin-announcement'
|
|
@ -0,0 +1,25 @@
|
|||
import { PinAnnouncement } from './pin-announcement'
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
const mockMessage = {
|
||||
text: 'Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit.',
|
||||
reactions: {},
|
||||
pinned: true,
|
||||
id: '1234-1234',
|
||||
}
|
||||
|
||||
const meta: Meta<typeof PinAnnouncement> = {
|
||||
component: PinAnnouncement,
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof PinAnnouncement>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
name: 'Pavel',
|
||||
message: mockMessage,
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
|
@ -0,0 +1,48 @@
|
|||
import { PinIcon } from '@status-im/icons/16'
|
||||
import { Stack } from '@tamagui/core'
|
||||
|
||||
import { Avatar, IconAvatar } from '../avatar'
|
||||
import { Text } from '../text'
|
||||
|
||||
import type { MessageProps } from '../messages'
|
||||
|
||||
type Props = {
|
||||
message: MessageProps
|
||||
name: string
|
||||
}
|
||||
|
||||
const PinAnnouncement = (props: Props) => {
|
||||
const { message, name } = props
|
||||
|
||||
return (
|
||||
<Stack flexDirection="row" space={8} padding={8}>
|
||||
<IconAvatar backgroundColor="$turquoise-50-opa-5" color="$neutral-100">
|
||||
<PinIcon />
|
||||
</IconAvatar>
|
||||
<Stack flexDirection="column" space={2}>
|
||||
<Stack flexDirection="row" space={4} alignItems="center">
|
||||
<Text size={13} weight="semibold">
|
||||
{name}
|
||||
</Text>
|
||||
<Text size={13}>pinned a message</Text>
|
||||
<Text size={11} color="$neutral-50">
|
||||
09:30
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack flexDirection="row" space={4}>
|
||||
<Avatar
|
||||
size={16}
|
||||
src="https://images.unsplash.com/photo-1524638431109-93d95c968f03?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=500&ixid=MnwxfDB8MXxyYW5kb218MHx8Z2lybHx8fHx8fDE2NzM4ODQ0NzU&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=500"
|
||||
/>
|
||||
<Text size={11} weight="semibold">
|
||||
Alisher Yakupov
|
||||
</Text>
|
||||
<Text size={11}>{message.text}</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export { PinAnnouncement }
|
||||
export type { Props as PinAnnouncementProps }
|
|
@ -16,10 +16,26 @@ import { BlurView } from 'expo-blur'
|
|||
import { Divider } from '../divider'
|
||||
import { DropdownMenu } from '../dropdown-menu'
|
||||
import { IconButton } from '../icon-button'
|
||||
import { PinnedMessage } from '../pinned-message'
|
||||
import { Text } from '../text'
|
||||
|
||||
import type { Channel } from '../sidebar/mock-data'
|
||||
|
||||
const mockMessages = [
|
||||
{
|
||||
text: 'Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit.',
|
||||
reactions: {},
|
||||
pinned: true,
|
||||
id: '1234-1234',
|
||||
},
|
||||
{
|
||||
text: 'Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam.',
|
||||
reactions: {},
|
||||
pinned: true,
|
||||
id: '4321-4321',
|
||||
},
|
||||
]
|
||||
|
||||
type Props = {
|
||||
showMembers: boolean
|
||||
onMembersPress: () => void
|
||||
|
@ -35,6 +51,7 @@ const Topbar = (props: Props) => {
|
|||
|
||||
return (
|
||||
<BlurView intensity={40} style={{ zIndex: 100 }}>
|
||||
<Stack flexDirection="column" width="100%" height={96}>
|
||||
<Stack
|
||||
flexDirection="row"
|
||||
height={56}
|
||||
|
@ -143,6 +160,8 @@ const Topbar = (props: Props) => {
|
|||
</DropdownMenu>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<PinnedMessage messages={mockMessages} />
|
||||
</Stack>
|
||||
</BlurView>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue