Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/components/Attachment/Attachment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,13 @@ export type AttachmentProps = {
attachments: (StreamAttachment | SharedLocationResponse)[];
/** The handler function to call when an action is performed on an attachment, examples include canceling a \/giphy command or shuffling the results. */
actionHandler?: ActionHandlerReturnType;
/** Which action should be focused on initial render, by attachment type (match by action.value) */
/**
* Which attachment action button receives focus on initial render, keyed by attachment type.
* Values must match an action's `value` (e.g. `'send'`, `'cancel'`, `'shuffle'` for giphy attachment).
* Default: `{ giphy: 'send' }`.
* To disable auto-focus (e.g. when rendering the Giphy preview above the composer so focus
* stays in the message input), pass an empty object: `attachmentActionsDefaultFocus={{}}`.
*/
attachmentActionsDefaultFocus?: AttachmentActionsDefaultFocusByType;
/** Custom UI component for displaying attachment actions, defaults to and accepts same props as: [AttachmentActions](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Attachment/AttachmentActions.tsx) */
AttachmentActions?: React.ComponentType<AttachmentActionsProps>;
Expand Down
2 changes: 1 addition & 1 deletion src/components/Avatar/styling/Avatar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
font-size: var(--typography-font-size-md);
}

&.str-chat__avatar--size-md {
&.str-chat__avatar--size-md {
--avatar-size: 32px;
--avatar-online-badge-size: 12px;
--avatar-icon-size: var(--icon-size-md);
Expand Down
3 changes: 1 addition & 2 deletions src/components/Dialog/styling/Alert.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

@mixin flex-column {
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -45,4 +44,4 @@
width: 100%;
}
}
}
}
5 changes: 0 additions & 5 deletions src/components/Dialog/styling/Prompt.scss
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
@use '../../../styling/utils';

// todo: once we have designs for dialogs + context menus create base class instead of a mixin
@mixin dialog-base {

}

