Mini Apps: Allow suggesting user emoji status (#5236)
This commit is contained in:
parent
ba9956f921
commit
0933603cb0
@ -712,20 +712,20 @@ export function buildInputChatReactions(chatReactions?: ApiChatReactions) {
|
||||
return new GramJs.ChatReactionsNone();
|
||||
}
|
||||
|
||||
export function buildInputEmojiStatus(emojiStatus: ApiSticker, expires?: number) {
|
||||
if (emojiStatus.id === DEFAULT_STATUS_ICON_ID) {
|
||||
export function buildInputEmojiStatus(emojiStatusId: string, expires?: number) {
|
||||
if (emojiStatusId === DEFAULT_STATUS_ICON_ID) {
|
||||
return new GramJs.EmojiStatusEmpty();
|
||||
}
|
||||
|
||||
if (expires) {
|
||||
return new GramJs.EmojiStatusUntil({
|
||||
documentId: BigInt(emojiStatus.id),
|
||||
documentId: BigInt(emojiStatusId),
|
||||
until: expires,
|
||||
});
|
||||
}
|
||||
|
||||
return new GramJs.EmojiStatus({
|
||||
documentId: BigInt(emojiStatus.id),
|
||||
documentId: BigInt(emojiStatusId),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import BigInt from 'big-integer';
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiPeer, ApiSticker, ApiUser,
|
||||
ApiChat, ApiPeer, ApiUser,
|
||||
} from '../../types';
|
||||
|
||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||
@ -304,9 +304,9 @@ export function reportSpam(userOrChat: ApiPeer) {
|
||||
});
|
||||
}
|
||||
|
||||
export function updateEmojiStatus(emojiStatus: ApiSticker, expires?: number) {
|
||||
export function updateEmojiStatus(emojiStatusId: string, expires?: number) {
|
||||
return invokeRequest(new GramJs.account.UpdateEmojiStatus({
|
||||
emojiStatus: buildInputEmojiStatus(emojiStatus, expires),
|
||||
emojiStatus: buildInputEmojiStatus(emojiStatusId, expires),
|
||||
}), {
|
||||
shouldReturnTrue: true,
|
||||
});
|
||||
|
||||
@ -125,6 +125,7 @@ export type ApiNotification = {
|
||||
disableClickDismiss?: boolean;
|
||||
shouldShowTimer?: boolean;
|
||||
icon?: IconName;
|
||||
customEmojiIconId?: string;
|
||||
dismissAction?: CallbackAction;
|
||||
};
|
||||
|
||||
|
||||
@ -1111,6 +1111,7 @@
|
||||
"LiveLocationUpdatedMinutesAgo_one" = "updated 1 minute ago";
|
||||
"LiveLocationUpdatedMinutesAgo_other" = "updated {count} minutes ago";
|
||||
"LiveLocationUpdatedTodayAt" = "updated at {time}";
|
||||
"RightNow" = "Just now";
|
||||
"Seconds_one" = "{count} second";
|
||||
"Seconds_other" = "{count} seconds";
|
||||
"Minutes_one" = "{count} minute";
|
||||
@ -1383,3 +1384,7 @@
|
||||
"CloseMiniApps" = "Close Mini Apps";
|
||||
"DoNotAskAgain" = "Don't ask again";
|
||||
"PaymentInfoDone" = "Proceed to checkout";
|
||||
"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}**?";
|
||||
"BotSuggestedStatusTitle" = "Set Emoji Status";
|
||||
"BotSuggestedStatusUpdated" = "Your emoji status is updated.";
|
||||
|
||||
@ -20,6 +20,7 @@ export { default as PremiumMainModal } from '../components/main/premium/PremiumM
|
||||
export { default as GiveawayModal } from '../components/main/premium/GiveawayModal';
|
||||
export { default as PremiumLimitReachedModal } from '../components/main/premium/common/PremiumLimitReachedModal';
|
||||
export { default as StatusPickerMenu } from '../components/left/main/StatusPickerMenu';
|
||||
export { default as SuggestedStatusModal } from '../components/modals/suggestedStatus/SuggestedStatusModal';
|
||||
export { default as BoostModal } from '../components/modals/boost/BoostModal';
|
||||
export { default as GiftCodeModal } from '../components/modals/giftcode/GiftCodeModal';
|
||||
export { default as ChatlistModal } from '../components/modals/chatlist/ChatlistModal';
|
||||
|
||||
@ -8,7 +8,7 @@ import type { ApiBotInlineMediaResult, ApiSticker } from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { getServerTimeOffset } from '../../util/serverTime';
|
||||
import { getServerTime } from '../../util/serverTime';
|
||||
import { IS_TOUCH_ENV } from '../../util/windowEnvironment';
|
||||
import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur';
|
||||
|
||||
@ -198,8 +198,8 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
handleContextMenuClose();
|
||||
onContextMenuClick?.();
|
||||
setEmojiStatus({
|
||||
emojiStatus: sticker,
|
||||
expires: Date.now() / 1000 + duration + getServerTimeOffset(),
|
||||
emojiStatusId: sticker.id,
|
||||
expires: getServerTime() + duration,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -2,19 +2,19 @@ import type { TeactNode } from '../../../lib/teact/teact';
|
||||
import React, { memo } from '../../../lib/teact/teact';
|
||||
import { withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiChat, ApiUser } from '../../../api/types';
|
||||
import type { ApiPeer } from '../../../api/types';
|
||||
import type { CustomPeer } from '../../../types';
|
||||
import type { IconName } from '../../../types/icons';
|
||||
|
||||
import { getChatTitle, getUserFirstOrLastName } from '../../../global/helpers';
|
||||
import { selectChat, selectUser } from '../../../global/selectors';
|
||||
import { isApiPeerChat } from '../../../global/helpers/peers';
|
||||
import { selectPeer, selectUser } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { getPeerColorClass } from '../helpers/peerColor';
|
||||
import renderText from '../helpers/renderText';
|
||||
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Avatar from '../Avatar';
|
||||
import FullNameTitle from '../FullNameTitle';
|
||||
import Icon from '../icons/Icon';
|
||||
|
||||
import './PickerSelectedItem.scss';
|
||||
@ -25,6 +25,7 @@ type OwnProps<T = undefined> = {
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
forceShowSelf?: boolean;
|
||||
customPeer?: CustomPeer;
|
||||
mockPeer?: ApiPeer;
|
||||
icon?: IconName;
|
||||
title?: string;
|
||||
isMinimized?: boolean;
|
||||
@ -32,13 +33,12 @@ type OwnProps<T = undefined> = {
|
||||
className?: string;
|
||||
fluid?: boolean;
|
||||
withPeerColors?: boolean;
|
||||
clickArg: T;
|
||||
onClick: (arg: T) => void;
|
||||
clickArg?: T;
|
||||
onClick?: (arg: T) => void;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
chat?: ApiChat;
|
||||
user?: ApiUser;
|
||||
peer?: ApiPeer;
|
||||
isSavedMessages?: boolean;
|
||||
};
|
||||
|
||||
@ -49,8 +49,8 @@ const PickerSelectedItem = <T,>({
|
||||
isMinimized,
|
||||
canClose,
|
||||
clickArg,
|
||||
chat,
|
||||
user,
|
||||
peer,
|
||||
mockPeer,
|
||||
customPeer,
|
||||
className,
|
||||
fluid,
|
||||
@ -60,6 +60,11 @@ const PickerSelectedItem = <T,>({
|
||||
}: OwnProps<T> & StateProps) => {
|
||||
const lang = useOldLang();
|
||||
|
||||
const apiPeer = mockPeer || peer;
|
||||
const anyPeer = customPeer || apiPeer;
|
||||
|
||||
const chat = apiPeer && isApiPeerChat(apiPeer) ? apiPeer : undefined;
|
||||
|
||||
let iconElement: TeactNode | undefined;
|
||||
let titleText: any;
|
||||
|
||||
@ -71,21 +76,16 @@ const PickerSelectedItem = <T,>({
|
||||
);
|
||||
|
||||
titleText = title;
|
||||
} else if (customPeer || user || chat) {
|
||||
} else if (anyPeer) {
|
||||
iconElement = (
|
||||
<Avatar
|
||||
peer={customPeer || user || chat}
|
||||
peer={anyPeer}
|
||||
size="small"
|
||||
isSavedMessages={isSavedMessages}
|
||||
/>
|
||||
);
|
||||
|
||||
const name = (customPeer && (customPeer.title || lang(customPeer.titleKey!)))
|
||||
|| (!chat || (user && !isSavedMessages)
|
||||
? getUserFirstOrLastName(user)
|
||||
: getChatTitle(lang, chat, isSavedMessages));
|
||||
|
||||
titleText = title || (name ? renderText(name) : undefined);
|
||||
titleText = title || <FullNameTitle peer={anyPeer} isSavedMessages={isSavedMessages} withEmojiStatus />;
|
||||
}
|
||||
|
||||
const fullClassName = buildClassName(
|
||||
@ -95,13 +95,13 @@ const PickerSelectedItem = <T,>({
|
||||
isMinimized && 'minimized',
|
||||
canClose && 'closeable',
|
||||
fluid && 'fluid',
|
||||
withPeerColors && getPeerColorClass(customPeer || chat || user),
|
||||
withPeerColors && getPeerColorClass(customPeer || peer),
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={fullClassName}
|
||||
onClick={() => onClick(clickArg)}
|
||||
onClick={() => onClick?.(clickArg!)}
|
||||
title={isMinimized ? titleText : undefined}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
>
|
||||
@ -126,13 +126,12 @@ export default memo(withGlobal<OwnProps>(
|
||||
return {};
|
||||
}
|
||||
|
||||
const chat = selectChat(global, peerId);
|
||||
const peer = selectPeer(global, peerId);
|
||||
const user = selectUser(global, peerId);
|
||||
const isSavedMessages = !forceShowSelf && user && user.isSelf;
|
||||
|
||||
return {
|
||||
chat,
|
||||
user,
|
||||
peer,
|
||||
isSavedMessages,
|
||||
};
|
||||
},
|
||||
|
||||
@ -6,7 +6,7 @@ import type { ApiEmojiStatus, ApiSticker } from '../../../api/types';
|
||||
|
||||
import { EMOJI_STATUS_LOOP_LIMIT } from '../../../config';
|
||||
import { selectUser } from '../../../global/selectors';
|
||||
import { getServerTimeOffset } from '../../../util/serverTime';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
|
||||
import useTimeout from '../../../hooks/schedulers/useTimeout';
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
@ -36,7 +36,7 @@ const StatusButton: FC<StateProps> = ({ emojiStatus }) => {
|
||||
const [isStatusPickerOpen, openStatusPicker, closeStatusPicker] = useFlag(false);
|
||||
const { isMobile } = useAppLayout();
|
||||
|
||||
const delay = emojiStatus?.until ? emojiStatus.until * 1000 - Date.now() + getServerTimeOffset() * 1000 : undefined;
|
||||
const delay = emojiStatus?.until ? (emojiStatus.until - getServerTime()) * 1000 : undefined;
|
||||
useTimeout(loadCurrentUser, delay);
|
||||
|
||||
useEffectWithPrevDeps(([prevEmojiStatus]) => {
|
||||
@ -48,7 +48,7 @@ const StatusButton: FC<StateProps> = ({ emojiStatus }) => {
|
||||
|
||||
const handleEmojiStatusSet = useCallback((sticker: ApiSticker) => {
|
||||
markShouldShowEffect();
|
||||
setEmojiStatus({ emojiStatus: sticker });
|
||||
setEmojiStatus({ emojiStatusId: sticker.id });
|
||||
}, [markShouldShowEffect, setEmojiStatus]);
|
||||
|
||||
useTimeout(hideEffect, isEffectShown ? EFFECT_DURATION_MS : undefined);
|
||||
|
||||
@ -138,6 +138,7 @@
|
||||
|
||||
&.has-inline-buttons {
|
||||
.message-content {
|
||||
--border-bottom-left-radius: var(--border-radius-messages-small);
|
||||
--border-bottom-right-radius: var(--border-radius-messages-small);
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@ import StarsBalanceModal from './stars/StarsBalanceModal.async';
|
||||
import StarsPaymentModal from './stars/StarsPaymentModal.async';
|
||||
import StarsSubscriptionModal from './stars/subscription/StarsSubscriptionModal.async';
|
||||
import StarsTransactionInfoModal from './stars/transaction/StarsTransactionModal.async';
|
||||
import SuggestedStatusModal from './suggestedStatus/SuggestedStatusModal.async';
|
||||
import UrlAuthModal from './urlAuth/UrlAuthModal.async';
|
||||
import WebAppModal from './webApp/WebAppModal.async';
|
||||
|
||||
@ -57,6 +58,7 @@ type ModalKey = keyof Pick<TabState,
|
||||
'isGiftRecipientPickerOpen' |
|
||||
'isWebAppsCloseConfirmationModalOpen' |
|
||||
'giftInfoModal' |
|
||||
'suggestedStatusModal' |
|
||||
'aboutAdsModal'
|
||||
>;
|
||||
|
||||
@ -96,6 +98,7 @@ const MODALS: ModalRegistry = {
|
||||
isGiftRecipientPickerOpen: GiftRecipientPicker,
|
||||
isWebAppsCloseConfirmationModalOpen: WebAppsCloseConfirmationModal,
|
||||
giftInfoModal: GiftInfoModal,
|
||||
suggestedStatusModal: SuggestedStatusModal,
|
||||
aboutAdsModal: AboutAdsModal,
|
||||
};
|
||||
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
|
||||
|
||||
@ -7,12 +7,13 @@ import type { TabState } from '../../../global/types';
|
||||
import { getChatTitle, isChatAdmin, isChatChannel } from '../../../global/helpers';
|
||||
import { selectChat, selectChatFullInfo, selectIsCurrentUserPremium } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatDateInFuture } from '../../../util/dates/dateFormat';
|
||||
import { formatShortDuration } from '../../../util/dates/dateFormat';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import { getBoostProgressInfo } from '../../common/helpers/boostInfo';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
@ -79,7 +80,8 @@ const BoostModal = ({
|
||||
|
||||
const isOpen = Boolean(modal);
|
||||
|
||||
const lang = useOldLang();
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
useEffect(() => {
|
||||
if (chat && !chatFullInfo) {
|
||||
@ -92,16 +94,16 @@ const BoostModal = ({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return getChatTitle(lang, chat);
|
||||
}, [chat, lang]);
|
||||
return getChatTitle(oldLang, chat);
|
||||
}, [chat, oldLang]);
|
||||
|
||||
const boostedChatTitle = useMemo(() => {
|
||||
if (!prevBoostedChat) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return getChatTitle(lang, prevBoostedChat);
|
||||
}, [prevBoostedChat, lang]);
|
||||
return getChatTitle(oldLang, prevBoostedChat);
|
||||
}, [prevBoostedChat, oldLang]);
|
||||
|
||||
const {
|
||||
isStatusLoaded,
|
||||
@ -118,7 +120,7 @@ const BoostModal = ({
|
||||
if (!modal?.boostStatus || !chat) {
|
||||
return {
|
||||
isStatusLoaded: false,
|
||||
title: lang('Loading'),
|
||||
title: oldLang('Loading'),
|
||||
};
|
||||
}
|
||||
|
||||
@ -140,23 +142,23 @@ const BoostModal = ({
|
||||
|
||||
const hasBoost = hasMyBoost;
|
||||
|
||||
const left = lang('BoostsLevel', currentLevel);
|
||||
const right = hasNextLevel ? lang('BoostsLevel', currentLevel + 1) : undefined;
|
||||
const left = oldLang('BoostsLevel', currentLevel);
|
||||
const right = hasNextLevel ? oldLang('BoostsLevel', currentLevel + 1) : undefined;
|
||||
|
||||
const moreBoosts = lang('ChannelBoost.MoreBoosts', remainingBoosts);
|
||||
const moreBoosts = oldLang('ChannelBoost.MoreBoosts', remainingBoosts);
|
||||
|
||||
const modalTitle = isChannel ? lang('BoostChannel') : lang('BoostGroup');
|
||||
const modalTitle = isChannel ? oldLang('BoostChannel') : oldLang('BoostGroup');
|
||||
|
||||
const boostsLeftToUnrestrict = (chatFullInfo?.boostsToUnrestrict || 0) - (chatFullInfo?.boostsApplied || 0);
|
||||
|
||||
let description: string | undefined;
|
||||
if (isMaxLevel) {
|
||||
description = lang('BoostsMaxLevelReached');
|
||||
description = oldLang('BoostsMaxLevelReached');
|
||||
} else if (boostsLeftToUnrestrict > 0 && !isChatAdmin(chat)) {
|
||||
const boostTimes = lang('GroupBoost.BoostToUnrestrict.Times', boostsLeftToUnrestrict);
|
||||
description = lang('GroupBoost.BoostToUnrestrict', [boostTimes, chatTitle]);
|
||||
const boostTimes = oldLang('GroupBoost.BoostToUnrestrict.Times', boostsLeftToUnrestrict);
|
||||
description = oldLang('GroupBoost.BoostToUnrestrict', [boostTimes, chatTitle]);
|
||||
} else {
|
||||
description = lang('ChannelBoost.MoreBoostsNeeded.Text', [chatTitle, moreBoosts]);
|
||||
description = oldLang('ChannelBoost.MoreBoostsNeeded.Text', [chatTitle, moreBoosts]);
|
||||
}
|
||||
|
||||
return {
|
||||
@ -172,7 +174,7 @@ const BoostModal = ({
|
||||
isBoosted: hasBoost,
|
||||
canBoostMore: areBoostsInDifferentChannels && !isMaxLevel,
|
||||
};
|
||||
}, [chat, chatTitle, modal, lang, chatFullInfo, isChannel]);
|
||||
}, [chat, chatTitle, modal, oldLang, chatFullInfo, isChannel]);
|
||||
|
||||
const isBoostDisabled = !modal?.myBoosts?.length && isCurrentUserPremium;
|
||||
const isReplacingBoost = boost?.chatId && boost.chatId !== modal?.chatId;
|
||||
@ -238,7 +240,7 @@ const BoostModal = ({
|
||||
/>
|
||||
{isBoosted && (
|
||||
<div className={buildClassName(styles.description, styles.bold)}>
|
||||
{lang('ChannelBoost.YouBoostedChannelText', chatTitle)}
|
||||
{oldLang('ChannelBoost.YouBoostedChannelText', chatTitle)}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.description}>
|
||||
@ -249,12 +251,12 @@ const BoostModal = ({
|
||||
{canBoostMore ? (
|
||||
<>
|
||||
<Icon name="boost" />
|
||||
{lang(isChannel ? 'ChannelBoost.BoostChannel' : 'GroupBoost.BoostGroup')}
|
||||
{oldLang(isChannel ? 'ChannelBoost.BoostChannel' : 'GroupBoost.BoostGroup')}
|
||||
</>
|
||||
) : lang('OK')}
|
||||
) : oldLang('OK')}
|
||||
</Button>
|
||||
<Button isText className="confirm-dialog-button" onClick={handleCloseClick}>
|
||||
{lang('Cancel')}
|
||||
{oldLang('Cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
@ -286,14 +288,16 @@ const BoostModal = ({
|
||||
<Avatar peer={chat} size="large" />
|
||||
</div>
|
||||
<div>
|
||||
{renderText(lang('ChannelBoost.ReplaceBoost', [boostedChatTitle, chatTitle]), ['simple_markdown', 'emoji'])}
|
||||
{renderText(
|
||||
oldLang('ChannelBoost.ReplaceBoost', [boostedChatTitle, chatTitle]), ['simple_markdown', 'emoji'],
|
||||
)}
|
||||
</div>
|
||||
<div className="dialog-buttons">
|
||||
<Button isText className="confirm-dialog-button" onClick={handleApplyBoost}>
|
||||
{lang('Replace')}
|
||||
{oldLang('Replace')}
|
||||
</Button>
|
||||
<Button isText className="confirm-dialog-button" onClick={closeReplaceModal}>
|
||||
{lang('Cancel')}
|
||||
{oldLang('Cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
@ -302,15 +306,15 @@ const BoostModal = ({
|
||||
<ConfirmDialog
|
||||
isOpen={isWaitDialogOpen}
|
||||
isOnlyConfirm
|
||||
confirmLabel={lang('OK')}
|
||||
title={lang('ChannelBoost.Error.BoostTooOftenTitle')}
|
||||
confirmLabel={oldLang('OK')}
|
||||
title={oldLang('ChannelBoost.Error.BoostTooOftenTitle')}
|
||||
onClose={closeWaitDialog}
|
||||
confirmHandler={closeWaitDialog}
|
||||
>
|
||||
{renderText(
|
||||
lang(
|
||||
oldLang(
|
||||
'ChannelBoost.Error.BoostTooOftenText',
|
||||
formatDateInFuture(lang, getServerTime(), boost!.cooldownUntil),
|
||||
formatShortDuration(lang, boost!.cooldownUntil - getServerTime()),
|
||||
),
|
||||
['simple_markdown', 'emoji'],
|
||||
)}
|
||||
@ -319,12 +323,12 @@ const BoostModal = ({
|
||||
{!isCurrentUserPremium && (
|
||||
<ConfirmDialog
|
||||
isOpen={isPremiumDialogOpen}
|
||||
confirmLabel={lang('Common.Yes')}
|
||||
title={lang('PremiumNeeded')}
|
||||
confirmLabel={oldLang('Common.Yes')}
|
||||
title={oldLang('PremiumNeeded')}
|
||||
onClose={closePremiumDialog}
|
||||
confirmHandler={handleProceedPremium}
|
||||
>
|
||||
{renderText(lang('PremiumNeededForBoosting'), ['simple_markdown', 'emoji'])}
|
||||
{renderText(oldLang('PremiumNeededForBoosting'), ['simple_markdown', 'emoji'])}
|
||||
</ConfirmDialog>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
@ -10,6 +10,7 @@ import buildClassName from '../../../../util/buildClassName';
|
||||
import { formatDateTimeToString } from '../../../../util/dates/dateFormat';
|
||||
import { formatStarsAsIcon, formatStarsAsText } from '../../../../util/localization/format';
|
||||
import { CUSTOM_PEER_HIDDEN } from '../../../../util/objects/customPeer';
|
||||
import { getServerTime } from '../../../../util/serverTime';
|
||||
import { formatInteger } from '../../../../util/textFormat';
|
||||
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
|
||||
|
||||
@ -67,7 +68,7 @@ const GiftInfoModal = ({
|
||||
const canUpdate = Boolean(userGift?.fromId && userGift.messageId);
|
||||
const isSender = userGift?.fromId === currentUserId;
|
||||
const canConvertDifference = (userGift && starGiftMaxConvertPeriod && (
|
||||
userGift.date + starGiftMaxConvertPeriod - Date.now() / 1000
|
||||
userGift.date + starGiftMaxConvertPeriod - getServerTime()
|
||||
)) || 0;
|
||||
const conversionLeft = Math.ceil(canConvertDifference / 60 / 60 / 24);
|
||||
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React from '../../../lib/teact/teact';
|
||||
|
||||
import type { OwnProps } from './SuggestedStatusModal';
|
||||
|
||||
import { Bundles } from '../../../util/moduleLoader';
|
||||
|
||||
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||
|
||||
const SuggestedStatusModalAsync: FC<OwnProps> = (props) => {
|
||||
const { modal } = props;
|
||||
const SuggestedStatusModal = useModuleLoader(Bundles.Extra, 'SuggestedStatusModal', !modal);
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return SuggestedStatusModal ? <SuggestedStatusModal {...props} /> : undefined;
|
||||
};
|
||||
|
||||
export default SuggestedStatusModalAsync;
|
||||
@ -0,0 +1,20 @@
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.topEmoji {
|
||||
--custom-emoji-size: 6rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
text-align: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
145
src/components/modals/suggestedStatus/SuggestedStatusModal.tsx
Normal file
145
src/components/modals/suggestedStatus/SuggestedStatusModal.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
import React, { memo, useMemo } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiUser } from '../../../api/types';
|
||||
import type { TabState } from '../../../global/types';
|
||||
|
||||
import { getUserFullName } from '../../../global/helpers';
|
||||
import { selectUser } from '../../../global/selectors';
|
||||
import { formatShortDuration } from '../../../util/dates/dateFormat';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
|
||||
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import CustomEmoji from '../../common/CustomEmoji';
|
||||
import PickerSelectedItem from '../../common/pickers/PickerSelectedItem';
|
||||
import Button from '../../ui/Button';
|
||||
import Modal from '../../ui/Modal';
|
||||
|
||||
import styles from './SuggestedStatusModal.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
modal: TabState['suggestedStatusModal'];
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
bot?: ApiUser;
|
||||
currentUser?: ApiUser;
|
||||
};
|
||||
|
||||
const CUSTOM_EMOJI_SIZE = 6 * REM;
|
||||
|
||||
const SuggestedStatusModal = ({ modal, currentUser, bot }: OwnProps & StateProps) => {
|
||||
const { setEmojiStatus, closeSuggestedStatusModal, sendWebAppEvent } = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const isOpen = Boolean(modal);
|
||||
const renderingModal = useCurrentOrPrev(modal);
|
||||
|
||||
const mockPeerWithStatus = useMemo(() => {
|
||||
if (!currentUser || !renderingModal) return undefined;
|
||||
return {
|
||||
...currentUser,
|
||||
emojiStatus: {
|
||||
documentId: renderingModal.customEmojiId,
|
||||
},
|
||||
} satisfies ApiUser;
|
||||
}, [currentUser, renderingModal]);
|
||||
|
||||
const description = useMemo(() => {
|
||||
if (!renderingModal || !bot) return undefined;
|
||||
|
||||
const botName = getUserFullName(bot);
|
||||
|
||||
if (renderingModal.duration) {
|
||||
return lang('BotSuggestedStatusFor', {
|
||||
bot: botName,
|
||||
duration: formatShortDuration(lang, renderingModal.duration),
|
||||
}, {
|
||||
withNodes: true,
|
||||
withMarkdown: true,
|
||||
});
|
||||
}
|
||||
|
||||
return lang('BotSuggestedStatus', { bot: botName }, { withNodes: true, withMarkdown: true });
|
||||
}, [bot, lang, renderingModal]);
|
||||
|
||||
const handleClose = useLastCallback(() => {
|
||||
const webAppKey = renderingModal?.webAppKey;
|
||||
|
||||
if (webAppKey) {
|
||||
sendWebAppEvent({
|
||||
webAppKey,
|
||||
event: {
|
||||
eventType: 'emoji_status_failed',
|
||||
eventData: {
|
||||
error: 'USER_DECLINED',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
closeSuggestedStatusModal();
|
||||
});
|
||||
|
||||
const handleSetStatus = useLastCallback(() => {
|
||||
if (!renderingModal) return;
|
||||
|
||||
const expires = renderingModal.duration ? getServerTime() + renderingModal.duration : undefined;
|
||||
|
||||
setEmojiStatus({
|
||||
referrerWebAppKey: renderingModal.webAppKey,
|
||||
emojiStatusId: renderingModal.customEmojiId,
|
||||
expires,
|
||||
});
|
||||
closeSuggestedStatusModal();
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
contentClassName={styles.content}
|
||||
hasAbsoluteCloseButton
|
||||
isSlim
|
||||
onClose={handleClose}
|
||||
>
|
||||
{renderingModal && (
|
||||
<CustomEmoji
|
||||
className={styles.topEmoji}
|
||||
documentId={renderingModal.customEmojiId}
|
||||
size={CUSTOM_EMOJI_SIZE}
|
||||
loopLimit={1}
|
||||
forceAlways
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<h3 className={styles.title}>{lang('BotSuggestedStatusTitle')}</h3>
|
||||
<p className={styles.description}>{description}</p>
|
||||
</div>
|
||||
{mockPeerWithStatus && (
|
||||
<PickerSelectedItem
|
||||
mockPeer={mockPeerWithStatus}
|
||||
/>
|
||||
)}
|
||||
<Button size="smaller" onClick={handleSetStatus}>
|
||||
{lang('GeneralConfirm')}
|
||||
</Button>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { modal }): StateProps => {
|
||||
const currentUser = selectUser(global, global.currentUserId!);
|
||||
const bot = modal?.botId ? selectUser(global, modal.botId) : undefined;
|
||||
|
||||
return {
|
||||
currentUser,
|
||||
bot,
|
||||
};
|
||||
},
|
||||
)(SuggestedStatusModal));
|
||||
@ -248,9 +248,10 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const handleToggleClick = useLastCallback(() => {
|
||||
if (attachBot) {
|
||||
const key = getWebAppKey(activeWebApp!);
|
||||
updateWebApp({
|
||||
webApp: {
|
||||
...activeWebApp!,
|
||||
key,
|
||||
update: {
|
||||
isRemoveModalOpen: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -140,13 +140,8 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
const isActive = (activeWebApp && webApp) && activeWebAppKey === webAppKey;
|
||||
|
||||
const updateCurrentWebApp = useLastCallback((updatedPartialWebApp: Partial<WebApp>) => {
|
||||
if (!webApp) return;
|
||||
const updatedWebApp = {
|
||||
...webApp,
|
||||
...updatedPartialWebApp,
|
||||
};
|
||||
webApp = updatedWebApp;
|
||||
updateWebApp({ webApp: updatedWebApp });
|
||||
if (!webAppKey) return;
|
||||
updateWebApp({ key: webAppKey, update: updatedPartialWebApp });
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { useCallback, useEffect, useRef } from '../../../../lib/teact/teact';
|
||||
import { getActions } from '../../../../global';
|
||||
|
||||
import type { WebApp } from '../../../../global/types';
|
||||
import type { WebAppInboundEvent, WebAppOutboundEvent } from '../../../../types/webapp';
|
||||
|
||||
import { getWebAppKey } from '../../../../global/helpers';
|
||||
import { extractCurrentThemeParams } from '../../../../util/themeStyle';
|
||||
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
@ -44,6 +46,8 @@ const useWebAppFrame = (
|
||||
setWebAppPaymentSlug,
|
||||
openInvoice,
|
||||
closeWebApp,
|
||||
openSuggestedStatusModal,
|
||||
updateWebApp,
|
||||
} = getActions();
|
||||
|
||||
const isReloadSupported = useRef<boolean>(false);
|
||||
@ -146,7 +150,10 @@ const useWebAppFrame = (
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_close') {
|
||||
if (webApp) closeWebApp({ webApp, skipClosingConfirmation: true });
|
||||
if (webApp) {
|
||||
const key = getWebAppKey(webApp);
|
||||
closeWebApp({ key, skipClosingConfirmation: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_request_viewport') {
|
||||
@ -214,6 +221,51 @@ const useWebAppFrame = (
|
||||
});
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_set_emoji_status') {
|
||||
const { custom_emoji_id, duration } = eventData;
|
||||
|
||||
if (!custom_emoji_id || typeof custom_emoji_id !== 'string') {
|
||||
sendEvent({
|
||||
eventType: 'emoji_status_failed',
|
||||
eventData: {
|
||||
error: 'SUGGESTED_EMOJI_INVALID',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (duration) {
|
||||
try {
|
||||
BigInt(duration);
|
||||
} catch (e) {
|
||||
sendEvent({
|
||||
eventType: 'emoji_status_failed',
|
||||
eventData: {
|
||||
error: 'DURATION_INVALID',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!webApp) {
|
||||
sendEvent({
|
||||
eventType: 'emoji_status_failed',
|
||||
eventData: {
|
||||
error: 'UNKNOWN_ERROR',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
openSuggestedStatusModal({
|
||||
webAppKey: getWebAppKey(webApp),
|
||||
customEmojiId: custom_emoji_id,
|
||||
duration: Number(duration),
|
||||
botId: webApp.botId,
|
||||
});
|
||||
}
|
||||
|
||||
onEvent(data);
|
||||
} catch (err) {
|
||||
// Ignore other messages
|
||||
@ -231,6 +283,21 @@ const useWebAppFrame = (
|
||||
sendViewport(isResizing);
|
||||
}, [sendViewport, windowSize]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!webApp?.plannedEvents?.length) return;
|
||||
const events = webApp.plannedEvents;
|
||||
events.forEach((event) => {
|
||||
sendEvent(event);
|
||||
});
|
||||
|
||||
updateWebApp({
|
||||
key: getWebAppKey(webApp),
|
||||
update: {
|
||||
plannedEvents: [],
|
||||
},
|
||||
});
|
||||
}, [sendEvent, webApp]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('message', handleMessage);
|
||||
return () => window.removeEventListener('message', handleMessage);
|
||||
|
||||
@ -165,7 +165,6 @@
|
||||
.modal-content,
|
||||
.modal-content > p {
|
||||
unicode-bidi: plaintext;
|
||||
text-align: initial;
|
||||
}
|
||||
|
||||
.modal-about {
|
||||
|
||||
@ -46,6 +46,13 @@
|
||||
|
||||
.notification-icon {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.notification-emoji-icon {
|
||||
--custom-emoji-size: 1.75rem;
|
||||
}
|
||||
|
||||
.notification-icon, .notification-emoji-icon {
|
||||
margin-inline-end: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
@ -13,12 +13,14 @@ import { isLangFnParam } from '../../util/localization/types';
|
||||
import { ANIMATION_END_DELAY } from '../../config';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||
import { REM } from '../common/helpers/mediaDimensions';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated';
|
||||
|
||||
import CustomEmoji from '../common/CustomEmoji';
|
||||
import Icon from '../common/icons/Icon';
|
||||
import Button from './Button';
|
||||
import Portal from './Portal';
|
||||
@ -32,6 +34,7 @@ type OwnProps = {
|
||||
|
||||
const DEFAULT_DURATION = 3000;
|
||||
const ANIMATION_DURATION = 150;
|
||||
const CUSTOM_EMOJI_SIZE = 1.75 * REM;
|
||||
|
||||
const Notification: FC<OwnProps> = ({
|
||||
notification,
|
||||
@ -51,6 +54,7 @@ const Notification: FC<OwnProps> = ({
|
||||
dismissAction,
|
||||
duration = DEFAULT_DURATION,
|
||||
icon,
|
||||
customEmojiIconId,
|
||||
shouldShowTimer,
|
||||
title,
|
||||
containerSelector,
|
||||
@ -155,7 +159,16 @@ const Notification: FC<OwnProps> = ({
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<Icon name={icon || 'info-filled'} className="notification-icon" />
|
||||
{customEmojiIconId ? (
|
||||
<CustomEmoji
|
||||
className="notification-emoji-icon"
|
||||
forceAlways
|
||||
size={CUSTOM_EMOJI_SIZE}
|
||||
documentId={customEmojiIconId}
|
||||
/>
|
||||
) : (
|
||||
<Icon name={icon || 'info-filled'} className="notification-icon" />
|
||||
)}
|
||||
<div className="content">
|
||||
{renderedTitle && (
|
||||
<div className="notification-title">{renderedTitle}</div>
|
||||
|
||||
@ -18,6 +18,7 @@ import './api/statistics';
|
||||
import './api/stories';
|
||||
import './ui/initial';
|
||||
import './ui/chats';
|
||||
import './ui/bots';
|
||||
import './ui/messages';
|
||||
import './ui/globalSearch';
|
||||
import './ui/middleSearch';
|
||||
|
||||
@ -4,7 +4,7 @@ import type {
|
||||
ActionReturnType, GlobalState, TabArgs, WebApp,
|
||||
} from '../../types';
|
||||
import {
|
||||
type ApiChat, type ApiChatType, type ApiContact, type ApiInputMessageReplyInfo, type ApiPeer, type ApiUrlAuthResult,
|
||||
type ApiChat, type ApiContact, type ApiInputMessageReplyInfo, type ApiPeer, type ApiUrlAuthResult,
|
||||
MAIN_THREAD_ID,
|
||||
} from '../../../api/types';
|
||||
import { ManagementProgress } from '../../../types';
|
||||
@ -30,10 +30,9 @@ import {
|
||||
} from '../../reducers';
|
||||
import {
|
||||
activateWebAppIfOpen,
|
||||
addWebAppToOpenList, clearOpenedWebApps, hasOpenedMoreThanOneWebApps,
|
||||
hasOpenedWebApps, removeActiveWebAppFromOpenList, removeWebAppFromOpenList,
|
||||
replaceInlineBotSettings, replaceInlineBotsIsLoading,
|
||||
replaceIsWebAppModalOpen, replaceWebAppModalState, updateWebApp,
|
||||
addWebAppToOpenList,
|
||||
replaceInlineBotSettings,
|
||||
replaceInlineBotsIsLoading,
|
||||
} from '../../reducers/bots';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
import {
|
||||
@ -692,18 +691,6 @@ addActionHandler('loadPreviewMedias', async (global, actions, payload): Promise<
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('openWebAppTab', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
webApp, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
if (webApp) {
|
||||
global = getGlobal();
|
||||
global = addWebAppToOpenList(global, webApp, true, true, tabId);
|
||||
setGlobal(global);
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('openWebAppsCloseConfirmationModal', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
tabId = getCurrentTabId(),
|
||||
@ -872,143 +859,6 @@ addActionHandler('sendWebViewData', (global, actions, payload): ActionReturnType
|
||||
});
|
||||
});
|
||||
|
||||
addActionHandler('updateWebApp', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
webApp, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
return updateWebApp(global, webApp, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('closeActiveWebApp', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
global = removeActiveWebAppFromOpenList(global, tabId);
|
||||
if (!hasOpenedWebApps(global, tabId)) return replaceIsWebAppModalOpen(global, false, tabId);
|
||||
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('openMoreAppsTab', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
const tabState = selectTabState(global, tabId);
|
||||
global = updateTabState(global, {
|
||||
webApps: {
|
||||
...tabState.webApps,
|
||||
activeWebApp: undefined,
|
||||
isMoreAppsTabActive: true,
|
||||
},
|
||||
}, tabId);
|
||||
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('closeMoreAppsTab', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
const tabState = selectTabState(global, tabId);
|
||||
|
||||
const openedWebApps = tabState.webApps.openedWebApps;
|
||||
|
||||
const openedWebAppsValues = Object.values(openedWebApps);
|
||||
const openedWebAppsCount = openedWebAppsValues.length;
|
||||
|
||||
global = updateTabState(global, {
|
||||
webApps: {
|
||||
...tabState.webApps,
|
||||
isMoreAppsTabActive: false,
|
||||
activeWebApp: openedWebAppsCount ? openedWebAppsValues[openedWebAppsCount - 1] : undefined,
|
||||
isModalOpen: openedWebAppsCount > 0,
|
||||
},
|
||||
}, tabId);
|
||||
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('closeWebApp', (global, actions, payload): ActionReturnType => {
|
||||
const { webApp, skipClosingConfirmation, tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
global = removeWebAppFromOpenList(global, webApp, skipClosingConfirmation, tabId);
|
||||
if (!hasOpenedWebApps(global, tabId)) return replaceIsWebAppModalOpen(global, false, tabId);
|
||||
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('closeWebAppModal', (global, actions, payload): ActionReturnType => {
|
||||
const { shouldSkipConfirmation, tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
const shouldShowConfirmation = !shouldSkipConfirmation
|
||||
&& !global.settings.byKey.shouldSkipWebAppCloseConfirmation && hasOpenedMoreThanOneWebApps(global, tabId);
|
||||
|
||||
if (shouldShowConfirmation) {
|
||||
actions.openWebAppsCloseConfirmationModal({ tabId });
|
||||
return global;
|
||||
}
|
||||
|
||||
global = clearOpenedWebApps(global, tabId);
|
||||
if (!hasOpenedWebApps(global, tabId)) return replaceIsWebAppModalOpen(global, false, tabId);
|
||||
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('changeWebAppModalState', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
const tabState = selectTabState(global, tabId);
|
||||
|
||||
const newModalState = tabState.webApps.modalState === 'maximized' ? 'minimized' : 'maximized';
|
||||
return replaceWebAppModalState(global, newModalState, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('setWebAppPaymentSlug', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload;
|
||||
const tabState = selectTabState(global, tabId);
|
||||
const activeWebApp = tabState.webApps.activeWebApp;
|
||||
if (!activeWebApp?.url) return undefined;
|
||||
|
||||
const updatedApp = {
|
||||
...activeWebApp,
|
||||
slug: payload.slug,
|
||||
};
|
||||
|
||||
return updateWebApp(global, updatedApp, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('cancelBotTrustRequest', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
return updateTabState(global, {
|
||||
botTrustRequest: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('markBotTrusted', (global, actions, payload): ActionReturnType => {
|
||||
const { botId, isWriteAllowed, tabId = getCurrentTabId() } = payload;
|
||||
const { trustedBotIds } = global;
|
||||
|
||||
const newTrustedBotIds = new Set(trustedBotIds);
|
||||
newTrustedBotIds.add(botId);
|
||||
|
||||
global = {
|
||||
...global,
|
||||
trustedBotIds: Array.from(newTrustedBotIds),
|
||||
};
|
||||
|
||||
const tabState = selectTabState(global, tabId);
|
||||
if (tabState.botTrustRequest?.onConfirm) {
|
||||
const { action, payload: callbackPayload } = tabState.botTrustRequest.onConfirm;
|
||||
// @ts-ignore
|
||||
actions[action]({
|
||||
...(callbackPayload as {}),
|
||||
isWriteAllowed,
|
||||
});
|
||||
}
|
||||
|
||||
global = updateTabState(global, {
|
||||
botTrustRequest: undefined,
|
||||
}, tabId);
|
||||
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('loadAttachBots', async (global): Promise<void> => {
|
||||
await loadAttachBots(global);
|
||||
|
||||
@ -1150,50 +1000,6 @@ addActionHandler('confirmAttachBotInstall', async (global, actions, payload): Pr
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('cancelAttachBotInstall', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
return updateTabState(global, {
|
||||
requestedAttachBotInstall: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('requestAttachBotInChat', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
bot, filter, startParam, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
const currentChatId = selectCurrentMessageList(global, tabId)?.chatId;
|
||||
|
||||
const supportedFilters = bot.attachMenuPeerTypes?.filter((type): type is ApiChatType => (
|
||||
type !== 'self' && filter.includes(type)
|
||||
));
|
||||
|
||||
if (!supportedFilters?.length) {
|
||||
actions.callAttachBot({
|
||||
chatId: currentChatId || bot.id,
|
||||
bot,
|
||||
startParam,
|
||||
tabId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
global = updateTabState(global, {
|
||||
requestedAttachBotInChat: {
|
||||
bot,
|
||||
filter: supportedFilters,
|
||||
startParam,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('cancelAttachBotInChat', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
return updateTabState(global, {
|
||||
requestedAttachBotInChat: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('requestBotUrlAuth', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
chatId, buttonId, messageId, url, tabId = getCurrentTabId(),
|
||||
|
||||
@ -29,9 +29,11 @@ import {
|
||||
updateUserSearch,
|
||||
updateUserSearchFetchingStatus,
|
||||
} from '../../reducers';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
import {
|
||||
selectChat,
|
||||
selectChatFullInfo,
|
||||
selectIsCurrentUserPremium,
|
||||
selectPeer,
|
||||
selectPeerPhotos,
|
||||
selectTabState,
|
||||
@ -388,10 +390,62 @@ addActionHandler('reportSpam', (global, actions, payload): ActionReturnType => {
|
||||
void callApi('reportSpam', peer);
|
||||
});
|
||||
|
||||
addActionHandler('setEmojiStatus', (global, actions, payload): ActionReturnType => {
|
||||
const { emojiStatus, expires } = payload;
|
||||
addActionHandler('setEmojiStatus', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
emojiStatusId, referrerWebAppKey, expires, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
void callApi('updateEmojiStatus', emojiStatus, expires);
|
||||
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
||||
if (!isCurrentUserPremium) {
|
||||
if (referrerWebAppKey) {
|
||||
actions.sendWebAppEvent({
|
||||
webAppKey: referrerWebAppKey,
|
||||
event: {
|
||||
eventType: 'emoji_status_failed',
|
||||
eventData: {
|
||||
error: 'USER_DECLINED',
|
||||
},
|
||||
},
|
||||
tabId,
|
||||
});
|
||||
}
|
||||
|
||||
actions.openPremiumModal({ initialSection: 'emoji_status', tabId });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await callApi('updateEmojiStatus', emojiStatusId, expires);
|
||||
|
||||
if (referrerWebAppKey) {
|
||||
if (!result) {
|
||||
actions.sendWebAppEvent({
|
||||
webAppKey: referrerWebAppKey,
|
||||
event: {
|
||||
eventType: 'emoji_status_failed',
|
||||
eventData: {
|
||||
error: 'SERVER_ERROR',
|
||||
},
|
||||
},
|
||||
tabId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
actions.sendWebAppEvent({
|
||||
webAppKey: referrerWebAppKey,
|
||||
event: {
|
||||
eventType: 'emoji_status_set',
|
||||
},
|
||||
tabId,
|
||||
});
|
||||
actions.showNotification({
|
||||
message: {
|
||||
key: 'BotSuggestedStatusUpdated',
|
||||
},
|
||||
customEmojiIconId: emojiStatusId,
|
||||
tabId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('saveCloseFriends', async (global, actions, payload): Promise<void> => {
|
||||
@ -418,3 +472,39 @@ addActionHandler('saveCloseFriends', async (global, actions, payload): Promise<v
|
||||
});
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('openSuggestedStatusModal', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
customEmojiId, duration, botId, webAppKey, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
const customEmoji = await callApi('fetchCustomEmoji', {
|
||||
documentId: [customEmojiId],
|
||||
});
|
||||
if (!customEmoji?.[0]) {
|
||||
if (webAppKey) {
|
||||
actions.sendWebAppEvent({
|
||||
webAppKey,
|
||||
event: {
|
||||
eventType: 'emoji_status_failed',
|
||||
eventData: {
|
||||
error: 'SUGGESTED_EMOJI_INVALID',
|
||||
},
|
||||
},
|
||||
tabId,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
global = updateTabState(global, {
|
||||
suggestedStatusModal: {
|
||||
customEmojiId,
|
||||
duration,
|
||||
webAppKey,
|
||||
botId,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
@ -1,8 +1,22 @@
|
||||
import type { ApiChatType } from '../../../api/types';
|
||||
import type { ActionReturnType } from '../../types';
|
||||
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { getWebAppKey } from '../../helpers';
|
||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||
import { addWebAppToOpenList } from '../../reducers/bots';
|
||||
import {
|
||||
addWebAppToOpenList,
|
||||
clearOpenedWebApps,
|
||||
hasOpenedMoreThanOneWebApps,
|
||||
hasOpenedWebApps,
|
||||
removeActiveWebAppFromOpenList,
|
||||
removeWebAppFromOpenList,
|
||||
replaceIsWebAppModalOpen,
|
||||
replaceWebAppModalState,
|
||||
updateWebApp,
|
||||
} from '../../reducers/bots';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
import { selectCurrentMessageList, selectTabState, selectWebApp } from '../../selectors';
|
||||
|
||||
addActionHandler('openWebAppTab', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
@ -15,3 +29,199 @@ addActionHandler('openWebAppTab', (global, actions, payload): ActionReturnType =
|
||||
global = addWebAppToOpenList(global, webApp, true, true, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('updateWebApp', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
key, update, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
return updateWebApp(global, key, update, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('closeActiveWebApp', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
global = removeActiveWebAppFromOpenList(global, tabId);
|
||||
if (!hasOpenedWebApps(global, tabId)) return replaceIsWebAppModalOpen(global, false, tabId);
|
||||
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('openMoreAppsTab', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
const tabState = selectTabState(global, tabId);
|
||||
global = updateTabState(global, {
|
||||
webApps: {
|
||||
...tabState.webApps,
|
||||
activeWebApp: undefined,
|
||||
isMoreAppsTabActive: true,
|
||||
},
|
||||
}, tabId);
|
||||
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('closeMoreAppsTab', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
const tabState = selectTabState(global, tabId);
|
||||
|
||||
const openedWebApps = tabState.webApps.openedWebApps;
|
||||
|
||||
const openedWebAppsValues = Object.values(openedWebApps);
|
||||
const openedWebAppsCount = openedWebAppsValues.length;
|
||||
|
||||
global = updateTabState(global, {
|
||||
webApps: {
|
||||
...tabState.webApps,
|
||||
isMoreAppsTabActive: false,
|
||||
activeWebApp: openedWebAppsCount ? openedWebAppsValues[openedWebAppsCount - 1] : undefined,
|
||||
isModalOpen: openedWebAppsCount > 0,
|
||||
},
|
||||
}, tabId);
|
||||
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('closeWebApp', (global, actions, payload): ActionReturnType => {
|
||||
const { key, skipClosingConfirmation, tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
global = removeWebAppFromOpenList(global, key, skipClosingConfirmation, tabId);
|
||||
if (!hasOpenedWebApps(global, tabId)) return replaceIsWebAppModalOpen(global, false, tabId);
|
||||
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('closeWebAppModal', (global, actions, payload): ActionReturnType => {
|
||||
const { shouldSkipConfirmation, tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
const shouldShowConfirmation = !shouldSkipConfirmation
|
||||
&& !global.settings.byKey.shouldSkipWebAppCloseConfirmation && hasOpenedMoreThanOneWebApps(global, tabId);
|
||||
|
||||
if (shouldShowConfirmation) {
|
||||
actions.openWebAppsCloseConfirmationModal({ tabId });
|
||||
return global;
|
||||
}
|
||||
|
||||
global = clearOpenedWebApps(global, tabId);
|
||||
if (!hasOpenedWebApps(global, tabId)) return replaceIsWebAppModalOpen(global, false, tabId);
|
||||
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('changeWebAppModalState', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
const tabState = selectTabState(global, tabId);
|
||||
|
||||
const newModalState = tabState.webApps.modalState === 'maximized' ? 'minimized' : 'maximized';
|
||||
return replaceWebAppModalState(global, newModalState, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('setWebAppPaymentSlug', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload;
|
||||
const tabState = selectTabState(global, tabId);
|
||||
const activeWebApp = tabState.webApps.activeWebApp;
|
||||
if (!activeWebApp?.url) return undefined;
|
||||
|
||||
const key = getWebAppKey(activeWebApp);
|
||||
|
||||
return updateWebApp(global, key, { slug: payload.slug }, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('cancelBotTrustRequest', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
return updateTabState(global, {
|
||||
botTrustRequest: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('markBotTrusted', (global, actions, payload): ActionReturnType => {
|
||||
const { botId, isWriteAllowed, tabId = getCurrentTabId() } = payload;
|
||||
const { trustedBotIds } = global;
|
||||
|
||||
const newTrustedBotIds = new Set(trustedBotIds);
|
||||
newTrustedBotIds.add(botId);
|
||||
|
||||
global = {
|
||||
...global,
|
||||
trustedBotIds: Array.from(newTrustedBotIds),
|
||||
};
|
||||
|
||||
const tabState = selectTabState(global, tabId);
|
||||
if (tabState.botTrustRequest?.onConfirm) {
|
||||
const { action, payload: callbackPayload } = tabState.botTrustRequest.onConfirm;
|
||||
// @ts-ignore
|
||||
actions[action]({
|
||||
...(callbackPayload as {}),
|
||||
isWriteAllowed,
|
||||
});
|
||||
}
|
||||
|
||||
global = updateTabState(global, {
|
||||
botTrustRequest: undefined,
|
||||
}, tabId);
|
||||
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('sendWebAppEvent', (global, actions, payload): ActionReturnType => {
|
||||
const { event, webAppKey, tabId = getCurrentTabId() } = payload;
|
||||
const webApp = selectWebApp(global, webAppKey, tabId);
|
||||
if (!webApp) return global;
|
||||
|
||||
const newPlannedEvents = webApp.plannedEvents ? [...webApp.plannedEvents, event] : [event];
|
||||
|
||||
actions.updateWebApp({
|
||||
key: webAppKey,
|
||||
update: {
|
||||
plannedEvents: newPlannedEvents,
|
||||
},
|
||||
tabId,
|
||||
});
|
||||
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('cancelAttachBotInstall', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
return updateTabState(global, {
|
||||
requestedAttachBotInstall: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('requestAttachBotInChat', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
bot, filter, startParam, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
const currentChatId = selectCurrentMessageList(global, tabId)?.chatId;
|
||||
|
||||
const supportedFilters = bot.attachMenuPeerTypes?.filter((type): type is ApiChatType => (
|
||||
type !== 'self' && filter.includes(type)
|
||||
));
|
||||
|
||||
if (!supportedFilters?.length) {
|
||||
actions.callAttachBot({
|
||||
chatId: currentChatId || bot.id,
|
||||
bot,
|
||||
startParam,
|
||||
tabId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
global = updateTabState(global, {
|
||||
requestedAttachBotInChat: {
|
||||
bot,
|
||||
filter: supportedFilters,
|
||||
startParam,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('cancelAttachBotInChat', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
return updateTabState(global, {
|
||||
requestedAttachBotInChat: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
@ -9,7 +9,7 @@ addActionHandler('setUserSearchQuery', (global, actions, payload): ActionReturnT
|
||||
const {
|
||||
query,
|
||||
tabId = getCurrentTabId(),
|
||||
} = payload!;
|
||||
} = payload;
|
||||
|
||||
return updateUserSearch(global, {
|
||||
globalUserIds: undefined,
|
||||
@ -20,7 +20,7 @@ addActionHandler('setUserSearchQuery', (global, actions, payload): ActionReturnT
|
||||
});
|
||||
|
||||
addActionHandler('openAddContactDialog', (global, actions, payload): ActionReturnType => {
|
||||
const { userId, tabId = getCurrentTabId() } = payload!;
|
||||
const { userId, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
return updateTabState(global, {
|
||||
newContact: { userId },
|
||||
@ -42,3 +42,11 @@ addActionHandler('closeNewContactDialog', (global, actions, payload): ActionRetu
|
||||
|
||||
return closeNewContactDialog(global, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('closeSuggestedStatusModal', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
return updateTabState(global, {
|
||||
suggestedStatusModal: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
@ -20,7 +20,7 @@ export function convertToApiChatType(type: string): ApiChatType | undefined {
|
||||
export function getWebAppKey(webApp: Partial<WebApp>) {
|
||||
if (webApp.requestUrl) return webApp.requestUrl;
|
||||
if (webApp.appName) return `${webApp.botId}?appName=${webApp.appName}`;
|
||||
return webApp.botId;
|
||||
return webApp.botId!;
|
||||
}
|
||||
|
||||
export function isSystemBot(botId: string) {
|
||||
|
||||
@ -37,20 +37,19 @@ export function replaceInlineBotsIsLoading<T extends GlobalState>(
|
||||
}
|
||||
|
||||
export function updateWebApp <T extends GlobalState>(
|
||||
global: T, webApp: Partial<WebApp>,
|
||||
global: T, key: string, webAppUpdate: Partial<WebApp>,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): T {
|
||||
const currentTabState = selectTabState(global, tabId);
|
||||
const openedWebApps = currentTabState.webApps.openedWebApps;
|
||||
|
||||
const key = webApp && getWebAppKey(webApp);
|
||||
const originalWebApp = key ? openedWebApps[key] : undefined;
|
||||
const originalWebApp = openedWebApps[key];
|
||||
|
||||
if (!originalWebApp) return global;
|
||||
|
||||
const updatedValue = {
|
||||
...originalWebApp,
|
||||
...webApp,
|
||||
...webAppUpdate,
|
||||
};
|
||||
|
||||
const updatedWebAppKey = getWebAppKey(updatedValue);
|
||||
@ -143,18 +142,22 @@ export function removeActiveWebAppFromOpenList<T extends GlobalState>(
|
||||
|
||||
if (!currentTabState.webApps.activeWebApp) return global;
|
||||
|
||||
return removeWebAppFromOpenList(global, currentTabState.webApps.activeWebApp, false, tabId);
|
||||
const key = getWebAppKey(currentTabState.webApps.activeWebApp);
|
||||
|
||||
return removeWebAppFromOpenList(global, key, false, tabId);
|
||||
}
|
||||
|
||||
export function removeWebAppFromOpenList<T extends GlobalState>(
|
||||
global: T, webApp: WebApp, skipClosingConfirmation?: boolean,
|
||||
global: T, key: string, skipClosingConfirmation?: boolean,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): T {
|
||||
const currentTabState = selectTabState(global, tabId);
|
||||
const openedWebApps = currentTabState.webApps.openedWebApps;
|
||||
const webApp = openedWebApps[key];
|
||||
if (!webApp) return global;
|
||||
|
||||
if (!skipClosingConfirmation && webApp.shouldConfirmClosing) {
|
||||
return updateWebApp(global, { ...webApp, isCloseModalOpen: true }, tabId);
|
||||
return updateWebApp(global, key, { isCloseModalOpen: true }, tabId);
|
||||
}
|
||||
|
||||
const updatedOpenedWebApps = { ...openedWebApps };
|
||||
@ -164,7 +167,7 @@ export function removeWebAppFromOpenList<T extends GlobalState>(
|
||||
|
||||
if (removingWebAppKey) {
|
||||
delete updatedOpenedWebApps[removingWebAppKey];
|
||||
newOpenedKeys = currentTabState.webApps.openedOrderedKeys.filter((key) => key !== removingWebAppKey);
|
||||
newOpenedKeys = currentTabState.webApps.openedOrderedKeys.filter((k) => k !== removingWebAppKey);
|
||||
}
|
||||
|
||||
const activeWebApp = currentTabState.webApps.activeWebApp;
|
||||
|
||||
@ -150,3 +150,9 @@ export function selectIsSynced<T extends GlobalState>(global: T) {
|
||||
export function selectCanAnimateSnapEffect<T extends GlobalState>(global: T) {
|
||||
return IS_SNAP_EFFECT_SUPPORTED && selectPerformanceSettingsValue(global, 'snapEffect');
|
||||
}
|
||||
|
||||
export function selectWebApp<T extends GlobalState>(
|
||||
global: T, key: string, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
return selectTabState(global, tabId).webApps.openedWebApps[key];
|
||||
}
|
||||
|
||||
@ -911,6 +911,13 @@ export type TabState = {
|
||||
userId?: string;
|
||||
gift: ApiUserStarGift | ApiStarGift;
|
||||
};
|
||||
|
||||
suggestedStatusModal?: {
|
||||
botId: string;
|
||||
webAppKey?: string;
|
||||
customEmojiId: string;
|
||||
duration?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type GlobalState = {
|
||||
@ -1333,6 +1340,7 @@ export type WebApp = {
|
||||
backgroundColor?: string;
|
||||
isBackButtonVisible?: boolean;
|
||||
isSettingsButtonVisible?: boolean;
|
||||
plannedEvents?: WebAppOutboundEvent[];
|
||||
sendEvent?: (event: WebAppOutboundEvent) => void;
|
||||
reloadFrame?: (url: string) => void;
|
||||
};
|
||||
@ -3115,7 +3123,8 @@ export interface ActionPayloads {
|
||||
startParam?: string;
|
||||
} & WithTabId;
|
||||
updateWebApp: {
|
||||
webApp: Partial<WebApp>;
|
||||
key: string;
|
||||
update: Partial<WebApp>;
|
||||
} & WithTabId;
|
||||
requestMainWebView: {
|
||||
botId: string;
|
||||
@ -3260,9 +3269,13 @@ export interface ActionPayloads {
|
||||
openMoreAppsTab: WithTabId | undefined;
|
||||
closeMoreAppsTab: WithTabId | undefined;
|
||||
closeWebApp: {
|
||||
webApp: WebApp;
|
||||
key: string;
|
||||
skipClosingConfirmation?: boolean;
|
||||
} & WithTabId;
|
||||
sendWebAppEvent: {
|
||||
webAppKey: string;
|
||||
event: WebAppOutboundEvent;
|
||||
} & WithTabId;
|
||||
closeWebAppModal: ({
|
||||
shouldSkipConfirmation?: boolean;
|
||||
} & WithTabId) | undefined;
|
||||
@ -3546,9 +3559,17 @@ export interface ActionPayloads {
|
||||
closeStarsGiftModal: WithTabId | undefined;
|
||||
|
||||
setEmojiStatus: {
|
||||
emojiStatus: ApiSticker;
|
||||
emojiStatusId: string;
|
||||
expires?: number;
|
||||
};
|
||||
referrerWebAppKey?: string;
|
||||
} & WithTabId;
|
||||
openSuggestedStatusModal: {
|
||||
botId: string;
|
||||
webAppKey?: string;
|
||||
customEmojiId: string;
|
||||
duration?: number;
|
||||
} & WithTabId;
|
||||
closeSuggestedStatusModal: WithTabId | undefined;
|
||||
|
||||
// Invoice
|
||||
openInvoice: Exclude<ApiInputInvoice, ApiInputInvoiceStarGift> & WithTabId;
|
||||
|
||||
@ -509,6 +509,7 @@ export type CustomPeer = {
|
||||
peerColorId?: number;
|
||||
isVerified?: boolean;
|
||||
fakeType?: ApiFakeType;
|
||||
emojiStatusId?: string;
|
||||
customPeerAvatarColor?: string;
|
||||
withPremiumGradient?: boolean;
|
||||
} & ({
|
||||
|
||||
10
src/types/language.d.ts
vendored
10
src/types/language.d.ts
vendored
@ -943,6 +943,7 @@ export interface LangPair {
|
||||
'ScheduleSendWhenOnline': undefined;
|
||||
'VoipIncoming': undefined;
|
||||
'LiveLocationUpdatedJustNow': undefined;
|
||||
'RightNow': undefined;
|
||||
'AudioPause': undefined;
|
||||
'AudioPlay': undefined;
|
||||
'ToggleUserNotifications': undefined;
|
||||
@ -1152,6 +1153,8 @@ export interface LangPair {
|
||||
'CloseMiniApps': undefined;
|
||||
'DoNotAskAgain': undefined;
|
||||
'PaymentInfoDone': undefined;
|
||||
'BotSuggestedStatusTitle': undefined;
|
||||
'BotSuggestedStatusUpdated': undefined;
|
||||
}
|
||||
|
||||
export interface LangPairWithVariables<V extends unknown = LangVariable> {
|
||||
@ -1541,6 +1544,13 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
|
||||
'StarsPerMonth': {
|
||||
'amount': V;
|
||||
};
|
||||
'BotSuggestedStatusFor': {
|
||||
'bot': V;
|
||||
'duration': V;
|
||||
};
|
||||
'BotSuggestedStatus': {
|
||||
'bot': V;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LangPairPlural {
|
||||
|
||||
@ -95,6 +95,10 @@ export type WebAppInboundEvent =
|
||||
WebAppEvent<'web_app_biometry_update_token', {
|
||||
token: string;
|
||||
}> |
|
||||
WebAppEvent<'web_app_set_emoji_status', {
|
||||
custom_emoji_id: string;
|
||||
duration?: number;
|
||||
}> |
|
||||
WebAppEvent<'web_app_request_viewport' | 'web_app_request_theme' | 'web_app_ready' | 'web_app_expand'
|
||||
| 'web_app_request_phone' | 'web_app_close' | 'web_app_close_scan_qr_popup'
|
||||
| 'web_app_request_write_access' | 'web_app_request_phone' | 'iframe_will_reload'
|
||||
@ -168,6 +172,10 @@ export type WebAppOutboundEvent =
|
||||
WebAppEvent<'biometry_token_updated', {
|
||||
status: 'updated' | 'removed' | 'failed';
|
||||
}> |
|
||||
WebAppEvent<'emoji_status_failed', {
|
||||
error: 'UNSUPPORTED' | 'USER_DECLINED' | 'SUGGESTED_EMOJI_INVALID'
|
||||
| 'DURATION_INVALID' | 'SERVER_ERROR' | 'UNKNOWN_ERROR';
|
||||
}> |
|
||||
WebAppEvent<'main_button_pressed' |
|
||||
'secondary_button_pressed' | 'back_button_pressed' | 'settings_button_pressed' | 'scan_qr_popup_closed'
|
||||
| 'reload_iframe', null>;
|
||||
| 'reload_iframe' | 'emoji_status_set', null>;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { OldLangFn } from '../../hooks/useOldLang';
|
||||
import type { TimeFormat } from '../../types';
|
||||
import type { LangFn } from '../localization';
|
||||
|
||||
import withCache from '../withCache';
|
||||
|
||||
@ -374,29 +375,28 @@ export function formatDateAtTime(
|
||||
return lang('formatDateAtTime', [formattedDate, time]);
|
||||
}
|
||||
|
||||
export function formatDateInFuture(
|
||||
lang: OldLangFn,
|
||||
currentTime: number,
|
||||
datetime: number,
|
||||
) {
|
||||
const diff = Math.ceil(datetime - currentTime);
|
||||
if (diff < 0) {
|
||||
export function formatShortDuration(lang: LangFn, duration: number) {
|
||||
if (duration < 0) {
|
||||
return lang('RightNow');
|
||||
}
|
||||
|
||||
if (diff < 60) {
|
||||
return lang('Seconds', diff);
|
||||
if (duration < 60) {
|
||||
const count = Math.ceil(duration);
|
||||
return lang('Seconds', { count }, { pluralValue: duration });
|
||||
}
|
||||
|
||||
if (diff < 60 * 60) {
|
||||
return lang('Minutes', Math.ceil(diff / 60));
|
||||
if (duration < 60 * 60) {
|
||||
const count = Math.ceil(duration / 60);
|
||||
return lang('Minutes', { count }, { pluralValue: count });
|
||||
}
|
||||
|
||||
if (diff < 60 * 60 * 24) {
|
||||
return lang('Hours', Math.ceil(diff / (60 * 60)));
|
||||
if (duration < 60 * 60 * 24) {
|
||||
const count = Math.ceil(duration / (60 * 60));
|
||||
return lang('Hours', { count }, { pluralValue: count });
|
||||
}
|
||||
|
||||
return lang('Days', Math.ceil(diff / (60 * 60 * 24)));
|
||||
const count = Math.ceil(duration / (60 * 60 * 24));
|
||||
return lang('Days', { count }, { pluralValue: count });
|
||||
}
|
||||
|
||||
function isValidDate(day: number, month: number, year = 2021): boolean {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user