Messages: Implement non-сontact Info (#5708)

Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com>
This commit is contained in:
Alexander Zinchuk 2025-04-04 13:03:09 +02:00
parent d4c7fdb7ed
commit dc61b12f9b
34 changed files with 677 additions and 316 deletions

View File

@ -13,7 +13,6 @@ import type {
ApiChatlistInvite,
ApiChatMember,
ApiChatReactions,
ApiChatSettings,
ApiExportedInvite,
ApiMissingInvitedUser,
ApiRestrictionReason,
@ -517,20 +516,6 @@ export function buildChatInviteImporter(importer: GramJs.ChatInviteImporter): Ap
};
}
export function buildApiChatSettings({
autoarchived,
reportSpam,
addContact,
blockContact,
}: GramJs.PeerSettings): ApiChatSettings {
return {
isAutoArchived: Boolean(autoarchived),
canReportSpam: Boolean(reportSpam),
canAddContact: Boolean(addContact),
canBlockContact: Boolean(blockContact),
};
}
export function buildApiChatReactions(chatReactions?: GramJs.TypeChatReactions): ApiChatReactions | undefined {
if (chatReactions instanceof GramJs.ChatReactionsAll) {
return {

View File

@ -2,6 +2,7 @@ import { Api as GramJs } from '../../../lib/gramjs';
import type {
ApiBirthday,
ApiPeerSettings,
ApiUser,
ApiUserFullInfo,
ApiUserStatus,
@ -24,7 +25,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
fallbackPhoto, personalPhoto, translationsDisabled, storiesPinnedAvailable,
contactRequirePremium, businessWorkHours, businessLocation, businessIntro,
birthday, personalChannelId, personalChannelMessage, sponsoredEnabled, stargiftsCount, botVerification,
botCanManageEmojiStatus,
botCanManageEmojiStatus, settings,
},
users,
} = mtpUserFull;
@ -55,6 +56,29 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
starGiftCount: stargiftsCount,
isBotCanManageEmojiStatus: botCanManageEmojiStatus,
hasScheduledMessages: hasScheduled,
settings: buildApiPeerSettings(settings),
};
}
export function buildApiPeerSettings({
autoarchived,
reportSpam,
addContact,
blockContact,
registrationMonth,
phoneCountry,
nameChangeDate,
photoChangeDate,
}: GramJs.PeerSettings): ApiPeerSettings {
return {
isAutoArchived: Boolean(autoarchived),
canReportSpam: Boolean(reportSpam),
canAddContact: Boolean(addContact),
canBlockContact: Boolean(blockContact),
registrationMonth,
phoneCountry,
nameChangeDate,
photoChangeDate,
};
}

View File

