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, ApiChatlistInvite,
ApiChatMember, ApiChatMember,
ApiChatReactions, ApiChatReactions,
ApiChatSettings,
ApiExportedInvite, ApiExportedInvite,
ApiMissingInvitedUser, ApiMissingInvitedUser,
ApiRestrictionReason, 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 { export function buildApiChatReactions(chatReactions?: GramJs.TypeChatReactions): ApiChatReactions | undefined {
if (chatReactions instanceof GramJs.ChatReactionsAll) { if (chatReactions instanceof GramJs.ChatReactionsAll) {
return { return {

View File

@ -2,6 +2,7 @@ import { Api as GramJs } from '../../../lib/gramjs';
import type { import type {
ApiBirthday, ApiBirthday,
ApiPeerSettings,
ApiUser, ApiUser,
ApiUserFullInfo, ApiUserFullInfo,
ApiUserStatus, ApiUserStatus,
@ -24,7 +25,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
fallbackPhoto, personalPhoto, translationsDisabled, storiesPinnedAvailable, fallbackPhoto, personalPhoto, translationsDisabled, storiesPinnedAvailable,
contactRequirePremium, businessWorkHours, businessLocation, businessIntro, contactRequirePremium, businessWorkHours, businessLocation, businessIntro,
birthday, personalChannelId, personalChannelMessage, sponsoredEnabled, stargiftsCount, botVerification, birthday, personalChannelId, personalChannelMessage, sponsoredEnabled, stargiftsCount, botVerification,
botCanManageEmojiStatus, botCanManageEmojiStatus, settings,
}, },
users, users,
} = mtpUserFull; } = mtpUserFull;
@ -55,6 +56,29 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
starGiftCount: stargiftsCount, starGiftCount: stargiftsCount,
isBotCanManageEmojiStatus: botCanManageEmojiStatus, isBotCanManageEmojiStatus: botCanManageEmojiStatus,
hasScheduledMessages: hasScheduled, 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, buildApiChatlistExportedInvite,
buildApiChatlistInvite, buildApiChatlistInvite,
buildApiChatReactions, buildApiChatReactions,
buildApiChatSettings,
buildApiMissingInvitedUser, buildApiMissingInvitedUser,
buildApiTopic, buildApiTopic,
buildChatMember, buildChatMember,
@ -56,7 +55,7 @@ import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages';
import { buildApiPeerNotifySettings } from '../apiBuilders/misc'; import { buildApiPeerNotifySettings } from '../apiBuilders/misc';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
import { buildStickerSet } from '../apiBuilders/symbols'; import { buildStickerSet } from '../apiBuilders/symbols';
import { buildApiUser, buildApiUserStatuses } from '../apiBuilders/users'; import { buildApiPeerSettings, buildApiUser, buildApiUserStatuses } from '../apiBuilders/users';
import { import {
buildChatAdminRights, buildChatAdminRights,
buildChatBannedRights, buildChatBannedRights,
@ -354,8 +353,8 @@ export function fetchFullChat(chat: ApiChat) {
: getFullChatInfo(id); : getFullChatInfo(id);
} }
export async function fetchChatSettings(chat: ApiChat) { export async function fetchPeerSettings(peer: ApiPeer) {
const { id, accessHash } = chat; const { id, accessHash } = peer;
const result = await invokeRequest(new GramJs.messages.GetPeerSettings({ const result = await invokeRequest(new GramJs.messages.GetPeerSettings({
peer: buildInputPeer(id, accessHash), peer: buildInputPeer(id, accessHash),
@ -368,7 +367,7 @@ export async function fetchChatSettings(chat: ApiChat) {
} }
return { return {
settings: buildApiChatSettings(result.settings), settings: buildApiPeerSettings(result.settings),
}; };
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -252,6 +252,10 @@
"ChatListDeleteChatConfirmation" = "Are you sure you want to delete the chat\nwith {chat}?"; "ChatListDeleteChatConfirmation" = "Are you sure you want to delete the chat\nwith {chat}?";
"DeleteAndStop" = "Delete and Block"; "DeleteAndStop" = "Delete and Block";
"ChatListDeleteForEveryone" = "Delete for me and {user}"; "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"; "DeleteForAll" = "Delete for all members";
"DeleteSingleMessagesTitle" = "Delete message"; "DeleteSingleMessagesTitle" = "Delete message";
"AreYouSureDeleteSingleMessage" = "Are you sure you want to delete this message?"; "AreYouSureDeleteSingleMessage" = "Are you sure you want to delete this message?";
@ -1117,6 +1121,7 @@
"WeekdaySaturday" = "Saturday"; "WeekdaySaturday" = "Saturday";
"WeekdaySunday" = "Sunday"; "WeekdaySunday" = "Sunday";
"WeekdayToday" = "Today"; "WeekdayToday" = "Today";
"Today" = "today";
"WeekdayYesterday" = "Yesterday"; "WeekdayYesterday" = "Yesterday";
"User" = "User"; "User" = "User";
"SecretChat" = "Secret Chat"; "SecretChat" = "Secret Chat";
@ -1193,6 +1198,14 @@
"Seconds_other" = "{count} seconds"; "Seconds_other" = "{count} seconds";
"Minutes_one" = "{count} minute"; "Minutes_one" = "{count} minute";
"Minutes_other" = "{count} minutes"; "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"; "AudioPause" = "Pause audio";
"AudioPlay" = "Play audio"; "AudioPlay" = "Play audio";
"ToggleUserNotifications" = "Toggle user notifications"; "ToggleUserNotifications" = "Toggle user notifications";
@ -1419,6 +1432,8 @@
"GiftInfoConvertDescription2" = "This action cannot be undone. This will permanently destroy the gift."; "GiftInfoConvertDescription2" = "This action cannot be undone. This will permanently destroy the gift.";
"GiftInfoConvertDescriptionPeriod_one" = "Conversion is available for the next **{count} days**."; "GiftInfoConvertDescriptionPeriod_one" = "Conversion is available for the next **{count} days**.";
"GiftInfoConvertDescriptionPeriod_other" = "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}"; "GiftInfoSaved" = "This gift is visible on your profile. {link}";
"GiftInfoHidden" = "This gift is hidden. Only you can see it. {link}"; "GiftInfoHidden" = "This gift is hidden. Only you can see it. {link}";
"GiftInfoChannelSaved" = "This gift is visible in your channel's profile. {link}"; "GiftInfoChannelSaved" = "This gift is visible in your channel's profile. {link}";
@ -1487,6 +1502,8 @@
"GiftWithdrawSubmit" = "Open Fragment"; "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_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."; "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}"; "StarsAmount" = "⭐️{amount}";
"StarsAmountText_one" = "{amount} Star"; "StarsAmountText_one" = "{amount} Star";
"StarsAmountText_other" = "{amount} Stars"; "StarsAmountText_other" = "{amount} Stars";

View File

@ -13,7 +13,7 @@ import { preloadImage } from '../../util/files';
import preloadFonts from '../../util/fonts'; import preloadFonts from '../../util/fonts';
import { pick } from '../../util/iteratees'; import { pick } from '../../util/iteratees';
import { oldSetLanguage } from '../../util/oldLangProvider'; 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 { IS_SAFARI, IS_TOUCH_ENV } from '../../util/windowEnvironment';
import { getSuggestedLanguage } from './helpers/getSuggestedLanguage'; import { getSuggestedLanguage } from './helpers/getSuggestedLanguage';
@ -101,7 +101,7 @@ const AuthPhoneNumber: FC<StateProps> = ({
useEffect(() => { useEffect(() => {
if (authNearestCountry && phoneCodeList && !country && !isTouched) { if (authNearestCountry && phoneCodeList && !country && !isTouched) {
setCountry(getCountryCodesByIso(phoneCodeList, authNearestCountry)[0]); setCountry(getCountryCodeByIso(phoneCodeList, authNearestCountry));
} }
}, [country, authNearestCountry, isTouched, phoneCodeList]); }, [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 { getActions, getGlobal, withGlobal } from '../../global';
import type { import type {
ApiChatFullInfo, ApiMessage, ApiRestrictionReason, ApiTopic, ApiChatFullInfo,
ApiMessage,
ApiRestrictionReason,
ApiTopic,
} from '../../api/types'; } from '../../api/types';
import type { OnIntersectPinnedMessage } from './hooks/usePinnedMessage'; import type { OnIntersectPinnedMessage } from './hooks/usePinnedMessage';
import { MAIN_THREAD_ID } from '../../api/types'; import { MAIN_THREAD_ID } from '../../api/types';
@ -74,7 +77,7 @@ import useStickyDates from './hooks/useStickyDates';
import Loading from '../ui/Loading'; import Loading from '../ui/Loading';
import ContactGreeting from './ContactGreeting'; import ContactGreeting from './ContactGreeting';
import MessageListBotInfo from './MessageListBotInfo'; import MessageListAccountInfo from './MessageListAccountInfo';
import MessageListContent from './MessageListContent'; import MessageListContent from './MessageListContent';
import NoMessages from './NoMessages'; import NoMessages from './NoMessages';
import PremiumRequiredMessage from './PremiumRequiredMessage'; import PremiumRequiredMessage from './PremiumRequiredMessage';
@ -106,6 +109,9 @@ type StateProps = {
isCreator?: boolean; isCreator?: boolean;
isChannelWithAvatars?: boolean; isChannelWithAvatars?: boolean;
isBot?: boolean; isBot?: boolean;
isNonContact?: boolean;
nameChangeDate?: number;
photoChangeDate?: number;
isSynced?: boolean; isSynced?: boolean;
messageIds?: number[]; messageIds?: number[];
messagesById?: Record<number, ApiMessage>; messagesById?: Record<number, ApiMessage>;
@ -159,6 +165,9 @@ const MessageList: FC<OwnProps & StateProps> = ({
isAnonymousForwards, isAnonymousForwards,
isCreator, isCreator,
isBot, isBot,
isNonContact,
nameChangeDate,
photoChangeDate,
messageIds, messageIds,
messagesById, messagesById,
firstUnreadId, firstUnreadId,
@ -682,8 +691,8 @@ const MessageList: FC<OwnProps & StateProps> = ({
</div> </div>
) : isContactRequirePremium && !hasMessages ? ( ) : isContactRequirePremium && !hasMessages ? (
<PremiumRequiredMessage userId={chatId} /> <PremiumRequiredMessage userId={chatId} />
) : isBot && !hasMessages ? ( ) : (isBot || isNonContact) && !hasMessages ? (
<MessageListBotInfo chatId={chatId} /> <MessageListAccountInfo chatId={chatId} />
) : shouldRenderGreeting ? ( ) : shouldRenderGreeting ? (
<ContactGreeting key={chatId} userId={chatId} /> <ContactGreeting key={chatId} userId={chatId} />
) : messageIds && (!messageGroups || isGroupChatJustCreated || isEmptyTopic) ? ( ) : messageIds && (!messageGroups || isGroupChatJustCreated || isEmptyTopic) ? (
@ -718,7 +727,9 @@ const MessageList: FC<OwnProps & StateProps> = ({
isReady={isReady} isReady={isReady}
hasLinkedChat={hasLinkedChat} hasLinkedChat={hasLinkedChat}
isSchedule={messageGroups ? type === 'scheduled' : false} isSchedule={messageGroups ? type === 'scheduled' : false}
shouldRenderBotInfo={isBot} shouldRenderAccountInfo={isBot || isNonContact}
nameChangeDate={nameChangeDate}
photoChangeDate={photoChangeDate}
noAppearanceAnimation={!messageGroups || !shouldAnimateAppearanceRef.current} noAppearanceAnimation={!messageGroups || !shouldAnimateAppearanceRef.current}
onScrollDownToggle={onScrollDownToggle} onScrollDownToggle={onScrollDownToggle}
onNotchToggle={onNotchToggle} onNotchToggle={onNotchToggle}
@ -735,6 +746,7 @@ export default memo(withGlobal<OwnProps>(
(global, { chatId, threadId, type }): StateProps => { (global, { chatId, threadId, type }): StateProps => {
const currentUserId = global.currentUserId!; const currentUserId = global.currentUserId!;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const userFullInfo = selectUserFullInfo(global, chatId);
if (!chat) { if (!chat) {
return { currentUserId }; return { currentUserId };
} }
@ -763,6 +775,9 @@ export default memo(withGlobal<OwnProps>(
); );
const chatBot = selectBot(global, chatId); 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 topic = selectTopic(global, chatId, threadId);
const chatFullInfo = !isUserId(chatId) ? selectChatFullInfo(global, chatId) : undefined; const chatFullInfo = !isUserId(chatId) ? selectChatFullInfo(global, chatId) : undefined;
@ -784,6 +799,9 @@ export default memo(withGlobal<OwnProps>(
isSystemBotChat: isSystemBot(chatId), isSystemBotChat: isSystemBot(chatId),
isAnonymousForwards: isAnonymousForwardsChat(chatId), isAnonymousForwards: isAnonymousForwardsChat(chatId),
isBot: Boolean(chatBot), isBot: Boolean(chatBot),
isNonContact,
nameChangeDate,
photoChangeDate,
isSynced: global.isSynced, isSynced: global.isSynced,
messageIds, messageIds,
messagesById, 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 Message from './message/Message';
import SenderGroupContainer from './message/SenderGroupContainer'; import SenderGroupContainer from './message/SenderGroupContainer';
import SponsoredMessage from './message/SponsoredMessage'; import SponsoredMessage from './message/SponsoredMessage';
import MessageListBotInfo from './MessageListBotInfo'; import MessageListAccountInfo from './MessageListAccountInfo';
interface OwnProps { interface OwnProps {
canShowAds?: boolean; canShowAds?: boolean;
@ -57,7 +57,9 @@ interface OwnProps {
isReady: boolean; isReady: boolean;
hasLinkedChat: boolean | undefined; hasLinkedChat: boolean | undefined;
isSchedule: boolean; isSchedule: boolean;
shouldRenderBotInfo?: boolean; shouldRenderAccountInfo?: boolean;
nameChangeDate?: number;
photoChangeDate?: number;
noAppearanceAnimation: boolean; noAppearanceAnimation: boolean;
isSavedDialog?: boolean; isSavedDialog?: boolean;
onScrollDownToggle: BooleanToVoidFunction; onScrollDownToggle: BooleanToVoidFunction;
@ -89,7 +91,9 @@ const MessageListContent: FC<OwnProps> = ({
isReady, isReady,
hasLinkedChat, hasLinkedChat,
isSchedule, isSchedule,
shouldRenderBotInfo, shouldRenderAccountInfo,
nameChangeDate,
photoChangeDate,
noAppearanceAnimation, noAppearanceAnimation,
isSavedDialog, isSavedDialog,
onScrollDownToggle, onScrollDownToggle,
@ -126,11 +130,11 @@ const MessageListContent: FC<OwnProps> = ({
isReady, isReady,
); );
const lang = useOldLang(); const oldLang = useOldLang();
const unreadDivider = ( const unreadDivider = (
<div className={buildClassName(UNREAD_DIVIDER_CLASS, 'local-action-message')} key="unread-messages"> <div className={buildClassName(UNREAD_DIVIDER_CLASS, 'local-action-message')} key="unread-messages">
<span>{lang('UnreadMessages')}</span> <span>{oldLang('UnreadMessages')}</span>
</div> </div>
); );
const messageCountToAnimate = noAppearanceAnimation ? 0 : messageGroups.reduce((acc, messageGroup) => { const messageCountToAnimate = noAppearanceAnimation ? 0 : messageGroups.reduce((acc, messageGroup) => {
@ -251,7 +255,7 @@ const MessageListContent: FC<OwnProps> = ({
/>, />,
message.id === threadId && ( message.id === threadId && (
<div className="local-action-message" key="discussion-started"> <div className="local-action-message" key="discussion-started">
<span>{lang(isEmptyThread <span>{oldLang(isEmptyThread
? (isComments ? 'NoComments' : 'NoReplies') : 'DiscussionStarted')} ? (isComments ? 'NoComments' : 'NoReplies') : 'DiscussionStarted')}
</span> </span>
</div> </div>
@ -301,7 +305,8 @@ const MessageListContent: FC<OwnProps> = ({
return ( return (
<div <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} key={dateGroup.datetime}
onMouseDown={preventMessageInputBlur} onMouseDown={preventMessageInputBlur}
teactFastList teactFastList
@ -314,12 +319,12 @@ const MessageListContent: FC<OwnProps> = ({
> >
<span dir="auto"> <span dir="auto">
{isSchedule && dateGroup.originalDate === SCHEDULED_WHEN_ONLINE && ( {isSchedule && dateGroup.originalDate === SCHEDULED_WHEN_ONLINE && (
lang('MessageScheduledUntilOnline') oldLang('MessageScheduledUntilOnline')
)} )}
{isSchedule && dateGroup.originalDate !== SCHEDULED_WHEN_ONLINE && ( {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> </span>
</div> </div>
{senderGroups.flat()} {senderGroups.flat()}
@ -330,7 +335,8 @@ const MessageListContent: FC<OwnProps> = ({
return ( return (
<div className="messages-container" teactFastList> <div className="messages-container" teactFastList>
{withHistoryTriggers && <div ref={backwardsTriggerRef} key="backwards-trigger" className="backwards-trigger" />} {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()} {dateGroups.flat()}
{withHistoryTriggers && ( {withHistoryTriggers && (
<div <div

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -217,7 +217,7 @@ addActionHandler('updateContact', async (global, actions, payload): Promise<void
} }
if (result) { if (result) {
actions.loadChatSettings({ chatId: userId }); actions.loadPeerSettings({ peerId: userId });
actions.loadPeerStories({ peerId: userId }); actions.loadPeerStories({ peerId: userId });
global = getGlobal(); global = getGlobal();
@ -506,6 +506,30 @@ addActionHandler('openSuggestedStatusModal', async (global, actions, payload): P
setGlobal(global); 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 => { addActionHandler('markBotVerificationInfoShown', (global, actions, payload): ActionReturnType => {
const { peerId } = payload; 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; return undefined;

View File

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

View File

@ -918,8 +918,8 @@ export interface ActionPayloads {
offsetUserId?: string; offsetUserId?: string;
limit?: number; limit?: number;
} & WithTabId; } & WithTabId;
hideChatReportPane: { hidePeerSettingsBar: {
chatId: string; peerId: string;
}; };
toggleManagement: ({ toggleManagement: ({
force?: boolean; force?: boolean;
@ -1032,8 +1032,8 @@ export interface ActionPayloads {
}; };
// Chats // Chats
loadChatSettings: { loadPeerSettings: {
chatId: string; peerId: string;
}; };
fetchChat: { fetchChat: {
chatId: string; 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: #1a82ea;
$color-message-reaction-chosen-hover-own: #3f9d4b; $color-message-reaction-chosen-hover-own: #3f9d4b;
$color-message-non-contact: #cceebf;
$color-message-story-mention-from: #4ef390; $color-message-story-mention-from: #4ef390;
$color-message-story-mention-to: #74bcff; $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: $color-message-reaction-chosen-hover;
--color-message-reaction-chosen-hover-own: $color-message-reaction-chosen-hover-own; --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-from: $color-message-story-mention-from;
--color-message-story-mention-to: $color-message-story-mention-to; --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-hover-own: rgb(91, 82, 155);
--color-message-reaction-chosen-hover: rgb(120, 100, 221); --color-message-reaction-chosen-hover: rgb(120, 100, 221);
--color-message-reaction-chosen-hover-own: rgb(245, 245, 245); --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: rgb(42, 42, 60);
--color-voice-transcribe-button-own: rgb(131, 115, 211); --color-voice-transcribe-button-own: rgb(131, 115, 211);
--color-topic-blue: rgb(111, 249, 240); --color-topic-blue: rgb(111, 249, 240);

View File

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

View File

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

View File

@ -2,8 +2,9 @@ import type { OldLangFn } from '../../hooks/useOldLang';
import type { TimeFormat } from '../../types'; import type { TimeFormat } from '../../types';
import type { LangFn } from '../localization'; import type { LangFn } from '../localization';
import { getServerTime } from '../serverTime';
import withCache from '../withCache'; 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 WEEKDAYS_FULL = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const MONTHS_FULL = [ const MONTHS_FULL = [
@ -78,11 +79,11 @@ export function formatPastTimeShort(lang: OldLangFn, datetime: number | Date, al
return alwaysShowTime ? lang('FullDateTimeFormat', [formattedDate, time]) : formattedDate; 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'); 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); 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; const seconds = currentTime - lastUpdated;
if (seconds < 60) { if (seconds < 60) {
return lang('LiveLocationUpdated.JustNow'); 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; const seconds = currentTime - lastUpdated;
if (seconds < 60) { if (seconds < 60) {
@ -160,6 +161,31 @@ export function formatRelativeTime(lang: OldLangFn, currentTime: number, lastUpd
return lang('Time.AtDate', formatFullDate(lang, lastUpdatedDate)); 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'; type DurationType = 'Seconds' | 'Minutes' | 'Hours' | 'Days' | 'Weeks';
export function formatTimeDuration(lang: OldLangFn, duration: number, showLast = 2) { export function formatTimeDuration(lang: OldLangFn, duration: number, showLast = 2) {
@ -449,3 +475,10 @@ function lowerFirst(str: string) {
function upperFirst(str: string) { function upperFirst(str: string) {
return `${str[0].toUpperCase()}${str.slice(1)}`; 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; 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}}`, preparedValue); return replaceInStringsWithTeact(result, `{${key}}`, renderText(preparedValue));
}, [part] as TeactNode[]); }, [part] as TeactNode[]);
}, },
}); });
@ -440,7 +440,7 @@ function processTranslationAdvanced(
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}}`, preparedValue); return replaceInStringsWithTeact(result, `{${key}}`, renderText(preparedValue));
}, tempResult); }, tempResult);
} }

View File

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