Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3333745
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
59 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Nov 23, 5:10 AM (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2560049
Default Alt Text
(59 KB)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment