Skip to content

Commit f42ea79

Browse files
authoredMar 11, 2024
chore(comments): add support for comments layout avatar config (#5944)
* chore(comments): add support for comments layout avatar config * chore(comments): remove show prop from SpacerAvatar * fix(comments): nitpicks * fix(comments): add min height to the comment list item header
1 parent 2d4f60c commit f42ea79

File tree

6 files changed

+150
-72
lines changed

6 files changed

+150
-72
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import type * as React from 'react'
2-
3-
export const AVATAR_HEIGHT = 25
4-
5-
const INLINE_STYLE: React.CSSProperties = {
6-
minWidth: AVATAR_HEIGHT,
7-
}
1+
import {type AvatarSize} from '@sanity/ui'
2+
// eslint-disable-next-line camelcase
3+
import {getTheme_v2} from '@sanity/ui/theme'
4+
import styled, {css} from 'styled-components'
85

96
/**
107
* This component is used to as a spacer in situations where we want to align
118
* components without avatars with components that have avatars.
129
*/
13-
export function SpacerAvatar() {
14-
return <div style={INLINE_STYLE} />
15-
}
10+
export const SpacerAvatar = styled.div<{$size?: AvatarSize}>((props) => {
11+
const theme = getTheme_v2(props.theme)
12+
const {$size = 1} = props
13+
return css`
14+
min-width: ${theme.avatar.sizes[$size]?.size}px;
15+
`
16+
})

‎packages/sanity/src/structure/comments/src/components/list/CommentsListItem.tsx

+24-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {ChevronDownIcon} from '@sanity/icons'
22
import {type CurrentUser} from '@sanity/types'
3-
import {Flex, Stack, useLayer} from '@sanity/ui'
3+
import {type AvatarSize, Flex, Stack, useLayer} from '@sanity/ui'
44
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
55
import {type UserListWithPermissionsHookValue, useTranslation} from 'sanity'
66
import styled, {css} from 'styled-components'
@@ -28,6 +28,13 @@ const EMPTY_ARRAY: [] = []
2828

2929
const MAX_COLLAPSED_REPLIES = 5
3030

31+
const DEFAULT_AVATAR_CONFIG: CommentsListItemProps['avatarConfig'] = {
32+
avatarSize: 1,
33+
parentCommentAvatar: true,
34+
replyAvatar: true,
35+
threadCommentsAvatar: true,
36+
}
37+
3138
// data-active = when the comment is selected
3239
// data-hovered = when the mouse is over the comment
3340
const StyledThreadCard = styled(ThreadCard)(() => {
@@ -75,6 +82,12 @@ const GhostButton = styled.button`
7582
`
7683

7784
interface CommentsListItemProps {
85+
avatarConfig?: {
86+
avatarSize: AvatarSize
87+
parentCommentAvatar: boolean
88+
replyAvatar: boolean
89+
threadCommentsAvatar: boolean
90+
}
7891
canReply?: boolean
7992
currentUser: CurrentUser
8093
hasReferencedValue?: boolean
@@ -97,6 +110,7 @@ interface CommentsListItemProps {
97110

98111
export const CommentsListItem = React.memo(function CommentsListItem(props: CommentsListItemProps) {
99112
const {
113+
avatarConfig = DEFAULT_AVATAR_CONFIG,
100114
canReply,
101115
currentUser,
102116
hasReferencedValue,
@@ -242,12 +256,14 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm
242256
splicedReplies.map((reply) => (
243257
<Stack as="li" key={reply._id} {...applyCommentIdAttr(reply._id)}>
244258
<CommentsListItemLayout
259+
avatarSize={avatarConfig.avatarSize}
245260
canDelete={reply.authorId === currentUser.id}
246261
canEdit={reply.authorId === currentUser.id}
247262
comment={reply}
248263
currentUser={currentUser}
249264
hasError={reply._state?.type === 'createError'}
250265
isRetrying={reply._state?.type === 'createRetrying'}
266+
intent={commentIntentIfDiffers(parentComment, reply)}
251267
mentionOptions={mentionOptions}
252268
mode={mode}
253269
onCopyLink={onCopyLink}
@@ -257,11 +273,13 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm
257273
onInputKeyDown={handleInputKeyDown}
258274
onReactionSelect={onReactionSelect}
259275
readOnly={readOnly}
260-
intent={commentIntentIfDiffers(parentComment, reply)}
276+
withAvatar={avatarConfig.threadCommentsAvatar}
261277
/>
262278
</Stack>
263279
)),
264280
[
281+
avatarConfig.threadCommentsAvatar,
282+
avatarConfig.avatarSize,
265283
currentUser,
266284
handleInputKeyDown,
267285
mentionOptions,
@@ -300,6 +318,7 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm
300318
>
301319
<Stack as="li" {...applyCommentIdAttr(parentComment._id)}>
302320
<CommentsListItemLayout
321+
avatarSize={avatarConfig.avatarSize}
303322
canDelete={parentComment.authorId === currentUser.id}
304323
canEdit={parentComment.authorId === currentUser.id}
305324
comment={parentComment}
@@ -319,6 +338,7 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm
319338
onReactionSelect={onReactionSelect}
320339
onStatusChange={onStatusChange}
321340
readOnly={readOnly}
341+
withAvatar={avatarConfig.parentCommentAvatar}
322342
/>
323343
</Stack>
324344

@@ -339,6 +359,7 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm
339359

340360
{canReply && (
341361
<CommentInput
362+
avatarSize={avatarConfig.avatarSize}
342363
currentUser={currentUser}
343364
expandOnFocus
344365
mentionOptions={mentionOptions}
@@ -355,6 +376,7 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm
355376
readOnly={readOnly || mode === 'upsell'}
356377
ref={replyInputRef}
357378
value={value}
379+
withAvatar={avatarConfig.replyAvatar}
358380
/>
359381
)}
360382
</Stack>

‎packages/sanity/src/structure/comments/src/components/list/CommentsListItemContextMenu.tsx

+43-41
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export function CommentsListItemContextMenu(props: CommentsListItemContextMenuPr
8181

8282
const {t} = useTranslation(commentsLocaleNamespace)
8383

84+
const hasContextMenuOptions = Boolean(canDelete || canEdit || onCopyLink)
8485
return (
8586
<TooltipDelayGroupProvider>
8687
<Flex>
@@ -117,51 +118,52 @@ export function CommentsListItemContextMenu(props: CommentsListItemContextMenuPr
117118
}}
118119
/>
119120
)}
120-
121-
<MenuButton
122-
id="comment-actions-menu"
123-
button={
124-
<ContextMenuButton
125-
aria-label={t('list-item.open-menu-aria-label')}
126-
disabled={readOnly}
127-
hidden={!showMenuButton}
128-
/>
129-
}
130-
onOpen={onMenuOpen}
131-
onClose={onMenuClose}
132-
menu={
133-
<Menu>
134-
<MenuItem
135-
hidden={!canEdit}
136-
icon={EditIcon}
137-
onClick={onEditStart}
138-
text={t('list-item.edit-comment')}
139-
tooltipProps={
140-
mode === 'upsell' ? {content: t('list-item.edit-comment-upsell')} : undefined
141-
}
142-
disabled={mode === 'upsell'}
121+
{hasContextMenuOptions && (
122+
<MenuButton
123+
id="comment-actions-menu"
124+
button={
125+
<ContextMenuButton
126+
aria-label={t('list-item.open-menu-aria-label')}
127+
disabled={readOnly}
128+
hidden={!showMenuButton}
143129
/>
130+
}
131+
onOpen={onMenuOpen}
132+
onClose={onMenuClose}
133+
menu={
134+
<Menu>
135+
<MenuItem
136+
hidden={!canEdit}
137+
icon={EditIcon}
138+
onClick={onEditStart}
139+
text={t('list-item.edit-comment')}
140+
tooltipProps={
141+
mode === 'upsell' ? {content: t('list-item.edit-comment-upsell')} : undefined
142+
}
143+
disabled={mode === 'upsell'}
144+
/>
144145

145-
<MenuItem
146-
hidden={!canDelete}
147-
icon={TrashIcon}
148-
onClick={onDeleteStart}
149-
text={t('list-item.delete-comment')}
150-
tone="critical"
151-
/>
146+
<MenuItem
147+
hidden={!canDelete}
148+
icon={TrashIcon}
149+
onClick={onDeleteStart}
150+
text={t('list-item.delete-comment')}
151+
tone="critical"
152+
/>
152153

153-
<MenuDivider hidden={!canDelete && !canEdit} />
154+
<MenuDivider hidden={!canDelete && !canEdit} />
154155

155-
<MenuItem
156-
hidden={!onCopyLink}
157-
icon={LinkIcon}
158-
onClick={onCopyLink}
159-
text={t('list-item.copy-link')}
160-
/>
161-
</Menu>
162-
}
163-
popover={POPOVER_PROPS}
164-
/>
156+
<MenuItem
157+
hidden={!onCopyLink}
158+
icon={LinkIcon}
159+
onClick={onCopyLink}
160+
text={t('list-item.copy-link')}
161+
/>
162+
</Menu>
163+
}
164+
popover={POPOVER_PROPS}
165+
/>
166+
)}
165167
</FloatingCard>
166168
</Flex>
167169
</TooltipDelayGroupProvider>

‎packages/sanity/src/structure/comments/src/components/list/CommentsListItemLayout.tsx

+41-14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
import {hues} from '@sanity/color'
22
import {type CurrentUser} from '@sanity/types'
3-
import {Box, Card, Flex, Stack, Text, TextSkeleton, useClickOutside} from '@sanity/ui'
3+
import {
4+
type AvatarSize,
5+
Box,
6+
Card,
7+
Flex,
8+
Stack,
9+
Text,
10+
TextSkeleton,
11+
useClickOutside,
12+
} from '@sanity/ui'
13+
// eslint-disable-next-line camelcase
14+
import {getTheme_v2} from '@sanity/ui/theme'
415
import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
516
import {
617
type RelativeTimeOptions,
@@ -26,7 +37,7 @@ import {
2637
type CommentsUIMode,
2738
type CommentUpdatePayload,
2839
} from '../../types'
29-
import {AVATAR_HEIGHT, CommentsAvatar, SpacerAvatar} from '../avatars'
40+
import {CommentsAvatar, SpacerAvatar} from '../avatars'
3041
import {FLEX_GAP} from '../constants'
3142
import {CommentMessageSerializer} from '../pte'
3243
import {CommentInput, type CommentInputHandle} from '../pte/comment-input'
@@ -53,6 +64,14 @@ const TimeText = styled(Text)(({theme}) => {
5364
`
5465
})
5566

67+
const HeaderFlex = styled(Flex)<{$size: AvatarSize}>((props) => {
68+
const theme = getTheme_v2(props.theme)
69+
70+
return css`
71+
min-height: ${theme.avatar.sizes[props.$size]?.size}px;
72+
`
73+
})
74+
5675
const IntentText = styled(Text)(({theme}) => {
5776
const isDark = theme.sanity.color.dark
5877
const fg = hues.gray[isDark ? 200 : 800].hex
@@ -72,9 +91,13 @@ const InnerStack = styled(Stack)`
7291
}
7392
`
7493

75-
const ErrorFlex = styled(Flex)`
76-
min-height: ${AVATAR_HEIGHT}px;
77-
`
94+
const ErrorFlex = styled(Flex)<{$size: AvatarSize}>((props) => {
95+
const theme = getTheme_v2(props.theme)
96+
97+
return css`
98+
min-height: ${theme.avatar.sizes[props.$size]?.size}px;
99+
`
100+
})
78101

79102
const RetryCardButton = styled(Card)`
80103
// Add not on hover
@@ -122,6 +145,7 @@ const RootStack = styled(Stack)(({theme}) => {
122145
})
123146

124147
interface CommentsListItemLayoutProps {
148+
avatarSize?: AvatarSize
125149
canDelete?: boolean
126150
canEdit?: boolean
127151
comment: CommentDocument
@@ -141,12 +165,14 @@ interface CommentsListItemLayoutProps {
141165
onReactionSelect?: (id: string, reaction: CommentReactionOption) => void
142166
onStatusChange?: (id: string, status: CommentStatus) => void
143167
readOnly?: boolean
168+
withAvatar?: boolean
144169
}
145170

146171
const RELATIVE_TIME_OPTIONS: RelativeTimeOptions = {useTemporalPhrase: true}
147172

148173
export function CommentsListItemLayout(props: CommentsListItemLayoutProps) {
149174
const {
175+
avatarSize = 1,
150176
canDelete,
151177
canEdit,
152178
comment,
@@ -166,6 +192,7 @@ export function CommentsListItemLayout(props: CommentsListItemLayoutProps) {
166192
onReactionSelect,
167193
onStatusChange,
168194
readOnly,
195+
withAvatar = true,
169196
} = props
170197
const {_createdAt, authorId, message, _id, lastEditedAt} = comment
171198
const [user] = useUser(authorId)
@@ -324,8 +351,8 @@ export function CommentsListItemLayout(props: CommentsListItemLayoutProps) {
324351
space={4}
325352
>
326353
<InnerStack space={1} data-muted={displayError}>
327-
<Flex align="center" gap={FLEX_GAP} flex={1}>
328-
<CommentsAvatar user={user} />
354+
<HeaderFlex align="center" gap={FLEX_GAP} flex={1} $size={avatarSize}>
355+
{withAvatar && <CommentsAvatar user={user} size={avatarSize} />}
329356

330357
<Flex direction="column" gap={2} paddingY={intent ? 2 : 0}>
331358
<Flex
@@ -397,11 +424,11 @@ export function CommentsListItemLayout(props: CommentsListItemLayoutProps) {
397424
/>
398425
</ContextMenuBox>
399426
)}
400-
</Flex>
427+
</HeaderFlex>
401428

402429
{isTextSelectionComment(comment) && Boolean(comment?.contentSnapshot) && (
403430
<Flex gap={FLEX_GAP} marginBottom={3}>
404-
<SpacerAvatar />
431+
{withAvatar && <SpacerAvatar $size={avatarSize} />}
405432

406433
<CommentsListItemReferencedValue
407434
hasReferencedValue={hasReferencedValue}
@@ -412,7 +439,7 @@ export function CommentsListItemLayout(props: CommentsListItemLayoutProps) {
412439

413440
{isEditing && (
414441
<Flex align="flex-start" gap={2}>
415-
<SpacerAvatar />
442+
{withAvatar && <SpacerAvatar $size={avatarSize} />}
416443

417444
<Stack flex={1}>
418445
<CommentInput
@@ -435,15 +462,15 @@ export function CommentsListItemLayout(props: CommentsListItemLayoutProps) {
435462

436463
{!isEditing && (
437464
<Flex gap={FLEX_GAP}>
438-
<SpacerAvatar />
465+
{withAvatar && <SpacerAvatar $size={avatarSize} />}
439466

440467
<CommentMessageSerializer blocks={message} />
441468
</Flex>
442469
)}
443470

444471
{hasReactions && (
445472
<Flex gap={FLEX_GAP} marginTop={2}>
446-
<SpacerAvatar />
473+
{withAvatar && <SpacerAvatar $size={avatarSize} />}
447474

448475
<Box onClick={stopPropagation}>
449476
<CommentReactionsBar
@@ -459,8 +486,8 @@ export function CommentsListItemLayout(props: CommentsListItemLayoutProps) {
459486
</InnerStack>
460487

461488
{displayError && (
462-
<ErrorFlex gap={FLEX_GAP}>
463-
<SpacerAvatar />
489+
<ErrorFlex gap={FLEX_GAP} $size={avatarSize}>
490+
{withAvatar && <SpacerAvatar $size={avatarSize} />}
464491

465492
<Flex align="center" gap={1} flex={1}>
466493
<Text muted size={1}>

‎packages/sanity/src/structure/comments/src/components/pte/comment-input/CommentInput.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {type EditorChange, keyGenerator, PortableTextEditor} from '@sanity/portable-text-editor'
22
import {type CurrentUser, type PortableTextBlock} from '@sanity/types'
3-
import {focusFirstDescendant, focusLastDescendant, Stack} from '@sanity/ui'
3+
import {type AvatarSize, focusFirstDescendant, focusLastDescendant, Stack} from '@sanity/ui'
44
import type * as React from 'react'
55
import {forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState} from 'react'
66
import {type UserListWithPermissionsHookValue} from 'sanity'
@@ -36,6 +36,7 @@ export interface CommentInputProps {
3636
readOnly?: boolean
3737
value: PortableTextBlock[] | null
3838
withAvatar?: boolean
39+
avatarSize?: AvatarSize
3940
}
4041

4142
interface CommentDiscardDialogController {
@@ -58,6 +59,7 @@ export interface CommentInputHandle {
5859
export const CommentInput = forwardRef<CommentInputHandle, CommentInputProps>(
5960
function CommentInput(props, ref) {
6061
const {
62+
avatarSize,
6163
currentUser,
6264
expandOnFocus,
6365
focusLock = false,
@@ -221,6 +223,7 @@ export const CommentInput = forwardRef<CommentInputHandle, CommentInputProps>(
221223

222224
<Stack ref={innerRef}>
223225
<CommentInputInner
226+
avatarSize={avatarSize}
224227
currentUser={currentUser}
225228
focusLock={focusLock}
226229
onBlur={onBlur}

‎packages/sanity/src/structure/comments/src/components/pte/comment-input/CommentInputInner.tsx

+27-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {type CurrentUser} from '@sanity/types'
2-
import {Box, Card, Flex, MenuDivider, Stack} from '@sanity/ui'
2+
import {type AvatarSize, Box, Card, Flex, MenuDivider, Stack} from '@sanity/ui'
33
// eslint-disable-next-line camelcase
44
import {getTheme_v2} from '@sanity/ui/theme'
55
import {useCallback} from 'react'
@@ -81,7 +81,17 @@ const RootCard = styled(Card)(({theme}) => {
8181
`
8282
})
8383

84+
const AvatarContainer = styled.div((props) => {
85+
const theme = getTheme_v2(props.theme)
86+
return `
87+
min-height: ${theme.avatar.sizes[1]?.size}px;
88+
display: flex;
89+
align-items: center;
90+
`
91+
})
92+
8493
interface CommentInputInnerProps {
94+
avatarSize?: AvatarSize
8595
currentUser: CurrentUser
8696
focusLock?: boolean
8797
onBlur?: (e: React.FormEvent<HTMLDivElement>) => void
@@ -93,15 +103,28 @@ interface CommentInputInnerProps {
93103
}
94104

95105
export function CommentInputInner(props: CommentInputInnerProps) {
96-
const {currentUser, focusLock, onBlur, onFocus, onKeyDown, onSubmit, placeholder, withAvatar} =
97-
props
106+
const {
107+
avatarSize = 1,
108+
currentUser,
109+
focusLock,
110+
onBlur,
111+
onFocus,
112+
onKeyDown,
113+
onSubmit,
114+
placeholder,
115+
withAvatar,
116+
} = props
98117

99118
const [user] = useUser(currentUser.id)
100119
const {canSubmit, expandOnFocus, focused, hasChanges, insertAtChar, openMentions, readOnly} =
101120
useCommentInput()
102121

103122
const {t} = useTranslation(commentsLocaleNamespace)
104-
const avatar = withAvatar ? <CommentsAvatar user={user} /> : null
123+
const avatar = withAvatar ? (
124+
<AvatarContainer>
125+
<CommentsAvatar user={user} size={avatarSize} />
126+
</AvatarContainer>
127+
) : null
105128

106129
const handleMentionButtonClicked = useCallback(
107130
(e: React.MouseEvent<HTMLButtonElement>) => {

0 commit comments

Comments
 (0)
Please sign in to comment.