[MM-53650] Add disable emoticon rendering setting to webapp (#29414)

* add renderEmoticonsAsEmoji to post_markdown

* add renderEmoticonsAsEmoji to preferences, config

* add EnableRenderEmoticonsAsEmoji to GenerateClientConfig

* add EnableRenderEmoticonsAsEmoji to TeamSettings

* create renderEmoticonsAsEmoji component

* update translation file

* update tests

* add tests for RenderEmoticonsAsEmoji

* remove unused prop

* remove unused variables

* add test cases for undefined/false values for renderemoticonsasemoji

* switch strings from backticks to single quotes

* Update webapp/channels/src/components/user_settings/display/render_emoticons_as_emoji/render_emoticons_as_emoji.tsx

Co-authored-by: Harrison Healey <harrisonmhealey@gmail.com>

* Update webapp/channels/src/components/user_settings/display/render_emoticons_as_emoji/render_emoticons_as_emoji.tsx

Co-authored-by: Harrison Healey <harrisonmhealey@gmail.com>

* Update webapp/channels/src/components/user_settings/display/render_emoticons_as_emoji/render_emoticons_as_emoji.tsx

Co-authored-by: Harrison Healey <harrisonmhealey@gmail.com>

* Update webapp/channels/src/utils/emoticons.tsx

Co-authored-by: Harrison Healey <harrisonmhealey@gmail.com>

* remove server setting

* remove usestate

* update i18n

* remove config setting

* fix broken test

* fix focus edit button logic

* update tests for when render emoticons option is undefined

* remove config usage from selectors

* update setting display text

* remove 'automatically' from setting description

* use default from preferences instead of hardcoded true

* update component snapshot

* remove renderemoticonsasemoji = true from emoticons tests

* Apply suggestions from code review

* Switch from useEffect to useDidUpdate

* Run fix-style

* Add RenderEmoticonsAsEmoji component to test spec

* Rewrite RenderEmoticonsAsEmoji to mirror ManageTimezones and ManageLanguages

* Update UserSettingsDisplay snapshot

* Switch test to renderWithContext, update props

* Move options into const objects

* Wrap functions in useCallback

---------

Co-authored-by: Harrison Healey <harrisonmhealey@gmail.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Uday Rana
2025-06-02 08:35:27 -04:00
коммит произвёл GitHub
родитель ccddb63abb
Коммит 58d6c71ed2
19 изменённых файлов: 975 добавлений и 7 удалений

Просмотреть файл

@@ -50,6 +50,7 @@ describe('Verify Accessibility Support in different sections in Settings and Pro
{key: 'click_to_reply', label: 'Click to open threads', type: 'radio'},
{key: 'channel_display_mode', label: 'Channel Display', type: 'radio'},
{key: 'one_click_reactions_enabled', label: 'Quick reactions on messages', type: 'radio'},
{key: 'renderEmoticonsAsEmoji', label: 'Render emoticons as emojis', type: 'radio'},
{key: 'languages', label: 'Language', type: 'dropdown'},
],
sidebar: [

Просмотреть файл

@@ -82,6 +82,7 @@ function makeMapStateToProps() {
hideGuestTags: getConfig(state).HideGuestTags === 'true',
isEnterpriseOrCloudOrSKUStarterFree: isEnterpriseOrCloudOrSKUStarterFree(license, subscriptionProduct, isEnterpriseReady),
isEnterpriseReady,
renderEmoticonsAsEmoji: getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.RENDER_EMOTICONS_AS_EMOJI, Preferences.RENDER_EMOTICONS_AS_EMOJI_DEFAULT === 'true'),
};
};
}

Просмотреть файл

