Introduce Boosts (#3909)

This commit is contained in:
Alexander Zinchuk 2023-10-10 13:35:19 +02:00
parent 8fc3df855d
commit b254b08b2c
39 changed files with 1289 additions and 232 deletions

View File

@ -161,7 +161,7 @@ function buildStatisticsOverview({ current, previous }: GramJs.StatsAbsValueAndP
};
}
function buildStatisticsPercentage(data: GramJs.StatsPercentValue): StatisticsOverviewPercentage {
export function buildStatisticsPercentage(data: GramJs.StatsPercentValue): StatisticsOverviewPercentage {
return {
percentage: ((data.part / data.total) * 100).toFixed(2),
};

View File

@ -1,14 +1,18 @@
import { Api as GramJs } from '../../../lib/gramjs';
import { Api as GramJs, errors } from '../../../lib/gramjs';
import type {
ApiApplyBoostInfo,
ApiBoostsStatus,
ApiMediaArea, ApiMediaAreaCoordinates, ApiMessage, ApiStealthMode, ApiStoryView, ApiTypeStory,
} from '../../types';
import { buildCollectionByCallback } from '../../../util/iteratees';
import { getServerTime } from '../../../util/serverTime';
import { buildPrivacyRules } from './common';
import { buildGeoPoint, buildMessageMediaContent, buildMessageTextContent } from './messageContent';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
import { buildApiReaction, buildReactionCount } from './reactions';
import { buildStatisticsPercentage } from './statistics';
export function buildApiStory(peerId: string, story: GramJs.TypeStoryItem): ApiTypeStory {
if (story instanceof GramJs.StoryItemDeleted) {
@ -161,3 +165,56 @@ export function buildApiPeerStories(peerStories: GramJs.PeerStories) {
return buildCollectionByCallback(peerStories.stories, (story) => [story.id, buildApiStory(peerId, story)]);
}
export function buildApiApplyBoostInfo(
applyBoostInfo: GramJs.stories.TypeCanApplyBoostResult,
): ApiApplyBoostInfo | undefined {
if (applyBoostInfo instanceof GramJs.stories.CanApplyBoostOk) {
return { type: 'ok' };
}
if (applyBoostInfo instanceof GramJs.stories.CanApplyBoostReplace) {
return {
type: 'replace',
boostedChatId: getApiChatIdFromMtpPeer(applyBoostInfo.currentBoost),
};
}
return undefined;
}
export function buildApiApplyBoostInfoFromError(
error: unknown,
): ApiApplyBoostInfo | undefined {
if (error instanceof errors.FloodWaitError) {
return {
type: 'wait',
waitUntil: getServerTime() + error.seconds,
};
}
if (error instanceof Error) {
if (error.message === 'BOOST_NOT_MODIFIED') {
return {
type: 'already',
};
}
}
return undefined;
}
export function buildApiBoostsStatus(boostStatus: GramJs.stories.BoostsStatus): ApiBoostsStatus {
const {
level, boostUrl, boosts, myBoost, currentLevelBoosts, nextLevelBoosts, premiumAudience,
} = boostStatus;
return {
level,
currentLevelBoosts,
boosts,
hasMyBoost: Boolean(myBoost),
boostUrl,
nextLevelBoosts,
...(premiumAudience && { premiumAudience: buildStatisticsPercentage(premiumAudience) }),
};
}

View File

@ -17,6 +17,9 @@ import { buildCollectionByCallback } from '../../../util/iteratees';
import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
import {
buildApiApplyBoostInfo,
buildApiApplyBoostInfoFromError,
buildApiBoostsStatus,
buildApiPeerStories,
buildApiStealthMode,
buildApiStory,
@ -426,3 +429,68 @@ export function activateStealthMode({
shouldReturnTrue: true,
});
}
export async function fetchCanApplyBoost({
chat,
} : {
chat: ApiChat;
}) {
let result: GramJs.stories.TypeCanApplyBoostResult | undefined;
try {
result = await invokeRequest(new GramJs.stories.CanApplyBoost({
peer: buildInputPeer(chat.id, chat.accessHash),
}), {
shouldThrow: true,
});
} catch (error) {
const info = buildApiApplyBoostInfoFromError(error);
if (!info) return undefined;
return {
info,
chats: [],
};
}
if (!result) {
return undefined;
}
const mtpChats = 'chats' in result ? result.chats : [];
addEntitiesToLocalDb(mtpChats);
const chats = mtpChats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
const info = buildApiApplyBoostInfo(result);
return {
info,
chats,
};
}
export function applyBoost({
chat,
} : {
chat: ApiChat;
}) {
return invokeRequest(new GramJs.stories.ApplyBoost({
peer: buildInputPeer(chat.id, chat.accessHash),
}), {
shouldReturnTrue: true,
});
}
export async function fetchBoostsStatus({
chat,
}: {
chat: ApiChat;
}) {
const result = await invokeRequest(new GramJs.stories.GetBoostsStatus({
peer: buildInputPeer(chat.id, chat.accessHash),
}));
if (!result) {
return undefined;
}
return buildApiBoostsStatus(result);
}

View File

@ -2,6 +2,7 @@ import type { ApiPrivacySettings } from '../../types';
import type {
ApiGeoPoint, ApiMessage, ApiReaction, ApiReactionCount,
} from './messages';
import type { StatisticsOverviewPercentage } from './statistics';
export interface ApiStory {
'@type'?: 'story';
@ -108,3 +109,33 @@ export type ApiMediaAreaSuggestedReaction = {
};
export type ApiMediaArea = ApiMediaAreaVenue | ApiMediaAreaGeoPoint | ApiMediaAreaSuggestedReaction;
export type ApiApplyBoostOk = {
type: 'ok';
};
export type ApiApplyBoostReplace = {
type: 'replace';
boostedChatId: string;
};
export type ApiApplyBoostWait = {
type: 'wait';
waitUntil: number;
};
export type ApiApplyBoostAlready = {
type: 'already';
};
export type ApiApplyBoostInfo = ApiApplyBoostOk | ApiApplyBoostReplace | ApiApplyBoostWait | ApiApplyBoostAlready;
export type ApiBoostsStatus = {
level: number;
currentLevelBoosts: number;
boosts: number;
nextLevelBoosts?: number;
hasMyBoost?: boolean;
boostUrl: string;
premiumAudience?: StatisticsOverviewPercentage;
};

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M19.322 13.194c-.69 0-1.216-.616-1.109-1.298l1.5-9.498c.187-1.182-1.36-1.797-2.035-.809L7.1 17.05c-.51.746.023 1.757.926 1.757h4.652c.69 0 1.216.616 1.109 1.298l-1.5 9.498c-.187 1.182 1.36 1.797 2.035.809L24.9 14.95a1.122 1.122 0 0 0-.926-1.757Z" style="stroke-width:1.68365"/></svg>

After

Width:  |  Height:  |  Size: 355 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M16 31c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15C7.716 1 1 7.716 1 16c0 8.284 6.716 15 15 15Zm1.476-17.743a.75.75 0 0 0 .74.867h3.109a.75.75 0 0 1 .619 1.173l-7.069 10.33c-.451.66-1.484.25-1.36-.54l1.003-6.347a.75.75 0 0 0-.741-.867h-3.108a.75.75 0 0 1-.62-1.173l7.069-10.33c.452-.66 1.484-.25 1.36.54z"/></svg>

After

Width:  |  Height:  |  Size: 387 B

View File

@ -19,6 +19,7 @@ export { default as PremiumMainModal } from '../components/main/premium/PremiumM
export { default as GiftPremiumModal } from '../components/main/premium/GiftPremiumModal';
export { default as PremiumLimitReachedModal } from '../components/main/premium/common/PremiumLimitReachedModal';
export { default as StatusPickerMenu } from '../components/left/main/StatusPickerMenu';
export { default as BoostModal } from '../components/modals/boost/BoostModal';
export { default as ChatlistModal } from '../components/modals/chatlist/ChatlistModal';

View File

@ -132,7 +132,7 @@ const Picker: FC<OwnProps> = ({
<div className="picker-header custom-scroll" dir={lang.isRtl ? 'rtl' : undefined}>
{lockedSelectedIds.map((id, i) => (
<PickerSelectedItem
chatOrUserId={id}
peerId={id}
isMinimized={shouldMinimize && i < selectedIds.length - ALWAYS_FULL_ITEMS_COUNT}
forceShowSelf={forceShowSelf}
onClick={handleItemClick}
@ -141,7 +141,7 @@ const Picker: FC<OwnProps> = ({
))}
{unlockedSelectedIds.map((id, i) => (
<PickerSelectedItem
chatOrUserId={id}
peerId={id}
isMinimized={
shouldMinimize && i + lockedSelectedIds.length < selectedIds.length - ALWAYS_FULL_ITEMS_COUNT
}

View File

@ -16,6 +16,10 @@
max-width: calc(50% - 0.5rem);
&.standalone {
max-width: 100%;
}
&.minimized {
padding-right: 0;
}

View File

@ -17,13 +17,14 @@ import Avatar from './Avatar';
import './PickerSelectedItem.scss';
type OwnProps = {
chatOrUserId?: string;
peerId?: string;
icon?: IconName;
title?: string;
isMinimized?: boolean;
isStandalone?: boolean;
canClose?: boolean;
forceShowSelf?: boolean;
clickArg: any;
clickArg?: any;
className?: string;
onClick: (arg: any) => void;
};
@ -38,6 +39,7 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
icon,
title,
isMinimized,
isStandalone,
canClose,
clickArg,
chat,
@ -81,6 +83,7 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
chat?.isForum && 'forum-avatar',
isMinimized && 'minimized',
canClose && 'closeable',
isStandalone && 'standalone',
);
return (
@ -106,13 +109,13 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
};
export default memo(withGlobal<OwnProps>(
(global, { chatOrUserId, forceShowSelf }): StateProps => {
if (!chatOrUserId) {
(global, { peerId, forceShowSelf }): StateProps => {
if (!peerId) {
return {};
}
const chat = selectChat(global, chatOrUserId);
const user = selectUser(global, chatOrUserId);
const chat = selectChat(global, peerId);
const user = selectUser(global, peerId);
const isSavedMessages = !forceShowSelf && user && user.isSelf;
return {

View File

@ -0,0 +1,123 @@
.root {
--percent: calc(var(--progress, 0.5) * 100%);
display: flex;
position: relative;
height: 2rem;
background: #F1F3F5;
border-radius: 0.625rem;
color: black;
}
.withBadge {
margin-top: 2rem;
}
.badgeContainer {
--shift-x: calc(clamp(10%, var(--percent), 90%) - 50%);
display: flex;
justify-content: center;
position: absolute;
top: -1.5rem;
left: 0;
right: 0;
transform: translate(var(--shift-x), -20px);
transition: transform 0.2s ease-in-out;
animation: slide-in 0.5s ease-in-out;
}
@keyframes slide-in {
from {
transform: translate(-50%, -20px);
}
to {
transform: translate(var(--shift-x), -20px);
}
}
.floating-badge {
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
position: relative;
padding: 0.25rem 0.75rem;
border-radius: 1rem;
background-color: #7E85FF;
animation: rotate-in 0.5s ease-in-out;
}
@keyframes rotate-in {
0% {
transform: rotate(0deg);
}
50% {
// Rotate more if progress is higher
transform: rotate(calc(-20deg * var(--progress)));
}
100% {
transform: rotate(0deg);
}
}
.floating-badge-triangle {
position: absolute;
bottom: -15px;
}
.floating-badge-icon {
font-size: 1.25rem;
margin-right: 0.25rem;
}
.floating-badge-value {
font-size: 16px;
font-weight: 500;
}
.left, .right {
position: absolute;
top: 0;
bottom: 0;
display: flex;
align-items: center;
font-weight: 500;
}
.left {
left: 0.75rem;
}
.right {
right: 0.75rem;
}
.progress {
--multiplier: calc(1 / var(--progress) - 1);
overflow: hidden;
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: var(--percent);
border-top-left-radius: 0.625rem;
border-bottom-left-radius: 0.625rem;
background-image: var(--premium-gradient);
background-size: calc(1 / var(--progress) * 100%) 100%;
.left, .right {
color: white;
white-space: nowrap;
}
.right {
right: calc(-100% * var(--multiplier) + 0.75rem);
}
}
.fullProgress {
border-radius: 0.625rem;
}

View File

@ -0,0 +1,79 @@
import type { FC } from '../../lib/teact/teact';
import React, { memo } from '../../lib/teact/teact';
import type { IconName } from '../../types/icons';
import buildClassName from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle';
import useLang from '../../hooks/useLang';
import Icon from './Icon';
import styles from './PremiumProgress.module.scss';
type OwnProps = {
leftText?: string;
rightText?: string;
floatingBadgeIcon?: IconName;
floatingBadgeText?: string;
progress?: number;
className?: string;
};
const LimitPreview: FC<OwnProps> = ({
leftText,
rightText,
floatingBadgeText,
floatingBadgeIcon,
progress,
className,
}) => {
const lang = useLang();
const hasFloatingBadge = Boolean(floatingBadgeIcon || floatingBadgeText);
const isProgressFull = Boolean(progress) && progress > 0.99;
return (
<div
className={buildClassName(
styles.root,
hasFloatingBadge && styles.withBadge,
className,
)}
style={buildStyle(progress !== undefined && `--progress: ${progress}`)}
>
{hasFloatingBadge && (
<div className={styles.badgeContainer}>
<div className={styles.floatingBadge}>
{floatingBadgeIcon && <Icon name={floatingBadgeIcon} className={styles.floatingBadgeIcon} />}
{floatingBadgeText && (
<div className={styles.floatingBadgeValue} dir={lang.isRtl ? 'rtl' : undefined}>{floatingBadgeText}</div>
)}
<div className={styles.floatingBadgeTriangle}>
<svg width="26" height="9" viewBox="0 0 26 9" fill="none">
<path d="M0 0H26H24.4853C22.894 0 21.3679 0.632141 20.2426 1.75736L14.4142 7.58579C13.6332 8.36684 12.3668 8.36683 11.5858 7.58579L5.75736 1.75736C4.63214 0.632139 3.10602 0 1.51472 0H0Z" fill="#7E85FF" />
</svg>
</div>
</div>
</div>
)}
<div className={styles.left}>
<span>{leftText}</span>
</div>
<div className={styles.right}>
<span>{rightText}</span>
</div>
<div className={buildClassName(styles.progress, isProgressFull && styles.fullProgress)}>
<div className={styles.left}>
<span>{leftText}</span>
</div>
<div className={styles.right}>
<span>{rightText}</span>
</div>
</div>
</div>
);
};
export default memo(LimitPreview);

View File

@ -0,0 +1,24 @@
import type { ApiBoostsStatus } from '../../../api/types';
export function getBoostProgressInfo(boostInfo: ApiBoostsStatus) {
const {
level, boosts, currentLevelBoosts, nextLevelBoosts, hasMyBoost,
} = boostInfo;
const currentLevel = level;
const hasNextLevel = Boolean(nextLevelBoosts);
const isJustUpgraded = boosts === currentLevelBoosts && hasMyBoost;
const levelProgress = (!nextLevelBoosts || isJustUpgraded) ? 1
: (boosts - currentLevelBoosts) / (nextLevelBoosts - currentLevelBoosts);
const remainingBoosts = nextLevelBoosts ? nextLevelBoosts - boosts : 0;
return {
currentLevel,
hasNextLevel,
boosts,
levelProgress,
remainingBoosts,
};
}

View File

@ -229,7 +229,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
)}
{globalSearchChatId && (
<PickerSelectedItem
chatOrUserId={globalSearchChatId}
peerId={globalSearchChatId}
onClick={setGlobalSearchChatId}
canClose
clickArg={CLEAR_CHAT_SEARCH_PARAM}

View File

@ -232,7 +232,7 @@ const ChatResults: FC<OwnProps & StateProps> = ({
>
{localResults.map((id) => (
<PickerSelectedItem
chatOrUserId={id}
peerId={id}
onClick={handlePickerItemClick}
clickArg={id}
/>

View File

@ -196,7 +196,7 @@ const SettingsFoldersChatsPicker: FC<OwnProps & StateProps> = ({
{selectedChatTypes.map(renderSelectedChatType)}
{selectedIds.map((id, i) => (
<PickerSelectedItem
chatOrUserId={id}
peerId={id}
isMinimized={shouldMinimize && i < selectedIds.length - ALWAYS_FULL_ITEMS_COUNT}
canClose
onClick={handleItemClick}

View File

@ -76,6 +76,7 @@ import ReactionPicker from '../middle/message/ReactionPicker.async';
import MessageListHistoryHandler from '../middle/MessageListHistoryHandler';
import MiddleColumn from '../middle/MiddleColumn';
import AttachBotInstallModal from '../modals/attachBotInstall/AttachBotInstallModal.async';
import BoostModal from '../modals/boost/BoostModal.async';
import ChatlistModal from '../modals/chatlist/ChatlistModal.async';
import MapModal from '../modals/map/MapModal.async';
import UrlAuthModal from '../modals/urlAuth/UrlAuthModal.async';
@ -153,6 +154,7 @@ type StateProps = {
isReactionPickerOpen: boolean;
isCurrentUserPremium?: boolean;
chatlistModal?: TabState['chatlistModal'];
boostModal?: TabState['boostModal'];
noRightColumnAnimation?: boolean;
withInterfaceAnimations?: boolean;
isSynced?: boolean;
@ -212,6 +214,7 @@ const Main: FC<OwnProps & StateProps> = ({
deleteFolderDialog,
isMasterTab,
chatlistModal,
boostModal,
noRightColumnAnimation,
isSynced,
}) => {
@ -554,6 +557,7 @@ const Main: FC<OwnProps & StateProps> = ({
userId={newContactUserId}
isByPhoneNumber={newContactByPhoneNumber}
/>
<BoostModal info={boostModal} />
<ChatlistModal info={chatlistModal} />
<GameModal openedGame={openedGame} gameTitle={gameTitle} />
<WebAppModal webApp={webApp} />
@ -616,6 +620,7 @@ export default memo(withGlobal<OwnProps>(
limitReachedModal,
deleteFolderDialogModal,
chatlistModal,
boostModal,
} = selectTabState(global);
const { chatId: audioChatId, messageId: audioMessageId } = audioPlayer;
@ -678,6 +683,7 @@ export default memo(withGlobal<OwnProps>(
isMasterTab,
requestedDraft,
chatlistModal,
boostModal,
noRightColumnAnimation,
isSynced: global.isSynced,
};

View File

@ -25,7 +25,9 @@ export function getWebpageButtonText(type?: string) {
case 'telegram_chatlist':
return 'ViewChatList';
case 'telegram_story':
return 'ViewStory';
return 'lng_view_button_story';
case 'telegram_channel_boost':
return 'lng_view_button_boost';
default:
return undefined;
}

View File

@ -0,0 +1,18 @@
import type { FC } from '../../../lib/teact/teact';
import React from '../../../lib/teact/teact';
import type { OwnProps } from './BoostModal';
import { Bundles } from '../../../util/moduleLoader';
import useModuleLoader from '../../../hooks/useModuleLoader';
const BoostModalAsync: FC<OwnProps> = (props) => {
const { info } = props;
const BoostModal = useModuleLoader(Bundles.Extra, 'BoostModal', !info);
// eslint-disable-next-line react/jsx-props-no-spreading
return BoostModal ? <BoostModal {...props} /> : undefined;
};
export default BoostModalAsync;

View File

@ -0,0 +1,69 @@
.content {
display: flex;
flex-direction: column;
gap: 1rem;
padding-top: 0 !important;
min-height: 14rem;
overflow: hidden;
}
.loading {
margin-block: auto;
}
.text-center {
text-align: center;
text-wrap: balance
}
.description {
padding: 0 0.75rem;
}
.chip {
align-self: center;
}
.replaceModal :global(.modal-dialog) {
max-width: 22rem;
}
.replaceModalContent {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.avatarContainer {
display: flex;
align-self: center;
gap: 0.25rem;
align-items: center;
margin-bottom: 0.5rem;
}
.arrow {
font-size: 2rem;
color: var(--color-text-secondary);
}
.boostedWrapper {
position: relative;
}
.boostedMark {
position: absolute;
bottom: -0.125rem;
right: -0.125rem;
font-size: 1.25rem;
background-color: var(--color-background);
padding: 0.125rem;
border-radius: 50%;
z-index: 10;
&::before {
background-image: var(--premium-gradient);
background-clip: text;
-webkit-text-fill-color: transparent;
}
}

View File

@ -0,0 +1,311 @@
import React, { memo, useMemo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiApplyBoostInfo, ApiChat } from '../../../api/types';
import type { TabState } from '../../../global/types';
import { getChatTitle } from '../../../global/helpers';
import { selectChat } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { formatDateInFuture } from '../../../util/dateFormat';
import { getServerTime } from '../../../util/serverTime';
import { getBoostProgressInfo } from '../../common/helpers/boostInfo';
import renderText from '../../common/helpers/renderText';
import useFlag from '../../../hooks/useFlag';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import Avatar from '../../common/Avatar';
import Icon from '../../common/Icon';
import PickerSelectedItem from '../../common/PickerSelectedItem';
import PremiumProgress from '../../common/PremiumProgress';
import Button from '../../ui/Button';
import ConfirmDialog from '../../ui/ConfirmDialog';
import Loading from '../../ui/Loading';
import Modal from '../../ui/Modal';
import styles from './BoostModal.module.scss';
type LoadedParams = {
applyInfo?: ApiApplyBoostInfo;
leftText: string;
rightText?: string;
value: string;
progress: number;
descriptionText: string;
isBoosted?: boolean;
};
type BoostInfo = ({
isStatusLoaded: false;
title: string;
} & Undefined<LoadedParams>) | ({
isStatusLoaded: true;
title: string;
} & LoadedParams);
export type OwnProps = {
info: TabState['boostModal'];
};
type StateProps = {
chat?: ApiChat;
boostedChat?: ApiChat;
};
const BoostModal = ({
info,
chat,
boostedChat,
}: OwnProps & StateProps) => {
const {
openChat,
applyBoost,
closeBoostModal,
requestConfetti,
} = getActions();
const [isReplaceModalOpen, openReplaceModal, closeReplaceModal] = useFlag();
const [isWaitDialogOpen, openWaitDialog, closeWaitDialog] = useFlag();
const isOpen = Boolean(info);
const lang = useLang();
const chatTitle = useMemo(() => {
if (!chat) {
return undefined;
}
return getChatTitle(lang, chat);
}, [chat, lang]);
const boostedChatTitle = useMemo(() => {
if (!boostedChat) {
return undefined;
}
return getChatTitle(lang, boostedChat);
}, [boostedChat, lang]);
const {
isStatusLoaded,
isBoosted,
applyInfo,
title,
leftText,
rightText,
value,
progress,
descriptionText,
}: BoostInfo = useMemo(() => {
if (!info?.boostStatus || !chat) {
return {
isStatusLoaded: false,
title: lang('Loading'),
};
}
const {
level, currentLevelBoosts, hasMyBoost,
} = info.boostStatus;
const {
boosts,
currentLevel,
hasNextLevel,
levelProgress,
remainingBoosts,
} = getBoostProgressInfo(info.boostStatus);
const hasBoost = hasMyBoost || info.applyInfo?.type === 'already';
const isJustUpgraded = boosts === currentLevelBoosts && hasBoost;
const left = lang('BoostsLevel', currentLevel);
const right = hasNextLevel ? lang('BoostsLevel', currentLevel + 1) : undefined;
const moreBoosts = lang('ChannelBoost.MoreBoosts', remainingBoosts);
const currentStoriesPerDay = lang('ChannelBoost.StoriesPerDay', level);
const nextLevelStoriesPerDay = lang('ChannelBoost.StoriesPerDay', level + 1);
const modalTitle = hasBoost ? lang('ChannelBoost.YouBoostedOtherChannel')
: level === 0 ? lang('lng_boost_channel_title_first') : lang('lng_boost_channel_title_more');
let description: string | undefined;
if (level === 0) {
if (!hasBoost) {
description = lang('ChannelBoost.EnableStoriesForChannelText', [chatTitle, moreBoosts]);
} else {
description = lang('ChannelBoost.EnableStoriesMoreRequired', moreBoosts);
}
} else if (isJustUpgraded) {
if (level === 1) {
description = lang('ChannelBoost.EnabledStoriesForChannelText');
} else {
description = lang('ChannelBoost.BoostedChannelReachedLevel', [level, currentStoriesPerDay]);
}
} else {
description = lang('ChannelBoost.HelpUpgradeChannelText', [chatTitle, moreBoosts, nextLevelStoriesPerDay]);
}
return {
isStatusLoaded: true,
title: modalTitle,
leftText: left,
rightText: right,
value: boosts.toString(),
progress: levelProgress,
remainingBoosts,
descriptionText: description,
applyInfo: info.applyInfo,
isBoosted: hasBoost,
};
}, [chat, chatTitle, info, lang]);
const handleOpenChat = useLastCallback(() => {
openChat({ id: chat!.id });
closeBoostModal();
});
const handleApplyBoost = useLastCallback(() => {
closeReplaceModal();
applyBoost({ chatId: chat!.id });
requestConfetti();
});
const handleButtonClick = useLastCallback(() => {
if (applyInfo?.type === 'ok') {
handleApplyBoost();
}
if (applyInfo?.type === 'replace') {
openReplaceModal();
}
if (applyInfo?.type === 'wait') {
openWaitDialog();
}
if (isBoosted) {
closeBoostModal();
}
});
function renderContent() {
if (!isStatusLoaded) {
return <Loading className={styles.loading} />;
}
return (
<>
{chat && (
<PickerSelectedItem
className={styles.chip}
peerId={chat.id}
isStandalone
onClick={handleOpenChat}
/>
)}
<PremiumProgress
leftText={leftText}
rightText={rightText}
progress={progress}
floatingBadgeText={value}
floatingBadgeIcon="boost"
/>
<div className={buildClassName(styles.description, styles.textCenter)}>
{renderText(descriptionText, ['simple_markdown', 'emoji'])}
</div>
<Button
className={styles.button}
size="smaller"
withPremiumGradient
isShiny
isLoading={!applyInfo}
ripple
onClick={handleButtonClick}
>
{!isBoosted ? (
<>
<Icon name="boost" />
{lang('ChannelBoost.BoostChannel')}
</>
) : lang('OK')}
</Button>
</>
);
}
return (
<Modal
isOpen={isOpen}
title={title}
contentClassName={styles.content}
onClose={closeBoostModal}
isSlim
hasCloseButton
>
{renderContent()}
{applyInfo?.type === 'replace' && boostedChatTitle && (
<Modal
isOpen={isReplaceModalOpen}
className={styles.replaceModal}
contentClassName={styles.replaceModalContent}
onClose={closeReplaceModal}
>
<div className={styles.avatarContainer}>
<div className={styles.boostedWrapper}>
<Avatar peer={boostedChat} size="large" />
<Icon name="boostcircle" className={styles.boostedMark} />
</div>
<Icon name="next" className={styles.arrow} />
<Avatar peer={chat} size="large" />
</div>
<div className={styles.textCenter}>
{renderText(lang('ChannelBoost.ReplaceBoost', [boostedChatTitle, chatTitle]), ['simple_markdown', 'emoji'])}
</div>
<div className="dialog-buttons">
<Button isText className="confirm-dialog-button" onClick={handleApplyBoost}>
{lang('Replace')}
</Button>
<Button isText className="confirm-dialog-button" onClick={closeReplaceModal}>
{lang('Cancel')}
</Button>
</div>
</Modal>
)}
{applyInfo?.type === 'wait' && (
<ConfirmDialog
isOpen={isWaitDialogOpen}
isOnlyConfirm
confirmLabel={lang('OK')}
title={lang('ChannelBoost.Error.BoostTooOftenTitle')}
onClose={closeWaitDialog}
confirmHandler={closeWaitDialog}
>
{renderText(
lang(
'ChannelBoost.Error.BoostTooOftenText',
formatDateInFuture(lang, getServerTime(), applyInfo.waitUntil),
),
['simple_markdown', 'emoji'],
)}
</ConfirmDialog>
)}
</Modal>
);
};
export default memo(withGlobal<OwnProps>(
(global, { info }): StateProps => {
const chat = info && selectChat(global, info?.chatId);
const boostedChat = info?.applyInfo?.type === 'replace'
? selectChat(global, info.applyInfo.boostedChatId) : undefined;
return {
chat,
boostedChat,
};
},
)(BoostModal));

View File

@ -359,7 +359,8 @@
.Spinner {
position: absolute;
right: 0.875rem;
top: 0.875rem;
top: 50%;
transform: translateY(-50%);
--spinner-size: 1.8125rem;
}

View File

@ -20,6 +20,7 @@ type OwnProps = {
confirmLabel?: string;
confirmIsDestructive?: boolean;
isConfirmDisabled?: boolean;
isOnlyConfirm?: boolean;
areButtonsInColumn?: boolean;
className?: string;
children?: React.ReactNode;
@ -37,6 +38,7 @@ const ConfirmDialog: FC<OwnProps> = ({
confirmLabel = 'Confirm',
confirmIsDestructive,
isConfirmDisabled,
isOnlyConfirm,
areButtonsInColumn,
className,
children,
@ -82,7 +84,7 @@ const ConfirmDialog: FC<OwnProps> = ({
>
{confirmLabel}
</Button>
<Button className="confirm-dialog-button" isText onClick={onClose}>{lang('Cancel')}</Button>
{!isOnlyConfirm && <Button className="confirm-dialog-button" isText onClick={onClose}>{lang('Cancel')}</Button>}
</div>
</Modal>
);

View File

@ -9,12 +9,15 @@ import './Loading.scss';
type OwnProps = {
color?: 'blue' | 'white' | 'black' | 'yellow';
backgroundColor?: 'light' | 'dark';
className?: string;
onClick?: NoneToVoidFunction;
};
const Loading = ({ color = 'blue', backgroundColor, onClick }: OwnProps) => {
const Loading = ({
color = 'blue', backgroundColor, className, onClick,
}: OwnProps) => {
return (
<div className={buildClassName('Loading', onClick && 'interactive')} onClick={onClick}>
<div className={buildClassName('Loading', onClick && 'interactive', className)} onClick={onClick}>
<Spinner color={color} backgroundColor={backgroundColor} />
</div>
);

View File

@ -42,6 +42,7 @@ import {
isChatSummaryOnly,
isChatSuperGroup,
isUserBot,
toChannelId,
} from '../../helpers';
import {
addActionHandler, getGlobal, setGlobal,
@ -973,6 +974,7 @@ addActionHandler('openTelegramLink', (global, actions, payload): ActionReturnTyp
checkChatlistInvite,
openChatByUsername: openChatByUsernameAction,
openStoryViewerByUsername,
processBoostParameters,
} = actions;
if (url.match(RE_TG_LINK)) {
@ -1002,6 +1004,7 @@ addActionHandler('openTelegramLink', (global, actions, payload): ActionReturnTyp
const hasStartApp = params.hasOwnProperty('startapp');
const choose = parseChooseParameter(params.choose);
const storyId = part2 === 's' && (Number(part3) || undefined);
const hasBoost = params.hasOwnProperty('boost');
if (part1.match(/^\+([0-9]+)(\?|$)/)) {
openChatByPhoneNumber({
@ -1064,19 +1067,39 @@ addActionHandler('openTelegramLink', (global, actions, payload): ActionReturnTyp
inviteHash: params.voicechat || params.livestream,
tabId,
});
} else if (part1 === 'boost') {
const username = part2;
const id = params.c;
const isPrivate = !username && Boolean(id);
processBoostParameters({
usernameOrId: username || id,
isPrivate,
tabId,
});
} else if (hasBoost) {
const isPrivate = part1 === 'c' && Boolean(chatOrChannelPostId);
processBoostParameters({
usernameOrId: chatOrChannelPostId || part1,
isPrivate,
tabId,
});
} else if (part1 === 'c' && chatOrChannelPostId && messageId) {
const chatId = `-100${chatOrChannelPostId}`;
const chatId = toChannelId(chatOrChannelPostId);
const chat = selectChat(global, chatId);
if (!chat) {
showNotification({ message: 'Chat does not exist', tabId });
return;
}
focusMessage({
chatId: chat.id,
messageId,
tabId,
});
if (messageId) {
focusMessage({
chatId: chat.id,
messageId,
tabId,
});
}
} else if (part1.startsWith('$')) {
openInvoice({
slug: part1.substring(1),
@ -1110,6 +1133,37 @@ addActionHandler('openTelegramLink', (global, actions, payload): ActionReturnTyp
}
});
addActionHandler('processBoostParameters', async (global, actions, payload): Promise<void> => {
const { usernameOrId, isPrivate, tabId = getCurrentTabId() } = payload;
let chat: ApiChat | undefined;
if (isPrivate) {
const chatId = toChannelId(usernameOrId);
chat = selectChat(global, chatId);
if (!chat) {
actions.showNotification({ message: 'Chat does not exist', tabId });
return;
}
} else {
chat = await fetchChatByUsername(global, usernameOrId);
if (!chat) {
actions.showNotification({ message: 'User does not exist', tabId });
return;
}
}
if (!isChatChannel(chat)) {
actions.openChat({ id: chat.id, tabId });
return;
}
actions.openBoostModal({
chatId: chat.id,
tabId,
});
});
addActionHandler('acceptInviteConfirmation', async (global, actions, payload): Promise<void> => {
const { hash, tabId = getCurrentTabId() } = payload!;
const result = await callApi('importChatInvite', { hash });
@ -1139,7 +1193,9 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise
return;
}
if (!isWebApp) {
await openChatByUsername(global, actions, username, threadId, messageId, startParam, startAttach, attach, tabId);
await openChatByUsername(
global, actions, username, threadId, messageId, startParam, startAttach, attach, tabId,
);
return;
}
}

View File

@ -6,7 +6,7 @@ import { buildCollectionByKey } from '../../../util/iteratees';
import { translate } from '../../../util/langProvider';
import { getServerTime } from '../../../util/serverTime';
import { callApi } from '../../../api/gramjs';
import { buildApiInputPrivacyRules } from '../../helpers';
import { buildApiInputPrivacyRules, isChatChannel } from '../../helpers';
import { addActionHandler, getGlobal, setGlobal } from '../../index';
import {
addChats,
@ -26,8 +26,10 @@ import {
updateStoryViews,
updateStoryViewsLoading,
} from '../../reducers';
import { updateTabState } from '../../reducers/tabs';
import {
selectPeer, selectPeerStories, selectPeerStory,
selectChat,
selectPeer, selectPeerStories, selectPeerStory, selectTabState,
} from '../../selectors';
const INFINITE_LOOP_MARKER = 100;
@ -502,3 +504,89 @@ addActionHandler('activateStealthMode', (global, actions, payload): ActionReturn
callApi('activateStealthMode', { isForPast: isForPast || true, isForFuture: isForFuture || true });
});
addActionHandler('openBoostModal', async (global, actions, payload): Promise<void> => {
const { chatId, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, chatId);
if (!chat || !isChatChannel(chat)) return;
global = updateTabState(global, {
boostModal: {
chatId,
},
}, tabId);
setGlobal(global);
const result = await callApi('fetchBoostsStatus', {
chat,
});
if (!result) {
actions.closeBoostModal({ tabId });
return;
}
global = getGlobal();
global = updateTabState(global, {
boostModal: {
chatId,
boostStatus: result,
},
}, tabId);
setGlobal(global);
const applyInfoResult = await callApi('fetchCanApplyBoost', {
chat,
});
if (!applyInfoResult?.info) return;
const applyInfo = applyInfoResult.info;
global = getGlobal();
const tabState = selectTabState(global, tabId);
if (!tabState.boostModal) return;
global = addChats(global, buildCollectionByKey(applyInfoResult.chats, 'id'));
global = updateTabState(global, {
boostModal: {
...tabState.boostModal,
applyInfo,
},
}, tabId);
setGlobal(global);
});
addActionHandler('applyBoost', async (global, actions, payload): Promise<void> => {
const { chatId, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
const result = await callApi('applyBoost', {
chat,
});
if (!result) {
return;
}
const newStatusResult = await callApi('fetchBoostsStatus', {
chat,
});
if (!newStatusResult) {
return;
}
global = getGlobal();
const tabState = selectTabState(global, tabId);
if (!tabState.boostModal?.boostStatus) return;
global = updateTabState(global, {
boostModal: {
...tabState.boostModal,
boostStatus: newStatusResult,
},
}, tabId);
setGlobal(global);
});

View File

@ -411,3 +411,11 @@ addActionHandler('updateStoryView', (global, actions, payload): ActionReturnType
},
}, tabId);
});
addActionHandler('closeBoostModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
boostModal: undefined,
}, tabId);
});

View File

@ -35,6 +35,10 @@ export function isChannelId(entityId: string) {
return entityId.length === CHANNEL_ID_LENGTH && entityId.startsWith('-100');
}
export function toChannelId(mtpId: string) {
return `-100${mtpId}`;
}
export function isChatGroup(chat: ApiChat) {
return isChatBasicGroup(chat) || isChatSuperGroup(chat);
}

View File

@ -1,8 +1,10 @@
import type {
ApiAppConfig,
ApiApplyBoostInfo,
ApiAttachBot,
ApiAttachment,
ApiAvailableReaction,
ApiBoostsStatus,
ApiChannelStatistics,
ApiChat,
ApiChatAdminRights,
@ -617,6 +619,12 @@ export type TabState = {
suggestedPeerIds?: string[];
};
};
boostModal?: {
chatId: string;
boostStatus?: ApiBoostsStatus;
applyInfo?: ApiApplyBoostInfo;
};
};
export type GlobalState = {
@ -1358,6 +1366,10 @@ export interface ActionPayloads {
startApp?: string;
originalParts?: string[];
} & WithTabId;
processBoostParameters: {
usernameOrId: string;
isPrivate?: boolean;
} & WithTabId;
requestThreadInfoUpdate: {
chatId: string;
threadId: number;
@ -2068,6 +2080,14 @@ export interface ActionPayloads {
isForFuture?: boolean;
} | undefined;
openBoostModal: {
chatId: string;
} & WithTabId;
closeBoostModal: WithTabId | undefined;
applyBoost: {
chatId: string;
} & WithTabId;
// Media Viewer & Audio Player
openMediaViewer: {
chatId?: string;

View File

@ -348,7 +348,7 @@ namespace Api {
export type TypeAccessPointRule = AccessPointRule;
export type TypeTlsClientHello = TlsClientHello;
export type TypeTlsBlock = TlsBlockString | TlsBlockRandom | TlsBlockZero | TlsBlockDomain | TlsBlockGrease | TlsBlockScope;
export namespace storage {
export type TypeFileType = storage.FileUnknown | storage.FilePartial | storage.FileJpeg | storage.FileGif | storage.FilePng | storage.FilePdf | storage.FileMp3 | storage.FileMov | storage.FileMp4 | storage.FileWebp;
@ -423,6 +423,7 @@ namespace Api {
export type TypeEmojiGroups = messages.EmojiGroupsNotModified | messages.EmojiGroups;
export type TypeTranslatedText = messages.TranslateResult;
export type TypeBotApp = messages.BotApp;
export type TypeWebPage = messages.WebPage;
}
export namespace updates {
@ -8915,7 +8916,7 @@ namespace Api {
}> {
entries: Api.TypeTlsBlock[];
};
export namespace storage {
export class FileUnknown extends VirtualClass<void> {};
@ -9753,6 +9754,15 @@ namespace Api {
hasSettings?: true;
app: Api.TypeBotApp;
};
export class WebPage extends VirtualClass<{
webpage: Api.TypeWebPage;
chats: Api.TypeChat[];
users: Api.TypeUser[];
}> {
webpage: Api.TypeWebPage;
chats: Api.TypeChat[];
users: Api.TypeUser[];
};
}
export namespace updates {
@ -10807,6 +10817,7 @@ namespace Api {
boosts: int;
nextLevelBoosts?: int;
premiumAudience?: Api.TypeStatsPercentValue;
boostUrl: string;
}> {
// flags: undefined;
myBoost?: true;
@ -10815,6 +10826,7 @@ namespace Api {
boosts: int;
nextLevelBoosts?: int;
premiumAudience?: Api.TypeStatsPercentValue;
boostUrl: string;
};
export class CanApplyBoostOk extends VirtualClass<void> {};
export class CanApplyBoostReplace extends VirtualClass<{
@ -10971,7 +10983,7 @@ namespace Api {
}>, Api.TypeDestroySessionRes> {
sessionId: long;
};
export namespace auth {
export class SendCode extends Request<Partial<{
@ -12727,7 +12739,7 @@ namespace Api {
export class GetWebPage extends Request<Partial<{
url: string;
hash: int;
}>, Api.TypeWebPage> {
}>, messages.TypeWebPage> {
url: string;
hash: int;
};

View File

@ -1148,11 +1148,12 @@ mediaAreaGeoPoint#df8b3b22 coordinates:MediaAreaCoordinates geo:GeoPoint = Media
mediaAreaSuggestedReaction#14455871 flags:# dark:flags.0?true flipped:flags.1?true coordinates:MediaAreaCoordinates reaction:Reaction = MediaArea;
peerStories#9a35e999 flags:# peer:Peer max_read_id:flags.0?int stories:Vector<StoryItem> = PeerStories;
stories.peerStories#cae68768 stories:PeerStories chats:Vector<Chat> users:Vector<User> = stories.PeerStories;
stories.boostsStatus#66ea1fef flags:# my_boost:flags.2?true level:int current_level_boosts:int boosts:int next_level_boosts:flags.0?int premium_audience:flags.1?StatsPercentValue = stories.BoostsStatus;
stories.boostsStatus#e5c1aa5c flags:# my_boost:flags.2?true level:int current_level_boosts:int boosts:int next_level_boosts:flags.0?int premium_audience:flags.1?StatsPercentValue boost_url:string = stories.BoostsStatus;
stories.canApplyBoostOk#c3173587 = stories.CanApplyBoostResult;
stories.canApplyBoostReplace#712c4655 current_boost:Peer chats:Vector<Chat> = stories.CanApplyBoostResult;
booster#e9e6380 user_id:long expires:int = Booster;
stories.boostersList#f3dd3d1d flags:# count:int boosters:Vector<Booster> next_offset:flags.0?string users:Vector<User> = stories.BoostersList;
messages.webPage#fd5e12bd webpage:WebPage chats:Vector<Chat> users:Vector<User> = messages.WebPage;
---functions---
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
initConnection#c1cd5ea9 {X:Type} flags:# api_id:int device_model:string system_version:string app_version:string system_lang_code:string lang_pack:string lang_code:string proxy:flags.0?InputClientProxy params:flags.1?JSONValue query:!X = X;
@ -1279,7 +1280,7 @@ messages.getRecentStickers#9da9403b flags:# attached:flags.0?true hash:long = me
messages.saveRecentSticker#392718f8 flags:# attached:flags.0?true id:InputDocument unsave:Bool = Bool;
messages.clearRecentStickers#8999602d flags:# attached:flags.0?true = Bool;
messages.getCommonChats#e40ca104 user_id:InputUser max_id:long limit:int = messages.Chats;
messages.getWebPage#32ca8f91 url:string hash:int = WebPage;
messages.getWebPage#8d9692a3 url:string hash:int = messages.WebPage;
messages.toggleDialogPin#a731e257 flags:# pinned:flags.0?true peer:InputDialogPeer = Bool;
messages.getPinnedDialogs#d6b94df2 folder_id:int = messages.PeerDialogs;
messages.uploadMedia#519bc2b1 peer:InputPeer media:InputMedia = MessageMedia;
@ -1468,4 +1469,8 @@ stories.activateStealthMode#57bbd166 flags:# past:flags.0?true future:flags.1?tr
stories.sendReaction#7fd736b2 flags:# add_to_recent:flags.0?true peer:InputPeer story_id:int reaction:Reaction = Updates;
stories.getPeerStories#2c4ada50 peer:InputPeer = stories.PeerStories;
stories.getPeerMaxIDs#535983c3 id:Vector<InputPeer> = Vector<int>;
stories.togglePeerStoriesHidden#bd0415c4 peer:InputPeer hidden:Bool = Bool;`;
stories.togglePeerStoriesHidden#bd0415c4 peer:InputPeer hidden:Bool = Bool;
stories.getBoostsStatus#4c449472 peer:InputPeer = stories.BoostsStatus;
stories.getBoostersList#337ef980 peer:InputPeer offset:string limit:int = stories.BoostersList;
stories.canApplyBoost#db05c1bd peer:InputPeer = stories.CanApplyBoostResult;
stories.applyBoost#f29d7c2b peer:InputPeer = Bool;`;

View File

@ -314,5 +314,9 @@
"stories.sendReaction",
"stories.getPeerMaxIDs",
"stories.togglePeerStoriesHidden",
"stories.getPeerStories"
"stories.getPeerStories",
"stories.getBoostsStatus",
"stories.getBoostersList",
"stories.canApplyBoost",
"stories.applyBoost"
]

View File

@ -288,6 +288,8 @@ $color-message-story-mention-to: #74bcff;
--drag-target-border: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='8' ry='8' stroke='%23DDDFE0' stroke-width='4' stroke-dasharray='9.1%2c 10.5' stroke-dashoffset='3' stroke-linecap='round'/%3e%3c/svg%3e");
--drag-target-border-hovered: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='8' ry='8' stroke='%2363A2E3' stroke-width='4' stroke-dasharray='9.1%2c 10.5' stroke-dashoffset='3' stroke-linecap='round'/%3e%3c/svg%3e");
--premium-gradient: linear-gradient(84.4deg, #6C93FF -4.85%, #976FFF 51.72%, #DF69D1 110.7%);
--layer-blackout-opacity: 0.3;
--layer-transition: 300ms cubic-bezier(0.33, 1, 0.68, 1);

View File

@ -3,8 +3,8 @@ $icons-font: "icons";
@font-face {
font-family: $icons-font;
src: url("./icons.woff2?2e8e2fec4b27141c4d298083615a0665") format("woff2"),
url("./icons.woff?2e8e2fec4b27141c4d298083615a0665") format("woff");
src: url("./icons.woff2?aa9c231863df4bab22759fc9f141c077") format("woff2"),
url("./icons.woff?aa9c231863df4bab22759fc9f141c077") format("woff");
font-weight: normal;
font-style: normal;
font-display: block;
@ -58,200 +58,202 @@ $icons-map: (
"avatar-deleted-account": "\f115",
"avatar-saved-messages": "\f116",
"bold": "\f117",
"bot-command": "\f118",
"bot-commands-filled": "\f119",
"bots": "\f11a",
"bug": "\f11b",
"calendar-filter": "\f11c",
"calendar": "\f11d",
"camera-add": "\f11e",
"camera": "\f11f",
"car": "\f120",
"card": "\f121",
"channel-filled": "\f122",
"channel": "\f123",
"channelviews": "\f124",
"chat-badge": "\f125",
"chats-badge": "\f126",
"check": "\f127",
"close-circle": "\f128",
"close-topic": "\f129",
"close": "\f12a",
"cloud-download": "\f12b",
"collapse": "\f12c",
"colorize": "\f12d",
"comments-sticker": "\f12e",
"comments": "\f12f",
"copy-media": "\f130",
"copy": "\f131",
"darkmode": "\f132",
"data": "\f133",
"delete-filled": "\f134",
"delete-left": "\f135",
"delete-user": "\f136",
"delete": "\f137",
"document": "\f138",
"double-badge": "\f139",
"down": "\f13a",
"download": "\f13b",
"eats": "\f13c",
"edit": "\f13d",
"email": "\f13e",
"enter": "\f13f",
"expand": "\f140",
"eye-closed-outline": "\f141",
"eye-closed": "\f142",
"eye-outline": "\f143",
"eye": "\f144",
"favorite-filled": "\f145",
"favorite": "\f146",
"file-badge": "\f147",
"flag": "\f148",
"folder-badge": "\f149",
"folder": "\f14a",
"fontsize": "\f14b",
"forums": "\f14c",
"forward": "\f14d",
"fullscreen": "\f14e",
"gifs": "\f14f",
"gift": "\f150",
"group-filled": "\f151",
"group": "\f152",
"grouped-disable": "\f153",
"grouped": "\f154",
"hand-stop": "\f155",
"hashtag": "\f156",
"heart-outline": "\f157",
"heart": "\f158",
"help": "\f159",
"info-filled": "\f15a",
"info": "\f15b",
"install": "\f15c",
"italic": "\f15d",
"key": "\f15e",
"keyboard": "\f15f",
"lamp": "\f160",
"language": "\f161",
"large-pause": "\f162",
"large-play": "\f163",
"link-badge": "\f164",
"link-broken": "\f165",
"link": "\f166",
"location": "\f167",
"lock-badge": "\f168",
"lock": "\f169",
"logout": "\f16a",
"loop": "\f16b",
"mention": "\f16c",
"message-failed": "\f16d",
"message-pending": "\f16e",
"message-read": "\f16f",
"message-succeeded": "\f170",
"message": "\f171",
"microphone-alt": "\f172",
"microphone": "\f173",
"monospace": "\f174",
"more-circle": "\f175",
"more": "\f176",
"mute": "\f177",
"muted": "\f178",
"new-chat-filled": "\f179",
"next": "\f17a",
"noise-suppression": "\f17b",
"non-contacts": "\f17c",
"open-in-new-tab": "\f17d",
"password-off": "\f17e",
"pause": "\f17f",
"permissions": "\f180",
"phone-discard-outline": "\f181",
"phone-discard": "\f182",
"phone": "\f183",
"photo": "\f184",
"pin-badge": "\f185",
"pin-list": "\f186",
"pin": "\f187",
"pinned-chat": "\f188",
"pinned-message": "\f189",
"pip": "\f18a",
"play-story": "\f18b",
"play": "\f18c",
"poll": "\f18d",
"premium": "\f18e",
"previous": "\f18f",
"privacy-policy": "\f190",
"readchats": "\f191",
"recent": "\f192",
"reload": "\f193",
"remove": "\f194",
"reopen-topic": "\f195",
"replace": "\f196",
"replies": "\f197",
"reply-filled": "\f198",
"reply": "\f199",
"revote": "\f19a",
"save-story": "\f19b",
"saved-messages": "\f19c",
"schedule": "\f19d",
"search": "\f19e",
"select": "\f19f",
"send-outline": "\f1a0",
"send": "\f1a1",
"settings-filled": "\f1a2",
"settings": "\f1a3",
"share-filled": "\f1a4",
"share-screen-outlined": "\f1a5",
"share-screen-stop": "\f1a6",
"share-screen": "\f1a7",
"sidebar": "\f1a8",
"skip-next": "\f1a9",
"skip-previous": "\f1aa",
"smallscreen": "\f1ab",
"smile": "\f1ac",
"sort": "\f1ad",
"speaker-muted-story": "\f1ae",
"speaker-outline": "\f1af",
"speaker-story": "\f1b0",
"speaker": "\f1b1",
"spoiler-disable": "\f1b2",
"spoiler": "\f1b3",
"sport": "\f1b4",
"stats": "\f1b5",
"stealth-future": "\f1b6",
"stealth-past": "\f1b7",
"stickers": "\f1b8",
"stop-raising-hand": "\f1b9",
"stop": "\f1ba",
"story-caption": "\f1bb",
"story-expired": "\f1bc",
"story-priority": "\f1bd",
"story-reply": "\f1be",
"strikethrough": "\f1bf",
"timer": "\f1c0",
"transcribe": "\f1c1",
"truck": "\f1c2",
"unarchive": "\f1c3",
"underlined": "\f1c4",
"unlock-badge": "\f1c5",
"unlock": "\f1c6",
"unmute": "\f1c7",
"unpin": "\f1c8",
"unread": "\f1c9",
"up": "\f1ca",
"user-filled": "\f1cb",
"user-online": "\f1cc",
"user": "\f1cd",
"video-outlined": "\f1ce",
"video-stop": "\f1cf",
"video": "\f1d0",
"voice-chat": "\f1d1",
"volume-1": "\f1d2",
"volume-2": "\f1d3",
"volume-3": "\f1d4",
"web": "\f1d5",
"webapp": "\f1d6",
"word-wrap": "\f1d7",
"zoom-in": "\f1d8",
"zoom-out": "\f1d9",
"boost": "\f118",
"boostcircle": "\f119",
"bot-command": "\f11a",
"bot-commands-filled": "\f11b",
"bots": "\f11c",
"bug": "\f11d",
"calendar-filter": "\f11e",
"calendar": "\f11f",
"camera-add": "\f120",
"camera": "\f121",
"car": "\f122",
"card": "\f123",
"channel-filled": "\f124",
"channel": "\f125",
"channelviews": "\f126",
"chat-badge": "\f127",
"chats-badge": "\f128",
"check": "\f129",
"close-circle": "\f12a",
"close-topic": "\f12b",
"close": "\f12c",
"cloud-download": "\f12d",
"collapse": "\f12e",
"colorize": "\f12f",
"comments-sticker": "\f130",
"comments": "\f131",
"copy-media": "\f132",
"copy": "\f133",
"darkmode": "\f134",
"data": "\f135",
"delete-filled": "\f136",
"delete-left": "\f137",
"delete-user": "\f138",
"delete": "\f139",
"document": "\f13a",
"double-badge": "\f13b",
"down": "\f13c",
"download": "\f13d",
"eats": "\f13e",
"edit": "\f13f",
"email": "\f140",
"enter": "\f141",
"expand": "\f142",
"eye-closed-outline": "\f143",
"eye-closed": "\f144",
"eye-outline": "\f145",
"eye": "\f146",
"favorite-filled": "\f147",
"favorite": "\f148",
"file-badge": "\f149",
"flag": "\f14a",
"folder-badge": "\f14b",
"folder": "\f14c",
"fontsize": "\f14d",
"forums": "\f14e",
"forward": "\f14f",
"fullscreen": "\f150",
"gifs": "\f151",
"gift": "\f152",
"group-filled": "\f153",
"group": "\f154",
"grouped-disable": "\f155",
"grouped": "\f156",
"hand-stop": "\f157",
"hashtag": "\f158",
"heart-outline": "\f159",
"heart": "\f15a",
"help": "\f15b",
"info-filled": "\f15c",
"info": "\f15d",
"install": "\f15e",
"italic": "\f15f",
"key": "\f160",
"keyboard": "\f161",
"lamp": "\f162",
"language": "\f163",
"large-pause": "\f164",
"large-play": "\f165",
"link-badge": "\f166",
"link-broken": "\f167",
"link": "\f168",
"location": "\f169",
"lock-badge": "\f16a",
"lock": "\f16b",
"logout": "\f16c",
"loop": "\f16d",
"mention": "\f16e",
"message-failed": "\f16f",
"message-pending": "\f170",
"message-read": "\f171",
"message-succeeded": "\f172",
"message": "\f173",
"microphone-alt": "\f174",
"microphone": "\f175",
"monospace": "\f176",
"more-circle": "\f177",
"more": "\f178",
"mute": "\f179",
"muted": "\f17a",
"new-chat-filled": "\f17b",
"next": "\f17c",
"noise-suppression": "\f17d",
"non-contacts": "\f17e",
"open-in-new-tab": "\f17f",
"password-off": "\f180",
"pause": "\f181",
"permissions": "\f182",
"phone-discard-outline": "\f183",
"phone-discard": "\f184",
"phone": "\f185",
"photo": "\f186",
"pin-badge": "\f187",
"pin-list": "\f188",
"pin": "\f189",
"pinned-chat": "\f18a",
"pinned-message": "\f18b",
"pip": "\f18c",
"play-story": "\f18d",
"play": "\f18e",
"poll": "\f18f",
"premium": "\f190",
"previous": "\f191",
"privacy-policy": "\f192",
"readchats": "\f193",
"recent": "\f194",
"reload": "\f195",
"remove": "\f196",
"reopen-topic": "\f197",
"replace": "\f198",
"replies": "\f199",
"reply-filled": "\f19a",
"reply": "\f19b",
"revote": "\f19c",
"save-story": "\f19d",
"saved-messages": "\f19e",
"schedule": "\f19f",
"search": "\f1a0",
"select": "\f1a1",
"send-outline": "\f1a2",
"send": "\f1a3",
"settings-filled": "\f1a4",
"settings": "\f1a5",
"share-filled": "\f1a6",
"share-screen-outlined": "\f1a7",
"share-screen-stop": "\f1a8",
"share-screen": "\f1a9",
"sidebar": "\f1aa",
"skip-next": "\f1ab",
"skip-previous": "\f1ac",
"smallscreen": "\f1ad",
"smile": "\f1ae",
"sort": "\f1af",
"speaker-muted-story": "\f1b0",
"speaker-outline": "\f1b1",
"speaker-story": "\f1b2",
"speaker": "\f1b3",
"spoiler-disable": "\f1b4",
"spoiler": "\f1b5",
"sport": "\f1b6",
"stats": "\f1b7",
"stealth-future": "\f1b8",
"stealth-past": "\f1b9",
"stickers": "\f1ba",
"stop-raising-hand": "\f1bb",
"stop": "\f1bc",
"story-caption": "\f1bd",
"story-expired": "\f1be",
"story-priority": "\f1bf",
"story-reply": "\f1c0",
"strikethrough": "\f1c1",
"timer": "\f1c2",
"transcribe": "\f1c3",
"truck": "\f1c4",
"unarchive": "\f1c5",
"underlined": "\f1c6",
"unlock-badge": "\f1c7",
"unlock": "\f1c8",
"unmute": "\f1c9",
"unpin": "\f1ca",
"unread": "\f1cb",
"up": "\f1cc",
"user-filled": "\f1cd",
"user-online": "\f1ce",
"user": "\f1cf",
"video-outlined": "\f1d0",
"video-stop": "\f1d1",
"video": "\f1d2",
"voice-chat": "\f1d3",
"volume-1": "\f1d4",
"volume-2": "\f1d5",
"volume-3": "\f1d6",
"web": "\f1d7",
"webapp": "\f1d8",
"word-wrap": "\f1d9",
"zoom-in": "\f1da",
"zoom-out": "\f1db",
);
.icon-active-sessions::before {
@ -323,6 +325,12 @@ $icons-map: (
.icon-bold::before {
content: map.get($icons-map, "bold");
}
.icon-boost::before {
content: map.get($icons-map, "boost");
}
.icon-boostcircle::before {
content: map.get($icons-map, "boostcircle");
}
.icon-bot-command::before {
content: map.get($icons-map, "bot-command");
}

Binary file not shown.

Binary file not shown.

View File

@ -22,6 +22,8 @@ export type FontIconName =
| 'avatar-deleted-account'
| 'avatar-saved-messages'
| 'bold'
| 'boost'
| 'boostcircle'
| 'bot-command'
| 'bot-commands-filled'
| 'bots'

View File

@ -363,6 +363,31 @@ export function formatDateAtTime(
return lang('formatDateAtTime', [formattedDate, time]);
}
export function formatDateInFuture(
lang: LangFn,
currentTime: number,
datetime: number,
) {
const diff = Math.ceil(datetime - currentTime);
if (diff < 0) {
return lang('RightNow');
}
if (diff < 60) {
return lang('Seconds', diff);
}
if (diff < 60 * 60) {
return lang('Minutes', Math.ceil(diff / 60));
}
if (diff < 60 * 60 * 24) {
return lang('Hours', Math.ceil(diff / (60 * 60)));
}
return lang('Days', Math.ceil(diff / (60 * 60 * 24)));
}
function isValidDate(day: number, month: number, year = 2021): boolean {
if (month > (MAX_MONTH_IN_YEAR - 1) || day > MAX_DAY_IN_MONTH) {
return false;

View File

@ -7,7 +7,7 @@ import { IS_SAFARI } from './windowEnvironment';
type DeepLinkMethod = 'resolve' | 'login' | 'passport' | 'settings' | 'join' | 'addstickers' | 'addemoji' |
'setlanguage' | 'addtheme' | 'confirmphone' | 'socks' | 'proxy' | 'privatepost' | 'bg' | 'share' | 'msg' | 'msg_url' |
'invoice' | 'addlist';
'invoice' | 'addlist' | 'boost';
export const processDeepLink = (url: string) => {
const {
@ -28,6 +28,7 @@ export const processDeepLink = (url: string) => {
openChatWithDraft,
checkChatlistInvite,
openStoryViewerByUsername,
processBoostParameters,
} = getActions();
// Safari thinks the path in tg://path links is hostname for some reason
@ -43,6 +44,7 @@ export const processDeepLink = (url: string) => {
const hasStartAttach = params.hasOwnProperty('startattach');
const hasStartApp = params.hasOwnProperty('startapp');
const hasBoost = params.hasOwnProperty('boost');
const choose = parseChooseParameter(params.choose);
const threadId = Number(thread) || Number(topic) || undefined;
@ -64,6 +66,8 @@ export const processDeepLink = (url: string) => {
username: domain,
inviteHash: voicechat || livestream,
});
} else if (hasBoost) {
processBoostParameters({ usernameOrId: domain });
} else if (phone) {
openChatByPhoneNumber({ phoneNumber: phone, startAttach: startattach, attach });
} else if (story) {
@ -87,6 +91,13 @@ export const processDeepLink = (url: string) => {
post, channel,
} = params;
const hasBoost = params.hasOwnProperty('boost');
if (hasBoost) {
processBoostParameters({ usernameOrId: channel, isPrivate: true });
return;
}
focusMessage({
chatId: `-${channel}`,
messageId: Number(post),
@ -138,6 +149,14 @@ export const processDeepLink = (url: string) => {
openInvoice({ slug });
break;
}
case 'boost': {
const { channel, domain } = params;
const isPrivate = Boolean(channel);
processBoostParameters({ usernameOrId: channel || domain, isPrivate });
break;
}
default:
// Unsupported deeplink