Render group member labels in quotes

This commit is contained in:
Scott Nonnenberg
2026-02-06 05:43:06 +10:00
committed by GitHub
parent c5ba980fdd
commit 85cc412b40
11 changed files with 96 additions and 18 deletions

View File

@@ -1095,6 +1095,10 @@ $message-padding-horizontal: 12px;
}
}
.module-message__author--with-quote {
margin-bottom: 3px;
}
.module-message__text {
@include mixins.font-body-1;

View File

@@ -54,6 +54,7 @@ $color-white-alpha-12: rgba($color-white, 0.12);
$color-white-alpha-16: rgba($color-white, 0.16);
$color-white-alpha-20: rgba($color-white, 0.2);
$color-white-alpha-30: rgba($color-white, 0.3);
$color-white-alpha-36: rgba($color-white, 0.36);
$color-white-alpha-40: rgba($color-white, 0.4);
$color-white-alpha-50: rgba($color-white, 0.5);
$color-white-alpha-55: rgba($color-white, 0.55);

View File

@@ -4,6 +4,7 @@
@use 'sass:map';
@use 'sass:color';
@use '../variables';
@use '../mixins';
button.module-contact-name {
@@ -305,6 +306,18 @@ $contact-colors: (
line-height: 12px;
}
&--label-pill--quote {
margin-bottom: 2px;
@include mixins.font-body-small;
color: variables.$color-gray-90;
background-color: variables.$color-white-alpha-36;
@include mixins.dark-theme() {
color: variables.$color-gray-05;
background-color: variables.$color-white-alpha-20;
}
}
&--label-pill--text {
display: inline-block;
}

View File

@@ -185,6 +185,10 @@
.module-quote__primary__author {
@include mixins.font-body-2-bold;
display: inline-flex;
align-items: center;
gap: 4px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
@@ -203,6 +207,10 @@
}
}
.module-quote__primary__author--with-label {
margin-top: -3px;
}
.module-quote__primary__text {
@include mixins.font-body-1;

View File

@@ -18,6 +18,7 @@ import { useFunEmojiLocalizer } from '../fun/useFunEmojiLocalizer.dom.js';
import { FunStaticEmoji } from '../fun/FunEmoji.dom.js';
import { missingEmojiPlaceholder } from '../../types/GroupMemberLabels.std.js';
import type { MemberLabelType } from '../../types/GroupMemberLabels.std.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';
@@ -113,7 +114,7 @@ export function ContactName({
);
}
export type Context = 'bubble' | 'list';
export type Context = 'bubble' | 'list' | 'quote';
export function GroupMemberLabel({
emojiSize = 12,
@@ -123,7 +124,7 @@ export function GroupMemberLabel({
module,
}: {
emojiSize?: FunStaticEmojiSize;
contactLabel?: { labelString: string; labelEmoji: string | undefined };
contactLabel?: MemberLabelType;
contactNameColor?: ContactNameColorType;
context: Context;
module?: string;

View File

@@ -127,6 +127,7 @@ import {
import { useGroupedAndOrderedReactions } from '../../util/groupAndOrderReactions.dom.js';
import type { AxoMenuBuilder } from '../../axo/AxoMenuBuilder.dom.js';
import type { RenderAudioAttachmentProps } from '../../state/smart/renderAudioAttachment.preload.js';
import type { MemberLabelType } from '../../types/GroupMemberLabels.std.js';
const { drop, take, unescape } = lodash;
@@ -247,7 +248,7 @@ export type PropsData = {
id: string;
renderingContext: RenderingContextType;
contactNameColor?: ContactNameColorType;
contactLabel?: { labelString: string; labelEmoji: string | undefined };
contactLabel?: MemberLabelType;
conversationColor: ConversationColorType;
conversationTitle: string;
customColor?: CustomColorType;
@@ -305,6 +306,7 @@ export type PropsData = {
authorPhoneNumber?: string;
authorProfileName?: string;
authorTitle: string;
authorLabel?: MemberLabelType;
authorName?: string;
bodyRanges?: HydratedBodyRangesType;
referencedMessageNotFound: boolean;
@@ -2120,6 +2122,7 @@ export class Message extends React.PureComponent<Props, State> {
payment={quote.payment}
isIncoming={isIncoming}
authorTitle={quote.authorTitle}
authorLabel={quote.authorLabel}
bodyRanges={quote.bodyRanges}
conversationColor={conversationColor}
conversationTitle={conversationTitle}

View File

@@ -168,6 +168,7 @@ const defaultMessageProps: TimelineMessagesProps = {
const renderInMessage = ({
authorTitle,
authorLabel,
conversationColor,
isFromMe,
rawAttachment,
@@ -182,6 +183,7 @@ const renderInMessage = ({
quote: {
authorId: 'an-author',
authorTitle,
authorLabel,
conversationColor,
conversationTitle: getDefaultConversation().title,
isFromMe,
@@ -223,6 +225,16 @@ IncomingByAnotherAuthor.args = {
isIncoming: true,
};
export const IncomingByAnotherWithLabel = Template.bind({});
IncomingByAnotherWithLabel.args = {
authorTitle: getDefaultConversation().title,
isIncoming: true,
authorLabel: {
labelEmoji: '1⃣',
labelString: 'First',
},
};
export const IncomingByMe = Template.bind({});
IncomingByMe.args = {
isFromMe: true,
@@ -233,7 +245,14 @@ export function IncomingOutgoingColors(args: Props): React.JSX.Element {
return (
<>
{ConversationColors.map(color =>
renderInMessage({ ...args, conversationColor: color })
renderInMessage({
...args,
conversationColor: color,
authorLabel: {
labelEmoji: '1⃣',
labelString: 'First',
},
})
)}
</>
);

View File

@@ -1,15 +1,25 @@
// Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactNode } from 'react';
import React, { useRef, useState, useEffect } from 'react';
import lodash from 'lodash';
import classNames from 'classnames';
import type { ReactNode } from 'react';
import * as MIME from '../../types/MIME.std.js';
import * as GoogleChrome from '../../util/GoogleChrome.std.js';
import { MessageBody } from './MessageBody.dom.js';
import { ContactName, GroupMemberLabel } from './ContactName.dom.js';
import { Emojify } from './Emojify.dom.js';
import { TextAttachment } from '../TextAttachment.dom.js';
import { getClassNamesFor } from '../../util/getClassNamesFor.std.js';
import { getCustomColorStyle } from '../../util/getCustomColorStyle.dom.js';
import { PaymentEventKind } from '../../types/Payment.std.js';
import { getPaymentEventNotificationText } from '../../messages/payments.std.js';
import { shouldTryToCopyFromQuotedMessage } from '../../messages/helpers.std.js';
import { RenderLocation } from './MessageTextRenderer.dom.js';
import type {
AttachmentType,
ThumbnailType,
@@ -20,17 +30,9 @@ import type {
ConversationColorType,
CustomColorType,
} from '../../types/Colors.std.js';
import { ContactName } from './ContactName.dom.js';
import { Emojify } from './Emojify.dom.js';
import { TextAttachment } from '../TextAttachment.dom.js';
import { getClassNamesFor } from '../../util/getClassNamesFor.std.js';
import { getCustomColorStyle } from '../../util/getCustomColorStyle.dom.js';
import type { AnyPaymentEvent } from '../../types/Payment.std.js';
import { PaymentEventKind } from '../../types/Payment.std.js';
import { getPaymentEventNotificationText } from '../../messages/payments.std.js';
import { shouldTryToCopyFromQuotedMessage } from '../../messages/helpers.std.js';
import { RenderLocation } from './MessageTextRenderer.dom.js';
import type { QuotedAttachmentType } from '../../model-types.d.ts';
import type { MemberLabelType } from '../../types/GroupMemberLabels.std.js';
const { noop } = lodash;
@@ -44,6 +46,7 @@ export type QuotedAttachmentForUIType = Pick<
export type Props = {
authorTitle: string;
authorLabel?: MemberLabelType;
conversationColor: ConversationColorType;
conversationTitle: string;
customColor?: CustomColorType;
@@ -157,6 +160,7 @@ export function Quote(props: Props): React.JSX.Element | null {
text,
bodyRanges,
authorTitle,
authorLabel,
conversationTitle,
isFromMe,
i18n,
@@ -471,7 +475,15 @@ export function Quote(props: Props): React.JSX.Element | null {
{title} &middot; {i18n('icu:Quote__story')}
</>
) : (
title
<>
{title}
{authorLabel && !isFromMe ? (
<>
{' '}
<GroupMemberLabel context="quote" contactLabel={authorLabel} />
</>
) : undefined}
</>
);
return (
@@ -479,7 +491,8 @@ export function Quote(props: Props): React.JSX.Element | null {
dir="auto"
className={classNames(
getClassName('__primary__author'),
isIncoming ? getClassName('__primary__author--incoming') : null
isIncoming ? getClassName('__primary__author--incoming') : null,
authorLabel ? getClassName('__primary__author--with-label') : null
)}
>
{author}

View File

@@ -125,7 +125,7 @@ export function GroupMemberLabelEditor({
setLabelEmoji(undefined);
}
// Remove trailing/leading whitespace, replace all whitespace with basic space
// Replace all whitespace with basic space
setLabelString(value.replace(/\s/g, ' '));
}}
ref={undefined}

View File

@@ -728,6 +728,16 @@ export const getPropsForQuote = (
const firstAttachment = quote.attachments && quote.attachments[0];
const conversation = getConversation(message, conversationSelector);
const authorMembership = conversation.memberships?.find(
membership => membership.aci === contact.serviceId
);
const authorLabel =
authorMembership && authorMembership.labelString
? {
labelEmoji: authorMembership.labelEmoji,
labelString: authorMembership.labelString,
}
: undefined;
const { conversationColor, customColor } = getConversationColorAttributes(
conversation,
@@ -736,6 +746,7 @@ export const getPropsForQuote = (
return {
authorId,
authorLabel,
authorName,
authorPhoneNumber,
authorProfileName,

View File

@@ -17,6 +17,11 @@ export const EMOJI_OUTGOING_BYTE_LIMIT = 48;
export const SERVER_STRING_BYTE_LIMIT = 512;
export const SERVER_EMOJI_BYTE_LIMIT = 64;
export type MemberLabelType = {
labelString: string;
labelEmoji: string | undefined;
};
export function getCanAddLabel(
conversation: ConversationType,
membership: MembershipType | undefined