@@ -32,6 +32,7 @@ describe('components/PostMarkdown', () => {
isEnterpriseOrCloudOrSKUStarterFree: true,
isEnterpriseReady: false,
dispatch: jest.fn(),
renderEmoticonsAsEmoji: true,
};
const state = {entities: {

Просмотреть файл

@@ -40,6 +40,11 @@ export type OwnProps = {
*/
showPostEditedIndicator?: boolean;
options?: TextFormattingOptions;
/**
* Whether or not to render text emoticons (:D) as emojis
*/
renderEmoticonsAsEmoji?: boolean;
};
type Props = PropsFromRedux & OwnProps;
@@ -52,12 +57,13 @@ export default class PostMarkdown extends React.PureComponent<Props> {
};
getOptions = memoize(
(options?: TextFormattingOptions, disableGroupHighlight?: boolean, mentionHighlight?: boolean, editedAt?: number) => {
(options?: TextFormattingOptions, disableGroupHighlight?: boolean, mentionHighlight?: boolean, editedAt?: number, renderEmoticonsAsEmoji?: boolean) => {
return {
...options,
disableGroupHighlight,
mentionHighlight,
editedAt,
renderEmoticonsAsEmoji,
};
});
@@ -110,6 +116,7 @@ export default class PostMarkdown extends React.PureComponent<Props> {
this.props.post?.props?.disable_group_highlight === true,
mentionHighlight,
this.props.post?.edit_at,
this.props?.renderEmoticonsAsEmoji,
);
let highlightKeys;

Просмотреть файл

@@ -314,6 +314,48 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
areAllSectionsInactive={false}
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Off"
id="user.settings.advance.off"
/>
}
max={
<Memo(Connect(RenderEmoticonsAsEmoji))
adminMode={false}
renderEmoticonsAsEmoji=""
updateSection={[Function]}
user={
Object {
"id": "user_id",
"locale": "en",
"timezone": Object {
"automaticTimezone": "America/New_York",
"manualTimezone": "",
"useAutomaticTimezone": "true",
},
"username": "username",
}
}
/>
}
section="renderEmoticonsAsEmoji"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Render emoticons as emojis"
id="user.settings.display.renderEmoticonsAsEmojiTitle"
/>
}
updateSection={[Function]}
/>
<div
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
@@ -669,6 +711,48 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
areAllSectionsInactive={false}
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Off"
id="user.settings.advance.off"
/>
}
max={
<Memo(Connect(RenderEmoticonsAsEmoji))
adminMode={false}
renderEmoticonsAsEmoji=""
updateSection={[Function]}
user={
Object {
"id": "user_id",
"locale": "en",
"timezone": Object {
"automaticTimezone": "America/New_York",
"manualTimezone": "",
"useAutomaticTimezone": "true",
},
"username": "username",
}
}
/>
}
section="renderEmoticonsAsEmoji"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Render emoticons as emojis"
id="user.settings.display.renderEmoticonsAsEmojiTitle"
/>
}
updateSection={[Function]}
/>
<div
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
@@ -1024,6 +1108,48 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
areAllSectionsInactive={false}
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Off"
id="user.settings.advance.off"
/>
}
max={
<Memo(Connect(RenderEmoticonsAsEmoji))
adminMode={false}
renderEmoticonsAsEmoji=""
updateSection={[Function]}
user={
Object {
"id": "user_id",
"locale": "en",
"timezone": Object {
"automaticTimezone": "America/New_York",
"manualTimezone": "",
"useAutomaticTimezone": "true",
},
"username": "username",
}
}
/>
}
section="renderEmoticonsAsEmoji"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Render emoticons as emojis"
id="user.settings.display.renderEmoticonsAsEmojiTitle"
/>
}
updateSection={[Function]}
/>
<div
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
@@ -1379,6 +1505,48 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
areAllSectionsInactive={false}
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Off"
id="user.settings.advance.off"
/>
}
max={
<Memo(Connect(RenderEmoticonsAsEmoji))
adminMode={false}
renderEmoticonsAsEmoji=""
updateSection={[Function]}
user={
Object {
"id": "user_id",
"locale": "en",
"timezone": Object {
"automaticTimezone": "America/New_York",
"manualTimezone": "",
"useAutomaticTimezone": "true",
},
"username": "username",
}
}
/>
}
section="renderEmoticonsAsEmoji"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Render emoticons as emojis"
id="user.settings.display.renderEmoticonsAsEmojiTitle"
/>
}
updateSection={[Function]}
/>
<div
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
@@ -1661,6 +1829,48 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
areAllSectionsInactive={false}
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Off"
id="user.settings.advance.off"
/>
}
max={
<Memo(Connect(RenderEmoticonsAsEmoji))
adminMode={false}
renderEmoticonsAsEmoji=""
updateSection={[Function]}
user={
Object {
"id": "user_id",
"locale": "en",
"timezone": Object {
"automaticTimezone": "America/New_York",
"manualTimezone": "",
"useAutomaticTimezone": "true",
},
"username": "username",
}
}
/>
}
section="renderEmoticonsAsEmoji"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Render emoticons as emojis"
id="user.settings.display.renderEmoticonsAsEmojiTitle"
/>
}
updateSection={[Function]}
/>
<div
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={true}
@@ -1925,6 +2135,48 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
areAllSectionsInactive={false}
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Off"
id="user.settings.advance.off"
/>
}
max={
<Memo(Connect(RenderEmoticonsAsEmoji))
adminMode={false}
renderEmoticonsAsEmoji=""
updateSection={[Function]}
user={
Object {
"id": "user_id",
"locale": "en",
"timezone": Object {
"automaticTimezone": "America/New_York",
"manualTimezone": "",
"useAutomaticTimezone": "true",
},
"username": "username",
}
}
/>
}
section="renderEmoticonsAsEmoji"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Render emoticons as emojis"
id="user.settings.display.renderEmoticonsAsEmojiTitle"
/>
}
updateSection={[Function]}
/>
<div
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
@@ -2280,6 +2532,48 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
areAllSectionsInactive={false}
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Off"
id="user.settings.advance.off"
/>
}
max={
<Memo(Connect(RenderEmoticonsAsEmoji))
adminMode={false}
renderEmoticonsAsEmoji=""
updateSection={[Function]}
user={
Object {
"id": "user_id",
"locale": "en",
"timezone": Object {
"automaticTimezone": "America/New_York",
"manualTimezone": "",
"useAutomaticTimezone": "true",
},
"username": "username",
}
}
/>
}
section="renderEmoticonsAsEmoji"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Render emoticons as emojis"
id="user.settings.display.renderEmoticonsAsEmojiTitle"
/>
}
updateSection={[Function]}
/>
<div
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
@@ -2653,6 +2947,48 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
areAllSectionsInactive={false}
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Off"
id="user.settings.advance.off"
/>
}
max={
<Memo(Connect(RenderEmoticonsAsEmoji))
adminMode={false}
renderEmoticonsAsEmoji=""
updateSection={[Function]}
user={
Object {
"id": "user_id",
"locale": "en",
"timezone": Object {
"automaticTimezone": "America/New_York",
"manualTimezone": "",
"useAutomaticTimezone": "true",
},
"username": "username",
}
}
/>
}
section="renderEmoticonsAsEmoji"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Render emoticons as emojis"
id="user.settings.display.renderEmoticonsAsEmojiTitle"
/>
}
updateSection={[Function]}
/>
<div
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
@@ -2935,6 +3271,48 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
areAllSectionsInactive={true}
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Off"
id="user.settings.advance.off"
/>
}
max={
<Memo(Connect(RenderEmoticonsAsEmoji))
adminMode={false}
renderEmoticonsAsEmoji=""
updateSection={[Function]}
user={
Object {
"id": "user_id",
"locale": "en",
"timezone": Object {
"automaticTimezone": "America/New_York",
"manualTimezone": "",
"useAutomaticTimezone": "true",
},
"username": "username",
}
}
/>
}
section="renderEmoticonsAsEmoji"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Render emoticons as emojis"
id="user.settings.display.renderEmoticonsAsEmojiTitle"
/>
}
updateSection={[Function]}
/>
<div
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
@@ -3217,6 +3595,48 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
areAllSectionsInactive={false}
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Off"
id="user.settings.advance.off"
/>
}
max={
<Memo(Connect(RenderEmoticonsAsEmoji))
adminMode={false}
renderEmoticonsAsEmoji=""
updateSection={[Function]}
user={
Object {
"id": "user_id",
"locale": "en",
"timezone": Object {
"automaticTimezone": "America/New_York",
"manualTimezone": "",
"useAutomaticTimezone": "true",
},
"username": "username",
}
}
/>
}
section="renderEmoticonsAsEmoji"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Render emoticons as emojis"
id="user.settings.display.renderEmoticonsAsEmojiTitle"
/>
}
updateSection={[Function]}
/>
<div
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
@@ -3499,6 +3919,48 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
areAllSectionsInactive={false}
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Off"
id="user.settings.advance.off"
/>
}
max={
<Memo(Connect(RenderEmoticonsAsEmoji))
adminMode={false}
renderEmoticonsAsEmoji=""
updateSection={[Function]}
user={
Object {
"id": "user_id",
"locale": "en",
"timezone": Object {
"automaticTimezone": "America/New_York",
"manualTimezone": "",
"useAutomaticTimezone": "true",
},
"username": "username",
}
}
/>
}
section="renderEmoticonsAsEmoji"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Render emoticons as emojis"
id="user.settings.display.renderEmoticonsAsEmojiTitle"
/>
}
updateSection={[Function]}
/>
<div
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
@@ -3793,6 +4255,48 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
areAllSectionsInactive={false}
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Off"
id="user.settings.advance.off"
/>
}
max={
<Memo(Connect(RenderEmoticonsAsEmoji))
adminMode={false}
renderEmoticonsAsEmoji=""
updateSection={[Function]}
user={
Object {
"id": "user_id",
"locale": "en",
"timezone": Object {
"automaticTimezone": "America/New_York",
"manualTimezone": "",
"useAutomaticTimezone": "true",
},
"username": "username",
}
}
/>
}
section="renderEmoticonsAsEmoji"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Render emoticons as emojis"
id="user.settings.display.renderEmoticonsAsEmojiTitle"
/>
}
updateSection={[Function]}
/>
<div
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
@@ -4075,6 +4579,48 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
areAllSectionsInactive={false}
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Off"
id="user.settings.advance.off"
/>
}
max={
<Memo(Connect(RenderEmoticonsAsEmoji))
adminMode={false}
renderEmoticonsAsEmoji=""
updateSection={[Function]}
user={
Object {
"id": "user_id",
"locale": "en",
"timezone": Object {
"automaticTimezone": "America/New_York",
"manualTimezone": "",
"useAutomaticTimezone": "true",
},
"username": "username",
}
}
/>
}
section="renderEmoticonsAsEmoji"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Render emoticons as emojis"
id="user.settings.display.renderEmoticonsAsEmojiTitle"
/>
}
updateSection={[Function]}
/>
<div
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
@@ -4333,6 +4879,48 @@ exports[`components/user_settings/display/UserSettingsDisplay should not show la
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}
areAllSectionsInactive={true}
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Off"
id="user.settings.advance.off"
/>
}
max={
<Memo(Connect(RenderEmoticonsAsEmoji))
adminMode={false}
renderEmoticonsAsEmoji=""
updateSection={[Function]}
user={
Object {
"id": "user_id",
"locale": "en",
"timezone": Object {
"automaticTimezone": "America/New_York",
"manualTimezone": "",
"useAutomaticTimezone": "true",
},
"username": "username",
}
}
/>
}
section="renderEmoticonsAsEmoji"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Render emoticons as emojis"
id="user.settings.display.renderEmoticonsAsEmojiTitle"
/>
}
updateSection={[Function]}
/>
<div
className="divider-dark"
/>
</div>
<div>
<Memo(SettingItem)
active={false}

Просмотреть файл

@@ -86,6 +86,7 @@ export function makeMapStateToProps() {
clickToReply: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CLICK_TO_REPLY, Preferences.CLICK_TO_REPLY_DEFAULT, userPreference),
linkPreviewDisplay: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.LINK_PREVIEW_DISPLAY, Preferences.LINK_PREVIEW_DISPLAY_DEFAULT, userPreference),
oneClickReactionsOnPosts: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.ONE_CLICK_REACTIONS_ENABLED, Preferences.ONE_CLICK_REACTIONS_ENABLED_DEFAULT, userPreference),
renderEmoticonsAsEmoji: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.RENDER_EMOTICONS_AS_EMOJI, Preferences.RENDER_EMOTICONS_AS_EMOJI_DEFAULT, userPreference),
emojiPickerEnabled,
lastActiveDisplay,
lastActiveTimeEnabled,

Просмотреть файл

@@ -0,0 +1,92 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/user_settings/display/render_emoticons_as_emoji/render_emoticons_as_emoji should match snapshot 1`] = `
<div>
<section
class="section-max form-horizontal "
>
<h4
class="col-sm-12 section-title"
id="settingTitle"
>
Render emoticons as emojis
</h4>
<div
class="sectionContent col-sm-10 col-sm-offset-2"
>
<div
class="setting-list"
tabindex="-1"
>
<div
class="setting-list-item"
>
<fieldset>
<legend
class="form-legend hidden-label"
>
Render emoticons as emojis
</legend>
<div
class="radio"
>
<label>
<input
checked=""
class="a11y--active a11y--focused"
id="renderEmoticonsAsEmojiOn"
name="renderEmoticonsAsEmoji"
type="radio"
value="true"
/>
On
</label>
<br />
</div>
<div
class="radio"
>
<label>
<input
id="renderEmoticonsAsEmojiOff"
name="renderEmoticonsAsEmoji"
type="radio"
value="false"
/>
Off
</label>
<br />
</div>
<div
class="mt-5"
>
When enabled, text emoticons in messages will be rendered as emojis (For example :D as 😄)
</div>
</fieldset>
</div>
<div
class="setting-list-item"
>
<hr />
<button
class="btn btn-primary "
data-testid="saveSetting"
id="saveSetting"
type="submit"
>
<span>
Save
</span>
</button>
<button
class="btn btn-tertiary"
id="cancelSetting"
>
Cancel
</button>
</div>
</div>
</div>
</section>
</div>
`;

