Skip to content

Commit 3af135f

Browse files
authored
feat: implement pinned message redesign (#2952)
1 parent fc547be commit 3af135f

19 files changed

Lines changed: 98 additions & 50 deletions

File tree

src/components/Message/MessageSimple.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { useChatContext, useTranslationContext } from '../../context';
4141
import { MessageEditedTimestamp } from './MessageEditedTimestamp';
4242

4343
import type { MessageUIComponentProps } from './types';
44+
import { PinIndicator as DefaultPinIndicator } from './PinIndicator';
4445
import { QuotedMessage as DefaultQuotedMessage } from './QuotedMessage';
4546

4647
type MessageSimpleWithContextProps = MessageContextValue;
@@ -79,7 +80,7 @@ const MessageSimpleWithContext = (props: MessageSimpleWithContextProps) => {
7980
MessageRepliesCountButton = DefaultMessageRepliesCountButton,
8081
MessageStatus = DefaultMessageStatus,
8182
MessageTimestamp = DefaultMessageTimestamp,
82-
PinIndicator,
83+
PinIndicator = DefaultPinIndicator,
8384
QuotedMessage = DefaultQuotedMessage,
8485
ReactionsList = DefaultReactionList,
8586
ReminderNotification = DefaultReminderNotification,
@@ -151,7 +152,7 @@ const MessageSimpleWithContext = (props: MessageSimpleWithContextProps) => {
151152
'str-chat__message--has-single-attachment': hasSingleAttachment,
152153
'str-chat__message--highlighted': highlighted,
153154
'str-chat__message--is-emoji-only': textHasEmojisOnly,
154-
'str-chat__message--pinned pinned-message': message.pinned,
155+
'str-chat__message--pinned': message.pinned,
155156
'str-chat__message--with-reactions': hasReactions,
156157
'str-chat__message-send-can-be-retried':
157158
message?.status === 'failed' && message?.error?.status !== 403,
@@ -175,7 +176,7 @@ const MessageSimpleWithContext = (props: MessageSimpleWithContextProps) => {
175176
)}
176177
{
177178
<div className={rootClassName} key={message.id}>
178-
{PinIndicator && <PinIndicator />}
179+
{message.pinned && <PinIndicator message={message} />}
179180
{!!reminder && <ReminderNotification reminder={reminder} />}
180181
{message.user && (
181182
<Avatar
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
3+
import { IconPin } from '../Icons';
4+
import { useTranslationContext } from '../../context';
5+
import type { LocalMessage } from 'stream-chat';
6+
7+
export type PinIndicatorProps = {
8+
message?: LocalMessage;
9+
};
10+
11+
/**
12+
* Default pinned message indicator. Renders "Pinned by [name]" with pin icon above the message bubble.
13+
* Name is taken from message.pinned_by (who pinned).
14+
*/
15+
export const PinIndicator = ({ message }: PinIndicatorProps) => {
16+
const { t } = useTranslationContext();
17+
18+
if (!message) return null;
19+
20+
const name = message.pinned_by?.name ?? message.pinned_by?.id ?? '';
21+
22+
const label = name ? t('Pinned by {{ name }}', { name }) : t('Message pinned');
23+
24+
return (
25+
<div className='str-chat__message-pin-indicator'>
26+
<div className='str-chat__message-pin-indicator__content'>
27+
<span aria-hidden className='str-chat__message-pin-indicator__icon'>
28+
<IconPin />
29+
</span>
30+
<span>{label}</span>
31+
</div>
32+
</div>
33+
);
34+
};

src/components/Message/icons.tsx

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import React from 'react';
22

3-
import type { PinIndicatorProps } from './types';
4-
53
import type { IconProps } from '../../types/types';
64

75
export const ActionsIcon = ({ className = '' }: IconProps) => (
@@ -52,28 +50,6 @@ export const PinIcon = () => (
5250
</svg>
5351
);
5452

55-
export const PinIndicator = ({ message, t }: PinIndicatorProps) => {
56-
if (!message || !t) return null;
57-
58-
return (
59-
<div style={{ alignItems: 'center', display: 'flex' }}>
60-
<PinIcon />
61-
<div
62-
style={{
63-
fontSize: '14px',
64-
marginBottom: '0',
65-
marginLeft: '8px',
66-
marginTop: '0',
67-
}}
68-
>
69-
{message.pinned_by
70-
? `${t('Pinned by')} ${message.pinned_by?.name || message.pinned_by?.id}`
71-
: t('Message pinned')}
72-
</div>
73-
</div>
74-
);
75-
};
76-
7753
export const MessageErrorIcon = () => (
7854
<div className='str-chat__message-error-icon'>
7955
<svg

src/components/Message/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from './MessageDeleted';
77
export * from './MessageEditedTimestamp';
88
export * from './MessageIsThreadReplyInChannelButtonIndicator';
99
export * from './MessageRepliesCountButton';
10+
export * from './PinIndicator';
1011
export * from './MessageSimple';
1112
export * from './MessageStatus';
1213
export * from './MessageText';

src/components/Message/styling/Message.scss

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
);
6969

7070
--str-chat__message-reminder-color: var(--str-chat__primary-color);
71-
--str-chat__message-reminder-background-color: var(--str-chat__grey50);
71+
--str-chat__message-reminder-background-color: transparent;
7272
--str-chat__message-reminder-border-block-start: none;
7373
--str-chat__message-reminder-border-block-end: none;
7474
--str-chat__message-reminder-border-inline-start: none;
@@ -77,6 +77,9 @@
7777
--str-chat__message-reminder-border-radius: 0;
7878
--str-chat__message-reactions-host-offset-x: -6px;
7979

80+
/* Background color for pinned messages (Figma: background/core/highlight) */
81+
--str-chat__message-pinned-background-color: var(--background-core-highlight);
82+
8083
/* The maximum allowed width of the message component */
8184
--str-chat__message-max-width: calc(var(--str-chat__spacing-px) * 480);
8285

@@ -217,6 +220,23 @@
217220
font: var(--str-chat__caption-medium-text);
218221
}
219222

223+
.str-chat__message-pin-indicator {
224+
grid-area: pin-indicator;
225+
padding-block: var(--spacing-xxs);
226+
227+
.str-chat__message-pin-indicator__content {
228+
display: flex;
229+
align-items: center;
230+
gap: var(--spacing-xxs);
231+
font: var(--str-chat__metadata-default-text);
232+
color: var(--text-primary-text);
233+
}
234+
}
235+
236+
&.str-chat__message--me .str-chat__message-pin-indicator .str-chat__message-pin-indicator__content {
237+
justify-content: flex-end;
238+
}
239+
220240
&.str-chat__message--other {
221241
grid-template-areas:
222242
'. message-reminder'
@@ -247,6 +267,26 @@
247267
}
248268
}
249269

270+
&.str-chat__message--pinned.str-chat__message--other {
271+
grid-template-areas:
272+
'. pin-indicator'
273+
'. message-reminder'
274+
'avatar message'
275+
'avatar translation-notice'
276+
'avatar custom-metadata'
277+
'avatar metadata';
278+
}
279+
280+
&.str-chat__message--pinned.str-chat__message--me {
281+
grid-template-areas:
282+
'pin-indicator .'
283+
'message-reminder .'
284+
'message avatar'
285+
'translation-notice avatar'
286+
'custom-metadata avatar'
287+
'metadata avatar';
288+
}
289+
250290
&.str-chat__message--me {
251291
grid-template-areas:
252292
'message-reminder .'
@@ -502,6 +542,10 @@
502542
background-color: var(--str-chat__message-highlighted-background-color);
503543
}
504544

545+
.str-chat__message--pinned {
546+
background-color: var(--str-chat__message-pinned-background-color);
547+
}
548+
505549
/* This rule won't be applied in browsers that don't support :has() */
506550
.str-chat__li:hover:not(:has(.str-chat__reaction-list:hover, .str-chat__modal--open)) {
507551
background-color: var(--str-chat__message-active-background-color);

src/components/Message/types.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
import type { TFunction } from 'i18next';
21
import type { ReactNode } from 'react';
3-
import type { ReactionSort, UserResponse } from 'stream-chat';
2+
import type { LocalMessage, ReactionSort, UserResponse } from 'stream-chat';
43

54
import type { PinPermissions, UserEventHandler } from './hooks';
65
import type { MessageActionsArray } from './utils';
7-
8-
import type { LocalMessage } from 'stream-chat';
96
import type { GroupStyle } from '../MessageList/utils';
107
import type { MessageInputProps } from '../MessageInput/MessageInput';
118
import type { ReactionDetailsComparator, ReactionsComparator } from '../Reactions/types';
@@ -114,8 +111,3 @@ export type MessageProps = {
114111
};
115112

116113
export type MessageUIComponentProps = Partial<MessageContextValue>;
117-
118-
export type PinIndicatorProps = {
119-
message?: LocalMessage;
120-
t?: TFunction;
121-
};

src/context/ComponentContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ export type ComponentContextValue = {
154154
Modal?: React.ComponentType<ModalProps>;
155155
/** Custom UI component for viewing message's image and giphy attachments with option to expand into the Gallery on Modal, defaults to and accepts the same props as [ModalGallery](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Gallery/ModalGallery.tsx) */
156156
ModalGallery?: React.ComponentType<ModalGalleryProps>;
157-
/** Custom UI component to override default pinned message indicator, defaults to and accepts same props as: [PinIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/icons.tsx) */
157+
/** Custom UI component to override default pinned message indicator, defaults to and accepts same props as: [PinIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/PinIndicator.tsx) */
158158
PinIndicator?: React.ComponentType<PinIndicatorProps>;
159159
/** Custom UI component to override default poll actions rendering in a message, defaults to and accepts same props as: [PollActions](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Poll/PollActions/PollActions.tsx) */
160160
PollActions?: React.ComponentType;

src/i18n/de.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@
187187
"People matching": "Passende Personen",
188188
"Photo": "Foto",
189189
"Pin": "Anheften",
190-
"Pinned by": "Angeheftet von",
190+
"Pinned by {{ name }}": "Angeheftet von {{ name }}",
191191
"Play video": "Video abspielen",
192192
"Poll": "Umfrage",
193193
"Poll comments": "Umfragekommentare",

src/i18n/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@
187187
"People matching": "People matching",
188188
"Photo": "Photo",
189189
"Pin": "Pin",
190-
"Pinned by": "Pinned by",
190+
"Pinned by {{ name }}": "Pinned by {{ name }}",
191191
"Play video": "Play video",
192192
"Poll": "Poll",
193193
"Poll comments": "Poll comments",

src/i18n/es.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@
192192
"People matching": "Personas que coinciden",
193193
"Photo": "Foto",
194194
"Pin": "Fijar",
195-
"Pinned by": "Fijado por",
195+
"Pinned by {{ name }}": "Fijado por {{ name }}",
196196
"Play video": "Reproducir video",
197197
"Poll": "Encuesta",
198198
"Poll comments": "Comentarios de la encuesta",

0 commit comments

Comments
 (0)