Introduce Boosts (#3909)
This commit is contained in:
parent
8fc3df855d
commit
b254b08b2c
@ -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),
|
||||
};
|
||||
|
||||
@ -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) }),
|
||||
};
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
1
src/assets/font-icons/boost.svg
Normal file
1
src/assets/font-icons/boost.svg
Normal 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 |
1
src/assets/font-icons/boostcircle.svg
Normal file
1
src/assets/font-icons/boostcircle.svg
Normal 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 |
@ -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';
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -16,6 +16,10 @@
|
||||
|
||||
max-width: calc(50% - 0.5rem);
|
||||
|
||||
&.standalone {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&.minimized {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
123
src/components/common/PremiumProgress.module.scss
Normal file
123
src/components/common/PremiumProgress.module.scss
Normal 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;
|
||||
}
|
||||
79
src/components/common/PremiumProgress.tsx
Normal file
79
src/components/common/PremiumProgress.tsx
Normal 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);
|
||||
24
src/components/common/helpers/boostInfo.ts
Normal file
24
src/components/common/helpers/boostInfo.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@ -229,7 +229,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
)}
|
||||
{globalSearchChatId && (
|
||||
<PickerSelectedItem
|
||||
chatOrUserId={globalSearchChatId}
|
||||
peerId={globalSearchChatId}
|
||||
onClick={setGlobalSearchChatId}
|
||||
canClose
|
||||
clickArg={CLEAR_CHAT_SEARCH_PARAM}
|
||||
|
||||
@ -232,7 +232,7 @@ const ChatResults: FC<OwnProps & StateProps> = ({
|
||||
>
|
||||
{localResults.map((id) => (
|
||||
<PickerSelectedItem
|
||||
chatOrUserId={id}
|
||||
peerId={id}
|
||||
onClick={handlePickerItemClick}
|
||||
clickArg={id}
|
||||
/>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
18
src/components/modals/boost/BoostModal.async.tsx
Normal file
18
src/components/modals/boost/BoostModal.async.tsx
Normal 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;
|
||||
69
src/components/modals/boost/BoostModal.module.scss
Normal file
69
src/components/modals/boost/BoostModal.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
311
src/components/modals/boost/BoostModal.tsx
Normal file
311
src/components/modals/boost/BoostModal.tsx
Normal 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));
|
||||
@ -359,7 +359,8 @@
|
||||
.Spinner {
|
||||
position: absolute;
|
||||
right: 0.875rem;
|
||||
top: 0.875rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
--spinner-size: 1.8125rem;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
20
src/lib/gramjs/tl/api.d.ts
vendored
20
src/lib/gramjs/tl/api.d.ts
vendored
@ -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;
|
||||
};
|
||||
|
||||
@ -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;`;
|
||||
|
||||
@ -314,5 +314,9 @@
|
||||
"stories.sendReaction",
|
||||
"stories.getPeerMaxIDs",
|
||||
"stories.togglePeerStoriesHidden",
|
||||
"stories.getPeerStories"
|
||||
"stories.getPeerStories",
|
||||
"stories.getBoostsStatus",
|
||||
"stories.getBoostersList",
|
||||
"stories.canApplyBoost",
|
||||
"stories.applyBoost"
|
||||
]
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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.
@ -22,6 +22,8 @@ export type FontIconName =
|
||||
| 'avatar-deleted-account'
|
||||
| 'avatar-saved-messages'
|
||||
| 'bold'
|
||||
| 'boost'
|
||||
| 'boostcircle'
|
||||
| 'bot-command'
|
||||
| 'bot-commands-filled'
|
||||
| 'bots'
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user