Просмотреть файл

@@ -0,0 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import type {Dispatch} from 'redux';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import RenderEmoticonsAsEmoji from './render_emoticons_as_emoji';
function mapDispatchToProps(dispatch: Dispatch) {
return {
actions: bindActionCreators({
savePreferences,
}, dispatch),
};
}
export default connect(null, mapDispatchToProps)(RenderEmoticonsAsEmoji);

Просмотреть файл

@@ -0,0 +1,56 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import type {UserProfile} from '@mattermost/types/users';
import {renderWithContext, userEvent} from 'tests/react_testing_utils';
import RenderEmoticonsAsEmoji from './render_emoticons_as_emoji';
describe('components/user_settings/display/render_emoticons_as_emoji/render_emoticons_as_emoji', () => {
const user = {
id: 'user_id',
username: 'username',
locale: 'en',
timezone: {
useAutomaticTimezone: 'true',
automaticTimezone: 'America/New_York',
manualTimezone: '',
},
};
const props = {
user: user as UserProfile,
renderEmoticonsAsEmoji: 'true',
updateSection: jest.fn(),
adminMode: false,
userPreferences: undefined,
actions: {
savePreferences: jest.fn(() => {
return new Promise<void>((resolve) => {
process.nextTick(() => resolve());
});
})},
};
test('should match snapshot', () => {
const {container} = renderWithContext(<RenderEmoticonsAsEmoji {...props}/>);
expect(container).toMatchSnapshot();
});
test('should call updateSection on submit', async () => {
const {getByRole, getByLabelText} = renderWithContext(<RenderEmoticonsAsEmoji {...props}/>);
const radioButtonOff = getByLabelText(/off/i);
userEvent.click(radioButtonOff);
const submitButton = getByRole('button', {name: /save/i});
userEvent.click(submitButton);
expect(props.actions.savePreferences).toHaveBeenCalled();
expect(props.updateSection).toHaveBeenCalledWith('');
});
});

