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";
|
"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}";
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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')}
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,
|
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(
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 || {};
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
33
src/types/language.d.ts
vendored
33
src/types/language.d.ts
vendored
@ -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 {
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user