diff --git a/apps/web/styles/app.css b/apps/web/styles/app.css index 2d2d2aa7..9b545c6d 100644 --- a/apps/web/styles/app.css +++ b/apps/web/styles/app.css @@ -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 { diff --git a/packages/components/hooks/use-blur.ts b/packages/components/hooks/use-blur.ts index 0905ae93..197ce512 100644 --- a/packages/components/hooks/use-blur.ts +++ b/packages/components/hooks/use-blur.ts @@ -17,7 +17,7 @@ type UseBlurProps = { const useBlur = (props: UseBlurProps): UseBlurReturn => { const { marginBlurBottom = 32, - heightTop = 56, + heightTop = 96, throttle = 100, ref, } = props || {} diff --git a/packages/components/src/avatar/icon-avatar.tsx b/packages/components/src/avatar/icon-avatar.tsx new file mode 100644 index 00000000..945617d2 --- /dev/null +++ b/packages/components/src/avatar/icon-avatar.tsx @@ -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 ( + + {cloneElement(children, { color })} + + ) +} + +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 } diff --git a/packages/components/src/avatar/index.tsx b/packages/components/src/avatar/index.tsx index 886c6ec3..0dac2863 100644 --- a/packages/components/src/avatar/index.tsx +++ b/packages/components/src/avatar/index.tsx @@ -1 +1,2 @@ export * from './avatar' +export * from './icon-avatar' diff --git a/packages/components/src/banner/banner.stories.tsx b/packages/components/src/banner/banner.stories.tsx new file mode 100644 index 00000000..3a78021f --- /dev/null +++ b/packages/components/src/banner/banner.stories.tsx @@ -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 = { + component: Banner, + argTypes: { + children: { + control: 'text', + }, + }, +} + +type Story = StoryObj + +export const Full: Story = { + args: { + icon: , + children: 'Banner message', + count: 5, + }, +} + +export const NoIcon: Story = { + args: { + children: 'Banner message', + count: 5, + }, +} + +export const NoCount: Story = { + args: { + icon: , + children: 'Banner message', + }, +} + +export const AllVariants: Story = { + args: {}, + render: () => ( + + } count={5}> + Banner message + + Banner message + }>Banner message + + ), +} + +export default meta diff --git a/packages/components/src/banner/banner.tsx b/packages/components/src/banner/banner.tsx new file mode 100644 index 00000000..0b1de159 --- /dev/null +++ b/packages/components/src/banner/banner.tsx @@ -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 ( + + + {icon} + + {children} + + + {count ? : null} + + ) +} + +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', +}) diff --git a/packages/components/src/banner/index.tsx b/packages/components/src/banner/index.tsx new file mode 100644 index 00000000..b7074b39 --- /dev/null +++ b/packages/components/src/banner/index.tsx @@ -0,0 +1 @@ +export { Banner, type BannerProps } from './banner' diff --git a/packages/components/src/context-tag/context-tag.stories.tsx b/packages/components/src/context-tag/context-tag.stories.tsx new file mode 100644 index 00000000..19c948f1 --- /dev/null +++ b/packages/components/src/context-tag/context-tag.stories.tsx @@ -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 = { + component: ContextTag, + argTypes: { + label: { + control: ['Rarible', '# channel-name'], + }, + size: [24, 32], + outline: [true, false], + blur: [true, false], + }, +} + +type Story = StoryObj + +export const Base: Story = { + args: { label: 'Name', size: 24, outline: false, blur: false }, +} + +export const AllVariants: Story = { + args: {}, + render: () => ( + + + + + + + + + + + + } + type="icon" + label="Context" + outline + /> + + + + ), +} + +export default meta diff --git a/packages/components/src/context-tag/context-tag.tsx b/packages/components/src/context-tag/context-tag.tsx new file mode 100644 index 00000000..5f4711ee --- /dev/null +++ b/packages/components/src/context-tag/context-tag.tsx @@ -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, TextProps['size']> = { + '32': 15, + '24': 13, +} + +const avatarSizes: Record, AvatarProps['size']> = { + '32': 28, + '24': 20, +} + +const Label = ({ children, size }: { children: string; size: 24 | 32 }) => ( + + {children} + +) + +const ContextTag = (props: Props) => { + const { + src, + icon, + label, + type = 'default', + size = 24, + blur = false, + outline, + } = props + + const hasImg = Boolean(src || icon) + + return ( + + {src && } + {icon && cloneElement(icon, { color: '$neutral-50' })} + + {Array.isArray(label) ? ( + label.map((item, i) => { + if (i !== 0) { + return ( + + + + + ) + } else { + return ( + + ) + } + }) + ) : ( + + )} + + ) +} + +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, +}) diff --git a/packages/components/src/context-tag/index.tsx b/packages/components/src/context-tag/index.tsx new file mode 100644 index 00000000..43417b05 --- /dev/null +++ b/packages/components/src/context-tag/index.tsx @@ -0,0 +1 @@ +export { ContextTag, type ContextTagProps } from './context-tag' diff --git a/packages/components/src/counter/counter.stories.tsx b/packages/components/src/counter/counter.stories.tsx new file mode 100644 index 00000000..1f56fc7d --- /dev/null +++ b/packages/components/src/counter/counter.stories.tsx @@ -0,0 +1,80 @@ +import { Stack } from '@tamagui/core' + +import { Counter } from './counter' + +import type { Meta, StoryObj } from '@storybook/react' + +const meta: Meta = { + component: Counter, + argTypes: { + value: { + control: { + type: 'number', + min: 0, + max: 1000, + }, + }, + type: { + control: 'select', + options: ['default', 'secondary', 'grey', 'outline'], + }, + }, +} + +type Story = StoryObj + +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: () => ( + + + + + + + + + + + + + + + + + + + + + ), +} + +export default meta diff --git a/packages/components/src/counter/counter.tsx b/packages/components/src/counter/counter.tsx new file mode 100644 index 00000000..89c4d514 --- /dev/null +++ b/packages/components/src/counter/counter.tsx @@ -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 ( + + + {value > 99 ? '99+' : value} + + + ) +} + +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, ColorTokens> = { + default: '$white-100', + secondary: '$neutral-100', + outline: '$neutral-100', + grey: '$neutral-100', +} diff --git a/packages/components/src/counter/index.tsx b/packages/components/src/counter/index.tsx new file mode 100644 index 00000000..5793f948 --- /dev/null +++ b/packages/components/src/counter/index.tsx @@ -0,0 +1 @@ +export { Counter, type CounterProps } from './counter' diff --git a/packages/components/src/dialog/dialog.tsx b/packages/components/src/dialog/dialog.tsx index 2f73427b..fa7b6ba9 100644 --- a/packages/components/src/dialog/dialog.tsx +++ b/packages/components/src/dialog/dialog.tsx @@ -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 */} - - {content} + + + {content} + ) diff --git a/packages/components/src/index.tsx b/packages/components/src/index.tsx index b878f49c..6d047f78 100644 --- a/packages/components/src/index.tsx +++ b/packages/components/src/index.tsx @@ -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' diff --git a/packages/components/src/messages/components/actions.tsx b/packages/components/src/messages/components/actions.tsx index a5cb8342..e79086aa 100644 --- a/packages/components/src/messages/components/actions.tsx +++ b/packages/components/src/messages/components/actions.tsx @@ -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 */} } /> - + } label="Edit message" @@ -92,11 +93,19 @@ export const Actions = (props: Props) => { label="Copy text" onSelect={() => console.log('copy')} /> - } - label="Pin to the channel" - onSelect={() => console.log('pin')} - /> + {pinned ? ( + } + label="Unpin message" + onSelect={() => console.log('unpin')} + /> + ) : ( + } + label="Pin to the channel" + onSelect={() => console.log('pin')} + /> + )} } label="Forward" diff --git a/packages/components/src/messages/index.tsx b/packages/components/src/messages/index.tsx index a08371bf..f0e44e07 100644 --- a/packages/components/src/messages/index.tsx +++ b/packages/components/src/messages/index.tsx @@ -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 = () => { { }, ]} reactions={{}} + id="1234-1237" /> + { }, ]} reactions={{}} + id="1234-1244" /> { }, ]} reactions={{}} + id="1234-1245" /> ) diff --git a/packages/components/src/messages/message.tsx b/packages/components/src/messages/message.tsx index f098c271..beb6f09f 100644 --- a/packages/components/src/messages/message.tsx +++ b/packages/components/src/messages/message.tsx @@ -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 ( 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} /> )} diff --git a/packages/components/src/pinned-message/index.tsx b/packages/components/src/pinned-message/index.tsx new file mode 100644 index 00000000..69270e89 --- /dev/null +++ b/packages/components/src/pinned-message/index.tsx @@ -0,0 +1 @@ +export { PinnedMessage, type PinnedMessageProps } from './pinned-message' diff --git a/packages/components/src/pinned-message/pinned-message.stories.tsx b/packages/components/src/pinned-message/pinned-message.stories.tsx new file mode 100644 index 00000000..e9b11ac9 --- /dev/null +++ b/packages/components/src/pinned-message/pinned-message.stories.tsx @@ -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 = { + component: PinnedMessage, + argTypes: { + messages: mockMessages, + }, +} + +type Story = StoryObj + +export const Primary: Story = { + args: { + messages: mockMessages, + // children: 'Click me', + }, +} + +export default meta diff --git a/packages/components/src/pinned-message/pinned-message.tsx b/packages/components/src/pinned-message/pinned-message.tsx new file mode 100644 index 00000000..010cf897 --- /dev/null +++ b/packages/components/src/pinned-message/pinned-message.tsx @@ -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 ? ( + + setIsDetailVisible(true)}> + }> + {messages[0].text} + + + + + + + + Pinned Messages + + + + + {messages.map(message => ( + + ))} + + + + ) : 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', +}) diff --git a/packages/components/src/system-messages/index.ts b/packages/components/src/system-messages/index.ts new file mode 100644 index 00000000..63e89270 --- /dev/null +++ b/packages/components/src/system-messages/index.ts @@ -0,0 +1 @@ +export { PinAnnouncement } from './pin-announcement' diff --git a/packages/components/src/system-messages/pin-announcement.stories.tsx b/packages/components/src/system-messages/pin-announcement.stories.tsx new file mode 100644 index 00000000..c5d659eb --- /dev/null +++ b/packages/components/src/system-messages/pin-announcement.stories.tsx @@ -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 = { + component: PinAnnouncement, +} + +type Story = StoryObj + +export const Primary: Story = { + args: { + name: 'Pavel', + message: mockMessage, + }, +} + +export default meta diff --git a/packages/components/src/system-messages/pin-announcement.tsx b/packages/components/src/system-messages/pin-announcement.tsx new file mode 100644 index 00000000..fab4f65e --- /dev/null +++ b/packages/components/src/system-messages/pin-announcement.tsx @@ -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 ( + + + + + + + + {name} + + pinned a message + + 09:30 + + + + + + Alisher Yakupov + + {message.text} + + + + ) +} + +export { PinAnnouncement } +export type { Props as PinAnnouncementProps } diff --git a/packages/components/src/topbar/topbar.tsx b/packages/components/src/topbar/topbar.tsx index 51adc21f..f6fe642c 100644 --- a/packages/components/src/topbar/topbar.tsx +++ b/packages/components/src/topbar/topbar.tsx @@ -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 ( - - - - } - onPress={() => goBack?.()} - blur={blur} - /> - - - {emoji && ( - - {emoji} - - )} - - {title && ( - - {title} - - )} - - - - - + - {description && ( - - - {description} - + + + } + onPress={() => goBack?.()} + blur={blur} + /> - )} - - } - selected={showMembers} - onPress={onMembersPress} - blur={blur} - /> + + {emoji && ( + + {emoji} + + )} + + {title && ( + + {title} + + )} + + + - - } /> + + {description && ( + + + {description} + + + )} + + } + selected={showMembers} + onPress={onMembersPress} + blur={blur} + /> + - - } - label="View channel members and details" - onSelect={() => console.log('click')} - /> - } - label="Mute channel" - onSelect={() => console.log('click')} - /> - } - label="Mark as read" - onSelect={() => console.log('click')} - /> - } - label="Fetch messages" - onSelect={() => console.log('click')} - /> - } - label="Share link to the channel" - onSelect={() => console.log('click')} - /> + + } /> - + + } + label="View channel members and details" + onSelect={() => console.log('click')} + /> + } + label="Mute channel" + onSelect={() => console.log('click')} + /> + } + label="Mark as read" + onSelect={() => console.log('click')} + /> + } + label="Fetch messages" + onSelect={() => console.log('click')} + /> + } + label="Share link to the channel" + onSelect={() => console.log('click')} + /> - } - label="Clear history" - onSelect={() => console.log('click')} - danger - /> - - + + + } + label="Clear history" + onSelect={() => console.log('click')} + danger + /> + + + + )