Просмотреть файл

@@ -0,0 +1,133 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useState} from 'react';
import {FormattedMessage} from 'react-intl';
import type {PreferencesType, PreferenceType} from '@mattermost/types/preferences';
import type {UserProfile} from '@mattermost/types/users';
import {Preferences} from 'mattermost-redux/constants';
import SettingItemMax from 'components/setting_item_max';
export type Actions = {
savePreferences: (userId: string, preferences: PreferenceType[]) => void;
};
export type OwnProps = {
user: UserProfile;
updateSection: (section: string) => void;
adminMode?: boolean;
userPreferences?: PreferencesType;
}
type Props = OwnProps & {
renderEmoticonsAsEmoji: string;
actions: Actions;
}
const RenderEmoticonsAsEmoji: React.FC<Props> = ({user, renderEmoticonsAsEmoji, updateSection, actions}) => {
const [value, setValue] = useState<string>(renderEmoticonsAsEmoji);
const [isSaving, setIsSaving] = useState<boolean>(false);
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.currentTarget.value);
}, []);
const submitPreference = useCallback(() => {
setIsSaving(true);
const pref: PreferenceType = {
user_id: user.id,
category: Preferences.CATEGORY_DISPLAY_SETTINGS,
name: Preferences.RENDER_EMOTICONS_AS_EMOJI,
value,
};
actions.savePreferences(user.id, [pref]);
setIsSaving(false);
updateSection('');
}, [user.id, updateSection, actions, value]);
const changePreference = useCallback(() => {
if (value === renderEmoticonsAsEmoji) {
updateSection('');
return;
}
submitPreference();
}, [renderEmoticonsAsEmoji, updateSection, value, submitPreference]);
const options = [
{
option: 'true',
inputId: 'renderEmoticonsAsEmojiOn',
messageId: 'user.settings.advance.on',
defaultMessage: 'On',
},
{
option: 'false',
inputId: 'renderEmoticonsAsEmojiOff',
messageId: 'user.settings.advance.off',
defaultMessage: 'Off',
},
];
const input = (
<fieldset key='renderEmoticonsAsEmojiSetting'>
<legend className='form-legend hidden-label'>
<FormattedMessage
id='user.settings.display.renderEmoticonsAsEmojiTitle'
defaultMessage='Render emoticons as emojis'
/>
</legend>
{options.map(({option, inputId, messageId, defaultMessage}) => {
return (
<div
className='radio'
key={option}
>
<label>
<input
id={inputId}
type='radio'
name='renderEmoticonsAsEmoji'
value={option}
checked={value === option}
onChange={handleChange}
/>
<FormattedMessage
id={messageId}
defaultMessage={defaultMessage}
/>
</label>
<br/>
</div>
);
})}
<div className='mt-5'>
<FormattedMessage
id='user.settings.display.renderEmoticonsAsEmojiDesc'
defaultMessage='When enabled, text emoticons in messages will be rendered as emojis (For example :D as 😄)'
/>
</div>
</fieldset>
);
return (
<SettingItemMax
title={
<FormattedMessage
id='user.settings.display.renderEmoticonsAsEmojiTitle'
defaultMessage='Render emoticons as emojis'
/>
}
inputs={[input]}
submit={changePreference}
saving={isSaving}
updateSection={updateSection}
disableEnterSubmit={true}
/>
);
};
export default RenderEmoticonsAsEmoji;

Просмотреть файл

@@ -94,6 +94,7 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
globalHeaderAllowed: true,
lastActiveDisplay: true,
oneClickReactionsOnPosts: '',
renderEmoticonsAsEmoji: '',
emojiPickerEnabled: true,
clickToReply: '',
lastActiveTimeEnabled: true,

Просмотреть файл

@@ -28,6 +28,7 @@ import {a11yFocus} from 'utils/utils';
import ManageLanguages from './manage_languages';
import ManageTimezones from './manage_timezones';
import RenderEmoticonsAsEmoji from './render_emoticons_as_emoji';
import SettingDesktopHeader from '../headers/setting_desktop_header';
import SettingMobileHeader from '../headers/setting_mobile_header';
@@ -120,6 +121,7 @@ type Props = OwnProps & {
timezoneLabel: string;
lastActiveDisplay: boolean;
lastActiveTimeEnabled: boolean;
renderEmoticonsAsEmoji: string;
actions: {
savePreferences: (userId: string, preferences: PreferenceType[]) => void;
autoUpdateTimezone: (deviceTimezone: string) => void;
@@ -1147,6 +1149,46 @@ export default class UserSettingsDisplay extends React.PureComponent<Props, Stat
});
}
const renderEmoticonsAsEmojiSection = (
<div>
<SettingItem
active={this.props.activeSection === 'renderEmoticonsAsEmoji'}
areAllSectionsInactive={this.props.activeSection === ''}
title={
<FormattedMessage
id='user.settings.display.renderEmoticonsAsEmojiTitle'
defaultMessage='Render emoticons as emojis'
/>
}
describe={
this.props.renderEmoticonsAsEmoji === 'true' ? (
<FormattedMessage
id='user.settings.advance.on'
defaultMessage='On'
/>
) : (
<FormattedMessage
id='user.settings.advance.off'
defaultMessage='Off'
/>
)
}
section='renderEmoticonsAsEmoji'
updateSection={this.updateSection}
max={(
<RenderEmoticonsAsEmoji
renderEmoticonsAsEmoji={this.props.renderEmoticonsAsEmoji}
user={this.props.user}
updateSection={this.updateSection}
adminMode={this.props.adminMode}
userPreferences={this.props.userPreferences}
/>
)}
/>
<div className='divider-dark'/>
</div>
);
return (
<div
id='displaySettings'
@@ -1187,6 +1229,7 @@ export default class UserSettingsDisplay extends React.PureComponent<Props, Stat
{clickToReply}
{channelDisplayModeSection}
{oneClickReactionsOnPostsSection}
{renderEmoticonsAsEmojiSection}
{languagesSection}
</div>
</div>

Просмотреть файл

@@ -5903,6 +5903,8 @@
"user.settings.display.oneClickReactionsOnPostsDescription": "When enabled, you can react quickly with recently used reactions when hovering over a message.",
"user.settings.display.oneClickReactionsOnPostsTitle": "Quick reactions on messages",
"user.settings.display.preferTime": "Select how you prefer time displayed.",
"user.settings.display.renderEmoticonsAsEmojiDesc": "When enabled, text emoticons in messages will automatically be rendered as emojis (For example :D as 😄)",
"user.settings.display.renderEmoticonsAsEmojiTitle": "Render emoticons as emojis",
"user.settings.display.teammateNameDisplay": "This field is handled through your System Administrator. If you want to change it, you need to do so through your System Administrator.",
"user.settings.display.teammateNameDisplayDescription": "Set how to display other user's names in posts and the Direct Messages list.",
"user.settings.display.teammateNameDisplayFullname": "Show first and last name",

Просмотреть файл

@@ -48,6 +48,7 @@ const Preferences = {
SHOW_UNREAD_SECTION: 'show_unread_section',
CATEGORY_ADVANCED_SETTINGS: 'advanced_settings',
ADVANCED_FILTER_JOIN_LEAVE: 'join_leave',
RENDER_EMOTICONS_AS_EMOJI: 'render_emoticons_as_emoji',
ADVANCED_CODE_BLOCK_ON_CTRL_ENTER: 'code_block_ctrl_enter',
ADVANCED_SEND_ON_CTRL_ENTER: 'send_on_ctrl_enter',
ADVANCED_SYNC_DRAFTS: 'sync_drafts',

Просмотреть файл

@@ -91,6 +91,8 @@ export const Preferences = {
CLICK_TO_REPLY: 'click_to_reply',
CLICK_TO_REPLY_DEFAULT: 'true',
COLLAPSED_REPLY_THREADS_FALLBACK_DEFAULT: 'off',
RENDER_EMOTICONS_AS_EMOJI: 'render_emoticons_as_emoji',
RENDER_EMOTICONS_AS_EMOJI_DEFAULT: 'true',
LINK_PREVIEW_DISPLAY: 'link_previews',
LINK_PREVIEW_DISPLAY_DEFAULT: 'true',
COLLAPSE_DISPLAY: 'collapse_previews',

Просмотреть файл

@@ -92,6 +92,11 @@ describe('Emoticons', () => {
expect(Emoticons.handleEmoticons('":P"', new Map())).
toEqual('"$MM_EMOTICON0$"');
});
test('shouldn\'t render text-based emoticons as emoji when renderEmoticonsAsEmoji is false', () => {
expect(Emoticons.handleEmoticons('":P"', new Map(), false)).
toEqual('":P"');
});
});
describe('matchEmoticons', () => {

Просмотреть файл

@@ -50,6 +50,7 @@ export function matchEmoticons(text: string): string[] | null {
export function handleEmoticons(
text: string,
tokens: Map<string, {value: string; originalText: string}>,
renderEmoticonsAsEmoji = true,
): string {
let output = text;
@@ -85,12 +86,14 @@ export function handleEmoticons(
);
// match text smilies like :D
for (const name of Object.keys(emoticonPatterns)) {
const pattern = emoticonPatterns[name];
if (renderEmoticonsAsEmoji) {
for (const name of Object.keys(emoticonPatterns)) {
const pattern = emoticonPatterns[name];
// this might look a bit funny, but since the name isn't contained in the actual match
// like with the named emoticons, we need to add it in manually
output = output.replace(pattern, (fullMatch, prefix, matchText) => replaceEmoticonWithToken(fullMatch, prefix, matchText, name));
// this might look a bit funny, but since the name isn't contained in the actual match
// like with the named emoticons, we need to add it in manually
output = output.replace(pattern, (fullMatch, prefix, matchText) => replaceEmoticonWithToken(fullMatch, prefix, matchText, name));
}
}
return output;

Просмотреть файл

@@ -90,6 +90,11 @@ describe('formatText', () => {
}
});
test('emoticons should not be rendered as emojis when renderEmoticonsAsEmoji is false', () => {
const output = formatText(':)', {renderEmoticonsAsEmoji: false}, emptyEmojiMap);
expect(output).toBe('<p>:)</p>');
});
test('code blocks newlines are not converted into <br/> with inline markdown image in the post', () => {
const output = formatText('```\nsome text\nsecond line\n```\n ![](https://example.com/image.png)', {}, emptyEmojiMap);
expect(output).not.toContain('<br/>');

Просмотреть файл

@@ -230,6 +230,11 @@ export interface TextFormattingOptionsBase {
* Defaults to `false`.
*/
unsafeLinks: boolean;
/**
* Whether or not to render text emoticons (:D) as emojis
*/
renderEmoticonsAsEmoji: boolean;
}
export type TextFormattingOptions = Partial<TextFormattingOptionsBase>;
@@ -258,6 +263,7 @@ const DEFAULT_OPTIONS: TextFormattingOptions = {
editedAt: 0,
postId: '',
unsafeLinks: false,
renderEmoticonsAsEmoji: true,
};
/**
@@ -401,7 +407,7 @@ export function doFormatText(text: string, options: TextFormattingOptions, emoji
output = autolinkHashtags(output, tokens, options.minimumHashtagLength);
if (!('emoticons' in options) || options.emoticons) {
output = Emoticons.handleEmoticons(output, tokens);
output = Emoticons.handleEmoticons(output, tokens, options.renderEmoticonsAsEmoji);
}
if (options.searchPatterns) {