diff --git a/native/chat/message-result.react.js b/native/chat/message-result.react.js
index fa7b4bcd7..a93cacf08 100644
--- a/native/chat/message-result.react.js
+++ b/native/chat/message-result.react.js
@@ -1,20 +1,68 @@
// @flow
import * as React from 'react';
-import { View } from 'react-native';
+import { Text, View } from 'react-native';
+import { ScrollView } from 'react-native-gesture-handler';
import { type ThreadInfo } from 'lib/types/thread-types.js';
+import { longAbsoluteDate } from 'lib/utils/date-utils.js';
+import { MessageListContextProvider } from './message-list-types.js';
+import { Message } from './message.react.js';
+import type { AppNavigationProp } from '../navigation/app-navigator.react';
+import type { NavigationRoute } from '../navigation/route-names';
+import { useStyles } from '../themes/colors.js';
import type { ChatMessageInfoItemWithHeight } from '../types/chat-types.js';
type MessageResultProps = {
+item: ChatMessageInfoItemWithHeight,
+threadInfo: ThreadInfo,
+ +navigation: AppNavigationProp<'TogglePinModal'>,
+ +route: NavigationRoute<'TogglePinModal'>,
};
-/* eslint-disable no-unused-vars */
function MessageResult(props: MessageResultProps): React.Node {
- return ;
+ const styles = useStyles(unboundStyles);
+
+ const onToggleFocus = React.useCallback(() => {}, []);
+
+ return (
+
+
+
+
+
+ {longAbsoluteDate(props.item.messageInfo.time)}
+
+
+
+
+ );
}
+const unboundStyles = {
+ container: {
+ marginTop: 5,
+ backgroundColor: 'panelForeground',
+ overflow: 'scroll',
+ maxHeight: 400,
+ },
+ viewContainer: {
+ marginTop: 10,
+ marginBottom: 10,
+ },
+ messageDate: {
+ color: 'messageLabel',
+ fontSize: 12,
+ marginLeft: 55,
+ },
+};
+
export default MessageResult;
diff --git a/native/chat/message.react.js b/native/chat/message.react.js
index 4aac856de..6aab78708 100644
--- a/native/chat/message.react.js
+++ b/native/chat/message.react.js
@@ -1,141 +1,144 @@
// @flow
import _isEqual from 'lodash/fp/isEqual.js';
import * as React from 'react';
import {
LayoutAnimation,
TouchableWithoutFeedback,
PixelRatio,
} from 'react-native';
import shallowequal from 'shallowequal';
import { messageKey } from 'lib/shared/message-utils.js';
import type { ChatNavigationProp } from './chat.react.js';
import MultimediaMessage from './multimedia-message.react.js';
import { RobotextMessage } from './robotext-message.react.js';
import { TextMessage } from './text-message.react.js';
import { messageItemHeight } from './utils.js';
import {
type KeyboardState,
KeyboardContext,
} from '../keyboard/keyboard-state.js';
+import type { AppNavigationProp } from '../navigation/app-navigator.react';
import type { NavigationRoute } from '../navigation/route-names.js';
import type { ChatMessageInfoItemWithHeight } from '../types/chat-types.js';
import { type VerticalBounds } from '../types/layout-types.js';
import type { LayoutEvent } from '../types/react-native.js';
type BaseProps = {
+item: ChatMessageInfoItemWithHeight,
+focused: boolean,
- +navigation: ChatNavigationProp<'MessageList'>,
- +route: NavigationRoute<'MessageList'>,
+ +navigation:
+ | ChatNavigationProp<'MessageList'>
+ | AppNavigationProp<'TogglePinModal'>,
+ +route: NavigationRoute<'MessageList'> | NavigationRoute<'TogglePinModal'>,
+toggleFocus: (messageKey: string) => void,
+verticalBounds: ?VerticalBounds,
};
type Props = {
...BaseProps,
+keyboardState: ?KeyboardState,
};
class Message extends React.Component {
shouldComponentUpdate(nextProps: Props): boolean {
const { item, ...props } = this.props;
const { item: nextItem, ...newProps } = nextProps;
return !_isEqual(item, nextItem) || !shallowequal(props, newProps);
}
componentDidUpdate(prevProps: Props) {
if (
(prevProps.focused || prevProps.item.startsConversation) !==
(this.props.focused || this.props.item.startsConversation)
) {
LayoutAnimation.easeInEaseOut();
}
}
render() {
let message;
if (this.props.item.messageShapeType === 'text') {
message = (
);
} else if (this.props.item.messageShapeType === 'multimedia') {
message = (
);
} else {
message = (
);
}
const onLayout = __DEV__ ? this.onLayout : undefined;
return (
{message}
);
}
onLayout = (event: LayoutEvent) => {
if (this.props.focused) {
return;
}
const measuredHeight = event.nativeEvent.layout.height;
const expectedHeight = messageItemHeight(this.props.item);
const pixelRatio = 1 / PixelRatio.get();
const distance = Math.abs(measuredHeight - expectedHeight);
if (distance < pixelRatio) {
return;
}
const approxMeasuredHeight = Math.round(measuredHeight * 100) / 100;
const approxExpectedHeight = Math.round(expectedHeight * 100) / 100;
console.log(
`Message height for ${this.props.item.messageShapeType} ` +
`${messageKey(this.props.item.messageInfo)} was expected to be ` +
`${approxExpectedHeight} but is actually ${approxMeasuredHeight}. ` +
"This means MessageList's FlatList isn't getting the right item " +
'height for some of its nodes, which is guaranteed to cause glitchy ' +
'behavior. Please investigate!!',
);
};
dismissKeyboard = () => {
const { keyboardState } = this.props;
keyboardState && keyboardState.dismissKeyboard();
};
}
const ConnectedMessage: React.ComponentType = React.memo(
function ConnectedMessage(props: BaseProps) {
const keyboardState = React.useContext(KeyboardContext);
return ;
},
);
export { ConnectedMessage as Message };
diff --git a/native/chat/robotext-message.react.js b/native/chat/robotext-message.react.js
index db7deeb3f..b0c05b85b 100644
--- a/native/chat/robotext-message.react.js
+++ b/native/chat/robotext-message.react.js
@@ -1,214 +1,217 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { View } from 'react-native';
import { messageKey } from 'lib/shared/message-utils.js';
import { useCanCreateSidebarFromMessage } from 'lib/shared/thread-utils.js';
import { inlineEngagementCenterStyle } from './chat-constants.js';
import type { ChatNavigationProp } from './chat.react.js';
import { InlineEngagement } from './inline-engagement.react.js';
import { InnerRobotextMessage } from './inner-robotext-message.react.js';
import { Timestamp } from './timestamp.react.js';
import { getMessageTooltipKey, useContentAndHeaderOpacity } from './utils.js';
import { ChatContext } from '../chat/chat-context.js';
import { KeyboardContext } from '../keyboard/keyboard-state.js';
+import type { AppNavigationProp } from '../navigation/app-navigator.react';
import { OverlayContext } from '../navigation/overlay-context.js';
import { RobotextMessageTooltipModalRouteName } from '../navigation/route-names.js';
import type { NavigationRoute } from '../navigation/route-names.js';
import { useStyles } from '../themes/colors.js';
import { fixedTooltipHeight } from '../tooltip/tooltip.react.js';
import type { ChatRobotextMessageInfoItemWithHeight } from '../types/chat-types.js';
import type { VerticalBounds } from '../types/layout-types.js';
import { AnimatedView } from '../types/styles.js';
type Props = {
...React.ElementConfig,
+item: ChatRobotextMessageInfoItemWithHeight,
- +navigation: ChatNavigationProp<'MessageList'>,
- +route: NavigationRoute<'MessageList'>,
+ +navigation:
+ | ChatNavigationProp<'MessageList'>
+ | AppNavigationProp<'TogglePinModal'>,
+ +route: NavigationRoute<'MessageList'> | NavigationRoute<'TogglePinModal'>,
+focused: boolean,
+toggleFocus: (messageKey: string) => void,
+verticalBounds: ?VerticalBounds,
};
function RobotextMessage(props: Props): React.Node {
const {
item,
navigation,
route,
focused,
toggleFocus,
verticalBounds,
...viewProps
} = props;
let timestamp = null;
if (focused || item.startsConversation) {
timestamp = (
);
}
const styles = useStyles(unboundStyles);
let inlineEngagement = null;
if (item.threadCreatedFromMessage || Object.keys(item.reactions).length > 0) {
inlineEngagement = (
);
}
const chatContext = React.useContext(ChatContext);
const keyboardState = React.useContext(KeyboardContext);
const key = messageKey(item.messageInfo);
const onPress = React.useCallback(() => {
const didDismiss =
keyboardState && keyboardState.dismissKeyboardIfShowing();
if (!didDismiss) {
toggleFocus(key);
}
}, [keyboardState, toggleFocus, key]);
const overlayContext = React.useContext(OverlayContext);
const viewRef = React.useRef>();
const canCreateSidebarFromMessage = useCanCreateSidebarFromMessage(
item.threadInfo,
item.messageInfo,
);
const visibleEntryIDs = React.useMemo(() => {
const result = [];
if (item.threadCreatedFromMessage || canCreateSidebarFromMessage) {
result.push('sidebar');
}
return result;
}, [item.threadCreatedFromMessage, canCreateSidebarFromMessage]);
const openRobotextTooltipModal = React.useCallback(
(x, y, width, height, pageX, pageY) => {
invariant(
verticalBounds,
'verticalBounds should be present in openRobotextTooltipModal',
);
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 = 0;
if (
messageBottom + belowSpace > boundsBottom &&
messageTop - aboveSpace > boundsTop
) {
margin = aboveMargin;
}
const currentInputBarHeight =
chatContext?.chatInputBarHeights.get(item.threadInfo.id) ?? 0;
props.navigation.navigate<'RobotextMessageTooltipModal'>({
name: RobotextMessageTooltipModalRouteName,
params: {
presentedFrom: props.route.key,
initialCoordinates: coordinates,
verticalBounds,
visibleEntryIDs,
tooltipLocation: 'fixed',
margin,
item,
chatInputBarHeight: currentInputBarHeight,
},
key: getMessageTooltipKey(item),
});
},
[
item,
props.navigation,
props.route.key,
verticalBounds,
visibleEntryIDs,
chatContext,
],
);
const onLongPress = React.useCallback(() => {
if (keyboardState && keyboardState.dismissKeyboardIfShowing()) {
return;
}
if (visibleEntryIDs.length === 0) {
return;
}
if (!viewRef.current || !verticalBounds) {
return;
}
if (!focused) {
toggleFocus(messageKey(item.messageInfo));
}
invariant(overlayContext, 'RobotextMessage should have OverlayContext');
overlayContext.setScrollBlockingModalStatus('open');
viewRef.current?.measure(openRobotextTooltipModal);
}, [
focused,
item,
keyboardState,
overlayContext,
toggleFocus,
verticalBounds,
viewRef,
visibleEntryIDs,
openRobotextTooltipModal,
]);
const onLayout = React.useCallback(() => {}, []);
const contentAndHeaderOpacity = useContentAndHeaderOpacity(item);
return (
{timestamp}
{inlineEngagement}
);
}
const unboundStyles = {
sidebar: {
marginTop: inlineEngagementCenterStyle.topOffset,
marginBottom: -inlineEngagementCenterStyle.topOffset,
alignSelf: 'center',
},
};
export { RobotextMessage };
diff --git a/native/chat/text-message.react.js b/native/chat/text-message.react.js
index 62ca8bf57..745e356ce 100644
--- a/native/chat/text-message.react.js
+++ b/native/chat/text-message.react.js
@@ -1,286 +1,289 @@
// @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 type { AppNavigationProp } from '../navigation/app-navigator.react';
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'>,
+ +navigation:
+ | ChatNavigationProp<'MessageList'>
+ | AppNavigationProp<'TogglePinModal'>,
+ +route: NavigationRoute<'MessageList'> | NavigationRoute<'TogglePinModal'>,
+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/chat/toggle-pin-modal.react.js b/native/chat/toggle-pin-modal.react.js
index 1311bef2b..0ae75f652 100644
--- a/native/chat/toggle-pin-modal.react.js
+++ b/native/chat/toggle-pin-modal.react.js
@@ -1,198 +1,203 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { Text, View } from 'react-native';
import {
toggleMessagePin,
toggleMessagePinActionTypes,
} from 'lib/actions/thread-actions.js';
import { type ThreadInfo } from 'lib/types/thread-types.js';
import {
useServerCall,
useDispatchActionPromise,
} from 'lib/utils/action-utils.js';
import MessageResult from './message-result.react.js';
import Button from '../components/button.react.js';
import Modal from '../components/modal.react.js';
import type { AppNavigationProp } from '../navigation/app-navigator.react.js';
import type { NavigationRoute } from '../navigation/route-names.js';
import { useStyles } from '../themes/colors.js';
import type { ChatMessageInfoItemWithHeight } from '../types/chat-types';
export type TogglePinModalParams = {
+item: ChatMessageInfoItemWithHeight,
+threadInfo: ThreadInfo,
};
type TogglePinModalProps = {
+navigation: AppNavigationProp<'TogglePinModal'>,
+route: NavigationRoute<'TogglePinModal'>,
};
function TogglePinModal(props: TogglePinModalProps): React.Node {
const { navigation, route } = props;
const { item, threadInfo } = route.params;
const { messageInfo, isPinned } = item;
const styles = useStyles(unboundStyles);
const callToggleMessagePin = useServerCall(toggleMessagePin);
const dispatchActionPromise = useDispatchActionPromise();
const modalInfo = React.useMemo(() => {
if (isPinned) {
return {
name: 'Remove Pinned Message',
action: 'unpin',
confirmationText:
'Are you sure you want to remove this pinned message?',
buttonText: 'Remove Pinned Message',
buttonStyle: styles.removePinButton,
};
}
return {
name: 'Pin Message',
action: 'pin',
confirmationText:
'You may pin this message to the channel you are ' +
'currently viewing. To unpin a message, select the pinned messages ' +
'icon in the channel.',
buttonText: 'Pin Message',
buttonStyle: styles.pinButton,
};
}, [isPinned, styles.pinButton, styles.removePinButton]);
const modifiedItem = React.useMemo(() => {
// The if / else if / else conditional is for Flow
if (item.messageShapeType === 'robotext') {
return item;
} else if (item.messageShapeType === 'multimedia') {
return {
...item,
threadCreatedFromMessage: undefined,
reactions: {},
startsConversation: false,
startsCluster: true,
endsCluster: true,
messageInfo: {
...item.messageInfo,
creator: {
...item.messageInfo.creator,
isViewer: false,
},
},
};
} else {
return {
...item,
threadCreatedFromMessage: undefined,
reactions: {},
startsConversation: false,
startsCluster: true,
endsCluster: true,
messageInfo: {
...item.messageInfo,
creator: {
...item.messageInfo.creator,
isViewer: false,
},
},
};
}
}, [item]);
const createToggleMessagePinPromise = React.useCallback(async () => {
invariant(messageInfo.id, 'messageInfo.id should be defined');
const result = await callToggleMessagePin({
messageID: messageInfo.id,
action: modalInfo.action,
});
return {
newMessageInfos: result.newMessageInfos,
threadID: result.threadID,
};
}, [callToggleMessagePin, messageInfo.id, modalInfo.action]);
const onPress = React.useCallback(() => {
dispatchActionPromise(
toggleMessagePinActionTypes,
createToggleMessagePinPromise(),
);
navigation.goBack();
}, [createToggleMessagePinPromise, dispatchActionPromise, navigation]);
const onCancel = React.useCallback(() => {
navigation.goBack();
}, [navigation]);
return (
{modalInfo.name}
{modalInfo.confirmationText}
-
+
);
}
const unboundStyles = {
modal: {
backgroundColor: 'modalForeground',
borderColor: 'modalForegroundBorder',
},
modalHeader: {
fontSize: 18,
color: 'modalForegroundLabel',
},
modalConfirmationText: {
fontSize: 12,
color: 'modalBackgroundLabel',
marginTop: 4,
},
buttonsContainer: {
flexDirection: 'column',
flex: 1,
justifyContent: 'flex-end',
marginBottom: 0,
height: 72,
paddingHorizontal: 16,
},
removePinButton: {
borderRadius: 5,
height: 48,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'vibrantRedButton',
},
pinButton: {
borderRadius: 5,
height: 48,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'purpleButton',
},
cancelButton: {
borderRadius: 5,
height: 48,
justifyContent: 'center',
alignItems: 'center',
},
textColor: {
color: 'modalButtonLabel',
},
};
export default TogglePinModal;