diff --git a/native/chat/text-message-tooltip-modal.react.js b/native/chat/text-message-tooltip-modal.react.js
index 6fb9a9ade..8a082e29e 100644
--- a/native/chat/text-message-tooltip-modal.react.js
+++ b/native/chat/text-message-tooltip-modal.react.js
@@ -1,113 +1,133 @@
// @flow
import Clipboard from '@react-native-clipboard/clipboard';
import invariant from 'invariant';
import * as React from 'react';
import { createMessageReply } from 'lib/shared/message-utils.js';
import { useOnPressReport } from './message-report-utils.js';
import { useAnimatedNavigateToSidebar } from './sidebar-navigation.js';
import TextMessageTooltipButton from './text-message-tooltip-button.react.js';
import CommIcon from '../components/comm-icon.react.js';
import SWMansionIcon from '../components/swmansion-icon.react.js';
import { InputStateContext } from '../input/input-state.js';
import { displayActionResultModal } from '../navigation/action-result-modal.js';
import {
createTooltip,
type TooltipParams,
type BaseTooltipProps,
type TooltipMenuProps,
} from '../tooltip/tooltip.react.js';
import type { ChatTextMessageInfoItemWithHeight } from '../types/chat-types.js';
export type TextMessageTooltipModalParams = TooltipParams<{
+item: ChatTextMessageInfoItemWithHeight,
}>;
const confirmCopy = () => displayActionResultModal('copied!');
function TooltipMenu(
props: TooltipMenuProps<'TextMessageTooltipModal'>,
): React.Node {
const { route, tooltipItem: TooltipItem } = props;
const inputState = React.useContext(InputStateContext);
const { text } = route.params.item.messageInfo;
const onPressReply = React.useCallback(() => {
invariant(
inputState,
'inputState should be set in TextMessageTooltipModal.onPressReply',
);
inputState.addReply(createMessageReply(text));
}, [inputState, text]);
const renderReplyIcon = React.useCallback(
style => ,
[],
);
const onPressSidebar = useAnimatedNavigateToSidebar(route.params.item);
const renderSidebarIcon = React.useCallback(
style => (
),
[],
);
+ const { messageInfo } = route.params.item;
+ const onPressEdit = React.useCallback(() => {
+ invariant(
+ inputState,
+ 'inputState should be set in TextMessageTooltipModal.onPressEdit',
+ );
+ inputState.setEditedMessageID(messageInfo.id);
+ }, [inputState, messageInfo.id]);
+ const renderEditIcon = React.useCallback(
+ style => ,
+ [],
+ );
+
const onPressCopy = React.useCallback(() => {
Clipboard.setString(text);
setTimeout(confirmCopy);
}, [text]);
const renderCopyIcon = React.useCallback(
style => ,
[],
);
const onPressReport = useOnPressReport(route);
const renderReportIcon = React.useCallback(
style => ,
[],
);
return (
<>
+
>
);
}
const TextMessageTooltipModal: React.ComponentType<
BaseTooltipProps<'TextMessageTooltipModal'>,
> = createTooltip<'TextMessageTooltipModal'>(
TextMessageTooltipButton,
TooltipMenu,
);
export default TextMessageTooltipModal;
diff --git a/native/chat/text-message.react.js b/native/chat/text-message.react.js
index 01c5244c5..59047f768 100644
--- a/native/chat/text-message.react.js
+++ b/native/chat/text-message.react.js
@@ -1,255 +1,274 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { View } from 'react-native';
+import { useCanEditMessage } from 'lib/shared/edit-messages-utils.js';
import { messageKey } from 'lib/shared/message-utils.js';
import {
threadHasPermission,
useCanCreateSidebarFromMessage,
} from 'lib/shared/thread-utils.js';
import { threadPermissions } from 'lib/types/thread-types.js';
import type { ChatNavigationProp } from './chat.react.js';
import ComposedMessage from './composed-message.react.js';
import { InnerTextMessage } from './inner-text-message.react.js';
import {
MessagePressResponderContext,
type MessagePressResponderContextType,
} from './message-press-responder-context.js';
import textMessageSendFailed from './text-message-send-failed.js';
import { getMessageTooltipKey } from './utils.js';
import { ChatContext, type ChatContextType } from '../chat/chat-context.js';
import { MarkdownContext } from '../markdown/markdown-context.js';
import {
OverlayContext,
type OverlayContextType,
} from '../navigation/overlay-context.js';
import type { NavigationRoute } from '../navigation/route-names.js';
import { TextMessageTooltipModalRouteName } from '../navigation/route-names.js';
import { fixedTooltipHeight } from '../tooltip/tooltip.react.js';
import type { ChatTextMessageInfoItemWithHeight } from '../types/chat-types.js';
import type { VerticalBounds } from '../types/layout-types.js';
+import { useShouldRenderEditButton } from '../utils/edit-messages-utils.js';
type BaseProps = {
...React.ElementConfig,
+item: ChatTextMessageInfoItemWithHeight,
+navigation: ChatNavigationProp<'MessageList'>,
+route: NavigationRoute<'MessageList'>,
+focused: boolean,
+toggleFocus: (messageKey: string) => void,
+verticalBounds: ?VerticalBounds,
};
type Props = {
...BaseProps,
// Redux state
+canCreateSidebarFromMessage: boolean,
// withOverlayContext
+overlayContext: ?OverlayContextType,
// ChatContext
+chatContext: ?ChatContextType,
// MarkdownContext
+isLinkModalActive: boolean,
+ +canEditMessage: boolean,
+ +shouldRenderEditButton: boolean,
};
class TextMessage extends React.PureComponent {
message: ?React.ElementRef;
messagePressResponderContext: MessagePressResponderContextType;
constructor(props: Props) {
super(props);
this.messagePressResponderContext = {
onPressMessage: this.onPress,
};
}
render() {
const {
item,
navigation,
route,
focused,
toggleFocus,
verticalBounds,
overlayContext,
chatContext,
isLinkModalActive,
canCreateSidebarFromMessage,
+ canEditMessage,
+ shouldRenderEditButton,
...viewProps
} = this.props;
let swipeOptions = 'none';
const canReply = this.canReply();
const canNavigateToSidebar = this.canNavigateToSidebar();
if (isLinkModalActive) {
swipeOptions = 'none';
} else if (canReply && canNavigateToSidebar) {
swipeOptions = 'both';
} else if (canReply) {
swipeOptions = 'reply';
} else if (canNavigateToSidebar) {
swipeOptions = 'sidebar';
}
return (
);
}
messageRef = (message: ?React.ElementRef) => {
this.message = message;
};
canReply() {
return threadHasPermission(
this.props.item.threadInfo,
threadPermissions.VOICED,
);
}
canNavigateToSidebar() {
return (
this.props.item.threadCreatedFromMessage ||
this.props.canCreateSidebarFromMessage
);
}
visibleEntryIDs() {
const result = ['copy'];
if (this.canReply()) {
result.push('reply');
}
+ if (this.props.canEditMessage && this.props.shouldRenderEditButton) {
+ result.push('edit');
+ }
+
if (
this.props.item.threadCreatedFromMessage ||
this.props.canCreateSidebarFromMessage
) {
result.push('sidebar');
}
if (!this.props.item.messageInfo.creator.isViewer) {
result.push('report');
}
return result;
}
onPress = () => {
const visibleEntryIDs = this.visibleEntryIDs();
if (visibleEntryIDs.length === 0) {
return;
}
const {
message,
props: { verticalBounds, isLinkModalActive },
} = this;
if (!message || !verticalBounds || isLinkModalActive) {
return;
}
const { focused, toggleFocus, item } = this.props;
if (!focused) {
toggleFocus(messageKey(item.messageInfo));
}
const { overlayContext } = this.props;
invariant(overlayContext, 'TextMessage should have OverlayContext');
overlayContext.setScrollBlockingModalStatus('open');
message.measure((x, y, width, height, pageX, pageY) => {
const coordinates = { x: pageX, y: pageY, width, height };
const messageTop = pageY;
const messageBottom = pageY + height;
const boundsTop = verticalBounds.y;
const boundsBottom = verticalBounds.y + verticalBounds.height;
const belowMargin = 20;
const belowSpace = fixedTooltipHeight + belowMargin;
const { isViewer } = item.messageInfo.creator;
const aboveMargin = isViewer ? 30 : 50;
const aboveSpace = fixedTooltipHeight + aboveMargin;
let margin = belowMargin;
if (
messageBottom + belowSpace > boundsBottom &&
messageTop - aboveSpace > boundsTop
) {
margin = aboveMargin;
}
const currentInputBarHeight =
this.props.chatContext?.chatInputBarHeights.get(item.threadInfo.id) ??
0;
this.props.navigation.navigate<'TextMessageTooltipModal'>({
name: TextMessageTooltipModalRouteName,
params: {
presentedFrom: this.props.route.key,
initialCoordinates: coordinates,
verticalBounds,
visibleEntryIDs,
tooltipLocation: 'fixed',
margin,
item,
chatInputBarHeight: currentInputBarHeight,
},
key: getMessageTooltipKey(item),
});
});
};
}
const ConnectedTextMessage: React.ComponentType =
React.memo(function ConnectedTextMessage(props: BaseProps) {
const overlayContext = React.useContext(OverlayContext);
const chatContext = React.useContext(ChatContext);
const markdownContext = React.useContext(MarkdownContext);
invariant(markdownContext, 'markdownContext should be set');
const { linkModalActive, clearMarkdownContextData } = markdownContext;
const key = messageKey(props.item.messageInfo);
// We check if there is an key in the object - if not, we
// default to false. The likely situation where the former statement
// evaluates to null is when the thread is opened for the first time.
const isLinkModalActive = linkModalActive[key] ?? false;
const canCreateSidebarFromMessage = useCanCreateSidebarFromMessage(
props.item.threadInfo,
props.item.messageInfo,
);
+ const shouldRenderEditButton = useShouldRenderEditButton();
+
+ const canEditMessage = useCanEditMessage(
+ props.item.threadInfo,
+ props.item.messageInfo,
+ );
+
React.useEffect(() => clearMarkdownContextData, [clearMarkdownContextData]);
return (
);
});
export { ConnectedTextMessage as TextMessage };
diff --git a/native/utils/edit-messages-utils.js b/native/utils/edit-messages-utils.js
new file mode 100644
index 000000000..1565ea43a
--- /dev/null
+++ b/native/utils/edit-messages-utils.js
@@ -0,0 +1,14 @@
+// @flow
+
+import * as React from 'react';
+
+import { FeatureFlagsContext } from '../components/feature-flags-provider.react.js';
+
+function useShouldRenderEditButton(): boolean {
+ const { configuration: featureFlagConfig } =
+ React.useContext(FeatureFlagsContext);
+
+ return !!featureFlagConfig['EDIT_BUTTON_DISPLAY'];
+}
+
+export { useShouldRenderEditButton };