Emojify and linkify group descriptions

This commit is contained in:
Evan Hahn 2021-06-17 12:15:51 -05:00 committed by GitHub
parent 68f1023946
commit 65a1e82857
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 87 additions and 18 deletions

View file

@ -0,0 +1,24 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { FunctionComponent } from 'react';
import { RenderTextCallbackType } from '../types/Util';
import { AddNewLines } from './conversation/AddNewLines';
import { Emojify } from './conversation/Emojify';
import { Linkify } from './conversation/Linkify';
type PropsType = {
text: string;
};
const renderNonLink: RenderTextCallbackType = ({ key, text }) => (
<Emojify key={key} text={text} />
);
const renderNonNewLine: RenderTextCallbackType = ({ key, text }) => (
<Linkify key={key} text={text} renderNonLink={renderNonLink} />
);
export const GroupDescriptionText: FunctionComponent<PropsType> = ({
text,
}) => <AddNewLines text={text} renderNonNewLine={renderNonNewLine} />;

View file

@ -30,3 +30,37 @@ story.add('Long', () => (
})} })}
/> />
)); ));
story.add('With newlines', () => (
<GroupDescription
{...createProps({
text: 'This is long\n\nSo many lines\n\nToo many lines?',
})}
/>
));
story.add('With emoji', () => (
<GroupDescription
{...createProps({
text: '🍒🍩🌭',
})}
/>
));
story.add('With link', () => (
<GroupDescription
{...createProps({
text:
'I love https://example.com and http://example.com and example.com, but not https://user:bar@example.com',
})}
/>
));
story.add('Kitchen sink', () => (
<GroupDescription
{...createProps({
text:
'🍒 https://example.com this is a long thing\nhttps://example.com on another line\nhttps://example.com',
})}
/>
));

View file

@ -1,10 +1,14 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { useEffect, useRef, useState } from 'react'; import React, { useLayoutEffect, useRef, useState } from 'react';
import { Modal } from '../Modal'; import { Modal } from '../Modal';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
import { AddNewLines } from './AddNewLines'; import { GroupDescriptionText } from '../GroupDescriptionText';
// Emojification can cause the scroll height to be *slightly* larger than the client
// height, so we add a little wiggle room.
const SHOW_READ_MORE_THRESHOLD = 5;
export type PropsType = { export type PropsType = {
i18n: LocalizerType; i18n: LocalizerType;
@ -21,13 +25,16 @@ export const GroupDescription = ({
const [hasReadMore, setHasReadMore] = useState(false); const [hasReadMore, setHasReadMore] = useState(false);
const [showFullDescription, setShowFullDescription] = useState(false); const [showFullDescription, setShowFullDescription] = useState(false);
useEffect(() => { useLayoutEffect(() => {
if (!textRef || !textRef.current) { if (!textRef || !textRef.current) {
return; return;
} }
setHasReadMore(textRef.current.scrollHeight > textRef.current.clientHeight); setHasReadMore(
}, [setHasReadMore, textRef]); textRef.current.scrollHeight - SHOW_READ_MORE_THRESHOLD >
textRef.current.clientHeight
);
}, [setHasReadMore, text, textRef]);
return ( return (
<> <>
@ -38,11 +45,11 @@ export const GroupDescription = ({
onClose={() => setShowFullDescription(false)} onClose={() => setShowFullDescription(false)}
title={title} title={title}
> >
<AddNewLines text={text} /> <GroupDescriptionText text={text} />
</Modal> </Modal>
)} )}
<div className="GroupDescription__text" ref={textRef}> <div className="GroupDescription__text" ref={textRef}>
{text} <GroupDescriptionText text={text} />
</div> </div>
{hasReadMore && ( {hasReadMore && (
<button <button

View file

@ -1,4 +1,4 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable-next-line max-classes-per-file */ /* eslint-disable-next-line max-classes-per-file */
@ -1368,7 +1368,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
{ {
type: 'description', type: 'description',
description: description:
'This is a long description.\n\nWe need a dialog to view it all!', 'This is a long description.\n\nWe need a dialog to view it all!\n\nIt has a link to https://example.com',
}, },
], ],
}, },
@ -1381,7 +1381,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
{ {
type: 'description', type: 'description',
description: description:
'This is a long description.\n\nWe need a dialog to view it all!', 'This is a long description.\n\nWe need a dialog to view it all!\n\nIt has a link to https://example.com',
}, },
], ],
}, },
@ -1393,7 +1393,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
{ {
type: 'description', type: 'description',
description: description:
'This is a long description.\n\nWe need a dialog to view it all!', 'This is a long description.\n\nWe need a dialog to view it all!\n\nIt has a link to https://example.com',
}, },
], ],
}, },

