From dc61b12f9bbd07a0cfeeb577a74c3e0a07768946 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 4 Apr 2025 13:03:09 +0200 Subject: [PATCH] =?UTF-8?q?Messages:=20Implement=20non-=D1=81ontact=20Info?= =?UTF-8?q?=20(#5708)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com> --- src/api/gramjs/apiBuilders/chats.ts | 15 - src/api/gramjs/apiBuilders/users.ts | 26 +- src/api/gramjs/methods/chats.ts | 9 +- src/api/gramjs/methods/management.ts | 6 +- src/api/gramjs/updates/mtpUpdateHandler.ts | 47 +-- src/api/types/chats.ts | 9 +- src/api/types/updates.ts | 9 +- src/api/types/users.ts | 3 +- src/assets/localization/fallback.strings | 17 + src/components/auth/AuthPhoneNumber.tsx | 4 +- src/components/middle/MessageList.tsx | 28 +- .../middle/MessageListAccountInfo.module.scss | 109 +++++++ .../middle/MessageListAccountInfo.tsx | 293 ++++++++++++++++++ .../middle/MessageListBotInfo.module.scss | 35 --- src/components/middle/MessageListBotInfo.tsx | 134 -------- src/components/middle/MessageListContent.tsx | 28 +- src/components/middle/MiddleColumn.tsx | 15 +- src/components/middle/MiddleHeaderPanes.tsx | 11 +- src/components/middle/message/Location.tsx | 4 +- .../middle/panes/ChatReportPane.tsx | 4 +- src/components/story/Story.tsx | 20 +- src/global/actions/api/chats.ts | 15 - src/global/actions/api/management.ts | 14 +- src/global/actions/api/users.ts | 26 +- src/global/actions/apiUpdaters/users.ts | 15 + src/global/reducers/users.ts | 3 +- src/global/types/actions.ts | 8 +- src/styles/_variables.scss | 4 + src/styles/index.scss | 1 + src/styles/themes.json | 1 + src/types/language.d.ts | 29 ++ src/util/dates/dateFormat.ts | 43 ++- src/util/localization/index.ts | 4 +- src/util/phoneNumber.ts | 4 +- 34 files changed, 677 insertions(+), 316 deletions(-) create mode 100644 src/components/middle/MessageListAccountInfo.module.scss create mode 100644 src/components/middle/MessageListAccountInfo.tsx delete mode 100644 src/components/middle/MessageListBotInfo.module.scss delete mode 100644 src/components/middle/MessageListBotInfo.tsx diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index 678066d95..029fcadd2 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -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 { diff --git a/src/api/gramjs/apiBuilders/users.ts b/src/api/gramjs/apiBuilders/users.ts index b299ce91f..d011f2017 100644 --- a/src/api/gramjs/apiBuilders/users.ts +++ b/src/api/gramjs/apiBuilders/users.ts @@ -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, }; } diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 5a499e824..6a185f16a 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -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), }; } diff --git a/src/api/gramjs/methods/management.ts b/src/api/gramjs/methods/management.ts index 66af71ddd..613a019a5 100644 --- a/src/api/gramjs/methods/management.ts +++ b/src/api/gramjs/methods/management.ts @@ -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), diff --git a/src/api/gramjs/updates/mtpUpdateHandler.ts b/src/api/gramjs/updates/mtpUpdateHandler.ts index ade8922de..58249d637 100644 --- a/src/api/gramjs/updates/mtpUpdateHandler.ts +++ b/src/api/gramjs/updates/mtpUpdateHandler.ts @@ -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, diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index 7239c81e3..6e3ab466f 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -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 { diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 3b6485090..118f77231 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -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 | diff --git a/src/api/types/users.ts b/src/api/types/users.ts index 5f6ea398c..62d6de2a9 100644 --- a/src/api/types/users.ts +++ b/src/api/types/users.ts @@ -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'; diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 98ae11101..c7d86685e 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -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"; diff --git a/src/components/auth/AuthPhoneNumber.tsx b/src/components/auth/AuthPhoneNumber.tsx index a4378031d..1bf9c3e47 100644 --- a/src/components/auth/AuthPhoneNumber.tsx +++ b/src/components/auth/AuthPhoneNumber.tsx @@ -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 = ({ useEffect(() => { if (authNearestCountry && phoneCodeList && !country && !isTouched) { - setCountry(getCountryCodesByIso(phoneCodeList, authNearestCountry)[0]); + setCountry(getCountryCodeByIso(phoneCodeList, authNearestCountry)); } }, [country, authNearestCountry, isTouched, phoneCodeList]); diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index 6ac482adf..f9016d800 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -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; @@ -159,6 +165,9 @@ const MessageList: FC = ({ isAnonymousForwards, isCreator, isBot, + isNonContact, + nameChangeDate, + photoChangeDate, messageIds, messagesById, firstUnreadId, @@ -682,8 +691,8 @@ const MessageList: FC = ({ ) : isContactRequirePremium && !hasMessages ? ( - ) : isBot && !hasMessages ? ( - + ) : (isBot || isNonContact) && !hasMessages ? ( + ) : shouldRenderGreeting ? ( ) : messageIds && (!messageGroups || isGroupChatJustCreated || isEmptyTopic) ? ( @@ -718,7 +727,9 @@ const MessageList: FC = ({ 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( (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( ); 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( isSystemBotChat: isSystemBot(chatId), isAnonymousForwards: isAnonymousForwardsChat(chatId), isBot: Boolean(chatBot), + isNonContact, + nameChangeDate, + photoChangeDate, isSynced: global.isSynced, messageIds, messagesById, diff --git a/src/components/middle/MessageListAccountInfo.module.scss b/src/components/middle/MessageListAccountInfo.module.scss new file mode 100644 index 000000000..a534810c9 --- /dev/null +++ b/src/components/middle/MessageListAccountInfo.module.scss @@ -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; +} diff --git a/src/components/middle/MessageListAccountInfo.tsx b/src/components/middle/MessageListAccountInfo.tsx new file mode 100644 index 000000000..76ef4eb2b --- /dev/null +++ b/src/components/middle/MessageListAccountInfo.tsx @@ -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 = ({ + 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) => { + stopEvent(e); + openChatWithInfo({ + id: chatId, shouldReplaceHistory: true, profileTab: 'commonChats', forceScrollProfileTab: true, + }); + }); + + const securityNameInfo = nameChangeDate && chat ? ( +
+ {lang('UserUpdatedName', { + user: chat.title, + time: formatPastDatetime(lang, nameChangeDate), + }, { withNodes: true, withMarkdown: true })} + +
+ ) : undefined; + + const securityPhotoInfo = photoChangeDate && chat ? ( +
+ {lang('UserUpdatedPhoto', { + user: chat.title, + time: formatPastDatetime(lang, photoChangeDate), + }, { withNodes: true, withMarkdown: true })} + +
+ ) : undefined; + + const tableData = useMemo((): TableEntry[] => { + const entries: TableEntry[] = []; + if (country) { + entries.push([ + oldLang('PrivacyPhone'), + + + {renderText(isoToEmoji(country?.iso2))} + + {country?.defaultName} + , + ]); + } + 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'), + + + {lang('ChatGroups', { + count: userFullInfo.commonChatsCount, + }, { + pluralValue: userFullInfo.commonChatsCount, + })} + + {Boolean(peers?.length) && } + + , + ]); + } + 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 ( +
+ {isLoadingFullUser && isChatInfoEmpty && {oldLang('Loading')}} + {(isBotInfoEmpty && isChatInfoEmpty) && !isLoadingFullUser && {oldLang('NoMessages')}} + {botInfo && ( +
+ {botInfoPhotoUrl && ( + Bot info + )} + {botInfoGifUrl && ( + + )} + {botInfoDimensions && !botInfoPhotoUrl && !botInfoGifUrl && ( + + )} + {isVerifyCodes && ( +
+ {oldLang('VerifyChatInfo')} +
+ )} + {!isVerifyCodes && botInfo.description && ( +
+

{oldLang('BotInfoTitle')}

+ {renderText(botInfo.description, ['br', 'emoji', 'links'])} +
+ )} +
+ )} + {!isChatInfoEmpty && chat && ( +
+

{renderText(getChatTitle(lang, chat))}

+

+ {lang('ChatNonContactUserSubtitle')} +

+ + {!chat?.isVerified && ( +
+ +

{lang('ContactInfoNotVerified')}

+
+ )} +
+ )} + {securityNameInfo} + {securityPhotoInfo} +
+ ); +}; + +export default memo(withGlobal( + (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)); diff --git a/src/components/middle/MessageListBotInfo.module.scss b/src/components/middle/MessageListBotInfo.module.scss deleted file mode 100644 index 4203c3478..000000000 --- a/src/components/middle/MessageListBotInfo.module.scss +++ /dev/null @@ -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; -} diff --git a/src/components/middle/MessageListBotInfo.tsx b/src/components/middle/MessageListBotInfo.tsx deleted file mode 100644 index 2fedc32c3..000000000 --- a/src/components/middle/MessageListBotInfo.tsx +++ /dev/null @@ -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 = ({ - 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 ( -
- {isLoadingBotInfo && {lang('Loading')}} - {isBotInfoEmpty && !isLoadingBotInfo && {lang('NoMessages')}} - {botInfo && ( -
- {botInfoPhotoUrl && ( - Bot info - )} - {botInfoGifUrl && ( - - )} - {botInfoDimensions && !botInfoPhotoUrl && !botInfoGifUrl && ( - - )} - {isVerifyCodes && ( -
- {lang('VerifyChatInfo')} -
- )} - {!isVerifyCodes && botInfo.description && ( -
-

{lang('BotInfoTitle')}

- {renderText(botInfo.description, ['br', 'emoji', 'links'])} -
- )} -
- )} -
- ); -}; - -export default memo(withGlobal( - (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)); diff --git a/src/components/middle/MessageListContent.tsx b/src/components/middle/MessageListContent.tsx index 63d263582..eca1c2cce 100644 --- a/src/components/middle/MessageListContent.tsx +++ b/src/components/middle/MessageListContent.tsx @@ -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 = ({ isReady, hasLinkedChat, isSchedule, - shouldRenderBotInfo, + shouldRenderAccountInfo, + nameChangeDate, + photoChangeDate, noAppearanceAnimation, isSavedDialog, onScrollDownToggle, @@ -126,11 +130,11 @@ const MessageListContent: FC = ({ isReady, ); - const lang = useOldLang(); + const oldLang = useOldLang(); const unreadDivider = (
- {lang('UnreadMessages')} + {oldLang('UnreadMessages')}
); const messageCountToAnimate = noAppearanceAnimation ? 0 : messageGroups.reduce((acc, messageGroup) => { @@ -251,7 +255,7 @@ const MessageListContent: FC = ({ />, message.id === threadId && (
- {lang(isEmptyThread + {oldLang(isEmptyThread ? (isComments ? 'NoComments' : 'NoReplies') : 'DiscussionStarted')}
@@ -301,7 +305,8 @@ const MessageListContent: FC = ({ return (
= ({ > {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)}
{senderGroups.flat()} @@ -330,7 +335,8 @@ const MessageListContent: FC = ({ return (
{withHistoryTriggers &&
} - {shouldRenderBotInfo && } + {shouldRenderAccountInfo + && } {dateGroups.flat()} {withHistoryTriggers && (
{ - 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( 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( chat, draftReplyInfo, isPrivate, - areChatSettingsLoaded: Boolean(chat?.settings), + arePeerSettingsLoaded: Boolean(userFullInfo?.settings), isComments: isMessageThread, canPost: !isPinnedMessageList diff --git a/src/components/middle/MiddleHeaderPanes.tsx b/src/components/middle/MiddleHeaderPanes.tsx index 17a35406f..1c6c4824d 100644 --- a/src/components/middle/MiddleHeaderPanes.tsx +++ b/src/components/middle/MiddleHeaderPanes.tsx @@ -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(FALLBACK_PANE_STATE); @@ -163,6 +166,7 @@ export default memo(withGlobal( }): 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( return { chat, + userFullInfo, isAudioPlayerRendered: Boolean(audioMessage), isMiddleSearchOpen, }; diff --git a/src/components/middle/message/Location.tsx b/src/components/middle/message/Location.tsx index 7bb4eed4d..c00f0b3db 100644 --- a/src/components/middle/message/Location.tsx +++ b/src/components/middle/message/Location.tsx @@ -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 = ({
{lang('AttachLiveLocation')}
- {formatLastUpdated(lang, serverTime, message.editDate)} + {formatLocationLastUpdate(lang, serverTime, message.editDate)}
{!isExpired && (
diff --git a/src/components/middle/panes/ChatReportPane.tsx b/src/components/middle/panes/ChatReportPane.tsx index fcff6b5b5..1021553d4 100644 --- a/src/components/middle/panes/ChatReportPane.tsx +++ b/src/components/middle/panes/ChatReportPane.tsx @@ -59,7 +59,7 @@ const ChatReportPane: FC = ({ deleteChatUser, deleteHistory, toggleChatArchived, - hideChatReportPane, + hidePeerSettingsBar, } = getActions(); const lang = useOldLang(); @@ -96,7 +96,7 @@ const ChatReportPane: FC = ({ }); const handleCloseReportPane = useLastCallback(() => { - hideChatReportPane({ chatId }); + hidePeerSettingsBar({ peerId: chatId }); }); const handleChatReportSpam = useLastCallback(() => { diff --git a/src/components/story/Story.tsx b/src/components/story/Story.tsx index 47c2e83f8..cf0f4e965 100644 --- a/src/components/story/Story.tsx +++ b/src/components/story/Story.tsx @@ -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({ )} {story && 'date' in story && ( - {formatRelativeTime(lang, serverTime, story.date)} + {formatRelativePastTime(lang, serverTime, story.date)} )} {isLoadedStory && story.isEdited && ( {lang('Story.HeaderEdited')} @@ -906,6 +907,7 @@ export default memo(withGlobal((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((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, }; diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index 5c01bc5d5..23d1585a3 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -2130,21 +2130,6 @@ addActionHandler('fetchChat', (global, actions, payload): ActionReturnType => { } }); -addActionHandler('loadChatSettings', async (global, actions, payload): Promise => { - 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 => { const { chatId, isEnabled } = payload; const chat = selectChat(global, chatId); diff --git a/src/global/actions/api/management.ts b/src/global/actions/api/management.ts index ca1822d7a..5577d5855 100644 --- a/src/global/actions/api/management.ts +++ b/src/global/actions/api/management.ts @@ -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 => { - const { chatId } = payload!; - const chat = selectChat(global, chatId); - if (!chat) return; +addActionHandler('hidePeerSettingsBar', async (global, actions, payload): Promise => { + 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); diff --git a/src/global/actions/api/users.ts b/src/global/actions/api/users.ts index 5d9a8a668..e68703127 100644 --- a/src/global/actions/api/users.ts +++ b/src/global/actions/api/users.ts @@ -217,7 +217,7 @@ addActionHandler('updateContact', async (global, actions, payload): Promise => { + 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; diff --git a/src/global/actions/apiUpdaters/users.ts b/src/global/actions/apiUpdaters/users.ts index f1dae5f24..77600a116 100644 --- a/src/global/actions/apiUpdaters/users.ts +++ b/src/global/actions/apiUpdaters/users.ts @@ -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; diff --git a/src/global/reducers/users.ts b/src/global/reducers/users.ts index 72b205d76..8ef7c8a2d 100644 --- a/src/global/reducers/users.ts +++ b/src/global/reducers/users.ts @@ -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(global: T, newById: Record): T { @@ -179,7 +178,7 @@ export function deleteContact(global: T, userId: string): }, }; - return updateChat(global, userId, { + return updateUserFullInfo(global, userId, { settings: undefined, }); } diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index 1411fa849..5867bd26a 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -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; diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index a7a5a9832..99782270f 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -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; diff --git a/src/styles/index.scss b/src/styles/index.scss index 4be03d70e..46a4fda4f 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -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); diff --git a/src/styles/themes.json b/src/styles/themes.json index 5518875db..08ba7f56d 100644 --- a/src/styles/themes.json +++ b/src/styles/themes.json @@ -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"], diff --git a/src/types/language.d.ts b/src/types/language.d.ts index f73e621e4..45c020f91 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -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 { 'LiveLocationUpdatedTodayAt': { 'time': V; }; + 'AtDateAgo': { + 'date': V; + }; 'MediaViewDownloading': { 'count': V; }; @@ -1792,6 +1801,14 @@ export interface LangPairWithVariables { 'peer': V; 'amount': V; }; + 'UserUpdatedName': { + 'user': V; + 'time': V; + }; + 'UserUpdatedPhoto': { + 'user': V; + 'time': V; + }; 'GiftInfoSaved': { 'link': V; }; @@ -2394,6 +2411,15 @@ export interface LangPairPluralWithVariables { 'Minutes': { 'count': V; }; + 'MinutesAgo': { + 'count': V; + }; + 'HoursAgo': { + 'count': V; + }; + 'DaysAgo': { + 'count': V; + }; 'PreviewForwardedMessage': { 'count': V; }; @@ -2436,6 +2462,9 @@ export interface LangPairPluralWithVariables { 'GiftWithdrawWait': { 'days': V; }; + 'ChatGroups': { + 'count': V; + }; 'StarsAmountText': { 'amount': V; }; diff --git a/src/util/dates/dateFormat.ts b/src/util/dates/dateFormat.ts index 76560fbef..5694eb573 100644 --- a/src/util/dates/dateFormat.ts +++ b/src/util/dates/dateFormat.ts @@ -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); +} diff --git a/src/util/localization/index.ts b/src/util/localization/index.ts index 7b8d5a506..70e9d49e3 100644 --- a/src/util/localization/index.ts +++ b/src/util/localization/index.ts @@ -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); } diff --git a/src/util/phoneNumber.ts b/src/util/phoneNumber.ts index 00fc8f69e..8d05531b2 100644 --- a/src/util/phoneNumber.ts +++ b/src/util/phoneNumber.ts @@ -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 = '') {