Chat List: Show suggestions (#6521)
This commit is contained in:
parent
ee78ee06b8
commit
f0df7a01e9
@ -7,7 +7,9 @@ import type {
|
|||||||
ApiCountry,
|
ApiCountry,
|
||||||
ApiLanguage,
|
ApiLanguage,
|
||||||
ApiOldLangString,
|
ApiOldLangString,
|
||||||
|
ApiPendingSuggestion,
|
||||||
ApiPrivacyKey,
|
ApiPrivacyKey,
|
||||||
|
ApiPromoData,
|
||||||
ApiRestrictionReason,
|
ApiRestrictionReason,
|
||||||
ApiSession,
|
ApiSession,
|
||||||
ApiTimezone,
|
ApiTimezone,
|
||||||
@ -22,6 +24,7 @@ import {
|
|||||||
} from '../../../util/iteratees';
|
} from '../../../util/iteratees';
|
||||||
import { toJSNumber } from '../../../util/numbers';
|
import { toJSNumber } from '../../../util/numbers';
|
||||||
import { addUserToLocalDb } from '../helpers/localDb';
|
import { addUserToLocalDb } from '../helpers/localDb';
|
||||||
|
import { buildApiFormattedText } from './common';
|
||||||
import { omitVirtualClassFields } from './helpers';
|
import { omitVirtualClassFields } from './helpers';
|
||||||
import { buildApiDocument, buildMessageTextContent } from './messageContent';
|
import { buildApiDocument, buildMessageTextContent } from './messageContent';
|
||||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
|
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) {
|
export function oldBuildLangPack(mtpLangPack: GramJs.LangPackDifference) {
|
||||||
return mtpLangPack.strings.reduce<Record<string, ApiOldLangString | undefined>>((acc, mtpString) => {
|
return mtpLangPack.strings.reduce<Record<string, ApiOldLangString | undefined>>((acc, mtpString) => {
|
||||||
acc[mtpString.key] = oldBuildLangPackString(mtpString);
|
acc[mtpString.key] = oldBuildLangPackString(mtpString);
|
||||||
|
|||||||
@ -46,3 +46,5 @@ export * from './fragment';
|
|||||||
export * from './stars';
|
export * from './stars';
|
||||||
|
|
||||||
export * from './forum';
|
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 { LANG_PACKS } from '../../../config';
|
||||||
import type {
|
import type {
|
||||||
ApiAppConfig,
|
ApiBirthday,
|
||||||
ApiConfig,
|
|
||||||
ApiDisallowedGiftsSettings,
|
ApiDisallowedGiftsSettings,
|
||||||
ApiInputPrivacyRules,
|
ApiInputPrivacyRules,
|
||||||
ApiLanguage,
|
ApiLanguage,
|
||||||
@ -24,11 +23,9 @@ import {
|
|||||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||||
import { toJSNumber } from '../../../util/numbers';
|
import { toJSNumber } from '../../../util/numbers';
|
||||||
import { BLOCKED_LIST_LIMIT } from '../../../limits';
|
import { BLOCKED_LIST_LIMIT } from '../../../limits';
|
||||||
import { buildAppConfig } from '../apiBuilders/appConfig';
|
|
||||||
import { buildApiPhoto, buildPrivacyRules } from '../apiBuilders/common';
|
import { buildApiPhoto, buildPrivacyRules } from '../apiBuilders/common';
|
||||||
import { buildApiDisallowedGiftsSettings } from '../apiBuilders/gifts';
|
import { buildApiDisallowedGiftsSettings } from '../apiBuilders/gifts';
|
||||||
import {
|
import {
|
||||||
buildApiConfig,
|
|
||||||
buildApiCountryList,
|
buildApiCountryList,
|
||||||
buildApiLanguage,
|
buildApiLanguage,
|
||||||
buildApiSession,
|
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) {
|
export async function updateProfilePhoto(photo?: ApiPhoto, isFallback?: boolean) {
|
||||||
const photoId = photo && buildInputPhoto(photo);
|
const photoId = photo && buildInputPhoto(photo);
|
||||||
const result = await invokeRequest(new GramJs.photos.UpdateProfilePhoto({
|
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) {
|
export async function fetchPeerColors(hash?: number) {
|
||||||
const result = await invokeRequest(new GramJs.help.GetPeerColors({
|
const result = await invokeRequest(new GramJs.help.GetPeerColors({
|
||||||
hash: hash ?? DEFAULT_PRIMITIVES.INT,
|
hash: hash ?? DEFAULT_PRIMITIVES.INT,
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import type { TeactNode } from '../../lib/teact/teact';
|
|||||||
import type { CallbackAction } from '../../global/types';
|
import type { CallbackAction } from '../../global/types';
|
||||||
import type { IconName } from '../../types/icons';
|
import type { IconName } from '../../types/icons';
|
||||||
import type { RegularLangFnParameters } from '../../util/localization';
|
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 { ApiPremiumSection } from './payments';
|
||||||
import type { ApiBotVerification } from './peers';
|
import type { ApiBotVerification } from './peers';
|
||||||
import type { ApiStarsSubscriptionPricing } from './stars';
|
import type { ApiStarsSubscriptionPricing } from './stars';
|
||||||
@ -290,6 +290,20 @@ export interface ApiConfig {
|
|||||||
maxForwardedCount: number;
|
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 {
|
export interface ApiTimezone {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@ -1443,6 +1443,7 @@
|
|||||||
"MenuNightMode" = "Night Mode";
|
"MenuNightMode" = "Night Mode";
|
||||||
"AriaMenuEnableNightMode" = "Enable night mode";
|
"AriaMenuEnableNightMode" = "Enable night mode";
|
||||||
"AriaMenuDisableNightMode" = "Disable night mode";
|
"AriaMenuDisableNightMode" = "Disable night mode";
|
||||||
|
"AriaSettingsEditProfilePhoto" = "Edit profile photo";
|
||||||
"MenuAnimationsSwitch" = "Animations";
|
"MenuAnimationsSwitch" = "Animations";
|
||||||
"MenuTelegramFeatures" = "Telegram Features";
|
"MenuTelegramFeatures" = "Telegram Features";
|
||||||
"TelegramFeaturesUsername" = "TelegramTips";
|
"TelegramFeaturesUsername" = "TelegramTips";
|
||||||
@ -2405,3 +2406,21 @@
|
|||||||
"StarGiftUpgradeCostModalTitle" = "Upgrade Cost";
|
"StarGiftUpgradeCostModalTitle" = "Upgrade Cost";
|
||||||
"StarGiftUpgradeCostHint" = "Users who upgrade their gifts first get collectibles with shorter numbers.";
|
"StarGiftUpgradeCostHint" = "Users who upgrade their gifts first get collectibles with shorter numbers.";
|
||||||
"StarGiftPriceDecreaseTimer" = "Price decreases in {timer}";
|
"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 AgeVerificationModal } from '../components/modals/ageVerification/AgeVerificationModal';
|
||||||
export { default as ChatlistModal } from '../components/modals/chatlist/ChatlistModal';
|
export { default as ChatlistModal } from '../components/modals/chatlist/ChatlistModal';
|
||||||
export { default as ChatInviteModal } from '../components/modals/chatInvite/ChatInviteModal';
|
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 AboutAdsModal } from '../components/modals/aboutAds/AboutAdsModal';
|
||||||
export { default as AboutMonetizationModal } from '../components/common/AboutMonetizationModal';
|
export { default as AboutMonetizationModal } from '../components/common/AboutMonetizationModal';
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
@use "../../../styles/mixins";
|
@use "../../../styles/mixins";
|
||||||
|
|
||||||
.GroupCallTopPane {
|
.GroupCallTopPane {
|
||||||
@include mixins.header-pane;
|
@include mixins.middle-header-pane;
|
||||||
|
|
||||||
cursor: var(--custom-cursor, pointer);
|
cursor: var(--custom-cursor, pointer);
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import Search from '../../../assets/tgs/Search.tgs';
|
|||||||
import SearchingDuck from '../../../assets/tgs/SearchingDuck.tgs';
|
import SearchingDuck from '../../../assets/tgs/SearchingDuck.tgs';
|
||||||
import Congratulations from '../../../assets/tgs/settings/Congratulations.tgs';
|
import Congratulations from '../../../assets/tgs/settings/Congratulations.tgs';
|
||||||
import DiscussionGroups from '../../../assets/tgs/settings/DiscussionGroupsDucks.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 Experimental from '../../../assets/tgs/settings/Experimental.tgs';
|
||||||
import FoldersAll from '../../../assets/tgs/settings/FoldersAll.tgs';
|
import FoldersAll from '../../../assets/tgs/settings/FoldersAll.tgs';
|
||||||
import FoldersNew from '../../../assets/tgs/settings/FoldersNew.tgs';
|
import FoldersNew from '../../../assets/tgs/settings/FoldersNew.tgs';
|
||||||
@ -81,4 +82,5 @@ export const LOCAL_TGS_URLS = {
|
|||||||
Diamond,
|
Diamond,
|
||||||
Search,
|
Search,
|
||||||
DuckNothingFound,
|
DuckNothingFound,
|
||||||
|
DuckCake,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -40,34 +40,34 @@ export default function renderText(
|
|||||||
return compact(filters.reduce((text, filter) => {
|
return compact(filters.reduce((text, filter) => {
|
||||||
switch (filter) {
|
switch (filter) {
|
||||||
case 'escape_html':
|
case 'escape_html':
|
||||||
return escapeHtml(text);
|
return escapeHtml(text, params?.markdownPostProcessor);
|
||||||
|
|
||||||
case 'hq_emoji':
|
case 'hq_emoji':
|
||||||
EMOJI_REGEX.lastIndex = 0;
|
EMOJI_REGEX.lastIndex = 0;
|
||||||
return replaceEmojis(text, 'big', 'jsx');
|
return replaceEmojis(text, 'big', 'jsx', params?.markdownPostProcessor);
|
||||||
|
|
||||||
case 'emoji':
|
case 'emoji':
|
||||||
EMOJI_REGEX.lastIndex = 0;
|
EMOJI_REGEX.lastIndex = 0;
|
||||||
return replaceEmojis(text, 'small', 'jsx');
|
return replaceEmojis(text, 'small', 'jsx', params?.markdownPostProcessor);
|
||||||
|
|
||||||
case 'emoji_html':
|
case 'emoji_html':
|
||||||
EMOJI_REGEX.lastIndex = 0;
|
EMOJI_REGEX.lastIndex = 0;
|
||||||
return replaceEmojis(text, 'small', 'html');
|
return replaceEmojis(text, 'small', 'html');
|
||||||
|
|
||||||
case 'br':
|
case 'br':
|
||||||
return addLineBreaks(text, 'jsx');
|
return addLineBreaks(text, 'jsx', params?.markdownPostProcessor);
|
||||||
|
|
||||||
case 'br_html':
|
case 'br_html':
|
||||||
return addLineBreaks(text, 'html');
|
return addLineBreaks(text, 'html');
|
||||||
|
|
||||||
case 'highlight':
|
case 'highlight':
|
||||||
return addHighlight(text, params!.highlight);
|
return addHighlight(text, params!.highlight, params?.markdownPostProcessor);
|
||||||
|
|
||||||
case 'links':
|
case 'links':
|
||||||
return addLinks(text);
|
return addLinks(text, undefined, params?.markdownPostProcessor);
|
||||||
|
|
||||||
case 'tg_links':
|
case 'tg_links':
|
||||||
return addLinks(text, true);
|
return addLinks(text, true, params?.markdownPostProcessor);
|
||||||
|
|
||||||
case 'simple_markdown':
|
case 'simple_markdown':
|
||||||
return replaceSimpleMarkdown(text, 'jsx', params?.markdownPostProcessor);
|
return replaceSimpleMarkdown(text, 'jsx', params?.markdownPostProcessor);
|
||||||
@ -80,7 +80,9 @@ export default function renderText(
|
|||||||
}, [part] as TextPart[]));
|
}, [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');
|
const divEl = document.createElement('div');
|
||||||
return textParts.reduce((result: TextPart[], part) => {
|
return textParts.reduce((result: TextPart[], part) => {
|
||||||
if (typeof part !== 'string') {
|
if (typeof part !== 'string') {
|
||||||
@ -89,15 +91,20 @@ function escapeHtml(textParts: TextPart[]): TextPart[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
divEl.innerText = part;
|
divEl.innerText = part;
|
||||||
result.push(divEl.innerHTML);
|
result.push(postProcess(divEl.innerHTML));
|
||||||
|
|
||||||
return result;
|
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) {
|
if (IS_EMOJI_SUPPORTED) {
|
||||||
return textParts;
|
return textParts.map((part) => typeof part === 'string' ? postProcess(part) : part);
|
||||||
}
|
}
|
||||||
|
|
||||||
return textParts.reduce((result: TextPart[], part: TextPart) => {
|
return textParts.reduce((result: TextPart[], part: TextPart) => {
|
||||||
@ -109,12 +116,12 @@ function replaceEmojis(textParts: TextPart[], size: 'big' | 'small', type: 'jsx'
|
|||||||
part = fixNonStandardEmoji(part);
|
part = fixNonStandardEmoji(part);
|
||||||
const parts = part.split(EMOJI_REGEX);
|
const parts = part.split(EMOJI_REGEX);
|
||||||
const emojis: string[] = part.match(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) => {
|
return emojis.reduce((emojiResult: TextPart[], emoji, i) => {
|
||||||
const code = nativeToUnifiedExtendedWithCache(emoji);
|
const code = nativeToUnifiedExtendedWithCache(emoji);
|
||||||
if (!code) {
|
if (!code) {
|
||||||
emojiResult.push(emoji);
|
emojiResult.push(postProcess(emoji));
|
||||||
} else {
|
} else {
|
||||||
const src = `./img-apple-${size === 'big' ? '160' : '64'}/${code}.png`;
|
const src = `./img-apple-${size === 'big' ? '160' : '64'}/${code}.png`;
|
||||||
const className = buildClassName(
|
const className = buildClassName(
|
||||||
@ -150,7 +157,7 @@ function replaceEmojis(textParts: TextPart[], size: 'big' | 'small', type: 'jsx'
|
|||||||
|
|
||||||
const index = i * 2 + 2;
|
const index = i * 2 + 2;
|
||||||
if (parts[index]) {
|
if (parts[index]) {
|
||||||
emojiResult.push(parts[index]);
|
emojiResult.push(postProcess(parts[index]));
|
||||||
}
|
}
|
||||||
|
|
||||||
return emojiResult;
|
return emojiResult;
|
||||||
@ -158,7 +165,11 @@ function replaceEmojis(textParts: TextPart[], size: 'big' | 'small', type: 'jsx'
|
|||||||
}, [] as TextPart[]);
|
}, [] 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) => {
|
return textParts.reduce((result: TextPart[], part) => {
|
||||||
if (typeof part !== 'string') {
|
if (typeof part !== 'string') {
|
||||||
result.push(part);
|
result.push(part);
|
||||||
@ -169,9 +180,10 @@ function addLineBreaks(textParts: TextPart[], type: 'jsx' | 'html'): TextPart[]
|
|||||||
.split(/\r\n|\r|\n/g)
|
.split(/\r\n|\r|\n/g)
|
||||||
.reduce((parts: TextPart[], line: string, i, source) => {
|
.reduce((parts: TextPart[], line: string, i, source) => {
|
||||||
// This adds non-breaking space if line was indented with spaces, to preserve the indentation
|
// 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;
|
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) {
|
if (i !== source.length - 1) {
|
||||||
parts.push(
|
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) => {
|
return textParts.reduce<TextPart[]>((result, part) => {
|
||||||
if (typeof part !== 'string' || !highlight) {
|
if (typeof part !== 'string' || !highlight) {
|
||||||
result.push(part);
|
result.push(part);
|
||||||
@ -196,25 +213,29 @@ function addHighlight(textParts: TextPart[], highlight: string | undefined): Tex
|
|||||||
const lowerCaseText = part.toLowerCase();
|
const lowerCaseText = part.toLowerCase();
|
||||||
const queryPosition = lowerCaseText.indexOf(highlight.toLowerCase());
|
const queryPosition = lowerCaseText.indexOf(highlight.toLowerCase());
|
||||||
if (queryPosition < 0) {
|
if (queryPosition < 0) {
|
||||||
result.push(part);
|
result.push(postProcess(part));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newParts: TextPart[] = [];
|
const newParts: TextPart[] = [];
|
||||||
newParts.push(part.substring(0, queryPosition));
|
newParts.push(postProcess(part.substring(0, queryPosition)));
|
||||||
newParts.push(
|
newParts.push(
|
||||||
<span className="matching-text-highlight">
|
<span className="matching-text-highlight">
|
||||||
{part.substring(queryPosition, queryPosition + highlight.length)}
|
{postProcess(part.substring(queryPosition, queryPosition + highlight.length))}
|
||||||
</span>,
|
</span>,
|
||||||
);
|
);
|
||||||
newParts.push(part.substring(queryPosition + highlight.length));
|
newParts.push(postProcess(part.substring(queryPosition + highlight.length)));
|
||||||
return [...result, ...newParts];
|
return [...result, ...newParts];
|
||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
const RE_LINK = new RegExp(`${RE_LINK_TEMPLATE}|${RE_MENTION_TEMPLATE}`, 'ig');
|
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) => {
|
return textParts.reduce<TextPart[]>((result, part) => {
|
||||||
if (typeof part !== 'string') {
|
if (typeof part !== 'string') {
|
||||||
result.push(part);
|
result.push(part);
|
||||||
@ -223,7 +244,7 @@ function addLinks(textParts: TextPart[], allowOnlyTgLinks?: boolean): TextPart[]
|
|||||||
|
|
||||||
const links = part.match(RE_LINK);
|
const links = part.match(RE_LINK);
|
||||||
if (!links || !links.length) {
|
if (!links || !links.length) {
|
||||||
result.push(part);
|
result.push(postProcess(part));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,11 +254,11 @@ function addLinks(textParts: TextPart[], allowOnlyTgLinks?: boolean): TextPart[]
|
|||||||
let lastIndex = 0;
|
let lastIndex = 0;
|
||||||
while (nextLink) {
|
while (nextLink) {
|
||||||
const index = part.indexOf(nextLink, lastIndex);
|
const index = part.indexOf(nextLink, lastIndex);
|
||||||
content.push(part.substring(lastIndex, index));
|
content.push(postProcess(part.substring(lastIndex, index)));
|
||||||
if (nextLink.startsWith('@')) {
|
if (nextLink.startsWith('@')) {
|
||||||
content.push(
|
content.push(
|
||||||
<MentionLink username={nextLink}>
|
<MentionLink username={nextLink}>
|
||||||
{nextLink}
|
{postProcess(nextLink)}
|
||||||
</MentionLink>,
|
</MentionLink>,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -250,13 +271,13 @@ function addLinks(textParts: TextPart[], allowOnlyTgLinks?: boolean): TextPart[]
|
|||||||
<SafeLink text={nextLink} url={nextLink} />,
|
<SafeLink text={nextLink} url={nextLink} />,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
content.push(nextLink);
|
content.push(postProcess(nextLink));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastIndex = index + nextLink.length;
|
lastIndex = index + nextLink.length;
|
||||||
nextLink = links.shift();
|
nextLink = links.shift();
|
||||||
}
|
}
|
||||||
content.push(part.substring(lastIndex));
|
content.push(postProcess(part.substring(lastIndex)));
|
||||||
|
|
||||||
return [...result, ...content];
|
return [...result, ...content];
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@ -401,6 +401,7 @@ const ChatExtra = ({
|
|||||||
<Chat
|
<Chat
|
||||||
chatId={personalChannel.id}
|
chatId={personalChannel.id}
|
||||||
orderDiff={0}
|
orderDiff={0}
|
||||||
|
shiftDiff={0}
|
||||||
animationType={ChatAnimationTypes.None}
|
animationType={ChatAnimationTypes.None}
|
||||||
isPreview
|
isPreview
|
||||||
previewMessageId={personalChannelMessageId}
|
previewMessageId={personalChannelMessageId}
|
||||||
|
|||||||
@ -66,13 +66,11 @@ const UserBirthday = ({
|
|||||||
age,
|
age,
|
||||||
} = useMemo(() => {
|
} = useMemo(() => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const date = new Date();
|
const date = new Date(
|
||||||
if (birthday.year) {
|
birthday.year || 2024, // Use leap year as fallback
|
||||||
date.setFullYear(birthday.year);
|
birthday.month - 1,
|
||||||
}
|
birthday.day,
|
||||||
date.setMonth(birthday.month - 1);
|
);
|
||||||
date.setDate(birthday.day);
|
|
||||||
date.setHours(0, 0, 0, 0);
|
|
||||||
|
|
||||||
const formatted = formatDateToString(date, lang.code, true, 'long');
|
const formatted = formatDateToString(date, lang.code, true, 'long');
|
||||||
const isBirthdayToday = date.getDate() === today.getDate() && date.getMonth() === today.getMonth();
|
const isBirthdayToday = date.getDate() === today.getDate() && date.getMonth() === today.getMonth();
|
||||||
|
|||||||
@ -1,11 +1,17 @@
|
|||||||
.root {
|
.root {
|
||||||
--background-color: var(--color-background);
|
--background-color: var(--color-background);
|
||||||
|
|
||||||
|
transition: transform var(--chat-transform-transition);
|
||||||
|
|
||||||
:global(.ListItem-button) {
|
:global(.ListItem-button) {
|
||||||
min-height: auto;
|
min-height: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.noAnimation {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
.minimized {
|
.minimized {
|
||||||
margin: -0.5rem -0.5rem 0 -0.5rem !important;
|
margin: -0.5rem -0.5rem 0 -0.5rem !important;
|
||||||
background-color: var(--color-background-secondary);
|
background-color: var(--color-background-secondary);
|
||||||
@ -14,7 +20,7 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.no-margin-top {
|
&.noMarginTop {
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +1,24 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
import { memo, useCallback, useMemo, useRef } from '../../../lib/teact/teact';
|
||||||
import { memo, useCallback, useMemo } from '../../../lib/teact/teact';
|
|
||||||
import { getActions, getGlobal } from '../../../global';
|
import { getActions, getGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { GlobalState } from '../../../global/types';
|
import type { GlobalState } from '../../../global/types';
|
||||||
import type { CustomPeer } from '../../../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 { getChatTitle } from '../../../global/helpers';
|
||||||
|
import { selectAnimationLevel } from '../../../global/selectors/sharedState';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import buildStyle from '../../../util/buildStyle';
|
||||||
|
import { waitForTransitionEnd } from '../../../util/cssAnimationEndListeners';
|
||||||
import { compact } from '../../../util/iteratees';
|
import { compact } from '../../../util/iteratees';
|
||||||
import { formatIntegerCompact } from '../../../util/textFormat';
|
import { formatIntegerCompact } from '../../../util/textFormat';
|
||||||
import renderText from '../../common/helpers/renderText';
|
import renderText from '../../common/helpers/renderText';
|
||||||
|
import { ChatAnimationTypes } from './hooks';
|
||||||
|
|
||||||
|
import useSelector from '../../../hooks/data/useSelector';
|
||||||
import { useFolderManagerForOrderedIds, useFolderManagerForUnreadCounters } from '../../../hooks/useFolderManager';
|
import { useFolderManagerForOrderedIds, useFolderManagerForUnreadCounters } from '../../../hooks/useFolderManager';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useSyncEffect from '../../../hooks/useSyncEffect';
|
||||||
|
|
||||||
import Avatar from '../../common/Avatar';
|
import Avatar from '../../common/Avatar';
|
||||||
import Icon from '../../common/icons/Icon';
|
import Icon from '../../common/icons/Icon';
|
||||||
@ -24,9 +29,11 @@ import styles from './Archive.module.scss';
|
|||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
archiveSettings: GlobalState['archiveSettings'];
|
archiveSettings: GlobalState['archiveSettings'];
|
||||||
|
isFoldersSidebarShown?: boolean;
|
||||||
|
offsetTop?: number;
|
||||||
|
animationType: ChatAnimationTypes;
|
||||||
onDragEnter?: NoneToVoidFunction;
|
onDragEnter?: NoneToVoidFunction;
|
||||||
onClick?: NoneToVoidFunction;
|
onClick?: NoneToVoidFunction;
|
||||||
isFoldersSidebarShown?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const PREVIEW_SLICE = 5;
|
const PREVIEW_SLICE = 5;
|
||||||
@ -37,15 +44,50 @@ const ARCHIVE_CUSTOM_PEER: CustomPeer = {
|
|||||||
customPeerAvatarColor: '#9EAAB5',
|
customPeerAvatarColor: '#9EAAB5',
|
||||||
};
|
};
|
||||||
|
|
||||||
const Archive: FC<OwnProps> = ({
|
const ANIMATION_RESET_DELAY = 200;
|
||||||
|
|
||||||
|
const Archive = ({
|
||||||
archiveSettings,
|
archiveSettings,
|
||||||
|
isFoldersSidebarShown,
|
||||||
|
offsetTop,
|
||||||
|
animationType,
|
||||||
onDragEnter,
|
onDragEnter,
|
||||||
onClick,
|
onClick,
|
||||||
isFoldersSidebarShown,
|
}: OwnProps) => {
|
||||||
}) => {
|
|
||||||
const { updateArchiveSettings } = getActions();
|
const { updateArchiveSettings } = getActions();
|
||||||
|
|
||||||
|
const ref = useRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
const animationLevel = useSelector(selectAnimationLevel);
|
||||||
|
const shouldAnimateRef = useRef(animationLevel !== ANIMATION_LEVEL_MIN);
|
||||||
const lang = useLang();
|
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 orderedChatIds = useFolderManagerForOrderedIds(ARCHIVED_FOLDER_ID);
|
||||||
const unreadCounters = useFolderManagerForUnreadCounters();
|
const unreadCounters = useFolderManagerForUnreadCounters();
|
||||||
const archiveUnreadCount = unreadCounters[ARCHIVED_FOLDER_ID]?.chatsCount;
|
const archiveUnreadCount = unreadCounters[ARCHIVED_FOLDER_ID]?.chatsCount;
|
||||||
@ -155,15 +197,19 @@ const Archive: FC<OwnProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
|
ref={ref}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onDragEnter={handleDragEnter}
|
onDragEnter={handleDragEnter}
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
styles.root,
|
styles.root,
|
||||||
archiveSettings.isMinimized && styles.minimized,
|
archiveSettings.isMinimized && styles.minimized,
|
||||||
isFoldersSidebarShown && archiveSettings.isMinimized && styles.noMarginTop,
|
isFoldersSidebarShown && archiveSettings.isMinimized && styles.noMarginTop,
|
||||||
|
!shouldAnimateRef.current && styles.noAnimation,
|
||||||
|
offsetTop && styles.noMarginTop,
|
||||||
'chat-item-clickable',
|
'chat-item-clickable',
|
||||||
'chat-item-archive',
|
'chat-item-archive',
|
||||||
)}
|
)}
|
||||||
|
style={buildStyle(Boolean(offsetTop) && `transform: translateY(${offsetTop}px)`)}
|
||||||
buttonClassName={styles.button}
|
buttonClassName={styles.button}
|
||||||
contextActions={contextActions}
|
contextActions={contextActions}
|
||||||
withPortalForMenu
|
withPortalForMenu
|
||||||
|
|||||||
@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
&.animate-transform {
|
&.animate-transform {
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
transition: transform 0.2s ease-out;
|
transition: transform var(--chat-transform-transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
|
|||||||
@ -87,6 +87,7 @@ type OwnProps = {
|
|||||||
chatId: string;
|
chatId: string;
|
||||||
folderId?: number;
|
folderId?: number;
|
||||||
orderDiff: number;
|
orderDiff: number;
|
||||||
|
shiftDiff: number;
|
||||||
animationType: ChatAnimationTypes;
|
animationType: ChatAnimationTypes;
|
||||||
isPinned?: boolean;
|
isPinned?: boolean;
|
||||||
offsetTop?: number;
|
offsetTop?: number;
|
||||||
@ -137,6 +138,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
|||||||
chatId,
|
chatId,
|
||||||
folderId,
|
folderId,
|
||||||
orderDiff,
|
orderDiff,
|
||||||
|
shiftDiff,
|
||||||
animationType,
|
animationType,
|
||||||
isPinned,
|
isPinned,
|
||||||
listedTopicIds,
|
listedTopicIds,
|
||||||
@ -239,6 +241,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
|||||||
animationType,
|
animationType,
|
||||||
withInterfaceAnimations,
|
withInterfaceAnimations,
|
||||||
orderDiff,
|
orderDiff,
|
||||||
|
shiftDiff,
|
||||||
isSavedDialog,
|
isSavedDialog,
|
||||||
isPreview,
|
isPreview,
|
||||||
onReorderAnimationEnd,
|
onReorderAnimationEnd,
|
||||||
|
|||||||
@ -2,13 +2,13 @@ import type { FC } from '@teact';
|
|||||||
import { memo, useEffect, useRef } from '@teact';
|
import { memo, useEffect, useRef } from '@teact';
|
||||||
import { getActions, withGlobal } from '../../../global';
|
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 { GlobalState } from '../../../global/types';
|
||||||
import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
|
import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
|
||||||
import type { AnimationLevel } from '../../../types';
|
import type { AnimationLevel } from '../../../types';
|
||||||
|
|
||||||
import { ALL_FOLDER_ID } from '../../../config';
|
import { ALL_FOLDER_ID } from '../../../config';
|
||||||
import { selectIsCurrentUserFrozen, selectTabState } from '../../../global/selectors';
|
import { selectTabState } from '../../../global/selectors';
|
||||||
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
||||||
import { selectSharedSettings } from '../../../global/selectors/sharedState';
|
import { selectSharedSettings } from '../../../global/selectors/sharedState';
|
||||||
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
|
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
|
||||||
@ -51,8 +51,6 @@ type StateProps = {
|
|||||||
hasArchivedStories?: boolean;
|
hasArchivedStories?: boolean;
|
||||||
archiveSettings: GlobalState['archiveSettings'];
|
archiveSettings: GlobalState['archiveSettings'];
|
||||||
isStoryRibbonShown?: boolean;
|
isStoryRibbonShown?: boolean;
|
||||||
sessions?: Record<string, ApiSession>;
|
|
||||||
isAccountFrozen?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const SAVED_MESSAGES_HOTKEY = '0';
|
const SAVED_MESSAGES_HOTKEY = '0';
|
||||||
@ -76,8 +74,6 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
|||||||
hasArchivedStories,
|
hasArchivedStories,
|
||||||
archiveSettings,
|
archiveSettings,
|
||||||
isStoryRibbonShown,
|
isStoryRibbonShown,
|
||||||
sessions,
|
|
||||||
isAccountFrozen,
|
|
||||||
isFoldersSidebarShown,
|
isFoldersSidebarShown,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
@ -232,8 +228,6 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
|||||||
isMainList
|
isMainList
|
||||||
canDisplayArchive={(hasArchivedChats || hasArchivedStories) && !archiveSettings.isHidden}
|
canDisplayArchive={(hasArchivedChats || hasArchivedStories) && !archiveSettings.isHidden}
|
||||||
archiveSettings={archiveSettings}
|
archiveSettings={archiveSettings}
|
||||||
sessions={sessions}
|
|
||||||
isAccountFrozen={isAccountFrozen}
|
|
||||||
isFoldersSidebarShown={isFoldersSidebarShown}
|
isFoldersSidebarShown={isFoldersSidebarShown}
|
||||||
isStoryRibbonShown={isStoryRibbonShown}
|
isStoryRibbonShown={isStoryRibbonShown}
|
||||||
withTags
|
withTags
|
||||||
@ -294,16 +288,12 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
archived: archivedStories,
|
archived: archivedStories,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
activeSessions: {
|
|
||||||
byHash: sessions,
|
|
||||||
},
|
|
||||||
currentUserId,
|
currentUserId,
|
||||||
archiveSettings,
|
archiveSettings,
|
||||||
} = global;
|
} = global;
|
||||||
const { animationLevel } = selectSharedSettings(global);
|
const { animationLevel } = selectSharedSettings(global);
|
||||||
const { shouldSkipHistoryAnimations, activeChatFolder } = selectTabState(global);
|
const { shouldSkipHistoryAnimations, activeChatFolder } = selectTabState(global);
|
||||||
const { storyViewer: { isRibbonShown: isStoryRibbonShown } } = selectTabState(global);
|
const { storyViewer: { isRibbonShown: isStoryRibbonShown } } = selectTabState(global);
|
||||||
const isAccountFrozen = selectIsCurrentUserFrozen(global);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chatFoldersById,
|
chatFoldersById,
|
||||||
@ -320,8 +310,6 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
maxChatLists: selectCurrentLimit(global, 'chatlistJoined'),
|
maxChatLists: selectCurrentLimit(global, 'chatlistJoined'),
|
||||||
archiveSettings,
|
archiveSettings,
|
||||||
isStoryRibbonShown,
|
isStoryRibbonShown,
|
||||||
sessions,
|
|
||||||
isAccountFrozen,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(ChatFolders));
|
)(ChatFolders));
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import type { FC } from '@teact';
|
|
||||||
import { memo, useEffect, useMemo, useRef, useState } from '@teact';
|
import { memo, useEffect, useMemo, useRef, useState } from '@teact';
|
||||||
import { getActions } from '../../../global';
|
import { getActions } from '../../../global';
|
||||||
|
|
||||||
import type { ApiSession } from '../../../api/types';
|
|
||||||
import type { GlobalState } from '../../../global/types';
|
import type { GlobalState } from '../../../global/types';
|
||||||
import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
|
import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
|
||||||
import { LeftColumnContent } from '../../../types';
|
import { LeftColumnContent } from '../../../types';
|
||||||
@ -13,14 +11,13 @@ import {
|
|||||||
ARCHIVED_FOLDER_ID,
|
ARCHIVED_FOLDER_ID,
|
||||||
CHAT_HEIGHT_PX,
|
CHAT_HEIGHT_PX,
|
||||||
CHAT_LIST_SLICE,
|
CHAT_LIST_SLICE,
|
||||||
FRESH_AUTH_PERIOD,
|
|
||||||
SAVED_FOLDER_ID,
|
SAVED_FOLDER_ID,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { IS_APP, IS_MAC_OS } from '../../../util/browser/windowEnvironment';
|
import { IS_APP, IS_MAC_OS } from '../../../util/browser/windowEnvironment';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
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 { getOrderKey, getPinnedChatsCount } from '../../../util/folderManager';
|
||||||
import { getServerTime } from '../../../util/serverTime';
|
import { ARCHIVE_ANIMATION_ID } from './hooks';
|
||||||
|
|
||||||
import usePeerStoriesPolling from '../../../hooks/polling/usePeerStoriesPolling';
|
import usePeerStoriesPolling from '../../../hooks/polling/usePeerStoriesPolling';
|
||||||
import useTopOverscroll from '../../../hooks/scroll/useTopOverscroll';
|
import useTopOverscroll from '../../../hooks/scroll/useTopOverscroll';
|
||||||
@ -36,8 +33,7 @@ import Loading from '../../ui/Loading';
|
|||||||
import Archive from './Archive';
|
import Archive from './Archive';
|
||||||
import Chat from './Chat';
|
import Chat from './Chat';
|
||||||
import EmptyFolder from './EmptyFolder';
|
import EmptyFolder from './EmptyFolder';
|
||||||
import FrozenAccountNotification from './FrozenAccountNotification';
|
import ChatListPanes from './panes/ChatListPanes';
|
||||||
import UnconfirmedSession from './UnconfirmedSession';
|
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -47,8 +43,6 @@ type OwnProps = {
|
|||||||
canDisplayArchive?: boolean;
|
canDisplayArchive?: boolean;
|
||||||
archiveSettings?: GlobalState['archiveSettings'];
|
archiveSettings?: GlobalState['archiveSettings'];
|
||||||
isForumPanelOpen?: boolean;
|
isForumPanelOpen?: boolean;
|
||||||
sessions?: Record<string, ApiSession>;
|
|
||||||
isAccountFrozen?: boolean;
|
|
||||||
isMainList?: boolean;
|
isMainList?: boolean;
|
||||||
withTags?: boolean;
|
withTags?: boolean;
|
||||||
isFoldersSidebarShown?: boolean;
|
isFoldersSidebarShown?: boolean;
|
||||||
@ -59,7 +53,7 @@ type OwnProps = {
|
|||||||
const INTERSECTION_THROTTLE = 200;
|
const INTERSECTION_THROTTLE = 200;
|
||||||
const RESERVED_HOTKEYS = new Set(['9', '0']);
|
const RESERVED_HOTKEYS = new Set(['9', '0']);
|
||||||
|
|
||||||
const ChatList: FC<OwnProps> = ({
|
const ChatList = ({
|
||||||
className,
|
className,
|
||||||
folderType,
|
folderType,
|
||||||
folderId,
|
folderId,
|
||||||
@ -67,24 +61,21 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
isForumPanelOpen,
|
isForumPanelOpen,
|
||||||
canDisplayArchive,
|
canDisplayArchive,
|
||||||
archiveSettings,
|
archiveSettings,
|
||||||
sessions,
|
|
||||||
isAccountFrozen,
|
|
||||||
isMainList,
|
isMainList,
|
||||||
withTags,
|
withTags,
|
||||||
isFoldersSidebarShown,
|
isFoldersSidebarShown,
|
||||||
isStoryRibbonShown,
|
isStoryRibbonShown,
|
||||||
foldersDispatch,
|
foldersDispatch,
|
||||||
}) => {
|
}: OwnProps) => {
|
||||||
const {
|
const {
|
||||||
openChat,
|
openChat,
|
||||||
openNextChat,
|
openNextChat,
|
||||||
closeForumPanel,
|
closeForumPanel,
|
||||||
toggleStoryRibbon,
|
toggleStoryRibbon,
|
||||||
openFrozenAccountModal,
|
|
||||||
openLeftColumnContent,
|
openLeftColumnContent,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
const containerRef = useRef<HTMLDivElement>();
|
const containerRef = useRef<HTMLDivElement>();
|
||||||
const [unconfirmedSessionHeight, setUnconfirmedSessionHeight] = useState(0);
|
const [panesHeight, setPanesHeight] = useState(0);
|
||||||
|
|
||||||
const isArchived = folderType === 'archived';
|
const isArchived = folderType === 'archived';
|
||||||
const isAllFolder = folderType === 'all';
|
const isAllFolder = folderType === 'all';
|
||||||
@ -94,7 +85,6 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const shouldDisplayArchive = isAllFolder && canDisplayArchive && archiveSettings;
|
const shouldDisplayArchive = isAllFolder && canDisplayArchive && archiveSettings;
|
||||||
const shouldShowFrozenAccountNotification = isAccountFrozen && isAllFolder;
|
|
||||||
|
|
||||||
const orderedIds = useFolderManagerForOrderedIds(resolvedFolderId);
|
const orderedIds = useFolderManagerForOrderedIds(resolvedFolderId);
|
||||||
usePeerStoriesPolling(orderedIds);
|
usePeerStoriesPolling(orderedIds);
|
||||||
@ -102,24 +92,13 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
const chatsHeight = (orderedIds?.length || 0) * CHAT_HEIGHT_PX;
|
const chatsHeight = (orderedIds?.length || 0) * CHAT_HEIGHT_PX;
|
||||||
const archiveHeight = shouldDisplayArchive
|
const archiveHeight = shouldDisplayArchive
|
||||||
? archiveSettings?.isMinimized ? ARCHIVE_MINIMIZED_HEIGHT : CHAT_HEIGHT_PX : 0;
|
? 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 [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
|
// Support <Alt>+<Up/Down> to navigate between chats
|
||||||
useHotkeys(useMemo(() => (isActive && orderedIds?.length ? {
|
useHotkeys(useMemo(() => (isActive && orderedIds?.length ? {
|
||||||
'Alt+ArrowUp': (e: KeyboardEvent) => {
|
'Alt+ArrowUp': (e: KeyboardEvent) => {
|
||||||
@ -178,10 +157,6 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
closeForumPanel();
|
closeForumPanel();
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleFrozenAccountNotificationClick = useLastCallback(() => {
|
|
||||||
openFrozenAccountModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleShowStoryRibbon = useLastCallback(() => {
|
const handleShowStoryRibbon = useLastCallback(() => {
|
||||||
toggleStoryRibbon({ isShown: true, isArchived });
|
toggleStoryRibbon({ isShown: true, isArchived });
|
||||||
});
|
});
|
||||||
@ -217,8 +192,7 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
|
|
||||||
return viewportIds!.map((id, i) => {
|
return viewportIds!.map((id, i) => {
|
||||||
const isPinned = viewportOffset + i < pinnedCount;
|
const isPinned = viewportOffset + i < pinnedCount;
|
||||||
const offsetTop = unconfirmedSessionHeight + archiveHeight + frozenNotificationHeight
|
const offsetTop = panesHeight + archiveHeight + (viewportOffset + i) * CHAT_HEIGHT_PX;
|
||||||
+ (viewportOffset + i) * CHAT_HEIGHT_PX;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Chat
|
<Chat
|
||||||
@ -230,6 +204,7 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
isSavedDialog={isSaved}
|
isSavedDialog={isSaved}
|
||||||
animationType={getAnimationType(id)}
|
animationType={getAnimationType(id)}
|
||||||
orderDiff={orderDiffById[id]}
|
orderDiff={orderDiffById[id]}
|
||||||
|
shiftDiff={shiftDiff}
|
||||||
onReorderAnimationEnd={onReorderAnimationEnd}
|
onReorderAnimationEnd={onReorderAnimationEnd}
|
||||||
offsetTop={offsetTop}
|
offsetTop={offsetTop}
|
||||||
observeIntersection={observe}
|
observeIntersection={observe}
|
||||||
@ -250,28 +225,18 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
itemSelector=".ListItem:not(.chat-item-archive)"
|
itemSelector=".ListItem:not(.chat-item-archive)"
|
||||||
preloadBackwards={CHAT_LIST_SLICE}
|
preloadBackwards={CHAT_LIST_SLICE}
|
||||||
withAbsolutePositioning
|
withAbsolutePositioning
|
||||||
maxHeight={chatsHeight + archiveHeight + frozenNotificationHeight + unconfirmedSessionHeight}
|
maxHeight={chatsHeight + archiveHeight + panesHeight}
|
||||||
onLoadMore={getMore}
|
onLoadMore={getMore}
|
||||||
>
|
>
|
||||||
{shouldShowUnconfirmedSessions && (
|
{isAllFolder && <ChatListPanes key="panes" onHeightChange={setPanesHeight} />}
|
||||||
<UnconfirmedSession
|
|
||||||
key="unconfirmed"
|
|
||||||
sessions={sessions!}
|
|
||||||
onHeightChange={setUnconfirmedSessionHeight}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{shouldShowFrozenAccountNotification && (
|
|
||||||
<FrozenAccountNotification
|
|
||||||
key="frozen"
|
|
||||||
onClick={handleFrozenAccountNotificationClick}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{shouldDisplayArchive && (
|
{shouldDisplayArchive && (
|
||||||
<Archive
|
<Archive
|
||||||
key="archive"
|
key="archive"
|
||||||
archiveSettings={archiveSettings}
|
archiveSettings={archiveSettings}
|
||||||
onClick={handleArchivedClick}
|
onClick={handleArchivedClick}
|
||||||
onDragEnter={handleArchivedDragEnter}
|
onDragEnter={handleArchivedDragEnter}
|
||||||
|
animationType={getAnimationType(ARCHIVE_ANIMATION_ID)}
|
||||||
|
offsetTop={panesHeight}
|
||||||
isFoldersSidebarShown={isFoldersSidebarShown}
|
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];
|
return [MAIN_THREAD_ID, ...ids];
|
||||||
}, [chat?.isBotForum, topicsInfo]);
|
}, [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(() => {
|
const [viewportIds, getMore] = useInfiniteScroll(() => {
|
||||||
if (!chat) return;
|
if (!chat) return;
|
||||||
@ -223,6 +223,7 @@ const ForumPanel = ({
|
|||||||
observeIntersection={observe}
|
observeIntersection={observe}
|
||||||
animationType={getAnimationType(id)}
|
animationType={getAnimationType(id)}
|
||||||
orderDiff={orderDiffById[id]}
|
orderDiff={orderDiffById[id]}
|
||||||
|
shiftDiff={shiftDiff}
|
||||||
onReorderAnimationEnd={onReorderAnimationEnd}
|
onReorderAnimationEnd={onReorderAnimationEnd}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import type { FC } from '../../../../lib/teact/teact';
|
|
||||||
import { memo } from '../../../../lib/teact/teact';
|
import { memo } from '../../../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../../../global';
|
import { getActions, withGlobal } from '../../../../global';
|
||||||
|
|
||||||
@ -55,6 +54,7 @@ type OwnProps = {
|
|||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
style: string;
|
style: string;
|
||||||
observeIntersection?: ObserveFn;
|
observeIntersection?: ObserveFn;
|
||||||
|
shiftDiff: number;
|
||||||
orderDiff: number;
|
orderDiff: number;
|
||||||
animationType: ChatAnimationTypes;
|
animationType: ChatAnimationTypes;
|
||||||
onReorderAnimationEnd?: NoneToVoidFunction;
|
onReorderAnimationEnd?: NoneToVoidFunction;
|
||||||
@ -76,7 +76,7 @@ type StateProps = {
|
|||||||
topics?: Record<number, ApiTopic>;
|
topics?: Record<number, ApiTopic>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Topic: FC<OwnProps & StateProps> = ({
|
const Topic = ({
|
||||||
topic,
|
topic,
|
||||||
isSelected,
|
isSelected,
|
||||||
chatId,
|
chatId,
|
||||||
@ -93,12 +93,13 @@ const Topic: FC<OwnProps & StateProps> = ({
|
|||||||
animationType,
|
animationType,
|
||||||
withInterfaceAnimations,
|
withInterfaceAnimations,
|
||||||
orderDiff,
|
orderDiff,
|
||||||
|
shiftDiff,
|
||||||
typingStatus,
|
typingStatus,
|
||||||
draft,
|
draft,
|
||||||
wasTopicOpened,
|
wasTopicOpened,
|
||||||
topics,
|
topics,
|
||||||
onReorderAnimationEnd,
|
onReorderAnimationEnd,
|
||||||
}) => {
|
}: OwnProps & StateProps) => {
|
||||||
const {
|
const {
|
||||||
openThread,
|
openThread,
|
||||||
deleteTopic,
|
deleteTopic,
|
||||||
@ -154,6 +155,7 @@ const Topic: FC<OwnProps & StateProps> = ({
|
|||||||
animationType,
|
animationType,
|
||||||
withInterfaceAnimations,
|
withInterfaceAnimations,
|
||||||
orderDiff,
|
orderDiff,
|
||||||
|
shiftDiff,
|
||||||
onReorderAnimationEnd,
|
onReorderAnimationEnd,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +1,34 @@
|
|||||||
import { useMemo } from '../../../../lib/teact/teact';
|
import { useMemo } from '../../../../lib/teact/teact';
|
||||||
|
|
||||||
export enum ChatAnimationTypes {
|
export enum ChatAnimationTypes {
|
||||||
|
Shift,
|
||||||
Move,
|
Move,
|
||||||
Opacity,
|
Opacity,
|
||||||
None,
|
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(() => {
|
return useMemo(() => {
|
||||||
|
if (isInitialRender) {
|
||||||
|
return () => ChatAnimationTypes.None;
|
||||||
|
}
|
||||||
|
|
||||||
const orderDiffs = Object.values<number>(orderDiffById);
|
const orderDiffs = Object.values<number>(orderDiffById);
|
||||||
const numberOfUp = orderDiffs.filter((diff) => diff < 0).length;
|
const numberOfUp = orderDiffs.filter((diff) => diff < 0).length;
|
||||||
const numberOfDown = orderDiffs.filter((diff) => diff > 0).length;
|
const numberOfDown = orderDiffs.filter((diff) => diff > 0).length;
|
||||||
|
|
||||||
return (chatId: T): ChatAnimationTypes => {
|
return (chatId: T): ChatAnimationTypes => {
|
||||||
const orderDiff = orderDiffById[chatId];
|
const orderDiff = orderDiffById[chatId];
|
||||||
if (orderDiff === 0) {
|
if (!orderDiff) {
|
||||||
|
if (isShifted) {
|
||||||
|
return ChatAnimationTypes.Shift;
|
||||||
|
}
|
||||||
return ChatAnimationTypes.None;
|
return ChatAnimationTypes.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,5 +43,5 @@ export function useChatAnimationType<T extends number | string>(orderDiffById: R
|
|||||||
|
|
||||||
return ChatAnimationTypes.Move;
|
return ChatAnimationTypes.Move;
|
||||||
};
|
};
|
||||||
}, [orderDiffById]);
|
}, [orderDiffById, isShifted, isInitialRender]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,6 +47,7 @@ export default function useChatListEntry({
|
|||||||
observeIntersection,
|
observeIntersection,
|
||||||
animationType,
|
animationType,
|
||||||
orderDiff,
|
orderDiff,
|
||||||
|
shiftDiff,
|
||||||
withInterfaceAnimations,
|
withInterfaceAnimations,
|
||||||
isTopic,
|
isTopic,
|
||||||
isSavedDialog,
|
isSavedDialog,
|
||||||
@ -71,6 +72,7 @@ export default function useChatListEntry({
|
|||||||
|
|
||||||
animationType: ChatAnimationTypes;
|
animationType: ChatAnimationTypes;
|
||||||
orderDiff: number;
|
orderDiff: number;
|
||||||
|
shiftDiff: number;
|
||||||
withInterfaceAnimations?: boolean;
|
withInterfaceAnimations?: boolean;
|
||||||
onReorderAnimationEnd?: NoneToVoidFunction;
|
onReorderAnimationEnd?: NoneToVoidFunction;
|
||||||
}) {
|
}) {
|
||||||
@ -194,8 +196,10 @@ export default function useChatListEntry({
|
|||||||
|
|
||||||
waitStartingTransitionsEnd(element).then(notifyAnimationEnd);
|
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(() => {
|
requestMutation(() => {
|
||||||
element.classList.add('animate-transform');
|
element.classList.add('animate-transform');
|
||||||
@ -203,14 +207,27 @@ export default function useChatListEntry({
|
|||||||
|
|
||||||
waitStartingTransitionsEnd(element).then(notifyAnimationEnd);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
isCancelled = true;
|
isCancelled = true;
|
||||||
};
|
};
|
||||||
}, [withInterfaceAnimations, orderDiff, animationType, onReorderAnimationEnd]);
|
}, [withInterfaceAnimations, orderDiff, shiftDiff, animationType, onReorderAnimationEnd]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
renderSubtitle,
|
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 { mapValues } from '../../../../util/iteratees';
|
||||||
import { useChatAnimationType } from './useChatAnimationType';
|
import { useChatAnimationType } from './useChatAnimationType';
|
||||||
|
|
||||||
@ -10,7 +11,7 @@ import useSyncEffect from '../../../../hooks/useSyncEffect';
|
|||||||
|
|
||||||
const EMPTY_ORDER_DIFF = {};
|
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(() => {
|
const orderById = useMemo(() => {
|
||||||
if (!orderedIds) {
|
if (!orderedIds) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -24,6 +25,14 @@ export default function useOrderDiff(orderedIds: (string | number)[] | undefined
|
|||||||
|
|
||||||
const prevOrderById = usePreviousDeprecated(orderById);
|
const prevOrderById = usePreviousDeprecated(orderById);
|
||||||
const prevChatId = usePreviousDeprecated(key);
|
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 orderDiffByIdRef = useRef<Record<string | number, number>>(EMPTY_ORDER_DIFF);
|
||||||
const forceUpdate = useForceUpdate();
|
const forceUpdate = useForceUpdate();
|
||||||
@ -35,8 +44,10 @@ export default function useOrderDiff(orderedIds: (string | number)[] | undefined
|
|||||||
forceUpdate();
|
forceUpdate();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const shiftDiff = prevTopOffset !== undefined ? topOffset - prevTopOffset : 0;
|
||||||
|
|
||||||
useSyncEffect(() => {
|
useSyncEffect(() => {
|
||||||
if (!orderById || !prevOrderById || key !== prevChatId) {
|
if (!orderById || !prevOrderById || key !== prevChatId || prevOrderById === orderById) {
|
||||||
orderDiffByIdRef.current = EMPTY_ORDER_DIFF;
|
orderDiffByIdRef.current = EMPTY_ORDER_DIFF;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -47,12 +58,17 @@ export default function useOrderDiff(orderedIds: (string | number)[] | undefined
|
|||||||
|
|
||||||
const hasChanges = Object.values(diff).some((value) => value !== 0);
|
const hasChanges = Object.values(diff).some((value) => value !== 0);
|
||||||
orderDiffByIdRef.current = hasChanges ? diff : EMPTY_ORDER_DIFF;
|
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 {
|
return {
|
||||||
orderDiffById: orderDiffByIdRef.current,
|
orderDiffById: orderDiffByIdRef.current,
|
||||||
|
shiftDiff,
|
||||||
getAnimationType,
|
getAnimationType,
|
||||||
onReorderAnimationEnd,
|
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 {
|
.root {
|
||||||
|
@include mixins.chat-list-pane;
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
margin-inline: -0.5rem;
|
|
||||||
padding-block: 0.75rem;
|
padding-block: 0.75rem;
|
||||||
padding-inline: 1rem;
|
padding-inline: 1rem;
|
||||||
|
border-radius: var(--border-radius-default);
|
||||||
|
|
||||||
background-color: var(--color-background-secondary);
|
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 */
|
@use "../../../../styles/mixins";
|
||||||
@value minimized from "./Archive.module.scss";
|
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
|
@include mixins.chat-list-pane;
|
||||||
|
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
& + :global(.minimized) {
|
|
||||||
margin-top: 0 !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.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 {
|
&-description {
|
||||||
margin: -0.5rem 1rem 1rem;
|
margin-block: 0;
|
||||||
|
margin-inline: 1rem;
|
||||||
|
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
line-height: 1.3125;
|
line-height: 1.3125;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
@ -284,6 +286,10 @@
|
|||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& + .settings-item-description {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-group {
|
.radio-group {
|
||||||
@ -390,3 +396,8 @@
|
|||||||
.settings-button {
|
.settings-button {
|
||||||
font-weight: var(--font-weight-semibold);
|
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 {
|
import {
|
||||||
memo, useCallback, useEffect, useMemo,
|
memo, useEffect, useMemo, useState,
|
||||||
useState,
|
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ApiUsername } from '../../../api/types';
|
import type { ApiBirthday, ApiUsername } from '../../../api/types';
|
||||||
import { ApiMediaFormat } 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 { PURCHASE_USERNAME, TME_LINK_PREFIX, USERNAME_PURCHASE_ERROR } from '../../../config';
|
||||||
import { getChatAvatarHash } from '../../../global/helpers';
|
import { getChatAvatarHash } from '../../../global/helpers';
|
||||||
import { selectTabState, selectUser, selectUserFullInfo } from '../../../global/selectors';
|
import { selectTabState, selectUser, selectUserFullInfo } from '../../../global/selectors';
|
||||||
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
||||||
|
import { formatDateToString } from '../../../util/dates/dateFormat';
|
||||||
import { throttle } from '../../../util/schedulers';
|
import { throttle } from '../../../util/schedulers';
|
||||||
import renderText from '../../common/helpers/renderText';
|
import renderText from '../../common/helpers/renderText';
|
||||||
|
|
||||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
import useMedia from '../../../hooks/useMedia';
|
import useMedia from '../../../hooks/useMedia';
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
import usePreviousDeprecated from '../../../hooks/usePreviousDeprecated';
|
import usePreviousDeprecated from '../../../hooks/usePreviousDeprecated';
|
||||||
@ -28,6 +28,8 @@ import UsernameInput from '../../common/UsernameInput';
|
|||||||
import AvatarEditable from '../../ui/AvatarEditable';
|
import AvatarEditable from '../../ui/AvatarEditable';
|
||||||
import FloatingActionButton from '../../ui/FloatingActionButton';
|
import FloatingActionButton from '../../ui/FloatingActionButton';
|
||||||
import InputText from '../../ui/InputText';
|
import InputText from '../../ui/InputText';
|
||||||
|
import Link from '../../ui/Link';
|
||||||
|
import ListItem from '../../ui/ListItem';
|
||||||
import TextArea from '../../ui/TextArea';
|
import TextArea from '../../ui/TextArea';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
@ -39,6 +41,7 @@ type StateProps = {
|
|||||||
currentAvatarHash?: string;
|
currentAvatarHash?: string;
|
||||||
currentFirstName?: string;
|
currentFirstName?: string;
|
||||||
currentLastName?: string;
|
currentLastName?: string;
|
||||||
|
currentBirthday?: ApiBirthday;
|
||||||
currentBio?: string;
|
currentBio?: string;
|
||||||
progress?: ProfileEditProgress;
|
progress?: ProfileEditProgress;
|
||||||
checkedUsername?: string;
|
checkedUsername?: string;
|
||||||
@ -52,11 +55,12 @@ const runThrottled = throttle((cb) => cb(), 60000, true);
|
|||||||
|
|
||||||
const ERROR_FIRST_NAME_MISSING = 'Please provide your first name';
|
const ERROR_FIRST_NAME_MISSING = 'Please provide your first name';
|
||||||
|
|
||||||
const SettingsEditProfile: FC<OwnProps & StateProps> = ({
|
const SettingsEditProfile = ({
|
||||||
isActive,
|
isActive,
|
||||||
currentAvatarHash,
|
currentAvatarHash,
|
||||||
currentFirstName,
|
currentFirstName,
|
||||||
currentLastName,
|
currentLastName,
|
||||||
|
currentBirthday,
|
||||||
currentBio,
|
currentBio,
|
||||||
progress,
|
progress,
|
||||||
checkedUsername,
|
checkedUsername,
|
||||||
@ -65,13 +69,16 @@ const SettingsEditProfile: FC<OwnProps & StateProps> = ({
|
|||||||
maxBioLength,
|
maxBioLength,
|
||||||
usernames,
|
usernames,
|
||||||
onReset,
|
onReset,
|
||||||
}) => {
|
}: OwnProps & StateProps) => {
|
||||||
const {
|
const {
|
||||||
loadCurrentUser,
|
loadCurrentUser,
|
||||||
updateProfile,
|
updateProfile,
|
||||||
|
openSettingsScreen,
|
||||||
|
openBirthdaySetupModal,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const lang = useOldLang();
|
const oldLang = useOldLang();
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
const firstEditableUsername = useMemo(() => usernames?.find(({ isEditable }) => isEditable), [usernames]);
|
const firstEditableUsername = useMemo(() => usernames?.find(({ isEditable }) => isEditable), [usernames]);
|
||||||
const currentUsername = firstEditableUsername?.username || '';
|
const currentUsername = firstEditableUsername?.username || '';
|
||||||
@ -137,31 +144,51 @@ const SettingsEditProfile: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
}, [progress]);
|
}, [progress]);
|
||||||
|
|
||||||
const handlePhotoChange = useCallback((newPhoto: File) => {
|
const formattedBirthday = useMemo(() => {
|
||||||
setPhoto(newPhoto);
|
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);
|
setFirstName(e.target.value);
|
||||||
setIsProfileFieldsTouched(true);
|
setIsProfileFieldsTouched(true);
|
||||||
}, []);
|
});
|
||||||
|
|
||||||
const handleLastNameChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
const handleLastNameChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setLastName(e.target.value);
|
setLastName(e.target.value);
|
||||||
setIsProfileFieldsTouched(true);
|
setIsProfileFieldsTouched(true);
|
||||||
}, []);
|
});
|
||||||
|
|
||||||
const handleBioChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
const handleBioChange = useLastCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setBio(e.target.value);
|
setBio(e.target.value);
|
||||||
setIsProfileFieldsTouched(true);
|
setIsProfileFieldsTouched(true);
|
||||||
}, []);
|
});
|
||||||
|
|
||||||
const handleUsernameChange = useCallback((value: string | false) => {
|
const handleUsernameChange = useLastCallback((value: string | false) => {
|
||||||
setEditableUsername(value);
|
setEditableUsername(value);
|
||||||
setIsUsernameTouched(currentUsername !== 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 trimmedFirstName = firstName.trim();
|
||||||
const trimmedLastName = lastName.trim();
|
const trimmedLastName = lastName.trim();
|
||||||
const trimmedBio = bio.trim();
|
const trimmedBio = bio.trim();
|
||||||
@ -184,19 +211,14 @@ const SettingsEditProfile: FC<OwnProps & StateProps> = ({
|
|||||||
username: editableUsername,
|
username: editableUsername,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}, [
|
});
|
||||||
photo,
|
|
||||||
firstName, lastName, bio, isProfileFieldsTouched,
|
|
||||||
editableUsername, isUsernameTouched,
|
|
||||||
updateProfile,
|
|
||||||
]);
|
|
||||||
|
|
||||||
function renderPurchaseLink() {
|
function renderPurchaseLink() {
|
||||||
const purchaseInfoLink = `${TME_LINK_PREFIX}${PURCHASE_USERNAME}`;
|
const purchaseInfoLink = `${TME_LINK_PREFIX}${PURCHASE_USERNAME}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p className="settings-item-description" dir={lang.isRtl ? 'rtl' : undefined}>
|
<p className="settings-item-description" dir={oldLang.isRtl ? 'rtl' : undefined}>
|
||||||
{(lang('lng_username_purchase_available'))
|
{(oldLang('lng_username_purchase_available'))
|
||||||
.replace('{link}', '%PURCHASE_LINK%')
|
.replace('{link}', '%PURCHASE_LINK%')
|
||||||
.split('%')
|
.split('%')
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
@ -214,39 +236,57 @@ const SettingsEditProfile: FC<OwnProps & StateProps> = ({
|
|||||||
<AvatarEditable
|
<AvatarEditable
|
||||||
currentAvatarBlobUrl={currentAvatarBlobUrl}
|
currentAvatarBlobUrl={currentAvatarBlobUrl}
|
||||||
onChange={handlePhotoChange}
|
onChange={handlePhotoChange}
|
||||||
title="Edit your profile photo"
|
title={lang('AriaSettingsEditProfilePhoto')}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
<InputText
|
<InputText
|
||||||
value={firstName}
|
value={firstName}
|
||||||
onChange={handleFirstNameChange}
|
onChange={handleFirstNameChange}
|
||||||
label={lang('FirstName')}
|
label={oldLang('FirstName')}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
error={error === ERROR_FIRST_NAME_MISSING ? error : undefined}
|
error={error === ERROR_FIRST_NAME_MISSING ? error : undefined}
|
||||||
/>
|
/>
|
||||||
<InputText
|
<InputText
|
||||||
value={lastName}
|
value={lastName}
|
||||||
onChange={handleLastNameChange}
|
onChange={handleLastNameChange}
|
||||||
label={lang('LastName')}
|
label={oldLang('LastName')}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
<TextArea
|
<TextArea
|
||||||
value={bio}
|
value={bio}
|
||||||
onChange={handleBioChange}
|
onChange={handleBioChange}
|
||||||
label={lang('UserBio')}
|
label={oldLang('UserBio')}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
maxLength={maxBioLength}
|
maxLength={maxBioLength}
|
||||||
maxLengthIndicator={maxBioLength ? (maxBioLength - bio.length).toString() : undefined}
|
maxLengthIndicator={maxBioLength ? (maxBioLength - bio.length).toString() : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="settings-item-description" dir={lang.isRtl ? 'rtl' : undefined}>
|
<p className="settings-item-description" dir={oldLang.isRtl ? 'rtl' : undefined}>
|
||||||
{renderText(lang('lng_settings_about_bio'), ['br', 'simple_markdown'])}
|
{renderText(oldLang('lng_settings_about_bio'), ['br', 'simple_markdown'])}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="settings-item">
|
<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">
|
<div className="settings-input">
|
||||||
<UsernameInput
|
<UsernameInput
|
||||||
@ -259,12 +299,12 @@ const SettingsEditProfile: FC<OwnProps & StateProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{editUsernameError === USERNAME_PURCHASE_ERROR && renderPurchaseLink()}
|
{editUsernameError === USERNAME_PURCHASE_ERROR && renderPurchaseLink()}
|
||||||
<p className="settings-item-description" dir={lang.isRtl ? 'rtl' : undefined}>
|
<p className="settings-item-description" dir={oldLang.isRtl ? 'rtl' : undefined}>
|
||||||
{renderText(lang('UsernameHelp'), ['br', 'simple_markdown'])}
|
{renderText(oldLang('UsernameHelp'), ['br', 'simple_markdown'])}
|
||||||
</p>
|
</p>
|
||||||
{editableUsername && (
|
{editableUsername && (
|
||||||
<p className="settings-item-description" dir={lang.isRtl ? 'rtl' : undefined}>
|
<p className="settings-item-description" dir={oldLang.isRtl ? 'rtl' : undefined}>
|
||||||
{lang('lng_username_link')}
|
{oldLang('lng_username_link')}
|
||||||
<br />
|
<br />
|
||||||
<span className="username-link">
|
<span className="username-link">
|
||||||
{TME_LINK_PREFIX}
|
{TME_LINK_PREFIX}
|
||||||
@ -286,7 +326,7 @@ const SettingsEditProfile: FC<OwnProps & StateProps> = ({
|
|||||||
isShown={isSaveButtonShown}
|
isShown={isSaveButtonShown}
|
||||||
onClick={handleProfileSave}
|
onClick={handleProfileSave}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
ariaLabel={lang('Save')}
|
ariaLabel={oldLang('Save')}
|
||||||
iconName="check"
|
iconName="check"
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
@ -316,6 +356,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
currentAvatarHash,
|
currentAvatarHash,
|
||||||
currentFirstName,
|
currentFirstName,
|
||||||
currentLastName,
|
currentLastName,
|
||||||
|
currentBirthday: currentUserFullInfo?.birthday,
|
||||||
currentBio: currentUserFullInfo?.bio,
|
currentBio: currentUserFullInfo?.bio,
|
||||||
progress,
|
progress,
|
||||||
isUsernameAvailable,
|
isUsernameAvailable,
|
||||||
|
|||||||
@ -68,9 +68,9 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
|||||||
canSetPasscode,
|
canSetPasscode,
|
||||||
needAgeVideoVerification,
|
needAgeVideoVerification,
|
||||||
privacy,
|
privacy,
|
||||||
onReset,
|
|
||||||
isCurrentUserFrozen,
|
isCurrentUserFrozen,
|
||||||
accountDaysTtl,
|
accountDaysTtl,
|
||||||
|
onReset,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
openDeleteAccountModal,
|
openDeleteAccountModal,
|
||||||
@ -89,7 +89,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isCurrentUserFrozen) {
|
if (!isCurrentUserFrozen) {
|
||||||
loadBlockedUsers();
|
loadBlockedUsers();
|
||||||
loadPrivacySettings();
|
loadPrivacySettings({});
|
||||||
loadWebAuthorizations();
|
loadWebAuthorizations();
|
||||||
}
|
}
|
||||||
}, [isCurrentUserFrozen]);
|
}, [isCurrentUserFrozen]);
|
||||||
|
|||||||
@ -65,7 +65,7 @@ const Dialogs: FC<StateProps> = ({ dialogs, currentMessageList }) => {
|
|||||||
{lang(
|
{lang(
|
||||||
'AreYouSureShareMyContactInfoBot',
|
'AreYouSureShareMyContactInfoBot',
|
||||||
undefined,
|
undefined,
|
||||||
{ withNodes: true, withMarkdown: true, renderTextFilters: ['br', 'emoji'],
|
{ withNodes: true, withMarkdown: true, renderTextFilters: ['br'],
|
||||||
})}
|
})}
|
||||||
<div className="dialog-buttons mt-2">
|
<div className="dialog-buttons mt-2">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -263,6 +263,7 @@ const Main = ({
|
|||||||
loadAllStories,
|
loadAllStories,
|
||||||
loadAllHiddenStories,
|
loadAllHiddenStories,
|
||||||
loadContentSettings,
|
loadContentSettings,
|
||||||
|
loadPromoData,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
if (DEBUG && !DEBUG_isLogged) {
|
if (DEBUG && !DEBUG_isLogged) {
|
||||||
@ -315,6 +316,7 @@ const Main = ({
|
|||||||
loadAllChats({ listType: 'saved' });
|
loadAllChats({ listType: 'saved' });
|
||||||
loadAllStories();
|
loadAllStories();
|
||||||
loadAllHiddenStories();
|
loadAllHiddenStories();
|
||||||
|
loadPromoData();
|
||||||
loadContentSettings();
|
loadContentSettings();
|
||||||
loadRecentReactions();
|
loadRecentReactions();
|
||||||
loadDefaultTagReactions();
|
loadDefaultTagReactions();
|
||||||
|
|||||||
@ -113,7 +113,7 @@ const MiddleHeaderPanes = ({
|
|||||||
const middleColumn = document.getElementById('MiddleColumn');
|
const middleColumn = document.getElementById('MiddleColumn');
|
||||||
if (!middleColumn) return;
|
if (!middleColumn) return;
|
||||||
|
|
||||||
applyAnimationState(stateArray, isFirstRender);
|
applyAnimationState({ list: stateArray, noTransition: isFirstRender });
|
||||||
|
|
||||||
setExtraStyles(middleColumn, {
|
setExtraStyles(middleColumn, {
|
||||||
'--middle-header-panes-height': `${totalHeight}px`,
|
'--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;
|
let cumulativeHeight = 0;
|
||||||
for (let i = 0; i < list.length; i++) {
|
for (let i = 0; i < list.length; i++) {
|
||||||
const state = list[i];
|
const state = list[i];
|
||||||
@ -139,7 +149,7 @@ export function applyAnimationState(list: PaneState[], noTransition = false) {
|
|||||||
|
|
||||||
const apply = () => {
|
const apply = () => {
|
||||||
setExtraStyles(element, {
|
setExtraStyles(element, {
|
||||||
transform: `translateY(${state.isOpen ? shiftPx : `calc(${shiftPx} - 100%)`})`,
|
transform: `translateY(${state.isOpen ? shiftPx : `calc(${shiftPx} - ${topMargin}px - 100%)`})`,
|
||||||
zIndex: String(-i),
|
zIndex: String(-i),
|
||||||
transition: noTransition ? 'none' : '',
|
transition: noTransition ? 'none' : '',
|
||||||
});
|
});
|
||||||
@ -148,8 +158,8 @@ export function applyAnimationState(list: PaneState[], noTransition = false) {
|
|||||||
if (!element.dataset.isPanelOpen && state.isOpen && !noTransition) {
|
if (!element.dataset.isPanelOpen && state.isOpen && !noTransition) {
|
||||||
// Start animation right above its final position
|
// Start animation right above its final position
|
||||||
setExtraStyles(element, {
|
setExtraStyles(element, {
|
||||||
transform: `translateY(calc(${shiftPx} - 100%))`,
|
transform: `translateY(calc(${shiftPx} - ${topMargin}px - 100%))`,
|
||||||
zIndex: String(-i),
|
zIndex: String(zIndexIncrease ? i : -i),
|
||||||
transition: 'none',
|
transition: 'none',
|
||||||
});
|
});
|
||||||
element.dataset.isPanelOpen = 'true';
|
element.dataset.isPanelOpen = 'true';
|
||||||
|
|||||||
@ -227,7 +227,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.full-width-player {
|
&.full-width-player {
|
||||||
@include mixins.header-pane;
|
@include mixins.middle-header-pane;
|
||||||
|
|
||||||
.AudioPlayer-content {
|
.AudioPlayer-content {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
@use "../../../styles/mixins";
|
@use "../../../styles/mixins";
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
@include mixins.header-pane;
|
@include mixins.middle-header-pane;
|
||||||
|
|
||||||
cursor: var(--custom-cursor, pointer);
|
cursor: var(--custom-cursor, pointer);
|
||||||
|
|
||||||
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
height: auto;
|
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-right: max(1rem, env(safe-area-inset-right));
|
||||||
padding-left: max(1rem, env(safe-area-inset-left));
|
padding-left: max(1rem, env(safe-area-inset-left));
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
@use "../../../styles/mixins";
|
@use "../../../styles/mixins";
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
@include mixins.header-pane;
|
@include mixins.middle-header-pane;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
@use "../../../styles/mixins";
|
@use "../../../styles/mixins";
|
||||||
|
|
||||||
.ChatReportPane {
|
.ChatReportPane {
|
||||||
@include mixins.header-pane;
|
@include mixins.middle-header-pane;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -49,7 +49,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fullWidth {
|
.fullWidth {
|
||||||
@include mixins.header-pane;
|
@include mixins.middle-header-pane;
|
||||||
|
|
||||||
height: 3.5rem;
|
height: 3.5rem;
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
@use "../../../styles/mixins";
|
@use "../../../styles/mixins";
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
@include mixins.header-pane;
|
@include mixins.middle-header-pane;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import WebAppsCloseConfirmationModal from '../main/WebAppsCloseConfirmationModal
|
|||||||
import AboutAdsModal from './aboutAds/AboutAdsModal.async';
|
import AboutAdsModal from './aboutAds/AboutAdsModal.async';
|
||||||
import AgeVerificationModal from './ageVerification/AgeVerificationModal.async';
|
import AgeVerificationModal from './ageVerification/AgeVerificationModal.async';
|
||||||
import AttachBotInstallModal from './attachBotInstall/AttachBotInstallModal.async';
|
import AttachBotInstallModal from './attachBotInstall/AttachBotInstallModal.async';
|
||||||
|
import BirthdaySetupModal from './birthday/BirthdaySetupModal.async';
|
||||||
import BoostModal from './boost/BoostModal.async';
|
import BoostModal from './boost/BoostModal.async';
|
||||||
import ChatInviteModal from './chatInvite/ChatInviteModal.async';
|
import ChatInviteModal from './chatInvite/ChatInviteModal.async';
|
||||||
import ChatlistModal from './chatlist/ChatlistModal.async';
|
import ChatlistModal from './chatlist/ChatlistModal.async';
|
||||||
@ -109,7 +110,8 @@ type ModalKey = keyof Pick<TabState,
|
|||||||
'isAgeVerificationModalOpen' |
|
'isAgeVerificationModalOpen' |
|
||||||
'profileRatingModal' |
|
'profileRatingModal' |
|
||||||
'quickPreview' |
|
'quickPreview' |
|
||||||
'storyStealthModal'
|
'storyStealthModal' |
|
||||||
|
'birthdaySetupModal'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
@ -175,6 +177,7 @@ const MODALS: ModalRegistry = {
|
|||||||
profileRatingModal: ProfileRatingModal,
|
profileRatingModal: ProfileRatingModal,
|
||||||
quickPreview: QuickPreviewModal,
|
quickPreview: QuickPreviewModal,
|
||||||
storyStealthModal: StealthModeModal,
|
storyStealthModal: StealthModeModal,
|
||||||
|
birthdaySetupModal: BirthdaySetupModal,
|
||||||
};
|
};
|
||||||
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
|
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
|
||||||
const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>;
|
const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>;
|
||||||
|
|||||||
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(
|
textParts={lang(
|
||||||
'AreYouSureShareMyContactInfoBot',
|
'AreYouSureShareMyContactInfoBot',
|
||||||
undefined,
|
undefined,
|
||||||
{ withNodes: true, withMarkdown: true, renderTextFilters: ['br', 'emoji'],
|
{ withNodes: true, withMarkdown: true, renderTextFilters: ['br'],
|
||||||
})}
|
})}
|
||||||
confirmHandler={handleAcceptPhone}
|
confirmHandler={handleAcceptPhone}
|
||||||
confirmLabel={lang('ContactShare')}
|
confirmLabel={lang('ContactShare')}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
ChangeEvent, FormEvent,
|
ChangeEvent, FormEvent,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import type { ElementRef, FC } from '../../lib/teact/teact';
|
import type { ElementRef } from '../../lib/teact/teact';
|
||||||
import { memo } from '../../lib/teact/teact';
|
import { memo } from '../../lib/teact/teact';
|
||||||
|
|
||||||
import { IS_TAURI } from '../../util/browser/globalEnvironment';
|
import { IS_TAURI } from '../../util/browser/globalEnvironment';
|
||||||
@ -31,9 +31,10 @@ type OwnProps = {
|
|||||||
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||||
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
||||||
onPaste?: (e: React.ClipboardEvent<HTMLInputElement>) => void;
|
onPaste?: (e: React.ClipboardEvent<HTMLInputElement>) => void;
|
||||||
|
onClick?: (e: React.MouseEvent<HTMLInputElement>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const InputText: FC<OwnProps> = ({
|
const InputText = ({
|
||||||
ref,
|
ref,
|
||||||
id,
|
id,
|
||||||
className,
|
className,
|
||||||
@ -55,7 +56,8 @@ const InputText: FC<OwnProps> = ({
|
|||||||
onKeyDown,
|
onKeyDown,
|
||||||
onBlur,
|
onBlur,
|
||||||
onPaste,
|
onPaste,
|
||||||
}) => {
|
onClick,
|
||||||
|
}: OwnProps) => {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const labelText = error || success || label;
|
const labelText = error || success || label;
|
||||||
const fullClassName = buildClassName(
|
const fullClassName = buildClassName(
|
||||||
@ -93,6 +95,7 @@ const InputText: FC<OwnProps> = ({
|
|||||||
onPaste={onPaste}
|
onPaste={onPaste}
|
||||||
aria-label={labelText}
|
aria-label={labelText}
|
||||||
teactExperimentControlled={teactExperimentControlled}
|
teactExperimentControlled={teactExperimentControlled}
|
||||||
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
{labelText && (
|
{labelText && (
|
||||||
<label htmlFor={id}>{labelText}</label>
|
<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> => {
|
addActionHandler('updateProfilePhoto', async (global, actions, payload): Promise<void> => {
|
||||||
const { photo, isFallback } = payload;
|
const { photo, isFallback } = payload;
|
||||||
const { currentUserId } = global;
|
const { currentUserId } = global;
|
||||||
@ -395,7 +406,12 @@ addActionHandler('loadLanguages', async (global): Promise<void> => {
|
|||||||
setGlobal(global);
|
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;
|
if (selectIsCurrentUserFrozen(global)) return;
|
||||||
|
|
||||||
const result = await Promise.all([
|
const result = await Promise.all([
|
||||||
@ -642,7 +658,7 @@ addActionHandler('ensureTimeFormat', async (global, actions): Promise<void> => {
|
|||||||
addActionHandler('loadAppConfig', async (global, actions, payload): Promise<void> => {
|
addActionHandler('loadAppConfig', async (global, actions, payload): Promise<void> => {
|
||||||
const hash = payload?.hash;
|
const hash = payload?.hash;
|
||||||
|
|
||||||
const appConfig = await callApi('fetchAppConfig', hash);
|
const appConfig = await callApi('fetchAppConfig', { hash });
|
||||||
if (!appConfig) return;
|
if (!appConfig) return;
|
||||||
|
|
||||||
requestActionTimeout({
|
requestActionTimeout({
|
||||||
@ -677,6 +693,33 @@ addActionHandler('loadConfig', async (global): Promise<void> => {
|
|||||||
setGlobal(global);
|
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> => {
|
addActionHandler('loadPeerColors', async (global): Promise<void> => {
|
||||||
const generalHash = global.peerColors?.generalHash;
|
const generalHash = global.peerColors?.generalHash;
|
||||||
const profileHash = global.peerColors?.profileHash;
|
const profileHash = global.peerColors?.profileHash;
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import * as langProvider from '../../../util/oldLangProvider';
|
|||||||
import updateIcon from '../../../util/updateIcon';
|
import updateIcon from '../../../util/updateIcon';
|
||||||
import { setPageTitle, setPageTitleInstant } from '../../../util/updatePageTitle';
|
import { setPageTitle, setPageTitleInstant } from '../../../util/updatePageTitle';
|
||||||
import { getAllowedAttachmentOptions, getChatTitle } from '../../helpers';
|
import { getAllowedAttachmentOptions, getChatTitle } from '../../helpers';
|
||||||
|
import { addTabStateResetterAction } from '../../helpers/meta';
|
||||||
import {
|
import {
|
||||||
addActionHandler, getActions, getGlobal, setGlobal,
|
addActionHandler, getActions, getGlobal, setGlobal,
|
||||||
} from '../../index';
|
} from '../../index';
|
||||||
@ -894,6 +895,17 @@ addActionHandler('closeCollectibleInfoModal', (global, actions, payload): Action
|
|||||||
}, tabId);
|
}, 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 => {
|
addActionHandler('setShouldCloseRightColumn', (global, actions, payload): ActionReturnType => {
|
||||||
const { value, tabId = getCurrentTabId() } = payload;
|
const { value, tabId = getCurrentTabId() } = payload;
|
||||||
return updateTabState(global, {
|
return updateTabState(global, {
|
||||||
|
|||||||
@ -163,6 +163,8 @@ addActionHandler('openLeftColumnContent', (global, actions, payload): ActionRetu
|
|||||||
addActionHandler('openSettingsScreen', (global, actions, payload): ActionReturnType => {
|
addActionHandler('openSettingsScreen', (global, actions, payload): ActionReturnType => {
|
||||||
const { screen, tabId = getCurrentTabId() } = payload;
|
const { screen, tabId = getCurrentTabId() } = payload;
|
||||||
const tabState = selectTabState(global, tabId);
|
const tabState = selectTabState(global, tabId);
|
||||||
|
|
||||||
|
actions.loadPrivacySettings({ skipIfCached: true });
|
||||||
// Force settings only if new screen is passed, do not on resets
|
// Force settings only if new screen is passed, do not on resets
|
||||||
if (payload.screen !== undefined) actions.openLeftColumnContent({ contentKey: LeftColumnContent.Settings, tabId });
|
if (payload.screen !== undefined) actions.openLeftColumnContent({ contentKey: LeftColumnContent.Settings, tabId });
|
||||||
return updateTabState(global, {
|
return updateTabState(global, {
|
||||||
|
|||||||
@ -11,3 +11,9 @@ export function selectSharedSettings<T extends GlobalState>(
|
|||||||
) {
|
) {
|
||||||
return selectSharedState(global).settings;
|
return selectSharedState(global).settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function selectAnimationLevel<T extends GlobalState>(
|
||||||
|
global: T,
|
||||||
|
) {
|
||||||
|
return selectSharedSettings(global).animationLevel;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
ApiAttachBot,
|
ApiAttachBot,
|
||||||
ApiAttachment,
|
ApiAttachment,
|
||||||
|
ApiBirthday,
|
||||||
ApiChat,
|
ApiChat,
|
||||||
ApiChatAdminRights,
|
ApiChatAdminRights,
|
||||||
ApiChatBannedRights,
|
ApiChatBannedRights,
|
||||||
@ -249,7 +250,9 @@ export interface ActionPayloads {
|
|||||||
notificationSoundVolume?: number;
|
notificationSoundVolume?: number;
|
||||||
};
|
};
|
||||||
loadLanguages: undefined;
|
loadLanguages: undefined;
|
||||||
loadPrivacySettings: undefined;
|
loadPrivacySettings: {
|
||||||
|
skipIfCached?: boolean;
|
||||||
|
};
|
||||||
setPrivacyVisibility: {
|
setPrivacyVisibility: {
|
||||||
privacyKey: ApiPrivacyKey;
|
privacyKey: ApiPrivacyKey;
|
||||||
visibility: PrivacyVisibility;
|
visibility: PrivacyVisibility;
|
||||||
@ -1831,6 +1834,13 @@ export interface ActionPayloads {
|
|||||||
bio?: string;
|
bio?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
} & WithTabId;
|
} & WithTabId;
|
||||||
|
updateBirthday: {
|
||||||
|
birthday?: ApiBirthday;
|
||||||
|
};
|
||||||
|
openBirthdaySetupModal: {
|
||||||
|
currentBirthday?: ApiBirthday;
|
||||||
|
} & WithTabId;
|
||||||
|
closeBirthdaySetupModal: WithTabId | undefined;
|
||||||
updateBotProfile: {
|
updateBotProfile: {
|
||||||
photo?: File;
|
photo?: File;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
@ -2452,6 +2462,10 @@ export interface ActionPayloads {
|
|||||||
loadAppConfig: {
|
loadAppConfig: {
|
||||||
hash: number;
|
hash: number;
|
||||||
} | undefined;
|
} | undefined;
|
||||||
|
loadPromoData: undefined;
|
||||||
|
dismissSuggestion: {
|
||||||
|
suggestion: string;
|
||||||
|
} & WithTabId;
|
||||||
loadPeerColors: undefined;
|
loadPeerColors: undefined;
|
||||||
loadTimezones: undefined;
|
loadTimezones: undefined;
|
||||||
openLeftColumnContent: {
|
openLeftColumnContent: {
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import type {
|
|||||||
ApiPoll,
|
ApiPoll,
|
||||||
ApiPrivacyKey,
|
ApiPrivacyKey,
|
||||||
ApiPrivacySettings,
|
ApiPrivacySettings,
|
||||||
|
ApiPromoData,
|
||||||
ApiQuickReply,
|
ApiQuickReply,
|
||||||
ApiReaction,
|
ApiReaction,
|
||||||
ApiReactionKey,
|
ApiReactionKey,
|
||||||
@ -81,6 +82,7 @@ export type GlobalState = {
|
|||||||
isInited: boolean;
|
isInited: boolean;
|
||||||
config?: ApiConfig;
|
config?: ApiConfig;
|
||||||
appConfig: ApiAppConfig;
|
appConfig: ApiAppConfig;
|
||||||
|
promoData?: ApiPromoData;
|
||||||
peerColors?: ApiPeerColors;
|
peerColors?: ApiPeerColors;
|
||||||
timezones?: {
|
timezones?: {
|
||||||
byId: Record<string, ApiTimezone>;
|
byId: Record<string, ApiTimezone>;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
ApiAttachBot,
|
ApiAttachBot,
|
||||||
|
ApiBirthday,
|
||||||
ApiBoost,
|
ApiBoost,
|
||||||
ApiBoostsStatus,
|
ApiBoostsStatus,
|
||||||
ApiChannelMonetizationStatistics,
|
ApiChannelMonetizationStatistics,
|
||||||
@ -783,6 +784,10 @@ export type TabState = {
|
|||||||
|
|
||||||
isAgeVerificationModalOpen?: boolean;
|
isAgeVerificationModalOpen?: boolean;
|
||||||
|
|
||||||
|
birthdaySetupModal?: {
|
||||||
|
currentBirthday?: ApiBirthday;
|
||||||
|
};
|
||||||
|
|
||||||
paidReactionModal?: {
|
paidReactionModal?: {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
messageId: number;
|
messageId: number;
|
||||||
|
|||||||
@ -1586,6 +1586,7 @@ account.updateEmojiStatus#fbd3de6b emoji_status:EmojiStatus = Bool;
|
|||||||
account.getRecentEmojiStatuses#f578105 hash:long = account.EmojiStatuses;
|
account.getRecentEmojiStatuses#f578105 hash:long = account.EmojiStatuses;
|
||||||
account.reorderUsernames#ef500eab order:Vector<string> = Bool;
|
account.reorderUsernames#ef500eab order:Vector<string> = Bool;
|
||||||
account.toggleUsername#58d6b376 username:string active:Bool = 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.resolveBusinessChatLink#5492e5ee slug:string = account.ResolvedBusinessChatLinks;
|
||||||
account.toggleSponsoredMessages#b9d9a38d enabled:Bool = Bool;
|
account.toggleSponsoredMessages#b9d9a38d enabled:Bool = Bool;
|
||||||
account.getCollectibleEmojiStatuses#2e7b4543 hash:long = account.EmojiStatuses;
|
account.getCollectibleEmojiStatuses#2e7b4543 hash:long = account.EmojiStatuses;
|
||||||
@ -1781,6 +1782,8 @@ help.getNearestDc#1fb33026 = NearestDc;
|
|||||||
help.getSupport#9cdf08cd = help.Support;
|
help.getSupport#9cdf08cd = help.Support;
|
||||||
help.acceptTermsOfService#ee72f79a id:DataJSON = Bool;
|
help.acceptTermsOfService#ee72f79a id:DataJSON = Bool;
|
||||||
help.getAppConfig#61e3f854 hash:int = help.AppConfig;
|
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.getCountriesList#735787a8 lang_code:string hash:int = help.CountriesList;
|
||||||
help.getPremiumPromo#b81b93d4 = help.PremiumPromo;
|
help.getPremiumPromo#b81b93d4 = help.PremiumPromo;
|
||||||
help.getPeerColors#da80f42f hash:int = help.PeerColors;
|
help.getPeerColors#da80f42f hash:int = help.PeerColors;
|
||||||
|
|||||||
@ -58,6 +58,7 @@
|
|||||||
"account.getRecentEmojiStatuses",
|
"account.getRecentEmojiStatuses",
|
||||||
"account.reorderUsernames",
|
"account.reorderUsernames",
|
||||||
"account.toggleUsername",
|
"account.toggleUsername",
|
||||||
|
"account.updateBirthday",
|
||||||
"account.resolveBusinessChatLink",
|
"account.resolveBusinessChatLink",
|
||||||
"account.toggleSponsoredMessages",
|
"account.toggleSponsoredMessages",
|
||||||
"account.getCollectibleEmojiStatuses",
|
"account.getCollectibleEmojiStatuses",
|
||||||
@ -251,14 +252,16 @@
|
|||||||
"upload.getWebFile",
|
"upload.getWebFile",
|
||||||
"help.getConfig",
|
"help.getConfig",
|
||||||
"help.getNearestDc",
|
"help.getNearestDc",
|
||||||
|
"help.getAppConfig",
|
||||||
"help.getSupport",
|
"help.getSupport",
|
||||||
"help.acceptTermsOfService",
|
"help.acceptTermsOfService",
|
||||||
|
"help.getPromoData",
|
||||||
"help.getCountriesList",
|
"help.getCountriesList",
|
||||||
"help.getAppConfig",
|
"help.getPremiumPromo",
|
||||||
|
"help.dismissSuggestion",
|
||||||
"help.getPeerColors",
|
"help.getPeerColors",
|
||||||
"help.getPeerProfileColors",
|
"help.getPeerProfileColors",
|
||||||
"help.getTimezonesList",
|
"help.getTimezonesList",
|
||||||
"help.getPremiumPromo",
|
|
||||||
"channels.readHistory",
|
"channels.readHistory",
|
||||||
"channels.deleteMessages",
|
"channels.deleteMessages",
|
||||||
"channels.deleteParticipantHistory",
|
"channels.deleteParticipantHistory",
|
||||||
|
|||||||
@ -121,7 +121,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin header-pane {
|
@mixin middle-header-pane {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
transform: translateY(-100%);
|
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 {
|
@mixin side-panel-section {
|
||||||
border-bottom: 0.625rem solid var(--color-background-secondary);
|
border-bottom: 0.625rem solid var(--color-background-secondary);
|
||||||
background-color: var(--color-background);
|
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);
|
--layer-transition-behind: 300ms cubic-bezier(0.33, 1, 0.68, 1);
|
||||||
--slide-transition: 300ms cubic-bezier(0.25, 1, 0.5, 1);
|
--slide-transition: 300ms cubic-bezier(0.25, 1, 0.5, 1);
|
||||||
--select-transition: 200ms ease-out;
|
--select-transition: 200ms ease-out;
|
||||||
|
--chat-transform-transition: 0.2s ease-out;
|
||||||
|
|
||||||
--safe-area-top: env(safe-area-inset-top);
|
--safe-area-top: env(safe-area-inset-top);
|
||||||
--safe-area-right: env(safe-area-inset-right);
|
--safe-area-right: env(safe-area-inset-right);
|
||||||
|
|||||||
@ -233,6 +233,10 @@ body:not(.is-ios) {
|
|||||||
touch-action: none;
|
touch-action: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-grow {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
@include iconsMixins.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;
|
'MenuNightMode': undefined;
|
||||||
'AriaMenuEnableNightMode': undefined;
|
'AriaMenuEnableNightMode': undefined;
|
||||||
'AriaMenuDisableNightMode': undefined;
|
'AriaMenuDisableNightMode': undefined;
|
||||||
|
'AriaSettingsEditProfilePhoto': undefined;
|
||||||
'MenuAnimationsSwitch': undefined;
|
'MenuAnimationsSwitch': undefined;
|
||||||
'MenuTelegramFeatures': undefined;
|
'MenuTelegramFeatures': undefined;
|
||||||
'TelegramFeaturesUsername': undefined;
|
'TelegramFeaturesUsername': undefined;
|
||||||
@ -1787,6 +1788,19 @@ export interface LangPair {
|
|||||||
'StarGiftPriceDecreaseInfoLink': undefined;
|
'StarGiftPriceDecreaseInfoLink': undefined;
|
||||||
'StarGiftUpgradeCostModalTitle': undefined;
|
'StarGiftUpgradeCostModalTitle': undefined;
|
||||||
'StarGiftUpgradeCostHint': 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> {
|
export interface LangPairWithVariables<V = LangVariable> {
|
||||||
@ -3084,6 +3098,24 @@ export interface LangPairWithVariables<V = LangVariable> {
|
|||||||
'StarGiftPriceDecreaseTimer': {
|
'StarGiftPriceDecreaseTimer': {
|
||||||
'timer': V;
|
'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 {
|
export interface LangPairPlural {
|
||||||
|
|||||||
@ -32,7 +32,7 @@ import { notifyLangpackUpdate } from '../browser/multitab';
|
|||||||
import { createCallbackManager } from '../callbacks';
|
import { createCallbackManager } from '../callbacks';
|
||||||
import readFallbackStrings from '../data/readFallbackStrings';
|
import readFallbackStrings from '../data/readFallbackStrings';
|
||||||
import { initialEstablishmentPromise, isCurrentTabMaster } from '../establishMultitabRole';
|
import { initialEstablishmentPromise, isCurrentTabMaster } from '../establishMultitabRole';
|
||||||
import { omit, unique } from '../iteratees';
|
import { omit } from '../iteratees';
|
||||||
import { replaceInStringsWithTeact } from '../replaceWithTeact';
|
import { replaceInStringsWithTeact } from '../replaceWithTeact';
|
||||||
import { fastRaf } from '../schedulers';
|
import { fastRaf } from '../schedulers';
|
||||||
|
|
||||||
@ -411,40 +411,46 @@ function processTranslationAdvanced(
|
|||||||
|
|
||||||
const variableEntries = variables ? Object.entries(variables) : [];
|
const variableEntries = variables ? Object.entries(variables) : [];
|
||||||
|
|
||||||
let tempResult: TeactNode = [string];
|
let tempResult: TeactNode = string;
|
||||||
if (options?.specialReplacement) {
|
if (options?.specialReplacement) {
|
||||||
const specialReplacements = Object.entries(options.specialReplacement);
|
const specialReplacements = Object.entries(options.specialReplacement);
|
||||||
tempResult = specialReplacements.reduce((acc, [key, value]) => {
|
tempResult = specialReplacements.reduce((acc, [key, value]) => {
|
||||||
return replaceInStringsWithTeact(acc, key, value);
|
return replaceInStringsWithTeact(acc, key, value);
|
||||||
}, tempResult);
|
}, tempResult as TeactNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
const withRenderText = options?.withMarkdown || options?.renderTextFilters;
|
const withRenderText = options?.withNodes;
|
||||||
|
|
||||||
if (withRenderText) {
|
if (withRenderText) {
|
||||||
const filters = options?.withMarkdown
|
const textFiltersSet = new Set(options?.renderTextFilters);
|
||||||
? unique((options.renderTextFilters || []).concat(['simple_markdown', 'emoji']))
|
textFiltersSet.add('emoji');
|
||||||
: options.renderTextFilters;
|
|
||||||
|
|
||||||
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') {
|
if (typeof curr !== 'string') {
|
||||||
return curr;
|
return curr;
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderText(curr, filters, {
|
return renderText(curr, filters, {
|
||||||
markdownPostProcessor: (part: string) => {
|
markdownPostProcessor: (part: string) => {
|
||||||
return variableEntries.reduce((result, [key, value]): TeactNode[] => {
|
return variableEntries.reduce((result, [key, value]): TeactNode => {
|
||||||
if (value === undefined) return result;
|
if (value === undefined) return result;
|
||||||
|
|
||||||
const preparedValue = Number.isFinite(value) ? formatters!.number.format(value as number) : value;
|
const preparedValue = Number.isFinite(value) ? formatters!.number.format(value as number) : value;
|
||||||
return replaceInStringsWithTeact(result, `{${key}}`, renderText(preparedValue));
|
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;
|
if (value === undefined) return result;
|
||||||
|
|
||||||
const preparedValue = Number.isFinite(value) ? formatters!.number.format(value as number) : value;
|
const preparedValue = Number.isFinite(value) ? formatters!.number.format(value as number) : value;
|
||||||
|
|||||||
@ -12,10 +12,14 @@ export function replaceWithTeact(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function replaceInStringsWithTeact(
|
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) => {
|
return input.flatMap((curr: TeactNode) => {
|
||||||
if (typeof curr === 'string') return replaceWithTeact(curr, searchValue, replaceValue);
|
if (typeof curr === 'string') return replaceWithTeact(curr, searchValue, replaceValue);
|
||||||
|
if (Array.isArray(curr)) return replaceInStringsWithTeact(curr, searchValue, replaceValue);
|
||||||
return curr;
|
return curr;
|
||||||
}, []);
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user