Chat List: Show suggestions (#6521)
This commit is contained in:
parent
ee78ee06b8
commit
f0df7a01e9
@ -7,7 +7,9 @@ import type {
|
||||
ApiCountry,
|
||||
ApiLanguage,
|
||||
ApiOldLangString,
|
||||
ApiPendingSuggestion,
|
||||
ApiPrivacyKey,
|
||||
ApiPromoData,
|
||||
ApiRestrictionReason,
|
||||
ApiSession,
|
||||
ApiTimezone,
|
||||
@ -22,6 +24,7 @@ import {
|
||||
} from '../../../util/iteratees';
|
||||
import { toJSNumber } from '../../../util/numbers';
|
||||
import { addUserToLocalDb } from '../helpers/localDb';
|
||||
import { buildApiFormattedText } from './common';
|
||||
import { omitVirtualClassFields } from './helpers';
|
||||
import { buildApiDocument, buildMessageTextContent } from './messageContent';
|
||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
|
||||
@ -210,6 +213,30 @@ export function buildApiConfig(config: GramJs.Config): ApiConfig {
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApiPromoData(promoData: GramJs.help.PromoData): ApiPromoData {
|
||||
const {
|
||||
expires, pendingSuggestions, dismissedSuggestions, customPendingSuggestion,
|
||||
} = promoData;
|
||||
return {
|
||||
expires,
|
||||
pendingSuggestions,
|
||||
dismissedSuggestions,
|
||||
customPendingSuggestion: customPendingSuggestion ? buildApiPendingSuggestion(customPendingSuggestion) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApiPendingSuggestion(pendingSuggestion: GramJs.TypePendingSuggestion): ApiPendingSuggestion {
|
||||
const {
|
||||
suggestion, title, description, url,
|
||||
} = pendingSuggestion;
|
||||
return {
|
||||
suggestion,
|
||||
title: buildApiFormattedText(title),
|
||||
description: buildApiFormattedText(description),
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
export function oldBuildLangPack(mtpLangPack: GramJs.LangPackDifference) {
|
||||
return mtpLangPack.strings.reduce<Record<string, ApiOldLangString | undefined>>((acc, mtpString) => {
|
||||
acc[mtpString.key] = oldBuildLangPackString(mtpString);
|
||||
|
||||
@ -46,3 +46,5 @@ export * from './fragment';
|
||||
export * from './stars';
|
||||
|
||||
export * from './forum';
|
||||
|
||||
export * from './misc';
|
||||
|
||||
37
src/api/gramjs/methods/misc.ts
Normal file
37
src/api/gramjs/methods/misc.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import type { ApiAppConfig, ApiConfig, ApiPromoData } from '../../types';
|
||||
|
||||
import { buildAppConfig } from '../apiBuilders/appConfig';
|
||||
import { buildApiConfig, buildApiPromoData } from '../apiBuilders/misc';
|
||||
import { DEFAULT_PRIMITIVES } from '../gramjsBuilders';
|
||||
import { invokeRequest } from './client';
|
||||
|
||||
export async function fetchAppConfig({ hash }: { hash?: number }): Promise<ApiAppConfig | undefined> {
|
||||
const result = await invokeRequest(new GramJs.help.GetAppConfig({ hash: hash ?? DEFAULT_PRIMITIVES.INT }));
|
||||
if (!result || result instanceof GramJs.help.AppConfigNotModified) return undefined;
|
||||
|
||||
const { config, hash: resultHash } = result;
|
||||
return buildAppConfig(config, resultHash);
|
||||
}
|
||||
|
||||
export async function fetchConfig(): Promise<ApiConfig | undefined> {
|
||||
const result = await invokeRequest(new GramJs.help.GetConfig());
|
||||
if (!result) return undefined;
|
||||
|
||||
return buildApiConfig(result);
|
||||
}
|
||||
|
||||
export async function fetchPromoData(): Promise<ApiPromoData | undefined> {
|
||||
const result = await invokeRequest(new GramJs.help.GetPromoData());
|
||||
if (!result || result instanceof GramJs.help.PromoDataEmpty) return undefined;
|
||||
|
||||
return buildApiPromoData(result);
|
||||
}
|
||||
|
||||
export async function dismissSuggestion(suggestion: string): Promise<void> {
|
||||
await invokeRequest(new GramJs.help.DismissSuggestion({
|
||||
peer: new GramJs.InputPeerEmpty(),
|
||||
suggestion,
|
||||
}));
|
||||
}
|
||||
@ -3,8 +3,7 @@ import { RPCError } from '../../../lib/gramjs/errors';
|
||||
|
||||
import type { LANG_PACKS } from '../../../config';
|
||||
import type {
|
||||
ApiAppConfig,
|
||||
ApiConfig,
|
||||
ApiBirthday,
|
||||
ApiDisallowedGiftsSettings,
|
||||
ApiInputPrivacyRules,
|
||||
ApiLanguage,
|
||||
@ -24,11 +23,9 @@ import {
|
||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||
import { toJSNumber } from '../../../util/numbers';
|
||||
import { BLOCKED_LIST_LIMIT } from '../../../limits';
|
||||
import { buildAppConfig } from '../apiBuilders/appConfig';
|
||||
import { buildApiPhoto, buildPrivacyRules } from '../apiBuilders/common';
|
||||
import { buildApiDisallowedGiftsSettings } from '../apiBuilders/gifts';
|
||||
import {
|
||||
buildApiConfig,
|
||||
buildApiCountryList,
|
||||
buildApiLanguage,
|
||||
buildApiSession,
|
||||
@ -104,6 +101,18 @@ export function updateUsername(username: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export function updateBirthday(birthday?: ApiBirthday) {
|
||||
return invokeRequest(new GramJs.account.UpdateBirthday({
|
||||
birthday: birthday ? new GramJs.Birthday({
|
||||
day: birthday.day,
|
||||
month: birthday.month,
|
||||
year: birthday.year,
|
||||
}) : undefined,
|
||||
}), {
|
||||
shouldReturnTrue: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateProfilePhoto(photo?: ApiPhoto, isFallback?: boolean) {
|
||||
const photoId = photo && buildInputPhoto(photo);
|
||||
const result = await invokeRequest(new GramJs.photos.UpdateProfilePhoto({
|
||||
@ -602,21 +611,6 @@ export function updateContentSettings(isEnabled: boolean) {
|
||||
}));
|
||||
}
|
||||
|
||||
export async function fetchAppConfig(hash?: number): Promise<ApiAppConfig | undefined> {
|
||||
const result = await invokeRequest(new GramJs.help.GetAppConfig({ hash: hash ?? DEFAULT_PRIMITIVES.INT }));
|
||||
if (!result || result instanceof GramJs.help.AppConfigNotModified) return undefined;
|
||||
|
||||
const { config, hash: resultHash } = result;
|
||||
return buildAppConfig(config, resultHash);
|
||||
}
|
||||
|
||||
export async function fetchConfig(): Promise<ApiConfig | undefined> {
|
||||
const result = await invokeRequest(new GramJs.help.GetConfig());
|
||||
if (!result) return undefined;
|
||||
|
||||
return buildApiConfig(result);
|
||||
}
|
||||
|
||||
export async function fetchPeerColors(hash?: number) {
|
||||
const result = await invokeRequest(new GramJs.help.GetPeerColors({
|
||||
hash: hash ?? DEFAULT_PRIMITIVES.INT,
|
||||
|
||||
@ -3,7 +3,7 @@ import type { TeactNode } from '../../lib/teact/teact';
|
||||
import type { CallbackAction } from '../../global/types';
|
||||
import type { IconName } from '../../types/icons';
|
||||
import type { RegularLangFnParameters } from '../../util/localization';
|
||||
import type { ApiDocument, ApiPhoto, ApiReaction } from './messages';
|
||||
import type { ApiDocument, ApiFormattedText, ApiPhoto, ApiReaction } from './messages';
|
||||
import type { ApiPremiumSection } from './payments';
|
||||
import type { ApiBotVerification } from './peers';
|
||||
import type { ApiStarsSubscriptionPricing } from './stars';
|
||||
@ -290,6 +290,20 @@ export interface ApiConfig {
|
||||
maxForwardedCount: number;
|
||||
}
|
||||
|
||||
export interface ApiPromoData {
|
||||
expires: number;
|
||||
pendingSuggestions: string[];
|
||||
dismissedSuggestions: string[];
|
||||
customPendingSuggestion?: ApiPendingSuggestion;
|
||||
}
|
||||
|
||||
export interface ApiPendingSuggestion {
|
||||
suggestion: string;
|
||||
title: ApiFormattedText;
|
||||
description: ApiFormattedText;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface ApiTimezone {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@ -1443,6 +1443,7 @@
|
||||
"MenuNightMode" = "Night Mode";
|
||||
"AriaMenuEnableNightMode" = "Enable night mode";
|
||||
"AriaMenuDisableNightMode" = "Disable night mode";
|
||||
"AriaSettingsEditProfilePhoto" = "Edit profile photo";
|
||||
"MenuAnimationsSwitch" = "Animations";
|
||||
"MenuTelegramFeatures" = "Telegram Features";
|
||||
"TelegramFeaturesUsername" = "TelegramTips";
|
||||
@ -2405,3 +2406,21 @@
|
||||
"StarGiftUpgradeCostModalTitle" = "Upgrade Cost";
|
||||
"StarGiftUpgradeCostHint" = "Users who upgrade their gifts first get collectibles with shorter numbers.";
|
||||
"StarGiftPriceDecreaseTimer" = "Price decreases in {timer}";
|
||||
"UnconfirmedAuthDeniedTitle" = "New Login Prevented";
|
||||
"UnconfirmedAuthDeniedMessage" = "We have terminated the login attempt from {location}.";
|
||||
"UnconfirmedAuthTitle" = "Someone just got access to your messages!";
|
||||
"UnconfirmedAuthSingle" = "We detected a new login to your account from {location}. Is it you?";
|
||||
"UnconfirmedAuthConfirm" = "Yes, it's me";
|
||||
"UnconfirmedAuthDeny" = "No, it’s not me!";
|
||||
"UnconfirmedAuthLocationRegion" = "{deviceModel}, {region}, {country}";
|
||||
"UnconfirmedAuthLocationCountry" = "{deviceModel}, {country}";
|
||||
"SuggestionBirthdaySetupTitle" = "Add your birthday! 🎂";
|
||||
"SuggestionBirthdaySetupMessage" = "Let your contacts know when you’re celebrating";
|
||||
"BirthdaySetupTitle" = "Date of Birth";
|
||||
"BirthdayInputDay" = "Day";
|
||||
"BirthdayInputMonth" = "Month";
|
||||
"BirthdayInputYear" = "Year";
|
||||
"BirthdayRemove" = "Remove from Profile";
|
||||
"BirthdayPrivacySuggestion" = "Choose who can see your birthday in {link}";
|
||||
"BirthdayPrivacySuggestionLink" = "Settings >";
|
||||
"SettingsBirthday" = "Birthday";
|
||||
|
||||
BIN
src/assets/tgs/settings/DuckCake.tgs
Normal file
BIN
src/assets/tgs/settings/DuckCake.tgs
Normal file
Binary file not shown.
@ -27,6 +27,7 @@ export { default as DeleteAccountModal } from '../components/modals/deleteAccoun
|
||||
export { default as AgeVerificationModal } from '../components/modals/ageVerification/AgeVerificationModal';
|
||||
export { default as ChatlistModal } from '../components/modals/chatlist/ChatlistModal';
|
||||
export { default as ChatInviteModal } from '../components/modals/chatInvite/ChatInviteModal';
|
||||
export { default as BirthdaySetupModal } from '../components/modals/birthday/BirthdaySetupModal';
|
||||
|
||||
export { default as AboutAdsModal } from '../components/modals/aboutAds/AboutAdsModal';
|
||||
export { default as AboutMonetizationModal } from '../components/common/AboutMonetizationModal';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
@use "../../../styles/mixins";
|
||||
|
||||
.GroupCallTopPane {
|
||||
@include mixins.header-pane;
|
||||
@include mixins.middle-header-pane;
|
||||
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@ import Search from '../../../assets/tgs/Search.tgs';
|
||||
import SearchingDuck from '../../../assets/tgs/SearchingDuck.tgs';
|
||||
import Congratulations from '../../../assets/tgs/settings/Congratulations.tgs';
|
||||
import DiscussionGroups from '../../../assets/tgs/settings/DiscussionGroupsDucks.tgs';
|
||||
import DuckCake from '../../../assets/tgs/settings/DuckCake.tgs';
|
||||
import Experimental from '../../../assets/tgs/settings/Experimental.tgs';
|
||||
import FoldersAll from '../../../assets/tgs/settings/FoldersAll.tgs';
|
||||
import FoldersNew from '../../../assets/tgs/settings/FoldersNew.tgs';
|
||||
@ -81,4 +82,5 @@ export const LOCAL_TGS_URLS = {
|
||||
Diamond,
|
||||
Search,
|
||||
DuckNothingFound,
|
||||
DuckCake,
|
||||
};
|
||||
|
||||
@ -40,34 +40,34 @@ export default function renderText(
|
||||
return compact(filters.reduce((text, filter) => {
|
||||
switch (filter) {
|
||||
case 'escape_html':
|
||||
return escapeHtml(text);
|
||||
return escapeHtml(text, params?.markdownPostProcessor);
|
||||
|
||||
case 'hq_emoji':
|
||||
EMOJI_REGEX.lastIndex = 0;
|
||||
return replaceEmojis(text, 'big', 'jsx');
|
||||
return replaceEmojis(text, 'big', 'jsx', params?.markdownPostProcessor);
|
||||
|
||||
case 'emoji':
|
||||
EMOJI_REGEX.lastIndex = 0;
|
||||
return replaceEmojis(text, 'small', 'jsx');
|
||||
return replaceEmojis(text, 'small', 'jsx', params?.markdownPostProcessor);
|
||||
|
||||
case 'emoji_html':
|
||||
EMOJI_REGEX.lastIndex = 0;
|
||||
return replaceEmojis(text, 'small', 'html');
|
||||
|
||||
case 'br':
|
||||
return addLineBreaks(text, 'jsx');
|
||||
return addLineBreaks(text, 'jsx', params?.markdownPostProcessor);
|
||||
|
||||
case 'br_html':
|
||||
return addLineBreaks(text, 'html');
|
||||
|
||||
case 'highlight':
|
||||
return addHighlight(text, params!.highlight);
|
||||
return addHighlight(text, params!.highlight, params?.markdownPostProcessor);
|
||||
|
||||
case 'links':
|
||||
return addLinks(text);
|
||||
return addLinks(text, undefined, params?.markdownPostProcessor);
|
||||
|
||||
case 'tg_links':
|
||||
return addLinks(text, true);
|
||||
return addLinks(text, true, params?.markdownPostProcessor);
|
||||
|
||||
case 'simple_markdown':
|
||||
return replaceSimpleMarkdown(text, 'jsx', params?.markdownPostProcessor);
|
||||
@ -80,7 +80,9 @@ export default function renderText(
|
||||
}, [part] as TextPart[]));
|
||||
}
|
||||
|
||||
function escapeHtml(textParts: TextPart[]): TextPart[] {
|
||||
function escapeHtml(textParts: TextPart[], postProcessor?: (part: string) => TeactNode): TextPart[] {
|
||||
const postProcess = postProcessor || ((part: string) => part);
|
||||
|
||||
const divEl = document.createElement('div');
|
||||
return textParts.reduce((result: TextPart[], part) => {
|
||||
if (typeof part !== 'string') {
|
||||
@ -89,15 +91,20 @@ function escapeHtml(textParts: TextPart[]): TextPart[] {
|
||||
}
|
||||
|
||||
divEl.innerText = part;
|
||||
result.push(divEl.innerHTML);
|
||||
result.push(postProcess(divEl.innerHTML));
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function replaceEmojis(textParts: TextPart[], size: 'big' | 'small', type: 'jsx' | 'html'): TextPart[] {
|
||||
function replaceEmojis(
|
||||
textParts: TextPart[], size: 'big' | 'small', type: 'jsx' | 'html',
|
||||
postProcessor?: (part: string) => TeactNode,
|
||||
): TextPart[] {
|
||||
const postProcess = postProcessor || ((part: string) => part);
|
||||
|
||||
if (IS_EMOJI_SUPPORTED) {
|
||||
return textParts;
|
||||
return textParts.map((part) => typeof part === 'string' ? postProcess(part) : part);
|
||||
}
|
||||
|
||||
return textParts.reduce((result: TextPart[], part: TextPart) => {
|
||||
@ -109,12 +116,12 @@ function replaceEmojis(textParts: TextPart[], size: 'big' | 'small', type: 'jsx'
|
||||
part = fixNonStandardEmoji(part);
|
||||
const parts = part.split(EMOJI_REGEX);
|
||||
const emojis: string[] = part.match(EMOJI_REGEX) || [];
|
||||
result.push(parts[0]);
|
||||
result.push(postProcess(parts[0]));
|
||||
|
||||
return emojis.reduce((emojiResult: TextPart[], emoji, i) => {
|
||||
const code = nativeToUnifiedExtendedWithCache(emoji);
|
||||
if (!code) {
|
||||
emojiResult.push(emoji);
|
||||
emojiResult.push(postProcess(emoji));
|
||||
} else {
|
||||
const src = `./img-apple-${size === 'big' ? '160' : '64'}/${code}.png`;
|
||||
const className = buildClassName(
|
||||
@ -150,7 +157,7 @@ function replaceEmojis(textParts: TextPart[], size: 'big' | 'small', type: 'jsx'
|
||||
|
||||
const index = i * 2 + 2;
|
||||
if (parts[index]) {
|
||||
emojiResult.push(parts[index]);
|
||||
emojiResult.push(postProcess(parts[index]));
|
||||
}
|
||||
|
||||
return emojiResult;
|
||||
@ -158,7 +165,11 @@ function replaceEmojis(textParts: TextPart[], size: 'big' | 'small', type: 'jsx'
|
||||
}, [] as TextPart[]);
|
||||
}
|
||||
|
||||
function addLineBreaks(textParts: TextPart[], type: 'jsx' | 'html'): TextPart[] {
|
||||
function addLineBreaks(
|
||||
textParts: TextPart[], type: 'jsx' | 'html', postProcessor?: (part: string) => TeactNode,
|
||||
): TextPart[] {
|
||||
const postProcess = postProcessor || ((part: string) => part);
|
||||
|
||||
return textParts.reduce((result: TextPart[], part) => {
|
||||
if (typeof part !== 'string') {
|
||||
result.push(part);
|
||||
@ -169,9 +180,10 @@ function addLineBreaks(textParts: TextPart[], type: 'jsx' | 'html'): TextPart[]
|
||||
.split(/\r\n|\r|\n/g)
|
||||
.reduce((parts: TextPart[], line: string, i, source) => {
|
||||
// This adds non-breaking space if line was indented with spaces, to preserve the indentation
|
||||
const trimmedLine = line.trimLeft();
|
||||
const trimmedLine = line.trimStart();
|
||||
const indentLength = line.length - trimmedLine.length;
|
||||
parts.push(String.fromCharCode(160).repeat(indentLength) + trimmedLine);
|
||||
parts.push(String.fromCharCode(160).repeat(indentLength));
|
||||
parts.push(postProcess(trimmedLine));
|
||||
|
||||
if (i !== source.length - 1) {
|
||||
parts.push(
|
||||
@ -186,7 +198,12 @@ function addLineBreaks(textParts: TextPart[], type: 'jsx' | 'html'): TextPart[]
|
||||
}, []);
|
||||
}
|
||||
|
||||
function addHighlight(textParts: TextPart[], highlight: string | undefined): TextPart[] {
|
||||
function addHighlight(
|
||||
textParts: TextPart[], highlight: string | undefined,
|
||||
postProcessor?: (part: string) => TeactNode,
|
||||
): TextPart[] {
|
||||
const postProcess = postProcessor || ((part: string) => part);
|
||||
|
||||
return textParts.reduce<TextPart[]>((result, part) => {
|
||||
if (typeof part !== 'string' || !highlight) {
|
||||
result.push(part);
|
||||
@ -196,25 +213,29 @@ function addHighlight(textParts: TextPart[], highlight: string | undefined): Tex
|
||||
const lowerCaseText = part.toLowerCase();
|
||||
const queryPosition = lowerCaseText.indexOf(highlight.toLowerCase());
|
||||
if (queryPosition < 0) {
|
||||
result.push(part);
|
||||
result.push(postProcess(part));
|
||||
return result;
|
||||
}
|
||||
|
||||
const newParts: TextPart[] = [];
|
||||
newParts.push(part.substring(0, queryPosition));
|
||||
newParts.push(postProcess(part.substring(0, queryPosition)));
|
||||
newParts.push(
|
||||
<span className="matching-text-highlight">
|
||||
{part.substring(queryPosition, queryPosition + highlight.length)}
|
||||
{postProcess(part.substring(queryPosition, queryPosition + highlight.length))}
|
||||
</span>,
|
||||
);
|
||||
newParts.push(part.substring(queryPosition + highlight.length));
|
||||
newParts.push(postProcess(part.substring(queryPosition + highlight.length)));
|
||||
return [...result, ...newParts];
|
||||
}, []);
|
||||
}
|
||||
|
||||
const RE_LINK = new RegExp(`${RE_LINK_TEMPLATE}|${RE_MENTION_TEMPLATE}`, 'ig');
|
||||
|
||||
function addLinks(textParts: TextPart[], allowOnlyTgLinks?: boolean): TextPart[] {
|
||||
function addLinks(
|
||||
textParts: TextPart[], allowOnlyTgLinks?: boolean, postProcessor?: (part: string) => TeactNode,
|
||||
): TextPart[] {
|
||||
const postProcess = postProcessor || ((part: string) => part);
|
||||
|
||||
return textParts.reduce<TextPart[]>((result, part) => {
|
||||
if (typeof part !== 'string') {
|
||||
result.push(part);
|
||||
@ -223,7 +244,7 @@ function addLinks(textParts: TextPart[], allowOnlyTgLinks?: boolean): TextPart[]
|
||||
|
||||
const links = part.match(RE_LINK);
|
||||
if (!links || !links.length) {
|
||||
result.push(part);
|
||||
result.push(postProcess(part));
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -233,11 +254,11 @@ function addLinks(textParts: TextPart[], allowOnlyTgLinks?: boolean): TextPart[]
|
||||
let lastIndex = 0;
|
||||
while (nextLink) {
|
||||
const index = part.indexOf(nextLink, lastIndex);
|
||||
content.push(part.substring(lastIndex, index));
|
||||
content.push(postProcess(part.substring(lastIndex, index)));
|
||||
if (nextLink.startsWith('@')) {
|
||||
content.push(
|
||||
<MentionLink username={nextLink}>
|
||||
{nextLink}
|
||||
{postProcess(nextLink)}
|
||||
</MentionLink>,
|
||||
);
|
||||
} else {
|
||||
@ -250,13 +271,13 @@ function addLinks(textParts: TextPart[], allowOnlyTgLinks?: boolean): TextPart[]
|
||||
<SafeLink text={nextLink} url={nextLink} />,
|
||||
);
|
||||
} else {
|
||||
content.push(nextLink);
|
||||
content.push(postProcess(nextLink));
|
||||
}
|
||||
}
|
||||
lastIndex = index + nextLink.length;
|
||||
nextLink = links.shift();
|
||||
}
|
||||
content.push(part.substring(lastIndex));
|
||||
content.push(postProcess(part.substring(lastIndex)));
|
||||
|
||||
return [...result, ...content];
|
||||
}, []);
|
||||
|
||||
@ -401,6 +401,7 @@ const ChatExtra = ({
|
||||
<Chat
|
||||
chatId={personalChannel.id}
|
||||
orderDiff={0}
|
||||
shiftDiff={0}
|
||||
animationType={ChatAnimationTypes.None}
|
||||
isPreview
|
||||
previewMessageId={personalChannelMessageId}
|
||||
|
||||
@ -66,13 +66,11 @@ const UserBirthday = ({
|
||||
age,
|
||||
} = useMemo(() => {
|
||||
const today = new Date();
|
||||
const date = new Date();
|
||||
if (birthday.year) {
|
||||
date.setFullYear(birthday.year);
|
||||
}
|
||||
date.setMonth(birthday.month - 1);
|
||||
date.setDate(birthday.day);
|
||||
date.setHours(0, 0, 0, 0);
|
||||
const date = new Date(
|
||||
birthday.year || 2024, // Use leap year as fallback
|
||||
birthday.month - 1,
|
||||
birthday.day,
|
||||
);
|
||||
|
||||
const formatted = formatDateToString(date, lang.code, true, 'long');
|
||||
const isBirthdayToday = date.getDate() === today.getDate() && date.getMonth() === today.getMonth();
|
||||
|
||||
@ -1,11 +1,17 @@
|
||||
.root {
|
||||
--background-color: var(--color-background);
|
||||
|
||||
transition: transform var(--chat-transform-transition);
|
||||
|
||||
:global(.ListItem-button) {
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.noAnimation {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.minimized {
|
||||
margin: -0.5rem -0.5rem 0 -0.5rem !important;
|
||||
background-color: var(--color-background-secondary);
|
||||
@ -14,7 +20,7 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.no-margin-top {
|
||||
&.noMarginTop {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
@ -1,19 +1,24 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import { memo, useCallback, useMemo } from '../../../lib/teact/teact';
|
||||
import { memo, useCallback, useMemo, useRef } from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal } from '../../../global';
|
||||
|
||||
import type { GlobalState } from '../../../global/types';
|
||||
import type { CustomPeer } from '../../../types';
|
||||
|
||||
import { ARCHIVED_FOLDER_ID } from '../../../config';
|
||||
import { ANIMATION_LEVEL_MIN, ARCHIVED_FOLDER_ID } from '../../../config';
|
||||
import { getChatTitle } from '../../../global/helpers';
|
||||
import { selectAnimationLevel } from '../../../global/selectors/sharedState';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import buildStyle from '../../../util/buildStyle';
|
||||
import { waitForTransitionEnd } from '../../../util/cssAnimationEndListeners';
|
||||
import { compact } from '../../../util/iteratees';
|
||||
import { formatIntegerCompact } from '../../../util/textFormat';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
import { ChatAnimationTypes } from './hooks';
|
||||
|
||||
import useSelector from '../../../hooks/data/useSelector';
|
||||
import { useFolderManagerForOrderedIds, useFolderManagerForUnreadCounters } from '../../../hooks/useFolderManager';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useSyncEffect from '../../../hooks/useSyncEffect';
|
||||
|
||||
import Avatar from '../../common/Avatar';
|
||||
import Icon from '../../common/icons/Icon';
|
||||
@ -24,9 +29,11 @@ import styles from './Archive.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
archiveSettings: GlobalState['archiveSettings'];
|
||||
isFoldersSidebarShown?: boolean;
|
||||
offsetTop?: number;
|
||||
animationType: ChatAnimationTypes;
|
||||
onDragEnter?: NoneToVoidFunction;
|
||||
onClick?: NoneToVoidFunction;
|
||||
isFoldersSidebarShown?: boolean;
|
||||
};
|
||||
|
||||
const PREVIEW_SLICE = 5;
|
||||
@ -37,15 +44,50 @@ const ARCHIVE_CUSTOM_PEER: CustomPeer = {
|
||||
customPeerAvatarColor: '#9EAAB5',
|
||||
};
|
||||
|
||||
const Archive: FC<OwnProps> = ({
|
||||
const ANIMATION_RESET_DELAY = 200;
|
||||
|
||||
const Archive = ({
|
||||
archiveSettings,
|
||||
isFoldersSidebarShown,
|
||||
offsetTop,
|
||||
animationType,
|
||||
onDragEnter,
|
||||
onClick,
|
||||
isFoldersSidebarShown,
|
||||
}) => {
|
||||
}: OwnProps) => {
|
||||
const { updateArchiveSettings } = getActions();
|
||||
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
|
||||
const animationLevel = useSelector(selectAnimationLevel);
|
||||
const shouldAnimateRef = useRef(animationLevel !== ANIMATION_LEVEL_MIN);
|
||||
const lang = useLang();
|
||||
|
||||
useSyncEffect(() => {
|
||||
if (animationLevel === ANIMATION_LEVEL_MIN) {
|
||||
shouldAnimateRef.current = false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (animationType !== ChatAnimationTypes.None) {
|
||||
shouldAnimateRef.current = true;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Keep animation alive slightly longer to avoid jumps
|
||||
const timeout = setTimeout(() => {
|
||||
shouldAnimateRef.current = false;
|
||||
}, ANIMATION_RESET_DELAY);
|
||||
|
||||
const element = ref.current;
|
||||
if (element) {
|
||||
waitForTransitionEnd(element, () => {
|
||||
shouldAnimateRef.current = false;
|
||||
}, 'transform');
|
||||
}
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [animationType, animationLevel]);
|
||||
|
||||
const orderedChatIds = useFolderManagerForOrderedIds(ARCHIVED_FOLDER_ID);
|
||||
const unreadCounters = useFolderManagerForUnreadCounters();
|
||||
const archiveUnreadCount = unreadCounters[ARCHIVED_FOLDER_ID]?.chatsCount;
|
||||
@ -155,15 +197,19 @@ const Archive: FC<OwnProps> = ({
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
onDragEnter={handleDragEnter}
|
||||
className={buildClassName(
|
||||
styles.root,
|
||||
archiveSettings.isMinimized && styles.minimized,
|
||||
isFoldersSidebarShown && archiveSettings.isMinimized && styles.noMarginTop,
|
||||
!shouldAnimateRef.current && styles.noAnimation,
|
||||
offsetTop && styles.noMarginTop,
|
||||
'chat-item-clickable',
|
||||
'chat-item-archive',
|
||||
)}
|
||||
style={buildStyle(Boolean(offsetTop) && `transform: translateY(${offsetTop}px)`)}
|
||||
buttonClassName={styles.button}
|
||||
contextActions={contextActions}
|
||||
withPortalForMenu
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
|
||||
&.animate-transform {
|
||||
will-change: transform;
|
||||
transition: transform 0.2s ease-out;
|
||||
transition: transform var(--chat-transform-transition);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
|
||||
@ -87,6 +87,7 @@ type OwnProps = {
|
||||
chatId: string;
|
||||
folderId?: number;
|
||||
orderDiff: number;
|
||||
shiftDiff: number;
|
||||
animationType: ChatAnimationTypes;
|
||||
isPinned?: boolean;
|
||||
offsetTop?: number;
|
||||
@ -137,6 +138,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
chatId,
|
||||
folderId,
|
||||
orderDiff,
|
||||
shiftDiff,
|
||||
animationType,
|
||||
isPinned,
|
||||
listedTopicIds,
|
||||
@ -239,6 +241,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
animationType,
|
||||
withInterfaceAnimations,
|
||||
orderDiff,
|
||||
shiftDiff,
|
||||
isSavedDialog,
|
||||
isPreview,
|
||||
onReorderAnimationEnd,
|
||||
|
||||
@ -2,13 +2,13 @@ import type { FC } from '@teact';
|
||||
import { memo, useEffect, useRef } from '@teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiChatFolder, ApiChatlistExportedInvite, ApiSession } from '../../../api/types';
|
||||
import type { ApiChatFolder, ApiChatlistExportedInvite } from '../../../api/types';
|
||||
import type { GlobalState } from '../../../global/types';
|
||||
import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
|
||||
import type { AnimationLevel } from '../../../types';
|
||||
|
||||
import { ALL_FOLDER_ID } from '../../../config';
|
||||
import { selectIsCurrentUserFrozen, selectTabState } from '../../../global/selectors';
|
||||
import { selectTabState } from '../../../global/selectors';
|
||||
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
||||
import { selectSharedSettings } from '../../../global/selectors/sharedState';
|
||||
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
|
||||
@ -51,8 +51,6 @@ type StateProps = {
|
||||
hasArchivedStories?: boolean;
|
||||
archiveSettings: GlobalState['archiveSettings'];
|
||||
isStoryRibbonShown?: boolean;
|
||||
sessions?: Record<string, ApiSession>;
|
||||
isAccountFrozen?: boolean;
|
||||
};
|
||||
|
||||
const SAVED_MESSAGES_HOTKEY = '0';
|
||||
@ -76,8 +74,6 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
||||
hasArchivedStories,
|
||||
archiveSettings,
|
||||
isStoryRibbonShown,
|
||||
sessions,
|
||||
isAccountFrozen,
|
||||
isFoldersSidebarShown,
|
||||
}) => {
|
||||
const {
|
||||
@ -232,8 +228,6 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
||||
isMainList
|
||||
canDisplayArchive={(hasArchivedChats || hasArchivedStories) && !archiveSettings.isHidden}
|
||||
archiveSettings={archiveSettings}
|
||||
sessions={sessions}
|
||||
isAccountFrozen={isAccountFrozen}
|
||||
isFoldersSidebarShown={isFoldersSidebarShown}
|
||||
isStoryRibbonShown={isStoryRibbonShown}
|
||||
withTags
|
||||
@ -294,16 +288,12 @@ export default memo(withGlobal<OwnProps>(
|
||||
archived: archivedStories,
|
||||
},
|
||||
},
|
||||
activeSessions: {
|
||||
byHash: sessions,
|
||||
},
|
||||
currentUserId,
|
||||
archiveSettings,
|
||||
} = global;
|
||||
const { animationLevel } = selectSharedSettings(global);
|
||||
const { shouldSkipHistoryAnimations, activeChatFolder } = selectTabState(global);
|
||||
const { storyViewer: { isRibbonShown: isStoryRibbonShown } } = selectTabState(global);
|
||||
const isAccountFrozen = selectIsCurrentUserFrozen(global);
|
||||
|
||||
return {
|
||||
chatFoldersById,
|
||||
@ -320,8 +310,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
maxChatLists: selectCurrentLimit(global, 'chatlistJoined'),
|
||||
archiveSettings,
|
||||
isStoryRibbonShown,
|
||||
sessions,
|
||||
isAccountFrozen,
|
||||
};
|
||||
},
|
||||
)(ChatFolders));
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import type { FC } from '@teact';
|
||||
import { memo, useEffect, useMemo, useRef, useState } from '@teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { ApiSession } from '../../../api/types';
|
||||
import type { GlobalState } from '../../../global/types';
|
||||
import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
|
||||
import { LeftColumnContent } from '../../../types';
|
||||
@ -13,14 +11,13 @@ import {
|
||||
ARCHIVED_FOLDER_ID,
|
||||
CHAT_HEIGHT_PX,
|
||||
CHAT_LIST_SLICE,
|
||||
FRESH_AUTH_PERIOD,
|
||||
SAVED_FOLDER_ID,
|
||||
} from '../../../config';
|
||||
import { IS_APP, IS_MAC_OS } from '../../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { onDragEnter, onDragLeave } from '../../../util/dragNDropHandlers.ts';
|
||||
import { onDragEnter, onDragLeave } from '../../../util/dragNDropHandlers';
|
||||
import { getOrderKey, getPinnedChatsCount } from '../../../util/folderManager';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import { ARCHIVE_ANIMATION_ID } from './hooks';
|
||||
|
||||
import usePeerStoriesPolling from '../../../hooks/polling/usePeerStoriesPolling';
|
||||
import useTopOverscroll from '../../../hooks/scroll/useTopOverscroll';
|
||||
@ -36,8 +33,7 @@ import Loading from '../../ui/Loading';
|
||||
import Archive from './Archive';
|
||||
import Chat from './Chat';
|
||||
import EmptyFolder from './EmptyFolder';
|
||||
import FrozenAccountNotification from './FrozenAccountNotification';
|
||||
import UnconfirmedSession from './UnconfirmedSession';
|
||||
import ChatListPanes from './panes/ChatListPanes';
|
||||
|
||||
type OwnProps = {
|
||||
className?: string;
|
||||
@ -47,8 +43,6 @@ type OwnProps = {
|
||||
canDisplayArchive?: boolean;
|
||||
archiveSettings?: GlobalState['archiveSettings'];
|
||||
isForumPanelOpen?: boolean;
|
||||
sessions?: Record<string, ApiSession>;
|
||||
isAccountFrozen?: boolean;
|
||||
isMainList?: boolean;
|
||||
withTags?: boolean;
|
||||
isFoldersSidebarShown?: boolean;
|
||||
@ -59,7 +53,7 @@ type OwnProps = {
|
||||
const INTERSECTION_THROTTLE = 200;
|
||||
const RESERVED_HOTKEYS = new Set(['9', '0']);
|
||||
|
||||
const ChatList: FC<OwnProps> = ({
|
||||
const ChatList = ({
|
||||
className,
|
||||
folderType,
|
||||
folderId,
|
||||
@ -67,24 +61,21 @@ const ChatList: FC<OwnProps> = ({
|
||||
isForumPanelOpen,
|
||||
canDisplayArchive,
|
||||
archiveSettings,
|
||||
sessions,
|
||||
isAccountFrozen,
|
||||
isMainList,
|
||||
withTags,
|
||||
isFoldersSidebarShown,
|
||||
isStoryRibbonShown,
|
||||
foldersDispatch,
|
||||
}) => {
|
||||
}: OwnProps) => {
|
||||
const {
|
||||
openChat,
|
||||
openNextChat,
|
||||
closeForumPanel,
|
||||
toggleStoryRibbon,
|
||||
openFrozenAccountModal,
|
||||
openLeftColumnContent,
|
||||
} = getActions();
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const [unconfirmedSessionHeight, setUnconfirmedSessionHeight] = useState(0);
|
||||
const [panesHeight, setPanesHeight] = useState(0);
|
||||
|
||||
const isArchived = folderType === 'archived';
|
||||
const isAllFolder = folderType === 'all';
|
||||
@ -94,7 +85,6 @@ const ChatList: FC<OwnProps> = ({
|
||||
);
|
||||
|
||||
const shouldDisplayArchive = isAllFolder && canDisplayArchive && archiveSettings;
|
||||
const shouldShowFrozenAccountNotification = isAccountFrozen && isAllFolder;
|
||||
|
||||
const orderedIds = useFolderManagerForOrderedIds(resolvedFolderId);
|
||||
usePeerStoriesPolling(orderedIds);
|
||||
@ -102,24 +92,13 @@ const ChatList: FC<OwnProps> = ({
|
||||
const chatsHeight = (orderedIds?.length || 0) * CHAT_HEIGHT_PX;
|
||||
const archiveHeight = shouldDisplayArchive
|
||||
? archiveSettings?.isMinimized ? ARCHIVE_MINIMIZED_HEIGHT : CHAT_HEIGHT_PX : 0;
|
||||
const frozenNotificationHeight = shouldShowFrozenAccountNotification ? 68 : 0;
|
||||
|
||||
const { orderDiffById, getAnimationType, onReorderAnimationEnd: onReorderAnimationEnd } = useOrderDiff(orderedIds);
|
||||
const {
|
||||
orderDiffById, shiftDiff, getAnimationType, onReorderAnimationEnd: onReorderAnimationEnd,
|
||||
} = useOrderDiff(orderedIds, panesHeight);
|
||||
|
||||
const [viewportIds, getMore] = useInfiniteScroll(undefined, orderedIds, undefined, CHAT_LIST_SLICE);
|
||||
|
||||
const shouldShowUnconfirmedSessions = useMemo(() => {
|
||||
const sessionsArray = Object.values(sessions || {});
|
||||
const current = sessionsArray.find((session) => session.isCurrent);
|
||||
if (!current || getServerTime() - current.dateCreated < FRESH_AUTH_PERIOD) return false;
|
||||
|
||||
return !isAccountFrozen && isAllFolder && sessionsArray.some((session) => session.isUnconfirmed);
|
||||
}, [isAllFolder, sessions, isAccountFrozen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldShowUnconfirmedSessions) setUnconfirmedSessionHeight(0);
|
||||
}, [shouldShowUnconfirmedSessions]);
|
||||
|
||||
// Support <Alt>+<Up/Down> to navigate between chats
|
||||
useHotkeys(useMemo(() => (isActive && orderedIds?.length ? {
|
||||
'Alt+ArrowUp': (e: KeyboardEvent) => {
|
||||
@ -178,10 +157,6 @@ const ChatList: FC<OwnProps> = ({
|
||||
closeForumPanel();
|
||||
});
|
||||
|
||||
const handleFrozenAccountNotificationClick = useLastCallback(() => {
|
||||
openFrozenAccountModal();
|
||||
});
|
||||
|
||||
const handleShowStoryRibbon = useLastCallback(() => {
|
||||
toggleStoryRibbon({ isShown: true, isArchived });
|
||||
});
|
||||
@ -217,8 +192,7 @@ const ChatList: FC<OwnProps> = ({
|
||||
|
||||
return viewportIds!.map((id, i) => {
|
||||
const isPinned = viewportOffset + i < pinnedCount;
|
||||
const offsetTop = unconfirmedSessionHeight + archiveHeight + frozenNotificationHeight
|
||||
+ (viewportOffset + i) * CHAT_HEIGHT_PX;
|
||||
const offsetTop = panesHeight + archiveHeight + (viewportOffset + i) * CHAT_HEIGHT_PX;
|
||||
|
||||
return (
|
||||
<Chat
|
||||
@ -230,6 +204,7 @@ const ChatList: FC<OwnProps> = ({
|
||||
isSavedDialog={isSaved}
|
||||
animationType={getAnimationType(id)}
|
||||
orderDiff={orderDiffById[id]}
|
||||
shiftDiff={shiftDiff}
|
||||
onReorderAnimationEnd={onReorderAnimationEnd}
|
||||
offsetTop={offsetTop}
|
||||
observeIntersection={observe}
|
||||
@ -250,28 +225,18 @@ const ChatList: FC<OwnProps> = ({
|
||||
itemSelector=".ListItem:not(.chat-item-archive)"
|
||||
preloadBackwards={CHAT_LIST_SLICE}
|
||||
withAbsolutePositioning
|
||||
maxHeight={chatsHeight + archiveHeight + frozenNotificationHeight + unconfirmedSessionHeight}
|
||||
maxHeight={chatsHeight + archiveHeight + panesHeight}
|
||||
onLoadMore={getMore}
|
||||
>
|
||||
{shouldShowUnconfirmedSessions && (
|
||||
<UnconfirmedSession
|
||||
key="unconfirmed"
|
||||
sessions={sessions!}
|
||||
onHeightChange={setUnconfirmedSessionHeight}
|
||||
/>
|
||||
)}
|
||||
{shouldShowFrozenAccountNotification && (
|
||||
<FrozenAccountNotification
|
||||
key="frozen"
|
||||
onClick={handleFrozenAccountNotificationClick}
|
||||
/>
|
||||
)}
|
||||
{isAllFolder && <ChatListPanes key="panes" onHeightChange={setPanesHeight} />}
|
||||
{shouldDisplayArchive && (
|
||||
<Archive
|
||||
key="archive"
|
||||
archiveSettings={archiveSettings}
|
||||
onClick={handleArchivedClick}
|
||||
onDragEnter={handleArchivedDragEnter}
|
||||
animationType={getAnimationType(ARCHIVE_ANIMATION_ID)}
|
||||
offsetTop={panesHeight}
|
||||
isFoldersSidebarShown={isFoldersSidebarShown}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
import { memo } from '../../../lib/teact/teact';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
import styles from './FrozenAccountNotification.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
const FrozenAccountNotification = ({ onClick }: OwnProps) => {
|
||||
const lang = useLang();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.root}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className={styles.title}>{lang('TitleFrozenAccount')}</div>
|
||||
<div className={styles.subtitle}>{lang('SubtitleFrozenAccount')}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(FrozenAccountNotification);
|
||||
@ -1,71 +0,0 @@
|
||||
import { memo, useMemo, useRef } from '../../../lib/teact/teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { ApiSession } from '../../../api/types';
|
||||
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
import useResizeObserver from '../../../hooks/useResizeObserver';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
|
||||
import styles from './UnconfirmedSession.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
sessions: Record<string, ApiSession>;
|
||||
onHeightChange: (height: number) => void;
|
||||
};
|
||||
|
||||
const UnconfirmedSession = ({ sessions, onHeightChange }: OwnProps) => {
|
||||
const { changeSessionSettings, terminateAuthorization, showNotification } = getActions();
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
const lang = useOldLang();
|
||||
|
||||
useResizeObserver(ref, (entry) => {
|
||||
const height = entry.borderBoxSize?.[0]?.blockSize || entry.contentRect.height;
|
||||
onHeightChange(height);
|
||||
});
|
||||
|
||||
const firstUnconfirmed = useMemo(() => {
|
||||
return Object.values(sessions).sort((a, b) => b.dateCreated - a.dateCreated)
|
||||
.find((session) => session.isUnconfirmed)!;
|
||||
}, [sessions]);
|
||||
|
||||
const locationString = useMemo(() => {
|
||||
return [firstUnconfirmed.deviceModel, firstUnconfirmed.region, firstUnconfirmed.country].filter(Boolean).join(', ');
|
||||
}, [firstUnconfirmed]);
|
||||
|
||||
const handleAccept = useLastCallback(() => {
|
||||
changeSessionSettings({
|
||||
hash: firstUnconfirmed.hash,
|
||||
isConfirmed: true,
|
||||
});
|
||||
});
|
||||
|
||||
const handleReject = useLastCallback(() => {
|
||||
terminateAuthorization({ hash: firstUnconfirmed.hash });
|
||||
showNotification({
|
||||
title: lang('UnconfirmedAuthDeniedTitle', 1),
|
||||
message: lang('UnconfirmedAuthDeniedMessageSingle', locationString),
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.root} ref={ref}>
|
||||
<h2 className={styles.title}>{lang('UnconfirmedAuthTitle')}</h2>
|
||||
<p className={styles.info}>
|
||||
{lang('UnconfirmedAuthSingle', locationString)}
|
||||
</p>
|
||||
<div className={styles.buttons}>
|
||||
<Button fluid isText className={styles.button} onClick={handleAccept}>
|
||||
{lang('UnconfirmedAuthConfirm')}
|
||||
</Button>
|
||||
<Button fluid isText color="danger" onClick={handleReject} className={styles.button}>
|
||||
{lang('UnconfirmedAuthDeny')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(UnconfirmedSession);
|
||||
@ -135,7 +135,7 @@ const ForumPanel = ({
|
||||
return [MAIN_THREAD_ID, ...ids];
|
||||
}, [chat?.isBotForum, topicsInfo]);
|
||||
|
||||
const { orderDiffById, getAnimationType, onReorderAnimationEnd } = useOrderDiff(orderedIds, chat?.id);
|
||||
const { orderDiffById, shiftDiff, getAnimationType, onReorderAnimationEnd } = useOrderDiff(orderedIds, 0, chat?.id);
|
||||
|
||||
const [viewportIds, getMore] = useInfiniteScroll(() => {
|
||||
if (!chat) return;
|
||||
@ -223,6 +223,7 @@ const ForumPanel = ({
|
||||
observeIntersection={observe}
|
||||
animationType={getAnimationType(id)}
|
||||
orderDiff={orderDiffById[id]}
|
||||
shiftDiff={shiftDiff}
|
||||
onReorderAnimationEnd={onReorderAnimationEnd}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { FC } from '../../../../lib/teact/teact';
|
||||
import { memo } from '../../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../../global';
|
||||
|
||||
@ -55,6 +54,7 @@ type OwnProps = {
|
||||
isSelected: boolean;
|
||||
style: string;
|
||||
observeIntersection?: ObserveFn;
|
||||
shiftDiff: number;
|
||||
orderDiff: number;
|
||||
animationType: ChatAnimationTypes;
|
||||
onReorderAnimationEnd?: NoneToVoidFunction;
|
||||
@ -76,7 +76,7 @@ type StateProps = {
|
||||
topics?: Record<number, ApiTopic>;
|
||||
};
|
||||
|
||||
const Topic: FC<OwnProps & StateProps> = ({
|
||||
const Topic = ({
|
||||
topic,
|
||||
isSelected,
|
||||
chatId,
|
||||
@ -93,12 +93,13 @@ const Topic: FC<OwnProps & StateProps> = ({
|
||||
animationType,
|
||||
withInterfaceAnimations,
|
||||
orderDiff,
|
||||
shiftDiff,
|
||||
typingStatus,
|
||||
draft,
|
||||
wasTopicOpened,
|
||||
topics,
|
||||
onReorderAnimationEnd,
|
||||
}) => {
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
openThread,
|
||||
deleteTopic,
|
||||
@ -154,6 +155,7 @@ const Topic: FC<OwnProps & StateProps> = ({
|
||||
animationType,
|
||||
withInterfaceAnimations,
|
||||
orderDiff,
|
||||
shiftDiff,
|
||||
onReorderAnimationEnd,
|
||||
});
|
||||
|
||||
|
||||
@ -1,20 +1,34 @@
|
||||
import { useMemo } from '../../../../lib/teact/teact';
|
||||
|
||||
export enum ChatAnimationTypes {
|
||||
Shift,
|
||||
Move,
|
||||
Opacity,
|
||||
None,
|
||||
}
|
||||
|
||||
export function useChatAnimationType<T extends number | string>(orderDiffById: Record<T, number>) {
|
||||
export const ARCHIVE_ANIMATION_ID = 'archive';
|
||||
|
||||
export function useChatAnimationType<T extends number | string>(
|
||||
orderDiffById: Record<T, number>,
|
||||
isInitialRender: boolean,
|
||||
isShifted?: boolean,
|
||||
) {
|
||||
return useMemo(() => {
|
||||
if (isInitialRender) {
|
||||
return () => ChatAnimationTypes.None;
|
||||
}
|
||||
|
||||
const orderDiffs = Object.values<number>(orderDiffById);
|
||||
const numberOfUp = orderDiffs.filter((diff) => diff < 0).length;
|
||||
const numberOfDown = orderDiffs.filter((diff) => diff > 0).length;
|
||||
|
||||
return (chatId: T): ChatAnimationTypes => {
|
||||
const orderDiff = orderDiffById[chatId];
|
||||
if (orderDiff === 0) {
|
||||
if (!orderDiff) {
|
||||
if (isShifted) {
|
||||
return ChatAnimationTypes.Shift;
|
||||
}
|
||||
return ChatAnimationTypes.None;
|
||||
}
|
||||
|
||||
@ -29,5 +43,5 @@ export function useChatAnimationType<T extends number | string>(orderDiffById: R
|
||||
|
||||
return ChatAnimationTypes.Move;
|
||||
};
|
||||
}, [orderDiffById]);
|
||||
}, [orderDiffById, isShifted, isInitialRender]);
|
||||
}
|
||||
|
||||
@ -47,6 +47,7 @@ export default function useChatListEntry({
|
||||
observeIntersection,
|
||||
animationType,
|
||||
orderDiff,
|
||||
shiftDiff,
|
||||
withInterfaceAnimations,
|
||||
isTopic,
|
||||
isSavedDialog,
|
||||
@ -71,6 +72,7 @@ export default function useChatListEntry({
|
||||
|
||||
animationType: ChatAnimationTypes;
|
||||
orderDiff: number;
|
||||
shiftDiff: number;
|
||||
withInterfaceAnimations?: boolean;
|
||||
onReorderAnimationEnd?: NoneToVoidFunction;
|
||||
}) {
|
||||
@ -194,8 +196,10 @@ export default function useChatListEntry({
|
||||
|
||||
waitStartingTransitionsEnd(element).then(notifyAnimationEnd);
|
||||
});
|
||||
} else if (animationType === ChatAnimationTypes.Move) {
|
||||
element.style.transform = `translate3d(0, ${-orderDiff * CHAT_HEIGHT_PX}px, 0)`;
|
||||
}
|
||||
|
||||
if (animationType === ChatAnimationTypes.Move) {
|
||||
element.style.transform = `translate3d(0, ${-orderDiff * CHAT_HEIGHT_PX - shiftDiff}px, 0)`;
|
||||
|
||||
requestMutation(() => {
|
||||
element.classList.add('animate-transform');
|
||||
@ -203,14 +207,27 @@ export default function useChatListEntry({
|
||||
|
||||
waitStartingTransitionsEnd(element).then(notifyAnimationEnd);
|
||||
});
|
||||
} else {
|
||||
}
|
||||
|
||||
if (animationType === ChatAnimationTypes.Shift) {
|
||||
element.style.transform = `translate3d(0, ${-shiftDiff}px, 0)`;
|
||||
|
||||
requestMutation(() => {
|
||||
element.classList.add('animate-transform');
|
||||
element.style.transform = '';
|
||||
|
||||
waitStartingTransitionsEnd(element).then(notifyAnimationEnd);
|
||||
});
|
||||
}
|
||||
|
||||
if (animationType === ChatAnimationTypes.None) {
|
||||
return;
|
||||
}
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [withInterfaceAnimations, orderDiff, animationType, onReorderAnimationEnd]);
|
||||
}, [withInterfaceAnimations, orderDiff, shiftDiff, animationType, onReorderAnimationEnd]);
|
||||
|
||||
return {
|
||||
renderSubtitle,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useMemo, useRef } from '../../../../lib/teact/teact';
|
||||
import { useEffect, useMemo, useRef } from '../../../../lib/teact/teact';
|
||||
|
||||
import { requestNextMutation } from '../../../../lib/fasterdom/fasterdom';
|
||||
import { mapValues } from '../../../../util/iteratees';
|
||||
import { useChatAnimationType } from './useChatAnimationType';
|
||||
|
||||
@ -10,7 +11,7 @@ import useSyncEffect from '../../../../hooks/useSyncEffect';
|
||||
|
||||
const EMPTY_ORDER_DIFF = {};
|
||||
|
||||
export default function useOrderDiff(orderedIds: (string | number)[] | undefined, key?: string) {
|
||||
export default function useOrderDiff(orderedIds: (string | number)[] | undefined, topOffset: number, key?: string) {
|
||||
const orderById = useMemo(() => {
|
||||
if (!orderedIds) {
|
||||
return undefined;
|
||||
@ -24,6 +25,14 @@ export default function useOrderDiff(orderedIds: (string | number)[] | undefined
|
||||
|
||||
const prevOrderById = usePreviousDeprecated(orderById);
|
||||
const prevChatId = usePreviousDeprecated(key);
|
||||
const prevTopOffset = usePreviousDeprecated(topOffset);
|
||||
const isInitialRenderRef = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
requestNextMutation(() => {
|
||||
isInitialRenderRef.current = false;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const orderDiffByIdRef = useRef<Record<string | number, number>>(EMPTY_ORDER_DIFF);
|
||||
const forceUpdate = useForceUpdate();
|
||||
@ -35,8 +44,10 @@ export default function useOrderDiff(orderedIds: (string | number)[] | undefined
|
||||
forceUpdate();
|
||||
});
|
||||
|
||||
const shiftDiff = prevTopOffset !== undefined ? topOffset - prevTopOffset : 0;
|
||||
|
||||
useSyncEffect(() => {
|
||||
if (!orderById || !prevOrderById || key !== prevChatId) {
|
||||
if (!orderById || !prevOrderById || key !== prevChatId || prevOrderById === orderById) {
|
||||
orderDiffByIdRef.current = EMPTY_ORDER_DIFF;
|
||||
return;
|
||||
}
|
||||
@ -47,12 +58,17 @@ export default function useOrderDiff(orderedIds: (string | number)[] | undefined
|
||||
|
||||
const hasChanges = Object.values(diff).some((value) => value !== 0);
|
||||
orderDiffByIdRef.current = hasChanges ? diff : EMPTY_ORDER_DIFF;
|
||||
}, [key, orderById, prevChatId, prevOrderById]);
|
||||
}, [key, orderById, prevChatId, prevOrderById, topOffset]);
|
||||
|
||||
const getAnimationType = useChatAnimationType(orderDiffByIdRef.current);
|
||||
const getAnimationType = useChatAnimationType(
|
||||
orderDiffByIdRef.current,
|
||||
isInitialRenderRef.current,
|
||||
Boolean(shiftDiff),
|
||||
);
|
||||
|
||||
return {
|
||||
orderDiffById: orderDiffByIdRef.current,
|
||||
shiftDiff,
|
||||
getAnimationType,
|
||||
onReorderAnimationEnd,
|
||||
};
|
||||
|
||||
5
src/components/left/main/panes/ChatListPanes.module.scss
Normal file
5
src/components/left/main/panes/ChatListPanes.module.scss
Normal file
@ -0,0 +1,5 @@
|
||||
.root {
|
||||
position: absolute;
|
||||
z-index: var(--z-left-header);
|
||||
width: 100%;
|
||||
}
|
||||
140
src/components/left/main/panes/ChatListPanes.tsx
Normal file
140
src/components/left/main/panes/ChatListPanes.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
import { memo, useMemo, useRef, useSignal } from '@teact';
|
||||
import { setExtraStyles } from '@teact/teact-dom';
|
||||
import { withGlobal } from '../../../../global';
|
||||
|
||||
import type { ApiPromoData, ApiSession } from '../../../../api/types';
|
||||
|
||||
import { FRESH_AUTH_PERIOD } from '../../../../config';
|
||||
import { selectIsCurrentUserFrozen } from '../../../../global/selectors';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { getServerTime } from '../../../../util/serverTime';
|
||||
import { REM } from '../../../common/helpers/mediaDimensions';
|
||||
|
||||
import useEffectOnce from '../../../../hooks/useEffectOnce';
|
||||
import useShowTransition from '../../../../hooks/useShowTransition';
|
||||
import { useSignalEffect } from '../../../../hooks/useSignalEffect';
|
||||
import { applyAnimationState, type PaneState } from '../../../middle/hooks/useHeaderPane';
|
||||
|
||||
import FrozenAccountPane from './FrozenAccountPane';
|
||||
import SuggestionPane from './SuggestionPane';
|
||||
import UnconfirmedSessionPane from './UnconfirmedSessionPane';
|
||||
|
||||
import styles from './ChatListPanes.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
className?: string;
|
||||
onHeightChange: (height: number) => void;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
sessions: Record<string, ApiSession>;
|
||||
promoData?: ApiPromoData;
|
||||
isAccountFrozen?: boolean;
|
||||
};
|
||||
|
||||
const TOP_MARGIN = 0.5 * REM;
|
||||
const BOTTOM_MARGIN = 0.25 * REM;
|
||||
const FALLBACK_PANE_STATE = { height: 0 };
|
||||
|
||||
const ChatListPanes = ({
|
||||
className,
|
||||
sessions,
|
||||
promoData,
|
||||
isAccountFrozen,
|
||||
onHeightChange,
|
||||
}: OwnProps & StateProps) => {
|
||||
const [getUnconfirmedSessionHeight, setUnconfirmedSessionHeight] = useSignal<PaneState>(FALLBACK_PANE_STATE);
|
||||
const [getFrozenAccountState, setFrozenAccountState] = useSignal<PaneState>(FALLBACK_PANE_STATE);
|
||||
const [getSuggestionState, setSuggestionState] = useSignal<PaneState>(FALLBACK_PANE_STATE);
|
||||
|
||||
const isFirstRenderRef = useRef(true);
|
||||
const {
|
||||
shouldRender,
|
||||
ref,
|
||||
} = useShowTransition({
|
||||
isOpen: true,
|
||||
withShouldRender: true,
|
||||
noMountTransition: true,
|
||||
});
|
||||
|
||||
useEffectOnce(() => {
|
||||
isFirstRenderRef.current = false;
|
||||
});
|
||||
|
||||
const unconfirmedSession = useMemo(() => {
|
||||
const sessionsArray = Object.values(sessions || {});
|
||||
const current = sessionsArray.find((session) => session.isCurrent);
|
||||
if (!current || getServerTime() - current.dateCreated < FRESH_AUTH_PERIOD) return undefined;
|
||||
|
||||
return sessionsArray.find((session) => session.isUnconfirmed);
|
||||
}, [sessions]);
|
||||
|
||||
const canShowUnconfirmedSession = !isAccountFrozen && unconfirmedSession;
|
||||
const canShowSuggestions = !isAccountFrozen && !unconfirmedSession && promoData;
|
||||
|
||||
useSignalEffect(() => {
|
||||
const unconfirmedSessionHeight = getUnconfirmedSessionHeight();
|
||||
const frozenAccountHeight = getFrozenAccountState();
|
||||
const suggestionHeight = getSuggestionState();
|
||||
|
||||
// Keep in sync with the order of the panes in the DOM
|
||||
const stateArray = [unconfirmedSessionHeight, frozenAccountHeight, suggestionHeight];
|
||||
|
||||
const isFirstRender = isFirstRenderRef.current;
|
||||
const panelsHeight = stateArray.reduce((acc, state) => acc + state.height, 0);
|
||||
const totalHeight = panelsHeight ? panelsHeight + BOTTOM_MARGIN : 0;
|
||||
|
||||
onHeightChange(totalHeight);
|
||||
|
||||
const leftColumn = document.getElementById('LeftColumn');
|
||||
if (!leftColumn) return;
|
||||
|
||||
applyAnimationState({
|
||||
list: stateArray,
|
||||
noTransition: isFirstRender,
|
||||
topMargin: TOP_MARGIN,
|
||||
zIndexIncrease: true,
|
||||
});
|
||||
|
||||
setExtraStyles(leftColumn, {
|
||||
'--chat-list-panes-height': `${totalHeight}px`,
|
||||
});
|
||||
}, [getUnconfirmedSessionHeight, getFrozenAccountState, getSuggestionState]);
|
||||
|
||||
if (!shouldRender) return undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={
|
||||
buildClassName(
|
||||
styles.root,
|
||||
className,
|
||||
)
|
||||
}
|
||||
>
|
||||
<FrozenAccountPane
|
||||
isAccountFrozen={isAccountFrozen}
|
||||
onPaneStateChange={setFrozenAccountState}
|
||||
/>
|
||||
<UnconfirmedSessionPane
|
||||
unconfirmedSession={canShowUnconfirmedSession ? unconfirmedSession : undefined}
|
||||
onPaneStateChange={setUnconfirmedSessionHeight}
|
||||
/>
|
||||
<SuggestionPane
|
||||
promoData={canShowSuggestions ? promoData : undefined}
|
||||
onPaneStateChange={setSuggestionState}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): Complete<StateProps> => {
|
||||
return {
|
||||
sessions: global.activeSessions.byHash,
|
||||
promoData: global.promoData,
|
||||
isAccountFrozen: selectIsCurrentUserFrozen(global),
|
||||
};
|
||||
},
|
||||
)(ChatListPanes));
|
||||
@ -1,9 +1,13 @@
|
||||
@use "../../../../styles/mixins";
|
||||
|
||||
.root {
|
||||
@include mixins.chat-list-pane;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
margin-inline: -0.5rem;
|
||||
padding-block: 0.75rem;
|
||||
padding-inline: 1rem;
|
||||
border-radius: var(--border-radius-default);
|
||||
|
||||
background-color: var(--color-background-secondary);
|
||||
|
||||
44
src/components/left/main/panes/FrozenAccountPane.tsx
Normal file
44
src/components/left/main/panes/FrozenAccountPane.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { memo } from '../../../../lib/teact/teact';
|
||||
import { getActions } from '../../../../global';
|
||||
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
import useHeaderPane, { type PaneState } from '../../../middle/hooks/useHeaderPane';
|
||||
|
||||
import styles from './FrozenAccountPane.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
isAccountFrozen?: boolean;
|
||||
onPaneStateChange: (state: PaneState) => void;
|
||||
};
|
||||
|
||||
const FrozenAccountPane = ({ isAccountFrozen, onPaneStateChange }: OwnProps) => {
|
||||
const { openFrozenAccountModal } = getActions();
|
||||
const lang = useLang();
|
||||
|
||||
const { ref, shouldRender } = useHeaderPane({
|
||||
isOpen: isAccountFrozen,
|
||||
onStateChange: onPaneStateChange,
|
||||
});
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
openFrozenAccountModal();
|
||||
});
|
||||
|
||||
if (!shouldRender) return undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={styles.root}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className={styles.title}>{lang('TitleFrozenAccount')}</div>
|
||||
<div className={styles.subtitle}>{lang('SubtitleFrozenAccount')}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(FrozenAccountPane);
|
||||
56
src/components/left/main/panes/SuggestionPane.module.scss
Normal file
56
src/components/left/main/panes/SuggestionPane.module.scss
Normal file
@ -0,0 +1,56 @@
|
||||
@use "../../../../styles/mixins";
|
||||
|
||||
.root {
|
||||
@include mixins.chat-list-pane;
|
||||
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr min-content;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
|
||||
border-radius: var(--border-radius-default);
|
||||
|
||||
line-height: 1.25;
|
||||
|
||||
background-color: var(--color-background-secondary);
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
--emoji-size: 1.125rem;
|
||||
--custom-emoji-size: var(--emoji-size);
|
||||
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 1 / 2;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
--emoji-size: 1rem;
|
||||
--custom-emoji-size: var(--emoji-size);
|
||||
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 2 / 3;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.closeIcon {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 1 / 2;
|
||||
place-self: center;
|
||||
|
||||
padding: 0.125rem;
|
||||
border-radius: 50%;
|
||||
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-interactive-element-hover);
|
||||
}
|
||||
}
|
||||
104
src/components/left/main/panes/SuggestionPane.tsx
Normal file
104
src/components/left/main/panes/SuggestionPane.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import { memo, useMemo } from '../../../../lib/teact/teact';
|
||||
import { getActions } from '../../../../global';
|
||||
|
||||
import type { ApiPromoData } from '../../../../api/types';
|
||||
import type { RegularLangKey } from '../../../../types/language';
|
||||
|
||||
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
|
||||
|
||||
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
import useHeaderPane, { type PaneState } from '../../../middle/hooks/useHeaderPane';
|
||||
|
||||
import Icon from '../../../common/icons/Icon';
|
||||
|
||||
import styles from './SuggestionPane.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
promoData?: ApiPromoData;
|
||||
onPaneStateChange: (state: PaneState) => void;
|
||||
};
|
||||
|
||||
// https://core.telegram.org/api/config#suggestions
|
||||
const BIRTHDAY_SETUP = 'BIRTHDAY_SETUP';
|
||||
const SUPPORTED_SUGGESTIONS = [BIRTHDAY_SETUP] as const;
|
||||
type Suggestion = (typeof SUPPORTED_SUGGESTIONS)[number];
|
||||
|
||||
const SUPPORTED_SUGGESTIONS_SET = new Set<string>(SUPPORTED_SUGGESTIONS);
|
||||
|
||||
const AUTOCLOSABLE_SUGGESTIONS = new Set<string>([BIRTHDAY_SETUP]);
|
||||
|
||||
const SUGGESTION_LANG_KEYS: Record<Suggestion, [RegularLangKey, RegularLangKey]> = {
|
||||
BIRTHDAY_SETUP: ['SuggestionBirthdaySetupTitle', 'SuggestionBirthdaySetupMessage'],
|
||||
};
|
||||
|
||||
const SuggestionPane = ({ promoData, onPaneStateChange }: OwnProps) => {
|
||||
const { openBirthdaySetupModal, dismissSuggestion, openUrl } = getActions();
|
||||
const lang = useLang();
|
||||
|
||||
const currentSuggestion = useMemo(() => {
|
||||
if (promoData?.customPendingSuggestion) return promoData.customPendingSuggestion;
|
||||
return promoData?.pendingSuggestions.find((suggestion): suggestion is Suggestion => (
|
||||
SUPPORTED_SUGGESTIONS_SET.has(suggestion)
|
||||
));
|
||||
}, [promoData]);
|
||||
const renderingSuggestion = useCurrentOrPrev(currentSuggestion);
|
||||
const isCustomSuggestion = typeof renderingSuggestion === 'object';
|
||||
|
||||
const { ref, shouldRender } = useHeaderPane({
|
||||
isOpen: Boolean(currentSuggestion),
|
||||
onStateChange: onPaneStateChange,
|
||||
});
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
if (!renderingSuggestion) return;
|
||||
|
||||
const suggestion = isCustomSuggestion ? renderingSuggestion.suggestion : renderingSuggestion;
|
||||
if (AUTOCLOSABLE_SUGGESTIONS.has(suggestion)) {
|
||||
dismissSuggestion({ suggestion });
|
||||
}
|
||||
|
||||
if (isCustomSuggestion) {
|
||||
openUrl({ url: renderingSuggestion.url });
|
||||
return;
|
||||
}
|
||||
|
||||
switch (renderingSuggestion) {
|
||||
case BIRTHDAY_SETUP:
|
||||
openBirthdaySetupModal({});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const handleDismiss = useLastCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (!renderingSuggestion) return;
|
||||
const suggestion = isCustomSuggestion ? renderingSuggestion.suggestion : renderingSuggestion;
|
||||
dismissSuggestion({ suggestion });
|
||||
});
|
||||
|
||||
if (!shouldRender || !renderingSuggestion) return undefined;
|
||||
|
||||
const title = isCustomSuggestion ? renderTextWithEntities(renderingSuggestion.title)
|
||||
: lang(SUGGESTION_LANG_KEYS[renderingSuggestion][0], undefined, { withNodes: true });
|
||||
const message = isCustomSuggestion ? renderTextWithEntities(renderingSuggestion.description)
|
||||
: lang(SUGGESTION_LANG_KEYS[renderingSuggestion][1], undefined, { withNodes: true });
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={styles.root}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<div className={styles.subtitle}>{message}</div>
|
||||
<Icon name="close" className={styles.closeIcon} onClick={handleDismiss} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SuggestionPane);
|
||||
@ -1,13 +1,10 @@
|
||||
/* stylelint-disable-next-line */
|
||||
@value minimized from "./Archive.module.scss";
|
||||
@use "../../../../styles/mixins";
|
||||
|
||||
.root {
|
||||
@include mixins.chat-list-pane;
|
||||
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
|
||||
& + :global(.minimized) {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
92
src/components/left/main/panes/UnconfirmedSessionPane.tsx
Normal file
92
src/components/left/main/panes/UnconfirmedSessionPane.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import { memo, useMemo } from '../../../../lib/teact/teact';
|
||||
import { getActions } from '../../../../global';
|
||||
|
||||
import type { ApiSession } from '../../../../api/types';
|
||||
|
||||
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
import useHeaderPane, { type PaneState } from '../../../middle/hooks/useHeaderPane';
|
||||
|
||||
import Button from '../../../ui/Button';
|
||||
|
||||
import styles from './UnconfirmedSessionPane.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
unconfirmedSession: ApiSession | undefined;
|
||||
onPaneStateChange: (state: PaneState) => void;
|
||||
};
|
||||
|
||||
const UnconfirmedSessionPane = ({
|
||||
unconfirmedSession,
|
||||
onPaneStateChange,
|
||||
}: OwnProps) => {
|
||||
const { changeSessionSettings, terminateAuthorization, showNotification } = getActions();
|
||||
const lang = useLang();
|
||||
|
||||
const isOpen = Boolean(unconfirmedSession);
|
||||
const renderingSession = useCurrentOrPrev(unconfirmedSession);
|
||||
|
||||
const { ref, shouldRender } = useHeaderPane({
|
||||
isOpen,
|
||||
withResizeObserver: true,
|
||||
onStateChange: onPaneStateChange,
|
||||
});
|
||||
|
||||
const locationString = useMemo(() => {
|
||||
if (!renderingSession) return '';
|
||||
if (!renderingSession.region) {
|
||||
return lang('UnconfirmedAuthLocationCountry', {
|
||||
deviceModel: renderingSession.deviceModel,
|
||||
country: renderingSession.country,
|
||||
});
|
||||
}
|
||||
|
||||
return lang('UnconfirmedAuthLocationRegion', {
|
||||
deviceModel: renderingSession.deviceModel,
|
||||
region: renderingSession.region,
|
||||
country: renderingSession.country,
|
||||
});
|
||||
}, [renderingSession, lang]);
|
||||
|
||||
const handleAccept = useLastCallback(() => {
|
||||
if (!renderingSession) return;
|
||||
changeSessionSettings({
|
||||
hash: renderingSession.hash,
|
||||
isConfirmed: true,
|
||||
});
|
||||
});
|
||||
|
||||
const handleReject = useLastCallback(() => {
|
||||
if (!renderingSession) return;
|
||||
terminateAuthorization({ hash: renderingSession.hash });
|
||||
showNotification({
|
||||
title: lang('UnconfirmedAuthDeniedTitle'),
|
||||
message: lang('UnconfirmedAuthDeniedMessage', { location: locationString }),
|
||||
});
|
||||
});
|
||||
|
||||
if (!shouldRender || !renderingSession) return undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.root}
|
||||
ref={ref}
|
||||
>
|
||||
<h2 className={styles.title}>{lang('UnconfirmedAuthTitle')}</h2>
|
||||
<p className={styles.info}>
|
||||
{lang('UnconfirmedAuthSingle', { location: locationString })}
|
||||
</p>
|
||||
<div className={styles.buttons}>
|
||||
<Button fluid isText className={styles.button} onClick={handleAccept}>
|
||||
{lang('UnconfirmedAuthConfirm')}
|
||||
</Button>
|
||||
<Button fluid isText color="danger" onClick={handleReject} className={styles.button}>
|
||||
{lang('UnconfirmedAuthDeny')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(UnconfirmedSessionPane);
|
||||
@ -177,7 +177,9 @@
|
||||
}
|
||||
|
||||
&-description {
|
||||
margin: -0.5rem 1rem 1rem;
|
||||
margin-block: 0;
|
||||
margin-inline: 1rem;
|
||||
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.3125;
|
||||
color: var(--color-text-secondary);
|
||||
@ -284,6 +286,10 @@
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& + .settings-item-description {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
@ -390,3 +396,8 @@
|
||||
.settings-button {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.settings-birthday-date {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
@ -1,23 +1,23 @@
|
||||
import type { ChangeEvent } from 'react';
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import {
|
||||
memo, useCallback, useEffect, useMemo,
|
||||
useState,
|
||||
memo, useEffect, useMemo, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiUsername } from '../../../api/types';
|
||||
import type { ApiBirthday, ApiUsername } from '../../../api/types';
|
||||
import { ApiMediaFormat } from '../../../api/types';
|
||||
import { ProfileEditProgress } from '../../../types';
|
||||
import { ProfileEditProgress, SettingsScreens } from '../../../types';
|
||||
|
||||
import { PURCHASE_USERNAME, TME_LINK_PREFIX, USERNAME_PURCHASE_ERROR } from '../../../config';
|
||||
import { getChatAvatarHash } from '../../../global/helpers';
|
||||
import { selectTabState, selectUser, selectUserFullInfo } from '../../../global/selectors';
|
||||
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
||||
import { formatDateToString } from '../../../util/dates/dateFormat';
|
||||
import { throttle } from '../../../util/schedulers';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
import usePreviousDeprecated from '../../../hooks/usePreviousDeprecated';
|
||||
@ -28,6 +28,8 @@ import UsernameInput from '../../common/UsernameInput';
|
||||
import AvatarEditable from '../../ui/AvatarEditable';
|
||||
import FloatingActionButton from '../../ui/FloatingActionButton';
|
||||
import InputText from '../../ui/InputText';
|
||||
import Link from '../../ui/Link';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import TextArea from '../../ui/TextArea';
|
||||
|
||||
type OwnProps = {
|
||||
@ -39,6 +41,7 @@ type StateProps = {
|
||||
currentAvatarHash?: string;
|
||||
currentFirstName?: string;
|
||||
currentLastName?: string;
|
||||
currentBirthday?: ApiBirthday;
|
||||
currentBio?: string;
|
||||
progress?: ProfileEditProgress;
|
||||
checkedUsername?: string;
|
||||
@ -52,11 +55,12 @@ const runThrottled = throttle((cb) => cb(), 60000, true);
|
||||
|
||||
const ERROR_FIRST_NAME_MISSING = 'Please provide your first name';
|
||||
|
||||
const SettingsEditProfile: FC<OwnProps & StateProps> = ({
|
||||
const SettingsEditProfile = ({
|
||||
isActive,
|
||||
currentAvatarHash,
|
||||
currentFirstName,
|
||||
currentLastName,
|
||||
currentBirthday,
|
||||
currentBio,
|
||||
progress,
|
||||
checkedUsername,
|
||||
@ -65,13 +69,16 @@ const SettingsEditProfile: FC<OwnProps & StateProps> = ({
|
||||
maxBioLength,
|
||||
usernames,
|
||||
onReset,
|
||||
}) => {
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
loadCurrentUser,
|
||||
updateProfile,
|
||||
openSettingsScreen,
|
||||
openBirthdaySetupModal,
|
||||
} = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
const firstEditableUsername = useMemo(() => usernames?.find(({ isEditable }) => isEditable), [usernames]);
|
||||
const currentUsername = firstEditableUsername?.username || '';
|
||||
@ -137,31 +144,51 @@ const SettingsEditProfile: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [progress]);
|
||||
|
||||
const handlePhotoChange = useCallback((newPhoto: File) => {
|
||||
setPhoto(newPhoto);
|
||||
}, []);
|
||||
const formattedBirthday = useMemo(() => {
|
||||
if (!currentBirthday) return undefined;
|
||||
|
||||
const handleFirstNameChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
const date = new Date(
|
||||
currentBirthday.year || 2024, // Use leap year as fallback
|
||||
currentBirthday.month - 1,
|
||||
currentBirthday.day,
|
||||
);
|
||||
|
||||
return formatDateToString(date, lang.code, true, 'long');
|
||||
}, [currentBirthday, lang]);
|
||||
|
||||
const handlePhotoChange = useLastCallback((newPhoto: File) => {
|
||||
setPhoto(newPhoto);
|
||||
});
|
||||
|
||||
const handleFirstNameChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFirstName(e.target.value);
|
||||
setIsProfileFieldsTouched(true);
|
||||
}, []);
|
||||
});
|
||||
|
||||
const handleLastNameChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
const handleLastNameChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setLastName(e.target.value);
|
||||
setIsProfileFieldsTouched(true);
|
||||
}, []);
|
||||
});
|
||||
|
||||
const handleBioChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const handleBioChange = useLastCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setBio(e.target.value);
|
||||
setIsProfileFieldsTouched(true);
|
||||
}, []);
|
||||
});
|
||||
|
||||
const handleUsernameChange = useCallback((value: string | false) => {
|
||||
const handleUsernameChange = useLastCallback((value: string | false) => {
|
||||
setEditableUsername(value);
|
||||
setIsUsernameTouched(currentUsername !== value);
|
||||
}, [currentUsername]);
|
||||
});
|
||||
|
||||
const handleProfileSave = useCallback(() => {
|
||||
const handleBirthdayPrivacyClick = useLastCallback(() => {
|
||||
openSettingsScreen({ screen: SettingsScreens.PrivacyBirthday });
|
||||
});
|
||||
|
||||
const handleBirthdayClick = useLastCallback(() => {
|
||||
openBirthdaySetupModal({ currentBirthday });
|
||||
});
|
||||
|
||||
const handleProfileSave = useLastCallback(() => {
|
||||
const trimmedFirstName = firstName.trim();
|
||||
const trimmedLastName = lastName.trim();
|
||||
const trimmedBio = bio.trim();
|
||||
@ -184,19 +211,14 @@ const SettingsEditProfile: FC<OwnProps & StateProps> = ({
|
||||
username: editableUsername,
|
||||
}),
|
||||
});
|
||||
}, [
|
||||
photo,
|
||||
firstName, lastName, bio, isProfileFieldsTouched,
|
||||
editableUsername, isUsernameTouched,
|
||||
updateProfile,
|
||||
]);
|
||||
});
|
||||
|
||||
function renderPurchaseLink() {
|
||||
const purchaseInfoLink = `${TME_LINK_PREFIX}${PURCHASE_USERNAME}`;
|
||||
|
||||
return (
|
||||
<p className="settings-item-description" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{(lang('lng_username_purchase_available'))
|
||||
<p className="settings-item-description" dir={oldLang.isRtl ? 'rtl' : undefined}>
|
||||
{(oldLang('lng_username_purchase_available'))
|
||||
.replace('{link}', '%PURCHASE_LINK%')
|
||||
.split('%')
|
||||
.map((s) => {
|
||||
@ -214,39 +236,57 @@ const SettingsEditProfile: FC<OwnProps & StateProps> = ({
|
||||
<AvatarEditable
|
||||
currentAvatarBlobUrl={currentAvatarBlobUrl}
|
||||
onChange={handlePhotoChange}
|
||||
title="Edit your profile photo"
|
||||
title={lang('AriaSettingsEditProfilePhoto')}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<InputText
|
||||
value={firstName}
|
||||
onChange={handleFirstNameChange}
|
||||
label={lang('FirstName')}
|
||||
label={oldLang('FirstName')}
|
||||
disabled={isLoading}
|
||||
error={error === ERROR_FIRST_NAME_MISSING ? error : undefined}
|
||||
/>
|
||||
<InputText
|
||||
value={lastName}
|
||||
onChange={handleLastNameChange}
|
||||
label={lang('LastName')}
|
||||
label={oldLang('LastName')}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<TextArea
|
||||
value={bio}
|
||||
onChange={handleBioChange}
|
||||
label={lang('UserBio')}
|
||||
label={oldLang('UserBio')}
|
||||
disabled={isLoading}
|
||||
maxLength={maxBioLength}
|
||||
maxLengthIndicator={maxBioLength ? (maxBioLength - bio.length).toString() : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="settings-item-description" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{renderText(lang('lng_settings_about_bio'), ['br', 'simple_markdown'])}
|
||||
<p className="settings-item-description" dir={oldLang.isRtl ? 'rtl' : undefined}>
|
||||
{renderText(oldLang('lng_settings_about_bio'), ['br', 'simple_markdown'])}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>{lang('Username')}</h4>
|
||||
<ListItem
|
||||
icon="gift"
|
||||
narrow
|
||||
rightElement={formattedBirthday ?
|
||||
<span className="settings-birthday-date">{formattedBirthday}</span>
|
||||
: undefined}
|
||||
onClick={handleBirthdayClick}
|
||||
>
|
||||
<span className="flex-grow">{lang('SettingsBirthday')}</span>
|
||||
</ListItem>
|
||||
<p className="settings-item-description" dir={oldLang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('BirthdayPrivacySuggestion', {
|
||||
link: <Link isPrimary onClick={handleBirthdayPrivacyClick}>{lang('BirthdayPrivacySuggestionLink')}</Link>,
|
||||
}, { withNodes: true })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<h4 className="settings-item-header" dir={oldLang.isRtl ? 'rtl' : undefined}>{oldLang('Username')}</h4>
|
||||
|
||||
<div className="settings-input">
|
||||
<UsernameInput
|
||||
@ -259,12 +299,12 @@ const SettingsEditProfile: FC<OwnProps & StateProps> = ({
|
||||
</div>
|
||||
|
||||
{editUsernameError === USERNAME_PURCHASE_ERROR && renderPurchaseLink()}
|
||||
<p className="settings-item-description" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{renderText(lang('UsernameHelp'), ['br', 'simple_markdown'])}
|
||||
<p className="settings-item-description" dir={oldLang.isRtl ? 'rtl' : undefined}>
|
||||
{renderText(oldLang('UsernameHelp'), ['br', 'simple_markdown'])}
|
||||
</p>
|
||||
{editableUsername && (
|
||||
<p className="settings-item-description" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('lng_username_link')}
|
||||
<p className="settings-item-description" dir={oldLang.isRtl ? 'rtl' : undefined}>
|
||||
{oldLang('lng_username_link')}
|
||||
<br />
|
||||
<span className="username-link">
|
||||
{TME_LINK_PREFIX}
|
||||
@ -286,7 +326,7 @@ const SettingsEditProfile: FC<OwnProps & StateProps> = ({
|
||||
isShown={isSaveButtonShown}
|
||||
onClick={handleProfileSave}
|
||||
disabled={isLoading}
|
||||
ariaLabel={lang('Save')}
|
||||
ariaLabel={oldLang('Save')}
|
||||
iconName="check"
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
@ -316,6 +356,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
currentAvatarHash,
|
||||
currentFirstName,
|
||||
currentLastName,
|
||||
currentBirthday: currentUserFullInfo?.birthday,
|
||||
currentBio: currentUserFullInfo?.bio,
|
||||
progress,
|
||||
isUsernameAvailable,
|
||||
|
||||
@ -68,9 +68,9 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
canSetPasscode,
|
||||
needAgeVideoVerification,
|
||||
privacy,
|
||||
onReset,
|
||||
isCurrentUserFrozen,
|
||||
accountDaysTtl,
|
||||
onReset,
|
||||
}) => {
|
||||
const {
|
||||
openDeleteAccountModal,
|
||||
@ -89,7 +89,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
useEffect(() => {
|
||||
if (!isCurrentUserFrozen) {
|
||||
loadBlockedUsers();
|
||||
loadPrivacySettings();
|
||||
loadPrivacySettings({});
|
||||
loadWebAuthorizations();
|
||||
}
|
||||
}, [isCurrentUserFrozen]);
|
||||
|
||||
@ -65,7 +65,7 @@ const Dialogs: FC<StateProps> = ({ dialogs, currentMessageList }) => {
|
||||
{lang(
|
||||
'AreYouSureShareMyContactInfoBot',
|
||||
undefined,
|
||||
{ withNodes: true, withMarkdown: true, renderTextFilters: ['br', 'emoji'],
|
||||
{ withNodes: true, withMarkdown: true, renderTextFilters: ['br'],
|
||||
})}
|
||||
<div className="dialog-buttons mt-2">
|
||||
<Button
|
||||
|
||||
@ -263,6 +263,7 @@ const Main = ({
|
||||
loadAllStories,
|
||||
loadAllHiddenStories,
|
||||
loadContentSettings,
|
||||
loadPromoData,
|
||||
} = getActions();
|
||||
|
||||
if (DEBUG && !DEBUG_isLogged) {
|
||||
@ -315,6 +316,7 @@ const Main = ({
|
||||
loadAllChats({ listType: 'saved' });
|
||||
loadAllStories();
|
||||
loadAllHiddenStories();
|
||||
loadPromoData();
|
||||
loadContentSettings();
|
||||
loadRecentReactions();
|
||||
loadDefaultTagReactions();
|
||||
|
||||
@ -113,7 +113,7 @@ const MiddleHeaderPanes = ({
|
||||
const middleColumn = document.getElementById('MiddleColumn');
|
||||
if (!middleColumn) return;
|
||||
|
||||
applyAnimationState(stateArray, isFirstRender);
|
||||
applyAnimationState({ list: stateArray, noTransition: isFirstRender });
|
||||
|
||||
setExtraStyles(middleColumn, {
|
||||
'--middle-header-panes-height': `${totalHeight}px`,
|
||||
|
||||
@ -126,7 +126,17 @@ export default function useHeaderPane<RefType extends HTMLElement = HTMLDivEleme
|
||||
};
|
||||
}
|
||||
|
||||
export function applyAnimationState(list: PaneState[], noTransition = false) {
|
||||
export function applyAnimationState({
|
||||
list,
|
||||
noTransition = false,
|
||||
zIndexIncrease,
|
||||
topMargin = 0,
|
||||
}: {
|
||||
list: PaneState[];
|
||||
noTransition?: boolean;
|
||||
zIndexIncrease?: boolean;
|
||||
topMargin?: number;
|
||||
}) {
|
||||
let cumulativeHeight = 0;
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const state = list[i];
|
||||
@ -139,7 +149,7 @@ export function applyAnimationState(list: PaneState[], noTransition = false) {
|
||||
|
||||
const apply = () => {
|
||||
setExtraStyles(element, {
|
||||
transform: `translateY(${state.isOpen ? shiftPx : `calc(${shiftPx} - 100%)`})`,
|
||||
transform: `translateY(${state.isOpen ? shiftPx : `calc(${shiftPx} - ${topMargin}px - 100%)`})`,
|
||||
zIndex: String(-i),
|
||||
transition: noTransition ? 'none' : '',
|
||||
});
|
||||
@ -148,8 +158,8 @@ export function applyAnimationState(list: PaneState[], noTransition = false) {
|
||||
if (!element.dataset.isPanelOpen && state.isOpen && !noTransition) {
|
||||
// Start animation right above its final position
|
||||
setExtraStyles(element, {
|
||||
transform: `translateY(calc(${shiftPx} - 100%))`,
|
||||
zIndex: String(-i),
|
||||
transform: `translateY(calc(${shiftPx} - ${topMargin}px - 100%))`,
|
||||
zIndex: String(zIndexIncrease ? i : -i),
|
||||
transition: 'none',
|
||||
});
|
||||
element.dataset.isPanelOpen = 'true';
|
||||
|
||||
@ -227,7 +227,7 @@
|
||||
}
|
||||
|
||||
&.full-width-player {
|
||||
@include mixins.header-pane;
|
||||
@include mixins.middle-header-pane;
|
||||
|
||||
.AudioPlayer-content {
|
||||
flex-grow: 1;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
@use "../../../styles/mixins";
|
||||
|
||||
.root {
|
||||
@include mixins.header-pane;
|
||||
@include mixins.middle-header-pane;
|
||||
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
height: auto;
|
||||
|
||||
// Slight variation from mixins.header-pane
|
||||
// Slight variation from mixins.middle-header-pane
|
||||
padding-right: max(1rem, env(safe-area-inset-right));
|
||||
padding-left: max(1rem, env(safe-area-inset-left));
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
@use "../../../styles/mixins";
|
||||
|
||||
.root {
|
||||
@include mixins.header-pane;
|
||||
@include mixins.middle-header-pane;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
@use "../../../styles/mixins";
|
||||
|
||||
.ChatReportPane {
|
||||
@include mixins.header-pane;
|
||||
@include mixins.middle-header-pane;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -49,7 +49,7 @@
|
||||
}
|
||||
|
||||
.fullWidth {
|
||||
@include mixins.header-pane;
|
||||
@include mixins.middle-header-pane;
|
||||
|
||||
height: 3.5rem;
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
@use "../../../styles/mixins";
|
||||
|
||||
.root {
|
||||
@include mixins.header-pane;
|
||||
@include mixins.middle-header-pane;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@ -12,6 +12,7 @@ import WebAppsCloseConfirmationModal from '../main/WebAppsCloseConfirmationModal
|
||||
import AboutAdsModal from './aboutAds/AboutAdsModal.async';
|
||||
import AgeVerificationModal from './ageVerification/AgeVerificationModal.async';
|
||||
import AttachBotInstallModal from './attachBotInstall/AttachBotInstallModal.async';
|
||||
import BirthdaySetupModal from './birthday/BirthdaySetupModal.async';
|
||||
import BoostModal from './boost/BoostModal.async';
|
||||
import ChatInviteModal from './chatInvite/ChatInviteModal.async';
|
||||
import ChatlistModal from './chatlist/ChatlistModal.async';
|
||||
@ -109,7 +110,8 @@ type ModalKey = keyof Pick<TabState,
|
||||
'isAgeVerificationModalOpen' |
|
||||
'profileRatingModal' |
|
||||
'quickPreview' |
|
||||
'storyStealthModal'
|
||||
'storyStealthModal' |
|
||||
'birthdaySetupModal'
|
||||
>;
|
||||
|
||||
type StateProps = {
|
||||
@ -175,6 +177,7 @@ const MODALS: ModalRegistry = {
|
||||
profileRatingModal: ProfileRatingModal,
|
||||
quickPreview: QuickPreviewModal,
|
||||
storyStealthModal: StealthModeModal,
|
||||
birthdaySetupModal: BirthdaySetupModal,
|
||||
};
|
||||
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
|
||||
const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>;
|
||||
|
||||
14
src/components/modals/birthday/BirthdaySetupModal.async.tsx
Normal file
14
src/components/modals/birthday/BirthdaySetupModal.async.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import type { OwnProps } from './BirthdaySetupModal';
|
||||
|
||||
import { Bundles } from '../../../util/moduleLoader';
|
||||
|
||||
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||
|
||||
const BirthdaySetupModalAsync = (props: OwnProps) => {
|
||||
const { modal } = props;
|
||||
const BirthdaySetupModal = useModuleLoader(Bundles.Extra, 'BirthdaySetupModal', !modal);
|
||||
|
||||
return BirthdaySetupModal ? <BirthdaySetupModal {...props} /> : undefined;
|
||||
};
|
||||
|
||||
export default BirthdaySetupModalAsync;
|
||||
@ -0,0 +1,61 @@
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.input {
|
||||
flex-shrink: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.month {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.privacySuggestion {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.monthDropdown {
|
||||
flex-basis: 100%;
|
||||
flex-shrink: 1;
|
||||
margin-bottom: 0;
|
||||
|
||||
:global(.Menu) {
|
||||
width: 100%;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.monthBubble {
|
||||
overflow: auto !important;
|
||||
width: 100%;
|
||||
min-width: auto !important;
|
||||
max-height: 12rem;
|
||||
}
|
||||
240
src/components/modals/birthday/BirthdaySetupModal.tsx
Normal file
240
src/components/modals/birthday/BirthdaySetupModal.tsx
Normal file
@ -0,0 +1,240 @@
|
||||
import { memo, useMemo, useRef, useState } from '@teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { TabState } from '../../../global/types';
|
||||
import { SettingsScreens } from '../../../types';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
|
||||
import Button from '../../ui/Button';
|
||||
import DropdownMenu from '../../ui/DropdownMenu';
|
||||
import InputText from '../../ui/InputText';
|
||||
import Link from '../../ui/Link';
|
||||
import MenuItem from '../../ui/MenuItem';
|
||||
import Modal from '../../ui/Modal';
|
||||
|
||||
import styles from './BirthdaySetupModal.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
modal: TabState['birthdaySetupModal'];
|
||||
};
|
||||
|
||||
const STICKER_SIZE = 120;
|
||||
|
||||
const MAX_AGE = 150;
|
||||
const CURRENT_YEAR = new Date().getFullYear();
|
||||
const MIN_YEAR = CURRENT_YEAR - MAX_AGE;
|
||||
|
||||
type MonthIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
||||
const MONTH_INDEXES = Array.from({ length: 12 }, (_, index) => index + 1) as MonthIndex[];
|
||||
|
||||
const BirthdaySetupModal = ({ modal }: OwnProps) => {
|
||||
const { closeBirthdaySetupModal, openSettingsScreen, updateBirthday } = getActions();
|
||||
|
||||
const { currentBirthday } = modal || {};
|
||||
|
||||
const dialogRef = useRef<HTMLDivElement>();
|
||||
|
||||
const [day, setDay] = useState<number | undefined>(currentBirthday?.day);
|
||||
const [month, setMonth] = useState<MonthIndex | undefined>(currentBirthday?.month as MonthIndex | undefined);
|
||||
const [year, setYear] = useState<number | undefined>(currentBirthday?.year);
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const handleClose = useLastCallback(() => {
|
||||
closeBirthdaySetupModal();
|
||||
});
|
||||
|
||||
const handleRemove = useLastCallback(() => {
|
||||
updateBirthday({
|
||||
birthday: undefined,
|
||||
});
|
||||
closeBirthdaySetupModal();
|
||||
});
|
||||
|
||||
const handlePrivacyClick = useLastCallback(() => {
|
||||
openSettingsScreen({ screen: SettingsScreens.PrivacyBirthday });
|
||||
closeBirthdaySetupModal();
|
||||
});
|
||||
|
||||
const maxDay = getMaxMonthDay(month, year);
|
||||
|
||||
const handleDayChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.value) {
|
||||
setDay(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const value = Number(e.target.value.replace(/[^\d]+/g, ''));
|
||||
if (!value) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (value > maxDay) {
|
||||
setDay(maxDay);
|
||||
return;
|
||||
}
|
||||
|
||||
setDay(Math.max(value, 1));
|
||||
});
|
||||
|
||||
const handleMonthUpdate = useLastCallback((value: MonthIndex) => {
|
||||
setMonth(value);
|
||||
if (day) setDay(Math.min(day, getMaxMonthDay(value, year)));
|
||||
});
|
||||
|
||||
const handleYearChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.value) {
|
||||
setYear(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const value = Number(e.target.value.replace(/[^\d]+/g, ''));
|
||||
if (!value) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (value > CURRENT_YEAR) {
|
||||
setYear(CURRENT_YEAR);
|
||||
return;
|
||||
}
|
||||
|
||||
setYear(value);
|
||||
|
||||
if (day) setDay(Math.min(day, getMaxMonthDay(month, value)));
|
||||
});
|
||||
|
||||
const handleYearBlur = useLastCallback(() => {
|
||||
if (!year) {
|
||||
setYear(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
if (year < 100) {
|
||||
setYear(1900 + year);
|
||||
return;
|
||||
}
|
||||
|
||||
if (year < MIN_YEAR) {
|
||||
setYear(MIN_YEAR);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
const handleSubmit = useLastCallback(() => {
|
||||
if (!day || !month) return;
|
||||
updateBirthday({
|
||||
birthday: {
|
||||
day,
|
||||
month,
|
||||
year,
|
||||
},
|
||||
});
|
||||
closeBirthdaySetupModal();
|
||||
});
|
||||
|
||||
const MonthTrigger = useMemo(() => {
|
||||
return ({ onTrigger, isOpen }: { onTrigger: () => void; isOpen?: boolean }) => (
|
||||
<InputText
|
||||
label={lang('BirthdayInputMonth')}
|
||||
className={buildClassName(styles.input, styles.month, isOpen && 'active')}
|
||||
value={month ? lang(`Month${month}`) : ''}
|
||||
onClick={onTrigger}
|
||||
inputMode="numeric"
|
||||
teactExperimentControlled
|
||||
/>
|
||||
);
|
||||
}, [lang, month]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={Boolean(modal)}
|
||||
hasCloseButton
|
||||
hasAbsoluteCloseButton
|
||||
isSlim
|
||||
dialogRef={dialogRef}
|
||||
contentClassName={styles.content}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
<AnimatedIconWithPreview
|
||||
tgsUrl={LOCAL_TGS_URLS.DuckCake}
|
||||
size={STICKER_SIZE}
|
||||
className="section-icon"
|
||||
/>
|
||||
<h3 className={styles.title}>{lang('BirthdaySetupTitle')}</h3>
|
||||
</div>
|
||||
<div className={styles.inputs}>
|
||||
<InputText
|
||||
label={lang('BirthdayInputDay')}
|
||||
className={styles.input}
|
||||
value={day?.toString()}
|
||||
onChange={handleDayChange}
|
||||
maxLength={2}
|
||||
inputMode="numeric"
|
||||
/>
|
||||
<DropdownMenu
|
||||
className={buildClassName(styles.monthDropdown, 'with-menu-transitions')}
|
||||
bubbleClassName={styles.monthBubble}
|
||||
autoClose
|
||||
positionY="bottom"
|
||||
trigger={MonthTrigger}
|
||||
>
|
||||
{MONTH_INDEXES.map((index: MonthIndex) => (
|
||||
<MenuItem key={index} onClick={() => handleMonthUpdate(index)}>
|
||||
{lang(`Month${index}`)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
<InputText
|
||||
label={lang('BirthdayInputYear')}
|
||||
className={styles.input}
|
||||
value={year?.toString()}
|
||||
onBlur={handleYearBlur}
|
||||
onChange={handleYearChange}
|
||||
maxLength={4}
|
||||
inputMode="numeric"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<span className={styles.privacySuggestion}>
|
||||
{lang('BirthdayPrivacySuggestion', {
|
||||
link: <Link isPrimary onClick={handlePrivacyClick}>{lang('BirthdayPrivacySuggestionLink')}</Link>,
|
||||
}, { withNodes: true })}
|
||||
</span>
|
||||
{currentBirthday && (
|
||||
<Button isText onClick={handleRemove}>
|
||||
{lang('BirthdayRemove')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
disabled={!day || !month}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{lang('Save')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const getMaxMonthDay = (month?: number, year?: number) => {
|
||||
if (!month) return 31;
|
||||
if (month === 2) {
|
||||
const isLeapYear = year ? year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) : true;
|
||||
return isLeapYear ? 29 : 28;
|
||||
}
|
||||
if (month === 4 || month === 6 || month === 9 || month === 11) {
|
||||
return 30;
|
||||
}
|
||||
return 31;
|
||||
};
|
||||
|
||||
export default memo(BirthdaySetupModal);
|
||||
@ -1142,7 +1142,7 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
textParts={lang(
|
||||
'AreYouSureShareMyContactInfoBot',
|
||||
undefined,
|
||||
{ withNodes: true, withMarkdown: true, renderTextFilters: ['br', 'emoji'],
|
||||
{ withNodes: true, withMarkdown: true, renderTextFilters: ['br'],
|
||||
})}
|
||||
confirmHandler={handleAcceptPhone}
|
||||
confirmLabel={lang('ContactShare')}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type {
|
||||
ChangeEvent, FormEvent,
|
||||
} from 'react';
|
||||
import type { ElementRef, FC } from '../../lib/teact/teact';
|
||||
import type { ElementRef } from '../../lib/teact/teact';
|
||||
import { memo } from '../../lib/teact/teact';
|
||||
|
||||
import { IS_TAURI } from '../../util/browser/globalEnvironment';
|
||||
@ -31,9 +31,10 @@ type OwnProps = {
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
||||
onPaste?: (e: React.ClipboardEvent<HTMLInputElement>) => void;
|
||||
onClick?: (e: React.MouseEvent<HTMLInputElement>) => void;
|
||||
};
|
||||
|
||||
const InputText: FC<OwnProps> = ({
|
||||
const InputText = ({
|
||||
ref,
|
||||
id,
|
||||
className,
|
||||
@ -55,7 +56,8 @@ const InputText: FC<OwnProps> = ({
|
||||
onKeyDown,
|
||||
onBlur,
|
||||
onPaste,
|
||||
}) => {
|
||||
onClick,
|
||||
}: OwnProps) => {
|
||||
const lang = useLang();
|
||||
const labelText = error || success || label;
|
||||
const fullClassName = buildClassName(
|
||||
@ -93,6 +95,7 @@ const InputText: FC<OwnProps> = ({
|
||||
onPaste={onPaste}
|
||||
aria-label={labelText}
|
||||
teactExperimentControlled={teactExperimentControlled}
|
||||
onClick={onClick}
|
||||
/>
|
||||
{labelText && (
|
||||
<label htmlFor={id}>{labelText}</label>
|
||||
|
||||
@ -103,6 +103,17 @@ addActionHandler('updateProfile', async (global, actions, payload): Promise<void
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('updateBirthday', async (global, actions, payload): Promise<void> => {
|
||||
const { birthday } = payload;
|
||||
const { currentUserId } = global;
|
||||
if (!currentUserId) return;
|
||||
|
||||
const result = await callApi('updateBirthday', birthday);
|
||||
if (!result) return;
|
||||
|
||||
actions.loadFullUser({ userId: currentUserId });
|
||||
});
|
||||
|
||||
addActionHandler('updateProfilePhoto', async (global, actions, payload): Promise<void> => {
|
||||
const { photo, isFallback } = payload;
|
||||
const { currentUserId } = global;
|
||||
@ -395,7 +406,12 @@ addActionHandler('loadLanguages', async (global): Promise<void> => {
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('loadPrivacySettings', async (global): Promise<void> => {
|
||||
addActionHandler('loadPrivacySettings', async (global, actions, payload): Promise<void> => {
|
||||
const { skipIfCached } = payload;
|
||||
if (skipIfCached && Object.keys(global.settings.privacy).length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectIsCurrentUserFrozen(global)) return;
|
||||
|
||||
const result = await Promise.all([
|
||||
@ -642,7 +658,7 @@ addActionHandler('ensureTimeFormat', async (global, actions): Promise<void> => {
|
||||
addActionHandler('loadAppConfig', async (global, actions, payload): Promise<void> => {
|
||||
const hash = payload?.hash;
|
||||
|
||||
const appConfig = await callApi('fetchAppConfig', hash);
|
||||
const appConfig = await callApi('fetchAppConfig', { hash });
|
||||
if (!appConfig) return;
|
||||
|
||||
requestActionTimeout({
|
||||
@ -677,6 +693,33 @@ addActionHandler('loadConfig', async (global): Promise<void> => {
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('loadPromoData', async (global): Promise<void> => {
|
||||
const promoData = await callApi('fetchPromoData');
|
||||
if (!promoData) return;
|
||||
|
||||
global = getGlobal();
|
||||
const timeout = promoData.expires - getServerTime();
|
||||
if (timeout > 0) {
|
||||
requestActionTimeout({
|
||||
action: 'loadPromoData',
|
||||
payload: undefined,
|
||||
}, timeout * 1000);
|
||||
}
|
||||
|
||||
global = {
|
||||
...global,
|
||||
promoData,
|
||||
};
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('dismissSuggestion', async (global, actions, payload): Promise<void> => {
|
||||
const { suggestion } = payload;
|
||||
await callApi('dismissSuggestion', suggestion);
|
||||
|
||||
actions.loadPromoData();
|
||||
});
|
||||
|
||||
addActionHandler('loadPeerColors', async (global): Promise<void> => {
|
||||
const generalHash = global.peerColors?.generalHash;
|
||||
const profileHash = global.peerColors?.profileHash;
|
||||
|
||||
@ -21,6 +21,7 @@ import * as langProvider from '../../../util/oldLangProvider';
|
||||
import updateIcon from '../../../util/updateIcon';
|
||||
import { setPageTitle, setPageTitleInstant } from '../../../util/updatePageTitle';
|
||||
import { getAllowedAttachmentOptions, getChatTitle } from '../../helpers';
|
||||
import { addTabStateResetterAction } from '../../helpers/meta';
|
||||
import {
|
||||
addActionHandler, getActions, getGlobal, setGlobal,
|
||||
} from '../../index';
|
||||
@ -894,6 +895,17 @@ addActionHandler('closeCollectibleInfoModal', (global, actions, payload): Action
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('openBirthdaySetupModal', (global, actions, payload): ActionReturnType => {
|
||||
const { currentBirthday, tabId = getCurrentTabId() } = payload || {};
|
||||
return updateTabState(global, {
|
||||
birthdaySetupModal: {
|
||||
currentBirthday,
|
||||
},
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addTabStateResetterAction('closeBirthdaySetupModal', 'birthdaySetupModal');
|
||||
|
||||
addActionHandler('setShouldCloseRightColumn', (global, actions, payload): ActionReturnType => {
|
||||
const { value, tabId = getCurrentTabId() } = payload;
|
||||
return updateTabState(global, {
|
||||
|
||||
@ -163,6 +163,8 @@ addActionHandler('openLeftColumnContent', (global, actions, payload): ActionRetu
|
||||
addActionHandler('openSettingsScreen', (global, actions, payload): ActionReturnType => {
|
||||
const { screen, tabId = getCurrentTabId() } = payload;
|
||||
const tabState = selectTabState(global, tabId);
|
||||
|
||||
actions.loadPrivacySettings({ skipIfCached: true });
|
||||
// Force settings only if new screen is passed, do not on resets
|
||||
if (payload.screen !== undefined) actions.openLeftColumnContent({ contentKey: LeftColumnContent.Settings, tabId });
|
||||
return updateTabState(global, {
|
||||
|
||||
@ -11,3 +11,9 @@ export function selectSharedSettings<T extends GlobalState>(
|
||||
) {
|
||||
return selectSharedState(global).settings;
|
||||
}
|
||||
|
||||
export function selectAnimationLevel<T extends GlobalState>(
|
||||
global: T,
|
||||
) {
|
||||
return selectSharedSettings(global).animationLevel;
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type {
|
||||
ApiAttachBot,
|
||||
ApiAttachment,
|
||||
ApiBirthday,
|
||||
ApiChat,
|
||||
ApiChatAdminRights,
|
||||
ApiChatBannedRights,
|
||||
@ -249,7 +250,9 @@ export interface ActionPayloads {
|
||||
notificationSoundVolume?: number;
|
||||
};
|
||||
loadLanguages: undefined;
|
||||
loadPrivacySettings: undefined;
|
||||
loadPrivacySettings: {
|
||||
skipIfCached?: boolean;
|
||||
};
|
||||
setPrivacyVisibility: {
|
||||
privacyKey: ApiPrivacyKey;
|
||||
visibility: PrivacyVisibility;
|
||||
@ -1831,6 +1834,13 @@ export interface ActionPayloads {
|
||||
bio?: string;
|
||||
username?: string;
|
||||
} & WithTabId;
|
||||
updateBirthday: {
|
||||
birthday?: ApiBirthday;
|
||||
};
|
||||
openBirthdaySetupModal: {
|
||||
currentBirthday?: ApiBirthday;
|
||||
} & WithTabId;
|
||||
closeBirthdaySetupModal: WithTabId | undefined;
|
||||
updateBotProfile: {
|
||||
photo?: File;
|
||||
firstName?: string;
|
||||
@ -2452,6 +2462,10 @@ export interface ActionPayloads {
|
||||
loadAppConfig: {
|
||||
hash: number;
|
||||
} | undefined;
|
||||
loadPromoData: undefined;
|
||||
dismissSuggestion: {
|
||||
suggestion: string;
|
||||
} & WithTabId;
|
||||
loadPeerColors: undefined;
|
||||
loadTimezones: undefined;
|
||||
openLeftColumnContent: {
|
||||
|
||||
@ -24,6 +24,7 @@ import type {
|
||||
ApiPoll,
|
||||
ApiPrivacyKey,
|
||||
ApiPrivacySettings,
|
||||
ApiPromoData,
|
||||
ApiQuickReply,
|
||||
ApiReaction,
|
||||
ApiReactionKey,
|
||||
@ -81,6 +82,7 @@ export type GlobalState = {
|
||||
isInited: boolean;
|
||||
config?: ApiConfig;
|
||||
appConfig: ApiAppConfig;
|
||||
promoData?: ApiPromoData;
|
||||
peerColors?: ApiPeerColors;
|
||||
timezones?: {
|
||||
byId: Record<string, ApiTimezone>;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type {
|
||||
ApiAttachBot,
|
||||
ApiBirthday,
|
||||
ApiBoost,
|
||||
ApiBoostsStatus,
|
||||
ApiChannelMonetizationStatistics,
|
||||
@ -783,6 +784,10 @@ export type TabState = {
|
||||
|
||||
isAgeVerificationModalOpen?: boolean;
|
||||
|
||||
birthdaySetupModal?: {
|
||||
currentBirthday?: ApiBirthday;
|
||||
};
|
||||
|
||||
paidReactionModal?: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
|
||||
@ -1586,6 +1586,7 @@ account.updateEmojiStatus#fbd3de6b emoji_status:EmojiStatus = Bool;
|
||||
account.getRecentEmojiStatuses#f578105 hash:long = account.EmojiStatuses;
|
||||
account.reorderUsernames#ef500eab order:Vector<string> = Bool;
|
||||
account.toggleUsername#58d6b376 username:string active:Bool = Bool;
|
||||
account.updateBirthday#cc6e0c11 flags:# birthday:flags.0?Birthday = Bool;
|
||||
account.resolveBusinessChatLink#5492e5ee slug:string = account.ResolvedBusinessChatLinks;
|
||||
account.toggleSponsoredMessages#b9d9a38d enabled:Bool = Bool;
|
||||
account.getCollectibleEmojiStatuses#2e7b4543 hash:long = account.EmojiStatuses;
|
||||
@ -1781,6 +1782,8 @@ help.getNearestDc#1fb33026 = NearestDc;
|
||||
help.getSupport#9cdf08cd = help.Support;
|
||||
help.acceptTermsOfService#ee72f79a id:DataJSON = Bool;
|
||||
help.getAppConfig#61e3f854 hash:int = help.AppConfig;
|
||||
help.getPromoData#c0977421 = help.PromoData;
|
||||
help.dismissSuggestion#f50dbaa1 peer:InputPeer suggestion:string = Bool;
|
||||
help.getCountriesList#735787a8 lang_code:string hash:int = help.CountriesList;
|
||||
help.getPremiumPromo#b81b93d4 = help.PremiumPromo;
|
||||
help.getPeerColors#da80f42f hash:int = help.PeerColors;
|
||||
|
||||
@ -58,6 +58,7 @@
|
||||
"account.getRecentEmojiStatuses",
|
||||
"account.reorderUsernames",
|
||||
"account.toggleUsername",
|
||||
"account.updateBirthday",
|
||||
"account.resolveBusinessChatLink",
|
||||
"account.toggleSponsoredMessages",
|
||||
"account.getCollectibleEmojiStatuses",
|
||||
@ -251,14 +252,16 @@
|
||||
"upload.getWebFile",
|
||||
"help.getConfig",
|
||||
"help.getNearestDc",
|
||||
"help.getAppConfig",
|
||||
"help.getSupport",
|
||||
"help.acceptTermsOfService",
|
||||
"help.getPromoData",
|
||||
"help.getCountriesList",
|
||||
"help.getAppConfig",
|
||||
"help.getPremiumPromo",
|
||||
"help.dismissSuggestion",
|
||||
"help.getPeerColors",
|
||||
"help.getPeerProfileColors",
|
||||
"help.getTimezonesList",
|
||||
"help.getPremiumPromo",
|
||||
"channels.readHistory",
|
||||
"channels.deleteMessages",
|
||||
"channels.deleteParticipantHistory",
|
||||
|
||||
@ -121,7 +121,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@mixin header-pane {
|
||||
@mixin middle-header-pane {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transform: translateY(-100%);
|
||||
@ -169,6 +169,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
@mixin chat-list-pane {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transform: translateY(calc(-100% - 0.5rem)); // Include top margin to hide fully
|
||||
|
||||
width: 100%;
|
||||
padding: 0.5625rem;
|
||||
|
||||
background-color: var(--color-background);
|
||||
|
||||
transition: transform var(--chat-transform-transition);
|
||||
|
||||
// Some panels might unmount without animation, so we provide same background above panel to make it less noticeable
|
||||
&::after {
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: -100%;
|
||||
right: 0;
|
||||
left: 0;
|
||||
|
||||
height: inherit;
|
||||
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin side-panel-section {
|
||||
border-bottom: 0.625rem solid var(--color-background-secondary);
|
||||
background-color: var(--color-background);
|
||||
|
||||
@ -301,6 +301,7 @@ $color-message-story-mention-to: #74bcff;
|
||||
--layer-transition-behind: 300ms cubic-bezier(0.33, 1, 0.68, 1);
|
||||
--slide-transition: 300ms cubic-bezier(0.25, 1, 0.5, 1);
|
||||
--select-transition: 200ms ease-out;
|
||||
--chat-transform-transition: 0.2s ease-out;
|
||||
|
||||
--safe-area-top: env(safe-area-inset-top);
|
||||
--safe-area-right: env(safe-area-inset-right);
|
||||
|
||||
@ -233,6 +233,10 @@ body:not(.is-ios) {
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.flex-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@include iconsMixins.icon;
|
||||
}
|
||||
|
||||
32
src/types/language.d.ts
vendored
32
src/types/language.d.ts
vendored
@ -1230,6 +1230,7 @@ export interface LangPair {
|
||||
'MenuNightMode': undefined;
|
||||
'AriaMenuEnableNightMode': undefined;
|
||||
'AriaMenuDisableNightMode': undefined;
|
||||
'AriaSettingsEditProfilePhoto': undefined;
|
||||
'MenuAnimationsSwitch': undefined;
|
||||
'MenuTelegramFeatures': undefined;
|
||||
'TelegramFeaturesUsername': undefined;
|
||||
@ -1787,6 +1788,19 @@ export interface LangPair {
|
||||
'StarGiftPriceDecreaseInfoLink': undefined;
|
||||
'StarGiftUpgradeCostModalTitle': undefined;
|
||||
'StarGiftUpgradeCostHint': undefined;
|
||||
'UnconfirmedAuthDeniedTitle': undefined;
|
||||
'UnconfirmedAuthTitle': undefined;
|
||||
'UnconfirmedAuthConfirm': undefined;
|
||||
'UnconfirmedAuthDeny': undefined;
|
||||
'SuggestionBirthdaySetupTitle': undefined;
|
||||
'SuggestionBirthdaySetupMessage': undefined;
|
||||
'BirthdaySetupTitle': undefined;
|
||||
'BirthdayInputDay': undefined;
|
||||
'BirthdayInputMonth': undefined;
|
||||
'BirthdayInputYear': undefined;
|
||||
'BirthdayRemove': undefined;
|
||||
'BirthdayPrivacySuggestionLink': undefined;
|
||||
'SettingsBirthday': undefined;
|
||||
}
|
||||
|
||||
export interface LangPairWithVariables<V = LangVariable> {
|
||||
@ -3084,6 +3098,24 @@ export interface LangPairWithVariables<V = LangVariable> {
|
||||
'StarGiftPriceDecreaseTimer': {
|
||||
'timer': V;
|
||||
};
|
||||
'UnconfirmedAuthDeniedMessage': {
|
||||
'location': V;
|
||||
};
|
||||
'UnconfirmedAuthSingle': {
|
||||
'location': V;
|
||||
};
|
||||
'UnconfirmedAuthLocationRegion': {
|
||||
'deviceModel': V;
|
||||
'region': V;
|
||||
'country': V;
|
||||
};
|
||||
'UnconfirmedAuthLocationCountry': {
|
||||
'deviceModel': V;
|
||||
'country': V;
|
||||
};
|
||||
'BirthdayPrivacySuggestion': {
|
||||
'link': V;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LangPairPlural {
|
||||
|
||||
@ -32,7 +32,7 @@ import { notifyLangpackUpdate } from '../browser/multitab';
|
||||
import { createCallbackManager } from '../callbacks';
|
||||
import readFallbackStrings from '../data/readFallbackStrings';
|
||||
import { initialEstablishmentPromise, isCurrentTabMaster } from '../establishMultitabRole';
|
||||
import { omit, unique } from '../iteratees';
|
||||
import { omit } from '../iteratees';
|
||||
import { replaceInStringsWithTeact } from '../replaceWithTeact';
|
||||
import { fastRaf } from '../schedulers';
|
||||
|
||||
@ -411,40 +411,46 @@ function processTranslationAdvanced(
|
||||
|
||||
const variableEntries = variables ? Object.entries(variables) : [];
|
||||
|
||||
let tempResult: TeactNode = [string];
|
||||
let tempResult: TeactNode = string;
|
||||
if (options?.specialReplacement) {
|
||||
const specialReplacements = Object.entries(options.specialReplacement);
|
||||
tempResult = specialReplacements.reduce((acc, [key, value]) => {
|
||||
return replaceInStringsWithTeact(acc, key, value);
|
||||
}, tempResult);
|
||||
}, tempResult as TeactNode);
|
||||
}
|
||||
|
||||
const withRenderText = options?.withMarkdown || options?.renderTextFilters;
|
||||
const withRenderText = options?.withNodes;
|
||||
|
||||
if (withRenderText) {
|
||||
const filters = options?.withMarkdown
|
||||
? unique((options.renderTextFilters || []).concat(['simple_markdown', 'emoji']))
|
||||
: options.renderTextFilters;
|
||||
const textFiltersSet = new Set(options?.renderTextFilters);
|
||||
textFiltersSet.add('emoji');
|
||||
|
||||
return tempResult.flatMap((curr: TeactNode) => {
|
||||
if (options?.withMarkdown) {
|
||||
textFiltersSet.add('simple_markdown');
|
||||
}
|
||||
|
||||
const filters = Array.from(textFiltersSet);
|
||||
|
||||
const tempResultArray = Array.isArray(tempResult) ? tempResult : [tempResult];
|
||||
return tempResultArray.flatMap((curr: TeactNode) => {
|
||||
if (typeof curr !== 'string') {
|
||||
return curr;
|
||||
}
|
||||
|
||||
return renderText(curr, filters, {
|
||||
markdownPostProcessor: (part: string) => {
|
||||
return variableEntries.reduce((result, [key, value]): TeactNode[] => {
|
||||
return variableEntries.reduce((result, [key, value]): TeactNode => {
|
||||
if (value === undefined) return result;
|
||||
|
||||
const preparedValue = Number.isFinite(value) ? formatters!.number.format(value as number) : value;
|
||||
return replaceInStringsWithTeact(result, `{${key}}`, renderText(preparedValue));
|
||||
}, [part] as TeactNode[]);
|
||||
}, part as TeactNode);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return variableEntries.reduce((result, [key, value]): TeactNode[] => {
|
||||
return variableEntries.reduce((result, [key, value]): TeactNode => {
|
||||
if (value === undefined) return result;
|
||||
|
||||
const preparedValue = Number.isFinite(value) ? formatters!.number.format(value as number) : value;
|
||||
|
||||
@ -12,10 +12,14 @@ export function replaceWithTeact(
|
||||
}
|
||||
|
||||
export function replaceInStringsWithTeact(
|
||||
input: TeactNode[], searchValue: string | RegExp, replaceValue: TeactNode,
|
||||
) {
|
||||
input: TeactNode, searchValue: string | RegExp, replaceValue: TeactNode,
|
||||
): TeactNode {
|
||||
if (typeof input === 'string') return replaceWithTeact(input, searchValue, replaceValue);
|
||||
if (!input || !Array.isArray(input)) return input;
|
||||
|
||||
return input.flatMap((curr: TeactNode) => {
|
||||
if (typeof curr === 'string') return replaceWithTeact(curr, searchValue, replaceValue);
|
||||
if (Array.isArray(curr)) return replaceInStringsWithTeact(curr, searchValue, replaceValue);
|
||||
return curr;
|
||||
}, []);
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user