Init Nicknames & Notes
This commit is contained in:
parent
ebecf2403f
commit
e26916702c
42 changed files with 1050 additions and 23 deletions
|
@ -1366,6 +1366,10 @@
|
|||
"messageformat": "You",
|
||||
"description": "Title of About modal when viewing your own information"
|
||||
},
|
||||
"icu:AboutContactModal__TitleAndTitleWithoutNickname": {
|
||||
"messageformat": "{nickname} <muted>({titleNoNickname})</muted>",
|
||||
"description": "Title of conversation when there is a nickname, example: 'Jim (James Smith)'"
|
||||
},
|
||||
"icu:AboutContactModal__verified": {
|
||||
"messageformat": "Verified",
|
||||
"description": "Text of a button on About modal leading to a safety number modal"
|
||||
|
@ -1390,6 +1394,10 @@
|
|||
"messageformat": "{name} is in your system contacts",
|
||||
"description": "Text of a row in the About modal describing that the contact is in system contacts"
|
||||
},
|
||||
"icu:NotePreviewModal__Title": {
|
||||
"messageformat": "Note",
|
||||
"description": "Title of Note Preview modal"
|
||||
},
|
||||
"icu:ContactModal__showSafetyNumber": {
|
||||
"messageformat": "View safety number",
|
||||
"description": "(Deleted 2024/03/07) Contact modal, label for button to show safety number modal"
|
||||
|
@ -4898,6 +4906,26 @@
|
|||
"messageformat": "When enabled, messages sent and received in this 1:1 chat will disappear after they've been seen.",
|
||||
"description": "This is the info about the disappearing messages setting, for direct conversations"
|
||||
},
|
||||
"icu:ConversationDetails--nickname-label": {
|
||||
"messageformat": "Nickname",
|
||||
"description": "This is the label for the nickname setting panel"
|
||||
},
|
||||
"icu:ConversationDetails--nickname-actions": {
|
||||
"messageformat": "Actions",
|
||||
"description": "This is the label for the nickname actions panel"
|
||||
},
|
||||
"icu:ConversationDetails--nickname-actions--delete": {
|
||||
"messageformat": "Delete",
|
||||
"description": "This is the label for the delete nickname action"
|
||||
},
|
||||
"icu:ConversationDetails__ConfirmDeleteNicknameAndNote__Title": {
|
||||
"messageformat": "Delete nickname?",
|
||||
"description": "Title for the modal to confirm deleting a nickname"
|
||||
},
|
||||
"icu:ConversationDetails__ConfirmDeleteNicknameAndNote__Description": {
|
||||
"messageformat": "This will permanently delete this nickname and note.",
|
||||
"description": "Description for the modal to confirm deleting a nickname"
|
||||
},
|
||||
"icu:ConversationDetails--notifications": {
|
||||
"messageformat": "Notifications",
|
||||
"description": "This is the label for notifications in the conversation details screen"
|
||||
|
@ -5022,6 +5050,38 @@
|
|||
"messageformat": "See all",
|
||||
"description": "This is a button on the conversation details (for a direct contact) to show all groups-in-common"
|
||||
},
|
||||
"icu:EditNicknameAndNoteModal__Title": {
|
||||
"messageformat": "Nickname",
|
||||
"description": "Title for the edit nickname and note modal"
|
||||
},
|
||||
"icu:EditNicknameAndNoteModal__Description": {
|
||||
"messageformat": "Nicknames & notes are stored using Signal’s end-to-end encrypted storage service. They are only visible to you.",
|
||||
"description": "Description for the edit nickname and note modal"
|
||||
},
|
||||
"icu:EditNicknameAndNoteModal__FirstName__Label": {
|
||||
"messageformat": "First name",
|
||||
"description": "Label for the first name input in the edit nickname and note modal"
|
||||
},
|
||||
"icu:EditNicknameAndNoteModal__FirstName__Placeholder": {
|
||||
"messageformat": "First name",
|
||||
"description": "Input placeholder for the first name input in the edit nickname and note modal"
|
||||
},
|
||||
"icu:EditNicknameAndNoteModal__LastName__Label": {
|
||||
"messageformat": "Last name",
|
||||
"description": "Label for the last name input in the edit nickname and note modal"
|
||||
},
|
||||
"icu:EditNicknameAndNoteModal__LastName__Placeholder": {
|
||||
"messageformat": "Last name",
|
||||
"description": "Input placeholder for the last name input in the edit nickname and note modal"
|
||||
},
|
||||
"icu:EditNicknameAndNoteModal__Note__Label": {
|
||||
"messageformat": "Note",
|
||||
"description": "Label for the note input in the edit nickname and note modal"
|
||||
},
|
||||
"icu:EditNicknameAndNoteModal__Note__Placeholder": {
|
||||
"messageformat": "Note",
|
||||
"description": "Input placeholder for the note input in the edit nickname and note modal"
|
||||
},
|
||||
"icu:ConversationNotificationsSettings__mentions__label": {
|
||||
"messageformat": "Mentions",
|
||||
"description": "In the conversation notifications settings, this is the label for the mentions option"
|
||||
|
|
|
@ -77,6 +77,11 @@ message ContactRecord {
|
|||
UNVERIFIED = 2;
|
||||
}
|
||||
|
||||
message Name {
|
||||
optional string given = 1;
|
||||
optional string family = 2;
|
||||
}
|
||||
|
||||
optional string aci = 1;
|
||||
optional string serviceE164 = 2;
|
||||
optional string pni = 15;
|
||||
|
@ -98,6 +103,8 @@ message ContactRecord {
|
|||
optional string systemNickname = 19;
|
||||
optional bool hidden = 20;
|
||||
optional bool pniSignatureVerified = 21;
|
||||
optional Name nickname = 22;
|
||||
optional string note = 23;
|
||||
}
|
||||
|
||||
message GroupV1Record {
|
||||
|
|
|
@ -97,12 +97,17 @@
|
|||
&--about {
|
||||
@include about-modal-icon('../images/icons/v3/edit/edit.svg');
|
||||
}
|
||||
|
||||
&--note {
|
||||
@include about-modal-icon('../images/icons/v3/note/note.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&__signal-connection {
|
||||
&__button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
|
||||
@include button-reset();
|
||||
cursor: pointer;
|
||||
|
@ -112,6 +117,7 @@
|
|||
display: inline-block;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
flex-shrink: 0;
|
||||
|
||||
@include color-svg(
|
||||
'../images/icons/v3/chevron/chevron-right-bold.svg',
|
||||
|
@ -125,3 +131,13 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.AboutContactModal__TitleWithoutNickname {
|
||||
color: $color-gray-45;
|
||||
}
|
||||
|
||||
.AboutContactModal__OneLineEllipsis {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
|
|
@ -493,3 +493,42 @@
|
|||
.ConversationDetails__CallHistoryGroup__ItemTimestamp {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ConversationDetails--nickname-actions {
|
||||
@include button-reset;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
&::before {
|
||||
display: inline-block;
|
||||
content: '';
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
@include light-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v3/chevron/chevron-down.svg',
|
||||
$color-black
|
||||
);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v3/chevron/chevron-down.svg',
|
||||
$color-white
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ConversationDetails--nickname-actions-label {
|
||||
@include sr-only;
|
||||
}
|
||||
|
||||
.ConversationDetails--nickname-actions--delete {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
@include light-theme {
|
||||
@include color-svg('../images/icons/v3/trash/trash.svg', $color-black);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg('../images/icons/v3/trash/trash.svg', $color-white);
|
||||
}
|
||||
}
|
||||
|
|
27
stylesheets/components/EditNicknameAndNoteModal.scss
Normal file
27
stylesheets/components/EditNicknameAndNoteModal.scss
Normal file
|
@ -0,0 +1,27 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.EditNicknameAndNoteModal__width-container {
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.EditNicknameAndNoteModal__Description {
|
||||
margin-block: 12px 24px;
|
||||
@include font-body-2;
|
||||
@include light-theme {
|
||||
color: $color-gray-75;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-25;
|
||||
}
|
||||
}
|
||||
|
||||
.EditNicknameAndNoteModal__Avatar {
|
||||
margin-block: 0 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.EditNicknameAndNoteModal__Label {
|
||||
@include sr-only;
|
||||
}
|
|
@ -76,7 +76,7 @@
|
|||
height: 280px;
|
||||
}
|
||||
|
||||
&--expandable {
|
||||
&--textarea {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
|
@ -129,5 +129,9 @@
|
|||
|
||||
margin: 12px;
|
||||
}
|
||||
|
||||
&--warn {
|
||||
color: $color-accent-red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,6 +87,7 @@
|
|||
@import './components/DisappearingTimerSelect.scss';
|
||||
@import './components/EditConversationAttributesModal.scss';
|
||||
@import './components/EditHistoryMessagesModal.scss';
|
||||
@import './components/EditNicknameAndNoteModal.scss';
|
||||
@import './components/EditUsernameModalBody.scss';
|
||||
@import './components/ForwardMessageModal.scss';
|
||||
@import './components/GradientDial.scss';
|
||||
|
|
|
@ -21,6 +21,7 @@ export type ConfigKeyType =
|
|||
| 'desktop.internalUser'
|
||||
| 'desktop.mediaQuality.levels'
|
||||
| 'desktop.messageCleanup'
|
||||
| 'desktop.nicknames'
|
||||
| 'desktop.retryRespondMaxAge'
|
||||
| 'desktop.senderKey.retry'
|
||||
| 'desktop.senderKeyMaxAge'
|
||||
|
|
56
ts/components/AutoSizeTextArea.tsx
Normal file
56
ts/components/AutoSizeTextArea.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { ForwardedRef } from 'react';
|
||||
import React, { forwardRef, useEffect, useLayoutEffect, useRef } from 'react';
|
||||
import { mergeRefs } from '@react-aria/utils';
|
||||
import { strictAssert } from '../util/assert';
|
||||
import type { PropsType } from './Input';
|
||||
import { Input } from './Input';
|
||||
|
||||
export const AutoSizeTextArea = forwardRef(function AutoSizeTextArea(
|
||||
props: PropsType,
|
||||
ref: ForwardedRef<HTMLTextAreaElement>
|
||||
): JSX.Element {
|
||||
const ownRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const textareaRef = mergeRefs(ownRef, ref);
|
||||
|
||||
function update(textarea: HTMLTextAreaElement) {
|
||||
const styles = window.getComputedStyle(textarea);
|
||||
const { scrollHeight } = textarea;
|
||||
let height = 'calc(';
|
||||
height += `${scrollHeight}px`;
|
||||
if (styles.boxSizing === 'border-box') {
|
||||
height += ` + ${styles.borderTopWidth} + ${styles.borderBottomWidth}`;
|
||||
} else {
|
||||
height += ` - ${styles.paddingTop} - ${styles.paddingBottom}`;
|
||||
}
|
||||
height += ')';
|
||||
Object.assign(textarea.style, {
|
||||
height,
|
||||
overflow: 'hidden',
|
||||
resize: 'none',
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
strictAssert(ownRef.current, 'inputRef.current should be defined');
|
||||
const textarea = ownRef.current;
|
||||
function onInput() {
|
||||
textarea.style.height = 'auto';
|
||||
requestAnimationFrame(() => update(textarea));
|
||||
}
|
||||
textarea.addEventListener('input', onInput);
|
||||
return () => {
|
||||
textarea.removeEventListener('input', onInput);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
strictAssert(ownRef.current, 'inputRef.current should be defined');
|
||||
const textarea = ownRef.current;
|
||||
textarea.style.height = 'auto';
|
||||
update(textarea);
|
||||
}, [props.value]);
|
||||
|
||||
return <Input ref={textareaRef} {...props} forceTextarea />;
|
||||
});
|
32
ts/components/EditNicknameAndNoteModal.stories.tsx
Normal file
32
ts/components/EditNicknameAndNoteModal.stories.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import * as React from 'react';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import type { ComponentMeta } from '../storybook/types';
|
||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import type { EditNicknameAndNoteModalProps } from './EditNicknameAndNoteModal';
|
||||
import { EditNicknameAndNoteModal } from './EditNicknameAndNoteModal';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
title: 'Components/EditNicknameAndNoteModal',
|
||||
component: EditNicknameAndNoteModal,
|
||||
argTypes: {},
|
||||
args: {
|
||||
conversation: getDefaultConversation({
|
||||
nicknameGivenName: 'Bestie',
|
||||
nicknameFamilyName: 'McBesterson',
|
||||
note: 'Met at UC Berkeley, mutual friends with Katie Hall.\n\nWebsite: https://example.com/',
|
||||
}),
|
||||
i18n,
|
||||
onClose: action('onClose'),
|
||||
onSave: action('onSave'),
|
||||
},
|
||||
} satisfies ComponentMeta<EditNicknameAndNoteModalProps>;
|
||||
|
||||
export function Normal(args: EditNicknameAndNoteModalProps): JSX.Element {
|
||||
return <EditNicknameAndNoteModal {...args} />;
|
||||
}
|
185
ts/components/EditNicknameAndNoteModal.tsx
Normal file
185
ts/components/EditNicknameAndNoteModal.tsx
Normal file
|
@ -0,0 +1,185 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { FormEvent } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import uuid from 'uuid';
|
||||
import { z } from 'zod';
|
||||
import { Modal } from './Modal';
|
||||
import type { LocalizerType } from '../types/I18N';
|
||||
import { Avatar, AvatarSize } from './Avatar';
|
||||
import type {
|
||||
ConversationType,
|
||||
NicknameAndNote,
|
||||
} from '../state/ducks/conversations';
|
||||
import { Input } from './Input';
|
||||
import { AutoSizeTextArea } from './AutoSizeTextArea';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
|
||||
const formSchema = z.object({
|
||||
nickname: z
|
||||
.object({
|
||||
givenName: z.string(),
|
||||
familyName: z.string().nullable(),
|
||||
})
|
||||
.nullable(),
|
||||
note: z.string().nullable(),
|
||||
});
|
||||
|
||||
function toOptionalStringValue(value: string): string | null {
|
||||
const trimmed = value.trim();
|
||||
return trimmed === '' ? null : trimmed;
|
||||
}
|
||||
|
||||
export type EditNicknameAndNoteModalProps = Readonly<{
|
||||
conversation: ConversationType;
|
||||
i18n: LocalizerType;
|
||||
onSave: (result: NicknameAndNote) => void;
|
||||
onClose: () => void;
|
||||
}>;
|
||||
|
||||
export function EditNicknameAndNoteModal({
|
||||
conversation,
|
||||
i18n,
|
||||
onSave,
|
||||
onClose,
|
||||
}: EditNicknameAndNoteModalProps): JSX.Element {
|
||||
const [givenName, setGivenName] = useState(
|
||||
conversation.nicknameGivenName ?? ''
|
||||
);
|
||||
const [familyName, setFamilyName] = useState(
|
||||
conversation.nicknameFamilyName ?? ''
|
||||
);
|
||||
const [note, setNote] = useState(conversation.note ?? '');
|
||||
|
||||
const [formId] = useState(() => uuid());
|
||||
const [givenNameId] = useState(() => uuid());
|
||||
const [familyNameId] = useState(() => uuid());
|
||||
const [noteId] = useState(() => uuid());
|
||||
|
||||
const formResult = useMemo(() => {
|
||||
const givenNameValue = toOptionalStringValue(givenName);
|
||||
const familyNameValue = toOptionalStringValue(familyName);
|
||||
const noteValue = toOptionalStringValue(note);
|
||||
const hasEitherName = givenNameValue != null || familyNameValue != null;
|
||||
return formSchema.safeParse({
|
||||
nickname: hasEitherName
|
||||
? { givenName: givenNameValue, familyName: familyNameValue }
|
||||
: null,
|
||||
note: noteValue,
|
||||
});
|
||||
}, [givenName, familyName, note]);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(event: MouseEvent | FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (formResult.success) {
|
||||
onSave(formResult.data);
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[formResult, onSave, onClose]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
modalName="EditNicknameAndNoteModal"
|
||||
moduleClassName="EditNicknameAndNoteModal"
|
||||
i18n={i18n}
|
||||
onClose={onClose}
|
||||
title={i18n('icu:EditNicknameAndNoteModal__Title')}
|
||||
hasXButton
|
||||
modalFooter={
|
||||
<>
|
||||
<Button variant={ButtonVariant.Secondary} onClick={onClose}>
|
||||
{i18n('icu:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={ButtonVariant.Primary}
|
||||
type="submit"
|
||||
form={formId}
|
||||
aria-disabled={!formResult.success}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{i18n('icu:save')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p className="EditNicknameAndNoteModal__Description">
|
||||
{i18n('icu:EditNicknameAndNoteModal__Description')}
|
||||
</p>
|
||||
<div className="EditNicknameAndNoteModal__Avatar">
|
||||
<Avatar
|
||||
{...conversation}
|
||||
conversationType={conversation.type}
|
||||
i18n={i18n}
|
||||
size={AvatarSize.EIGHTY}
|
||||
badge={undefined}
|
||||
theme={undefined}
|
||||
/>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<label
|
||||
htmlFor={givenNameId}
|
||||
className="EditNicknameAndNoteModal__Label"
|
||||
>
|
||||
{i18n('icu:EditNicknameAndNoteModal__FirstName__Label')}
|
||||
</label>
|
||||
<Input
|
||||
id={givenNameId}
|
||||
i18n={i18n}
|
||||
placeholder={i18n(
|
||||
'icu:EditNicknameAndNoteModal__FirstName__Placeholder'
|
||||
)}
|
||||
value={givenName}
|
||||
hasClearButton
|
||||
maxLengthCount={26}
|
||||
maxByteCount={128}
|
||||
onChange={value => {
|
||||
setGivenName(value);
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
htmlFor={familyNameId}
|
||||
className="EditNicknameAndNoteModal__Label"
|
||||
>
|
||||
{i18n('icu:EditNicknameAndNoteModal__LastName__Label')}
|
||||
</label>
|
||||
<Input
|
||||
id={familyNameId}
|
||||
i18n={i18n}
|
||||
placeholder={i18n(
|
||||
'icu:EditNicknameAndNoteModal__LastName__Placeholder'
|
||||
)}
|
||||
value={familyName}
|
||||
hasClearButton
|
||||
maxLengthCount={26}
|
||||
maxByteCount={128}
|
||||
onChange={value => {
|
||||
setFamilyName(value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<label htmlFor={noteId} className="EditNicknameAndNoteModal__Label">
|
||||
{i18n('icu:EditNicknameAndNoteModal__Note__Label')}
|
||||
</label>
|
||||
<AutoSizeTextArea
|
||||
i18n={i18n}
|
||||
id={noteId}
|
||||
placeholder={i18n('icu:EditNicknameAndNoteModal__Note__Placeholder')}
|
||||
value={note}
|
||||
maxByteCount={240}
|
||||
maxLengthCount={240}
|
||||
whenToShowRemainingCount={140}
|
||||
whenToWarnRemainingCount={235}
|
||||
onChange={value => {
|
||||
setNote(value);
|
||||
}}
|
||||
/>
|
||||
<button type="submit" hidden>
|
||||
{i18n('icu:submit')}
|
||||
</button>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -7,6 +7,7 @@ import type {
|
|||
ContactModalStateType,
|
||||
DeleteMessagesPropsType,
|
||||
EditHistoryMessagesType,
|
||||
EditNicknameAndNoteModalPropsType,
|
||||
FormattingWarningDataType,
|
||||
ForwardMessagesPropsType,
|
||||
MessageRequestActionsConfirmationPropsType,
|
||||
|
@ -40,6 +41,9 @@ export type PropsType = {
|
|||
// EditHistoryMessagesModal
|
||||
editHistoryMessages: EditHistoryMessagesType | undefined;
|
||||
renderEditHistoryMessagesModal: () => JSX.Element;
|
||||
// EditNicknameAndNoteModal
|
||||
editNicknameAndNoteModalProps: EditNicknameAndNoteModalPropsType | null;
|
||||
renderEditNicknameAndNoteModal: () => JSX.Element;
|
||||
// ErrorModal
|
||||
errorModalProps:
|
||||
| { buttonVariant?: ButtonVariant; description?: string; title?: string }
|
||||
|
@ -63,6 +67,9 @@ export type PropsType = {
|
|||
// MessageRequestActionsConfirmation
|
||||
messageRequestActionsConfirmationProps: MessageRequestActionsConfirmationPropsType | null;
|
||||
renderMessageRequestActionsConfirmation: () => JSX.Element;
|
||||
// NotePreviewModal
|
||||
notePreviewModalProps: { conversationId: string } | null;
|
||||
renderNotePreviewModal: () => JSX.Element;
|
||||
// ProfileEditor
|
||||
isProfileEditorVisible: boolean;
|
||||
renderProfileEditor: () => JSX.Element;
|
||||
|
@ -122,6 +129,9 @@ export function GlobalModalContainer({
|
|||
// EditHistoryMessages
|
||||
editHistoryMessages,
|
||||
renderEditHistoryMessagesModal,
|
||||
// EditNicknameAndNoteModal
|
||||
editNicknameAndNoteModalProps,
|
||||
renderEditNicknameAndNoteModal,
|
||||
// ErrorModal
|
||||
errorModalProps,
|
||||
renderErrorModal,
|
||||
|
@ -137,6 +147,9 @@ export function GlobalModalContainer({
|
|||
// MessageRequestActionsConfirmation
|
||||
messageRequestActionsConfirmationProps,
|
||||
renderMessageRequestActionsConfirmation,
|
||||
// NotePreviewModal
|
||||
notePreviewModalProps,
|
||||
renderNotePreviewModal,
|
||||
// ProfileEditor
|
||||
isProfileEditorVisible,
|
||||
renderProfileEditor,
|
||||
|
@ -205,6 +218,10 @@ export function GlobalModalContainer({
|
|||
return renderEditHistoryMessagesModal();
|
||||
}
|
||||
|
||||
if (editNicknameAndNoteModalProps) {
|
||||
return renderEditNicknameAndNoteModal();
|
||||
}
|
||||
|
||||
if (deleteMessagesProps) {
|
||||
return renderDeleteMessagesModal();
|
||||
}
|
||||
|
@ -234,6 +251,10 @@ export function GlobalModalContainer({
|
|||
return renderMessageRequestActionsConfirmation();
|
||||
}
|
||||
|
||||
if (notePreviewModalProps) {
|
||||
return renderNotePreviewModal();
|
||||
}
|
||||
|
||||
if (isProfileEditorVisible) {
|
||||
return renderProfileEditor();
|
||||
}
|
||||
|
|
|
@ -23,9 +23,11 @@ export type PropsType = {
|
|||
disabled?: boolean;
|
||||
disableSpellcheck?: boolean;
|
||||
expandable?: boolean;
|
||||
forceTextarea?: boolean;
|
||||
hasClearButton?: boolean;
|
||||
i18n: LocalizerType;
|
||||
icon?: ReactNode;
|
||||
id?: string;
|
||||
maxByteCount?: number;
|
||||
maxLengthCount?: number;
|
||||
moduleClassName?: string;
|
||||
|
@ -34,6 +36,7 @@ export type PropsType = {
|
|||
placeholder: string;
|
||||
value?: string;
|
||||
whenToShowRemainingCount?: number;
|
||||
whenToWarnRemainingCount?: number;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
|
@ -64,9 +67,11 @@ export const Input = forwardRef<
|
|||
disabled,
|
||||
disableSpellcheck,
|
||||
expandable,
|
||||
forceTextarea,
|
||||
hasClearButton,
|
||||
i18n,
|
||||
icon,
|
||||
id,
|
||||
maxByteCount = 0,
|
||||
maxLengthCount = 0,
|
||||
moduleClassName,
|
||||
|
@ -75,6 +80,7 @@ export const Input = forwardRef<
|
|||
placeholder,
|
||||
value = '',
|
||||
whenToShowRemainingCount = Infinity,
|
||||
whenToWarnRemainingCount = Infinity,
|
||||
children,
|
||||
},
|
||||
ref
|
||||
|
@ -195,14 +201,17 @@ export const Input = forwardRef<
|
|||
const lengthCount = maxLengthCount ? countLength(value) : -1;
|
||||
const getClassName = getClassNamesFor('Input', moduleClassName);
|
||||
|
||||
const isTextarea = expandable || forceTextarea;
|
||||
|
||||
const inputProps = {
|
||||
className: classNames(
|
||||
getClassName('__input'),
|
||||
icon && getClassName('__input--with-icon'),
|
||||
isLarge && getClassName('__input--large'),
|
||||
expandable && getClassName('__input--expandable')
|
||||
isTextarea && getClassName('__input--textarea')
|
||||
),
|
||||
disabled: Boolean(disabled),
|
||||
id,
|
||||
spellCheck: !disableSpellcheck,
|
||||
onChange: handleChange,
|
||||
onKeyDown: handleKeyDown,
|
||||
|
@ -228,7 +237,12 @@ export const Input = forwardRef<
|
|||
) : null;
|
||||
|
||||
const lengthCountElement = lengthCount >= whenToShowRemainingCount && (
|
||||
<div className={getClassName('__remaining-count')}>
|
||||
<div
|
||||
className={classNames(getClassName('__remaining-count'), {
|
||||
[getClassName('__remaining-count--warn')]:
|
||||
lengthCount >= whenToWarnRemainingCount,
|
||||
})}
|
||||
>
|
||||
{maxLengthCount - lengthCount}
|
||||
</div>
|
||||
);
|
||||
|
@ -242,7 +256,7 @@ export const Input = forwardRef<
|
|||
)}
|
||||
>
|
||||
{icon ? <div className={getClassName('__icon')}>{icon}</div> : null}
|
||||
{expandable ? (
|
||||
{isTextarea || forceTextarea ? (
|
||||
<textarea dir="auto" rows={1} {...inputProps} />
|
||||
) : (
|
||||
<input dir="auto" {...inputProps} />
|
||||
|
|
32
ts/components/NotePreviewModal.stories.tsx
Normal file
32
ts/components/NotePreviewModal.stories.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import * as React from 'react';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import type { ComponentMeta } from '../storybook/types';
|
||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import {
|
||||
NotePreviewModal,
|
||||
type NotePreviewModalProps,
|
||||
} from './NotePreviewModal';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
title: 'Components/NotePreviewModal',
|
||||
component: NotePreviewModal,
|
||||
argTypes: {},
|
||||
args: {
|
||||
conversation: getDefaultConversation({
|
||||
note: 'Met at UC Berkeley, mutual friends with Katie Hall.\n\nWebsite: https://example.com/',
|
||||
}),
|
||||
i18n,
|
||||
onClose: action('onClose'),
|
||||
onEdit: action('onEdit'),
|
||||
},
|
||||
} satisfies ComponentMeta<NotePreviewModalProps>;
|
||||
|
||||
export function Normal(args: NotePreviewModalProps): JSX.Element {
|
||||
return <NotePreviewModal {...args} />;
|
||||
}
|
47
ts/components/NotePreviewModal.tsx
Normal file
47
ts/components/NotePreviewModal.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type { LocalizerType } from '../types/I18N';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { Modal } from './Modal';
|
||||
import { Linkify } from './conversation/Linkify';
|
||||
|
||||
export type NotePreviewModalProps = Readonly<{
|
||||
conversation: ConversationType;
|
||||
i18n: LocalizerType;
|
||||
onClose: () => void;
|
||||
onEdit: () => void;
|
||||
}>;
|
||||
|
||||
export function NotePreviewModal({
|
||||
conversation,
|
||||
i18n,
|
||||
onClose,
|
||||
onEdit,
|
||||
}: NotePreviewModalProps): JSX.Element {
|
||||
return (
|
||||
<Modal
|
||||
modalName="NotePreviewModal"
|
||||
i18n={i18n}
|
||||
title={i18n('icu:NotePreviewModal__Title')}
|
||||
onClose={onClose}
|
||||
hasXButton
|
||||
modalFooter={
|
||||
<>
|
||||
<Button onClick={onEdit} variant={ButtonVariant.Secondary}>
|
||||
{i18n('icu:edit')}
|
||||
</Button>
|
||||
<Button onClick={onClose} variant={ButtonVariant.Primary}>
|
||||
{i18n('icu:done')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div dir="auto">
|
||||
<Linkify text={conversation.note ?? ''} />
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -330,7 +330,6 @@ export function ProfileEditor({
|
|||
i18n={i18n}
|
||||
maxLengthCount={26}
|
||||
maxByteCount={128}
|
||||
whenToShowRemainingCount={0}
|
||||
onChange={newFirstName => {
|
||||
setStagedProfile(profileData => ({
|
||||
...profileData,
|
||||
|
@ -345,7 +344,6 @@ export function ProfileEditor({
|
|||
i18n={i18n}
|
||||
maxLengthCount={26}
|
||||
maxByteCount={128}
|
||||
whenToShowRemainingCount={0}
|
||||
onChange={newFamilyName => {
|
||||
setStagedProfile(profileData => ({
|
||||
...profileData,
|
||||
|
|
|
@ -59,6 +59,7 @@ export default {
|
|||
args: {
|
||||
i18n,
|
||||
onClose: action('onClose'),
|
||||
onOpenNotePreviewModal: action('onOpenNotePreviewModal'),
|
||||
toggleSignalConnectionsModal: action('toggleSignalConnections'),
|
||||
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
|
||||
updateSharedGroups: action('updateSharedGroups'),
|
||||
|
|
|
@ -11,10 +11,19 @@ import { Modal } from '../Modal';
|
|||
import { UserText } from '../UserText';
|
||||
import { SharedGroupNames } from '../SharedGroupNames';
|
||||
import { About } from './About';
|
||||
import { Intl } from '../Intl';
|
||||
import { areNicknamesEnabled } from '../../util/nicknames';
|
||||
|
||||
function muted(parts: Array<string | JSX.Element>) {
|
||||
return (
|
||||
<span className="AboutContactModal__TitleWithoutNickname">{parts}</span>
|
||||
);
|
||||
}
|
||||
|
||||
export type PropsType = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
onClose: () => void;
|
||||
onOpenNotePreviewModal: () => void;
|
||||
conversation: ConversationType;
|
||||
isSignalConnection: boolean;
|
||||
toggleSignalConnectionsModal: () => void;
|
||||
|
@ -32,6 +41,7 @@ export function AboutContactModal({
|
|||
updateSharedGroups,
|
||||
unblurAvatar,
|
||||
onClose,
|
||||
onOpenNotePreviewModal,
|
||||
}: PropsType): JSX.Element {
|
||||
const { isMe } = conversation;
|
||||
|
||||
|
@ -135,7 +145,26 @@ export function AboutContactModal({
|
|||
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--profile" />
|
||||
|
||||
{areNicknamesEnabled() &&
|
||||
conversation.nicknameGivenName &&
|
||||
conversation.titleNoNickname ? (
|
||||
<span>
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:AboutContactModal__TitleAndTitleWithoutNickname"
|
||||
components={{
|
||||
nickname: <UserText text={conversation.title} />,
|
||||
titleNoNickname: (
|
||||
<UserText text={conversation.titleNoNickname} />
|
||||
),
|
||||
muted,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<UserText text={conversation.title} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isMe && conversation.isVerified ? (
|
||||
|
@ -166,7 +195,7 @@ export function AboutContactModal({
|
|||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--connections" />
|
||||
<button
|
||||
type="button"
|
||||
className="AboutContactModal__signal-connection"
|
||||
className="AboutContactModal__button"
|
||||
onClick={onSignalConnectionClick}
|
||||
>
|
||||
{i18n('icu:AboutContactModal__signal-connection')}
|
||||
|
@ -205,6 +234,21 @@ export function AboutContactModal({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{areNicknamesEnabled() && conversation.note && (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--note" />
|
||||
<button
|
||||
type="button"
|
||||
className="AboutContactModal__button"
|
||||
onClick={onOpenNotePreviewModal}
|
||||
>
|
||||
<div className="AboutContactModal__OneLineEllipsis">
|
||||
<UserText text={conversation.note} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{statusRow}
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
@ -30,6 +31,7 @@ export function FromContact(): JSX.Element {
|
|||
oldName: 'Mr. Fire 🔥 Old',
|
||||
newName: 'Mr. Fire 🔥 New',
|
||||
}}
|
||||
onOpenEditNicknameAndNoteModal={action('onOpenEditNicknameAndNoteModal')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -48,6 +50,7 @@ export function FromNonContact(): JSX.Element {
|
|||
oldName: 'Mr. Fire 🔥 Old',
|
||||
newName: 'Mr. Fire 🔥 New',
|
||||
}}
|
||||
onOpenEditNicknameAndNoteModal={action('onOpenEditNicknameAndNoteModal')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -66,6 +69,7 @@ export function FromContactWithLongNamesBeforeAndAfter(): JSX.Element {
|
|||
oldName: '💅🤷🏽♀️🏯'.repeat(50),
|
||||
newName: '☎️🎉🏝'.repeat(50),
|
||||
}}
|
||||
onOpenEditNicknameAndNoteModal={action('onOpenEditNicknameAndNoteModal')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,16 +9,40 @@ import { SystemMessage } from './SystemMessage';
|
|||
import { Emojify } from './Emojify';
|
||||
import type { ProfileNameChangeType } from '../../util/getStringForProfileChange';
|
||||
import { getStringForProfileChange } from '../../util/getStringForProfileChange';
|
||||
import { Button, ButtonSize, ButtonVariant } from '../Button';
|
||||
import { areNicknamesEnabled } from '../../util/nicknames';
|
||||
|
||||
export type PropsType = {
|
||||
change: ProfileNameChangeType;
|
||||
changedContact: ConversationType;
|
||||
i18n: LocalizerType;
|
||||
onOpenEditNicknameAndNoteModal: () => void;
|
||||
};
|
||||
|
||||
export function ProfileChangeNotification(props: PropsType): JSX.Element {
|
||||
const { change, changedContact, i18n } = props;
|
||||
export function ProfileChangeNotification({
|
||||
change,
|
||||
changedContact,
|
||||
i18n,
|
||||
onOpenEditNicknameAndNoteModal,
|
||||
}: PropsType): JSX.Element {
|
||||
const message = getStringForProfileChange(change, changedContact, i18n);
|
||||
|
||||
return <SystemMessage icon="profile" contents={<Emojify text={message} />} />;
|
||||
return (
|
||||
<SystemMessage
|
||||
icon="profile"
|
||||
contents={<Emojify text={message} />}
|
||||
button={
|
||||
areNicknamesEnabled() &&
|
||||
changedContact.nicknameGivenName != null && (
|
||||
<Button
|
||||
onClick={onOpenEditNicknameAndNoteModal}
|
||||
size={ButtonSize.Small}
|
||||
variant={ButtonVariant.SystemMessage}
|
||||
>
|
||||
{i18n('icu:update')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -314,7 +314,7 @@ const actions = () => ({
|
|||
toggleForwardMessagesModal: action('toggleForwardMessagesModal'),
|
||||
|
||||
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
|
||||
|
||||
onOpenEditNicknameAndNoteModal: action('onOpenEditNicknameAndNoteModal'),
|
||||
onOutgoingAudioCallInConversation: action(
|
||||
'onOutgoingAudioCallInConversation'
|
||||
),
|
||||
|
|
|
@ -81,6 +81,7 @@ const getDefaultProps = () => ({
|
|||
showConversation: action('showConversation'),
|
||||
openGiftBadge: action('openGiftBadge'),
|
||||
saveAttachment: action('saveAttachment'),
|
||||
onOpenEditNicknameAndNoteModal: action('onOpenEditNicknameAndNoteModal'),
|
||||
onOutgoingAudioCallInConversation: action(
|
||||
'onOutgoingAudioCallInConversation'
|
||||
),
|
||||
|
|
|
@ -181,6 +181,7 @@ type PropsLocalType = {
|
|||
isTargeted: boolean;
|
||||
targetMessage: (messageId: string, conversationId: string) => unknown;
|
||||
shouldRenderDateHeader: boolean;
|
||||
onOpenEditNicknameAndNoteModal: () => void;
|
||||
onOpenMessageRequestActionsConfirmation(state: MessageRequestState): void;
|
||||
platform: string;
|
||||
renderContact: SmartContactRendererType<JSX.Element>;
|
||||
|
@ -219,6 +220,7 @@ export const TimelineItem = memo(function TimelineItem({
|
|||
isNextItemCallingNotification,
|
||||
isTargeted,
|
||||
item,
|
||||
onOpenEditNicknameAndNoteModal,
|
||||
onOpenMessageRequestActionsConfirmation,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
|
@ -383,6 +385,7 @@ export const TimelineItem = memo(function TimelineItem({
|
|||
{...reducedProps}
|
||||
{...item.data}
|
||||
i18n={i18n}
|
||||
onOpenEditNicknameAndNoteModal={onOpenEditNicknameAndNoteModal}
|
||||
/>
|
||||
);
|
||||
} else if (item.type === 'paymentEvent') {
|
||||
|
|
|
@ -104,6 +104,8 @@ const createProps = (
|
|||
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
|
||||
toggleAboutContactModal: action('toggleAboutContactModal'),
|
||||
toggleAddUserToAnotherGroupModal: action('toggleAddUserToAnotherGroup'),
|
||||
onDeleteNicknameAndNote: action('onDeleteNicknameAndNote'),
|
||||
onOpenEditNicknameAndNoteModal: action('onOpenEditNicknameAndNoteModal'),
|
||||
onOutgoingAudioCallInConversation: action(
|
||||
'onOutgoingAudioCallInConversation'
|
||||
),
|
||||
|
|
|
@ -63,6 +63,8 @@ import {
|
|||
} from '../../../types/CallDisposition';
|
||||
import { formatDate, formatTime } from '../../../util/timestamp';
|
||||
import { NavTab } from '../../../state/ducks/nav';
|
||||
import { ContextMenu } from '../../ContextMenu';
|
||||
import { areNicknamesEnabled } from '../../../util/nicknames';
|
||||
|
||||
function describeCallHistory(
|
||||
i18n: LocalizerType,
|
||||
|
@ -86,11 +88,12 @@ function describeCallHistory(
|
|||
}
|
||||
|
||||
enum ModalState {
|
||||
NothingOpen,
|
||||
AddingGroupMembers,
|
||||
ConfirmDeleteNicknameAndNote,
|
||||
EditingGroupDescription,
|
||||
EditingGroupTitle,
|
||||
AddingGroupMembers,
|
||||
MuteNotifications,
|
||||
NothingOpen,
|
||||
UnmuteNotifications,
|
||||
}
|
||||
|
||||
|
@ -140,6 +143,8 @@ type ActionProps = {
|
|||
getProfilesForConversation: (id: string) => unknown;
|
||||
leaveGroup: (conversationId: string) => void;
|
||||
loadRecentMediaItems: (id: string, limit: number) => void;
|
||||
onDeleteNicknameAndNote: () => void;
|
||||
onOpenEditNicknameAndNoteModal: () => void;
|
||||
onOutgoingAudioCallInConversation: (conversationId: string) => unknown;
|
||||
onOutgoingVideoCallInConversation: (conversationId: string) => unknown;
|
||||
pushPanelForConversation: PushPanelForConversationActionType;
|
||||
|
@ -207,6 +212,8 @@ export function ConversationDetails({
|
|||
memberships,
|
||||
maxGroupSize,
|
||||
maxRecommendedGroupSize,
|
||||
onDeleteNicknameAndNote,
|
||||
onOpenEditNicknameAndNoteModal,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
pendingApprovalMemberships,
|
||||
|
@ -344,6 +351,30 @@ export function ConversationDetails({
|
|||
/>
|
||||
);
|
||||
break;
|
||||
case ModalState.ConfirmDeleteNicknameAndNote:
|
||||
modalNode = (
|
||||
<ConfirmationDialog
|
||||
dialogName="ConversationDetails.ConfirmDeleteNicknameAndNote"
|
||||
actions={[
|
||||
{
|
||||
action: onDeleteNicknameAndNote,
|
||||
style: 'negative',
|
||||
text: i18n('icu:delete'),
|
||||
},
|
||||
]}
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
title={i18n(
|
||||
'icu:ConversationDetails__ConfirmDeleteNicknameAndNote__Title'
|
||||
)}
|
||||
onClose={onCloseModal}
|
||||
>
|
||||
{i18n(
|
||||
'icu:ConversationDetails__ConfirmDeleteNicknameAndNote__Description'
|
||||
)}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
break;
|
||||
case ModalState.MuteNotifications:
|
||||
modalNode = (
|
||||
<ConversationNotificationsModal
|
||||
|
@ -375,6 +406,7 @@ export function ConversationDetails({
|
|||
</ConfirmationDialog>
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw missingCaseError(modalState);
|
||||
}
|
||||
|
@ -534,6 +566,57 @@ export function ConversationDetails({
|
|||
}
|
||||
/>
|
||||
) : null}
|
||||
{areNicknamesEnabled() && !isGroup && (
|
||||
<PanelRow
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('icu:ConversationDetails--nickname-label')}
|
||||
icon={IconType.edit}
|
||||
/>
|
||||
}
|
||||
label={i18n('icu:ConversationDetails--nickname-label')}
|
||||
onClick={onOpenEditNicknameAndNoteModal}
|
||||
actions={
|
||||
(conversation.nicknameGivenName ||
|
||||
conversation.nicknameFamilyName ||
|
||||
conversation.note) && (
|
||||
<ContextMenu
|
||||
i18n={i18n}
|
||||
portalToRoot
|
||||
popperOptions={{
|
||||
placement: 'bottom',
|
||||
strategy: 'absolute',
|
||||
}}
|
||||
menuOptions={[
|
||||
{
|
||||
icon: 'ConversationDetails--nickname-actions--delete',
|
||||
label: i18n(
|
||||
'icu:ConversationDetails--nickname-actions--delete'
|
||||
),
|
||||
onClick: () => {
|
||||
setModalState(ModalState.ConfirmDeleteNicknameAndNote);
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
{({ openMenu }) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="ConversationDetails--nickname-actions"
|
||||
onClick={openMenu}
|
||||
>
|
||||
<span className="ConversationDetails--nickname-actions-label">
|
||||
{i18n('icu:ConversationDetails--nickname-actions')}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}}
|
||||
</ContextMenu>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{selectedNavTab === NavTab.Chats && (
|
||||
<PanelRow
|
||||
icon={
|
||||
|
|
5
ts/model-types.d.ts
vendored
5
ts/model-types.d.ts
vendored
|
@ -374,6 +374,9 @@ export type ConversationAttributesType = {
|
|||
systemGivenName?: string;
|
||||
systemFamilyName?: string;
|
||||
systemNickname?: string;
|
||||
nicknameGivenName?: string | null;
|
||||
nicknameFamilyName?: string | null;
|
||||
note?: string | null;
|
||||
needsStorageServiceSync?: boolean;
|
||||
needsVerification?: boolean;
|
||||
profileSharing?: boolean;
|
||||
|
@ -469,6 +472,8 @@ export type ConversationRenderInfoType = Pick<
|
|||
| 'systemGivenName'
|
||||
| 'systemFamilyName'
|
||||
| 'systemNickname'
|
||||
| 'nicknameGivenName'
|
||||
| 'nicknameFamilyName'
|
||||
| 'type'
|
||||
| 'username'
|
||||
>;
|
||||
|
|
|
@ -5032,6 +5032,7 @@ export class ConversationModel extends window.Backbone
|
|||
// [X] verified!
|
||||
// [-] profileName
|
||||
// [-] profileFamilyName
|
||||
// [X] nicknameAndNote
|
||||
// [X] blocked
|
||||
// [X] whitelisted
|
||||
// [X] archived
|
||||
|
|
|
@ -202,6 +202,18 @@ export async function toContactRecord(
|
|||
if (profileFamilyName) {
|
||||
contactRecord.familyName = profileFamilyName;
|
||||
}
|
||||
const nicknameGivenName = conversation.get('nicknameGivenName');
|
||||
if (nicknameGivenName) {
|
||||
const nicknameFamilyName = conversation.get('nicknameFamilyName');
|
||||
contactRecord.nickname = {
|
||||
given: nicknameGivenName,
|
||||
family: nicknameFamilyName,
|
||||
};
|
||||
}
|
||||
const note = conversation.get('note');
|
||||
if (note) {
|
||||
contactRecord.note = note;
|
||||
}
|
||||
const systemGivenName = conversation.get('systemGivenName');
|
||||
if (systemGivenName) {
|
||||
contactRecord.systemGivenName = systemGivenName;
|
||||
|
@ -1082,6 +1094,9 @@ export async function mergeContactRecord(
|
|||
systemGivenName: dropNull(contactRecord.systemGivenName),
|
||||
systemFamilyName: dropNull(contactRecord.systemFamilyName),
|
||||
systemNickname: dropNull(contactRecord.systemNickname),
|
||||
nicknameGivenName: dropNull(contactRecord.nickname?.given),
|
||||
nicknameFamilyName: dropNull(contactRecord.nickname?.family),
|
||||
note: dropNull(contactRecord.note),
|
||||
});
|
||||
|
||||
// https://github.com/signalapp/Signal-Android/blob/fc3db538bcaa38dc149712a483d3032c9c1f3998/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt#L921-L936
|
||||
|
|
|
@ -243,6 +243,9 @@ export type ConversationType = ReadonlyDeep<
|
|||
pni?: PniString;
|
||||
e164?: string;
|
||||
name?: string;
|
||||
nicknameGivenName?: string;
|
||||
nicknameFamilyName?: string;
|
||||
note?: string;
|
||||
systemGivenName?: string;
|
||||
systemFamilyName?: string;
|
||||
systemNickname?: string;
|
||||
|
@ -317,6 +320,7 @@ export type ConversationType = ReadonlyDeep<
|
|||
sortedGroupMembers?: ReadonlyArray<ConversationType>;
|
||||
title: string;
|
||||
titleNoDefault?: string;
|
||||
titleNoNickname?: string;
|
||||
searchableTitle?: string;
|
||||
unreadCount?: number;
|
||||
unreadMentionsCount?: number;
|
||||
|
@ -1158,6 +1162,7 @@ export const actions = {
|
|||
updateConversationModelSharedGroups,
|
||||
updateGroupAttributes,
|
||||
updateLastMessage,
|
||||
updateNicknameAndNote,
|
||||
updateSharedGroups,
|
||||
verifyConversationsStoppingSend,
|
||||
};
|
||||
|
@ -4672,6 +4677,39 @@ export function updateLastMessage(
|
|||
};
|
||||
}
|
||||
|
||||
export type NicknameAndNote = ReadonlyDeep<{
|
||||
nickname: {
|
||||
givenName: string;
|
||||
familyName: string | null;
|
||||
} | null;
|
||||
note: string | null;
|
||||
}>;
|
||||
|
||||
function updateNicknameAndNote(
|
||||
conversationId: string,
|
||||
nicknameAndNote: NicknameAndNote
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return async dispatch => {
|
||||
const { nickname, note } = nicknameAndNote;
|
||||
const conversationModel = window.ConversationController.get(conversationId);
|
||||
|
||||
strictAssert(
|
||||
conversationModel != null,
|
||||
'updateNicknameAndNote: Conversation not found'
|
||||
);
|
||||
|
||||
conversationModel.set({
|
||||
nicknameGivenName: nickname?.givenName,
|
||||
nicknameFamilyName: nickname?.familyName,
|
||||
note,
|
||||
});
|
||||
window.Signal.Data.updateConversation(conversationModel.attributes);
|
||||
const conversation = conversationModel.format();
|
||||
dispatch(conversationChanged(conversationId, conversation));
|
||||
conversationModel.captureChange('nicknameAndNote');
|
||||
};
|
||||
}
|
||||
|
||||
export function reducer(
|
||||
state: Readonly<ConversationsStateType> = getEmptyState(),
|
||||
action: Readonly<
|
||||
|
|
|
@ -49,6 +49,9 @@ import type { MessageRequestState } from '../../components/conversation/MessageR
|
|||
export type EditHistoryMessagesType = ReadonlyDeep<
|
||||
Array<MessageAttributesType>
|
||||
>;
|
||||
export type EditNicknameAndNoteModalPropsType = ReadonlyDeep<{
|
||||
conversationId: string;
|
||||
}>;
|
||||
export type DeleteMessagesPropsType = ReadonlyDeep<{
|
||||
conversationId: string;
|
||||
messageIds: ReadonlyArray<string>;
|
||||
|
@ -63,6 +66,9 @@ export type MessageRequestActionsConfirmationPropsType = ReadonlyDeep<{
|
|||
conversationId: string;
|
||||
state: MessageRequestState;
|
||||
}>;
|
||||
export type NotePreviewModalPropsType = ReadonlyDeep<{
|
||||
conversationId: string;
|
||||
}>;
|
||||
export type SafetyNumberChangedBlockingDataType = ReadonlyDeep<{
|
||||
promiseUuid: SingleServePromise.SingleServePromiseIdString;
|
||||
source?: SafetyNumberChangeSource;
|
||||
|
@ -91,6 +97,7 @@ export type GlobalModalsStateType = ReadonlyDeep<{
|
|||
contactModalState?: ContactModalStateType;
|
||||
deleteMessagesProps?: DeleteMessagesPropsType;
|
||||
editHistoryMessages?: EditHistoryMessagesType;
|
||||
editNicknameAndNoteModalProps: EditNicknameAndNoteModalPropsType | null;
|
||||
errorModalProps?: {
|
||||
buttonVariant?: ButtonVariant;
|
||||
description?: string;
|
||||
|
@ -107,6 +114,7 @@ export type GlobalModalsStateType = ReadonlyDeep<{
|
|||
isStoriesSettingsVisible: boolean;
|
||||
isWhatsNewVisible: boolean;
|
||||
messageRequestActionsConfirmationProps: MessageRequestActionsConfirmationPropsType | null;
|
||||
notePreviewModalProps: NotePreviewModalPropsType | null;
|
||||
usernameOnboardingState: UsernameOnboardingState;
|
||||
profileEditorHasError: boolean;
|
||||
profileEditorInitialEditState: ProfileEditorEditState | undefined;
|
||||
|
@ -133,6 +141,7 @@ const TOGGLE_DELETE_MESSAGES_MODAL =
|
|||
'globalModals/TOGGLE_DELETE_MESSAGES_MODAL';
|
||||
const TOGGLE_FORWARD_MESSAGES_MODAL =
|
||||
'globalModals/TOGGLE_FORWARD_MESSAGES_MODAL';
|
||||
const TOGGLE_NOTE_PREVIEW_MODAL = 'globalModals/TOGGLE_NOTE_PREVIEW_MODAL';
|
||||
const TOGGLE_PROFILE_EDITOR = 'globalModals/TOGGLE_PROFILE_EDITOR';
|
||||
export const TOGGLE_PROFILE_EDITOR_ERROR =
|
||||
'globalModals/TOGGLE_PROFILE_EDITOR_ERROR';
|
||||
|
@ -150,6 +159,8 @@ const SHOW_STICKER_PACK_PREVIEW = 'globalModals/SHOW_STICKER_PACK_PREVIEW';
|
|||
const CLOSE_STICKER_PACK_PREVIEW = 'globalModals/CLOSE_STICKER_PACK_PREVIEW';
|
||||
const CLOSE_ERROR_MODAL = 'globalModals/CLOSE_ERROR_MODAL';
|
||||
export const SHOW_ERROR_MODAL = 'globalModals/SHOW_ERROR_MODAL';
|
||||
const TOGGLE_EDIT_NICKNAME_AND_NOTE_MODAL =
|
||||
'globalModals/TOGGLE_EDIT_NICKNAME_AND_NOTE_MODAL';
|
||||
const TOGGLE_MESSAGE_REQUEST_ACTIONS_CONFIRMATION =
|
||||
'globalModals/TOGGLE_MESSAGE_REQUEST_ACTIONS_CONFIRMATION';
|
||||
const SHOW_FORMATTING_WARNING_MODAL =
|
||||
|
@ -221,6 +232,11 @@ type ToggleForwardMessagesModalActionType = ReadonlyDeep<{
|
|||
payload: ForwardMessagesPropsType | undefined;
|
||||
}>;
|
||||
|
||||
type ToggleNotePreviewModalActionType = ReadonlyDeep<{
|
||||
type: typeof TOGGLE_NOTE_PREVIEW_MODAL;
|
||||
payload: NotePreviewModalPropsType | null;
|
||||
}>;
|
||||
|
||||
type ToggleProfileEditorActionType = ReadonlyDeep<{
|
||||
type: typeof TOGGLE_PROFILE_EDITOR;
|
||||
payload: {
|
||||
|
@ -324,6 +340,11 @@ export type ShowErrorModalActionType = ReadonlyDeep<{
|
|||
};
|
||||
}>;
|
||||
|
||||
type ToggleEditNicknameAndNoteModalActionType = ReadonlyDeep<{
|
||||
type: typeof TOGGLE_EDIT_NICKNAME_AND_NOTE_MODAL;
|
||||
payload: EditNicknameAndNoteModalPropsType | null;
|
||||
}>;
|
||||
|
||||
type ToggleMessageRequestActionsConfirmationActionType = ReadonlyDeep<{
|
||||
type: typeof TOGGLE_MESSAGE_REQUEST_ACTIONS_CONFIRMATION;
|
||||
payload: MessageRequestActionsConfirmationPropsType | null;
|
||||
|
@ -386,6 +407,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
|
|||
| ShowContactModalActionType
|
||||
| ShowEditHistoryModalActionType
|
||||
| ShowErrorModalActionType
|
||||
| ToggleEditNicknameAndNoteModalActionType
|
||||
| ToggleMessageRequestActionsConfirmationActionType
|
||||
| ShowFormattingWarningModalActionType
|
||||
| ShowSendAnywayDialogActionType
|
||||
|
@ -401,6 +423,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
|
|||
| ToggleConfirmationModalActionType
|
||||
| ToggleDeleteMessagesModalActionType
|
||||
| ToggleForwardMessagesModalActionType
|
||||
| ToggleNotePreviewModalActionType
|
||||
| ToggleProfileEditorActionType
|
||||
| ToggleProfileEditorErrorActionType
|
||||
| ToggleSafetyNumberModalActionType
|
||||
|
@ -428,6 +451,7 @@ export const actions = {
|
|||
showContactModal,
|
||||
showEditHistoryModal,
|
||||
showErrorModal,
|
||||
toggleEditNicknameAndNoteModal,
|
||||
toggleMessageRequestActionsConfirmation,
|
||||
showFormattingWarningModal,
|
||||
showSendEditWarningModal,
|
||||
|
@ -442,6 +466,7 @@ export const actions = {
|
|||
toggleConfirmationModal,
|
||||
toggleDeleteMessagesModal,
|
||||
toggleForwardMessagesModal,
|
||||
toggleNotePreviewModal,
|
||||
toggleProfileEditor,
|
||||
toggleProfileEditorHasError,
|
||||
toggleSafetyNumberModal,
|
||||
|
@ -626,6 +651,15 @@ function toggleForwardMessagesModal(
|
|||
};
|
||||
}
|
||||
|
||||
function toggleNotePreviewModal(
|
||||
payload: NotePreviewModalPropsType | null
|
||||
): ToggleNotePreviewModalActionType {
|
||||
return {
|
||||
type: TOGGLE_NOTE_PREVIEW_MODAL,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
function toggleProfileEditor(
|
||||
initialEditState?: ProfileEditorEditState
|
||||
): ToggleProfileEditorActionType {
|
||||
|
@ -765,6 +799,15 @@ function showErrorModal({
|
|||
};
|
||||
}
|
||||
|
||||
function toggleEditNicknameAndNoteModal(
|
||||
payload: EditNicknameAndNoteModalPropsType | null
|
||||
): ToggleEditNicknameAndNoteModalActionType {
|
||||
return {
|
||||
type: TOGGLE_EDIT_NICKNAME_AND_NOTE_MODAL,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
function toggleMessageRequestActionsConfirmation(
|
||||
payload: {
|
||||
conversationId: string;
|
||||
|
@ -927,6 +970,7 @@ function copyOverMessageAttributesIntoForwardMessages(
|
|||
export function getEmptyState(): GlobalModalsStateType {
|
||||
return {
|
||||
hasConfirmationModal: false,
|
||||
editNicknameAndNoteModalProps: null,
|
||||
isProfileEditorVisible: false,
|
||||
isShortcutGuideModalVisible: false,
|
||||
isSignalConnectionsVisible: false,
|
||||
|
@ -936,6 +980,7 @@ export function getEmptyState(): GlobalModalsStateType {
|
|||
profileEditorHasError: false,
|
||||
profileEditorInitialEditState: undefined,
|
||||
messageRequestActionsConfirmationProps: null,
|
||||
notePreviewModalProps: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -950,6 +995,13 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === TOGGLE_NOTE_PREVIEW_MODAL) {
|
||||
return {
|
||||
...state,
|
||||
notePreviewModalProps: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === TOGGLE_PROFILE_EDITOR) {
|
||||
return {
|
||||
...state,
|
||||
|
@ -1160,6 +1212,13 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === TOGGLE_EDIT_NICKNAME_AND_NOTE_MODAL) {
|
||||
return {
|
||||
...state,
|
||||
editNicknameAndNoteModalProps: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === TOGGLE_MESSAGE_REQUEST_ACTIONS_CONFIRMATION) {
|
||||
return {
|
||||
...state,
|
||||
|
|
|
@ -61,3 +61,13 @@ export const getProfileEditorInitialEditState = createSelector(
|
|||
getGlobalModalsState,
|
||||
({ profileEditorInitialEditState }) => profileEditorInitialEditState
|
||||
);
|
||||
|
||||
export const getEditNicknameAndNoteModalProps = createSelector(
|
||||
getGlobalModalsState,
|
||||
({ editNicknameAndNoteModalProps }) => editNicknameAndNoteModalProps
|
||||
);
|
||||
|
||||
export const getNotePreviewModalProps = createSelector(
|
||||
getGlobalModalsState,
|
||||
({ notePreviewModalProps }) => notePreviewModalProps
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AboutContactModal } from '../../components/conversation/AboutContactModal';
|
||||
import { isSignalConnection } from '../../util/getSignalConnections';
|
||||
|
@ -9,6 +9,7 @@ import { getGlobalModalsState } from '../selectors/globalModals';
|
|||
import { getConversationSelector } from '../selectors/conversations';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
|
||||
export const SmartAboutContactModal = memo(function SmartAboutContactModal() {
|
||||
const i18n = useSelector(getIntl);
|
||||
|
@ -18,18 +19,25 @@ export const SmartAboutContactModal = memo(function SmartAboutContactModal() {
|
|||
|
||||
const { updateSharedGroups, unblurAvatar } = useConversationsActions();
|
||||
|
||||
const conversation = getConversation(contactId);
|
||||
const { id: conversationId } = conversation ?? {};
|
||||
|
||||
const {
|
||||
toggleAboutContactModal,
|
||||
toggleSignalConnectionsModal,
|
||||
toggleSafetyNumberModal,
|
||||
toggleNotePreviewModal,
|
||||
} = useGlobalModalActions();
|
||||
|
||||
if (!contactId) {
|
||||
const handleOpenNotePreviewModal = useCallback(() => {
|
||||
strictAssert(conversationId != null, 'conversationId is required');
|
||||
toggleNotePreviewModal({ conversationId });
|
||||
}, [toggleNotePreviewModal, conversationId]);
|
||||
|
||||
if (conversation == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const conversation = getConversation(contactId);
|
||||
|
||||
return (
|
||||
<AboutContactModal
|
||||
i18n={i18n}
|
||||
|
@ -40,6 +48,7 @@ export const SmartAboutContactModal = memo(function SmartAboutContactModal() {
|
|||
toggleSafetyNumberModal={toggleSafetyNumberModal}
|
||||
isSignalConnection={isSignalConnection(conversation)}
|
||||
onClose={toggleAboutContactModal}
|
||||
onOpenNotePreviewModal={handleOpenNotePreviewModal}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { sortBy } from 'lodash';
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { ConversationDetails } from '../../components/conversation/conversation-details/ConversationDetails';
|
||||
import {
|
||||
|
@ -114,6 +114,7 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
|
|||
setMuteExpiration,
|
||||
showConversation,
|
||||
updateGroupAttributes,
|
||||
updateNicknameAndNote,
|
||||
} = useConversationsActions();
|
||||
const {
|
||||
onOutgoingAudioCallInConversation,
|
||||
|
@ -124,6 +125,7 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
|
|||
showContactModal,
|
||||
toggleAboutContactModal,
|
||||
toggleAddUserToAnotherGroupModal,
|
||||
toggleEditNicknameAndNoteModal,
|
||||
toggleSafetyNumberModal,
|
||||
} = useGlobalModalActions();
|
||||
const { showLightboxWithMedia } = useLightboxActions();
|
||||
|
@ -162,6 +164,14 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
|
|||
const maxRecommendedGroupSize = getGroupSizeRecommendedLimit(151);
|
||||
const userAvatarData = conversation.avatars ?? [];
|
||||
|
||||
const handleDeleteNicknameAndNote = useCallback(() => {
|
||||
updateNicknameAndNote(conversationId, { nickname: null, note: null });
|
||||
}, [conversationId, updateNicknameAndNote]);
|
||||
|
||||
const handleOpenEditNicknameAndNoteModal = useCallback(() => {
|
||||
toggleEditNicknameAndNoteModal({ conversationId });
|
||||
}, [conversationId, toggleEditNicknameAndNoteModal]);
|
||||
|
||||
return (
|
||||
<ConversationDetails
|
||||
acceptConversation={acceptConversation}
|
||||
|
@ -187,6 +197,8 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
|
|||
maxGroupSize={maxGroupSize}
|
||||
maxRecommendedGroupSize={maxRecommendedGroupSize}
|
||||
memberships={memberships}
|
||||
onDeleteNicknameAndNote={handleDeleteNicknameAndNote}
|
||||
onOpenEditNicknameAndNoteModal={handleOpenEditNicknameAndNoteModal}
|
||||
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
|
||||
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
|
||||
pendingApprovalMemberships={pendingApprovalMemberships}
|
||||
|
|
51
ts/state/smart/EditNicknameAndNoteModal.tsx
Normal file
51
ts/state/smart/EditNicknameAndNoteModal.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
import { getEditNicknameAndNoteModalProps } from '../selectors/globalModals';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { EditNicknameAndNoteModal } from '../../components/EditNicknameAndNoteModal';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
import type { NicknameAndNote } from '../ducks/conversations';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
|
||||
export const SmartEditNicknameAndNoteModal = memo(
|
||||
function SmartEditNicknameAndNoteModal(): JSX.Element {
|
||||
const props = useSelector(getEditNicknameAndNoteModalProps);
|
||||
strictAssert(props != null, 'EditNicknameAndNoteModal requires props');
|
||||
const { conversationId } = props;
|
||||
|
||||
const i18n = useSelector(getIntl);
|
||||
const conversationSelector = useSelector(getConversationSelector);
|
||||
const conversation = conversationSelector(conversationId);
|
||||
strictAssert(
|
||||
conversation != null,
|
||||
'EditNicknameAndNoteModal requires conversation'
|
||||
);
|
||||
|
||||
const { toggleEditNicknameAndNoteModal } = useGlobalModalActions();
|
||||
const { updateNicknameAndNote } = useConversationsActions();
|
||||
|
||||
const handleSave = useCallback(
|
||||
(nicknameAndNote: NicknameAndNote) => {
|
||||
updateNicknameAndNote(conversationId, nicknameAndNote);
|
||||
},
|
||||
[conversationId, updateNicknameAndNote]
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
toggleEditNicknameAndNoteModal(null);
|
||||
}, [toggleEditNicknameAndNoteModal]);
|
||||
|
||||
return (
|
||||
<EditNicknameAndNoteModal
|
||||
i18n={i18n}
|
||||
conversation={conversation}
|
||||
onSave={handleSave}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -24,11 +24,17 @@ import { useGlobalModalActions } from '../ducks/globalModals';
|
|||
import { SmartDeleteMessagesModal } from './DeleteMessagesModal';
|
||||
import { SmartMessageRequestActionsConfirmation } from './MessageRequestActionsConfirmation';
|
||||
import { getGlobalModalsState } from '../selectors/globalModals';
|
||||
import { SmartEditNicknameAndNoteModal } from './EditNicknameAndNoteModal';
|
||||
import { SmartNotePreviewModal } from './NotePreviewModal';
|
||||
|
||||
function renderEditHistoryMessagesModal(): JSX.Element {
|
||||
return <SmartEditHistoryMessagesModal />;
|
||||
}
|
||||
|
||||
function renderEditNicknameAndNoteModal(): JSX.Element {
|
||||
return <SmartEditNicknameAndNoteModal />;
|
||||
}
|
||||
|
||||
function renderProfileEditor(): JSX.Element {
|
||||
return <SmartProfileEditorModal />;
|
||||
}
|
||||
|
@ -53,6 +59,10 @@ function renderMessageRequestActionsConfirmation(): JSX.Element {
|
|||
return <SmartMessageRequestActionsConfirmation />;
|
||||
}
|
||||
|
||||
function renderNotePreviewModal(): JSX.Element {
|
||||
return <SmartNotePreviewModal />;
|
||||
}
|
||||
|
||||
function renderStoriesSettings(): JSX.Element {
|
||||
return <SmartStoriesSettingsModal />;
|
||||
}
|
||||
|
@ -84,10 +94,12 @@ export const SmartGlobalModalContainer = memo(
|
|||
contactModalState,
|
||||
deleteMessagesProps,
|
||||
editHistoryMessages,
|
||||
editNicknameAndNoteModalProps,
|
||||
errorModalProps,
|
||||
formattingWarningData,
|
||||
forwardMessagesProps,
|
||||
messageRequestActionsConfirmationProps,
|
||||
notePreviewModalProps,
|
||||
isAuthorizingArtCreator,
|
||||
isProfileEditorVisible,
|
||||
isShortcutGuideModalVisible,
|
||||
|
@ -166,6 +178,7 @@ export const SmartGlobalModalContainer = memo(
|
|||
}
|
||||
contactModalState={contactModalState}
|
||||
editHistoryMessages={editHistoryMessages}
|
||||
editNicknameAndNoteModalProps={editNicknameAndNoteModalProps}
|
||||
errorModalProps={errorModalProps}
|
||||
deleteMessagesProps={deleteMessagesProps}
|
||||
formattingWarningData={formattingWarningData}
|
||||
|
@ -173,6 +186,7 @@ export const SmartGlobalModalContainer = memo(
|
|||
messageRequestActionsConfirmationProps={
|
||||
messageRequestActionsConfirmationProps
|
||||
}
|
||||
notePreviewModalProps={notePreviewModalProps}
|
||||
hasSafetyNumberChangeModal={hasSafetyNumberChangeModal}
|
||||
hideUserNotFoundModal={hideUserNotFoundModal}
|
||||
hideWhatsNewModal={hideWhatsNewModal}
|
||||
|
@ -187,12 +201,14 @@ export const SmartGlobalModalContainer = memo(
|
|||
renderAddUserToAnotherGroup={renderAddUserToAnotherGroup}
|
||||
renderContactModal={renderContactModal}
|
||||
renderEditHistoryMessagesModal={renderEditHistoryMessagesModal}
|
||||
renderEditNicknameAndNoteModal={renderEditNicknameAndNoteModal}
|
||||
renderErrorModal={renderErrorModal}
|
||||
renderDeleteMessagesModal={renderDeleteMessagesModal}
|
||||
renderForwardMessagesModal={renderForwardMessagesModal}
|
||||
renderMessageRequestActionsConfirmation={
|
||||
renderMessageRequestActionsConfirmation
|
||||
}
|
||||
renderNotePreviewModal={renderNotePreviewModal}
|
||||
renderProfileEditor={renderProfileEditor}
|
||||
renderUsernameOnboarding={renderUsernameOnboarding}
|
||||
renderSafetyNumber={renderSafetyNumber}
|
||||
|
|
41
ts/state/smart/NotePreviewModal.tsx
Normal file
41
ts/state/smart/NotePreviewModal.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { NotePreviewModal } from '../../components/NotePreviewModal';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
import { getNotePreviewModalProps } from '../selectors/globalModals';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
|
||||
export const SmartNotePreviewModal = memo(function SmartNotePreviewModal() {
|
||||
const i18n = useSelector(getIntl);
|
||||
const props = useSelector(getNotePreviewModalProps);
|
||||
strictAssert(props != null, 'props is required');
|
||||
const { conversationId } = props;
|
||||
const conversationSelector = useSelector(getConversationSelector);
|
||||
const conversation = conversationSelector(conversationId);
|
||||
strictAssert(conversation != null, 'conversation is required');
|
||||
|
||||
const { toggleNotePreviewModal, toggleEditNicknameAndNoteModal } =
|
||||
useGlobalModalActions();
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
toggleNotePreviewModal(null);
|
||||
}, [toggleNotePreviewModal]);
|
||||
|
||||
const handleEdit = useCallback(() => {
|
||||
toggleEditNicknameAndNoteModal({ conversationId });
|
||||
}, [toggleEditNicknameAndNoteModal, conversationId]);
|
||||
|
||||
return (
|
||||
<NotePreviewModal
|
||||
conversation={conversation}
|
||||
i18n={i18n}
|
||||
onClose={handleClose}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
);
|
||||
});
|
|
@ -143,6 +143,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
|
|||
showEditHistoryModal,
|
||||
toggleMessageRequestActionsConfirmation,
|
||||
toggleDeleteMessagesModal,
|
||||
toggleEditNicknameAndNoteModal,
|
||||
toggleForwardMessagesModal,
|
||||
toggleSafetyNumberModal,
|
||||
} = useGlobalModalActions();
|
||||
|
@ -155,6 +156,10 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
|
|||
returnToActiveCall,
|
||||
} = useCallingActions();
|
||||
|
||||
const onOpenEditNicknameAndNoteModal = useCallback(() => {
|
||||
toggleEditNicknameAndNoteModal({ conversationId });
|
||||
}, [conversationId, toggleEditNicknameAndNoteModal]);
|
||||
|
||||
const onOpenMessageRequestActionsConfirmation = useCallback(
|
||||
(state: MessageRequestState) => {
|
||||
toggleMessageRequestActionsConfirmation({ conversationId, state });
|
||||
|
@ -198,6 +203,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
|
|||
pushPanelForConversation={pushPanelForConversation}
|
||||
reactToMessage={reactToMessage}
|
||||
copyMessageText={copyMessageText}
|
||||
onOpenEditNicknameAndNoteModal={onOpenEditNicknameAndNoteModal}
|
||||
onOpenMessageRequestActionsConfirmation={
|
||||
onOpenMessageRequestActionsConfirmation
|
||||
}
|
||||
|
|
|
@ -7,11 +7,16 @@ export function getParticipantName(
|
|||
participant: Readonly<
|
||||
Pick<
|
||||
ConversationType,
|
||||
'firstName' | 'systemGivenName' | 'systemNickname' | 'title'
|
||||
| 'firstName'
|
||||
| 'systemGivenName'
|
||||
| 'systemNickname'
|
||||
| 'title'
|
||||
| 'nicknameGivenName'
|
||||
>
|
||||
>
|
||||
): string {
|
||||
return (
|
||||
participant.nicknameGivenName ||
|
||||
participant.systemNickname ||
|
||||
participant.systemGivenName ||
|
||||
participant.firstName ||
|
||||
|
|
|
@ -206,6 +206,9 @@ export function getConversation(model: ConversationModel): ConversationType {
|
|||
expireTimer: attributes.expireTimer,
|
||||
muteExpiresAt: attributes.muteExpiresAt,
|
||||
dontNotifyForMentionsIfMuted: attributes.dontNotifyForMentionsIfMuted,
|
||||
nicknameFamilyName: dropNull(attributes.nicknameFamilyName),
|
||||
nicknameGivenName: dropNull(attributes.nicknameGivenName),
|
||||
note: dropNull(attributes.note),
|
||||
name: attributes.name,
|
||||
systemGivenName: attributes.systemGivenName,
|
||||
systemFamilyName: attributes.systemFamilyName,
|
||||
|
@ -222,6 +225,7 @@ export function getConversation(model: ConversationModel): ConversationType {
|
|||
timestamp: dropNull(timestamp),
|
||||
title: getTitle(attributes),
|
||||
titleNoDefault: getTitleNoDefault(attributes),
|
||||
titleNoNickname: getTitle(attributes, { ignoreNickname: true }),
|
||||
typingContactIdTimestamps,
|
||||
searchableTitle: isMe(attributes)
|
||||
? window.i18n('icu:noteToSelf')
|
||||
|
|
|
@ -9,10 +9,16 @@ import { combineNames } from './combineNames';
|
|||
import { getRegionCodeForNumber } from './libphonenumberUtil';
|
||||
import { isDirectConversation } from './whatTypeOfConversation';
|
||||
import { getE164 } from './getE164';
|
||||
import { areNicknamesEnabled } from './nicknames';
|
||||
|
||||
type TitleOptions = {
|
||||
isShort?: boolean;
|
||||
ignoreNickname?: boolean;
|
||||
};
|
||||
|
||||
export function getTitle(
|
||||
attributes: ConversationRenderInfoType,
|
||||
options?: { isShort?: boolean }
|
||||
options?: TitleOptions
|
||||
): string {
|
||||
const title = getTitleNoDefault(attributes, options);
|
||||
if (title) {
|
||||
|
@ -27,7 +33,7 @@ export function getTitle(
|
|||
|
||||
export function getTitleNoDefault(
|
||||
attributes: ConversationRenderInfoType,
|
||||
{ isShort = false }: { isShort?: boolean } = {}
|
||||
{ isShort = false, ignoreNickname = false }: TitleOptions = {}
|
||||
): string | undefined {
|
||||
if (!isDirectConversation(attributes)) {
|
||||
return attributes.name;
|
||||
|
@ -35,7 +41,15 @@ export function getTitleNoDefault(
|
|||
|
||||
const { username } = attributes;
|
||||
|
||||
let nicknameValue: string | undefined;
|
||||
if (areNicknamesEnabled() && !ignoreNickname) {
|
||||
nicknameValue =
|
||||
(isShort ? attributes.nicknameGivenName : undefined) ||
|
||||
getNicknameName(attributes);
|
||||
}
|
||||
|
||||
return (
|
||||
nicknameValue ||
|
||||
(isShort ? attributes.systemGivenName : undefined) ||
|
||||
getSystemName(attributes) ||
|
||||
(isShort ? attributes.profileName : undefined) ||
|
||||
|
@ -59,6 +73,8 @@ export function canHaveUsername(
|
|||
| 'systemGivenName'
|
||||
| 'systemFamilyName'
|
||||
| 'systemNickname'
|
||||
| 'nicknameGivenName'
|
||||
| 'nicknameFamilyName'
|
||||
| 'type'
|
||||
>,
|
||||
ourConversationId: string | undefined
|
||||
|
@ -72,6 +88,7 @@ export function canHaveUsername(
|
|||
}
|
||||
|
||||
return (
|
||||
!getNicknameName(attributes) &&
|
||||
!getSystemName(attributes) &&
|
||||
!getProfileName(attributes) &&
|
||||
!getNumber(attributes)
|
||||
|
@ -91,6 +108,25 @@ export function getProfileName(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
export function getNicknameName(
|
||||
attributes: Pick<
|
||||
ConversationAttributesType,
|
||||
'nicknameGivenName' | 'nicknameFamilyName' | 'type'
|
||||
>
|
||||
): string | undefined {
|
||||
if (!areNicknamesEnabled()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isDirectConversation(attributes)) {
|
||||
return combineNames(
|
||||
attributes.nicknameGivenName ?? undefined,
|
||||
attributes.nicknameFamilyName ?? undefined
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getSystemName(
|
||||
attributes: Pick<
|
||||
ConversationAttributesType,
|
||||
|
@ -151,6 +187,7 @@ export function hasNumberTitle(
|
|||
>
|
||||
): boolean {
|
||||
return (
|
||||
!getNicknameName(attributes) &&
|
||||
!getSystemName(attributes) &&
|
||||
!getProfileName(attributes) &&
|
||||
Boolean(getNumber(attributes))
|
||||
|
@ -164,6 +201,7 @@ export function hasUsernameTitle(
|
|||
>
|
||||
): boolean {
|
||||
return (
|
||||
!getNicknameName(attributes) &&
|
||||
!getSystemName(attributes) &&
|
||||
!getProfileName(attributes) &&
|
||||
!getNumber(attributes) &&
|
||||
|
|
|
@ -2773,6 +2773,13 @@
|
|||
"updated": "2024-01-11T16:58:57.146Z",
|
||||
"reasonDetail": "Needs access to a hidden span element to get its width"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/AutoSizeTextArea.tsx",
|
||||
"line": " const ownRef = useRef<HTMLTextAreaElement | null>(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2024-03-26T17:14:14.370Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/AvatarTextEditor.tsx",
|
||||
|
|
8
ts/util/nicknames.ts
Normal file
8
ts/util/nicknames.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as RemoteConfig from '../RemoteConfig';
|
||||
|
||||
export function areNicknamesEnabled(): boolean {
|
||||
return RemoteConfig.getValue('desktop.nicknames') === 'TRUE';
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue