(null);
const getCallHistoryGroupsCountRef = useRef(getCallHistoryGroupsCount);
const getCallHistoryGroupsRef = useRef(getCallHistoryGroups);
useEffect(() => {
getCallHistoryGroupsCountRef.current = getCallHistoryGroupsCount;
getCallHistoryGroupsRef.current = getCallHistoryGroups;
}, [getCallHistoryGroupsCount, getCallHistoryGroups]);
useEffect(() => {
const controller = new AbortController();
async function search() {
const options: CallHistoryFilterOptions = {
query: queryInput.toLowerCase().normalize().trim(),
status,
};
let timer = setTimeout(() => {
setSearchState(prevSearchState => {
if (prevSearchState.state === 'init') {
return defaultPendingState;
}
return prevSearchState;
});
timer = setTimeout(() => {
// Show loading indicator after a delay
setSearchState(defaultPendingState);
}, 300);
}, 50);
let results: SearchResults | null = null;
try {
const [count, items] = await Promise.all([
getCallHistoryGroupsCountRef.current(options),
getCallHistoryGroupsRef.current(options, {
offset: 0,
limit: 100, // preloaded rows
}),
]);
results = { count, items };
} catch (error) {
log.error('CallsList#fetchTotal error fetching', error);
}
// Clear the loading indicator timeout
clearTimeout(timer);
// Ignore old requests
if (controller.signal.aborted) {
return;
}
// Only commit the new search state once the results are ready
setSearchState({
state: results == null ? 'rejected' : 'fulfilled',
options,
results,
});
const isUpdatingSameSearch =
prevOptionsRef.current != null &&
isSameOptions(options, prevOptionsRef.current);
// Commit only at the end in case the search was aborted.
prevOptionsRef.current = options;
// Only reset the scroll position to the top when the user has changed the
// search parameters
if (!isUpdatingSameSearch) {
infiniteLoaderRef.current?.resetLoadMoreRowsCache(true);
listRef.current?.scrollToPosition(0);
}
}
drop(search());
return () => {
controller.abort();
};
}, [queryInput, status, callHistoryEdition]);
const loadMoreRows = useCallback(
async (props: IndexRange) => {
const { state, options } = searchState;
if (state !== 'fulfilled') {
return;
}
strictAssert(
options != null,
'options should never be null when status is fulfilled'
);
let { startIndex, stopIndex } = props;
if (startIndex > stopIndex) {
// flip
[startIndex, stopIndex] = [stopIndex, startIndex];
}
const offset = startIndex;
const limit = stopIndex - startIndex + 1;
try {
const groups = await getCallHistoryGroupsRef.current(options, {
offset,
limit,
});
if (searchState.options !== options) {
return;
}
setSearchState(prevSearchState => {
strictAssert(
prevSearchState.results != null,
'results should never be null here'
);
const newItems = prevSearchState.results.items.slice();
newItems.splice(startIndex, stopIndex, ...groups);
return {
...prevSearchState,
results: {
...prevSearchState.results,
items: newItems,
},
};
});
} catch (error) {
log.error('CallsList#loadMoreRows error fetching', error);
}
},
[searchState]
);
const isRowLoaded = useCallback(
(props: Index) => {
return searchState.results?.items[props.index] != null;
},
[searchState]
);
const rowRenderer = useCallback(
({ key, index, style }: ListRowProps) => {
const item = searchState.results?.items.at(index) ?? null;
const conversation = item != null ? getConversation(item.peerId) : null;
if (
searchState.state === 'pending' ||
item == null ||
conversation == null
) {
return (
}
title={
}
subtitleMaxLines={1}
subtitle={
}
/>
);
}
const isSelected =
selectedCallHistoryGroup != null &&
isSameCallHistoryGroup(item, selectedCallHistoryGroup);
const wasMissed =
item.direction === CallDirection.Incoming &&
(item.status === DirectCallStatus.Missed ||
item.status === GroupCallStatus.Missed);
let statusText;
if (wasMissed) {
statusText = i18n('icu:CallsList__ItemCallInfo--Missed');
} else if (item.type === CallType.Group) {
statusText = i18n('icu:CallsList__ItemCallInfo--GroupCall');
} else if (item.direction === CallDirection.Outgoing) {
statusText = i18n('icu:CallsList__ItemCallInfo--Outgoing');
} else if (item.direction === CallDirection.Incoming) {
statusText = i18n('icu:CallsList__ItemCallInfo--Incoming');
} else {
strictAssert(false, 'Cannot format call');
}
return (
}
trailing={
{
if (item.type === CallType.Audio) {
onOutgoingAudioCallInConversation(conversation.id);
} else {
onOutgoingVideoCallInConversation(conversation.id);
}
}}
/>
}
title={
}
subtitleMaxLines={1}
subtitle={
{item.children.length > 1 ? `(${item.children.length}) ` : ''}
{statusText} ·{' '}
}
onClick={() => {
onSelectCallHistoryGroup(conversation.id, item);
}}
/>
);
},
[
hasActiveCall,
searchState,
getConversation,
selectedCallHistoryGroup,
onSelectCallHistoryGroup,
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
i18n,
]
);
const handleSearchInputChange = useCallback(
(event: ChangeEvent) => {
setQueryInput(event.target.value);
},
[]
);
const handleSearchInputClear = useCallback(() => {
setQueryInput('');
}, []);
const handleStatusToggle = useCallback(() => {
setStatus(prevStatus => {
return prevStatus === CallHistoryFilterStatus.All
? CallHistoryFilterStatus.Missed
: CallHistoryFilterStatus.All;
});
}, []);
const filteringByMissed = status === CallHistoryFilterStatus.Missed;
const hasEmptyResults = searchState.results?.count === 0;
const currentQuery = searchState.options?.query ?? '';
return (
<>
{hasEmptyResults && (
{currentQuery === '' ? (
i18n('icu:CallsList__EmptyState--noQuery')
) : (
,
}}
/>
)}
)}
{(ref, size) => {
return (
{size != null && (
{({ onRowsRendered, registerChild }) => {
return (
);
}}
)}
);
}}
>
);
}