Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ dist/
lib/

plans/
/.codex/
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ export const wrapperStyles: CSSObject = {
zIndex: 100000000,
boxShadow: '3px 1px 6px 1px #000',
};

export const messagePreprocessingSwitchStyles: CSSObject = {
marginBottom: '10px',
};
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import {useClickOutside} from 'src/script/hooks/useClickOutside';
import {CoreCryptoLogLevel} from 'Util/debugUtil';

import {wrapperStyles} from './ConfigToolbar.styles';
import {messagePreprocessingSwitchStyles, wrapperStyles} from './ConfigToolbar.styles';

export function ConfigToolbar() {
const [showConfig, setShowConfig] = useState(false);
Expand All @@ -48,6 +48,9 @@
const [videoBackgroundEffectsFeatureEnabled, setVideoBackgroundEffectsFeatureEnabled] = useState(
window.wire?.app?.debug?.isVideoBackgroundEffectsFeatureEnabled() ?? false,
);
const [isMessagePreprocessingDisabled, setIsMessagePreprocessingDisabled] = useState(
window.wire?.app?.debug?.isMessagePreprocessingDisabled() ?? false,

Check warning on line 52 in apps/webapp/src/script/components/ConfigToolbar/ConfigToolbar.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-webapp&issues=AZ4MEthfJxyiBuPnmlDZ&open=AZ4MEthfJxyiBuPnmlDZ&pullRequest=21275
);
const [coreCryptoLevel, setCoreCryptoLevel] = useState<CoreCryptoLogLevel>(CoreCryptoLogLevel.Info);

// Toggle config tool on 'cmd/ctrl + shift + 2'
Expand Down Expand Up @@ -264,6 +267,23 @@
);
};

const renderMessagePreprocessingSwitch = () => {
return (
<div css={messagePreprocessingSwitchStyles}>
<label htmlFor="message-preprocessing-checkbox" style={{display: 'block', fontWeight: 'bold'}}>
DISABLE MESSAGE PREPROCESSING
</label>
<Switch
id="message-preprocessing-checkbox"
checked={isMessagePreprocessingDisabled}
onToggle={isChecked => {
setIsMessagePreprocessingDisabled(window.wire?.app?.debug?.disableMessagePreprocessing(isChecked) === true);

Check warning on line 280 in apps/webapp/src/script/components/ConfigToolbar/ConfigToolbar.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-webapp&issues=AZ4MEthfJxyiBuPnmlDa&open=AZ4MEthfJxyiBuPnmlDa&pullRequest=21275
}}
/>
</div>
);
};

const renderCoreCryptoLogLevelSelect = () => {
const options: Array<{label: string; value: CoreCryptoLogLevel}> = [
{label: 'Off', value: CoreCryptoLogLevel.Off},
Expand All @@ -285,7 +305,7 @@
onChange={event => {
const val = Number(event.currentTarget.value) as CoreCryptoLogLevel;
setCoreCryptoLevel(val);
void window.wire?.app?.debug?.setCoreCryptoMaxLogLevel(val);

Check warning on line 308 in apps/webapp/src/script/components/ConfigToolbar/ConfigToolbar.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-webapp&issues=AZ6FNo91Nrsyk6IO4KuG&open=AZ6FNo91Nrsyk6IO4KuG&pullRequest=21275
}}
style={{padding: '6px 8px'}}
>
Expand Down Expand Up @@ -351,6 +371,10 @@

<hr />

<div>{renderMessagePreprocessingSwitch()}</div>

<hr />

<div>{renderCoreCryptoLogLevelSelect()}</div>

<hr />
Expand Down
29 changes: 25 additions & 4 deletions apps/webapp/src/script/components/InputBar/InputBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
*
*/

import {useCallback, useRef, useState} from 'react';
import {useCallback, useEffect, useRef, useState} from 'react';

import is from '@sindresorhus/is';
import {amplify} from 'amplify';
import cx from 'classnames';
import {LexicalEditor, $createTextNode, $insertNodes} from 'lexical';
Expand All @@ -45,6 +46,7 @@
import {EventName} from 'Repositories/tracking/EventName';
import {CONVERSATION_TYPING_INDICATOR_MODE} from 'Repositories/user/TypingIndicatorMode';
import {useKoSubscribableChildren} from 'Util/componentUtil';
import {DISABLE_MESSAGE_PREPROCESSING_EVENT, isMessagePreprocessingDisabled} from 'Util/debugMessagePreprocessingUtil';
import {t} from 'Util/localizerUtil';
import {TIME_IN_MILLIS} from 'Util/timeUtil';

Expand Down Expand Up @@ -148,6 +150,7 @@
* It's directly derived from the editor state
*/
const [messageContent, setMessageContent] = useState<MessageContent>({text: ''});
const [disableMessagePreprocessing, setDisableMessagePreprocessing] = useState(isMessagePreprocessingDisabled);

const formatToolbar = useFormatToolbar();

Expand Down Expand Up @@ -206,6 +209,22 @@
() => propertiesRepository.getPreference(PROPERTIES_TYPE.INTERFACE.MARKDOWN_PREVIEW),
WebAppEvents.PROPERTIES.UPDATE.INTERFACE.MARKDOWN_PREVIEW,
);
const effectiveShowMarkdownPreview = showMarkdownPreview && !disableMessagePreprocessing;

useEffect(() => {
const handleMessagePreprocessingChange = (event: Event) => {
const isDisabled =
event instanceof CustomEvent && is.boolean(event.detail) ? event.detail : isMessagePreprocessingDisabled();

setDisableMessagePreprocessing(isDisabled);
};

window.addEventListener(DISABLE_MESSAGE_PREPROCESSING_EVENT, handleMessagePreprocessingChange);

Check warning on line 222 in apps/webapp/src/script/components/InputBar/InputBar.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-webapp&issues=AZ4MEtZBJxyiBuPnmlDX&open=AZ4MEtZBJxyiBuPnmlDX&pullRequest=21275

return () => {
window.removeEventListener(DISABLE_MESSAGE_PREPROCESSING_EVENT, handleMessagePreprocessingChange);

Check warning on line 225 in apps/webapp/src/script/components/InputBar/InputBar.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-webapp&issues=AZ4MEtZBJxyiBuPnmlDY&open=AZ4MEtZBJxyiBuPnmlDY&pullRequest=21275
};
}, []);

const {
editedMessage,
Expand All @@ -232,6 +251,7 @@
editorRef,
pastedFile: fileHandling.pastedFile,
sendPastedFile: fileHandling.sendPastedFile,
disableMessagePreprocessing,
});

if (fileHandling.pastedFile && !!isCellsEnabled) {
Expand Down Expand Up @@ -260,7 +280,7 @@
return;
}

void sendMessage();

Check failure on line 283 in apps/webapp/src/script/components/InputBar/InputBar.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this use of the "void" operator.

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-webapp&issues=AZ6FNo0MNrsyk6IO4KuF&open=AZ6FNo0MNrsyk6IO4KuF&pullRequest=21275
}, [isSendingDisabled, sendMessage]);

