Changed the Edited text style in the post from (edited) to icon + Edited (#8937)

* Viewing Files in Edit mode in mobile with ability to delete and save

* Added upload attachment to keyboard tracker view

* using state instread of use ref

* Minor

* Added tests

* intl extract

* new function getFiles
ById, batch file deletion and tests

* Files fetching in edit options and tests

* Removed DeviceEventEmitter and used React context

* Added support to check minimum required version to show edit file attachments

* resolve forward ref issue

* Minor

* memotized props for context and observe config with value

* Ability to show quick action and add files to edit post

* type safety for EditPostContext

* Reverted back the post priority props

* constant shift

* Added test for QuickAction to show slashcomand

* Added test for edit_post, upload_item and upload_remove

* Added test for Edit_post_input and edit_post index

* fix the height issue between attachment and keyboard due to safeArea

* Minor: removed debugging border color

* Changed the Edited text style in the post from (edited) to icon + Edited

* Import fixes

* Ability to show quick action and add files to edit post

* type safety for EditPostContext

* Reverted back the post priority props

* constant shift

* Added test for QuickAction to show slashcomand

* Added test for edit_post, upload_item and upload_remove

* Added test for Edit_post_input and edit_post index

* fix the height issue between attachment and keyboard due to safeArea

* Minor: removed debugging border color

* Addressed dev review comments

* Import fixes

* Address UX comments

* Fixed props for UploadItem and remove effective Edit mode

* handled save button disabled state when uploading attachments

* handled newly added and retry file removal without alert message

* Test updated

* Added test for input_quick_action index.tsx

* Added test for not in edit mode for upload_item index

* Added test for upload_remove component when not in edit mode

* added tests for file_upload_error hook

* Added test for calling callback when in edit mode

* Test for edit post input for server version check

* linter fixes

* Changed font size from 16 to 14px for edited

* separated common component and test

* Removed the margin styles from the text as it is not been applied.

* removed the duplicate code from rebase
This commit is contained in:
Rajat Dabade 2025-06-29 12:39:18 +05:30 committed by GitHub
parent a204494e41
commit 5cceaee7c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 247 additions and 89 deletions

View file

@ -0,0 +1,128 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Preferences} from '@constants';
import {renderWithIntlAndTheme} from '@test/intl-test-helper';
import EditedIndicator from './index';
describe('components/EditedIndicator', () => {
const baseProps = {
baseTextStyle: {fontSize: 16, color: '#000'},
theme: Preferences.THEMES.denim,
context: ['paragraph'],
testID: 'edited-indicator-test',
};
it('should render with default props', () => {
const {getByTestId} = renderWithIntlAndTheme(
<EditedIndicator {...baseProps}/>,
);
const indicator = getByTestId('edited-indicator-test');
expect(indicator).toBeTruthy();
expect(indicator.props.children).toEqual([
' ', // spacer for paragraph context
expect.objectContaining({
props: expect.objectContaining({
name: 'pencil-outline',
size: 14, // default icon size
}),
}),
expect.objectContaining({
props: expect.objectContaining({
id: 'post_message_view.edited',
defaultMessage: 'Edited',
}),
}),
]);
});
it('should render with custom icon size', () => {
const {getByTestId} = renderWithIntlAndTheme(
<EditedIndicator
{...baseProps}
iconSize={20}
/>,
);
const indicator = getByTestId('edited-indicator-test');
const icon = indicator.props.children[1];
expect(icon.props.size).toBe(20);
});
it('should render with custom testID', () => {
const {getByTestId} = renderWithIntlAndTheme(
<EditedIndicator
{...baseProps}
testID='custom-test-id'
/>,
);
expect(getByTestId('custom-test-id')).toBeTruthy();
});
describe('component structure', () => {
it('should render pencil-outline icon', () => {
const {getByTestId} = renderWithIntlAndTheme(
<EditedIndicator {...baseProps}/>,
);
const indicator = getByTestId('edited-indicator-test');
const icon = indicator.props.children[1];
expect(icon.props.name).toBe('pencil-outline');
});
it('should render "Edited" text with correct props', () => {
const {getByTestId} = renderWithIntlAndTheme(
<EditedIndicator {...baseProps}/>,
);
const indicator = getByTestId('edited-indicator-test');
const formattedText = indicator.props.children[2];
expect(formattedText.props.id).toBe('post_message_view.edited');
expect(formattedText.props.defaultMessage).toBe('Edited');
});
});
describe('edge cases', () => {
it('should handle empty context array', () => {
const {getByTestId} = renderWithIntlAndTheme(
<EditedIndicator
{...baseProps}
context={[]}
/>,
);
const indicator = getByTestId('edited-indicator-test');
expect(indicator.props.children[0]).toBe('');
});
it('should handle undefined context elements', () => {
const {getByTestId} = renderWithIntlAndTheme(
<EditedIndicator
{...baseProps}
context={[undefined] as any}
checkHeadings={true}
/>,
);
const indicator = getByTestId('edited-indicator-test');
expect(indicator.props.children[0]).toBe('');
});
it('should handle context with multiple elements', () => {
const {getByTestId} = renderWithIntlAndTheme(
<EditedIndicator
{...baseProps}
context={['paragraph', 'strong', 'em']}
/>,
);
const indicator = getByTestId('edited-indicator-test');
expect(indicator.props.children[0]).toBe(' ');
});
});
});

View file

@ -0,0 +1,83 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Platform, type StyleProp, Text, type TextStyle} from 'react-native';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import {blendColors, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
type EditedIndicatorProps = {
baseTextStyle: StyleProp<TextStyle>;
theme: Theme;
context: string[];
iconSize?: number;
checkHeadings?: boolean;
testID?: string;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
// Android has trouble giving text transparency depending on how it's nested,
// so we calculate the resulting colour manually
const editedOpacity = Platform.select({
ios: 0.56,
android: 1.0,
});
const editedColor = Platform.select({
ios: theme.centerChannelColor,
android: blendColors(theme.centerChannelBg, theme.centerChannelColor, 0.3),
});
return {
editedIndicatorText: {
color: editedColor,
opacity: editedOpacity,
fontStyle: 'italic',
...typography('Body', 25, 'Regular'),
},
editedText: {
...typography('Body', 100, 'Regular'),
},
};
});
const EditedIndicator = ({
baseTextStyle,
theme,
context,
iconSize = 14,
checkHeadings = false,
testID = 'edited_indicator',
}: EditedIndicatorProps) => {
const style = getStyleSheet(theme);
let spacer = '';
const styles = [baseTextStyle, style.editedIndicatorText];
// Add space for paragraphs, and optionally for headings
if (context[0] === 'paragraph' || (checkHeadings && context[0]?.startsWith('heading'))) {
spacer = ' ';
}
return (
<Text
style={styles}
testID={testID}
>
{spacer}
<CompassIcon
name='pencil-outline'
size={iconSize}
color={theme.centerChannelColor}
/>
<FormattedText
id='post_message_view.edited'
defaultMessage='Edited'
style={style.editedText}
/>
</Text>
);
};
export default EditedIndicator;

View file

@ -4,12 +4,11 @@
import {Node, Parser} from 'commonmark';
import Renderer from 'commonmark-react-renderer';
import React, {type ReactElement, useRef} from 'react';
import {Platform, type StyleProp, Text, type TextStyle, View} from 'react-native';
import {type StyleProp, StyleSheet, Text, type TextStyle, View} from 'react-native';
import EditedIndicator from '@components/EditedIndicator';
import Emoji from '@components/emoji';
import FormattedText from '@components/formatted_text';
import {useTheme} from '@context/theme';
import {blendColors, makeStyleSheetFromTheme} from '@utils/theme';
type JumboEmojiProps = {
baseTextStyle: StyleProp<TextStyle>;
@ -17,41 +16,23 @@ type JumboEmojiProps = {
value: string;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
// Android has trouble giving text transparency depending on how it's nested,
// so we calculate the resulting colour manually
const editedOpacity = Platform.select({
ios: 0.3,
android: 1.0,
});
const editedColor = Platform.select({
ios: theme.centerChannelColor,
android: blendColors(theme.centerChannelBg, theme.centerChannelColor, 0.3),
});
return {
block: {
alignItems: 'flex-start',
flexDirection: 'row',
flexWrap: 'wrap',
},
editedIndicatorText: {
color: editedColor,
opacity: editedOpacity,
},
jumboEmoji: {
fontSize: 50,
lineHeight: 60,
},
newLine: {
lineHeight: 60,
},
};
const style = StyleSheet.create({
block: {
alignItems: 'flex-start',
flexDirection: 'row',
flexWrap: 'wrap',
},
jumboEmoji: {
fontSize: 50,
lineHeight: 60,
},
newLine: {
lineHeight: 60,
},
});
const JumboEmoji = ({baseTextStyle, isEdited, value}: JumboEmojiProps) => {
const theme = useTheme();
const style = getStyleSheet(theme);
const renderEmoji = ({emojiName, literal}: {context: string[]; emojiName: string; literal: string}) => {
return (
@ -81,27 +62,15 @@ const JumboEmoji = ({baseTextStyle, isEdited, value}: JumboEmojiProps) => {
};
const renderEditedIndicator = ({context}: {context: string[]}) => {
let spacer = '';
if (context[0] === 'paragraph') {
spacer = ' ';
}
const styles = [
baseTextStyle,
style.editedIndicatorText,
];
return (
<Text
style={styles}
<EditedIndicator
baseTextStyle={baseTextStyle}
theme={theme}
context={context}
iconSize={14}
checkHeadings={false}
testID='edited_indicator'
>
{spacer}
<FormattedText
id='post_message_view.edited'
defaultMessage='(edited)'
/>
</Text>
/>
);
};

View file

@ -1,18 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable max-lines */
import {useManagedConfig} from '@mattermost/react-native-emm';
import {Parser, Node} from 'commonmark';
import Renderer from 'commonmark-react-renderer';
import React, {type ReactElement, useMemo, useRef} from 'react';
import {Dimensions, type GestureResponderEvent, Platform, type StyleProp, StyleSheet, Text, type TextStyle, View, type ViewStyle} from 'react-native';
import {Dimensions, type GestureResponderEvent, type StyleProp, StyleSheet, Text, type TextStyle, View, type ViewStyle} from 'react-native';
import CompassIcon from '@components/compass_icon';
import EditedIndicator from '@components/EditedIndicator';
import Emoji from '@components/emoji';
import FormattedText from '@components/formatted_text';
import {logError} from '@utils/log';
import {computeTextStyle} from '@utils/markdown';
import {blendColors, changeOpacity, concatStyles, makeStyleSheetFromTheme} from '@utils/theme';
import {changeOpacity, concatStyles, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {getScheme} from '@utils/url';
@ -81,27 +83,12 @@ type MarkdownProps = {
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
// Android has trouble giving text transparency depending on how it's nested,
// so we calculate the resulting colour manually
const editedOpacity = Platform.select({
ios: 0.3,
android: 1.0,
});
const editedColor = Platform.select({
ios: theme.centerChannelColor,
android: blendColors(theme.centerChannelBg, theme.centerChannelColor, 0.3),
});
return {
block: {
alignItems: 'flex-start',
flexDirection: 'row',
flexWrap: 'wrap',
},
editedIndicatorText: {
color: editedColor,
opacity: editedOpacity,
},
errorMessage: {
color: theme.errorTextColor,
...typography('Body', 100),
@ -262,24 +249,15 @@ const Markdown = ({
};
const renderEditedIndicator = ({context}: {context: string[]}) => {
let spacer = '';
const styles = [baseTextStyle, style.editedIndicatorText];
if (context[0] === 'paragraph') {
spacer = ' ';
}
return (
<Text
style={styles}
<EditedIndicator
baseTextStyle={baseTextStyle}
theme={theme}
context={context}
iconSize={14}
checkHeadings={true}
testID='edited_indicator'
>
{spacer}
<FormattedText
id='post_message_view.edited'
defaultMessage='(edited)'
/>
</Text>
/>
);
};
@ -638,7 +616,7 @@ const Markdown = ({
if (isEdited) {
const editIndicatorNode = new Node('edited_indicator');
if (ast.lastChild && ['heading', 'paragraph'].includes(ast.lastChild.type)) {
ast.appendChild(editIndicatorNode);
ast.lastChild.appendChild(editIndicatorNode);
} else {
const node = new Node('paragraph');
node.appendChild(editIndicatorNode);

View file

@ -155,7 +155,7 @@ const Body = ({
value={post.message}
/>
);
} else if (post.message.length) {
} else if (post.message.length || isEdited) { // isEdited is added to handle the case where the post is edited and the message is empty
message = (
<Message
highlight={highlight}

View file

@ -1002,7 +1002,7 @@
"post_info.edit": "Edit",
"post_info.guest": "Guest",
"post_info.system": "System",
"post_message_view.edited": "(edited)",
"post_message_view.edited": "Edited",
"post_priority.button.acknowledge": "Acknowledge",
"post_priority.label.important": "IMPORTANT",
"post_priority.label.urgent": "URGENT",