diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 58fae90f4..8a336fe4c 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -642,6 +642,7 @@ "AttachmentMenuPhotoOrVideo" = "Photo or Video"; "AttachDocument" = "File"; "Poll" = "Poll"; +"SlowModePlaceholder" = "Next message in {timer}"; "SlowModeHint" = "Slow Mode is active. You can send\nyour next message in {time}."; "SendMessageAsTitle" = "Send message as..."; "Message" = "Message"; @@ -2342,3 +2343,27 @@ "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."; "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}"; diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index 8f78d6594..44b7d661c 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -105,3 +105,4 @@ export { default as WebAppsCloseConfirmationModal } from '../components/main/Web export { default as FrozenAccountModal } from '../components/modals/frozenAccount/FrozenAccountModal'; export { default as ProfileRatingModal } from '../components/modals/profileRating/ProfileRatingModal'; export { default as QuickPreviewModal } from '../components/modals/quickPreview/QuickPreviewModal'; +export { default as StealthModeModal } from '../components/modals/storyStealthMode/StealthModeModal'; diff --git a/src/components/common/AboutMonetizationModal.tsx b/src/components/common/AboutMonetizationModal.tsx index 328e7b512..bb70d34d7 100644 --- a/src/components/common/AboutMonetizationModal.tsx +++ b/src/components/common/AboutMonetizationModal.tsx @@ -94,6 +94,7 @@ const AboutMonetizationModal: FC = ({ isOpen={isOpen} listItemData={modalData.listItemData} headerIconName="cash-circle" + headerIconPremiumGradient withSeparator header={modalData.header} footer={modalData.footer} diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx index 36e0ac2cb..ac4d07b44 100644 --- a/src/components/common/Composer.tsx +++ b/src/components/common/Composer.tsx @@ -136,6 +136,7 @@ import useContextMenuHandlers from '../../hooks/useContextMenuHandlers'; import useDerivedState from '../../hooks/useDerivedState'; import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps'; import useFlag from '../../hooks/useFlag'; +import useForceUpdate from '../../hooks/useForceUpdate'; import useGetSelectionRange from '../../hooks/useGetSelectionRange'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; @@ -187,6 +188,7 @@ import ReactionSelector from '../middle/message/reactions/ReactionSelector'; import Button from '../ui/Button'; import ResponsiveHoverButton from '../ui/ResponsiveHoverButton'; import Spinner from '../ui/Spinner'; +import TextTimer from '../ui/TextTimer'; import Transition from '../ui/Transition'; import AnimatedCounter from './AnimatedCounter'; import Avatar from './Avatar'; @@ -484,6 +486,7 @@ const Composer: FC = ({ const lastMessageSendTimeSeconds = useRef(); const prevDropAreaState = usePreviousDeprecated(dropAreaState); const { width: windowWidth } = windowSize.get(); + const forceUpdate = useForceUpdate(); const isInMessageList = type === 'messageList'; const isInStoryViewer = type === 'story'; @@ -1662,18 +1665,6 @@ const Composer: FC = ({ && messageListType === 'thread'; 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 || isMentionTooltipOpen || isInlineBotTooltipOpen || isBotCommandMenuOpen || isAttachMenuOpen || isStickerTooltipOpen || isChatCommandTooltipOpen || isCustomEmojiTooltipOpen || isBotMenuButtonOpen @@ -1681,12 +1672,21 @@ const Composer: FC = ({ const isReactionSelectorOpen = isComposerHasFocus && !isReactionPickerOpen && isInStoryViewer && !isAttachMenuOpen && !isSymbolMenuOpen; + const slowModePlaceholder = (() => { + if (!slowMode?.nextSendDate || slowMode.nextSendDate < getServerTime()) return undefined; + + return lang('SlowModePlaceholder', { + timer: , + }, { withNodes: true }); + })(); + const placeholder = useMemo(() => { if (activeVoiceRecording && windowWidth <= SCREEN_WIDTH_TO_HIDE_PLACEHOLDER) { return ''; } if (!isComposerBlocked) { + if (slowModePlaceholder) return slowModePlaceholder; if (botKeyboardPlaceholder) return botKeyboardPlaceholder; if (inputPlaceholder) return inputPlaceholder; if (paidMessagesStars) { @@ -1701,6 +1701,12 @@ const Composer: FC = ({ return lang('ComposerPlaceholderCaption'); } + if (stealthMode?.activeUntil && isInStoryViewer && stealthMode.activeUntil > getServerTime()) { + return lang('StealthModeComposerPlaceholder', { + timer: , + }, { withNodes: true }); + } + if (chat?.adminRights?.anonymous) { return lang('ComposerPlaceholderAnonymous'); } @@ -1722,7 +1728,7 @@ const Composer: FC = ({ }, [ activeVoiceRecording, botKeyboardPlaceholder, chat, inputPlaceholder, isChannel, isComposerBlocked, isInStoryViewer, isSilentPosting, lang, replyToTopic, isReplying, threadId, windowWidth, paidMessagesStars, - hasSuggestedPost, + hasSuggestedPost, slowModePlaceholder, stealthMode?.activeUntil, ]); useEffect(() => { @@ -2172,8 +2178,6 @@ const Composer: FC = ({ isActive={!hasAttachments} getHtml={getHtml} placeholder={placeholder} - timedPlaceholderDate={timedPlaceholderDate} - timedPlaceholderLangKey={timedPlaceholderLangKey} forcedPlaceholder={inlineBotHelp} canAutoFocus={isReady && isForCurrentMessageList && !hasAttachments && isInMessageList} noFocusInterception={hasAttachments} diff --git a/src/components/left/search/PublicPostsSearchLauncher.tsx b/src/components/left/search/PublicPostsSearchLauncher.tsx index fbc535e88..c637e94ea 100644 --- a/src/components/left/search/PublicPostsSearchLauncher.tsx +++ b/src/components/left/search/PublicPostsSearchLauncher.tsx @@ -138,11 +138,9 @@ const PublicPostsSearchLauncher = ({ {Boolean(waitTill) && (
- + {lang('UnlockTimerPublicPostsSearch', { + time: , + }, { withNodes: true })}
)} diff --git a/src/components/middle/composer/MessageInput.tsx b/src/components/middle/composer/MessageInput.tsx index 99f383300..1dc3cf3e9 100644 --- a/src/components/middle/composer/MessageInput.tsx +++ b/src/components/middle/composer/MessageInput.tsx @@ -41,7 +41,6 @@ import useInputCustomEmojis from './hooks/useInputCustomEmojis'; import Icon from '../../common/icons/Icon'; import Button from '../../ui/Button'; -import TextTimer from '../../ui/TextTimer'; import TextFormatter from './TextFormatter.async'; const CONTEXT_MENU_CLOSE_DELAY_MS = 100; @@ -63,23 +62,21 @@ type OwnProps = { isActive: boolean; getHtml: Signal; placeholder: TeactNode | string; - timedPlaceholderLangKey?: string; - timedPlaceholderDate?: number; forcedPlaceholder?: string; noFocusInterception?: boolean; canAutoFocus: boolean; shouldSuppressFocus?: boolean; shouldSuppressTextFormatter?: boolean; canSendPlainText?: boolean; + isNeedPremium?: boolean; + messageListType?: MessageListType; + captionLimit?: number; onUpdate: (html: string) => void; onSuppressedFocus?: () => void; onSend: () => void; onScroll?: (event: React.UIEvent) => void; - captionLimit?: number; onFocus?: NoneToVoidFunction; onBlur?: NoneToVoidFunction; - isNeedPremium?: boolean; - messageListType?: MessageListType; }; type StateProps = { @@ -127,8 +124,6 @@ const MessageInput: FC = ({ isActive, getHtml, placeholder, - timedPlaceholderLangKey, - timedPlaceholderDate, forcedPlaceholder, canSendPlainText, canAutoFocus, @@ -139,14 +134,14 @@ const MessageInput: FC = ({ isSelectModeActive, canPlayAnimatedEmojis, messageSendKeyCombo, + isNeedPremium, + messageListType, onUpdate, onSuppressedFocus, onSend, onScroll, onFocus, onBlur, - isNeedPremium, - messageListType, }) => { const { editLastMessage, @@ -177,16 +172,6 @@ const MessageInput: FC = ({ const { isMobile } = useAppLayout(); 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( getHtml, inputRef, @@ -611,9 +596,7 @@ const MessageInput: FC = ({ > {!isAttachmentModalInput && !canSendPlainText && } - {shouldDisplayTimer ? ( - - ) : placeholder} + {placeholder} {isStoryInput && isNeedPremium && ( )} diff --git a/src/components/modals/frozenAccount/FrozenAccountModal.tsx b/src/components/modals/frozenAccount/FrozenAccountModal.tsx index 6c50e6532..639b44ea6 100644 --- a/src/components/modals/frozenAccount/FrozenAccountModal.tsx +++ b/src/components/modals/frozenAccount/FrozenAccountModal.tsx @@ -91,27 +91,31 @@ const FrozenAccountModal = ({ ); }, [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 = ( - - {formatUsername(botFreezeAppealUsername)} - - ); + const botLink = ( + + {formatUsername(botFreezeAppealUsername)} + + ); - const listItemData = [ - ['hand-stop', lang('FrozenAccountViolationTitle'), lang('FrozenAccountViolationSubtitle')], - ['lock', lang('FrozenAccountReadOnlyTitle'), lang('FrozenAccountReadOnlySubtitle')], - ['frozen-time', lang('FrozenAccountAppealTitle'), - lang('FrozenAccountAppealSubtitle', { - botLink, - date: formatDateToString(date, lang.code), - }, { - withNodes: true, - })], - ] satisfies TableAboutData; + return [ + ['hand-stop', lang('FrozenAccountViolationTitle'), lang('FrozenAccountViolationSubtitle')], + ['lock', lang('FrozenAccountReadOnlyTitle'), lang('FrozenAccountReadOnlySubtitle')], + ['frozen-time', lang('FrozenAccountAppealTitle'), + lang('FrozenAccountAppealSubtitle', { + botLink, + date: formatDateToString(date, lang.code), + }, { + withNodes: true, + })], + ] satisfies TableAboutData; + }, [lang, botFreezeAppealUsername, freezeUntilDate]); + + if (!listItemData) return undefined; return ( -
-
- { - lang('UniqueStatusWearTitle', { - gift: mockPeerWithStatus?.emojiStatus?.title, - }) - } -
-
- { - lang('UniqueStatusBenefitsDescription') - } -
+
+ {lang('UniqueStatusWearTitle', { + gift: mockPeerWithStatus?.emojiStatus?.title, + })} +
+
+ {lang('UniqueStatusBenefitsDescription')}
); diff --git a/src/components/modals/storyStealthMode/StealthModeModal.async.tsx b/src/components/modals/storyStealthMode/StealthModeModal.async.tsx new file mode 100644 index 000000000..07d22d194 --- /dev/null +++ b/src/components/modals/storyStealthMode/StealthModeModal.async.tsx @@ -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 ? : undefined; +}; + +export default StealthModeModalAsync; diff --git a/src/components/modals/storyStealthMode/StealthModeModal.module.scss b/src/components/modals/storyStealthMode/StealthModeModal.module.scss new file mode 100644 index 000000000..f5a59d87a --- /dev/null +++ b/src/components/modals/storyStealthMode/StealthModeModal.module.scss @@ -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; +} diff --git a/src/components/modals/storyStealthMode/StealthModeModal.tsx b/src/components/modals/storyStealthMode/StealthModeModal.tsx new file mode 100644 index 000000000..9af45d0ed --- /dev/null +++ b/src/components/modals/storyStealthMode/StealthModeModal.tsx @@ -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 ( + <> +

{lang('StealthModeTitle')}

+
+ {lang(isCurrentUserPremium ? 'StealthModeDescription' : 'StealthModeDescriptionPremium')} +
+ + ); + }, [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: , + }, { withNodes: true }); + } + if (targetPeerId) return lang('StealthModeButtonToStory'); + return lang('StealthModeButton'); + }, [isCurrentUserPremium, isOnCooldown, lang, stealthMode, targetPeerId]); + + return ( + + ); +}; + +export default memo(withGlobal((global): Complete => { + return { + isStoryViewerOpen: selectIsStoryViewerOpen(global), + stealthMode: global.stories.stealthMode, + isCurrentUserPremium: selectIsCurrentUserPremium(global), + }; +})(StealthModeModal)); diff --git a/src/components/story/StealthModeModal.module.scss b/src/components/story/StealthModeModal.module.scss deleted file mode 100644 index bb7e31109..000000000 --- a/src/components/story/StealthModeModal.module.scss +++ /dev/null @@ -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; -} diff --git a/src/components/story/StealthModeModal.tsx b/src/components/story/StealthModeModal.tsx deleted file mode 100644 index 9fd3c4594..000000000 --- a/src/components/story/StealthModeModal.tsx +++ /dev/null @@ -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 ( - - -
- -
-
{lang('StealthMode')}
-
- {lang(isCurrentUserPremium ? 'StealthModeHint' : 'StealthModePremiumHint')} -
- } - > - {lang('HideRecentViews')} - {lang('HideRecentViewsDescription')} - - } - > - {lang('HideNextViews')} - {lang('HideNextViewsDescription')} - - -
- ); -}; - -export default memo(withGlobal((global): Complete => { - const tabState = selectTabState(global); - - return { - isOpen: tabState.storyViewer?.isStealthModalOpen, - stealthMode: global.stories.stealthMode, - isCurrentUserPremium: selectIsCurrentUserPremium(global), - }; -})(StealthModeModal)); diff --git a/src/components/story/Story.tsx b/src/components/story/Story.tsx index 7c9dc75cc..f10d23da4 100644 --- a/src/components/story/Story.tsx +++ b/src/components/story/Story.tsx @@ -137,11 +137,11 @@ function Story({ isCurrentUserPremium, stealthMode, withHeaderAnimation, + paidMessagesStars, + isAccountFrozen, onDelete, onClose, onReport, - paidMessagesStars, - isAccountFrozen, }: OwnProps & StateProps) { const { viewStory, @@ -158,7 +158,7 @@ function Story({ loadPeerSettings, fetchChat, loadStoryViews, - toggleStealthModal, + openStealthModal, } = getActions(); const serverTime = getServerTime(); @@ -211,7 +211,7 @@ function Story({ isOut && (story.date + viewersExpirePeriod) < getServerTime(), ); - const forwardSenderTitle = forwardSender ? getPeerTitle(oldLang, forwardSender) + const forwardSenderTitle = forwardSender ? getPeerTitle(lang, forwardSender) : (isLoadedStory && story.forwardInfo?.fromName); const canCopyLink = Boolean( @@ -505,7 +505,7 @@ function Story({ : story.isForContacts ? 'contacts' : (story.isForCloseFriends ? 'closeFriends' : 'nobody'); let message; - const myName = getPeerTitle(oldLang, peer); + const myName = getPeerTitle(lang, peer); switch (visibility) { case 'nobody': message = oldLang('StorySelectedContactsHint', myName); @@ -538,14 +538,14 @@ function Story({ if (stealthMode.activeUntil && getServerTime() < stealthMode.activeUntil) { const diff = stealthMode.activeUntil - getServerTime(); showNotification({ - title: oldLang('StealthModeOn'), - message: oldLang('Story.ToastStealthModeActiveText', formatMediaDuration(diff)), + title: lang('StealthModeOnTitle'), + message: lang('StealthModeOnHint', { time: formatMediaDuration(diff) }), duration: STEALTH_MODE_NOTIFICATION_DURATION, }); return; } - toggleStealthModal({ isOpen: true }); + openStealthModal({}); }); const handleDownload = useLastCallback(() => { @@ -656,7 +656,7 @@ function Story({ />
- {renderText(getPeerTitle(oldLang, peer) || '')} + {renderText(getPeerTitle(lang, peer) || '')}
{forwardSenderTitle && ( @@ -681,7 +681,7 @@ function Story({ > - {renderText(getPeerTitle(oldLang, fromPeer) || '')} + {renderText(getPeerTitle(lang, fromPeer) || '')} )} @@ -882,7 +882,7 @@ function Story({ withStory storyViewerMode="disabled" /> -
{renderText(getPeerTitle(oldLang, peer) || '')}
+
{renderText(getPeerTitle(lang, peer) || '')}
)} @@ -949,7 +949,6 @@ export default memo(withGlobal((global, { isMuted, viewModal, isPrivacyModalOpen, - isStealthModalOpen, storyList, }, forwardMessages: { storyId: forwardedStoryId }, @@ -959,8 +958,10 @@ export default memo(withGlobal((global, { reportModal, giftInfoModal, isPaymentMessageConfirmDialogOpen, + storyStealthModal, } = tabState; const { isOpen: isPremiumModalOpen } = premiumModal || {}; + const isStealthModalOpen = Boolean(storyStealthModal); const story = selectPeerStory(global, peerId, storyId); const isLoadedStory = story && 'content' in story; const shouldForcePause = Boolean( diff --git a/src/components/story/StoryRibbon.tsx b/src/components/story/StoryRibbon.tsx index 48748df0c..4a4893c2f 100644 --- a/src/components/story/StoryRibbon.tsx +++ b/src/components/story/StoryRibbon.tsx @@ -21,6 +21,7 @@ interface OwnProps { interface StateProps { orderedPeerIds: string[]; + stealthModeActiveUntil?: number; usersById: Record; chatsById: Record; } @@ -29,6 +30,7 @@ function StoryRibbon({ isArchived, className, orderedPeerIds, + stealthModeActiveUntil, usersById, chatsById, isClosing, @@ -65,6 +67,7 @@ function StoryRibbon({ key={peerId} peer={peer} isArchived={isArchived} + stealthModeActiveUntil={stealthModeActiveUntil} /> ); })} @@ -78,8 +81,11 @@ export default memo(withGlobal( const usersById = global.users.byId; const chatsById = global.chats.byId; + const stealthMode = global.stories.stealthMode; + return { orderedPeerIds: isArchived ? archived : active, + stealthModeActiveUntil: stealthMode.activeUntil, usersById, chatsById, }; diff --git a/src/components/story/StoryRibbonButton.tsx b/src/components/story/StoryRibbonButton.tsx index 8973df99d..6b04827d8 100644 --- a/src/components/story/StoryRibbonButton.tsx +++ b/src/components/story/StoryRibbonButton.tsx @@ -1,4 +1,3 @@ -import type React from '../../lib/teact/teact'; import { memo, useRef } from '../../lib/teact/teact'; import { getActions } from '../../global'; @@ -7,12 +6,14 @@ import { StoryViewerOrigin } from '../../types'; import { getPeerTitle } from '../../global/helpers/peers'; import buildClassName from '../../util/buildClassName'; +import { formatMediaDuration } from '../../util/dates/dateFormat'; import { isUserId } from '../../util/entities/ids'; +import { getServerTime } from '../../util/serverTime'; import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur'; import useContextMenuHandlers from '../../hooks/useContextMenuHandlers'; +import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; -import useOldLang from '../../hooks/useOldLang'; import useStoryPreloader from './hooks/useStoryPreloader'; import Avatar from '../common/Avatar'; @@ -24,17 +25,22 @@ import styles from './StoryRibbon.module.scss'; interface OwnProps { peer: ApiPeer; isArchived?: boolean; + stealthModeActiveUntil?: number; } -function StoryRibbonButton({ peer, isArchived }: OwnProps) { +const STEALTH_MODE_NOTIFICATION_DURATION = 1000; + +function StoryRibbonButton({ peer, isArchived, stealthModeActiveUntil }: OwnProps) { const { openChat, openChatWithInfo, openStoryViewer, toggleStoriesHidden, + openStealthModal, + showNotification, } = getActions(); - const lang = useOldLang(); + const lang = useLang(); const ref = useRef(); const isSelf = 'isSelf' in peer && peer.isSelf; @@ -84,6 +90,21 @@ function StoryRibbonButton({ peer, isArchived }: OwnProps) { 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 (
- {isSelf ? lang('MyStory') : getPeerTitle(lang, peer)} + {isSelf ? lang('StoryRibbonMyStory') : getPeerTitle(lang, peer)}
{contextMenuAnchor !== undefined && ( - {lang('StoryList.Context.SavedStories')} + {lang('StoryMenuSavedStories')} - {lang('StoryList.Context.ArchivedStories')} + {lang('StoryMenuArchivedStories')} ) : ( <> {!isChannel && ( - {lang('SendMessageTitle')} + {lang('StoryMenuSendMessage')} )} {isChannel ? ( - {lang('ChatList.ContextOpenChannel')} + {lang('StoryMenuViewChannel')} ) : ( - {lang('StoryList.Context.ViewProfile')} + {lang('StoryMenuViewProfile')} + + )} + {!isChannel && ( + + {lang('StoryMenuOpenStealth')} )} - {lang(isArchived ? 'StoryList.Context.Unarchive' : 'StoryList.Context.Archive')} + {lang(isArchived ? 'StoryMenuUnarchivePeer' : 'StoryMenuArchivePeer')} )} diff --git a/src/components/story/StoryViewer.tsx b/src/components/story/StoryViewer.tsx index 7a71b6a2c..eca27fa64 100644 --- a/src/components/story/StoryViewer.tsx +++ b/src/components/story/StoryViewer.tsx @@ -29,7 +29,6 @@ import useStoryProps from './hooks/useStoryProps'; import Icon from '../common/icons/Icon'; import Button from '../ui/Button'; import ShowTransition from '../ui/ShowTransition'; -import StealthModeModal from './StealthModeModal'; import StoryDeleteConfirmModal from './StoryDeleteConfirmModal'; import StorySettings from './StorySettings'; import StorySlides from './StorySlides'; @@ -175,7 +174,6 @@ function StoryViewer({ onClose={handleCloseDeleteModal} /> - ); diff --git a/src/components/ui/Button.scss b/src/components/ui/Button.scss index 1c6a627c2..336639d29 100644 --- a/src/components/ui/Button.scss +++ b/src/components/ui/Button.scss @@ -25,7 +25,6 @@ } .Button { - --premium-gradient: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%); cursor: var(--custom-cursor, pointer); @@ -56,6 +55,8 @@ transition: background-color 0.15s, color 0.15s, opacity 0.15s; + white-space-collapse: preserve; + // @optimization &:active, &.clicked, @@ -67,6 +68,10 @@ text-transform: none; } + &.inline { + display: inline; + } + &.disabled { cursor: var(--custom-cursor, default); diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 122f18f90..b76e8ec20 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -35,6 +35,7 @@ export type OwnProps = { pill?: boolean; badge?: boolean; fluid?: boolean; + inline?: boolean; isText?: boolean; isLoading?: boolean; ariaLabel?: string; @@ -88,6 +89,7 @@ const Button: FC = ({ pill, badge, fluid, + inline, isText, isLoading, isShiny, @@ -157,6 +159,7 @@ const Button: FC = ({ withPremiumGradient && 'premium', isRectangular && 'rectangular', noForcedUpperCase && 'no-upper-case', + inline && 'inline', iconAlignment && iconName && `content-with-icon-${iconAlignment}`, ); diff --git a/src/components/ui/TextTimer.tsx b/src/components/ui/TextTimer.tsx index 157aeb2c2..6aee9ed87 100644 --- a/src/components/ui/TextTimer.tsx +++ b/src/components/ui/TextTimer.tsx @@ -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 { getServerTime } from '../../util/serverTime'; import useInterval from '../../hooks/schedulers/useInterval'; import useForceUpdate from '../../hooks/useForceUpdate'; -import useLang from '../../hooks/useLang'; -import useOldLang from '../../hooks/useOldLang'; import AnimatedCounter from '../common/AnimatedCounter'; type OwnProps = { - langKey: string; endsAt: number; onEnd?: NoneToVoidFunction; }; const UPDATE_FREQUENCY = 500; // Sometimes second gets skipped if using 1000 -const TextTimer: FC = ({ langKey, endsAt, onEnd }) => { - const lang = useLang(); - const oldLang = useOldLang(); +const TextTimer = ({ endsAt, onEnd }: OwnProps) => { const forceUpdate = useForceUpdate(); const serverTime = getServerTime(); @@ -50,21 +45,11 @@ const TextTimer: FC = ({ langKey, endsAt, onEnd }) => { ); - const isTypedKey = langKey === 'UnlockTimerPublicPostsSearch'; - - if (isTypedKey) { - return ( - - {lang(langKey, { time: timeCounter }, { withNodes: true })} - - ); - } - return ( - {oldLang(langKey, time)} + {timeCounter} ); }; -export default memo(TextTimer); +export default TextTimer; diff --git a/src/global/actions/ui/stories.ts b/src/global/actions/ui/stories.ts index a802878d5..6d3fbd291 100644 --- a/src/global/actions/ui/stories.ts +++ b/src/global/actions/ui/stories.ts @@ -5,6 +5,7 @@ import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { omit } from '../../../util/iteratees'; import * as langProvider from '../../../util/oldLangProvider'; import { callApi } from '../../../api/gramjs'; +import { addTabStateResetterAction } from '../../helpers/meta'; import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { addStoriesForPeer } from '../../reducers'; import { updateTabState } from '../../reducers/tabs'; @@ -352,18 +353,18 @@ addActionHandler('closeStoryPrivacyEditor', (global, actions, payload): ActionRe }, tabId); }); -addActionHandler('toggleStealthModal', (global, actions, payload): ActionReturnType => { - const { isOpen, tabId = getCurrentTabId() } = payload || {}; - const tabState = selectTabState(global, tabId); +addActionHandler('openStealthModal', (global, actions, payload): ActionReturnType => { + const { targetPeerId, tabId = getCurrentTabId() } = payload; return updateTabState(global, { - storyViewer: { - ...tabState.storyViewer, - isStealthModalOpen: isOpen, + storyStealthModal: { + targetPeerId, }, }, tabId); }); +addTabStateResetterAction('closeStealthModal', 'storyStealthModal'); + addActionHandler('clearStoryViews', (global, actions, payload): ActionReturnType => { const { isLoading, tabId = getCurrentTabId() } = payload || {}; diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index d89d3a8c7..2ea4ec4fb 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -1700,9 +1700,10 @@ export interface ActionPayloads { reaction?: ApiReaction; shouldAddToRecent?: boolean; } & WithTabId; - toggleStealthModal: { - isOpen: boolean; - } & WithTabId; + openStealthModal: ({ + targetPeerId: string; + } | Record) & WithTabId; + closeStealthModal: WithTabId | undefined; activateStealthMode: { isForPast?: boolean; isForFuture?: boolean; diff --git a/src/global/types/tabState.ts b/src/global/types/tabState.ts index 87337dbbf..04b274f8c 100644 --- a/src/global/types/tabState.ts +++ b/src/global/types/tabState.ts @@ -335,7 +335,6 @@ export type TabState = { lastViewedByPeerId?: Record; isPrivacyModalOpen?: boolean; isPaymentConfirmDialogOpen?: boolean; - isStealthModalOpen?: boolean; viewModal?: { storyId: number; views?: ApiTypeStoryView[]; @@ -349,6 +348,9 @@ export type TabState = { storyIdsByPeerId: Record; }; }; + storyStealthModal?: { + targetPeerId: string; + } | Record; selectedStoryAlbumId?: number; diff --git a/src/types/language.d.ts b/src/types/language.d.ts index aa0a7452e..b26800428 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -1745,6 +1745,27 @@ export interface LangPair { 'InviteRestrictedPremiumReason': undefined; 'InviteViaLinkButton': 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 { @@ -1900,6 +1921,9 @@ export interface LangPairWithVariables { 'ReportChat': { 'peer': V; }; + 'SlowModePlaceholder': { + 'timer': V; + }; 'SlowModeHint': { 'time': V; }; @@ -2998,6 +3022,15 @@ export interface LangPairWithVariables { 'InviteRestrictedPremiumReasonMultiple': { 'list': V; }; + 'StealthModeOnHint': { + 'time': V; + }; + 'StealthModeButtonRecharge': { + 'timer': V; + }; + 'StealthModeComposerPlaceholder': { + 'timer': V; + }; } export interface LangPairPlural { diff --git a/src/util/localization/index.ts b/src/util/localization/index.ts index 4ec8e5379..1f37325a7 100644 --- a/src/util/localization/index.ts +++ b/src/util/localization/index.ts @@ -369,9 +369,12 @@ function processTranslation( variables?: Record, options?: LangFnOptions | LangFnOptionsWithPlural, ): string { - const cacheKey = `${langKey}-${JSON.stringify(variables)}-${JSON.stringify(options)}`; - if (TRANSLATION_CACHE.has(cacheKey)) { - return TRANSLATION_CACHE.get(cacheKey)!; + const isCacheable = !options?.withNodes; + const cacheKey = isCacheable ? `${langKey}-${JSON.stringify(variables)}-${JSON.stringify(options)}` : undefined; + if (cacheKey) { + if (TRANSLATION_CACHE.has(cacheKey)) { + return TRANSLATION_CACHE.get(cacheKey)!; + } } const pluralValue = options && 'pluralValue' in options ? Number(options.pluralValue) : 0; @@ -390,7 +393,9 @@ function processTranslation( return result.replaceAll(`{${key}}`, valueAsString); }, string); - TRANSLATION_CACHE.set(cacheKey, finalString); + if (cacheKey) { + TRANSLATION_CACHE.set(cacheKey, finalString); + } return finalString; }