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:
Jakub Kotula 2023-03-30 13:32:26 +02:00 committed by GitHub
parent a167396062
commit c338bf7aae
No known key found for this signature in database
GPG Key ID: 0EB8D75C775AB6F1
25 changed files with 890 additions and 117 deletions

View File

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

View File

@ -17,7 +17,7 @@ type UseBlurProps = {
const useBlur = (props: UseBlurProps): UseBlurReturn => {
const {
marginBlurBottom = 32,
heightTop = 56,
heightTop = 96,
throttle = 100,
ref,
} = props || {}

View File

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

View File

@ -1 +1,2 @@
export * from './avatar'
export * from './icon-avatar'

View File

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

View File

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

View File

@ -0,0 +1 @@
export { Banner, type BannerProps } from './banner'

View File

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

View File

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

View File

@ -0,0 +1 @@
export { ContextTag, type ContextTagProps } from './context-tag'

View File

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

View File

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

View File

@ -0,0 +1 @@
export { Counter, type CounterProps } from './counter'

View File

@ -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,14 +61,16 @@ const Dialog = (props: Props) => {
{/* CONTENT */}
<Portal>
<Overlay
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
}}
/>
{content}
<Wrapper>
<Overlay
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
}}
/>
{content}
</Wrapper>
</Portal>
</Root>
)

View File

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

View File

@ -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')}
/>
<DropdownMenu.Item
icon={<PinIcon />}
label="Pin to the channel"
onSelect={() => console.log('pin')}
/>
{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"

View File

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

View File

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

View File

@ -0,0 +1 @@
export { PinnedMessage, type PinnedMessageProps } from './pinned-message'

View File

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

View File

@ -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)}>
&times;
</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',
})

View File

@ -0,0 +1 @@
export { PinAnnouncement } from './pin-announcement'

View File

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

View File

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

View File

@ -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,113 +51,116 @@ const Topbar = (props: Props) => {
return (
<BlurView intensity={40} style={{ zIndex: 100 }}>
<Stack
flexDirection="row"
height={56}
alignItems="center"
justifyContent="space-between"
padding={16}
backgroundColor={'$blurBackground'}
borderBottomWidth={1}
borderColor={blur ? 'transparent' : '$neutral-80-opa-10'}
width="100%"
>
<Stack flexDirection="row" alignItems="center" flexWrap="wrap">
<Stack mr={12} $gtSm={{ display: 'none' }}>
<IconButton
icon={<ArrowLeftIcon />}
onPress={() => goBack?.()}
blur={blur}
/>
</Stack>
{emoji && (
<Stack marginRight={12}>
<RNText>{emoji}</RNText>
</Stack>
)}
{title && (
<Text size={15} weight="semibold">
{title}
</Text>
)}
<LockedIcon color="$neutral-80-opa-40" />
<Divider height={16} $sm={{ display: 'none' }} />
</Stack>
<Stack flexDirection="column" width="100%" height={96}>
<Stack
space={12}
flexDirection="row"
height={56}
alignItems="center"
justifyContent={description ? 'space-between' : 'flex-end'}
flexGrow={1}
flexShrink={1}
$sm={{ justifyContent: 'flex-end' }}
justifyContent="space-between"
padding={16}
backgroundColor={'$blurBackground'}
borderBottomWidth={1}
borderColor={blur ? 'transparent' : '$neutral-80-opa-10'}
width="100%"
>
{description && (
<Stack flexGrow={1} flexShrink={1} $sm={{ display: 'none' }}>
<Text
weight="medium"
color="$neutral-80-opa-50"
size={13}
truncate
>
{description}
</Text>
<Stack flexDirection="row" alignItems="center" flexWrap="wrap">
<Stack mr={12} $gtSm={{ display: 'none' }}>
<IconButton
icon={<ArrowLeftIcon />}
onPress={() => goBack?.()}
blur={blur}
/>
</Stack>
)}
<Stack $sm={{ display: 'none' }}>
<IconButton
icon={<MembersIcon />}
selected={showMembers}
onPress={onMembersPress}
blur={blur}
/>
{emoji && (
<Stack marginRight={12}>
<RNText>{emoji}</RNText>
</Stack>
)}
{title && (
<Text size={15} weight="semibold">
{title}
</Text>
)}
<LockedIcon color="$neutral-80-opa-40" />
<Divider height={16} $sm={{ display: 'none' }} />
</Stack>
<DropdownMenu>
<IconButton icon={<OptionsIcon />} />
<Stack
space={12}
flexDirection="row"
alignItems="center"
justifyContent={description ? 'space-between' : 'flex-end'}
flexGrow={1}
flexShrink={1}
$sm={{ justifyContent: 'flex-end' }}
>
{description && (
<Stack flexGrow={1} flexShrink={1} $sm={{ display: 'none' }}>
<Text
weight="medium"
color="$neutral-80-opa-50"
size={13}
truncate
>
{description}
</Text>
</Stack>
)}
<Stack $sm={{ display: 'none' }}>
<IconButton
icon={<MembersIcon />}
selected={showMembers}
onPress={onMembersPress}
blur={blur}
/>
</Stack>
<DropdownMenu.Content align="end" sideOffset={4}>
<DropdownMenu.Item
icon={<CommunitiesIcon />}
label="View channel members and details"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Item
icon={<MutedIcon />}
label="Mute channel"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Item
icon={<UpToDateIcon />}
label="Mark as read"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Item
icon={<DownloadIcon />}
label="Fetch messages"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Item
icon={<ShareIcon />}
label="Share link to the channel"
onSelect={() => console.log('click')}
/>
<DropdownMenu>
<IconButton icon={<OptionsIcon />} />
<DropdownMenu.Separator />
<DropdownMenu.Content align="end" sideOffset={4}>
<DropdownMenu.Item
icon={<CommunitiesIcon />}
label="View channel members and details"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Item
icon={<MutedIcon />}
label="Mute channel"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Item
icon={<UpToDateIcon />}
label="Mark as read"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Item
icon={<DownloadIcon />}
label="Fetch messages"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Item
icon={<ShareIcon />}
label="Share link to the channel"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Item
icon={<DeleteIcon />}
label="Clear history"
onSelect={() => console.log('click')}
danger
/>
</DropdownMenu.Content>
</DropdownMenu>
<DropdownMenu.Separator />
<DropdownMenu.Item
icon={<DeleteIcon />}
label="Clear history"
onSelect={() => console.log('click')}
danger
/>
</DropdownMenu.Content>
</DropdownMenu>
</Stack>
</Stack>
<PinnedMessage messages={mockMessages} />
</Stack>
</BlurView>
)