diff --git a/native/chat/chat-item-height-measurer.react.js b/native/chat/chat-item-height-measurer.react.js
index 74e2b0ec5..659af3aeb 100644
--- a/native/chat/chat-item-height-measurer.react.js
+++ b/native/chat/chat-item-height-measurer.react.js
@@ -1,188 +1,190 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { messageID } from 'lib/shared/message-utils.js';
import {
messageTypes,
type MessageType,
} from 'lib/types/message-types-enum.js';
import { entityTextToRawString } from 'lib/utils/entity-text.js';
import type { MeasurementTask } from './chat-context-provider.react.js';
import { useComposedMessageMaxWidth } from './composed-message-width.js';
import { dummyNodeForRobotextMessageHeightMeasurement } from './inner-robotext-message.react.js';
import { dummyNodeForTextMessageHeightMeasurement } from './inner-text-message.react.js';
import type { NativeChatMessageItem } from './message-data.react.js';
import { MessageListContextProvider } from './message-list-types.js';
import { multimediaMessageContentSizes } from './multimedia-message-utils.js';
import { chatMessageItemKey } from './utils.js';
import NodeHeightMeasurer from '../components/node-height-measurer.react.js';
import { InputStateContext } from '../input/input-state.js';
type Props = {
+measurement: MeasurementTask,
};
const heightMeasurerKey = (item: NativeChatMessageItem) => {
if (item.itemType !== 'message') {
return null;
}
const { messageInfo } = item;
if (messageInfo.type === messageTypes.TEXT) {
return JSON.stringify({ text: messageInfo.text });
} else if (item.robotext) {
const { threadID } = item.messageInfo;
return JSON.stringify({
robotext: entityTextToRawString(item.robotext, { threadID }),
});
}
return null;
};
// ESLint doesn't recognize that invariant always throws
// eslint-disable-next-line consistent-return
const heightMeasurerDummy = (item: NativeChatMessageItem) => {
invariant(
item.itemType === 'message',
'NodeHeightMeasurer asked for dummy for non-message item',
);
const { messageInfo } = item;
if (messageInfo.type === messageTypes.TEXT) {
return dummyNodeForTextMessageHeightMeasurement(messageInfo.text);
} else if (item.robotext) {
return dummyNodeForRobotextMessageHeightMeasurement(
item.robotext,
item.messageInfo.threadID,
);
}
invariant(false, 'NodeHeightMeasurer asked for dummy for non-text message');
};
function ChatItemHeightMeasurer(props: Props) {
const composedMessageMaxWidth = useComposedMessageMaxWidth();
const inputState = React.useContext(InputStateContext);
const inputStatePendingUploads = inputState?.pendingUploads;
const { measurement } = props;
const { threadInfo } = measurement;
const heightMeasurerMergeItem = React.useCallback(
(item: NativeChatMessageItem, height: ?number) => {
if (item.itemType !== 'message') {
return item;
}
const { messageInfo } = item;
const messageType: MessageType = messageInfo.type;
invariant(
messageType !== messageTypes.SIDEBAR_SOURCE,
'Sidebar source messages should be replaced by sourceMessage before being measured',
);
if (
messageInfo.type === messageTypes.IMAGES ||
messageInfo.type === messageTypes.MULTIMEDIA
) {
// Conditional due to Flow...
const localMessageInfo = item.localMessageInfo
? item.localMessageInfo
: null;
const id = messageID(messageInfo);
const pendingUploads = inputStatePendingUploads?.[id];
const sizes = multimediaMessageContentSizes(
messageInfo,
composedMessageMaxWidth,
);
return {
itemType: 'message',
messageShapeType: 'multimedia',
messageInfo,
localMessageInfo,
threadInfo,
startsConversation: item.startsConversation,
startsCluster: item.startsCluster,
endsCluster: item.endsCluster,
threadCreatedFromMessage: item.threadCreatedFromMessage,
pendingUploads,
reactions: item.reactions,
hasBeenEdited: item.hasBeenEdited,
+ isPinned: item.isPinned,
...sizes,
};
}
invariant(
height !== null && height !== undefined,
'height should be set',
);
if (messageInfo.type === messageTypes.TEXT) {
// Conditional due to Flow...
const localMessageInfo = item.localMessageInfo
? item.localMessageInfo
: null;
return {
itemType: 'message',
messageShapeType: 'text',
messageInfo,
localMessageInfo,
threadInfo,
startsConversation: item.startsConversation,
startsCluster: item.startsCluster,
endsCluster: item.endsCluster,
threadCreatedFromMessage: item.threadCreatedFromMessage,
contentHeight: height,
reactions: item.reactions,
hasBeenEdited: item.hasBeenEdited,
+ isPinned: item.isPinned,
};
}
invariant(
item.messageInfoType !== 'composable',
'ChatItemHeightMeasurer was handed a messageInfoType=composable, but ' +
`does not know how to handle MessageType ${messageInfo.type}`,
);
invariant(
item.messageInfoType === 'robotext',
'ChatItemHeightMeasurer was handed a messageInfoType that it does ' +
`not recognize: ${item.messageInfoType}`,
);
return {
itemType: 'message',
messageShapeType: 'robotext',
messageInfo,
threadInfo,
startsConversation: item.startsConversation,
startsCluster: item.startsCluster,
endsCluster: item.endsCluster,
threadCreatedFromMessage: item.threadCreatedFromMessage,
robotext: item.robotext,
contentHeight: height,
reactions: item.reactions,
};
},
[composedMessageMaxWidth, inputStatePendingUploads, threadInfo],
);
return (
);
}
const MemoizedChatItemHeightMeasurer: React.ComponentType =
React.memo(ChatItemHeightMeasurer);
export default MemoizedChatItemHeightMeasurer;
diff --git a/native/chat/multimedia-message-tooltip-modal.react.js b/native/chat/multimedia-message-tooltip-modal.react.js
index 7bbe523ba..3d9c86631 100644
--- a/native/chat/multimedia-message-tooltip-modal.react.js
+++ b/native/chat/multimedia-message-tooltip-modal.react.js
@@ -1,69 +1,94 @@
// @flow
import * as React from 'react';
import { useOnPressReport } from './message-report-utils.js';
import MultimediaMessageTooltipButton from './multimedia-message-tooltip-button.react.js';
import { useAnimatedNavigateToSidebar } from './sidebar-navigation.js';
+import CommIcon from '../components/comm-icon.react.js';
import SWMansionIcon from '../components/swmansion-icon.react.js';
import {
createTooltip,
type TooltipParams,
type BaseTooltipProps,
type TooltipMenuProps,
} from '../tooltip/tooltip.react.js';
import type { ChatMultimediaMessageInfoItem } from '../types/chat-types.js';
import type { VerticalBounds } from '../types/layout-types.js';
export type MultimediaMessageTooltipModalParams = TooltipParams<{
+item: ChatMultimediaMessageInfoItem,
+verticalBounds: VerticalBounds,
}>;
function TooltipMenu(
props: TooltipMenuProps<'MultimediaMessageTooltipModal'>,
): React.Node {
const { route, tooltipItem: TooltipItem } = props;
+ const onPressTogglePin = React.useCallback(() => {}, []);
+ const renderPinIcon = React.useCallback(
+ style => ,
+ [],
+ );
+ const renderUnpinIcon = React.useCallback(
+ style => ,
+ [],
+ );
+
const onPressSidebar = useAnimatedNavigateToSidebar(route.params.item);
const renderSidebarIcon = React.useCallback(
style => (
),
[],
);
const onPressReport = useOnPressReport(route);
const renderReportIcon = React.useCallback(
style => ,
[],
);
return (
<>
+
+
>
);
}
const MultimediaMessageTooltipModal: React.ComponentType<
BaseTooltipProps<'MultimediaMessageTooltipModal'>,
> = createTooltip<'MultimediaMessageTooltipModal'>(
MultimediaMessageTooltipButton,
TooltipMenu,
);
export default MultimediaMessageTooltipModal;
diff --git a/native/chat/multimedia-message.react.js b/native/chat/multimedia-message.react.js
index 02b7f2f8e..b6ceb7211 100644
--- a/native/chat/multimedia-message.react.js
+++ b/native/chat/multimedia-message.react.js
@@ -1,251 +1,266 @@
// @flow
import type {
LeafRoute,
NavigationProp,
ParamListBase,
} from '@react-navigation/native';
import { useNavigation, useRoute } from '@react-navigation/native';
import * as React from 'react';
import { StyleSheet, View } from 'react-native';
import { messageKey } from 'lib/shared/message-utils.js';
-import { useCanCreateSidebarFromMessage } from 'lib/shared/thread-utils.js';
+import {
+ threadHasPermission,
+ useCanCreateSidebarFromMessage,
+} from 'lib/shared/thread-utils.js';
import type { MediaInfo } from 'lib/types/media-types.js';
+import { threadPermissions } from 'lib/types/thread-types.js';
import ComposedMessage from './composed-message.react.js';
import { InnerMultimediaMessage } from './inner-multimedia-message.react.js';
import {
getMediaKey,
multimediaMessageSendFailed,
} from './multimedia-message-utils.js';
import { getMessageTooltipKey } from './utils.js';
import { ChatContext, type ChatContextType } from '../chat/chat-context.js';
import { OverlayContext } from '../navigation/overlay-context.js';
import type { OverlayContextType } from '../navigation/overlay-context.js';
import {
ImageModalRouteName,
MultimediaMessageTooltipModalRouteName,
VideoPlaybackModalRouteName,
} from '../navigation/route-names.js';
import { fixedTooltipHeight } from '../tooltip/tooltip.react.js';
import type { ChatMultimediaMessageInfoItem } from '../types/chat-types.js';
import type {
VerticalBounds,
LayoutCoordinates,
} from '../types/layout-types.js';
type BaseProps = {
...React.ElementConfig,
+item: ChatMultimediaMessageInfoItem,
+focused: boolean,
+toggleFocus: (messageKey: string) => void,
+verticalBounds: ?VerticalBounds,
};
type Props = {
...BaseProps,
+navigation: NavigationProp,
+route: LeafRoute<>,
+overlayContext: ?OverlayContextType,
+chatContext: ?ChatContextType,
+canCreateSidebarFromMessage: boolean,
+ +canTogglePins: boolean,
};
type State = {
+clickable: boolean,
};
class MultimediaMessage extends React.PureComponent {
state: State = {
clickable: true,
};
view: ?React.ElementRef;
setClickable = (clickable: boolean) => {
this.setState({ clickable });
};
onPressMultimedia = (
mediaInfo: MediaInfo,
initialCoordinates: LayoutCoordinates,
) => {
const { navigation, item, route, verticalBounds } = this.props;
navigation.navigate<'VideoPlaybackModal' | 'ImageModal'>({
name:
mediaInfo.type === 'video' || mediaInfo.type === 'encrypted_video'
? VideoPlaybackModalRouteName
: ImageModalRouteName,
key: getMediaKey(item, mediaInfo),
params: {
presentedFrom: route.key,
mediaInfo,
item,
initialCoordinates,
verticalBounds,
},
});
};
visibleEntryIDs() {
const result = [];
+ if (this.props.canTogglePins) {
+ this.props.item.isPinned ? result.push('unpin') : result.push('pin');
+ }
+
if (
this.props.item.threadCreatedFromMessage ||
this.props.canCreateSidebarFromMessage
) {
result.push('sidebar');
}
if (!this.props.item.messageInfo.creator.isViewer) {
result.push('report');
}
return result;
}
onLayout = () => {};
viewRef = (view: ?React.ElementRef) => {
this.view = view;
};
onLongPress = () => {
const visibleEntryIDs = this.visibleEntryIDs();
if (visibleEntryIDs.length === 0) {
return;
}
const {
view,
props: { verticalBounds },
} = this;
if (!view || !verticalBounds) {
return;
}
if (!this.state.clickable) {
return;
}
this.setClickable(false);
const { item } = this.props;
if (!this.props.focused) {
this.props.toggleFocus(messageKey(item.messageInfo));
}
this.props.overlayContext?.setScrollBlockingModalStatus('open');
view.measure((x, y, width, height, pageX, pageY) => {
const coordinates = { x: pageX, y: pageY, width, height };
const multimediaTop = pageY;
const multimediaBottom = 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 (
multimediaBottom + belowSpace > boundsBottom &&
multimediaTop - aboveSpace > boundsTop
) {
margin = aboveMargin;
}
const currentInputBarHeight =
this.props.chatContext?.chatInputBarHeights.get(item.threadInfo.id) ??
0;
this.props.navigation.navigate<'MultimediaMessageTooltipModal'>({
name: MultimediaMessageTooltipModalRouteName,
params: {
presentedFrom: this.props.route.key,
item,
initialCoordinates: coordinates,
verticalBounds,
tooltipLocation: 'fixed',
margin,
visibleEntryIDs,
chatInputBarHeight: currentInputBarHeight,
},
key: getMessageTooltipKey(item),
});
});
};
canNavigateToSidebar() {
return (
this.props.item.threadCreatedFromMessage ||
this.props.canCreateSidebarFromMessage
);
}
render() {
const {
item,
focused,
toggleFocus,
verticalBounds,
navigation,
route,
overlayContext,
chatContext,
canCreateSidebarFromMessage,
+ canTogglePins,
...viewProps
} = this.props;
return (
);
}
}
const styles = StyleSheet.create({
expand: {
flex: 1,
},
});
const ConnectedMultimediaMessage: React.ComponentType =
React.memo(function ConnectedMultimediaMessage(props: BaseProps) {
const navigation = useNavigation();
const route = useRoute();
const overlayContext = React.useContext(OverlayContext);
const chatContext = React.useContext(ChatContext);
const canCreateSidebarFromMessage = useCanCreateSidebarFromMessage(
props.item.threadInfo,
props.item.messageInfo,
);
+ const canTogglePins = threadHasPermission(
+ props.item.threadInfo,
+ threadPermissions.MANAGE_PINS,
+ );
return (
);
});
export default ConnectedMultimediaMessage;
diff --git a/native/chat/text-message-tooltip-modal.react.js b/native/chat/text-message-tooltip-modal.react.js
index 00876b615..40675a48b 100644
--- a/native/chat/text-message-tooltip-modal.react.js
+++ b/native/chat/text-message-tooltip-modal.react.js
@@ -1,150 +1,174 @@
// @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';
import { exitEditAlert } from '../utils/edit-messages-utils.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.editInputMessage({
message: createMessageReply(text),
mode: 'prepend',
});
}, [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',
);
const updateInputBar = () => {
inputState.editInputMessage({
message: text,
mode: 'replace',
});
};
const enterEditMode = () => {
inputState.setEditedMessage(messageInfo, updateInputBar);
};
if (inputState.editState.editedMessage) {
exitEditAlert(enterEditMode);
} else {
enterEditMode();
}
}, [inputState, messageInfo, text]);
const renderEditIcon = React.useCallback(
style => ,
[],
);
+ const onPressTogglePin = React.useCallback(() => {}, []);
+ const renderPinIcon = React.useCallback(
+ style => ,
+ [],
+ );
+ const renderUnpinIcon = 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 59047f768..62ca8bf57 100644
--- a/native/chat/text-message.react.js
+++ b/native/chat/text-message.react.js
@@ -1,274 +1,286 @@
// @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,
+ +canTogglePins: 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,
+ canTogglePins,
...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.canTogglePins) {
+ this.props.item.isPinned ? result.push('unpin') : result.push('pin');
+ }
+
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,
);
+ const canTogglePins = threadHasPermission(
+ props.item.threadInfo,
+ threadPermissions.MANAGE_PINS,
+ );
+
React.useEffect(() => clearMarkdownContextData, [clearMarkdownContextData]);
return (
);
});
export { ConnectedTextMessage as TextMessage };
diff --git a/native/types/chat-types.js b/native/types/chat-types.js
index a9409062d..74b84807e 100644
--- a/native/types/chat-types.js
+++ b/native/types/chat-types.js
@@ -1,72 +1,74 @@
// @flow
import type { ReactionInfo } from 'lib/selectors/chat-selectors.js';
import type {
LocalMessageInfo,
MultimediaMessageInfo,
RobotextMessageInfo,
} from 'lib/types/message-types.js';
import type { TextMessageInfo } from 'lib/types/messages/text.js';
import type { ThreadInfo } from 'lib/types/thread-types.js';
import type { EntityText } from 'lib/utils/entity-text.js';
import type { MessagePendingUploads } from '../input/input-state.js';
export type ChatRobotextMessageInfoItemWithHeight = {
+itemType: 'message',
+messageShapeType: 'robotext',
+messageInfo: RobotextMessageInfo,
+threadInfo: ThreadInfo,
+startsConversation: boolean,
+startsCluster: boolean,
+endsCluster: boolean,
+robotext: EntityText,
+threadCreatedFromMessage: ?ThreadInfo,
+contentHeight: number,
+reactions: ReactionInfo,
};
export type ChatTextMessageInfoItemWithHeight = {
+itemType: 'message',
+messageShapeType: 'text',
+messageInfo: TextMessageInfo,
+localMessageInfo: ?LocalMessageInfo,
+threadInfo: ThreadInfo,
+startsConversation: boolean,
+startsCluster: boolean,
+endsCluster: boolean,
+contentHeight: number,
+threadCreatedFromMessage: ?ThreadInfo,
+reactions: ReactionInfo,
+hasBeenEdited: ?boolean,
+ +isPinned: ?boolean,
};
export type MultimediaContentSizes = {
+imageHeight: number,
+contentHeight: number,
+contentWidth: number,
};
export type ChatMultimediaMessageInfoItem = {
...MultimediaContentSizes,
+itemType: 'message',
+messageShapeType: 'multimedia',
+messageInfo: MultimediaMessageInfo,
+localMessageInfo: ?LocalMessageInfo,
+threadInfo: ThreadInfo,
+startsConversation: boolean,
+startsCluster: boolean,
+endsCluster: boolean,
+threadCreatedFromMessage: ?ThreadInfo,
+pendingUploads: ?MessagePendingUploads,
+reactions: ReactionInfo,
+hasBeenEdited: ?boolean,
+ +isPinned: ?boolean,
};
export type ChatMessageInfoItemWithHeight =
| ChatRobotextMessageInfoItemWithHeight
| ChatTextMessageInfoItemWithHeight
| ChatMultimediaMessageInfoItem;
export type ChatMessageItemWithHeight =
| { itemType: 'loader' }
| ChatMessageInfoItemWithHeight;