Skip to content

Commit

Permalink
[lib][web] Await lastUpdatedTime and update ChatThreadItems if different
Browse files Browse the repository at this point in the history
Summary:
In D13913, I said:

> For now, in this diff we're ignoring any messages whose `MessageSpec.getLastUpdatedTime` returns a `Promise`, and assuming it's the same as if it returned `null`. In later diffs we'll update the logic to be smarter.

In this diff we address this. When the `ThreadStore` changes, `useFilteredChatListData` now has the potential to issue two updates: the first with the "initial" value before any promises are resolved, and the second with the "final" value after resolving all promises.

Depends on D13915

Test Plan: Tested in combination with the rest of the stack. See video in D13918

Reviewers: tomek

Reviewed By: tomek

Differential Revision: https://phab.comm.dev/D13916
  • Loading branch information
Ashoat committed Nov 13, 2024
1 parent ed9e9cf commit 4d3afd3
Show file tree
Hide file tree
Showing 2 changed files with 188 additions and 27 deletions.
200 changes: 181 additions & 19 deletions lib/selectors/chat-selectors.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// @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 { createSelector } from 'reselect';
Expand All @@ -26,6 +23,7 @@ import { messageSpecs } from '../shared/messages/message-specs.js';
import {
getSidebarItems,
getAllInitialSidebarItems,
getAllFinalSidebarItems,
type SidebarItem,
} from '../shared/sidebar-item-utils.js';
import { threadInChatList, threadIsPending } from '../shared/thread-utils.js';
Expand All @@ -44,6 +42,7 @@ import type {
} from '../types/minimally-encoded-thread-permissions-types.js';
import type { BaseAppState } from '../types/redux-types.js';
import { threadTypeIsSidebar } from '../types/thread-types-enum.js';
import type { SidebarInfo, LastUpdatedTimes } from '../types/thread-types.js';
import type {
AccountUserInfo,
RelativeUserInfo,
Expand All @@ -53,15 +52,18 @@ import type { EntityText } from '../utils/entity-text.js';
import memoize2 from '../utils/memoize.js';
import { useSelector } from '../utils/redux-utils.js';

export type ChatThreadItem = {
type ChatThreadItemBase = {
+type: 'chatThreadItem',
+threadInfo: ThreadInfo,
+mostRecentNonLocalMessage: ?string,
+lastUpdatedTime: number,
+lastUpdatedTimeIncludingSidebars: number,
+sidebars: $ReadOnlyArray<SidebarItem>,
+pendingPersonalThreadUserInfo?: UserInfo,
};
export type ChatThreadItem = $ReadOnly<{
...ChatThreadItemBase,
+lastUpdatedTime: number,
+lastUpdatedTimeIncludingSidebars: number,
}>;

const messageInfoSelector: (state: BaseAppState<>) => {
+[id: string]: ?MessageInfo,
Expand All @@ -81,7 +83,14 @@ function isEmptyMediaMessage(messageInfo: MessageInfo): boolean {
);
}

function useCreateChatThreadItem(): ThreadInfo => ChatThreadItem {
type CreatedChatThreadItem = $ReadOnly<{
...ChatThreadItemBase,
...LastUpdatedTimes,
+lastUpdatedTimeIncludingSidebars: Promise<number>,
+lastUpdatedAtLeastTimeIncludingSidebars: number,
+allSidebarInfos: $ReadOnlyArray<SidebarInfo>,
}>;
function useCreateChatThreadItem(): ThreadInfo => CreatedChatThreadItem {
const messageInfos = useSelector(messageInfoSelector);
const sidebarInfos = useSidebarInfos();
const messageStore = useSelector(state => state.messageStore);
Expand All @@ -93,7 +102,7 @@ function useCreateChatThreadItem(): ThreadInfo => ChatThreadItem {
messageStore,
);

const { lastUpdatedAtLeastTime } = getLastUpdatedTimes(
const { lastUpdatedTime, lastUpdatedAtLeastTime } = getLastUpdatedTimes(
threadInfo,
messageStore,
messageInfos,
Expand All @@ -105,17 +114,28 @@ function useCreateChatThreadItem(): ThreadInfo => ChatThreadItem {
? Math.max(lastUpdatedAtLeastTime, sidebars[0].lastUpdatedAtLeastTime)
: lastUpdatedAtLeastTime;

const lastUpdatedTimeIncludingSidebars = (async () => {
if (sidebars.length === 0) {
return await lastUpdatedTime;
}
const [lastUpdatedTimeResult, sidebarLastUpdatedTimeResult] =
await Promise.all([lastUpdatedTime, sidebars[0].lastUpdatedTime]);
return Math.max(lastUpdatedTimeResult, sidebarLastUpdatedTimeResult);
})();

const allInitialSidebarItems = getAllInitialSidebarItems(sidebars);
const sidebarItems = getSidebarItems(allInitialSidebarItems);

return {
type: 'chatThreadItem',
threadInfo,
mostRecentNonLocalMessage,
lastUpdatedTime: lastUpdatedAtLeastTime,
lastUpdatedTimeIncludingSidebars:
lastUpdatedAtLeastTimeIncludingSidebars,
lastUpdatedTime,
lastUpdatedAtLeastTime,
lastUpdatedTimeIncludingSidebars,
lastUpdatedAtLeastTimeIncludingSidebars,
sidebars: sidebarItems,
allSidebarInfos: sidebars,
};
},
[messageInfos, messageStore, sidebarInfos, getLastUpdatedTimes],
Expand All @@ -130,16 +150,158 @@ function useFilteredChatListData(
filterFunction: (threadInfo: ?(ThreadInfo | RawThreadInfo)) => boolean,
): $ReadOnlyArray<ChatThreadItem> {
const threadInfos = useSelector(threadInfoSelector);
const filteredThreadInfos = React.useMemo(
() => Object.values(threadInfos).filter(filterFunction),
[threadInfos, filterFunction],
);
return useChatThreadItems(filteredThreadInfos);
}

const sortFunc = _orderBy('lastUpdatedTimeIncludingSidebars')('desc');
function useChatThreadItems(
threadInfos: $ReadOnlyArray<ThreadInfo>,
): $ReadOnlyArray<ChatThreadItem> {
const getChatThreadItem = useCreateChatThreadItem();
return React.useMemo(
const createdChatThreadItems = React.useMemo(
() => threadInfos.map(getChatThreadItem),
[threadInfos, getChatThreadItem],
);

const initialChatThreadItems = React.useMemo(
() =>
_flow(
_filter(filterFunction),
_map(getChatThreadItem),
_orderBy('lastUpdatedTimeIncludingSidebars')('desc'),
)(threadInfos),
[getChatThreadItem, filterFunction, threadInfos],
createdChatThreadItems.map(createdChatThreadItem => {
const {
allSidebarInfos,
lastUpdatedTime,
lastUpdatedAtLeastTime,
lastUpdatedTimeIncludingSidebars,
lastUpdatedAtLeastTimeIncludingSidebars,
...rest
} = createdChatThreadItem;
return {
...rest,
lastUpdatedTime: lastUpdatedAtLeastTime,
lastUpdatedTimeIncludingSidebars:
lastUpdatedAtLeastTimeIncludingSidebars,
};
}),
[createdChatThreadItems],
);

const [chatThreadItems, setChatThreadItems] = React.useState<
$ReadOnlyArray<ChatThreadItem>,
>(initialChatThreadItems);

const prevCreatedChatThreadItemsRef = React.useRef(createdChatThreadItems);
React.useEffect(() => {
if (createdChatThreadItems === prevCreatedChatThreadItemsRef.current) {
return;
}
prevCreatedChatThreadItemsRef.current = createdChatThreadItems;

setChatThreadItems(initialChatThreadItems);

void (async () => {
const finalChatThreadItems = await Promise.all(
createdChatThreadItems.map(async createdChatThreadItem => {
const {
allSidebarInfos,
lastUpdatedTime: lastUpdatedTimePromise,
lastUpdatedAtLeastTime,
lastUpdatedTimeIncludingSidebars:
lastUpdatedTimeIncludingSidebarsPromise,
lastUpdatedAtLeastTimeIncludingSidebars,
...rest
} = createdChatThreadItem;
const [
lastUpdatedTime,
lastUpdatedTimeIncludingSidebars,
allSidebarItems,
] = await Promise.all([
lastUpdatedTimePromise,
lastUpdatedTimeIncludingSidebarsPromise,
getAllFinalSidebarItems(allSidebarInfos),
]);
const sidebars = getSidebarItems(allSidebarItems);
return {
...rest,
lastUpdatedTime,
lastUpdatedTimeIncludingSidebars,
sidebars,
};
}),
);
if (createdChatThreadItems !== prevCreatedChatThreadItemsRef.current) {
// If these aren't equal, it indicates that the effect has fired again.
// We should discard this result as it is now outdated.
return;
}
// The callback below is basically
// setChatThreadItems(finalChatThreadItems), but it has extra logic to
// preserve objects if they are unchanged.
setChatThreadItems(prevChatThreadItems => {
if (prevChatThreadItems.length !== finalChatThreadItems.length) {
console.log(
'unexpected: prevChatThreadItems.length !== ' +
'finalChatThreadItems.length',
);
return finalChatThreadItems;
}
let somethingChanged = false;
const result: Array<ChatThreadItem> = [];
for (let i = 0; i < prevChatThreadItems.length; i++) {
const prevChatThreadItem = prevChatThreadItems[i];
const newChatThreadItem = finalChatThreadItems[i];
if (
prevChatThreadItem.threadInfo.id !== newChatThreadItem.threadInfo.id
) {
console.log(
'unexpected: prevChatThreadItem.threadInfo.id !== ' +
'newChatThreadItem.threadInfo.id',
);
return finalChatThreadItems;
}
if (
prevChatThreadItem.lastUpdatedTime !==
newChatThreadItem.lastUpdatedTime ||
prevChatThreadItem.lastUpdatedTimeIncludingSidebars !==
newChatThreadItem.lastUpdatedTimeIncludingSidebars ||
prevChatThreadItem.sidebars.length !==
newChatThreadItem.sidebars.length
) {
somethingChanged = true;
result[i] = newChatThreadItem;
continue;
}
const sidebarsMatching = prevChatThreadItem.sidebars.every(
(prevSidebar, j) => {
const newSidebar = newChatThreadItem.sidebars[j];
if (
newSidebar.type !== 'sidebar' ||
prevSidebar.type !== 'sidebar'
) {
return newSidebar.type === prevSidebar.type;
}
return newSidebar.threadInfo.id === prevSidebar.threadInfo.id;
},
);
if (!sidebarsMatching) {
somethingChanged = true;
result[i] = newChatThreadItem;
continue;
}
result[i] = prevChatThreadItem;
}
if (somethingChanged) {
return result;
} else {
return prevChatThreadItems;
}
});
})();
}, [createdChatThreadItems, initialChatThreadItems]);

return React.useMemo(() => sortFunc(chatThreadItems), [chatThreadItems]);
}

export type RobotextChatMessageInfoItem = {
Expand Down Expand Up @@ -604,10 +766,10 @@ function useMessageListData({

export {
messageInfoSelector,
useCreateChatThreadItem,
createChatMessageItems,
messageListData,
useFlattenedChatListData,
useFilteredChatListData,
useChatThreadItems,
useMessageListData,
};
15 changes: 7 additions & 8 deletions web/selectors/chat-selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as React from 'react';

import {
type ChatThreadItem,
useCreateChatThreadItem,
useChatThreadItems,
} from 'lib/selectors/chat-selectors.js';
import { threadInfoSelector } from 'lib/selectors/thread-selectors.js';
import { threadIsPending } from 'lib/shared/thread-utils.js';
Expand All @@ -13,13 +13,12 @@ import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-
import { useSelector } from '../redux/redux-utils.js';

function useChatThreadItem(threadInfo: ?ThreadInfo): ?ChatThreadItem {
const createChatThreadItem = useCreateChatThreadItem();
return React.useMemo(() => {
if (!threadInfo) {
return null;
}
return createChatThreadItem(threadInfo);
}, [createChatThreadItem, threadInfo]);
const threadInfos = React.useMemo(
() => [threadInfo].filter(Boolean),
[threadInfo],
);
const [item] = useChatThreadItems(threadInfos);
return item;
}

function useActiveChatThreadItem(): ?ChatThreadItem {
Expand Down

0 comments on commit 4d3afd3

Please sign in to comment.