.str-chat__dialog-overlay {
inset: 0;
position: absolute;
Expand Down
27 changes: 25 additions & 2 deletions src/components/Icons/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ export const IconArrowRight = createIcon(
<path d='M8.90918 3.4095C9.14349 3.17519 9.5235 3.17519 9.75781 3.4095L13.9238 7.57552C14.0363 7.68804 14.0996 7.84119 14.0996 8.00032C14.0995 8.15933 14.0363 8.3117 13.9238 8.42415L9.75781 12.5911C9.52355 12.8254 9.14351 12.8253 8.90918 12.5911C8.67487 12.3568 8.67487 11.9768 8.90918 11.7425L12.0518 8.59993H2.5C2.16874 8.59993 1.90057 8.33154 1.90039 8.00032C1.90039 7.66895 2.16863 7.39973 2.5 7.39973H12.0518L8.90918 4.25716C8.67508 4.02288 8.67508 3.64377 8.90918 3.4095Z' />,
);

export const IconArrowRightUp = createIcon(
'IconArrowRightUp',
<path
d='M12.1667 10.1663V3.83301M12.1667 3.83301H5.83333M12.1667 3.83301L4 11.9997'
fill='none'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
/>,
);

export const IconArrowRotateClockwise = createIcon(
'IconArrowRotateClockwise',
<path d='M1.88638 8C1.88638 4.63106 4.61706 1.90039 7.98599 1.90039C9.79486 1.90039 11.0876 2.58323 12.2331 3.69824V2.66699C12.2331 2.33562 12.5023 2.06641 12.8336 2.06641C13.1649 2.06658 13.4333 2.33573 13.4333 2.66699V4.83301C13.4333 5.44042 12.941 5.93342 12.3336 5.93359H10.1667C9.83529 5.93359 9.56705 5.66438 9.56705 5.33301C9.56722 5.00179 9.8354 4.7334 10.1667 4.7334H11.571C10.5361 3.66485 9.48807 3.09961 7.98599 3.09961C5.2798 3.09961 3.0856 5.2938 3.0856 8C3.0856 10.7062 5.27981 12.9004 7.98599 12.9004C10.1184 12.9004 11.934 11.5375 12.6071 9.63379C12.7175 9.32146 13.0604 9.15735 13.3727 9.26758C13.6851 9.37799 13.8493 9.72081 13.7389 10.0332C12.9018 12.4016 10.6429 14.0996 7.98599 14.0996C4.61706 14.0996 1.88638 11.3689 1.88638 8Z' />,
Expand Down Expand Up @@ -73,7 +84,13 @@ export const IconAtSolid = createIcon(

export const IconBellNotification = createIcon(
'IconBellNotification',
<path d='M12.8926 10.7972L12.0674 9.19757C11.9412 8.95287 11.8684 8.68417 11.8545 8.40948L11.7314 5.97003C11.632 3.99113 9.99271 2.43292 8 2.43292C6.00726 2.43292 4.368 3.99015 4.26855 5.96906L4.14453 8.40948C4.13061 8.68431 4.0587 8.95302 3.93262 9.19757L3.10742 10.7972C3.1024 10.807 3.09961 10.8182 3.09961 10.8294C3.09962 10.8684 3.1319 10.8997 3.1709 10.8997H12.8291C12.8681 10.8997 12.9004 10.8684 12.9004 10.8294C12.9004 10.8183 12.8977 10.8071 12.8926 10.7972ZM6.02246 12.0999C6.2796 12.9486 7.06733 13.5667 8 13.5667C8.93265 13.5667 9.72039 12.9486 9.97754 12.0999H6.02246ZM14.0996 10.8294C14.0996 11.5311 13.5308 12.0999 12.8291 12.0999H11.2109C10.9292 13.6174 9.59912 14.7669 8 14.7669C6.40085 14.7669 5.07082 13.6174 4.78906 12.0999H3.1709C2.46916 12.0999 1.90041 11.5311 1.90039 10.8294C1.90039 10.627 1.94825 10.4274 2.04102 10.2474L2.86621 8.64777C2.91399 8.5551 2.94099 8.45314 2.94629 8.34894L3.06934 5.90948C3.20084 3.28834 5.37132 1.2337 8 1.2337C10.6286 1.2337 12.7992 3.28834 12.9307 5.90948L13.0537 8.34894C13.059 8.45327 13.0861 8.55526 13.1338 8.64777L13.959 10.2474C14.0517 10.4273 14.0996 10.6269 14.0996 10.8294Z' />,
<path
d='M10.6667 11.4997C10.6667 12.9724 9.47273 14.1663 8 14.1663C6.52724 14.1663 5.33333 12.9724 5.33333 11.4997M13.5 10.8291C13.5 11.1994 13.1997 11.4997 12.8294 11.4997H3.17062C2.80025 11.4997 2.5 11.1994 2.5 10.8291C2.5 10.7221 2.52557 10.6167 2.57457 10.5217L3.39922 8.92241C3.48623 8.75367 3.53621 8.56827 3.54578 8.37861L3.66901 5.93899C3.7844 3.63889 5.68924 1.83301 8 1.83301C10.3107 1.83301 12.2156 3.63889 12.331 5.93899L12.4542 8.37861C12.4638 8.56827 12.5137 8.75367 12.6008 8.92241L13.4254 10.5217C13.4744 10.6167 13.5 10.7221 13.5 10.8291Z'
fill='none'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
/>,
);

export const IconBellOff = createIcon(
Expand All @@ -90,7 +107,13 @@ export const IconBellOff = createIcon(

export const IconBookmark = createIcon(
'IconBookmark',
<path d='M6.92056 11.5033C7.57321 11.0641 8.42712 11.0641 9.07974 11.5033L12.1295 13.556C12.145 13.5664 12.1554 13.5685 12.1628 13.5687C12.1722 13.569 12.1849 13.5668 12.1979 13.5599C12.2109 13.553 12.2202 13.5436 12.2253 13.5355C12.2291 13.5293 12.233 13.5196 12.2331 13.5013V3.16638C12.2329 2.76152 11.9046 2.43298 11.4997 2.43298H4.49966C4.09491 2.43316 3.76644 2.76163 3.76627 3.16638V13.5013C3.76631 13.5198 3.77121 13.5293 3.77505 13.5355C3.7801 13.5436 3.7894 13.553 3.8024 13.5599C3.81508 13.5666 3.82721 13.569 3.83658 13.5687C3.84393 13.5685 3.85513 13.5666 3.87076 13.556L6.92056 11.5033ZM13.4333 13.5013C13.433 14.5152 12.3009 15.1181 11.4596 14.5521L8.40982 12.4994C8.19302 12.3535 7.91754 12.3351 7.68619 12.4447L7.59048 12.4994L4.54068 14.5521C3.69947 15.1182 2.56727 14.5154 2.56705 13.5013V3.16638C2.56722 2.09889 3.43217 1.23394 4.49966 1.23376H11.4997C12.5673 1.23376 13.4331 2.09879 13.4333 3.16638V13.5013Z' />,
<path
d='M12.8333 13.501V3.16666C12.8333 2.43028 12.2364 1.83333 11.5 1.83333H4.49999C3.76361 1.83333 3.16666 2.43028 3.16666 3.16666V13.501C3.16666 14.0348 3.76275 14.3521 4.20558 14.054L7.25546 12.0011C7.70559 11.6982 8.29439 11.6982 8.74452 12.0011L11.7944 14.054C12.2373 14.3521 12.8333 14.0348 12.8333 13.501Z'
fill='none'
stroke='black'
strokeLinecap='round'
strokeLinejoin='round'
/>,
);

export const IconBookmarkRemove = createIcon(
Expand Down
22 changes: 22 additions & 0 deletions src/components/Message/MessageAlsoSentInChannelIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';

import { IconArrowRightUp } from '../Icons';
import { useMessageContext, useTranslationContext } from '../../context';

/**
* Indicator shown in thread message lists when the message was also sent to the main channel (show_in_channel === true).
* Only visible inside Thread, not in the main channel list.
*/
export const MessageAlsoSentInChannelIndicator = () => {
const { message, threadList } = useMessageContext('MessageAlsoSentInChannelIndicator');
const { t } = useTranslationContext();

if (!threadList || !message?.show_in_channel) return null;

return (
<div className='str-chat__message-also-sent-in-channel' role='status'>
<IconArrowRightUp />
<span>{t('Also sent in channel')}</span>
</div>
);
};
17 changes: 13 additions & 4 deletions src/components/Message/MessageSimple.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { MessageText } from './MessageText';
import { MessageTimestamp as DefaultMessageTimestamp } from './MessageTimestamp';
import { StreamedMessageText as DefaultStreamedMessageText } from './StreamedMessageText';
import { isDateSeparatorMessage } from '../MessageList';
import { MessageAlsoSentInChannelIndicator as DefaultMessageAlsoSentInChannelIndicator } from './MessageAlsoSentInChannelIndicator';
import { MessageIsThreadReplyInChannelButtonIndicator as DefaultMessageIsThreadReplyInChannelButtonIndicator } from './MessageIsThreadReplyInChannelButtonIndicator';
import { ReminderNotification as DefaultReminderNotification } from './ReminderNotification';
import { useMessageReminder } from './hooks';
Expand All @@ -38,7 +39,11 @@ import { useComponentContext } from '../../context/ComponentContext';
import type { MessageContextValue } from '../../context/MessageContext';
import { useMessageContext } from '../../context/MessageContext';

import { useChatContext, useTranslationContext } from '../../context';
import {
useChannelStateContext,
useChatContext,
useTranslationContext,
} from '../../context';
import { MessageEditedTimestamp } from './MessageEditedTimestamp';

import type { MessageUIComponentProps } from './types';
Expand All @@ -64,8 +69,10 @@ const MessageSimpleWithContext = (props: MessageSimpleWithContextProps) => {
showAvatar = 'incoming',
threadList,
} = props;
const { client } = useChatContext('MessageSimple');
const { t } = useTranslationContext('MessageSimple');
const { channel } = useChannelStateContext();
const { client } = useChatContext();
const { t } = useTranslationContext();
const memberCount = Object.keys(channel?.state?.members ?? {}).length;
const [isBounceDialogOpen, setIsBounceDialogOpen] = useState(false);
const [isEditedTimestampOpen, setEditedTimestampOpen] = useState(false);
const reminder = useMessageReminder(message.id);
Expand All @@ -74,6 +81,7 @@ const MessageSimpleWithContext = (props: MessageSimpleWithContextProps) => {
Attachment = DefaultAttachment,
Avatar = DefaultAvatar,
MessageActions = DefaultMessageActions,
MessageAlsoSentInChannelIndicator = DefaultMessageAlsoSentInChannelIndicator,
MessageBlocked = DefaultMessageBlocked,
MessageBouncePrompt = DefaultMessageBouncePrompt,
MessageDeleted = DefaultMessageDeleted,
Expand Down Expand Up @@ -183,6 +191,7 @@ const MessageSimpleWithContext = (props: MessageSimpleWithContextProps) => {
{
<div className={rootClassName} key={message.id}>
{message.pinned && <PinIndicator message={message} />}
{threadList && message.show_in_channel && <MessageAlsoSentInChannelIndicator />}
{!!reminder && <ReminderNotification reminder={reminder} />}
{message.user && (
<Avatar
Expand Down Expand Up @@ -231,7 +240,7 @@ const MessageSimpleWithContext = (props: MessageSimpleWithContextProps) => {
{showMetadata && (
<div className='str-chat__message-metadata'>
<MessageStatus />
{!isMyMessage() && !!message.user && (
{!isMyMessage() && !!message.user && memberCount > 2 && (
<span className='str-chat__message-simple-name'>
{message.user.name || message.user.id}
</span>
Expand Down
10 changes: 8 additions & 2 deletions src/components/Message/PinIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';

import { IconPin } from '../Icons';
import { useTranslationContext } from '../../context';
import { useChatContext, useTranslationContext } from '../../context';
import type { LocalMessage } from 'stream-chat';

export type PinIndicatorProps = {
Expand All @@ -14,12 +14,18 @@ export type PinIndicatorProps = {
*/
export const PinIndicator = ({ message }: PinIndicatorProps) => {
const { t } = useTranslationContext();
const { client } = useChatContext();

if (!message) return null;

const isOwnPin = !!message.pinned_by?.id && message.pinned_by.id === client.user?.id;
const name = message.pinned_by?.name ?? message.pinned_by?.id ?? '';

const label = name ? t('Pinned by {{ name }}', { name }) : t('Message pinned');
const label = isOwnPin
? t('Pinned by You')
: name
? t('Pinned by {{ name }}', { name })
: t('Message pinned');

return (
<div className='str-chat__message-pin-indicator'>
Expand Down
95 changes: 74 additions & 21 deletions src/components/Message/ReminderNotification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { useTranslationContext } from '../../context';
import { useStateStore } from '../../store';
import type { Reminder, ReminderState } from 'stream-chat';
import { IconBellNotification, IconBookmark } from '../Icons';

export type ReminderNotificationProps = {
reminder?: Reminder;
Expand All @@ -11,40 +12,92 @@ const reminderStateSelector = (state: ReminderState) => ({
timeLeftMs: state.timeLeftMs,
});

export const ReminderNotification = ({ reminder }: ReminderNotificationProps) => {
function SavedForLaterContent() {
const { t } = useTranslationContext();
return (
<p className='str-chat__message-saved-for-later'>
<IconBookmark />
<span>{t('Saved for later')}</span>
</p>
);
}

const THRESHOLD_RELATIVE_MINUTES = 59;

function RemindMeContent({ reminder }: { reminder: Reminder }) {
const { t } = useTranslationContext();
const { timeLeftMs } = useStateStore(reminder?.state, reminderStateSelector) ?? {};

const stopRefreshBoundaryMs = reminder?.timer.stopRefreshBoundaryMs;
const stopRefreshTimeStamp =
reminder?.remindAt && stopRefreshBoundaryMs
? reminder?.remindAt.getTime() + stopRefreshBoundaryMs
? reminder.remindAt.getTime() + stopRefreshBoundaryMs
: undefined;

const isBehindRefreshBoundary =
!!stopRefreshTimeStamp && new Date().getTime() > stopRefreshTimeStamp;

if (timeLeftMs === null || !reminder.remindAt) return null;

const nowMs = Date.now();
const remindAtMs = reminder.remindAt.getTime();
const diffMs = remindAtMs - nowMs;
const diffMinutes = Math.abs(diffMs) / (60 * 1000);
const useAbsoluteFormat = diffMinutes > THRESHOLD_RELATIVE_MINUTES;

const renderTime = () => {
if (isBehindRefreshBoundary) {
// Past: reminder time has passed
if (useAbsoluteFormat) {
// > 59 min ago: calendar + time (same as DateSeparator + HH:mm)
// e.g. "Due since Today at 15:00", "Due since Yesterday at 09:30"
return t('Due since {{ dueSince }}', {
dueSince: t('timestamp/ReminderNotification', {
timestamp: reminder.remindAt,
}),
});
}
// Within 59 min ago: relative
// e.g. "Due since 5 minutes ago", "Due since a minute ago"
return t('Due since {{ dueSince }}', {
dueSince: t('duration/Message reminder', {
milliseconds: diffMs,
}),
});
}
// Future: reminder not yet due
if (useAbsoluteFormat) {
// > 59 min from now: calendar + time (no "Due" prefix)
// e.g. "Today at 15:00", "Tomorrow at 09:30"
return t('timestamp/ReminderNotification', {
timestamp: reminder.remindAt,
});
}
// Within 59 min from now: relative
// e.g. "Due in 30 minutes", "Due in a minute"
return t('Due {{ timeLeft }}', {
timeLeft: t('duration/Message reminder', {
milliseconds: timeLeftMs,
}),
});
};

return (
<p className='str-chat__message-reminder'>
<span>{t('Saved for later')}</span>
{reminder?.remindAt && timeLeftMs !== null && (
<>
<span> | </span>
<span>
{isBehindRefreshBoundary
? t('Due since {{ dueSince }}', {
dueSince: t('timestamp/ReminderNotification', {
timestamp: reminder.remindAt,
}),
})
: t('Due {{ timeLeft }}', {
timeLeft: t('duration/Message reminder', {
milliseconds: timeLeftMs,
}),
})}
</span>
</>
)}
<IconBellNotification />
<span>{t('Reminder set')}</span>
<span> · </span>
<span className='str-chat__message-reminder__time-left'>{renderTime()}</span>
</p>
);
}

export const ReminderNotification = ({ reminder }: ReminderNotificationProps) => {
if (!reminder) return null;

if (!reminder.remindAt) {
return <SavedForLaterContent />;
}

return <RemindMeContent reminder={reminder} />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ const renderComponent = async ({ reminder }) => {
};

describe('ReminderNotification', () => {
it('displays text for bookmark notifications', async () => {
it('displays text for bookmark notifications (saved for later)', async () => {
const reminder = new Reminder({ data: generateReminderResponse() });
const { container } = await renderComponent({ reminder });
expect(container).toMatchSnapshot();
});
it('displays text for time due in case of timed reminders', async () => {
it('displays text for time due in case of timed reminders (remind me)', async () => {
const reminder = new Reminder({
data: generateReminderResponse({
scheduleOffsetMs: 60 * 1000,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`ReminderNotification displays text for bookmark notifications 1`] = `
exports[`ReminderNotification displays text for bookmark notifications (saved for later) 1`] = `
<div>
<p
class="str-chat__message-reminder"
class="str-chat__message-saved-for-later"
>
<span>
Saved for later
Expand All @@ -18,30 +18,34 @@ exports[`ReminderNotification displays text for reminder deadline if trespassed
class="str-chat__message-reminder"
>
<span>
Saved for later
Reminder set
</span>
<span>
|
·
</span>
<span>
<span
class="str-chat__message-reminder__time-left"
>
Due since 01/01/1970
</span>
</p>
</div>
`;

exports[`ReminderNotification displays text for time due in case of timed reminders 1`] = `
exports[`ReminderNotification displays text for time due in case of timed reminders (remind me) 1`] = `
<div>
<p
class="str-chat__message-reminder"
>
<span>
Saved for later
Reminder set
</span>
<span>
|
·
</span>
<span>
<span
class="str-chat__message-reminder__time-left"
>
Due in a minute
</span>
</p>
Expand Down
Loading
Loading