diff --git a/app/components/EditedIndicator/index.test.tsx b/app/components/EditedIndicator/index.test.tsx
new file mode 100644
index 000000000..393019bfa
--- /dev/null
+++ b/app/components/EditedIndicator/index.test.tsx
@@ -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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ expect(getByTestId('custom-test-id')).toBeTruthy();
+ });
+
+ describe('component structure', () => {
+ it('should render pencil-outline icon', () => {
+ const {getByTestId} = renderWithIntlAndTheme(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ const indicator = getByTestId('edited-indicator-test');
+ expect(indicator.props.children[0]).toBe('');
+ });
+
+ it('should handle undefined context elements', () => {
+ const {getByTestId} = renderWithIntlAndTheme(
+ ,
+ );
+
+ const indicator = getByTestId('edited-indicator-test');
+ expect(indicator.props.children[0]).toBe('');
+ });
+
+ it('should handle context with multiple elements', () => {
+ const {getByTestId} = renderWithIntlAndTheme(
+ ,
+ );
+
+ const indicator = getByTestId('edited-indicator-test');
+ expect(indicator.props.children[0]).toBe(' ');
+ });
+ });
+});
diff --git a/app/components/EditedIndicator/index.tsx b/app/components/EditedIndicator/index.tsx
new file mode 100644
index 000000000..25a9c1e6e
--- /dev/null
+++ b/app/components/EditedIndicator/index.tsx
@@ -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;
+ 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 (
+
+ {spacer}
+
+
+
+ );
+};
+
+export default EditedIndicator;
diff --git a/app/components/jumbo_emoji/index.tsx b/app/components/jumbo_emoji/index.tsx
index 53dec971d..7488c7271 100644
--- a/app/components/jumbo_emoji/index.tsx
+++ b/app/components/jumbo_emoji/index.tsx
@@ -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;
@@ -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 (
-
- {spacer}
-
-
+ />
);
};
diff --git a/app/components/markdown/markdown.tsx b/app/components/markdown/markdown.tsx
index 38f23a527..f39473a74 100644
--- a/app/components/markdown/markdown.tsx
+++ b/app/components/markdown/markdown.tsx
@@ -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 (
-
- {spacer}
-
-
+ />
);
};
@@ -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);
diff --git a/app/components/post_list/post/body/index.tsx b/app/components/post_list/post/body/index.tsx
index fa9679dd5..df41b6e7d 100644
--- a/app/components/post_list/post/body/index.tsx
+++ b/app/components/post_list/post/body/index.tsx
@@ -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 = (