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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,7 +33,7 @@ import {
selectChat,
selectChatFullInfo,
selectChatMessages,
selectCurrentMediaSearch,
selectCurrentSharedMediaSearch,
selectIsCurrentUserPremium,
selectIsRightColumnShown,
selectPeerFullInfo,
@ -186,11 +186,11 @@ const Profile: FC<OwnProps & StateProps> = ({
forceScrollProfileTab,
}) => {
const {
setLocalMediaSearchType,
setSharedMediaSearchType,
loadMoreMembers,
loadCommonChats,
openChat,
searchMediaMessagesLocal,
searchSharedMediaMessages,
openMediaViewer,
openAudioPlayer,
focusMessage,
@ -275,7 +275,7 @@ const Profile: FC<OwnProps & StateProps> = ({
const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds(
loadMoreMembers,
loadCommonChats,
searchMediaMessagesLocal,
searchSharedMediaMessages,
handleLoadPeerStories,
handleLoadStoriesArchive,
tabType,
@ -329,8 +329,8 @@ const Profile: FC<OwnProps & StateProps> = ({
// Update search type when switching tabs or forum topics
useEffect(() => {
setLocalMediaSearchType({ mediaType: tabType as SharedMediaType });
}, [setLocalMediaSearchType, tabType, threadId]);
setSharedMediaSearchType({ mediaType: tabType as SharedMediaType });
}, [setSharedMediaSearchType, tabType, threadId]);
useEffect(() => {
loadProfilePhotos({ profileId });
@ -683,7 +683,7 @@ export default memo(withGlobal<OwnProps>(
const chat = selectChat(global, chatId);
const chatFullInfo = selectChatFullInfo(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 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_LOAD_SLICE = 100;
export const SHARED_MEDIA_SLICE = 42;
export const CHAT_MEDIA_SLICE = 42;
export const MESSAGE_SEARCH_SLICE = 42;
export const GLOBAL_SEARCH_SLICE = 20;
export const GLOBAL_TOPIC_SEARCH_SLICE = 5;

View File

@ -1,12 +1,17 @@
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 { 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 { buildCollectionByKey } from '../../../util/iteratees';
import { buildCollectionByKey, isInsideSortedArrayRange } from '../../../util/iteratees';
import { callApi } from '../../../api/gramjs';
import { getIsSavedDialog, isSameReaction } from '../../helpers';
import { getChatMediaMessageIds, getIsSavedDialog, isSameReaction } from '../../helpers';
import {
addActionHandler, getGlobal, setGlobal,
} from '../../index';
@ -14,16 +19,23 @@ import {
addChatMessagesById,
addChats,
addUsers,
updateLocalMediaSearchResults,
initializeChatMediaSearchResults,
mergeWithChatMediaSearchSegment,
setChatMediaSearchLoading,
updateChatMediaSearchResults,
updateLocalTextSearchResults,
updateSharedMediaSearchResults,
} from '../../reducers';
import {
selectChat,
selectCurrentMediaSearch,
selectCurrentChatMediaSearch,
selectCurrentMessageList,
selectCurrentSharedMediaSearch,
selectCurrentTextSearch,
} from '../../selectors';
const MEDIA_PRELOAD_OFFSET = 9;
addActionHandler('searchTextMessagesLocal', async (global, actions, payload): Promise<void> => {
const { tabId = getCurrentTabId() } = payload || {};
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
@ -86,7 +98,7 @@ addActionHandler('searchTextMessagesLocal', async (global, actions, payload): Pr
setGlobal(global);
});
addActionHandler('searchMediaMessagesLocal', (global, actions, payload): ActionReturnType => {
addActionHandler('searchSharedMediaMessages', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
if (!chatId || !threadId) {
@ -97,7 +109,7 @@ addActionHandler('searchMediaMessagesLocal', (global, actions, payload): ActionR
const realChatId = isSavedDialog ? String(threadId) : chatId;
const chat = selectChat(global, realChatId);
const currentSearch = selectCurrentMediaSearch(global, tabId);
const currentSearch = selectCurrentSharedMediaSearch(global, tabId);
if (!chat || !currentSearch) {
return;
@ -113,6 +125,42 @@ addActionHandler('searchMediaMessagesLocal', (global, actions, payload): ActionR
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> => {
const { timestamp, tabId = getCurrentTabId() } = payload;
@ -177,7 +225,7 @@ async function searchSharedMedia<T extends GlobalState>(
global = getGlobal();
const currentSearch = selectCurrentMediaSearch(global, tabId);
const currentSearch = selectCurrentSharedMediaSearch(global, tabId);
if (!currentSearch) {
return;
}
@ -185,7 +233,7 @@ async function searchSharedMedia<T extends GlobalState>(
global = addChats(global, buildCollectionByKey(chats, 'id'));
global = addUsers(global, buildCollectionByKey(users, 'id'));
global = addChatMessagesById(global, resultChatId, byId);
global = updateLocalMediaSearchResults(
global = updateSharedMediaSearchResults(
global, resultChatId, threadId, type, newFoundIds, totalCount, nextOffsetId, tabId,
);
setGlobal(global);
@ -194,3 +242,172 @@ async function searchSharedMedia<T extends GlobalState>(
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,
updateChat,
updateChatLastMessageId,
updateChatMediaLoadingState,
updateChatMessage,
updateListedIds,
updateMessageTranslations,
@ -109,6 +110,9 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
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 (isLocal && message.isOutgoing && !(message.content?.action) && !storyReplyInfo?.storyId
&& !message.content?.storyData) {

View File

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

View File

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

View File

@ -545,3 +545,10 @@ export function canReplaceMessageMedia(message: ApiMessage, attachment: ApiAttac
|| (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 { updatePeerColors } from '../util/theme';
import { IS_MULTITAB_SUPPORTED } from '../util/windowEnvironment';
import { initializeChatMediaSearchResults } from './reducers/localSearch';
import { updateTabState } from './reducers/tabs';
import { initCache, loadCache } from './cache';
import {
@ -79,6 +80,7 @@ addActionHandler('init', (global, actions, payload): ActionReturnType => {
global = replaceThreadParam(global, chatId, threadId, 'lastViewportIds', undefined);
return;
}
global = initializeChatMediaSearchResults(global, chatId, threadId, tabId);
global = replaceTabThreadParam(
global,
chatId,

View File

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

View File

@ -1,11 +1,15 @@
import type { ApiMessageSearchType, ApiReaction } from '../../api/types';
import type { SharedMediaType, ThreadId } from '../../types';
import type { ApiMessage, ApiMessageSearchType, ApiReaction } from '../../api/types';
import type {
ChatMediaSearchParams, ChatMediaSearchSegment, LoadingState,
SharedMediaType, ThreadId,
} from '../../types';
import type { GlobalState, TabArgs } from '../types';
import { getCurrentTabId } from '../../util/establishMultitabRole';
import { areSortedArraysEqual, unique } from '../../util/iteratees';
import { buildChatThreadKey } from '../helpers';
import { areSortedArraysEqual, areSortedArraysIntersecting, unique } from '../../util/iteratees';
import { buildChatThreadKey, isMediaLoadableInViewer } from '../helpers';
import { selectTabState } from '../selectors';
import { selectChatMediaSearch } from '../selectors/localSearch';
import { updateTabState } from './tabs';
interface TextSearchParams {
@ -18,7 +22,7 @@ interface TextSearchParams {
};
}
interface MediaSearchParams {
interface SharedMediaSearchParams {
currentType?: SharedMediaType;
resultsByType?: Partial<Record<SharedMediaType, {
totalCount?: number;
@ -110,32 +114,32 @@ export function updateLocalTextSearchResults<T extends GlobalState>(
const chatThreadKey = buildChatThreadKey(chatId, threadId);
const { results } = selectTabState(global, tabId).localTextSearch.byChatThreadKey[chatThreadKey] || {};
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;
return replaceLocalTextSearchResults(global, chatId, threadId, foundOrPrevFoundIds, totalCount, nextOffsetId, tabId);
}
function replaceLocalMediaSearch<T extends GlobalState>(
function replaceSharedMediaSearch<T extends GlobalState>(
global: T,
chatId: string,
threadId: ThreadId,
searchParams: MediaSearchParams,
searchParams: SharedMediaSearchParams,
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
const chatThreadKey = buildChatThreadKey(chatId, threadId);
return updateTabState(global, {
localMediaSearch: {
sharedMediaSearch: {
byChatThreadKey: {
...selectTabState(global, tabId).localMediaSearch.byChatThreadKey,
...selectTabState(global, tabId).sharedMediaSearch.byChatThreadKey,
[chatThreadKey]: searchParams,
},
},
}, tabId);
}
export function updateLocalMediaSearchType<T extends GlobalState>(
export function updateSharedMediaSearchType<T extends GlobalState>(
global: T,
chatId: string,
threadId: ThreadId,
@ -144,13 +148,13 @@ export function updateLocalMediaSearchType<T extends GlobalState>(
): T {
const chatThreadKey = buildChatThreadKey(chatId, threadId);
return replaceLocalMediaSearch(global, chatId, threadId, {
...selectTabState(global, tabId).localMediaSearch.byChatThreadKey[chatThreadKey],
return replaceSharedMediaSearch(global, chatId, threadId, {
...selectTabState(global, tabId).sharedMediaSearch.byChatThreadKey[chatThreadKey],
currentType,
}, tabId);
}
export function replaceLocalMediaSearchResults<T extends GlobalState>(
export function replaceSharedMediaSearchResults<T extends GlobalState>(
global: T,
chatId: string,
threadId: ThreadId,
@ -162,10 +166,10 @@ export function replaceLocalMediaSearchResults<T extends GlobalState>(
): T {
const chatThreadKey = buildChatThreadKey(chatId, threadId);
return replaceLocalMediaSearch(global, chatId, threadId, {
...selectTabState(global, tabId).localMediaSearch.byChatThreadKey[chatThreadKey],
return replaceSharedMediaSearch(global, chatId, threadId, {
...selectTabState(global, tabId).sharedMediaSearch.byChatThreadKey[chatThreadKey],
resultsByType: {
...(selectTabState(global, tabId).localMediaSearch.byChatThreadKey[chatThreadKey] || {}).resultsByType,
...(selectTabState(global, tabId).sharedMediaSearch.byChatThreadKey[chatThreadKey] || {}).resultsByType,
[type]: {
foundIds,
totalCount,
@ -175,7 +179,7 @@ export function replaceLocalMediaSearchResults<T extends GlobalState>(
}, tabId);
}
export function updateLocalMediaSearchResults<T extends GlobalState>(
export function updateSharedMediaSearchResults<T extends GlobalState>(
global: T,
chatId: string,
threadId: ThreadId,
@ -187,12 +191,12 @@ export function updateLocalMediaSearchResults<T extends GlobalState>(
): T {
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 foundIds = orderFoundIds(unique(Array.prototype.concat(prevFoundIds, newFoundIds)));
const foundIds = orderFoundIdsByDescending(unique(Array.prototype.concat(prevFoundIds, newFoundIds)));
const foundOrPrevFoundIds = areSortedArraysEqual(prevFoundIds, foundIds) ? prevFoundIds : foundIds;
return replaceLocalMediaSearchResults(
return replaceSharedMediaSearchResults(
global,
chatId,
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);
}
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';
import { isLocalMessageId, type MessageKey } from '../../util/messageKey';
import {
hasMessageTtl, mergeIdRanges, orderHistoryIds, orderPinnedIds,
hasMessageTtl, isMediaLoadableInViewer,
mergeIdRanges, orderHistoryIds, orderPinnedIds,
} from '../helpers';
import {
selectChat,
@ -37,6 +38,7 @@ import {
selectThreadInfo,
selectViewportIds,
} from '../selectors';
import { removeIdFromSearchResults } from './localSearch';
import { updateTabState } from './tabs';
import { clearMessageTranslation } from './translations';
@ -296,9 +298,13 @@ export function deleteChatMessages<T extends GlobalState>(
const updatedThreads = new Map<ThreadId, number[]>();
updatedThreads.set(MAIN_THREAD_ID, messageIds);
const mediaIdsToRemove: number[] = [];
messageIds.forEach((messageId) => {
const message = byId[messageId];
if (!message) return;
if (isMediaLoadableInViewer(message)) {
mediaIdsToRemove.push(messageId);
}
const threadId = selectThreadIdFromMessage(global, message);
if (!threadId || threadId === MAIN_THREAD_ID) {
return;
@ -339,6 +345,10 @@ export function deleteChatMessages<T extends GlobalState>(
}
Object.values(global.byTabId).forEach(({ id: tabId }) => {
mediaIdsToRemove.forEach((mediaId) => {
global = removeIdFromSearchResults(global, chatId, threadId, mediaId, tabId);
});
const viewportIds = selectViewportIds(global, chatId, threadId, tabId);
if (!viewportIds) return;

View File

@ -1,3 +1,4 @@
import type { ThreadId } from '../../types';
import type { GlobalState, TabArgs } from '../types';
import { getCurrentTabId } from '../../util/establishMultitabRole';
@ -23,7 +24,7 @@ export function selectCurrentTextSearch<T extends GlobalState>(
return currentSearch;
}
export function selectCurrentMediaSearch<T extends GlobalState>(
export function selectCurrentSharedMediaSearch<T extends GlobalState>(
global: T,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
@ -34,5 +35,32 @@ export function selectCurrentMediaSearch<T extends GlobalState>(
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,
AudioOrigin,
ChatCreationProgress,
ChatMediaSearchParams,
EmojiKeywords,
FocusDirection,
GlobalSearchContent,
@ -375,7 +376,7 @@ export type TabState = {
}>;
};
localMediaSearch: {
sharedMediaSearch: {
byChatThreadKey: Record<string, {
currentType?: SharedMediaType;
resultsByType?: Partial<Record<SharedMediaType, {
@ -386,6 +387,10 @@ export type TabState = {
}>;
};
chatMediaSearch: {
byChatThreadKey: Record<string, ChatMediaSearchParams>;
};
management: {
progress?: ManagementProgress;
byChatId: Record<string, ManagementState>;
@ -431,6 +436,7 @@ export type TabState = {
playbackRate: number;
isMuted: boolean;
isHidden?: boolean;
withDynamicLoading?: boolean;
};
audioPlayer: {
@ -1280,11 +1286,18 @@ export interface ActionPayloads {
setLocalTextSearchTag: {
tag: ApiReaction | undefined;
} & WithTabId;
setLocalMediaSearchType: {
setSharedMediaSearchType: {
mediaType: SharedMediaType;
} & WithTabId;
searchTextMessagesLocal: WithTabId | undefined;
searchMediaMessagesLocal: WithTabId | undefined;
searchSharedMediaMessages: WithTabId | undefined;
searchChatMediaMessages: {
currentMediaMessageId: number;
direction?: LoadMoreDirection;
chatId?: string;
threadId? : ThreadId;
limit?: number;
} & WithTabId;
searchMessagesByDate: {
timestamp: number;
} & WithTabId;
@ -2414,6 +2427,7 @@ export interface ActionPayloads {
volume?: number;
playbackRate?: number;
isMuted?: boolean;
withDynamicLoading?: boolean;
} & WithTabId;
closeMediaViewer: WithTabId | undefined;
setMediaViewerVolume: {

View File

@ -388,6 +388,22 @@ export type ApiPrivacyKey = 'phoneNumber' | 'addByPhone' | 'lastSeen' | 'profile
'forwards' | 'chatInvite' | 'phoneCall' | 'phoneP2P' | 'bio' | 'birthday';
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 {
Profile,
SharedMedia,

View File

@ -125,6 +125,9 @@ export function areSortedArraysEqual(array1: any[], array2: any[]) {
export function areSortedArraysIntersecting(array1: any[], array2: any[]) {
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[] {
return array.filter((a) => set.has(a));