Media Viewer: Chat media loading (#4606)

This commit is contained in:
Alexander Zinchuk 2024-06-12 18:11:10 +02:00
parent 7803e4dbe2
commit bd2f99efaa
20 changed files with 672 additions and 71 deletions

View File

@ -16,7 +16,9 @@ import {
selectChatMessage, selectChatMessage,
selectChatMessages, selectChatMessages,
selectChatScheduledMessages, selectChatScheduledMessages,
selectCurrentMediaSearch, selectIsChatWithSelf, selectCurrentChatMediaSearch,
selectCurrentSharedMediaSearch,
selectIsChatWithSelf,
selectListedIds, selectListedIds,
selectOutlyingListByMessageId, selectOutlyingListByMessageId,
selectPerformanceSettingsValue, selectPerformanceSettingsValue,
@ -71,6 +73,9 @@ type StateProps = {
isHidden?: boolean; isHidden?: boolean;
withAnimation?: boolean; withAnimation?: boolean;
shouldSkipHistoryAnimations?: boolean; shouldSkipHistoryAnimations?: boolean;
withDynamicLoading?: boolean;
isLoadingMoreMedia?: boolean;
isSynced?: boolean;
}; };
const ANIMATION_DURATION = 250; const ANIMATION_DURATION = 250;
@ -91,6 +96,9 @@ const MediaViewer: FC<StateProps> = ({
withAnimation, withAnimation,
isHidden, isHidden,
shouldSkipHistoryAnimations, shouldSkipHistoryAnimations,
withDynamicLoading,
isLoadingMoreMedia,
isSynced,
}) => { }) => {
const { const {
openMediaViewer, openMediaViewer,
@ -98,6 +106,7 @@ const MediaViewer: FC<StateProps> = ({
openForwardMenu, openForwardMenu,
focusMessage, focusMessage,
toggleChatInfo, toggleChatInfo,
searchChatMediaMessages,
} = getActions(); } = getActions();
const isOpen = Boolean(avatarOwner || mediaId); const isOpen = Boolean(avatarOwner || mediaId);
@ -133,17 +142,14 @@ const MediaViewer: FC<StateProps> = ({
const isVisible = !isHidden && isOpen; const isVisible = !isHidden && isOpen;
/* Navigation */ /* Navigation */
const singleMediaId = webPagePhoto || webPageVideo || actionPhoto ? mediaId : undefined; const singleMediaId = webPagePhoto || webPageVideo || actionPhoto || isGif ? mediaId : undefined;
const mediaIds = useMemo(() => { const mediaIds = useMemo(() => {
if (singleMediaId) { if (singleMediaId) return [singleMediaId];
return [singleMediaId]; if (avatarOwner) return avatarOwner.photos?.map((p, i) => i) || [];
} else if (avatarOwner) { if (withDynamicLoading) return collectionIds || [];
return avatarOwner.photos?.map((p, i) => i) || []; return getChatMediaMessageIds(chatMessages || {}, collectionIds || [], isFromSharedMedia);
} else { }, [singleMediaId, avatarOwner, chatMessages, collectionIds, isFromSharedMedia, withDynamicLoading]);
return getChatMediaMessageIds(chatMessages || {}, collectionIds || [], isFromSharedMedia);
}
}, [singleMediaId, avatarOwner, chatMessages, collectionIds, isFromSharedMedia]);
const selectedMediaIndex = mediaId ? mediaIds.indexOf(mediaId) : -1; const selectedMediaIndex = mediaId ? mediaIds.indexOf(mediaId) : -1;
@ -252,6 +258,7 @@ const MediaViewer: FC<StateProps> = ({
mediaId: id, mediaId: id,
avatarOwnerId: avatarOwner?.id, avatarOwnerId: avatarOwner?.id,
origin: origin!, origin: origin!,
withDynamicLoading,
}, { }, {
forceOnHeavyAnimation: true, forceOnHeavyAnimation: true,
}); });
@ -269,6 +276,11 @@ const MediaViewer: FC<StateProps> = ({
const mediaIdsRef = useStateRef(mediaIds); const mediaIdsRef = useStateRef(mediaIds);
const loadMoreMediaIfNeeded = useLastCallback((activeMediaId?: number) => {
if (!activeMediaId || !withDynamicLoading || isLoadingMoreMedia) return;
searchChatMediaMessages({ chatId, threadId, currentMediaMessageId: activeMediaId });
});
const getMediaId = useLastCallback((fromId?: number, direction?: number): number | undefined => { const getMediaId = useLastCallback((fromId?: number, direction?: number): number | undefined => {
if (fromId === undefined) return undefined; if (fromId === undefined) return undefined;
const mIds = mediaIdsRef.current; const mIds = mediaIdsRef.current;
@ -276,6 +288,8 @@ const MediaViewer: FC<StateProps> = ({
if ((direction === -1 && index > 0) || (direction === 1 && index < mIds.length - 1)) { if ((direction === -1 && index > 0) || (direction === 1 && index < mIds.length - 1)) {
return mIds[index + direction]; return mIds[index + direction];
} }
// Fallback
if (isVisible) loadMoreMediaIfNeeded(fromId);
return undefined; return undefined;
}); });
@ -359,6 +373,9 @@ const MediaViewer: FC<StateProps> = ({
</div> </div>
<MediaViewerSlides <MediaViewerSlides
mediaId={mediaId} mediaId={mediaId}
loadMoreMediaIfNeeded={loadMoreMediaIfNeeded}
isLoadingMoreMedia={isLoadingMoreMedia}
isSynced={isSynced}
getMediaId={getMediaId} getMediaId={getMediaId}
chatId={chatId} chatId={chatId}
isPhoto={isPhoto} isPhoto={isPhoto}
@ -389,10 +406,11 @@ export default memo(withGlobal(
avatarOwnerId, avatarOwnerId,
origin, origin,
isHidden, isHidden,
withDynamicLoading,
} = mediaViewer; } = mediaViewer;
const withAnimation = selectPerformanceSettingsValue(global, 'mediaViewerAnimations'); const withAnimation = selectPerformanceSettingsValue(global, 'mediaViewerAnimations');
const { currentUserId } = global; const { currentUserId, isSynced } = global;
let isChatWithSelf = !!chatId && selectIsChatWithSelf(global, chatId); let isChatWithSelf = !!chatId && selectIsChatWithSelf(global, chatId);
if (origin === MediaViewerOrigin.SearchResult) { if (origin === MediaViewerOrigin.SearchResult) {
@ -466,16 +484,24 @@ export default memo(withGlobal(
} else { } else {
chatMessages = selectChatMessages(global, chatId); chatMessages = selectChatMessages(global, chatId);
} }
let isLoadingMoreMedia = false;
const isOriginInline = origin === MediaViewerOrigin.Inline;
const isOriginAlbum = origin === MediaViewerOrigin.Album;
let collectionIds: number[] | undefined; let collectionIds: number[] | undefined;
if (origin === MediaViewerOrigin.Inline if (withDynamicLoading && (isOriginInline || isOriginAlbum)) {
|| origin === MediaViewerOrigin.Album) { const currentSearch = selectCurrentChatMediaSearch(global);
collectionIds = selectOutlyingListByMessageId(global, chatId, threadId, message.id) isLoadingMoreMedia = Boolean(currentSearch?.isLoading);
|| selectListedIds(global, chatId, threadId); const { foundIds } = (currentSearch?.currentSegment) || {};
collectionIds = foundIds;
} else if (origin === MediaViewerOrigin.SharedMedia) { } else if (origin === MediaViewerOrigin.SharedMedia) {
const currentSearch = selectCurrentMediaSearch(global); const currentSearch = selectCurrentSharedMediaSearch(global);
const { foundIds } = (currentSearch && currentSearch.resultsByType && currentSearch.resultsByType.media) || {}; const { foundIds } = (currentSearch && currentSearch.resultsByType && currentSearch.resultsByType.media) || {};
collectionIds = foundIds; collectionIds = foundIds;
} else if (isOriginInline || isOriginAlbum) {
const outlyingList = selectOutlyingListByMessageId(global, chatId, threadId, message.id);
collectionIds = outlyingList || selectListedIds(global, chatId, threadId);
} }
return { return {
@ -491,6 +517,9 @@ export default memo(withGlobal(
withAnimation, withAnimation,
isHidden, isHidden,
shouldSkipHistoryAnimations, shouldSkipHistoryAnimations,
withDynamicLoading,
isLoadingMoreMedia,
isSynced,
}; };
}, },
)(MediaViewer)); )(MediaViewer));

View File

@ -39,6 +39,9 @@ const { easeOutCubic, easeOutQuart } = timingFunctions;
type OwnProps = { type OwnProps = {
mediaId?: number; mediaId?: number;
loadMoreMediaIfNeeded: (activeMediaId?: number) => void;
isLoadingMoreMedia?: boolean;
isSynced?: boolean;
getMediaId: (fromId?: number, direction?: number) => number | undefined; getMediaId: (fromId?: number, direction?: number) => number | undefined;
isVideo?: boolean; isVideo?: boolean;
isGif?: boolean; isGif?: boolean;
@ -84,6 +87,7 @@ enum SwipeDirection {
const MediaViewerSlides: FC<OwnProps> = ({ const MediaViewerSlides: FC<OwnProps> = ({
mediaId, mediaId,
loadMoreMediaIfNeeded,
getMediaId, getMediaId,
selectMedia, selectMedia,
isVideo, isVideo,
@ -91,6 +95,8 @@ const MediaViewerSlides: FC<OwnProps> = ({
isOpen, isOpen,
withAnimation, withAnimation,
isHidden, isHidden,
isLoadingMoreMedia,
isSynced,
...rest ...rest
}) => { }) => {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
@ -156,6 +162,11 @@ const MediaViewerSlides: FC<OwnProps> = ({
} }
}, [mediaId, setActiveMediaId, transformRef]); }, [mediaId, setActiveMediaId, transformRef]);
useEffect(() => {
if (!isSynced || isLoadingMoreMedia) return;
loadMoreMediaIfNeeded(activeMediaId);
}, [activeMediaId, loadMoreMediaIfNeeded, isSynced, isLoadingMoreMedia]);
useLayoutEffect(() => { useLayoutEffect(() => {
const { x, y, scale } = getTransform(); const { x, y, scale } = getTransform();
lockControls(scale !== 1); lockControls(scale !== 1);
@ -643,6 +654,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
[ [
onClose, onClose,
setTransform, setTransform,
loadMoreMediaIfNeeded,
getMediaId, getMediaId,
windowWidth, windowWidth,
windowHeight, windowHeight,

View File

@ -581,6 +581,8 @@ const Message: FC<OwnProps & StateProps> = ({
handleMediaClick, handleMediaClick,
handleAudioPlay, handleAudioPlay,
handleAlbumMediaClick, handleAlbumMediaClick,
handlePhotoMediaClick,
handleVideoMediaClick,
handleMetaClick, handleMetaClick,
handleTranslationClick, handleTranslationClick,
handleOpenThread, handleOpenThread,
@ -1105,7 +1107,7 @@ const Message: FC<OwnProps & StateProps> = ({
asForwarded={asForwarded} asForwarded={asForwarded}
theme={theme} theme={theme}
forcedWidth={contentWidth} forcedWidth={contentWidth}
onClick={handleMediaClick} onClick={handlePhotoMediaClick}
onCancelUpload={handleCancelUpload} onCancelUpload={handleCancelUpload}
/> />
)} )}
@ -1131,7 +1133,7 @@ const Message: FC<OwnProps & StateProps> = ({
isDownloading={isDownloading} isDownloading={isDownloading}
isProtected={isProtected} isProtected={isProtected}
asForwarded={asForwarded} asForwarded={asForwarded}
onClick={handleMediaClick} onClick={handleVideoMediaClick}
onCancelUpload={handleCancelUpload} onCancelUpload={handleCancelUpload}
/> />
)} )}

View File

@ -51,7 +51,7 @@ export type OwnProps = {
asForwarded?: boolean; asForwarded?: boolean;
isDownloading?: boolean; isDownloading?: boolean;
isProtected?: boolean; isProtected?: boolean;
onClick?: (id: number) => void; onClick?: (id: number, isGif?: boolean) => void;
onCancelUpload?: (message: ApiMessage) => void; onCancelUpload?: (message: ApiMessage) => void;
}; };
@ -191,7 +191,7 @@ const Video: FC<OwnProps> = ({
return; return;
} }
onClick?.(message.id); onClick?.(message.id, video?.isGif);
}); });
const className = buildClassName( const className = buildClassName(

View File

@ -35,7 +35,7 @@ export default function useInnerHandlers(
const { const {
openChat, showNotification, focusMessage, openMediaViewer, openAudioPlayer, openChat, showNotification, focusMessage, openMediaViewer, openAudioPlayer,
markMessagesRead, cancelUploadMedia, sendPollVote, openForwardMenu, markMessagesRead, cancelUploadMedia, sendPollVote, openForwardMenu,
openChatLanguageModal, openThread, openStoryViewer, openChatLanguageModal, openThread, openStoryViewer, searchChatMediaMessages,
} = getActions(); } = getActions();
const { const {
@ -102,17 +102,39 @@ export default function useInnerHandlers(
origin: isScheduled ? MediaViewerOrigin.ScheduledInline : MediaViewerOrigin.Inline, origin: isScheduled ? MediaViewerOrigin.ScheduledInline : MediaViewerOrigin.Inline,
}); });
}); });
const openMediaViewerWithPhotoOrVideo = useLastCallback((withDynamicLoading: boolean): void => {
if (withDynamicLoading) {
searchChatMediaMessages({ chatId, threadId, currentMediaMessageId: messageId });
}
openMediaViewer({
chatId,
threadId,
mediaId: messageId,
origin: isScheduled ? MediaViewerOrigin.ScheduledInline : MediaViewerOrigin.Inline,
withDynamicLoading,
});
});
const handlePhotoMediaClick = useLastCallback((): void => {
const withDynamicLoading = !isScheduled;
openMediaViewerWithPhotoOrVideo(withDynamicLoading);
});
const handleVideoMediaClick = useLastCallback((id: number, isGif?: boolean): void => {
const withDynamicLoading = !isGif && !isScheduled;
openMediaViewerWithPhotoOrVideo(withDynamicLoading);
});
const handleAudioPlay = useLastCallback((): void => { const handleAudioPlay = useLastCallback((): void => {
openAudioPlayer({ chatId, messageId }); openAudioPlayer({ chatId, messageId });
}); });
const handleAlbumMediaClick = useLastCallback((albumMessageId: number): void => { const handleAlbumMediaClick = useLastCallback((albumMessageId: number): void => {
searchChatMediaMessages({ chatId, threadId, currentMediaMessageId: messageId });
openMediaViewer({ openMediaViewer({
chatId, chatId,
threadId, threadId,
mediaId: albumMessageId, mediaId: albumMessageId,
origin: isScheduled ? MediaViewerOrigin.ScheduledAlbum : MediaViewerOrigin.Album, origin: isScheduled ? MediaViewerOrigin.ScheduledAlbum : MediaViewerOrigin.Album,
withDynamicLoading: true,
}); });
}); });
@ -213,6 +235,8 @@ export default function useInnerHandlers(
handleMediaClick, handleMediaClick,
handleAudioPlay, handleAudioPlay,
handleAlbumMediaClick, handleAlbumMediaClick,
handlePhotoMediaClick,
handleVideoMediaClick,
handleMetaClick: selectWithGroupedId, handleMetaClick: selectWithGroupedId,
handleTranslationClick, handleTranslationClick,
handleOpenThread, handleOpenThread,

View File

@ -33,7 +33,7 @@ import {
selectChat, selectChat,
selectChatFullInfo, selectChatFullInfo,
selectChatMessages, selectChatMessages,
selectCurrentMediaSearch, selectCurrentSharedMediaSearch,
selectIsCurrentUserPremium, selectIsCurrentUserPremium,
selectIsRightColumnShown, selectIsRightColumnShown,
selectPeerFullInfo, selectPeerFullInfo,
@ -186,11 +186,11 @@ const Profile: FC<OwnProps & StateProps> = ({
forceScrollProfileTab, forceScrollProfileTab,
}) => { }) => {
const { const {
setLocalMediaSearchType, setSharedMediaSearchType,
loadMoreMembers, loadMoreMembers,
loadCommonChats, loadCommonChats,
openChat, openChat,
searchMediaMessagesLocal, searchSharedMediaMessages,
openMediaViewer, openMediaViewer,
openAudioPlayer, openAudioPlayer,
focusMessage, focusMessage,
@ -275,7 +275,7 @@ const Profile: FC<OwnProps & StateProps> = ({
const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds( const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds(
loadMoreMembers, loadMoreMembers,
loadCommonChats, loadCommonChats,
searchMediaMessagesLocal, searchSharedMediaMessages,
handleLoadPeerStories, handleLoadPeerStories,
handleLoadStoriesArchive, handleLoadStoriesArchive,
tabType, tabType,
@ -329,8 +329,8 @@ const Profile: FC<OwnProps & StateProps> = ({
// Update search type when switching tabs or forum topics // Update search type when switching tabs or forum topics
useEffect(() => { useEffect(() => {
setLocalMediaSearchType({ mediaType: tabType as SharedMediaType }); setSharedMediaSearchType({ mediaType: tabType as SharedMediaType });
}, [setLocalMediaSearchType, tabType, threadId]); }, [setSharedMediaSearchType, tabType, threadId]);
useEffect(() => { useEffect(() => {
loadProfilePhotos({ profileId }); loadProfilePhotos({ profileId });
@ -683,7 +683,7 @@ export default memo(withGlobal<OwnProps>(
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const chatFullInfo = selectChatFullInfo(global, chatId); const chatFullInfo = selectChatFullInfo(global, chatId);
const messagesById = selectChatMessages(global, chatId); const messagesById = selectChatMessages(global, chatId);
const { currentType: mediaSearchType, resultsByType } = selectCurrentMediaSearch(global) || {}; const { currentType: mediaSearchType, resultsByType } = selectCurrentSharedMediaSearch(global) || {};
const { foundIds } = (resultsByType && mediaSearchType && resultsByType[mediaSearchType]) || {}; const { foundIds } = (resultsByType && mediaSearchType && resultsByType[mediaSearchType]) || {};
const isTopicInfo = Boolean(chat?.isForum && threadId && threadId !== MAIN_THREAD_ID); const isTopicInfo = Boolean(chat?.isForum && threadId && threadId !== MAIN_THREAD_ID);

View File

@ -72,6 +72,7 @@ export const TOPIC_HEIGHT_PX = 65;
export const CHAT_LIST_SLICE = isBigScreen ? 30 : 25; export const CHAT_LIST_SLICE = isBigScreen ? 30 : 25;
export const CHAT_LIST_LOAD_SLICE = 100; export const CHAT_LIST_LOAD_SLICE = 100;
export const SHARED_MEDIA_SLICE = 42; export const SHARED_MEDIA_SLICE = 42;
export const CHAT_MEDIA_SLICE = 42;
export const MESSAGE_SEARCH_SLICE = 42; export const MESSAGE_SEARCH_SLICE = 42;
export const GLOBAL_SEARCH_SLICE = 20; export const GLOBAL_SEARCH_SLICE = 20;
export const GLOBAL_TOPIC_SEARCH_SLICE = 5; export const GLOBAL_TOPIC_SEARCH_SLICE = 5;

View File

@ -1,12 +1,17 @@
import type { ApiChat } from '../../../api/types'; import type { ApiChat } from '../../../api/types';
import type { SharedMediaType, ThreadId } from '../../../types'; import type {
ChatMediaSearchParams, ChatMediaSearchSegment, LoadingState, SharedMediaType, ThreadId,
} from '../../../types';
import type { ActionReturnType, GlobalState, TabArgs } from '../../types'; import type { ActionReturnType, GlobalState, TabArgs } from '../../types';
import { LoadMoreDirection } from '../../../types';
import { MESSAGE_SEARCH_SLICE, SHARED_MEDIA_SLICE } from '../../../config'; import {
CHAT_MEDIA_SLICE, MESSAGE_SEARCH_SLICE, SHARED_MEDIA_SLICE,
} from '../../../config';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { buildCollectionByKey } from '../../../util/iteratees'; import { buildCollectionByKey, isInsideSortedArrayRange } from '../../../util/iteratees';
import { callApi } from '../../../api/gramjs'; import { callApi } from '../../../api/gramjs';
import { getIsSavedDialog, isSameReaction } from '../../helpers'; import { getChatMediaMessageIds, getIsSavedDialog, isSameReaction } from '../../helpers';
import { import {
addActionHandler, getGlobal, setGlobal, addActionHandler, getGlobal, setGlobal,
} from '../../index'; } from '../../index';
@ -14,16 +19,23 @@ import {
addChatMessagesById, addChatMessagesById,
addChats, addChats,
addUsers, addUsers,
updateLocalMediaSearchResults, initializeChatMediaSearchResults,
mergeWithChatMediaSearchSegment,
setChatMediaSearchLoading,
updateChatMediaSearchResults,
updateLocalTextSearchResults, updateLocalTextSearchResults,
updateSharedMediaSearchResults,
} from '../../reducers'; } from '../../reducers';
import { import {
selectChat, selectChat,
selectCurrentMediaSearch, selectCurrentChatMediaSearch,
selectCurrentMessageList, selectCurrentMessageList,
selectCurrentSharedMediaSearch,
selectCurrentTextSearch, selectCurrentTextSearch,
} from '../../selectors'; } from '../../selectors';
const MEDIA_PRELOAD_OFFSET = 9;
addActionHandler('searchTextMessagesLocal', async (global, actions, payload): Promise<void> => { addActionHandler('searchTextMessagesLocal', async (global, actions, payload): Promise<void> => {
const { tabId = getCurrentTabId() } = payload || {}; const { tabId = getCurrentTabId() } = payload || {};
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {}; const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
@ -86,7 +98,7 @@ addActionHandler('searchTextMessagesLocal', async (global, actions, payload): Pr
setGlobal(global); setGlobal(global);
}); });
addActionHandler('searchMediaMessagesLocal', (global, actions, payload): ActionReturnType => { addActionHandler('searchSharedMediaMessages', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {}; const { tabId = getCurrentTabId() } = payload || {};
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {}; const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
if (!chatId || !threadId) { if (!chatId || !threadId) {
@ -97,7 +109,7 @@ addActionHandler('searchMediaMessagesLocal', (global, actions, payload): ActionR
const realChatId = isSavedDialog ? String(threadId) : chatId; const realChatId = isSavedDialog ? String(threadId) : chatId;
const chat = selectChat(global, realChatId); const chat = selectChat(global, realChatId);
const currentSearch = selectCurrentMediaSearch(global, tabId); const currentSearch = selectCurrentSharedMediaSearch(global, tabId);
if (!chat || !currentSearch) { if (!chat || !currentSearch) {
return; return;
@ -113,6 +125,42 @@ addActionHandler('searchMediaMessagesLocal', (global, actions, payload): ActionR
void searchSharedMedia(global, chat, threadId, type, offsetId, undefined, isSavedDialog, tabId); void searchSharedMedia(global, chat, threadId, type, offsetId, undefined, isSavedDialog, tabId);
}); });
addActionHandler('searchChatMediaMessages', (global, actions, payload): ActionReturnType => {
const {
chatId, threadId, currentMediaMessageId, limit, direction, tabId = getCurrentTabId(),
} = payload;
if (!chatId || !threadId || !currentMediaMessageId) {
return;
}
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
const realChatId = isSavedDialog ? String(threadId) : chatId;
const chat = selectChat(global, realChatId);
if (!chat) {
return;
}
let currentSearch = selectCurrentChatMediaSearch(global, tabId);
if (!currentSearch) {
global = initializeChatMediaSearchResults(global, chatId, threadId, tabId);
setGlobal(global);
currentSearch = selectCurrentChatMediaSearch(global, tabId);
if (!currentSearch) {
return;
}
}
void searchChatMedia(global,
chat,
threadId,
currentMediaMessageId,
currentSearch,
direction,
isSavedDialog,
limit,
tabId);
});
addActionHandler('searchMessagesByDate', async (global, actions, payload): Promise<void> => { addActionHandler('searchMessagesByDate', async (global, actions, payload): Promise<void> => {
const { timestamp, tabId = getCurrentTabId() } = payload; const { timestamp, tabId = getCurrentTabId() } = payload;
@ -177,7 +225,7 @@ async function searchSharedMedia<T extends GlobalState>(
global = getGlobal(); global = getGlobal();
const currentSearch = selectCurrentMediaSearch(global, tabId); const currentSearch = selectCurrentSharedMediaSearch(global, tabId);
if (!currentSearch) { if (!currentSearch) {
return; return;
} }
@ -185,7 +233,7 @@ async function searchSharedMedia<T extends GlobalState>(
global = addChats(global, buildCollectionByKey(chats, 'id')); global = addChats(global, buildCollectionByKey(chats, 'id'));
global = addUsers(global, buildCollectionByKey(users, 'id')); global = addUsers(global, buildCollectionByKey(users, 'id'));
global = addChatMessagesById(global, resultChatId, byId); global = addChatMessagesById(global, resultChatId, byId);
global = updateLocalMediaSearchResults( global = updateSharedMediaSearchResults(
global, resultChatId, threadId, type, newFoundIds, totalCount, nextOffsetId, tabId, global, resultChatId, threadId, type, newFoundIds, totalCount, nextOffsetId, tabId,
); );
setGlobal(global); setGlobal(global);
@ -194,3 +242,172 @@ async function searchSharedMedia<T extends GlobalState>(
void searchSharedMedia(global, chat, threadId, type, nextOffsetId, true, isSavedDialog, tabId); void searchSharedMedia(global, chat, threadId, type, nextOffsetId, true, isSavedDialog, tabId);
} }
} }
function selectCurrentChatMediaSearchSegment(
params: ChatMediaSearchParams,
currentMediaMessageId: number,
): ChatMediaSearchSegment | undefined {
if (isInsideSortedArrayRange(currentMediaMessageId, params.currentSegment.foundIds)) {
return params.currentSegment;
}
const index = params.segments.findIndex(
(segment) => isInsideSortedArrayRange(currentMediaMessageId, segment.foundIds),
);
if (index === -1) {
if (params.currentSegment && params.currentSegment.foundIds.length) {
params.segments.push(params.currentSegment);
}
return undefined;
}
const result = params.segments.splice(index, 1)[0];
params.segments.push(params.currentSegment);
return result;
}
function calcChatMediaSearchAddOffset(
direction: LoadMoreDirection,
limit: number,
): number {
if (direction === LoadMoreDirection.Backwards) return 0;
if (direction === LoadMoreDirection.Forwards) return -(limit + 1);
return -(Math.round(limit / 2) + 1);
}
function calcChatMediaSearchOffsetId(
direction: LoadMoreDirection,
currentMessageId: number,
segment?: ChatMediaSearchSegment,
) : number {
if (!segment) return currentMessageId;
if (direction === LoadMoreDirection.Backwards) return segment.foundIds[0];
if (direction === LoadMoreDirection.Forwards) return segment.foundIds[segment.foundIds.length - 1];
return currentMessageId;
}
function calcLoadMoreDirection(currentMessageId: number, currentSegment?: ChatMediaSearchSegment) {
if (!currentSegment) return LoadMoreDirection.Around;
const currentSegmentFoundIdsCount = currentSegment.foundIds.length;
const idIndexInSegment = currentSegment.foundIds.indexOf(currentMessageId);
if (idIndexInSegment === -1) return LoadMoreDirection.Around;
if (currentSegment.loadingState.areAllItemsLoadedBackwards
&& currentSegment.loadingState.areAllItemsLoadedForwards) {
return undefined;
}
const halfMediaCount = Math.floor(currentSegmentFoundIdsCount / 2);
const preloadOffset = MEDIA_PRELOAD_OFFSET > halfMediaCount ? 0 : MEDIA_PRELOAD_OFFSET;
const lastMediaIndex = currentSegmentFoundIdsCount - 1;
if (idIndexInSegment <= preloadOffset) {
if (currentSegment.loadingState.areAllItemsLoadedBackwards) return undefined;
return LoadMoreDirection.Backwards;
}
if (idIndexInSegment >= lastMediaIndex - preloadOffset) {
if (currentSegment.loadingState.areAllItemsLoadedForwards) return undefined;
return LoadMoreDirection.Forwards;
}
return undefined;
}
function calcLoadingState(
direction : LoadMoreDirection,
limit : number, newFoundIdsCount : number,
currentSegment?: ChatMediaSearchSegment,
) : LoadingState {
let areAllItemsLoadedForwards = Boolean(currentSegment?.loadingState.areAllItemsLoadedForwards);
let areAllItemsLoadedBackwards = Boolean(currentSegment?.loadingState.areAllItemsLoadedBackwards);
if (newFoundIdsCount < limit) {
if (direction === LoadMoreDirection.Forwards) {
areAllItemsLoadedForwards = true;
} else if (direction === LoadMoreDirection.Backwards) {
areAllItemsLoadedBackwards = true;
}
}
return {
areAllItemsLoadedForwards,
areAllItemsLoadedBackwards,
};
}
async function searchChatMedia<T extends GlobalState>(
global: T,
chat: ApiChat,
threadId: ThreadId,
currentMediaMessageId: number,
chatMediaSearchParams: ChatMediaSearchParams,
direction?: LoadMoreDirection,
isSavedDialog?: boolean,
limit = CHAT_MEDIA_SLICE,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const { isSynced } = global;
if (!isSynced || chatMediaSearchParams.isLoading) {
return;
}
let currentSegment = selectCurrentChatMediaSearchSegment(chatMediaSearchParams, currentMediaMessageId);
if (direction === undefined) {
direction = calcLoadMoreDirection(currentMediaMessageId, currentSegment);
}
if (direction === undefined) {
return;
}
const offsetId = calcChatMediaSearchOffsetId(direction, currentMediaMessageId, currentSegment);
const addOffset = calcChatMediaSearchAddOffset(direction, limit);
const resultChatId = isSavedDialog ? global.currentUserId! : chat.id;
global = setChatMediaSearchLoading(global, resultChatId, threadId, true, tabId);
setGlobal(global);
const result = await callApi('searchMessagesLocal', {
chat,
type: 'media',
limit,
threadId,
offsetId,
isSavedDialog,
addOffset,
});
global = getGlobal();
if (!result) {
global = setChatMediaSearchLoading(global, resultChatId, threadId, false, tabId);
setGlobal(global);
return;
}
const {
chats, users, messages,
} = result;
const byId = buildCollectionByKey(messages, 'id');
const newFoundIds = Object.keys(byId).map(Number);
global = addChats(global, buildCollectionByKey(chats, 'id'));
global = addUsers(global, buildCollectionByKey(users, 'id'));
global = addChatMessagesById(global, resultChatId, byId);
const loadingState = calcLoadingState(direction, limit, newFoundIds.length, currentSegment);
const filteredIds = getChatMediaMessageIds(byId, newFoundIds, false);
currentSegment = mergeWithChatMediaSearchSegment(
filteredIds,
loadingState,
currentSegment,
);
global = updateChatMediaSearchResults(
global, resultChatId, threadId, currentSegment, chatMediaSearchParams, tabId,
);
global = setChatMediaSearchLoading(global, resultChatId, threadId, false, tabId);
setGlobal(global);
}

View File

@ -36,6 +36,7 @@ import {
replaceThreadParam, replaceThreadParam,
updateChat, updateChat,
updateChatLastMessageId, updateChatLastMessageId,
updateChatMediaLoadingState,
updateChatMessage, updateChatMessage,
updateListedIds, updateListedIds,
updateMessageTranslations, updateMessageTranslations,
@ -109,6 +110,9 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
global = updateChatLastMessage(global, chatId, newMessage); global = updateChatLastMessage(global, chatId, newMessage);
} }
const threadId = selectThreadIdFromMessage(global, newMessage);
global = updateChatMediaLoadingState(global, newMessage, chatId, threadId, tabId);
if (selectIsMessageInCurrentMessageList(global, chatId, message as ApiMessage, tabId)) { if (selectIsMessageInCurrentMessageList(global, chatId, message as ApiMessage, tabId)) {
if (isLocal && message.isOutgoing && !(message.content?.action) && !storyReplyInfo?.storyId if (isLocal && message.isOutgoing && !(message.content?.action) && !storyReplyInfo?.storyId
&& !message.content?.storyData) { && !message.content?.storyData) {

View File

@ -6,9 +6,9 @@ import { buildChatThreadKey, isSameReaction } from '../../helpers';
import { addActionHandler } from '../../index'; import { addActionHandler } from '../../index';
import { import {
replaceLocalTextSearchResults, replaceLocalTextSearchResults,
updateLocalMediaSearchType,
updateLocalTextSearch, updateLocalTextSearch,
updateLocalTextSearchTag, updateLocalTextSearchTag,
updateSharedMediaSearchType,
} from '../../reducers'; } from '../../reducers';
import { selectCurrentMessageList, selectTabState } from '../../selectors'; import { selectCurrentMessageList, selectTabState } from '../../selectors';
@ -67,14 +67,14 @@ addActionHandler('setLocalTextSearchTag', (global, actions, payload): ActionRetu
return global; return global;
}); });
addActionHandler('setLocalMediaSearchType', (global, actions, payload): ActionReturnType => { addActionHandler('setSharedMediaSearchType', (global, actions, payload): ActionReturnType => {
const { mediaType, tabId = getCurrentTabId() } = payload; const { mediaType, tabId = getCurrentTabId() } = payload;
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {}; const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
if (!chatId || !threadId) { if (!chatId || !threadId) {
return undefined; return undefined;
} }
return updateLocalMediaSearchType(global, chatId, threadId, mediaType, tabId); return updateSharedMediaSearchType(global, chatId, threadId, mediaType, tabId);
}); });
export function closeLocalTextSearch<T extends GlobalState>( export function closeLocalTextSearch<T extends GlobalState>(

View File

@ -9,7 +9,7 @@ import { selectTabState } from '../../selectors';
addActionHandler('openMediaViewer', (global, actions, payload): ActionReturnType => { addActionHandler('openMediaViewer', (global, actions, payload): ActionReturnType => {
const { const {
chatId, threadId, mediaId, avatarOwnerId, profilePhotoIndex, origin, volume, playbackRate, isMuted, chatId, threadId, mediaId, avatarOwnerId, profilePhotoIndex, origin, volume, playbackRate, isMuted,
tabId = getCurrentTabId(), withDynamicLoading, tabId = getCurrentTabId(),
} = payload; } = payload;
const tabState = selectTabState(global, tabId); const tabState = selectTabState(global, tabId);
@ -32,6 +32,7 @@ addActionHandler('openMediaViewer', (global, actions, payload): ActionReturnType
|| DEFAULT_PLAYBACK_RATE || DEFAULT_PLAYBACK_RATE
), ),
isMuted: isMuted || tabState.mediaViewer.isMuted, isMuted: isMuted || tabState.mediaViewer.isMuted,
withDynamicLoading,
}, },
forwardMessages: {}, forwardMessages: {},
}, tabId); }, tabId);

View File

@ -545,3 +545,10 @@ export function canReplaceMessageMedia(message: ApiMessage, attachment: ApiAttac
|| (isFile && (fileType === 'audio' || fileType === 'file')) || (isFile && (fileType === 'audio' || fileType === 'file'))
); );
} }
export function isMediaLoadableInViewer(newMessage: ApiMessage) {
if (!newMessage.content) return false;
if (newMessage.content.photo) return true;
if (newMessage.content.video && !newMessage.content.video.isRound && !newMessage.content.video.isGif) return true;
return false;
}

View File

@ -12,6 +12,7 @@ import { parseLocationHash } from '../util/routing';
import { clearStoredSession } from '../util/sessions'; import { clearStoredSession } from '../util/sessions';
import { updatePeerColors } from '../util/theme'; import { updatePeerColors } from '../util/theme';
import { IS_MULTITAB_SUPPORTED } from '../util/windowEnvironment'; import { IS_MULTITAB_SUPPORTED } from '../util/windowEnvironment';
import { initializeChatMediaSearchResults } from './reducers/localSearch';
import { updateTabState } from './reducers/tabs'; import { updateTabState } from './reducers/tabs';
import { initCache, loadCache } from './cache'; import { initCache, loadCache } from './cache';
import { import {
@ -79,6 +80,7 @@ addActionHandler('init', (global, actions, payload): ActionReturnType => {
global = replaceThreadParam(global, chatId, threadId, 'lastViewportIds', undefined); global = replaceThreadParam(global, chatId, threadId, 'lastViewportIds', undefined);
return; return;
} }
global = initializeChatMediaSearchResults(global, chatId, threadId, tabId);
global = replaceTabThreadParam( global = replaceTabThreadParam(
global, global,
chatId, chatId,

View File

@ -313,7 +313,11 @@ export const INITIAL_TAB_STATE: TabState = {
byChatThreadKey: {}, byChatThreadKey: {},
}, },
localMediaSearch: { sharedMediaSearch: {
byChatThreadKey: {},
},
chatMediaSearch: {
byChatThreadKey: {}, byChatThreadKey: {},
}, },

View File

@ -1,11 +1,15 @@
import type { ApiMessageSearchType, ApiReaction } from '../../api/types'; import type { ApiMessage, ApiMessageSearchType, ApiReaction } from '../../api/types';
import type { SharedMediaType, ThreadId } from '../../types'; import type {
ChatMediaSearchParams, ChatMediaSearchSegment, LoadingState,
SharedMediaType, ThreadId,
} from '../../types';
import type { GlobalState, TabArgs } from '../types'; import type { GlobalState, TabArgs } from '../types';
import { getCurrentTabId } from '../../util/establishMultitabRole'; import { getCurrentTabId } from '../../util/establishMultitabRole';
import { areSortedArraysEqual, unique } from '../../util/iteratees'; import { areSortedArraysEqual, areSortedArraysIntersecting, unique } from '../../util/iteratees';
import { buildChatThreadKey } from '../helpers'; import { buildChatThreadKey, isMediaLoadableInViewer } from '../helpers';
import { selectTabState } from '../selectors'; import { selectTabState } from '../selectors';
import { selectChatMediaSearch } from '../selectors/localSearch';
import { updateTabState } from './tabs'; import { updateTabState } from './tabs';
interface TextSearchParams { interface TextSearchParams {
@ -18,7 +22,7 @@ interface TextSearchParams {
}; };
} }
interface MediaSearchParams { interface SharedMediaSearchParams {
currentType?: SharedMediaType; currentType?: SharedMediaType;
resultsByType?: Partial<Record<SharedMediaType, { resultsByType?: Partial<Record<SharedMediaType, {
totalCount?: number; totalCount?: number;
@ -110,32 +114,32 @@ export function updateLocalTextSearchResults<T extends GlobalState>(
const chatThreadKey = buildChatThreadKey(chatId, threadId); const chatThreadKey = buildChatThreadKey(chatId, threadId);
const { results } = selectTabState(global, tabId).localTextSearch.byChatThreadKey[chatThreadKey] || {}; const { results } = selectTabState(global, tabId).localTextSearch.byChatThreadKey[chatThreadKey] || {};
const prevFoundIds = (results?.foundIds) || []; const prevFoundIds = (results?.foundIds) || [];
const foundIds = orderFoundIds(unique(Array.prototype.concat(prevFoundIds, newFoundIds))); const foundIds = orderFoundIdsByDescending(unique(Array.prototype.concat(prevFoundIds, newFoundIds)));
const foundOrPrevFoundIds = areSortedArraysEqual(prevFoundIds, foundIds) ? prevFoundIds : foundIds; const foundOrPrevFoundIds = areSortedArraysEqual(prevFoundIds, foundIds) ? prevFoundIds : foundIds;
return replaceLocalTextSearchResults(global, chatId, threadId, foundOrPrevFoundIds, totalCount, nextOffsetId, tabId); return replaceLocalTextSearchResults(global, chatId, threadId, foundOrPrevFoundIds, totalCount, nextOffsetId, tabId);
} }
function replaceLocalMediaSearch<T extends GlobalState>( function replaceSharedMediaSearch<T extends GlobalState>(
global: T, global: T,
chatId: string, chatId: string,
threadId: ThreadId, threadId: ThreadId,
searchParams: MediaSearchParams, searchParams: SharedMediaSearchParams,
...[tabId = getCurrentTabId()]: TabArgs<T> ...[tabId = getCurrentTabId()]: TabArgs<T>
): T { ): T {
const chatThreadKey = buildChatThreadKey(chatId, threadId); const chatThreadKey = buildChatThreadKey(chatId, threadId);
return updateTabState(global, { return updateTabState(global, {
localMediaSearch: { sharedMediaSearch: {
byChatThreadKey: { byChatThreadKey: {
...selectTabState(global, tabId).localMediaSearch.byChatThreadKey, ...selectTabState(global, tabId).sharedMediaSearch.byChatThreadKey,
[chatThreadKey]: searchParams, [chatThreadKey]: searchParams,
}, },
}, },
}, tabId); }, tabId);
} }
export function updateLocalMediaSearchType<T extends GlobalState>( export function updateSharedMediaSearchType<T extends GlobalState>(
global: T, global: T,
chatId: string, chatId: string,
threadId: ThreadId, threadId: ThreadId,
@ -144,13 +148,13 @@ export function updateLocalMediaSearchType<T extends GlobalState>(
): T { ): T {
const chatThreadKey = buildChatThreadKey(chatId, threadId); const chatThreadKey = buildChatThreadKey(chatId, threadId);
return replaceLocalMediaSearch(global, chatId, threadId, { return replaceSharedMediaSearch(global, chatId, threadId, {
...selectTabState(global, tabId).localMediaSearch.byChatThreadKey[chatThreadKey], ...selectTabState(global, tabId).sharedMediaSearch.byChatThreadKey[chatThreadKey],
currentType, currentType,
}, tabId); }, tabId);
} }
export function replaceLocalMediaSearchResults<T extends GlobalState>( export function replaceSharedMediaSearchResults<T extends GlobalState>(
global: T, global: T,
chatId: string, chatId: string,
threadId: ThreadId, threadId: ThreadId,
@ -162,10 +166,10 @@ export function replaceLocalMediaSearchResults<T extends GlobalState>(
): T { ): T {
const chatThreadKey = buildChatThreadKey(chatId, threadId); const chatThreadKey = buildChatThreadKey(chatId, threadId);
return replaceLocalMediaSearch(global, chatId, threadId, { return replaceSharedMediaSearch(global, chatId, threadId, {
...selectTabState(global, tabId).localMediaSearch.byChatThreadKey[chatThreadKey], ...selectTabState(global, tabId).sharedMediaSearch.byChatThreadKey[chatThreadKey],
resultsByType: { resultsByType: {
...(selectTabState(global, tabId).localMediaSearch.byChatThreadKey[chatThreadKey] || {}).resultsByType, ...(selectTabState(global, tabId).sharedMediaSearch.byChatThreadKey[chatThreadKey] || {}).resultsByType,
[type]: { [type]: {
foundIds, foundIds,
totalCount, totalCount,
@ -175,7 +179,7 @@ export function replaceLocalMediaSearchResults<T extends GlobalState>(
}, tabId); }, tabId);
} }
export function updateLocalMediaSearchResults<T extends GlobalState>( export function updateSharedMediaSearchResults<T extends GlobalState>(
global: T, global: T,
chatId: string, chatId: string,
threadId: ThreadId, threadId: ThreadId,
@ -187,12 +191,12 @@ export function updateLocalMediaSearchResults<T extends GlobalState>(
): T { ): T {
const chatThreadKey = buildChatThreadKey(chatId, threadId); const chatThreadKey = buildChatThreadKey(chatId, threadId);
const { resultsByType } = selectTabState(global, tabId).localMediaSearch.byChatThreadKey[chatThreadKey] || {}; const { resultsByType } = selectTabState(global, tabId).sharedMediaSearch.byChatThreadKey[chatThreadKey] || {};
const prevFoundIds = resultsByType?.[type] ? resultsByType[type]!.foundIds : []; const prevFoundIds = resultsByType?.[type] ? resultsByType[type]!.foundIds : [];
const foundIds = orderFoundIds(unique(Array.prototype.concat(prevFoundIds, newFoundIds))); const foundIds = orderFoundIdsByDescending(unique(Array.prototype.concat(prevFoundIds, newFoundIds)));
const foundOrPrevFoundIds = areSortedArraysEqual(prevFoundIds, foundIds) ? prevFoundIds : foundIds; const foundOrPrevFoundIds = areSortedArraysEqual(prevFoundIds, foundIds) ? prevFoundIds : foundIds;
return replaceLocalMediaSearchResults( return replaceSharedMediaSearchResults(
global, global,
chatId, chatId,
threadId, threadId,
@ -204,6 +208,229 @@ export function updateLocalMediaSearchResults<T extends GlobalState>(
); );
} }
function orderFoundIds(listedIds: number[]) { function orderFoundIdsByDescending(listedIds: number[]) {
return listedIds.sort((a, b) => b - a); return listedIds.sort((a, b) => b - a);
} }
function orderFoundIdsByAscending(array: number[]) {
return array.sort((a, b) => a - b);
}
export function mergeWithChatMediaSearchSegment(
foundIds: number[],
loadingState: LoadingState,
segment?: ChatMediaSearchSegment,
)
: ChatMediaSearchSegment {
if (!segment) {
return {
foundIds,
loadingState,
};
}
const mergedFoundIds = orderFoundIdsByAscending(unique(Array.prototype.concat(segment.foundIds, foundIds)));
if (!areSortedArraysEqual(segment.foundIds, foundIds)) {
segment.foundIds = mergedFoundIds;
}
const mergedLoadingState : LoadingState = {
areAllItemsLoadedForwards: loadingState.areAllItemsLoadedForwards
|| segment.loadingState.areAllItemsLoadedForwards,
areAllItemsLoadedBackwards: loadingState.areAllItemsLoadedBackwards
|| segment.loadingState.areAllItemsLoadedBackwards,
};
segment.loadingState = mergedLoadingState;
return segment;
}
function mergeChatMediaSearchSegments(currentSegment: ChatMediaSearchSegment, segments: ChatMediaSearchSegment[]) {
return segments.reduce((acc, segment) => {
const hasIntersection = areSortedArraysIntersecting(segment.foundIds, currentSegment.foundIds);
if (hasIntersection) {
currentSegment = mergeWithChatMediaSearchSegment(
currentSegment.foundIds,
currentSegment.loadingState,
segment,
);
} else {
acc.push(segment);
}
return acc;
}, [] as ChatMediaSearchSegment[]);
}
export function updateChatMediaSearchResults<T extends GlobalState>(
global: T,
chatId: string,
threadId: ThreadId,
currentSegment: ChatMediaSearchSegment,
searchParams: ChatMediaSearchParams,
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
const segments = mergeChatMediaSearchSegments(currentSegment, searchParams.segments);
return replaceChatMediaSearchResults(
global,
chatId,
threadId,
currentSegment,
segments,
tabId,
);
}
function removeIdFromSegment(id: number, segment: ChatMediaSearchSegment): ChatMediaSearchSegment {
const foundIds = segment.foundIds.filter((foundId) => foundId !== id);
return {
...segment,
foundIds,
};
}
function removeIdsFromChatMediaSearchParams(
id: number,
searchParams: ChatMediaSearchParams,
): ChatMediaSearchParams {
const currentSegment = removeIdFromSegment(id, searchParams.currentSegment);
const segments = searchParams.segments.map((segment) => removeIdFromSegment(id, segment));
return {
...searchParams,
currentSegment,
segments,
};
}
export function removeIdFromSearchResults<T extends GlobalState>(
global: T,
chatId: string,
threadId: ThreadId,
id: number,
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
const searchParams = selectChatMediaSearch(global, chatId, threadId, tabId);
if (!searchParams) return global;
const updatedSearchParams = removeIdsFromChatMediaSearchParams(id, searchParams);
return replaceChatMediaSearch(
global,
chatId,
threadId,
updatedSearchParams,
tabId,
);
}
function resetForwardsLoadingStateInParams(
searchParams: ChatMediaSearchParams,
) {
searchParams.currentSegment.loadingState.areAllItemsLoadedForwards = false;
searchParams.segments.forEach((segment) => {
segment.loadingState.areAllItemsLoadedForwards = false;
});
}
export function updateChatMediaLoadingState<T extends GlobalState>(
global: T,
newMessage: ApiMessage,
chatId: string,
threadId: ThreadId,
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
if (!isMediaLoadableInViewer(newMessage)) {
return global;
}
const searchParams = selectChatMediaSearch(global, chatId, threadId, tabId);
if (!searchParams) return global;
resetForwardsLoadingStateInParams(searchParams);
return replaceChatMediaSearch(
global,
chatId,
threadId,
searchParams,
tabId,
);
}
export function initializeChatMediaSearchResults<T extends GlobalState>(
global: T,
chatId: string,
threadId: ThreadId,
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
const loadingState: LoadingState = {
areAllItemsLoadedForwards: false,
areAllItemsLoadedBackwards: false,
};
const currentSegment: ChatMediaSearchSegment = {
foundIds: [],
loadingState,
};
const segments: ChatMediaSearchSegment[] = [];
const isLoading = false;
return replaceChatMediaSearch(global, chatId, threadId, {
currentSegment,
segments,
isLoading,
}, tabId);
}
export function setChatMediaSearchLoading<T extends GlobalState>(
global: T,
chatId: string,
threadId: ThreadId,
isLoading: boolean,
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
const chatThreadKey = buildChatThreadKey(chatId, threadId);
const searchParams = selectTabState(global, tabId).chatMediaSearch.byChatThreadKey[chatThreadKey];
if (!searchParams) {
return global;
}
return replaceChatMediaSearch(global, chatId, threadId, {
...searchParams,
isLoading,
}, tabId);
}
export function replaceChatMediaSearchResults<T extends GlobalState>(
global: T,
chatId: string,
threadId: ThreadId,
currentSegment: ChatMediaSearchSegment,
segments: ChatMediaSearchSegment[],
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
const chatThreadKey = buildChatThreadKey(chatId, threadId);
return replaceChatMediaSearch(global, chatId, threadId, {
...selectTabState(global, tabId).chatMediaSearch.byChatThreadKey[chatThreadKey],
currentSegment,
segments,
}, tabId);
}
function replaceChatMediaSearch<T extends GlobalState>(
global: T,
chatId: string,
threadId: ThreadId,
searchParams: ChatMediaSearchParams,
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
const chatThreadKey = buildChatThreadKey(chatId, threadId);
return updateTabState(global, {
chatMediaSearch: {
byChatThreadKey: {
...selectTabState(global, tabId).chatMediaSearch.byChatThreadKey,
[chatThreadKey]: searchParams,
},
},
}, tabId);
}

View File

@ -16,7 +16,8 @@ import {
} from '../../util/iteratees'; } from '../../util/iteratees';
import { isLocalMessageId, type MessageKey } from '../../util/messageKey'; import { isLocalMessageId, type MessageKey } from '../../util/messageKey';
import { import {
hasMessageTtl, mergeIdRanges, orderHistoryIds, orderPinnedIds, hasMessageTtl, isMediaLoadableInViewer,
mergeIdRanges, orderHistoryIds, orderPinnedIds,
} from '../helpers'; } from '../helpers';
import { import {
selectChat, selectChat,
@ -37,6 +38,7 @@ import {
selectThreadInfo, selectThreadInfo,
selectViewportIds, selectViewportIds,
} from '../selectors'; } from '../selectors';
import { removeIdFromSearchResults } from './localSearch';
import { updateTabState } from './tabs'; import { updateTabState } from './tabs';
import { clearMessageTranslation } from './translations'; import { clearMessageTranslation } from './translations';
@ -296,9 +298,13 @@ export function deleteChatMessages<T extends GlobalState>(
const updatedThreads = new Map<ThreadId, number[]>(); const updatedThreads = new Map<ThreadId, number[]>();
updatedThreads.set(MAIN_THREAD_ID, messageIds); updatedThreads.set(MAIN_THREAD_ID, messageIds);
const mediaIdsToRemove: number[] = [];
messageIds.forEach((messageId) => { messageIds.forEach((messageId) => {
const message = byId[messageId]; const message = byId[messageId];
if (!message) return; if (!message) return;
if (isMediaLoadableInViewer(message)) {
mediaIdsToRemove.push(messageId);
}
const threadId = selectThreadIdFromMessage(global, message); const threadId = selectThreadIdFromMessage(global, message);
if (!threadId || threadId === MAIN_THREAD_ID) { if (!threadId || threadId === MAIN_THREAD_ID) {
return; return;
@ -339,6 +345,10 @@ export function deleteChatMessages<T extends GlobalState>(
} }
Object.values(global.byTabId).forEach(({ id: tabId }) => { Object.values(global.byTabId).forEach(({ id: tabId }) => {
mediaIdsToRemove.forEach((mediaId) => {
global = removeIdFromSearchResults(global, chatId, threadId, mediaId, tabId);
});
const viewportIds = selectViewportIds(global, chatId, threadId, tabId); const viewportIds = selectViewportIds(global, chatId, threadId, tabId);
if (!viewportIds) return; if (!viewportIds) return;

View File

@ -1,3 +1,4 @@
import type { ThreadId } from '../../types';
import type { GlobalState, TabArgs } from '../types'; import type { GlobalState, TabArgs } from '../types';
import { getCurrentTabId } from '../../util/establishMultitabRole'; import { getCurrentTabId } from '../../util/establishMultitabRole';
@ -23,7 +24,7 @@ export function selectCurrentTextSearch<T extends GlobalState>(
return currentSearch; return currentSearch;
} }
export function selectCurrentMediaSearch<T extends GlobalState>( export function selectCurrentSharedMediaSearch<T extends GlobalState>(
global: T, global: T,
...[tabId = getCurrentTabId()]: TabArgs<T> ...[tabId = getCurrentTabId()]: TabArgs<T>
) { ) {
@ -34,5 +35,32 @@ export function selectCurrentMediaSearch<T extends GlobalState>(
const chatThreadKey = buildChatThreadKey(chatId, threadId); const chatThreadKey = buildChatThreadKey(chatId, threadId);
return selectTabState(global, tabId).localMediaSearch.byChatThreadKey[chatThreadKey]; return selectTabState(global, tabId).sharedMediaSearch.byChatThreadKey[chatThreadKey];
}
export function selectCurrentChatMediaSearch<T extends GlobalState>(
global: T,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
if (!chatId || !threadId) {
return undefined;
}
const chatThreadKey = buildChatThreadKey(chatId, threadId);
return selectTabState(global, tabId).chatMediaSearch.byChatThreadKey[chatThreadKey];
}
export function selectChatMediaSearch<T extends GlobalState>(
global: T, chatId?: string, threadId?: ThreadId,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
if (!chatId || !threadId) {
return undefined;
}
const chatThreadKey = buildChatThreadKey(chatId, threadId);
return selectTabState(global, tabId).chatMediaSearch.byChatThreadKey[chatThreadKey];
} }

View File

@ -92,6 +92,7 @@ import type {
ApiPrivacySettings, ApiPrivacySettings,
AudioOrigin, AudioOrigin,
ChatCreationProgress, ChatCreationProgress,
ChatMediaSearchParams,
EmojiKeywords, EmojiKeywords,
FocusDirection, FocusDirection,
GlobalSearchContent, GlobalSearchContent,
@ -375,7 +376,7 @@ export type TabState = {
}>; }>;
}; };
localMediaSearch: { sharedMediaSearch: {
byChatThreadKey: Record<string, { byChatThreadKey: Record<string, {
currentType?: SharedMediaType; currentType?: SharedMediaType;
resultsByType?: Partial<Record<SharedMediaType, { resultsByType?: Partial<Record<SharedMediaType, {
@ -386,6 +387,10 @@ export type TabState = {
}>; }>;
}; };
chatMediaSearch: {
byChatThreadKey: Record<string, ChatMediaSearchParams>;
};
management: { management: {
progress?: ManagementProgress; progress?: ManagementProgress;
byChatId: Record<string, ManagementState>; byChatId: Record<string, ManagementState>;
@ -431,6 +436,7 @@ export type TabState = {
playbackRate: number; playbackRate: number;
isMuted: boolean; isMuted: boolean;
isHidden?: boolean; isHidden?: boolean;
withDynamicLoading?: boolean;
}; };
audioPlayer: { audioPlayer: {
@ -1280,11 +1286,18 @@ export interface ActionPayloads {
setLocalTextSearchTag: { setLocalTextSearchTag: {
tag: ApiReaction | undefined; tag: ApiReaction | undefined;
} & WithTabId; } & WithTabId;
setLocalMediaSearchType: { setSharedMediaSearchType: {
mediaType: SharedMediaType; mediaType: SharedMediaType;
} & WithTabId; } & WithTabId;
searchTextMessagesLocal: WithTabId | undefined; searchTextMessagesLocal: WithTabId | undefined;
searchMediaMessagesLocal: WithTabId | undefined; searchSharedMediaMessages: WithTabId | undefined;
searchChatMediaMessages: {
currentMediaMessageId: number;
direction?: LoadMoreDirection;
chatId?: string;
threadId? : ThreadId;
limit?: number;
} & WithTabId;
searchMessagesByDate: { searchMessagesByDate: {
timestamp: number; timestamp: number;
} & WithTabId; } & WithTabId;
@ -2414,6 +2427,7 @@ export interface ActionPayloads {
volume?: number; volume?: number;
playbackRate?: number; playbackRate?: number;
isMuted?: boolean; isMuted?: boolean;
withDynamicLoading?: boolean;
} & WithTabId; } & WithTabId;
closeMediaViewer: WithTabId | undefined; closeMediaViewer: WithTabId | undefined;
setMediaViewerVolume: { setMediaViewerVolume: {

View File

@ -388,6 +388,22 @@ export type ApiPrivacyKey = 'phoneNumber' | 'addByPhone' | 'lastSeen' | 'profile
'forwards' | 'chatInvite' | 'phoneCall' | 'phoneP2P' | 'bio' | 'birthday'; 'forwards' | 'chatInvite' | 'phoneCall' | 'phoneP2P' | 'bio' | 'birthday';
export type PrivacyVisibility = 'everybody' | 'contacts' | 'closeFriends' | 'nonContacts' | 'nobody'; export type PrivacyVisibility = 'everybody' | 'contacts' | 'closeFriends' | 'nonContacts' | 'nobody';
export interface LoadingState {
areAllItemsLoadedForwards: boolean;
areAllItemsLoadedBackwards: boolean;
}
export interface ChatMediaSearchSegment {
foundIds: number[];
loadingState: LoadingState;
}
export interface ChatMediaSearchParams {
currentSegment: ChatMediaSearchSegment;
segments: ChatMediaSearchSegment[];
isLoading: boolean;
}
export enum ProfileState { export enum ProfileState {
Profile, Profile,
SharedMedia, SharedMedia,

View File

@ -125,6 +125,9 @@ export function areSortedArraysEqual(array1: any[], array2: any[]) {
export function areSortedArraysIntersecting(array1: any[], array2: any[]) { export function areSortedArraysIntersecting(array1: any[], array2: any[]) {
return array1[0] <= array2[array2.length - 1] && array1[array1.length - 1] >= array2[0]; return array1[0] <= array2[array2.length - 1] && array1[array1.length - 1] >= array2[0];
} }
export function isInsideSortedArrayRange(value:any, array: any[]) {
return array[0] <= value && value <= array[array.length - 1];
}
export function findIntersectionWithSet<T>(array: T[], set: Set<T>): T[] { export function findIntersectionWithSet<T>(array: T[], set: Set<T>): T[] {
return array.filter((a) => set.has(a)); return array.filter((a) => set.has(a));