Story Ribbon: Allow opening with Stealth Mode (#6471)

This commit is contained in:
zubiden 2025-11-17 12:18:22 +04:00 committed by Alexander Zinchuk
parent a9e68aa473
commit 880a6c6977
30 changed files with 394 additions and 341 deletions

View File

@ -642,6 +642,7 @@
"AttachmentMenuPhotoOrVideo" = "Photo or Video"; "AttachmentMenuPhotoOrVideo" = "Photo or Video";
"AttachDocument" = "File"; "AttachDocument" = "File";
"Poll" = "Poll"; "Poll" = "Poll";
"SlowModePlaceholder" = "Next message in {timer}";
"SlowModeHint" = "Slow Mode is active. You can send\nyour next message in {time}."; "SlowModeHint" = "Slow Mode is active. You can send\nyour next message in {time}.";
"SendMessageAsTitle" = "Send message as..."; "SendMessageAsTitle" = "Send message as...";
"Message" = "Message"; "Message" = "Message";
@ -2342,3 +2343,27 @@
"InviteRestrictedPremiumReasonMultipleMore_one" = "{list} and **{count} more** person only accept invitations to groups from contacts and **Premium** users."; "InviteRestrictedPremiumReasonMultipleMore_one" = "{list} and **{count} more** person only accept invitations to groups from contacts and **Premium** users.";
"InviteRestrictedPremiumReasonMultipleMore_other" = "{list} and **{count} more** people only accept invitations to groups from contacts and **Premium** users."; "InviteRestrictedPremiumReasonMultipleMore_other" = "{list} and **{count} more** people only accept invitations to groups from contacts and **Premium** users.";
"StoryUnsupported" = "This story is not supported in Telegram Web A. Try viewing it in the Telegram app."; "StoryUnsupported" = "This story is not supported in Telegram Web A. Try viewing it in the Telegram app.";
"StoryRibbonMyStory" = "My Story";
"StoryMenuSavedStories" = "Posted Stories";
"StoryMenuArchivedStories" = "Archived Stories";
"StoryMenuSendMessage" = "Send Message";
"StoryMenuViewProfile" = "Open Profile";
"StoryMenuViewChannel" = "Open Channel";
"StoryMenuOpenStealth" = "View Anonymously";
"StoryMenuArchivePeer" = "Hide Stories";
"StoryMenuUnarchivePeer" = "Unhide Stories";
"StealthModeOnTitle" = "Stealth Mode On";
"StealthModeOnHintEnabled" = "The creators of stories you viewed in the past 5 minutes or will view in the next 25 minutes won't see you in the list of viewers.";
"StealthModeOnHint" = "The creators of stories you view in **{time}** won't see you in their list of viewers.";
"StealthModeTitle" = "Stealth Mode";
"StealthModeDescription" = "Turn Stealth Mode on to watch stories without appearing in the list of viewers.";
"StealthModeDescriptionPremium" = "Subscribe to Telegram Premium to watch stories without appearing in the list of viewers.";
"StealthModeHideRecentTitle" = "Hide Recent Views";
"StealthModeHideRecentDescription" = "Hide my views from the past 5 minutes.";
"StealthModeHideFutureTitle" = "Hide Upcoming Views";
"StealthModeHideFutureDescription" = "Hide my views for the next 25 minutes.";
"StealthModeButtonPremium" = "Unlock Stealth Mode";
"StealthModeButton" = "Enable Stealth Mode";
"StealthModeButtonToStory" = "Enable and Open the Story";
"StealthModeButtonRecharge" = "Available in {timer}";
"StealthModeComposerPlaceholder" = "Stealth Mode active {timer}";

View File

@ -105,3 +105,4 @@ export { default as WebAppsCloseConfirmationModal } from '../components/main/Web
export { default as FrozenAccountModal } from '../components/modals/frozenAccount/FrozenAccountModal'; export { default as FrozenAccountModal } from '../components/modals/frozenAccount/FrozenAccountModal';
export { default as ProfileRatingModal } from '../components/modals/profileRating/ProfileRatingModal'; export { default as ProfileRatingModal } from '../components/modals/profileRating/ProfileRatingModal';
export { default as QuickPreviewModal } from '../components/modals/quickPreview/QuickPreviewModal'; export { default as QuickPreviewModal } from '../components/modals/quickPreview/QuickPreviewModal';
export { default as StealthModeModal } from '../components/modals/storyStealthMode/StealthModeModal';

View File

@ -94,6 +94,7 @@ const AboutMonetizationModal: FC<OwnProps> = ({
isOpen={isOpen} isOpen={isOpen}
listItemData={modalData.listItemData} listItemData={modalData.listItemData}
headerIconName="cash-circle" headerIconName="cash-circle"
headerIconPremiumGradient
withSeparator withSeparator
header={modalData.header} header={modalData.header}
footer={modalData.footer} footer={modalData.footer}

View File

@ -136,6 +136,7 @@ import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
import useDerivedState from '../../hooks/useDerivedState'; import useDerivedState from '../../hooks/useDerivedState';
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps'; import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
import useForceUpdate from '../../hooks/useForceUpdate';
import useGetSelectionRange from '../../hooks/useGetSelectionRange'; import useGetSelectionRange from '../../hooks/useGetSelectionRange';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
@ -187,6 +188,7 @@ import ReactionSelector from '../middle/message/reactions/ReactionSelector';
import Button from '../ui/Button'; import Button from '../ui/Button';
import ResponsiveHoverButton from '../ui/ResponsiveHoverButton'; import ResponsiveHoverButton from '../ui/ResponsiveHoverButton';
import Spinner from '../ui/Spinner'; import Spinner from '../ui/Spinner';
import TextTimer from '../ui/TextTimer';
import Transition from '../ui/Transition'; import Transition from '../ui/Transition';
import AnimatedCounter from './AnimatedCounter'; import AnimatedCounter from './AnimatedCounter';
import Avatar from './Avatar'; import Avatar from './Avatar';
@ -484,6 +486,7 @@ const Composer: FC<OwnProps & StateProps> = ({
const lastMessageSendTimeSeconds = useRef<number>(); const lastMessageSendTimeSeconds = useRef<number>();
const prevDropAreaState = usePreviousDeprecated(dropAreaState); const prevDropAreaState = usePreviousDeprecated(dropAreaState);
const { width: windowWidth } = windowSize.get(); const { width: windowWidth } = windowSize.get();
const forceUpdate = useForceUpdate();
const isInMessageList = type === 'messageList'; const isInMessageList = type === 'messageList';
const isInStoryViewer = type === 'story'; const isInStoryViewer = type === 'story';
@ -1662,18 +1665,6 @@ const Composer: FC<OwnProps & StateProps> = ({
&& messageListType === 'thread'; && messageListType === 'thread';
const isBotMenuButtonOpen = withBotMenuButton && !hasText && !activeVoiceRecording; const isBotMenuButtonOpen = withBotMenuButton && !hasText && !activeVoiceRecording;
const [timedPlaceholderLangKey, timedPlaceholderDate] = useMemo(() => {
if (slowMode?.nextSendDate) {
return ['SlowModeWait', slowMode.nextSendDate];
}
if (stealthMode?.activeUntil && isInStoryViewer) {
return ['StealthModeActiveHint', stealthMode.activeUntil];
}
return [];
}, [isInStoryViewer, slowMode?.nextSendDate, stealthMode?.activeUntil]);
const isComposerHasFocus = isBotKeyboardOpen || isSymbolMenuOpen || isEmojiTooltipOpen || isSendAsMenuOpen const isComposerHasFocus = isBotKeyboardOpen || isSymbolMenuOpen || isEmojiTooltipOpen || isSendAsMenuOpen
|| isMentionTooltipOpen || isInlineBotTooltipOpen || isBotCommandMenuOpen || isAttachMenuOpen || isMentionTooltipOpen || isInlineBotTooltipOpen || isBotCommandMenuOpen || isAttachMenuOpen
|| isStickerTooltipOpen || isChatCommandTooltipOpen || isCustomEmojiTooltipOpen || isBotMenuButtonOpen || isStickerTooltipOpen || isChatCommandTooltipOpen || isCustomEmojiTooltipOpen || isBotMenuButtonOpen
@ -1681,12 +1672,21 @@ const Composer: FC<OwnProps & StateProps> = ({
const isReactionSelectorOpen = isComposerHasFocus && !isReactionPickerOpen && isInStoryViewer && !isAttachMenuOpen const isReactionSelectorOpen = isComposerHasFocus && !isReactionPickerOpen && isInStoryViewer && !isAttachMenuOpen
&& !isSymbolMenuOpen; && !isSymbolMenuOpen;
const slowModePlaceholder = (() => {
if (!slowMode?.nextSendDate || slowMode.nextSendDate < getServerTime()) return undefined;
return lang('SlowModePlaceholder', {
timer: <TextTimer endsAt={slowMode.nextSendDate} onEnd={forceUpdate} />,
}, { withNodes: true });
})();
const placeholder = useMemo(() => { const placeholder = useMemo(() => {
if (activeVoiceRecording && windowWidth <= SCREEN_WIDTH_TO_HIDE_PLACEHOLDER) { if (activeVoiceRecording && windowWidth <= SCREEN_WIDTH_TO_HIDE_PLACEHOLDER) {
return ''; return '';
} }
if (!isComposerBlocked) { if (!isComposerBlocked) {
if (slowModePlaceholder) return slowModePlaceholder;
if (botKeyboardPlaceholder) return botKeyboardPlaceholder; if (botKeyboardPlaceholder) return botKeyboardPlaceholder;
if (inputPlaceholder) return inputPlaceholder; if (inputPlaceholder) return inputPlaceholder;
if (paidMessagesStars) { if (paidMessagesStars) {
@ -1701,6 +1701,12 @@ const Composer: FC<OwnProps & StateProps> = ({
return lang('ComposerPlaceholderCaption'); return lang('ComposerPlaceholderCaption');
} }
if (stealthMode?.activeUntil && isInStoryViewer && stealthMode.activeUntil > getServerTime()) {
return lang('StealthModeComposerPlaceholder', {
timer: <TextTimer endsAt={stealthMode.activeUntil} onEnd={forceUpdate} />,
}, { withNodes: true });
}
if (chat?.adminRights?.anonymous) { if (chat?.adminRights?.anonymous) {
return lang('ComposerPlaceholderAnonymous'); return lang('ComposerPlaceholderAnonymous');
} }
@ -1722,7 +1728,7 @@ const Composer: FC<OwnProps & StateProps> = ({
}, [ }, [
activeVoiceRecording, botKeyboardPlaceholder, chat, inputPlaceholder, isChannel, isComposerBlocked, activeVoiceRecording, botKeyboardPlaceholder, chat, inputPlaceholder, isChannel, isComposerBlocked,
isInStoryViewer, isSilentPosting, lang, replyToTopic, isReplying, threadId, windowWidth, paidMessagesStars, isInStoryViewer, isSilentPosting, lang, replyToTopic, isReplying, threadId, windowWidth, paidMessagesStars,
hasSuggestedPost, hasSuggestedPost, slowModePlaceholder, stealthMode?.activeUntil,
]); ]);
useEffect(() => { useEffect(() => {
@ -2172,8 +2178,6 @@ const Composer: FC<OwnProps & StateProps> = ({
isActive={!hasAttachments} isActive={!hasAttachments}
getHtml={getHtml} getHtml={getHtml}
placeholder={placeholder} placeholder={placeholder}
timedPlaceholderDate={timedPlaceholderDate}
timedPlaceholderLangKey={timedPlaceholderLangKey}
forcedPlaceholder={inlineBotHelp} forcedPlaceholder={inlineBotHelp}
canAutoFocus={isReady && isForCurrentMessageList && !hasAttachments && isInMessageList} canAutoFocus={isReady && isForCurrentMessageList && !hasAttachments && isInMessageList}
noFocusInterception={hasAttachments} noFocusInterception={hasAttachments}

View File

@ -138,11 +138,9 @@ const PublicPostsSearchLauncher = ({
</Button> </Button>
{Boolean(waitTill) && ( {Boolean(waitTill) && (
<div className={styles.freeSearchUnlock}> <div className={styles.freeSearchUnlock}>
<TextTimer {lang('UnlockTimerPublicPostsSearch', {
langKey="UnlockTimerPublicPostsSearch" time: <TextTimer endsAt={waitTill + WAIT_DELAY} onEnd={onCheckFlood} />,
endsAt={waitTill + WAIT_DELAY} }, { withNodes: true })}
onEnd={onCheckFlood}
/>
</div> </div>
)} )}
</div> </div>

View File

@ -41,7 +41,6 @@ import useInputCustomEmojis from './hooks/useInputCustomEmojis';
import Icon from '../../common/icons/Icon'; import Icon from '../../common/icons/Icon';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import TextTimer from '../../ui/TextTimer';
import TextFormatter from './TextFormatter.async'; import TextFormatter from './TextFormatter.async';
const CONTEXT_MENU_CLOSE_DELAY_MS = 100; const CONTEXT_MENU_CLOSE_DELAY_MS = 100;
@ -63,23 +62,21 @@ type OwnProps = {
isActive: boolean; isActive: boolean;
getHtml: Signal<string>; getHtml: Signal<string>;
placeholder: TeactNode | string; placeholder: TeactNode | string;
timedPlaceholderLangKey?: string;
timedPlaceholderDate?: number;
forcedPlaceholder?: string; forcedPlaceholder?: string;
noFocusInterception?: boolean; noFocusInterception?: boolean;
canAutoFocus: boolean; canAutoFocus: boolean;
shouldSuppressFocus?: boolean; shouldSuppressFocus?: boolean;
shouldSuppressTextFormatter?: boolean; shouldSuppressTextFormatter?: boolean;
canSendPlainText?: boolean; canSendPlainText?: boolean;
isNeedPremium?: boolean;
messageListType?: MessageListType;
captionLimit?: number;
onUpdate: (html: string) => void; onUpdate: (html: string) => void;
onSuppressedFocus?: () => void; onSuppressedFocus?: () => void;
onSend: () => void; onSend: () => void;
onScroll?: (event: React.UIEvent<HTMLElement>) => void; onScroll?: (event: React.UIEvent<HTMLElement>) => void;
captionLimit?: number;
onFocus?: NoneToVoidFunction; onFocus?: NoneToVoidFunction;
onBlur?: NoneToVoidFunction; onBlur?: NoneToVoidFunction;
isNeedPremium?: boolean;
messageListType?: MessageListType;
}; };
type StateProps = { type StateProps = {
@ -127,8 +124,6 @@ const MessageInput: FC<OwnProps & StateProps> = ({
isActive, isActive,
getHtml, getHtml,
placeholder, placeholder,
timedPlaceholderLangKey,
timedPlaceholderDate,
forcedPlaceholder, forcedPlaceholder,
canSendPlainText, canSendPlainText,
canAutoFocus, canAutoFocus,
@ -139,14 +134,14 @@ const MessageInput: FC<OwnProps & StateProps> = ({
isSelectModeActive, isSelectModeActive,
canPlayAnimatedEmojis, canPlayAnimatedEmojis,
messageSendKeyCombo, messageSendKeyCombo,
isNeedPremium,
messageListType,
onUpdate, onUpdate,
onSuppressedFocus, onSuppressedFocus,
onSend, onSend,
onScroll, onScroll,
onFocus, onFocus,
onBlur, onBlur,
isNeedPremium,
messageListType,
}) => { }) => {
const { const {
editLastMessage, editLastMessage,
@ -177,16 +172,6 @@ const MessageInput: FC<OwnProps & StateProps> = ({
const { isMobile } = useAppLayout(); const { isMobile } = useAppLayout();
const isMobileDevice = isMobile && (IS_IOS || IS_ANDROID); const isMobileDevice = isMobile && (IS_IOS || IS_ANDROID);
const [shouldDisplayTimer, setShouldDisplayTimer] = useState(false);
useEffect(() => {
setShouldDisplayTimer(Boolean(timedPlaceholderLangKey && timedPlaceholderDate));
}, [timedPlaceholderDate, timedPlaceholderLangKey]);
const handleTimerEnd = useLastCallback(() => {
setShouldDisplayTimer(false);
});
useInputCustomEmojis( useInputCustomEmojis(
getHtml, getHtml,
inputRef, inputRef,
@ -611,9 +596,7 @@ const MessageInput: FC<OwnProps & StateProps> = ({
> >
{!isAttachmentModalInput && !canSendPlainText {!isAttachmentModalInput && !canSendPlainText
&& <Icon name="lock-badge" className="placeholder-icon" />} && <Icon name="lock-badge" className="placeholder-icon" />}
{shouldDisplayTimer ? ( {placeholder}
<TextTimer langKey={timedPlaceholderLangKey!} endsAt={timedPlaceholderDate!} onEnd={handleTimerEnd} />
) : placeholder}
{isStoryInput && isNeedPremium && ( {isStoryInput && isNeedPremium && (
<Button className="unlock-button" size="tiny" color="adaptive" onClick={handleOpenPremiumModal}> <Button className="unlock-button" size="tiny" color="adaptive" onClick={handleOpenPremiumModal}>
{oldLang('StoryRepliesLockedButton')} {oldLang('StoryRepliesLockedButton')}

View File

@ -50,6 +50,7 @@ import StarsBalanceModal from './stars/StarsBalanceModal.async';
import StarsPaymentModal from './stars/StarsPaymentModal.async'; import StarsPaymentModal from './stars/StarsPaymentModal.async';
import StarsSubscriptionModal from './stars/subscription/StarsSubscriptionModal.async'; import StarsSubscriptionModal from './stars/subscription/StarsSubscriptionModal.async';
import StarsTransactionInfoModal from './stars/transaction/StarsTransactionModal.async'; import StarsTransactionInfoModal from './stars/transaction/StarsTransactionModal.async';
import StealthModeModal from './storyStealthMode/StealthModeModal.async';
import SuggestedPostApprovalModal from './suggestedPostApproval/SuggestedPostApprovalModal.async'; import SuggestedPostApprovalModal from './suggestedPostApproval/SuggestedPostApprovalModal.async';
import SuggestedStatusModal from './suggestedStatus/SuggestedStatusModal.async'; import SuggestedStatusModal from './suggestedStatus/SuggestedStatusModal.async';
import SuggestMessageModal from './suggestMessage/SuggestMessageModal.async'; import SuggestMessageModal from './suggestMessage/SuggestMessageModal.async';
@ -105,7 +106,8 @@ type ModalKey = keyof Pick<TabState,
'deleteAccountModal' | 'deleteAccountModal' |
'isAgeVerificationModalOpen' | 'isAgeVerificationModalOpen' |
'profileRatingModal' | 'profileRatingModal' |
'quickPreview' 'quickPreview' |
'storyStealthModal'
>; >;
type StateProps = { type StateProps = {
@ -169,6 +171,7 @@ const MODALS: ModalRegistry = {
isAgeVerificationModalOpen: AgeVerificationModal, isAgeVerificationModalOpen: AgeVerificationModal,
profileRatingModal: ProfileRatingModal, profileRatingModal: ProfileRatingModal,
quickPreview: QuickPreviewModal, quickPreview: QuickPreviewModal,
storyStealthModal: StealthModeModal,
}; };
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[]; const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>; const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>;

View File

@ -133,6 +133,7 @@ const AboutAdsModal = ({ modal, minLevelToRestrictAds }: OwnProps & StateProps)
isOpen={isOpen} isOpen={isOpen}
listItemData={modalData?.listItemData} listItemData={modalData?.listItemData}
headerIconName="channel" headerIconName="channel"
headerIconPremiumGradient
withSeparator withSeparator
header={modalData?.header} header={modalData?.header}
footer={modalData?.footer} footer={modalData?.footer}

View File

@ -12,24 +12,28 @@
} }
.topIcon { .topIcon {
--premium-gradient: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%);
display: grid; display: grid;
flex-shrink: 0; flex-shrink: 0;
place-items: center; place-items: center;
width: 6rem; width: 6rem;
height: 6rem; height: 6rem;
margin-bottom: 1rem;
border-radius: 50%; border-radius: 50%;
font-size: 4rem; font-size: 4rem;
color: white; color: var(--color-white);
background: var(--color-primary);
}
.premiumGradient {
--premium-gradient: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%);
background: var(--premium-gradient); background: var(--premium-gradient);
} }
.listItemIcon { .listItemIcon {
font-size: 1.875rem !important;
color: var(--accent-color) !important; color: var(--accent-color) !important;
} }
@ -37,6 +41,7 @@
overflow-x: hidden; overflow-x: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.5rem;
align-items: center; align-items: center;
} }

View File

@ -15,13 +15,15 @@ import styles from './TableAboutModal.module.scss';
export type TableAboutData = [IconName | undefined, TeactNode, TeactNode][]; export type TableAboutData = [IconName | undefined, TeactNode, TeactNode][];
type OwnProps = { type OwnProps = {
className?: string;
contentClassName?: string; contentClassName?: string;
isOpen?: boolean; isOpen?: boolean;
listItemData?: TableAboutData; listItemData?: TableAboutData;
headerIconName?: IconName; headerIconName?: IconName;
headerIconPremiumGradient?: boolean;
header?: TeactNode; header?: TeactNode;
footer?: TeactNode; footer?: TeactNode;
buttonText?: string; buttonText?: TeactNode;
hasBackdrop?: boolean; hasBackdrop?: boolean;
withSeparator?: boolean; withSeparator?: boolean;
onClose: NoneToVoidFunction; onClose: NoneToVoidFunction;
@ -29,28 +31,34 @@ type OwnProps = {
}; };
const TableAboutModal = ({ const TableAboutModal = ({
className,
isOpen, isOpen,
listItemData, listItemData,
headerIconName, headerIconName,
headerIconPremiumGradient,
header, header,
footer, footer,
buttonText, buttonText,
hasBackdrop, hasBackdrop,
withSeparator, withSeparator,
contentClassName,
onClose, onClose,
onButtonClick, onButtonClick,
contentClassName,
}: OwnProps) => { }: OwnProps) => {
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
className={buildClassName(styles.root, contentClassName)} className={buildClassName(styles.root, className)}
contentClassName={styles.content} contentClassName={buildClassName(styles.content, contentClassName)}
hasAbsoluteCloseButton hasAbsoluteCloseButton
absoluteCloseButtonColor={hasBackdrop ? 'translucent-white' : undefined} absoluteCloseButtonColor={hasBackdrop ? 'translucent-white' : undefined}
onClose={onClose} onClose={onClose}
> >
{headerIconName && <div className={styles.topIcon}><Icon name={headerIconName} /></div>} {headerIconName && (
<div className={buildClassName(styles.topIcon, headerIconPremiumGradient && styles.premiumGradient)}>
<Icon name={headerIconName} />
</div>
)}
{header} {header}
<div> <div>
{listItemData?.map(([icon, title, subtitle]) => { {listItemData?.map(([icon, title, subtitle]) => {
@ -69,7 +77,7 @@ const TableAboutModal = ({
</div> </div>
{withSeparator && <Separator className={styles.separator} />} {withSeparator && <Separator className={styles.separator} />}
{footer} {footer}
{buttonText && ( {Boolean(buttonText) && (
<Button onClick={onButtonClick || onClose}>{buttonText}</Button> <Button onClick={onButtonClick || onClose}>{buttonText}</Button>
)} )}
</Modal> </Modal>

View File

@ -91,27 +91,31 @@ const FrozenAccountModal = ({
); );
}, [lang, isOpen]); }, [lang, isOpen]);
if (!freezeUntilDate || !botFreezeAppealUsername) return undefined; const listItemData = useMemo(() => {
if (!freezeUntilDate || !botFreezeAppealUsername) return undefined;
const date = new Date(freezeUntilDate * 1000); const date = new Date(freezeUntilDate * 1000);
const botLink = ( const botLink = (
<Link onClick={handleAppeal} isPrimary> <Link onClick={handleAppeal} isPrimary>
{formatUsername(botFreezeAppealUsername)} {formatUsername(botFreezeAppealUsername)}
</Link> </Link>
); );
const listItemData = [ return [
['hand-stop', lang('FrozenAccountViolationTitle'), lang('FrozenAccountViolationSubtitle')], ['hand-stop', lang('FrozenAccountViolationTitle'), lang('FrozenAccountViolationSubtitle')],
['lock', lang('FrozenAccountReadOnlyTitle'), lang('FrozenAccountReadOnlySubtitle')], ['lock', lang('FrozenAccountReadOnlyTitle'), lang('FrozenAccountReadOnlySubtitle')],
['frozen-time', lang('FrozenAccountAppealTitle'), ['frozen-time', lang('FrozenAccountAppealTitle'),
lang('FrozenAccountAppealSubtitle', { lang('FrozenAccountAppealSubtitle', {
botLink, botLink,
date: formatDateToString(date, lang.code), date: formatDateToString(date, lang.code),
}, { }, {
withNodes: true, withNodes: true,
})], })],
] satisfies TableAboutData; ] satisfies TableAboutData;
}, [lang, botFreezeAppealUsername, freezeUntilDate]);
if (!listItemData) return undefined;
return ( return (
<TableAboutModal <TableAboutModal

View File

@ -6,10 +6,6 @@
width: 100%; width: 100%;
} }
.titleContainer {
padding-bottom: 0.5rem;
}
.profileBlock { .profileBlock {
position: relative; position: relative;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
@ -47,7 +43,6 @@
} }
.status { .status {
padding-bottom: 1rem;
font-size: 0.875rem; font-size: 0.875rem;
text-align: center; text-align: center;
} }
@ -58,7 +53,6 @@
} }
.giftTitle { .giftTitle {
padding-bottom: 0.5rem;
font-size: 1.5rem; font-size: 1.5rem;
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
text-align: center; text-align: center;
@ -71,5 +65,4 @@
.footer { .footer {
display: flex; display: flex;
align-self: stretch; align-self: stretch;
margin-top: 0.5rem;
} }

View File

@ -107,19 +107,13 @@ const GiftStatusInfoModal = ({
{lang('Online')} {lang('Online')}
</p> </p>
</div> </div>
<div className={styles.titleContainer}> <div className={styles.giftTitle}>
<div className={styles.giftTitle}> {lang('UniqueStatusWearTitle', {
{ gift: mockPeerWithStatus?.emojiStatus?.title,
lang('UniqueStatusWearTitle', { })}
gift: mockPeerWithStatus?.emojiStatus?.title, </div>
}) <div className={styles.infoDescription}>
} {lang('UniqueStatusBenefitsDescription')}
</div>
<div className={styles.infoDescription}>
{
lang('UniqueStatusBenefitsDescription')
}
</div>
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,14 @@
import type { OwnProps } from './StealthModeModal';
import { Bundles } from '../../../util/moduleLoader';
import useModuleLoader from '../../../hooks/useModuleLoader';
const StealthModeModalAsync = (props: OwnProps) => {
const { modal } = props;
const StealthModeModal = useModuleLoader(Bundles.Extra, 'StealthModeModal', !modal);
return StealthModeModal ? <StealthModeModal {...props} /> : undefined;
};
export default StealthModeModalAsync;

View File

@ -0,0 +1,12 @@
.title {
margin-block: 0;
font-weight: var(--font-weight-medium);
color: var(--color-text);
}
.description {
font-size: 0.875rem;
color: var(--color-text-secondary);
text-align: center;
text-wrap: balance;
}

View File

@ -0,0 +1,131 @@
import { memo, useMemo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiStealthMode } from '../../../api/types';
import type { TabState } from '../../../global/types';
import { selectIsCurrentUserPremium, selectIsStoryViewerOpen } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { getServerTime } from '../../../util/serverTime';
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
import useForceUpdate from '../../../hooks/useForceUpdate';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import TextTimer from '../../ui/TextTimer';
import TableAboutModal, { type TableAboutData } from '../common/TableAboutModal';
import styles from './StealthModeModal.module.scss';
export type OwnProps = {
modal: TabState['storyStealthModal'];
};
type StateProps = {
isStoryViewerOpen?: boolean;
stealthMode?: ApiStealthMode;
isCurrentUserPremium?: boolean;
};
const StealthModeModal = ({
modal, isStoryViewerOpen, stealthMode, isCurrentUserPremium,
}: OwnProps & StateProps) => {
const {
closeStealthModal,
activateStealthMode,
showNotification,
openPremiumModal,
openStoryViewer,
} = getActions();
const lang = useLang();
const forceUpdate = useForceUpdate();
const isOnCooldown = Boolean(stealthMode?.cooldownUntil && stealthMode.cooldownUntil > getServerTime());
const isOpen = Boolean(modal);
const renderingModal = useCurrentOrPrev(modal);
const { targetPeerId } = renderingModal || {};
const handleTimerEnds = useLastCallback(() => {
forceUpdate();
});
const handleClose = useLastCallback(() => {
closeStealthModal();
});
const handleActivate = useLastCallback(() => {
if (!isCurrentUserPremium) {
openPremiumModal({ initialSection: 'stories' });
handleClose();
return;
}
activateStealthMode();
showNotification({
title: {
key: 'StealthModeOnTitle',
},
message: {
key: 'StealthModeOnHintEnabled',
},
});
if (targetPeerId) {
openStoryViewer({ peerId: targetPeerId });
}
handleClose();
});
const header = useMemo(() => {
return (
<>
<h3 className={styles.title}>{lang('StealthModeTitle')}</h3>
<div className={styles.description}>
{lang(isCurrentUserPremium ? 'StealthModeDescription' : 'StealthModeDescriptionPremium')}
</div>
</>
);
}, [lang, isCurrentUserPremium]);
const listItemData = useMemo(() => {
return [
['stealth-past', lang('StealthModeHideRecentTitle'), lang('StealthModeHideRecentDescription')],
['stealth-future', lang('StealthModeHideFutureTitle'), lang('StealthModeHideFutureDescription')],
] satisfies TableAboutData;
}, [lang]);
const buttonText = useMemo(() => {
if (!isCurrentUserPremium) return lang('StealthModeButtonPremium');
if (isOnCooldown) {
return lang('StealthModeButtonRecharge', {
timer: <TextTimer endsAt={stealthMode!.cooldownUntil!} onEnd={handleTimerEnds} />,
}, { withNodes: true });
}
if (targetPeerId) return lang('StealthModeButtonToStory');
return lang('StealthModeButton');
}, [isCurrentUserPremium, isOnCooldown, lang, stealthMode, targetPeerId]);
return (
<TableAboutModal
isOpen={isOpen}
className={buildClassName(isStoryViewerOpen && 'component-theme-dark')}
header={header}
headerIconName="eye-crossed-outline"
listItemData={listItemData}
buttonText={buttonText}
onButtonClick={handleActivate}
onClose={handleClose}
/>
);
};
export default memo(withGlobal((global): Complete<StateProps> => {
return {
isStoryViewerOpen: selectIsStoryViewerOpen(global),
stealthMode: global.stories.stealthMode,
isCurrentUserPremium: selectIsCurrentUserPremium(global),
};
})(StealthModeModal));

View File

@ -1,55 +0,0 @@
.root {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
.closeButton {
position: absolute;
top: 0.5rem;
left: 0.5rem;
}
.stealthIcon {
display: grid;
place-items: center;
width: 5rem;
height: 5rem;
border-radius: 50%;
font-size: 3rem;
background-color: var(--color-primary);
}
.title {
margin-top: 0.75rem;
font-weight: var(--font-weight-medium);
color: var(--color-text);
}
.description {
margin-top: 0.5rem;
font-size: 0.875rem;
color: var(--color-text-secondary);
text-align: center;
}
.listItem {
align-self: stretch;
}
.icon {
margin-right: 1rem !important;
color: var(--color-primary) !important;
}
.button {
margin-top: 1rem;
}
.subtitle {
line-height: 1.25rem !important;
}

View File

@ -1,135 +0,0 @@
import { memo, useEffect, useState } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import type { ApiStealthMode } from '../../api/types';
import { selectIsCurrentUserPremium, selectTabState } from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import { getServerTime } from '../../util/serverTime';
import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang';
import Icon from '../common/icons/Icon';
import Button from '../ui/Button';
import ListItem from '../ui/ListItem';
import Modal from '../ui/Modal';
import TextTimer from '../ui/TextTimer';
import styles from './StealthModeModal.module.scss';
type StateProps = {
isOpen?: boolean;
stealthMode?: ApiStealthMode;
isCurrentUserPremium?: boolean;
};
const StealthModeModal = ({ isOpen, stealthMode, isCurrentUserPremium }: StateProps) => {
const {
toggleStealthModal,
activateStealthMode,
showNotification,
openPremiumModal,
} = getActions();
const [isOnCooldown, setIsOnCooldown] = useState(false);
useEffect(() => {
if (!stealthMode) return;
const serverTime = getServerTime();
if (stealthMode.cooldownUntil && stealthMode.cooldownUntil > serverTime) {
setIsOnCooldown(true);
}
}, [stealthMode, isOpen]);
const lang = useOldLang();
const handleTimerEnds = useLastCallback(() => {
setIsOnCooldown(false);
});
const handleClose = useLastCallback(() => {
toggleStealthModal({ isOpen: false });
});
const handleActivate = useLastCallback(() => {
if (!isCurrentUserPremium) {
openPremiumModal({ initialSection: 'stories' });
return;
}
activateStealthMode();
showNotification({
title: lang('StealthModeOn'),
message: lang('StealthModeOnHint'),
});
toggleStealthModal({ isOpen: false });
});
return (
<Modal
className="component-theme-dark"
contentClassName={styles.root}
isOpen={isOpen}
isSlim
onClose={handleClose}
>
<Button
round
color="translucent"
size="smaller"
className={styles.closeButton}
ariaLabel={lang('Close')}
onClick={handleClose}
>
<Icon name="close" />
</Button>
<div className={styles.stealthIcon}>
<Icon name="eye-crossed-outline" />
</div>
<div className={styles.title}>{lang('StealthMode')}</div>
<div className={styles.description}>
{lang(isCurrentUserPremium ? 'StealthModeHint' : 'StealthModePremiumHint')}
</div>
<ListItem
className={buildClassName(styles.listItem, 'smaller-icon')}
multiline
inactive
leftElement={<Icon name="stealth-past" className={styles.icon} />}
>
<span className="title">{lang('HideRecentViews')}</span>
<span className={buildClassName('subtitle', styles.subtitle)}>{lang('HideRecentViewsDescription')}</span>
</ListItem>
<ListItem
className={buildClassName(styles.listItem, 'smaller-icon')}
multiline
inactive
leftElement={<Icon name="stealth-future" className={styles.icon} aria-hidden />}
>
<span className="title">{lang('HideNextViews')}</span>
<span className={buildClassName('subtitle', styles.subtitle)}>{lang('HideNextViewsDescription')}</span>
</ListItem>
<Button
className={styles.button}
disabled={isOnCooldown}
isShiny={!isCurrentUserPremium}
withPremiumGradient={!isCurrentUserPremium}
onClick={handleActivate}
>
{!isCurrentUserPremium ? lang('UnlockStealthMode')
: isOnCooldown
? (<TextTimer langKey="AvailableIn" endsAt={stealthMode!.cooldownUntil!} onEnd={handleTimerEnds} />)
: lang('EnableStealthMode')}
</Button>
</Modal>
);
};
export default memo(withGlobal((global): Complete<StateProps> => {
const tabState = selectTabState(global);
return {
isOpen: tabState.storyViewer?.isStealthModalOpen,
stealthMode: global.stories.stealthMode,
isCurrentUserPremium: selectIsCurrentUserPremium(global),
};
})(StealthModeModal));

View File

@ -137,11 +137,11 @@ function Story({
isCurrentUserPremium, isCurrentUserPremium,
stealthMode, stealthMode,
withHeaderAnimation, withHeaderAnimation,
paidMessagesStars,
isAccountFrozen,
onDelete, onDelete,
onClose, onClose,
onReport, onReport,
paidMessagesStars,
isAccountFrozen,
}: OwnProps & StateProps) { }: OwnProps & StateProps) {
const { const {
viewStory, viewStory,
@ -158,7 +158,7 @@ function Story({
loadPeerSettings, loadPeerSettings,
fetchChat, fetchChat,
loadStoryViews, loadStoryViews,
toggleStealthModal, openStealthModal,
} = getActions(); } = getActions();
const serverTime = getServerTime(); const serverTime = getServerTime();
@ -211,7 +211,7 @@ function Story({
isOut && (story.date + viewersExpirePeriod) < getServerTime(), isOut && (story.date + viewersExpirePeriod) < getServerTime(),
); );
const forwardSenderTitle = forwardSender ? getPeerTitle(oldLang, forwardSender) const forwardSenderTitle = forwardSender ? getPeerTitle(lang, forwardSender)
: (isLoadedStory && story.forwardInfo?.fromName); : (isLoadedStory && story.forwardInfo?.fromName);
const canCopyLink = Boolean( const canCopyLink = Boolean(
@ -505,7 +505,7 @@ function Story({
: story.isForContacts ? 'contacts' : (story.isForCloseFriends ? 'closeFriends' : 'nobody'); : story.isForContacts ? 'contacts' : (story.isForCloseFriends ? 'closeFriends' : 'nobody');
let message; let message;
const myName = getPeerTitle(oldLang, peer); const myName = getPeerTitle(lang, peer);
switch (visibility) { switch (visibility) {
case 'nobody': case 'nobody':
message = oldLang('StorySelectedContactsHint', myName); message = oldLang('StorySelectedContactsHint', myName);
@ -538,14 +538,14 @@ function Story({
if (stealthMode.activeUntil && getServerTime() < stealthMode.activeUntil) { if (stealthMode.activeUntil && getServerTime() < stealthMode.activeUntil) {
const diff = stealthMode.activeUntil - getServerTime(); const diff = stealthMode.activeUntil - getServerTime();
showNotification({ showNotification({
title: oldLang('StealthModeOn'), title: lang('StealthModeOnTitle'),
message: oldLang('Story.ToastStealthModeActiveText', formatMediaDuration(diff)), message: lang('StealthModeOnHint', { time: formatMediaDuration(diff) }),
duration: STEALTH_MODE_NOTIFICATION_DURATION, duration: STEALTH_MODE_NOTIFICATION_DURATION,
}); });
return; return;
} }
toggleStealthModal({ isOpen: true }); openStealthModal({});
}); });
const handleDownload = useLastCallback(() => { const handleDownload = useLastCallback(() => {
@ -656,7 +656,7 @@ function Story({
/> />
<div className={styles.senderMeta}> <div className={styles.senderMeta}>
<span onClick={handleOpenChat} className={styles.senderName}> <span onClick={handleOpenChat} className={styles.senderName}>
{renderText(getPeerTitle(oldLang, peer) || '')} {renderText(getPeerTitle(lang, peer) || '')}
</span> </span>
<div className={styles.storyMetaRow}> <div className={styles.storyMetaRow}>
{forwardSenderTitle && ( {forwardSenderTitle && (
@ -681,7 +681,7 @@ function Story({
> >
<Avatar peer={fromPeer} size="micro" /> <Avatar peer={fromPeer} size="micro" />
<span className={styles.headerTitle}> <span className={styles.headerTitle}>
{renderText(getPeerTitle(oldLang, fromPeer) || '')} {renderText(getPeerTitle(lang, fromPeer) || '')}
</span> </span>
</span> </span>
)} )}
@ -882,7 +882,7 @@ function Story({
withStory withStory
storyViewerMode="disabled" storyViewerMode="disabled"
/> />
<div className={styles.name}>{renderText(getPeerTitle(oldLang, peer) || '')}</div> <div className={styles.name}>{renderText(getPeerTitle(lang, peer) || '')}</div>
</div> </div>
</div> </div>
)} )}
@ -949,7 +949,6 @@ export default memo(withGlobal<OwnProps>((global, {
isMuted, isMuted,
viewModal, viewModal,
isPrivacyModalOpen, isPrivacyModalOpen,
isStealthModalOpen,
storyList, storyList,
}, },
forwardMessages: { storyId: forwardedStoryId }, forwardMessages: { storyId: forwardedStoryId },
@ -959,8 +958,10 @@ export default memo(withGlobal<OwnProps>((global, {
reportModal, reportModal,
giftInfoModal, giftInfoModal,
isPaymentMessageConfirmDialogOpen, isPaymentMessageConfirmDialogOpen,
storyStealthModal,
} = tabState; } = tabState;
const { isOpen: isPremiumModalOpen } = premiumModal || {}; const { isOpen: isPremiumModalOpen } = premiumModal || {};
const isStealthModalOpen = Boolean(storyStealthModal);
const story = selectPeerStory(global, peerId, storyId); const story = selectPeerStory(global, peerId, storyId);
const isLoadedStory = story && 'content' in story; const isLoadedStory = story && 'content' in story;
const shouldForcePause = Boolean( const shouldForcePause = Boolean(

View File

@ -21,6 +21,7 @@ interface OwnProps {
interface StateProps { interface StateProps {
orderedPeerIds: string[]; orderedPeerIds: string[];
stealthModeActiveUntil?: number;
usersById: Record<string, ApiUser>; usersById: Record<string, ApiUser>;
chatsById: Record<string, ApiChat>; chatsById: Record<string, ApiChat>;
} }
@ -29,6 +30,7 @@ function StoryRibbon({
isArchived, isArchived,
className, className,
orderedPeerIds, orderedPeerIds,
stealthModeActiveUntil,
usersById, usersById,
chatsById, chatsById,
isClosing, isClosing,
@ -65,6 +67,7 @@ function StoryRibbon({
key={peerId} key={peerId}
peer={peer} peer={peer}
isArchived={isArchived} isArchived={isArchived}
stealthModeActiveUntil={stealthModeActiveUntil}
/> />
); );
})} })}
@ -78,8 +81,11 @@ export default memo(withGlobal<OwnProps>(
const usersById = global.users.byId; const usersById = global.users.byId;
const chatsById = global.chats.byId; const chatsById = global.chats.byId;
const stealthMode = global.stories.stealthMode;
return { return {
orderedPeerIds: isArchived ? archived : active, orderedPeerIds: isArchived ? archived : active,
stealthModeActiveUntil: stealthMode.activeUntil,
usersById, usersById,
chatsById, chatsById,
}; };

View File

@ -1,4 +1,3 @@
import type React from '../../lib/teact/teact';
import { memo, useRef } from '../../lib/teact/teact'; import { memo, useRef } from '../../lib/teact/teact';
import { getActions } from '../../global'; import { getActions } from '../../global';
@ -7,12 +6,14 @@ import { StoryViewerOrigin } from '../../types';
import { getPeerTitle } from '../../global/helpers/peers'; import { getPeerTitle } from '../../global/helpers/peers';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { formatMediaDuration } from '../../util/dates/dateFormat';
import { isUserId } from '../../util/entities/ids'; import { isUserId } from '../../util/entities/ids';
import { getServerTime } from '../../util/serverTime';
import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur'; import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur';
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers'; import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang';
import useStoryPreloader from './hooks/useStoryPreloader'; import useStoryPreloader from './hooks/useStoryPreloader';
import Avatar from '../common/Avatar'; import Avatar from '../common/Avatar';
@ -24,17 +25,22 @@ import styles from './StoryRibbon.module.scss';
interface OwnProps { interface OwnProps {
peer: ApiPeer; peer: ApiPeer;
isArchived?: boolean; isArchived?: boolean;
stealthModeActiveUntil?: number;
} }
function StoryRibbonButton({ peer, isArchived }: OwnProps) { const STEALTH_MODE_NOTIFICATION_DURATION = 1000;
function StoryRibbonButton({ peer, isArchived, stealthModeActiveUntil }: OwnProps) {
const { const {
openChat, openChat,
openChatWithInfo, openChatWithInfo,
openStoryViewer, openStoryViewer,
toggleStoriesHidden, toggleStoriesHidden,
openStealthModal,
showNotification,
} = getActions(); } = getActions();
const lang = useOldLang(); const lang = useLang();
const ref = useRef<HTMLDivElement>(); const ref = useRef<HTMLDivElement>();
const isSelf = 'isSelf' in peer && peer.isSelf; const isSelf = 'isSelf' in peer && peer.isSelf;
@ -84,6 +90,21 @@ function StoryRibbonButton({ peer, isArchived }: OwnProps) {
toggleStoriesHidden({ peerId: peer.id, isHidden: !isArchived }); toggleStoriesHidden({ peerId: peer.id, isHidden: !isArchived });
}); });
const handleOpenStealth = useLastCallback(() => {
const diff = stealthModeActiveUntil ? stealthModeActiveUntil - getServerTime() : 0;
if (diff > 0) {
showNotification({
title: lang('StealthModeOnTitle'),
message: lang('StealthModeOnHint', { time: formatMediaDuration(diff) }),
duration: STEALTH_MODE_NOTIFICATION_DURATION,
});
openStoryViewer({ peerId: peer.id, origin: StoryViewerOrigin.StoryRibbon });
return;
}
openStealthModal({ targetPeerId: peer.id });
});
return ( return (
<div <div
ref={ref} ref={ref}
@ -102,7 +123,7 @@ function StoryRibbonButton({ peer, isArchived }: OwnProps) {
storyViewerMode="full" storyViewerMode="full"
/> />
<div className={buildClassName(styles.name, peer.hasUnreadStories && styles.name_hasUnreadStory)}> <div className={buildClassName(styles.name, peer.hasUnreadStories && styles.name_hasUnreadStory)}>
{isSelf ? lang('MyStory') : getPeerTitle(lang, peer)} {isSelf ? lang('StoryRibbonMyStory') : getPeerTitle(lang, peer)}
</div> </div>
{contextMenuAnchor !== undefined && ( {contextMenuAnchor !== undefined && (
<Menu <Menu
@ -121,33 +142,38 @@ function StoryRibbonButton({ peer, isArchived }: OwnProps) {
{isSelf ? ( {isSelf ? (
<> <>
<MenuItem onClick={handleSavedStories} icon="play-story"> <MenuItem onClick={handleSavedStories} icon="play-story">
{lang('StoryList.Context.SavedStories')} {lang('StoryMenuSavedStories')}
</MenuItem> </MenuItem>
<MenuItem onClick={handleArchivedStories} icon="archive"> <MenuItem onClick={handleArchivedStories} icon="archive">
{lang('StoryList.Context.ArchivedStories')} {lang('StoryMenuArchivedStories')}
</MenuItem> </MenuItem>
</> </>
) : ( ) : (
<> <>
{!isChannel && ( {!isChannel && (
<MenuItem onClick={handleOpenChat} icon="message"> <MenuItem onClick={handleOpenChat} icon="message">
{lang('SendMessageTitle')} {lang('StoryMenuSendMessage')}
</MenuItem> </MenuItem>
)} )}
{isChannel ? ( {isChannel ? (
<MenuItem onClick={handleOpenProfile} icon="channel"> <MenuItem onClick={handleOpenProfile} icon="channel">
{lang('ChatList.ContextOpenChannel')} {lang('StoryMenuViewChannel')}
</MenuItem> </MenuItem>
) : ( ) : (
<MenuItem onClick={handleOpenProfile} icon="user"> <MenuItem onClick={handleOpenProfile} icon="user">
{lang('StoryList.Context.ViewProfile')} {lang('StoryMenuViewProfile')}
</MenuItem>
)}
{!isChannel && (
<MenuItem onClick={handleOpenStealth} icon="eye-crossed-outline">
{lang('StoryMenuOpenStealth')}
</MenuItem> </MenuItem>
)} )}
<MenuItem <MenuItem
onClick={handleArchivePeer} onClick={handleArchivePeer}
icon={isArchived ? 'unarchive' : 'archive'} icon={isArchived ? 'unarchive' : 'archive'}
> >
{lang(isArchived ? 'StoryList.Context.Unarchive' : 'StoryList.Context.Archive')} {lang(isArchived ? 'StoryMenuUnarchivePeer' : 'StoryMenuArchivePeer')}
</MenuItem> </MenuItem>
</> </>
)} )}

View File

@ -29,7 +29,6 @@ import useStoryProps from './hooks/useStoryProps';
import Icon from '../common/icons/Icon'; import Icon from '../common/icons/Icon';
import Button from '../ui/Button'; import Button from '../ui/Button';
import ShowTransition from '../ui/ShowTransition'; import ShowTransition from '../ui/ShowTransition';
import StealthModeModal from './StealthModeModal';
import StoryDeleteConfirmModal from './StoryDeleteConfirmModal'; import StoryDeleteConfirmModal from './StoryDeleteConfirmModal';
import StorySettings from './StorySettings'; import StorySettings from './StorySettings';
import StorySlides from './StorySlides'; import StorySlides from './StorySlides';
@ -175,7 +174,6 @@ function StoryViewer({
onClose={handleCloseDeleteModal} onClose={handleCloseDeleteModal}
/> />
<StoryViewModal /> <StoryViewModal />
<StealthModeModal />
<StorySettings isOpen={isPrivacyModalOpen} onClose={closeStoryPrivacyEditor} /> <StorySettings isOpen={isPrivacyModalOpen} onClose={closeStoryPrivacyEditor} />
</ShowTransition> </ShowTransition>
); );

View File

@ -25,7 +25,6 @@
} }
.Button { .Button {
--premium-gradient: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%); --premium-gradient: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%);
cursor: var(--custom-cursor, pointer); cursor: var(--custom-cursor, pointer);
@ -56,6 +55,8 @@
transition: background-color 0.15s, color 0.15s, opacity 0.15s; transition: background-color 0.15s, color 0.15s, opacity 0.15s;
white-space-collapse: preserve;
// @optimization // @optimization
&:active, &:active,
&.clicked, &.clicked,
@ -67,6 +68,10 @@
text-transform: none; text-transform: none;
} }
&.inline {
display: inline;
}
&.disabled { &.disabled {
cursor: var(--custom-cursor, default); cursor: var(--custom-cursor, default);

View File

@ -35,6 +35,7 @@ export type OwnProps = {
pill?: boolean; pill?: boolean;
badge?: boolean; badge?: boolean;
fluid?: boolean; fluid?: boolean;
inline?: boolean;
isText?: boolean; isText?: boolean;
isLoading?: boolean; isLoading?: boolean;
ariaLabel?: string; ariaLabel?: string;
@ -88,6 +89,7 @@ const Button: FC<OwnProps> = ({
pill, pill,
badge, badge,
fluid, fluid,
inline,
isText, isText,
isLoading, isLoading,
isShiny, isShiny,
@ -157,6 +159,7 @@ const Button: FC<OwnProps> = ({
withPremiumGradient && 'premium', withPremiumGradient && 'premium',
isRectangular && 'rectangular', isRectangular && 'rectangular',
noForcedUpperCase && 'no-upper-case', noForcedUpperCase && 'no-upper-case',
inline && 'inline',
iconAlignment && iconName && `content-with-icon-${iconAlignment}`, iconAlignment && iconName && `content-with-icon-${iconAlignment}`,
); );

View File

@ -1,26 +1,21 @@
import { type FC, memo, useEffect } from '../../lib/teact/teact'; import { useEffect } from '../../lib/teact/teact';
import { formatMediaDuration } from '../../util/dates/dateFormat'; import { formatMediaDuration } from '../../util/dates/dateFormat';
import { getServerTime } from '../../util/serverTime'; import { getServerTime } from '../../util/serverTime';
import useInterval from '../../hooks/schedulers/useInterval'; import useInterval from '../../hooks/schedulers/useInterval';
import useForceUpdate from '../../hooks/useForceUpdate'; import useForceUpdate from '../../hooks/useForceUpdate';
import useLang from '../../hooks/useLang';
import useOldLang from '../../hooks/useOldLang';
import AnimatedCounter from '../common/AnimatedCounter'; import AnimatedCounter from '../common/AnimatedCounter';
type OwnProps = { type OwnProps = {
langKey: string;
endsAt: number; endsAt: number;
onEnd?: NoneToVoidFunction; onEnd?: NoneToVoidFunction;
}; };
const UPDATE_FREQUENCY = 500; // Sometimes second gets skipped if using 1000 const UPDATE_FREQUENCY = 500; // Sometimes second gets skipped if using 1000
const TextTimer: FC<OwnProps> = ({ langKey, endsAt, onEnd }) => { const TextTimer = ({ endsAt, onEnd }: OwnProps) => {
const lang = useLang();
const oldLang = useOldLang();
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();
const serverTime = getServerTime(); const serverTime = getServerTime();
@ -50,21 +45,11 @@ const TextTimer: FC<OwnProps> = ({ langKey, endsAt, onEnd }) => {
</span> </span>
); );
const isTypedKey = langKey === 'UnlockTimerPublicPostsSearch';
if (isTypedKey) {
return (
<span>
{lang(langKey, { time: timeCounter }, { withNodes: true })}
</span>
);
}
return ( return (
<span> <span>
{oldLang(langKey, time)} {timeCounter}
</span> </span>
); );
}; };
export default memo(TextTimer); export default TextTimer;

View File

@ -5,6 +5,7 @@ import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { omit } from '../../../util/iteratees'; import { omit } from '../../../util/iteratees';
import * as langProvider from '../../../util/oldLangProvider'; import * as langProvider from '../../../util/oldLangProvider';
import { callApi } from '../../../api/gramjs'; import { callApi } from '../../../api/gramjs';
import { addTabStateResetterAction } from '../../helpers/meta';
import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { addActionHandler, getGlobal, setGlobal } from '../../index';
import { addStoriesForPeer } from '../../reducers'; import { addStoriesForPeer } from '../../reducers';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
@ -352,18 +353,18 @@ addActionHandler('closeStoryPrivacyEditor', (global, actions, payload): ActionRe
}, tabId); }, tabId);
}); });
addActionHandler('toggleStealthModal', (global, actions, payload): ActionReturnType => { addActionHandler('openStealthModal', (global, actions, payload): ActionReturnType => {
const { isOpen, tabId = getCurrentTabId() } = payload || {}; const { targetPeerId, tabId = getCurrentTabId() } = payload;
const tabState = selectTabState(global, tabId);
return updateTabState(global, { return updateTabState(global, {
storyViewer: { storyStealthModal: {
...tabState.storyViewer, targetPeerId,
isStealthModalOpen: isOpen,
}, },
}, tabId); }, tabId);
}); });
addTabStateResetterAction('closeStealthModal', 'storyStealthModal');
addActionHandler('clearStoryViews', (global, actions, payload): ActionReturnType => { addActionHandler('clearStoryViews', (global, actions, payload): ActionReturnType => {
const { isLoading, tabId = getCurrentTabId() } = payload || {}; const { isLoading, tabId = getCurrentTabId() } = payload || {};

View File

@ -1700,9 +1700,10 @@ export interface ActionPayloads {
reaction?: ApiReaction; reaction?: ApiReaction;
shouldAddToRecent?: boolean; shouldAddToRecent?: boolean;
} & WithTabId; } & WithTabId;
toggleStealthModal: { openStealthModal: ({
isOpen: boolean; targetPeerId: string;
} & WithTabId; } | Record<string, never>) & WithTabId;
closeStealthModal: WithTabId | undefined;
activateStealthMode: { activateStealthMode: {
isForPast?: boolean; isForPast?: boolean;
isForFuture?: boolean; isForFuture?: boolean;

View File

@ -335,7 +335,6 @@ export type TabState = {
lastViewedByPeerId?: Record<string, number>; lastViewedByPeerId?: Record<string, number>;
isPrivacyModalOpen?: boolean; isPrivacyModalOpen?: boolean;
isPaymentConfirmDialogOpen?: boolean; isPaymentConfirmDialogOpen?: boolean;
isStealthModalOpen?: boolean;
viewModal?: { viewModal?: {
storyId: number; storyId: number;
views?: ApiTypeStoryView[]; views?: ApiTypeStoryView[];
@ -349,6 +348,9 @@ export type TabState = {
storyIdsByPeerId: Record<string, number[]>; storyIdsByPeerId: Record<string, number[]>;
}; };
}; };
storyStealthModal?: {
targetPeerId: string;
} | Record<string, never>;
selectedStoryAlbumId?: number; selectedStoryAlbumId?: number;

View File

@ -1745,6 +1745,27 @@ export interface LangPair {
'InviteRestrictedPremiumReason': undefined; 'InviteRestrictedPremiumReason': undefined;
'InviteViaLinkButton': undefined; 'InviteViaLinkButton': undefined;
'StoryUnsupported': undefined; 'StoryUnsupported': undefined;
'StoryRibbonMyStory': undefined;
'StoryMenuSavedStories': undefined;
'StoryMenuArchivedStories': undefined;
'StoryMenuSendMessage': undefined;
'StoryMenuViewProfile': undefined;
'StoryMenuViewChannel': undefined;
'StoryMenuOpenStealth': undefined;
'StoryMenuArchivePeer': undefined;
'StoryMenuUnarchivePeer': undefined;
'StealthModeOnTitle': undefined;
'StealthModeOnHintEnabled': undefined;
'StealthModeTitle': undefined;
'StealthModeDescription': undefined;
'StealthModeDescriptionPremium': undefined;
'StealthModeHideRecentTitle': undefined;
'StealthModeHideRecentDescription': undefined;
'StealthModeHideFutureTitle': undefined;
'StealthModeHideFutureDescription': undefined;
'StealthModeButtonPremium': undefined;
'StealthModeButton': undefined;
'StealthModeButtonToStory': undefined;
} }
export interface LangPairWithVariables<V = LangVariable> { export interface LangPairWithVariables<V = LangVariable> {
@ -1900,6 +1921,9 @@ export interface LangPairWithVariables<V = LangVariable> {
'ReportChat': { 'ReportChat': {
'peer': V; 'peer': V;
}; };
'SlowModePlaceholder': {
'timer': V;
};
'SlowModeHint': { 'SlowModeHint': {
'time': V; 'time': V;
}; };
@ -2998,6 +3022,15 @@ export interface LangPairWithVariables<V = LangVariable> {
'InviteRestrictedPremiumReasonMultiple': { 'InviteRestrictedPremiumReasonMultiple': {
'list': V; 'list': V;
}; };
'StealthModeOnHint': {
'time': V;
};
'StealthModeButtonRecharge': {
'timer': V;
};
'StealthModeComposerPlaceholder': {
'timer': V;
};
} }
export interface LangPairPlural { export interface LangPairPlural {

View File

@ -369,9 +369,12 @@ function processTranslation(
variables?: Record<string, LangVariable | RegularLangFnParameters>, variables?: Record<string, LangVariable | RegularLangFnParameters>,
options?: LangFnOptions | LangFnOptionsWithPlural, options?: LangFnOptions | LangFnOptionsWithPlural,
): string { ): string {
const cacheKey = `${langKey}-${JSON.stringify(variables)}-${JSON.stringify(options)}`; const isCacheable = !options?.withNodes;
if (TRANSLATION_CACHE.has(cacheKey)) { const cacheKey = isCacheable ? `${langKey}-${JSON.stringify(variables)}-${JSON.stringify(options)}` : undefined;
return TRANSLATION_CACHE.get(cacheKey)!; if (cacheKey) {
if (TRANSLATION_CACHE.has(cacheKey)) {
return TRANSLATION_CACHE.get(cacheKey)!;
}
} }
const pluralValue = options && 'pluralValue' in options ? Number(options.pluralValue) : 0; const pluralValue = options && 'pluralValue' in options ? Number(options.pluralValue) : 0;
@ -390,7 +393,9 @@ function processTranslation(
return result.replaceAll(`{${key}}`, valueAsString); return result.replaceAll(`{${key}}`, valueAsString);
}, string); }, string);
TRANSLATION_CACHE.set(cacheKey, finalString); if (cacheKey) {
TRANSLATION_CACHE.set(cacheKey, finalString);
}
return finalString; return finalString;
} }