@ -44,7 +44,6 @@ import {
buildApiChatlistExportedInvite,
buildApiChatlistInvite,
buildApiChatReactions,
buildApiChatSettings,
buildApiMissingInvitedUser,
buildApiTopic,
buildChatMember,
@ -56,7 +55,7 @@ import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages';
import { buildApiPeerNotifySettings } from '../apiBuilders/misc';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
import { buildStickerSet } from '../apiBuilders/symbols';
import { buildApiUser, buildApiUserStatuses } from '../apiBuilders/users';
import { buildApiPeerSettings, buildApiUser, buildApiUserStatuses } from '../apiBuilders/users';
import {
buildChatAdminRights,
buildChatBannedRights,
@ -354,8 +353,8 @@ export function fetchFullChat(chat: ApiChat) {
: getFullChatInfo(id);
}
export async function fetchChatSettings(chat: ApiChat) {
const { id, accessHash } = chat;
export async function fetchPeerSettings(peer: ApiPeer) {
const { id, accessHash } = peer;
const result = await invokeRequest(new GramJs.messages.GetPeerSettings({
peer: buildInputPeer(id, accessHash),
@ -368,7 +367,7 @@ export async function fetchChatSettings(chat: ApiChat) {
}
return {
settings: buildApiChatSettings(result.settings),
settings: buildApiPeerSettings(result.settings),
};
}

View File

@ -1,7 +1,7 @@
import { Api as GramJs } from '../../../lib/gramjs';
import type {
ApiChat, ApiError, ApiUser, ApiUsername,
ApiChat, ApiError, ApiPeer, ApiUser, ApiUsername,
} from '../../types';
import { ACCEPTABLE_USERNAME_ERRORS } from '../../../config';
@ -280,8 +280,8 @@ export function hideAllChatJoinRequests({
});
}
export function hideChatReportPane(chat: ApiChat) {
const { id, accessHash } = chat;
export function hidePeerSettingsBar(peer: ApiPeer) {
const { id, accessHash } = peer;
return invokeRequest(new GramJs.messages.HidePeerSettingsBar({
peer: buildInputPeer(id, accessHash),

View File

@ -22,7 +22,6 @@ import {
import {
buildApiChatFolder,
buildApiChatFromPreview,
buildApiChatSettings,
buildChatMember,
buildChatMembers,
buildChatTypingStatus,
@ -61,7 +60,7 @@ import {
import { buildApiStealthMode, buildApiStory } from '../apiBuilders/stories';
import { buildApiEmojiInteraction, buildStickerSet } from '../apiBuilders/symbols';
import {
buildApiUser,
buildApiPeerSettings,
buildApiUserStatus,
} from '../apiBuilders/users';
import {
@ -776,42 +775,14 @@ export function updater(update: Update) {
user: { phoneNumber: phone },
});
} else if (update instanceof GramJs.UpdatePeerSettings) {
// eslint-disable-next-line @typescript-eslint/naming-convention
const { _entities, settings } = update;
if (!_entities) {
return;
}
if (_entities?.length) {
_entities
.filter((e) => e instanceof GramJs.User && !e.contact)
.forEach((user) => {
sendApiUpdate({
'@type': 'deleteContact',
id: buildApiPeerId(user.id, 'user'),
});
});
_entities
.filter((e) => e instanceof GramJs.User && e.contact)
.map(buildApiUser)
.forEach((user) => {
if (!user) {
return;
}
sendApiUpdate({
'@type': 'updateUser',
id: user.id,
user: {
...user,
...(settings && { settings: buildApiChatSettings(settings) }),
},
});
});
}
// Settings
const { peer, settings } = update;
const peerId = getApiChatIdFromMtpPeer(peer);
const apiSettings = buildApiPeerSettings(settings);
sendApiUpdate({
'@type': 'updatePeerSettings',
id: peerId,
settings: apiSettings,
});
} else if (update instanceof GramJs.UpdateNotifySettings) {
const {
notifySettings,

View File

@ -68,9 +68,6 @@ export interface ApiChat {
accessHash?: string;
};
// Obtained from GetChatSettings
settings?: ApiChatSettings;
joinRequests?: ApiChatInviteImporter[];
isJoinToSend?: boolean;
isJoinRequest?: boolean;
@ -230,11 +227,15 @@ export interface ApiChatFolder {
hasMyInvites?: true;
}
export interface ApiChatSettings {
export interface ApiPeerSettings {
isAutoArchived?: boolean;
canReportSpam?: boolean;
canAddContact?: boolean;
canBlockContact?: boolean;
registrationMonth?: string;
phoneCountry?: string;
nameChangeDate?: number;
photoChangeDate?: number;
}
export interface ApiSendAsPeerId {

View File

@ -18,6 +18,7 @@ import type {
ApiChatFullInfo,
ApiChatMember,
ApiDraft,
ApiPeerSettings,
ApiTypingStatus,
} from './chats';
import type {
@ -403,6 +404,12 @@ export type ApiDeleteContact = {
id: string;
};
export type ApiUpdatePeerSettings = {
'@type': 'updatePeerSettings';
id: string;
settings: ApiPeerSettings;
};
export type ApiUpdateUser = {
'@type': 'updateUser';
id: string;
@ -822,7 +829,7 @@ export type ApiUpdate = (
ApiUpdateDeleteMessages | ApiUpdateMessagePoll | ApiUpdateMessagePollVote | ApiUpdateDeleteHistory |
ApiDeleteParticipantHistory | ApiUpdateMessageSendSucceeded | ApiUpdateMessageSendFailed |
ApiUpdateServiceNotification | ApiDeleteContact | ApiUpdateUser | ApiUpdateUserStatus |
ApiUpdateUserFullInfo | ApiUpdateVideoProcessingPending |
ApiUpdateUserFullInfo | ApiUpdateVideoProcessingPending | ApiUpdatePeerSettings |
ApiUpdateAvatar | ApiUpdateMessageImage | ApiUpdateDraftMessage |
ApiUpdateError | ApiUpdateResetContacts | ApiUpdateStartEmojiInteraction |
ApiUpdateFavoriteStickers | ApiUpdateStickerSet | ApiUpdateStickerSets | ApiUpdateStickerSetsOrder |

View File

@ -1,7 +1,7 @@
import type { API_CHAT_TYPES } from '../../config';
import type { ApiBotInfo } from './bots';
import type { ApiBusinessIntro, ApiBusinessLocation, ApiBusinessWorkHours } from './business';
import type { ApiPeerColor } from './chats';
import type { ApiPeerColor, ApiPeerSettings } from './chats';
import type { ApiDocument, ApiPhoto } from './messages';
import type { ApiBotVerification } from './misc';
import type { ApiSavedStarGift } from './stars';
@ -65,6 +65,7 @@ export interface ApiUserFullInfo {
isBotAccessEmojiGranted?: boolean;
hasScheduledMessages?: boolean;
botVerification?: ApiBotVerification;
settings?: ApiPeerSettings;
}
export type ApiFakeType = 'fake' | 'scam';

View File

@ -252,6 +252,10 @@
"ChatListDeleteChatConfirmation" = "Are you sure you want to delete the chat\nwith {chat}?";
"DeleteAndStop" = "Delete and Block";
"ChatListDeleteForEveryone" = "Delete for me and {user}";
"ChatNonContactUserSubtitle" = "Not a contact";
"ChatNonContactUserGroups" = "Shared Groups";
"ContactInfoRegistration" = "Registration";
"ContactInfoNotVerified" = "Not an official account";
"DeleteForAll" = "Delete for all members";
"DeleteSingleMessagesTitle" = "Delete message";
"AreYouSureDeleteSingleMessage" = "Are you sure you want to delete this message?";
@ -1117,6 +1121,7 @@
"WeekdaySaturday" = "Saturday";
"WeekdaySunday" = "Sunday";
"WeekdayToday" = "Today";
"Today" = "today";
"WeekdayYesterday" = "Yesterday";
"User" = "User";
"SecretChat" = "Secret Chat";
@ -1193,6 +1198,14 @@
"Seconds_other" = "{count} seconds";
"Minutes_one" = "{count} minute";
"Minutes_other" = "{count} minutes";
"JustNowAgo" = "just now";
"MinutesAgo_one" = "1 minute ago";
"MinutesAgo_other" = "{count} minutes ago";
"HoursAgo_one" = "1 hour ago";
"HoursAgo_other" = "{count} hours ago";
"DaysAgo_one" = "1 day ago";
"DaysAgo_other" = "{count} days ago";
"AtDateAgo" = "on {date}";
"AudioPause" = "Pause audio";
"AudioPlay" = "Play audio";
"ToggleUserNotifications" = "Toggle user notifications";
@ -1419,6 +1432,8 @@
"GiftInfoConvertDescription2" = "This action cannot be undone. This will permanently destroy the gift.";
"GiftInfoConvertDescriptionPeriod_one" = "Conversion is available for the next **{count} days**.";
"GiftInfoConvertDescriptionPeriod_other" = "Conversion is available for the next **{count} days**.";
"UserUpdatedName" = "{user} updated name {time}";
"UserUpdatedPhoto" = "{user} updated photo {time}";
"GiftInfoSaved" = "This gift is visible on your profile. {link}";
"GiftInfoHidden" = "This gift is hidden. Only you can see it. {link}";
"GiftInfoChannelSaved" = "This gift is visible in your channel's profile. {link}";
@ -1487,6 +1502,8 @@
"GiftWithdrawSubmit" = "Open Fragment";
"GiftWithdrawWait_one" = "In {days} day, you'll be able to send this collectible to any TON blockchain address outside Telegram for sale or auction.";
"GiftWithdrawWait_other" = "In {days} days, you'll be able to send this collectible to any TON blockchain address outside Telegram for sale or auction.";
"ChatGroups_one" = "{count} group";
"ChatGroups_other" = "{count} groups";
"StarsAmount" = "⭐️{amount}";
"StarsAmountText_one" = "{amount} Star";
"StarsAmountText_other" = "{amount} Stars";

View File

@ -13,7 +13,7 @@ import { preloadImage } from '../../util/files';
import preloadFonts from '../../util/fonts';
import { pick } from '../../util/iteratees';
import { oldSetLanguage } from '../../util/oldLangProvider';
import { formatPhoneNumber, getCountryCodesByIso, getCountryFromPhoneNumber } from '../../util/phoneNumber';
import { formatPhoneNumber, getCountryCodeByIso, getCountryFromPhoneNumber } from '../../util/phoneNumber';
import { IS_SAFARI, IS_TOUCH_ENV } from '../../util/windowEnvironment';
import { getSuggestedLanguage } from './helpers/getSuggestedLanguage';
@ -101,7 +101,7 @@ const AuthPhoneNumber: FC<StateProps> = ({
useEffect(() => {
if (authNearestCountry && phoneCodeList && !country && !isTouched) {
setCountry(getCountryCodesByIso(phoneCodeList, authNearestCountry)[0]);
setCountry(getCountryCodeByIso(phoneCodeList, authNearestCountry));
}
}, [country, authNearestCountry, isTouched, phoneCodeList]);

View File

@ -6,7 +6,10 @@ import { addExtraClass, removeExtraClass } from '../../lib/teact/teact-dom';
import { getActions, getGlobal, withGlobal } from '../../global';
import type {
ApiChatFullInfo, ApiMessage, ApiRestrictionReason, ApiTopic,
ApiChatFullInfo,
ApiMessage,
ApiRestrictionReason,
ApiTopic,
} from '../../api/types';
import type { OnIntersectPinnedMessage } from './hooks/usePinnedMessage';
import { MAIN_THREAD_ID } from '../../api/types';
@ -74,7 +77,7 @@ import useStickyDates from './hooks/useStickyDates';
import Loading from '../ui/Loading';
import ContactGreeting from './ContactGreeting';
import MessageListBotInfo from './MessageListBotInfo';
import MessageListAccountInfo from './MessageListAccountInfo';
import MessageListContent from './MessageListContent';
import NoMessages from './NoMessages';
import PremiumRequiredMessage from './PremiumRequiredMessage';
@ -106,6 +109,9 @@ type StateProps = {
isCreator?: boolean;
isChannelWithAvatars?: boolean;
isBot?: boolean;
isNonContact?: boolean;
nameChangeDate?: number;
photoChangeDate?: number;
isSynced?: boolean;
messageIds?: number[];
messagesById?: Record<number, ApiMessage>;
@ -159,6 +165,9 @@ const MessageList: FC<OwnProps & StateProps> = ({
isAnonymousForwards,
isCreator,
isBot,
isNonContact,
nameChangeDate,
photoChangeDate,
messageIds,
messagesById,
firstUnreadId,
@ -682,8 +691,8 @@ const MessageList: FC<OwnProps & StateProps> = ({
</div>
) : isContactRequirePremium && !hasMessages ? (
<PremiumRequiredMessage userId={chatId} />
) : isBot && !hasMessages ? (
<MessageListBotInfo chatId={chatId} />
) : (isBot || isNonContact) && !hasMessages ? (
<MessageListAccountInfo chatId={chatId} />
) : shouldRenderGreeting ? (
<ContactGreeting key={chatId} userId={chatId} />
) : messageIds && (!messageGroups || isGroupChatJustCreated || isEmptyTopic) ? (
@ -718,7 +727,9 @@ const MessageList: FC<OwnProps & StateProps> = ({
isReady={isReady}
hasLinkedChat={hasLinkedChat}
isSchedule={messageGroups ? type === 'scheduled' : false}
shouldRenderBotInfo={isBot}
shouldRenderAccountInfo={isBot || isNonContact}
nameChangeDate={nameChangeDate}
photoChangeDate={photoChangeDate}
noAppearanceAnimation={!messageGroups || !shouldAnimateAppearanceRef.current}
onScrollDownToggle={onScrollDownToggle}
onNotchToggle={onNotchToggle}
@ -735,6 +746,7 @@ export default memo(withGlobal<OwnProps>(
(global, { chatId, threadId, type }): StateProps => {
const currentUserId = global.currentUserId!;
const chat = selectChat(global, chatId);
const userFullInfo = selectUserFullInfo(global, chatId);
if (!chat) {
return { currentUserId };
}
@ -763,6 +775,9 @@ export default memo(withGlobal<OwnProps>(
);
const chatBot = selectBot(global, chatId);
const isNonContact = Boolean(userFullInfo?.settings?.canAddContact);
const nameChangeDate = userFullInfo?.settings?.nameChangeDate;
const photoChangeDate = userFullInfo?.settings?.photoChangeDate;
const topic = selectTopic(global, chatId, threadId);
const chatFullInfo = !isUserId(chatId) ? selectChatFullInfo(global, chatId) : undefined;
@ -784,6 +799,9 @@ export default memo(withGlobal<OwnProps>(
isSystemBotChat: isSystemBot(chatId),
isAnonymousForwards: isAnonymousForwardsChat(chatId),
isBot: Boolean(chatBot),
isNonContact,
nameChangeDate,
photoChangeDate,
isSynced: global.isSynced,
messageIds,
messagesById,

View File

@ -0,0 +1,109 @@
.root {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 0 0.6875rem;
}
.chatInfo {
max-width: min(80%, 25rem);
min-width: 15.8125rem;
display: flex;
flex-direction: column;
font-size: calc(var(--message-text-size, 1rem) - 0.0625rem);
border-radius: var(--border-radius-messages);
overflow: hidden;
text-align: center;
}
.chatBackground {
background-color: var(--action-message-bg);
color: white;
padding: 1rem 1.5rem;
margin: 5rem 0 2rem;
contain: layout; // Force new stacking context for text blending
}
.botBackground {
background-color: var(--color-background);
color: var(--color-text);
}
.bot-info-description {
padding: 0.5rem 1rem;
text-wrap: pretty;
}
.bot-info-title {
font-weight: var(--font-weight-medium);
margin-bottom: 0.25rem;
}
.media {
max-width: 100%;
height: auto !important;
}
.chatInfoTitle {
font-weight: var(--font-weight-semibold);
margin: 0 0 0.25rem;
}
.chatInfoSubtitle {
margin-bottom: 0.25rem;
}
.chatDescription {
display: flex;
justify-content: flex-start;
align-items: center;
gap: 0.25rem;
margin: 0;
}
.country {
width: 1rem;
height: 1rem;
}
:global(.emoji-small) {
width: 1rem;
height: 1rem;
margin-top: -0.1875rem;
}
.chatNotVerified {
display: flex;
justify-content: center;
align-items: center;
gap: 0.25rem;
margin-top: 1rem;
}
.verifiedTitle {
margin-bottom: 0;
}
.link {
display: flex;
align-items: center;
gap: 0.25rem;
&:hover {
text-decoration: none;
}
}
.linkInfo:hover {
text-decoration: underline;
}
.icon {
margin-inline-start: -0.1875rem;
}
.textColor {
color: white;
mix-blend-mode: overlay;
}

View File

@ -0,0 +1,293 @@
import type { FC } from '../../lib/teact/teact';
import React, {
memo,
useEffect,
useMemo,
useRef,
} from '../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../global';
import type {
ApiBotInfo, ApiChat, ApiCountryCode, ApiUserCommonChats, ApiUserFullInfo,
} from '../../api/types';
import {
getBotCoverMediaHash,
getChatTitle,
getPhotoFullDimensions,
getVideoDimensions,
getVideoMediaHash,
isChatWithVerificationCodesBot,
} from '../../global/helpers';
import {
selectBot, selectChat, selectPeer, selectUserCommonChats, selectUserFullInfo,
} from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle';
import { formatPastDatetime, formatRegistrationMonth } from '../../util/dates/dateFormat';
import { isoToEmoji } from '../../util/emoji/emoji';
import { getCountryCodeByIso } from '../../util/phoneNumber';
import stopEvent from '../../util/stopEvent';
import renderText from '../common/helpers/renderText';
import useEffectOnce from '../../hooks/useEffectOnce';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import useMedia from '../../hooks/useMedia';
import useOldLang from '../../hooks/useOldLang';
import useShowTransition from '../../hooks/useShowTransition';
import AvatarList from '../common/AvatarList';
import Icon from '../common/icons/Icon';
import MiniTable, { type TableEntry } from '../common/MiniTable';
import Link from '../ui/Link';
import OptimizedVideo from '../ui/OptimizedVideo';
import Skeleton from '../ui/placeholder/Skeleton';
import styles from './MessageListAccountInfo.module.scss';
type OwnProps = {
chatId: string;
isInMessageList?: boolean;
};
type StateProps = {
chat?: ApiChat;
botInfo?: ApiBotInfo;
isLoadingFullUser?: boolean;
phoneCodeList?: ApiCountryCode[];
commonChats?: ApiUserCommonChats;
userFullInfo?: ApiUserFullInfo;
};
const MessageListAccountInfo: FC<OwnProps & StateProps> = ({
chat,
chatId,
botInfo,
isLoadingFullUser,
isInMessageList,
phoneCodeList,
commonChats,
userFullInfo,
}) => {
const { loadCommonChats, openChatWithInfo } = getActions();
const oldLang = useOldLang();
const lang = useLang();
const {
phoneCountry,
registrationMonth,
nameChangeDate,
photoChangeDate,
} = userFullInfo?.settings || {};
useEffect(() => {
loadCommonChats({ userId: chatId });
}, [chatId]);
const country = useMemo(() => {
if (!phoneCodeList || !phoneCountry) return undefined;
return getCountryCodeByIso(phoneCodeList, phoneCountry);
}, [phoneCodeList, phoneCountry]);
const botInfoPhotoUrl = useMedia(botInfo?.photo ? getBotCoverMediaHash(botInfo.photo) : undefined);
const botInfoGifUrl = useMedia(botInfo?.gif ? getVideoMediaHash(botInfo.gif, 'full') : undefined);
const botInfoDimensions = botInfo?.photo ? getPhotoFullDimensions(botInfo.photo) : botInfo?.gif
? getVideoDimensions(botInfo.gif) : undefined;
const isBotInfoEmpty = botInfo && !botInfo.description && !botInfo.gif && !botInfo.photo;
const isChatInfoEmpty = !country || !registrationMonth;
const isVerifyCodes = isChatWithVerificationCodesBot(chatId);
const { width, height } = botInfoDimensions || {};
const handleClick = useLastCallback((e: React.SyntheticEvent<any>) => {
stopEvent(e);
openChatWithInfo({
id: chatId, shouldReplaceHistory: true, profileTab: 'commonChats', forceScrollProfileTab: true,
});
});
const securityNameInfo = nameChangeDate && chat ? (
<div className="local-action-message" key="security-name-message">
<span>{lang('UserUpdatedName', {
user: chat.title,
time: formatPastDatetime(lang, nameChangeDate),
}, { withNodes: true, withMarkdown: true })}
</span>
</div>
) : undefined;
const securityPhotoInfo = photoChangeDate && chat ? (
<div className="local-action-message" key="security-photo-message">
<span>{lang('UserUpdatedPhoto', {
user: chat.title,
time: formatPastDatetime(lang, photoChangeDate),
}, { withNodes: true, withMarkdown: true })}
</span>
</div>
) : undefined;
const tableData = useMemo((): TableEntry[] => {
const entries: TableEntry[] = [];
if (country) {
entries.push([
oldLang('PrivacyPhone'),
<span className={styles.chatDescription}>
<span className={styles.country}>
{renderText(isoToEmoji(country?.iso2))}
</span>
{country?.defaultName}
</span>,
]);
}
if (registrationMonth) {
entries.push([
lang('ContactInfoRegistration'),
formatRegistrationMonth(lang.code, registrationMonth),
]);
}
if (userFullInfo?.commonChatsCount) {
const global = getGlobal();
const peers = commonChats?.ids.slice(0, 3).map((id) => selectPeer(global, id)!).filter(Boolean);
entries.push([
lang('ChatNonContactUserGroups'),
<Link className={styles.link} onClick={handleClick}>
<span className={styles.linkInfo}>
{lang('ChatGroups', {
count: userFullInfo.commonChatsCount,
}, {
pluralValue: userFullInfo.commonChatsCount,
})}
</span>
{Boolean(peers?.length) && <AvatarList size="micro" peers={peers} />}
<Icon name="next" className={styles.icon} />
</Link>,
]);
}
return entries;
}, [lang, oldLang, country, registrationMonth, commonChats, userFullInfo]);
const isEmptyOrLoading = (isBotInfoEmpty && isChatInfoEmpty) || isLoadingFullUser;
const isFirstRenderRef = useRef(true);
const {
shouldRender,
ref,
} = useShowTransition({
isOpen: !isEmptyOrLoading && isInMessageList,
withShouldRender: true,
});
useEffectOnce(() => {
isFirstRenderRef.current = false;
});
if (!shouldRender) return undefined;
return (
<div ref={ref} className={buildClassName(styles.root, 'empty')}>
{isLoadingFullUser && isChatInfoEmpty && <span>{oldLang('Loading')}</span>}
{(isBotInfoEmpty && isChatInfoEmpty) && !isLoadingFullUser && <span>{oldLang('NoMessages')}</span>}
{botInfo && (
<div
className={buildClassName(styles.chatInfo, styles.botBackground)}
style={buildStyle(
width ? `width: ${width}px` : undefined,
)}
>
{botInfoPhotoUrl && (
<img
className={styles.media}
src={botInfoPhotoUrl}
width={width}
height={height}
alt="Bot info"
/>
)}
{botInfoGifUrl && (
<OptimizedVideo
canPlay
className={styles.media}
src={botInfoGifUrl}
loop
disablePictureInPicture
muted
playsInline
style={buildStyle(Boolean(width) && `width: ${width}px`, Boolean(height) && `height: ${height}px`)}
/>
)}
{botInfoDimensions && !botInfoPhotoUrl && !botInfoGifUrl && (
<Skeleton
className={styles.media}
width={width}
height={height}
forceAspectRatio
/>
)}
{isVerifyCodes && (
<div className={styles.botInfoDescription}>
{oldLang('VerifyChatInfo')}
</div>
)}
{!isVerifyCodes && botInfo.description && (
<div className={styles.botInfoDescription}>
<p className={styles.botInfoTitle}>{oldLang('BotInfoTitle')}</p>
{renderText(botInfo.description, ['br', 'emoji', 'links'])}
</div>
)}
</div>
)}
{!isChatInfoEmpty && chat && (
<div
className={buildClassName(styles.chatInfo, styles.chatBackground)}
>
<h3 className={styles.chatInfoTitle}>{renderText(getChatTitle(lang, chat))}</h3>
<p className={buildClassName(styles.chatInfoSubtitle, styles.textColor)}>
{lang('ChatNonContactUserSubtitle')}
</p>
<MiniTable keyClassName={styles.textColor} data={tableData} />
{!chat?.isVerified && (
<div className={buildClassName(styles.chatNotVerified, styles.textColor)}>
<Icon name="info-filled" />
<p className={styles.verifiedTitle}>{lang('ContactInfoNotVerified')}</p>
</div>
)}
</div>
)}
{securityNameInfo}
{securityPhotoInfo}
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, { chatId }) => {
const {
countryList: { phoneCodes: phoneCodeList },
} = global;
const chat = selectChat(global, chatId);
const userFullInfo = selectUserFullInfo(global, chatId);
const commonChats = selectUserCommonChats(global, chatId);
const chatBot = selectBot(global, chatId);
let isLoadingFullUser = false;
let botInfo;
if (chatBot) {
if (userFullInfo) {
botInfo = userFullInfo.botInfo;
} else {
isLoadingFullUser = true;
}
}
return {
chat,
userFullInfo,
botInfo,
isLoadingFullUser,
phoneCodeList,
commonChats,
};
},
)(MessageListAccountInfo));

View File

@ -1,35 +0,0 @@
.root {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.bot-info {
max-width: min(80%, 25rem);
display: flex;
flex-direction: column;
background-color: var(--color-background);
color: var(--color-text);
font-size: calc(var(--message-text-size, 1rem) - 0.0625rem);
border-radius: var(--border-radius-messages);
overflow: hidden;
text-align: initial;
}
.bot-info-description {
padding: 0.5rem 1rem;
text-wrap: pretty;
}
.bot-info-title {
font-weight: var(--font-weight-medium);
margin-bottom: 0.25rem;
}
.media {
max-width: 100%;
height: auto !important;
}

View File

@ -1,134 +0,0 @@
import type { FC } from '../../lib/teact/teact';
import React, { memo } from '../../lib/teact/teact';
import { withGlobal } from '../../global';
import type { ApiBotInfo } from '../../api/types';
import {
getBotCoverMediaHash,
getPhotoFullDimensions,
getVideoDimensions,
getVideoMediaHash,
isChatWithVerificationCodesBot,
} from '../../global/helpers';
import { selectBot, selectUserFullInfo } from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle';
import renderText from '../common/helpers/renderText';
import useMedia from '../../hooks/useMedia';
import useOldLang from '../../hooks/useOldLang';
import OptimizedVideo from '../ui/OptimizedVideo';
import Skeleton from '../ui/placeholder/Skeleton';
import styles from './MessageListBotInfo.module.scss';
type OwnProps = {
chatId: string;
isInMessageList?: boolean;
};
type StateProps = {
botInfo?: ApiBotInfo;
isLoadingBotInfo?: boolean;
};
const MessageListBotInfo: FC<OwnProps & StateProps> = ({
chatId,
botInfo,
isLoadingBotInfo,
isInMessageList,
}) => {
const lang = useOldLang();
const botInfoPhotoUrl = useMedia(botInfo?.photo ? getBotCoverMediaHash(botInfo.photo) : undefined);
const botInfoGifUrl = useMedia(botInfo?.gif ? getVideoMediaHash(botInfo.gif, 'full') : undefined);
const botInfoDimensions = botInfo?.photo ? getPhotoFullDimensions(botInfo.photo) : botInfo?.gif
? getVideoDimensions(botInfo.gif) : undefined;
const isBotInfoEmpty = botInfo && !botInfo.description && !botInfo.gif && !botInfo.photo;
const isVerifyCodes = isChatWithVerificationCodesBot(chatId);
const { width, height } = botInfoDimensions || {};
const isEmptyOrLoading = isBotInfoEmpty || isLoadingBotInfo;
if (isEmptyOrLoading && isInMessageList) return undefined;
return (
<div className={buildClassName(styles.root, 'empty')}>
{isLoadingBotInfo && <span>{lang('Loading')}</span>}
{isBotInfoEmpty && !isLoadingBotInfo && <span>{lang('NoMessages')}</span>}
{botInfo && (
<div
className={styles.botInfo}
style={buildStyle(
width ? `width: ${width}px` : undefined,
)}
>
{botInfoPhotoUrl && (
<img
className={styles.media}
src={botInfoPhotoUrl}
width={width}
height={height}
alt="Bot info"
/>
)}
{botInfoGifUrl && (
<OptimizedVideo
canPlay
className={styles.media}
src={botInfoGifUrl}
loop
disablePictureInPicture
muted
playsInline
style={buildStyle(Boolean(width) && `width: ${width}px`, Boolean(height) && `height: ${height}px`)}
/>
)}
{botInfoDimensions && !botInfoPhotoUrl && !botInfoGifUrl && (
<Skeleton
className={styles.media}
width={width}
height={height}
forceAspectRatio
/>
)}
{isVerifyCodes && (
<div className={styles.botInfoDescription}>
{lang('VerifyChatInfo')}
</div>
)}
{!isVerifyCodes && botInfo.description && (
<div className={styles.botInfoDescription}>
<p className={styles.botInfoTitle}>{lang('BotInfoTitle')}</p>
{renderText(botInfo.description, ['br', 'emoji', 'links'])}
</div>
)}
</div>
)}
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, { chatId }) => {
const chatBot = selectBot(global, chatId);
let isLoadingBotInfo = false;
let botInfo;
if (chatBot) {
const chatBotFullInfo = selectUserFullInfo(global, chatBot.id);
if (chatBotFullInfo) {
botInfo = chatBotFullInfo.botInfo;
} else {
isLoadingBotInfo = true;
}
}
return {
botInfo,
isLoadingBotInfo,
};
},
)(MessageListBotInfo));

View File

@ -33,7 +33,7 @@ import ActionMessage from './message/ActionMessage';
import Message from './message/Message';
import SenderGroupContainer from './message/SenderGroupContainer';
import SponsoredMessage from './message/SponsoredMessage';
import MessageListBotInfo from './MessageListBotInfo';
import MessageListAccountInfo from './MessageListAccountInfo';
interface OwnProps {
canShowAds?: boolean;
@ -57,7 +57,9 @@ interface OwnProps {
isReady: boolean;
hasLinkedChat: boolean | undefined;
isSchedule: boolean;
shouldRenderBotInfo?: boolean;
shouldRenderAccountInfo?: boolean;
nameChangeDate?: number;
photoChangeDate?: number;
noAppearanceAnimation: boolean;
isSavedDialog?: boolean;
onScrollDownToggle: BooleanToVoidFunction;
@ -89,7 +91,9 @@ const MessageListContent: FC<OwnProps> = ({
isReady,
hasLinkedChat,
isSchedule,
shouldRenderBotInfo,
shouldRenderAccountInfo,
nameChangeDate,
photoChangeDate,
noAppearanceAnimation,
isSavedDialog,
onScrollDownToggle,
@ -126,11 +130,11 @@ const MessageListContent: FC<OwnProps> = ({
isReady,
);
const lang = useOldLang();
const oldLang = useOldLang();
const unreadDivider = (
<div className={buildClassName(UNREAD_DIVIDER_CLASS, 'local-action-message')} key="unread-messages">
<span>{lang('UnreadMessages')}</span>
<span>{oldLang('UnreadMessages')}</span>
</div>
);
const messageCountToAnimate = noAppearanceAnimation ? 0 : messageGroups.reduce((acc, messageGroup) => {
@ -251,7 +255,7 @@ const MessageListContent: FC<OwnProps> = ({
/>,
message.id === threadId && (
<div className="local-action-message" key="discussion-started">
<span>{lang(isEmptyThread
<span>{oldLang(isEmptyThread
? (isComments ? 'NoComments' : 'NoReplies') : 'DiscussionStarted')}
</span>
</div>
@ -301,7 +305,8 @@ const MessageListContent: FC<OwnProps> = ({
return (
<div
className={buildClassName('message-date-group', dateGroupIndex === 0 && 'first-message-date-group')}
className={buildClassName('message-date-group', !(nameChangeDate || photoChangeDate)
&& dateGroupIndex === 0 && 'first-message-date-group')}
key={dateGroup.datetime}
onMouseDown={preventMessageInputBlur}
teactFastList
@ -314,12 +319,12 @@ const MessageListContent: FC<OwnProps> = ({
>
<span dir="auto">
{isSchedule && dateGroup.originalDate === SCHEDULED_WHEN_ONLINE && (
lang('MessageScheduledUntilOnline')
oldLang('MessageScheduledUntilOnline')
)}
{isSchedule && dateGroup.originalDate !== SCHEDULED_WHEN_ONLINE && (
lang('MessageScheduledOn', formatHumanDate(lang, dateGroup.datetime, undefined, true))
oldLang('MessageScheduledOn', formatHumanDate(oldLang, dateGroup.datetime, undefined, true))
)}
{!isSchedule && formatHumanDate(lang, dateGroup.datetime)}
{!isSchedule && formatHumanDate(oldLang, dateGroup.datetime)}
</span>
</div>
{senderGroups.flat()}
@ -330,7 +335,8 @@ const MessageListContent: FC<OwnProps> = ({
return (
<div className="messages-container" teactFastList>
{withHistoryTriggers && <div ref={backwardsTriggerRef} key="backwards-trigger" className="backwards-trigger" />}
{shouldRenderBotInfo && <MessageListBotInfo isInMessageList key={`bot_info_${chatId}`} chatId={chatId} />}
{shouldRenderAccountInfo
&& <MessageListAccountInfo isInMessageList key={`account_info_${chatId}`} chatId={chatId} />}
{dateGroups.flat()}
{withHistoryTriggers && (
<div

View File

@ -138,7 +138,7 @@ type StateProps = {
shouldSkipHistoryAnimations?: boolean;
currentTransitionKey: number;
isChannel?: boolean;
areChatSettingsLoaded?: boolean;
arePeerSettingsLoaded?: boolean;
canSubscribe?: boolean;
canStartBot?: boolean;
canRestartBot?: boolean;
@ -198,7 +198,7 @@ function MiddleColumn({
shouldSkipHistoryAnimations,
currentTransitionKey,
isChannel,
areChatSettingsLoaded,
arePeerSettingsLoaded,
canSubscribe,
canStartBot,
canRestartBot,
@ -219,7 +219,7 @@ function MiddleColumn({
openPreviousChat,
unpinAllMessages,
loadUser,
loadChatSettings,
loadPeerSettings,
exitMessageSelectMode,
joinChannel,
sendBotCommand,
@ -339,10 +339,10 @@ function MiddleColumn({
}, [chatId, isPrivate, loadUser]);
useEffect(() => {
if (!areChatSettingsLoaded) {
loadChatSettings({ chatId: chatId! });
if (!arePeerSettingsLoaded) {
loadPeerSettings({ peerId: chatId! });
}
}, [chatId, isPrivate, areChatSettingsLoaded]);
}, [chatId, isPrivate, arePeerSettingsLoaded]);
useEffect(() => {
if (chatId && shouldLoadFullChat && isReady) {
@ -762,6 +762,7 @@ export default memo(withGlobal<OwnProps>(
const bot = selectBot(global, chatId);
const pinnedIds = selectPinnedIds(global, chatId, threadId);
const chatFullInfo = chatId ? selectChatFullInfo(global, chatId) : undefined;
const userFullInfo = chatId ? selectUserFullInfo(global, chatId) : undefined;
const threadInfo = selectThreadInfo(global, chatId, threadId);
const isMessageThread = Boolean(!threadInfo?.isCommentsInfo && threadInfo?.fromChannelId);
@ -809,7 +810,7 @@ export default memo(withGlobal<OwnProps>(
chat,
draftReplyInfo,
isPrivate,
areChatSettingsLoaded: Boolean(chat?.settings),
arePeerSettingsLoaded: Boolean(userFullInfo?.settings),
isComments: isMessageThread,
canPost:
!isPinnedMessageList

View File

@ -4,12 +4,13 @@ import React, {
import { setExtraStyles } from '../../lib/teact/teact-dom';
import { withGlobal } from '../../global';
import type { ApiChat, ApiUserFullInfo } from '../../api/types';
import type { MessageListType, ThreadId } from '../../types';
import type { Signal } from '../../util/signals';
import { type ApiChat, MAIN_THREAD_ID } from '../../api/types';
import { MAIN_THREAD_ID } from '../../api/types';
import {
selectChat, selectChatMessage, selectCurrentMiddleSearch, selectTabState,
selectChat, selectChatMessage, selectCurrentMiddleSearch, selectTabState, selectUserFullInfo,
} from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
@ -40,6 +41,7 @@ type OwnProps = {
type StateProps = {
chat?: ApiChat;
userFullInfo?: ApiUserFullInfo;
isAudioPlayerRendered?: boolean;
isMiddleSearchOpen?: boolean;
};
@ -52,13 +54,14 @@ const MiddleHeaderPanes = ({
threadId,
messageListType,
chat,
userFullInfo,
getCurrentPinnedIndex,
getLoadingPinnedId,
isAudioPlayerRendered,
isMiddleSearchOpen,
onFocusPinnedMessage,
}: OwnProps & StateProps) => {
const { settings } = chat || {};
const { settings } = userFullInfo || {};
const { isDesktop } = useAppLayout();
const [getAudioPlayerState, setAudioPlayerState] = useSignal<PaneState>(FALLBACK_PANE_STATE);
@ -163,6 +166,7 @@ export default memo(withGlobal<OwnProps>(
}): StateProps => {
const { audioPlayer } = selectTabState(global);
const chat = selectChat(global, chatId);
const userFullInfo = selectUserFullInfo(global, chatId);
const { chatId: audioChatId, messageId: audioMessageId } = audioPlayer;
const audioMessage = audioChatId && audioMessageId
@ -173,6 +177,7 @@ export default memo(withGlobal<OwnProps>(
return {
chat,
userFullInfo,
isAudioPlayerRendered: Boolean(audioMessage),
isMiddleSearchOpen,
};

View File

@ -14,7 +14,7 @@ import {
isGeoLiveExpired,
} from '../../../global/helpers';
import buildClassName from '../../../util/buildClassName';
import { formatCountdownShort, formatLastUpdated } from '../../../util/dates/dateFormat';
import { formatCountdownShort, formatLocationLastUpdate } from '../../../util/dates/dateFormat';
import {
getMetersPerPixel, getVenueColor, getVenueIconUrl,
} from '../../../util/map';
@ -161,7 +161,7 @@ const Location: FC<OwnProps> = ({
<div className="location-info">
<div className="location-info-title">{lang('AttachLiveLocation')}</div>
<div className="location-info-subtitle">
{formatLastUpdated(lang, serverTime, message.editDate)}
{formatLocationLastUpdate(lang, serverTime, message.editDate)}
</div>
{!isExpired && (
<div className="geo-countdown" ref={countdownRef}>

View File

@ -59,7 +59,7 @@ const ChatReportPane: FC<OwnProps & StateProps> = ({
deleteChatUser,
deleteHistory,
toggleChatArchived,
hideChatReportPane,
hidePeerSettingsBar,
} = getActions();
const lang = useOldLang();
@ -96,7 +96,7 @@ const ChatReportPane: FC<OwnProps & StateProps> = ({
});
const handleCloseReportPane = useLastCallback(() => {
hideChatReportPane({ chatId });
hidePeerSettingsBar({ peerId: chatId });
});
const handleChatReportSpam = useLastCallback(() => {

View File

@ -24,10 +24,11 @@ import {
selectPerformanceSettingsValue,
selectTabState,
selectUser,
selectUserFullInfo,
} from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import captureKeyboardListeners from '../../util/captureKeyboardListeners';
import { formatMediaDuration, formatRelativeTime } from '../../util/dates/dateFormat';
import { formatMediaDuration, formatRelativePastTime } from '../../util/dates/dateFormat';
import download from '../../util/download';
import { round } from '../../util/math';
import { getServerTime } from '../../util/serverTime';
@ -94,7 +95,7 @@ interface StateProps {
storyChangelogUserId?: string;
viewersExpirePeriod: number;
isChatExist?: boolean;
areChatSettingsLoaded?: boolean;
arePeerSettingsLoaded?: boolean;
isCurrentUserPremium?: boolean;
stealthMode: ApiStealthMode;
withHeaderAnimation?: boolean;
@ -122,7 +123,7 @@ function Story({
storyChangelogUserId,
viewersExpirePeriod,
isChatExist,
areChatSettingsLoaded,
arePeerSettingsLoaded,
getIsAnimating,
isCurrentUserPremium,
stealthMode,
@ -143,7 +144,7 @@ function Story({
openChat,
showNotification,
openStoryPrivacyEditor,
loadChatSettings,
loadPeerSettings,
fetchChat,
loadStoryViews,
toggleStealthModal,
@ -279,10 +280,10 @@ function Story({
}
}, [isChatExist, peerId]);
useEffect(() => {
if (isChatExist && !areChatSettingsLoaded) {
loadChatSettings({ chatId: peerId });
if (isChatExist && !arePeerSettingsLoaded) {
loadPeerSettings({ peerId });
}
}, [areChatSettingsLoaded, isChatExist, peerId]);
}, [arePeerSettingsLoaded, isChatExist, peerId]);
const handlePauseStory = useLastCallback(() => {
if (isVideo) {
@ -672,7 +673,7 @@ function Story({
</span>
)}
{story && 'date' in story && (
<span className={styles.storyMeta}>{formatRelativeTime(lang, serverTime, story.date)}</span>
<span className={styles.storyMeta}>{formatRelativePastTime(lang, serverTime, story.date)}</span>
)}
{isLoadedStory && story.isEdited && (
<span className={styles.storyMeta}>{lang('Story.HeaderEdited')}</span>
@ -906,6 +907,7 @@ export default memo(withGlobal<OwnProps>((global, {
const { appConfig } = global;
const user = selectUser(global, peerId);
const chat = selectChat(global, peerId);
const userFullInfo = selectUserFullInfo(global, peerId);
const tabState = selectTabState(global);
const {
storyViewer: {
@ -951,7 +953,7 @@ export default memo(withGlobal<OwnProps>((global, {
storyChangelogUserId: appConfig!.storyChangelogUserId,
viewersExpirePeriod: appConfig!.storyExpirePeriod + appConfig!.storyViewersExpirePeriod,
isChatExist: Boolean(chat),
areChatSettingsLoaded: Boolean(chat?.settings),
arePeerSettingsLoaded: Boolean(userFullInfo?.settings),
stealthMode: global.stories.stealthMode,
withHeaderAnimation,
};

View File

@ -2130,21 +2130,6 @@ addActionHandler('fetchChat', (global, actions, payload): ActionReturnType => {
}
});
addActionHandler('loadChatSettings', async (global, actions, payload): Promise<void> => {
const { chatId } = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
const result = await callApi('fetchChatSettings', chat);
if (!result) return;
const { settings } = result;
global = getGlobal();
global = updateChat(global, chat.id, { settings });
setGlobal(global);
});
addActionHandler('toggleJoinToSend', async (global, actions, payload): Promise<void> => {
const { chatId, isEnabled } = payload;
const chat = selectChat(global, chatId);

View File

@ -7,7 +7,7 @@ import { callApi } from '../../../api/gramjs';
import { getUserFirstOrLastName } from '../../helpers';
import { addActionHandler, getGlobal, setGlobal } from '../../index';
import {
updateChat, updateChatFullInfo, updateManagement, updateManagementProgress,
updateChat, updateChatFullInfo, updateManagement, updateManagementProgress, updateUserFullInfo,
} from '../../reducers';
import {
selectChat, selectCurrentMessageList, selectTabState, selectUser,
@ -394,16 +394,16 @@ addActionHandler('hideAllChatJoinRequests', async (global, actions, payload): Pr
setGlobal(global);
});
addActionHandler('hideChatReportPane', async (global, actions, payload): Promise<void> => {
const { chatId } = payload!;
const chat = selectChat(global, chatId);
if (!chat) return;
addActionHandler('hidePeerSettingsBar', async (global, actions, payload): Promise<void> => {
const { peerId } = payload!;
const user = selectUser(global, peerId);
if (!user) return;
const result = await callApi('hideChatReportPane', chat);
const result = await callApi('hidePeerSettingsBar', user);
if (!result) return;
global = getGlobal();
global = updateChat(global, chatId, {
global = updateUserFullInfo(global, peerId, {
settings: undefined,
});
setGlobal(global);

View File

@ -217,7 +217,7 @@ addActionHandler('updateContact', async (global, actions, payload): Promise<void
}
if (result) {
actions.loadChatSettings({ chatId: userId });
actions.loadPeerSettings({ peerId: userId });
actions.loadPeerStories({ peerId: userId });
global = getGlobal();
@ -506,6 +506,30 @@ addActionHandler('openSuggestedStatusModal', async (global, actions, payload): P
setGlobal(global);
});
addActionHandler('loadPeerSettings', async (global, actions, payload): Promise<void> => {
const { peerId } = payload;
const userFullInfo = selectUserFullInfo(global, peerId);
if (!userFullInfo) {
actions.loadFullUser({ userId: peerId });
return;
}
const user = selectUser(global, peerId);
if (!user) {
return;
}
const result = await callApi('fetchPeerSettings', user);
if (!result) return;
const { settings } = result;
global = getGlobal();
global = updateUserFullInfo(global, peerId, { settings });
setGlobal(global);
});
addActionHandler('markBotVerificationInfoShown', (global, actions, payload): ActionReturnType => {
const { peerId } = payload;

View File

@ -106,6 +106,21 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
},
});
}
case 'updatePeerSettings': {
const { id, settings } = update;
const targetUserFullInfo = selectUserFullInfo(global, id);
if (!targetUserFullInfo?.botInfo) {
actions.loadFullUser({ userId: id });
return undefined;
}
global = updateUserFullInfo(global, id, {
settings,
});
return global;
}
}
return undefined;

View File

@ -14,7 +14,6 @@ import { getCurrentTabId } from '../../util/establishMultitabRole';
import { omit, omitUndefined, unique } from '../../util/iteratees';
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
import { selectTabState } from '../selectors';
import { updateChat } from './chats';
import { updateTabState } from './tabs';
export function replaceUsers<T extends GlobalState>(global: T, newById: Record<string, ApiUser>): T {
@ -179,7 +178,7 @@ export function deleteContact<T extends GlobalState>(global: T, userId: string):
},
};
return updateChat(global, userId, {
return updateUserFullInfo(global, userId, {
settings: undefined,
});
}

View File

@ -918,8 +918,8 @@ export interface ActionPayloads {
offsetUserId?: string;
limit?: number;
} & WithTabId;
hideChatReportPane: {
chatId: string;
hidePeerSettingsBar: {
peerId: string;
};
toggleManagement: ({
force?: boolean;
@ -1032,8 +1032,8 @@ export interface ActionPayloads {
};
// Chats
loadChatSettings: {
chatId: string;
loadPeerSettings: {
peerId: string;
};
fetchChat: {
chatId: string;

View File

@ -58,6 +58,8 @@ $color-message-reaction-own-hover: #b5e0a4;
$color-message-reaction-chosen-hover: #1a82ea;
$color-message-reaction-chosen-hover-own: #3f9d4b;
$color-message-non-contact: #cceebf;
$color-message-story-mention-from: #4ef390;
$color-message-story-mention-to: #74bcff;
@ -151,6 +153,8 @@ $color-message-story-mention-to: #74bcff;
--color-message-reaction-chosen-hover: $color-message-reaction-chosen-hover;
--color-message-reaction-chosen-hover-own: $color-message-reaction-chosen-hover-own;
--color-message-non-contact: $color-message-non-contact;
--color-message-story-mention-from: $color-message-story-mention-from;
--color-message-story-mention-to: $color-message-story-mention-to;

View File

@ -400,6 +400,7 @@ body:not(.is-ios) {
--color-message-reaction-hover-own: rgb(91, 82, 155);
--color-message-reaction-chosen-hover: rgb(120, 100, 221);
--color-message-reaction-chosen-hover-own: rgb(245, 245, 245);
--color-message-non-contact: rgb(204, 238, 191);
--color-voice-transcribe-button: rgb(42, 42, 60);
--color-voice-transcribe-button-own: rgb(131, 115, 211);
--color-topic-blue: rgb(111, 249, 240);

View File

@ -57,6 +57,7 @@
"--color-message-reaction-hover-own": ["#b5e0a4", "#5B529B"],
"--color-message-reaction-chosen-hover": ["#1a82ea", "#7864dd"],
"--color-message-reaction-chosen-hover-own": ["#3f9d4b", "#f5f5f5"],
"--color-message-non-contact": ["#cceebf", "#AAAAAA"],
"--color-voice-transcribe-button": ["#e8f3ff", "#2a2a3c"],
"--color-voice-transcribe-button-own": ["#cceebf", "#8373d3"],
"--color-topic-blue": ["#2F7772", "#6ff9f0"],

View File

@ -224,6 +224,10 @@ export interface LangPair {
'WalletAddressCopied': undefined;
'Copy': undefined;
'DeleteAndStop': undefined;
'ChatNonContactUserSubtitle': undefined;
'ChatNonContactUserGroups': undefined;
'ContactInfoRegistration': undefined;
'ContactInfoNotVerified': undefined;
'DeleteForAll': undefined;
'DeleteSingleMessagesTitle': undefined;
'AreYouSureDeleteSingleMessage': undefined;
@ -961,6 +965,7 @@ export interface LangPair {
'WeekdaySaturday': undefined;
'WeekdaySunday': undefined;
'WeekdayToday': undefined;
'Today': undefined;
'WeekdayYesterday': undefined;
'User': undefined;
'SecretChat': undefined;
@ -1010,6 +1015,7 @@ export interface LangPair {
'VoipIncoming': undefined;
'LiveLocationUpdatedJustNow': undefined;
'RightNow': undefined;
'JustNowAgo': undefined;
'AudioPause': undefined;
'AudioPlay': undefined;
'ToggleUserNotifications': undefined;
@ -1721,6 +1727,9 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
'LiveLocationUpdatedTodayAt': {
'time': V;
};
'AtDateAgo': {
'date': V;
};
'MediaViewDownloading': {
'count': V;
};
@ -1792,6 +1801,14 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
'peer': V;
'amount': V;
};
'UserUpdatedName': {
'user': V;
'time': V;
};
'UserUpdatedPhoto': {
'user': V;
'time': V;
};
'GiftInfoSaved': {
'link': V;
};
@ -2394,6 +2411,15 @@ export interface LangPairPluralWithVariables<V extends unknown = LangVariable> {
'Minutes': {
'count': V;
};
'MinutesAgo': {
'count': V;
};
'HoursAgo': {
'count': V;
};
'DaysAgo': {
'count': V;
};
'PreviewForwardedMessage': {
'count': V;
};
@ -2436,6 +2462,9 @@ export interface LangPairPluralWithVariables<V extends unknown = LangVariable> {
'GiftWithdrawWait': {
'days': V;
};
'ChatGroups': {
'count': V;
};
'StarsAmountText': {
'amount': V;
};

View File

@ -2,8 +2,9 @@ import type { OldLangFn } from '../../hooks/useOldLang';
import type { TimeFormat } from '../../types';
import type { LangFn } from '../localization';
import { getServerTime } from '../serverTime';
import withCache from '../withCache';
import { getDays } from './units';
import { getDays, getHours, getMinutes } from './units';
const WEEKDAYS_FULL = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const MONTHS_FULL = [
@ -78,11 +79,11 @@ export function formatPastTimeShort(lang: OldLangFn, datetime: number | Date, al
return alwaysShowTime ? lang('FullDateTimeFormat', [formattedDate, time]) : formattedDate;
}
export function formatFullDate(lang: OldLangFn, datetime: number | Date) {
export function formatFullDate(lang: OldLangFn | LangFn, datetime: number | Date) {
return formatDateToString(datetime, lang.code, false, 'numeric');
}
export function formatMonthAndYear(lang: OldLangFn, date: Date, isShort = false) {
export function formatMonthAndYear(lang: OldLangFn | LangFn, date: Date, isShort = false) {
return formatDateToString(date, lang.code, false, isShort ? 'short' : 'long', true);
}
@ -122,7 +123,7 @@ export function formatCountdownShort(lang: OldLangFn, msLeft: number): string {
}
}
export function formatLastUpdated(lang: OldLangFn, currentTime: number, lastUpdated = currentTime) {
export function formatLocationLastUpdate(lang: OldLangFn, currentTime: number, lastUpdated = currentTime) {
const seconds = currentTime - lastUpdated;
if (seconds < 60) {
return lang('LiveLocationUpdated.JustNow');
@ -133,7 +134,7 @@ export function formatLastUpdated(lang: OldLangFn, currentTime: number, lastUpda
}
}
export function formatRelativeTime(lang: OldLangFn, currentTime: number, lastUpdated = currentTime) {
export function formatRelativePastTime(lang: OldLangFn, currentTime: number, lastUpdated = currentTime) {
const seconds = currentTime - lastUpdated;
if (seconds < 60) {
@ -160,6 +161,31 @@ export function formatRelativeTime(lang: OldLangFn, currentTime: number, lastUpd
return lang('Time.AtDate', formatFullDate(lang, lastUpdatedDate));
}
export function formatPastDatetime(lang: LangFn, pastTime: number, currentTime = getServerTime()) {
const seconds = currentTime - pastTime;
const minutes = getMinutes(seconds);
const hours = getHours(seconds);
const days = getDays(seconds);
if (seconds < 60) {
return lang('JustNowAgo');
}
if (minutes < 60) {
return lang('MinutesAgo', { count: minutes }, { pluralValue: minutes });
}
if (hours < 24) {
return lang('HoursAgo', { count: hours }, { pluralValue: hours });
}
if (days < 28) {
return lang('DaysAgo', { count: days }, { pluralValue: days });
}
return lang('AtDateAgo', { date: formatFullDate(lang, pastTime) });
}
type DurationType = 'Seconds' | 'Minutes' | 'Hours' | 'Days' | 'Weeks';
export function formatTimeDuration(lang: OldLangFn, duration: number, showLast = 2) {
@ -449,3 +475,10 @@ function lowerFirst(str: string) {
function upperFirst(str: string) {
return `${str[0].toUpperCase()}${str.slice(1)}`;
}
export function formatRegistrationMonth(lang: string, dateString: string) {
const [month, year] = dateString.split('.');
const date = new Date(`${year}-${month}`);
return new Intl.DateTimeFormat(lang, { month: 'long', year: 'numeric' }).format(date);
}

View File

@ -429,7 +429,7 @@ function processTranslationAdvanced(
if (value === undefined) return result;
const preparedValue = Number.isFinite(value) ? formatters!.number.format(value as number) : value;
return replaceInStringsWithTeact(result, `{${key}}`, preparedValue);
return replaceInStringsWithTeact(result, `{${key}}`, renderText(preparedValue));
}, [part] as TeactNode[]);
},
});
@ -440,7 +440,7 @@ function processTranslationAdvanced(
if (value === undefined) return result;
const preparedValue = Number.isFinite(value) ? formatters!.number.format(value as number) : value;
return replaceInStringsWithTeact(result, `{${key}}`, preparedValue);
return replaceInStringsWithTeact(result, `{${key}}`, renderText(preparedValue));
}, tempResult);
}

View File

@ -3,8 +3,8 @@ import type { ApiCountryCode } from '../api/types';
const PATTERN_PLACEHOLDER = 'X';
const DEFAULT_PATTERN = 'XXX XXX XXX XXX';
export function getCountryCodesByIso(phoneCodeList: ApiCountryCode[], iso: string) {
return phoneCodeList.filter((country) => country.iso2 === iso);
export function getCountryCodeByIso(phoneCodeList: ApiCountryCode[], iso: string) {
return phoneCodeList.find((country) => country.iso2 === iso);
}
export function getCountryFromPhoneNumber(phoneCodeList: ApiCountryCode[], input = '') {