Handle scheduled video processing (#5242)

This commit is contained in:
zubiden 2024-12-06 19:43:48 +04:00 committed by Alexander Zinchuk
parent 8bdd24ea76
commit afe9582b3a
20 changed files with 210 additions and 71 deletions

View File

@ -8,6 +8,8 @@ type VirtualFields =
| 'classType' | 'classType'
| 'getBytes'; | 'getBytes';
export type OmitVirtualFields<T> = Omit<T, VirtualFields>;
export function bytesToDataUri(bytes: Buffer, shouldOmitPrefix = false, mimeType: string = 'image/jpeg') { export function bytesToDataUri(bytes: Buffer, shouldOmitPrefix = false, mimeType: string = 'image/jpeg') {
const prefix = shouldOmitPrefix ? '' : `data:${mimeType};base64,`; const prefix = shouldOmitPrefix ? '' : `data:${mimeType};base64,`;
@ -16,7 +18,7 @@ export function bytesToDataUri(bytes: Buffer, shouldOmitPrefix = false, mimeType
export function omitVirtualClassFields<T extends GramJs.VirtualClass<T> & { flags?: any }>( export function omitVirtualClassFields<T extends GramJs.VirtualClass<T> & { flags?: any }>(
instance: T, instance: T,
): Omit<T, VirtualFields> { ): OmitVirtualFields<T> {
const { const {
flags, flags,
CONSTRUCTOR_ID, CONSTRUCTOR_ID,

View File

@ -215,6 +215,7 @@ export function buildApiMessageWithChatId(
const hasComments = mtpMessage.replies?.comments; const hasComments = mtpMessage.replies?.comments;
const senderBoosts = mtpMessage.fromBoostsApplied; const senderBoosts = mtpMessage.fromBoostsApplied;
const factCheck = mtpMessage.factcheck && buildApiFactCheck(mtpMessage.factcheck); const factCheck = mtpMessage.factcheck && buildApiFactCheck(mtpMessage.factcheck);
const isVideoProcessingPending = mtpMessage.videoProcessingPending;
const isInvertedMedia = mtpMessage.invertMedia; const isInvertedMedia = mtpMessage.invertMedia;
@ -262,6 +263,7 @@ export function buildApiMessageWithChatId(
factCheck, factCheck,
effectId: mtpMessage.effect?.toString(), effectId: mtpMessage.effect?.toString(),
isInvertedMedia, isInvertedMedia,
isVideoProcessingPending,
}); });
} }

View File

@ -19,7 +19,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
const { const {
fullUser: { fullUser: {
about, commonChatsCount, pinnedMsgId, botInfo, blocked, about, commonChatsCount, pinnedMsgId, botInfo, blocked,
profilePhoto, voiceMessagesForbidden, premiumGifts, profilePhoto, voiceMessagesForbidden, premiumGifts, hasScheduled,
fallbackPhoto, personalPhoto, translationsDisabled, storiesPinnedAvailable, fallbackPhoto, personalPhoto, translationsDisabled, storiesPinnedAvailable,
contactRequirePremium, businessWorkHours, businessLocation, businessIntro, contactRequirePremium, businessWorkHours, businessLocation, businessIntro,
birthday, personalChannelId, personalChannelMessage, sponsoredEnabled, stargiftsCount, birthday, personalChannelId, personalChannelMessage, sponsoredEnabled, stargiftsCount,
@ -51,6 +51,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
personalChannelMessageId: personalChannelMessage, personalChannelMessageId: personalChannelMessage,
areAdsEnabled: sponsoredEnabled, areAdsEnabled: sponsoredEnabled,
starGiftCount: stargiftsCount, starGiftCount: stargiftsCount,
hasScheduledMessages: hasScheduled,
}; };
} }

View File

@ -511,6 +511,7 @@ async function getFullChatInfo(chatId: string): Promise<FullChatData | undefined
chatPhoto, chatPhoto,
translationsDisabled, translationsDisabled,
reactionsLimit, reactionsLimit,
hasScheduled,
} = result.fullChat; } = result.fullChat;
if (chatPhoto) { if (chatPhoto) {
@ -540,6 +541,7 @@ async function getFullChatInfo(chatId: string): Promise<FullChatData | undefined
recentRequesterIds: recentRequesters?.map((userId) => buildApiPeerId(userId, 'user')), recentRequesterIds: recentRequesters?.map((userId) => buildApiPeerId(userId, 'user')),
isTranslationDisabled: translationsDisabled, isTranslationDisabled: translationsDisabled,
isPreHistoryHidden: true, isPreHistoryHidden: true,
hasScheduledMessages: hasScheduled,
}, },
chats, chats,
userStatusesById, userStatusesById,
@ -602,6 +604,7 @@ async function getFullChannelInfo(
boostsUnrestrict, boostsUnrestrict,
canViewRevenue: canViewMonetization, canViewRevenue: canViewMonetization,
paidReactionsAvailable, paidReactionsAvailable,
hasScheduled,
} = result.fullChat; } = result.fullChat;
if (chatPhoto) { if (chatPhoto) {
@ -692,6 +695,7 @@ async function getFullChannelInfo(
boostsApplied, boostsApplied,
boostsToUnrestrict: boostsUnrestrict, boostsToUnrestrict: boostsUnrestrict,
isPaidReactionAvailable: paidReactionsAvailable, isPaidReactionAvailable: paidReactionsAvailable,
hasScheduledMessages: hasScheduled,
}, },
chats, chats,
userStatusesById: statusesById, userStatusesById: statusesById,

View File

@ -45,7 +45,7 @@ import { getEmojiOnlyCountForMessage } from '../../../global/helpers/getEmojiOnl
import { fetchFile } from '../../../util/files'; import { fetchFile } from '../../../util/files';
import { compact, split } from '../../../util/iteratees'; import { compact, split } from '../../../util/iteratees';
import { getMessageKey } from '../../../util/keys/messageKey'; import { getMessageKey } from '../../../util/keys/messageKey';
import { getServerTimeOffset } from '../../../util/serverTime'; import { getServerTime, getServerTimeOffset } from '../../../util/serverTime';
import { interpolateArray } from '../../../util/waveform'; import { interpolateArray } from '../../../util/waveform';
import { import {
buildApiChatFromPreview, buildApiChatFromPreview,
@ -1942,22 +1942,42 @@ function handleMultipleLocalMessagesUpdate(
return; return;
} }
update.updates.forEach((u) => { const updateMessageIds = update.updates.filter((u): u is GramJs.UpdateMessageID => (
if (u instanceof GramJs.UpdateMessageID) { u instanceof GramJs.UpdateMessageID
const localMessage = localMessages[u.randomId.toString()]; ));
handleLocalMessageUpdate(localMessage, u);
} else { // Server can return `UpdateNewScheduledMessage` that we currently process as video that requires processing
handleGramJsUpdate(u); updateMessageIds.forEach((updateMessageId) => {
} const updateNewScheduledMessage = update.updates
.find((scheduledUpdate): scheduledUpdate is GramJs.UpdateNewScheduledMessage => {
if (!(scheduledUpdate instanceof GramJs.UpdateNewScheduledMessage)) return false;
return scheduledUpdate.message.id === updateMessageId.id;
});
const localMessage = localMessages[updateMessageId.randomId.toString()];
handleLocalMessageUpdate(localMessage, updateMessageId, updateNewScheduledMessage);
}); });
const otherUpdates = update.updates.filter((u) => {
if (u instanceof GramJs.UpdateMessageID) return false;
if (u instanceof GramJs.UpdateNewScheduledMessage) return false;
return true;
});
handleGramJsUpdate(otherUpdates);
} }
function handleLocalMessageUpdate(localMessage: ApiMessage, update: GramJs.TypeUpdates) { function handleLocalMessageUpdate(
localMessage: ApiMessage, update: GramJs.TypeUpdates, scheduledMessageUpdate?: GramJs.UpdateNewScheduledMessage,
) {
let messageUpdate; let messageUpdate;
if (update instanceof GramJs.UpdateShortSentMessage || update instanceof GramJs.UpdateMessageID) { if (update instanceof GramJs.UpdateShortSentMessage || update instanceof GramJs.UpdateMessageID) {
messageUpdate = update; messageUpdate = update;
} else if ('updates' in update) { } else if ('updates' in update) {
messageUpdate = update.updates.find((u): u is GramJs.UpdateMessageID => u instanceof GramJs.UpdateMessageID); messageUpdate = update.updates.find((u): u is GramJs.UpdateMessageID => u instanceof GramJs.UpdateMessageID);
scheduledMessageUpdate = update.updates.find((u): u is GramJs.UpdateNewScheduledMessage => (
u instanceof GramJs.UpdateNewScheduledMessage
));
} }
if (!messageUpdate) { if (!messageUpdate) {
@ -1987,16 +2007,20 @@ function handleLocalMessageUpdate(localMessage: ApiMessage, update: GramJs.TypeU
processMessageAndUpdateThreadInfo(mtpMessage); processMessageAndUpdateThreadInfo(mtpMessage);
} }
// Edge case for "Send When Online" const newScheduledMessage = scheduledMessageUpdate?.message && buildApiMessage(scheduledMessageUpdate.message);
const isSentBefore = 'date' in messageUpdate && messageUpdate.date * 1000 < Date.now() + getServerTimeOffset() * 1000;
sendApiUpdate({ // Edge case for "Send When Online"
'@type': localMessage.isScheduled && !isSentBefore const isSentBefore = 'date' in messageUpdate && messageUpdate.date < getServerTime();
? 'updateScheduledMessageSendSucceeded'
: 'updateMessageSendSucceeded', if (newScheduledMessage?.isVideoProcessingPending) {
chatId: localMessage.chatId, sendApiUpdate({
localId: localMessage.id, '@type': 'updateVideoProcessingPending',
message: { chatId: localMessage.chatId,
localId: localMessage.id,
newScheduledMessageId: newScheduledMessage?.id,
});
} else {
const updatedMessage: ApiMessage = {
...localMessage, ...localMessage,
...(newContent && { ...(newContent && {
content: { content: {
@ -2007,9 +2031,18 @@ function handleLocalMessageUpdate(localMessage: ApiMessage, update: GramJs.TypeU
id: messageUpdate.id, id: messageUpdate.id,
sendingState: undefined, sendingState: undefined,
...('date' in messageUpdate && { date: messageUpdate.date }), ...('date' in messageUpdate && { date: messageUpdate.date }),
}, };
poll,
}); sendApiUpdate({
'@type': localMessage.isScheduled && !isSentBefore
? 'updateScheduledMessageSendSucceeded'
: 'updateMessageSendSucceeded',
chatId: localMessage.chatId,
localId: localMessage.id,
message: updatedMessage,
poll,
});
}
handleGramJsUpdate(update); handleGramJsUpdate(update);
} }

View File

@ -384,6 +384,7 @@ export function updater(update: Update) {
sendApiUpdate({ sendApiUpdate({
'@type': 'deleteScheduledMessages', '@type': 'deleteScheduledMessages',
ids: update.messages, ids: update.messages,
newIds: update.sentMessages,
chatId: getApiChatIdFromMtpPeer(update.peer), chatId: getApiChatIdFromMtpPeer(update.peer),
}); });
} else if (update instanceof GramJs.UpdateDeleteChannelMessages) { } else if (update instanceof GramJs.UpdateDeleteChannelMessages) {

View File

@ -139,6 +139,7 @@ export interface ApiChatFullInfo {
isTranslationDisabled?: true; isTranslationDisabled?: true;
hasPinnedStories?: boolean; hasPinnedStories?: boolean;
isPaidReactionAvailable?: boolean; isPaidReactionAvailable?: boolean;
hasScheduledMessages?: boolean;
boostsApplied?: number; boostsApplied?: number;
boostsToUnrestrict?: number; boostsToUnrestrict?: number;

View File

@ -754,6 +754,7 @@ export interface ApiMessage {
factCheck?: ApiFactCheck; factCheck?: ApiFactCheck;
effectId?: string; effectId?: string;
isInvertedMedia?: true; isInvertedMedia?: true;
isVideoProcessingPending?: true;
} }
export interface ApiReactions { export interface ApiReactions {

View File

@ -285,6 +285,13 @@ export type ApiUpdateMessageSendSucceeded = {
poll?: ApiPoll; poll?: ApiPoll;
}; };
export type ApiUpdateVideoProcessingPending = {
'@type': 'updateVideoProcessingPending';
chatId: string;
localId: number;
newScheduledMessageId: number;
};
export type ApiUpdateMessageSendFailed = { export type ApiUpdateMessageSendFailed = {
'@type': 'updateMessageSendFailed'; '@type': 'updateMessageSendFailed';
chatId: string; chatId: string;
@ -332,7 +339,8 @@ export type ApiUpdateDeleteMessages = {
export type ApiUpdateDeleteScheduledMessages = { export type ApiUpdateDeleteScheduledMessages = {
'@type': 'deleteScheduledMessages'; '@type': 'deleteScheduledMessages';
ids: number[]; ids: number[];
chatId?: string; newIds?: number[];
chatId: string;
}; };
export type ApiUpdateDeleteHistory = { export type ApiUpdateDeleteHistory = {
@ -801,7 +809,7 @@ export type ApiUpdate = (
ApiUpdateNewMessage | ApiUpdateMessage | ApiUpdateThreadInfo | ApiUpdateCommonBoxMessages | ApiUpdateNewMessage | ApiUpdateMessage | ApiUpdateThreadInfo | ApiUpdateCommonBoxMessages |
ApiUpdateDeleteMessages | ApiUpdateMessagePoll | ApiUpdateMessagePollVote | ApiUpdateDeleteHistory | ApiUpdateDeleteMessages | ApiUpdateMessagePoll | ApiUpdateMessagePollVote | ApiUpdateDeleteHistory |
ApiUpdateMessageSendSucceeded | ApiUpdateMessageSendFailed | ApiUpdateServiceNotification | ApiUpdateMessageSendSucceeded | ApiUpdateMessageSendFailed | ApiUpdateServiceNotification |
ApiDeleteContact | ApiUpdateUser | ApiUpdateUserStatus | ApiUpdateUserFullInfo | ApiDeleteContact | ApiUpdateUser | ApiUpdateUserStatus | ApiUpdateUserFullInfo | ApiUpdateVideoProcessingPending |
ApiUpdateAvatar | ApiUpdateMessageImage | ApiUpdateDraftMessage | ApiUpdateAvatar | ApiUpdateMessageImage | ApiUpdateDraftMessage |
ApiUpdateError | ApiUpdateResetContacts | ApiUpdateStartEmojiInteraction | ApiUpdateError | ApiUpdateResetContacts | ApiUpdateStartEmojiInteraction |
ApiUpdateFavoriteStickers | ApiUpdateStickerSet | ApiUpdateStickerSets | ApiUpdateStickerSetsOrder | ApiUpdateFavoriteStickers | ApiUpdateStickerSet | ApiUpdateStickerSets | ApiUpdateStickerSetsOrder |

View File

@ -60,6 +60,7 @@ export interface ApiUserFullInfo {
businessWorkHours?: ApiBusinessWorkHours; businessWorkHours?: ApiBusinessWorkHours;
businessIntro?: ApiBusinessIntro; businessIntro?: ApiBusinessIntro;
starGiftCount?: number; starGiftCount?: number;
hasScheduledMessages?: boolean;
} }
export type ApiFakeType = 'fake' | 'scam'; export type ApiFakeType = 'fake' | 'scam';

View File

@ -1384,6 +1384,10 @@
"CloseMiniApps" = "Close Mini Apps"; "CloseMiniApps" = "Close Mini Apps";
"DoNotAskAgain" = "Don't ask again"; "DoNotAskAgain" = "Don't ask again";
"PaymentInfoDone" = "Proceed to checkout"; "PaymentInfoDone" = "Proceed to checkout";
"VideoConversionTitle" = "Improving Video...";
"VideoConversionText" = "The video will be published after it's optimized for the best viewing experience.";
"VideoConversionDone" = "Video published.";
"VideoConversionView" = "View";
"BotSuggestedStatusFor" = "Do you want to set this emoji status suggested by **{bot}** for **{duration}**?"; "BotSuggestedStatusFor" = "Do you want to set this emoji status suggested by **{bot}** for **{duration}**?";
"BotSuggestedStatus" = "Do you want to set this emoji status suggested by **{bot}**?"; "BotSuggestedStatus" = "Do you want to set this emoji status suggested by **{bot}**?";
"BotSuggestedStatusTitle" = "Set Emoji Status"; "BotSuggestedStatusTitle" = "Set Emoji Status";

View File

@ -84,7 +84,6 @@ import {
selectPerformanceSettingsValue, selectPerformanceSettingsValue,
selectRequestedDraft, selectRequestedDraft,
selectRequestedDraftFiles, selectRequestedDraftFiles,
selectScheduledIds,
selectTabState, selectTabState,
selectTheme, selectTheme,
selectTopicFromMessage, selectTopicFromMessage,
@ -1771,7 +1770,7 @@ const Composer: FC<OwnProps & StateProps> = ({
onActivate={handleActivateBotCommandMenu} onActivate={handleActivateBotCommandMenu}
ariaLabel="Open bot command keyboard" ariaLabel="Open bot command keyboard"
> >
<i className="icon icon-bot-commands-filled" /> <Icon name="bot-commands-filled" />
</ResponsiveHoverButton> </ResponsiveHoverButton>
)} )}
{canShowSendAs && (sendAsUser || sendAsChat) && ( {canShowSendAs && (sendAsUser || sendAsChat) && (
@ -1866,7 +1865,7 @@ const Composer: FC<OwnProps & StateProps> = ({
onClick={handleAllScheduledClick} onClick={handleAllScheduledClick}
ariaLabel="Open scheduled messages" ariaLabel="Open scheduled messages"
> >
<i className="icon icon-schedule" /> <Icon name="schedule" />
</Button> </Button>
)} )}
{Boolean(botKeyboardMessageId) && !activeVoiceRecording && !editingMessage && ( {Boolean(botKeyboardMessageId) && !activeVoiceRecording && !editingMessage && (
@ -1877,7 +1876,7 @@ const Composer: FC<OwnProps & StateProps> = ({
onActivate={openBotKeyboard} onActivate={openBotKeyboard}
ariaLabel="Open bot command keyboard" ariaLabel="Open bot command keyboard"
> >
<i className="icon icon-bot-command" /> <Icon name="bot-command" />
</ResponsiveHoverButton> </ResponsiveHoverButton>
)} )}
</> </>
@ -1974,7 +1973,7 @@ const Composer: FC<OwnProps & StateProps> = ({
onClick={stopRecordingVoice} onClick={stopRecordingVoice}
ariaLabel="Cancel voice recording" ariaLabel="Cancel voice recording"
> >
<i className="icon icon-delete" /> <Icon name="delete" />
</Button> </Button>
)} )}
{isInStoryViewer && !activeVoiceRecording && ( {isInStoryViewer && !activeVoiceRecording && (
@ -1997,14 +1996,7 @@ const Composer: FC<OwnProps & StateProps> = ({
/> />
)} )}
{(!sentStoryReaction || isSentStoryReactionHeart) && ( {(!sentStoryReaction || isSentStoryReactionHeart) && (
<i <Icon name="heart" className={buildClassName(isSentStoryReactionHeart && 'story-reaction-heart')} />
className={buildClassName(
'icon',
'icon-heart',
isSentStoryReactionHeart && 'story-reaction-heart',
)}
aria-hidden
/>
)} )}
</Button> </Button>
)} )}
@ -2027,11 +2019,11 @@ const Composer: FC<OwnProps & StateProps> = ({
mainButtonState === MainButtonState.Send && canShowCustomSendMenu ? handleContextMenu : undefined mainButtonState === MainButtonState.Send && canShowCustomSendMenu ? handleContextMenu : undefined
} }
> >
<i className="icon icon-send" /> <Icon name="send" />
<i className="icon icon-microphone-alt" /> <Icon name="microphone-alt" />
{onForward && <i className="icon icon-forward" />} {onForward && <Icon name="forward" />}
{isInMessageList && <i className="icon icon-schedule" />} {isInMessageList && <Icon name="schedule" />}
{isInMessageList && <i className="icon icon-check" />} {isInMessageList && <Icon name="check" />}
</Button> </Button>
{effectEmoji && ( {effectEmoji && (
<span className="effect-icon" onClick={handleRemoveEffect}> <span className="effect-icon" onClick={handleRemoveEffect}>
@ -2083,11 +2075,10 @@ export default memo(withGlobal<OwnProps>(
const isChatWithBot = Boolean(chatBot); const isChatWithBot = Boolean(chatBot);
const isChatWithSelf = selectIsChatWithSelf(global, chatId); const isChatWithSelf = selectIsChatWithSelf(global, chatId);
const isChatWithUser = isUserId(chatId); const isChatWithUser = isUserId(chatId);
const chatBotFullInfo = isChatWithBot ? selectUserFullInfo(global, chatBot.id) : undefined; const userFullInfo = isChatWithUser ? selectUserFullInfo(global, chatId) : undefined;
const chatFullInfo = !isChatWithUser ? selectChatFullInfo(global, chatId) : undefined; const chatFullInfo = !isChatWithUser ? selectChatFullInfo(global, chatId) : undefined;
const messageWithActualBotKeyboard = (isChatWithBot || !isChatWithUser) const messageWithActualBotKeyboard = (isChatWithBot || !isChatWithUser)
&& selectNewestMessageWithBotKeyboardButtons(global, chatId, threadId); && selectNewestMessageWithBotKeyboardButtons(global, chatId, threadId);
const scheduledIds = selectScheduledIds(global, chatId, threadId);
const { const {
language, shouldSuggestStickers, shouldSuggestCustomEmoji, shouldUpdateStickerSetOrder, language, shouldSuggestStickers, shouldSuggestCustomEmoji, shouldUpdateStickerSetOrder,
} = global.settings.byKey; } = global.settings.byKey;
@ -2117,7 +2108,7 @@ export default memo(withGlobal<OwnProps>(
&& messageListType === currentMessageList?.type && messageListType === currentMessageList?.type
&& !isStoryViewerOpen; && !isStoryViewerOpen;
const user = selectUser(global, chatId); const user = selectUser(global, chatId);
const canSendVoiceByPrivacy = (user && !selectUserFullInfo(global, user.id)?.noVoiceMessages) ?? true; const canSendVoiceByPrivacy = (user && !userFullInfo?.noVoiceMessages) ?? true;
const slowMode = chatFullInfo?.slowMode; const slowMode = chatFullInfo?.slowMode;
const isCurrentUserPremium = selectIsCurrentUserPremium(global); const isCurrentUserPremium = selectIsCurrentUserPremium(global);
@ -2140,7 +2131,6 @@ export default memo(withGlobal<OwnProps>(
const noWebPage = selectNoWebPage(global, chatId, threadId); const noWebPage = selectNoWebPage(global, chatId, threadId);
const isContactRequirePremium = selectUserFullInfo(global, chatId)?.isContactRequirePremium;
const areEffectsSupported = isChatWithUser && !isChatWithBot const areEffectsSupported = isChatWithUser && !isChatWithBot
&& !isInScheduledList && !isChatWithSelf && type !== 'story' && chatId !== SERVICE_NOTIFICATIONS_USER_ID; && !isInScheduledList && !isChatWithSelf && type !== 'story' && chatId !== SERVICE_NOTIFICATIONS_USER_ID;
const canPlayEffect = selectPerformanceSettingsValue(global, 'stickerEffects'); const canPlayEffect = selectPerformanceSettingsValue(global, 'stickerEffects');
@ -2165,7 +2155,7 @@ export default memo(withGlobal<OwnProps>(
isSelectModeActive: selectIsInSelectMode(global), isSelectModeActive: selectIsInSelectMode(global),
withScheduledButton: ( withScheduledButton: (
messageListType === 'thread' messageListType === 'thread'
&& Boolean(scheduledIds?.length) && (userFullInfo || chatFullInfo)?.hasScheduledMessages
), ),
isInScheduledList, isInScheduledList,
botKeyboardMessageId, botKeyboardMessageId,
@ -2187,8 +2177,8 @@ export default memo(withGlobal<OwnProps>(
emojiKeywords: emojiKeywords?.keywords, emojiKeywords: emojiKeywords?.keywords,
inlineBots: tabState.inlineBots.byUsername, inlineBots: tabState.inlineBots.byUsername,
isInlineBotLoading: tabState.inlineBots.isLoading, isInlineBotLoading: tabState.inlineBots.isLoading,
botCommands: chatBotFullInfo ? (chatBotFullInfo.botInfo?.commands || false) : undefined, botCommands: userFullInfo ? (userFullInfo.botInfo?.commands || false) : undefined,
botMenuButton: chatBotFullInfo?.botInfo?.menuButton, botMenuButton: userFullInfo?.botInfo?.menuButton,
sendAsUser, sendAsUser,
sendAsChat, sendAsChat,
sendAsId, sendAsId,
@ -2218,7 +2208,7 @@ export default memo(withGlobal<OwnProps>(
canSendQuickReplies, canSendQuickReplies,
noWebPage, noWebPage,
webPagePreview: selectTabState(global).webPagePreview, webPagePreview: selectTabState(global).webPagePreview,
isContactRequirePremium, isContactRequirePremium: userFullInfo?.isContactRequirePremium,
effect, effect,
effectReactions, effectReactions,
areEffectsSupported, areEffectsSupported,

View File

@ -163,6 +163,7 @@ const MessageMeta: FC<OwnProps> = ({
</> </>
)} )}
{message.isEdited && `${lang('EditedMessage')} `} {message.isEdited && `${lang('EditedMessage')} `}
{message.isVideoProcessingPending && `${lang('lng_approximate')} `}
{date} {date}
</span> </span>
{outgoingStatus && ( {outgoingStatus && (

View File

@ -249,6 +249,7 @@ const Video = <T,>({
muted muted
loop loop
playsInline playsInline
disablePictureInPicture
draggable={!isProtected} draggable={!isProtected}
onTimeUpdate={handleTimeUpdate} onTimeUpdate={handleTimeUpdate}
onReady={markPlayerReady} onReady={markPlayerReady}

View File

@ -132,7 +132,7 @@ const HeaderPinnedMessage = ({
if (isSynced && (threadId === MAIN_THREAD_ID || chat?.isForum)) { if (isSynced && (threadId === MAIN_THREAD_ID || chat?.isForum)) {
loadPinnedMessages({ chatId, threadId }); loadPinnedMessages({ chatId, threadId });
} }
}, [chatId, threadId, isSynced, chat]); }, [chatId, threadId, isSynced, chat?.isForum]);
useEnsureMessage(chatId, pinnedMessageId, pinnedMessage); useEnsureMessage(chatId, pinnedMessageId, pinnedMessage);

View File

@ -51,7 +51,7 @@ export const MEDIA_PROGRESSIVE_CACHE_DISABLED = false;
export const MEDIA_PROGRESSIVE_CACHE_NAME = 'tt-media-progressive'; export const MEDIA_PROGRESSIVE_CACHE_NAME = 'tt-media-progressive';
export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg'; export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg';
export const LANG_CACHE_NAME = 'tt-lang-packs-v44'; export const LANG_CACHE_NAME = 'tt-lang-packs-v45';
export const ASSET_CACHE_NAME = 'tt-assets'; export const ASSET_CACHE_NAME = 'tt-assets';
export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500]; export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500];
export const DATA_BROADCAST_CHANNEL_NAME = 'tt-global'; export const DATA_BROADCAST_CHANNEL_NAME = 'tt-global';

View File

@ -84,6 +84,7 @@ import {
updateListedIds, updateListedIds,
updateMessageTranslation, updateMessageTranslation,
updateOutlyingLists, updateOutlyingLists,
updatePeerFullInfo,
updateQuickReplies, updateQuickReplies,
updateQuickReplyMessages, updateQuickReplyMessages,
updateRequestedMessageTranslation, updateRequestedMessageTranslation,
@ -1213,6 +1214,10 @@ addActionHandler('loadScheduledHistory', async (global, actions, payload): Promi
global = getGlobal(); global = getGlobal();
global = updateScheduledMessages(global, chat.id, byId); global = updateScheduledMessages(global, chat.id, byId);
global = replaceThreadParam(global, chat.id, MAIN_THREAD_ID, 'scheduledIds', ids); global = replaceThreadParam(global, chat.id, MAIN_THREAD_ID, 'scheduledIds', ids);
if (!ids.length) {
global = updatePeerFullInfo(global, chat.id, { hasScheduledMessages: false });
}
if (chat?.isForum) { if (chat?.isForum) {
const scheduledPerThread: Record<ThreadId, number[]> = {}; const scheduledPerThread: Record<ThreadId, number[]> = {};
messages.forEach((message) => { messages.forEach((message) => {

View File

@ -162,6 +162,8 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
return undefined; return undefined;
} }
const isLocal = isLocalMessageId(message.id!);
const chat = selectChat(global, update.chatId); const chat = selectChat(global, update.chatId);
if (!chat) { if (!chat) {
return undefined; return undefined;
@ -169,19 +171,21 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
const hasMention = Boolean(update.message.id && update.message.hasUnreadMention); const hasMention = Boolean(update.message.id && update.message.hasUnreadMention);
global = updateChat(global, update.chatId, { if (!isLocal) {
unreadCount: chat.unreadCount ? chat.unreadCount + 1 : 1, global = updateChat(global, update.chatId, {
}); unreadCount: chat.unreadCount ? chat.unreadCount + 1 : 1,
if (hasMention) {
global = addUnreadMentions(global, update.chatId, chat, [update.message.id!], true);
}
const topic = chat.isForum ? selectTopicFromMessage(global, message as ApiMessage) : undefined;
if (topic) {
global = updateTopic(global, update.chatId, topic.id, {
unreadCount: topic.unreadCount ? topic.unreadCount + 1 : 1,
}); });
if (hasMention) {
global = addUnreadMentions(global, update.chatId, chat, [update.message.id!], true);
}
const topic = chat.isForum ? selectTopicFromMessage(global, message as ApiMessage) : undefined;
if (topic) {
global = updateTopic(global, update.chatId, topic.id, {
unreadCount: topic.unreadCount ? topic.unreadCount + 1 : 1,
});
}
} }
setGlobal(global); setGlobal(global);

View File

@ -43,6 +43,7 @@ import {
updateChatMessage, updateChatMessage,
updateListedIds, updateListedIds,
updateMessageTranslations, updateMessageTranslations,
updatePeerFullInfo,
updatePoll, updatePoll,
updatePollVote, updatePollVote,
updateQuickReplies, updateQuickReplies,
@ -87,6 +88,8 @@ import {
const ANIMATION_DELAY = 350; const ANIMATION_DELAY = 350;
const SNAP_ANIMATION_DELAY = 1000; const SNAP_ANIMATION_DELAY = 1000;
const VIDEO_PROCESSING_NOTIFICATION_DELAY = 1000;
let lastVideoProcessingNotificationTime = 0;
addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
switch (update['@type']) { switch (update['@type']) {
@ -233,6 +236,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
global = updatePoll(global, poll.id, poll); global = updatePoll(global, poll.id, poll);
} }
global = updatePeerFullInfo(global, chatId, {
hasScheduledMessages: true,
});
setGlobal(global); setGlobal(global);
break; break;
@ -335,6 +342,49 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
break; break;
} }
case 'updateVideoProcessingPending': {
const {
chatId, localId, newScheduledMessageId,
} = update;
global = deleteChatMessages(global, chatId, [localId]);
global = updatePeerFullInfo(global, chatId, {
hasScheduledMessages: true,
});
setGlobal(global);
Object.values(global.byTabId).forEach(({ id: tabId }) => {
const currentMessageList = selectCurrentMessageList(global, tabId);
if (currentMessageList?.chatId !== chatId) return;
const now = Date.now();
if (now - lastVideoProcessingNotificationTime < VIDEO_PROCESSING_NOTIFICATION_DELAY) {
return;
}
lastVideoProcessingNotificationTime = now;
actions.showNotification({
message: {
key: 'VideoConversionText',
},
title: {
key: 'VideoConversionTitle',
},
tabId,
});
actions.focusMessage({
chatId,
messageId: newScheduledMessageId,
messageListType: 'scheduled',
tabId,
});
});
break;
}
case 'updateMessageSendSucceeded': { case 'updateMessageSendSucceeded': {
const { const {
chatId, localId, message, poll, chatId, localId, message, poll,
@ -524,7 +574,37 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
} }
case 'deleteScheduledMessages': { case 'deleteScheduledMessages': {
const { ids, chatId } = update; const { ids, newIds, chatId } = update;
const hadVideoProcessing = ids?.some((id) => (
selectScheduledMessage(global, chatId, id)?.isVideoProcessingPending
));
const processedVideoId = newIds?.find((id) => {
const message = selectChatMessage(global, chatId, id);
return message?.content.video;
});
if (hadVideoProcessing && processedVideoId) {
Object.values(global.byTabId).forEach(({ id: tabId }) => {
actions.showNotification({
message: {
key: 'VideoConversionDone',
},
actionText: {
key: 'VideoConversionView',
},
action: {
action: 'focusMessage',
payload: {
chatId,
messageId: processedVideoId,
tabId,
},
},
tabId,
});
});
}
deleteScheduledMessages(chatId, ids, actions, global); deleteScheduledMessages(chatId, ids, actions, global);
break; break;
@ -1150,12 +1230,8 @@ export function deleteMessages<T extends GlobalState>(
} }
function deleteScheduledMessages<T extends GlobalState>( function deleteScheduledMessages<T extends GlobalState>(
chatId: string | undefined, ids: number[], actions: RequiredGlobalActions, global: T, chatId: string, ids: number[], actions: RequiredGlobalActions, global: T,
) { ) {
if (!chatId) {
return;
}
ids.forEach((id) => { ids.forEach((id) => {
global = updateScheduledMessage(global, chatId, id, { global = updateScheduledMessage(global, chatId, id, {
isDeleting: true, isDeleting: true,

View File

@ -1153,6 +1153,10 @@ export interface LangPair {
'CloseMiniApps': undefined; 'CloseMiniApps': undefined;
'DoNotAskAgain': undefined; 'DoNotAskAgain': undefined;
'PaymentInfoDone': undefined; 'PaymentInfoDone': undefined;
'VideoConversionTitle': undefined;
'VideoConversionText': undefined;
'VideoConversionDone': undefined;
'VideoConversionView': undefined;
'BotSuggestedStatusTitle': undefined; 'BotSuggestedStatusTitle': undefined;
'BotSuggestedStatusUpdated': undefined; 'BotSuggestedStatusUpdated': undefined;
} }