diff --git a/.fantasticonrc.cjs b/.fantasticonrc.cjs index d95a56152..02192ffda 100644 --- a/.fantasticonrc.cjs +++ b/.fantasticonrc.cjs @@ -5,7 +5,7 @@ module.exports = { fontTypes: ['woff2', 'woff'], assetTypes: ['scss', 'ts'], tag: '', - // Use a custom Handlebars template + normalize: true, templates: { scss: './dev/icons.scss.hbs', }, diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index b1e0e496e..bf8f03605 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -48,6 +48,8 @@ type AnyToVoidFunction = (...args: any[]) => void; type BooleanToVoidFunction = (value: boolean) => void; type NoneToVoidFunction = () => void; +type StringAutocomplete = T | (string & {}); + type Complete = { [P in keyof Required]: Pick extends Required> ? T[P] : (T[P] | undefined); }; diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index 56ee53bcd..696248622 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -29,14 +29,17 @@ import { getServerTimeOffset } from '../../../util/serverTime'; import { addPhotoToLocalDb, addUserToLocalDb } from '../helpers/localDb'; import { serializeBytes } from '../helpers/misc'; import { - buildApiBotVerification, buildApiFormattedText, buildApiPhoto, buildApiUsernames, buildAvatarPhotoId, + buildApiFormattedText, buildApiPhoto, buildApiUsernames, } from './common'; import { omitVirtualClassFields } from './helpers'; -import { buildApiPeerNotifySettings, buildApiRestrictionReasons } from './misc'; +import { buildApiRestrictionReasons } from './misc'; import { + buildApiBotVerification, buildApiEmojiStatus, buildApiPeerColor, buildApiPeerId, + buildApiPeerNotifySettings, + buildAvatarPhotoId, getApiChatIdFromMtpPeer, isMtpPeerChat, isMtpPeerUser, diff --git a/src/api/gramjs/apiBuilders/common.ts b/src/api/gramjs/apiBuilders/common.ts index 01005b0b4..fe5a77c53 100644 --- a/src/api/gramjs/apiBuilders/common.ts +++ b/src/api/gramjs/apiBuilders/common.ts @@ -3,7 +3,6 @@ import type { Entity } from '../../../lib/gramjs/types'; import { strippedPhotoToJpg } from '../../../lib/gramjs/Utils'; import type { - ApiBotVerification, ApiFormattedText, ApiMessageEntity, ApiMessageEntityDefault, @@ -293,19 +292,3 @@ export function buildApiMessageEntity(entity: GramJs.TypeMessageEntity): ApiMess length, }; } - -export function buildAvatarPhotoId(photo: GramJs.TypeUserProfilePhoto | GramJs.TypeChatPhoto) { - if ('photoId' in photo) { - return photo.photoId.toString(); - } - - return undefined; -} - -export function buildApiBotVerification(botVerification: GramJs.BotVerification): ApiBotVerification { - return { - botId: buildApiPeerId(botVerification.botId, 'user'), - iconId: botVerification.icon.toString(), - description: botVerification.description, - }; -} diff --git a/src/api/gramjs/apiBuilders/misc.ts b/src/api/gramjs/apiBuilders/misc.ts index aeb817de9..751952f15 100644 --- a/src/api/gramjs/apiBuilders/misc.ts +++ b/src/api/gramjs/apiBuilders/misc.ts @@ -7,9 +7,6 @@ import type { ApiCountry, ApiLanguage, ApiOldLangString, - ApiPeerColors, - ApiPeerNotifySettings, - ApiPeerProfileColorSet, ApiPrivacyKey, ApiRestrictionReason, ApiSession, @@ -20,9 +17,8 @@ import type { LangPackStringValue, } from '../../types'; -import { numberToHexColor } from '../../../util/colors'; import { - buildCollectionByCallback, omit, omitUndefined, pick, + omit, omitUndefined, pick, } from '../../../util/iteratees'; import { toJSNumber } from '../../../util/numbers'; import { addUserToLocalDb } from '../helpers/localDb'; @@ -111,23 +107,6 @@ export function buildPrivacyKey(key: GramJs.TypePrivacyKey): ApiPrivacyKey | und return undefined; } -export function buildApiPeerNotifySettings( - notifySettings: GramJs.TypePeerNotifySettings, -): ApiPeerNotifySettings { - const { - silent, muteUntil, showPreviews, otherSound, - } = notifySettings; - - const hasSound = !(otherSound instanceof GramJs.NotificationSoundNone); - - return { - hasSound, - isSilentPosting: silent, - mutedUntil: muteUntil, - shouldShowPreviews: showPreviews, - }; -} - function buildApiCountry(country: GramJs.help.Country, code: GramJs.help.CountryCode) { const { hidden, iso2, defaultName, name, @@ -295,46 +274,6 @@ export function buildApiLanguage(lang: GramJs.TypeLangPackLanguage): ApiLanguage }; } -function buildApiPeerColorSet(colorSet: GramJs.help.PeerColorSet) { - return colorSet.colors.map((color) => numberToHexColor(color)); -} - -function buildApiPeerProfileColorSet(colorSet: GramJs.help.PeerColorProfileSet): ApiPeerProfileColorSet { - return { - paletteColors: colorSet.paletteColors.map((color) => numberToHexColor(color)), - bgColors: colorSet.bgColors.map((color) => numberToHexColor(color)), - storyColors: colorSet.storyColors.map((color) => numberToHexColor(color)), - }; -} - -export function buildApiPeerColors(wrapper: GramJs.help.TypePeerColors): ApiPeerColors['general'] | undefined { - if (!(wrapper instanceof GramJs.help.PeerColors)) return undefined; - - return buildCollectionByCallback(wrapper.colors, (color) => { - return [color.colorId, { - isHidden: color.hidden, - colors: color.colors instanceof GramJs.help.PeerColorSet - ? buildApiPeerColorSet(color.colors) : undefined, - darkColors: color.darkColors instanceof GramJs.help.PeerColorSet - ? buildApiPeerColorSet(color.darkColors) : undefined, - }]; - }); -} - -export function buildApiPeerProfileColors(wrapper: GramJs.help.TypePeerColors): ApiPeerColors['profile'] | undefined { - if (!(wrapper instanceof GramJs.help.PeerColors)) return undefined; - - return buildCollectionByCallback(wrapper.colors, (color) => { - return [color.colorId, { - isHidden: color.hidden, - colors: color.colors instanceof GramJs.help.PeerColorProfileSet - ? buildApiPeerProfileColorSet(color.colors) : undefined, - darkColors: color.darkColors instanceof GramJs.help.PeerColorProfileSet - ? buildApiPeerProfileColorSet(color.darkColors) : undefined, - }]; - }); -} - export function buildApiTimezone(timezone: GramJs.TypeTimezone): ApiTimezone { const { id, name, utcOffset } = timezone; return { diff --git a/src/api/gramjs/apiBuilders/peers.ts b/src/api/gramjs/apiBuilders/peers.ts index 26384b464..cec9bf525 100644 --- a/src/api/gramjs/apiBuilders/peers.ts +++ b/src/api/gramjs/apiBuilders/peers.ts @@ -1,9 +1,18 @@ import { Api as GramJs } from '../../../lib/gramjs'; -import type { ApiEmojiStatusType, ApiPeerColor } from '../../types'; +import type { + ApiBotVerification, + ApiEmojiStatusType, + ApiPeerColor, + ApiPeerColors, + ApiPeerNotifySettings, + ApiPeerProfileColorSet, + ApiProfileTab, +} from '../../types'; import { CHANNEL_ID_BASE } from '../../../config'; import { numberToHexColor } from '../../../util/colors'; +import { buildCollectionByCallback } from '../../../util/iteratees'; type TypePeerOrInput = GramJs.TypePeer | GramJs.TypeInputPeer | GramJs.TypeInputUser | GramJs.TypeInputChannel; @@ -49,6 +58,46 @@ export function buildApiPeerColor(peerColor: GramJs.TypePeerColor): ApiPeerColor }; } +function buildApiPeerColorSet(colorSet: GramJs.help.PeerColorSet) { + return colorSet.colors.map((color) => numberToHexColor(color)); +} + +function buildApiPeerProfileColorSet(colorSet: GramJs.help.PeerColorProfileSet): ApiPeerProfileColorSet { + return { + paletteColors: colorSet.paletteColors.map((color) => numberToHexColor(color)), + bgColors: colorSet.bgColors.map((color) => numberToHexColor(color)), + storyColors: colorSet.storyColors.map((color) => numberToHexColor(color)), + }; +} + +export function buildApiPeerColors(wrapper: GramJs.help.TypePeerColors): ApiPeerColors['general'] | undefined { + if (!(wrapper instanceof GramJs.help.PeerColors)) return undefined; + + return buildCollectionByCallback(wrapper.colors, (color) => { + return [color.colorId, { + isHidden: color.hidden, + colors: color.colors instanceof GramJs.help.PeerColorSet + ? buildApiPeerColorSet(color.colors) : undefined, + darkColors: color.darkColors instanceof GramJs.help.PeerColorSet + ? buildApiPeerColorSet(color.darkColors) : undefined, + }]; + }); +} + +export function buildApiPeerProfileColors(wrapper: GramJs.help.TypePeerColors): ApiPeerColors['profile'] | undefined { + if (!(wrapper instanceof GramJs.help.PeerColors)) return undefined; + + return buildCollectionByCallback(wrapper.colors, (color) => { + return [color.colorId, { + isHidden: color.hidden, + colors: color.colors instanceof GramJs.help.PeerColorProfileSet + ? buildApiPeerProfileColorSet(color.colors) : undefined, + darkColors: color.darkColors instanceof GramJs.help.PeerColorProfileSet + ? buildApiPeerProfileColorSet(color.darkColors) : undefined, + }]; + }); +} + export function buildApiEmojiStatus(mtpEmojiStatus: GramJs.TypeEmojiStatus): ApiEmojiStatusType | undefined { if (mtpEmojiStatus instanceof GramJs.EmojiStatus) { @@ -77,3 +126,61 @@ ApiEmojiStatusType | undefined { return undefined; } + +export function buildAvatarPhotoId(photo: GramJs.TypeUserProfilePhoto | GramJs.TypeChatPhoto) { + if ('photoId' in photo) { + return photo.photoId.toString(); + } + + return undefined; +} + +export function buildApiBotVerification(botVerification: GramJs.BotVerification): ApiBotVerification { + return { + botId: buildApiPeerId(botVerification.botId, 'user'), + iconId: botVerification.icon.toString(), + description: botVerification.description, + }; +} + +export function buildApiPeerNotifySettings( + notifySettings: GramJs.TypePeerNotifySettings, +): ApiPeerNotifySettings { + const { + silent, muteUntil, showPreviews, otherSound, + } = notifySettings; + + const hasSound = !(otherSound instanceof GramJs.NotificationSoundNone); + + return { + hasSound, + isSilentPosting: silent, + mutedUntil: muteUntil, + shouldShowPreviews: showPreviews, + }; +} + +export function buildApiProfileTab(profileTab: GramJs.TypeProfileTab): ApiProfileTab { + switch (profileTab.className) { + case 'ProfileTabPosts': + return 'stories'; + case 'ProfileTabGifts': + return 'gifts'; + case 'ProfileTabMedia': + return 'media'; + case 'ProfileTabFiles': + return 'documents'; + case 'ProfileTabMusic': + return 'audio'; + case 'ProfileTabVoice': + return 'voice'; + case 'ProfileTabLinks': + return 'links'; + case 'ProfileTabGifs': + return 'gif'; + default: { + const _exhaustiveCheck: never = profileTab; + return _exhaustiveCheck; + } + } +} diff --git a/src/api/gramjs/apiBuilders/statistics.ts b/src/api/gramjs/apiBuilders/statistics.ts index a3038257b..f0af93198 100644 --- a/src/api/gramjs/apiBuilders/statistics.ts +++ b/src/api/gramjs/apiBuilders/statistics.ts @@ -16,9 +16,9 @@ import type { TypeStatisticsGraph, } from '../../types'; -import { buildApiUsernames, buildAvatarPhotoId } from './common'; +import { buildApiUsernames } from './common'; import { buildApiCurrencyAmount } from './payments'; -import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; +import { buildApiPeerId, buildAvatarPhotoId, getApiChatIdFromMtpPeer } from './peers'; export function buildChannelStatistics(stats: GramJs.stats.BroadcastStats): ApiChannelStatistics { return { diff --git a/src/api/gramjs/apiBuilders/users.ts b/src/api/gramjs/apiBuilders/users.ts index 4befffb21..66092ffbe 100644 --- a/src/api/gramjs/apiBuilders/users.ts +++ b/src/api/gramjs/apiBuilders/users.ts @@ -14,11 +14,18 @@ import { toJSNumber } from '../../../util/numbers'; import { buildApiBotInfo } from './bots'; import { buildApiBusinessIntro, buildApiBusinessLocation, buildApiBusinessWorkHours } from './business'; import { - buildApiBotVerification, buildApiPhoto, buildApiUsernames, buildAvatarPhotoId, + buildApiPhoto, buildApiUsernames, } from './common'; import { buildApiDisallowedGiftsSettings } from './gifts'; import { omitVirtualClassFields } from './helpers'; -import { buildApiEmojiStatus, buildApiPeerColor, buildApiPeerId } from './peers'; +import { + buildApiBotVerification, + buildApiEmojiStatus, + buildApiPeerColor, + buildApiPeerId, + buildApiProfileTab, + buildAvatarPhotoId, +} from './peers'; export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUserFullInfo { const { @@ -29,7 +36,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse contactRequirePremium, businessWorkHours, businessLocation, businessIntro, birthday, personalChannelId, personalChannelMessage, sponsoredEnabled, stargiftsCount, botVerification, botCanManageEmojiStatus, settings, sendPaidMessagesStars, displayGiftsButton, disallowedGifts, - starsRating, starsMyPendingRating, starsMyPendingRatingDate, + starsRating, starsMyPendingRating, starsMyPendingRatingDate, mainTab, }, users, } = mtpUserFull; @@ -68,6 +75,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse hasScheduledMessages: hasScheduled, paidMessagesStars: toJSNumber(sendPaidMessagesStars), settings: buildApiPeerSettings(settings), + mainTab: mainTab && buildApiProfileTab(mainTab), }; } diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index d1855ddab..fcf227868 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -23,6 +23,7 @@ import type { ApiPoll, ApiPremiumGiftCodeOption, ApiPrivacyKey, + ApiProfileTab, ApiReactionWithPaid, ApiReportReason, ApiRequestInputInvoice, @@ -980,3 +981,28 @@ export function buildInputSavedStarGift(inputGift: ApiRequestInputSavedStarGift) savedId: BigInt(inputGift.savedId), }); } + +export function buildInputProfileTab(profileTab: ApiProfileTab) { + switch (profileTab) { + case 'stories': + return new GramJs.ProfileTabPosts(); + case 'gifts': + return new GramJs.ProfileTabGifts(); + case 'media': + return new GramJs.ProfileTabMedia(); + case 'documents': + return new GramJs.ProfileTabFiles(); + case 'audio': + return new GramJs.ProfileTabMusic(); + case 'voice': + return new GramJs.ProfileTabVoice(); + case 'links': + return new GramJs.ProfileTabLinks(); + case 'gif': + return new GramJs.ProfileTabGifs(); + default: { + const _exhaustiveCheck: never = profileTab; + return _exhaustiveCheck; + } + } +} diff --git a/src/api/gramjs/methods/account.ts b/src/api/gramjs/methods/account.ts index ae7f28f6c..fbba9e3ce 100644 --- a/src/api/gramjs/methods/account.ts +++ b/src/api/gramjs/methods/account.ts @@ -1,11 +1,17 @@ import { Api as GramJs } from '../../../lib/gramjs'; import type { - ApiPeer, ApiPhoto, ApiReportReason, + ApiPeer, ApiPhoto, ApiProfileTab, ApiReportReason, } from '../../types'; import { buildApiChatLink } from '../apiBuilders/misc'; -import { buildInputPeer, buildInputPhoto, buildInputReportReason, DEFAULT_PRIMITIVES } from '../gramjsBuilders'; +import { + buildInputPeer, + buildInputPhoto, + buildInputProfileTab, + buildInputReportReason, + DEFAULT_PRIMITIVES, +} from '../gramjsBuilders'; import { invokeRequest } from './client'; export async function reportPeer({ @@ -124,3 +130,11 @@ export function setAccountTTL({ days }: { days: number }) { shouldReturnTrue: true, }); } + +export function setAccountMainProfileTab({ tab }: { tab: ApiProfileTab }) { + return invokeRequest(new GramJs.account.SetMainProfileTab({ + tab: buildInputProfileTab(tab), + }), { + shouldReturnTrue: true, + }); +} diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 3421b86ca..bbc0941cc 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -17,6 +17,7 @@ import type { ApiPeer, ApiPeerNotifySettings, ApiPhoto, + ApiProfileTab, ApiTopic, ApiUser, ApiUserStatus, @@ -51,10 +52,15 @@ import { buildChatMembers, getPeerKey, } from '../apiBuilders/chats'; -import { buildApiBotVerification, buildApiPhoto } from '../apiBuilders/common'; +import { buildApiPhoto } from '../apiBuilders/common'; import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages'; -import { buildApiPeerNotifySettings } from '../apiBuilders/misc'; -import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; +import { + buildApiBotVerification, + buildApiPeerId, + buildApiPeerNotifySettings, + buildApiProfileTab, + getApiChatIdFromMtpPeer, +} from '../apiBuilders/peers'; import { buildStickerSet } from '../apiBuilders/symbols'; import { buildApiPeerSettings, buildApiUser, buildApiUserStatuses } from '../apiBuilders/users'; import { @@ -66,6 +72,7 @@ import { buildInputChatReactions, buildInputPeer, buildInputPhoto, + buildInputProfileTab, buildInputReplyTo, buildInputSuggestedPost, buildInputUser, @@ -654,6 +661,7 @@ async function getFullChannelInfo( stargiftsCount, stargiftsAvailable, paidMessagesAvailable, + mainTab, } = result.fullChat; if (chatPhoto) { @@ -753,6 +761,7 @@ async function getFullChannelInfo( starGiftCount: stargiftsCount, areStarGiftsAvailable: Boolean(stargiftsAvailable), arePaidMessagesAvailable: paidMessagesAvailable, + mainTab: mainTab && buildApiProfileTab(mainTab), }, chats, userStatusesById: statusesById, @@ -2092,3 +2101,12 @@ export function toggleAutoTranslation({ shouldReturnTrue: true, }); } + +export function setChannelMainProfileTab({ chat, tab }: { chat: ApiChat; tab: ApiProfileTab }) { + return invokeRequest(new GramJs.channels.SetMainProfileTab({ + channel: buildInputChannel(chat.id, chat.accessHash), + tab: buildInputProfileTab(tab), + }), { + shouldReturnTrue: true, + }); +} diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 6865023b6..dd3e1aeb4 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -1494,6 +1494,9 @@ export async function searchMessagesInChat({ case 'profilePhoto': filter = new GramJs.InputMessagesFilterChatPhotos(); break; + case 'gif': + filter = new GramJs.InputMessagesFilterGif(); + break; case 'text': default: { filter = new GramJs.InputMessagesFilterEmpty(); diff --git a/src/api/gramjs/methods/settings.ts b/src/api/gramjs/methods/settings.ts index 7ed4b5684..482a8fff4 100644 --- a/src/api/gramjs/methods/settings.ts +++ b/src/api/gramjs/methods/settings.ts @@ -31,9 +31,6 @@ import { buildApiConfig, buildApiCountryList, buildApiLanguage, - buildApiPeerColors, - buildApiPeerNotifySettings, - buildApiPeerProfileColors, buildApiSession, buildApiTimezone, buildApiWallpaper, @@ -41,7 +38,12 @@ import { buildLangStrings, oldBuildLangPack, } from '../apiBuilders/misc'; -import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; +import { + buildApiPeerColors, + buildApiPeerNotifySettings, + buildApiPeerProfileColors, + getApiChatIdFromMtpPeer, +} from '../apiBuilders/peers'; import { buildDisallowedGiftsSettings, buildInputChannel, diff --git a/src/api/gramjs/updates/mtpUpdateHandler.ts b/src/api/gramjs/updates/mtpUpdateHandler.ts index 61666135d..4127fa23e 100644 --- a/src/api/gramjs/updates/mtpUpdateHandler.ts +++ b/src/api/gramjs/updates/mtpUpdateHandler.ts @@ -49,12 +49,16 @@ import { buildMessageDraft, } from '../apiBuilders/messages'; import { - buildApiPeerNotifySettings, buildLangStrings, buildPrivacyKey, } from '../apiBuilders/misc'; import { buildApiCurrencyAmount } from '../apiBuilders/payments'; -import { buildApiEmojiStatus, buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; +import { + buildApiEmojiStatus, + buildApiPeerId, + buildApiPeerNotifySettings, + getApiChatIdFromMtpPeer, +} from '../apiBuilders/peers'; import { buildApiPaidReactionPrivacy, buildApiReaction, diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index 246ca1f99..788b2df7f 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -2,9 +2,17 @@ import type { ApiBotCommand } from './bots'; import type { ApiChatReactions, ApiFormattedText, ApiInputMessageReplyInfo, ApiInputSuggestedPostInfo, ApiPhoto, ApiStickerSet, } from './messages'; -import type { ApiBotVerification, ApiChatInviteImporter, ApiPeerNotifySettings, ApiRestrictionReason } from './misc'; +import type { ApiChatInviteImporter, ApiPeerNotifySettings, ApiRestrictionReason } from './misc'; import type { - ApiEmojiStatusType, ApiFakeType, ApiUser, ApiUsername, + ApiBotVerification, + ApiEmojiStatusType, + ApiFakeType, + ApiPeerColor, + ApiProfileTab, + ApiSendAsPeerId, +} from './peers'; +import type { + ApiUser, ApiUsername, } from './users'; type ApiChatType = ( @@ -156,6 +164,7 @@ export interface ApiChatFullInfo { boostsApplied?: number; boostsToUnrestrict?: number; botVerification?: ApiBotVerification; + mainTab?: ApiProfileTab; } export interface ApiChatMember { @@ -235,23 +244,6 @@ export interface ApiChatFolder { hasMyInvites?: true; } -export interface ApiPeerSettings { - isAutoArchived?: boolean; - canReportSpam?: boolean; - canAddContact?: boolean; - canBlockContact?: boolean; - chargedPaidMessageStars?: number; - registrationMonth?: string; - phoneCountry?: string; - nameChangeDate?: number; - photoChangeDate?: number; -} - -export interface ApiSendAsPeerId { - id: string; - isPremium?: boolean; -} - export interface ApiTopic { id: number; isClosed?: boolean; @@ -296,11 +288,6 @@ export interface ApiChatlistExportedInvite { peerIds: string[]; } -export interface ApiPeerColor { - color?: number; - backgroundEmojiId?: string; -} - export interface ApiMissingInvitedUser { id: string; isRequiringPremiumToInvite?: boolean; diff --git a/src/api/types/index.ts b/src/api/types/index.ts index f810a25a3..9fe4a0530 100644 --- a/src/api/types/index.ts +++ b/src/api/types/index.ts @@ -12,3 +12,4 @@ export * from './statistics'; export * from './stories'; export * from './business'; export * from './stars'; +export * from './peers'; diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 439469361..7f659da96 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -4,12 +4,12 @@ import type { ApiBotInlineResult, ApiWebDocument, } from './bots'; -import type { ApiPeerColor } from './chats'; import type { ApiMessageAction } from './messageActions'; import type { ApiRestrictionReason } from './misc'; import type { ApiLabeledPrice, } from './payments'; +import type { ApiPeerColor } from './peers'; import type { ApiStarGiftUnique, ApiTypeCurrencyAmount } from './stars'; import type { ApiMessageStoryData, ApiStory, ApiWebPageStickerData, ApiWebPageStoryData, @@ -957,7 +957,8 @@ export type ApiTranscription = { transcriptionId: string; }; -export type ApiMessageSearchType = 'text' | 'media' | 'documents' | 'links' | 'audio' | 'voice' | 'profilePhoto'; +export type ApiMessageSearchType = 'text' | 'media' | 'documents' | 'links' | 'audio' | 'voice' | 'gif' + | 'profilePhoto'; export type ApiGlobalMessageSearchType = 'text' | 'channels' | 'media' | 'documents' | 'links' | 'audio' | 'voice' | 'publicPosts'; export type ApiMessageSearchContext = 'all' | 'users' | 'groups' | 'channels'; diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 81d2d320c..c30fb3fe9 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -5,6 +5,7 @@ import type { IconName } from '../../types/icons'; import type { RegularLangFnParameters } from '../../util/localization'; import type { ApiDocument, ApiPhoto, ApiReaction } from './messages'; import type { ApiPremiumSection } from './payments'; +import type { ApiBotVerification } from './peers'; import type { ApiStarsSubscriptionPricing } from './stars'; import type { ApiUser } from './users'; @@ -287,26 +288,6 @@ export interface ApiConfig { maxForwardedCount: number; } -export type ApiPeerColorSet = string[]; -export type ApiPeerProfileColorSet = { - paletteColors: string[]; - bgColors: string[]; - storyColors: string[]; -}; - -export type ApiPeerColorOption = { - isHidden?: true; - colors?: T; - darkColors?: T; -}; - -export interface ApiPeerColors { - general: Record>; - generalHash?: number; - profile: Record>; - profileHash?: number; -} - export interface ApiTimezone { id: string; name: string; @@ -352,21 +333,6 @@ export interface ApiCollectibleInfo { url: string; } -export interface ApiPeerPhotos { - fallbackPhoto?: ApiPhoto; - personalPhoto?: ApiPhoto; - photos: ApiPhoto[]; - count: number; - nextOffset?: number; - isLoading?: boolean; -} - -export interface ApiBotVerification { - botId: string; - iconId: string; - description: string; -} - export type ApiLimitType = | 'uploadMaxFileparts' | 'stickersFaved' diff --git a/src/api/types/peers.ts b/src/api/types/peers.ts new file mode 100644 index 000000000..9735dc615 --- /dev/null +++ b/src/api/types/peers.ts @@ -0,0 +1,84 @@ +import type { ApiPhoto } from './messages'; + +export interface ApiPeerPhotos { + fallbackPhoto?: ApiPhoto; + personalPhoto?: ApiPhoto; + photos: ApiPhoto[]; + count: number; + nextOffset?: number; + isLoading?: boolean; +} + +export type ApiFakeType = 'fake' | 'scam'; + +export interface ApiBotVerification { + botId: string; + iconId: string; + description: string; +} + +export type ApiEmojiStatusType = ApiEmojiStatus | ApiEmojiStatusCollectible; + +export interface ApiEmojiStatus { + type: 'regular'; + documentId: string; + until?: number; +} + +export interface ApiEmojiStatusCollectible { + type: 'collectible'; + collectibleId: string; + documentId: string; + title: string; + slug: string; + patternDocumentId: string; + centerColor: string; + edgeColor: string; + patternColor: string; + textColor: string; + until?: number; +} + +export interface ApiPeerSettings { + isAutoArchived?: boolean; + canReportSpam?: boolean; + canAddContact?: boolean; + canBlockContact?: boolean; + chargedPaidMessageStars?: number; + registrationMonth?: string; + phoneCountry?: string; + nameChangeDate?: number; + photoChangeDate?: number; +} + +export interface ApiSendAsPeerId { + id: string; + isPremium?: boolean; +} + +export interface ApiPeerColor { + color?: number; + backgroundEmojiId?: string; +} + +export type ApiPeerColorSet = string[]; +export type ApiPeerProfileColorSet = { + paletteColors: string[]; + bgColors: string[]; + storyColors: string[]; +}; + +export type ApiPeerColorOption = { + isHidden?: true; + colors?: T; + darkColors?: T; +}; + +export interface ApiPeerColors { + general: Record>; + generalHash?: number; + profile: Record>; + profileHash?: number; +} + +export type ApiProfileTab = 'stories' | 'gifts' | 'media' | 'documents' | 'audio' | 'voice' | 'links' | 'gif'; diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index e999465eb..980dc2144 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -18,7 +18,6 @@ import type { ApiChatFullInfo, ApiChatMember, ApiDraft, - ApiPeerSettings, ApiTypingStatus, } from './chats'; import type { @@ -43,11 +42,12 @@ import type { ApiPeerNotifySettings, ApiSessionData, } from './misc'; +import type { ApiEmojiStatusType, ApiPeerSettings } from './peers'; import type { ApiPrivacyKey, LangPackStringValue, PrivacyVisibility } from './settings'; import type { ApiTypeCurrencyAmount } from './stars'; import type { ApiStealthMode, ApiStory, ApiStorySkipped } from './stories'; import type { - ApiEmojiStatusType, ApiUser, ApiUserFullInfo, ApiUserStatus, + ApiUser, ApiUserFullInfo, ApiUserStatus, } from './users'; export type ApiUpdateReady = { diff --git a/src/api/types/users.ts b/src/api/types/users.ts index 885b736a5..3811032da 100644 --- a/src/api/types/users.ts +++ b/src/api/types/users.ts @@ -1,9 +1,15 @@ import type { API_CHAT_TYPES } from '../../config'; import type { ApiBotInfo } from './bots'; import type { ApiBusinessIntro, ApiBusinessLocation, ApiBusinessWorkHours } from './business'; -import type { ApiPeerColor, ApiPeerSettings } from './chats'; import type { ApiDocument, ApiPhoto } from './messages'; -import type { ApiBotVerification } from './misc'; +import type { + ApiBotVerification, + ApiEmojiStatusType, + ApiFakeType, + ApiPeerColor, + ApiPeerSettings, + ApiProfileTab, +} from './peers'; import type { ApiSavedStarGift, ApiStarsRating } from './stars'; export interface ApiUser { @@ -75,10 +81,9 @@ export interface ApiUserFullInfo { botVerification?: ApiBotVerification; paidMessagesStars?: number; settings?: ApiPeerSettings; + mainTab?: ApiProfileTab; } -export type ApiFakeType = 'fake' | 'scam'; - export type ApiUserType = 'userTypeBot' | 'userTypeRegular' | 'userTypeDeleted' | 'userTypeUnknown'; export interface ApiUserStatus { @@ -136,28 +141,6 @@ export interface ApiAttachBotIcon { document: ApiDocument; } -export type ApiEmojiStatusType = ApiEmojiStatus | ApiEmojiStatusCollectible; - -export interface ApiEmojiStatus { - type: 'regular'; - documentId: string; - until?: number; -} - -export interface ApiEmojiStatusCollectible { - type: 'collectible'; - collectibleId: string; - documentId: string; - title: string; - slug: string; - patternDocumentId: string; - centerColor: string; - edgeColor: string; - patternColor: string; - textColor: string; - until?: number; -} - export interface ApiBirthday { day: number; month: number; diff --git a/src/assets/font-icons/group-filled.svg b/src/assets/font-icons/group-filled.svg index a5e34eca2..e9909b8ae 100644 --- a/src/assets/font-icons/group-filled.svg +++ b/src/assets/font-icons/group-filled.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/assets/font-icons/one-filled.svg b/src/assets/font-icons/one-filled.svg index 88076a85b..b388b19d7 100644 --- a/src/assets/font-icons/one-filled.svg +++ b/src/assets/font-icons/one-filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index aec7e6233..bc0be526b 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -1434,6 +1434,8 @@ "ProfileBirthdayValueYear" = "{date} ({age} years old)"; "ProfileBirthdayTodayValue" = "🎂 {date}"; "ProfileBirthdayTodayValueYear" = "🎂 {date} ({age} years old)"; +"ProfileMenuSetMainTab" = "Set as Main Tab"; +"ProfileItemSubscribers" = "Subscribers"; "MonetizationInfoTONTitle" = "What is 💎 TON?"; "ChannelEarnLearnCoinAbout" = "TON is a blockchain platform and cryptocurrency that Telegram uses for its high speed and low commissions on transactions. {link}"; "MonetizationBalanceZeroInfo" = "You will be able to collect rewards using Fragment, a third-party platform used by advertisers to pay for ads. {link}"; @@ -1710,6 +1712,7 @@ "ProfileTabLinks" = "Links"; "ProfileTabMusic" = "Music"; "ProfileTabVoice" = "Voice"; +"ProfileTabGifs" = "GIF"; "ProfileTabSharedGroups" = "Groups"; "ProfileTabSimilarChannels" = "Similar Channels"; "ProfileTabSimilarBots" = "Similar Bots"; diff --git a/src/components/common/AnimatedTabList.tsx b/src/components/common/AnimatedTabList.tsx index d1833cd7a..283b3f751 100644 --- a/src/components/common/AnimatedTabList.tsx +++ b/src/components/common/AnimatedTabList.tsx @@ -3,6 +3,7 @@ import { memo, useEffect, useRef, useState } from '../../lib/teact/teact'; import type { ApiSticker } from '../../api/types'; import type { AnimationLevel } from '../../types'; +import { ANIMATION_LEVEL_MIN } from '../../config'; import buildClassName from '../../util/buildClassName'; import useHorizontalScroll from '../../hooks/useHorizontalScroll'; @@ -38,7 +39,7 @@ const AnimatedTabList = ({ const clipPathContainerRef = useRef(); const selectedIndex = items.findIndex((item) => item.id === selectedItemId) || 0; const [clipPath, setClipPath] = useState(''); - const shouldAnimate = animationLevel > 0; + const shouldAnimate = animationLevel > ANIMATION_LEVEL_MIN; useHorizontalScroll(containerRef, !items.length, true); diff --git a/src/components/common/Media.tsx b/src/components/common/Media.tsx index 3e98e002b..6d56b30d9 100644 --- a/src/components/common/Media.tsx +++ b/src/components/common/Media.tsx @@ -1,4 +1,3 @@ -import type { FC } from '../../lib/teact/teact'; import { memo, useRef } from '../../lib/teact/teact'; import type { ApiMessage } from '../../api/types'; @@ -8,7 +7,9 @@ import { getMessageHtmlId, getMessageIsSpoiler, getMessageVideo, + getVideoMediaHash, } from '../../global/helpers'; +import { IS_TOUCH_ENV } from '../../util/browser/windowEnvironment'; import buildClassName from '../../util/buildClassName'; import { formatMediaDuration } from '../../util/dates/dateFormat'; import stopEvent from '../../util/stopEvent'; @@ -21,6 +22,7 @@ import useLastCallback from '../../hooks/useLastCallback'; import useMedia from '../../hooks/useMedia'; import useMediaTransitionDeprecated from '../../hooks/useMediaTransitionDeprecated'; +import OptimizedVideo from '../ui/OptimizedVideo'; import MediaSpoiler from './MediaSpoiler'; import './Media.scss'; @@ -29,26 +31,32 @@ type OwnProps = { message: ApiMessage; idPrefix?: string; isProtected?: boolean; + canAutoPlay?: boolean; observeIntersection?: ObserveFn; onClick?: (messageId: number, chatId: string) => void; }; -const Media: FC = ({ +const Media = ({ message, idPrefix = 'shared-media', isProtected, + canAutoPlay, observeIntersection, onClick, -}) => { +}: OwnProps) => { const ref = useRef(); const isIntersecting = useIsIntersecting(ref, observeIntersection); + const [isHovering, markMouseOver, markMouseOut] = useFlag(); + const thumbDataUri = useThumbnail(message); const mediaHash = useMessageMediaHash(message, 'pictogram'); const mediaBlobUrl = useMedia(mediaHash, !isIntersecting); const transitionClassNames = useMediaTransitionDeprecated(mediaBlobUrl); const video = getMessageVideo(message); + const fullGiftHash = video?.isGif ? getVideoMediaHash(video, 'full') : undefined; + const fullGifBlobUrl = useMedia(fullGiftHash, !isIntersecting); const hasSpoiler = getMessageIsSpoiler(message); const [isSpoilerShown, , hideSpoiler] = useFlag(hasSpoiler); @@ -64,6 +72,8 @@ const Media: FC = ({ id={`${idPrefix}${getMessageHtmlId(message.id)}`} className="Media scroll-item" onClick={onClick ? handleClick : undefined} + onMouseOver={!IS_TOUCH_ENV ? markMouseOver : undefined} + onMouseOut={!IS_TOUCH_ENV ? markMouseOut : undefined} > = ({ decoding="async" onContextMenu={isProtected ? stopEvent : undefined} /> - + {fullGifBlobUrl ? ( + + ) : ( + + )} {hasSpoiler && ( = ({ +const ChatExtra = ({ chatOrUserId, user, chat, @@ -124,7 +125,8 @@ const ChatExtra: FC = ({ className, style, isInSettings, -}) => { + canViewSubscribers, +}: OwnProps & StateProps) => { const { showNotification, updateChatMutedState, @@ -136,6 +138,7 @@ const ChatExtra: FC = ({ requestMainWebView, toggleUserEmojiStatusPermission, toggleUserLocationPermission, + requestNextManagementScreen, } = getActions(); const { @@ -261,6 +264,10 @@ const ChatExtra: FC = ({ copy(formatUsername(username.username, isChat), oldLang(isChat ? 'Link' : 'Username')); }); + const handleOpenSubscribers = useLastCallback(() => { + requestNextManagementScreen({ screen: ManagementScreens.ChannelSubscribers }); + }); + const handleOpenApp = useLastCallback(() => { const botId = user?.id; if (!botId) { @@ -476,6 +483,12 @@ const ChatExtra: FC = ({ /> )} + {canViewSubscribers && ( + +
{lang('ProfileItemSubscribers')}
+ {lang.number(chat?.membersCount || 0)} +
+ )} {botVerification && (
( const chatInviteLink = chatFullInfo?.inviteLink; const description = userFullInfo?.bio || chatFullInfo?.about; + const canViewSubscribers = chat && isChatChannel(chat) && isChatAdmin(chat); const canInviteUsers = chat && !user && ( (!isChatChannel(chat) && !isUserRightBanned(chat, 'inviteUsers')) || getHasAdminRight(chat, 'inviteUsers') @@ -542,6 +556,7 @@ export default memo(withGlobal( hasMainMiniApp, isBotCanManageEmojiStatus: userFullInfo?.isBotCanManageEmojiStatus, botVerification, + canViewSubscribers, }; }, )(ChatExtra)); diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx index f1dbff135..9f3c8bc2d 100644 --- a/src/components/mediaViewer/MediaViewer.tsx +++ b/src/components/mediaViewer/MediaViewer.tsx @@ -18,7 +18,8 @@ import { type MediaViewerMedia, MediaViewerOrigin, type ThreadId } from '../../t import { ANIMATION_END_DELAY } from '../../config'; import { requestMutation } from '../../lib/fasterdom/fasterdom'; import { - getChatMediaMessageIds, getMessagePaidMedia, isChatAdmin, + getMessageContentIds, + getMessagePaidMedia, isChatAdmin, } from '../../global/helpers'; import { selectChatMessage, @@ -156,7 +157,7 @@ const MediaViewer = ({ bestData, dimensions, isGif, - isFromSharedMedia, + contentType, } = useMediaProps({ media, isAvatar: Boolean(avatarOwner), origin, delay: isGhostAnimation && ANIMATION_DURATION, }); @@ -173,8 +174,8 @@ const MediaViewer = ({ const messageMediaIds = useMemo(() => { return withDynamicLoading ? collectedMessageIds - : getChatMediaMessageIds(chatMessages || {}, collectedMessageIds || [], isFromSharedMedia); - }, [chatMessages, collectedMessageIds, isFromSharedMedia, withDynamicLoading]); + : getMessageContentIds(chatMessages || {}, collectedMessageIds || [], contentType); + }, [chatMessages, collectedMessageIds, contentType, withDynamicLoading]); if (isOpen && (!prevSenderId || prevSenderId !== senderId || animationKey.current === undefined)) { animationKey.current = isSingle ? 0 : (messageId || mediaIndex); @@ -520,7 +521,7 @@ export default memo(withGlobal( const currentItem = getMediaViewerItem({ avatarOwner, standaloneMedia, profilePhotos, mediaIndex, }); - const viewableMedia = selectViewableMedia(global, currentItem); + const viewableMedia = selectViewableMedia(global, origin, currentItem); return { profilePhotos, @@ -565,6 +566,11 @@ export default memo(withGlobal( } } + const currentItem = getMediaViewerItem({ + message, standaloneMedia, mediaIndex, sponsoredMessage, + }); + const viewableMedia = selectViewableMedia(global, origin, currentItem); + let chatMessages: Record | undefined; if (chatId) { @@ -588,7 +594,8 @@ export default memo(withGlobal( collectedMessageIds = foundIds; } else if (origin === MediaViewerOrigin.SharedMedia) { const currentSearch = selectCurrentSharedMediaSearch(global); - const { foundIds } = (currentSearch && currentSearch.resultsByType && currentSearch.resultsByType.media) || {}; + const resultsByType = currentSearch?.resultsByType; + const { foundIds } = (viewableMedia?.isGif ? resultsByType?.gif : resultsByType?.media) || {}; collectedMessageIds = foundIds; } else if (isOriginInline || isOriginAlbum) { const outlyingList = selectOutlyingListByMessageId(global, chatId, threadId, messageId); @@ -596,11 +603,6 @@ export default memo(withGlobal( } } - const currentItem = getMediaViewerItem({ - message, standaloneMedia, mediaIndex, sponsoredMessage, - }); - const viewableMedia = selectViewableMedia(global, currentItem); - return { chatId, threadId, diff --git a/src/components/mediaViewer/MediaViewerActions.tsx b/src/components/mediaViewer/MediaViewerActions.tsx index e6e6d4906..1929bd33e 100644 --- a/src/components/mediaViewer/MediaViewerActions.tsx +++ b/src/components/mediaViewer/MediaViewerActions.tsx @@ -413,7 +413,7 @@ export default memo(withGlobal( const canDelete = canDeleteMessage || canDeleteAvatar; const canUpdate = canUpdateMedia && Boolean(avatarPhoto) && !isCurrentAvatar; const messageListType = currentMessageList?.type; - const viewableMedia = selectViewableMedia(global, item); + const viewableMedia = selectViewableMedia(global, origin, item); return { activeDownloads, diff --git a/src/components/mediaViewer/MediaViewerContent.tsx b/src/components/mediaViewer/MediaViewerContent.tsx index abccc3ad7..70d5bc6c4 100644 --- a/src/components/mediaViewer/MediaViewerContent.tsx +++ b/src/components/mediaViewer/MediaViewerContent.tsx @@ -257,7 +257,7 @@ export default memo(withGlobal( const message = item.type === 'message' ? item.message : undefined; const sponsoredMessage = item.type === 'sponsoredMessage' ? item.message : undefined; const textMessage = message || sponsoredMessage; - const viewableMedia = selectViewableMedia(global, item); + const viewableMedia = selectViewableMedia(global, origin, item); const maxTimestamp = message && selectMessageTimestampableDuration(global, message, true); diff --git a/src/components/mediaViewer/helpers/getViewableMedia.ts b/src/components/mediaViewer/helpers/getViewableMedia.ts index 79765ab51..ac4fc26b8 100644 --- a/src/components/mediaViewer/helpers/getViewableMedia.ts +++ b/src/components/mediaViewer/helpers/getViewableMedia.ts @@ -2,7 +2,7 @@ import type { ApiMessage, ApiPeer, ApiPeerPhotos, ApiSponsoredMessage, } from '../../../api/types'; import type { GlobalState } from '../../../global/types'; -import type { MediaViewerMedia } from '../../../types'; +import { type MediaViewerMedia, MediaViewerOrigin } from '../../../types'; import { getMessageContent, isDocumentPhoto, isDocumentVideo } from '../../../global/helpers'; import { selectWebPageFromMessage } from '../../../global/selectors'; @@ -28,6 +28,7 @@ export type MediaViewerItem = { export type ViewableMedia = { media: MediaViewerMedia; + isGif?: boolean; isSingle?: boolean; }; @@ -77,12 +78,16 @@ export function getMediaViewerItem({ return undefined; } -export default function selectViewableMedia(global: GlobalState, params?: MediaViewerItem): ViewableMedia | undefined { +export default function selectViewableMedia( + global: GlobalState, origin?: MediaViewerOrigin, params?: MediaViewerItem, +): ViewableMedia | undefined { if (!params) return undefined; if (params.type === 'standalone') { + const media = params.media[params.mediaIndex]; return { - media: params.media[params.mediaIndex], + media, + isGif: media.mediaType === 'video' && media.isGif, isSingle: params.media.length === 1, }; } @@ -134,6 +139,7 @@ export default function selectViewableMedia(global: GlobalState, params?: MediaV const { photo: extendedPhoto, video: extendedVideo } = extendedMedia; return { media: (extendedPhoto || extendedVideo)!, + isGif: extendedVideo?.isGif, }; } } @@ -143,7 +149,8 @@ export default function selectViewableMedia(global: GlobalState, params?: MediaV if (media) { return { media, - isSingle: video?.isGif, + isGif: video?.isGif, + isSingle: video?.isGif && origin !== MediaViewerOrigin.SharedMedia, }; } diff --git a/src/components/mediaViewer/hooks/useMediaProps.ts b/src/components/mediaViewer/hooks/useMediaProps.ts index 9aac2b976..158c046af 100644 --- a/src/components/mediaViewer/hooks/useMediaProps.ts +++ b/src/components/mediaViewer/hooks/useMediaProps.ts @@ -1,7 +1,7 @@ import { useMemo } from '../../../lib/teact/teact'; import type { MediaViewerMedia } from '../../../types'; -import { ApiMediaFormat } from '../../../api/types'; +import { ApiMediaFormat, type ApiMessageSearchType } from '../../../api/types'; import { MediaViewerOrigin } from '../../../types'; import { @@ -46,6 +46,8 @@ export const useMediaProps = ({ const isFromSharedMedia = origin === MediaViewerOrigin.SharedMedia; const isFromSearch = origin === MediaViewerOrigin.SearchResult; + const contentType: ApiMessageSearchType = isGif ? 'gif' : 'media'; + const getMediaOrAvatarHash = useMemo(() => (isFull?: boolean) => { if (!media) return undefined; @@ -133,7 +135,7 @@ export const useMediaProps = ({ bestImageData, bestData, dimensions, - isFromSharedMedia, + contentType, isVideoAvatar, isLocal, loadProgress, diff --git a/src/components/right/Profile.scss b/src/components/right/Profile.scss index 5857a1b22..e14b4feb5 100644 --- a/src/components/right/Profile.scss +++ b/src/components/right/Profile.scss @@ -1,6 +1,8 @@ @use '../../styles/mixins'; .Profile { + scrollbar-gutter: stable; + overflow-x: hidden; overflow-y: scroll; display: flex; @@ -134,6 +136,7 @@ &.storiesArchive-list, &.stories-list, &.media-list, + &.gif-list, &.previewMedia-list, &.gifts-list { display: grid; diff --git a/src/components/right/Profile.tsx b/src/components/right/Profile.tsx index e1b4ed879..cb23d6a75 100644 --- a/src/components/right/Profile.tsx +++ b/src/components/right/Profile.tsx @@ -6,6 +6,7 @@ import type { ApiChat, ApiChatMember, ApiMessage, + ApiProfileTab, ApiSavedStarGift, ApiStarGiftCollection, ApiStoryAlbum, @@ -29,7 +30,6 @@ import { getIsDownloading, getIsSavedDialog, getMessageDocument, - isChatAdmin, isChatChannel, isChatGroup, isUserBot, @@ -38,6 +38,7 @@ import { import { getSavedGiftKey } from '../../global/helpers/stars'; import { selectActiveDownloads, + selectCanUpdateMainTab, selectChat, selectChatFullInfo, selectChatMessages, @@ -47,6 +48,7 @@ import { selectIsRightColumnShown, selectMonoforumChannel, selectPeerStories, + selectPerformanceSettingsValue, selectSimilarBotsIds, selectSimilarChannelIds, selectTabState, @@ -86,6 +88,7 @@ import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; import useOldLang from '../../hooks/useOldLang'; import useSyncEffect from '../../hooks/useSyncEffect'; +import useSyncEffectWithPrevDeps from '../../hooks/useSyncEffectWithPrevDeps.ts'; import useAsyncRendering from './hooks/useAsyncRendering'; import useProfileState from './hooks/useProfileState'; import useProfileViewportIds from './hooks/useProfileViewportIds'; @@ -112,7 +115,7 @@ import InfiniteScroll from '../ui/InfiniteScroll'; import Link from '../ui/Link'; import ListItem, { type MenuItemContextAction } from '../ui/ListItem'; import Spinner from '../ui/Spinner'; -import TabList from '../ui/TabList'; +import TabList, { type TabWithProperties } from '../ui/TabList'; import Transition from '../ui/Transition'; import DeleteMemberModal from './DeleteMemberModal'; import StarGiftCollectionList from './gifts/StarGiftCollectionList'; @@ -179,24 +182,40 @@ type StateProps = { isSavedMessages?: boolean; isSynced?: boolean; hasAvatar?: boolean; + mainTab?: ApiProfileTab; + canUpdateMainTab?: boolean; + canAutoPlayGifs?: boolean; }; -type TabProps = { +type LocalTabProps = { type: ProfileTabType; key: RegularLangKey; }; -const TABS: TabProps[] = [ +type TabWithPropertiesAndType = TabWithProperties & { + type: ProfileTabType; +}; + +const TABS: LocalTabProps[] = [ { type: 'media', key: 'ProfileTabMedia' }, { type: 'documents', key: 'ProfileTabFiles' }, { type: 'links', key: 'ProfileTabLinks' }, { type: 'audio', key: 'ProfileTabMusic' }, + { type: 'gif', key: 'ProfileTabGifs' }, ]; const HIDDEN_RENDER_DELAY = 1000; const INTERSECTION_THROTTLE = 500; -const SHARED_MEDIA_TYPES = new Set(['media', 'documents', 'links', 'audio', 'voice']); +const VALID_CHANNEL_MAIN_TAB_TYPES = new Set>([ + 'stories', 'gifts', 'media', 'documents', 'audio', 'voice', 'links', 'gif', +]); +const VALID_USER_MAIN_TAB_TYPES = new Set>([ + 'stories', 'gifts', +]); +const SHARED_MEDIA_TYPES = new Set>([ + 'media', 'documents', 'links', 'audio', 'voice', 'gif', +]); const Profile = ({ chatId, @@ -252,6 +271,9 @@ const Profile = ({ isSavedMessages, isSynced, hasAvatar, + mainTab, + canUpdateMainTab, + canAutoPlayGifs, onProfileStateChange, }: OwnProps & StateProps) => { const { @@ -276,11 +298,14 @@ const Profile = ({ loadStoryAlbums, resetSelectedStoryAlbum, changeProfileTab, + setMainProfileTab, } = getActions(); const containerRef = useRef(); const transitionRef = useRef(); + const shouldSkipTransitionRef = useRef(false); + const oldLang = useOldLang(); const lang = useLang(); @@ -296,8 +321,11 @@ const Profile = ({ const [restoreContentHeightKey, setRestoreContentHeightKey] = useState(0); + const isUser = isUserId(chatId); + const validMainTabTypes = isUser ? VALID_USER_MAIN_TAB_TYPES : VALID_CHANNEL_MAIN_TAB_TYPES; + const tabs = useMemo(() => { - const arr: TabProps[] = []; + const arr: LocalTabProps[] = []; if (isGeneralSavedMessages) { arr.push({ type: 'dialogs', key: 'ProfileTabSavedDialogs' }); } @@ -306,16 +334,16 @@ const Profile = ({ arr.push({ type: 'stories', key: 'ProfileTabStories' }); } - if (hasStoriesTab && isOwnProfile) { - arr.push({ type: 'storiesArchive', key: 'ProfileTabStoriesArchive' }); - } - if (hasGiftsTab) { arr.push({ type: 'gifts', key: 'ProfileTabGifts' }); } + if (hasStoriesTab && isOwnProfile) { + arr.push({ type: 'storiesArchive', key: 'ProfileTabStoriesArchive' }); + } + if (hasMembersTab && !isOwnProfile) { - arr.push({ type: 'members', key: isChannel ? 'ProfileTabSubscribers' : 'ProfileTabMembers' }); + arr.push({ type: 'members', key: 'ProfileTabMembers' }); } if (hasPreviewMediaTab && !isOwnProfile) { @@ -349,13 +377,35 @@ const Profile = ({ arr.push(TABS[0]); } - return arr.map((tab) => ({ - type: tab.type, - title: lang(tab.key), - })); + if (mainTab) { + const mainTabIndex = arr.findIndex((tab) => tab.type === mainTab); + if (mainTabIndex !== -1) { + const newFirstTab = arr[mainTabIndex]; + arr.splice(mainTabIndex, 1); + arr.unshift(newFirstTab); + } + } + + return arr.map((tab) => { + const contextActions: MenuItemContextAction[] | undefined = canUpdateMainTab && mainTab !== tab.type + && validMainTabTypes.has(tab.type) ? [{ + title: lang('ProfileMenuSetMainTab'), + icon: 'replace', + handler: () => { + setMainProfileTab({ chatId, tab: tab.type as ApiProfileTab }); + }, + }] : undefined; + + return { + type: tab.type, + title: lang(tab.key), + contextActions, + } satisfies TabWithPropertiesAndType; + }); }, [ isGeneralSavedMessages, hasStoriesTab, hasGiftsTab, hasMembersTab, hasPreviewMediaTab, isTopicInfo, hasCommonChatsTab, isChannel, isBot, similarChannels?.length, similarBots?.length, lang, isOwnProfile, + mainTab, chatId, canUpdateMainTab, validMainTabTypes, ]); const [allowAutoScrollToTabs, startAutoScrollToTabsIfNeeded, stopAutoScrollToTabs] = useFlag(false); @@ -377,6 +427,11 @@ const Profile = ({ setActiveTab(tabs[0].type); // Set default tab }, [isClosed, profileTab, tabs]); + useEffectWithPrevDeps(([prevMainTab]) => { + if (prevMainTab || !mainTab) return; + setActiveTab(mainTab); // Only focus when loading full info + }, [mainTab]); + const handleSwitchTab = useCallback((index: number) => { startAutoScrollToTabsIfNeeded(); setActiveTab(tabs[index].type); @@ -430,6 +485,17 @@ const Profile = ({ return index === -1 ? 0 : index; }, [profileTab, tabs]); + // Reset skip transition flag from previous render + if (shouldSkipTransitionRef.current) { + shouldSkipTransitionRef.current = false; + } + + useSyncEffectWithPrevDeps(([prevProfileTab, prevActiveTabIndex]) => { + if (prevProfileTab === profileTab && prevActiveTabIndex !== activeTabIndex) { + shouldSkipTransitionRef.current = true; + } + }, [profileTab, activeTabIndex]); + const tabType = tabs[activeTabIndex].type; const handleLoadCommonChats = useCallback(() => { loadCommonChats({ userId: chatId }); @@ -498,10 +564,7 @@ const Profile = ({ const shouldRenderProfileInfo = !noProfileInfo && !isSavedMessages; - const isFirstTab = (isGeneralSavedMessages && resultType === 'dialogs') - || (hasStoriesTab && resultType === 'stories') - || resultType === 'members' - || (!hasMembersTab && resultType === 'media'); + const isFirstTab = tabs[0].type === resultType; const activeKey = tabs.findIndex(({ type }) => type === resultType); const [isGiftCollectionsShowed, markGiftCollectionsShowed, unmarkGiftCollectionsShowed] = useFlag(false); @@ -561,7 +624,7 @@ const Profile = ({ handleStopAutoScrollToTabs, }); - const { applyTransitionFix, releaseTransitionFix } = useTransitionFixes(containerRef); + useTransitionFixes(containerRef); const [cacheBuster, resetCacheBuster] = useCacheBuster(); @@ -570,11 +633,6 @@ const Profile = ({ throttleMs: INTERSECTION_THROTTLE, }); - const handleTransitionStop = useLastCallback(() => { - releaseTransitionFix(); - resetCacheBuster(); - }); - const handleNewMemberDialogOpen = useLastCallback(() => { setNewChatMembersDialogState({ newChatMembersProgress: NewChatMembersProgress.InProgress }); }); @@ -613,16 +671,6 @@ const Profile = ({ setDeletingUserId(undefined); }); - useEffectWithPrevDeps(([prevHasMemberTabs]) => { - if (prevHasMemberTabs === undefined || activeTabIndex === 0 || prevHasMemberTabs === hasMembersTab) { - return; - } - - const newActiveTab = Math.min(activeTabIndex + (hasMembersTab ? 1 : -1), tabs.length - 1); - - setActiveTab(tabs[newActiveTab].type); - }, [hasMembersTab, activeTabIndex, tabs]); - const handleResetGiftsFilter = useLastCallback(() => { resetGiftProfileFilter({ peerId: chatId }); }); @@ -800,6 +848,9 @@ const Profile = ({ case 'storiesArchive': text = oldLang('StoryList.ArchivedEmptyState.Title'); break; + case 'gif': + text = oldLang('lng_media_gif_empty'); + break; default: text = oldLang('SharedMedia.EmptyTitle'); } @@ -825,15 +876,16 @@ const Profile = ({ shouldShowContentPanel && 'showContentPanel', noTransition && 'noTransition', )} - dir={oldLang.isRtl && resultType === 'media' ? 'rtl' : undefined} + dir={lang.isRtl && (resultType === 'media' || resultType === 'gif') ? 'rtl' : undefined} teactFastList > - {resultType === 'media' ? ( + {resultType === 'media' || resultType === 'gif' ? ( (viewportIds as number[]).map((id) => messagesById[id] && ( @@ -988,17 +1040,10 @@ const Profile = ({ onClick={() => openChat({ id: userId })} > - {isUserId(userId) ? ( - - ) : ( - - )} + ))} {!isCurrentUserPremium && ( @@ -1081,7 +1126,7 @@ const Profile = ({ .Transition_slide-active > .Transition > .Transition_slide-active > .content' @@ -1218,8 +1263,7 @@ export default memo(withGlobal( const isGroup = chat && isChatGroup(chat); const isChannel = chat && isChatChannel(chat); const isBot = user && isUserBot(user); - const hasMembersTab = !isTopicInfo && !isSavedDialog - && (isGroup || (isChannel && isChatAdmin(chat))) && !chat?.isMonoforum; + const hasMembersTab = !isTopicInfo && !isSavedDialog && isGroup && !chat?.isMonoforum; const members = chatFullInfo?.members; const adminMembersById = chatFullInfo?.adminMembersById; const areMembersHidden = hasMembersTab && chat @@ -1265,6 +1309,8 @@ export default memo(withGlobal( const isRestricted = chat && selectIsChatRestricted(global, chat.id); const hasAvatar = Boolean(peer?.avatarPhotoId); + const canAutoPlayGifs = selectPerformanceSettingsValue(global, 'autoplayGifs'); + return { theme: selectTheme(global), isChannel, @@ -1315,6 +1361,9 @@ export default memo(withGlobal( commonChatIds: commonChats?.ids, monoforumChannel, hasAvatar, + mainTab: peerFullInfo?.mainTab, + canUpdateMainTab: selectCanUpdateMainTab(global, chatId), + canAutoPlayGifs, }; }, )(Profile)); diff --git a/src/components/right/hooks/useProfileViewportIds.ts b/src/components/right/hooks/useProfileViewportIds.ts index f2dd55093..92239b8ef 100644 --- a/src/components/right/hooks/useProfileViewportIds.ts +++ b/src/components/right/hooks/useProfileViewportIds.ts @@ -89,6 +89,10 @@ export default function useProfileViewportIds({ 'media', resultType, searchMessages, chatMessages, foundIds, threadId, ); + const [gifViewportIds, getMoreGifs, noProfileInfoForGifs] = useInfiniteScrollForSharedMedia( + 'gif', resultType, searchMessages, chatMessages, foundIds, threadId, + ); + const [documentViewportIds, getMoreDocuments, noProfileInfoForDocuments] = useInfiniteScrollForSharedMedia( 'documents', resultType, searchMessages, chatMessages, foundIds, threadId, ); @@ -153,6 +157,11 @@ export default function useProfileViewportIds({ getMore = getMoreMedia; noProfileInfo = noProfileInfoForMedia; break; + case 'gif': + viewportIds = gifViewportIds; + getMore = getMoreGifs; + noProfileInfo = noProfileInfoForGifs; + break; case 'documents': viewportIds = documentViewportIds; getMore = getMoreDocuments; diff --git a/src/components/right/hooks/useTransitionFixes.ts b/src/components/right/hooks/useTransitionFixes.ts index b514001e4..7748265ff 100644 --- a/src/components/right/hooks/useTransitionFixes.ts +++ b/src/components/right/hooks/useTransitionFixes.ts @@ -1,9 +1,7 @@ import type { ElementRef } from '../../../lib/teact/teact'; import { useEffect } from '../../../lib/teact/teact'; -import { requestMeasure, requestMutation } from '../../../lib/fasterdom/fasterdom'; - -import useLastCallback from '../../../hooks/useLastCallback'; +import { requestMutation } from '../../../lib/fasterdom/fasterdom'; export default function useTransitionFixes( containerRef: ElementRef, @@ -32,28 +30,4 @@ export default function useTransitionFixes( window.removeEventListener('resize', setMinHeight, false); }; }, [containerRef, transitionElSelector]); - - // Workaround for scrollable content flickering during animation. - const applyTransitionFix = useLastCallback(() => { - // This callback is called from `Transition.onStart` which is "mutate" phase - requestMeasure(() => { - const container = containerRef.current!; - if (container.style.overflowY === 'hidden') return; - - const scrollBarWidth = container.offsetWidth - container.clientWidth; - - requestMutation(() => { - container.style.overflowY = 'hidden'; - container.style.paddingRight = `${scrollBarWidth}px`; - }); - }); - }); - - const releaseTransitionFix = useLastCallback(() => { - const container = containerRef.current!; - container.style.overflowY = 'scroll'; - container.style.paddingRight = '0'; - }); - - return { applyTransitionFix, releaseTransitionFix }; } diff --git a/src/components/ui/Tab.tsx b/src/components/ui/Tab.tsx index 778b78995..0079f2252 100644 --- a/src/components/ui/Tab.tsx +++ b/src/components/ui/Tab.tsx @@ -1,5 +1,4 @@ -import type { FC, TeactNode } from '../../lib/teact/teact'; -import type React from '../../lib/teact/teact'; +import type { TeactNode } from '../../lib/teact/teact'; import { useEffect, useLayoutEffect, useRef } from '../../lib/teact/teact'; import type { MenuItemContextAction } from './ListItem'; @@ -40,7 +39,7 @@ const classNames = { badgeActive: 'Tab__badge--active', }; -const Tab: FC = ({ +const Tab = ({ className, title, isActive, @@ -48,11 +47,11 @@ const Tab: FC = ({ badgeCount, isBadgeActive, previousActiveTab, - onClick, - clickArg, contextActions, contextRootElementSelector, -}) => { + clickArg, + onClick, +}: OwnProps) => { const tabRef = useRef(); useLayoutEffect(() => { diff --git a/src/components/ui/TabList.tsx b/src/components/ui/TabList.tsx index b44e91698..8dc939ab2 100644 --- a/src/components/ui/TabList.tsx +++ b/src/components/ui/TabList.tsx @@ -1,4 +1,4 @@ -import type { FC, TeactNode } from '../../lib/teact/teact'; +import type { TeactNode } from '../../lib/teact/teact'; import { memo, useEffect, useRef } from '../../lib/teact/teact'; import type { MenuItemContextAction } from './ListItem'; @@ -37,10 +37,14 @@ const TAB_SCROLL_THRESHOLD_PX = 16; // Should match duration from `--slide-transition` CSS variable const SCROLL_DURATION = IS_IOS ? 450 : IS_ANDROID ? 400 : 300; -const TabList: FC = ({ - tabs, activeTab, onSwitchTab, - contextRootElementSelector, className, tabClassName, -}) => { +const TabList = ({ + tabs, + activeTab, + className, + tabClassName, + contextRootElementSelector, + onSwitchTab, +}: OwnProps) => { const containerRef = useRef(); const previousActiveTab = usePreviousDeprecated(activeTab); diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index ab54a6991..345191d15 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -118,6 +118,7 @@ import { selectIsCurrentUserFrozen, selectLastServiceNotification, selectPeer, + selectPeerFullInfo, selectSimilarChannelIds, selectStickerSet, selectSupportChat, @@ -3061,6 +3062,32 @@ addActionHandler('toggleAutoTranslation', async (global, actions, payload): Prom setGlobal(global); }); +addActionHandler('setMainProfileTab', async (global, actions, payload): Promise => { + const { chatId, tab } = payload; + const chat = selectChat(global, chatId); + if (!chat) return; + + const peerFullInfo = selectPeerFullInfo(global, chatId); + const oldMainTab = peerFullInfo?.mainTab; + if (oldMainTab === tab) return; + + global = updatePeerFullInfo(global, chatId, { mainTab: tab }); + setGlobal(global); + + let result; + if (chatId === global.currentUserId) { + result = await callApi('setAccountMainProfileTab', { tab }); + } else { + result = await callApi('setChannelMainProfileTab', { chat, tab }); + } + + if (!result) { + global = getGlobal(); + global = updatePeerFullInfo(global, chatId, { mainTab: oldMainTab }); + setGlobal(global); + } +}); + addActionHandler('resolveBusinessChatLink', async (global, actions, payload): Promise => { const { slug, tabId = getCurrentTabId() } = payload; const result = await callApi('resolveBusinessChatLink', { slug }); diff --git a/src/global/actions/api/middleSearch.ts b/src/global/actions/api/middleSearch.ts index 470ecc4f5..144e875c5 100644 --- a/src/global/actions/api/middleSearch.ts +++ b/src/global/actions/api/middleSearch.ts @@ -12,7 +12,7 @@ import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { buildCollectionByKey, isInsideSortedArrayRange } from '../../../util/iteratees'; import { getSearchResultKey } from '../../../util/keys/searchResultKey'; import { callApi } from '../../../api/gramjs'; -import { getChatMediaMessageIds, getIsSavedDialog, isSameReaction } from '../../helpers'; +import { getIsSavedDialog, getMessageContentIds, isSameReaction } from '../../helpers'; import { addActionHandler, getGlobal, setGlobal, } from '../../index'; @@ -479,7 +479,7 @@ async function searchChatMedia( const loadingState = calcLoadingState(direction, limit, newFoundIds.length, currentSegment); - const filteredIds = getChatMediaMessageIds(byId, newFoundIds, false); + const filteredIds = getMessageContentIds(byId, newFoundIds, 'inlineMedia'); currentSegment = mergeWithChatMediaSearchSegment( filteredIds, loadingState, diff --git a/src/global/helpers/messageMedia.ts b/src/global/helpers/messageMedia.ts index 1b80d72ba..c73ae45cb 100644 --- a/src/global/helpers/messageMedia.ts +++ b/src/global/helpers/messageMedia.ts @@ -429,12 +429,6 @@ export function hasMediaLocalBlobUrl(media: ApiPhoto | ApiVideo | ApiDocument) { return false; } -export function getChatMediaMessageIds( - messages: Record, listedIds: number[], isFromSharedMedia = false, -) { - return getMessageContentIds(messages, listedIds, isFromSharedMedia ? 'media' : 'inlineMedia'); -} - export function getPhotoFullDimensions(photo: Pick): ApiDimensions | undefined { return ( photo.sizes.find((size) => size.type === 'w') @@ -488,6 +482,13 @@ export function getMessageContentIds( validator = getMessageDocument; break; + case 'gif': + validator = (message: ApiMessage) => { + const video = getMessageVideo(message); + return video?.isGif; + }; + break; + case 'links': validator = (message: ApiMessage) => getMessageWebPage(message) || matchLinkInMessageText(message); break; diff --git a/src/global/selectors/peers.ts b/src/global/selectors/peers.ts index 7d3dad997..13edd8746 100644 --- a/src/global/selectors/peers.ts +++ b/src/global/selectors/peers.ts @@ -4,7 +4,7 @@ import type { GlobalState, TabArgs } from '../types'; import { SERVICE_NOTIFICATIONS_USER_ID } from '../../config'; import { isUserId } from '../../util/entities/ids'; import { getCurrentTabId } from '../../util/establishMultitabRole'; -import { isChatAdmin, isDeletedUser } from '../helpers'; +import { getHasAdminRight, isChatAdmin, isChatChannel, isDeletedUser } from '../helpers'; import { selectChat, selectChatFullInfo } from './chats'; import { type ProfileCollectionKey } from './payments'; import { selectTabState } from './tabs'; @@ -74,3 +74,12 @@ export function selectPeerHasProfileBackground(global: T, const peer = selectPeer(global, peerId); return Boolean(peer?.profileColor || peer?.emojiStatus?.type === 'collectible'); } + +export function selectCanUpdateMainTab(global: T, peerId: string) { + if (global.currentUserId === peerId) { + return true; + } + + const chat = selectChat(global, peerId); + return Boolean(chat && isChatChannel(chat) && getHasAdminRight(chat, 'postMessages')); +} diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index 307a8847b..2339ab50d 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -37,6 +37,7 @@ import type { ApiPreparedInlineMessage, ApiPrivacyKey, ApiPrivacySettings, + ApiProfileTab, ApiReaction, ApiReactionWithPaid, ApiReportReason, @@ -1142,6 +1143,10 @@ export interface ActionPayloads { chatId: string; isEnabled: boolean; } & WithTabId; + setMainProfileTab: { + chatId: string; + tab: ApiProfileTab; + }; updateChat: { chatId: string; diff --git a/src/global/types/tabState.ts b/src/global/types/tabState.ts index a438bbdef..a8861b2ea 100644 --- a/src/global/types/tabState.ts +++ b/src/global/types/tabState.ts @@ -10,6 +10,7 @@ import type { ApiCheckedGiftCode, ApiCollectibleInfo, ApiContact, + ApiEmojiStatusCollectible, ApiError, ApiFormattedText, ApiGeoPoint, @@ -54,7 +55,6 @@ import type { ApiUser, ApiVideo, } from '../../api/types'; -import type { ApiEmojiStatusCollectible } from '../../api/types/users'; import type { FoldersActions } from '../../hooks/reducers/useFoldersReducer'; import type { ReducerAction } from '../../hooks/useReducer'; import type { diff --git a/src/hooks/animations/useViewTransition.ts b/src/hooks/animations/useViewTransition.ts index 5e02ca6ab..9b9cfd8cc 100644 --- a/src/hooks/animations/useViewTransition.ts +++ b/src/hooks/animations/useViewTransition.ts @@ -9,7 +9,7 @@ import { getGlobal } from '../../global'; import type { AnimationLevel } from '../../types'; import type { VTTypes } from '../../util/animations/viewTransitionTypes'; -import { VT_CLASS_NAME, VT_TYPE_CLASS_PREFIX } from '../../config'; +import { ANIMATION_LEVEL_MED, VT_CLASS_NAME, VT_TYPE_CLASS_PREFIX } from '../../config'; import { requestMutation, requestNextMutation } from '../../lib/fasterdom/fasterdom'; import { selectSharedSettings } from '../../global/selectors/sharedState'; import { IS_VIEW_TRANSITION_SUPPORTED } from '../../util/browser/windowEnvironment'; @@ -98,7 +98,7 @@ export function useViewTransition(): ViewTransitionController { function startViewTransition( types: VTTypes, updateCallback?: TransitionFunction, - minimumAnimationLevel: AnimationLevel = 1, + minimumAnimationLevel: AnimationLevel = ANIMATION_LEVEL_MED, ): PromiseLike | void { const global = getGlobal(); const { animationLevel } = selectSharedSettings(global); diff --git a/src/lib/gramjs/tl/apiTl.ts b/src/lib/gramjs/tl/apiTl.ts index 1b98bb515..0b8eeea30 100644 --- a/src/lib/gramjs/tl/apiTl.ts +++ b/src/lib/gramjs/tl/apiTl.ts @@ -1537,6 +1537,7 @@ account.toggleSponsoredMessages#b9d9a38d enabled:Bool = Bool; account.getCollectibleEmojiStatuses#2e7b4543 hash:long = account.EmojiStatuses; account.getPaidMessagesRevenue#19ba4a67 flags:# parent_peer:flags.0?InputPeer user_id:InputUser = account.PaidMessagesRevenue; account.toggleNoPaidMessagesException#fe2eda76 flags:# refund_charged:flags.0?true require_payment:flags.2?true parent_peer:flags.1?InputPeer user_id:InputUser = Bool; +account.setMainProfileTab#5dee78b0 tab:ProfileTab = Bool; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#b60f5918 id:InputUser = users.UserFull; contacts.getContacts#5dd69e12 hash:long = contacts.Contacts; @@ -1770,6 +1771,7 @@ channels.searchPosts#f2c4f24d flags:# hashtag:flags.0?string query:flags.1?strin channels.updatePaidMessagesPrice#4b12327b flags:# broadcast_messages_allowed:flags.0?true channel:InputChannel send_paid_messages_stars:long = Updates; channels.toggleAutotranslation#167fc0a1 channel:InputChannel enabled:Bool = Updates; channels.checkSearchPostsFlood#22567115 flags:# query:flags.0?string = SearchPostsFlood; +channels.setMainProfileTab#3583fcb1 channel:InputChannel tab:ProfileTab = Bool; bots.setBotInfo#10cf3123 flags:# bot:flags.2?InputUser lang_code:string name:flags.3?string about:flags.0?string description:flags.1?string = Bool; bots.canSendMessage#1359f4e6 bot:InputUser = Bool; bots.allowSendMessage#f132e3ef bot:InputUser = Updates; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index 5e1949382..f9e2d8517 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -66,6 +66,7 @@ "account.getPaidMessagesRevenue", "account.getAccountTTL", "account.setAccountTTL", + "account.setMainProfileTab", "users.getUsers", "users.getFullUser", "contacts.getContacts", @@ -288,6 +289,7 @@ "channels.updatePaidMessagesPrice", "channels.checkSearchPostsFlood", "channels.toggleAutotranslation", + "channels.setMainProfileTab", "bots.getBotRecommendations", "bots.canSendMessage", "bots.allowSendMessage", diff --git a/src/styles/icons.woff b/src/styles/icons.woff index e73721657..6a1786371 100644 Binary files a/src/styles/icons.woff and b/src/styles/icons.woff differ diff --git a/src/styles/icons.woff2 b/src/styles/icons.woff2 index db4c6369d..b54b25fbb 100644 Binary files a/src/styles/icons.woff2 and b/src/styles/icons.woff2 differ diff --git a/src/types/index.ts b/src/types/index.ts index d574cb237..61daad7b1 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -394,13 +394,14 @@ export type ProfileTabType = | 'links' | 'audio' | 'voice' + | 'gif' | 'stories' | 'storiesArchive' | 'similarChannels' | 'similarBots' | 'dialogs' | 'gifts'; -export type SharedMediaType = 'media' | 'documents' | 'links' | 'audio' | 'voice'; +export type SharedMediaType = 'media' | 'documents' | 'links' | 'audio' | 'voice' | 'gif'; export type MiddleSearchType = 'chat' | 'myChats' | 'channels'; export type MiddleSearchParams = { requestedQuery?: string; diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 9341ad68e..bf31fb57c 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -1222,6 +1222,8 @@ export interface LangPair { 'ProfileBotOpenAppInfoLink': undefined; 'ProfileBirthday': undefined; 'ProfileBirthdayToday': undefined; + 'ProfileMenuSetMainTab': undefined; + 'ProfileItemSubscribers': undefined; 'MonetizationInfoTONTitle': undefined; 'AriaSearchOlderResult': undefined; 'AriaSearchNewerResult': undefined; @@ -1395,6 +1397,7 @@ export interface LangPair { 'ProfileTabLinks': undefined; 'ProfileTabMusic': undefined; 'ProfileTabVoice': undefined; + 'ProfileTabGifs': undefined; 'ProfileTabSharedGroups': undefined; 'ProfileTabSimilarChannels': undefined; 'ProfileTabSimilarBots': undefined;