Story Ribbon: Allow opening with Stealth Mode (#6471)
This commit is contained in:
parent
a9e68aa473
commit
880a6c6977
@ -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}";
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -94,6 +94,7 @@ const AboutMonetizationModal: FC<OwnProps> = ({
|
||||
isOpen={isOpen}
|
||||
listItemData={modalData.listItemData}
|
||||
headerIconName="cash-circle"
|
||||
headerIconPremiumGradient
|
||||
withSeparator
|
||||
header={modalData.header}
|
||||
footer={modalData.footer}
|
||||
|
||||
@ -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<OwnProps & StateProps> = ({
|
||||
const lastMessageSendTimeSeconds = useRef<number>();
|
||||
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<OwnProps & StateProps> = ({
|
||||
&& 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<OwnProps & StateProps> = ({
|
||||
const isReactionSelectorOpen = isComposerHasFocus && !isReactionPickerOpen && isInStoryViewer && !isAttachMenuOpen
|
||||
&& !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(() => {
|
||||
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<OwnProps & StateProps> = ({
|
||||
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) {
|
||||
return lang('ComposerPlaceholderAnonymous');
|
||||
}
|
||||
@ -1722,7 +1728,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
}, [
|
||||
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<OwnProps & StateProps> = ({
|
||||
isActive={!hasAttachments}
|
||||
getHtml={getHtml}
|
||||
placeholder={placeholder}
|
||||
timedPlaceholderDate={timedPlaceholderDate}
|
||||
timedPlaceholderLangKey={timedPlaceholderLangKey}
|
||||
forcedPlaceholder={inlineBotHelp}
|
||||
canAutoFocus={isReady && isForCurrentMessageList && !hasAttachments && isInMessageList}
|
||||
noFocusInterception={hasAttachments}
|
||||
|
||||
@ -138,11 +138,9 @@ const PublicPostsSearchLauncher = ({
|
||||
</Button>
|
||||
{Boolean(waitTill) && (
|
||||
<div className={styles.freeSearchUnlock}>
|
||||
<TextTimer
|
||||
langKey="UnlockTimerPublicPostsSearch"
|
||||
endsAt={waitTill + WAIT_DELAY}
|
||||
onEnd={onCheckFlood}
|
||||
/>
|
||||
{lang('UnlockTimerPublicPostsSearch', {
|
||||
time: <TextTimer endsAt={waitTill + WAIT_DELAY} onEnd={onCheckFlood} />,
|
||||
}, { withNodes: true })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -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<string>;
|
||||
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<HTMLElement>) => void;
|
||||
captionLimit?: number;
|
||||
onFocus?: NoneToVoidFunction;
|
||||
onBlur?: NoneToVoidFunction;
|
||||
isNeedPremium?: boolean;
|
||||
messageListType?: MessageListType;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -127,8 +124,6 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
||||
isActive,
|
||||
getHtml,
|
||||
placeholder,
|
||||
timedPlaceholderLangKey,
|
||||
timedPlaceholderDate,
|
||||
forcedPlaceholder,
|
||||
canSendPlainText,
|
||||
canAutoFocus,
|
||||
@ -139,14 +134,14 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
||||
isSelectModeActive,
|
||||
canPlayAnimatedEmojis,
|
||||
messageSendKeyCombo,
|
||||
isNeedPremium,
|
||||
messageListType,
|
||||
onUpdate,
|
||||
onSuppressedFocus,
|
||||
onSend,
|
||||
onScroll,
|
||||
onFocus,
|
||||
onBlur,
|
||||
isNeedPremium,
|
||||
messageListType,
|
||||
}) => {
|
||||
const {
|
||||
editLastMessage,
|
||||
@ -177,16 +172,6 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
||||
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<OwnProps & StateProps> = ({
|
||||
>
|
||||
{!isAttachmentModalInput && !canSendPlainText
|
||||
&& <Icon name="lock-badge" className="placeholder-icon" />}
|
||||
{shouldDisplayTimer ? (
|
||||
<TextTimer langKey={timedPlaceholderLangKey!} endsAt={timedPlaceholderDate!} onEnd={handleTimerEnd} />
|
||||
) : placeholder}
|
||||
{placeholder}
|
||||
{isStoryInput && isNeedPremium && (
|
||||
<Button className="unlock-button" size="tiny" color="adaptive" onClick={handleOpenPremiumModal}>
|
||||
{oldLang('StoryRepliesLockedButton')}
|
||||
|
||||
@ -50,6 +50,7 @@ import StarsBalanceModal from './stars/StarsBalanceModal.async';
|
||||
import StarsPaymentModal from './stars/StarsPaymentModal.async';
|
||||
import StarsSubscriptionModal from './stars/subscription/StarsSubscriptionModal.async';
|
||||
import StarsTransactionInfoModal from './stars/transaction/StarsTransactionModal.async';
|
||||
import StealthModeModal from './storyStealthMode/StealthModeModal.async';
|
||||
import SuggestedPostApprovalModal from './suggestedPostApproval/SuggestedPostApprovalModal.async';
|
||||
import SuggestedStatusModal from './suggestedStatus/SuggestedStatusModal.async';
|
||||
import SuggestMessageModal from './suggestMessage/SuggestMessageModal.async';
|
||||
@ -105,7 +106,8 @@ type ModalKey = keyof Pick<TabState,
|
||||
'deleteAccountModal' |
|
||||
'isAgeVerificationModalOpen' |
|
||||
'profileRatingModal' |
|
||||
'quickPreview'
|
||||
'quickPreview' |
|
||||
'storyStealthModal'
|
||||
>;
|
||||
|
||||
type StateProps = {
|
||||
@ -169,6 +171,7 @@ const MODALS: ModalRegistry = {
|
||||
isAgeVerificationModalOpen: AgeVerificationModal,
|
||||
profileRatingModal: ProfileRatingModal,
|
||||
quickPreview: QuickPreviewModal,
|
||||
storyStealthModal: StealthModeModal,
|
||||
};
|
||||
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
|
||||
const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>;
|
||||
|
||||
@ -133,6 +133,7 @@ const AboutAdsModal = ({ modal, minLevelToRestrictAds }: OwnProps & StateProps)
|
||||
isOpen={isOpen}
|
||||
listItemData={modalData?.listItemData}
|
||||
headerIconName="channel"
|
||||
headerIconPremiumGradient
|
||||
withSeparator
|
||||
header={modalData?.header}
|
||||
footer={modalData?.footer}
|
||||
|
||||
@ -12,24 +12,28 @@
|
||||
}
|
||||
|
||||
.topIcon {
|
||||
--premium-gradient: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%);
|
||||
|
||||
display: grid;
|
||||
flex-shrink: 0;
|
||||
place-items: center;
|
||||
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 50%;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
.listItemIcon {
|
||||
font-size: 1.875rem !important;
|
||||
color: var(--accent-color) !important;
|
||||
}
|
||||
|
||||
@ -37,6 +41,7 @@
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
@ -15,13 +15,15 @@ import styles from './TableAboutModal.module.scss';
|
||||
export type TableAboutData = [IconName | undefined, TeactNode, TeactNode][];
|
||||
|
||||
type OwnProps = {
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
isOpen?: boolean;
|
||||
listItemData?: TableAboutData;
|
||||
headerIconName?: IconName;
|
||||
headerIconPremiumGradient?: boolean;
|
||||
header?: TeactNode;
|
||||
footer?: TeactNode;
|
||||
buttonText?: string;
|
||||
buttonText?: TeactNode;
|
||||
hasBackdrop?: boolean;
|
||||
withSeparator?: boolean;
|
||||
onClose: NoneToVoidFunction;
|
||||
@ -29,28 +31,34 @@ type OwnProps = {
|
||||
};
|
||||
|
||||
const TableAboutModal = ({
|
||||
className,
|
||||
isOpen,
|
||||
listItemData,
|
||||
headerIconName,
|
||||
headerIconPremiumGradient,
|
||||
header,
|
||||
footer,
|
||||
buttonText,
|
||||
hasBackdrop,
|
||||
withSeparator,
|
||||
contentClassName,
|
||||
onClose,
|
||||
onButtonClick,
|
||||
contentClassName,
|
||||
}: OwnProps) => {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
className={buildClassName(styles.root, contentClassName)}
|
||||
contentClassName={styles.content}
|
||||
className={buildClassName(styles.root, className)}
|
||||
contentClassName={buildClassName(styles.content, contentClassName)}
|
||||
hasAbsoluteCloseButton
|
||||
absoluteCloseButtonColor={hasBackdrop ? 'translucent-white' : undefined}
|
||||
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}
|
||||
<div>
|
||||
{listItemData?.map(([icon, title, subtitle]) => {
|
||||
@ -69,7 +77,7 @@ const TableAboutModal = ({
|
||||
</div>
|
||||
{withSeparator && <Separator className={styles.separator} />}
|
||||
{footer}
|
||||
{buttonText && (
|
||||
{Boolean(buttonText) && (
|
||||
<Button onClick={onButtonClick || onClose}>{buttonText}</Button>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
@ -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 = (
|
||||
<Link onClick={handleAppeal} isPrimary>
|
||||
{formatUsername(botFreezeAppealUsername)}
|
||||
</Link>
|
||||
);
|
||||
const botLink = (
|
||||
<Link onClick={handleAppeal} isPrimary>
|
||||
{formatUsername(botFreezeAppealUsername)}
|
||||
</Link>
|
||||
);
|
||||
|
||||
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 (
|
||||
<TableAboutModal
|
||||
|
||||
@ -6,10 +6,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.titleContainer {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.profileBlock {
|
||||
position: relative;
|
||||
margin-bottom: 0.5rem;
|
||||
@ -47,7 +43,6 @@
|
||||
}
|
||||
|
||||
.status {
|
||||
padding-bottom: 1rem;
|
||||
font-size: 0.875rem;
|
||||
text-align: center;
|
||||
}
|
||||
@ -58,7 +53,6 @@
|
||||
}
|
||||
|
||||
.giftTitle {
|
||||
padding-bottom: 0.5rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
text-align: center;
|
||||
@ -71,5 +65,4 @@
|
||||
.footer {
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
@ -107,19 +107,13 @@ const GiftStatusInfoModal = ({
|
||||
{lang('Online')}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.titleContainer}>
|
||||
<div className={styles.giftTitle}>
|
||||
{
|
||||
lang('UniqueStatusWearTitle', {
|
||||
gift: mockPeerWithStatus?.emojiStatus?.title,
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div className={styles.infoDescription}>
|
||||
{
|
||||
lang('UniqueStatusBenefitsDescription')
|
||||
}
|
||||
</div>
|
||||
<div className={styles.giftTitle}>
|
||||
{lang('UniqueStatusWearTitle', {
|
||||
gift: mockPeerWithStatus?.emojiStatus?.title,
|
||||
})}
|
||||
</div>
|
||||
<div className={styles.infoDescription}>
|
||||
{lang('UniqueStatusBenefitsDescription')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
131
src/components/modals/storyStealthMode/StealthModeModal.tsx
Normal file
131
src/components/modals/storyStealthMode/StealthModeModal.tsx
Normal 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));
|
||||
@ -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;
|
||||
}
|
||||
@ -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));
|
||||
@ -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({
|
||||
/>
|
||||
<div className={styles.senderMeta}>
|
||||
<span onClick={handleOpenChat} className={styles.senderName}>
|
||||
{renderText(getPeerTitle(oldLang, peer) || '')}
|
||||
{renderText(getPeerTitle(lang, peer) || '')}
|
||||
</span>
|
||||
<div className={styles.storyMetaRow}>
|
||||
{forwardSenderTitle && (
|
||||
@ -681,7 +681,7 @@ function Story({
|
||||
>
|
||||
<Avatar peer={fromPeer} size="micro" />
|
||||
<span className={styles.headerTitle}>
|
||||
{renderText(getPeerTitle(oldLang, fromPeer) || '')}
|
||||
{renderText(getPeerTitle(lang, fromPeer) || '')}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
@ -882,7 +882,7 @@ function Story({
|
||||
withStory
|
||||
storyViewerMode="disabled"
|
||||
/>
|
||||
<div className={styles.name}>{renderText(getPeerTitle(oldLang, peer) || '')}</div>
|
||||
<div className={styles.name}>{renderText(getPeerTitle(lang, peer) || '')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -949,7 +949,6 @@ export default memo(withGlobal<OwnProps>((global, {
|
||||
isMuted,
|
||||
viewModal,
|
||||
isPrivacyModalOpen,
|
||||
isStealthModalOpen,
|
||||
storyList,
|
||||
},
|
||||
forwardMessages: { storyId: forwardedStoryId },
|
||||
@ -959,8 +958,10 @@ export default memo(withGlobal<OwnProps>((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(
|
||||
|
||||
@ -21,6 +21,7 @@ interface OwnProps {
|
||||
|
||||
interface StateProps {
|
||||
orderedPeerIds: string[];
|
||||
stealthModeActiveUntil?: number;
|
||||
usersById: Record<string, ApiUser>;
|
||||
chatsById: Record<string, ApiChat>;
|
||||
}
|
||||
@ -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<OwnProps>(
|
||||
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,
|
||||
};
|
||||
|
||||
@ -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<HTMLDivElement>();
|
||||
|
||||
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 (
|
||||
<div
|
||||
ref={ref}
|
||||
@ -102,7 +123,7 @@ function StoryRibbonButton({ peer, isArchived }: OwnProps) {
|
||||
storyViewerMode="full"
|
||||
/>
|
||||
<div className={buildClassName(styles.name, peer.hasUnreadStories && styles.name_hasUnreadStory)}>
|
||||
{isSelf ? lang('MyStory') : getPeerTitle(lang, peer)}
|
||||
{isSelf ? lang('StoryRibbonMyStory') : getPeerTitle(lang, peer)}
|
||||
</div>
|
||||
{contextMenuAnchor !== undefined && (
|
||||
<Menu
|
||||
@ -121,33 +142,38 @@ function StoryRibbonButton({ peer, isArchived }: OwnProps) {
|
||||
{isSelf ? (
|
||||
<>
|
||||
<MenuItem onClick={handleSavedStories} icon="play-story">
|
||||
{lang('StoryList.Context.SavedStories')}
|
||||
{lang('StoryMenuSavedStories')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleArchivedStories} icon="archive">
|
||||
{lang('StoryList.Context.ArchivedStories')}
|
||||
{lang('StoryMenuArchivedStories')}
|
||||
</MenuItem>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{!isChannel && (
|
||||
<MenuItem onClick={handleOpenChat} icon="message">
|
||||
{lang('SendMessageTitle')}
|
||||
{lang('StoryMenuSendMessage')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isChannel ? (
|
||||
<MenuItem onClick={handleOpenProfile} icon="channel">
|
||||
{lang('ChatList.ContextOpenChannel')}
|
||||
{lang('StoryMenuViewChannel')}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem onClick={handleOpenProfile} icon="user">
|
||||
{lang('StoryList.Context.ViewProfile')}
|
||||
{lang('StoryMenuViewProfile')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{!isChannel && (
|
||||
<MenuItem onClick={handleOpenStealth} icon="eye-crossed-outline">
|
||||
{lang('StoryMenuOpenStealth')}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
onClick={handleArchivePeer}
|
||||
icon={isArchived ? 'unarchive' : 'archive'}
|
||||
>
|
||||
{lang(isArchived ? 'StoryList.Context.Unarchive' : 'StoryList.Context.Archive')}
|
||||
{lang(isArchived ? 'StoryMenuUnarchivePeer' : 'StoryMenuArchivePeer')}
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
<StoryViewModal />
|
||||
<StealthModeModal />
|
||||
<StorySettings isOpen={isPrivacyModalOpen} onClose={closeStoryPrivacyEditor} />
|
||||
</ShowTransition>
|
||||
);
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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<OwnProps> = ({
|
||||
pill,
|
||||
badge,
|
||||
fluid,
|
||||
inline,
|
||||
isText,
|
||||
isLoading,
|
||||
isShiny,
|
||||
@ -157,6 +159,7 @@ const Button: FC<OwnProps> = ({
|
||||
withPremiumGradient && 'premium',
|
||||
isRectangular && 'rectangular',
|
||||
noForcedUpperCase && 'no-upper-case',
|
||||
inline && 'inline',
|
||||
iconAlignment && iconName && `content-with-icon-${iconAlignment}`,
|
||||
);
|
||||
|
||||
|
||||
@ -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<OwnProps> = ({ 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<OwnProps> = ({ langKey, endsAt, onEnd }) => {
|
||||
</span>
|
||||
);
|
||||
|
||||
const isTypedKey = langKey === 'UnlockTimerPublicPostsSearch';
|
||||
|
||||
if (isTypedKey) {
|
||||
return (
|
||||
<span>
|
||||
{lang(langKey, { time: timeCounter }, { withNodes: true })}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
{oldLang(langKey, time)}
|
||||
{timeCounter}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(TextTimer);
|
||||
export default TextTimer;
|
||||
|
||||
@ -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 || {};
|
||||
|
||||
|
||||
@ -1700,9 +1700,10 @@ export interface ActionPayloads {
|
||||
reaction?: ApiReaction;
|
||||
shouldAddToRecent?: boolean;
|
||||
} & WithTabId;
|
||||
toggleStealthModal: {
|
||||
isOpen: boolean;
|
||||
} & WithTabId;
|
||||
openStealthModal: ({
|
||||
targetPeerId: string;
|
||||
} | Record<string, never>) & WithTabId;
|
||||
closeStealthModal: WithTabId | undefined;
|
||||
activateStealthMode: {
|
||||
isForPast?: boolean;
|
||||
isForFuture?: boolean;
|
||||
|
||||
@ -335,7 +335,6 @@ export type TabState = {
|
||||
lastViewedByPeerId?: Record<string, number>;
|
||||
isPrivacyModalOpen?: boolean;
|
||||
isPaymentConfirmDialogOpen?: boolean;
|
||||
isStealthModalOpen?: boolean;
|
||||
viewModal?: {
|
||||
storyId: number;
|
||||
views?: ApiTypeStoryView[];
|
||||
@ -349,6 +348,9 @@ export type TabState = {
|
||||
storyIdsByPeerId: Record<string, number[]>;
|
||||
};
|
||||
};
|
||||
storyStealthModal?: {
|
||||
targetPeerId: string;
|
||||
} | Record<string, never>;
|
||||
|
||||
selectedStoryAlbumId?: number;
|
||||
|
||||
|
||||
33
src/types/language.d.ts
vendored
33
src/types/language.d.ts
vendored
@ -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<V = LangVariable> {
|
||||
@ -1900,6 +1921,9 @@ export interface LangPairWithVariables<V = LangVariable> {
|
||||
'ReportChat': {
|
||||
'peer': V;
|
||||
};
|
||||
'SlowModePlaceholder': {
|
||||
'timer': V;
|
||||
};
|
||||
'SlowModeHint': {
|
||||
'time': V;
|
||||
};
|
||||
@ -2998,6 +3022,15 @@ export interface LangPairWithVariables<V = LangVariable> {
|
||||
'InviteRestrictedPremiumReasonMultiple': {
|
||||
'list': V;
|
||||
};
|
||||
'StealthModeOnHint': {
|
||||
'time': V;
|
||||
};
|
||||
'StealthModeButtonRecharge': {
|
||||
'timer': V;
|
||||
};
|
||||
'StealthModeComposerPlaceholder': {
|
||||
'timer': V;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LangPairPlural {
|
||||
|
||||
@ -369,9 +369,12 @@ function processTranslation(
|
||||
variables?: Record<string, LangVariable | RegularLangFnParameters>,
|
||||
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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user