View file

@ -1,4 +1,4 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { ReactElement, useState } from 'react'; import React, { ReactElement, useState } from 'react';
@ -6,7 +6,7 @@ import React, { ReactElement, useState } from 'react';
import { ReplacementValuesType } from '../../types/I18N'; import { ReplacementValuesType } from '../../types/I18N';
import { FullJSXType, Intl } from '../Intl'; import { FullJSXType, Intl } from '../Intl';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
import { AddNewLines } from './AddNewLines'; import { GroupDescriptionText } from '../GroupDescriptionText';
import { Button, ButtonSize, ButtonVariant } from '../Button'; import { Button, ButtonSize, ButtonVariant } from '../Button';
import { GroupV2ChangeType, GroupV2DescriptionChangeType } from '../../groups'; import { GroupV2ChangeType, GroupV2DescriptionChangeType } from '../../groups';
@ -55,10 +55,10 @@ export function GroupV2Change(props: PropsType): ReactElement {
setIsGroupDescriptionDialogOpen, setIsGroupDescriptionDialogOpen,
] = useState<boolean>(false); ] = useState<boolean>(false);
const groupDescriptionChange = change.details.find( const newGroupDescription = change.details.find(
(item): item is GroupV2DescriptionChangeType => (item): item is GroupV2DescriptionChangeType =>
Boolean(item.type === 'description' && item.description) Boolean(item.type === 'description' && item.description)
); )?.description;
return ( return (
<div className="module-group-v2-change"> <div className="module-group-v2-change">
@ -75,7 +75,7 @@ export function GroupV2Change(props: PropsType): ReactElement {
// eslint-disable-next-line react/no-array-index-key // eslint-disable-next-line react/no-array-index-key
<div key={index}>{item}</div> <div key={index}>{item}</div>
))} ))}
{groupDescriptionChange ? ( {newGroupDescription ? (
<div className="module-group-v2-change--button-container"> <div className="module-group-v2-change--button-container">
<Button <Button
size={ButtonSize.Small} size={ButtonSize.Small}
@ -86,14 +86,14 @@ export function GroupV2Change(props: PropsType): ReactElement {
</Button> </Button>
</div> </div>
) : null} ) : null}
{groupDescriptionChange && isGroupDescriptionDialogOpen ? ( {newGroupDescription && isGroupDescriptionDialogOpen ? (
<Modal <Modal
hasXButton hasXButton
i18n={i18n} i18n={i18n}
title={groupName} title={groupName}
onClose={() => setIsGroupDescriptionDialogOpen(false)} onClose={() => setIsGroupDescriptionDialogOpen(false)}
> >
<AddNewLines text={groupDescriptionChange.description} /> <GroupDescriptionText text={newGroupDescription} />
</Modal> </Modal>
) : null} ) : null}
</div> </div>

View file

@ -79,6 +79,10 @@ export const ConversationDetailsHeader: React.ComponentType<Props> = ({
<button <button
type="button" type="button"
onClick={ev => { onClick={ev => {
if (ev.target instanceof HTMLAnchorElement) {
return;
}
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
startEditing(false); startEditing(false);