mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-05-08 17:08:57 +01:00
Support for Group Member Labels
This commit is contained in:
@@ -2,28 +2,31 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import lodash from 'lodash';
|
||||
import type {
|
||||
ConversationType,
|
||||
ShowConversationType,
|
||||
} from '../state/ducks/conversations.preload.js';
|
||||
import type { ShowConversationType } from '../state/ducks/conversations.preload.js';
|
||||
import { I18n } from './I18n.dom.js';
|
||||
import type { LocalizerType, ThemeType } from '../types/Util.std.js';
|
||||
import { Modal } from './Modal.dom.js';
|
||||
import { ConversationListItem } from './conversationList/ConversationListItem.dom.js';
|
||||
|
||||
const { noop } = lodash;
|
||||
import { Avatar, AvatarSize } from './Avatar.dom.js';
|
||||
import { GroupMemberLabel } from './conversation/ContactName.dom.js';
|
||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges.preload.js';
|
||||
import { tw } from '../axo/tw.dom.js';
|
||||
import type { AdminMembershipType } from '../state/selectors/conversations.dom.js';
|
||||
import { UserText } from './UserText.dom.js';
|
||||
|
||||
type PropsType = {
|
||||
groupAdmins: Array<ConversationType>;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
groupAdmins: Array<AdminMembershipType>;
|
||||
memberColors: Map<string, string>;
|
||||
i18n: LocalizerType;
|
||||
showConversation: ShowConversationType;
|
||||
theme: ThemeType;
|
||||
};
|
||||
|
||||
export function AnnouncementsOnlyGroupBanner({
|
||||
getPreferredBadge,
|
||||
groupAdmins,
|
||||
i18n,
|
||||
memberColors,
|
||||
showConversation,
|
||||
theme,
|
||||
}: PropsType): React.JSX.Element {
|
||||
@@ -33,25 +36,58 @@ export function AnnouncementsOnlyGroupBanner({
|
||||
<>
|
||||
{isShowingAdmins && (
|
||||
<Modal
|
||||
modalName="AnnouncmentsOnlyGroupBanner"
|
||||
i18n={i18n}
|
||||
hasXButton
|
||||
modalName="AnnouncmentsOnlyGroupBanner"
|
||||
onClose={() => setIsShowingAdmins(false)}
|
||||
title={i18n('icu:AnnouncementsOnlyGroupBanner--modal')}
|
||||
>
|
||||
{groupAdmins.map(admin => (
|
||||
<ConversationListItem
|
||||
{...admin}
|
||||
draftPreview={undefined}
|
||||
i18n={i18n}
|
||||
lastMessage={undefined}
|
||||
lastUpdated={undefined}
|
||||
onClick={() => {
|
||||
showConversation({ conversationId: admin.id });
|
||||
}}
|
||||
onMouseDown={noop}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
{groupAdmins.map(admin => {
|
||||
const { member, labelEmoji, labelString } = admin;
|
||||
const contactNameColor = memberColors.get(member.id);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
showConversation({ conversationId: member.id });
|
||||
}}
|
||||
className={tw('flex flex-row items-center p-2')}
|
||||
>
|
||||
<div className={tw('pe-3')}>
|
||||
<Avatar
|
||||
conversationType="direct"
|
||||
badge={getPreferredBadge(member.badges)}
|
||||
i18n={i18n}
|
||||
size={AvatarSize.THIRTY_SIX}
|
||||
theme={theme}
|
||||
{...member}
|
||||
/>
|
||||
</div>
|
||||
<div className={tw('flex flex-col items-start')}>
|
||||
<div>
|
||||
<UserText
|
||||
text={member.isMe ? i18n('icu:you') : member.title}
|
||||
/>
|
||||
</div>
|
||||
{labelString && contactNameColor && (
|
||||
<div>
|
||||
<GroupMemberLabel
|
||||
contactNameColor={contactNameColor}
|
||||
contactLabel={{
|
||||
labelEmoji,
|
||||
labelString,
|
||||
}}
|
||||
context="list"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Modal>
|
||||
)}
|
||||
<div className="AnnouncementsOnlyGroupBanner__banner">
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ThemeType } from '../types/Util.std.js';
|
||||
import { Theme } from '../util/theme.std.js';
|
||||
import { UserText } from './UserText.dom.js';
|
||||
import { SharedGroupNames } from './SharedGroupNames.dom.js';
|
||||
import type { ContactModalStateType } from '../state/ducks/globalModals.preload.js';
|
||||
|
||||
export type CallLinkPendingParticipantModalProps = {
|
||||
readonly i18n: LocalizerType;
|
||||
@@ -21,7 +22,7 @@ export type CallLinkPendingParticipantModalProps = {
|
||||
readonly denyUser: (payload: PendingUserActionPayloadType) => void;
|
||||
readonly onClose: () => void;
|
||||
readonly sharedGroupNames: ReadonlyArray<string>;
|
||||
readonly toggleAboutContactModal: (conversationId: string) => void;
|
||||
readonly toggleAboutContactModal: (options: ContactModalStateType) => void;
|
||||
};
|
||||
|
||||
export function CallLinkPendingParticipantModal({
|
||||
@@ -75,7 +76,7 @@ export function CallLinkPendingParticipantModal({
|
||||
onClick={ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
toggleAboutContactModal(conversation.id);
|
||||
toggleAboutContactModal({ contactId: conversation.id });
|
||||
}}
|
||||
className="CallLinkPendingParticipantModal__NameButton"
|
||||
>
|
||||
|
||||
@@ -12,13 +12,47 @@ import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext.st
|
||||
import { fakeDraftAttachment } from '../test-helpers/fakeAttachment.std.js';
|
||||
import { landscapeGreenUrl } from '../storybook/Fixtures.std.js';
|
||||
import { RecordingState } from '../types/AudioRecorder.std.js';
|
||||
import { ConversationColors } from '../types/Colors.std.js';
|
||||
import { ContactNameColors, ConversationColors } from '../types/Colors.std.js';
|
||||
import { getDefaultConversation } from '../test-helpers/getDefaultConversation.std.js';
|
||||
import { PaymentEventKind } from '../types/Payment.std.js';
|
||||
import { EmojiSkinTone } from './fun/data/emojis.std.js';
|
||||
import { isNotNil } from '../util/isNotNil.std.js';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
const groupAdmins = [
|
||||
{
|
||||
member: getDefaultConversation(),
|
||||
labelEmoji: undefined,
|
||||
labelString: undefined,
|
||||
},
|
||||
{
|
||||
member: getDefaultConversation(),
|
||||
labelEmoji: '✅',
|
||||
labelString: 'Planner',
|
||||
},
|
||||
{
|
||||
member: getDefaultConversation(),
|
||||
labelEmoji: '#',
|
||||
labelString: 'Invalid Emoji',
|
||||
},
|
||||
{
|
||||
member: getDefaultConversation(),
|
||||
labelEmoji: undefined,
|
||||
labelString: 'No Emoji',
|
||||
},
|
||||
];
|
||||
const memberColors = new Map(
|
||||
groupAdmins
|
||||
.map((admin, i): [string, string] | null => {
|
||||
if (!admin.member.id) {
|
||||
return null;
|
||||
}
|
||||
return [admin.member.id?.toString(), ContactNameColors[i]];
|
||||
})
|
||||
.filter(isNotNil)
|
||||
);
|
||||
|
||||
export default {
|
||||
title: 'Components/CompositionArea',
|
||||
decorators: [
|
||||
@@ -102,7 +136,8 @@ export default {
|
||||
announcementsOnly: false,
|
||||
areWeAdmin: false,
|
||||
areWePendingApproval: false,
|
||||
groupAdmins: [],
|
||||
groupAdmins,
|
||||
memberColors,
|
||||
cancelJoinRequest: action('cancelJoinRequest'),
|
||||
showConversation: action('showConversation'),
|
||||
isSmsOnlyOrUnregistered: false,
|
||||
|
||||
@@ -112,7 +112,11 @@ export type OwnProps = Readonly<{
|
||||
errorDialogAudioRecorderType: ErrorDialogAudioRecorderType | null;
|
||||
errorRecording: (e: ErrorDialogAudioRecorderType) => unknown;
|
||||
focusCounter: number;
|
||||
groupAdmins: Array<ConversationType>;
|
||||
groupAdmins: Array<{
|
||||
member: ConversationType;
|
||||
labelEmoji: string | undefined;
|
||||
labelString: string | undefined;
|
||||
}>;
|
||||
groupVersion: 1 | 2 | null;
|
||||
i18n: LocalizerType;
|
||||
imageToBlurHash: typeof imageToBlurHash;
|
||||
@@ -126,6 +130,7 @@ export type OwnProps = Readonly<{
|
||||
lastEditableMessageId: string | null;
|
||||
recordingState: RecordingState;
|
||||
messageCompositionId: string;
|
||||
memberColors: Map<string, string>;
|
||||
shouldHidePopovers: boolean | null;
|
||||
isMuted: boolean;
|
||||
isSmsOnlyOrUnregistered: boolean | null;
|
||||
@@ -321,6 +326,7 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
announcementsOnly,
|
||||
areWeAdmin,
|
||||
groupAdmins,
|
||||
memberColors,
|
||||
cancelJoinRequest,
|
||||
showConversation,
|
||||
// SMS-only contacts
|
||||
@@ -1005,8 +1011,10 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
if (announcementsOnly && !areWeAdmin) {
|
||||
return (
|
||||
<AnnouncementsOnlyGroupBanner
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
groupAdmins={groupAdmins}
|
||||
i18n={i18n}
|
||||
memberColors={memberColors}
|
||||
showConversation={showConversation}
|
||||
theme={theme}
|
||||
/>
|
||||
|
||||
@@ -558,7 +558,7 @@ export function NotificationProfilesHome({
|
||||
i18n={i18n}
|
||||
isEditing
|
||||
onBack={() => setPage(HomePage.Edit)}
|
||||
onNext={() => setPage(HomePage.Edit)} // TODO: probably don't show Next button?
|
||||
onNext={() => setPage(HomePage.Edit)}
|
||||
onSetIsEnabled={(scheduleEnabled: boolean) => {
|
||||
const newProfile = {
|
||||
...profile,
|
||||
|
||||
@@ -61,18 +61,23 @@ export default {
|
||||
isSignalConnection: { control: { type: 'boolean' } },
|
||||
},
|
||||
args: {
|
||||
canAddLabel: false,
|
||||
contact: conversation,
|
||||
contactLabelEmoji: undefined,
|
||||
contactLabelString: undefined,
|
||||
contactNameColor: undefined,
|
||||
fromOrAddedByTrustedContact: false,
|
||||
i18n,
|
||||
isSignalConnection: false,
|
||||
isEditMemberLabelEnabled: true,
|
||||
onClose: action('onClose'),
|
||||
onOpenNotePreviewModal: action('onOpenNotePreviewModal'),
|
||||
toggleSignalConnectionsModal: action('toggleSignalConnections'),
|
||||
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
|
||||
toggleProfileNameWarningModal: action('toggleProfileNameWarningModal'),
|
||||
startAvatarDownload: action('startAvatarDownload'),
|
||||
pendingAvatarDownload: false,
|
||||
conversation,
|
||||
sharedGroupNames: [],
|
||||
fromOrAddedByTrustedContact: false,
|
||||
isSignalConnection: false,
|
||||
startAvatarDownload: action('startAvatarDownload'),
|
||||
toggleProfileNameWarningModal: action('toggleProfileNameWarningModal'),
|
||||
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
|
||||
toggleSignalConnectionsModal: action('toggleSignalConnections'),
|
||||
},
|
||||
} satisfies ComponentMeta<PropsType>;
|
||||
|
||||
@@ -81,27 +86,80 @@ export function Defaults(args: PropsType): React.JSX.Element {
|
||||
}
|
||||
|
||||
export function Me(args: PropsType): React.JSX.Element {
|
||||
return <AboutContactModal {...args} conversation={me} />;
|
||||
return <AboutContactModal {...args} contact={me} />;
|
||||
}
|
||||
|
||||
export function MeWithLabel(args: PropsType): React.JSX.Element {
|
||||
return (
|
||||
<AboutContactModal
|
||||
{...{
|
||||
...args,
|
||||
contactLabelEmoji: '🐝',
|
||||
contactLabelString: 'Worker Bee',
|
||||
contactNameColor: '270',
|
||||
}}
|
||||
contact={me}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function MeWithInvalidLabelEmoji(args: PropsType): React.JSX.Element {
|
||||
return (
|
||||
<AboutContactModal
|
||||
{...{
|
||||
...args,
|
||||
contactLabelEmoji: '@',
|
||||
contactLabelString: 'Worker Bee',
|
||||
contactNameColor: '270',
|
||||
}}
|
||||
contact={me}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function MeWithAddLabel(args: PropsType): React.JSX.Element {
|
||||
return (
|
||||
<AboutContactModal
|
||||
{...{
|
||||
...args,
|
||||
canAddLabel: true,
|
||||
}}
|
||||
contact={me}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function MeWithAddLabelEditDisabled(args: PropsType): React.JSX.Element {
|
||||
return (
|
||||
<AboutContactModal
|
||||
{...{
|
||||
...args,
|
||||
canAddLabel: true,
|
||||
}}
|
||||
contact={me}
|
||||
isEditMemberLabelEnabled={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Verified(args: PropsType): React.JSX.Element {
|
||||
return <AboutContactModal {...args} conversation={verifiedConversation} />;
|
||||
return <AboutContactModal {...args} contact={verifiedConversation} />;
|
||||
}
|
||||
|
||||
export function Blocked(args: PropsType): React.JSX.Element {
|
||||
return <AboutContactModal {...args} conversation={blockedConversation} />;
|
||||
return <AboutContactModal {...args} contact={blockedConversation} />;
|
||||
}
|
||||
|
||||
export function Pending(args: PropsType): React.JSX.Element {
|
||||
return <AboutContactModal {...args} conversation={pendingConversation} />;
|
||||
return <AboutContactModal {...args} contact={pendingConversation} />;
|
||||
}
|
||||
|
||||
export function NoMessages(args: PropsType): React.JSX.Element {
|
||||
return <AboutContactModal {...args} conversation={noMessages} />;
|
||||
return <AboutContactModal {...args} contact={noMessages} />;
|
||||
}
|
||||
|
||||
export function WithAbout(args: PropsType): React.JSX.Element {
|
||||
return <AboutContactModal {...args} conversation={conversationWithAbout} />;
|
||||
return <AboutContactModal {...args} contact={conversationWithAbout} />;
|
||||
}
|
||||
|
||||
export function SignalConnection(args: PropsType): React.JSX.Element {
|
||||
@@ -110,11 +168,7 @@ export function SignalConnection(args: PropsType): React.JSX.Element {
|
||||
|
||||
export function SystemContact(args: PropsType): React.JSX.Element {
|
||||
return (
|
||||
<AboutContactModal
|
||||
{...args}
|
||||
conversation={systemContact}
|
||||
isSignalConnection
|
||||
/>
|
||||
<AboutContactModal {...args} contact={systemContact} isSignalConnection />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -122,7 +176,7 @@ export function WithSharedGroups(args: PropsType): React.JSX.Element {
|
||||
return (
|
||||
<AboutContactModal
|
||||
{...args}
|
||||
conversation={conversationWithSharedGroups}
|
||||
contact={conversationWithSharedGroups}
|
||||
sharedGroupNames={['Axolotl lovers']}
|
||||
isSignalConnection
|
||||
/>
|
||||
@@ -133,7 +187,7 @@ export function DirectFromTrustedContact(args: PropsType): React.JSX.Element {
|
||||
return (
|
||||
<AboutContactModal
|
||||
{...args}
|
||||
conversation={conversation}
|
||||
contact={conversation}
|
||||
fromOrAddedByTrustedContact
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -13,6 +13,14 @@ import { About } from './About.dom.js';
|
||||
import { I18n } from '../I18n.dom.js';
|
||||
import { canHaveNicknameAndNote } from '../../util/nicknames.dom.js';
|
||||
import { Tooltip, TooltipPlacement } from '../Tooltip.dom.js';
|
||||
import { useFunEmojiLocalizer } from '../fun/useFunEmojiLocalizer.dom.js';
|
||||
import {
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantKeyByValue,
|
||||
isEmojiVariantValue,
|
||||
} from '../fun/data/emojis.std.js';
|
||||
import { FunStaticEmoji } from '../fun/FunEmoji.dom.js';
|
||||
import { missingEmojiPlaceholder } from './ContactName.dom.js';
|
||||
|
||||
function muted(parts: Array<string | React.JSX.Element>) {
|
||||
return (
|
||||
@@ -22,11 +30,16 @@ function muted(parts: Array<string | React.JSX.Element>) {
|
||||
|
||||
export type PropsType = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
canAddLabel: boolean;
|
||||
contact: ConversationType;
|
||||
contactLabelEmoji: string | undefined;
|
||||
contactLabelString: string | undefined;
|
||||
contactNameColor: string | undefined;
|
||||
fromOrAddedByTrustedContact?: boolean;
|
||||
isEditMemberLabelEnabled: boolean;
|
||||
isSignalConnection: boolean;
|
||||
onClose: () => void;
|
||||
onOpenNotePreviewModal: () => void;
|
||||
conversation: ConversationType;
|
||||
fromOrAddedByTrustedContact?: boolean;
|
||||
isSignalConnection: boolean;
|
||||
pendingAvatarDownload?: boolean;
|
||||
sharedGroupNames: ReadonlyArray<string>;
|
||||
startAvatarDownload?: (id: string) => unknown;
|
||||
@@ -37,8 +50,13 @@ export type PropsType = Readonly<{
|
||||
|
||||
export function AboutContactModal({
|
||||
i18n,
|
||||
conversation,
|
||||
canAddLabel,
|
||||
contact,
|
||||
contactLabelEmoji,
|
||||
contactLabelString,
|
||||
contactNameColor,
|
||||
fromOrAddedByTrustedContact,
|
||||
isEditMemberLabelEnabled,
|
||||
isSignalConnection,
|
||||
pendingAvatarDownload,
|
||||
sharedGroupNames,
|
||||
@@ -49,7 +67,7 @@ export function AboutContactModal({
|
||||
onClose,
|
||||
onOpenNotePreviewModal,
|
||||
}: PropsType): React.JSX.Element {
|
||||
const { avatarUrl, hasAvatar, isMe } = conversation;
|
||||
const { avatarUrl, hasAvatar, isMe } = contact;
|
||||
|
||||
// If hasAvatar is true, we show the download button instead of blur
|
||||
const enableClickToLoad = !avatarUrl && !isMe && hasAvatar;
|
||||
@@ -64,11 +82,11 @@ export function AboutContactModal({
|
||||
}
|
||||
return () => {
|
||||
if (!pendingAvatarDownload && startAvatarDownload) {
|
||||
startAvatarDownload(conversation.id);
|
||||
startAvatarDownload(contact.id);
|
||||
}
|
||||
};
|
||||
}, [
|
||||
conversation.id,
|
||||
contact.id,
|
||||
startAvatarDownload,
|
||||
enableClickToLoad,
|
||||
pendingAvatarDownload,
|
||||
@@ -85,9 +103,9 @@ export function AboutContactModal({
|
||||
const onVerifiedClick = useCallback(
|
||||
(ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
toggleSafetyNumberModal(conversation.id);
|
||||
toggleSafetyNumberModal(contact.id);
|
||||
},
|
||||
[toggleSafetyNumberModal, conversation.id]
|
||||
[toggleSafetyNumberModal, contact.id]
|
||||
);
|
||||
|
||||
const onProfileNameWarningClick = useCallback(
|
||||
@@ -99,31 +117,58 @@ export function AboutContactModal({
|
||||
);
|
||||
|
||||
let statusRow: React.JSX.Element | undefined;
|
||||
const hasLabel = contactNameColor && contactLabelString;
|
||||
const shouldShowLabel = isMe && hasLabel;
|
||||
const shouldShowAddLabel =
|
||||
isMe && !hasLabel && canAddLabel && isEditMemberLabelEnabled;
|
||||
const emojiLocalizer = useFunEmojiLocalizer();
|
||||
|
||||
let labelEmojiElement;
|
||||
if (
|
||||
shouldShowLabel &&
|
||||
contactLabelEmoji &&
|
||||
isEmojiVariantValue(contactLabelEmoji)
|
||||
) {
|
||||
const emojiKey = getEmojiVariantKeyByValue(contactLabelEmoji);
|
||||
const labelEmojiData = getEmojiVariantByKey(emojiKey);
|
||||
labelEmojiElement = (
|
||||
<>
|
||||
<FunStaticEmoji
|
||||
role="img"
|
||||
aria-label={emojiLocalizer.getLocaleShortName(labelEmojiData.key)}
|
||||
size={16}
|
||||
emoji={labelEmojiData}
|
||||
/>{' '}
|
||||
</>
|
||||
);
|
||||
} else if (shouldShowLabel && contactLabelEmoji) {
|
||||
labelEmojiElement = `${missingEmojiPlaceholder} `;
|
||||
}
|
||||
|
||||
if (isMe) {
|
||||
// No status for ourselves
|
||||
} else if (conversation.isBlocked) {
|
||||
} else if (contact.isBlocked) {
|
||||
statusRow = (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--blocked" />
|
||||
{i18n('icu:AboutContactModal__blocked', {
|
||||
name: conversation.title,
|
||||
name: contact.title,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
} else if (!conversation.acceptedMessageRequest) {
|
||||
} else if (!contact.acceptedMessageRequest) {
|
||||
statusRow = (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--message-request" />
|
||||
{i18n('icu:AboutContactModal__message-request')}
|
||||
</div>
|
||||
);
|
||||
} else if (!conversation.hasMessages && !conversation.profileSharing) {
|
||||
} else if (!contact.hasMessages && !contact.profileSharing) {
|
||||
statusRow = (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--no-dms" />
|
||||
{i18n('icu:AboutContactModal__no-dms', {
|
||||
name: conversation.title,
|
||||
name: contact.title,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
@@ -140,22 +185,21 @@ export function AboutContactModal({
|
||||
>
|
||||
<div className="AboutContactModal__row AboutContactModal__row--centered">
|
||||
<Avatar
|
||||
avatarPlaceholderGradient={conversation.avatarPlaceholderGradient}
|
||||
avatarUrl={conversation.avatarUrl}
|
||||
avatarPlaceholderGradient={contact.avatarPlaceholderGradient}
|
||||
avatarUrl={contact.avatarUrl}
|
||||
blur={avatarBlur}
|
||||
onClick={avatarOnClick}
|
||||
badge={undefined}
|
||||
color={conversation.color}
|
||||
color={contact.color}
|
||||
conversationType="direct"
|
||||
hasAvatar={conversation.hasAvatar}
|
||||
hasAvatar={contact.hasAvatar}
|
||||
i18n={i18n}
|
||||
loading={pendingAvatarDownload && !conversation.avatarUrl}
|
||||
profileName={conversation.profileName}
|
||||
loading={pendingAvatarDownload && !contact.avatarUrl}
|
||||
profileName={contact.profileName}
|
||||
size={AvatarSize.TWO_HUNDRED_SIXTEEN}
|
||||
title={conversation.title}
|
||||
title={contact.title}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="AboutContactModal__row">
|
||||
<h3 className="AboutContactModal__title">
|
||||
{isMe
|
||||
@@ -163,19 +207,18 @@ export function AboutContactModal({
|
||||
: i18n('icu:AboutContactModal__title')}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--profile" />
|
||||
|
||||
{canHaveNicknameAndNote(conversation) &&
|
||||
conversation.titleNoNickname !== conversation.title &&
|
||||
conversation.titleNoNickname ? (
|
||||
{canHaveNicknameAndNote(contact) &&
|
||||
contact.titleNoNickname !== contact.title &&
|
||||
contact.titleNoNickname ? (
|
||||
<span>
|
||||
<I18n
|
||||
i18n={i18n}
|
||||
id="icu:AboutContactModal__TitleAndTitleWithoutNickname"
|
||||
components={{
|
||||
nickname: <UserText text={conversation.title} />,
|
||||
nickname: <UserText text={contact.title} />,
|
||||
titleNoNickname: (
|
||||
<Tooltip
|
||||
className="AboutContactModal__TitleWithoutNickname__Tooltip"
|
||||
@@ -185,15 +228,13 @@ export function AboutContactModal({
|
||||
i18n={i18n}
|
||||
id="icu:AboutContactModal__TitleWithoutNickname__Tooltip"
|
||||
components={{
|
||||
title: (
|
||||
<UserText text={conversation.titleNoNickname} />
|
||||
),
|
||||
title: <UserText text={contact.titleNoNickname} />,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
delay={0}
|
||||
>
|
||||
<UserText text={conversation.titleNoNickname} />
|
||||
<UserText text={contact.titleNoNickname} />
|
||||
</Tooltip>
|
||||
),
|
||||
muted,
|
||||
@@ -201,14 +242,13 @@ export function AboutContactModal({
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<UserText text={conversation.title} />
|
||||
<UserText text={contact.title} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isMe && !fromOrAddedByTrustedContact ? (
|
||||
<div className="AboutContactModal__row">
|
||||
<i
|
||||
className={`AboutContactModal__row__icon AboutContactModal__row__icon--${conversation.type === 'group' ? 'group' : 'direct'}-question`}
|
||||
className={`AboutContactModal__row__icon AboutContactModal__row__icon--${contact.type === 'group' ? 'group' : 'direct'}-question`}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -222,7 +262,7 @@ export function AboutContactModal({
|
||||
}}
|
||||
i18n={i18n}
|
||||
id={
|
||||
conversation.type === 'group'
|
||||
contact.type === 'group'
|
||||
? 'icu:ConversationHero--group-names'
|
||||
: 'icu:ConversationHero--profile-names'
|
||||
}
|
||||
@@ -230,8 +270,7 @@ export function AboutContactModal({
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isMe && conversation.isVerified ? (
|
||||
{!isMe && contact.isVerified ? (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--verified" />
|
||||
<button
|
||||
@@ -243,17 +282,12 @@ export function AboutContactModal({
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isMe && conversation.about ? (
|
||||
{!isMe && contact.about ? (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--about" />
|
||||
<About
|
||||
className="AboutContactModal__about"
|
||||
text={conversation.about}
|
||||
/>
|
||||
<About className="AboutContactModal__about" text={contact.about} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isMe && isSignalConnection ? (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--connections" />
|
||||
@@ -266,23 +300,35 @@ export function AboutContactModal({
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isMe && isInSystemContacts(conversation) ? (
|
||||
{!isMe && isInSystemContacts(contact) ? (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--person" />
|
||||
{i18n('icu:AboutContactModal__system-contact', {
|
||||
name:
|
||||
conversation.systemGivenName ||
|
||||
conversation.firstName ||
|
||||
conversation.title,
|
||||
name: contact.systemGivenName || contact.firstName || contact.title,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{conversation.phoneNumber ? (
|
||||
{shouldShowLabel && (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--label" />
|
||||
<div className="AboutContactModal__label-container">
|
||||
{labelEmojiElement}
|
||||
{contactLabelString}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{shouldShowAddLabel && (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--label" />
|
||||
{i18n('icu:AboutContactModal__add-member-label')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{contact.phoneNumber ? (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--phone" />
|
||||
<UserText text={conversation.phoneNumber} />
|
||||
<UserText text={contact.phoneNumber} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -294,8 +340,7 @@ export function AboutContactModal({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{conversation.note && (
|
||||
{contact.note && (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--note" />
|
||||
<button
|
||||
@@ -304,12 +349,11 @@ export function AboutContactModal({
|
||||
onClick={onOpenNotePreviewModal}
|
||||
>
|
||||
<div className="AboutContactModal__OneLineEllipsis">
|
||||
<UserText text={conversation.note} />
|
||||
<UserText text={contact.note} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{statusRow}
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -39,6 +39,9 @@ export default {
|
||||
badges: [],
|
||||
blockConversation: action('blockConversation'),
|
||||
contact: defaultContact,
|
||||
contactLabelEmoji: undefined,
|
||||
contactLabelString: undefined,
|
||||
contactNameColor: undefined,
|
||||
conversation: defaultGroup,
|
||||
hasActiveCall: false,
|
||||
hasStories: undefined,
|
||||
@@ -70,6 +73,29 @@ AsNonAdmin.args = {
|
||||
areWeAdmin: false,
|
||||
};
|
||||
|
||||
export const WithLabel = Template.bind({});
|
||||
WithLabel.args = {
|
||||
areWeAdmin: false,
|
||||
contactLabelEmoji: '💪🏼',
|
||||
contactLabelString: 'Strong',
|
||||
contactNameColor: '180',
|
||||
};
|
||||
|
||||
export const WithLabelNoEmoji = Template.bind({});
|
||||
WithLabelNoEmoji.args = {
|
||||
areWeAdmin: false,
|
||||
contactLabelString: 'Strong',
|
||||
contactNameColor: '220',
|
||||
};
|
||||
|
||||
export const WithLabelInvalidEmoji = Template.bind({});
|
||||
WithLabelInvalidEmoji.args = {
|
||||
areWeAdmin: false,
|
||||
contactLabelEmoji: '%',
|
||||
contactLabelString: 'Strong',
|
||||
contactNameColor: '220',
|
||||
};
|
||||
|
||||
export const AsAdmin = Template.bind({});
|
||||
AsAdmin.args = {
|
||||
areWeAdmin: true,
|
||||
@@ -97,14 +123,6 @@ WithoutPhoneNumber.args = {
|
||||
},
|
||||
};
|
||||
|
||||
export const ViewingSelf = Template.bind({});
|
||||
ViewingSelf.args = {
|
||||
contact: {
|
||||
...defaultContact,
|
||||
isMe: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithBadges = Template.bind({});
|
||||
WithBadges.args = {
|
||||
badges: getFakeBadges(2),
|
||||
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
InAnotherCallTooltip,
|
||||
getTooltipContent,
|
||||
} from './InAnotherCallTooltip.dom.js';
|
||||
import type { ContactModalStateType } from '../../state/ducks/globalModals.preload.js';
|
||||
import { GroupMemberLabel } from './ContactName.dom.js';
|
||||
|
||||
const log = createLogger('ContactModal');
|
||||
|
||||
@@ -39,6 +41,9 @@ export type PropsDataType = {
|
||||
areWeAdmin: boolean;
|
||||
badges: ReadonlyArray<BadgeType>;
|
||||
contact?: ConversationType;
|
||||
contactLabelEmoji: string | undefined;
|
||||
contactLabelString: string | undefined;
|
||||
contactNameColor: string | undefined;
|
||||
conversation?: ConversationType;
|
||||
hasStories?: HasStories;
|
||||
readonly i18n: LocalizerType;
|
||||
@@ -59,7 +64,7 @@ type PropsActionType = {
|
||||
showConversation: ShowConversationType;
|
||||
startAvatarDownload: () => void;
|
||||
toggleAdmin: (conversationId: string, contactId: string) => void;
|
||||
toggleAboutContactModal: (conversationId: string) => unknown;
|
||||
toggleAboutContactModal: (options: ContactModalStateType) => unknown;
|
||||
togglePip: () => void;
|
||||
toggleSafetyNumberModal: (conversationId: string) => unknown;
|
||||
toggleAddUserToAnotherGroupModal: (conversationId: string) => void;
|
||||
@@ -87,6 +92,9 @@ export function ContactModal({
|
||||
badges,
|
||||
blockConversation,
|
||||
contact,
|
||||
contactLabelEmoji,
|
||||
contactLabelString,
|
||||
contactNameColor,
|
||||
conversation,
|
||||
hasActiveCall,
|
||||
hasStories,
|
||||
@@ -344,7 +352,7 @@ export function ContactModal({
|
||||
className="ContactModal__name"
|
||||
onClick={ev => {
|
||||
ev.preventDefault();
|
||||
toggleAboutContactModal(contact.id);
|
||||
toggleAboutContactModal({ contactId: contact.id });
|
||||
}}
|
||||
>
|
||||
<div className="ContactModal__name__text">
|
||||
@@ -361,6 +369,19 @@ export function ContactModal({
|
||||
</div>
|
||||
<i className="ContactModal__name__chevron" />
|
||||
</button>
|
||||
{contactLabelString && contactNameColor && (
|
||||
<div className="ContactModal__member-label">
|
||||
<GroupMemberLabel
|
||||
emojiSize={14}
|
||||
contactLabel={{
|
||||
labelEmoji: contactLabelEmoji,
|
||||
labelString: contactLabelString,
|
||||
}}
|
||||
contactNameColor={contactNameColor}
|
||||
context="list"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!contact.isMe && renderQuickActions(contact.id)}
|
||||
<div className="ContactModal__divider" />
|
||||
<div className="ContactModal__button-container">
|
||||
|
||||
@@ -36,3 +36,57 @@ export function Colors(): React.JSX.Element {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ColorsWithLabels(): React.JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{ContactNameColors.map(color => (
|
||||
<div key={color}>
|
||||
<ContactName
|
||||
title={`Hello ${color}`}
|
||||
contactNameColor={color}
|
||||
contactLabel={{ labelEmoji: '✅', labelString: 'Task Wrangler' }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ColorsWithNoLabelEmoji(): React.JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{ContactNameColors.map(color => (
|
||||
<div key={color}>
|
||||
<ContactName
|
||||
title={`Hello ${color}`}
|
||||
contactNameColor={color}
|
||||
contactLabel={{
|
||||
labelEmoji: undefined,
|
||||
labelString: 'Task Wrangler',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ColorsWithInvalidLabelEmoji(): React.JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{ContactNameColors.map(color => (
|
||||
<div key={color}>
|
||||
<ContactName
|
||||
title={`Hello ${color}`}
|
||||
contactNameColor={color}
|
||||
contactLabel={{
|
||||
labelEmoji: '&',
|
||||
labelString: 'Task Wrangler',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,14 +4,28 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { Emojify } from './Emojify.dom.js';
|
||||
import type { ContactNameColorType } from '../../types/Colors.std.js';
|
||||
import { getClassNamesFor } from '../../util/getClassNamesFor.std.js';
|
||||
import type { ConversationType } from '../../state/ducks/conversations.preload.js';
|
||||
import { isSignalConversation as getIsSignalConversation } from '../../util/isSignalConversation.dom.js';
|
||||
import {
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantKeyByValue,
|
||||
isEmojiVariantValue,
|
||||
} from '../fun/data/emojis.std.js';
|
||||
import { useFunEmojiLocalizer } from '../fun/useFunEmojiLocalizer.dom.js';
|
||||
import { FunStaticEmoji } from '../fun/FunEmoji.dom.js';
|
||||
|
||||
import type { ConversationType } from '../../state/ducks/conversations.preload.js';
|
||||
import type { ContactNameColorType } from '../../types/Colors.std.js';
|
||||
import type { FunStaticEmojiSize } from '../fun/FunEmoji.dom.js';
|
||||
|
||||
export const missingEmojiPlaceholder = '⍰';
|
||||
|
||||
export type ContactNameData = {
|
||||
contactNameColor?: ContactNameColorType;
|
||||
contactLabel?: { labelString: string; labelEmoji: string | undefined };
|
||||
firstName?: string;
|
||||
isSignalConversation?: boolean;
|
||||
isMe?: boolean;
|
||||
@@ -47,6 +61,7 @@ export type PropsType = ContactNameData & {
|
||||
};
|
||||
|
||||
export function ContactName({
|
||||
contactLabel,
|
||||
contactNameColor,
|
||||
firstName,
|
||||
isSignalConversation,
|
||||
@@ -85,6 +100,96 @@ export function ContactName({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{contactLabel && (
|
||||
<>
|
||||
{' '}
|
||||
<GroupMemberLabel
|
||||
contactLabel={contactLabel}
|
||||
context="bubble"
|
||||
contactNameColor={contactNameColor}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</WrappingElement>
|
||||
);
|
||||
}
|
||||
|
||||
export type Context = 'bubble' | 'list';
|
||||
|
||||
export function GroupMemberLabel({
|
||||
emojiSize = 12,
|
||||
contactLabel,
|
||||
contactNameColor,
|
||||
context,
|
||||
module,
|
||||
}: {
|
||||
emojiSize?: FunStaticEmojiSize;
|
||||
contactLabel?: { labelString: string; labelEmoji: string | undefined };
|
||||
contactNameColor?: ContactNameColorType;
|
||||
context: Context;
|
||||
module?: string;
|
||||
}): ReactNode {
|
||||
const emojiLocalizer = useFunEmojiLocalizer();
|
||||
const getClassName = getClassNamesFor('module-contact-name', module);
|
||||
|
||||
if (!contactLabel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { labelEmoji, labelString } = contactLabel;
|
||||
|
||||
let emojiElement;
|
||||
if (labelEmoji && isEmojiVariantValue(labelEmoji)) {
|
||||
const emojiKey = getEmojiVariantKeyByValue(labelEmoji);
|
||||
const emojiData = getEmojiVariantByKey(emojiKey);
|
||||
|
||||
emojiElement = (
|
||||
<span
|
||||
className={classNames(
|
||||
getClassName('--label-pill--emoji'),
|
||||
getClassName(`--label-pill--${context}--emoji`)
|
||||
)}
|
||||
>
|
||||
<FunStaticEmoji
|
||||
role="img"
|
||||
aria-label={emojiLocalizer.getLocaleShortName(emojiData.key)}
|
||||
size={emojiSize}
|
||||
emoji={emojiData}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
} else if (labelEmoji) {
|
||||
emojiElement = (
|
||||
<span
|
||||
className={classNames(
|
||||
getClassName('--label-pill--emoji'),
|
||||
getClassName(`--label-pill--${context}--emoji`)
|
||||
)}
|
||||
>
|
||||
{missingEmojiPlaceholder}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
getClassName('--label-pill'),
|
||||
getClassName(`--label-pill--${context}`),
|
||||
getClassName(`--${contactNameColor}--label-pill--${context}`)
|
||||
)}
|
||||
>
|
||||
<span className={getClassName('--label-pill--inner')}>
|
||||
{emojiElement}
|
||||
<span
|
||||
className={classNames(
|
||||
getClassName('--label-pill--text'),
|
||||
getClassName(`--label-pill--${context}--text`)
|
||||
)}
|
||||
>
|
||||
<Emojify text={labelString} />
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ const createMemberships = ({
|
||||
return Array.from(new Array(count)).map(
|
||||
(_, i): GroupV2Membership => ({
|
||||
isAdmin: i % 3 === 0,
|
||||
labelEmoji: i % 6 === 0 ? '🟢' : undefined,
|
||||
labelString: i % 3 === 0 ? `Task Wrangler ${i}` : undefined,
|
||||
member: unknownContactIndices.includes(i)
|
||||
? getDefaultConversation({
|
||||
isMe: includeMe && i === 0,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { StoryViewModeType } from '../../types/Stories.std.js';
|
||||
import { Button, ButtonVariant } from '../Button.dom.js';
|
||||
import { SafetyTipsModal } from '../SafetyTipsModal.dom.js';
|
||||
import { I18n } from '../I18n.dom.js';
|
||||
import type { ContactModalStateType } from '../../state/ducks/globalModals.preload.js';
|
||||
|
||||
export type Props = {
|
||||
about?: string;
|
||||
@@ -41,7 +42,7 @@ export type Props = {
|
||||
startAvatarDownload: () => void;
|
||||
theme: ThemeType;
|
||||
viewUserStories: ViewUserStoriesActionCreatorType;
|
||||
toggleAboutContactModal: (conversationId: string) => unknown;
|
||||
toggleAboutContactModal: (options: ContactModalStateType) => unknown;
|
||||
toggleProfileNameWarningModal: (conversationType?: string) => unknown;
|
||||
} & Omit<AvatarProps, 'onClick' | 'size' | 'noteToSelf'>;
|
||||
|
||||
@@ -306,7 +307,7 @@ export function ConversationHero({
|
||||
className="module-conversation-hero__title"
|
||||
onClick={ev => {
|
||||
ev.preventDefault();
|
||||
toggleAboutContactModal(id);
|
||||
toggleAboutContactModal({ contactId: id });
|
||||
}}
|
||||
>
|
||||
<ContactName title={title} />
|
||||
|
||||
@@ -247,6 +247,7 @@ export type PropsData = {
|
||||
id: string;
|
||||
renderingContext: RenderingContextType;
|
||||
contactNameColor?: ContactNameColorType;
|
||||
contactLabel?: { labelString: string; labelEmoji: string | undefined };
|
||||
conversationColor: ConversationColorType;
|
||||
conversationTitle: string;
|
||||
customColor?: CustomColorType;
|
||||
@@ -1135,7 +1136,8 @@ export class Message extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
#renderAuthor(): ReactNode {
|
||||
const { author, contactNameColor, i18n, isSticker, quote } = this.props;
|
||||
const { author, contactLabel, contactNameColor, i18n, isSticker, quote } =
|
||||
this.props;
|
||||
|
||||
if (!this.#shouldRenderAuthor()) {
|
||||
return null;
|
||||
@@ -1153,6 +1155,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||
>
|
||||
<ContactName
|
||||
contactNameColor={contactNameColor}
|
||||
contactLabel={contactLabel}
|
||||
title={author.isMe ? i18n('icu:you') : author.title}
|
||||
module={moduleName}
|
||||
/>
|
||||
@@ -3244,6 +3247,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||
: null,
|
||||
isTargeted ? 'module-message__container--targeted' : null,
|
||||
lighterSelect ? 'module-message__container--targeted-lighter' : null,
|
||||
isStickerLike ? 'module-message__container--sticker-like' : null,
|
||||
!isStickerLike ? `module-message__container--${direction}` : null,
|
||||
isEmojiOnly ? 'module-message__container--emoji' : null,
|
||||
!isStickerLike && direction === 'outgoing'
|
||||
|
||||
@@ -256,6 +256,8 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
conversationId: overrideProps.conversationId ?? '',
|
||||
conversationType: overrideProps.conversationType || 'direct',
|
||||
contact: overrideProps.contact,
|
||||
contactNameColor: overrideProps.contactNameColor,
|
||||
contactLabel: overrideProps.contactLabel,
|
||||
// disableMenu: overrideProps.disableMenu,
|
||||
deletedForEveryone: overrideProps.deletedForEveryone,
|
||||
disableScroll: overrideProps.disableScroll,
|
||||
@@ -370,7 +372,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
});
|
||||
|
||||
const renderMany = (propsArray: ReadonlyArray<Props>) => (
|
||||
<>
|
||||
<div className="module-timeline--width-wide">
|
||||
{propsArray.map((message, index) => (
|
||||
<TimelineMessage
|
||||
key={`${message.text}_${index}_${message.direction}`}
|
||||
@@ -379,7 +381,7 @@ const renderMany = (propsArray: ReadonlyArray<Props>) => (
|
||||
shouldCollapseBelow={Boolean(propsArray[index + 1])}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderThree = (props: Props) =>
|
||||
@@ -402,7 +404,7 @@ const renderBothDirections = (props: Props) => (
|
||||
);
|
||||
|
||||
const renderOneInBothDirections = (props: Props) => (
|
||||
<>
|
||||
<div className="module-timeline--width-wide">
|
||||
<TimelineMessage {...props} />
|
||||
<TimelineMessage
|
||||
{...props}
|
||||
@@ -412,7 +414,7 @@ const renderOneInBothDirections = (props: Props) => (
|
||||
canEndPoll: true,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const PlainMessage = Template.bind({});
|
||||
@@ -757,6 +759,7 @@ export const AvatarInGroup = Template.bind({});
|
||||
AvatarInGroup.args = {
|
||||
author: getDefaultConversation({ avatarUrl: pngUrl }),
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
status: 'sent',
|
||||
text: 'Hello it is me, the saxophone.',
|
||||
};
|
||||
@@ -765,10 +768,55 @@ export const BadgeInGroup = Template.bind({});
|
||||
BadgeInGroup.args = {
|
||||
conversationType: 'group',
|
||||
getPreferredBadge: () => getFakeBadge(),
|
||||
contactNameColor: '300',
|
||||
status: 'sent',
|
||||
text: 'Hello it is me, the saxophone.',
|
||||
};
|
||||
|
||||
export const LabelInGroup = Template.bind({});
|
||||
LabelInGroup.args = {
|
||||
conversationType: 'group',
|
||||
status: 'sent',
|
||||
text: 'Hello it is me, the saxophone.',
|
||||
contactNameColor: '260',
|
||||
contactLabel: {
|
||||
labelEmoji: '🍗',
|
||||
labelString: 'Chicken Taster',
|
||||
},
|
||||
};
|
||||
|
||||
export const LabelInGroupWithLongName = Template.bind({});
|
||||
LabelInGroupWithLongName.args = {
|
||||
conversationType: 'group',
|
||||
status: 'sent',
|
||||
text: 'Hello it is me, the saxophone.',
|
||||
author: {
|
||||
...getDefaultConversation(),
|
||||
title: 'Long long long long long long long long long long long name',
|
||||
},
|
||||
contactNameColor: '260',
|
||||
contactLabel: {
|
||||
labelEmoji: '🍗',
|
||||
labelString: 'Chicken Taster',
|
||||
},
|
||||
};
|
||||
|
||||
export const LabelInGroupWithLongNameAndLongMessage = Template.bind({});
|
||||
LabelInGroupWithLongNameAndLongMessage.args = {
|
||||
conversationType: 'group',
|
||||
status: 'sent',
|
||||
text: 'Hello it is me, the saxophone. I am a good friend of yours. Do you remember? A long long long long long long long time ago.',
|
||||
author: {
|
||||
...getDefaultConversation(),
|
||||
title: 'Long long long long long long long long long long long name',
|
||||
},
|
||||
contactNameColor: '260',
|
||||
contactLabel: {
|
||||
labelEmoji: '🍗',
|
||||
labelString: 'Chicken Taster',
|
||||
},
|
||||
};
|
||||
|
||||
export const Sticker = Template.bind({});
|
||||
Sticker.args = {
|
||||
attachments: [
|
||||
@@ -784,6 +832,69 @@ Sticker.args = {
|
||||
status: 'sent',
|
||||
};
|
||||
|
||||
export const StickerInGroup = Template.bind({});
|
||||
StickerInGroup.args = {
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
url: '/fixtures/512x515-thumbs-up-lincoln.webp',
|
||||
fileName: '512x515-thumbs-up-lincoln.webp',
|
||||
contentType: IMAGE_WEBP,
|
||||
width: 128,
|
||||
height: 128,
|
||||
}),
|
||||
],
|
||||
conversationType: 'group',
|
||||
contactNameColor: '180',
|
||||
isSticker: true,
|
||||
status: 'sent',
|
||||
};
|
||||
|
||||
export const StickerWithLabelInGroup = Template.bind({});
|
||||
StickerWithLabelInGroup.args = {
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
url: '/fixtures/512x515-thumbs-up-lincoln.webp',
|
||||
fileName: '512x515-thumbs-up-lincoln.webp',
|
||||
contentType: IMAGE_WEBP,
|
||||
width: 128,
|
||||
height: 128,
|
||||
}),
|
||||
],
|
||||
conversationType: 'group',
|
||||
isSticker: true,
|
||||
status: 'sent',
|
||||
contactNameColor: '260',
|
||||
contactLabel: {
|
||||
labelEmoji: '🍗',
|
||||
labelString: 'Chicken Taster',
|
||||
},
|
||||
};
|
||||
|
||||
export const StickerWithLongNameAndLabelInGroup = Template.bind({});
|
||||
StickerWithLongNameAndLabelInGroup.args = {
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
url: '/fixtures/512x515-thumbs-up-lincoln.webp',
|
||||
fileName: '512x515-thumbs-up-lincoln.webp',
|
||||
contentType: IMAGE_WEBP,
|
||||
width: 128,
|
||||
height: 128,
|
||||
}),
|
||||
],
|
||||
conversationType: 'group',
|
||||
isSticker: true,
|
||||
status: 'sent',
|
||||
author: {
|
||||
...getDefaultConversation(),
|
||||
title: 'Long long long long long long long long long long long name',
|
||||
},
|
||||
contactNameColor: '280',
|
||||
contactLabel: {
|
||||
labelEmoji: '🍗',
|
||||
labelString: 'Chicken Taster',
|
||||
},
|
||||
};
|
||||
|
||||
export const Quote = Template.bind({});
|
||||
Quote.args = {
|
||||
quote: {
|
||||
@@ -806,6 +917,7 @@ Quote.args = {
|
||||
badges: [],
|
||||
},
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
};
|
||||
|
||||
export function Deleted(): React.JSX.Element {
|
||||
@@ -834,6 +946,7 @@ export const DeletedWithExpireTimer = Template.bind({});
|
||||
DeletedWithExpireTimer.args = {
|
||||
timestamp: Date.now() - 60 * 1000,
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
deletedForEveryone: true,
|
||||
canForward: false,
|
||||
expirationLength: 5 * 60 * 1000,
|
||||
@@ -846,6 +959,7 @@ export function DeletedWithError(): React.JSX.Element {
|
||||
timestamp: Date.now() - 60 * 1000,
|
||||
// canDeleteForEveryone: true,
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
deletedForEveryone: true,
|
||||
status: 'partial-sent',
|
||||
direction: 'outgoing',
|
||||
@@ -854,6 +968,7 @@ export function DeletedWithError(): React.JSX.Element {
|
||||
timestamp: Date.now() - 60 * 1000,
|
||||
// canDeleteForEveryone: true,
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
deletedForEveryone: true,
|
||||
status: 'error',
|
||||
direction: 'outgoing',
|
||||
@@ -985,6 +1100,7 @@ LinkPreviewInGroup.args = {
|
||||
status: 'sent',
|
||||
text: 'Be sure to look at https://www.signal.org',
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
};
|
||||
|
||||
export const LinkPreviewWithLongWord = Template.bind({});
|
||||
@@ -1010,6 +1126,7 @@ LinkPreviewWithLongWord.args = {
|
||||
status: 'sent',
|
||||
text: 'Be sure to look at https://www.signal.org',
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
};
|
||||
|
||||
export const LinkPreviewWithQuote = Template.bind({});
|
||||
@@ -1048,6 +1165,7 @@ LinkPreviewWithQuote.args = {
|
||||
status: 'sent',
|
||||
text: 'Be sure to look at https://www.signal.org',
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
};
|
||||
|
||||
export const LinkPreviewWithSmallImage = Template.bind({});
|
||||
@@ -1851,6 +1969,7 @@ GifInAGroup.args = {
|
||||
}),
|
||||
],
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
status: 'sent',
|
||||
};
|
||||
|
||||
@@ -2168,6 +2287,7 @@ function createMockPollWithVoters(
|
||||
export const Poll = Template.bind({});
|
||||
Poll.args = {
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
poll: {
|
||||
question: 'What should we have for lunch?',
|
||||
options: ['Pizza 🍕', 'Sushi 🍱', 'Tacos 🌮', 'Salad 🥗'],
|
||||
@@ -2182,6 +2302,7 @@ Poll.args = {
|
||||
export const PollMultipleChoice = Template.bind({});
|
||||
PollMultipleChoice.args = {
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
poll: {
|
||||
question: 'Which features would you like to see in the next update?',
|
||||
options: ['Dark mode', 'Video calls', 'File sharing', 'Reactions', 'Polls'],
|
||||
@@ -2196,6 +2317,7 @@ PollMultipleChoice.args = {
|
||||
export const PollWithVotes = Template.bind({});
|
||||
PollWithVotes.args = {
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
poll: createMockPollWithVoters(
|
||||
'Best day for the team meeting?',
|
||||
['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
|
||||
@@ -2220,6 +2342,7 @@ PollWithVotes.args = {
|
||||
export const PollWithPendingVotes = Template.bind({});
|
||||
PollWithPendingVotes.args = {
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
poll: createMockPollWithVoters(
|
||||
'Best day for the team meeting?',
|
||||
['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
|
||||
@@ -2248,6 +2371,7 @@ PollWithPendingVotes.args = {
|
||||
export const PollTerminated = Template.bind({});
|
||||
PollTerminated.args = {
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
poll: createMockPollWithVoters(
|
||||
'Quick poll: Coffee or tea?',
|
||||
['Coffee ☕', 'Tea 🍵'],
|
||||
@@ -2274,6 +2398,7 @@ PollTerminated.args = {
|
||||
export const PollLongText = Template.bind({});
|
||||
PollLongText.args = {
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
poll: createMockPollWithVoters(
|
||||
'Given the current situation with remote work becoming more prevalent, what would be your preferred working arrangement for the future once everything stabilizes?',
|
||||
[
|
||||
@@ -2299,6 +2424,7 @@ PollLongText.args = {
|
||||
export const PollMultipleChoiceWithVotes = Template.bind({});
|
||||
PollMultipleChoiceWithVotes.args = {
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
poll: createMockPollWithVoters(
|
||||
'Which toppings do you want on the pizza?',
|
||||
[
|
||||
@@ -2414,6 +2540,7 @@ export function PollAnimationPlayground(): React.JSX.Element {
|
||||
|
||||
const props = createProps({
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
poll,
|
||||
status: 'sent',
|
||||
sendPollVote: handleSendPollVote,
|
||||
@@ -2660,6 +2787,7 @@ TapToViewImageInGroup.args = {
|
||||
isTapToView: true,
|
||||
status: 'sent',
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
};
|
||||
|
||||
export const TapToViewVideo = Template.bind({});
|
||||
@@ -2754,7 +2882,7 @@ export function Colors(): React.JSX.Element {
|
||||
<>
|
||||
{ConversationColors.map(color => (
|
||||
<div key={color}>
|
||||
{renderBothDirections(
|
||||
{renderOneInBothDirections(
|
||||
createProps({
|
||||
conversationColor: color,
|
||||
text: `Here is a preview of the chat color: ${color}. The color is visible to only you.`,
|
||||
@@ -2897,18 +3025,21 @@ export const CollapsingTextOnlyGroupMessages = (): React.JSX.Element => {
|
||||
createProps({
|
||||
author,
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
text: 'One',
|
||||
timestamp: Date.now() - 2 * MINUTE,
|
||||
}),
|
||||
createProps({
|
||||
author,
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
text: 'Two',
|
||||
timestamp: Date.now() - MINUTE,
|
||||
}),
|
||||
createProps({
|
||||
author,
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
text: 'Three',
|
||||
}),
|
||||
]);
|
||||
|
||||
+126
-87
@@ -18,6 +18,8 @@ import { ThemeType } from '../../../types/Util.std.js';
|
||||
import { DurationInSeconds } from '../../../util/durations/index.std.js';
|
||||
import { NavTab } from '../../../types/Nav.std.js';
|
||||
import { getFakeCallHistoryGroup } from '../../../test-helpers/getFakeCallHistoryGroup.std.js';
|
||||
import { ContactNameColors } from '../../../types/Colors.std.js';
|
||||
import { isNotNil } from '../../../util/isNotNil.std.js';
|
||||
|
||||
const { times } = lodash;
|
||||
|
||||
@@ -41,97 +43,118 @@ const allCandidateContacts = times(10, () => getDefaultConversation());
|
||||
const createProps = (
|
||||
hasGroupLink = false,
|
||||
expireTimer?: DurationInSeconds
|
||||
): Props => ({
|
||||
acceptConversation: action('acceptConversation'),
|
||||
addMembersToGroup: async () => {
|
||||
action('addMembersToGroup');
|
||||
},
|
||||
areWeASubscriber: false,
|
||||
blockConversation: action('blockConversation'),
|
||||
canEditGroupInfo: false,
|
||||
canAddNewMembers: false,
|
||||
conversation: expireTimer
|
||||
? {
|
||||
...conversation,
|
||||
expireTimer,
|
||||
}
|
||||
: conversation,
|
||||
hasActiveCall: false,
|
||||
hasGroupLink,
|
||||
getPreferredBadge: () => undefined,
|
||||
getProfilesForConversation: action('getProfilesForConversation'),
|
||||
groupsInCommon: [],
|
||||
i18n,
|
||||
isAdmin: false,
|
||||
isGroup: true,
|
||||
isSignalConversation: false,
|
||||
leaveGroup: action('leaveGroup'),
|
||||
hasMedia: true,
|
||||
memberships: times(32, i => ({
|
||||
): Props => {
|
||||
const memberships = times(32, i => ({
|
||||
isAdmin: i === 1,
|
||||
labelEmoji: i % 6 === 0 ? '🟢' : undefined,
|
||||
labelString: i % 3 === 0 ? `Task Wrangler ${i}` : undefined,
|
||||
member: getDefaultConversation({
|
||||
isMe: i === 2,
|
||||
}),
|
||||
})),
|
||||
maxGroupSize: 1001,
|
||||
maxRecommendedGroupSize: 151,
|
||||
pendingApprovalMemberships: times(8, () => ({
|
||||
member: getDefaultConversation(),
|
||||
})),
|
||||
pendingMemberships: times(5, () => ({
|
||||
metadata: {},
|
||||
member: getDefaultConversation(),
|
||||
})),
|
||||
selectedNavTab: NavTab.Chats,
|
||||
setDisappearingMessages: action('setDisappearingMessages'),
|
||||
showContactModal: action('showContactModal'),
|
||||
pushPanelForConversation: action('pushPanelForConversation'),
|
||||
showConversation: action('showConversation'),
|
||||
startAvatarDownload: action('startAvatarDownload'),
|
||||
updateGroupAttributes: async () => {
|
||||
action('updateGroupAttributes')();
|
||||
},
|
||||
deleteAvatarFromDisk: action('deleteAvatarFromDisk'),
|
||||
replaceAvatar: action('replaceAvatar'),
|
||||
saveAvatarToDisk: action('saveAvatarToDisk'),
|
||||
setMuteExpiration: action('setMuteExpiration'),
|
||||
userAvatarData: [],
|
||||
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
|
||||
toggleAboutContactModal: action('toggleAboutContactModal'),
|
||||
toggleAddUserToAnotherGroupModal: action('toggleAddUserToAnotherGroup'),
|
||||
onDeleteNicknameAndNote: action('onDeleteNicknameAndNote'),
|
||||
onOpenEditNicknameAndNoteModal: action('onOpenEditNicknameAndNoteModal'),
|
||||
onOutgoingAudioCallInConversation: action(
|
||||
'onOutgoingAudioCallInConversation'
|
||||
),
|
||||
onOutgoingVideoCallInConversation: action(
|
||||
'onOutgoingVideoCallInConversation'
|
||||
),
|
||||
searchInConversation: action('searchInConversation'),
|
||||
theme: ThemeType.light,
|
||||
renderChooseGroupMembersModal: props => {
|
||||
return (
|
||||
<ChooseGroupMembersModal
|
||||
{...props}
|
||||
candidateContacts={allCandidateContacts}
|
||||
selectedContacts={[]}
|
||||
regionCode="US"
|
||||
theme={ThemeType.light}
|
||||
i18n={i18n}
|
||||
lookupConversationWithoutServiceId={makeFakeLookupConversationWithoutServiceId()}
|
||||
ourE164={undefined}
|
||||
ourUsername={undefined}
|
||||
showUserNotFoundModal={action('showUserNotFoundModal')}
|
||||
username={undefined}
|
||||
/>
|
||||
);
|
||||
},
|
||||
renderConfirmAdditionsModal: props => {
|
||||
return (
|
||||
<ConfirmAdditionsModal {...props} selectedContacts={[]} i18n={i18n} />
|
||||
);
|
||||
},
|
||||
});
|
||||
}));
|
||||
const memberColors = new Map<string, string>(
|
||||
memberships
|
||||
.map((membership, i): [string, string] | null => {
|
||||
const { serviceId } = membership.member;
|
||||
|
||||
if (!serviceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [serviceId.toString(), ContactNameColors[i]];
|
||||
})
|
||||
.filter(isNotNil)
|
||||
);
|
||||
|
||||
return {
|
||||
acceptConversation: action('acceptConversation'),
|
||||
addMembersToGroup: async () => {
|
||||
action('addMembersToGroup');
|
||||
},
|
||||
areWeASubscriber: false,
|
||||
blockConversation: action('blockConversation'),
|
||||
canEditGroupInfo: false,
|
||||
canAddNewMembers: false,
|
||||
conversation: expireTimer
|
||||
? {
|
||||
...conversation,
|
||||
expireTimer,
|
||||
}
|
||||
: conversation,
|
||||
hasActiveCall: false,
|
||||
hasGroupLink,
|
||||
getPreferredBadge: () => undefined,
|
||||
getProfilesForConversation: action('getProfilesForConversation'),
|
||||
groupsInCommon: [],
|
||||
i18n,
|
||||
isAdmin: false,
|
||||
isEditMemberLabelEnabled: true,
|
||||
isGroup: true,
|
||||
isSignalConversation: false,
|
||||
leaveGroup: action('leaveGroup'),
|
||||
hasMedia: true,
|
||||
memberships,
|
||||
memberColors,
|
||||
maxGroupSize: 1001,
|
||||
maxRecommendedGroupSize: 151,
|
||||
pendingApprovalMemberships: times(8, () => ({
|
||||
member: getDefaultConversation(),
|
||||
})),
|
||||
pendingMemberships: times(5, () => ({
|
||||
metadata: {},
|
||||
member: getDefaultConversation(),
|
||||
})),
|
||||
selectedNavTab: NavTab.Chats,
|
||||
setDisappearingMessages: action('setDisappearingMessages'),
|
||||
showContactModal: action('showContactModal'),
|
||||
pushPanelForConversation: action('pushPanelForConversation'),
|
||||
showConversation: action('showConversation'),
|
||||
startAvatarDownload: action('startAvatarDownload'),
|
||||
updateGroupAttributes: async () => {
|
||||
action('updateGroupAttributes')();
|
||||
},
|
||||
deleteAvatarFromDisk: action('deleteAvatarFromDisk'),
|
||||
replaceAvatar: action('replaceAvatar'),
|
||||
saveAvatarToDisk: action('saveAvatarToDisk'),
|
||||
setMuteExpiration: action('setMuteExpiration'),
|
||||
userAvatarData: [],
|
||||
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
|
||||
toggleAboutContactModal: action('toggleAboutContactModal'),
|
||||
toggleAddUserToAnotherGroupModal: action('toggleAddUserToAnotherGroup'),
|
||||
onDeleteNicknameAndNote: action('onDeleteNicknameAndNote'),
|
||||
onOpenEditNicknameAndNoteModal: action('onOpenEditNicknameAndNoteModal'),
|
||||
onOutgoingAudioCallInConversation: action(
|
||||
'onOutgoingAudioCallInConversation'
|
||||
),
|
||||
onOutgoingVideoCallInConversation: action(
|
||||
'onOutgoingVideoCallInConversation'
|
||||
),
|
||||
searchInConversation: action('searchInConversation'),
|
||||
theme: ThemeType.light,
|
||||
renderChooseGroupMembersModal: props => {
|
||||
return (
|
||||
<ChooseGroupMembersModal
|
||||
{...props}
|
||||
candidateContacts={allCandidateContacts}
|
||||
selectedContacts={[]}
|
||||
regionCode="US"
|
||||
theme={ThemeType.light}
|
||||
i18n={i18n}
|
||||
lookupConversationWithoutServiceId={makeFakeLookupConversationWithoutServiceId()}
|
||||
ourE164={undefined}
|
||||
ourUsername={undefined}
|
||||
showUserNotFoundModal={action('showUserNotFoundModal')}
|
||||
username={undefined}
|
||||
/>
|
||||
);
|
||||
},
|
||||
renderConfirmAdditionsModal: props => {
|
||||
return (
|
||||
<ConfirmAdditionsModal {...props} selectedContacts={[]} i18n={i18n} />
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export function Basic(): React.JSX.Element {
|
||||
const props = createProps();
|
||||
@@ -170,6 +193,8 @@ export function AsLastAdmin(): React.JSX.Element {
|
||||
isAdmin
|
||||
memberships={times(32, i => ({
|
||||
isAdmin: i === 2,
|
||||
labelEmoji: i % 6 === 0 ? '🟢' : undefined,
|
||||
labelString: i % 3 === 0 ? `Last Admin ${i}` : undefined,
|
||||
member: getDefaultConversation({
|
||||
isMe: i === 2,
|
||||
}),
|
||||
@@ -188,6 +213,8 @@ export function AsOnlyAdmin(): React.JSX.Element {
|
||||
memberships={[
|
||||
{
|
||||
isAdmin: true,
|
||||
labelEmoji: undefined,
|
||||
labelString: undefined,
|
||||
member: getDefaultConversation({
|
||||
isMe: true,
|
||||
}),
|
||||
@@ -203,6 +230,18 @@ export function GroupEditable(): React.JSX.Element {
|
||||
return <ConversationDetails {...props} canEditGroupInfo />;
|
||||
}
|
||||
|
||||
export function GroupEditableEditLabelDisabled(): React.JSX.Element {
|
||||
const props = createProps();
|
||||
|
||||
return (
|
||||
<ConversationDetails
|
||||
{...props}
|
||||
canEditGroupInfo
|
||||
isEditMemberLabelEnabled={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function GroupEditableWithCustomDisappearingTimeout(): React.JSX.Element {
|
||||
const props = createProps(false, DurationInSeconds.fromDays(3));
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
ConversationType,
|
||||
PushPanelForConversationActionType,
|
||||
ShowConversationType,
|
||||
UpdateGroupAttributesType,
|
||||
} from '../../../state/ducks/conversations.preload.js';
|
||||
import type { PreferredBadgeSelectorType } from '../../../state/selectors/badges.preload.js';
|
||||
import type { SmartChooseGroupMembersModalPropsType } from '../../../state/smart/ChooseGroupMembersModal.preload.js';
|
||||
@@ -63,6 +64,7 @@ import {
|
||||
getTooltipContent,
|
||||
} from '../InAnotherCallTooltip.dom.js';
|
||||
import { BadgeSustainerInstructionsDialog } from '../../BadgeSustainerInstructionsDialog.dom.js';
|
||||
import type { ContactModalStateType } from '../../../state/ducks/globalModals.preload.js';
|
||||
|
||||
enum ModalState {
|
||||
AddingGroupMembers,
|
||||
@@ -88,12 +90,14 @@ export type StateProps = {
|
||||
hasActiveCall: boolean;
|
||||
i18n: LocalizerType;
|
||||
isAdmin: boolean;
|
||||
isEditMemberLabelEnabled: boolean;
|
||||
isGroup: boolean;
|
||||
isSignalConversation: boolean;
|
||||
groupsInCommon: ReadonlyArray<ConversationType>;
|
||||
maxGroupSize: number;
|
||||
maxRecommendedGroupSize: number;
|
||||
memberships: ReadonlyArray<GroupV2Membership>;
|
||||
memberColors: Map<string, string>;
|
||||
pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
|
||||
pendingAvatarDownload?: boolean;
|
||||
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
|
||||
@@ -136,21 +140,10 @@ type ActionProps = {
|
||||
setMuteExpiration: (id: string, muteExpiresAt: undefined | number) => unknown;
|
||||
showContactModal: (contactId: string, conversationId?: string) => void;
|
||||
showConversation: ShowConversationType;
|
||||
toggleAboutContactModal: (contactId: string) => void;
|
||||
toggleAboutContactModal: (options: ContactModalStateType) => void;
|
||||
toggleAddUserToAnotherGroupModal: (contactId?: string) => void;
|
||||
toggleSafetyNumberModal: (conversationId: string) => unknown;
|
||||
updateGroupAttributes: (
|
||||
conversationId: string,
|
||||
_: Readonly<{
|
||||
avatar?: undefined | Uint8Array;
|
||||
description?: string;
|
||||
title?: string;
|
||||
}>,
|
||||
opts: {
|
||||
onSuccess?: () => unknown;
|
||||
onFailure?: () => unknown;
|
||||
}
|
||||
) => unknown;
|
||||
updateGroupAttributes: UpdateGroupAttributesType;
|
||||
};
|
||||
|
||||
export type Props = StateProps & ActionProps;
|
||||
@@ -188,10 +181,12 @@ export function ConversationDetails({
|
||||
hasActiveCall,
|
||||
i18n,
|
||||
isAdmin,
|
||||
isEditMemberLabelEnabled,
|
||||
isGroup,
|
||||
isSignalConversation,
|
||||
leaveGroup,
|
||||
memberships,
|
||||
memberColors,
|
||||
maxGroupSize,
|
||||
maxRecommendedGroupSize,
|
||||
onDeleteNicknameAndNote,
|
||||
@@ -732,6 +727,7 @@ export function ConversationDetails({
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
i18n={i18n}
|
||||
memberships={memberships}
|
||||
memberColors={memberColors}
|
||||
showContactModal={showContactModal}
|
||||
startAddingNewMembers={() => {
|
||||
setModalState(ModalState.AddingGroupMembers);
|
||||
@@ -759,6 +755,22 @@ export function ConversationDetails({
|
||||
right={hasGroupLink ? i18n('icu:on') : i18n('icu:off')}
|
||||
/>
|
||||
) : null}
|
||||
{canEditGroupInfo && isEditMemberLabelEnabled ? (
|
||||
<PanelRow
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('icu:ConversationDetails--member-label')}
|
||||
icon={IconType.tag}
|
||||
/>
|
||||
}
|
||||
label={i18n('icu:ConversationDetails--member-label')}
|
||||
onClick={() =>
|
||||
pushPanelForConversation({
|
||||
type: PanelType.GroupMemberLabelEditor,
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<PanelRow
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { BadgeType } from '../../../badges/types.std.js';
|
||||
import { UserText } from '../../UserText.dom.js';
|
||||
import { isInSystemContacts } from '../../../util/isInSystemContacts.std.js';
|
||||
import { InContactsIcon } from '../../InContactsIcon.dom.js';
|
||||
import type { ContactModalStateType } from '../../../state/ducks/globalModals.preload.js';
|
||||
|
||||
export type Props = {
|
||||
areWeASubscriber: boolean;
|
||||
@@ -30,7 +31,7 @@ export type Props = {
|
||||
pendingAvatarDownload: boolean;
|
||||
startAvatarDownload: () => void;
|
||||
startEditing: (isGroupTitle: boolean) => void;
|
||||
toggleAboutContactModal: (contactId: string) => void;
|
||||
toggleAboutContactModal: (options: ContactModalStateType) => void;
|
||||
theme: ThemeType;
|
||||
};
|
||||
|
||||
@@ -237,7 +238,7 @@ export function ConversationDetailsHeader({
|
||||
onClick={ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
toggleAboutContactModal(conversation.id);
|
||||
toggleAboutContactModal({ contactId: conversation.id });
|
||||
}}
|
||||
className="ConversationDetailsHeader__about-button"
|
||||
>
|
||||
|
||||
@@ -31,6 +31,7 @@ export enum IconType {
|
||||
'reset' = 'reset',
|
||||
'share' = 'share',
|
||||
'spinner' = 'spinner',
|
||||
'tag' = 'tag',
|
||||
'timer' = 'timer',
|
||||
'trash' = 'trash',
|
||||
'verify' = 'verify',
|
||||
|
||||
+41
-27
@@ -12,9 +12,37 @@ import type {
|
||||
GroupV2Membership,
|
||||
} from './ConversationDetailsMembershipList.dom.js';
|
||||
import { ConversationDetailsMembershipList } from './ConversationDetailsMembershipList.dom.js';
|
||||
import { ContactNameColors } from '../../../types/Colors.std.js';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
const createMemberships = (
|
||||
numberOfMemberships = 10
|
||||
): Array<GroupV2Membership> => {
|
||||
return Array.from(new Array(numberOfMemberships)).map(
|
||||
(_, i): GroupV2Membership => ({
|
||||
isAdmin: i % 4 === 0,
|
||||
labelEmoji: i % 6 === 0 ? '🟢' : undefined,
|
||||
labelString: i % 3 === 0 ? `Task Wrangler ${i}` : undefined,
|
||||
member: getDefaultConversation({
|
||||
isMe: i === 2,
|
||||
}),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const getMemberColors = (
|
||||
memberships: Array<GroupV2Membership>
|
||||
): Map<string, string> =>
|
||||
new Map(
|
||||
memberships.map((membership, i) => [
|
||||
membership.member.id,
|
||||
ContactNameColors[i],
|
||||
])
|
||||
);
|
||||
|
||||
const defaultMemberships = createMemberships();
|
||||
|
||||
export default {
|
||||
title:
|
||||
'Components/Conversation/ConversationDetails/ConversationDetailsMembershipList',
|
||||
@@ -24,49 +52,37 @@ export default {
|
||||
conversationId: '123',
|
||||
getPreferredBadge: () => undefined,
|
||||
i18n,
|
||||
memberships: [],
|
||||
memberships: defaultMemberships,
|
||||
memberColors: getMemberColors(defaultMemberships),
|
||||
showContactModal: action('showContactModal'),
|
||||
startAddingNewMembers: action('startAddingNewMembers'),
|
||||
theme: ThemeType.light,
|
||||
},
|
||||
} satisfies Meta<Props>;
|
||||
|
||||
const createMemberships = (
|
||||
numberOfMemberships = 10
|
||||
): Array<GroupV2Membership> => {
|
||||
return Array.from(new Array(numberOfMemberships)).map(
|
||||
(_, i): GroupV2Membership => ({
|
||||
isAdmin: i % 3 === 0,
|
||||
member: getDefaultConversation({
|
||||
isMe: i === 2,
|
||||
}),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export function Few(args: Props): React.JSX.Element {
|
||||
const memberships = createMemberships(3);
|
||||
const memberships = defaultMemberships.slice(3);
|
||||
return (
|
||||
<ConversationDetailsMembershipList {...args} memberships={memberships} />
|
||||
);
|
||||
}
|
||||
|
||||
export function Limit(args: Props): React.JSX.Element {
|
||||
const memberships = createMemberships(5);
|
||||
const memberships = defaultMemberships.slice(5);
|
||||
return (
|
||||
<ConversationDetailsMembershipList {...args} memberships={memberships} />
|
||||
);
|
||||
}
|
||||
|
||||
export function Limit1(args: Props): React.JSX.Element {
|
||||
const memberships = createMemberships(6);
|
||||
const memberships = defaultMemberships.slice(6);
|
||||
return (
|
||||
<ConversationDetailsMembershipList {...args} memberships={memberships} />
|
||||
);
|
||||
}
|
||||
|
||||
export function Limit2(args: Props): React.JSX.Element {
|
||||
const memberships = createMemberships(7);
|
||||
const memberships = defaultMemberships.slice(7);
|
||||
return (
|
||||
<ConversationDetailsMembershipList {...args} memberships={memberships} />
|
||||
);
|
||||
@@ -74,8 +90,13 @@ export function Limit2(args: Props): React.JSX.Element {
|
||||
|
||||
export function Many(args: Props): React.JSX.Element {
|
||||
const memberships = createMemberships(100);
|
||||
const memberColors = getMemberColors(memberships);
|
||||
return (
|
||||
<ConversationDetailsMembershipList {...args} memberships={memberships} />
|
||||
<ConversationDetailsMembershipList
|
||||
{...args}
|
||||
memberships={memberships}
|
||||
memberColors={memberColors}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,12 +105,5 @@ export function None(args: Props): React.JSX.Element {
|
||||
}
|
||||
|
||||
export function CanAddNewMembers(args: Props): React.JSX.Element {
|
||||
const memberships = createMemberships(10);
|
||||
return (
|
||||
<ConversationDetailsMembershipList
|
||||
{...args}
|
||||
memberships={memberships}
|
||||
canAddNewMembers
|
||||
/>
|
||||
);
|
||||
return <ConversationDetailsMembershipList {...args} canAddNewMembers />;
|
||||
}
|
||||
|
||||
+48
-19
@@ -16,10 +16,13 @@ import type { ConversationType } from '../../../state/ducks/conversations.preloa
|
||||
import type { PreferredBadgeSelectorType } from '../../../state/selectors/badges.preload.js';
|
||||
import { PanelRow } from './PanelRow.dom.js';
|
||||
import { PanelSection } from './PanelSection.dom.js';
|
||||
import { GroupMemberLabel } from '../ContactName.dom.js';
|
||||
|
||||
export type GroupV2Membership = {
|
||||
isAdmin: boolean;
|
||||
member: ConversationType;
|
||||
labelEmoji: string | undefined;
|
||||
labelString: string | undefined;
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
@@ -29,6 +32,7 @@ export type Props = {
|
||||
i18n: LocalizerType;
|
||||
maxShownMemberCount?: number;
|
||||
memberships: ReadonlyArray<GroupV2Membership>;
|
||||
memberColors: Map<string, string>;
|
||||
showContactModal: (contactId: string, conversationId?: string) => void;
|
||||
startAddingNewMembers?: () => void;
|
||||
theme: ThemeType;
|
||||
@@ -79,6 +83,7 @@ export function ConversationDetailsMembershipList({
|
||||
getPreferredBadge,
|
||||
i18n,
|
||||
maxShownMemberCount = 5,
|
||||
memberColors,
|
||||
memberships,
|
||||
showContactModal,
|
||||
startAddingNewMembers,
|
||||
@@ -109,26 +114,50 @@ export function ConversationDetailsMembershipList({
|
||||
onClick={() => startAddingNewMembers?.()}
|
||||
/>
|
||||
)}
|
||||
{sortedMemberships.slice(0, membersToShow).map(({ isAdmin, member }) => (
|
||||
<PanelRow
|
||||
key={member.id}
|
||||
onClick={() => showContactModal(member.id, conversationId)}
|
||||
icon={
|
||||
<Avatar
|
||||
conversationType="direct"
|
||||
badge={getPreferredBadge(member.badges)}
|
||||
i18n={i18n}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
theme={theme}
|
||||
{...member}
|
||||
{sortedMemberships
|
||||
.slice(0, membersToShow)
|
||||
.map(({ isAdmin, member, labelEmoji, labelString }) => {
|
||||
const contactNameColor = memberColors.get(member.id);
|
||||
|
||||
return (
|
||||
<PanelRow
|
||||
key={member.id}
|
||||
onClick={() => showContactModal(member.id, conversationId)}
|
||||
icon={
|
||||
<Avatar
|
||||
conversationType="direct"
|
||||
badge={getPreferredBadge(member.badges)}
|
||||
i18n={i18n}
|
||||
size={AvatarSize.THIRTY_SIX}
|
||||
theme={theme}
|
||||
{...member}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<div>
|
||||
<div>
|
||||
<Emojify
|
||||
text={member.isMe ? i18n('icu:you') : member.title}
|
||||
/>
|
||||
</div>
|
||||
{labelString && contactNameColor && (
|
||||
<div>
|
||||
<GroupMemberLabel
|
||||
contactNameColor={contactNameColor}
|
||||
contactLabel={{
|
||||
labelEmoji,
|
||||
labelString,
|
||||
}}
|
||||
context="list"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
right={isAdmin ? i18n('icu:GroupV2--admin') : ''}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Emojify text={member.isMe ? i18n('icu:you') : member.title} />
|
||||
}
|
||||
right={isAdmin ? i18n('icu:GroupV2--admin') : ''}
|
||||
/>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
{showAllMembers === false && shouldHideRestMembers && (
|
||||
<PanelRow
|
||||
className="ConversationDetails-membership-list--show-all"
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './GroupMemberLabelEditor.dom.js';
|
||||
import { GroupMemberLabelEditor } from './GroupMemberLabelEditor.dom.js';
|
||||
import type { ConversationType } from '../../../state/ducks/conversations.preload.js';
|
||||
import { getDefaultConversation } from '../../../test-helpers/getDefaultConversation.std.js';
|
||||
import { ThemeType } from '../../../types/Util.std.js';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
export default {
|
||||
title: 'Components/Conversation/ConversationDetails/GroupMemberLabelEditor',
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
const createProps = (conversation?: ConversationType): PropsType => ({
|
||||
conversation: conversation || getDefaultConversation({ type: 'group' }),
|
||||
existingLabelEmoji: '🐘',
|
||||
existingLabelString: 'Good Memory',
|
||||
i18n,
|
||||
popPanelForConversation: action('popPanelForConversation'),
|
||||
theme: ThemeType.light,
|
||||
updateGroupMemberLabel: action('changeHasGroupLink'),
|
||||
});
|
||||
|
||||
export function NoExistingLabel(): React.JSX.Element {
|
||||
const props = {
|
||||
...createProps(),
|
||||
existingLabelEmoji: undefined,
|
||||
existingLabelString: undefined,
|
||||
};
|
||||
|
||||
return <GroupMemberLabelEditor {...props} />;
|
||||
}
|
||||
|
||||
export function ExistingLabel(): React.JSX.Element {
|
||||
const props = createProps();
|
||||
|
||||
return <GroupMemberLabelEditor {...props} />;
|
||||
}
|
||||
|
||||
export function StringButNoEmoji(): React.JSX.Element {
|
||||
const props = {
|
||||
...createProps(),
|
||||
existingLabelEmoji: undefined,
|
||||
};
|
||||
|
||||
return <GroupMemberLabelEditor {...props} />;
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Input } from '../../Input.dom.js';
|
||||
import { FunEmojiPicker } from '../../fun/FunEmojiPicker.dom.js';
|
||||
import {
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantKeyByValue,
|
||||
isEmojiVariantValue,
|
||||
} from '../../fun/data/emojis.std.js';
|
||||
import { FunEmojiPickerButton } from '../../fun/FunButton.dom.js';
|
||||
|
||||
import type { EmojiVariantKey } from '../../fun/data/emojis.std.js';
|
||||
import type {
|
||||
ConversationType,
|
||||
UpdateGroupMemberLabelType,
|
||||
} from '../../../state/ducks/conversations.preload.js';
|
||||
import type { LocalizerType, ThemeType } from '../../../types/Util.std.js';
|
||||
import { tw } from '../../../axo/tw.dom.js';
|
||||
import { AxoButton } from '../../../axo/AxoButton.dom.js';
|
||||
|
||||
export type PropsDataType = {
|
||||
conversation: ConversationType;
|
||||
existingLabelEmoji: string | undefined;
|
||||
existingLabelString: string | undefined;
|
||||
i18n: LocalizerType;
|
||||
theme: ThemeType;
|
||||
};
|
||||
|
||||
export type PropsType = PropsDataType & {
|
||||
popPanelForConversation: () => void;
|
||||
updateGroupMemberLabel: UpdateGroupMemberLabelType;
|
||||
};
|
||||
|
||||
function getEmojiVariantKey(value: string): EmojiVariantKey | undefined {
|
||||
if (isEmojiVariantValue(value)) {
|
||||
return getEmojiVariantKeyByValue(value);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function GroupMemberLabelEditor({
|
||||
conversation,
|
||||
existingLabelEmoji,
|
||||
existingLabelString,
|
||||
i18n,
|
||||
popPanelForConversation,
|
||||
theme,
|
||||
updateGroupMemberLabel,
|
||||
}: PropsType): React.JSX.Element {
|
||||
const [labelEmoji, setLabelEmoji] = useState(existingLabelEmoji);
|
||||
const [labelString, setLabelString] = useState(existingLabelString);
|
||||
|
||||
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false);
|
||||
|
||||
const emojiKey = labelEmoji ? getEmojiVariantKey(labelEmoji) : null;
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const isDirty =
|
||||
labelEmoji !== existingLabelEmoji || labelString !== existingLabelString;
|
||||
const spinner = isSaving
|
||||
? {
|
||||
'aria-label': i18n('icu:ConversationDetails--member-label--saving'),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className={tw('mx-auto flex h-full max-w-[640px] flex-col')}>
|
||||
<div>
|
||||
<Input
|
||||
autoFocus
|
||||
hasClearButton
|
||||
i18n={i18n}
|
||||
icon={
|
||||
<FunEmojiPicker
|
||||
open={emojiPickerOpen}
|
||||
onOpenChange={(open: boolean) => setEmojiPickerOpen(open)}
|
||||
placement="bottom"
|
||||
onSelectEmoji={data => {
|
||||
const newEmoji = getEmojiVariantByKey(data.variantKey)?.value;
|
||||
|
||||
setLabelEmoji(newEmoji);
|
||||
}}
|
||||
closeOnSelect
|
||||
theme={theme}
|
||||
>
|
||||
<FunEmojiPickerButton i18n={i18n} selectedEmoji={emojiKey} />
|
||||
</FunEmojiPicker>
|
||||
}
|
||||
maxLengthCount={24}
|
||||
maxByteCount={96}
|
||||
moduleClassName="GroupMemberLabelEditor"
|
||||
onChange={value => {
|
||||
if (!value) {
|
||||
setLabelEmoji(undefined);
|
||||
}
|
||||
setLabelString(value);
|
||||
}}
|
||||
ref={undefined}
|
||||
placeholder={i18n(
|
||||
'icu:ConversationDetails--member-label--placeholder'
|
||||
)}
|
||||
value={labelString}
|
||||
whenToShowRemainingCount={20}
|
||||
/>
|
||||
</div>
|
||||
<div className={tw('text-label-secondary')}>
|
||||
{i18n('icu:ConversationDetails--member-label--description')}
|
||||
</div>
|
||||
|
||||
<div className={tw('flex-grow')} />
|
||||
<div className={tw('mb-3 flex w-full justify-end gap-2')}>
|
||||
<AxoButton.Root
|
||||
variant="secondary"
|
||||
size="md"
|
||||
onClick={() => {
|
||||
popPanelForConversation();
|
||||
}}
|
||||
>
|
||||
{i18n('icu:cancel')}
|
||||
</AxoButton.Root>
|
||||
|
||||
<AxoButton.Root
|
||||
variant="primary"
|
||||
size="md"
|
||||
experimentalSpinner={spinner}
|
||||
disabled={!isDirty || isSaving}
|
||||
onClick={() => {
|
||||
setIsSaving(true);
|
||||
updateGroupMemberLabel(
|
||||
{
|
||||
conversationId: conversation.id,
|
||||
labelEmoji,
|
||||
labelString,
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
setIsSaving(false);
|
||||
popPanelForConversation();
|
||||
},
|
||||
onFailure() {
|
||||
// TODO: DESKTOP-9698
|
||||
},
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:save')}
|
||||
</AxoButton.Root>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -39,6 +39,7 @@ function getEmojiJumboBackground(
|
||||
|
||||
export type FunStaticEmojiSize =
|
||||
| 12
|
||||
| 14
|
||||
| 16
|
||||
| 18
|
||||
| 20
|
||||
@@ -62,6 +63,7 @@ export enum FunJumboEmojiSize {
|
||||
|
||||
const funStaticEmojiSizeClasses = {
|
||||
12: 'FunStaticEmoji--Size12',
|
||||
14: 'FunStaticEmoji--Size14',
|
||||
16: 'FunStaticEmoji--Size16',
|
||||
18: 'FunStaticEmoji--Size18',
|
||||
20: 'FunStaticEmoji--Size20',
|
||||
|
||||
Reference in New Issue
Block a user