diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index a779921f4..27f7dcaa4 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -19,13 +19,17 @@ import type { ApiTopic, } from '../../types'; -import { pick, pickTruthy } from '../../../util/iteratees'; +import { omitUndefined, pick, pickTruthy } from '../../../util/iteratees'; import { getServerTime, getServerTimeOffset } from '../../../util/serverTime'; import { buildApiUsernames } from './common'; import { omitVirtualClassFields } from './helpers'; import { + buildApiEmojiStatus, buildApiPeerColor, - buildApiPeerId, getApiChatIdFromMtpPeer, isPeerChat, isPeerUser, + buildApiPeerId, + getApiChatIdFromMtpPeer, + isPeerChat, + isPeerUser, } from './peers'; import { buildApiReaction } from './reactions'; @@ -40,10 +44,10 @@ function buildApiChatFieldsFromPeerEntity( isSupport = false, ): PeerEntityApiChatFields { const isMin = Boolean('min' in peerEntity && peerEntity.min); - const accessHash = ('accessHash' in peerEntity) && String(peerEntity.accessHash); + const accessHash = ('accessHash' in peerEntity) ? String(peerEntity.accessHash) : undefined; const hasVideoAvatar = 'photo' in peerEntity && peerEntity.photo && 'hasVideo' in peerEntity.photo && peerEntity.photo.hasVideo; - const avatarHash = ('photo' in peerEntity) && peerEntity.photo && buildAvatarHash(peerEntity.photo); + const avatarHash = ('photo' in peerEntity) && peerEntity.photo ? buildAvatarHash(peerEntity.photo) : undefined; const isSignaturesShown = Boolean('signatures' in peerEntity && peerEntity.signatures); const hasPrivateLink = Boolean('hasLink' in peerEntity && peerEntity.hasLink); const isScam = Boolean('scam' in peerEntity && peerEntity.scam); @@ -56,15 +60,17 @@ function buildApiChatFieldsFromPeerEntity( const maxStoryId = 'storiesMaxId' in peerEntity ? peerEntity.storiesMaxId : undefined; const storiesUnavailable = Boolean('storiesUnavailable' in peerEntity && peerEntity.storiesUnavailable); const color = ('color' in peerEntity && peerEntity.color) ? buildApiPeerColor(peerEntity.color) : undefined; + const emojiStatus = ('emojiStatus' in peerEntity && peerEntity.emojiStatus) + ? buildApiEmojiStatus(peerEntity.emojiStatus) : undefined; - return { + return omitUndefined({ isMin, hasPrivateLink, isSignaturesShown, usernames, - ...(accessHash && { accessHash }), + accessHash, hasVideoAvatar, - ...(avatarHash && { avatarHash }), + avatarHash, ...('verified' in peerEntity && { isVerified: peerEntity.verified }), ...('callActive' in peerEntity && { isCallActive: peerEntity.callActive }), ...('callNotEmpty' in peerEntity && { isCallNotEmpty: peerEntity.callNotEmpty }), @@ -73,7 +79,7 @@ function buildApiChatFieldsFromPeerEntity( membersCount: peerEntity.participantsCount, }), ...('noforwards' in peerEntity && { isProtected: Boolean(peerEntity.noforwards) }), - ...(isSupport && { isSupport: true }), + isSupport: isSupport || undefined, ...buildApiChatPermissions(peerEntity), ...('creator' in peerEntity && { isCreator: peerEntity.creator }), ...buildApiChatRestrictions(peerEntity), @@ -86,7 +92,8 @@ function buildApiChatFieldsFromPeerEntity( areStoriesHidden, maxStoryId, hasStories: Boolean(maxStoryId) && !storiesUnavailable, - }; + emojiStatus, + }); } export function buildApiChatFromDialog( diff --git a/src/api/gramjs/apiBuilders/messageContent.ts b/src/api/gramjs/apiBuilders/messageContent.ts index 2e43ca914..1aead322e 100644 --- a/src/api/gramjs/apiBuilders/messageContent.ts +++ b/src/api/gramjs/apiBuilders/messageContent.ts @@ -7,6 +7,7 @@ import type { ApiFormattedText, ApiGame, ApiGiveaway, + ApiGiveawayResults, ApiInvoice, ApiLocation, ApiMessageExtendedMediaPreview, @@ -122,6 +123,9 @@ export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): MediaC const giveaway = buildGiweawayFromMedia(media); if (giveaway) return { giveaway }; + const giveawayResults = buildGiweawayResultsFromMedia(media); + if (giveawayResults) return { giveawayResults }; + return undefined; } @@ -480,7 +484,7 @@ function buildGiweawayFromMedia(media: GramJs.TypeMessageMedia): ApiGiveaway | u function buildGiveaway(media: GramJs.MessageMediaGiveaway): ApiGiveaway | undefined { const { - channels, months, quantity, untilDate, countriesIso2, onlyNewSubscribers, + channels, months, quantity, untilDate, countriesIso2, onlyNewSubscribers, prizeDescription, } = media; const channelIds = channels.map((channel) => buildApiPeerId(channel, 'channel')); @@ -492,6 +496,38 @@ function buildGiveaway(media: GramJs.MessageMediaGiveaway): ApiGiveaway | undefi untilDate, countries: countriesIso2, isOnlyForNewSubscribers: onlyNewSubscribers, + prizeDescription, + }; +} + +function buildGiweawayResultsFromMedia(media: GramJs.TypeMessageMedia): ApiGiveawayResults | undefined { + if (!(media instanceof GramJs.MessageMediaGiveawayResults)) { + return undefined; + } + + return buildGiveawayResults(media); +} + +function buildGiveawayResults(media: GramJs.MessageMediaGiveawayResults): ApiGiveawayResults | undefined { + const { + months, untilDate, onlyNewSubscribers, launchMsgId, unclaimedCount, winners, winnersCount, + additionalPeersCount, prizeDescription, refunded, channelId, + } = media; + + const winnerIds = winners.map((winner) => buildApiPeerId(winner, 'user')); + + return { + months, + untilDate, + isOnlyForNewSubscribers: onlyNewSubscribers, + launchMessageId: launchMsgId, + channelId: buildApiPeerId(channelId, 'channel'), + unclaimedCount, + additionalPeersCount, + isRefunded: refunded, + prizeDescription, + winnerIds, + winnersCount, }; } diff --git a/src/api/gramjs/apiBuilders/payments.ts b/src/api/gramjs/apiBuilders/payments.ts index 7fa293b74..f279a3975 100644 --- a/src/api/gramjs/apiBuilders/payments.ts +++ b/src/api/gramjs/apiBuilders/payments.ts @@ -248,7 +248,7 @@ export function buildApiGiveawayInfo(info: GramJs.payments.TypeGiveawayInfo): Ap type: 'active', startDate, isParticipating: participating, - adminDisallowedChatId: adminDisallowedChatId?.toString(), + adminDisallowedChatId: adminDisallowedChatId && buildApiPeerId(adminDisallowedChatId, 'channel'), disallowedCountry, joinedTooEarlyDate, isPreparingResults: preparingResults, diff --git a/src/api/gramjs/apiBuilders/peers.ts b/src/api/gramjs/apiBuilders/peers.ts index 0d1b1d1ec..13e18a8a9 100644 --- a/src/api/gramjs/apiBuilders/peers.ts +++ b/src/api/gramjs/apiBuilders/peers.ts @@ -1,7 +1,7 @@ import type BigInt from 'big-integer'; +import { Api as GramJs } from '../../../lib/gramjs'; -import type { Api as GramJs } from '../../../lib/gramjs'; -import type { ApiPeerColor } from '../../types'; +import type { ApiEmojiStatus, ApiPeerColor } from '../../types'; export function isPeerUser(peer: GramJs.TypePeer | GramJs.TypeInputPeer): peer is GramJs.PeerUser { return peer.hasOwnProperty('userId'); @@ -44,3 +44,15 @@ export function buildApiPeerColor(peerColor: GramJs.TypePeerColor): ApiPeerColor backgroundEmojiId: backgroundEmojiId?.toString(), }; } + +export function buildApiEmojiStatus(mtpEmojiStatus: GramJs.TypeEmojiStatus): ApiEmojiStatus | undefined { + if (mtpEmojiStatus instanceof GramJs.EmojiStatus) { + return { documentId: mtpEmojiStatus.documentId.toString() }; + } + + if (mtpEmojiStatus instanceof GramJs.EmojiStatusUntil) { + return { documentId: mtpEmojiStatus.documentId.toString(), until: mtpEmojiStatus.until }; + } + + return undefined; +} diff --git a/src/api/gramjs/apiBuilders/stories.ts b/src/api/gramjs/apiBuilders/stories.ts index 03e61a29f..82867cd42 100644 --- a/src/api/gramjs/apiBuilders/stories.ts +++ b/src/api/gramjs/apiBuilders/stories.ts @@ -5,14 +5,17 @@ import type { ApiMediaAreaCoordinates, ApiStealthMode, ApiStoryForwardInfo, - ApiStoryView, ApiStoryViews, + ApiStoryView, + ApiStoryViews, ApiTypeStory, + ApiTypeStoryView, MediaContent, } from '../../types'; -import { buildCollectionByCallback } from '../../../util/iteratees'; +import { buildCollectionByCallback, omitUndefined } from '../../../util/iteratees'; import { buildPrivacyRules } from './common'; import { buildGeoPoint, buildMessageMediaContent, buildMessageTextContent } from './messageContent'; +import { buildApiMessage } from './messages'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; import { buildApiReaction, buildReactionCount } from './reactions'; @@ -88,17 +91,53 @@ function buildApiStoryViews(views: GramJs.TypeStoryViews): ApiStoryViews | undef }; } -export function buildApiStoryView(view: GramJs.TypeStoryView): ApiStoryView { +export function buildApiStoryView(view: GramJs.TypeStoryView): ApiTypeStoryView | undefined { const { - userId, date, reaction, blockedMyStoriesFrom, blocked, + blockedMyStoriesFrom, blocked, } = view; - return { - userId: userId.toString(), - date, - ...(reaction && { reaction: buildApiReaction(reaction) }), - areStoriesBlocked: blocked || blockedMyStoriesFrom, - isUserBlocked: blocked, - }; + + if (view instanceof GramJs.StoryView) { + return omitUndefined({ + type: 'user', + peerId: buildApiPeerId(view.userId, 'user'), + date: view.date, + reaction: view.reaction && buildApiReaction(view.reaction), + areStoriesBlocked: blocked || blockedMyStoriesFrom, + isUserBlocked: blocked, + }); + } + + if (view instanceof GramJs.StoryViewPublicForward) { + const message = buildApiMessage(view.message); + if (!message) return undefined; + return { + type: 'forward', + peerId: message.chatId, + messageId: message.id, + message, + date: message.date, + areStoriesBlocked: blocked || blockedMyStoriesFrom, + isUserBlocked: blocked, + }; + } + + if (view instanceof GramJs.StoryViewPublicRepost) { + const peerId = getApiChatIdFromMtpPeer(view.peerId); + const story = buildApiStory(peerId, view.story); + if (!('content' in story)) return undefined; + + return { + type: 'repost', + peerId, + storyId: view.story.id, + date: story.date, + story, + areStoriesBlocked: blocked || blockedMyStoriesFrom, + isUserBlocked: blocked, + }; + } + + return undefined; } export function buildApiStealthMode(stealthMode: GramJs.TypeStoriesStealthMode): ApiStealthMode { @@ -167,6 +206,17 @@ export function buildApiMediaArea(area: GramJs.TypeMediaArea): ApiMediaArea | un }; } + if (area instanceof GramJs.MediaAreaChannelPost) { + const { coordinates, channelId, msgId } = area; + + return { + type: 'channelPost', + coordinates: buildApiMediaAreaCoordinates(coordinates), + channelId: buildApiPeerId(channelId, 'channel'), + messageId: msgId, + }; + } + return undefined; } @@ -177,11 +227,14 @@ export function buildApiPeerStories(peerStories: GramJs.PeerStories) { } export function buildApiStoryForwardInfo(forwardHeader: GramJs.TypeStoryFwdHeader): ApiStoryForwardInfo { - const { from, fromName, storyId } = forwardHeader; + const { + from, fromName, storyId, modified, + } = forwardHeader; return { storyId, fromPeerId: from && getApiChatIdFromMtpPeer(from), fromName, + isModified: modified, }; } diff --git a/src/api/gramjs/apiBuilders/users.ts b/src/api/gramjs/apiBuilders/users.ts index 8cd0980aa..db74a705e 100644 --- a/src/api/gramjs/apiBuilders/users.ts +++ b/src/api/gramjs/apiBuilders/users.ts @@ -1,7 +1,6 @@ import { Api as GramJs } from '../../../lib/gramjs'; import type { - ApiEmojiStatus, ApiPremiumGiftOption, ApiUser, ApiUserFullInfo, @@ -11,7 +10,7 @@ import type { import { buildApiBotInfo } from './bots'; import { buildApiPhoto, buildApiUsernames } from './common'; -import { buildApiPeerColor, buildApiPeerId } from './peers'; +import { buildApiEmojiStatus, buildApiPeerColor, buildApiPeerId } from './peers'; export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUserFullInfo { const { @@ -57,7 +56,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined { : undefined; const userType = buildApiUserType(mtpUser); const usernames = buildApiUsernames(mtpUser); - const emojiStatus = mtpUser.emojiStatus ? buildApiUserEmojiStatus(mtpUser.emojiStatus) : undefined; + const emojiStatus = mtpUser.emojiStatus ? buildApiEmojiStatus(mtpUser.emojiStatus) : undefined; return { id: buildApiPeerId(id, 'user'), @@ -116,18 +115,6 @@ export function buildApiUserStatus(mtpStatus?: GramJs.TypeUserStatus): ApiUserSt } } -export function buildApiUserEmojiStatus(mtpEmojiStatus: GramJs.TypeEmojiStatus): ApiEmojiStatus | undefined { - if (mtpEmojiStatus instanceof GramJs.EmojiStatus) { - return { documentId: mtpEmojiStatus.documentId.toString() }; - } - - if (mtpEmojiStatus instanceof GramJs.EmojiStatusUntil) { - return { documentId: mtpEmojiStatus.documentId.toString(), until: mtpEmojiStatus.until }; - } - - return undefined; -} - export function buildApiUsersAndStatuses(mtpUsers: GramJs.TypeUser[]) { const userStatusesById: Record = {}; const usersById: Record = {}; diff --git a/src/api/gramjs/methods/statistics.ts b/src/api/gramjs/methods/statistics.ts index c367659da..31384c6b7 100644 --- a/src/api/gramjs/methods/statistics.ts +++ b/src/api/gramjs/methods/statistics.ts @@ -87,22 +87,23 @@ export async function fetchMessagePublicForwards({ chat, messageId, dcId, - offsetRate = 0, + offset, }: { chat: ApiChat; messageId: number; dcId?: number; - offsetRate?: number; + offset?: string; }): Promise<{ forwards?: ApiMessagePublicForward[]; count?: number; - nextRate?: number; + nextOffset?: string; + chats: ApiChat[]; + users: ApiUser[]; } | undefined> { const result = await invokeRequest(new GramJs.stats.GetMessagePublicForwards({ channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel, msgId: messageId, - offsetPeer: new GramJs.InputPeerEmpty(), - offsetRate, + offset, limit: STATISTICS_PUBLIC_FORWARDS_LIMIT, }), { dcId, @@ -112,16 +113,15 @@ export async function fetchMessagePublicForwards({ return undefined; } - if ('chats' in result) { - addEntitiesToLocalDb(result.chats); - } + addEntitiesToLocalDb(result.chats); + addEntitiesToLocalDb(result.users); return { forwards: buildMessagePublicForwards(result), - ...('nextRate' in result ? { - count: result.count, - nextRate: result.nextRate, - } : undefined), + count: result.count, + nextOffset: result.nextOffset, + chats: result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean), + users: result.users.map(buildApiUser).filter(Boolean), }; } @@ -177,23 +177,23 @@ export async function fetchStoryPublicForwards({ chat, storyId, dcId, - offsetId = '0', + offset, }: { chat: ApiChat; storyId: number; dcId?: number; - offsetId?: string; + offset?: string; }): Promise<{ publicForwards: (ApiMessagePublicForward | ApiStoryPublicForward)[] | undefined; users: ApiUser[]; chats: ApiChat[]; count?: number; - nextOffsetId?: string; + nextOffset?: string; } | undefined> { const result = await invokeRequest(new GramJs.stats.GetStoryPublicForwards({ peer: buildInputPeer(chat.id, chat.accessHash), id: storyId, - offset: offsetId, + offset, limit: STATISTICS_PUBLIC_FORWARDS_LIMIT, }), { dcId, @@ -211,6 +211,6 @@ export async function fetchStoryPublicForwards({ users: result.users.map(buildApiUser).filter(Boolean), chats: result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean), count: result.count, - nextOffsetId: result.nextOffset, + nextOffset: result.nextOffset, }; } diff --git a/src/api/gramjs/methods/stories.ts b/src/api/gramjs/methods/stories.ts index 89f7e6023..36ad42abd 100644 --- a/src/api/gramjs/methods/stories.ts +++ b/src/api/gramjs/methods/stories.ts @@ -285,11 +285,14 @@ export async function fetchStoryViewList({ } addEntitiesToLocalDb(result.users); + addEntitiesToLocalDb(result.chats); const users = result.users.map(buildApiUser).filter(Boolean); - const views = result.views.map(buildApiStoryView); + const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); + const views = result.views.map(buildApiStoryView).filter(Boolean); return { users, + chats, views, nextOffset: result.nextOffset, reactionsCount: result.reactionsCount, diff --git a/src/api/gramjs/methods/symbols.ts b/src/api/gramjs/methods/symbols.ts index b050c4ffb..31192d6af 100644 --- a/src/api/gramjs/methods/symbols.ts +++ b/src/api/gramjs/methods/symbols.ts @@ -7,10 +7,10 @@ import type { import { DEFAULT_GIF_SEARCH_BOT_USERNAME, RECENT_STATUS_LIMIT, RECENT_STICKERS_LIMIT } from '../../../config'; import { buildVideoFromDocument } from '../apiBuilders/messageContent'; +import { buildApiEmojiStatus } from '../apiBuilders/peers'; import { buildStickerSet, buildStickerSetCovered, processStickerPackResult, processStickerResult, } from '../apiBuilders/symbols'; -import { buildApiUserEmojiStatus } from '../apiBuilders/users'; import { buildInputDocument, buildInputStickerSet, buildInputStickerSetShortName } from '../gramjsBuilders'; import localDb from '../localDb'; import { invokeRequest } from './client'; @@ -445,7 +445,7 @@ export async function fetchRecentEmojiStatuses(hash = '0') { const documentIds = result.statuses .slice(0, RECENT_STATUS_LIMIT) - .map(buildApiUserEmojiStatus) + .map(buildApiEmojiStatus) .filter(Boolean) .map(({ documentId }) => documentId); const emojiStatuses = await fetchCustomEmoji({ documentId: documentIds }); diff --git a/src/api/gramjs/updater.ts b/src/api/gramjs/updater.ts index 0d530849c..f558c6d86 100644 --- a/src/api/gramjs/updater.ts +++ b/src/api/gramjs/updater.ts @@ -46,7 +46,7 @@ import { buildApiNotifyExceptionTopic, buildPrivacyKey, } from './apiBuilders/misc'; -import { buildApiPeerId, getApiChatIdFromMtpPeer } from './apiBuilders/peers'; +import { buildApiEmojiStatus, buildApiPeerId, getApiChatIdFromMtpPeer } from './apiBuilders/peers'; import { buildApiReaction, buildMessageReactions, @@ -55,7 +55,6 @@ import { buildApiStealthMode, buildApiStory } from './apiBuilders/stories'; import { buildApiEmojiInteraction, buildStickerSet } from './apiBuilders/symbols'; import { buildApiUser, - buildApiUserEmojiStatus, buildApiUserStatus, } from './apiBuilders/users'; import { @@ -795,7 +794,7 @@ export function updater(update: Update) { id: buildApiPeerId(update.userId, 'user'), }); } else if (update instanceof GramJs.UpdateUserEmojiStatus) { - const emojiStatus = buildApiUserEmojiStatus(update.emojiStatus); + const emojiStatus = buildApiEmojiStatus(update.emojiStatus); onUpdate({ '@type': 'updateUserEmojiStatus', userId: buildApiPeerId(update.userId, 'user'), diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index 60d8d85a4..4dddfc705 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -3,7 +3,9 @@ import type { ApiChatReactions, ApiMessage, ApiPhoto, ApiStickerSet, } from './messages'; import type { ApiChatInviteImporter } from './misc'; -import type { ApiFakeType, ApiUser, ApiUsername } from './users'; +import type { + ApiEmojiStatus, ApiFakeType, ApiUser, ApiUsername, +} from './users'; type ApiChatType = ( 'chatTypePrivate' | 'chatTypeSecret' | @@ -43,6 +45,7 @@ export interface ApiChat { isProtected?: boolean; fakeType?: ApiFakeType; color?: ApiPeerColor; + emojiStatus?: ApiEmojiStatus; isForum?: boolean; isForumAsMessages?: true; topics?: Record; diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 9bf4b5684..4f7b87636 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -260,6 +260,21 @@ export type ApiGiveaway = { isOnlyForNewSubscribers?: true; countries?: string[]; channelIds: string[]; + prizeDescription?: string; +}; + +export type ApiGiveawayResults = { + months: number; + untilDate: number; + isRefunded?: true; + isOnlyForNewSubscribers?: true; + channelId: string; + prizeDescription?: string; + winnersCount?: number; + winnerIds: string[]; + additionalPeersCount?: number; + launchMessageId: number; + unclaimedCount: number; }; export type ApiNewPoll = { @@ -370,6 +385,7 @@ export interface ApiStoryForwardInfo { fromPeerId?: string; fromName?: string; storyId?: number; + isModified?: boolean; } export type ApiMessageEntityDefault = { @@ -458,6 +474,7 @@ export type MediaContent = { game?: ApiGame; storyData?: ApiMessageStoryData; giveaway?: ApiGiveaway; + giveawayResults?: ApiGiveawayResults; }; export interface ApiMessage { diff --git a/src/api/types/statistics.ts b/src/api/types/statistics.ts index b0c9cefd1..459e4da90 100644 --- a/src/api/types/statistics.ts +++ b/src/api/types/statistics.ts @@ -46,8 +46,7 @@ export interface ApiPostStatistics { publicForwards?: number; publicForwardsData?: (ApiMessagePublicForward | ApiStoryPublicForward)[]; - nextRate?: number; - nextOffsetId?: string; + nextOffset?: string; } export interface ApiBoostStatistics { diff --git a/src/api/types/stories.ts b/src/api/types/stories.ts index 13fd8fe92..760725be1 100644 --- a/src/api/types/stories.ts +++ b/src/api/types/stories.ts @@ -1,6 +1,6 @@ import type { ApiPrivacySettings } from '../../types'; import type { - ApiGeoPoint, ApiReaction, ApiReactionCount, ApiStoryForwardInfo, MediaContent, + ApiGeoPoint, ApiMessage, ApiReaction, ApiReactionCount, ApiStoryForwardInfo, MediaContent, } from './messages'; export interface ApiStory { @@ -71,14 +71,37 @@ export type ApiWebPageStoryData = { peerId: string; }; +export type ApiStoryViewPublicForward = { + type: 'forward'; + peerId: string; + messageId: number; + message: ApiMessage; + date: number; + isUserBlocked?: true; + areStoriesBlocked?: true; +}; + +export type ApiStoryViewPublicRepost = { + type: 'repost'; + isUserBlocked?: true; + areStoriesBlocked?: true; + date: number; + peerId: string; + storyId: number; + story: ApiStory; +}; + export type ApiStoryView = { - userId: string; + type: 'user'; + peerId: string; date: number; reaction?: ApiReaction; isUserBlocked?: true; areStoriesBlocked?: true; }; +export type ApiTypeStoryView = ApiStoryView | ApiStoryViewPublicForward | ApiStoryViewPublicRepost; + export type ApiStealthMode = { activeUntil?: number; cooldownUntil?: number; @@ -113,4 +136,12 @@ export type ApiMediaAreaSuggestedReaction = { isFlipped?: boolean; }; -export type ApiMediaArea = ApiMediaAreaVenue | ApiMediaAreaGeoPoint | ApiMediaAreaSuggestedReaction; +export type ApiMediaAreaChannelPost = { + type: 'channelPost'; + coordinates: ApiMediaAreaCoordinates; + channelId: string; + messageId: number; +}; + +export type ApiMediaArea = ApiMediaAreaVenue | ApiMediaAreaGeoPoint | ApiMediaAreaSuggestedReaction +| ApiMediaAreaChannelPost; diff --git a/src/assets/tgs/general/PartyPopper.tgs b/src/assets/tgs/general/PartyPopper.tgs new file mode 100644 index 000000000..d1112ecbe Binary files /dev/null and b/src/assets/tgs/general/PartyPopper.tgs differ diff --git a/src/components/common/FullNameTitle.tsx b/src/components/common/FullNameTitle.tsx index 8bd5a53ab..d5d5a3106 100644 --- a/src/components/common/FullNameTitle.tsx +++ b/src/components/common/FullNameTitle.tsx @@ -53,7 +53,6 @@ const FullNameTitle: FC = ({ const { showNotification } = getActions(); const isUser = isUserId(peer.id); const title = isUser ? getUserFullName(peer as ApiUser) : getChatTitle(lang, peer as ApiChat); - const emojiStatus = isUser && (peer as ApiUser).emojiStatus; const isPremium = isUser && (peer as ApiUser).isPremium; const handleTitleClick = useLastCallback((e) => { @@ -86,16 +85,16 @@ const FullNameTitle: FC = ({ {!noVerified && peer.isVerified && } {!noFake && peer.fakeType && } - {withEmojiStatus && emojiStatus && ( + {withEmojiStatus && peer.emojiStatus && ( )} - {withEmojiStatus && !emojiStatus && isPremium && } + {withEmojiStatus && !peer.emojiStatus && isPremium && } ); }; diff --git a/src/components/common/GroupChatInfo.tsx b/src/components/common/GroupChatInfo.tsx index 32bcf0f70..5e135596d 100644 --- a/src/components/common/GroupChatInfo.tsx +++ b/src/components/common/GroupChatInfo.tsx @@ -6,6 +6,7 @@ import type { ApiChat, ApiThreadInfo, ApiTopic, ApiTypingStatus, } from '../../api/types'; import type { LangFn } from '../../hooks/useLang'; +import type { IconName } from '../../types/icons'; import { MediaViewerOrigin, type StoryViewerOrigin } from '../../types'; import { @@ -30,6 +31,7 @@ import useLastCallback from '../../hooks/useLastCallback'; import Avatar from './Avatar'; import DotAnimation from './DotAnimation'; import FullNameTitle from './FullNameTitle'; +import Icon from './Icon'; import TopicIcon from './TopicIcon'; import TypingStatus from './TypingStatus'; @@ -39,6 +41,7 @@ type OwnProps = { chatId: string; threadId?: number; className?: string; + statusIcon?: IconName; typingStatus?: ApiTypingStatus; avatarSize?: 'tiny' | 'small' | 'medium' | 'large' | 'jumbo'; status?: string; @@ -48,6 +51,8 @@ type OwnProps = { withFullInfo?: boolean; withUpdatingStatus?: boolean; withChatType?: boolean; + noEmojiStatus?: boolean; + emojiStatusSize?: number; noRtl?: boolean; noAvatar?: boolean; noStatusOrTyping?: boolean; @@ -69,6 +74,7 @@ type StateProps = const GroupChatInfo: FC = ({ typingStatus, className, + statusIcon, avatarSize = 'medium', noAvatar, status, @@ -88,6 +94,8 @@ const GroupChatInfo: FC = ({ noStatusOrTyping, withStory, storyViewerOrigin, + noEmojiStatus, + emojiStatusSize, onClick, }) => { const { @@ -133,7 +141,10 @@ const GroupChatInfo: FC = ({ return withDots ? ( ) : ( - {status} + + {statusIcon && } + {renderText(status)} + ); } @@ -206,7 +217,7 @@ const GroupChatInfo: FC = ({
{topic ?

{renderText(topic.title)}

- : } + : } {!noStatusOrTyping && renderStatusOrTyping()}
diff --git a/src/components/common/PickerSelectedItem.tsx b/src/components/common/PickerSelectedItem.tsx index c0343b1aa..1048d352c 100644 --- a/src/components/common/PickerSelectedItem.tsx +++ b/src/components/common/PickerSelectedItem.tsx @@ -8,6 +8,7 @@ import type { IconName } from '../../types/icons'; import { getChatTitle, getUserFirstOrLastName } from '../../global/helpers'; import { selectChat, selectUser } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; +import { getPeerColorClass } from './helpers/peerColor'; import renderText from './helpers/renderText'; import useLang from '../../hooks/useLang'; @@ -26,6 +27,7 @@ type OwnProps = { clickArg?: any; className?: string; fluid?: boolean; + withPeerColors?: boolean; onClick: (arg: any) => void; }; @@ -46,6 +48,7 @@ const PickerSelectedItem: FC = ({ className, fluid, isSavedMessages, + withPeerColors, onClick, }) => { const lang = useLang(); @@ -84,6 +87,7 @@ const PickerSelectedItem: FC = ({ isMinimized && 'minimized', canClose && 'closeable', fluid && 'fluid', + withPeerColors && getPeerColorClass(chat || user), ); return ( diff --git a/src/components/common/PrivateChatInfo.tsx b/src/components/common/PrivateChatInfo.tsx index 7cf0ff191..bb219275f 100644 --- a/src/components/common/PrivateChatInfo.tsx +++ b/src/components/common/PrivateChatInfo.tsx @@ -21,6 +21,7 @@ import RippleEffect from '../ui/RippleEffect'; import Avatar from './Avatar'; import DotAnimation from './DotAnimation'; import FullNameTitle from './FullNameTitle'; +import Icon from './Icon'; import TypingStatus from './TypingStatus'; type OwnProps = { @@ -122,7 +123,7 @@ const PrivateChatInfo: FC = ({ ) : ( - {statusIcon && } + {statusIcon && } {renderText(status)} ); diff --git a/src/components/common/helpers/animatedAssets.ts b/src/components/common/helpers/animatedAssets.ts index 5f98c1116..a1fac8420 100644 --- a/src/components/common/helpers/animatedAssets.ts +++ b/src/components/common/helpers/animatedAssets.ts @@ -7,6 +7,7 @@ import VoiceAllowTalk from '../../../assets/tgs/calls/VoiceAllowTalk.tgs'; import VoiceMini from '../../../assets/tgs/calls/VoiceMini.tgs'; import VoiceMuted from '../../../assets/tgs/calls/VoiceMuted.tgs'; import VoiceOutlined from '../../../assets/tgs/calls/VoiceOutlined.tgs'; +import PartyPopper from '../../../assets/tgs/general/PartyPopper.tgs'; import Invite from '../../../assets/tgs/invites/Invite.tgs'; import JoinRequest from '../../../assets/tgs/invites/Requests.tgs'; import MonkeyClose from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyClose.tgs'; @@ -44,4 +45,5 @@ export const LOCAL_TGS_URLS = { QrPlane, Congratulations, Experimental, + PartyPopper, }; diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index b94de2222..07747e51e 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -179,7 +179,6 @@ const Main: FC = ({ isMediaViewerOpen, isStoryViewerOpen, isForwardModalOpen, - currentUserId, hasNotifications, hasDialogs, audioMessage, @@ -568,7 +567,7 @@ const Main: FC = ({ isByPhoneNumber={newContactByPhoneNumber} /> - + diff --git a/src/components/middle/ActionMessage.tsx b/src/components/middle/ActionMessage.tsx index c07e90fee..045e50524 100644 --- a/src/components/middle/ActionMessage.tsx +++ b/src/components/middle/ActionMessage.tsx @@ -203,7 +203,7 @@ const ActionMessage: FC = ({ const handleGiftCodeClick = () => { const slug = message.content.action?.slug; if (!slug) return; - checkGiftCode({ slug }); + checkGiftCode({ slug, message: { chatId: message.chatId, messageId: message.id } }); }; // TODO Refactoring for action rendering diff --git a/src/components/middle/MiddleHeader.tsx b/src/components/middle/MiddleHeader.tsx index 931fce55d..07ec90a98 100644 --- a/src/components/middle/MiddleHeader.tsx +++ b/src/components/middle/MiddleHeader.tsx @@ -405,6 +405,7 @@ const MiddleHeader: FC = ({ withUpdatingStatus withStory storyViewerOrigin={StoryViewerOrigin.MiddleHeaderAvatar} + emojiStatusSize={EMOJI_STATUS_SIZE} noRtl /> )} diff --git a/src/components/middle/message/Giveaway.module.scss b/src/components/middle/message/Giveaway.module.scss index 3e0059b71..086792937 100644 --- a/src/components/middle/message/Giveaway.module.scss +++ b/src/components/middle/message/Giveaway.module.scss @@ -7,14 +7,19 @@ .title { display: block; + margin-top: 0.5rem; } -.gift { +.sticker { position: relative; margin-top: -2.5rem; margin-bottom: 1rem; } +.resultSticker { + margin-top: 0; +} + .count { position: absolute; bottom: 0; @@ -43,15 +48,16 @@ margin-bottom: 0; } -.channels { +.peers { display: flex; - flex-direction: column; + flex-wrap: wrap; + justify-content: center; align-items: center; gap: 0.5rem; margin-block: 0.25rem; } -.channel { +.peer { background-color: var(--accent-background-color); color: var(--accent-color); margin: unset; @@ -64,3 +70,29 @@ .button { margin-bottom: 1rem; } + +.result { + font-weight: 500; +} + +.separator { + display: flex; + align-items: center; + text-align: center; + color: var(--color-text-secondary); +} + +.separator::before, +.separator::after { + content: ''; + flex: 1; + border-bottom: 1px solid var(--color-dividers); +} + +.separator:not(:empty)::before { + margin-right: 0.25em; +} + +.separator:not(:empty)::after { + margin-left: 0.25em; +} diff --git a/src/components/middle/message/Giveaway.tsx b/src/components/middle/message/Giveaway.tsx index 1e70c2ccc..17e560c7d 100644 --- a/src/components/middle/message/Giveaway.tsx +++ b/src/components/middle/message/Giveaway.tsx @@ -4,25 +4,30 @@ import React, { import { getActions, getGlobal, withGlobal } from '../../../global'; import type { - ApiChat, ApiGiveawayInfo, ApiMessage, ApiPeer, ApiSticker, + ApiChat, ApiGiveaway, ApiGiveawayInfo, ApiGiveawayResults, ApiMessage, ApiPeer, ApiSticker, } from '../../../api/types'; -import { getChatTitle, getUserFullName, isApiPeerChat } from '../../../global/helpers'; +import { + getChatTitle, getUserFullName, isApiPeerChat, isOwnMessage, +} from '../../../global/helpers'; import { selectCanPlayAnimatedEmojis, selectChat, selectForwardedSender, selectGiftStickerForDuration, } from '../../../global/selectors'; +import buildClassName from '../../../util/buildClassName'; import { formatDateAtTime, formatDateTimeToString } from '../../../util/dateFormat'; import { isoToEmoji } from '../../../util/emoji'; import { getServerTime } from '../../../util/serverTime'; import { callApi } from '../../../api/gramjs'; +import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets'; import renderText from '../../common/helpers/renderText'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; +import AnimatedIcon from '../../common/AnimatedIcon'; import AnimatedIconFromSticker from '../../common/AnimatedIconFromSticker'; import PickerSelectedItem from '../../common/PickerSelectedItem'; import Button from '../../ui/Button'; @@ -43,6 +48,7 @@ type StateProps = { const NBSP = '\u00A0'; const GIFT_STICKER_SIZE = 175; +const RESULT_STICKER_SIZE = 150; const Giveaway = ({ chat, @@ -57,20 +63,27 @@ const Giveaway = ({ const [giveawayInfo, setGiveawayInfo] = useState(); const lang = useLang(); + const { giveaway, giveawayResults } = message.content; + const isResults = Boolean(giveawayResults); const { - months, quantity, channelIds, untilDate, countries, - } = message.content.giveaway!; + months, untilDate, prizeDescription, + } = (giveaway || giveawayResults)!; + + const isOwn = isOwnMessage(message); + + const quantity = isResults ? giveawayResults.winnersCount : giveaway!.quantity; const hasEnded = getServerTime() > untilDate; const countryList = useMemo(() => { + if (isResults) return undefined; const translatedNames = new Intl.DisplayNames([lang.code!, 'en'].filter(Boolean), { type: 'region' }); - return countries?.map((countryCode) => ( + return giveaway?.countries?.map((countryCode) => ( `${isoToEmoji(countryCode)}${NBSP}${translatedNames.of(countryCode)}` )).join(', '); - }, [countries, lang.code]); + }, [giveaway, isResults, lang.code]); - const handleChannelClick = useLastCallback((channelId: string) => { + const handlePeerClick = useLastCallback((channelId: string) => { openChat({ id: channelId }); }); @@ -95,36 +108,149 @@ const Giveaway = ({ return lang(giveawayInfo.type === 'results' ? 'BoostingGiveawayEnd' : 'BoostingGiveAwayAbout'); }, [giveawayInfo, lang]); + function renderGiveawayDescription(media: ApiGiveaway) { + const channelIds = media.channelIds; + return ( + <> +
+ + {renderText(lang('BoostingGiveawayPrizes'), ['simple_markdown'])} + + {prizeDescription && ( + <> +

+ {renderText( + lang('BoostingGiveawayMsgPrizes', [quantity, prizeDescription], undefined, quantity), + ['simple_markdown'], + )} +

+
{lang('BoostingGiveawayMsgWithDivider')}
+ + )} +

+ {renderText(lang('Chat.Giveaway.Info.Subscriptions', quantity), ['simple_markdown'])} +
+ {renderText(lang( + 'ActionGiftPremiumSubtitle', + lang('Chat.Giveaway.Info.Months', months), + ), ['simple_markdown'])} +

+
+
+ + {renderText(lang('BoostingGiveawayMsgParticipants'), ['simple_markdown'])} + +

+ {renderText(lang('BoostingGiveawayMsgAllSubsPlural', channelIds.length), ['simple_markdown'])} +

+
+ {channelIds.map((peerId) => ( + + ))} +
+ {countryList && ( + {renderText(lang('Chat.Giveaway.Message.CountriesFrom', countryList))} + )} +
+
+ + {renderText(lang('BoostingWinnersDate'), ['simple_markdown'])} + +

+ {formatDateTimeToString(untilDate * 1000, lang.code, true)} +

+
+ + ); + } + + function renderGiveawayResultsDescription(media: ApiGiveawayResults) { + const winnerIds = media.winnerIds; + return ( + <> +
+ + {renderText(lang('BoostingGiveawayResultsMsgWinnersSelected'), ['simple_markdown'])} + +

+ {renderText(lang('BoostingGiveawayResultsMsgWinnersTitle', winnerIds.length), ['simple_markdown'])} +

+ + {lang('lng_prizes_results_winners')} + +
+ {winnerIds.map((peerId) => ( + + ))} +
+
+
+

+ {lang('BoostingGiveawayResultsMsgAllWinnersReceivedLinks')} +

+
+ + ); + } + function renderGiveawayInfo() { if (!sender || !giveawayInfo) return undefined; - const isResults = giveawayInfo.type === 'results'; + const isResultsInfo = giveawayInfo.type === 'results'; const chatTitle = isApiPeerChat(sender) ? getChatTitle(lang, sender) : getUserFullName(sender); const duration = lang('Chat.Giveaway.Info.Months', months); const endDate = formatDateAtTime(lang, untilDate * 1000); - const otherChannelsCount = channelIds.length ? channelIds.length - 1 : 0; + const otherChannelsCount = giveaway?.channelIds ? giveaway.channelIds.length - 1 : 0; const otherChannelsString = lang('Chat.Giveaway.Info.OtherChannels', otherChannelsCount); const isSeveral = otherChannelsCount > 0; - const firstKey = isResults ? 'BoostingGiveawayHowItWorksTextEnd' : 'BoostingGiveawayHowItWorksText'; + const firstKey = isResultsInfo ? 'BoostingGiveawayHowItWorksTextEnd' : 'BoostingGiveawayHowItWorksText'; const firstParagraph = lang(firstKey, [chatTitle, quantity, duration], undefined, quantity); + const additionalPrizes = prizeDescription + ? lang('BoostingGiveawayHowItWorksIncludeText', [chatTitle, quantity, prizeDescription], undefined, quantity) + : undefined; + let secondKey = ''; - if (isResults) { + if (isResultsInfo) { secondKey = isSeveral ? 'BoostingGiveawayHowItWorksSubTextSeveralEnd' : 'BoostingGiveawayHowItWorksSubTextEnd'; } else { secondKey = isSeveral ? 'BoostingGiveawayHowItWorksSubTextSeveral' : 'BoostingGiveawayHowItWorksSubText'; } let secondParagraph = lang(secondKey, [endDate, quantity, chatTitle, otherChannelsCount], undefined, quantity); - if (isResults && giveawayInfo.activatedCount) { + if (isResultsInfo && giveawayInfo.activatedCount) { secondParagraph += ` ${lang('BoostingGiveawayUsedLinksPlural', giveawayInfo.activatedCount)}`; } + let result = ''; + + if (isResultsInfo) { + if (giveawayInfo.isRefunded) { + result = lang('BoostingGiveawayCanceledByPayment'); + } else { + result = lang(giveawayInfo.isWinner ? 'BoostingGiveawayYouWon' : 'BoostingGiveawayYouNotWon'); + } + } + let lastParagraph = ''; - if (isResults && giveawayInfo.isRefunded) { - lastParagraph = lang('BoostingGiveawayCanceledByPayment'); - } else if (isResults) { - lastParagraph = lang(giveawayInfo.isWinner ? 'BoostingGiveawayYouWon' : 'BoostingGiveawayYouNotWon'); + if (isResultsInfo) { + // Nothing } else if (giveawayInfo.disallowedCountry) { lastParagraph = lang('BoostingGiveawayNotEligibleCountry'); } else if (giveawayInfo.adminDisallowedChatId) { @@ -148,78 +274,55 @@ const Giveaway = ({ return ( <> + {result && ( +

+ {renderText(result, ['simple_markdown'])} +

+ )}

{renderText(firstParagraph, ['simple_markdown'])}

+ {additionalPrizes && ( +

+ {renderText(additionalPrizes, ['simple_markdown'])} +

+ )}

{renderText(secondParagraph, ['simple_markdown'])}

-

- {renderText(lastParagraph, ['simple_markdown'])} -

+ {lastParagraph && ( +

+ {renderText(lastParagraph, ['simple_markdown'])} +

+ )} ); } return (
-
- +
+ {isResults ? ( + + ) : ( + + )} {`x${quantity}`}
-
- - {renderText(lang('BoostingGiveawayPrizes'), ['simple_markdown'])} - -

- {renderText(lang('Chat.Giveaway.Info.Subscriptions', quantity), ['simple_markdown'])} -
- {renderText(lang( - 'ActionGiftPremiumSubtitle', - lang('Chat.Giveaway.Info.Months', months), - ), ['simple_markdown'])} -

-
-
- - {renderText(lang('BoostingGiveawayMsgParticipants'), ['simple_markdown'])} - -

- {renderText(lang('BoostingGiveawayMsgAllSubsPlural', channelIds.length), ['simple_markdown'])} -

-
- {channelIds.map((channelId) => ( - - ))} -
- {Boolean(countries?.length) && ( - {renderText(lang('Chat.Giveaway.Message.CountriesFrom', countryList))} - )} -
-
- - {renderText(lang('BoostingWinnersDate'), ['simple_markdown'])} - -

- {formatDateTimeToString(untilDate * 1000, lang.code, true)} -

-
+ {isResults ? renderGiveawayResultsDescription(giveawayResults) : renderGiveawayDescription(giveaway!)}