const showAvatar = !!messageContent.text.length;
Expand All @@ -281,7 +301,7 @@
<div
className={cx(`conversation-input-bar__input input-bar-container`, {
[`conversation-input-bar__input--editing`]: isEditing,
'input-bar-container--with-toolbar': formatToolbar.open && showMarkdownPreview,
'input-bar-container--with-toolbar': formatToolbar.open && effectiveShowMarkdownPreview,
'input-bar-container--with-files': !!files.length,
})}
>
Expand All @@ -303,7 +323,7 @@
editedMessage={editedMessage}
inputPlaceholder={inputPlaceholder}
hasLocalEphemeralTimer={hasLocalEphemeralTimer}
showMarkdownPreview={showMarkdownPreview}
showMarkdownPreview={effectiveShowMarkdownPreview}
formatToolbar={formatToolbar}
onSetup={editor => {
editorRef.current = editor;
Expand All @@ -321,6 +341,7 @@
getMentionCandidates={getMentionCandidates}
saveDraftState={draftState.save}
loadDraftState={draftState.load}
disableMessagePreprocessing={disableMessagePreprocessing}
replaceEmojis={shouldReplaceEmoji}
>
{!!files.length && <FilePreviews files={files} conversationQualifiedId={conversation.qualifiedId} />}
Expand All @@ -332,7 +353,7 @@
messageContent={messageContent}
isEditing={isEditing}
isSendingDisabled={isSendingDisabled}
showMarkdownPreview={showMarkdownPreview}
showMarkdownPreview={effectiveShowMarkdownPreview}
showGiphyButton={giphy.showGiphyButton}
formatToolbar={formatToolbar}
emojiPicker={emojiPicker}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ interface InputBarEditorProps {
getMentionCandidates: (search?: string | null) => User[];
saveDraftState: (editorState: string, plainMessage: string, replyId?: string) => void;
loadDraftState: () => Promise<DraftState>;
disableMessagePreprocessing: boolean;
replaceEmojis: boolean;
children: React.ReactNode;
}
Expand All @@ -70,6 +71,7 @@ export const InputBarEditor = ({
getMentionCandidates,
saveDraftState,
loadDraftState,
disableMessagePreprocessing,
replaceEmojis,
children,
}: InputBarEditorProps) => {
Expand All @@ -83,6 +85,7 @@ export const InputBarEditor = ({
onEscape={onEscape}
onArrowUp={onArrowUp}
getMentionCandidates={getMentionCandidates}
disableMessagePreprocessing={disableMessagePreprocessing}
replaceEmojis={replaceEmojis}
placeholder={inputPlaceholder}
onUpdate={onUpdate}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,12 @@ import {ListItemTabIndentationPlugin} from './plugins/ListIndentationPlugin/List
import {ListMaxIndentLevelPlugin} from './plugins/ListMaxIndentLevelPlugin/ListMaxIndentLevelPlugin';
import {MentionsPlugin} from './plugins/MentionsPlugin';
import {PastePlugin} from './plugins/PastePlugin/PastePlugin';
import {PlainTextPastePlugin} from './plugins/PlainTextPastePlugin';
import {ReplaceCarriageReturnPlugin} from './plugins/ReplaceCarriageReturnPlugin/ReplaceCarriageReturnPlugin';
import {SendPlugin} from './plugins/SendPlugin/SendPlugin';
import {markdownTransformers} from './utils/markdownTransformers';
import {parseMentions} from './utils/parseMentions';
import {transformMessage} from './utils/transformMessage';
import {getRawMessageText, transformMessage} from './utils/transformMessage';
import {useEditorDraftState} from './utils/useEditorDraftState';

interface RichTextEditorProps {
Expand All @@ -69,6 +70,7 @@ interface RichTextEditorProps {
hasLocalEphemeralTimer: boolean;
showFormatToolbar: boolean;
showMarkdownPreview: boolean;
disableMessagePreprocessing: boolean;
getMentionCandidates: (search?: string | null) => User[];
saveDraftState: (editor: string, plainMessage: string, replyId?: string) => void;
loadDraftState: () => Promise<DraftState>;
Expand All @@ -89,6 +91,7 @@ export const RichTextEditor = ({
editedMessage,
showFormatToolbar,
showMarkdownPreview,
disableMessagePreprocessing,
onUpdate,
saveDraftState,
loadDraftState,
Expand All @@ -108,6 +111,7 @@ export const RichTextEditor = ({
editorRef,
saveDraftState,
replaceEmojis,
disableMessagePreprocessing,
});

const handleChange = (editorState: EditorState) => {
Expand All @@ -116,13 +120,14 @@ export const RichTextEditor = ({
return;
}

const markdown = $convertToMarkdownString(markdownTransformers, undefined, true);

const text = transformMessage({replaceEmojis, markdown});
const markdown = disableMessagePreprocessing
? ''
: $convertToMarkdownString(markdownTransformers, undefined, true);
const text = disableMessagePreprocessing ? getRawMessageText() : transformMessage({replaceEmojis, markdown});

onUpdate({
text,
mentions: parseMentions(editorRef.current, markdown, getMentionCandidates()),
mentions: parseMentions(editorRef.current, text, getMentionCandidates()),
});

saveDraft();
Expand All @@ -145,11 +150,11 @@ export const RichTextEditor = ({
/>
<DraftStatePlugin loadDraftState={loadDraftState} />
<EditedMessagePlugin message={editedMessage} showMarkdownPreview={showMarkdownPreview} />
<EmojiPickerPlugin openStateRef={emojiPickerOpen} />
{!disableMessagePreprocessing && <EmojiPickerPlugin openStateRef={emojiPickerOpen} />}
<HistoryPlugin />

{replaceEmojis && <ReplaceEmojiPlugin />}
<ReplaceCarriageReturnPlugin />
{replaceEmojis && !disableMessagePreprocessing && <ReplaceEmojiPlugin />}
{!disableMessagePreprocessing && <ReplaceCarriageReturnPlugin />}

{showMarkdownPreview && (
<>
Expand Down Expand Up @@ -177,12 +182,16 @@ export const RichTextEditor = ({
<OnChangePlugin onChange={handleChange} ignoreSelectionChange />
<SendPlugin
onSend={() => {
if (!mentionsOpen.current && !emojiPickerOpen.current) {
if (!mentionsOpen.current && (disableMessagePreprocessing || !emojiPickerOpen.current)) {
onSend();
}
}}
/>
<PastePlugin getMentionCandidates={getMentionCandidates} isPreviewMode={showMarkdownPreview} />
{disableMessagePreprocessing ? (
<PlainTextPastePlugin />
) : (
<PastePlugin getMentionCandidates={getMentionCandidates} isPreviewMode={showMarkdownPreview} />
)}
</div>
</div>
{showFormatToolbar && showMarkdownPreview && (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Wire
* Copyright (C) 2026 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 {useCallback, useEffect} from 'react';

import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {$getSelection, $isRangeSelection, COMMAND_PRIORITY_HIGH, PASTE_COMMAND} from 'lexical';

export const PlainTextPastePlugin = (): null => {
const [editor] = useLexicalComposerContext();

const handlePaste = useCallback(
(event: ClipboardEvent) => {
const plainText = event.clipboardData?.getData('text/plain');

if (plainText === undefined) {
return false;
}

editor.update(() => {
const selection = $getSelection();

if ($isRangeSelection(selection)) {
selection.insertText(plainText);
}
});

event.preventDefault();
return true;
},
[editor],
);

useEffect(() => {
return editor.registerCommand(PASTE_COMMAND, handlePaste, COMMAND_PRIORITY_HIGH);
}, [editor, handlePaste]);

return null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Wire
* Copyright (C) 2026 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/.
*
*/

export * from './PlainTextPastePlugin';
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,17 @@
*
*/

import {$getRoot} from 'lexical';

import {findAndTransformEmoji} from '../plugins/InlineEmojiReplacementPlugin';

export const transformMessage = ({replaceEmojis, markdown}: {replaceEmojis: boolean; markdown: string}) => {
return replaceEmojis ? findAndTransformEmoji(markdown) : markdown;
};

export const getRawMessageText = () => {
return $getRoot()
.getChildren()
.map(node => node.getTextContent())
.join('\n');
};
Loading
Loading