Page MenuHomePhabricator

No OneTemporary

diff --git a/lib/selectors/chat-selectors.js b/lib/selectors/chat-selectors.js
index 5014784cc..ebbd0887f 100644
--- a/lib/selectors/chat-selectors.js
+++ b/lib/selectors/chat-selectors.js
@@ -1,611 +1,611 @@
// @flow
import invariant from 'invariant';
import _filter from 'lodash/fp/filter.js';
import _flow from 'lodash/fp/flow.js';
import _map from 'lodash/fp/map.js';
import _orderBy from 'lodash/fp/orderBy.js';
import * as React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { createObjectSelector } from 'reselect-map';
import {
threadInfoSelector,
sidebarInfoSelector,
threadInfoFromSourceMessageIDSelector,
} from './thread-selectors.js';
import {
messageKey,
robotextForMessageInfo,
createMessageInfo,
getMostRecentNonLocalMessageID,
sortMessageInfoList,
} from '../shared/message-utils.js';
import {
threadIsPending,
threadIsTopLevel,
threadInChatList,
} from '../shared/thread-utils.js';
import {
type MessageInfo,
type MessageStore,
type ComposableMessageInfo,
type RobotextMessageInfo,
type LocalMessageInfo,
messageTypes,
isComposableMessageType,
} from '../types/message-types.js';
import type { BaseAppState } from '../types/redux-types.js';
import {
type ThreadInfo,
type RawThreadInfo,
type SidebarInfo,
maxReadSidebars,
maxUnreadSidebars,
threadTypes,
} from '../types/thread-types.js';
import type {
UserInfo,
AccountUserInfo,
RelativeUserInfo,
} from '../types/user-types.js';
import { threeDays } from '../utils/date-utils.js';
import type { EntityText } from '../utils/entity-text.js';
import memoize2 from '../utils/memoize.js';
export type SidebarItem =
| {
...SidebarInfo,
+type: 'sidebar',
}
| {
+type: 'seeMore',
+unread: boolean,
}
| { +type: 'spacer' };
export type ChatThreadItem = {
+type: 'chatThreadItem',
+threadInfo: ThreadInfo,
+mostRecentMessageInfo: ?MessageInfo,
+mostRecentNonLocalMessage: ?string,
+lastUpdatedTime: number,
+lastUpdatedTimeIncludingSidebars: number,
+sidebars: $ReadOnlyArray<SidebarItem>,
+pendingPersonalThreadUserInfo?: UserInfo,
};
const messageInfoSelector: (state: BaseAppState<*>) => {
+[id: string]: ?MessageInfo,
} = createObjectSelector(
(state: BaseAppState<*>) => state.messageStore.messages,
(state: BaseAppState<*>) => state.currentUserInfo && state.currentUserInfo.id,
(state: BaseAppState<*>) => state.userStore.userInfos,
threadInfoSelector,
createMessageInfo,
);
function isEmptyMediaMessage(messageInfo: MessageInfo): boolean {
return (
(messageInfo.type === messageTypes.MULTIMEDIA ||
messageInfo.type === messageTypes.IMAGES) &&
messageInfo.media.length === 0
);
}
function getMostRecentMessageInfo(
threadInfo: ThreadInfo,
messageStore: MessageStore,
messages: { +[id: string]: ?MessageInfo },
): ?MessageInfo {
const thread = messageStore.threads[threadInfo.id];
if (!thread) {
return null;
}
for (const messageID of thread.messageIDs) {
const messageInfo = messages[messageID];
if (!messageInfo || isEmptyMediaMessage(messageInfo)) {
continue;
}
return messageInfo;
}
return null;
}
function getLastUpdatedTime(
threadInfo: ThreadInfo,
mostRecentMessageInfo: ?MessageInfo,
): number {
return mostRecentMessageInfo
? mostRecentMessageInfo.time
: threadInfo.creationTime;
}
function createChatThreadItem(
threadInfo: ThreadInfo,
messageStore: MessageStore,
messages: { +[id: string]: ?MessageInfo },
sidebarInfos: ?$ReadOnlyArray<SidebarInfo>,
): ChatThreadItem {
const mostRecentMessageInfo = getMostRecentMessageInfo(
threadInfo,
messageStore,
messages,
);
const mostRecentNonLocalMessage = getMostRecentNonLocalMessageID(
threadInfo.id,
messageStore,
);
const lastUpdatedTime = getLastUpdatedTime(threadInfo, mostRecentMessageInfo);
const sidebars = sidebarInfos ?? [];
const allSidebarItems = sidebars.map(sidebarInfo => ({
type: 'sidebar',
...sidebarInfo,
}));
const lastUpdatedTimeIncludingSidebars =
allSidebarItems.length > 0
? Math.max(lastUpdatedTime, allSidebarItems[0].lastUpdatedTime)
: lastUpdatedTime;
const numUnreadSidebars = allSidebarItems.filter(
sidebar => sidebar.threadInfo.currentUser.unread,
).length;
let numReadSidebarsToShow = maxReadSidebars - numUnreadSidebars;
const threeDaysAgo = Date.now() - threeDays;
const sidebarItems = [];
for (const sidebar of allSidebarItems) {
if (sidebarItems.length >= maxUnreadSidebars) {
break;
} else if (sidebar.threadInfo.currentUser.unread) {
sidebarItems.push(sidebar);
} else if (
sidebar.lastUpdatedTime > threeDaysAgo &&
numReadSidebarsToShow > 0
) {
sidebarItems.push(sidebar);
numReadSidebarsToShow--;
}
}
const numReadButRecentSidebars = allSidebarItems.filter(
sidebar =>
!sidebar.threadInfo.currentUser.unread &&
sidebar.lastUpdatedTime > threeDaysAgo,
).length;
if (
sidebarItems.length < numUnreadSidebars + numReadButRecentSidebars ||
(sidebarItems.length < allSidebarItems.length && sidebarItems.length > 0)
) {
sidebarItems.push({
type: 'seeMore',
unread: numUnreadSidebars > maxUnreadSidebars,
});
}
if (sidebarItems.length !== 0) {
sidebarItems.push({
type: 'spacer',
});
}
return {
type: 'chatThreadItem',
threadInfo,
mostRecentMessageInfo,
mostRecentNonLocalMessage,
lastUpdatedTime,
lastUpdatedTimeIncludingSidebars,
sidebars: sidebarItems,
};
}
const chatListData: (state: BaseAppState<*>) => $ReadOnlyArray<ChatThreadItem> =
createSelector(
threadInfoSelector,
(state: BaseAppState<*>) => state.messageStore,
messageInfoSelector,
sidebarInfoSelector,
(
threadInfos: { +[id: string]: ThreadInfo },
messageStore: MessageStore,
messageInfos: { +[id: string]: ?MessageInfo },
sidebarInfos: { +[id: string]: $ReadOnlyArray<SidebarInfo> },
): $ReadOnlyArray<ChatThreadItem> =>
getChatThreadItems(
threadInfos,
messageStore,
messageInfos,
sidebarInfos,
threadIsTopLevel,
),
);
function useFlattenedChatListData(): $ReadOnlyArray<ChatThreadItem> {
return useFilteredChatListData(threadInChatList);
}
function useFilteredChatListData(
filterFunction: (threadInfo: ?(ThreadInfo | RawThreadInfo)) => boolean,
): $ReadOnlyArray<ChatThreadItem> {
const threadInfos = useSelector(threadInfoSelector);
const messageInfos = useSelector(messageInfoSelector);
const sidebarInfos = useSelector(sidebarInfoSelector);
const messageStore = useSelector(state => state.messageStore);
return React.useMemo(
() =>
getChatThreadItems(
threadInfos,
messageStore,
messageInfos,
sidebarInfos,
filterFunction,
),
[messageInfos, messageStore, sidebarInfos, filterFunction, threadInfos],
);
}
function getChatThreadItems(
threadInfos: { +[id: string]: ThreadInfo },
messageStore: MessageStore,
messageInfos: { +[id: string]: ?MessageInfo },
sidebarInfos: { +[id: string]: $ReadOnlyArray<SidebarInfo> },
filterFunction: (threadInfo: ?(ThreadInfo | RawThreadInfo)) => boolean,
): $ReadOnlyArray<ChatThreadItem> {
return _flow(
_filter(filterFunction),
_map((threadInfo: ThreadInfo): ChatThreadItem =>
createChatThreadItem(
threadInfo,
messageStore,
messageInfos,
sidebarInfos[threadInfo.id],
),
),
_orderBy('lastUpdatedTimeIncludingSidebars')('desc'),
)(threadInfos);
}
export type RobotextChatMessageInfoItem = {
+itemType: 'message',
+messageInfoType: 'robotext',
+messageInfo: RobotextMessageInfo,
+startsConversation: boolean,
+startsCluster: boolean,
endsCluster: boolean,
+robotext: EntityText,
+threadCreatedFromMessage: ?ThreadInfo,
+reactions: ReactionInfo,
};
export type ChatMessageInfoItem =
| RobotextChatMessageInfoItem
| {
+itemType: 'message',
+messageInfoType: 'composable',
+messageInfo: ComposableMessageInfo,
+localMessageInfo: ?LocalMessageInfo,
+startsConversation: boolean,
+startsCluster: boolean,
endsCluster: boolean,
+threadCreatedFromMessage: ?ThreadInfo,
+reactions: ReactionInfo,
};
export type ChatMessageItem = { itemType: 'loader' } | ChatMessageInfoItem;
export type ReactionInfo = { +[reaction: string]: MessageReactionInfo };
type MessageReactionInfo = {
+viewerReacted: boolean,
+users: $ReadOnlyArray<RelativeUserInfo>,
};
type TargetMessageReactions = Map<string, Map<string, RelativeUserInfo>>;
const msInFiveMinutes = 5 * 60 * 1000;
function createChatMessageItems(
threadID: string,
messageStore: MessageStore,
messageInfos: { +[id: string]: ?MessageInfo },
threadInfos: { +[id: string]: ThreadInfo },
threadInfoFromSourceMessageID: { +[id: string]: ThreadInfo },
additionalMessages: $ReadOnlyArray<MessageInfo>,
viewerID: string,
): ChatMessageItem[] {
const thread = messageStore.threads[threadID];
const threadMessageInfos = (thread?.messageIDs ?? [])
.map((messageID: string) => messageInfos[messageID])
.filter(Boolean);
const messages =
additionalMessages.length > 0
? sortMessageInfoList([...threadMessageInfos, ...additionalMessages])
: threadMessageInfos;
const targetMessageReactionsMap = new Map<string, TargetMessageReactions>();
// We need to iterate backwards to put the order of messages in chronological
// order, starting with the oldest. This avoids the scenario where the most
// recent message with the remove_reaction action may try to remove a user
// that hasn't been added to the messageReactionUsersInfoMap, causing it
// to be skipped.
for (let i = messages.length - 1; i >= 0; i--) {
const messageInfo = messages[i];
if (messageInfo.type !== messageTypes.REACTION) {
continue;
}
if (!targetMessageReactionsMap.has(messageInfo.targetMessageID)) {
const reactsMap: TargetMessageReactions = new Map();
targetMessageReactionsMap.set(messageInfo.targetMessageID, reactsMap);
}
const messageReactsMap = targetMessageReactionsMap.get(
messageInfo.targetMessageID,
);
invariant(messageReactsMap, 'messageReactsInfo should be set');
if (!messageReactsMap.has(messageInfo.reaction)) {
const usersInfoMap = new Map<string, RelativeUserInfo>();
messageReactsMap.set(messageInfo.reaction, usersInfoMap);
}
const messageReactionUsersInfoMap = messageReactsMap.get(
messageInfo.reaction,
);
invariant(
messageReactionUsersInfoMap,
'messageReactionUsersInfoMap should be set',
);
if (messageInfo.action === 'add_reaction') {
messageReactionUsersInfoMap.set(
messageInfo.creator.id,
messageInfo.creator,
);
} else {
messageReactionUsersInfoMap.delete(messageInfo.creator.id);
}
}
const chatMessageItems = [];
let lastMessageInfo = null;
for (let i = messages.length - 1; i >= 0; i--) {
const messageInfo = messages[i];
if (messageInfo.type === messageTypes.REACTION) {
continue;
}
const originalMessageInfo =
messageInfo.type === messageTypes.SIDEBAR_SOURCE
? messageInfo.sourceMessage
: messageInfo;
if (isEmptyMediaMessage(originalMessageInfo)) {
continue;
}
let startsConversation = true;
let startsCluster = true;
if (
lastMessageInfo &&
lastMessageInfo.time + msInFiveMinutes > originalMessageInfo.time
) {
startsConversation = false;
if (
isComposableMessageType(lastMessageInfo.type) &&
isComposableMessageType(originalMessageInfo.type) &&
lastMessageInfo.creator.id === originalMessageInfo.creator.id
) {
startsCluster = false;
}
}
if (startsCluster && chatMessageItems.length > 0) {
const lastMessageItem = chatMessageItems[chatMessageItems.length - 1];
invariant(lastMessageItem.itemType === 'message', 'should be message');
lastMessageItem.endsCluster = true;
}
const threadCreatedFromMessage =
messageInfo.id && threadInfos[threadID]?.type !== threadTypes.SIDEBAR
? threadInfoFromSourceMessageID[messageInfo.id]
: undefined;
const renderedReactions: ReactionInfo = (() => {
const result = {};
let messageReactsMap;
if (originalMessageInfo.id) {
messageReactsMap = targetMessageReactionsMap.get(
originalMessageInfo.id,
);
}
if (!messageReactsMap) {
return result;
}
for (const reaction of messageReactsMap.keys()) {
const reactionUsersInfoMap = messageReactsMap.get(reaction);
invariant(reactionUsersInfoMap, 'reactionUsersInfoMap should be set');
if (reactionUsersInfoMap.size === 0) {
continue;
}
const reactionUserInfos = [...reactionUsersInfoMap.values()];
const messageReactionInfo = {
users: reactionUserInfos,
viewerReacted: reactionUsersInfoMap.has(viewerID),
};
result[reaction] = messageReactionInfo;
}
return result;
})();
if (isComposableMessageType(originalMessageInfo.type)) {
// We use these invariants instead of just checking the messageInfo.type
// directly in the conditional above so that isComposableMessageType can
// be the source of truth
invariant(
originalMessageInfo.type === messageTypes.TEXT ||
originalMessageInfo.type === messageTypes.IMAGES ||
originalMessageInfo.type === messageTypes.MULTIMEDIA,
"Flow doesn't understand isComposableMessageType above",
);
const localMessageInfo =
messageStore.local[messageKey(originalMessageInfo)];
chatMessageItems.push({
itemType: 'message',
messageInfoType: 'composable',
messageInfo: originalMessageInfo,
localMessageInfo,
startsConversation,
startsCluster,
endsCluster: false,
threadCreatedFromMessage,
reactions: renderedReactions,
});
} else {
invariant(
originalMessageInfo.type !== messageTypes.TEXT &&
originalMessageInfo.type !== messageTypes.IMAGES &&
originalMessageInfo.type !== messageTypes.MULTIMEDIA,
"Flow doesn't understand isComposableMessageType above",
);
const robotext = robotextForMessageInfo(
originalMessageInfo,
threadInfos[threadID],
);
chatMessageItems.push({
itemType: 'message',
messageInfoType: 'robotext',
messageInfo: originalMessageInfo,
startsConversation,
startsCluster,
endsCluster: false,
threadCreatedFromMessage,
robotext,
reactions: renderedReactions,
});
}
lastMessageInfo = originalMessageInfo;
}
if (chatMessageItems.length > 0) {
const lastMessageItem = chatMessageItems[chatMessageItems.length - 1];
invariant(lastMessageItem.itemType === 'message', 'should be message');
lastMessageItem.endsCluster = true;
}
chatMessageItems.reverse();
const hideSpinner = thread ? thread.startReached : threadIsPending(threadID);
if (hideSpinner) {
return chatMessageItems;
}
return [...chatMessageItems, ({ itemType: 'loader' }: ChatMessageItem)];
}
const baseMessageListData = (
threadID: ?string,
additionalMessages: $ReadOnlyArray<MessageInfo>,
) =>
createSelector(
(state: BaseAppState<*>) => state.messageStore,
messageInfoSelector,
threadInfoSelector,
threadInfoFromSourceMessageIDSelector,
(state: BaseAppState<*>) =>
state.currentUserInfo && state.currentUserInfo.id,
(
messageStore: MessageStore,
messageInfos: { +[id: string]: ?MessageInfo },
threadInfos: { +[id: string]: ThreadInfo },
threadInfoFromSourceMessageID: { +[id: string]: ThreadInfo },
viewerID: ?string,
): ?(ChatMessageItem[]) => {
if (!threadID || !viewerID) {
return null;
}
return createChatMessageItems(
threadID,
messageStore,
messageInfos,
threadInfos,
threadInfoFromSourceMessageID,
additionalMessages,
viewerID,
);
},
);
type MessageListData = ?(ChatMessageItem[]);
const messageListData: (
threadID: ?string,
additionalMessages: $ReadOnlyArray<MessageInfo>,
) => (state: BaseAppState<*>) => MessageListData =
memoize2(baseMessageListData);
-type UseMessageListDataArgs = {
+export type UseMessageListDataArgs = {
+searching: boolean,
+userInfoInputArray: $ReadOnlyArray<AccountUserInfo>,
+threadInfo: ?ThreadInfo,
};
function useMessageListData({
searching,
userInfoInputArray,
threadInfo,
}: UseMessageListDataArgs): MessageListData {
const pendingSidebarSourceMessageInfo = useSelector(state => {
const sourceMessageID = threadInfo?.sourceMessageID;
if (
!threadInfo ||
threadInfo.type !== threadTypes.SIDEBAR ||
!sourceMessageID
) {
return null;
}
const thread = state.messageStore.threads[threadInfo.id];
const messageInfos = messageInfoSelector(state);
const shouldSourceBeAdded =
!thread ||
(thread.startReached &&
thread.messageIDs.every(
id => messageInfos[id]?.type !== messageTypes.SIDEBAR_SOURCE,
));
return shouldSourceBeAdded ? messageInfos[sourceMessageID] : null;
});
invariant(
!pendingSidebarSourceMessageInfo ||
pendingSidebarSourceMessageInfo.type !== messageTypes.SIDEBAR_SOURCE,
'sidebars can not be created from sidebar_source message',
);
const additionalMessages = React.useMemo(
() =>
pendingSidebarSourceMessageInfo ? [pendingSidebarSourceMessageInfo] : [],
[pendingSidebarSourceMessageInfo],
);
const boundMessageListData = useSelector(
messageListData(threadInfo?.id, additionalMessages),
);
return React.useMemo(() => {
if (searching && userInfoInputArray.length === 0) {
return [];
}
return boundMessageListData;
}, [searching, userInfoInputArray.length, boundMessageListData]);
}
export {
messageInfoSelector,
createChatThreadItem,
chatListData,
createChatMessageItems,
messageListData,
useFlattenedChatListData,
useFilteredChatListData,
useMessageListData,
};
diff --git a/native/chat/chat-context-provider.react.js b/native/chat/chat-context-provider.react.js
index 12d9421d4..f80d6a4b0 100644
--- a/native/chat/chat-context-provider.react.js
+++ b/native/chat/chat-context-provider.react.js
@@ -1,170 +1,170 @@
// @flow
import * as React from 'react';
-import type { ChatMessageItem } from 'lib/selectors/chat-selectors.js';
import type { ThreadInfo } from 'lib/types/thread-types.js';
import { ChatContext } from './chat-context.js';
import type { SidebarAnimationType } from './chat-context.js';
import ChatItemHeightMeasurer from './chat-item-height-measurer.react.js';
+import type { NativeChatMessageItem } from './message-data.react.js';
import type { ChatMessageItemWithHeight } from '../types/chat-types.js';
type Props = {
+children: React.Node,
};
export type MeasurementTask = {
- +messages: $ReadOnlyArray<ChatMessageItem>,
+ +messages: $ReadOnlyArray<NativeChatMessageItem>,
+threadInfo: ThreadInfo,
+onMessagesMeasured: (
messagesWithHeight: $ReadOnlyArray<ChatMessageItemWithHeight>,
measuredHeights: $ReadOnlyMap<string, number>,
) => mixed,
+measurerID: number,
+initialMeasuredHeights: ?$ReadOnlyMap<string, number>,
};
function ChatContextProvider(props: Props): React.Node {
const [measurements, setMeasurements] = React.useState<
$ReadOnlyArray<MeasurementTask>,
>([]);
const nextMeasurerID = React.useRef(0);
const measuredHeights = React.useRef<
Map<number, $ReadOnlyMap<string, number>>,
>(new Map());
const measureMessages = React.useCallback(
(
- messages: ?$ReadOnlyArray<ChatMessageItem>,
+ messages: ?$ReadOnlyArray<NativeChatMessageItem>,
threadInfo: ?ThreadInfo,
onMessagesMeasured: ($ReadOnlyArray<ChatMessageItemWithHeight>) => mixed,
measurerID: number,
) => {
if (!threadInfo) {
// When threadInfo is not present, we can't measure the messages: we can
// determine the height, but we can't merge the result as it requires
// threadInfo to be present.
return;
}
if (!messages) {
return;
}
const measureCallback = (
messagesWithHeight: $ReadOnlyArray<ChatMessageItemWithHeight>,
newMeasuredHeights: $ReadOnlyMap<string, number>,
) => {
measuredHeights.current.set(measurerID, newMeasuredHeights);
onMessagesMeasured(messagesWithHeight);
};
let initialMeasuredHeights = null;
const isMeasurementPresent = measuredHeights.current.has(measurerID);
if (!isMeasurementPresent) {
const sourceMeasurerID = measurements.find(
measurement => measurement.threadInfo.id === threadInfo.id,
)?.measurerID;
initialMeasuredHeights = sourceMeasurerID
? measuredHeights.current.get(sourceMeasurerID)
: null;
}
const newMeasurement = {
messages,
threadInfo,
onMessagesMeasured: measureCallback,
measurerID,
initialMeasuredHeights,
};
setMeasurements(prevMeasurements => {
const withoutCurrentMeasurement = prevMeasurements.filter(
measurement => measurement.measurerID !== measurerID,
);
return [...withoutCurrentMeasurement, newMeasurement];
});
},
[measurements],
);
const registerMeasurer = React.useCallback(() => {
const measurerID = nextMeasurerID.current++;
return {
measure: (
- messages: ?$ReadOnlyArray<ChatMessageItem>,
+ messages: ?$ReadOnlyArray<NativeChatMessageItem>,
threadInfo: ?ThreadInfo,
onMessagesMeasured: (
$ReadOnlyArray<ChatMessageItemWithHeight>,
) => mixed,
) =>
measureMessages(messages, threadInfo, onMessagesMeasured, measurerID),
unregister: () => {
setMeasurements(prevMeasurements =>
prevMeasurements.filter(
measurement => measurement.measurerID !== measurerID,
),
);
measuredHeights.current.delete(measurerID);
},
};
}, [measureMessages]);
const [
currentTransitionSidebarSourceID,
setCurrentTransitionSidebarSourceID,
] = React.useState<?string>(null);
const chatInputBarHeights = React.useRef<Map<string, number>>(new Map());
const setChatInputBarHeight = React.useCallback(
(threadID: string, height: number) =>
chatInputBarHeights.current.set(threadID, height),
[],
);
const deleteChatInputBarHeight = React.useCallback(
(threadID: string) => chatInputBarHeights.current.delete(threadID),
[],
);
const [sidebarAnimationType, setSidebarAnimationType] =
React.useState<SidebarAnimationType>('move_source_message');
const contextValue = React.useMemo(
() => ({
registerMeasurer,
currentTransitionSidebarSourceID,
setCurrentTransitionSidebarSourceID,
setChatInputBarHeight,
deleteChatInputBarHeight,
chatInputBarHeights: chatInputBarHeights.current,
sidebarAnimationType,
setSidebarAnimationType,
}),
[
currentTransitionSidebarSourceID,
deleteChatInputBarHeight,
registerMeasurer,
setChatInputBarHeight,
sidebarAnimationType,
],
);
const heightMeasurers = React.useMemo(
() =>
measurements.map(measurement => (
<ChatItemHeightMeasurer
key={measurement.measurerID}
measurement={measurement}
/>
)),
[measurements],
);
return (
<ChatContext.Provider value={contextValue}>
{heightMeasurers}
{props.children}
</ChatContext.Provider>
);
}
export default ChatContextProvider;
diff --git a/native/chat/chat-context.js b/native/chat/chat-context.js
index 1f8e55bed..a23baa972 100644
--- a/native/chat/chat-context.js
+++ b/native/chat/chat-context.js
@@ -1,53 +1,53 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
-import type { ChatMessageItem } from 'lib/selectors/chat-selectors.js';
import type { SetState } from 'lib/types/hook-types.js';
import type { ThreadInfo } from 'lib/types/thread-types.js';
+import type { NativeChatMessageItem } from './message-data.react.js';
import type { ChatMessageItemWithHeight } from '../types/chat-types.js';
export type MessagesMeasurer = (
- ?$ReadOnlyArray<ChatMessageItem>,
+ ?$ReadOnlyArray<NativeChatMessageItem>,
?ThreadInfo,
($ReadOnlyArray<ChatMessageItemWithHeight>) => mixed,
) => void;
export type RegisteredMeasurer = {
+measure: MessagesMeasurer,
+unregister: () => void,
};
export type SidebarAnimationType =
| 'fade_source_message'
| 'move_source_message';
export type ChatContextType = {
+registerMeasurer: () => RegisteredMeasurer,
+currentTransitionSidebarSourceID: ?string,
+setCurrentTransitionSidebarSourceID: SetState<?string>,
+setChatInputBarHeight: (threadID: string, height: number) => mixed,
+deleteChatInputBarHeight: (threadID: string) => mixed,
+chatInputBarHeights: $ReadOnlyMap<string, number>,
+sidebarAnimationType: SidebarAnimationType,
+setSidebarAnimationType: (animationType: SidebarAnimationType) => mixed,
};
const ChatContext: React.Context<?ChatContextType> = React.createContext(null);
function useHeightMeasurer(): MessagesMeasurer {
const chatContext = React.useContext(ChatContext);
invariant(chatContext, 'Chat context should be set');
const measureRegistrationRef = React.useRef();
if (!measureRegistrationRef.current) {
measureRegistrationRef.current = chatContext.registerMeasurer();
}
const measureRegistration = measureRegistrationRef.current;
React.useEffect(() => {
return measureRegistration.unregister;
}, [measureRegistration]);
return measureRegistration.measure;
}
export { ChatContext, useHeightMeasurer };
diff --git a/native/chat/chat-item-height-measurer.react.js b/native/chat/chat-item-height-measurer.react.js
index c52f97c30..53f13a5f6 100644
--- a/native/chat/chat-item-height-measurer.react.js
+++ b/native/chat/chat-item-height-measurer.react.js
@@ -1,181 +1,181 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
-import type { ChatMessageItem } from 'lib/selectors/chat-selectors.js';
import { messageID } from 'lib/shared/message-utils.js';
import { messageTypes, type MessageType } from 'lib/types/message-types.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: ChatMessageItem) => {
+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;
};
-const heightMeasurerDummy = (item: ChatMessageItem) => {
+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: ChatMessageItem, height: ?number) => {
+ (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,
...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,
};
}
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 (
<MessageListContextProvider
threadInfo={threadInfo}
key={measurement.measurerID}
>
<NodeHeightMeasurer
listData={measurement.messages}
itemToID={chatMessageItemKey}
itemToMeasureKey={heightMeasurerKey}
itemToDummy={heightMeasurerDummy}
mergeItemWithHeight={heightMeasurerMergeItem}
allHeightsMeasured={measurement.onMessagesMeasured}
inputState={inputState}
composedMessageMaxWidth={composedMessageMaxWidth}
initialMeasuredHeights={measurement.initialMeasuredHeights}
/>
</MessageListContextProvider>
);
}
const MemoizedChatItemHeightMeasurer: React.ComponentType<Props> =
React.memo<Props>(ChatItemHeightMeasurer);
export default MemoizedChatItemHeightMeasurer;
diff --git a/native/chat/message-data.react.js b/native/chat/message-data.react.js
new file mode 100644
index 000000000..1f26b9ec7
--- /dev/null
+++ b/native/chat/message-data.react.js
@@ -0,0 +1,19 @@
+// @flow
+
+import {
+ type ChatMessageItem,
+ type UseMessageListDataArgs,
+ useMessageListData,
+} from 'lib/selectors/chat-selectors.js';
+
+export type NativeChatMessageItem = ChatMessageItem;
+
+type MessageListData = ?(NativeChatMessageItem[]);
+
+function useNativeMessageListData(
+ args: UseMessageListDataArgs,
+): MessageListData {
+ return useMessageListData(args);
+}
+
+export { useNativeMessageListData };
diff --git a/native/chat/message-list-container.react.js b/native/chat/message-list-container.react.js
index d769bb811..05598301e 100644
--- a/native/chat/message-list-container.react.js
+++ b/native/chat/message-list-container.react.js
@@ -1,375 +1,375 @@
// @flow
import { useNavigationState } from '@react-navigation/native';
import invariant from 'invariant';
import * as React from 'react';
import { View } from 'react-native';
import genesis from 'lib/facts/genesis.js';
-import {
- type ChatMessageItem,
- useMessageListData,
-} from 'lib/selectors/chat-selectors.js';
import { threadInfoSelector } from 'lib/selectors/thread-selectors.js';
import {
userInfoSelectorForPotentialMembers,
userSearchIndexForPotentialMembers,
} from 'lib/selectors/user-selectors.js';
import { getPotentialMemberItems } from 'lib/shared/search-utils.js';
import {
useExistingThreadInfoFinder,
pendingThreadType,
} from 'lib/shared/thread-utils.js';
import type { ThreadInfo } from 'lib/types/thread-types.js';
import type { AccountUserInfo, UserListItem } from 'lib/types/user-types.js';
import { type MessagesMeasurer, useHeightMeasurer } from './chat-context.js';
import { ChatInputBar } from './chat-input-bar.react.js';
import type { ChatNavigationProp } from './chat.react.js';
+import {
+ type NativeChatMessageItem,
+ useNativeMessageListData,
+} from './message-data.react.js';
import MessageListThreadSearch from './message-list-thread-search.react.js';
import { MessageListContextProvider } from './message-list-types.js';
import MessageList from './message-list.react.js';
import ParentThreadHeader from './parent-thread-header.react.js';
import ContentLoading from '../components/content-loading.react.js';
import { InputStateContext } from '../input/input-state.js';
import {
OverlayContext,
type OverlayContextType,
} from '../navigation/overlay-context.js';
import type { NavigationRoute } from '../navigation/route-names.js';
import { ThreadSettingsRouteName } from '../navigation/route-names.js';
import { useSelector } from '../redux/redux-utils.js';
import { type Colors, useColors, useStyles } from '../themes/colors.js';
import type { ChatMessageItemWithHeight } from '../types/chat-types.js';
type BaseProps = {
+navigation: ChatNavigationProp<'MessageList'>,
+route: NavigationRoute<'MessageList'>,
};
type Props = {
...BaseProps,
// Redux state
+usernameInputText: string,
+updateUsernameInput: (text: string) => void,
+userInfoInputArray: $ReadOnlyArray<AccountUserInfo>,
+updateTagInput: (items: $ReadOnlyArray<AccountUserInfo>) => void,
+resolveToUser: (user: AccountUserInfo) => void,
+otherUserInfos: { [id: string]: AccountUserInfo },
+userSearchResults: $ReadOnlyArray<UserListItem>,
+threadInfo: ThreadInfo,
+genesisThreadInfo: ?ThreadInfo,
- +messageListData: ?$ReadOnlyArray<ChatMessageItem>,
+ +messageListData: ?$ReadOnlyArray<NativeChatMessageItem>,
+colors: Colors,
+styles: typeof unboundStyles,
// withOverlayContext
+overlayContext: ?OverlayContextType,
+measureMessages: MessagesMeasurer,
};
type State = {
+listDataWithHeights: ?$ReadOnlyArray<ChatMessageItemWithHeight>,
};
class MessageListContainer extends React.PureComponent<Props, State> {
state: State = {
listDataWithHeights: null,
};
pendingListDataWithHeights: ?$ReadOnlyArray<ChatMessageItemWithHeight>;
get frozen() {
const { overlayContext } = this.props;
invariant(
overlayContext,
'MessageListContainer should have OverlayContext',
);
return overlayContext.scrollBlockingModalStatus !== 'closed';
}
setListData = (
listDataWithHeights: $ReadOnlyArray<ChatMessageItemWithHeight>,
) => {
this.setState({ listDataWithHeights });
};
componentDidMount() {
this.props.measureMessages(
this.props.messageListData,
this.props.threadInfo,
this.setListData,
);
}
componentDidUpdate(prevProps: Props) {
const oldListData = prevProps.messageListData;
const newListData = this.props.messageListData;
if (!newListData && oldListData) {
this.setState({ listDataWithHeights: null });
}
if (
oldListData !== newListData ||
prevProps.threadInfo !== this.props.threadInfo ||
prevProps.measureMessages !== this.props.measureMessages
) {
this.props.measureMessages(
newListData,
this.props.threadInfo,
this.allHeightsMeasured,
);
}
if (!this.frozen && this.pendingListDataWithHeights) {
this.setState({ listDataWithHeights: this.pendingListDataWithHeights });
this.pendingListDataWithHeights = undefined;
}
}
render() {
const { threadInfo, styles } = this.props;
const { listDataWithHeights } = this.state;
const { searching } = this.props.route.params;
let searchComponent = null;
if (searching) {
const { userInfoInputArray, genesisThreadInfo } = this.props;
// It's technically possible for the client to be missing the Genesis
// ThreadInfo when it first opens up (before the server delivers it)
let parentThreadHeader;
if (genesisThreadInfo) {
parentThreadHeader = (
<ParentThreadHeader
parentThreadInfo={genesisThreadInfo}
childThreadType={pendingThreadType(userInfoInputArray.length)}
/>
);
}
searchComponent = (
<>
{parentThreadHeader}
<MessageListThreadSearch
usernameInputText={this.props.usernameInputText}
updateUsernameInput={this.props.updateUsernameInput}
userInfoInputArray={userInfoInputArray}
updateTagInput={this.props.updateTagInput}
resolveToUser={this.props.resolveToUser}
otherUserInfos={this.props.otherUserInfos}
userSearchResults={this.props.userSearchResults}
/>
</>
);
}
const showMessageList =
!searching || this.props.userInfoInputArray.length > 0;
let messageList;
if (showMessageList && listDataWithHeights) {
messageList = (
<MessageList
threadInfo={threadInfo}
messageListData={listDataWithHeights}
navigation={this.props.navigation}
route={this.props.route}
/>
);
} else if (showMessageList) {
messageList = (
<ContentLoading fillType="flex" colors={this.props.colors} />
);
}
const threadContentStyles = showMessageList
? [styles.threadContent]
: [styles.hiddenThreadContent];
const pointerEvents = showMessageList ? 'auto' : 'none';
const threadContent = (
<View style={threadContentStyles} pointerEvents={pointerEvents}>
{messageList}
<ChatInputBar
threadInfo={threadInfo}
navigation={this.props.navigation}
route={this.props.route}
/>
</View>
);
return (
<View style={styles.container}>
{searchComponent}
{threadContent}
</View>
);
}
allHeightsMeasured = (
listDataWithHeights: $ReadOnlyArray<ChatMessageItemWithHeight>,
) => {
if (this.frozen) {
this.pendingListDataWithHeights = listDataWithHeights;
} else {
this.setState({ listDataWithHeights });
}
};
}
const unboundStyles = {
container: {
backgroundColor: 'listBackground',
flex: 1,
},
threadContent: {
flex: 1,
},
hiddenThreadContent: {
height: 0,
opacity: 0,
},
};
const ConnectedMessageListContainer: React.ComponentType<BaseProps> =
React.memo<BaseProps>(function ConnectedMessageListContainer(
props: BaseProps,
) {
const [usernameInputText, setUsernameInputText] = React.useState('');
const [userInfoInputArray, setUserInfoInputArray] = React.useState<
$ReadOnlyArray<AccountUserInfo>,
>([]);
const otherUserInfos = useSelector(userInfoSelectorForPotentialMembers);
const userSearchIndex = useSelector(userSearchIndexForPotentialMembers);
const userSearchResults = React.useMemo(
() =>
getPotentialMemberItems(
usernameInputText,
otherUserInfos,
userSearchIndex,
userInfoInputArray.map(userInfo => userInfo.id),
),
[usernameInputText, otherUserInfos, userSearchIndex, userInfoInputArray],
);
const [baseThreadInfo, setBaseThreadInfo] = React.useState(
props.route.params.threadInfo,
);
const existingThreadInfoFinder =
useExistingThreadInfoFinder(baseThreadInfo);
const isSearching = !!props.route.params.searching;
const threadInfo = React.useMemo(
() =>
existingThreadInfoFinder({
searching: isSearching,
userInfoInputArray,
}),
[existingThreadInfoFinder, isSearching, userInfoInputArray],
);
invariant(
threadInfo,
'threadInfo must be specified in messageListContainer',
);
const inputState = React.useContext(InputStateContext);
invariant(inputState, 'inputState should be set in MessageListContainer');
const isFocused = props.navigation.isFocused();
const { setPendingThreadUpdateHandler } = inputState;
React.useEffect(() => {
if (!isFocused) {
return;
}
setPendingThreadUpdateHandler(threadInfo.id, setBaseThreadInfo);
return () => {
setPendingThreadUpdateHandler(threadInfo.id, undefined);
};
}, [setPendingThreadUpdateHandler, isFocused, threadInfo.id]);
const { setParams } = props.navigation;
const navigationStack = useNavigationState(state => state.routes);
React.useEffect(() => {
const topRoute = navigationStack[navigationStack.length - 1];
if (topRoute?.name !== ThreadSettingsRouteName) {
return;
}
setBaseThreadInfo(threadInfo);
if (isSearching) {
setParams({ searching: false });
}
}, [isSearching, navigationStack, setParams, threadInfo]);
const hideSearch = React.useCallback(() => {
setBaseThreadInfo(threadInfo);
setParams({ searching: false });
}, [setParams, threadInfo]);
React.useEffect(() => {
if (!isSearching) {
return;
}
inputState.registerSendCallback(hideSearch);
return () => inputState.unregisterSendCallback(hideSearch);
}, [hideSearch, inputState, isSearching]);
React.useEffect(() => {
setParams({ threadInfo });
}, [setParams, threadInfo]);
const updateTagInput = React.useCallback(
(input: $ReadOnlyArray<AccountUserInfo>) => setUserInfoInputArray(input),
[],
);
const updateUsernameInput = React.useCallback(
(text: string) => setUsernameInputText(text),
[],
);
const { addReply } = inputState;
const resolveToUser = React.useCallback(
(user: AccountUserInfo) => {
const resolvedThreadInfo = existingThreadInfoFinder({
searching: true,
userInfoInputArray: [user],
});
invariant(
resolvedThreadInfo,
'resolvedThreadInfo must be specified in messageListContainer',
);
addReply('');
setBaseThreadInfo(resolvedThreadInfo);
setParams({ searching: false, threadInfo: resolvedThreadInfo });
},
[setParams, existingThreadInfoFinder, addReply],
);
- const messageListData = useMessageListData({
+ const messageListData = useNativeMessageListData({
searching: isSearching,
userInfoInputArray,
threadInfo,
});
const colors = useColors();
const styles = useStyles(unboundStyles);
const overlayContext = React.useContext(OverlayContext);
const measureMessages = useHeightMeasurer();
const genesisThreadInfo = useSelector(
state => threadInfoSelector(state)[genesis.id],
);
return (
<MessageListContextProvider threadInfo={threadInfo}>
<MessageListContainer
{...props}
usernameInputText={usernameInputText}
updateUsernameInput={updateUsernameInput}
userInfoInputArray={userInfoInputArray}
updateTagInput={updateTagInput}
resolveToUser={resolveToUser}
otherUserInfos={otherUserInfos}
userSearchResults={userSearchResults}
threadInfo={threadInfo}
genesisThreadInfo={genesisThreadInfo}
messageListData={messageListData}
colors={colors}
styles={styles}
overlayContext={overlayContext}
measureMessages={measureMessages}
/>
</MessageListContextProvider>
);
});
export default ConnectedMessageListContainer;
diff --git a/native/chat/utils.js b/native/chat/utils.js
index 6ee21adea..1606c0aea 100644
--- a/native/chat/utils.js
+++ b/native/chat/utils.js
@@ -1,410 +1,412 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import Animated from 'react-native-reanimated';
import { useLoggedInUserInfo } from 'lib/hooks/account-hooks.js';
-import { useMessageListData } from 'lib/selectors/chat-selectors.js';
-import type { ChatMessageItem } from 'lib/selectors/chat-selectors.js';
import { colorIsDark } from 'lib/shared/color-utils.js';
import { messageKey } from 'lib/shared/message-utils.js';
import { viewerIsMember } from 'lib/shared/thread-utils.js';
import type { ThreadInfo } from 'lib/types/thread-types.js';
import { clusterEndHeight, inlineEngagementStyle } from './chat-constants.js';
import { ChatContext, useHeightMeasurer } from './chat-context.js';
import { failedSendHeight } from './failed-send.react.js';
+import {
+ useNativeMessageListData,
+ type NativeChatMessageItem,
+} from './message-data.react.js';
import { authorNameHeight } from './message-header.react.js';
import { multimediaMessageItemHeight } from './multimedia-message-utils.js';
import { getUnresolvedSidebarThreadInfo } from './sidebar-navigation.js';
import textMessageSendFailed from './text-message-send-failed.js';
import { timestampHeight } from './timestamp.react.js';
import { KeyboardContext } from '../keyboard/keyboard-state.js';
import { OverlayContext } from '../navigation/overlay-context.js';
import {
MultimediaMessageTooltipModalRouteName,
RobotextMessageTooltipModalRouteName,
TextMessageTooltipModalRouteName,
} from '../navigation/route-names.js';
import type {
ChatMessageInfoItemWithHeight,
ChatMessageItemWithHeight,
ChatRobotextMessageInfoItemWithHeight,
ChatTextMessageInfoItemWithHeight,
} from '../types/chat-types.js';
import type {
LayoutCoordinates,
VerticalBounds,
} from '../types/layout-types.js';
import type { AnimatedViewStyle } from '../types/styles.js';
/* eslint-disable import/no-named-as-default-member */
const {
Node,
Extrapolate,
interpolateNode,
interpolateColors,
block,
call,
eq,
cond,
sub,
} = Animated;
/* eslint-enable import/no-named-as-default-member */
function textMessageItemHeight(
item: ChatTextMessageInfoItemWithHeight,
): number {
const { messageInfo, contentHeight, startsCluster, endsCluster } = item;
const { isViewer } = messageInfo.creator;
let height = 5 + contentHeight; // 5 from marginBottom in ComposedMessage
if (!isViewer && startsCluster) {
height += authorNameHeight;
}
if (endsCluster) {
height += clusterEndHeight;
}
if (textMessageSendFailed(item)) {
height += failedSendHeight;
}
if (item.threadCreatedFromMessage || Object.keys(item.reactions).length > 0) {
height +=
inlineEngagementStyle.height +
inlineEngagementStyle.marginTop +
inlineEngagementStyle.marginBottom;
}
return height;
}
function robotextMessageItemHeight(
item: ChatRobotextMessageInfoItemWithHeight,
): number {
if (item.threadCreatedFromMessage || Object.keys(item.reactions).length > 0) {
return item.contentHeight + inlineEngagementStyle.height;
}
return item.contentHeight;
}
function messageItemHeight(item: ChatMessageInfoItemWithHeight): number {
let height = 0;
if (item.messageShapeType === 'text') {
height += textMessageItemHeight(item);
} else if (item.messageShapeType === 'multimedia') {
height += multimediaMessageItemHeight(item);
} else {
height += robotextMessageItemHeight(item);
}
if (item.startsConversation) {
height += timestampHeight;
}
return height;
}
function chatMessageItemHeight(item: ChatMessageItemWithHeight): number {
if (item.itemType === 'loader') {
return 56;
}
return messageItemHeight(item);
}
function useMessageTargetParameters(
sourceMessage: ChatMessageInfoItemWithHeight,
initialCoordinates: LayoutCoordinates,
messageListVerticalBounds: VerticalBounds,
currentInputBarHeight: number,
targetInputBarHeight: number,
sidebarThreadInfo: ?ThreadInfo,
): {
+position: number,
+color: string,
} {
- const messageListData = useMessageListData({
+ const messageListData = useNativeMessageListData({
searching: false,
userInfoInputArray: [],
threadInfo: sidebarThreadInfo,
});
const [messagesWithHeight, setMessagesWithHeight] =
React.useState<?$ReadOnlyArray<ChatMessageItemWithHeight>>(null);
const measureMessages = useHeightMeasurer();
React.useEffect(() => {
if (messageListData) {
measureMessages(
messageListData,
sidebarThreadInfo,
setMessagesWithHeight,
);
}
}, [measureMessages, messageListData, sidebarThreadInfo]);
const sourceMessageID = sourceMessage.messageInfo?.id;
const targetDistanceFromBottom = React.useMemo(() => {
if (!messagesWithHeight) {
return 0;
}
let offset = 0;
for (const message of messagesWithHeight) {
offset += chatMessageItemHeight(message);
if (message.messageInfo && message.messageInfo.id === sourceMessageID) {
return offset;
}
}
return (
messageListVerticalBounds.height + chatMessageItemHeight(sourceMessage)
);
}, [
messageListVerticalBounds.height,
messagesWithHeight,
sourceMessage,
sourceMessageID,
]);
if (!sidebarThreadInfo) {
return {
position: 0,
color: sourceMessage.threadInfo.color,
};
}
const authorNameComponentHeight = sourceMessage.messageInfo.creator.isViewer
? 0
: authorNameHeight;
const currentDistanceFromBottom =
messageListVerticalBounds.height +
messageListVerticalBounds.y -
initialCoordinates.y +
timestampHeight +
authorNameComponentHeight +
currentInputBarHeight;
return {
position:
targetDistanceFromBottom +
targetInputBarHeight -
currentDistanceFromBottom,
color: sidebarThreadInfo.color,
};
}
type AnimatedMessageArgs = {
+sourceMessage: ChatMessageInfoItemWithHeight,
+initialCoordinates: LayoutCoordinates,
+messageListVerticalBounds: VerticalBounds,
+progress: Node,
+targetInputBarHeight: ?number,
};
function useAnimatedMessageTooltipButton({
sourceMessage,
initialCoordinates,
messageListVerticalBounds,
progress,
targetInputBarHeight,
}: AnimatedMessageArgs): {
+style: AnimatedViewStyle,
+threadColorOverride: ?Node,
+isThreadColorDarkOverride: ?boolean,
} {
const chatContext = React.useContext(ChatContext);
invariant(chatContext, 'chatContext should be set');
const {
currentTransitionSidebarSourceID,
setCurrentTransitionSidebarSourceID,
chatInputBarHeights,
sidebarAnimationType,
setSidebarAnimationType,
} = chatContext;
const loggedInUserInfo = useLoggedInUserInfo();
const sidebarThreadInfo = React.useMemo(
() => getUnresolvedSidebarThreadInfo({ sourceMessage, loggedInUserInfo }),
[sourceMessage, loggedInUserInfo],
);
const currentInputBarHeight =
chatInputBarHeights.get(sourceMessage.threadInfo.id) ?? 0;
const keyboardState = React.useContext(KeyboardContext);
const newSidebarAnimationType =
!currentInputBarHeight ||
!targetInputBarHeight ||
keyboardState?.keyboardShowing ||
!viewerIsMember(sidebarThreadInfo)
? 'fade_source_message'
: 'move_source_message';
React.useEffect(() => {
setSidebarAnimationType(newSidebarAnimationType);
}, [setSidebarAnimationType, newSidebarAnimationType]);
const { position: targetPosition, color: targetColor } =
useMessageTargetParameters(
sourceMessage,
initialCoordinates,
messageListVerticalBounds,
currentInputBarHeight,
targetInputBarHeight ?? currentInputBarHeight,
sidebarThreadInfo,
);
React.useEffect(() => {
return () => setCurrentTransitionSidebarSourceID(null);
}, [setCurrentTransitionSidebarSourceID]);
const bottom = React.useMemo(
() =>
interpolateNode(progress, {
inputRange: [0.3, 1],
outputRange: [targetPosition, 0],
extrapolate: Extrapolate.CLAMP,
}),
[progress, targetPosition],
);
const [isThreadColorDarkOverride, setThreadColorDarkOverride] =
React.useState<?boolean>(null);
const setThreadColorBrightness = React.useCallback(() => {
const isSourceThreadDark = colorIsDark(sourceMessage.threadInfo.color);
const isTargetThreadDark = colorIsDark(targetColor);
if (isSourceThreadDark !== isTargetThreadDark) {
setThreadColorDarkOverride(isTargetThreadDark);
}
}, [sourceMessage.threadInfo.color, targetColor]);
const threadColorOverride = React.useMemo(() => {
if (
sourceMessage.messageShapeType !== 'text' ||
!currentTransitionSidebarSourceID
) {
return null;
}
return block([
cond(eq(progress, 1), call([], setThreadColorBrightness)),
interpolateColors(progress, {
inputRange: [0, 1],
outputColorRange: [
`#${targetColor}`,
`#${sourceMessage.threadInfo.color}`,
],
}),
]);
}, [
currentTransitionSidebarSourceID,
progress,
setThreadColorBrightness,
sourceMessage.messageShapeType,
sourceMessage.threadInfo.color,
targetColor,
]);
const messageContainerStyle = React.useMemo(() => {
return {
bottom: currentTransitionSidebarSourceID ? bottom : 0,
opacity:
currentTransitionSidebarSourceID &&
sidebarAnimationType === 'fade_source_message'
? 0
: 1,
};
}, [bottom, currentTransitionSidebarSourceID, sidebarAnimationType]);
return {
style: messageContainerStyle,
threadColorOverride,
isThreadColorDarkOverride,
};
}
function getMessageTooltipKey(item: ChatMessageInfoItemWithHeight): string {
return `tooltip|${messageKey(item.messageInfo)}`;
}
function isMessageTooltipKey(key: string): boolean {
return key.startsWith('tooltip|');
}
function useOverlayPosition(item: ChatMessageInfoItemWithHeight) {
const overlayContext = React.useContext(OverlayContext);
invariant(overlayContext, 'should be set');
for (const overlay of overlayContext.visibleOverlays) {
if (
(overlay.routeName === MultimediaMessageTooltipModalRouteName ||
overlay.routeName === TextMessageTooltipModalRouteName ||
overlay.routeName === RobotextMessageTooltipModalRouteName) &&
overlay.routeKey === getMessageTooltipKey(item)
) {
return overlay.position;
}
}
return undefined;
}
function useContentAndHeaderOpacity(
item: ChatMessageInfoItemWithHeight,
): number | Node {
const overlayPosition = useOverlayPosition(item);
const chatContext = React.useContext(ChatContext);
return React.useMemo(
() =>
overlayPosition &&
chatContext?.sidebarAnimationType === 'move_source_message'
? sub(
1,
interpolateNode(overlayPosition, {
inputRange: [0.05, 0.06],
outputRange: [0, 1],
extrapolate: Extrapolate.CLAMP,
}),
)
: 1,
[chatContext?.sidebarAnimationType, overlayPosition],
);
}
function useDeliveryIconOpacity(
item: ChatMessageInfoItemWithHeight,
): number | Node {
const overlayPosition = useOverlayPosition(item);
const chatContext = React.useContext(ChatContext);
return React.useMemo(() => {
if (
!overlayPosition ||
!chatContext?.currentTransitionSidebarSourceID ||
chatContext?.sidebarAnimationType === 'fade_source_message'
) {
return 1;
}
return interpolateNode(overlayPosition, {
inputRange: [0.05, 0.06, 1],
outputRange: [1, 0, 0],
extrapolate: Extrapolate.CLAMP,
});
}, [
chatContext?.currentTransitionSidebarSourceID,
chatContext?.sidebarAnimationType,
overlayPosition,
]);
}
function chatMessageItemKey(
- item: ChatMessageItemWithHeight | ChatMessageItem,
+ item: ChatMessageItemWithHeight | NativeChatMessageItem,
): string {
if (item.itemType === 'loader') {
return 'loader';
}
return messageKey(item.messageInfo);
}
export {
chatMessageItemKey,
chatMessageItemHeight,
useAnimatedMessageTooltipButton,
messageItemHeight,
getMessageTooltipKey,
isMessageTooltipKey,
useContentAndHeaderOpacity,
useDeliveryIconOpacity,
};

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 23, 5:10 AM (1 d, 12 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2560049
Default Alt Text
(59 KB)

Event Timeline