Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: markdown preview setting [WPB-15101] #18546

Merged
merged 17 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
"@wireapp/avs": "10.0.4",
"@wireapp/avs-debugger": "0.0.7",
"@wireapp/commons": "5.4.0",
"@wireapp/core": "46.15.5",
"@wireapp/core": "46.15.6",
"@wireapp/react-ui-kit": "9.28.1",
"@wireapp/store-engine-dexie": "2.1.15",
"@wireapp/telemetry": "0.1.5",
"@wireapp/webapp-events": "0.27.0",
"@wireapp/webapp-events": "0.28.0",
"amplify": "https://github.com/wireapp/amplify#head=master",
"beautiful-react-hooks": "5.0.2",
"classnames": "2.5.1",
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1344,6 +1344,8 @@
"preferencesOptionsFontSizeSmall": "Small",
"preferencesOptionsInputLevelDetected": "Audio detected from microphone",
"preferencesOptionsInputLevelNotDetected": "No audio detected from microphone",
"preferencesOptionsMarkdownPreview": "Preview text formatting",
"preferencesOptionsMarkdownPreviewDetails": "When this is on, you can select various formatting options and see the chosen format while typing a message. Otherwise, you need to use markdown commands. This setting applies to your Wire apps on desktop and Wire for web.",
"preferencesOptionsNotifications": "Notifications",
"preferencesOptionsNotificationsNone": "Off",
"preferencesOptionsNotificationsObfuscate": "Hide details",
Expand Down
14 changes: 11 additions & 3 deletions src/script/components/InputBar/InputBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,9 @@ export const InputBar = ({
const hasLocalEphemeralTimer = isSelfDeletingMessagesEnabled && !!localMessageTimer && !hasGlobalMessageTimer;
const isTypingRef = useRef(false);

const messageFormatButtonsEnabled = CONFIG.FEATURE.ENABLE_MESSAGE_FORMAT_BUTTONS;
const isMessageFormatButtonsFlagEnabled = CONFIG.FEATURE.ENABLE_MESSAGE_FORMAT_BUTTONS;

const showGiphyButton = messageFormatButtonsEnabled
const showGiphyButton = isMessageFormatButtonsFlagEnabled
? textValue.length > 0
: textValue.length > 0 && textValue.length <= CONFIG.GIPHY_TEXT_LENGTH;

Expand Down Expand Up @@ -542,6 +542,11 @@ export const InputBar = ({
};
}, [pastedFile]);

const showMarkdownPreview = useUserPropertyValue<boolean>(
() => propertiesRepository.getPreference(PROPERTIES_TYPE.INTERFACE.MARKDOWN_PREVIEW),
WebAppEvents.PROPERTIES.UPDATE.INTERFACE.MARKDOWN_PREVIEW,
);

const controlButtonsProps = {
conversation: conversation,
disableFilesharing: !isFileSharingSendingEnabled,
Expand All @@ -554,6 +559,8 @@ export const InputBar = ({
onSelectFiles: uploadFiles,
onSelectImages: uploadImages,
showGiphyButton: showGiphyButton,
showFormatButton: isMessageFormatButtonsFlagEnabled && showMarkdownPreview,
showEmojiButton: isMessageFormatButtonsFlagEnabled,
isFormatActive: formatToolbar.open,
onFormatClick: formatToolbar.handleClick,
isEmojiActive: emojiPicker.open,
Expand Down Expand Up @@ -582,7 +589,7 @@ export const InputBar = ({
<div
className={cx(`${conversationInputBarClassName}__input input-bar-container`, {
[`${conversationInputBarClassName}__input--editing`]: isEditing,
'input-bar-container--with-toolbar': formatToolbar.open,
'input-bar-container--with-toolbar': formatToolbar.open && showMarkdownPreview,
})}
>
{!isOutgoingRequest && (
Expand Down Expand Up @@ -621,6 +628,7 @@ export const InputBar = ({
onUpdate={setMessageContent}
hasLocalEphemeralTimer={hasLocalEphemeralTimer}
showFormatToolbar={formatToolbar.open}
showMarkdownPreview={showMarkdownPreview}
saveDraftState={saveDraft}
loadDraftState={loadDraft}
onShiftTab={onShiftTab}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ const defaultParams: PropsType = {
onSelectFiles: jest.fn(),
onSelectImages: jest.fn(),
showGiphyButton: true,
showFormatButton: true,
showEmojiButton: true,
isFormatActive: true,
isEmojiActive: true,
onFormatClick: jest.fn(),
Expand All @@ -53,7 +55,7 @@ describe('ControlButtons', () => {
[{isEditing: true}, []],
])('renders the right buttons depending on props (%s)', (overrides, buttonTitles) => {
const params = {...defaultParams, ...overrides};
const {getByTitle, queryByTitle} = render(<ControlButtons {...params} />);
const {getByTitle, queryByTitle} = render(withTheme(<ControlButtons {...params} />));
// check that the relevant buttons are present
buttonTitles.forEach(button => expect(getByTitle(button)).not.toBe(null));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export type ControlButtonsProps = {
isFormatActive: boolean;
isEmojiActive: boolean;
showGiphyButton?: boolean;
showFormatButton: boolean;
showEmojiButton: boolean;
onClickPing: () => void;
onSelectFiles: (files: File[]) => void;
onSelectImages: (files: File[]) => void;
Expand All @@ -59,6 +61,8 @@ const ControlButtons: React.FC<ControlButtonsProps> = ({
isFormatActive,
isEmojiActive,
showGiphyButton,
showFormatButton,
showEmojiButton,
onClickPing,
onSelectFiles,
onSelectImages,
Expand All @@ -67,8 +71,6 @@ const ControlButtons: React.FC<ControlButtonsProps> = ({
onFormatClick,
onEmojiClick,
}) => {
const messageFormatButtonsEnabled = Config.getConfig().FEATURE.ENABLE_MESSAGE_FORMAT_BUTTONS;

if (isEditing) {
return (
<li>
Expand All @@ -80,22 +82,23 @@ const ControlButtons: React.FC<ControlButtonsProps> = ({
if (input.length === 0) {
return (
<>
{messageFormatButtonsEnabled && (
<>
<li>
<FormatTextButton isActive={isFormatActive} onClick={onFormatClick} />
</li>
<li>
<EmojiButton isActive={isEmojiActive} onClick={onEmojiClick} />
</li>
</>
{showFormatButton && (
<li>
<FormatTextButton isActive={isFormatActive} onClick={onFormatClick} />
</li>
)}

{showEmojiButton && (
<li>
<EmojiButton isActive={isEmojiActive} hasRoundedCorners={!showFormatButton} onClick={onEmojiClick} />
</li>
)}

{!disableFilesharing && (
<>
<li>
<ImageUploadButton
hasRoundedCorners={!messageFormatButtonsEnabled}
hasRoundedCorners={!showFormatButton && !showEmojiButton}
onSelectImages={onSelectImages}
acceptedImageTypes={Config.getConfig().ALLOWED_IMAGE_TYPES}
/>
Expand All @@ -122,17 +125,17 @@ const ControlButtons: React.FC<ControlButtonsProps> = ({
<>
{showGiphyButton && !disableFilesharing && (
<>
{messageFormatButtonsEnabled && (
<>
<li>
<FormatTextButton isActive={isFormatActive} onClick={onFormatClick} />
</li>
<li>
<EmojiButton isActive={isEmojiActive} onClick={onEmojiClick} />
</li>
</>
{showFormatButton && (
<li>
<FormatTextButton isActive={isFormatActive} onClick={onFormatClick} />
</li>
)}
{showEmojiButton && (
<li>
<EmojiButton isActive={isEmojiActive} hasRoundedCorners={!showFormatButton} onClick={onEmojiClick} />
</li>
)}
<GiphyButton onGifClick={onGifClick} hasRoundedLeftCorner={!messageFormatButtonsEnabled} />
<GiphyButton onGifClick={onGifClick} hasRoundedLeftCorner={!showFormatButton && !showEmojiButton} />
</>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,16 @@ import {t} from 'Util/LocalizerUtil';

interface EmojiButtonProps {
isActive: boolean;
hasRoundedCorners: boolean;
onClick: (event: MouseEvent<HTMLButtonElement>) => void;
}

export const EmojiButton = ({isActive, onClick}: EmojiButtonProps) => {
export const EmojiButton = ({isActive, hasRoundedCorners, onClick}: EmojiButtonProps) => {
return (
<button
className={cx('controls-right-button no-radius', {
active: isActive,
'buttons-group-button-left': hasRoundedCorners,
})}
type="button"
onClick={onClick}
Expand Down
25 changes: 17 additions & 8 deletions src/script/components/RichTextEditor/RichTextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ interface RichTextEditorProps {
children: ReactElement;
hasLocalEphemeralTimer: boolean;
showFormatToolbar: boolean;
showMarkdownPreview: boolean;
getMentionCandidates: (search?: string | null) => User[];
saveDraftState: (editor: string) => void;
loadDraftState: () => Promise<DraftState>;
Expand Down Expand Up @@ -206,6 +207,7 @@ export const RichTextEditor = ({
replaceEmojis,
editedMessage,
showFormatToolbar,
showMarkdownPreview,
onUpdate,
saveDraftState,
loadDraftState,
Expand Down Expand Up @@ -251,17 +253,23 @@ export const RichTextEditor = ({
}}
/>
<DraftStatePlugin loadDraftState={loadDraftState} />
<EditedMessagePlugin message={editedMessage} />
<ListItemTabIndentationPlugin />
<ListMaxIndentLevelPlugin maxDepth={3} />
<EditedMessagePlugin message={editedMessage} showMarkdownPreview={showMarkdownPreview} />
<EmojiPickerPlugin openStateRef={emojiPickerOpen} />
<HistoryPlugin />
<ListPlugin />
{replaceEmojis && <ReplaceEmojiPlugin />}

<ReplaceCarriageReturnPlugin />
<MarkdownShortcutPlugin transformers={markdownTransformers} />
<CodeHighlightPlugin />

{showMarkdownPreview && (
<>
<ListItemTabIndentationPlugin />
<ListMaxIndentLevelPlugin maxDepth={3} />
<MarkdownShortcutPlugin transformers={markdownTransformers} />
<CodeHighlightPlugin />
</>
)}

<RichTextPlugin
contentEditable={<ContentEditable className="conversation-input-bar-text" data-uie-name="input-message" />}
placeholder={<Placeholder text={placeholder} hasLocalEphemeralTimer={hasLocalEphemeralTimer} />}
Expand All @@ -275,7 +283,6 @@ export const RichTextEditor = ({
/>

<OnChangePlugin onChange={handleChange} ignoreSelectionChange />

<SendPlugin
onSend={() => {
if (!mentionsOpen.current && !emojiPickerOpen.current) {
Expand All @@ -285,7 +292,7 @@ export const RichTextEditor = ({
/>
</div>
</div>
{showFormatToolbar && (
{showFormatToolbar && showMarkdownPreview && (
<div className="input-bar-toolbar">
<FormatToolbar />
</div>
Expand All @@ -298,7 +305,9 @@ export const RichTextEditor = ({
function Placeholder({text, hasLocalEphemeralTimer}: {text: string; hasLocalEphemeralTimer: boolean}) {
return (
<div
className={cx('editor-placeholder', {'conversation-input-bar-text--accent': hasLocalEphemeralTimer})}
className={cx('editor-placeholder', {
'conversation-input-bar-text--accent': hasLocalEphemeralTimer,
})}
data-uie-name="input-placeholder"
>
{text}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ import {ContentMessage} from 'src/script/entity/message/ContentMessage';

import {getMentionMarkdownTransformer} from './getMentionMarkdownTransformer/getMentionMarkdownTransformer';
import {getMentionNodesFromMessage} from './getMentionNodesFromMessage/getMentionNodesFromMessage';
import {getRawMarkdownNodesWithMentions} from './getRawMarkdownFromMessage/getRawMarkdownFromMessage';
import {wrapMentionsWithTags} from './wrapMentionsWithTags/wrapMentionsWithTags';

type Props = {
message?: ContentMessage;
showMarkdownPreview: boolean;
};

export function EditedMessagePlugin({message}: Props): null {
export function EditedMessagePlugin({message, showMarkdownPreview}: Props): null {
const [editor] = useLexicalComposerContext();

useEffect(() => {
Expand All @@ -47,6 +49,12 @@ export function EditedMessagePlugin({message}: Props): null {
// This behaviour is needed to clear selection, if we not clear selection will be on beginning.
$setSelection(null);

if (!showMarkdownPreview) {
const rawMarkdownNodes = getRawMarkdownNodesWithMentions(message);
root.append(rawMarkdownNodes);
return;
}

const messageContent = message.getFirstAsset().text;

const mentionNodes = getMentionNodesFromMessage(message);
Expand All @@ -68,7 +76,7 @@ export function EditedMessagePlugin({message}: Props): null {
});
});
}
}, [editor, message]);
}, [editor, message, showMarkdownPreview]);

return null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import {$createParagraphNode, $createTextNode} from 'lexical';

import {$createMentionNode} from 'Components/RichTextEditor/nodes/MentionNode';
import {ContentMessage} from 'src/script/entity/message/ContentMessage';
import {Text} from 'src/script/entity/message/Text';

import {createNodes} from '../../../utils/generateNodes';

export const getRawMarkdownNodesWithMentions = (message: ContentMessage) => {
const firstAsset = message.getFirstAsset() as Text;
const newMentions = firstAsset.mentions().slice();
const nodes = createNodes(newMentions, firstAsset.text);

const paragraphs = nodes.map(node => {
if (node.type === 'Mention') {
return $createMentionNode('@', node.data.slice(1));
}

return $createTextNode(node.data);
});

const paragraphNode = $createParagraphNode();
paragraphNode.append(...paragraphs);
return paragraphNode;
};
Loading
Loading