diff --git a/native/chat/message-result.react.js b/native/chat/message-result.react.js
index bbc7f4e3e..04dfa9944 100644
--- a/native/chat/message-result.react.js
+++ b/native/chat/message-result.react.js
@@ -1,73 +1,75 @@
// @flow
import * as React from 'react';
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 type { ChatNavigationProp } from './chat.react';
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';
+import type { VerticalBounds } from '../types/layout-types.js';
type MessageResultProps = {
+item: ChatMessageInfoItemWithHeight,
+threadInfo: ThreadInfo,
+navigation:
| AppNavigationProp<'TogglePinModal'>
| ChatNavigationProp<'MessageResultsScreen'>,
+route:
| NavigationRoute<'TogglePinModal'>
| NavigationRoute<'MessageResultsScreen'>,
+ +messageVerticalBounds: ?VerticalBounds,
};
function MessageResult(props: MessageResultProps): React.Node {
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-results-screen.react.js b/native/chat/message-results-screen.react.js
index c768070f2..b519cc957 100644
--- a/native/chat/message-results-screen.react.js
+++ b/native/chat/message-results-screen.react.js
@@ -1,179 +1,200 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { View } from 'react-native';
import { ScrollView } from 'react-native-gesture-handler';
import { fetchPinnedMessages } from 'lib/actions/message-actions.js';
import { messageListData } from 'lib/selectors/chat-selectors.js';
import { createMessageInfo } from 'lib/shared/message-utils.js';
import type { ThreadInfo } from 'lib/types/thread-types.js';
import { useServerCall } from 'lib/utils/action-utils.js';
import { useHeightMeasurer } from './chat-context.js';
import type { ChatNavigationProp } from './chat.react';
import MessageResult from './message-result.react.js';
import type { NavigationRoute } from '../navigation/route-names';
import { useSelector } from '../redux/redux-utils.js';
import type { ChatMessageItemWithHeight } from '../types/chat-types.js';
export type MessageResultsScreenParams = {
+threadInfo: ThreadInfo,
};
type MessageResultsScreenProps = {
+navigation: ChatNavigationProp<'MessageResultsScreen'>,
+route: NavigationRoute<'MessageResultsScreen'>,
};
function MessageResultsScreen(props: MessageResultsScreenProps): React.Node {
const { navigation, route } = props;
const { threadInfo } = route.params;
const { id: threadID } = threadInfo;
const [rawMessageResults, setRawMessageResults] = React.useState([]);
const measureMessages = useHeightMeasurer();
const [measuredMessages, setMeasuredMessages] = React.useState([]);
+ const [messageVerticalBounds, setMessageVerticalBounds] = React.useState();
+ const scrollViewContainerRef = React.useRef();
+
const callFetchPinnedMessages = useServerCall(fetchPinnedMessages);
const userInfos = useSelector(state => state.userStore.userInfos);
React.useEffect(() => {
(async () => {
const result = await callFetchPinnedMessages({ threadID });
setRawMessageResults(result.pinnedMessages);
})();
}, [callFetchPinnedMessages, threadID]);
const translatedMessageResults = React.useMemo(() => {
const threadInfos = { [threadID]: threadInfo };
return rawMessageResults
.map(messageInfo =>
createMessageInfo(messageInfo, null, userInfos, threadInfos),
)
.filter(Boolean);
}, [rawMessageResults, userInfos, threadID, threadInfo]);
const chatMessageInfos = useSelector(
messageListData(threadInfo.id, translatedMessageResults),
);
const sortedUniqueChatMessageInfoItems = React.useMemo(() => {
if (!chatMessageInfos) {
return [];
}
const chatMessageInfoItems = chatMessageInfos.filter(
item => item.itemType === 'message' && item.isPinned,
);
// By the nature of using messageListData and passing in
// the desired translatedMessageResults as additional
// messages, we will have duplicate ChatMessageInfoItems.
const uniqueChatMessageInfoItemsMap = new Map();
chatMessageInfoItems.forEach(
item =>
item.messageInfo &&
item.messageInfo.id &&
uniqueChatMessageInfoItemsMap.set(item.messageInfo.id, item),
);
// Push the items in the order they appear in the rawMessageResults
// since the messages fetched from the server are already sorted
// in the order of pin_time (newest first).
const sortedChatMessageInfoItems = [];
for (let i = 0; i < rawMessageResults.length; i++) {
sortedChatMessageInfoItems.push(
uniqueChatMessageInfoItemsMap.get(rawMessageResults[i].id),
);
}
return sortedChatMessageInfoItems.filter(Boolean);
}, [chatMessageInfos, rawMessageResults]);
const measureCallback = React.useCallback(
(listDataWithHeights: $ReadOnlyArray) => {
setMeasuredMessages(listDataWithHeights);
},
[],
);
React.useEffect(() => {
measureMessages(
sortedUniqueChatMessageInfoItems,
threadInfo,
measureCallback,
);
}, [
measureCallback,
measureMessages,
sortedUniqueChatMessageInfoItems,
threadInfo,
]);
const modifiedItems = React.useMemo(
() =>
measuredMessages.map(item => {
invariant(item.itemType !== 'loader', 'should not be loader');
invariant(
item.messageShapeType !== 'robotext',
'should not be robotext',
);
if (item.messageShapeType === 'multimedia') {
return {
...item,
startsConversation: false,
startsCluster: true,
endsCluster: true,
messageInfo: {
...item.messageInfo,
creator: {
...item.messageInfo.creator,
isViewer: false,
},
},
};
}
return {
...item,
startsConversation: false,
startsCluster: true,
endsCluster: true,
messageInfo: {
...item.messageInfo,
creator: {
...item.messageInfo.creator,
isViewer: false,
},
},
};
}),
[measuredMessages],
);
+ const onLayout = React.useCallback(() => {
+ scrollViewContainerRef.current?.measure(
+ (x, y, width, height, pageX, pageY) => {
+ if (
+ height === null ||
+ height === undefined ||
+ pageY === null ||
+ pageY === undefined
+ ) {
+ return;
+ }
+
+ setMessageVerticalBounds({ height, y: pageY });
+ },
+ );
+ }, []);
+
const messageResultsToDisplay = React.useMemo(
() =>
modifiedItems.map(item => (
)),
- [modifiedItems, threadInfo, navigation, route],
+ [modifiedItems, threadInfo, navigation, route, messageVerticalBounds],
);
return (
-
+
{messageResultsToDisplay}
);
}
export default MessageResultsScreen;
diff --git a/native/chat/toggle-pin-modal.react.js b/native/chat/toggle-pin-modal.react.js
index 0ae75f652..de85a06c9 100644
--- a/native/chat/toggle-pin-modal.react.js
+++ b/native/chat/toggle-pin-modal.react.js
@@ -1,203 +1,204 @@
// @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;