Introduce Stories (#3154)
This commit is contained in:
parent
e367d82c6c
commit
fc605350ea
@ -5,7 +5,12 @@ import { Api as GramJs } from '../../../lib/gramjs';
|
|||||||
import type { ApiAppConfig } from '../../types';
|
import type { ApiAppConfig } from '../../types';
|
||||||
import type { ApiLimitType } from '../../../global/types';
|
import type { ApiLimitType } from '../../../global/types';
|
||||||
import { buildJson } from './misc';
|
import { buildJson } from './misc';
|
||||||
import { DEFAULT_LIMITS } from '../../../config';
|
import {
|
||||||
|
DEFAULT_LIMITS,
|
||||||
|
SERVICE_NOTIFICATIONS_USER_ID,
|
||||||
|
STORY_EXPIRE_PERIOD,
|
||||||
|
STORY_VIEWERS_EXPIRE_PERIOD,
|
||||||
|
} from '../../../config';
|
||||||
|
|
||||||
type LimitType = 'default' | 'premium';
|
type LimitType = 'default' | 'premium';
|
||||||
type Limit = 'upload_max_fileparts' | 'stickers_faved_limit' | 'saved_gifs_limit' | 'dialog_filters_chats_limit' |
|
type Limit = 'upload_max_fileparts' | 'stickers_faved_limit' | 'saved_gifs_limit' | 'dialog_filters_chats_limit' |
|
||||||
@ -39,6 +44,11 @@ export interface GramJsAppConfig extends LimitsConfig {
|
|||||||
autoarchive_setting_available: boolean;
|
autoarchive_setting_available: boolean;
|
||||||
// Forums
|
// Forums
|
||||||
topics_pinned_limit: number;
|
topics_pinned_limit: number;
|
||||||
|
// Stories
|
||||||
|
stories_all_hidden?: boolean;
|
||||||
|
story_expire_period: number;
|
||||||
|
story_viewers_expire_period: number;
|
||||||
|
stories_changelog_user_id?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildEmojiSounds(appConfig: GramJsAppConfig) {
|
function buildEmojiSounds(appConfig: GramJsAppConfig) {
|
||||||
@ -100,5 +110,9 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
|
|||||||
chatlistJoined: getLimit(appConfig, 'chatlist_joined_limit', 'chatlistJoined'),
|
chatlistJoined: getLimit(appConfig, 'chatlist_joined_limit', 'chatlistJoined'),
|
||||||
},
|
},
|
||||||
hash,
|
hash,
|
||||||
|
areStoriesHidden: appConfig.stories_all_hidden,
|
||||||
|
storyExpirePeriod: appConfig.story_expire_period ?? STORY_EXPIRE_PERIOD,
|
||||||
|
storyViewersExpirePeriod: appConfig.story_viewers_expire_period ?? STORY_VIEWERS_EXPIRE_PERIOD,
|
||||||
|
storyChangelogUserId: appConfig.stories_changelog_user_id?.toString() ?? SERVICE_NOTIFICATIONS_USER_ID,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,10 +36,17 @@ import type {
|
|||||||
ApiMessageExtendedMediaPreview,
|
ApiMessageExtendedMediaPreview,
|
||||||
ApiReaction,
|
ApiReaction,
|
||||||
ApiReactionEmoji,
|
ApiReactionEmoji,
|
||||||
|
ApiTypeReplyTo,
|
||||||
|
ApiStory,
|
||||||
|
ApiStorySkipped,
|
||||||
|
ApiWebPageStoryData,
|
||||||
|
ApiMessageStoryData,
|
||||||
|
ApiTypeStory,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import {
|
import {
|
||||||
ApiMessageEntityTypes,
|
ApiMessageEntityTypes,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
import type { ApiPrivacySettings, PrivacyVisibility } from '../../../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DELETED_COMMENTS_CHANNEL_ID,
|
DELETED_COMMENTS_CHANNEL_ID,
|
||||||
@ -50,14 +57,19 @@ import {
|
|||||||
SUPPORTED_VIDEO_CONTENT_TYPES,
|
SUPPORTED_VIDEO_CONTENT_TYPES,
|
||||||
VIDEO_WEBM_TYPE,
|
VIDEO_WEBM_TYPE,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { pick } from '../../../util/iteratees';
|
import { buildCollectionByCallback, pick } from '../../../util/iteratees';
|
||||||
import { buildStickerFromDocument } from './symbols';
|
import { buildStickerFromDocument } from './symbols';
|
||||||
import {
|
import {
|
||||||
buildApiPhoto, buildApiPhotoSize, buildApiThumbnailFromPath, buildApiThumbnailFromStripped,
|
buildApiPhoto, buildApiPhotoSize, buildApiThumbnailFromPath, buildApiThumbnailFromStripped,
|
||||||
} from './common';
|
} from './common';
|
||||||
import { interpolateArray } from '../../../util/waveform';
|
import { interpolateArray } from '../../../util/waveform';
|
||||||
import { buildPeer } from '../gramjsBuilders';
|
import { buildPeer } from '../gramjsBuilders';
|
||||||
import { addPhotoToLocalDb, resolveMessageApiChatId, serializeBytes } from '../helpers';
|
import {
|
||||||
|
addPhotoToLocalDb,
|
||||||
|
addStoryToLocalDb,
|
||||||
|
resolveMessageApiChatId,
|
||||||
|
serializeBytes,
|
||||||
|
} from '../helpers';
|
||||||
import { buildApiPeerId, getApiChatIdFromMtpPeer, isPeerUser } from './peers';
|
import { buildApiPeerId, getApiChatIdFromMtpPeer, isPeerUser } from './peers';
|
||||||
import { buildApiCallDiscardReason } from './calls';
|
import { buildApiCallDiscardReason } from './calls';
|
||||||
import { getEmojiOnlyCountForMessage } from '../../../global/helpers/getEmojiOnlyCountForMessage';
|
import { getEmojiOnlyCountForMessage } from '../../../global/helpers/getEmojiOnlyCountForMessage';
|
||||||
@ -181,9 +193,23 @@ export function buildApiMessageWithChatId(
|
|||||||
const isInvoiceMedia = mtpMessage.media instanceof GramJs.MessageMediaInvoice
|
const isInvoiceMedia = mtpMessage.media instanceof GramJs.MessageMediaInvoice
|
||||||
&& Boolean(mtpMessage.media.extendedMedia);
|
&& Boolean(mtpMessage.media.extendedMedia);
|
||||||
|
|
||||||
const {
|
let replyToMsgId: number | undefined;
|
||||||
replyToMsgId, replyToTopId, forumTopic, replyToPeerId,
|
let replyToTopId: number | undefined;
|
||||||
} = mtpMessage.replyTo || {};
|
let replyToStoryUserId: string | undefined;
|
||||||
|
let replyToStoryId: number | undefined;
|
||||||
|
let forumTopic: boolean | undefined;
|
||||||
|
let replyToPeerId: GramJs.TypePeer | undefined;
|
||||||
|
if (mtpMessage.replyTo instanceof GramJs.MessageReplyHeader) {
|
||||||
|
replyToMsgId = mtpMessage.replyTo.replyToMsgId;
|
||||||
|
replyToTopId = mtpMessage.replyTo.replyToTopId;
|
||||||
|
forumTopic = mtpMessage.replyTo.forumTopic;
|
||||||
|
replyToPeerId = mtpMessage.replyTo.replyToPeerId;
|
||||||
|
}
|
||||||
|
if (mtpMessage.replyTo instanceof GramJs.MessageReplyStoryHeader) {
|
||||||
|
replyToStoryUserId = buildApiPeerId(mtpMessage.replyTo.userId, 'user');
|
||||||
|
replyToStoryId = mtpMessage.replyTo.storyId;
|
||||||
|
}
|
||||||
|
|
||||||
const isEdited = mtpMessage.editDate && !mtpMessage.editHide;
|
const isEdited = mtpMessage.editDate && !mtpMessage.editHide;
|
||||||
const {
|
const {
|
||||||
inlineButtons, keyboardButtons, keyboardPlaceholder, isKeyboardSingleUse, isKeyboardSelective,
|
inlineButtons, keyboardButtons, keyboardPlaceholder, isKeyboardSingleUse, isKeyboardSelective,
|
||||||
@ -219,6 +245,7 @@ export function buildApiMessageWithChatId(
|
|||||||
...(replyToPeerId && { replyToChatId: getApiChatIdFromMtpPeer(replyToPeerId) }),
|
...(replyToPeerId && { replyToChatId: getApiChatIdFromMtpPeer(replyToPeerId) }),
|
||||||
...(replyToTopId && { replyToTopMessageId: replyToTopId }),
|
...(replyToTopId && { replyToTopMessageId: replyToTopId }),
|
||||||
...(forwardInfo && { forwardInfo }),
|
...(forwardInfo && { forwardInfo }),
|
||||||
|
...(replyToStoryUserId && { replyToStoryUserId, replyToStoryId }),
|
||||||
...(isEdited && { isEdited }),
|
...(isEdited && { isEdited }),
|
||||||
...(mtpMessage.editDate && { editDate: mtpMessage.editDate }),
|
...(mtpMessage.editDate && { editDate: mtpMessage.editDate }),
|
||||||
...(isMediaUnread && { isMediaUnread }),
|
...(isMediaUnread && { isMediaUnread }),
|
||||||
@ -399,7 +426,8 @@ export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): ApiMes
|
|||||||
if (photo) return { photo };
|
if (photo) return { photo };
|
||||||
|
|
||||||
const video = buildVideo(media);
|
const video = buildVideo(media);
|
||||||
if (video) return { video };
|
const altVideo = buildAltVideo(media);
|
||||||
|
if (video) return { video, altVideo };
|
||||||
|
|
||||||
const audio = buildAudio(media);
|
const audio = buildAudio(media);
|
||||||
if (audio) return { audio };
|
if (audio) return { audio };
|
||||||
@ -428,6 +456,9 @@ export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): ApiMes
|
|||||||
const game = buildGameFromMedia(media);
|
const game = buildGameFromMedia(media);
|
||||||
if (game) return { game };
|
if (game) return { game };
|
||||||
|
|
||||||
|
const storyData = buildMessageStoryData(media);
|
||||||
|
if (storyData) return { storyData };
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -499,6 +530,7 @@ export function buildVideoFromDocument(document: GramJs.Document, isSpoiler?: bo
|
|||||||
h: height,
|
h: height,
|
||||||
supportsStreaming = false,
|
supportsStreaming = false,
|
||||||
roundMessage: isRound = false,
|
roundMessage: isRound = false,
|
||||||
|
nosound,
|
||||||
} = videoAttr;
|
} = videoAttr;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -514,6 +546,7 @@ export function buildVideoFromDocument(document: GramJs.Document, isSpoiler?: bo
|
|||||||
thumbnail: buildApiThumbnailFromStripped(thumbs),
|
thumbnail: buildApiThumbnailFromStripped(thumbs),
|
||||||
size: size.toJSNumber(),
|
size: size.toJSNumber(),
|
||||||
isSpoiler,
|
isSpoiler,
|
||||||
|
...(nosound && { noSound: true }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -529,6 +562,18 @@ function buildVideo(media: GramJs.TypeMessageMedia): ApiVideo | undefined {
|
|||||||
return buildVideoFromDocument(media.document, media.spoiler);
|
return buildVideoFromDocument(media.document, media.spoiler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildAltVideo(media: GramJs.TypeMessageMedia): ApiVideo | undefined {
|
||||||
|
if (
|
||||||
|
!(media instanceof GramJs.MessageMediaDocument)
|
||||||
|
|| !(media.altDocument instanceof GramJs.Document)
|
||||||
|
|| !media.altDocument.mimeType.startsWith('video')
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildVideoFromDocument(media.altDocument, media.spoiler);
|
||||||
|
}
|
||||||
|
|
||||||
function buildAudio(media: GramJs.TypeMessageMedia): ApiAudio | undefined {
|
function buildAudio(media: GramJs.TypeMessageMedia): ApiAudio | undefined {
|
||||||
if (
|
if (
|
||||||
!(media instanceof GramJs.MessageMediaDocument)
|
!(media instanceof GramJs.MessageMediaDocument)
|
||||||
@ -856,12 +901,28 @@ export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undef
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, photo, document } = media.webpage;
|
const {
|
||||||
|
id, photo, document, attributes,
|
||||||
|
} = media.webpage;
|
||||||
|
|
||||||
let video;
|
let video;
|
||||||
if (document instanceof GramJs.Document && document.mimeType.startsWith('video/')) {
|
if (document instanceof GramJs.Document && document.mimeType.startsWith('video/')) {
|
||||||
video = buildVideoFromDocument(document);
|
video = buildVideoFromDocument(document);
|
||||||
}
|
}
|
||||||
|
let story: ApiWebPageStoryData | undefined;
|
||||||
|
const attributeStory = attributes
|
||||||
|
?.find((a: any): a is GramJs.WebPageAttributeStory => a instanceof GramJs.WebPageAttributeStory);
|
||||||
|
if (attributeStory) {
|
||||||
|
const userId = buildApiPeerId(attributeStory.userId, 'user');
|
||||||
|
story = {
|
||||||
|
id: attributeStory.id,
|
||||||
|
userId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (attributeStory.story instanceof GramJs.StoryItem) {
|
||||||
|
addStoryToLocalDb(attributeStory.story, userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: Number(id),
|
id: Number(id),
|
||||||
@ -877,9 +938,20 @@ export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undef
|
|||||||
photo: photo instanceof GramJs.Photo ? buildApiPhoto(photo) : undefined,
|
photo: photo instanceof GramJs.Photo ? buildApiPhoto(photo) : undefined,
|
||||||
document: !video && document ? buildApiDocument(document) : undefined,
|
document: !video && document ? buildApiDocument(document) : undefined,
|
||||||
video,
|
video,
|
||||||
|
story,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildMessageStoryData(media: GramJs.TypeMessageMedia): ApiMessageStoryData | undefined {
|
||||||
|
if (!(media instanceof GramJs.MessageMediaStory)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = buildApiPeerId(media.userId, 'user');
|
||||||
|
|
||||||
|
return { id: media.id, userId, ...(media.viaMention && { isMention: true }) };
|
||||||
|
}
|
||||||
|
|
||||||
function buildAction(
|
function buildAction(
|
||||||
action: GramJs.TypeMessageAction,
|
action: GramJs.TypeMessageAction,
|
||||||
senderId: string | undefined,
|
senderId: string | undefined,
|
||||||
@ -1284,8 +1356,7 @@ export function buildLocalMessage(
|
|||||||
chat: ApiChat,
|
chat: ApiChat,
|
||||||
text?: string,
|
text?: string,
|
||||||
entities?: ApiMessageEntity[],
|
entities?: ApiMessageEntity[],
|
||||||
replyingTo?: number,
|
replyingTo?: ApiTypeReplyTo,
|
||||||
replyingToTopId?: number,
|
|
||||||
attachment?: ApiAttachment,
|
attachment?: ApiAttachment,
|
||||||
sticker?: ApiSticker,
|
sticker?: ApiSticker,
|
||||||
gif?: ApiVideo,
|
gif?: ApiVideo,
|
||||||
@ -1294,12 +1365,27 @@ export function buildLocalMessage(
|
|||||||
groupedId?: string,
|
groupedId?: string,
|
||||||
scheduledAt?: number,
|
scheduledAt?: number,
|
||||||
sendAs?: ApiChat | ApiUser,
|
sendAs?: ApiChat | ApiUser,
|
||||||
|
story?: ApiStory | ApiStorySkipped,
|
||||||
): ApiMessage {
|
): ApiMessage {
|
||||||
const localId = getNextLocalMessageId(chat.lastMessage?.id);
|
const localId = getNextLocalMessageId(chat.lastMessage?.id);
|
||||||
const media = attachment && buildUploadingMedia(attachment);
|
const media = attachment && buildUploadingMedia(attachment);
|
||||||
const isChannel = chat.type === 'chatTypeChannel';
|
const isChannel = chat.type === 'chatTypeChannel';
|
||||||
const isForum = chat.isForum;
|
const isForum = chat.isForum;
|
||||||
|
|
||||||
|
let replyToMessageId: number | undefined;
|
||||||
|
let replyingToTopId: number | undefined;
|
||||||
|
let replyToStoryUserId: string | undefined;
|
||||||
|
let replyToStoryId: number | undefined;
|
||||||
|
if (replyingTo) {
|
||||||
|
if ('replyingTo' in replyingTo) {
|
||||||
|
replyToMessageId = replyingTo.replyingTo;
|
||||||
|
replyingToTopId = replyingTo.replyingToTopId;
|
||||||
|
} else {
|
||||||
|
replyToStoryUserId = replyingTo.userId;
|
||||||
|
replyToStoryId = replyingTo.storyId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
id: localId,
|
id: localId,
|
||||||
chatId: chat.id,
|
chatId: chat.id,
|
||||||
@ -1315,13 +1401,15 @@ export function buildLocalMessage(
|
|||||||
...(gif && { video: gif }),
|
...(gif && { video: gif }),
|
||||||
...(poll && buildNewPoll(poll, localId)),
|
...(poll && buildNewPoll(poll, localId)),
|
||||||
...(contact && { contact }),
|
...(contact && { contact }),
|
||||||
|
...(story && { storyData: story }),
|
||||||
},
|
},
|
||||||
date: scheduledAt || Math.round(Date.now() / 1000) + getServerTimeOffset(),
|
date: scheduledAt || Math.round(Date.now() / 1000) + getServerTimeOffset(),
|
||||||
isOutgoing: !isChannel,
|
isOutgoing: !isChannel,
|
||||||
senderId: sendAs?.id || currentUserId,
|
senderId: sendAs?.id || currentUserId,
|
||||||
...(replyingTo && { replyToMessageId: replyingTo }),
|
...(replyToMessageId && { replyToMessageId }),
|
||||||
...(replyingToTopId && { replyToTopMessageId: replyingToTopId }),
|
...(replyingToTopId && { replyToTopMessageId: replyingToTopId }),
|
||||||
...((replyingTo || replyingToTopId) && isForum && { isTopicReply: true }),
|
...((replyToMessageId || replyingToTopId) && isForum && { isTopicReply: true }),
|
||||||
|
...(replyToStoryUserId && { replyToStoryUserId, replyToStoryId }),
|
||||||
...(groupedId && {
|
...(groupedId && {
|
||||||
groupedId,
|
groupedId,
|
||||||
...(media && (media.photo || media.video) && { isInAlbum: true }),
|
...(media && (media.photo || media.video) && { isInAlbum: true }),
|
||||||
@ -1627,3 +1715,111 @@ export function buildApiFormattedText(textWithEntities: GramJs.TextWithEntities)
|
|||||||
entities: entities.map(buildApiMessageEntity),
|
entities: entities.map(buildApiMessageEntity),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildApiUsersStories(userStories: GramJs.UserStories) {
|
||||||
|
const userId = buildApiPeerId(userStories.userId, 'user');
|
||||||
|
|
||||||
|
return buildCollectionByCallback(userStories.stories, (story) => [story.id, buildApiStory(userId, story)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildApiStory(userId: string, story: GramJs.TypeStoryItem): ApiTypeStory {
|
||||||
|
if (story instanceof GramJs.StoryItemDeleted) {
|
||||||
|
return {
|
||||||
|
id: story.id,
|
||||||
|
userId,
|
||||||
|
isDeleted: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (story instanceof GramJs.StoryItemSkipped) {
|
||||||
|
const {
|
||||||
|
id, date, expireDate, closeFriends,
|
||||||
|
} = story;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
...(closeFriends && { isForCloseFriends: true }),
|
||||||
|
date,
|
||||||
|
expireDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
edited, pinned, expireDate, id, date, caption,
|
||||||
|
entities, media, privacy, views,
|
||||||
|
public: isPublic, noforwards, closeFriends, contacts, selectedContacts,
|
||||||
|
} = story;
|
||||||
|
|
||||||
|
const content: ApiMessage['content'] = {
|
||||||
|
...buildMessageMediaContent(media),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (caption) {
|
||||||
|
content.text = buildMessageTextContent(caption, entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
date,
|
||||||
|
expireDate,
|
||||||
|
content,
|
||||||
|
...(isPublic && { isPublic }),
|
||||||
|
...(edited && { isEdited: true }),
|
||||||
|
...(pinned && { isPinned: true }),
|
||||||
|
...(contacts && { isForContacts: true }),
|
||||||
|
...(selectedContacts && { isForSelectedContacts: true }),
|
||||||
|
...(closeFriends && { isForCloseFriends: true }),
|
||||||
|
...(noforwards && { noForwards: true }),
|
||||||
|
...(views?.viewsCount && { viewsCount: views.viewsCount }),
|
||||||
|
...(views?.recentViewers && {
|
||||||
|
recentViewerIds: views.recentViewers.map((viewerId) => buildApiPeerId(viewerId, 'user')),
|
||||||
|
}),
|
||||||
|
...(privacy && { visibility: buildPrivacyRules(privacy) }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPrivacyRules(rules: GramJs.TypePrivacyRule[]): ApiPrivacySettings {
|
||||||
|
let visibility: PrivacyVisibility | undefined;
|
||||||
|
let allowUserIds: string[] | undefined;
|
||||||
|
let allowChatIds: string[] | undefined;
|
||||||
|
let blockUserIds: string[] | undefined;
|
||||||
|
let blockChatIds: string[] | undefined;
|
||||||
|
|
||||||
|
rules.forEach((rule) => {
|
||||||
|
if (rule instanceof GramJs.PrivacyValueAllowAll) {
|
||||||
|
visibility ||= 'everybody';
|
||||||
|
} else if (rule instanceof GramJs.PrivacyValueAllowContacts) {
|
||||||
|
visibility ||= 'contacts';
|
||||||
|
} else if (rule instanceof GramJs.PrivacyValueAllowCloseFriends) {
|
||||||
|
visibility ||= 'closeFriends';
|
||||||
|
} else if (rule instanceof GramJs.PrivacyValueDisallowContacts) {
|
||||||
|
visibility ||= 'nonContacts';
|
||||||
|
} else if (rule instanceof GramJs.PrivacyValueDisallowAll) {
|
||||||
|
visibility ||= 'nobody';
|
||||||
|
} else if (rule instanceof GramJs.PrivacyValueAllowUsers) {
|
||||||
|
visibility ||= 'selectedContacts';
|
||||||
|
allowUserIds = rule.users.map((chatId) => buildApiPeerId(chatId, 'user'));
|
||||||
|
} else if (rule instanceof GramJs.PrivacyValueDisallowUsers) {
|
||||||
|
blockUserIds = rule.users.map((chatId) => buildApiPeerId(chatId, 'user'));
|
||||||
|
} else if (rule instanceof GramJs.PrivacyValueAllowChatParticipants) {
|
||||||
|
allowChatIds = rule.chats.map((chatId) => buildApiPeerId(chatId, 'chat'));
|
||||||
|
} else if (rule instanceof GramJs.PrivacyValueDisallowChatParticipants) {
|
||||||
|
blockChatIds = rule.chats.map((chatId) => buildApiPeerId(chatId, 'chat'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!visibility) {
|
||||||
|
// Disallow by default
|
||||||
|
visibility = 'nobody';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
visibility,
|
||||||
|
allowUserIds: allowUserIds || [],
|
||||||
|
allowChatIds: allowChatIds || [],
|
||||||
|
blockUserIds: blockUserIds || [],
|
||||||
|
blockChatIds: blockChatIds || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { Api as GramJs } from '../../../lib/gramjs';
|
|||||||
import type {
|
import type {
|
||||||
ApiConfig, ApiCountry, ApiSession, ApiUrlAuthResult, ApiWallpaper, ApiWebSession, ApiLangString,
|
ApiConfig, ApiCountry, ApiSession, ApiUrlAuthResult, ApiWallpaper, ApiWebSession, ApiLangString,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import type { ApiPrivacySettings, ApiPrivacyKey, PrivacyVisibility } from '../../../types';
|
import type { ApiPrivacyKey } from '../../../types';
|
||||||
|
|
||||||
import { buildApiDocument, buildApiReaction } from './messages';
|
import { buildApiDocument, buildApiReaction } from './messages';
|
||||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
|
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
|
||||||
@ -81,47 +81,6 @@ export function buildPrivacyKey(key: GramJs.TypePrivacyKey): ApiPrivacyKey | und
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildPrivacyRules(rules: GramJs.TypePrivacyRule[]): ApiPrivacySettings {
|
|
||||||
let visibility: PrivacyVisibility | undefined;
|
|
||||||
let allowUserIds: string[] | undefined;
|
|
||||||
let allowChatIds: string[] | undefined;
|
|
||||||
let blockUserIds: string[] | undefined;
|
|
||||||
let blockChatIds: string[] | undefined;
|
|
||||||
|
|
||||||
rules.forEach((rule) => {
|
|
||||||
if (rule instanceof GramJs.PrivacyValueAllowAll) {
|
|
||||||
visibility = visibility || 'everybody';
|
|
||||||
} else if (rule instanceof GramJs.PrivacyValueAllowContacts) {
|
|
||||||
visibility = visibility || 'contacts';
|
|
||||||
} else if (rule instanceof GramJs.PrivacyValueDisallowContacts) {
|
|
||||||
visibility = visibility || 'nonContacts';
|
|
||||||
} else if (rule instanceof GramJs.PrivacyValueDisallowAll) {
|
|
||||||
visibility = visibility || 'nobody';
|
|
||||||
} else if (rule instanceof GramJs.PrivacyValueAllowUsers) {
|
|
||||||
allowUserIds = rule.users.map((chatId) => buildApiPeerId(chatId, 'user'));
|
|
||||||
} else if (rule instanceof GramJs.PrivacyValueDisallowUsers) {
|
|
||||||
blockUserIds = rule.users.map((chatId) => buildApiPeerId(chatId, 'user'));
|
|
||||||
} else if (rule instanceof GramJs.PrivacyValueAllowChatParticipants) {
|
|
||||||
allowChatIds = rule.chats.map((chatId) => buildApiPeerId(chatId, 'chat'));
|
|
||||||
} else if (rule instanceof GramJs.PrivacyValueDisallowChatParticipants) {
|
|
||||||
blockChatIds = rule.chats.map((chatId) => buildApiPeerId(chatId, 'chat'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!visibility) {
|
|
||||||
// disallow by default.
|
|
||||||
visibility = 'nobody';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
visibility,
|
|
||||||
allowUserIds: allowUserIds || [],
|
|
||||||
allowChatIds: allowChatIds || [],
|
|
||||||
blockUserIds: blockUserIds || [],
|
|
||||||
blockChatIds: blockChatIds || [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildApiNotifyException(
|
export function buildApiNotifyException(
|
||||||
notifySettings: GramJs.TypePeerNotifySettings, peer: GramJs.TypePeer,
|
notifySettings: GramJs.TypePeerNotifySettings, peer: GramJs.TypePeer,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
|
|||||||
fullUser: {
|
fullUser: {
|
||||||
about, commonChatsCount, pinnedMsgId, botInfo, blocked,
|
about, commonChatsCount, pinnedMsgId, botInfo, blocked,
|
||||||
profilePhoto, voiceMessagesForbidden, premiumGifts,
|
profilePhoto, voiceMessagesForbidden, premiumGifts,
|
||||||
fallbackPhoto, personalPhoto, translationsDisabled,
|
fallbackPhoto, personalPhoto, translationsDisabled, storiesPinnedAvailable,
|
||||||
},
|
},
|
||||||
users,
|
users,
|
||||||
} = mtpUserFull;
|
} = mtpUserFull;
|
||||||
@ -29,6 +29,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
|
|||||||
pinnedMessageId: pinnedMsgId,
|
pinnedMessageId: pinnedMsgId,
|
||||||
isBlocked: Boolean(blocked),
|
isBlocked: Boolean(blocked),
|
||||||
noVoiceMessages: voiceMessagesForbidden,
|
noVoiceMessages: voiceMessagesForbidden,
|
||||||
|
hasPinnedStories: Boolean(storiesPinnedAvailable),
|
||||||
isTranslationDisabled: translationsDisabled,
|
isTranslationDisabled: translationsDisabled,
|
||||||
...(profilePhoto instanceof GramJs.Photo && { profilePhoto: buildApiPhoto(profilePhoto) }),
|
...(profilePhoto instanceof GramJs.Photo && { profilePhoto: buildApiPhoto(profilePhoto) }),
|
||||||
...(fallbackPhoto instanceof GramJs.Photo && { fallbackPhoto: buildApiPhoto(fallbackPhoto) }),
|
...(fallbackPhoto instanceof GramJs.Photo && { fallbackPhoto: buildApiPhoto(fallbackPhoto) }),
|
||||||
@ -44,7 +45,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
id, firstName, lastName, fake, scam,
|
id, firstName, lastName, fake, scam, support, closeFriend, storiesUnavailable, storiesMaxId,
|
||||||
} = mtpUser;
|
} = mtpUser;
|
||||||
const hasVideoAvatar = mtpUser.photo instanceof GramJs.UserProfilePhoto
|
const hasVideoAvatar = mtpUser.photo instanceof GramJs.UserProfilePhoto
|
||||||
? Boolean(mtpUser.photo.hasVideo)
|
? Boolean(mtpUser.photo.hasVideo)
|
||||||
@ -63,6 +64,8 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
|
|||||||
...(mtpUser.self && { isSelf: true }),
|
...(mtpUser.self && { isSelf: true }),
|
||||||
isPremium: Boolean(mtpUser.premium),
|
isPremium: Boolean(mtpUser.premium),
|
||||||
...(mtpUser.verified && { isVerified: true }),
|
...(mtpUser.verified && { isVerified: true }),
|
||||||
|
...(closeFriend && { isCloseFriend: true }),
|
||||||
|
...(support && { isSupport: true }),
|
||||||
...((mtpUser.contact || mtpUser.mutualContact) && { isContact: true }),
|
...((mtpUser.contact || mtpUser.mutualContact) && { isContact: true }),
|
||||||
type: userType,
|
type: userType,
|
||||||
firstName,
|
firstName,
|
||||||
@ -75,6 +78,9 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
|
|||||||
...(avatarHash && { avatarHash }),
|
...(avatarHash && { avatarHash }),
|
||||||
emojiStatus,
|
emojiStatus,
|
||||||
hasVideoAvatar,
|
hasVideoAvatar,
|
||||||
|
areStoriesHidden: Boolean(mtpUser.storiesHidden),
|
||||||
|
maxStoryId: storiesMaxId,
|
||||||
|
hasStories: Boolean(storiesMaxId) && !storiesUnavailable,
|
||||||
...(mtpUser.bot && mtpUser.botInlinePlaceholder && { botPlaceholder: mtpUser.botInlinePlaceholder }),
|
...(mtpUser.bot && mtpUser.botInlinePlaceholder && { botPlaceholder: mtpUser.botInlinePlaceholder }),
|
||||||
...(mtpUser.bot && mtpUser.botAttachMenu && { isAttachBot: mtpUser.botAttachMenu }),
|
...(mtpUser.bot && mtpUser.botAttachMenu && { isAttachBot: mtpUser.botAttachMenu }),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import BigInt from 'big-integer';
|
import BigInt from 'big-integer';
|
||||||
import { Api as GramJs } from '../../../lib/gramjs';
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
|
|
||||||
import type { ApiPrivacyKey } from '../../../types';
|
import type { ApiPrivacyKey, PrivacyVisibility } from '../../../types';
|
||||||
|
|
||||||
import { generateRandomBytes, readBigIntFromBuffer } from '../../../lib/gramjs/Helpers';
|
import { generateRandomBytes, readBigIntFromBuffer } from '../../../lib/gramjs/Helpers';
|
||||||
import type {
|
import type {
|
||||||
@ -24,6 +24,10 @@ import type {
|
|||||||
ApiReaction,
|
ApiReaction,
|
||||||
ApiFormattedText,
|
ApiFormattedText,
|
||||||
ApiBotApp,
|
ApiBotApp,
|
||||||
|
ApiStory,
|
||||||
|
ApiStorySkipped,
|
||||||
|
ApiUser,
|
||||||
|
ApiTypeReplyTo,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import {
|
import {
|
||||||
ApiMessageEntityTypes,
|
ApiMessageEntityTypes,
|
||||||
@ -282,6 +286,14 @@ export function buildFilterFromApiFolder(folder: ApiChatFolder): GramJs.DialogFi
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildInputStory(story: ApiStory | ApiStorySkipped) {
|
||||||
|
const user = localDb.users[story.userId];
|
||||||
|
return new GramJs.InputMediaStory({
|
||||||
|
userId: new GramJs.InputUser({ userId: BigInt(user!.id), accessHash: user!.accessHash! }),
|
||||||
|
id: story.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function generateRandomBigInt() {
|
export function generateRandomBigInt() {
|
||||||
return readBigIntFromBuffer(generateRandomBytes(8), true, true);
|
return readBigIntFromBuffer(generateRandomBytes(8), true, true);
|
||||||
}
|
}
|
||||||
@ -625,3 +637,82 @@ export function buildInputBotApp(app: ApiBotApp) {
|
|||||||
accessHash: BigInt(app.accessHash),
|
accessHash: BigInt(app.accessHash),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildInputReplyToMessage(replyToMsgId: number, topMsgId?: number) {
|
||||||
|
return new GramJs.InputReplyToMessage({
|
||||||
|
replyToMsgId,
|
||||||
|
topMsgId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildInputReplyToStory(userId: string, storyId: number) {
|
||||||
|
return new GramJs.InputReplyToStory({
|
||||||
|
userId: buildInputPeerFromLocalDb(userId)!,
|
||||||
|
storyId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildInputReplyTo(replyingTo: ApiTypeReplyTo) {
|
||||||
|
return 'replyingTo' in replyingTo
|
||||||
|
? buildInputReplyToMessage(replyingTo.replyingTo, replyingTo.replyingToTopId)
|
||||||
|
: buildInputReplyToStory(replyingTo.userId, replyingTo.storyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildInputPrivacyRules(
|
||||||
|
visibility: PrivacyVisibility,
|
||||||
|
allowedUserList?: ApiUser[],
|
||||||
|
deniedUserList?: ApiUser[],
|
||||||
|
) {
|
||||||
|
const rules: GramJs.TypeInputPrivacyRule[] = [];
|
||||||
|
|
||||||
|
switch (visibility) {
|
||||||
|
case 'everybody':
|
||||||
|
rules.push(new GramJs.InputPrivacyValueAllowAll());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'contacts': {
|
||||||
|
rules.push(new GramJs.InputPrivacyValueAllowContacts());
|
||||||
|
|
||||||
|
const users = deniedUserList?.reduce<GramJs.InputUser[]>((acc, { id, accessHash }) => {
|
||||||
|
acc.push(new GramJs.InputPeerUser({
|
||||||
|
userId: buildMtpPeerId(id, 'user'),
|
||||||
|
accessHash: BigInt(accessHash!),
|
||||||
|
}));
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (users?.length) {
|
||||||
|
rules.push(new GramJs.InputPrivacyValueDisallowUsers({ users }));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'closeFriends':
|
||||||
|
rules.push(new GramJs.InputPrivacyValueAllowCloseFriends());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'nonContacts':
|
||||||
|
rules.push(new GramJs.InputPrivacyValueDisallowContacts());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'selectedContacts': {
|
||||||
|
const users = (allowedUserList || []).reduce<GramJs.InputUser[]>((acc, { id, accessHash }) => {
|
||||||
|
acc.push(new GramJs.InputPeerUser({
|
||||||
|
userId: buildMtpPeerId(id, 'user'),
|
||||||
|
accessHash: BigInt(accessHash!),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
rules.push(new GramJs.InputPrivacyValueAllowUsers({ users }));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'nobody':
|
||||||
|
rules.push(new GramJs.InputPrivacyValueDisallowAll());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Api as GramJs } from '../../lib/gramjs';
|
import { Api as GramJs } from '../../lib/gramjs';
|
||||||
import localDb from './localDb';
|
import localDb from './localDb';
|
||||||
|
import type { StoryRepairInfo } from './localDb';
|
||||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './apiBuilders/peers';
|
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './apiBuilders/peers';
|
||||||
|
|
||||||
const LOG_BACKGROUND = '#111111DD';
|
const LOG_BACKGROUND = '#111111DD';
|
||||||
@ -78,6 +79,36 @@ export function addMessageToLocalDb(message: GramJs.Message | GramJs.MessageServ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function addStoryToLocalDb(story: GramJs.TypeStoryItem, userId: string) {
|
||||||
|
if (!(story instanceof GramJs.StoryItem)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const storyData = {
|
||||||
|
id: story.id,
|
||||||
|
userId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (story.media instanceof GramJs.MessageMediaPhoto) {
|
||||||
|
const photo = story.media.photo as GramJs.Photo & StoryRepairInfo;
|
||||||
|
photo.storyData = storyData;
|
||||||
|
addPhotoToLocalDb(photo);
|
||||||
|
}
|
||||||
|
if (story.media instanceof GramJs.MessageMediaDocument) {
|
||||||
|
if (story.media.document instanceof GramJs.Document) {
|
||||||
|
const doc = story.media.document as GramJs.Document & StoryRepairInfo;
|
||||||
|
doc.storyData = storyData;
|
||||||
|
localDb.documents[String(story.media.document.id)] = doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (story.media.altDocument instanceof GramJs.Document) {
|
||||||
|
const doc = story.media.altDocument as GramJs.Document & StoryRepairInfo;
|
||||||
|
doc.storyData = storyData;
|
||||||
|
localDb.documents[String(story.media.altDocument.id)] = doc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function addPhotoToLocalDb(photo: GramJs.TypePhoto) {
|
export function addPhotoToLocalDb(photo: GramJs.TypePhoto) {
|
||||||
if (photo instanceof GramJs.Photo) {
|
if (photo instanceof GramJs.Photo) {
|
||||||
localDb.photos[String(photo.id)] = photo;
|
localDb.photos[String(photo.id)] = photo;
|
||||||
|
|||||||
@ -8,16 +8,22 @@ import { throttle } from '../../util/schedulers';
|
|||||||
// eslint-disable-next-line no-restricted-globals
|
// eslint-disable-next-line no-restricted-globals
|
||||||
const IS_MULTITAB_SUPPORTED = 'BroadcastChannel' in self;
|
const IS_MULTITAB_SUPPORTED = 'BroadcastChannel' in self;
|
||||||
|
|
||||||
|
export type StoryRepairInfo = {
|
||||||
|
storyData?: {
|
||||||
|
userId: string;
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export interface LocalDb {
|
export interface LocalDb {
|
||||||
// Used for loading avatars and media through in-memory Gram JS instances.
|
// Used for loading avatars and media through in-memory Gram JS instances.
|
||||||
chats: Record<string, GramJs.Chat | GramJs.Channel>;
|
chats: Record<string, GramJs.Chat | GramJs.Channel>;
|
||||||
users: Record<string, GramJs.User>;
|
users: Record<string, GramJs.User>;
|
||||||
messages: Record<string, GramJs.Message | GramJs.MessageService>;
|
messages: Record<string, GramJs.Message | GramJs.MessageService>;
|
||||||
documents: Record<string, GramJs.Document>;
|
documents: Record<string, GramJs.Document & StoryRepairInfo>;
|
||||||
stickerSets: Record<string, GramJs.StickerSet>;
|
stickerSets: Record<string, GramJs.StickerSet>;
|
||||||
photos: Record<string, GramJs.Photo>;
|
photos: Record<string, GramJs.Photo & StoryRepairInfo>;
|
||||||
webDocuments: Record<string, GramJs.TypeWebDocument>;
|
webDocuments: Record<string, GramJs.TypeWebDocument>;
|
||||||
|
|
||||||
commonBoxState: Record<string, number>;
|
commonBoxState: Record<string, number>;
|
||||||
channelPtsById: Record<string, number>;
|
channelPtsById: Record<string, number>;
|
||||||
}
|
}
|
||||||
@ -79,7 +85,7 @@ function convertToVirtualClass(value: any): any {
|
|||||||
|
|
||||||
function createLocalDbInitial(initial?: LocalDb): LocalDb {
|
function createLocalDbInitial(initial?: LocalDb): LocalDb {
|
||||||
return [
|
return [
|
||||||
'localMessages', 'chats', 'users', 'messages', 'documents', 'stickerSets', 'photos', 'webDocuments',
|
'localMessages', 'chats', 'users', 'messages', 'documents', 'stickerSets', 'photos', 'webDocuments', 'stories',
|
||||||
'commonBoxState', 'channelPtsById',
|
'commonBoxState', 'channelPtsById',
|
||||||
]
|
]
|
||||||
.reduce((acc: Record<string, any>, key) => {
|
.reduce((acc: Record<string, any>, key) => {
|
||||||
|
|||||||
@ -10,7 +10,12 @@ import localDb from '../localDb';
|
|||||||
import { WEB_APP_PLATFORM } from '../../../config';
|
import { WEB_APP_PLATFORM } from '../../../config';
|
||||||
import { invokeRequest } from './client';
|
import { invokeRequest } from './client';
|
||||||
import {
|
import {
|
||||||
buildInputBotApp, buildInputEntity, buildInputPeer, buildInputThemeParams, generateRandomBigInt,
|
buildInputBotApp,
|
||||||
|
buildInputEntity,
|
||||||
|
buildInputPeer,
|
||||||
|
buildInputReplyToMessage,
|
||||||
|
buildInputThemeParams,
|
||||||
|
generateRandomBigInt,
|
||||||
} from '../gramjsBuilders';
|
} from '../gramjsBuilders';
|
||||||
import { buildApiUser } from '../apiBuilders/users';
|
import { buildApiUser } from '../apiBuilders/users';
|
||||||
import {
|
import {
|
||||||
@ -189,13 +194,12 @@ export async function requestWebView({
|
|||||||
silent: isSilent || undefined,
|
silent: isSilent || undefined,
|
||||||
peer: buildInputPeer(peer.id, peer.accessHash),
|
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||||
bot: buildInputPeer(bot.id, bot.accessHash),
|
bot: buildInputPeer(bot.id, bot.accessHash),
|
||||||
replyToMsgId: replyToMessageId,
|
|
||||||
url,
|
url,
|
||||||
startParam,
|
startParam,
|
||||||
themeParams: theme ? buildInputThemeParams(theme) : undefined,
|
themeParams: theme ? buildInputThemeParams(theme) : undefined,
|
||||||
fromBotMenu: isFromBotMenu || undefined,
|
fromBotMenu: isFromBotMenu || undefined,
|
||||||
platform: WEB_APP_PLATFORM,
|
platform: WEB_APP_PLATFORM,
|
||||||
...(threadId && { topMsgId: threadId }),
|
...(replyToMessageId && { replyTo: buildInputReplyToMessage(replyToMessageId, threadId) }),
|
||||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -294,8 +298,7 @@ export function prolongWebView({
|
|||||||
peer: buildInputPeer(peer.id, peer.accessHash),
|
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||||
bot: buildInputPeer(bot.id, bot.accessHash),
|
bot: buildInputPeer(bot.id, bot.accessHash),
|
||||||
queryId: BigInt(queryId),
|
queryId: BigInt(queryId),
|
||||||
replyToMsgId: replyToMessageId,
|
...(replyToMessageId && { replyTo: buildInputReplyToMessage(replyToMessageId, threadId) }),
|
||||||
...(threadId && { topMsgId: threadId }),
|
|
||||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
sessions, Api as GramJs, connection,
|
sessions, Api as GramJs,
|
||||||
} from '../../../lib/gramjs';
|
} from '../../../lib/gramjs';
|
||||||
import TelegramClient from '../../../lib/gramjs/client/TelegramClient';
|
import TelegramClient from '../../../lib/gramjs/client/TelegramClient';
|
||||||
|
|
||||||
@ -26,7 +26,8 @@ import { buildApiUser, buildApiUserFullInfo } from '../apiBuilders/users';
|
|||||||
import localDb, { clearLocalDb } from '../localDb';
|
import localDb, { clearLocalDb } from '../localDb';
|
||||||
import { buildApiPeerId } from '../apiBuilders/peers';
|
import { buildApiPeerId } from '../apiBuilders/peers';
|
||||||
import {
|
import {
|
||||||
addMessageToLocalDb, addUserToLocalDb, isResponseUpdate, log,
|
addEntitiesToLocalDb,
|
||||||
|
addMessageToLocalDb, addStoryToLocalDb, addUserToLocalDb, isResponseUpdate, log,
|
||||||
} from '../helpers';
|
} from '../helpers';
|
||||||
import { ChatAbortController } from '../ChatAbortController';
|
import { ChatAbortController } from '../ChatAbortController';
|
||||||
import {
|
import {
|
||||||
@ -52,7 +53,6 @@ const ABORT_CONTROLLERS = new Map<string, AbortController>();
|
|||||||
|
|
||||||
let onUpdate: OnApiUpdate;
|
let onUpdate: OnApiUpdate;
|
||||||
let client: TelegramClient;
|
let client: TelegramClient;
|
||||||
let isConnected = false;
|
|
||||||
let currentUserId: string | undefined;
|
let currentUserId: string | undefined;
|
||||||
|
|
||||||
export async function init(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs) {
|
export async function init(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs) {
|
||||||
@ -197,9 +197,7 @@ type UpdateConfig = GramJs.UpdateConfig & { _entities?: (GramJs.TypeUser | GramJ
|
|||||||
export function handleGramJsUpdate(update: any) {
|
export function handleGramJsUpdate(update: any) {
|
||||||
processUpdate(update);
|
processUpdate(update);
|
||||||
|
|
||||||
if (update instanceof connection.UpdateConnectionState) {
|
if (update instanceof GramJs.UpdatesTooLong) {
|
||||||
isConnected = update.state === connection.UpdateConnectionState.connected;
|
|
||||||
} else if (update instanceof GramJs.UpdatesTooLong) {
|
|
||||||
void handleTerminatedSession();
|
void handleTerminatedSession();
|
||||||
} else {
|
} else {
|
||||||
const updates = 'updates' in update ? update.updates : [update];
|
const updates = 'updates' in update ? update.updates : [update];
|
||||||
@ -314,16 +312,18 @@ export function invokeRequestBeacon<T extends GramJs.AnyRequest>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadMedia(
|
export async function downloadMedia(
|
||||||
args: { url: string; mediaFormat: ApiMediaFormat; start?: number; end?: number; isHtmlAllowed?: boolean },
|
args: {
|
||||||
|
url: string; mediaFormat: ApiMediaFormat; start?: number; end?: number; isHtmlAllowed?: boolean;
|
||||||
|
},
|
||||||
onProgress?: ApiOnProgress,
|
onProgress?: ApiOnProgress,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
return (await downloadMediaWithClient(args, client, isConnected, onProgress));
|
return (await downloadMediaWithClient(args, client, onProgress));
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.message.startsWith('FILE_REFERENCE')) {
|
if (err.message.startsWith('FILE_REFERENCE')) {
|
||||||
const isFileReferenceRepaired = await repairFileReference({ url: args.url });
|
const isFileReferenceRepaired = await repairFileReference({ url: args.url });
|
||||||
if (isFileReferenceRepaired) {
|
if (isFileReferenceRepaired) {
|
||||||
return downloadMediaWithClient(args, client, isConnected, onProgress);
|
return downloadMediaWithClient(args, client, onProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
@ -439,8 +439,21 @@ export async function repairFileReference({
|
|||||||
entityType, entityId, mediaMatchType,
|
entityType, entityId, mediaMatchType,
|
||||||
} = parsed;
|
} = parsed;
|
||||||
|
|
||||||
if (mediaMatchType === 'file') {
|
if (mediaMatchType === 'document' || mediaMatchType === 'photo') {
|
||||||
return false;
|
const entity = mediaMatchType === 'document' ? localDb.documents[entityId] : localDb.photos[entityId];
|
||||||
|
if (!entity.storyData) return false;
|
||||||
|
const user = localDb.users[entity.storyData.userId];
|
||||||
|
if (!user?.accessHash) return false;
|
||||||
|
|
||||||
|
const result = await invokeRequest(new GramJs.stories.GetStoriesByID({
|
||||||
|
userId: new GramJs.InputUser({ userId: user.id, accessHash: user.accessHash }),
|
||||||
|
id: [entity.storyData.id],
|
||||||
|
}));
|
||||||
|
if (!result) return false;
|
||||||
|
|
||||||
|
addEntitiesToLocalDb(result.users);
|
||||||
|
result.stories.forEach((story) => addStoryToLocalDb(story, entity.storyData!.userId));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entityType === 'msg') {
|
if (entityType === 'msg') {
|
||||||
@ -470,6 +483,8 @@ export async function repairFileReference({
|
|||||||
|
|
||||||
const message = result.messages[0];
|
const message = result.messages[0];
|
||||||
if (message instanceof GramJs.MessageEmpty) return false;
|
if (message instanceof GramJs.MessageEmpty) return false;
|
||||||
|
addEntitiesToLocalDb(result.users);
|
||||||
|
addEntitiesToLocalDb(result.chats);
|
||||||
addMessageToLocalDb(message);
|
addMessageToLocalDb(message);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,6 +39,7 @@ export {
|
|||||||
export {
|
export {
|
||||||
fetchFullUser, fetchNearestCountry, fetchTopUsers, fetchContactList, fetchUsers,
|
fetchFullUser, fetchNearestCountry, fetchTopUsers, fetchContactList, fetchUsers,
|
||||||
updateContact, importContact, deleteContact, fetchProfilePhotos, fetchCommonChats, reportSpam, updateEmojiStatus,
|
updateContact, importContact, deleteContact, fetchProfilePhotos, fetchCommonChats, reportSpam, updateEmojiStatus,
|
||||||
|
saveCloseFriends,
|
||||||
} from './users';
|
} from './users';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -107,3 +108,9 @@ export {
|
|||||||
export {
|
export {
|
||||||
broadcastLocalDbUpdateFull,
|
broadcastLocalDbUpdateFull,
|
||||||
} from '../localDb';
|
} from '../localDb';
|
||||||
|
|
||||||
|
export {
|
||||||
|
fetchAllStories, fetchUserStories, fetchUserPinnedStories, fetchUserStoriesByIds, viewStory, markStoryRead,
|
||||||
|
deleteStory, toggleStoryPinned, fetchStorySeenBy, fetchStoryLink, fetchStoriesArchive, reportStory, editStoryPrivacy,
|
||||||
|
toggleStoriesHidden, fetchStoriesMaxIds,
|
||||||
|
} from './stories';
|
||||||
|
|||||||
@ -27,12 +27,11 @@ export default async function downloadMedia(
|
|||||||
url: string; mediaFormat: ApiMediaFormat; start?: number; end?: number; isHtmlAllowed?: boolean;
|
url: string; mediaFormat: ApiMediaFormat; start?: number; end?: number; isHtmlAllowed?: boolean;
|
||||||
},
|
},
|
||||||
client: TelegramClient,
|
client: TelegramClient,
|
||||||
isConnected: boolean,
|
|
||||||
onProgress?: ApiOnProgress,
|
onProgress?: ApiOnProgress,
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
data, mimeType, fullSize,
|
data, mimeType, fullSize,
|
||||||
} = await download(url, client, isConnected, onProgress, start, end, mediaFormat, isHtmlAllowed) || {};
|
} = await download(url, client, onProgress, start, end, mediaFormat, isHtmlAllowed) || {};
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -71,7 +70,6 @@ export type EntityType = (
|
|||||||
async function download(
|
async function download(
|
||||||
url: string,
|
url: string,
|
||||||
client: TelegramClient,
|
client: TelegramClient,
|
||||||
isConnected: boolean,
|
|
||||||
onProgress?: ApiOnProgress,
|
onProgress?: ApiOnProgress,
|
||||||
start?: number,
|
start?: number,
|
||||||
end?: number,
|
end?: number,
|
||||||
@ -218,14 +216,17 @@ function getMessageMediaMimeType(message: GramJs.Message, sizeType?: string) {
|
|||||||
return 'image/png';
|
return 'image/png';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.media instanceof GramJs.MessageMediaDocument && message.media.document instanceof GramJs.Document) {
|
if (message.media instanceof GramJs.MessageMediaDocument) {
|
||||||
if (sizeType) {
|
const document = message.media.document;
|
||||||
return message.media.document!.attributes.some((a) => a instanceof GramJs.DocumentAttributeSticker)
|
if (document instanceof GramJs.Document) {
|
||||||
? 'image/webp'
|
if (sizeType) {
|
||||||
: 'image/jpeg';
|
return document.attributes.some((a) => a instanceof GramJs.DocumentAttributeSticker)
|
||||||
}
|
? 'image/webp'
|
||||||
|
: 'image/jpeg';
|
||||||
|
}
|
||||||
|
|
||||||
return message.media.document!.mimeType;
|
return document.mimeType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.media instanceof GramJs.MessageMediaWebPage
|
if (message.media instanceof GramJs.MessageMediaWebPage
|
||||||
|
|||||||
@ -17,6 +17,9 @@ import type {
|
|||||||
ApiContact,
|
ApiContact,
|
||||||
ApiPoll,
|
ApiPoll,
|
||||||
ApiFormattedText,
|
ApiFormattedText,
|
||||||
|
ApiTypeReplyTo,
|
||||||
|
ApiStory,
|
||||||
|
ApiStorySkipped,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import {
|
import {
|
||||||
MAIN_THREAD_ID,
|
MAIN_THREAD_ID,
|
||||||
@ -57,6 +60,8 @@ import {
|
|||||||
buildInputPollFromExisting,
|
buildInputPollFromExisting,
|
||||||
buildInputTextWithEntities,
|
buildInputTextWithEntities,
|
||||||
buildMessageFromUpdate,
|
buildMessageFromUpdate,
|
||||||
|
buildInputStory,
|
||||||
|
buildInputReplyTo,
|
||||||
} from '../gramjsBuilders';
|
} from '../gramjsBuilders';
|
||||||
import { buildApiChatFromPreview, buildApiSendAsPeerId } from '../apiBuilders/chats';
|
import { buildApiChatFromPreview, buildApiSendAsPeerId } from '../apiBuilders/chats';
|
||||||
import { fetchFile } from '../../../util/files';
|
import { fetchFile } from '../../../util/files';
|
||||||
@ -233,9 +238,9 @@ export function sendMessage(
|
|||||||
text,
|
text,
|
||||||
entities,
|
entities,
|
||||||
replyingTo,
|
replyingTo,
|
||||||
replyingToTopId,
|
|
||||||
attachment,
|
attachment,
|
||||||
sticker,
|
sticker,
|
||||||
|
story,
|
||||||
gif,
|
gif,
|
||||||
poll,
|
poll,
|
||||||
contact,
|
contact,
|
||||||
@ -250,10 +255,10 @@ export function sendMessage(
|
|||||||
lastMessageId?: number;
|
lastMessageId?: number;
|
||||||
text?: string;
|
text?: string;
|
||||||
entities?: ApiMessageEntity[];
|
entities?: ApiMessageEntity[];
|
||||||
replyingTo?: number;
|
replyingTo?: ApiTypeReplyTo;
|
||||||
replyingToTopId?: number;
|
|
||||||
attachment?: ApiAttachment;
|
attachment?: ApiAttachment;
|
||||||
sticker?: ApiSticker;
|
sticker?: ApiSticker;
|
||||||
|
story?: ApiStory | ApiStorySkipped;
|
||||||
gif?: ApiVideo;
|
gif?: ApiVideo;
|
||||||
poll?: ApiNewPoll;
|
poll?: ApiNewPoll;
|
||||||
contact?: ApiContact;
|
contact?: ApiContact;
|
||||||
@ -271,7 +276,6 @@ export function sendMessage(
|
|||||||
text,
|
text,
|
||||||
entities,
|
entities,
|
||||||
replyingTo,
|
replyingTo,
|
||||||
replyingToTopId,
|
|
||||||
attachment,
|
attachment,
|
||||||
sticker,
|
sticker,
|
||||||
gif,
|
gif,
|
||||||
@ -280,6 +284,7 @@ export function sendMessage(
|
|||||||
groupedId,
|
groupedId,
|
||||||
scheduledAt,
|
scheduledAt,
|
||||||
sendAs,
|
sendAs,
|
||||||
|
story,
|
||||||
);
|
);
|
||||||
|
|
||||||
onUpdate({
|
onUpdate({
|
||||||
@ -310,7 +315,6 @@ export function sendMessage(
|
|||||||
text,
|
text,
|
||||||
entities,
|
entities,
|
||||||
replyingTo,
|
replyingTo,
|
||||||
replyingToTopId,
|
|
||||||
attachment: attachment!,
|
attachment: attachment!,
|
||||||
groupedId,
|
groupedId,
|
||||||
isSilent,
|
isSilent,
|
||||||
@ -339,6 +343,8 @@ export function sendMessage(
|
|||||||
media = buildInputMediaDocument(gif);
|
media = buildInputMediaDocument(gif);
|
||||||
} else if (poll) {
|
} else if (poll) {
|
||||||
media = buildInputPoll(poll, randomId);
|
media = buildInputPoll(poll, randomId);
|
||||||
|
} else if (story) {
|
||||||
|
media = buildInputStory(story);
|
||||||
} else if (contact) {
|
} else if (contact) {
|
||||||
media = new GramJs.InputMediaContact({
|
media = new GramJs.InputMediaContact({
|
||||||
phoneNumber: contact.phoneNumber,
|
phoneNumber: contact.phoneNumber,
|
||||||
@ -349,6 +355,7 @@ export function sendMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const RequestClass = media ? GramJs.messages.SendMedia : GramJs.messages.SendMessage;
|
const RequestClass = media ? GramJs.messages.SendMedia : GramJs.messages.SendMessage;
|
||||||
|
const replyTo = replyingTo ? buildInputReplyTo(replyingTo) : undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const update = await invokeRequest(new RequestClass({
|
const update = await invokeRequest(new RequestClass({
|
||||||
@ -359,8 +366,7 @@ export function sendMessage(
|
|||||||
randomId,
|
randomId,
|
||||||
...(isSilent && { silent: isSilent }),
|
...(isSilent && { silent: isSilent }),
|
||||||
...(scheduledAt && { scheduleDate: scheduledAt }),
|
...(scheduledAt && { scheduleDate: scheduledAt }),
|
||||||
...(replyingTo && { replyToMsgId: replyingTo }),
|
...(replyTo && { replyTo }),
|
||||||
...(replyingToTopId && { topMsgId: replyingToTopId }),
|
|
||||||
...(media && { media }),
|
...(media && { media }),
|
||||||
...(noWebPage && { noWebpage: noWebPage }),
|
...(noWebPage && { noWebpage: noWebPage }),
|
||||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||||
@ -396,7 +402,6 @@ function sendGroupedMedia(
|
|||||||
text,
|
text,
|
||||||
entities,
|
entities,
|
||||||
replyingTo,
|
replyingTo,
|
||||||
replyingToTopId,
|
|
||||||
attachment,
|
attachment,
|
||||||
groupedId,
|
groupedId,
|
||||||
isSilent,
|
isSilent,
|
||||||
@ -406,8 +411,7 @@ function sendGroupedMedia(
|
|||||||
chat: ApiChat;
|
chat: ApiChat;
|
||||||
text?: string;
|
text?: string;
|
||||||
entities?: ApiMessageEntity[];
|
entities?: ApiMessageEntity[];
|
||||||
replyingTo?: number;
|
replyingTo?: ApiTypeReplyTo;
|
||||||
replyingToTopId?: number;
|
|
||||||
attachment: ApiAttachment;
|
attachment: ApiAttachment;
|
||||||
groupedId: string;
|
groupedId: string;
|
||||||
isSilent?: boolean;
|
isSilent?: boolean;
|
||||||
@ -479,13 +483,13 @@ function sendGroupedMedia(
|
|||||||
|
|
||||||
const { singleMediaByIndex, localMessages } = groupedUploads[groupedId];
|
const { singleMediaByIndex, localMessages } = groupedUploads[groupedId];
|
||||||
delete groupedUploads[groupedId];
|
delete groupedUploads[groupedId];
|
||||||
|
const replyTo = replyingTo ? buildInputReplyTo(replyingTo) : undefined;
|
||||||
|
|
||||||
const update = await invokeRequest(new GramJs.messages.SendMultiMedia({
|
const update = await invokeRequest(new GramJs.messages.SendMultiMedia({
|
||||||
clearDraft: true,
|
clearDraft: true,
|
||||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
multiMedia: Object.values(singleMediaByIndex), // Object keys are usually ordered
|
multiMedia: Object.values(singleMediaByIndex), // Object keys are usually ordered
|
||||||
replyToMsgId: replyingTo,
|
...(replyingTo && { replyTo }),
|
||||||
...(replyingToTopId && { topMsgId: replyingToTopId }),
|
|
||||||
...(isSilent && { silent: isSilent }),
|
...(isSilent && { silent: isSilent }),
|
||||||
...(scheduledAt && { scheduleDate: scheduledAt }),
|
...(scheduledAt && { scheduleDate: scheduledAt }),
|
||||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||||
|
|||||||
@ -22,9 +22,8 @@ import {
|
|||||||
buildApiSession,
|
buildApiSession,
|
||||||
buildApiWallpaper,
|
buildApiWallpaper,
|
||||||
buildApiWebSession, buildLangPack, buildLangPackString,
|
buildApiWebSession, buildLangPack, buildLangPackString,
|
||||||
buildPrivacyRules,
|
|
||||||
} from '../apiBuilders/misc';
|
} from '../apiBuilders/misc';
|
||||||
|
import { buildPrivacyRules } from '../apiBuilders/messages';
|
||||||
import { buildApiPhoto } from '../apiBuilders/common';
|
import { buildApiPhoto } from '../apiBuilders/common';
|
||||||
import { buildApiUser } from '../apiBuilders/users';
|
import { buildApiUser } from '../apiBuilders/users';
|
||||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||||
@ -620,7 +619,7 @@ export async function updateGlobalPrivacySettings({ shouldArchiveAndMuteNewNonCo
|
|||||||
}) {
|
}) {
|
||||||
const result = await invokeRequest(new GramJs.account.SetGlobalPrivacySettings({
|
const result = await invokeRequest(new GramJs.account.SetGlobalPrivacySettings({
|
||||||
settings: new GramJs.GlobalPrivacySettings({
|
settings: new GramJs.GlobalPrivacySettings({
|
||||||
archiveAndMuteNewNoncontactPeers: shouldArchiveAndMuteNewNonContact,
|
...(shouldArchiveAndMuteNewNonContact && { archiveAndMuteNewNoncontactPeers: true }),
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
343
src/api/gramjs/methods/stories.ts
Normal file
343
src/api/gramjs/methods/stories.ts
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
import BigInt from 'big-integer';
|
||||||
|
import { invokeRequest } from './client';
|
||||||
|
import type {
|
||||||
|
ApiUser, ApiUserStories, ApiReportReason, ApiTypeStory,
|
||||||
|
} from '../../types';
|
||||||
|
import type { PrivacyVisibility } from '../../../types';
|
||||||
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
|
import { addEntitiesToLocalDb, addStoryToLocalDb } from '../helpers';
|
||||||
|
import { buildApiUser } from '../apiBuilders/users';
|
||||||
|
import { buildApiStory, buildApiUsersStories } from '../apiBuilders/messages';
|
||||||
|
import { buildApiPeerId } from '../apiBuilders/peers';
|
||||||
|
import {
|
||||||
|
buildInputPeer,
|
||||||
|
buildInputPeerFromLocalDb,
|
||||||
|
buildInputPrivacyRules,
|
||||||
|
buildInputReportReason,
|
||||||
|
} from '../gramjsBuilders';
|
||||||
|
import { STORY_LIST_LIMIT } from '../../../config';
|
||||||
|
import { buildCollectionByCallback } from '../../../util/iteratees';
|
||||||
|
|
||||||
|
export async function fetchAllStories({
|
||||||
|
stateHash,
|
||||||
|
isFirstRequest = false,
|
||||||
|
isHidden = false,
|
||||||
|
}: {
|
||||||
|
isFirstRequest?: boolean;
|
||||||
|
stateHash?: string;
|
||||||
|
isHidden?: boolean;
|
||||||
|
}): Promise<
|
||||||
|
undefined
|
||||||
|
| { state: string }
|
||||||
|
| { users: ApiUser[]; userStories: Record<string, ApiUserStories>; hasMore?: true; state: string }> {
|
||||||
|
const params: ConstructorParameters<typeof GramJs.stories.GetAllStories>[0] = isFirstRequest
|
||||||
|
? (isHidden ? { hidden: true } : {})
|
||||||
|
: { state: stateHash, next: true, ...(isHidden && { hidden: true }) };
|
||||||
|
const result = await invokeRequest(new GramJs.stories.GetAllStories(params));
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result instanceof GramJs.stories.AllStoriesNotModified) {
|
||||||
|
return {
|
||||||
|
state: result.state,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
addEntitiesToLocalDb(result.users);
|
||||||
|
result.userStories.forEach((userStories) => (
|
||||||
|
userStories.stories.forEach((story) => addStoryToLocalDb(story, buildApiPeerId(userStories.userId, 'user')))
|
||||||
|
));
|
||||||
|
|
||||||
|
const allUserStories = result.userStories.reduce<Record<string, ApiUserStories>>((acc, userStories) => {
|
||||||
|
const userId = buildApiPeerId(userStories.userId, 'user');
|
||||||
|
const stories = buildApiUsersStories(userStories);
|
||||||
|
const { pinnedIds, orderedIds, lastUpdatedAt } = Object.values(stories).reduce<
|
||||||
|
{
|
||||||
|
pinnedIds: number[];
|
||||||
|
orderedIds: number[];
|
||||||
|
lastUpdatedAt?: number;
|
||||||
|
}
|
||||||
|
>((dataAcc, story) => {
|
||||||
|
if ('isPinned' in story && story.isPinned) {
|
||||||
|
dataAcc.pinnedIds.push(story.id);
|
||||||
|
}
|
||||||
|
if (!('isDeleted' in story)) {
|
||||||
|
dataAcc.orderedIds.push(story.id);
|
||||||
|
dataAcc.lastUpdatedAt = Math.max(story.date, dataAcc.lastUpdatedAt || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataAcc;
|
||||||
|
}, {
|
||||||
|
pinnedIds: [],
|
||||||
|
orderedIds: [],
|
||||||
|
lastUpdatedAt: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (orderedIds.length === 0) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[userId] = {
|
||||||
|
byId: stories,
|
||||||
|
orderedIds,
|
||||||
|
pinnedIds,
|
||||||
|
lastUpdatedAt,
|
||||||
|
lastReadId: userStories.maxReadId,
|
||||||
|
};
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
users: result.users.map(buildApiUser).filter(Boolean),
|
||||||
|
userStories: allUserStories,
|
||||||
|
hasMore: result.hasMore,
|
||||||
|
state: result.state,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchUserStories({
|
||||||
|
user,
|
||||||
|
}: {
|
||||||
|
user: ApiUser;
|
||||||
|
}) {
|
||||||
|
const result = await invokeRequest(new GramJs.stories.GetUserStories({
|
||||||
|
userId: buildInputPeer(user.id, user.accessHash),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEntitiesToLocalDb(result.users);
|
||||||
|
result.stories.stories.forEach((story) => addStoryToLocalDb(story, user.id));
|
||||||
|
|
||||||
|
const users = result.users.map(buildApiUser).filter(Boolean);
|
||||||
|
const stories = buildCollectionByCallback(result.stories.stories, (story) => (
|
||||||
|
[story.id, buildApiStory(user.id, story)]
|
||||||
|
));
|
||||||
|
|
||||||
|
return {
|
||||||
|
users,
|
||||||
|
stories,
|
||||||
|
lastReadStoryId: result.stories.maxReadId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUserPinnedStories({
|
||||||
|
user, offsetId,
|
||||||
|
}: {
|
||||||
|
user: ApiUser;
|
||||||
|
offsetId?: number;
|
||||||
|
}) {
|
||||||
|
return fetchCommonStoriesRequest({
|
||||||
|
method: new GramJs.stories.GetPinnedStories({
|
||||||
|
userId: buildInputPeer(user.id, user.accessHash),
|
||||||
|
offsetId,
|
||||||
|
limit: STORY_LIST_LIMIT,
|
||||||
|
}),
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchStoriesArchive({
|
||||||
|
currentUserId,
|
||||||
|
offsetId,
|
||||||
|
}: {
|
||||||
|
currentUserId: string;
|
||||||
|
offsetId?: number;
|
||||||
|
}) {
|
||||||
|
return fetchCommonStoriesRequest({
|
||||||
|
method: new GramJs.stories.GetStoriesArchive({
|
||||||
|
offsetId,
|
||||||
|
limit: STORY_LIST_LIMIT,
|
||||||
|
}),
|
||||||
|
userId: currentUserId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchUserStoriesByIds({ user, ids }: { user: ApiUser; ids: number[] }) {
|
||||||
|
const result = await invokeRequest(new GramJs.stories.GetStoriesByID({
|
||||||
|
userId: buildInputPeer(user.id, user.accessHash),
|
||||||
|
id: ids,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEntitiesToLocalDb(result.users);
|
||||||
|
result.stories.forEach((story) => addStoryToLocalDb(story, user.id));
|
||||||
|
|
||||||
|
const users = result.users.map(buildApiUser).filter(Boolean);
|
||||||
|
const stories = ids.reduce<Record<string, ApiTypeStory>>((acc, id) => {
|
||||||
|
const story = result.stories.find(({ id: currentId }) => currentId === id);
|
||||||
|
if (story) {
|
||||||
|
acc[id] = buildApiStory(user.id, story);
|
||||||
|
} else {
|
||||||
|
acc[id] = {
|
||||||
|
id,
|
||||||
|
userId: user.id,
|
||||||
|
isDeleted: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
users,
|
||||||
|
stories,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function viewStory({ user, storyId }: { user: ApiUser; storyId: number }) {
|
||||||
|
return invokeRequest(new GramJs.stories.IncrementStoryViews({
|
||||||
|
userId: buildInputPeer(user.id, user.accessHash),
|
||||||
|
id: [storyId],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markStoryRead({ user, storyId }: { user: ApiUser; storyId: number }) {
|
||||||
|
return invokeRequest(new GramJs.stories.ReadStories({
|
||||||
|
userId: buildInputPeer(user.id, user.accessHash),
|
||||||
|
maxId: storyId,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteStory({ storyId }: { storyId: number }) {
|
||||||
|
return invokeRequest(new GramJs.stories.DeleteStories({ id: [storyId] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleStoryPinned({ storyId, isPinned }: { storyId: number; isPinned?: boolean }) {
|
||||||
|
return invokeRequest(new GramJs.stories.TogglePinned({ id: [storyId], pinned: isPinned }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchStorySeenBy({
|
||||||
|
storyId, limit = STORY_LIST_LIMIT, offsetDate = 0, offsetId = 0,
|
||||||
|
}: {
|
||||||
|
storyId: number;
|
||||||
|
limit?: number;
|
||||||
|
offsetDate?: number;
|
||||||
|
offsetId?: number;
|
||||||
|
}) {
|
||||||
|
const result = await invokeRequest(new GramJs.stories.GetStoryViewsList({
|
||||||
|
id: storyId,
|
||||||
|
limit,
|
||||||
|
offsetDate,
|
||||||
|
offsetId: BigInt(offsetId),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEntitiesToLocalDb(result.users);
|
||||||
|
const users = result.users.map(buildApiUser).filter(Boolean);
|
||||||
|
const seenByDates = result.views.reduce<Record<string, number>>((acc, view) => {
|
||||||
|
acc[buildApiPeerId(view.userId, 'user')] = view.date;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return { users, seenByDates, count: result.count };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchStoryLink({ userId, storyId }: { userId: string; storyId: number }) {
|
||||||
|
const inputUser = buildInputPeerFromLocalDb(userId);
|
||||||
|
if (!inputUser) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await invokeRequest(new GramJs.stories.ExportStoryLink({
|
||||||
|
userId: inputUser,
|
||||||
|
id: storyId,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.link;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reportStory({
|
||||||
|
user,
|
||||||
|
storyId,
|
||||||
|
reason,
|
||||||
|
description,
|
||||||
|
}: {
|
||||||
|
user: ApiUser; storyId: number; reason: ApiReportReason; description?: string;
|
||||||
|
}) {
|
||||||
|
return invokeRequest(new GramJs.stories.Report({
|
||||||
|
userId: buildInputPeer(user.id, user.accessHash),
|
||||||
|
id: [storyId],
|
||||||
|
reason: buildInputReportReason(reason),
|
||||||
|
message: description,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editStoryPrivacy({
|
||||||
|
id, visibility, allowedUserList, deniedUserList,
|
||||||
|
}: {
|
||||||
|
id: number;
|
||||||
|
visibility: PrivacyVisibility;
|
||||||
|
allowedUserList?: ApiUser[];
|
||||||
|
deniedUserList?: ApiUser[];
|
||||||
|
}) {
|
||||||
|
return invokeRequest(new GramJs.stories.EditStory({
|
||||||
|
id,
|
||||||
|
privacyRules: buildInputPrivacyRules(visibility, allowedUserList, deniedUserList),
|
||||||
|
}), {
|
||||||
|
shouldReturnTrue: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleStoriesHidden({
|
||||||
|
user,
|
||||||
|
isHidden,
|
||||||
|
}: {
|
||||||
|
user: ApiUser;
|
||||||
|
isHidden: boolean;
|
||||||
|
}) {
|
||||||
|
return invokeRequest(new GramJs.contacts.ToggleStoriesHidden({
|
||||||
|
id: buildInputPeer(user.id, user.accessHash),
|
||||||
|
hidden: isHidden,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchStoriesMaxIds({
|
||||||
|
users,
|
||||||
|
}: {
|
||||||
|
users: ApiUser[];
|
||||||
|
}) {
|
||||||
|
return invokeRequest(new GramJs.users.GetStoriesMaxIDs({
|
||||||
|
id: users.map((user) => buildInputPeer(user.id, user.accessHash)),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchCommonStoriesRequest({ method, userId }: {
|
||||||
|
method: GramJs.stories.GetPinnedStories | GramJs.stories.GetStoriesArchive;
|
||||||
|
userId: string;
|
||||||
|
}) {
|
||||||
|
const result = await invokeRequest(method);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEntitiesToLocalDb(result.users);
|
||||||
|
result.stories.forEach((story) => addStoryToLocalDb(story, userId));
|
||||||
|
|
||||||
|
const users = result.users.map(buildApiUser).filter(Boolean);
|
||||||
|
const stories = buildCollectionByCallback(result.stories, (story) => (
|
||||||
|
[story.id, buildApiStory(userId, story)]
|
||||||
|
));
|
||||||
|
|
||||||
|
return {
|
||||||
|
users,
|
||||||
|
stories,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -301,6 +301,14 @@ export function updateEmojiStatus(emojiStatus: ApiSticker, expires?: number) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function saveCloseFriends(userIds: string[]) {
|
||||||
|
const id = userIds.map((userId) => buildMtpPeerId(userId, 'user'));
|
||||||
|
|
||||||
|
return invokeRequest(new GramJs.contacts.EditCloseFriends({ id }), {
|
||||||
|
shouldReturnTrue: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function updateLocalDb(result: (GramJs.photos.Photos | GramJs.photos.PhotosSlice | GramJs.messages.Chats)) {
|
function updateLocalDb(result: (GramJs.photos.Photos | GramJs.photos.PhotosSlice | GramJs.messages.Chats)) {
|
||||||
if ('chats' in result) {
|
if ('chats' in result) {
|
||||||
addEntitiesToLocalDb(result.chats);
|
addEntitiesToLocalDb(result.chats);
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type { GroupCallConnectionData } from '../../lib/secret-sauce';
|
|||||||
import { Api as GramJs, connection } from '../../lib/gramjs';
|
import { Api as GramJs, connection } from '../../lib/gramjs';
|
||||||
import type {
|
import type {
|
||||||
ApiMessage, ApiMessageExtendedMediaPreview, ApiUpdate, ApiUpdateConnectionStateType, OnApiUpdate,
|
ApiMessage, ApiMessageExtendedMediaPreview, ApiUpdate, ApiUpdateConnectionStateType, OnApiUpdate,
|
||||||
|
ApiStory, ApiStorySkipped,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
import { DEBUG, GENERAL_TOPIC_ID } from '../../config';
|
import { DEBUG, GENERAL_TOPIC_ID } from '../../config';
|
||||||
@ -18,6 +19,8 @@ import {
|
|||||||
buildMessageDraft,
|
buildMessageDraft,
|
||||||
buildMessageReactions,
|
buildMessageReactions,
|
||||||
buildApiMessageExtendedMediaPreview,
|
buildApiMessageExtendedMediaPreview,
|
||||||
|
buildApiStory,
|
||||||
|
buildPrivacyRules,
|
||||||
} from './apiBuilders/messages';
|
} from './apiBuilders/messages';
|
||||||
import {
|
import {
|
||||||
buildChatMember,
|
buildChatMember,
|
||||||
@ -49,12 +52,12 @@ import {
|
|||||||
log,
|
log,
|
||||||
swapLocalInvoiceMedia,
|
swapLocalInvoiceMedia,
|
||||||
isChatFolder,
|
isChatFolder,
|
||||||
|
addStoryToLocalDb,
|
||||||
} from './helpers';
|
} from './helpers';
|
||||||
import {
|
import {
|
||||||
buildApiNotifyException,
|
buildApiNotifyException,
|
||||||
buildApiNotifyExceptionTopic,
|
buildApiNotifyExceptionTopic,
|
||||||
buildPrivacyKey,
|
buildPrivacyKey,
|
||||||
buildPrivacyRules,
|
|
||||||
} from './apiBuilders/misc';
|
} from './apiBuilders/misc';
|
||||||
import { buildApiPhoto, buildApiUsernames } from './apiBuilders/common';
|
import { buildApiPhoto, buildApiUsernames } from './apiBuilders/common';
|
||||||
import {
|
import {
|
||||||
@ -300,7 +303,9 @@ export function updater(update: Update) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (action instanceof GramJs.MessageActionTopicEdit) {
|
} else if (action instanceof GramJs.MessageActionTopicEdit) {
|
||||||
const { replyTo } = update.message;
|
const replyTo = update.message.replyTo instanceof GramJs.MessageReplyHeader
|
||||||
|
? update.message.replyTo
|
||||||
|
: undefined;
|
||||||
const {
|
const {
|
||||||
replyToMsgId, replyToTopId, forumTopic: isTopicReply,
|
replyToMsgId, replyToTopId, forumTopic: isTopicReply,
|
||||||
} = replyTo || {};
|
} = replyTo || {};
|
||||||
@ -1050,6 +1055,37 @@ export function updater(update: Update) {
|
|||||||
});
|
});
|
||||||
} else if (update instanceof GramJs.UpdateRecentEmojiStatuses) {
|
} else if (update instanceof GramJs.UpdateRecentEmojiStatuses) {
|
||||||
onUpdate({ '@type': 'updateRecentEmojiStatuses' });
|
onUpdate({ '@type': 'updateRecentEmojiStatuses' });
|
||||||
|
} else if (update instanceof GramJs.UpdateStory) {
|
||||||
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
|
const entities = update._entities;
|
||||||
|
if (entities) {
|
||||||
|
addEntitiesToLocalDb(entities);
|
||||||
|
dispatchUserAndChatUpdates(entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { story } = update;
|
||||||
|
const userId = buildApiPeerId(update.userId, 'user');
|
||||||
|
addStoryToLocalDb(story, userId);
|
||||||
|
|
||||||
|
if (story instanceof GramJs.StoryItemDeleted) {
|
||||||
|
onUpdate({
|
||||||
|
'@type': 'deleteStory',
|
||||||
|
userId,
|
||||||
|
storyId: story.id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
onUpdate({
|
||||||
|
'@type': 'updateStory',
|
||||||
|
userId,
|
||||||
|
story: buildApiStory(userId, story) as ApiStory | ApiStorySkipped,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (update instanceof GramJs.UpdateReadStories) {
|
||||||
|
onUpdate({
|
||||||
|
'@type': 'updateReadStories',
|
||||||
|
userId: buildApiPeerId(update.userId, 'user'),
|
||||||
|
lastReadId: update.maxId,
|
||||||
|
});
|
||||||
} else if (DEBUG) {
|
} else if (DEBUG) {
|
||||||
const params = typeof update === 'object' && 'className' in update ? update.className : update;
|
const params = typeof update === 'object' && 'className' in update ? update.className : update;
|
||||||
log('UNEXPECTED UPDATE', params);
|
log('UNEXPECTED UPDATE', params);
|
||||||
|
|||||||
@ -35,7 +35,6 @@ const savedLocalDb: LocalDb = {
|
|||||||
stickerSets: {},
|
stickerSets: {},
|
||||||
photos: {},
|
photos: {},
|
||||||
webDocuments: {},
|
webDocuments: {},
|
||||||
|
|
||||||
commonBoxState: {},
|
commonBoxState: {},
|
||||||
channelPtsById: {},
|
channelPtsById: {},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,3 +9,4 @@ export * from './bots';
|
|||||||
export * from './misc';
|
export * from './misc';
|
||||||
export * from './calls';
|
export * from './calls';
|
||||||
export * from './statistics';
|
export * from './statistics';
|
||||||
|
export * from './stories';
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { ApiWebDocument } from './bots';
|
import type { ApiWebDocument } from './bots';
|
||||||
import type { ApiGroupCall, PhoneCallAction } from './calls';
|
import type { ApiGroupCall, PhoneCallAction } from './calls';
|
||||||
import type { ApiChat } from './chats';
|
import type { ApiChat } from './chats';
|
||||||
|
import type { ApiMessageStoryData, ApiWebPageStoryData } from './stories';
|
||||||
|
|
||||||
export interface ApiDimensions {
|
export interface ApiDimensions {
|
||||||
width: number;
|
width: number;
|
||||||
@ -95,6 +96,7 @@ export interface ApiVideo {
|
|||||||
blobUrl?: string;
|
blobUrl?: string;
|
||||||
previewBlobUrl?: string;
|
previewBlobUrl?: string;
|
||||||
size: number;
|
size: number;
|
||||||
|
noSound?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiAudio {
|
export interface ApiAudio {
|
||||||
@ -293,6 +295,19 @@ export interface ApiWebPage {
|
|||||||
duration?: number;
|
duration?: number;
|
||||||
document?: ApiDocument;
|
document?: ApiDocument;
|
||||||
video?: ApiVideo;
|
video?: ApiVideo;
|
||||||
|
story?: ApiWebPageStoryData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiTypeReplyTo = ApiMessageReplyTo | ApiStoryReplyTo;
|
||||||
|
|
||||||
|
export interface ApiMessageReplyTo {
|
||||||
|
replyingTo: number;
|
||||||
|
replyingToTopId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiStoryReplyTo {
|
||||||
|
userId: string;
|
||||||
|
storyId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiMessageForwardInfo {
|
export interface ApiMessageForwardInfo {
|
||||||
@ -383,6 +398,7 @@ export interface ApiMessage {
|
|||||||
text?: ApiFormattedText;
|
text?: ApiFormattedText;
|
||||||
photo?: ApiPhoto;
|
photo?: ApiPhoto;
|
||||||
video?: ApiVideo;
|
video?: ApiVideo;
|
||||||
|
altVideo?: ApiVideo;
|
||||||
document?: ApiDocument;
|
document?: ApiDocument;
|
||||||
sticker?: ApiSticker;
|
sticker?: ApiSticker;
|
||||||
contact?: ApiContact;
|
contact?: ApiContact;
|
||||||
@ -394,6 +410,7 @@ export interface ApiMessage {
|
|||||||
invoice?: ApiInvoice;
|
invoice?: ApiInvoice;
|
||||||
location?: ApiLocation;
|
location?: ApiLocation;
|
||||||
game?: ApiGame;
|
game?: ApiGame;
|
||||||
|
storyData?: ApiMessageStoryData;
|
||||||
};
|
};
|
||||||
date: number;
|
date: number;
|
||||||
isOutgoing: boolean;
|
isOutgoing: boolean;
|
||||||
@ -402,6 +419,8 @@ export interface ApiMessage {
|
|||||||
replyToMessageId?: number;
|
replyToMessageId?: number;
|
||||||
replyToTopMessageId?: number;
|
replyToTopMessageId?: number;
|
||||||
isTopicReply?: true;
|
isTopicReply?: true;
|
||||||
|
replyToStoryUserId?: string;
|
||||||
|
replyToStoryId?: number;
|
||||||
sendingState?: 'messageSendingStatePending' | 'messageSendingStateFailed';
|
sendingState?: 'messageSendingStatePending' | 'messageSendingStateFailed';
|
||||||
forwardInfo?: ApiMessageForwardInfo;
|
forwardInfo?: ApiMessageForwardInfo;
|
||||||
isDeleting?: boolean;
|
isDeleting?: boolean;
|
||||||
|
|||||||
@ -109,7 +109,7 @@ export type ApiNotification = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
message: string;
|
message: string;
|
||||||
actionText?: string;
|
actionText?: string;
|
||||||
action?: CallbackAction;
|
action?: CallbackAction | CallbackAction[];
|
||||||
className?: string;
|
className?: string;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
};
|
};
|
||||||
@ -191,6 +191,10 @@ export interface ApiAppConfig {
|
|||||||
hiddenMembersMinCount: number;
|
hiddenMembersMinCount: number;
|
||||||
limits: Record<ApiLimitType, readonly [number, number]>;
|
limits: Record<ApiLimitType, readonly [number, number]>;
|
||||||
canDisplayAutoarchiveSetting: boolean;
|
canDisplayAutoarchiveSetting: boolean;
|
||||||
|
areStoriesHidden?: boolean;
|
||||||
|
storyExpirePeriod: number;
|
||||||
|
storyViewersExpirePeriod: number;
|
||||||
|
storyChangelogUserId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiConfig {
|
export interface ApiConfig {
|
||||||
|
|||||||
59
src/api/types/stories.ts
Normal file
59
src/api/types/stories.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import type { ApiMessage } from './messages';
|
||||||
|
import type { ApiPrivacySettings } from '../../types';
|
||||||
|
|
||||||
|
export interface ApiStory {
|
||||||
|
'@type'?: 'story';
|
||||||
|
id: number;
|
||||||
|
userId: string;
|
||||||
|
date: number;
|
||||||
|
expireDate: number;
|
||||||
|
content: ApiMessage['content'];
|
||||||
|
isPinned?: boolean;
|
||||||
|
isEdited?: boolean;
|
||||||
|
isForCloseFriends?: boolean;
|
||||||
|
isForContacts?: boolean;
|
||||||
|
isForSelectedContacts?: boolean;
|
||||||
|
isPublic?: boolean;
|
||||||
|
noForwards?: boolean;
|
||||||
|
viewsCount?: number;
|
||||||
|
recentViewerIds?: string[];
|
||||||
|
visibility?: ApiPrivacySettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiStorySkipped {
|
||||||
|
'@type'?: 'storySkipped';
|
||||||
|
id: number;
|
||||||
|
userId: string;
|
||||||
|
isForCloseFriends?: boolean;
|
||||||
|
date: number;
|
||||||
|
expireDate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiStoryDeleted {
|
||||||
|
'@type'?: 'storyDeleted';
|
||||||
|
id: number;
|
||||||
|
userId: string;
|
||||||
|
isDeleted: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiTypeStory = ApiStory | ApiStorySkipped | ApiStoryDeleted;
|
||||||
|
|
||||||
|
export type ApiUserStories = {
|
||||||
|
byId: Record<number, ApiTypeStory>;
|
||||||
|
orderedIds: number[]; // Actual user stories
|
||||||
|
pinnedIds: number[]; // Profile Shared Media: Pinned Stories tab
|
||||||
|
archiveIds?: number[]; // Profile Shared Media: Archive Stories tab
|
||||||
|
lastUpdatedAt?: number;
|
||||||
|
lastReadId?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiMessageStoryData = {
|
||||||
|
id: number;
|
||||||
|
userId: string;
|
||||||
|
isMention?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiWebPageStoryData = {
|
||||||
|
id: number;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
@ -33,6 +33,7 @@ import type {
|
|||||||
} from './calls';
|
} from './calls';
|
||||||
import type { ApiBotMenuButton } from './bots';
|
import type { ApiBotMenuButton } from './bots';
|
||||||
import type { ApiPrivacyKey, PrivacyVisibility } from '../../types';
|
import type { ApiPrivacyKey, PrivacyVisibility } from '../../types';
|
||||||
|
import type { ApiStory, ApiStorySkipped } from './stories';
|
||||||
|
|
||||||
export type ApiUpdateReady = {
|
export type ApiUpdateReady = {
|
||||||
'@type': 'updateApiReady';
|
'@type': 'updateApiReady';
|
||||||
@ -624,6 +625,24 @@ export type ApiRequestReconnectApi = {
|
|||||||
'@type': 'requestReconnectApi';
|
'@type': 'requestReconnectApi';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ApiUpdateStory = {
|
||||||
|
'@type': 'updateStory';
|
||||||
|
userId: string;
|
||||||
|
story: ApiStory | ApiStorySkipped;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiUpdateDeleteStory = {
|
||||||
|
'@type': 'deleteStory';
|
||||||
|
userId: string;
|
||||||
|
storyId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiUpdateReadStories = {
|
||||||
|
'@type': 'updateReadStories';
|
||||||
|
userId: string;
|
||||||
|
lastReadId: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type ApiRequestSync = {
|
export type ApiRequestSync = {
|
||||||
'@type': 'requestSync';
|
'@type': 'requestSync';
|
||||||
};
|
};
|
||||||
@ -654,7 +673,8 @@ export type ApiUpdate = (
|
|||||||
ApiUpdatePhoneCallConnectionState | ApiUpdateBotMenuButton | ApiUpdateTranscribedAudio | ApiUpdateUserEmojiStatus |
|
ApiUpdatePhoneCallConnectionState | ApiUpdateBotMenuButton | ApiUpdateTranscribedAudio | ApiUpdateUserEmojiStatus |
|
||||||
ApiUpdateMessageExtendedMedia | ApiUpdateConfig | ApiUpdateTopicNotifyExceptions | ApiUpdatePinnedTopic |
|
ApiUpdateMessageExtendedMedia | ApiUpdateConfig | ApiUpdateTopicNotifyExceptions | ApiUpdatePinnedTopic |
|
||||||
ApiUpdatePinnedTopicsOrder | ApiUpdateTopic | ApiUpdateTopics | ApiUpdateRecentEmojiStatuses |
|
ApiUpdatePinnedTopicsOrder | ApiUpdateTopic | ApiUpdateTopics | ApiUpdateRecentEmojiStatuses |
|
||||||
ApiUpdateRecentReactions | ApiRequestReconnectApi | ApiRequestSync | ApiUpdateFetchingDifference
|
ApiUpdateRecentReactions | ApiUpdateStory | ApiUpdateReadStories | ApiUpdateDeleteStory |
|
||||||
|
ApiRequestReconnectApi | ApiRequestSync | ApiUpdateFetchingDifference | ApiUpdateChannelMessages
|
||||||
);
|
);
|
||||||
|
|
||||||
export type OnApiUpdate = (update: ApiUpdate) => void;
|
export type OnApiUpdate = (update: ApiUpdate) => void;
|
||||||
|
|||||||
@ -8,7 +8,9 @@ export interface ApiUser {
|
|||||||
isSelf?: true;
|
isSelf?: true;
|
||||||
isVerified?: true;
|
isVerified?: true;
|
||||||
isPremium?: boolean;
|
isPremium?: boolean;
|
||||||
|
isCloseFriend?: boolean;
|
||||||
isContact?: true;
|
isContact?: true;
|
||||||
|
isSupport?: true;
|
||||||
type: ApiUserType;
|
type: ApiUserType;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
@ -29,6 +31,10 @@ export interface ApiUser {
|
|||||||
fakeType?: ApiFakeType;
|
fakeType?: ApiFakeType;
|
||||||
isAttachBot?: boolean;
|
isAttachBot?: boolean;
|
||||||
emojiStatus?: ApiEmojiStatus;
|
emojiStatus?: ApiEmojiStatus;
|
||||||
|
areStoriesHidden?: boolean;
|
||||||
|
hasStories?: boolean;
|
||||||
|
hasUnreadStories?: boolean;
|
||||||
|
maxStoryId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiUserFullInfo {
|
export interface ApiUserFullInfo {
|
||||||
@ -43,6 +49,7 @@ export interface ApiUserFullInfo {
|
|||||||
noVoiceMessages?: boolean;
|
noVoiceMessages?: boolean;
|
||||||
premiumGifts?: ApiPremiumGiftOption[];
|
premiumGifts?: ApiPremiumGiftOption[];
|
||||||
isTranslationDisabled?: true;
|
isTranslationDisabled?: true;
|
||||||
|
hasPinnedStories?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ApiFakeType = 'fake' | 'scam';
|
export type ApiFakeType = 'fake' | 'scam';
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -1,4 +1,5 @@
|
|||||||
export { default as MediaViewer } from '../components/mediaViewer/MediaViewer';
|
export { default as MediaViewer } from '../components/mediaViewer/MediaViewer';
|
||||||
|
export { default as StoryViewer } from '../components/story/StoryViewer';
|
||||||
|
|
||||||
export { default as ForwardRecipientPicker } from '../components/main/ForwardRecipientPicker';
|
export { default as ForwardRecipientPicker } from '../components/main/ForwardRecipientPicker';
|
||||||
export { default as DraftRecipientPicker } from '../components/main/DraftRecipientPicker';
|
export { default as DraftRecipientPicker } from '../components/main/DraftRecipientPicker';
|
||||||
|
|||||||
@ -11,6 +11,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:global(.Avatar) {
|
:global(.Avatar) {
|
||||||
|
--radius: 0;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|||||||
@ -1,20 +1,33 @@
|
|||||||
.Avatar {
|
.Avatar {
|
||||||
--color-user: var(--color-primary);
|
--color-user: var(--color-primary);
|
||||||
--radius: 50%;
|
--radius: 50%;
|
||||||
|
|
||||||
flex: none;
|
flex: none;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 3.375rem;
|
width: 3.375rem;
|
||||||
height: 3.375rem;
|
height: 3.375rem;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background-image: linear-gradient(var(--color-white) -125%, var(--color-user));
|
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
display: flex;
|
display: flex;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
|
||||||
|
> .inner {
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-image: linear-gradient(var(--color-white) -125%, var(--color-user));
|
||||||
|
}
|
||||||
|
|
||||||
&__media {
|
&__media {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -120,6 +133,68 @@
|
|||||||
cursor: var(--custom-cursor, pointer);
|
cursor: var(--custom-cursor, pointer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.with-story-circle {
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
> .inner {
|
||||||
|
width: calc(100% - 0.375rem);
|
||||||
|
height: calc(100% - 0.375rem);
|
||||||
|
left: 0.1875rem;
|
||||||
|
top: 0.1875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.with-story-solid {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
margin: 0.1875rem;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 3.5rem;
|
||||||
|
height: 3.5rem;
|
||||||
|
left: -0.25rem;
|
||||||
|
top: -0.25rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 0.125rem;
|
||||||
|
|
||||||
|
background: var(--color-borders-read-story);
|
||||||
|
mask: linear-gradient(to bottom, #fff 0%, #fff 100%) content-box, linear-gradient(to bottom, #fff 0%, #fff 100%);
|
||||||
|
mask-composite: exclude;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.size-tiny {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
width: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.size-medium {
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.online::after {
|
||||||
|
bottom: -0.125rem;
|
||||||
|
right: -0.125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.has-unread-story::before {
|
||||||
|
background-image: linear-gradient(215.87deg, var(--color-avatar-story-unread-from) -1.61%, var(--color-avatar-story-unread-to) 97.44%);
|
||||||
|
}
|
||||||
|
|
||||||
.poster {
|
.poster {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|||||||
@ -1,12 +1,9 @@
|
|||||||
import type { MouseEvent as ReactMouseEvent } from 'react';
|
import type { MouseEvent as ReactMouseEvent } from 'react';
|
||||||
import React, {
|
import React, { memo, useRef } from '../../lib/teact/teact';
|
||||||
memo, useRef,
|
import { getActions } from '../../global';
|
||||||
} from '../../lib/teact/teact';
|
|
||||||
|
|
||||||
import type { FC, TeactNode } from '../../lib/teact/teact';
|
import type { FC, TeactNode } from '../../lib/teact/teact';
|
||||||
import type {
|
import type { ApiChat, ApiPhoto, ApiUser } from '../../api/types';
|
||||||
ApiChat, ApiPhoto, ApiUser,
|
|
||||||
} from '../../api/types';
|
|
||||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
import { ApiMediaFormat } from '../../api/types';
|
import { ApiMediaFormat } from '../../api/types';
|
||||||
|
|
||||||
@ -27,10 +24,11 @@ import renderText from './helpers/renderText';
|
|||||||
import useMedia from '../../hooks/useMedia';
|
import useMedia from '../../hooks/useMedia';
|
||||||
import useMediaTransition from '../../hooks/useMediaTransition';
|
import useMediaTransition from '../../hooks/useMediaTransition';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import { useFastClick } from '../../hooks/useFastClick';
|
|
||||||
import useLastCallback from '../../hooks/useLastCallback';
|
import useLastCallback from '../../hooks/useLastCallback';
|
||||||
|
import { useFastClick } from '../../hooks/useFastClick';
|
||||||
|
|
||||||
import OptimizedVideo from '../ui/OptimizedVideo';
|
import OptimizedVideo from '../ui/OptimizedVideo';
|
||||||
|
import AvatarStoryCircle from './AvatarStoryCircle';
|
||||||
|
|
||||||
import './Avatar.scss';
|
import './Avatar.scss';
|
||||||
|
|
||||||
@ -50,6 +48,10 @@ type OwnProps = {
|
|||||||
text?: string;
|
text?: string;
|
||||||
isSavedMessages?: boolean;
|
isSavedMessages?: boolean;
|
||||||
withVideo?: boolean;
|
withVideo?: boolean;
|
||||||
|
withStory?: boolean;
|
||||||
|
withStoryGap?: boolean;
|
||||||
|
withStorySolid?: boolean;
|
||||||
|
storyViewerMode?: 'full' | 'single-user' | 'disabled';
|
||||||
loopIndefinitely?: boolean;
|
loopIndefinitely?: boolean;
|
||||||
noPersonalPhoto?: boolean;
|
noPersonalPhoto?: boolean;
|
||||||
observeIntersection?: ObserveFn;
|
observeIntersection?: ObserveFn;
|
||||||
@ -64,10 +66,16 @@ const Avatar: FC<OwnProps> = ({
|
|||||||
text,
|
text,
|
||||||
isSavedMessages,
|
isSavedMessages,
|
||||||
withVideo,
|
withVideo,
|
||||||
|
withStory,
|
||||||
|
withStoryGap,
|
||||||
|
withStorySolid,
|
||||||
|
storyViewerMode = 'single-user',
|
||||||
loopIndefinitely,
|
loopIndefinitely,
|
||||||
noPersonalPhoto,
|
noPersonalPhoto,
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { openStoryViewer } = getActions();
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const videoLoopCountRef = useRef(0);
|
const videoLoopCountRef = useRef(0);
|
||||||
@ -163,6 +171,7 @@ const Avatar: FC<OwnProps> = ({
|
|||||||
className={buildClassName(cn.media, 'avatar-media', transitionClassNames, videoBlobUrl && 'poster')}
|
className={buildClassName(cn.media, 'avatar-media', transitionClassNames, videoBlobUrl && 'poster')}
|
||||||
alt={author}
|
alt={author}
|
||||||
decoding="async"
|
decoding="async"
|
||||||
|
draggable={false}
|
||||||
/>
|
/>
|
||||||
{shouldPlayVideo && (
|
{shouldPlayVideo && (
|
||||||
<OptimizedVideo
|
<OptimizedVideo
|
||||||
@ -174,6 +183,7 @@ const Avatar: FC<OwnProps> = ({
|
|||||||
autoPlay
|
autoPlay
|
||||||
disablePictureInPicture
|
disablePictureInPicture
|
||||||
playsInline
|
playsInline
|
||||||
|
draggable={false}
|
||||||
onEnded={handleVideoEnded}
|
onEnded={handleVideoEnded}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -197,6 +207,9 @@ const Avatar: FC<OwnProps> = ({
|
|||||||
isDeleted && 'deleted-account',
|
isDeleted && 'deleted-account',
|
||||||
isReplies && 'replies-bot-account',
|
isReplies && 'replies-bot-account',
|
||||||
isForum && 'forum',
|
isForum && 'forum',
|
||||||
|
withStory && user?.hasStories && 'with-story-circle',
|
||||||
|
withStorySolid && user?.hasStories && 'with-story-solid',
|
||||||
|
withStorySolid && user?.hasUnreadStories && 'has-unread-story',
|
||||||
onClick && 'interactive',
|
onClick && 'interactive',
|
||||||
(!isSavedMessages && !imgBlobUrl) && 'no-photo',
|
(!isSavedMessages && !imgBlobUrl) && 'no-photo',
|
||||||
);
|
);
|
||||||
@ -204,6 +217,13 @@ const Avatar: FC<OwnProps> = ({
|
|||||||
const hasMedia = Boolean(isSavedMessages || imgBlobUrl);
|
const hasMedia = Boolean(isSavedMessages || imgBlobUrl);
|
||||||
|
|
||||||
const { handleClick, handleMouseDown } = useFastClick((e: ReactMouseEvent<HTMLDivElement, MouseEvent>) => {
|
const { handleClick, handleMouseDown } = useFastClick((e: ReactMouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||||
|
if (withStory && storyViewerMode !== 'disabled' && user?.hasStories) {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
openStoryViewer({ userId: user.id, isSingleUser: storyViewerMode === 'single-user' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
onClick(e, hasMedia);
|
onClick(e, hasMedia);
|
||||||
}
|
}
|
||||||
@ -218,7 +238,12 @@ const Avatar: FC<OwnProps> = ({
|
|||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
>
|
>
|
||||||
{typeof content === 'string' ? renderText(content, [size === 'jumbo' ? 'hq_emoji' : 'emoji']) : content}
|
<div className="inner">
|
||||||
|
{typeof content === 'string' ? renderText(content, [size === 'jumbo' ? 'hq_emoji' : 'emoji']) : content}
|
||||||
|
</div>
|
||||||
|
{withStory && user?.hasStories && (
|
||||||
|
<AvatarStoryCircle userId={user.id} size={size} withExtraGap={withStoryGap} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
199
src/components/common/AvatarStoryCircle.tsx
Normal file
199
src/components/common/AvatarStoryCircle.tsx
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
import React, {
|
||||||
|
memo, useLayoutEffect, useMemo, useRef,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
|
import { withGlobal } from '../../global';
|
||||||
|
|
||||||
|
import type { AvatarSize } from './Avatar';
|
||||||
|
import type { ThemeKey } from '../../types';
|
||||||
|
|
||||||
|
import { REM } from './helpers/mediaDimensions';
|
||||||
|
import { DPR } from '../../util/windowEnvironment';
|
||||||
|
import { selectTheme, selectUser, selectUserStories } from '../../global/selectors';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
|
||||||
|
interface OwnProps {
|
||||||
|
// eslint-disable-next-line react/no-unused-prop-types
|
||||||
|
userId: string;
|
||||||
|
className?: string;
|
||||||
|
size: AvatarSize;
|
||||||
|
withExtraGap?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StateProps {
|
||||||
|
isCloseFriend?: boolean;
|
||||||
|
storyIds?: number[];
|
||||||
|
lastReadId?: number;
|
||||||
|
appTheme: ThemeKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SIZES: Record<AvatarSize, number> = {
|
||||||
|
micro: 1.125 * DPR * REM,
|
||||||
|
tiny: 2.125 * DPR * REM,
|
||||||
|
mini: 1.625 * DPR * REM,
|
||||||
|
small: 2.25 * DPR * REM,
|
||||||
|
'small-mobile': 2.625 * DPR * REM,
|
||||||
|
medium: 2.875 * DPR * REM,
|
||||||
|
large: 3.5 * DPR * REM,
|
||||||
|
jumbo: 7.625 * DPR * REM,
|
||||||
|
};
|
||||||
|
|
||||||
|
const BLUE = ['#34C578', '#3CA3F3'];
|
||||||
|
const GREEN = ['#C9EB38', '#09C167'];
|
||||||
|
const GRAY = '#C4C9CC';
|
||||||
|
const DARK_GRAY = '#737373';
|
||||||
|
const STROKE_WIDTH = 0.125 * DPR * REM;
|
||||||
|
const STROKE_WIDTH_READ = 0.0625 * DPR * REM;
|
||||||
|
const GAP_PERCENT = 2;
|
||||||
|
const SEGMENTS_MAX = 45; // More than this breaks rendering in Safari and Chrome
|
||||||
|
|
||||||
|
const GAP_PERCENT_EXTRA = 10;
|
||||||
|
const EXTRA_GAP_ANGLE = Math.PI / 4;
|
||||||
|
const EXTRA_GAP_SIZE = (GAP_PERCENT_EXTRA / 100) * (2 * Math.PI);
|
||||||
|
const EXTRA_GAP_START = EXTRA_GAP_ANGLE - EXTRA_GAP_SIZE / 2;
|
||||||
|
const EXTRA_GAP_END = EXTRA_GAP_ANGLE + EXTRA_GAP_SIZE / 2;
|
||||||
|
|
||||||
|
function AvatarStoryCircle({
|
||||||
|
size = 'large',
|
||||||
|
className,
|
||||||
|
isCloseFriend,
|
||||||
|
storyIds,
|
||||||
|
lastReadId,
|
||||||
|
withExtraGap,
|
||||||
|
appTheme,
|
||||||
|
}: OwnProps & StateProps) {
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const ref = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
const values = useMemo(() => {
|
||||||
|
return (storyIds || []).reduce((acc, id) => {
|
||||||
|
acc.total += 1;
|
||||||
|
if (lastReadId && id <= lastReadId) {
|
||||||
|
acc.read += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, { total: 0, read: 0 });
|
||||||
|
}, [lastReadId, storyIds]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!ref.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawGradientCircle({
|
||||||
|
canvas: ref.current,
|
||||||
|
size: SIZES[size],
|
||||||
|
segmentsCount: values.total,
|
||||||
|
color: isCloseFriend ? 'green' : 'blue',
|
||||||
|
readSegmentsCount: values.read,
|
||||||
|
withExtraGap,
|
||||||
|
readSegmentColor: appTheme === 'dark' ? DARK_GRAY : GRAY,
|
||||||
|
});
|
||||||
|
}, [appTheme, isCloseFriend, size, values.read, values.total, withExtraGap]);
|
||||||
|
|
||||||
|
if (!values.total) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxSize = SIZES[size] / DPR;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
ref={ref}
|
||||||
|
className={buildClassName('story-circle', size, className)}
|
||||||
|
style={`max-width: ${maxSize}px; max-height: ${maxSize}px;`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>((global, { userId }): StateProps => {
|
||||||
|
const user = selectUser(global, userId);
|
||||||
|
const userStories = selectUserStories(global, userId);
|
||||||
|
const appTheme = selectTheme(global);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isCloseFriend: user?.isCloseFriend,
|
||||||
|
storyIds: userStories?.orderedIds,
|
||||||
|
lastReadId: userStories?.lastReadId,
|
||||||
|
appTheme,
|
||||||
|
};
|
||||||
|
})(AvatarStoryCircle));
|
||||||
|
|
||||||
|
function drawGradientCircle({
|
||||||
|
canvas,
|
||||||
|
size,
|
||||||
|
color,
|
||||||
|
segmentsCount,
|
||||||
|
readSegmentsCount = 0,
|
||||||
|
withExtraGap = false,
|
||||||
|
readSegmentColor,
|
||||||
|
}: {
|
||||||
|
canvas: HTMLCanvasElement;
|
||||||
|
size: number;
|
||||||
|
color: string;
|
||||||
|
segmentsCount: number;
|
||||||
|
readSegmentsCount?: number;
|
||||||
|
withExtraGap?: boolean;
|
||||||
|
readSegmentColor: string;
|
||||||
|
}) {
|
||||||
|
if (segmentsCount > SEGMENTS_MAX) {
|
||||||
|
readSegmentsCount = Math.round(readSegmentsCount * (SEGMENTS_MAX / segmentsCount));
|
||||||
|
|
||||||
|
segmentsCount = SEGMENTS_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.width = size;
|
||||||
|
canvas.height = size;
|
||||||
|
const centerCoordinate = size / 2;
|
||||||
|
const radius = (size - STROKE_WIDTH) / 2;
|
||||||
|
const segmentAngle = (2 * Math.PI) / segmentsCount;
|
||||||
|
const gapSize = (GAP_PERCENT / 100) * (2 * Math.PI);
|
||||||
|
const gradient = ctx.createLinearGradient(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
Math.ceil(size * Math.cos(Math.PI / 2)),
|
||||||
|
Math.ceil(size * Math.sin(Math.PI / 2)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const colorStops = color === 'green' ? GREEN : BLUE;
|
||||||
|
colorStops.forEach((colorStop, index) => {
|
||||||
|
gradient.addColorStop(index / (colorStops.length - 1), colorStop);
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.clearRect(0, 0, size, size);
|
||||||
|
|
||||||
|
Array.from({ length: segmentsCount }).forEach((_, i) => {
|
||||||
|
const isRead = i < readSegmentsCount;
|
||||||
|
let startAngle = i * segmentAngle - Math.PI / 2 + gapSize / 2;
|
||||||
|
let endAngle = startAngle + segmentAngle - (segmentsCount > 1 ? gapSize : 0);
|
||||||
|
|
||||||
|
ctx.strokeStyle = isRead ? readSegmentColor : gradient;
|
||||||
|
ctx.lineWidth = isRead ? STROKE_WIDTH_READ : STROKE_WIDTH;
|
||||||
|
|
||||||
|
if (withExtraGap) {
|
||||||
|
if (startAngle >= EXTRA_GAP_START && endAngle <= EXTRA_GAP_END) { // Segment is inside extra gap
|
||||||
|
return;
|
||||||
|
} else if (startAngle < EXTRA_GAP_START && endAngle > EXTRA_GAP_END) { // Extra gap is inside segment
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerCoordinate, centerCoordinate, radius, EXTRA_GAP_END, endAngle);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
endAngle = EXTRA_GAP_START;
|
||||||
|
} else if (startAngle < EXTRA_GAP_START && endAngle > EXTRA_GAP_START) { // Segment ends in extra gap
|
||||||
|
endAngle = EXTRA_GAP_START;
|
||||||
|
} else if (startAngle < EXTRA_GAP_END && endAngle > EXTRA_GAP_END) { // Segment starts in extra gap
|
||||||
|
startAngle = EXTRA_GAP_END;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerCoordinate, centerCoordinate, radius, startAngle, endAngle);
|
||||||
|
ctx.stroke();
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -75,6 +75,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
|||||||
showNotification,
|
showNotification,
|
||||||
updateChatMutedState,
|
updateChatMutedState,
|
||||||
updateTopicMutedState,
|
updateTopicMutedState,
|
||||||
|
loadUserStories,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -95,6 +96,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
loadFullUser({ userId });
|
loadFullUser({ userId });
|
||||||
|
loadUserStories({ userId });
|
||||||
}, [userId]);
|
}, [userId]);
|
||||||
|
|
||||||
const isTopicInfo = Boolean(topicId && topicId !== MAIN_THREAD_ID);
|
const isTopicInfo = Boolean(topicId && topicId !== MAIN_THREAD_ID);
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { REM } from './helpers/mediaDimensions';
|
|||||||
import { CHAT_HEIGHT_PX } from '../../config';
|
import { CHAT_HEIGHT_PX } from '../../config';
|
||||||
import renderText from './helpers/renderText';
|
import renderText from './helpers/renderText';
|
||||||
import { getCanPostInChat, isUserId } from '../../global/helpers';
|
import { getCanPostInChat, isUserId } from '../../global/helpers';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
|
||||||
import useLastCallback from '../../hooks/useLastCallback';
|
import useLastCallback from '../../hooks/useLastCallback';
|
||||||
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
||||||
@ -37,6 +38,7 @@ export type OwnProps = {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
searchPlaceholder: string;
|
searchPlaceholder: string;
|
||||||
search: string;
|
search: string;
|
||||||
|
className?: string;
|
||||||
loadMore?: NoneToVoidFunction;
|
loadMore?: NoneToVoidFunction;
|
||||||
onSearchChange: (search: string) => void;
|
onSearchChange: (search: string) => void;
|
||||||
onSelectChatOrUser: (chatOrUserId: string, threadId?: number) => void;
|
onSelectChatOrUser: (chatOrUserId: string, threadId?: number) => void;
|
||||||
@ -55,6 +57,7 @@ const ChatOrUserPicker: FC<OwnProps> = ({
|
|||||||
chatsById,
|
chatsById,
|
||||||
search,
|
search,
|
||||||
searchPlaceholder,
|
searchPlaceholder,
|
||||||
|
className,
|
||||||
loadMore,
|
loadMore,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onSelectChatOrUser,
|
onSelectChatOrUser,
|
||||||
@ -264,7 +267,7 @@ const ChatOrUserPicker: FC<OwnProps> = ({
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
className="ChatOrUserPicker"
|
className={buildClassName('ChatOrUserPicker', className)}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onCloseAnimationEnd={onCloseAnimationEnd}
|
onCloseAnimationEnd={onCloseAnimationEnd}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
.Composer {
|
.Composer {
|
||||||
|
--base-height: 3.5rem;
|
||||||
|
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
|
||||||
.select-mode-active + .middle-column-footer & {
|
.select-mode-active + .middle-column-footer & {
|
||||||
@ -40,7 +42,7 @@
|
|||||||
|
|
||||||
to {
|
to {
|
||||||
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||||
width: 3.5rem;
|
width: var(--base-height);
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -48,6 +50,8 @@
|
|||||||
> .Button {
|
> .Button {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
|
width: var(--base-height);
|
||||||
|
height: var(--base-height);
|
||||||
|
|
||||||
&:not(.danger) {
|
&:not(.danger) {
|
||||||
color: var(--color-composer-button);
|
color: var(--color-composer-button);
|
||||||
@ -60,6 +64,7 @@
|
|||||||
|
|
||||||
.icon-send,
|
.icon-send,
|
||||||
.icon-schedule,
|
.icon-schedule,
|
||||||
|
.icon-forward,
|
||||||
.icon-microphone-alt,
|
.icon-microphone-alt,
|
||||||
.icon-check {
|
.icon-check {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -69,6 +74,7 @@
|
|||||||
&:not(:active):not(:focus):not(:hover) {
|
&:not(:active):not(:focus):not(:hover) {
|
||||||
.icon-send,
|
.icon-send,
|
||||||
.icon-schedule,
|
.icon-schedule,
|
||||||
|
.icon-forward,
|
||||||
.icon-check {
|
.icon-check {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
@ -79,6 +85,7 @@
|
|||||||
&:not(:active):not(:focus) {
|
&:not(:active):not(:focus) {
|
||||||
.icon-send,
|
.icon-send,
|
||||||
.icon-schedule,
|
.icon-schedule,
|
||||||
|
.icon-forward,
|
||||||
.icon-check {
|
.icon-check {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
@ -107,6 +114,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon-microphone-alt,
|
.icon-microphone-alt,
|
||||||
|
.icon-forward,
|
||||||
.icon-check,
|
.icon-check,
|
||||||
.icon-schedule {
|
.icon-schedule {
|
||||||
animation: hide-icon 0.4s forwards ease-out;
|
animation: hide-icon 0.4s forwards ease-out;
|
||||||
@ -120,6 +128,7 @@
|
|||||||
|
|
||||||
.icon-microphone-alt,
|
.icon-microphone-alt,
|
||||||
.icon-check,
|
.icon-check,
|
||||||
|
.icon-forward,
|
||||||
.icon-send {
|
.icon-send {
|
||||||
animation: hide-icon 0.4s forwards ease-out;
|
animation: hide-icon 0.4s forwards ease-out;
|
||||||
}
|
}
|
||||||
@ -132,6 +141,7 @@
|
|||||||
|
|
||||||
.icon-send,
|
.icon-send,
|
||||||
.icon-check,
|
.icon-check,
|
||||||
|
.icon-forward,
|
||||||
.icon-schedule {
|
.icon-schedule {
|
||||||
animation: hide-icon 0.4s forwards ease-out;
|
animation: hide-icon 0.4s forwards ease-out;
|
||||||
}
|
}
|
||||||
@ -143,6 +153,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon-send,
|
.icon-send,
|
||||||
|
.icon-forward,
|
||||||
|
.icon-microphone-alt,
|
||||||
|
.icon-schedule {
|
||||||
|
animation: hide-icon 0.4s forwards ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.forward {
|
||||||
|
--color-primary: #212121;
|
||||||
|
|
||||||
|
.icon-forward {
|
||||||
|
--color-primary: #707478;
|
||||||
|
|
||||||
|
animation: grow-icon 0.4s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-send,
|
||||||
|
.icon-check,
|
||||||
.icon-microphone-alt,
|
.icon-microphone-alt,
|
||||||
.icon-schedule {
|
.icon-schedule {
|
||||||
animation: hide-icon 0.4s forwards ease-out;
|
animation: hide-icon 0.4s forwards ease-out;
|
||||||
@ -172,6 +200,32 @@
|
|||||||
animation: 0.25s ease-in-out forwards show-send-as-button;
|
animation: 0.25s ease-in-out forwards show-send-as-button;
|
||||||
transform-origin: right;
|
transform-origin: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .ReactionSelector {
|
||||||
|
--color-background-compact-menu: rgba(0, 0, 0, 0.3);
|
||||||
|
--color-interactive-element-hover: rgba(255, 255, 255, 0.1);
|
||||||
|
--color-text: #fff;
|
||||||
|
|
||||||
|
left: 50%;
|
||||||
|
right: auto;
|
||||||
|
top: -3.875rem;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
top: -4.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ReactionSelector__bubble-small,
|
||||||
|
.ReactionSelector__bubble-big {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ReactionSelector__show-more {
|
||||||
|
transform: scaleY(-1);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-symbol-menu-button {
|
.mobile-symbol-menu-button {
|
||||||
@ -225,17 +279,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.composer-wrapper {
|
||||||
#message-compose {
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
max-width: calc(100% - 4rem);
|
max-width: calc(100% - 4rem);
|
||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
border-radius: var(--border-radius-messages);
|
border-radius: var(--border-radius-default-small);
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
box-shadow: 0 1px 2px var(--color-default-shadow);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
|
&.full-featured {
|
||||||
|
box-shadow: 0 1px 2px var(--color-default-shadow);
|
||||||
|
border-radius: var(--border-radius-messages);
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.svg-appendix {
|
.svg-appendix {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -0.1875rem;
|
bottom: -0.1875rem;
|
||||||
@ -274,8 +331,8 @@
|
|||||||
> .Button {
|
> .Button {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: none !important;
|
background: none !important;
|
||||||
width: 3.5rem;
|
width: var(--base-height, 3.5rem);
|
||||||
height: 3.5rem;
|
height: var(--base-height, 3.5rem);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
@ -380,8 +437,8 @@
|
|||||||
.recording-state {
|
.recording-state {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
line-height: 3.5rem;
|
line-height: var(--base-height);
|
||||||
height: 3.5rem;
|
height: var(--base-height);
|
||||||
padding: 0 3.125rem 0 1rem;
|
padding: 0 3.125rem 0 1rem;
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
@ -424,7 +481,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.input-scroller {
|
.input-scroller {
|
||||||
min-height: 3.5rem;
|
min-height: var(--base-height, 3.5rem);
|
||||||
max-height: 26rem;
|
max-height: 26rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
@ -474,12 +531,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#message-input-text,
|
#message-input-text,
|
||||||
|
#story-input-text,
|
||||||
#caption-input-text {
|
#caption-input-text {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
.form-control {
|
.form-control {
|
||||||
padding: calc((3.5rem - var(--composer-text-size, 1rem) * 1.375) / 2) 0.875rem;
|
padding: calc((var(--base-height, 3.5rem) - var(--composer-text-size, 1rem) * 1.375) / 2) 0.875rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: auto;
|
height: auto;
|
||||||
line-height: 1.375;
|
line-height: 1.375;
|
||||||
@ -495,8 +553,9 @@
|
|||||||
caret-color: var(--color-text);
|
caret-color: var(--color-text);
|
||||||
|
|
||||||
&.touched {
|
&.touched {
|
||||||
& + .placeholder-text {
|
& ~ .placeholder-text {
|
||||||
display: none;
|
opacity: 0;
|
||||||
|
transform: translateX(1rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -520,6 +579,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
transition: opacity 200ms ease-out, transform 200ms ease-out;
|
||||||
|
|
||||||
&.with-icon {
|
&.with-icon {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@ -544,8 +604,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[dir="rtl"] .placeholder-text {
|
&[dir="rtl"] {
|
||||||
right: 0;
|
.placeholder-text {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.touched ~ .placeholder-text {
|
||||||
|
transform: translateX(-1rem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-entity-link {
|
.text-entity-link {
|
||||||
@ -561,7 +627,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.spoiler {
|
.spoiler {
|
||||||
background-image: url("../../../assets/spoiler-dots-black.png");
|
background-image: url("../../assets/spoiler-dots-black.png");
|
||||||
background-size: auto min(100%, 1.125rem);
|
background-size: auto min(100%, 1.125rem);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
padding: 0 0.3125rem 0.125rem 0.3125rem;
|
padding: 0 0.3125rem 0.125rem 0.3125rem;
|
||||||
@ -570,7 +636,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
html.theme-dark & .spoiler {
|
html.theme-dark & .spoiler {
|
||||||
background-image: url("../../../assets/spoiler-dots-white.png");
|
background-image: url("../../assets/spoiler-dots-white.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
.clone {
|
.clone {
|
||||||
@ -590,7 +656,7 @@
|
|||||||
.form-control {
|
.form-control {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
line-height: 1.3125;
|
line-height: 1.3125;
|
||||||
padding: calc((3.5rem - var(--composer-text-size, 1rem) * 1.3125) / 2) 0;
|
padding: calc((var(--base-height, 3.5rem) - var(--composer-text-size, 1rem) * 1.3125) / 2) 0;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
||||||
@ -601,7 +667,7 @@
|
|||||||
|
|
||||||
.forced-placeholder,
|
.forced-placeholder,
|
||||||
.placeholder-text {
|
.placeholder-text {
|
||||||
top: calc((3.5rem - var(--composer-text-size, 1rem) * 1.3125) / 2);
|
top: calc((var(--base-height, 3.5rem) - var(--composer-text-size, 1rem) * 1.3125) / 2);
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
top: calc((2.875rem - var(--composer-text-size, 1rem) * 1.3125) / 2);
|
top: calc((2.875rem - var(--composer-text-size, 1rem) * 1.3125) / 2);
|
||||||
@ -627,10 +693,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#story-input-text,
|
||||||
#caption-input-text {
|
#caption-input-text {
|
||||||
--margin-for-scrollbar: 5rem;
|
--margin-for-scrollbar: 2rem;
|
||||||
.input-scroller {
|
.input-scroller {
|
||||||
min-height: 3.5rem;
|
min-height: var(--base-height, 3.5rem);
|
||||||
max-height: 10rem;
|
max-height: 10rem;
|
||||||
|
|
||||||
margin-right: calc((var(--margin-for-scrollbar) + 1rem) * -1);
|
margin-right: calc((var(--margin-for-scrollbar) + 1rem) * -1);
|
||||||
@ -646,8 +713,8 @@
|
|||||||
|
|
||||||
.placeholder-text {
|
.placeholder-text {
|
||||||
top: auto;
|
top: auto;
|
||||||
bottom: 1.125rem;
|
bottom: 0.875rem;
|
||||||
left: 0.9375rem;
|
left: 0.875rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -153,6 +153,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
: Object.values(pickTruthy(customEmojisById!, recentCustomEmojiIds!));
|
: Object.values(pickTruthy(customEmojisById!, recentCustomEmojiIds!));
|
||||||
}, [customEmojisById, isStatusPicker, recentCustomEmojiIds, recentStatusEmojis]);
|
}, [customEmojisById, isStatusPicker, recentCustomEmojiIds, recentStatusEmojis]);
|
||||||
|
|
||||||
|
const prefix = `${idPrefix}-custom-emoji`;
|
||||||
const {
|
const {
|
||||||
activeSetIndex,
|
activeSetIndex,
|
||||||
observeIntersectionForSet,
|
observeIntersectionForSet,
|
||||||
@ -160,7 +161,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
observeIntersectionForShowingItems,
|
observeIntersectionForShowingItems,
|
||||||
observeIntersectionForCovers,
|
observeIntersectionForCovers,
|
||||||
selectStickerSet,
|
selectStickerSet,
|
||||||
} = useStickerPickerObservers(containerRef, headerRef, idPrefix, isHidden);
|
} = useStickerPickerObservers(containerRef, headerRef, prefix, isHidden);
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
@ -401,7 +402,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
stickerSet={stickerSet}
|
stickerSet={stickerSet}
|
||||||
loadAndPlay={Boolean(canAnimate && loadAndPlay)}
|
loadAndPlay={Boolean(canAnimate && loadAndPlay)}
|
||||||
index={i}
|
index={i}
|
||||||
idPrefix={idPrefix}
|
idPrefix={prefix}
|
||||||
observeIntersection={observeIntersectionForSet}
|
observeIntersection={observeIntersectionForSet}
|
||||||
observeIntersectionForPlayingItems={observeIntersectionForPlayingItems}
|
observeIntersectionForPlayingItems={observeIntersectionForPlayingItems}
|
||||||
observeIntersectionForShowingItems={observeIntersectionForShowingItems}
|
observeIntersectionForShowingItems={observeIntersectionForShowingItems}
|
||||||
|
|||||||
@ -69,6 +69,12 @@
|
|||||||
font-size: calc(var(--message-text-size, 1rem) - 0.125rem);
|
font-size: calc(var(--message-text-size, 1rem) - 0.125rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 0.9375rem;
|
||||||
|
vertical-align: -0.1875rem;
|
||||||
|
}
|
||||||
|
|
||||||
.message-text {
|
.message-text {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-inline-start: 0.5rem;
|
margin-inline-start: 0.5rem;
|
||||||
@ -113,6 +119,10 @@
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.with-message-color {
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.embedded-action-message {
|
.embedded-action-message {
|
||||||
|
|||||||
118
src/components/common/EmbeddedStory.tsx
Normal file
118
src/components/common/EmbeddedStory.tsx
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import React, { useRef } from '../../lib/teact/teact';
|
||||||
|
import { getActions } from '../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
import type { ApiUser, ApiChat, ApiTypeStory } from '../../api/types';
|
||||||
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getSenderTitle,
|
||||||
|
getUserColorKey,
|
||||||
|
getStoryMediaHash,
|
||||||
|
} from '../../global/helpers';
|
||||||
|
import renderText from './helpers/renderText';
|
||||||
|
import { getPictogramDimensions } from './helpers/mediaDimensions';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
|
||||||
|
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||||
|
import useMedia from '../../hooks/useMedia';
|
||||||
|
import useLang from '../../hooks/useLang';
|
||||||
|
import { useFastClick } from '../../hooks/useFastClick';
|
||||||
|
import useLastCallback from '../../hooks/useLastCallback';
|
||||||
|
|
||||||
|
import './EmbeddedMessage.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
story?: ApiTypeStory;
|
||||||
|
sender?: ApiUser | ApiChat;
|
||||||
|
noUserColors?: boolean;
|
||||||
|
isProtected?: boolean;
|
||||||
|
observeIntersectionForLoading?: ObserveFn;
|
||||||
|
onClick: NoneToVoidFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NBSP = '\u00A0';
|
||||||
|
|
||||||
|
const EmbeddedStory: FC<OwnProps> = ({
|
||||||
|
story,
|
||||||
|
sender,
|
||||||
|
noUserColors,
|
||||||
|
isProtected,
|
||||||
|
observeIntersectionForLoading,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
const { showNotification } = getActions();
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const isIntersecting = useIsIntersecting(ref, observeIntersectionForLoading);
|
||||||
|
const isFullStory = story && 'content' in story;
|
||||||
|
const isExpiredStory = story && 'isDeleted' in story;
|
||||||
|
const isVideoStory = isFullStory && Boolean(story.content.video);
|
||||||
|
const title = isFullStory ? 'Story' : (isExpiredStory ? 'ExpiredStory' : 'Loading');
|
||||||
|
|
||||||
|
const mediaBlobUrl = useMedia(isFullStory && getStoryMediaHash(story, 'pictogram'), !isIntersecting);
|
||||||
|
const mediaThumbnail = isVideoStory ? story.content.video!.thumbnail?.dataUri : undefined;
|
||||||
|
const pictogramUrl = mediaBlobUrl || mediaThumbnail;
|
||||||
|
|
||||||
|
const senderTitle = sender ? getSenderTitle(lang, sender) : undefined;
|
||||||
|
const handleFastClick = useLastCallback(() => {
|
||||||
|
if (story && !isExpiredStory) {
|
||||||
|
onClick();
|
||||||
|
} else {
|
||||||
|
showNotification({
|
||||||
|
message: lang('StoryNotFound'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleClick, handleMouseDown } = useFastClick(handleFastClick);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={buildClassName(
|
||||||
|
'EmbeddedMessage',
|
||||||
|
sender && !noUserColors && `color-${getUserColorKey(sender)}`,
|
||||||
|
)}
|
||||||
|
onClick={handleClick}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
>
|
||||||
|
{pictogramUrl && renderPictogram(pictogramUrl, isProtected)}
|
||||||
|
<div className={buildClassName('message-text', isExpiredStory && 'with-message-color')}>
|
||||||
|
<p dir="auto">
|
||||||
|
{isExpiredStory && (
|
||||||
|
<i className="icon icon-story-expired" aria-hidden />
|
||||||
|
)}
|
||||||
|
{lang(title)}
|
||||||
|
</p>
|
||||||
|
<div className="message-title" dir="auto">{renderText(senderTitle || NBSP)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderPictogram(
|
||||||
|
srcUrl: string,
|
||||||
|
isProtected?: boolean,
|
||||||
|
) {
|
||||||
|
const { width, height } = getPictogramDimensions();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="embedded-thumb">
|
||||||
|
<img
|
||||||
|
src={srcUrl}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
alt=""
|
||||||
|
className="pictogram"
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
{isProtected && <span className="protector" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EmbeddedStory;
|
||||||
@ -58,12 +58,12 @@ const InviteLink: FC<OwnProps> = ({
|
|||||||
color="translucent"
|
color="translucent"
|
||||||
className={isOpen ? 'active' : ''}
|
className={isOpen ? 'active' : ''}
|
||||||
onClick={onTrigger}
|
onClick={onTrigger}
|
||||||
ariaLabel="Actions"
|
ariaLabel={lang('AccDescrOpenMenu2')}
|
||||||
>
|
>
|
||||||
<i className="icon icon-more" />
|
<i className="icon icon-more" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}, [isMobile]);
|
}, [isMobile, lang]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="settings-item">
|
<div className="settings-item">
|
||||||
|
|||||||
@ -66,7 +66,7 @@ function MessageSummary({
|
|||||||
function renderMessageText() {
|
function renderMessageText() {
|
||||||
return (
|
return (
|
||||||
<MessageText
|
<MessageText
|
||||||
message={message}
|
messageOrStory={message}
|
||||||
translatedText={translatedText}
|
translatedText={translatedText}
|
||||||
highlight={highlight}
|
highlight={highlight}
|
||||||
isSimple
|
isSimple
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import React, {
|
|||||||
memo, useMemo, useRef,
|
memo, useMemo, useRef,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
|
|
||||||
import type { ApiFormattedText, ApiMessage } from '../../api/types';
|
import type { ApiFormattedText, ApiMessage, ApiStory } from '../../api/types';
|
||||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
|
|
||||||
import { ApiMessageEntityTypes } from '../../api/types';
|
import { ApiMessageEntityTypes } from '../../api/types';
|
||||||
@ -12,7 +12,7 @@ import { renderTextWithEntities } from './helpers/renderTextWithEntities';
|
|||||||
import useSyncEffect from '../../hooks/useSyncEffect';
|
import useSyncEffect from '../../hooks/useSyncEffect';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
message: ApiMessage;
|
messageOrStory: ApiMessage | ApiStory;
|
||||||
translatedText?: ApiFormattedText;
|
translatedText?: ApiFormattedText;
|
||||||
isForAnimation?: boolean;
|
isForAnimation?: boolean;
|
||||||
emojiSize?: number;
|
emojiSize?: number;
|
||||||
@ -30,7 +30,7 @@ interface OwnProps {
|
|||||||
const MIN_CUSTOM_EMOJIS_FOR_SHARED_CANVAS = 3;
|
const MIN_CUSTOM_EMOJIS_FOR_SHARED_CANVAS = 3;
|
||||||
|
|
||||||
function MessageText({
|
function MessageText({
|
||||||
message,
|
messageOrStory,
|
||||||
translatedText,
|
translatedText,
|
||||||
isForAnimation,
|
isForAnimation,
|
||||||
emojiSize,
|
emojiSize,
|
||||||
@ -51,7 +51,7 @@ function MessageText({
|
|||||||
|
|
||||||
const textCacheBusterRef = useRef(0);
|
const textCacheBusterRef = useRef(0);
|
||||||
|
|
||||||
const formattedText = translatedText || extractMessageText(message, inChatList);
|
const formattedText = translatedText || extractMessageText(messageOrStory, inChatList);
|
||||||
const adaptedFormattedText = isForAnimation && formattedText ? stripCustomEmoji(formattedText) : formattedText;
|
const adaptedFormattedText = isForAnimation && formattedText ? stripCustomEmoji(formattedText) : formattedText;
|
||||||
const { text, entities } = adaptedFormattedText || {};
|
const { text, entities } = adaptedFormattedText || {};
|
||||||
|
|
||||||
@ -70,7 +70,7 @@ function MessageText({
|
|||||||
}, [entities]) || 0;
|
}, [entities]) || 0;
|
||||||
|
|
||||||
if (!text) {
|
if (!text) {
|
||||||
const contentNotSupportedText = getMessageText(message);
|
const contentNotSupportedText = getMessageText(messageOrStory);
|
||||||
return contentNotSupportedText ? [trimText(contentNotSupportedText, truncateLength)] : undefined as any;
|
return contentNotSupportedText ? [trimText(contentNotSupportedText, truncateLength)] : undefined as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +85,7 @@ function MessageText({
|
|||||||
highlight,
|
highlight,
|
||||||
emojiSize,
|
emojiSize,
|
||||||
shouldRenderAsHtml,
|
shouldRenderAsHtml,
|
||||||
messageId: message.id,
|
messageId: messageOrStory.id,
|
||||||
isSimple,
|
isSimple,
|
||||||
isProtected,
|
isProtected,
|
||||||
observeIntersectionForLoading,
|
observeIntersectionForLoading,
|
||||||
|
|||||||
@ -37,6 +37,7 @@ type OwnProps = {
|
|||||||
isSearchable?: boolean;
|
isSearchable?: boolean;
|
||||||
isRoundCheckbox?: boolean;
|
isRoundCheckbox?: boolean;
|
||||||
lockedIds?: string[];
|
lockedIds?: string[];
|
||||||
|
forceShowSelf?: boolean;
|
||||||
onSelectedIdsChange?: (ids: string[]) => void;
|
onSelectedIdsChange?: (ids: string[]) => void;
|
||||||
onFilterChange?: (value: string) => void;
|
onFilterChange?: (value: string) => void;
|
||||||
onDisabledClick?: (id: string) => void;
|
onDisabledClick?: (id: string) => void;
|
||||||
@ -61,6 +62,7 @@ const Picker: FC<OwnProps> = ({
|
|||||||
isSearchable,
|
isSearchable,
|
||||||
isRoundCheckbox,
|
isRoundCheckbox,
|
||||||
lockedIds,
|
lockedIds,
|
||||||
|
forceShowSelf,
|
||||||
onSelectedIdsChange,
|
onSelectedIdsChange,
|
||||||
onFilterChange,
|
onFilterChange,
|
||||||
onDisabledClick,
|
onDisabledClick,
|
||||||
@ -134,6 +136,7 @@ const Picker: FC<OwnProps> = ({
|
|||||||
<PickerSelectedItem
|
<PickerSelectedItem
|
||||||
chatOrUserId={id}
|
chatOrUserId={id}
|
||||||
isMinimized={shouldMinimize && i < selectedIds.length - ALWAYS_FULL_ITEMS_COUNT}
|
isMinimized={shouldMinimize && i < selectedIds.length - ALWAYS_FULL_ITEMS_COUNT}
|
||||||
|
forceShowSelf={forceShowSelf}
|
||||||
onClick={handleItemClick}
|
onClick={handleItemClick}
|
||||||
clickArg={id}
|
clickArg={id}
|
||||||
/>
|
/>
|
||||||
@ -189,7 +192,7 @@ const Picker: FC<OwnProps> = ({
|
|||||||
>
|
>
|
||||||
{!isRoundCheckbox ? renderCheckbox() : undefined}
|
{!isRoundCheckbox ? renderCheckbox() : undefined}
|
||||||
{isUserId(id) ? (
|
{isUserId(id) ? (
|
||||||
<PrivateChatInfo userId={id} />
|
<PrivateChatInfo forceShowSelf={forceShowSelf} userId={id} />
|
||||||
) : (
|
) : (
|
||||||
<GroupChatInfo chatId={id} />
|
<GroupChatInfo chatId={id} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -20,15 +20,16 @@ type OwnProps = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
isMinimized?: boolean;
|
isMinimized?: boolean;
|
||||||
canClose?: boolean;
|
canClose?: boolean;
|
||||||
onClick: (arg: any) => void;
|
forceShowSelf?: boolean;
|
||||||
clickArg: any;
|
clickArg: any;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
onClick: (arg: any) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
chat?: ApiChat;
|
chat?: ApiChat;
|
||||||
user?: ApiUser;
|
user?: ApiUser;
|
||||||
currentUserId?: string;
|
isSavedMessages?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PickerSelectedItem: FC<OwnProps & StateProps> = ({
|
const PickerSelectedItem: FC<OwnProps & StateProps> = ({
|
||||||
@ -40,7 +41,7 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
|
|||||||
chat,
|
chat,
|
||||||
user,
|
user,
|
||||||
className,
|
className,
|
||||||
currentUserId,
|
isSavedMessages,
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
@ -61,13 +62,13 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
|
|||||||
<Avatar
|
<Avatar
|
||||||
peer={user || chat}
|
peer={user || chat}
|
||||||
size="small"
|
size="small"
|
||||||
isSavedMessages={user?.isSelf}
|
isSavedMessages={isSavedMessages}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const name = !chat || (user && !user.isSelf)
|
const name = !chat || (user && !isSavedMessages)
|
||||||
? getUserFirstOrLastName(user)
|
? getUserFirstOrLastName(user)
|
||||||
: getChatTitle(lang, chat, chat.id === currentUserId);
|
: getChatTitle(lang, chat, isSavedMessages);
|
||||||
|
|
||||||
titleText = name ? renderText(name) : undefined;
|
titleText = name ? renderText(name) : undefined;
|
||||||
}
|
}
|
||||||
@ -103,18 +104,19 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global, { chatOrUserId }): StateProps => {
|
(global, { chatOrUserId, forceShowSelf }): StateProps => {
|
||||||
if (!chatOrUserId) {
|
if (!chatOrUserId) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const chat = chatOrUserId ? selectChat(global, chatOrUserId) : undefined;
|
const chat = chatOrUserId ? selectChat(global, chatOrUserId) : undefined;
|
||||||
const user = isUserId(chatOrUserId) ? selectUser(global, chatOrUserId) : undefined;
|
const user = isUserId(chatOrUserId) ? selectUser(global, chatOrUserId) : undefined;
|
||||||
|
const isSavedMessages = !forceShowSelf && user && user.isSelf;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chat,
|
chat,
|
||||||
user,
|
user,
|
||||||
currentUserId: global.currentUserId,
|
isSavedMessages,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(PickerSelectedItem));
|
)(PickerSelectedItem));
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import Avatar from './Avatar';
|
|||||||
import TypingStatus from './TypingStatus';
|
import TypingStatus from './TypingStatus';
|
||||||
import DotAnimation from './DotAnimation';
|
import DotAnimation from './DotAnimation';
|
||||||
import FullNameTitle from './FullNameTitle';
|
import FullNameTitle from './FullNameTitle';
|
||||||
|
import RippleEffect from '../ui/RippleEffect';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
userId: string;
|
userId: string;
|
||||||
@ -31,9 +32,11 @@ type OwnProps = {
|
|||||||
forceShowSelf?: boolean;
|
forceShowSelf?: boolean;
|
||||||
status?: string;
|
status?: string;
|
||||||
statusIcon?: string;
|
statusIcon?: string;
|
||||||
|
ripple?: boolean;
|
||||||
withDots?: boolean;
|
withDots?: boolean;
|
||||||
withMediaViewer?: boolean;
|
withMediaViewer?: boolean;
|
||||||
withUsername?: boolean;
|
withUsername?: boolean;
|
||||||
|
withStory?: boolean;
|
||||||
withFullInfo?: boolean;
|
withFullInfo?: boolean;
|
||||||
withUpdatingStatus?: boolean;
|
withUpdatingStatus?: boolean;
|
||||||
noEmojiStatus?: boolean;
|
noEmojiStatus?: boolean;
|
||||||
@ -59,6 +62,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
|||||||
withDots,
|
withDots,
|
||||||
withMediaViewer,
|
withMediaViewer,
|
||||||
withUsername,
|
withUsername,
|
||||||
|
withStory,
|
||||||
withFullInfo,
|
withFullInfo,
|
||||||
withUpdatingStatus,
|
withUpdatingStatus,
|
||||||
emojiStatusSize,
|
emojiStatusSize,
|
||||||
@ -70,6 +74,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
|||||||
isSavedMessages,
|
isSavedMessages,
|
||||||
areMessagesLoaded,
|
areMessagesLoaded,
|
||||||
adminMember,
|
adminMember,
|
||||||
|
ripple,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
loadFullUser,
|
loadFullUser,
|
||||||
@ -177,12 +182,15 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
|||||||
size={avatarSize}
|
size={avatarSize}
|
||||||
peer={user}
|
peer={user}
|
||||||
isSavedMessages={isSavedMessages}
|
isSavedMessages={isSavedMessages}
|
||||||
|
withStory={withStory}
|
||||||
|
storyViewerMode="single-user"
|
||||||
onClick={withMediaViewer ? handleAvatarViewerOpen : undefined}
|
onClick={withMediaViewer ? handleAvatarViewerOpen : undefined}
|
||||||
/>
|
/>
|
||||||
<div className="info">
|
<div className="info">
|
||||||
{renderNameTitle()}
|
{renderNameTitle()}
|
||||||
{(status || (!isSavedMessages && !noStatusOrTyping)) && renderStatusOrTyping()}
|
{(status || (!isSavedMessages && !noStatusOrTyping)) && renderStatusOrTyping()}
|
||||||
</div>
|
</div>
|
||||||
|
{ripple && <RippleEffect />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
|||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
searchPlaceholder: string;
|
searchPlaceholder: string;
|
||||||
|
className?: string;
|
||||||
filter?: ApiChatType[];
|
filter?: ApiChatType[];
|
||||||
loadMore?: NoneToVoidFunction;
|
loadMore?: NoneToVoidFunction;
|
||||||
onSelectRecipient: (peerId: string, threadId?: number) => void;
|
onSelectRecipient: (peerId: string, threadId?: number) => void;
|
||||||
@ -49,6 +50,7 @@ const RecipientPicker: FC<OwnProps & StateProps> = ({
|
|||||||
pinnedIds,
|
pinnedIds,
|
||||||
contactIds,
|
contactIds,
|
||||||
filter = API_CHAT_TYPES,
|
filter = API_CHAT_TYPES,
|
||||||
|
className,
|
||||||
searchPlaceholder,
|
searchPlaceholder,
|
||||||
loadMore,
|
loadMore,
|
||||||
onSelectRecipient,
|
onSelectRecipient,
|
||||||
@ -93,6 +95,7 @@ const RecipientPicker: FC<OwnProps & StateProps> = ({
|
|||||||
return (
|
return (
|
||||||
<ChatOrUserPicker
|
<ChatOrUserPicker
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
|
className={className}
|
||||||
chatOrUserIds={renderingIds}
|
chatOrUserIds={renderingIds}
|
||||||
chatsById={chatsById}
|
chatsById={chatsById}
|
||||||
searchPlaceholder={searchPlaceholder}
|
searchPlaceholder={searchPlaceholder}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { getActions } from '../../global';
|
|||||||
|
|
||||||
import type { ApiPhoto, ApiReportReason } from '../../api/types';
|
import type { ApiPhoto, ApiReportReason } from '../../api/types';
|
||||||
|
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
import useLastCallback from '../../hooks/useLastCallback';
|
import useLastCallback from '../../hooks/useLastCallback';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
|
|
||||||
@ -16,10 +17,12 @@ import InputText from '../ui/InputText';
|
|||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
subject?: 'peer' | 'messages' | 'media';
|
subject?: 'peer' | 'messages' | 'media' | 'story';
|
||||||
chatId?: string;
|
chatId?: string;
|
||||||
|
userId?: string;
|
||||||
photo?: ApiPhoto;
|
photo?: ApiPhoto;
|
||||||
messageIds?: number[];
|
messageIds?: number[];
|
||||||
|
storyId?: number;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onCloseAnimationEnd?: () => void;
|
onCloseAnimationEnd?: () => void;
|
||||||
};
|
};
|
||||||
@ -28,8 +31,10 @@ const ReportModal: FC<OwnProps> = ({
|
|||||||
isOpen,
|
isOpen,
|
||||||
subject = 'messages',
|
subject = 'messages',
|
||||||
chatId,
|
chatId,
|
||||||
|
userId,
|
||||||
photo,
|
photo,
|
||||||
messageIds,
|
messageIds,
|
||||||
|
storyId,
|
||||||
onClose,
|
onClose,
|
||||||
onCloseAnimationEnd,
|
onCloseAnimationEnd,
|
||||||
}) => {
|
}) => {
|
||||||
@ -37,6 +42,7 @@ const ReportModal: FC<OwnProps> = ({
|
|||||||
reportMessages,
|
reportMessages,
|
||||||
reportPeer,
|
reportPeer,
|
||||||
reportProfilePhoto,
|
reportProfilePhoto,
|
||||||
|
reportStory,
|
||||||
exitMessageSelectMode,
|
exitMessageSelectMode,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
@ -57,6 +63,10 @@ const ReportModal: FC<OwnProps> = ({
|
|||||||
chatId, photo, reason: selectedReason, description,
|
chatId, photo, reason: selectedReason, description,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case 'story':
|
||||||
|
reportStory({
|
||||||
|
userId: userId!, storyId: storyId!, reason: selectedReason, description,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
onClose();
|
onClose();
|
||||||
});
|
});
|
||||||
@ -86,6 +96,7 @@ const ReportModal: FC<OwnProps> = ({
|
|||||||
(subject === 'messages' && !messageIds)
|
(subject === 'messages' && !messageIds)
|
||||||
|| (subject === 'peer' && !chatId)
|
|| (subject === 'peer' && !chatId)
|
||||||
|| (subject === 'media' && (!chatId || !photo))
|
|| (subject === 'media' && (!chatId || !photo))
|
||||||
|
|| (subject === 'story' && (!storyId || !userId))
|
||||||
) {
|
) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -100,7 +111,7 @@ const ReportModal: FC<OwnProps> = ({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onEnter={isOpen ? handleReport : undefined}
|
onEnter={isOpen ? handleReport : undefined}
|
||||||
onCloseAnimationEnd={onCloseAnimationEnd}
|
onCloseAnimationEnd={onCloseAnimationEnd}
|
||||||
className="narrow"
|
className={buildClassName('narrow', subject === 'story' && 'component-theme-dark')}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
|
|||||||
@ -41,7 +41,7 @@ type OwnProps = {
|
|||||||
stickerSet: StickerSetOrReactionsSetOrRecent;
|
stickerSet: StickerSetOrReactionsSetOrRecent;
|
||||||
loadAndPlay: boolean;
|
loadAndPlay: boolean;
|
||||||
index: number;
|
index: number;
|
||||||
idPrefix?: string;
|
idPrefix: string;
|
||||||
isNearActive: boolean;
|
isNearActive: boolean;
|
||||||
favoriteStickers?: ApiSticker[];
|
favoriteStickers?: ApiSticker[];
|
||||||
isSavedMessages?: boolean;
|
isSavedMessages?: boolean;
|
||||||
@ -53,6 +53,7 @@ type OwnProps = {
|
|||||||
withDefaultTopicIcon?: boolean;
|
withDefaultTopicIcon?: boolean;
|
||||||
withDefaultStatusIcon?: boolean;
|
withDefaultStatusIcon?: boolean;
|
||||||
isTranslucent?: boolean;
|
isTranslucent?: boolean;
|
||||||
|
noContextMenus?: boolean;
|
||||||
observeIntersection?: ObserveFn;
|
observeIntersection?: ObserveFn;
|
||||||
observeIntersectionForPlayingItems: ObserveFn;
|
observeIntersectionForPlayingItems: ObserveFn;
|
||||||
observeIntersectionForShowingItems: ObserveFn;
|
observeIntersectionForShowingItems: ObserveFn;
|
||||||
@ -90,6 +91,7 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
selectedReactionIds,
|
selectedReactionIds,
|
||||||
withDefaultStatusIcon,
|
withDefaultStatusIcon,
|
||||||
isTranslucent,
|
isTranslucent,
|
||||||
|
noContextMenus,
|
||||||
observeIntersection,
|
observeIntersection,
|
||||||
observeIntersectionForPlayingItems,
|
observeIntersectionForPlayingItems,
|
||||||
observeIntersectionForShowingItems,
|
observeIntersectionForShowingItems,
|
||||||
@ -243,7 +245,7 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
key={stickerSet.id}
|
key={stickerSet.id}
|
||||||
id={`${idPrefix || 'sticker-set'}-${index}`}
|
id={`${idPrefix}-${index}`}
|
||||||
className={
|
className={
|
||||||
buildClassName('symbol-set', isLocked && 'symbol-set-locked')
|
buildClassName('symbol-set', isLocked && 'symbol-set-locked')
|
||||||
}
|
}
|
||||||
@ -343,6 +345,7 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
isSavedMessages={isSavedMessages}
|
isSavedMessages={isSavedMessages}
|
||||||
isStatusPicker={isStatusPicker}
|
isStatusPicker={isStatusPicker}
|
||||||
canViewSet
|
canViewSet
|
||||||
|
noContextMenu={noContextMenus}
|
||||||
isCurrentUserPremium={isCurrentUserPremium}
|
isCurrentUserPremium={isCurrentUserPremium}
|
||||||
sharedCanvasRef={canvasRef}
|
sharedCanvasRef={canvasRef}
|
||||||
withTranslucentThumb={isTranslucent}
|
withTranslucentThumb={isTranslucent}
|
||||||
|
|||||||
@ -8,7 +8,8 @@
|
|||||||
html.theme-dark &,
|
html.theme-dark &,
|
||||||
html.theme-light .ListItem.selected &,
|
html.theme-light .ListItem.selected &,
|
||||||
.ActionMessage &,
|
.ActionMessage &,
|
||||||
.MediaViewerFooter & {
|
.MediaViewerFooter &,
|
||||||
|
#StoryViewer & {
|
||||||
background-image: url('../../../assets/spoiler-dots-white.png');
|
background-image: url('../../../assets/spoiler-dots-white.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,8 +2,13 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.chat-list {
|
.left-header {
|
||||||
height: calc(100% - var(--header-height));
|
position: relative;
|
||||||
|
z-index: var(--z-left-header);
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-header-shadow {
|
||||||
|
box-shadow: 0 2px 2px var(--color-light-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.DropdownMenuFiller {
|
.DropdownMenuFiller {
|
||||||
@ -32,4 +37,29 @@
|
|||||||
.archived-chats-more-menu {
|
.archived-chats-more-menu {
|
||||||
margin-left: auto !important;
|
margin-left: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.story-toggler-wrapper {
|
||||||
|
flex-grow: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-list-wrapper {
|
||||||
|
--story-ribbon-height: 5.5rem;
|
||||||
|
height: calc(100% - var(--header-height));
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.shown {
|
||||||
|
transform: translateY(calc(var(--story-ribbon-height) * -1));
|
||||||
|
height: calc(100% - var(--header-height) + var(--story-ribbon-height));
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open, &.closing {
|
||||||
|
transition: transform 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,8 @@ import ChatList from './main/ChatList';
|
|||||||
import ForumPanel from './main/ForumPanel';
|
import ForumPanel from './main/ForumPanel';
|
||||||
import DropdownMenu from '../ui/DropdownMenu';
|
import DropdownMenu from '../ui/DropdownMenu';
|
||||||
import MenuItem from '../ui/MenuItem';
|
import MenuItem from '../ui/MenuItem';
|
||||||
|
import StoryRibbon from '../story/StoryRibbon';
|
||||||
|
import StoryToggler from '../story/StoryToggler';
|
||||||
|
|
||||||
import './ArchivedChats.scss';
|
import './ArchivedChats.scss';
|
||||||
|
|
||||||
@ -27,6 +29,7 @@ export type OwnProps = {
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
isForumPanelOpen?: boolean;
|
isForumPanelOpen?: boolean;
|
||||||
archiveSettings: GlobalState['archiveSettings'];
|
archiveSettings: GlobalState['archiveSettings'];
|
||||||
|
isStoryRibbonShown?: boolean;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
onTopicSearch: NoneToVoidFunction;
|
onTopicSearch: NoneToVoidFunction;
|
||||||
onSettingsScreenSelect: (screen: SettingsScreens) => void;
|
onSettingsScreenSelect: (screen: SettingsScreens) => void;
|
||||||
@ -38,6 +41,7 @@ const ArchivedChats: FC<OwnProps> = ({
|
|||||||
isActive,
|
isActive,
|
||||||
isForumPanelOpen,
|
isForumPanelOpen,
|
||||||
archiveSettings,
|
archiveSettings,
|
||||||
|
isStoryRibbonShown,
|
||||||
onReset,
|
onReset,
|
||||||
onTopicSearch,
|
onTopicSearch,
|
||||||
onSettingsScreenSelect,
|
onSettingsScreenSelect,
|
||||||
@ -72,9 +76,15 @@ const ArchivedChats: FC<OwnProps> = ({
|
|||||||
} = useForumPanelRender(isForumPanelOpen);
|
} = useForumPanelRender(isForumPanelOpen);
|
||||||
const isForumPanelVisible = isForumPanelOpen && isAnimationStarted;
|
const isForumPanelVisible = isForumPanelOpen && isAnimationStarted;
|
||||||
|
|
||||||
|
const {
|
||||||
|
shouldRender: shouldRenderStoryRibbon,
|
||||||
|
transitionClassNames: storyRibbonClassNames,
|
||||||
|
isClosing: isStoryRibbonClosing,
|
||||||
|
} = useShowTransition(isStoryRibbonShown, undefined, undefined, '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ArchivedChats">
|
<div className="ArchivedChats">
|
||||||
<div className="left-header">
|
<div className={buildClassName('left-header', !shouldRenderStoryRibbon && 'left-header-shadow')}>
|
||||||
{lang.isRtl && <div className="DropdownMenuFiller" />}
|
{lang.isRtl && <div className="DropdownMenuFiller" />}
|
||||||
<Button
|
<Button
|
||||||
round
|
round
|
||||||
@ -92,6 +102,9 @@ const ArchivedChats: FC<OwnProps> = ({
|
|||||||
<i className="icon icon-arrow-left" />
|
<i className="icon icon-arrow-left" />
|
||||||
</Button>
|
</Button>
|
||||||
{shouldRenderTitle && <h3 className={titleClassNames}>{lang('ArchivedChats')}</h3>}
|
{shouldRenderTitle && <h3 className={titleClassNames}>{lang('ArchivedChats')}</h3>}
|
||||||
|
<div className="story-toggler-wrapper">
|
||||||
|
<StoryToggler canShow isArchived />
|
||||||
|
</div>
|
||||||
{archiveSettings.isHidden && (
|
{archiveSettings.isHidden && (
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
className="archived-chats-more-menu"
|
className="archived-chats-more-menu"
|
||||||
@ -104,15 +117,20 @@ const ArchivedChats: FC<OwnProps> = ({
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ChatList
|
<div className={buildClassName('chat-list-wrapper', storyRibbonClassNames)}>
|
||||||
folderType="archived"
|
{shouldRenderStoryRibbon && (
|
||||||
isActive={isActive}
|
<StoryRibbon isArchived className="left-header-shadow" isClosing={isStoryRibbonClosing} />
|
||||||
isForumPanelOpen={isForumPanelVisible}
|
)}
|
||||||
onSettingsScreenSelect={onSettingsScreenSelect}
|
<ChatList
|
||||||
onLeftColumnContentChange={onLeftColumnContentChange}
|
folderType="archived"
|
||||||
foldersDispatch={foldersDispatch}
|
isActive={isActive}
|
||||||
archiveSettings={archiveSettings}
|
isForumPanelOpen={isForumPanelVisible}
|
||||||
/>
|
onSettingsScreenSelect={onSettingsScreenSelect}
|
||||||
|
onLeftColumnContentChange={onLeftColumnContentChange}
|
||||||
|
foldersDispatch={foldersDispatch}
|
||||||
|
archiveSettings={archiveSettings}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{shouldRenderForumPanel && (
|
{shouldRenderForumPanel && (
|
||||||
<ForumPanel
|
<ForumPanel
|
||||||
isOpen={isForumPanelOpen}
|
isOpen={isForumPanelOpen}
|
||||||
|
|||||||
@ -45,6 +45,7 @@ type StateProps = {
|
|||||||
forumPanelChatId?: string;
|
forumPanelChatId?: string;
|
||||||
isClosingSearch?: boolean;
|
isClosingSearch?: boolean;
|
||||||
archiveSettings: GlobalState['archiveSettings'];
|
archiveSettings: GlobalState['archiveSettings'];
|
||||||
|
isArchivedStoryRibbonShown?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum ContentType {
|
enum ContentType {
|
||||||
@ -77,6 +78,7 @@ function LeftColumn({
|
|||||||
forumPanelChatId,
|
forumPanelChatId,
|
||||||
isClosingSearch,
|
isClosingSearch,
|
||||||
archiveSettings,
|
archiveSettings,
|
||||||
|
isArchivedStoryRibbonShown,
|
||||||
}: OwnProps & StateProps) {
|
}: OwnProps & StateProps) {
|
||||||
const {
|
const {
|
||||||
setGlobalSearchQuery,
|
setGlobalSearchQuery,
|
||||||
@ -439,6 +441,7 @@ function LeftColumn({
|
|||||||
onLeftColumnContentChange={setContent}
|
onLeftColumnContentChange={setContent}
|
||||||
isForumPanelOpen={isForumPanelOpen}
|
isForumPanelOpen={isForumPanelOpen}
|
||||||
archiveSettings={archiveSettings}
|
archiveSettings={archiveSettings}
|
||||||
|
isStoryRibbonShown={isArchivedStoryRibbonShown}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case ContentType.Settings:
|
case ContentType.Settings:
|
||||||
@ -525,6 +528,9 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
activeChatFolder,
|
activeChatFolder,
|
||||||
nextSettingsScreen,
|
nextSettingsScreen,
|
||||||
nextFoldersAction,
|
nextFoldersAction,
|
||||||
|
storyViewer: {
|
||||||
|
isArchivedRibbonShown,
|
||||||
|
},
|
||||||
} = tabState;
|
} = tabState;
|
||||||
const {
|
const {
|
||||||
currentUserId,
|
currentUserId,
|
||||||
@ -555,6 +561,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
forumPanelChatId,
|
forumPanelChatId,
|
||||||
isClosingSearch: tabState.globalSearch.isClosing,
|
isClosingSearch: tabState.globalSearch.isClosing,
|
||||||
archiveSettings,
|
archiveSettings,
|
||||||
|
isArchivedStoryRibbonShown: isArchivedRibbonShown,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(LeftColumn));
|
)(LeftColumn));
|
||||||
|
|||||||
@ -2,6 +2,11 @@
|
|||||||
--background-color: var(--color-background);
|
--background-color: var(--color-background);
|
||||||
--thumbs-background: var(--background-color);
|
--thumbs-background: var(--background-color);
|
||||||
|
|
||||||
|
--z-forum-indicator: 2;
|
||||||
|
--z-badge: 4;
|
||||||
|
--z-ripple: 6;
|
||||||
|
--z-status: 8; // Avatar stories require a higher z-index than the ripple to work
|
||||||
|
|
||||||
body.is-ios &,
|
body.is-ios &,
|
||||||
body.is-macos & {
|
body.is-macos & {
|
||||||
--color-text-meta: var(--color-text-meta-apple);
|
--color-text-meta: var(--color-text-meta-apple);
|
||||||
@ -168,7 +173,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
background: var(--color-primary);
|
background: var(--color-primary);
|
||||||
z-index: 1;
|
z-index: var(--z-forum-indicator);
|
||||||
|
|
||||||
border-start-end-radius: var(--border-radius-default);
|
border-start-end-radius: var(--border-radius-default);
|
||||||
border-end-end-radius: var(--border-radius-default);
|
border-end-end-radius: var(--border-radius-default);
|
||||||
@ -191,7 +196,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ripple-container {
|
.ripple-container {
|
||||||
z-index: 2;
|
z-index: var(--z-ripple);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
@ -199,15 +204,15 @@
|
|||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 1;
|
z-index: var(--z-status);
|
||||||
background: var(--background-color);
|
background-color: var(--background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-badge-wrapper {
|
.avatar-badge-wrapper {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0.5rem;
|
right: 0.5rem;
|
||||||
z-index: 2;
|
z-index: var(--z-badge);
|
||||||
|
|
||||||
--outline-color: var(--color-background);
|
--outline-color: var(--color-background);
|
||||||
|
|
||||||
@ -312,8 +317,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.colon {
|
.colon, .forward {
|
||||||
margin-inline-end: 0.25rem;
|
margin-inline-end: 0.1875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forward {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
display: inline-block;
|
||||||
|
transform: translateY(1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-preview-spoiler {
|
.media-preview-spoiler {
|
||||||
|
|||||||
@ -259,10 +259,13 @@ const Chat: FC<OwnProps & StateProps> = ({
|
|||||||
onDragEnter={handleDragEnter}
|
onDragEnter={handleDragEnter}
|
||||||
withPortalForMenu
|
withPortalForMenu
|
||||||
>
|
>
|
||||||
<div className="status">
|
<div className={buildClassName('status', 'status-clickable')}>
|
||||||
<Avatar
|
<Avatar
|
||||||
peer={peer}
|
peer={peer}
|
||||||
isSavedMessages={user?.isSelf}
|
isSavedMessages={user?.isSelf}
|
||||||
|
withStory={user && !user?.isSelf}
|
||||||
|
withStoryGap={isAvatarOnlineShown}
|
||||||
|
storyViewerMode="single-user"
|
||||||
/>
|
/>
|
||||||
<div className="avatar-badge-wrapper">
|
<div className="avatar-badge-wrapper">
|
||||||
<div className={buildClassName('avatar-online', isAvatarOnlineShown && 'avatar-online-shown')} />
|
<div className={buildClassName('avatar-online', isAvatarOnlineShown && 'avatar-online-shown')} />
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import React, {
|
import React, {
|
||||||
memo, useEffect, useMemo, useRef,
|
memo, useEffect, useLayoutEffect, useMemo, useRef, useState,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions, getGlobal, withGlobal } from '../../../global';
|
import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||||
|
|
||||||
@ -10,7 +10,7 @@ import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReduc
|
|||||||
import type { GlobalState } from '../../../global/types';
|
import type { GlobalState } from '../../../global/types';
|
||||||
import type { TabWithProperties } from '../../ui/TabList';
|
import type { TabWithProperties } from '../../ui/TabList';
|
||||||
|
|
||||||
import { ALL_FOLDER_ID } from '../../../config';
|
import { ALL_FOLDER_ID, ANIMATION_END_DELAY } from '../../../config';
|
||||||
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
||||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||||
import { captureEvents, SwipeDirection } from '../../../util/captureEvents';
|
import { captureEvents, SwipeDirection } from '../../../util/captureEvents';
|
||||||
@ -28,6 +28,7 @@ import { useFolderManagerForUnreadCounters } from '../../../hooks/useFolderManag
|
|||||||
import Transition from '../../ui/Transition';
|
import Transition from '../../ui/Transition';
|
||||||
import TabList from '../../ui/TabList';
|
import TabList from '../../ui/TabList';
|
||||||
import ChatList from './ChatList';
|
import ChatList from './ChatList';
|
||||||
|
import StoryRibbon from '../../story/StoryRibbon';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
onSettingsScreenSelect: (screen: SettingsScreens) => void;
|
onSettingsScreenSelect: (screen: SettingsScreens) => void;
|
||||||
@ -48,11 +49,14 @@ type StateProps = {
|
|||||||
maxChatLists: number;
|
maxChatLists: number;
|
||||||
maxFolderInvites: number;
|
maxFolderInvites: number;
|
||||||
hasArchivedChats?: boolean;
|
hasArchivedChats?: boolean;
|
||||||
|
hasArchivedStories?: boolean;
|
||||||
archiveSettings: GlobalState['archiveSettings'];
|
archiveSettings: GlobalState['archiveSettings'];
|
||||||
|
isStoryRibbonShown?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SAVED_MESSAGES_HOTKEY = '0';
|
const SAVED_MESSAGES_HOTKEY = '0';
|
||||||
const FIRST_FOLDER_INDEX = 0;
|
const FIRST_FOLDER_INDEX = 0;
|
||||||
|
const STORY_RIBBON_APPEARANCE_DURATION_MS = 200 + ANIMATION_END_DELAY;
|
||||||
|
|
||||||
const ChatFolders: FC<OwnProps & StateProps> = ({
|
const ChatFolders: FC<OwnProps & StateProps> = ({
|
||||||
foldersDispatch,
|
foldersDispatch,
|
||||||
@ -70,7 +74,9 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
|||||||
folderInvitesById,
|
folderInvitesById,
|
||||||
maxFolderInvites,
|
maxFolderInvites,
|
||||||
hasArchivedChats,
|
hasArchivedChats,
|
||||||
|
hasArchivedStories,
|
||||||
archiveSettings,
|
archiveSettings,
|
||||||
|
isStoryRibbonShown,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
loadChatFolders,
|
loadChatFolders,
|
||||||
@ -86,11 +92,34 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
|||||||
const transitionRef = useRef<HTMLDivElement>(null);
|
const transitionRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
const [isStoryRibbonAnimated, setIsStoryRibbonAnimated] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadChatFolders();
|
loadChatFolders();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
let timeoutId: number;
|
||||||
|
|
||||||
|
if (isStoryRibbonShown) {
|
||||||
|
timeoutId = window.setTimeout(() => {
|
||||||
|
setIsStoryRibbonAnimated(true);
|
||||||
|
}, STORY_RIBBON_APPEARANCE_DURATION_MS);
|
||||||
|
} else {
|
||||||
|
setIsStoryRibbonAnimated(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
|
}, [isStoryRibbonShown]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
shouldRender: shouldRenderStoryRibbon,
|
||||||
|
transitionClassNames: storyRibbonClassNames,
|
||||||
|
isClosing: isStoryRibbonClosing,
|
||||||
|
} = useShowTransition(isStoryRibbonShown, undefined, undefined, '');
|
||||||
|
|
||||||
const allChatsFolder: ApiChatFolder = useMemo(() => {
|
const allChatsFolder: ApiChatFolder = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
id: ALL_FOLDER_ID,
|
id: ALL_FOLDER_ID,
|
||||||
@ -285,7 +314,7 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
|||||||
foldersDispatch={foldersDispatch}
|
foldersDispatch={foldersDispatch}
|
||||||
onSettingsScreenSelect={onSettingsScreenSelect}
|
onSettingsScreenSelect={onSettingsScreenSelect}
|
||||||
onLeftColumnContentChange={onLeftColumnContentChange}
|
onLeftColumnContentChange={onLeftColumnContentChange}
|
||||||
canDisplayArchive={hasArchivedChats && !archiveSettings.isHidden}
|
canDisplayArchive={(hasArchivedChats || hasArchivedStories) && !archiveSettings.isHidden}
|
||||||
archiveSettings={archiveSettings}
|
archiveSettings={archiveSettings}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -298,8 +327,11 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
|||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
'ChatFolders',
|
'ChatFolders',
|
||||||
shouldRenderFolders && shouldHideFolderTabs && 'ChatFolders--tabs-hidden',
|
shouldRenderFolders && shouldHideFolderTabs && 'ChatFolders--tabs-hidden',
|
||||||
|
shouldRenderStoryRibbon && !isStoryRibbonAnimated && 'withStoryRibbon',
|
||||||
|
storyRibbonClassNames,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{shouldRenderStoryRibbon && <StoryRibbon isClosing={isStoryRibbonClosing} />}
|
||||||
{shouldRenderFolders ? (
|
{shouldRenderFolders ? (
|
||||||
<TabList
|
<TabList
|
||||||
contextRootElementSelector="#LeftColumn"
|
contextRootElementSelector="#LeftColumn"
|
||||||
@ -336,10 +368,16 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
archived,
|
archived,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
stories: {
|
||||||
|
orderedUserIds: {
|
||||||
|
archived: archivedStories,
|
||||||
|
},
|
||||||
|
},
|
||||||
currentUserId,
|
currentUserId,
|
||||||
archiveSettings,
|
archiveSettings,
|
||||||
} = global;
|
} = global;
|
||||||
const { shouldSkipHistoryAnimations, activeChatFolder } = selectTabState(global);
|
const { shouldSkipHistoryAnimations, activeChatFolder } = selectTabState(global);
|
||||||
|
const { storyViewer: { isRibbonShown: isStoryRibbonShown } } = selectTabState(global);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chatFoldersById,
|
chatFoldersById,
|
||||||
@ -349,10 +387,12 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
currentUserId,
|
currentUserId,
|
||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
hasArchivedChats: Boolean(archived?.length),
|
hasArchivedChats: Boolean(archived?.length),
|
||||||
|
hasArchivedStories: Boolean(archivedStories?.length),
|
||||||
maxFolders: selectCurrentLimit(global, 'dialogFilters'),
|
maxFolders: selectCurrentLimit(global, 'dialogFilters'),
|
||||||
maxFolderInvites: selectCurrentLimit(global, 'chatlistInvites'),
|
maxFolderInvites: selectCurrentLimit(global, 'chatlistInvites'),
|
||||||
maxChatLists: selectCurrentLimit(global, 'chatlistJoined'),
|
maxChatLists: selectCurrentLimit(global, 'chatlistJoined'),
|
||||||
archiveSettings,
|
archiveSettings,
|
||||||
|
isStoryRibbonShown,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(ChatFolders));
|
)(ChatFolders));
|
||||||
|
|||||||
@ -27,12 +27,14 @@ import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'
|
|||||||
import { useHotkeys } from '../../../hooks/useHotkeys';
|
import { useHotkeys } from '../../../hooks/useHotkeys';
|
||||||
import useDebouncedCallback from '../../../hooks/useDebouncedCallback';
|
import useDebouncedCallback from '../../../hooks/useDebouncedCallback';
|
||||||
import useOrderDiff from './hooks/useOrderDiff';
|
import useOrderDiff from './hooks/useOrderDiff';
|
||||||
|
import useUserStoriesPolling from '../../../hooks/polling/useUserStoriesPolling';
|
||||||
|
|
||||||
import InfiniteScroll from '../../ui/InfiniteScroll';
|
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||||
import Loading from '../../ui/Loading';
|
import Loading from '../../ui/Loading';
|
||||||
import Chat from './Chat';
|
import Chat from './Chat';
|
||||||
import EmptyFolder from './EmptyFolder';
|
import EmptyFolder from './EmptyFolder';
|
||||||
import Archive from './Archive';
|
import Archive from './Archive';
|
||||||
|
import useTopOverscroll from '../../../hooks/scroll/useTopOverscroll';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
folderType: 'all' | 'archived' | 'folder';
|
folderType: 'all' | 'archived' | 'folder';
|
||||||
@ -41,6 +43,8 @@ type OwnProps = {
|
|||||||
canDisplayArchive?: boolean;
|
canDisplayArchive?: boolean;
|
||||||
archiveSettings: GlobalState['archiveSettings'];
|
archiveSettings: GlobalState['archiveSettings'];
|
||||||
isForumPanelOpen?: boolean;
|
isForumPanelOpen?: boolean;
|
||||||
|
isStoryRibbonShown?: boolean;
|
||||||
|
className?: string;
|
||||||
foldersDispatch: FolderEditDispatch;
|
foldersDispatch: FolderEditDispatch;
|
||||||
onSettingsScreenSelect: (screen: SettingsScreens) => void;
|
onSettingsScreenSelect: (screen: SettingsScreens) => void;
|
||||||
onLeftColumnContentChange: (content: LeftColumnContent) => void;
|
onLeftColumnContentChange: (content: LeftColumnContent) => void;
|
||||||
@ -61,18 +65,25 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
onSettingsScreenSelect,
|
onSettingsScreenSelect,
|
||||||
onLeftColumnContentChange,
|
onLeftColumnContentChange,
|
||||||
}) => {
|
}) => {
|
||||||
const { openChat, openNextChat, closeForumPanel } = getActions();
|
const {
|
||||||
|
openChat,
|
||||||
|
openNextChat,
|
||||||
|
closeForumPanel,
|
||||||
|
toggleStoryRibbon,
|
||||||
|
} = getActions();
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const shouldIgnoreDragRef = useRef(false);
|
const shouldIgnoreDragRef = useRef(false);
|
||||||
|
|
||||||
|
const isArchived = folderType === 'archived';
|
||||||
const resolvedFolderId = (
|
const resolvedFolderId = (
|
||||||
folderType === 'all' ? ALL_FOLDER_ID : folderType === 'archived' ? ARCHIVED_FOLDER_ID : folderId!
|
folderType === 'all' ? ALL_FOLDER_ID : isArchived ? ARCHIVED_FOLDER_ID : folderId!
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldDisplayArchive = folderType === 'all' && canDisplayArchive;
|
const shouldDisplayArchive = folderType === 'all' && canDisplayArchive;
|
||||||
|
|
||||||
const orderedIds = useFolderManagerForOrderedIds(resolvedFolderId);
|
const orderedIds = useFolderManagerForOrderedIds(resolvedFolderId);
|
||||||
|
useUserStoriesPolling(orderedIds);
|
||||||
|
|
||||||
const chatsHeight = (orderedIds?.length || 0) * CHAT_HEIGHT_PX;
|
const chatsHeight = (orderedIds?.length || 0) * CHAT_HEIGHT_PX;
|
||||||
const archiveHeight = shouldDisplayArchive
|
const archiveHeight = shouldDisplayArchive
|
||||||
@ -162,6 +173,16 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
shouldIgnoreDragRef.current = true;
|
shouldIgnoreDragRef.current = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleShowStoryRibbon = useLastCallback(() => {
|
||||||
|
toggleStoryRibbon({ isShown: true, isArchived });
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleHideStoryRibbon = useLastCallback(() => {
|
||||||
|
toggleStoryRibbon({ isShown: false, isArchived });
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderedOverflowTrigger = useTopOverscroll(containerRef, handleShowStoryRibbon, handleHideStoryRibbon);
|
||||||
|
|
||||||
function renderChats() {
|
function renderChats() {
|
||||||
const viewportOffset = orderedIds!.indexOf(viewportIds![0]);
|
const viewportOffset = orderedIds!.indexOf(viewportIds![0]);
|
||||||
|
|
||||||
@ -196,6 +217,7 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
itemSelector=".ListItem:not(.chat-item-archive)"
|
itemSelector=".ListItem:not(.chat-item-archive)"
|
||||||
preloadBackwards={CHAT_LIST_SLICE}
|
preloadBackwards={CHAT_LIST_SLICE}
|
||||||
withAbsolutePositioning
|
withAbsolutePositioning
|
||||||
|
beforeChildren={renderedOverflowTrigger}
|
||||||
maxHeight={chatsHeight + archiveHeight}
|
maxHeight={chatsHeight + archiveHeight}
|
||||||
onLoadMore={getMore}
|
onLoadMore={getMore}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
|
|||||||
@ -71,12 +71,11 @@ const ContactList: FC<OwnProps & StateProps> = ({
|
|||||||
viewportIds.map((id) => (
|
viewportIds.map((id) => (
|
||||||
<ListItem
|
<ListItem
|
||||||
key={id}
|
key={id}
|
||||||
className="chat-item-clickable"
|
className="chat-item-clickable contact-list-item"
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => handleClick(id)}
|
onClick={() => handleClick(id)}
|
||||||
ripple={!isMobile}
|
|
||||||
>
|
>
|
||||||
<PrivateChatInfo userId={id} forceShowSelf avatarSize="large" />
|
<PrivateChatInfo userId={id} forceShowSelf avatarSize="large" withStory ripple={!isMobile} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))
|
))
|
||||||
) : viewportIds && !viewportIds.length ? (
|
) : viewportIds && !viewportIds.length ? (
|
||||||
|
|||||||
@ -16,6 +16,23 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.withStoryRibbon {
|
||||||
|
--story-ribbon-height: 5.5rem;
|
||||||
|
&.shown {
|
||||||
|
transform: translateY(calc(var(--story-ribbon-height) * -1));
|
||||||
|
height: calc(100% + var(--story-ribbon-height));
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open, &.closing {
|
||||||
|
transition: transform 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tabs-placeholder {
|
.tabs-placeholder {
|
||||||
height: 2.625rem;
|
height: 2.625rem;
|
||||||
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||||
|
|||||||
@ -61,6 +61,7 @@ import ShowTransition from '../../ui/ShowTransition';
|
|||||||
import ConnectionStatusOverlay from '../ConnectionStatusOverlay';
|
import ConnectionStatusOverlay from '../ConnectionStatusOverlay';
|
||||||
import StatusButton from './StatusButton';
|
import StatusButton from './StatusButton';
|
||||||
import Toggle from '../../ui/Toggle';
|
import Toggle from '../../ui/Toggle';
|
||||||
|
import StoryToggler from '../../story/StoryToggler';
|
||||||
|
|
||||||
import './LeftMainHeader.scss';
|
import './LeftMainHeader.scss';
|
||||||
|
|
||||||
@ -132,6 +133,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
openChat,
|
openChat,
|
||||||
|
openChatWithInfo,
|
||||||
setGlobalSearchDate,
|
setGlobalSearchDate,
|
||||||
setSettingOption,
|
setSettingOption,
|
||||||
setGlobalSearchChatId,
|
setGlobalSearchChatId,
|
||||||
@ -173,6 +175,10 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleOpenMyStories = useLastCallback(() => {
|
||||||
|
openChatWithInfo({ id: currentUserId, shouldReplaceHistory: true, profileTab: 'stories' });
|
||||||
|
});
|
||||||
|
|
||||||
useHotkeys(canSetPasscode ? {
|
useHotkeys(canSetPasscode ? {
|
||||||
'Ctrl+Shift+L': handleLockScreenHotkey,
|
'Ctrl+Shift+L': handleLockScreenHotkey,
|
||||||
'Alt+Shift+L': handleLockScreenHotkey,
|
'Alt+Shift+L': handleLockScreenHotkey,
|
||||||
@ -317,6 +323,12 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
>
|
>
|
||||||
{lang('Contacts')}
|
{lang('Contacts')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
icon="play-story"
|
||||||
|
onClick={handleOpenMyStories}
|
||||||
|
>
|
||||||
|
{lang('Settings.MyStories')}
|
||||||
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="settings"
|
icon="settings"
|
||||||
onClick={onSelectSettings}
|
onClick={onSelectSettings}
|
||||||
@ -453,6 +465,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
onSpinnerClick={connectionStatusPosition === 'minimized' ? toggleConnectionStatus : undefined}
|
onSpinnerClick={connectionStatusPosition === 'minimized' ? toggleConnectionStatus : undefined}
|
||||||
>
|
>
|
||||||
{searchContent}
|
{searchContent}
|
||||||
|
<StoryToggler canShow={!isSearchFocused && !selectedSearchDate && !globalSearchChatId} />
|
||||||
</SearchInput>
|
</SearchInput>
|
||||||
{isCurrentUserPremium && <StatusButton />}
|
{isCurrentUserPremium && <StatusButton />}
|
||||||
{hasPasscode && (
|
{hasPasscode && (
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
:global(body:not(.no-menu-blur)) & {
|
:global(body:not(.no-menu-blur)) & {
|
||||||
--color-background: var(--color-background-compact-menu);
|
--color-background: var(--color-background-compact-menu);
|
||||||
|
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(25px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 26rem) {
|
@media (max-width: 26rem) {
|
||||||
|
|||||||
@ -151,6 +151,7 @@ export default function useChatListEntry({
|
|||||||
<span className="colon">:</span>
|
<span className="colon">:</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{lastMessage.forwardInfo && (<i className="icon icon-share-filled forward" />)}
|
||||||
{renderSummary(lang, lastMessage, observeIntersection, mediaBlobUrl || mediaThumbnail, isRoundVideo)}
|
{renderSummary(lang, lastMessage, observeIntersection, mediaBlobUrl || mediaThumbnail, isRoundVideo)}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -117,6 +117,10 @@
|
|||||||
|
|
||||||
.ListItem.search-result {
|
.ListItem.search-result {
|
||||||
.ChatInfo {
|
.ChatInfo {
|
||||||
|
// Fix for overflow hidden and stories indicator
|
||||||
|
padding: 0.0625rem;
|
||||||
|
margin: -0.0625rem;
|
||||||
|
|
||||||
.handle {
|
.handle {
|
||||||
unicode-bidi: plaintext;
|
unicode-bidi: plaintext;
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
|
|||||||
@ -85,7 +85,7 @@ const LeftSearchResultChat: FC<OwnProps & StateProps> = ({
|
|||||||
buttonRef={buttonRef}
|
buttonRef={buttonRef}
|
||||||
>
|
>
|
||||||
{isUserId(chatId) ? (
|
{isUserId(chatId) ? (
|
||||||
<PrivateChatInfo userId={chatId} withUsername={withUsername} avatarSize="large" />
|
<PrivateChatInfo userId={chatId} withUsername={withUsername} withStory avatarSize="large" />
|
||||||
) : (
|
) : (
|
||||||
<GroupChatInfo chatId={chatId} withUsername={withUsername} avatarSize="large" />
|
<GroupChatInfo chatId={chatId} withUsername={withUsername} avatarSize="large" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -2,13 +2,15 @@ import type { FC } from '../../lib/teact/teact';
|
|||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect,
|
memo, useCallback, useEffect,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../global';
|
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||||
|
|
||||||
import { selectTabState } from '../../global/selectors';
|
import { selectChat, selectTabState, selectUser } from '../../global/selectors';
|
||||||
|
import { getChatTitle, getUserFirstOrLastName, isUserId } from '../../global/helpers';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import useFlag from '../../hooks/useFlag';
|
import useFlag from '../../hooks/useFlag';
|
||||||
|
|
||||||
import RecipientPicker from '../common/RecipientPicker';
|
import RecipientPicker from '../common/RecipientPicker';
|
||||||
|
import usePrevious from '../../hooks/usePrevious';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -17,22 +19,26 @@ export type OwnProps = {
|
|||||||
interface StateProps {
|
interface StateProps {
|
||||||
currentUserId?: string;
|
currentUserId?: string;
|
||||||
isManyMessages?: boolean;
|
isManyMessages?: boolean;
|
||||||
|
isStory?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
|
const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
isManyMessages,
|
isManyMessages,
|
||||||
|
isStory,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
setForwardChatOrTopic,
|
setForwardChatOrTopic,
|
||||||
exitForwardMode,
|
exitForwardMode,
|
||||||
forwardToSavedMessages,
|
forwardToSavedMessages,
|
||||||
|
forwardStory,
|
||||||
showNotification,
|
showNotification,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
|
const renderingIsStory = usePrevious(isStory, true);
|
||||||
const [isShown, markIsShown, unmarkIsShown] = useFlag();
|
const [isShown, markIsShown, unmarkIsShown] = useFlag();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
@ -41,17 +47,43 @@ const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
|
|||||||
}, [isOpen, markIsShown]);
|
}, [isOpen, markIsShown]);
|
||||||
|
|
||||||
const handleSelectRecipient = useCallback((recipientId: string, threadId?: number) => {
|
const handleSelectRecipient = useCallback((recipientId: string, threadId?: number) => {
|
||||||
if (recipientId === currentUserId) {
|
const isSelf = recipientId === currentUserId;
|
||||||
forwardToSavedMessages();
|
if (isStory) {
|
||||||
showNotification({
|
forwardStory({ toChatId: recipientId });
|
||||||
message: lang(isManyMessages
|
const global = getGlobal();
|
||||||
|
if (isUserId(recipientId)) {
|
||||||
|
showNotification({
|
||||||
|
message: isSelf
|
||||||
|
? lang('Conversation.StoryForwardTooltip.SavedMessages.One')
|
||||||
|
: lang(
|
||||||
|
'StorySharedTo',
|
||||||
|
getUserFirstOrLastName(selectUser(global, recipientId)),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const chat = selectChat(global, recipientId);
|
||||||
|
if (!chat) return;
|
||||||
|
|
||||||
|
showNotification({
|
||||||
|
message: lang('StorySharedTo', getChatTitle(lang, chat)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSelf) {
|
||||||
|
const message = lang(
|
||||||
|
isManyMessages
|
||||||
? 'Conversation.ForwardTooltip.SavedMessages.Many'
|
? 'Conversation.ForwardTooltip.SavedMessages.Many'
|
||||||
: 'Conversation.ForwardTooltip.SavedMessages.One'),
|
: 'Conversation.ForwardTooltip.SavedMessages.One',
|
||||||
});
|
);
|
||||||
|
|
||||||
|
forwardToSavedMessages();
|
||||||
|
showNotification({ message });
|
||||||
} else {
|
} else {
|
||||||
setForwardChatOrTopic({ chatId: recipientId, topicId: threadId });
|
setForwardChatOrTopic({ chatId: recipientId, topicId: threadId });
|
||||||
}
|
}
|
||||||
}, [currentUserId, forwardToSavedMessages, isManyMessages, lang, setForwardChatOrTopic, showNotification]);
|
}, [currentUserId, isManyMessages, isStory, lang]);
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
exitForwardMode();
|
exitForwardMode();
|
||||||
@ -64,6 +96,7 @@ const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
|
|||||||
return (
|
return (
|
||||||
<RecipientPicker
|
<RecipientPicker
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
|
className={renderingIsStory ? 'component-theme-dark' : undefined}
|
||||||
searchPlaceholder={lang('ForwardTo')}
|
searchPlaceholder={lang('ForwardTo')}
|
||||||
onSelectRecipient={handleSelectRecipient}
|
onSelectRecipient={handleSelectRecipient}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
@ -73,8 +106,10 @@ const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default memo(withGlobal<OwnProps>((global): StateProps => {
|
export default memo(withGlobal<OwnProps>((global): StateProps => {
|
||||||
|
const { messageIds, storyId } = selectTabState(global).forwardMessages;
|
||||||
return {
|
return {
|
||||||
currentUserId: global.currentUserId,
|
currentUserId: global.currentUserId,
|
||||||
isManyMessages: (selectTabState(global).forwardMessages.messageIds?.length || 0) > 1,
|
isManyMessages: (messageIds?.length || 0) > 1,
|
||||||
|
isStory: Boolean(storyId),
|
||||||
};
|
};
|
||||||
})(ForwardRecipientPicker));
|
})(ForwardRecipientPicker));
|
||||||
|
|||||||
@ -40,10 +40,6 @@
|
|||||||
|
|
||||||
background-color: var(--color-background);
|
background-color: var(--color-background);
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
height: calc(var(--vh, 1vh) * 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 926px) {
|
@media (min-width: 926px) {
|
||||||
--left-column-max-width: 40vw;
|
--left-column-max-width: 40vw;
|
||||||
}
|
}
|
||||||
@ -115,6 +111,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
|
height: calc(var(--vh, 1vh) * 100);
|
||||||
max-width: none;
|
max-width: none;
|
||||||
--left-column-max-width: calc(100vw - env(safe-area-inset-left));
|
--left-column-max-width: calc(100vw - env(safe-area-inset-left));
|
||||||
transform: translate3d(-20vw, 0, 0);
|
transform: translate3d(-20vw, 0, 0);
|
||||||
@ -126,6 +123,18 @@
|
|||||||
left: 0 !important;
|
left: 0 !important;
|
||||||
width: 100vw !important;
|
width: 100vw !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fix: when opening the SymbolMenu, the chat list flashes in the background
|
||||||
|
body.is-symbol-menu-open &::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: var(--color-background);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,7 @@ import {
|
|||||||
selectPerformanceSettingsValue,
|
selectPerformanceSettingsValue,
|
||||||
selectCanAnimateInterface,
|
selectCanAnimateInterface,
|
||||||
selectChatFolder,
|
selectChatFolder,
|
||||||
|
selectIsStoryViewerOpen,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
import { getUserFullName } from '../../global/helpers';
|
import { getUserFullName } from '../../global/helpers';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
@ -57,7 +58,6 @@ import useInterval from '../../hooks/useInterval';
|
|||||||
import { useFullscreenStatus } from '../../hooks/useFullscreen';
|
import { useFullscreenStatus } from '../../hooks/useFullscreen';
|
||||||
import useAppLayout from '../../hooks/useAppLayout';
|
import useAppLayout from '../../hooks/useAppLayout';
|
||||||
import useTimeout from '../../hooks/useTimeout';
|
import useTimeout from '../../hooks/useTimeout';
|
||||||
import useFlag from '../../hooks/useFlag';
|
|
||||||
|
|
||||||
import StickerSetModal from '../common/StickerSetModal.async';
|
import StickerSetModal from '../common/StickerSetModal.async';
|
||||||
import UnreadCount from '../common/UnreadCounter';
|
import UnreadCount from '../common/UnreadCounter';
|
||||||
@ -94,6 +94,7 @@ import DraftRecipientPicker from './DraftRecipientPicker.async';
|
|||||||
import AttachBotRecipientPicker from './AttachBotRecipientPicker.async';
|
import AttachBotRecipientPicker from './AttachBotRecipientPicker.async';
|
||||||
import ReactionPicker from '../middle/message/ReactionPicker.async';
|
import ReactionPicker from '../middle/message/ReactionPicker.async';
|
||||||
import ChatlistModal from '../modals/chatlist/ChatlistModal.async';
|
import ChatlistModal from '../modals/chatlist/ChatlistModal.async';
|
||||||
|
import StoryViewer from '../story/StoryViewer.async';
|
||||||
|
|
||||||
import './Main.scss';
|
import './Main.scss';
|
||||||
|
|
||||||
@ -108,6 +109,7 @@ type StateProps = {
|
|||||||
isMiddleColumnOpen: boolean;
|
isMiddleColumnOpen: boolean;
|
||||||
isRightColumnOpen: boolean;
|
isRightColumnOpen: boolean;
|
||||||
isMediaViewerOpen: boolean;
|
isMediaViewerOpen: boolean;
|
||||||
|
isStoryViewerOpen: boolean;
|
||||||
isForwardModalOpen: boolean;
|
isForwardModalOpen: boolean;
|
||||||
hasNotifications: boolean;
|
hasNotifications: boolean;
|
||||||
hasDialogs: boolean;
|
hasDialogs: boolean;
|
||||||
@ -152,7 +154,6 @@ type StateProps = {
|
|||||||
|
|
||||||
const APP_OUTDATED_TIMEOUT_MS = 5 * 60 * 1000; // 5 min
|
const APP_OUTDATED_TIMEOUT_MS = 5 * 60 * 1000; // 5 min
|
||||||
const CALL_BUNDLE_LOADING_DELAY_MS = 5000; // 5 sec
|
const CALL_BUNDLE_LOADING_DELAY_MS = 5000; // 5 sec
|
||||||
const REACTION_PICKER_LOADING_DELAY_MS = 7000; // 7 sec
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
let DEBUG_isLogged = false;
|
let DEBUG_isLogged = false;
|
||||||
@ -163,6 +164,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
isMiddleColumnOpen,
|
isMiddleColumnOpen,
|
||||||
isRightColumnOpen,
|
isRightColumnOpen,
|
||||||
isMediaViewerOpen,
|
isMediaViewerOpen,
|
||||||
|
isStoryViewerOpen,
|
||||||
isForwardModalOpen,
|
isForwardModalOpen,
|
||||||
hasNotifications,
|
hasNotifications,
|
||||||
hasDialogs,
|
hasDialogs,
|
||||||
@ -256,9 +258,6 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
void loadBundle(Bundles.Calls);
|
void loadBundle(Bundles.Calls);
|
||||||
}, CALL_BUNDLE_LOADING_DELAY_MS);
|
}, CALL_BUNDLE_LOADING_DELAY_MS);
|
||||||
|
|
||||||
const [shouldLoadReactionPicker, markShouldLoadReactionPicker] = useFlag(false);
|
|
||||||
useTimeout(markShouldLoadReactionPicker, REACTION_PICKER_LOADING_DELAY_MS);
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -503,7 +502,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
// Online status and browser tab indicators
|
// Online status and browser tab indicators
|
||||||
useBackgroundMode(handleBlur, handleFocus, !!IS_ELECTRON);
|
useBackgroundMode(handleBlur, handleFocus, !!IS_ELECTRON);
|
||||||
useBeforeUnload(handleBlur);
|
useBeforeUnload(handleBlur);
|
||||||
usePreventPinchZoomGesture(isMediaViewerOpen);
|
usePreventPinchZoomGesture(isMediaViewerOpen || isStoryViewerOpen);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} id="Main" className={className}>
|
<div ref={containerRef} id="Main" className={className}>
|
||||||
@ -511,6 +510,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
<MiddleColumn leftColumnRef={leftColumnRef} isMobile={isMobile} />
|
<MiddleColumn leftColumnRef={leftColumnRef} isMobile={isMobile} />
|
||||||
<RightColumn isMobile={isMobile} />
|
<RightColumn isMobile={isMobile} />
|
||||||
<MediaViewer isOpen={isMediaViewerOpen} />
|
<MediaViewer isOpen={isMediaViewerOpen} />
|
||||||
|
<StoryViewer isOpen={isStoryViewerOpen} />
|
||||||
<ForwardRecipientPicker isOpen={isForwardModalOpen} />
|
<ForwardRecipientPicker isOpen={isForwardModalOpen} />
|
||||||
<DraftRecipientPicker requestedDraft={requestedDraft} />
|
<DraftRecipientPicker requestedDraft={requestedDraft} />
|
||||||
<Notifications isOpen={hasNotifications} />
|
<Notifications isOpen={hasNotifications} />
|
||||||
@ -556,7 +556,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
<PaymentModal isOpen={isPaymentModalOpen} onClose={closePaymentModal} />
|
<PaymentModal isOpen={isPaymentModalOpen} onClose={closePaymentModal} />
|
||||||
<ReceiptModal isOpen={isReceiptModalOpen} onClose={clearReceipt} />
|
<ReceiptModal isOpen={isReceiptModalOpen} onClose={clearReceipt} />
|
||||||
<DeleteFolderDialog folder={deleteFolderDialog} />
|
<DeleteFolderDialog folder={deleteFolderDialog} />
|
||||||
<ReactionPicker isOpen={isReactionPickerOpen} shouldLoad={shouldLoadReactionPicker} />
|
<ReactionPicker isOpen={isReactionPickerOpen} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -616,6 +616,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isMiddleColumnOpen: Boolean(chatId),
|
isMiddleColumnOpen: Boolean(chatId),
|
||||||
isRightColumnOpen: selectIsRightColumnShown(global, isMobile),
|
isRightColumnOpen: selectIsRightColumnShown(global, isMobile),
|
||||||
isMediaViewerOpen: selectIsMediaViewerOpen(global),
|
isMediaViewerOpen: selectIsMediaViewerOpen(global),
|
||||||
|
isStoryViewerOpen: selectIsStoryViewerOpen(global),
|
||||||
isForwardModalOpen: selectIsForwardModalOpen(global),
|
isForwardModalOpen: selectIsForwardModalOpen(global),
|
||||||
isReactionPickerOpen: selectIsReactionPickerOpen(global),
|
isReactionPickerOpen: selectIsReactionPickerOpen(global),
|
||||||
hasNotifications: Boolean(notifications.length),
|
hasNotifications: Boolean(notifications.length),
|
||||||
|
|||||||
@ -8,8 +8,8 @@ import type { ApiDimensions } from '../../api/types';
|
|||||||
|
|
||||||
import useLastCallback from '../../hooks/useLastCallback';
|
import useLastCallback from '../../hooks/useLastCallback';
|
||||||
import useSignal from '../../hooks/useSignal';
|
import useSignal from '../../hooks/useSignal';
|
||||||
|
import useCurrentTimeSignal from '../../hooks/useCurrentTimeSignal';
|
||||||
import { useThrottledSignal } from '../../hooks/useAsyncResolvers';
|
import { useThrottledSignal } from '../../hooks/useAsyncResolvers';
|
||||||
import useCurrentTimeSignal from './hooks/useCurrentTimeSignal';
|
|
||||||
import useVideoWaitingSignal from './hooks/useVideoWaitingSignal';
|
import useVideoWaitingSignal from './hooks/useVideoWaitingSignal';
|
||||||
|
|
||||||
import { captureEvents } from '../../util/captureEvents';
|
import { captureEvents } from '../../util/captureEvents';
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import usePictureInPicture from '../../hooks/usePictureInPicture';
|
|||||||
import useShowTransition from '../../hooks/useShowTransition';
|
import useShowTransition from '../../hooks/useShowTransition';
|
||||||
import useVideoCleanup from '../../hooks/useVideoCleanup';
|
import useVideoCleanup from '../../hooks/useVideoCleanup';
|
||||||
import useAppLayout from '../../hooks/useAppLayout';
|
import useAppLayout from '../../hooks/useAppLayout';
|
||||||
import useCurrentTimeSignal from './hooks/useCurrentTimeSignal';
|
import useCurrentTimeSignal from '../../hooks/useCurrentTimeSignal';
|
||||||
import useControlsSignal from './hooks/useControlsSignal';
|
import useControlsSignal from './hooks/useControlsSignal';
|
||||||
import useVideoWaitingSignal from './hooks/useVideoWaitingSignal';
|
import useVideoWaitingSignal from './hooks/useVideoWaitingSignal';
|
||||||
import useUnsupportedMedia from '../../hooks/media/useUnsupportedMedia';
|
import useUnsupportedMedia from '../../hooks/media/useUnsupportedMedia';
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import useFlag from '../../hooks/useFlag';
|
|||||||
import useAppLayout from '../../hooks/useAppLayout';
|
import useAppLayout from '../../hooks/useAppLayout';
|
||||||
import useDerivedState from '../../hooks/useDerivedState';
|
import useDerivedState from '../../hooks/useDerivedState';
|
||||||
import useSignal from '../../hooks/useSignal';
|
import useSignal from '../../hooks/useSignal';
|
||||||
import useCurrentTimeSignal from './hooks/useCurrentTimeSignal';
|
import useCurrentTimeSignal from '../../hooks/useCurrentTimeSignal';
|
||||||
import useControlsSignal from './hooks/useControlsSignal';
|
import useControlsSignal from './hooks/useControlsSignal';
|
||||||
|
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
|||||||
@ -123,6 +123,7 @@ type StateProps = {
|
|||||||
|
|
||||||
const MESSAGE_REACTIONS_POLLING_INTERVAL = 15 * 1000;
|
const MESSAGE_REACTIONS_POLLING_INTERVAL = 15 * 1000;
|
||||||
const MESSAGE_COMMENTS_POLLING_INTERVAL = 15 * 1000;
|
const MESSAGE_COMMENTS_POLLING_INTERVAL = 15 * 1000;
|
||||||
|
const MESSAGE_STORY_POLLING_INTERVAL = 5 * 60 * 1000;
|
||||||
const BOTTOM_THRESHOLD = 50;
|
const BOTTOM_THRESHOLD = 50;
|
||||||
const UNREAD_DIVIDER_TOP = 10;
|
const UNREAD_DIVIDER_TOP = 10;
|
||||||
const UNREAD_DIVIDER_TOP_WITH_TOOLS = 60;
|
const UNREAD_DIVIDER_TOP_WITH_TOOLS = 60;
|
||||||
@ -173,7 +174,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
loadViewportMessages, setScrollOffset, loadSponsoredMessages, loadMessageReactions, copyMessagesByIds,
|
loadViewportMessages, setScrollOffset, loadSponsoredMessages, loadMessageReactions, copyMessagesByIds,
|
||||||
loadMessageViews,
|
loadMessageViews, loadUserStoriesByIds,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -259,6 +260,28 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
|||||||
loadMessageReactions({ chatId, ids });
|
loadMessageReactions({ chatId, ids });
|
||||||
}, MESSAGE_REACTIONS_POLLING_INTERVAL);
|
}, MESSAGE_REACTIONS_POLLING_INTERVAL);
|
||||||
|
|
||||||
|
useInterval(() => {
|
||||||
|
if (!messageIds || !messagesById || type === 'scheduled') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const storyDataList = messageIds.map((id) => messagesById[id]?.content.storyData).filter(Boolean);
|
||||||
|
|
||||||
|
if (!storyDataList.length) return;
|
||||||
|
|
||||||
|
const storiesByUserIds = storyDataList.reduce((acc, storyData) => {
|
||||||
|
const { userId, id } = storyData!;
|
||||||
|
if (!acc[userId]) {
|
||||||
|
acc[userId] = [];
|
||||||
|
}
|
||||||
|
acc[userId].push(id);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, number[]>);
|
||||||
|
|
||||||
|
Object.entries(storiesByUserIds).forEach(([userId, storyIds]) => {
|
||||||
|
loadUserStoriesByIds({ userId, storyIds });
|
||||||
|
});
|
||||||
|
}, MESSAGE_STORY_POLLING_INTERVAL);
|
||||||
|
|
||||||
useInterval(() => {
|
useInterval(() => {
|
||||||
if (!messageIds || !messagesById || threadId !== MAIN_THREAD_ID || type === 'scheduled') {
|
if (!messageIds || !messagesById || threadId !== MAIN_THREAD_ID || type === 'scheduled') {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -35,7 +35,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.Composer {
|
.Composer {
|
||||||
#message-compose {
|
.composer-wrapper {
|
||||||
transform: scaleX(1) translateX(0);
|
transform: scaleX(1) translateX(0);
|
||||||
transition: transform var(--select-transition), border-bottom-right-radius var(--select-transition);
|
transition: transform var(--select-transition), border-bottom-right-radius var(--select-transition);
|
||||||
|
|
||||||
@ -83,7 +83,7 @@
|
|||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#message-compose {
|
.composer-wrapper {
|
||||||
transform: scaleX(var(--composer-hidden-scale, 1)) translateX(var(--composer-translate-x, 0));
|
transform: scaleX(var(--composer-hidden-scale, 1)) translateX(var(--composer-translate-x, 0));
|
||||||
border-bottom-right-radius: var(--border-radius-messages);
|
border-bottom-right-radius: var(--border-radius-messages);
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,8 @@ import {
|
|||||||
GENERAL_TOPIC_ID,
|
GENERAL_TOPIC_ID,
|
||||||
TMP_CHAT_ID,
|
TMP_CHAT_ID,
|
||||||
MAX_SCREEN_WIDTH_FOR_EXPAND_PINNED_MESSAGES,
|
MAX_SCREEN_WIDTH_FOR_EXPAND_PINNED_MESSAGES,
|
||||||
|
EDITABLE_INPUT_ID,
|
||||||
|
EDITABLE_INPUT_CSS_SELECTOR,
|
||||||
} from '../../config';
|
} from '../../config';
|
||||||
import {
|
import {
|
||||||
IS_ANDROID, IS_IOS, IS_TRANSLATION_SUPPORTED, MASK_IMAGE_DISABLED,
|
IS_ANDROID, IS_IOS, IS_TRANSLATION_SUPPORTED, MASK_IMAGE_DISABLED,
|
||||||
@ -78,7 +80,6 @@ import Transition from '../ui/Transition';
|
|||||||
import MiddleHeader from './MiddleHeader';
|
import MiddleHeader from './MiddleHeader';
|
||||||
import MessageList from './MessageList';
|
import MessageList from './MessageList';
|
||||||
import FloatingActionButtons from './FloatingActionButtons';
|
import FloatingActionButtons from './FloatingActionButtons';
|
||||||
import Composer from './composer/Composer';
|
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import MobileSearch from './MobileSearch.async';
|
import MobileSearch from './MobileSearch.async';
|
||||||
import MessageSelectToolbar from './MessageSelectToolbar.async';
|
import MessageSelectToolbar from './MessageSelectToolbar.async';
|
||||||
@ -88,6 +89,7 @@ import EmojiInteractionAnimation from './EmojiInteractionAnimation.async';
|
|||||||
import ReactorListModal from './ReactorListModal.async';
|
import ReactorListModal from './ReactorListModal.async';
|
||||||
import GiftPremiumModal from '../main/premium/GiftPremiumModal.async';
|
import GiftPremiumModal from '../main/premium/GiftPremiumModal.async';
|
||||||
import ChatLanguageModal from './ChatLanguageModal.async';
|
import ChatLanguageModal from './ChatLanguageModal.async';
|
||||||
|
import Composer from '../common/Composer';
|
||||||
|
|
||||||
import './MiddleColumn.scss';
|
import './MiddleColumn.scss';
|
||||||
|
|
||||||
@ -529,6 +531,7 @@ function MiddleColumn({
|
|||||||
<div className={footerClassName}>
|
<div className={footerClassName}>
|
||||||
{renderingCanPost && (
|
{renderingCanPost && (
|
||||||
<Composer
|
<Composer
|
||||||
|
type="messageList"
|
||||||
chatId={renderingChatId!}
|
chatId={renderingChatId!}
|
||||||
threadId={renderingThreadId!}
|
threadId={renderingThreadId!}
|
||||||
messageListType={renderingMessageListType!}
|
messageListType={renderingMessageListType!}
|
||||||
@ -536,6 +539,9 @@ function MiddleColumn({
|
|||||||
onDropHide={handleHideDropArea}
|
onDropHide={handleHideDropArea}
|
||||||
isReady={isReady}
|
isReady={isReady}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
|
editableInputId={EDITABLE_INPUT_ID}
|
||||||
|
editableInputCssSelector={EDITABLE_INPUT_CSS_SELECTOR}
|
||||||
|
inputId="message-input-text"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isPinnedMessageList && canUnpin && (
|
{isPinnedMessageList && canUnpin && (
|
||||||
|
|||||||
@ -31,10 +31,12 @@
|
|||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: var(--z-middle-header);
|
z-index: var(--z-middle-header);
|
||||||
padding-top: 0.5rem;
|
padding-top: 0.25rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.25rem;
|
||||||
padding-left: max(1.5rem, env(safe-area-inset-left));
|
padding-left: max(1.4375rem, env(safe-area-inset-left));
|
||||||
padding-right: max(0.8125rem, env(safe-area-inset-right));
|
padding-right: max(0.8125rem, env(safe-area-inset-right));
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 3.5rem;
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -214,6 +216,8 @@
|
|||||||
cursor: var(--custom-cursor, pointer);
|
cursor: var(--custom-cursor, pointer);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
// Space for unread story circle
|
||||||
|
padding: 0.0625rem 0 0.0625rem 0.0625rem;
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -266,6 +270,11 @@
|
|||||||
.custom-emoji {
|
.custom-emoji {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.story-circle {
|
||||||
|
max-width: 2.625rem !important;
|
||||||
|
max-height: 2.625rem !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Avatar, .topic-header-icon {
|
.Avatar, .topic-header-icon {
|
||||||
|
|||||||
@ -367,6 +367,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
|||||||
withDots={Boolean(connectionStatusText)}
|
withDots={Boolean(connectionStatusText)}
|
||||||
withFullInfo
|
withFullInfo
|
||||||
withMediaViewer
|
withMediaViewer
|
||||||
|
withStory={!isChatWithSelf}
|
||||||
withUpdatingStatus
|
withUpdatingStatus
|
||||||
emojiStatusSize={EMOJI_STATUS_SIZE}
|
emojiStatusSize={EMOJI_STATUS_SIZE}
|
||||||
noRtl
|
noRtl
|
||||||
|
|||||||
@ -40,11 +40,13 @@ export type OwnProps = {
|
|||||||
canSendDocuments: boolean;
|
canSendDocuments: boolean;
|
||||||
canSendAudios: boolean;
|
canSendAudios: boolean;
|
||||||
isScheduled?: boolean;
|
isScheduled?: boolean;
|
||||||
attachBots: GlobalState['attachMenu']['bots'];
|
attachBots?: GlobalState['attachMenu']['bots'];
|
||||||
peerType?: ApiAttachMenuPeerType;
|
peerType?: ApiAttachMenuPeerType;
|
||||||
shouldCollectDebugLogs?: boolean;
|
shouldCollectDebugLogs?: boolean;
|
||||||
onFileSelect: (files: File[], shouldSuggestCompression?: boolean) => void;
|
onFileSelect: (files: File[], shouldSuggestCompression?: boolean) => void;
|
||||||
onPollCreate: () => void;
|
onPollCreate: NoneToVoidFunction;
|
||||||
|
onMenuOpen: NoneToVoidFunction;
|
||||||
|
onMenuClose: NoneToVoidFunction;
|
||||||
theme: ISettings['theme'];
|
theme: ISettings['theme'];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -62,6 +64,8 @@ const AttachMenu: FC<OwnProps> = ({
|
|||||||
peerType,
|
peerType,
|
||||||
isScheduled,
|
isScheduled,
|
||||||
onFileSelect,
|
onFileSelect,
|
||||||
|
onMenuOpen,
|
||||||
|
onMenuClose,
|
||||||
onPollCreate,
|
onPollCreate,
|
||||||
theme,
|
theme,
|
||||||
shouldCollectDebugLogs,
|
shouldCollectDebugLogs,
|
||||||
@ -73,12 +77,22 @@ const AttachMenu: FC<OwnProps> = ({
|
|||||||
const canSendVideoOrPhoto = canSendPhotos || canSendVideos;
|
const canSendVideoOrPhoto = canSendPhotos || canSendVideos;
|
||||||
|
|
||||||
const [isAttachmentBotMenuOpen, markAttachmentBotMenuOpen, unmarkAttachmentBotMenuOpen] = useFlag();
|
const [isAttachmentBotMenuOpen, markAttachmentBotMenuOpen, unmarkAttachmentBotMenuOpen] = useFlag();
|
||||||
|
const isMenuOpen = isAttachMenuOpen || isAttachmentBotMenuOpen;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAttachMenuOpen) {
|
if (isAttachMenuOpen) {
|
||||||
markMouseInside();
|
markMouseInside();
|
||||||
}
|
}
|
||||||
}, [isAttachMenuOpen, markMouseInside]);
|
}, [isAttachMenuOpen, markMouseInside]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isMenuOpen) {
|
||||||
|
onMenuOpen();
|
||||||
|
} else {
|
||||||
|
onMenuClose();
|
||||||
|
}
|
||||||
|
}, [isMenuOpen, onMenuClose, onMenuOpen]);
|
||||||
|
|
||||||
const handleToggleAttachMenu = useLastCallback(() => {
|
const handleToggleAttachMenu = useLastCallback(() => {
|
||||||
if (isAttachMenuOpen) {
|
if (isAttachMenuOpen) {
|
||||||
closeAttachMenu();
|
closeAttachMenu();
|
||||||
@ -118,13 +132,15 @@ const AttachMenu: FC<OwnProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const bots = useMemo(() => {
|
const bots = useMemo(() => {
|
||||||
return Object.values(attachBots).filter((bot) => {
|
return attachBots
|
||||||
if (!peerType) return false;
|
? Object.values(attachBots).filter((bot) => {
|
||||||
if (peerType === 'bots' && bot.id === chatId && bot.peerTypes.includes('self')) {
|
if (!peerType) return false;
|
||||||
return true;
|
if (peerType === 'bots' && bot.id === chatId && bot.peerTypes.includes('self')) {
|
||||||
}
|
return true;
|
||||||
return bot.peerTypes.includes(peerType);
|
}
|
||||||
});
|
return bot.peerTypes.includes(peerType);
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
}, [attachBots, chatId, peerType]);
|
}, [attachBots, chatId, peerType]);
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
@ -149,7 +165,7 @@ const AttachMenu: FC<OwnProps> = ({
|
|||||||
</ResponsiveHoverButton>
|
</ResponsiveHoverButton>
|
||||||
<Menu
|
<Menu
|
||||||
id="attach-menu-controls"
|
id="attach-menu-controls"
|
||||||
isOpen={isAttachMenuOpen || isAttachmentBotMenuOpen}
|
isOpen={isMenuOpen}
|
||||||
autoClose
|
autoClose
|
||||||
positionX="right"
|
positionX="right"
|
||||||
positionY="bottom"
|
positionY="bottom"
|
||||||
@ -193,7 +209,7 @@ const AttachMenu: FC<OwnProps> = ({
|
|||||||
<MenuItem icon="poll" onClick={onPollCreate}>{lang('Poll')}</MenuItem>
|
<MenuItem icon="poll" onClick={onPollCreate}>{lang('Poll')}</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{canAttachMedia && !isScheduled && bots.map((bot) => (
|
{canAttachMedia && !isScheduled && bots?.map((bot) => (
|
||||||
<AttachBotItem
|
<AttachBotItem
|
||||||
bot={bot}
|
bot={bot}
|
||||||
chatId={chatId}
|
chatId={chatId}
|
||||||
|
|||||||
@ -64,11 +64,13 @@ export type OwnProps = {
|
|||||||
getHtml: Signal<string>;
|
getHtml: Signal<string>;
|
||||||
canShowCustomSendMenu?: boolean;
|
canShowCustomSendMenu?: boolean;
|
||||||
isReady: boolean;
|
isReady: boolean;
|
||||||
|
isForMessage?: boolean;
|
||||||
shouldSchedule?: boolean;
|
shouldSchedule?: boolean;
|
||||||
shouldSuggestCompression?: boolean;
|
shouldSuggestCompression?: boolean;
|
||||||
shouldForceCompression?: boolean;
|
shouldForceCompression?: boolean;
|
||||||
shouldForceAsFile?: boolean;
|
shouldForceAsFile?: boolean;
|
||||||
isForCurrentMessageList?: boolean;
|
isForCurrentMessageList?: boolean;
|
||||||
|
forceDarkTheme?: boolean;
|
||||||
onCaptionUpdate: (html: string) => void;
|
onCaptionUpdate: (html: string) => void;
|
||||||
onSend: (sendCompressed: boolean, sendGrouped: boolean) => void;
|
onSend: (sendCompressed: boolean, sendGrouped: boolean) => void;
|
||||||
onFileAppend: (files: File[], isSpoiler?: boolean) => void;
|
onFileAppend: (files: File[], isSpoiler?: boolean) => void;
|
||||||
@ -112,6 +114,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
|||||||
recentEmojis,
|
recentEmojis,
|
||||||
baseEmojiKeywords,
|
baseEmojiKeywords,
|
||||||
emojiKeywords,
|
emojiKeywords,
|
||||||
|
isForMessage,
|
||||||
shouldSchedule,
|
shouldSchedule,
|
||||||
shouldSuggestCustomEmoji,
|
shouldSuggestCustomEmoji,
|
||||||
customEmojiForEmoji,
|
customEmojiForEmoji,
|
||||||
@ -120,6 +123,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
|||||||
shouldForceCompression,
|
shouldForceCompression,
|
||||||
shouldForceAsFile,
|
shouldForceAsFile,
|
||||||
isForCurrentMessageList,
|
isForCurrentMessageList,
|
||||||
|
forceDarkTheme,
|
||||||
onAttachmentsUpdate,
|
onAttachmentsUpdate,
|
||||||
onCaptionUpdate,
|
onCaptionUpdate,
|
||||||
onSend,
|
onSend,
|
||||||
@ -194,7 +198,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
|||||||
insertEmoji,
|
insertEmoji,
|
||||||
closeEmojiTooltip,
|
closeEmojiTooltip,
|
||||||
} = useEmojiTooltip(
|
} = useEmojiTooltip(
|
||||||
Boolean(isReady && isForCurrentMessageList && renderingIsOpen),
|
Boolean(isReady && (isForCurrentMessageList || !isForMessage) && renderingIsOpen),
|
||||||
getHtml,
|
getHtml,
|
||||||
onCaptionUpdate,
|
onCaptionUpdate,
|
||||||
EDITABLE_INPUT_MODAL_ID,
|
EDITABLE_INPUT_MODAL_ID,
|
||||||
@ -208,7 +212,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
|||||||
insertCustomEmoji,
|
insertCustomEmoji,
|
||||||
closeCustomEmojiTooltip,
|
closeCustomEmojiTooltip,
|
||||||
} = useCustomEmojiTooltip(
|
} = useCustomEmojiTooltip(
|
||||||
Boolean(isReady && isForCurrentMessageList && renderingIsOpen && shouldSuggestCustomEmoji),
|
Boolean(isReady && (isForCurrentMessageList || !isForMessage) && renderingIsOpen && shouldSuggestCustomEmoji),
|
||||||
getHtml,
|
getHtml,
|
||||||
onCaptionUpdate,
|
onCaptionUpdate,
|
||||||
getSelectionRange,
|
getSelectionRange,
|
||||||
@ -256,7 +260,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const sendAttachments = useLastCallback((isSilent?: boolean, shouldSendScheduled?: boolean) => {
|
const sendAttachments = useLastCallback((isSilent?: boolean, shouldSendScheduled?: boolean) => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
const send = (shouldSchedule || shouldSendScheduled) ? onSendScheduled
|
const send = ((shouldSchedule || shouldSendScheduled) && isForMessage) ? onSendScheduled
|
||||||
: isSilent ? onSendSilent : onSend;
|
: isSilent ? onSendSilent : onSend;
|
||||||
send(isSendingCompressed, shouldSendGrouped);
|
send(isSendingCompressed, shouldSendGrouped);
|
||||||
updateAttachmentSettings({
|
updateAttachmentSettings({
|
||||||
@ -508,6 +512,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
|||||||
!areAttachmentsNotScrolled && styles.headerBorder,
|
!areAttachmentsNotScrolled && styles.headerBorder,
|
||||||
isMobile && styles.mobile,
|
isMobile && styles.mobile,
|
||||||
isSymbolMenuOpen && styles.symbolMenuOpen,
|
isSymbolMenuOpen && styles.symbolMenuOpen,
|
||||||
|
forceDarkTheme && 'component-theme-dark',
|
||||||
)}
|
)}
|
||||||
noBackdropClose
|
noBackdropClose
|
||||||
>
|
>
|
||||||
@ -586,6 +591,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
|||||||
isAttachmentModal
|
isAttachmentModal
|
||||||
canSendPlainText
|
canSendPlainText
|
||||||
className="attachment-modal-symbol-menu with-menu-transitions"
|
className="attachment-modal-symbol-menu with-menu-transitions"
|
||||||
|
idPrefix="attachment"
|
||||||
/>
|
/>
|
||||||
<MessageInput
|
<MessageInput
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
@ -593,6 +599,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
|||||||
chatId={chatId}
|
chatId={chatId}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
isAttachmentModalInput
|
isAttachmentModalInput
|
||||||
|
customEmojiPrefix="attachment"
|
||||||
isReady={isReady}
|
isReady={isReady}
|
||||||
isActive={isOpen}
|
isActive={isOpen}
|
||||||
getHtml={getHtml}
|
getHtml={getHtml}
|
||||||
@ -618,6 +625,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
|||||||
{canShowCustomSendMenu && (
|
{canShowCustomSendMenu && (
|
||||||
<CustomSendMenu
|
<CustomSendMenu
|
||||||
isOpen={isCustomSendMenuOpen}
|
isOpen={isCustomSendMenuOpen}
|
||||||
|
canSchedule={isForMessage}
|
||||||
onSendSilent={!isChatWithSelf ? handleSendSilent : undefined}
|
onSendSilent={!isChatWithSelf ? handleSendSilent : undefined}
|
||||||
onSendSchedule={handleScheduleClick}
|
onSendSchedule={handleScheduleClick}
|
||||||
onClose={handleContextMenuClose}
|
onClose={handleContextMenuClose}
|
||||||
|
|||||||
@ -52,6 +52,7 @@ type StateProps = {
|
|||||||
noCaptions?: boolean;
|
noCaptions?: boolean;
|
||||||
forwardsHaveCaptions?: boolean;
|
forwardsHaveCaptions?: boolean;
|
||||||
isCurrentUserPremium?: boolean;
|
isCurrentUserPremium?: boolean;
|
||||||
|
isContextMenuDisabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
@ -73,6 +74,7 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
|
|||||||
forwardsHaveCaptions,
|
forwardsHaveCaptions,
|
||||||
shouldForceShowEditing,
|
shouldForceShowEditing,
|
||||||
isCurrentUserPremium,
|
isCurrentUserPremium,
|
||||||
|
isContextMenuDisabled,
|
||||||
onClear,
|
onClear,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
@ -201,7 +203,7 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
|
|||||||
customText={customText}
|
customText={customText}
|
||||||
title={editingId ? lang('EditMessage') : noAuthors ? lang('HiddenSendersNameDescription') : undefined}
|
title={editingId ? lang('EditMessage') : noAuthors ? lang('HiddenSendersNameDescription') : undefined}
|
||||||
onClick={handleMessageClick}
|
onClick={handleMessageClick}
|
||||||
hasContextMenu={isForwarding}
|
hasContextMenu={isForwarding && !isContextMenuDisabled}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className="embedded-cancel"
|
className="embedded-cancel"
|
||||||
@ -213,7 +215,7 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
|
|||||||
>
|
>
|
||||||
<i className="icon icon-close" />
|
<i className="icon icon-close" />
|
||||||
</Button>
|
</Button>
|
||||||
{isForwarding && (
|
{isForwarding && !isContextMenuDisabled && (
|
||||||
<Menu
|
<Menu
|
||||||
isOpen={isContextMenuOpen}
|
isOpen={isContextMenuOpen}
|
||||||
transformOriginX={transformOriginX}
|
transformOriginX={transformOriginX}
|
||||||
@ -338,6 +340,9 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
forward?.content.text && Object.keys(forward.content).length > 1
|
forward?.content.text && Object.keys(forward.content).length > 1
|
||||||
));
|
));
|
||||||
|
|
||||||
|
const isContextMenuDisabled = isForwarding && forwardMessageIds!.length === 1
|
||||||
|
&& Boolean(message?.content.storyData);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
replyingToId,
|
replyingToId,
|
||||||
editingId,
|
editingId,
|
||||||
@ -349,6 +354,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
noCaptions,
|
noCaptions,
|
||||||
forwardsHaveCaptions,
|
forwardsHaveCaptions,
|
||||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||||
|
isContextMenuDisabled,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(ComposerEmbeddedMessage));
|
)(ComposerEmbeddedMessage));
|
||||||
|
|||||||
@ -17,6 +17,7 @@ export type OwnProps = {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
isOpenToBottom?: boolean;
|
isOpenToBottom?: boolean;
|
||||||
isSavedMessages?: boolean;
|
isSavedMessages?: boolean;
|
||||||
|
canSchedule?: boolean;
|
||||||
canScheduleUntilOnline?: boolean;
|
canScheduleUntilOnline?: boolean;
|
||||||
onSendSilent?: NoneToVoidFunction;
|
onSendSilent?: NoneToVoidFunction;
|
||||||
onSendSchedule?: NoneToVoidFunction;
|
onSendSchedule?: NoneToVoidFunction;
|
||||||
@ -29,6 +30,7 @@ const CustomSendMenu: FC<OwnProps> = ({
|
|||||||
isOpen,
|
isOpen,
|
||||||
isOpenToBottom = false,
|
isOpenToBottom = false,
|
||||||
isSavedMessages,
|
isSavedMessages,
|
||||||
|
canSchedule,
|
||||||
canScheduleUntilOnline,
|
canScheduleUntilOnline,
|
||||||
onSendSilent,
|
onSendSilent,
|
||||||
onSendSchedule,
|
onSendSchedule,
|
||||||
@ -62,12 +64,12 @@ const CustomSendMenu: FC<OwnProps> = ({
|
|||||||
noCloseOnBackdrop={!IS_TOUCH_ENV}
|
noCloseOnBackdrop={!IS_TOUCH_ENV}
|
||||||
>
|
>
|
||||||
{onSendSilent && <MenuItem icon="mute" onClick={onSendSilent}>{lang('SendWithoutSound')}</MenuItem>}
|
{onSendSilent && <MenuItem icon="mute" onClick={onSendSilent}>{lang('SendWithoutSound')}</MenuItem>}
|
||||||
{onSendSchedule && (
|
{canSchedule && onSendSchedule && (
|
||||||
<MenuItem icon="schedule" onClick={onSendSchedule}>
|
<MenuItem icon="schedule" onClick={onSendSchedule}>
|
||||||
{lang(isSavedMessages ? 'SetReminder' : 'ScheduleMessage')}
|
{lang(isSavedMessages ? 'SetReminder' : 'ScheduleMessage')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{onSendSchedule && displayScheduleUntilOnline && (
|
{canSchedule && onSendSchedule && displayScheduleUntilOnline && (
|
||||||
<MenuItem icon="user-online" onClick={onSendWhenOnline}>
|
<MenuItem icon="user-online" onClick={onSendWhenOnline}>
|
||||||
{lang('SendWhenOnline')}
|
{lang('SendWhenOnline')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|||||||
@ -47,6 +47,8 @@ type OwnProps = {
|
|||||||
chatId: string;
|
chatId: string;
|
||||||
threadId: number;
|
threadId: number;
|
||||||
isAttachmentModalInput?: boolean;
|
isAttachmentModalInput?: boolean;
|
||||||
|
isStoryInput?: boolean;
|
||||||
|
customEmojiPrefix: string;
|
||||||
editableInputId?: string;
|
editableInputId?: string;
|
||||||
isReady: boolean;
|
isReady: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
@ -63,6 +65,8 @@ type OwnProps = {
|
|||||||
onSend: () => void;
|
onSend: () => void;
|
||||||
onScroll?: (event: React.UIEvent<HTMLElement>) => void;
|
onScroll?: (event: React.UIEvent<HTMLElement>) => void;
|
||||||
captionLimit?: number;
|
captionLimit?: number;
|
||||||
|
onFocus?: NoneToVoidFunction;
|
||||||
|
onBlur?: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
@ -73,6 +77,7 @@ type StateProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MAX_ATTACHMENT_MODAL_INPUT_HEIGHT = 160;
|
const MAX_ATTACHMENT_MODAL_INPUT_HEIGHT = 160;
|
||||||
|
const MAX_STORY_MODAL_INPUT_HEIGHT = 128;
|
||||||
const TAB_INDEX_PRIORITY_TIMEOUT = 2000;
|
const TAB_INDEX_PRIORITY_TIMEOUT = 2000;
|
||||||
// Heuristics allowing the user to make a triple click
|
// Heuristics allowing the user to make a triple click
|
||||||
const SELECTION_RECALCULATE_DELAY_MS = 260;
|
const SELECTION_RECALCULATE_DELAY_MS = 260;
|
||||||
@ -102,6 +107,8 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
|||||||
chatId,
|
chatId,
|
||||||
captionLimit,
|
captionLimit,
|
||||||
isAttachmentModalInput,
|
isAttachmentModalInput,
|
||||||
|
isStoryInput,
|
||||||
|
customEmojiPrefix,
|
||||||
editableInputId,
|
editableInputId,
|
||||||
isReady,
|
isReady,
|
||||||
isActive,
|
isActive,
|
||||||
@ -121,6 +128,8 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
|||||||
onSuppressedFocus,
|
onSuppressedFocus,
|
||||||
onSend,
|
onSend,
|
||||||
onScroll,
|
onScroll,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
editLastMessage,
|
editLastMessage,
|
||||||
@ -162,13 +171,15 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
|||||||
sharedCanvasRef,
|
sharedCanvasRef,
|
||||||
sharedCanvasHqRef,
|
sharedCanvasHqRef,
|
||||||
absoluteContainerRef,
|
absoluteContainerRef,
|
||||||
isAttachmentModalInput ? 'attachment' : 'composer',
|
customEmojiPrefix,
|
||||||
canPlayAnimatedEmojis,
|
canPlayAnimatedEmojis,
|
||||||
isReady,
|
isReady,
|
||||||
isActive,
|
isActive,
|
||||||
);
|
);
|
||||||
|
|
||||||
const maxInputHeight = isAttachmentModalInput ? MAX_ATTACHMENT_MODAL_INPUT_HEIGHT : (isMobile ? 256 : 416);
|
const maxInputHeight = isAttachmentModalInput
|
||||||
|
? MAX_ATTACHMENT_MODAL_INPUT_HEIGHT
|
||||||
|
: isStoryInput ? MAX_STORY_MODAL_INPUT_HEIGHT : (isMobile ? 256 : 416);
|
||||||
const updateInputHeight = useLastCallback((willSend = false) => {
|
const updateInputHeight = useLastCallback((willSend = false) => {
|
||||||
requestForcedReflow(() => {
|
requestForcedReflow(() => {
|
||||||
const scroller = inputRef.current!.closest<HTMLDivElement>(`.${SCROLLER_CLASS}`)!;
|
const scroller = inputRef.current!.closest<HTMLDivElement>(`.${SCROLLER_CLASS}`)!;
|
||||||
@ -351,7 +362,6 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
|||||||
const { isComposing } = e;
|
const { isComposing } = e;
|
||||||
|
|
||||||
const html = getHtml();
|
const html = getHtml();
|
||||||
|
|
||||||
if (!isComposing && !html && (e.metaKey || e.ctrlKey)) {
|
if (!isComposing && !html && (e.metaKey || e.ctrlKey)) {
|
||||||
const targetIndexDelta = e.key === 'ArrowDown' ? 1 : e.key === 'ArrowUp' ? -1 : undefined;
|
const targetIndexDelta = e.key === 'ArrowDown' ? 1 : e.key === 'ArrowUp' ? -1 : undefined;
|
||||||
if (targetIndexDelta) {
|
if (targetIndexDelta) {
|
||||||
@ -549,6 +559,8 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
|||||||
onContextMenu={IS_ANDROID ? handleAndroidContextMenu : undefined}
|
onContextMenu={IS_ANDROID ? handleAndroidContextMenu : undefined}
|
||||||
onTouchCancel={IS_ANDROID ? processSelectionWithTimeout : undefined}
|
onTouchCancel={IS_ANDROID ? processSelectionWithTimeout : undefined}
|
||||||
aria-label={placeholder}
|
aria-label={placeholder}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
/>
|
/>
|
||||||
{!forcedPlaceholder && (
|
{!forcedPlaceholder && (
|
||||||
<span
|
<span
|
||||||
|
|||||||
@ -53,6 +53,8 @@ type OwnProps = {
|
|||||||
isTranslucent?: boolean;
|
isTranslucent?: boolean;
|
||||||
loadAndPlay: boolean;
|
loadAndPlay: boolean;
|
||||||
canSendStickers?: boolean;
|
canSendStickers?: boolean;
|
||||||
|
noContextMenus?: boolean;
|
||||||
|
idPrefix: string;
|
||||||
onStickerSelect: (
|
onStickerSelect: (
|
||||||
sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean, canUpdateStickerSetsOrder?: boolean,
|
sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean, canUpdateStickerSetsOrder?: boolean,
|
||||||
) => void;
|
) => void;
|
||||||
@ -90,6 +92,8 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
canAnimate,
|
canAnimate,
|
||||||
isSavedMessages,
|
isSavedMessages,
|
||||||
isCurrentUserPremium,
|
isCurrentUserPremium,
|
||||||
|
noContextMenus,
|
||||||
|
idPrefix,
|
||||||
onStickerSelect,
|
onStickerSelect,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
@ -114,6 +118,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const sendMessageAction = useSendMessageAction(chat!.id, threadId);
|
const sendMessageAction = useSendMessageAction(chat!.id, threadId);
|
||||||
|
|
||||||
|
const prefix = `${idPrefix}-sticker-set`;
|
||||||
const {
|
const {
|
||||||
activeSetIndex,
|
activeSetIndex,
|
||||||
observeIntersectionForSet,
|
observeIntersectionForSet,
|
||||||
@ -121,7 +126,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
observeIntersectionForShowingItems,
|
observeIntersectionForShowingItems,
|
||||||
observeIntersectionForCovers,
|
observeIntersectionForCovers,
|
||||||
selectStickerSet,
|
selectStickerSet,
|
||||||
} = useStickerPickerObservers(containerRef, headerRef, 'sticker-set', isHidden);
|
} = useStickerPickerObservers(containerRef, headerRef, prefix, isHidden);
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
@ -355,7 +360,9 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
key={stickerSet.id}
|
key={stickerSet.id}
|
||||||
stickerSet={stickerSet}
|
stickerSet={stickerSet}
|
||||||
loadAndPlay={Boolean(canAnimate && loadAndPlay)}
|
loadAndPlay={Boolean(canAnimate && loadAndPlay)}
|
||||||
|
noContextMenus={noContextMenus}
|
||||||
index={i}
|
index={i}
|
||||||
|
idPrefix={prefix}
|
||||||
observeIntersection={observeIntersectionForSet}
|
observeIntersection={observeIntersectionForSet}
|
||||||
observeIntersectionForPlayingItems={observeIntersectionForPlayingItems}
|
observeIntersectionForPlayingItems={observeIntersectionForPlayingItems}
|
||||||
observeIntersectionForShowingItems={observeIntersectionForShowingItems}
|
observeIntersectionForShowingItems={observeIntersectionForShowingItems}
|
||||||
|
|||||||
@ -51,7 +51,7 @@
|
|||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.left-column-open {
|
&.left-column-open.in-middle-column {
|
||||||
transform: translate3d(100vw, 0, 0) !important;
|
transform: translate3d(100vw, 0, 0) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -154,7 +154,7 @@
|
|||||||
|
|
||||||
body:not(.no-menu-blur) & {
|
body:not(.no-menu-blur) & {
|
||||||
background: var(--color-background-compact-menu);
|
background: var(--color-background-compact-menu);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(25px);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.open) {
|
&:not(.open) {
|
||||||
@ -224,6 +224,7 @@
|
|||||||
|
|
||||||
&-external {
|
&-external {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
|
text-align: start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -39,6 +39,8 @@ export type OwnProps = {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
canSendStickers?: boolean;
|
canSendStickers?: boolean;
|
||||||
canSendGifs?: boolean;
|
canSendGifs?: boolean;
|
||||||
|
isMessageComposer?: boolean;
|
||||||
|
idPrefix: string;
|
||||||
onLoad: () => void;
|
onLoad: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onEmojiSelect: (emoji: string) => void;
|
onEmojiSelect: (emoji: string) => void;
|
||||||
@ -79,27 +81,29 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
|||||||
isOpen,
|
isOpen,
|
||||||
canSendStickers,
|
canSendStickers,
|
||||||
canSendGifs,
|
canSendGifs,
|
||||||
|
isMessageComposer,
|
||||||
isLeftColumnShown,
|
isLeftColumnShown,
|
||||||
isCurrentUserPremium,
|
isCurrentUserPremium,
|
||||||
onLoad,
|
idPrefix,
|
||||||
onClose,
|
|
||||||
onEmojiSelect,
|
|
||||||
isAttachmentModal,
|
isAttachmentModal,
|
||||||
canSendPlainText,
|
canSendPlainText,
|
||||||
onCustomEmojiSelect,
|
|
||||||
onStickerSelect,
|
|
||||||
className,
|
className,
|
||||||
onGifSelect,
|
|
||||||
onRemoveSymbol,
|
|
||||||
onSearchOpen,
|
|
||||||
addRecentEmoji,
|
|
||||||
addRecentCustomEmoji,
|
|
||||||
positionX,
|
positionX,
|
||||||
positionY,
|
positionY,
|
||||||
transformOriginX,
|
transformOriginX,
|
||||||
transformOriginY,
|
transformOriginY,
|
||||||
style,
|
style,
|
||||||
isBackgroundTranslucent,
|
isBackgroundTranslucent,
|
||||||
|
onLoad,
|
||||||
|
onClose,
|
||||||
|
onEmojiSelect,
|
||||||
|
onCustomEmojiSelect,
|
||||||
|
onStickerSelect,
|
||||||
|
onGifSelect,
|
||||||
|
onRemoveSymbol,
|
||||||
|
onSearchOpen,
|
||||||
|
addRecentEmoji,
|
||||||
|
addRecentCustomEmoji,
|
||||||
}) => {
|
}) => {
|
||||||
const { loadPremiumSetStickers } = getActions();
|
const { loadPremiumSetStickers } = getActions();
|
||||||
const [activeTab, setActiveTab] = useState<number>(0);
|
const [activeTab, setActiveTab] = useState<number>(0);
|
||||||
@ -218,6 +222,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
|||||||
<CustomEmojiPicker
|
<CustomEmojiPicker
|
||||||
className="picker-tab"
|
className="picker-tab"
|
||||||
isHidden={!isOpen || !isActive}
|
isHidden={!isOpen || !isActive}
|
||||||
|
idPrefix={idPrefix}
|
||||||
loadAndPlay={isOpen && (isActive || isFrom)}
|
loadAndPlay={isOpen && (isActive || isFrom)}
|
||||||
chatId={chatId}
|
chatId={chatId}
|
||||||
isTranslucent={!isMobile && isBackgroundTranslucent}
|
isTranslucent={!isMobile && isBackgroundTranslucent}
|
||||||
@ -230,7 +235,9 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
|||||||
className="picker-tab"
|
className="picker-tab"
|
||||||
isHidden={!isOpen || !isActive}
|
isHidden={!isOpen || !isActive}
|
||||||
loadAndPlay={canSendStickers ? isOpen && (isActive || isFrom) : false}
|
loadAndPlay={canSendStickers ? isOpen && (isActive || isFrom) : false}
|
||||||
|
idPrefix={idPrefix}
|
||||||
canSendStickers={canSendStickers}
|
canSendStickers={canSendStickers}
|
||||||
|
noContextMenus={!isMessageComposer}
|
||||||
chatId={chatId}
|
chatId={chatId}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
isTranslucent={!isMobile && isBackgroundTranslucent}
|
isTranslucent={!isMobile && isBackgroundTranslucent}
|
||||||
@ -285,6 +292,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
|||||||
activeTab={activeTab}
|
activeTab={activeTab}
|
||||||
onSwitchTab={setActiveTab}
|
onSwitchTab={setActiveTab}
|
||||||
onRemoveSymbol={onRemoveSymbol}
|
onRemoveSymbol={onRemoveSymbol}
|
||||||
|
canSearch={isMessageComposer}
|
||||||
onSearchOpen={handleSearch}
|
onSearchOpen={handleSearch}
|
||||||
isAttachmentModal={isAttachmentModal}
|
isAttachmentModal={isAttachmentModal}
|
||||||
canSendPlainText={canSendPlainText}
|
canSendPlainText={canSendPlainText}
|
||||||
@ -302,6 +310,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
|||||||
transitionClassNames,
|
transitionClassNames,
|
||||||
isLeftColumnShown && 'left-column-open',
|
isLeftColumnShown && 'left-column-open',
|
||||||
isAttachmentModal && 'in-attachment-modal',
|
isAttachmentModal && 'in-attachment-modal',
|
||||||
|
isMessageComposer && 'in-middle-column',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isAttachmentModal) {
|
if (isAttachmentModal) {
|
||||||
|
|||||||
@ -27,6 +27,8 @@ type OwnProps = {
|
|||||||
isSymbolMenuOpen?: boolean;
|
isSymbolMenuOpen?: boolean;
|
||||||
canSendGifs?: boolean;
|
canSendGifs?: boolean;
|
||||||
canSendStickers?: boolean;
|
canSendStickers?: boolean;
|
||||||
|
isMessageComposer?: boolean;
|
||||||
|
idPrefix: string;
|
||||||
openSymbolMenu: VoidFunction;
|
openSymbolMenu: VoidFunction;
|
||||||
closeSymbolMenu: VoidFunction;
|
closeSymbolMenu: VoidFunction;
|
||||||
onCustomEmojiSelect: (emoji: ApiSticker) => void;
|
onCustomEmojiSelect: (emoji: ApiSticker) => void;
|
||||||
@ -46,6 +48,7 @@ type OwnProps = {
|
|||||||
isAttachmentModal?: boolean;
|
isAttachmentModal?: boolean;
|
||||||
canSendPlainText?: boolean;
|
canSendPlainText?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
inputCssSelector?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SymbolMenuButton: FC<OwnProps> = ({
|
const SymbolMenuButton: FC<OwnProps> = ({
|
||||||
@ -54,21 +57,24 @@ const SymbolMenuButton: FC<OwnProps> = ({
|
|||||||
isMobile,
|
isMobile,
|
||||||
canSendGifs,
|
canSendGifs,
|
||||||
canSendStickers,
|
canSendStickers,
|
||||||
|
isMessageComposer,
|
||||||
isReady,
|
isReady,
|
||||||
isSymbolMenuOpen,
|
isSymbolMenuOpen,
|
||||||
|
idPrefix,
|
||||||
|
isAttachmentModal,
|
||||||
|
canSendPlainText,
|
||||||
|
isSymbolMenuForced,
|
||||||
|
className,
|
||||||
|
inputCssSelector = EDITABLE_INPUT_CSS_SELECTOR,
|
||||||
openSymbolMenu,
|
openSymbolMenu,
|
||||||
closeSymbolMenu,
|
closeSymbolMenu,
|
||||||
onCustomEmojiSelect,
|
onCustomEmojiSelect,
|
||||||
onStickerSelect,
|
onStickerSelect,
|
||||||
onGifSelect,
|
onGifSelect,
|
||||||
isAttachmentModal,
|
|
||||||
canSendPlainText,
|
|
||||||
onRemoveSymbol,
|
onRemoveSymbol,
|
||||||
onEmojiSelect,
|
onEmojiSelect,
|
||||||
closeBotCommandMenu,
|
closeBotCommandMenu,
|
||||||
closeSendAsMenu,
|
closeSendAsMenu,
|
||||||
isSymbolMenuForced,
|
|
||||||
className,
|
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
setStickerSearchQuery,
|
setStickerSearchQuery,
|
||||||
@ -113,7 +119,7 @@ const SymbolMenuButton: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const handleSymbolMenuOpen = useLastCallback(() => {
|
const handleSymbolMenuOpen = useLastCallback(() => {
|
||||||
const messageInput = document.querySelector<HTMLDivElement>(
|
const messageInput = document.querySelector<HTMLDivElement>(
|
||||||
isAttachmentModal ? EDITABLE_INPUT_MODAL_CSS_SELECTOR : EDITABLE_INPUT_CSS_SELECTOR,
|
isAttachmentModal ? EDITABLE_INPUT_MODAL_CSS_SELECTOR : inputCssSelector,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isMobile || messageInput !== document.activeElement) {
|
if (!isMobile || messageInput !== document.activeElement) {
|
||||||
@ -176,6 +182,8 @@ const SymbolMenuButton: FC<OwnProps> = ({
|
|||||||
isOpen={isSymbolMenuOpen || Boolean(isSymbolMenuForced)}
|
isOpen={isSymbolMenuOpen || Boolean(isSymbolMenuForced)}
|
||||||
canSendGifs={canSendGifs}
|
canSendGifs={canSendGifs}
|
||||||
canSendStickers={canSendStickers}
|
canSendStickers={canSendStickers}
|
||||||
|
isMessageComposer={isMessageComposer}
|
||||||
|
idPrefix={idPrefix}
|
||||||
onLoad={onSymbolMenuLoadingComplete}
|
onLoad={onSymbolMenuLoadingComplete}
|
||||||
onClose={closeSymbolMenu}
|
onClose={closeSymbolMenu}
|
||||||
onEmojiSelect={onEmojiSelect}
|
onEmojiSelect={onEmojiSelect}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ type OwnProps = {
|
|||||||
onSearchOpen: (type: 'stickers' | 'gifs') => void;
|
onSearchOpen: (type: 'stickers' | 'gifs') => void;
|
||||||
isAttachmentModal?: boolean;
|
isAttachmentModal?: boolean;
|
||||||
canSendPlainText?: boolean;
|
canSendPlainText?: boolean;
|
||||||
|
canSearch?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum SymbolMenuTabs {
|
export enum SymbolMenuTabs {
|
||||||
@ -40,7 +41,7 @@ const SYMBOL_MENU_TAB_ICONS = {
|
|||||||
|
|
||||||
const SymbolMenuFooter: FC<OwnProps> = ({
|
const SymbolMenuFooter: FC<OwnProps> = ({
|
||||||
activeTab, onSwitchTab, onRemoveSymbol, onSearchOpen, isAttachmentModal,
|
activeTab, onSwitchTab, onRemoveSymbol, onSearchOpen, isAttachmentModal,
|
||||||
canSendPlainText,
|
canSendPlainText, canSearch,
|
||||||
}) => {
|
}) => {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
@ -70,7 +71,7 @@ const SymbolMenuFooter: FC<OwnProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="SymbolMenu-footer" onClick={stopPropagation} dir={lang.isRtl ? 'rtl' : undefined}>
|
<div className="SymbolMenu-footer" onClick={stopPropagation} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
{activeTab !== SymbolMenuTabs.Emoji && activeTab !== SymbolMenuTabs.CustomEmoji && (
|
{activeTab !== SymbolMenuTabs.Emoji && activeTab !== SymbolMenuTabs.CustomEmoji && canSearch && (
|
||||||
<Button
|
<Button
|
||||||
className="symbol-search-button"
|
className="symbol-search-button"
|
||||||
ariaLabel={activeTab === SymbolMenuTabs.Stickers ? 'Search Stickers' : 'Search GIFs'}
|
ariaLabel={activeTab === SymbolMenuTabs.Stickers ? 'Search Stickers' : 'Search GIFs'}
|
||||||
|
|||||||
@ -55,7 +55,7 @@ export default function useCustomEmojiTooltip(
|
|||||||
const hasCustomEmojis = Boolean(customEmojis?.length);
|
const hasCustomEmojis = Boolean(customEmojis?.length);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isEnabled) return;
|
if (!isEnabled || !isActive) return;
|
||||||
|
|
||||||
const lastEmoji = getLastEmoji();
|
const lastEmoji = getLastEmoji();
|
||||||
if (lastEmoji) {
|
if (lastEmoji) {
|
||||||
@ -67,7 +67,7 @@ export default function useCustomEmojiTooltip(
|
|||||||
} else {
|
} else {
|
||||||
clearCustomEmojiForEmoji();
|
clearCustomEmojiForEmoji();
|
||||||
}
|
}
|
||||||
}, [isEnabled, getLastEmoji, hasCustomEmojis, clearCustomEmojiForEmoji, loadCustomEmojiForEmoji]);
|
}, [isEnabled, isActive, getLastEmoji, hasCustomEmojis, clearCustomEmojiForEmoji, loadCustomEmojiForEmoji]);
|
||||||
|
|
||||||
const insertCustomEmoji = useLastCallback((emoji: ApiSticker) => {
|
const insertCustomEmoji = useLastCallback((emoji: ApiSticker) => {
|
||||||
const lastEmoji = getLastEmoji();
|
const lastEmoji = getLastEmoji();
|
||||||
|
|||||||
@ -37,13 +37,14 @@ const useDraft = (
|
|||||||
getHtml: Signal<string>,
|
getHtml: Signal<string>,
|
||||||
setHtml: (html: string) => void,
|
setHtml: (html: string) => void,
|
||||||
editedMessage: ApiMessage | undefined,
|
editedMessage: ApiMessage | undefined,
|
||||||
|
isDisabled: boolean | undefined,
|
||||||
) => {
|
) => {
|
||||||
const { saveDraft, clearDraft, loadCustomEmojis } = getActions();
|
const { saveDraft, clearDraft, loadCustomEmojis } = getActions();
|
||||||
|
|
||||||
const isEditing = Boolean(editedMessage);
|
const isEditing = Boolean(editedMessage);
|
||||||
|
|
||||||
const updateDraft = useLastCallback((prevState: { chatId?: string; threadId?: number } = {}, shouldForce = false) => {
|
const updateDraft = useLastCallback((prevState: { chatId?: string; threadId?: number } = {}, shouldForce = false) => {
|
||||||
if (isEditing) return;
|
if (isDisabled || isEditing) return;
|
||||||
|
|
||||||
const html = getHtml();
|
const html = getHtml();
|
||||||
|
|
||||||
@ -68,6 +69,10 @@ const useDraft = (
|
|||||||
|
|
||||||
// Restore draft on chat change
|
// Restore draft on chat change
|
||||||
useEffectWithPrevDeps(([prevChatId, prevThreadId, prevDraft]) => {
|
useEffectWithPrevDeps(([prevChatId, prevThreadId, prevDraft]) => {
|
||||||
|
if (isDisabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (chatId === prevChatId && threadId === prevThreadId) {
|
if (chatId === prevChatId && threadId === prevThreadId) {
|
||||||
if (!draft && prevDraft) {
|
if (!draft && prevDraft) {
|
||||||
setHtml('');
|
setHtml('');
|
||||||
@ -97,10 +102,14 @@ const useDraft = (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [chatId, threadId, draft, setHtml, editedMessage, loadCustomEmojis]);
|
}, [chatId, threadId, draft, setHtml, editedMessage, loadCustomEmojis, isDisabled]);
|
||||||
|
|
||||||
// Save draft on chat change
|
// Save draft on chat change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isDisabled) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
|
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
|
||||||
if (!isEditing) {
|
if (!isEditing) {
|
||||||
@ -110,12 +119,12 @@ const useDraft = (
|
|||||||
|
|
||||||
freeze();
|
freeze();
|
||||||
};
|
};
|
||||||
}, [chatId, threadId, isEditing, updateDraftRef]);
|
}, [chatId, threadId, isEditing, updateDraftRef, isDisabled]);
|
||||||
|
|
||||||
const chatIdRef = useStateRef(chatId);
|
const chatIdRef = useStateRef(chatId);
|
||||||
const threadIdRef = useStateRef(threadId);
|
const threadIdRef = useStateRef(threadId);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isFrozen) {
|
if (isDisabled || isFrozen) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,7 +142,7 @@ const useDraft = (
|
|||||||
updateDraftRef.current();
|
updateDraftRef.current();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [chatIdRef, getHtml, runDebouncedForSaveDraft, threadIdRef, updateDraftRef]);
|
}, [chatIdRef, getHtml, isDisabled, runDebouncedForSaveDraft, threadIdRef, updateDraftRef]);
|
||||||
|
|
||||||
function forceUpdateDraft() {
|
function forceUpdateDraft() {
|
||||||
updateDraft(undefined, true);
|
updateDraft(undefined, true);
|
||||||
|
|||||||
@ -48,7 +48,7 @@ export default function useStickerTooltip(
|
|||||||
const hasStickers = Boolean(stickers?.length);
|
const hasStickers = Boolean(stickers?.length);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isEnabled) return;
|
if (!isEnabled || !isActive) return;
|
||||||
|
|
||||||
const singleEmoji = getSingleEmoji();
|
const singleEmoji = getSingleEmoji();
|
||||||
if (singleEmoji) {
|
if (singleEmoji) {
|
||||||
@ -58,7 +58,7 @@ export default function useStickerTooltip(
|
|||||||
} else {
|
} else {
|
||||||
clearStickersForEmoji();
|
clearStickersForEmoji();
|
||||||
}
|
}
|
||||||
}, [isEnabled, getSingleEmoji, hasStickers, loadStickersForEmoji, clearStickersForEmoji]);
|
}, [isEnabled, isActive, getSingleEmoji, hasStickers, loadStickersForEmoji, clearStickersForEmoji]);
|
||||||
|
|
||||||
useEffect(unmarkManuallyClosed, [unmarkManuallyClosed, getHtml]);
|
useEffect(unmarkManuallyClosed, [unmarkManuallyClosed, getHtml]);
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,8 @@ const useVoiceRecording = () => {
|
|||||||
if (recordButtonRef.current) {
|
if (recordButtonRef.current) {
|
||||||
if (startRecordTimeRef.current && Date.now() % 4 === 0) {
|
if (startRecordTimeRef.current && Date.now() % 4 === 0) {
|
||||||
requestMutation(() => {
|
requestMutation(() => {
|
||||||
recordButtonRef.current!.style.boxShadow = `0 0 0 ${(tickVolume || 0) * 50}px rgba(0,0,0,.15)`;
|
if (!recordButtonRef.current) return;
|
||||||
|
recordButtonRef.current.style.boxShadow = `0 0 0 ${(tickVolume || 0) * 50}px rgba(0,0,0,.15)`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setCurrentRecordTime(Date.now());
|
setCurrentRecordTime(Date.now());
|
||||||
@ -78,7 +79,7 @@ const useVoiceRecording = () => {
|
|||||||
|
|
||||||
requestMutation(() => {
|
requestMutation(() => {
|
||||||
if (recordButtonRef.current) {
|
if (recordButtonRef.current) {
|
||||||
recordButtonRef.current!.style.boxShadow = 'none';
|
recordButtonRef.current.style.boxShadow = 'none';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
53
src/components/middle/message/BaseStory.module.scss
Normal file
53
src/components/middle/message/BaseStory.module.scss
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
.root {
|
||||||
|
display: block !important;
|
||||||
|
width: 12rem;
|
||||||
|
height: 0;
|
||||||
|
// Aspect-ratio trick https://css-tricks.com/aspect-ratio-boxes/ (192:344)
|
||||||
|
padding-bottom: 179%;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
cursor: var(--custom-cursor, pointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
// Aspect ratio 12:11, preview in webpage
|
||||||
|
padding-bottom: 91.67%;
|
||||||
|
|
||||||
|
.linkPreview {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkPreview {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nonInteractive {
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
:global(.message-content.story) &:global(.media-inner) {
|
||||||
|
margin-bottom: 2rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired {
|
||||||
|
height: auto;
|
||||||
|
padding-bottom: 0;
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expiredIcon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 0.9375rem;
|
||||||
|
vertical-align: -0.1875rem;
|
||||||
|
}
|
||||||
108
src/components/middle/message/BaseStory.tsx
Normal file
108
src/components/middle/message/BaseStory.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import React, { memo, useEffect } from '../../../lib/teact/teact';
|
||||||
|
import { getActions } from '../../../global';
|
||||||
|
|
||||||
|
import type { ApiMessageStoryData, ApiTypeStory } from '../../../api/types';
|
||||||
|
|
||||||
|
import { IS_CANVAS_FILTER_SUPPORTED } from '../../../util/windowEnvironment';
|
||||||
|
import { getStoryMediaHash } from '../../../global/helpers';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { formatMediaDuration } from '../../../util/dateFormat';
|
||||||
|
|
||||||
|
import useMedia from '../../../hooks/useMedia';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useCanvasBlur from '../../../hooks/useCanvasBlur';
|
||||||
|
import useAppLayout from '../../../hooks/useAppLayout';
|
||||||
|
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||||
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
|
|
||||||
|
import styles from './BaseStory.module.scss';
|
||||||
|
|
||||||
|
interface OwnProps {
|
||||||
|
story?: ApiTypeStory | ApiMessageStoryData;
|
||||||
|
isPreview?: boolean;
|
||||||
|
isProtected?: boolean;
|
||||||
|
isConnected?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BaseStory({
|
||||||
|
story, isPreview, isProtected, isConnected,
|
||||||
|
}: OwnProps) {
|
||||||
|
const { openStoryViewer, loadUserStoriesByIds, showNotification } = getActions();
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
const { isMobile } = useAppLayout();
|
||||||
|
const isExpired = story && 'isDeleted' in story;
|
||||||
|
const isLoaded = story && 'content' in story;
|
||||||
|
const video = isLoaded ? story.content.video : undefined;
|
||||||
|
const imageHash = isLoaded ? getStoryMediaHash(story) : undefined;
|
||||||
|
const imgBlobUrl = useMedia(imageHash);
|
||||||
|
const thumbnail = isLoaded ? (video ? video.thumbnail?.dataUri : story.content.photo?.thumbnail?.dataUri) : undefined;
|
||||||
|
const mediaUrl = useCurrentOrPrev(imgBlobUrl, true);
|
||||||
|
const { shouldRender, transitionClassNames } = useShowTransition(Boolean(mediaUrl));
|
||||||
|
const blurredBackgroundRef = useCanvasBlur(
|
||||||
|
thumbnail,
|
||||||
|
isExpired && !isPreview,
|
||||||
|
isMobile && !IS_CANVAS_FILTER_SUPPORTED,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fullClassName = buildClassName(
|
||||||
|
styles.root,
|
||||||
|
'media-inner',
|
||||||
|
(!isConnected || isExpired) && styles.nonInteractive,
|
||||||
|
isExpired && styles.expired,
|
||||||
|
isPreview && styles.preview,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (story && !(isLoaded || isExpired)) {
|
||||||
|
loadUserStoriesByIds({ userId: story.userId, storyIds: [story.id] });
|
||||||
|
}
|
||||||
|
}, [story, isExpired, isLoaded]);
|
||||||
|
|
||||||
|
const handleClick = useLastCallback(() => {
|
||||||
|
if (isExpired) {
|
||||||
|
showNotification({
|
||||||
|
message: lang('StoryNotFound'),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
openStoryViewer({
|
||||||
|
userId: story!.userId,
|
||||||
|
storyId: story!.id,
|
||||||
|
isSingleUser: true,
|
||||||
|
isSingleStory: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={fullClassName}
|
||||||
|
onClick={isConnected ? handleClick : undefined}
|
||||||
|
>
|
||||||
|
{!isExpired && isPreview && <canvas ref={blurredBackgroundRef} className="thumbnail blurred-bg" />}
|
||||||
|
{shouldRender && (
|
||||||
|
<img
|
||||||
|
src={mediaUrl}
|
||||||
|
alt=""
|
||||||
|
className={buildClassName(styles.media, isPreview && styles.linkPreview, transitionClassNames)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isExpired && (
|
||||||
|
<span>
|
||||||
|
<i className={buildClassName(styles.expiredIcon, 'icon icon-story-expired')} aria-hidden />
|
||||||
|
{lang('StoryExpiredSubtitle')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{Boolean(video?.duration) && (
|
||||||
|
<div className="message-media-duration">
|
||||||
|
{formatMediaDuration(video!.duration)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isProtected && <span className="protector" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(BaseStory);
|
||||||
@ -40,7 +40,7 @@ import {
|
|||||||
getMessageVideo,
|
getMessageVideo,
|
||||||
getChatMessageLink,
|
getChatMessageLink,
|
||||||
} from '../../../global/helpers';
|
} from '../../../global/helpers';
|
||||||
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
|
import { PREVIEW_AVATAR_COUNT, SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { copyTextToClipboard } from '../../../util/clipboard';
|
import { copyTextToClipboard } from '../../../util/clipboard';
|
||||||
|
|
||||||
@ -194,7 +194,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
requestMessageTranslation,
|
requestMessageTranslation,
|
||||||
showOriginalMessage,
|
showOriginalMessage,
|
||||||
openChatLanguageModal,
|
openChatLanguageModal,
|
||||||
openReactionPicker,
|
openMessageReactionPicker,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
@ -245,14 +245,16 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
({ peerId }) => usersById[peerId] || chatsById[peerId],
|
({ peerId }) => usersById[peerId] || chatsById[peerId],
|
||||||
));
|
));
|
||||||
|
|
||||||
return Array.from(uniqueReactors).filter(Boolean).slice(0, 3);
|
return Array.from(uniqueReactors).filter(Boolean).slice(0, PREVIEW_AVATAR_COUNT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!message.seenByDates) {
|
if (!message.seenByDates) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.keys(message.seenByDates).slice(0, 3).map((id) => usersById[id] || chatsById[id]).filter(Boolean);
|
return Object.keys(message.seenByDates).slice(0, PREVIEW_AVATAR_COUNT)
|
||||||
|
.map((id) => usersById[id] || chatsById[id])
|
||||||
|
.filter(Boolean);
|
||||||
}, [message.reactions?.recentReactions, message.seenByDates]);
|
}, [message.reactions?.recentReactions, message.seenByDates]);
|
||||||
|
|
||||||
const isDownloading = useMemo(() => {
|
const isDownloading = useMemo(() => {
|
||||||
@ -435,7 +437,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleReactionPickerOpen = useLastCallback((position: IAnchorPosition) => {
|
const handleReactionPickerOpen = useLastCallback((position: IAnchorPosition) => {
|
||||||
openReactionPicker({ chatId: message.chatId, messageId: message.id, position });
|
openMessageReactionPicker({ chatId: message.chatId, messageId: message.id, position });
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleTranslate = useLastCallback(() => {
|
const handleTranslate = useLastCallback(() => {
|
||||||
|
|||||||
@ -26,12 +26,14 @@ const MentionLink: FC<OwnProps & StateProps> = ({
|
|||||||
const {
|
const {
|
||||||
openChat,
|
openChat,
|
||||||
openChatByUsername,
|
openChatByUsername,
|
||||||
|
closeStoryViewer,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (userOrChat) {
|
if (userOrChat) {
|
||||||
openChat({ id: userOrChat.id });
|
openChat({ id: userOrChat.id });
|
||||||
} else if (username) {
|
} else if (username) {
|
||||||
|
closeStoryViewer();
|
||||||
openChatByUsername({ username: username.substring(1) });
|
openChatByUsername({ username: username.substring(1) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -254,6 +254,88 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-story-mention {
|
||||||
|
--background-color: var(--pattern-color);
|
||||||
|
|
||||||
|
.message-content-wrapper {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-message-story-mention {
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: white;
|
||||||
|
font-size: calc(var(--message-text-size, 1rem) - 0.0625rem);
|
||||||
|
padding: 0.75rem 0.5rem;
|
||||||
|
border-radius: var(--border-radius-messages);
|
||||||
|
word-break: break-word;
|
||||||
|
text-align: center;
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 9.625rem;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
body.is-ios &,
|
||||||
|
body.is-macos & {
|
||||||
|
font-size: calc(var(--message-text-size, 1rem) - 0.125rem);
|
||||||
|
line-height: calc(var(--message-text-size, 1rem) + 0.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.with-preview {
|
||||||
|
padding: 1.25rem 0.5rem 0.75rem;
|
||||||
|
cursor: var(--custom-cursor, pointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.with-preview::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 1rem;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 6rem;
|
||||||
|
height: 6rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
padding: 0.0625rem;
|
||||||
|
/* stylelint-disable-next-line plugin/whole-pixel */
|
||||||
|
box-shadow: 0 0 0 0.03125rem rgba(255, 255, 255, 0.3);
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
mask: linear-gradient(to bottom, #fff 0%, #fff 100%) content-box, linear-gradient(to bottom, #fff 0%, #fff 100%);
|
||||||
|
mask-composite: exclude;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-unread::before {
|
||||||
|
padding: 0.125rem;
|
||||||
|
background: linear-gradient(215.87deg, var(--color-message-story-mention-from) -1.61%, var(--color-message-story-mention-to) 97.44%);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-media-wrapper {
|
||||||
|
width: 5.5rem;
|
||||||
|
height: 5.5rem;
|
||||||
|
margin: 0 auto 1rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-media {
|
||||||
|
object-fit: cover;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-title {
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.has-views {
|
&.has-views {
|
||||||
--meta-safe-area-extra-width: 4rem;
|
--meta-safe-area-extra-width: 4rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import type {
|
|||||||
ApiMessageOutgoingStatus,
|
ApiMessageOutgoingStatus,
|
||||||
ApiReaction,
|
ApiReaction,
|
||||||
ApiStickerSet,
|
ApiStickerSet,
|
||||||
|
ApiTypeStory,
|
||||||
ApiThreadInfo,
|
ApiThreadInfo,
|
||||||
ApiTopic,
|
ApiTopic,
|
||||||
ApiUser,
|
ApiUser,
|
||||||
@ -56,6 +57,7 @@ import {
|
|||||||
selectMessageIdsByGroupId,
|
selectMessageIdsByGroupId,
|
||||||
selectOutgoingStatus,
|
selectOutgoingStatus,
|
||||||
selectPerformanceSettingsValue,
|
selectPerformanceSettingsValue,
|
||||||
|
selectUserStory,
|
||||||
selectReplySender,
|
selectReplySender,
|
||||||
selectRequestedChatTranslationLanguage,
|
selectRequestedChatTranslationLanguage,
|
||||||
selectRequestedMessageTranslationLanguage,
|
selectRequestedMessageTranslationLanguage,
|
||||||
@ -123,6 +125,7 @@ import useMessageTranslation from './hooks/useMessageTranslation';
|
|||||||
import usePrevious from '../../../hooks/usePrevious';
|
import usePrevious from '../../../hooks/usePrevious';
|
||||||
import useTextLanguage from '../../../hooks/useTextLanguage';
|
import useTextLanguage from '../../../hooks/useTextLanguage';
|
||||||
import useAuthorWidth from '../hooks/useAuthorWidth';
|
import useAuthorWidth from '../hooks/useAuthorWidth';
|
||||||
|
import useEnsureStory from '../../../hooks/useEnsureStory';
|
||||||
import { dispatchHeavyAnimationEvent } from '../../../hooks/useHeavyAnimationCheck';
|
import { dispatchHeavyAnimationEvent } from '../../../hooks/useHeavyAnimationCheck';
|
||||||
import useDetectChatLanguage from './hooks/useDetectChatLanguage';
|
import useDetectChatLanguage from './hooks/useDetectChatLanguage';
|
||||||
|
|
||||||
@ -158,6 +161,9 @@ import PremiumIcon from '../../common/PremiumIcon';
|
|||||||
import FakeIcon from '../../common/FakeIcon';
|
import FakeIcon from '../../common/FakeIcon';
|
||||||
import MessageText from '../../common/MessageText';
|
import MessageText from '../../common/MessageText';
|
||||||
import TopicChip from '../../common/TopicChip';
|
import TopicChip from '../../common/TopicChip';
|
||||||
|
import EmbeddedStory from '../../common/EmbeddedStory';
|
||||||
|
import Story from './Story';
|
||||||
|
import StoryMention from './StoryMention';
|
||||||
|
|
||||||
import './Message.scss';
|
import './Message.scss';
|
||||||
|
|
||||||
@ -203,6 +209,8 @@ type StateProps = {
|
|||||||
shouldHideReply?: boolean;
|
shouldHideReply?: boolean;
|
||||||
replyMessage?: ApiMessage;
|
replyMessage?: ApiMessage;
|
||||||
replyMessageSender?: ApiUser | ApiChat;
|
replyMessageSender?: ApiUser | ApiChat;
|
||||||
|
replyStory?: ApiTypeStory;
|
||||||
|
storySender?: ApiUser;
|
||||||
outgoingStatus?: ApiMessageOutgoingStatus;
|
outgoingStatus?: ApiMessageOutgoingStatus;
|
||||||
uploadProgress?: number;
|
uploadProgress?: number;
|
||||||
isInDocumentGroup: boolean;
|
isInDocumentGroup: boolean;
|
||||||
@ -255,6 +263,7 @@ type StateProps = {
|
|||||||
requestedChatTranslationLanguage?: string;
|
requestedChatTranslationLanguage?: string;
|
||||||
withReactionEffects?: boolean;
|
withReactionEffects?: boolean;
|
||||||
withStickerEffects?: boolean;
|
withStickerEffects?: boolean;
|
||||||
|
webPageStory?: ApiTypeStory;
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -311,6 +320,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
shouldHideReply,
|
shouldHideReply,
|
||||||
replyMessage,
|
replyMessage,
|
||||||
replyMessageSender,
|
replyMessageSender,
|
||||||
|
replyStory,
|
||||||
|
storySender,
|
||||||
outgoingStatus,
|
outgoingStatus,
|
||||||
uploadProgress,
|
uploadProgress,
|
||||||
isInDocumentGroup,
|
isInDocumentGroup,
|
||||||
@ -361,6 +372,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
requestedChatTranslationLanguage,
|
requestedChatTranslationLanguage,
|
||||||
withReactionEffects,
|
withReactionEffects,
|
||||||
withStickerEffects,
|
withStickerEffects,
|
||||||
|
webPageStory,
|
||||||
isConnected,
|
isConnected,
|
||||||
onPinnedIntersectionChange,
|
onPinnedIntersectionChange,
|
||||||
getIsMessageListReady,
|
getIsMessageListReady,
|
||||||
@ -446,6 +458,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
const isOwn = isOwnMessage(message);
|
const isOwn = isOwnMessage(message);
|
||||||
const isScheduled = messageListType === 'scheduled' || message.isScheduled;
|
const isScheduled = messageListType === 'scheduled' || message.isScheduled;
|
||||||
const hasReply = isReplyMessage(message) && !shouldHideReply;
|
const hasReply = isReplyMessage(message) && !shouldHideReply;
|
||||||
|
const hasStoryReply = Boolean(message.replyToStoryId);
|
||||||
const hasThread = Boolean(repliesThreadInfo) && messageListType === 'thread';
|
const hasThread = Boolean(repliesThreadInfo) && messageListType === 'thread';
|
||||||
const isCustomShape = getMessageCustomShape(message);
|
const isCustomShape = getMessageCustomShape(message);
|
||||||
const hasAnimatedEmoji = isCustomShape && (animatedEmoji || animatedCustomEmoji);
|
const hasAnimatedEmoji = isCustomShape && (animatedEmoji || animatedCustomEmoji);
|
||||||
@ -456,7 +469,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
&& !isRepliesChat
|
&& !isRepliesChat
|
||||||
&& !forwardInfo.isLinkedChannelPost
|
&& !forwardInfo.isLinkedChannelPost
|
||||||
&& !isCustomShape
|
&& !isCustomShape
|
||||||
);
|
) || Boolean(message.content.storyData && !message.content.storyData.isMention);
|
||||||
|
const isStoryMention = message.content.storyData?.isMention;
|
||||||
const isAlbum = Boolean(album) && album!.messages.length > 1
|
const isAlbum = Boolean(album) && album!.messages.length > 1
|
||||||
&& !album?.messages.some((msg) => Object.keys(msg.content).length === 0);
|
&& !album?.messages.some((msg) => Object.keys(msg.content).length === 0);
|
||||||
const isInDocumentGroupNotFirst = isInDocumentGroup && !isFirstInDocumentGroup;
|
const isInDocumentGroupNotFirst = isInDocumentGroup && !isFirstInDocumentGroup;
|
||||||
@ -465,6 +479,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
const canShowActionButton = (
|
const canShowActionButton = (
|
||||||
!(isContextMenuShown || isInSelectMode || isForwarding)
|
!(isContextMenuShown || isInSelectMode || isForwarding)
|
||||||
&& !isInDocumentGroupNotLast
|
&& !isInDocumentGroupNotLast
|
||||||
|
&& !isStoryMention
|
||||||
);
|
);
|
||||||
const canForward = isChannel && !isScheduled && message.isForwardingAllowed && !isChatProtected;
|
const canForward = isChannel && !isScheduled && message.isForwardingAllowed && !isChatProtected;
|
||||||
const canFocus = Boolean(isPinnedList
|
const canFocus = Boolean(isPinnedList
|
||||||
@ -473,7 +488,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
&& forwardInfo.fromMessageId
|
&& forwardInfo.fromMessageId
|
||||||
));
|
));
|
||||||
|
|
||||||
const hasSubheader = hasTopicChip || hasReply;
|
const hasSubheader = hasTopicChip || hasReply || hasStoryReply;
|
||||||
|
|
||||||
const selectMessage = useLastCallback((e?: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => {
|
const selectMessage = useLastCallback((e?: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => {
|
||||||
toggleMessageSelection({
|
toggleMessageSelection({
|
||||||
@ -539,6 +554,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
handleFocusForwarded,
|
handleFocusForwarded,
|
||||||
handleDocumentGroupSelectAll,
|
handleDocumentGroupSelectAll,
|
||||||
handleTopicChipClick,
|
handleTopicChipClick,
|
||||||
|
handleStoryClick,
|
||||||
} = useInnerHandlers(
|
} = useInnerHandlers(
|
||||||
lang,
|
lang,
|
||||||
selectMessage,
|
selectMessage,
|
||||||
@ -555,6 +571,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
botSender,
|
botSender,
|
||||||
messageTopic,
|
messageTopic,
|
||||||
Boolean(requestedChatTranslationLanguage),
|
Boolean(requestedChatTranslationLanguage),
|
||||||
|
replyStory && 'content' in replyStory ? replyStory : undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -594,10 +611,14 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
transitionClassNames,
|
transitionClassNames,
|
||||||
isJustAdded && 'is-just-added',
|
isJustAdded && 'is-just-added',
|
||||||
(Boolean(activeReactions) || hasActiveStickerEffect) && 'has-active-reaction',
|
(Boolean(activeReactions) || hasActiveStickerEffect) && 'has-active-reaction',
|
||||||
|
isStoryMention && 'is-story-mention',
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
text, photo, video, audio, voice, document, sticker, contact, poll, webPage, invoice, location, action, game,
|
text, photo, video, audio,
|
||||||
|
voice, document, sticker, contact,
|
||||||
|
poll, webPage, invoice, location,
|
||||||
|
action, game, storyData,
|
||||||
} = getMessageContent(message);
|
} = getMessageContent(message);
|
||||||
|
|
||||||
const detectedLanguage = useTextLanguage(
|
const detectedLanguage = useTextLanguage(
|
||||||
@ -625,7 +646,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
const withCommentButton = repliesThreadInfo && !isInDocumentGroupNotLast && messageListType === 'thread'
|
const withCommentButton = repliesThreadInfo && !isInDocumentGroupNotLast && messageListType === 'thread'
|
||||||
&& !noComments;
|
&& !noComments;
|
||||||
const withQuickReactionButton = !isTouchScreen && !phoneCall && !isInSelectMode && defaultReaction
|
const withQuickReactionButton = !isTouchScreen && !phoneCall && !isInSelectMode && defaultReaction
|
||||||
&& !isInDocumentGroupNotLast;
|
&& !isInDocumentGroupNotLast && !isStoryMention;
|
||||||
|
|
||||||
const contentClassName = buildContentClassName(message, {
|
const contentClassName = buildContentClassName(message, {
|
||||||
hasSubheader,
|
hasSubheader,
|
||||||
@ -658,7 +679,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
let reactionsPosition!: ReactionsPosition;
|
let reactionsPosition!: ReactionsPosition;
|
||||||
if (hasReactions) {
|
if (hasReactions) {
|
||||||
if (isCustomShape || ((photo || video || (location && location.type === 'geo')) && !hasText)) {
|
if (isCustomShape || ((photo || video || storyData || (location && location.type === 'geo')) && !hasText)) {
|
||||||
reactionsPosition = 'outside';
|
reactionsPosition = 'outside';
|
||||||
} else if (asForwarded) {
|
} else if (asForwarded) {
|
||||||
metaPosition = 'standalone';
|
metaPosition = 'standalone';
|
||||||
@ -679,6 +700,12 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
message.id,
|
message.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEnsureStory(
|
||||||
|
message.replyToStoryUserId ? message.replyToStoryUserId : chatId,
|
||||||
|
message.replyToStoryId,
|
||||||
|
replyStory,
|
||||||
|
);
|
||||||
|
|
||||||
useFocusMessage(
|
useFocusMessage(
|
||||||
ref, chatId, isFocused, focusDirection, noFocusHighlight, isResizingContainer, isJustAdded,
|
ref, chatId, isFocused, focusDirection, noFocusHighlight, isResizingContainer, isJustAdded,
|
||||||
);
|
);
|
||||||
@ -818,7 +845,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
function renderMessageText(isForAnimation?: boolean) {
|
function renderMessageText(isForAnimation?: boolean) {
|
||||||
return (
|
return (
|
||||||
<MessageText
|
<MessageText
|
||||||
message={message}
|
messageOrStory={message}
|
||||||
translatedText={requestedTranslationLanguage ? currentTranslatedText : undefined}
|
translatedText={requestedTranslationLanguage ? currentTranslatedText : undefined}
|
||||||
isForAnimation={isForAnimation}
|
isForAnimation={isForAnimation}
|
||||||
emojiSize={emojiSize}
|
emojiSize={emojiSize}
|
||||||
@ -932,6 +959,16 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
onClick={handleReplyClick}
|
onClick={handleReplyClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{hasStoryReply && (
|
||||||
|
<EmbeddedStory
|
||||||
|
story={replyStory}
|
||||||
|
sender={storySender}
|
||||||
|
noUserColors={isOwn || isChannel}
|
||||||
|
isProtected={isProtected}
|
||||||
|
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||||
|
onClick={handleStoryClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{sticker && (
|
{sticker && (
|
||||||
@ -1070,6 +1107,13 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
isDownloading={isDownloading}
|
isDownloading={isDownloading}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{storyData && !isStoryMention && (
|
||||||
|
<Story
|
||||||
|
message={message}
|
||||||
|
isProtected={isProtected}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isStoryMention && <StoryMention message={message} />}
|
||||||
{contact && (
|
{contact && (
|
||||||
<Contact contact={contact} />
|
<Contact contact={contact} />
|
||||||
)}
|
)}
|
||||||
@ -1128,6 +1172,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
isDownloading={isDownloading}
|
isDownloading={isDownloading}
|
||||||
isProtected={isProtected}
|
isProtected={isProtected}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
story={webPageStory}
|
||||||
|
isConnected={isConnected}
|
||||||
onMediaClick={handleMediaClick}
|
onMediaClick={handleMediaClick}
|
||||||
onCancelMediaTransfer={handleCancelUpload}
|
onCancelMediaTransfer={handleCancelUpload}
|
||||||
/>
|
/>
|
||||||
@ -1175,6 +1221,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
} else if (forwardInfo?.hiddenUserName) {
|
} else if (forwardInfo?.hiddenUserName) {
|
||||||
senderTitle = forwardInfo.hiddenUserName;
|
senderTitle = forwardInfo.hiddenUserName;
|
||||||
|
} else if (storyData && originSender) {
|
||||||
|
senderTitle = getSenderTitle(lang, originSender!);
|
||||||
}
|
}
|
||||||
const senderEmojiStatus = senderPeer && 'emojiStatus' in senderPeer && senderPeer.emojiStatus;
|
const senderEmojiStatus = senderPeer && 'emojiStatus' in senderPeer && senderPeer.emojiStatus;
|
||||||
const senderIsPremium = senderPeer && 'isPremium' in senderPeer && senderPeer.isPremium;
|
const senderIsPremium = senderPeer && 'isPremium' in senderPeer && senderPeer.isPremium;
|
||||||
@ -1284,12 +1332,12 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
>
|
>
|
||||||
{asForwarded && !isInDocumentGroupNotFirst && (
|
{asForwarded && !isInDocumentGroupNotFirst && (
|
||||||
<div className="message-title">
|
<div className="message-title">
|
||||||
{lang('ForwardedMessage')}
|
{lang(storyData ? 'ForwardedStory' : 'ForwardedMessage')}
|
||||||
{forwardAuthor && <span className="admin-title" dir="auto">{forwardAuthor}</span>}
|
{forwardAuthor && <span className="admin-title" dir="auto">{forwardAuthor}</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
{!isInDocumentGroupNotLast && metaPosition === 'standalone' && renderReactionsAndMeta()}
|
{!isInDocumentGroupNotLast && metaPosition === 'standalone' && !isStoryMention && renderReactionsAndMeta()}
|
||||||
{canShowActionButton && canForward ? (
|
{canShowActionButton && canForward ? (
|
||||||
<Button
|
<Button
|
||||||
className="message-action-button"
|
className="message-action-button"
|
||||||
@ -1320,7 +1368,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
{message.inlineButtons && (
|
{message.inlineButtons && (
|
||||||
<InlineButtons message={message} onClick={clickBotInlineButton} />
|
<InlineButtons message={message} onClick={clickBotInlineButton} />
|
||||||
)}
|
)}
|
||||||
{reactionsPosition === 'outside' && (
|
{reactionsPosition === 'outside' && !isStoryMention && (
|
||||||
<Reactions
|
<Reactions
|
||||||
message={reactionMessage!}
|
message={reactionMessage!}
|
||||||
isOutside
|
isOutside
|
||||||
@ -1387,8 +1435,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
message, album, withSenderName, withAvatar, threadId, messageListType, isLastInDocumentGroup, isFirstInGroup,
|
message, album, withSenderName, withAvatar, threadId, messageListType, isLastInDocumentGroup, isFirstInGroup,
|
||||||
} = ownProps;
|
} = ownProps;
|
||||||
const {
|
const {
|
||||||
id, chatId, viaBotId, replyToChatId, replyToMessageId, isOutgoing, repliesThreadInfo, forwardInfo,
|
id, chatId, viaBotId, replyToChatId, replyToMessageId, isOutgoing, forwardInfo,
|
||||||
transcriptionId, isPinned,
|
transcriptionId, isPinned, replyToStoryUserId, replyToStoryId, repliesThreadInfo,
|
||||||
} = message;
|
} = message;
|
||||||
|
|
||||||
const chat = selectChat(global, chatId);
|
const chat = selectChat(global, chatId);
|
||||||
@ -1398,6 +1446,10 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const isGroup = chat && isChatGroup(chat);
|
const isGroup = chat && isChatGroup(chat);
|
||||||
const chatUsernames = chat?.usernames;
|
const chatUsernames = chat?.usernames;
|
||||||
const chatFullInfo = !isUserId(chatId) ? selectChatFullInfo(global, chatId) : undefined;
|
const chatFullInfo = !isUserId(chatId) ? selectChatFullInfo(global, chatId) : undefined;
|
||||||
|
const webPageStoryData = message.content.webPage?.story;
|
||||||
|
const webPageStory = webPageStoryData
|
||||||
|
? selectUserStory(global, webPageStoryData.userId, webPageStoryData.id)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const isForwarding = forwardMessages.messageIds && forwardMessages.messageIds.includes(id);
|
const isForwarding = forwardMessages.messageIds && forwardMessages.messageIds.includes(id);
|
||||||
const forceSenderName = !isChatWithSelf && isAnonymousOwnMessage(message);
|
const forceSenderName = !isChatWithSelf && isAnonymousOwnMessage(message);
|
||||||
@ -1418,6 +1470,10 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
: undefined;
|
: undefined;
|
||||||
const replyMessageSender = replyMessage && selectReplySender(global, replyMessage, Boolean(forwardInfo));
|
const replyMessageSender = replyMessage && selectReplySender(global, replyMessage, Boolean(forwardInfo));
|
||||||
const isReplyToTopicStart = replyMessage?.content.action?.type === 'topicCreate';
|
const isReplyToTopicStart = replyMessage?.content.action?.type === 'topicCreate';
|
||||||
|
const replyStory = replyToStoryId && replyToStoryUserId
|
||||||
|
? selectUserStory(global, replyToStoryUserId, replyToStoryId)
|
||||||
|
: undefined;
|
||||||
|
const storySender = replyToStoryUserId ? selectUser(global, replyToStoryUserId) : undefined;
|
||||||
|
|
||||||
const uploadProgress = selectUploadProgress(global, message);
|
const uploadProgress = selectUploadProgress(global, message);
|
||||||
const isFocused = messageListType === 'thread' && (
|
const isFocused = messageListType === 'thread' && (
|
||||||
@ -1485,6 +1541,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isThreadTop,
|
isThreadTop,
|
||||||
replyMessage,
|
replyMessage,
|
||||||
replyMessageSender,
|
replyMessageSender,
|
||||||
|
replyStory,
|
||||||
|
storySender,
|
||||||
isInDocumentGroup,
|
isInDocumentGroup,
|
||||||
isProtected: selectIsMessageProtected(global, message),
|
isProtected: selectIsMessageProtected(global, message),
|
||||||
isChatProtected: selectIsChatProtected(global, chatId),
|
isChatProtected: selectIsChatProtected(global, chatId),
|
||||||
@ -1536,6 +1594,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
hasLinkedChat: Boolean(chatFullInfo?.linkedChatId),
|
hasLinkedChat: Boolean(chatFullInfo?.linkedChatId),
|
||||||
withReactionEffects: selectPerformanceSettingsValue(global, 'reactionEffects'),
|
withReactionEffects: selectPerformanceSettingsValue(global, 'reactionEffects'),
|
||||||
withStickerEffects: selectPerformanceSettingsValue(global, 'stickerEffects'),
|
withStickerEffects: selectPerformanceSettingsValue(global, 'stickerEffects'),
|
||||||
|
webPageStory,
|
||||||
isConnected,
|
isConnected,
|
||||||
...((canShowSender || isLocation) && { sender }),
|
...((canShowSender || isLocation) && { sender }),
|
||||||
...(isOutgoing && { outgoingStatus: selectOutgoingStatus(global, message, messageListType === 'scheduled') }),
|
...(isOutgoing && { outgoingStatus: selectOutgoingStatus(global, message, messageListType === 'scheduled') }),
|
||||||
|
|||||||
@ -302,7 +302,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
return enableScrolling;
|
return enableScrolling;
|
||||||
}, [withScroll]);
|
}, [withScroll]);
|
||||||
|
|
||||||
const handleOpenReactionPicker = useLastCallback((position: IAnchorPosition) => {
|
const handleOpenMessageReactionPicker = useLastCallback((position: IAnchorPosition) => {
|
||||||
onReactionPickerOpen!(position);
|
onReactionPickerOpen!(position);
|
||||||
hideItems();
|
hideItems();
|
||||||
});
|
});
|
||||||
@ -337,7 +337,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
canBuyPremium={canBuyPremium}
|
canBuyPremium={canBuyPremium}
|
||||||
isCurrentUserPremium={isCurrentUserPremium}
|
isCurrentUserPremium={isCurrentUserPremium}
|
||||||
canPlayAnimatedEmojis={canPlayAnimatedEmojis}
|
canPlayAnimatedEmojis={canPlayAnimatedEmojis}
|
||||||
onShowMore={handleOpenReactionPicker}
|
onShowMore={handleOpenMessageReactionPicker}
|
||||||
className={buildClassName(areItemsHidden && 'ReactionSelector-hidden')}
|
className={buildClassName(areItemsHidden && 'ReactionSelector-hidden')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -5,13 +5,9 @@ import type { OwnProps } from './ReactionPicker';
|
|||||||
import { Bundles } from '../../../util/moduleLoader';
|
import { Bundles } from '../../../util/moduleLoader';
|
||||||
import useModuleLoader from '../../../hooks/useModuleLoader';
|
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||||
|
|
||||||
interface LocalOwnProps {
|
const ReactionPickerAsync: FC<OwnProps> = (props) => {
|
||||||
shouldLoad?: boolean;
|
const { isOpen } = props;
|
||||||
}
|
const ReactionPicker = useModuleLoader(Bundles.Extra, 'ReactionPicker', !isOpen);
|
||||||
|
|
||||||
const ReactionPickerAsync: FC<OwnProps & LocalOwnProps> = (props) => {
|
|
||||||
const { isOpen, shouldLoad } = props;
|
|
||||||
const ReactionPicker = useModuleLoader(Bundles.Extra, 'ReactionPicker', !isOpen && !shouldLoad);
|
|
||||||
|
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
return ReactionPicker ? <ReactionPicker {...props} /> : undefined;
|
return ReactionPicker ? <ReactionPicker {...props} /> : undefined;
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
:global(body:not(.no-menu-blur)) & {
|
:global(body:not(.no-menu-blur)) & {
|
||||||
--color-background: var(--color-background-compact-menu);
|
--color-background: var(--color-background-compact-menu);
|
||||||
|
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(25px);
|
||||||
}
|
}
|
||||||
|
|
||||||
width: calc(var(--symbol-menu-width) + var(--scrollbar-width));
|
width: calc(var(--symbol-menu-width) + var(--scrollbar-width));
|
||||||
@ -50,6 +50,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.storyMenu {
|
||||||
|
--color-background-compact-menu: rgba(0, 0, 0, 0.3);
|
||||||
|
--color-text-secondary: #fff;
|
||||||
|
--color-text-secondary-rgb: 255, 255, 255;
|
||||||
|
--color-default-shadow: rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
:global(.StickerButton.custom-emoji), :global(.sticker-set-cover) {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
transform-origin: 70% 100% !important;
|
||||||
|
|
||||||
|
@media (max-width: 440px) {
|
||||||
|
&:global(.bubble) {
|
||||||
|
transform-origin: 30% 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.onlyReactions {
|
.onlyReactions {
|
||||||
height: auto;
|
height: auto;
|
||||||
transform-origin: 9rem 1.75rem !important;
|
transform-origin: 9rem 1.75rem !important;
|
||||||
|
|||||||
@ -1,21 +1,28 @@
|
|||||||
import React, { memo, useMemo, useRef } from '../../../lib/teact/teact';
|
import React, { memo, useMemo, useRef } from '../../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import type {
|
import type {
|
||||||
ApiMessage, ApiReaction, ApiSticker, ApiReactionCustomEmoji,
|
ApiMessage, ApiReaction, ApiSticker, ApiReactionCustomEmoji, ApiStory, ApiStorySkipped,
|
||||||
|
ApiMessageEntity,
|
||||||
} from '../../../api/types';
|
} from '../../../api/types';
|
||||||
import type { IAnchorPosition } from '../../../types';
|
import type { IAnchorPosition } from '../../../types';
|
||||||
|
|
||||||
|
import { REM } from '../../common/helpers/mediaDimensions';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { isUserId } from '../../../global/helpers';
|
import { isUserId } from '../../../global/helpers';
|
||||||
import {
|
import {
|
||||||
selectChat, selectChatFullInfo, selectChatMessage, selectIsContextMenuTranslucent, selectTabState,
|
selectChat, selectChatFullInfo, selectChatMessage, selectIsContextMenuTranslucent, selectIsCurrentUserPremium,
|
||||||
|
selectTabState, selectUserStory,
|
||||||
} from '../../../global/selectors';
|
} from '../../../global/selectors';
|
||||||
|
import parseMessageInput from '../../../util/parseMessageInput';
|
||||||
|
import { buildCustomEmojiHtml } from '../composer/helpers/customEmoji';
|
||||||
|
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||||
import useMenuPosition from '../../../hooks/useMenuPosition';
|
import useMenuPosition from '../../../hooks/useMenuPosition';
|
||||||
|
import { getIsMobile } from '../../../hooks/useAppLayout';
|
||||||
|
|
||||||
import CustomEmojiPicker from '../../common/CustomEmojiPicker';
|
import CustomEmojiPicker from '../../common/CustomEmojiPicker';
|
||||||
import ReactionPickerLimited from './ReactionPickerLimited';
|
import ReactionPickerLimited from './ReactionPickerLimited';
|
||||||
@ -30,24 +37,35 @@ export type OwnProps = {
|
|||||||
interface StateProps {
|
interface StateProps {
|
||||||
withCustomReactions?: boolean;
|
withCustomReactions?: boolean;
|
||||||
message?: ApiMessage;
|
message?: ApiMessage;
|
||||||
|
story?: ApiStory | ApiStorySkipped;
|
||||||
|
isCurrentUserPremium?: boolean;
|
||||||
position?: IAnchorPosition;
|
position?: IAnchorPosition;
|
||||||
isTranslucent?: boolean;
|
isTranslucent?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FULL_PICKER_SHIFT_DELTA = { x: -23, y: -64 };
|
const FULL_PICKER_SHIFT_DELTA = { x: -23, y: -64 };
|
||||||
const LIMITED_PICKER_SHIFT_DELTA = { x: -21, y: -10 };
|
const LIMITED_PICKER_SHIFT_DELTA = { x: -21, y: -10 };
|
||||||
|
const REACTION_SELECTOR_WIDTH = 16.375 * REM;
|
||||||
|
|
||||||
const ReactionPicker: FC<OwnProps & StateProps> = ({
|
const ReactionPicker: FC<OwnProps & StateProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
message,
|
message,
|
||||||
|
story,
|
||||||
position,
|
position,
|
||||||
isTranslucent,
|
isTranslucent,
|
||||||
|
isCurrentUserPremium,
|
||||||
withCustomReactions,
|
withCustomReactions,
|
||||||
}) => {
|
}) => {
|
||||||
const { toggleReaction, closeReactionPicker } = getActions();
|
const {
|
||||||
|
toggleReaction, closeReactionPicker, sendMessage, showNotification,
|
||||||
|
} = getActions();
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
const renderedMessageId = useCurrentOrPrev(message?.id, true);
|
const renderedMessageId = useCurrentOrPrev(message?.id, true);
|
||||||
const renderedChatId = useCurrentOrPrev(message?.chatId, true);
|
const renderedChatId = useCurrentOrPrev(message?.chatId, true);
|
||||||
|
const renderedStoryUserId = useCurrentOrPrev(story?.userId, true);
|
||||||
|
const renderedStoryId = useCurrentOrPrev(story?.id);
|
||||||
const storedPosition = useCurrentOrPrev(position, true);
|
const storedPosition = useCurrentOrPrev(position, true);
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
@ -56,14 +74,24 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (renderedStoryId) {
|
||||||
|
return storedPosition;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: storedPosition.x + (withCustomReactions ? FULL_PICKER_SHIFT_DELTA.x : LIMITED_PICKER_SHIFT_DELTA.x),
|
x: storedPosition.x + (withCustomReactions ? FULL_PICKER_SHIFT_DELTA.x : LIMITED_PICKER_SHIFT_DELTA.x),
|
||||||
y: storedPosition.y + (withCustomReactions ? FULL_PICKER_SHIFT_DELTA.y : LIMITED_PICKER_SHIFT_DELTA.y),
|
y: storedPosition.y + (withCustomReactions ? FULL_PICKER_SHIFT_DELTA.y : LIMITED_PICKER_SHIFT_DELTA.y),
|
||||||
};
|
};
|
||||||
}, [storedPosition, withCustomReactions]);
|
}, [renderedStoryId, storedPosition, withCustomReactions]);
|
||||||
|
|
||||||
const getMenuElement = useLastCallback(() => menuRef.current);
|
const getMenuElement = useLastCallback(() => menuRef.current);
|
||||||
const getLayout = useLastCallback(() => ({ withPortal: true, isDense: true }));
|
const getLayout = useLastCallback(() => ({
|
||||||
|
withPortal: true,
|
||||||
|
isDense: !renderedStoryUserId,
|
||||||
|
deltaX: !getIsMobile() && menuRef.current
|
||||||
|
? -(menuRef.current.offsetWidth - REACTION_SELECTOR_WIDTH) / 2 - FULL_PICKER_SHIFT_DELTA.x / 2
|
||||||
|
: 0,
|
||||||
|
}));
|
||||||
const {
|
const {
|
||||||
positionX, positionY, transformOriginX, transformOriginY, style,
|
positionX, positionY, transformOriginX, transformOriginY, style,
|
||||||
} = useMenuPosition(renderingPosition, getTriggerElement, getRootElement, getMenuElement, getLayout);
|
} = useMenuPosition(renderingPosition, getTriggerElement, getRootElement, getMenuElement, getLayout);
|
||||||
@ -93,6 +121,41 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
|
|||||||
closeReactionPicker();
|
closeReactionPicker();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleStoryReactionSelect = useLastCallback((reaction: ApiReaction | ApiSticker) => {
|
||||||
|
let text: string | undefined;
|
||||||
|
let entities: ApiMessageEntity[] | undefined;
|
||||||
|
|
||||||
|
if ('emoticon' in reaction) {
|
||||||
|
text = reaction.emoticon;
|
||||||
|
} else {
|
||||||
|
const sticker = 'documentId' in reaction ? getGlobal().customEmojis.byId[reaction.documentId] : reaction;
|
||||||
|
if (!sticker) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sticker.isFree && !isCurrentUserPremium) {
|
||||||
|
showNotification({
|
||||||
|
message: lang('UnlockPremiumEmojiHint'),
|
||||||
|
action: {
|
||||||
|
action: 'openPremiumModal',
|
||||||
|
payload: { initialSection: 'animated_emoji' },
|
||||||
|
},
|
||||||
|
actionText: lang('PremiumMore'),
|
||||||
|
});
|
||||||
|
|
||||||
|
closeReactionPicker();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const customEmojiMessage = parseMessageInput(buildCustomEmojiHtml(sticker));
|
||||||
|
text = customEmojiMessage.text;
|
||||||
|
entities = customEmojiMessage.entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage({ text, entities, isReaction: true });
|
||||||
|
closeReactionPicker();
|
||||||
|
});
|
||||||
|
|
||||||
const selectedReactionIds = useMemo(() => {
|
const selectedReactionIds = useMemo(() => {
|
||||||
return (message?.reactions?.results || []).reduce<string[]>((acc, { chosenOrder, reaction }) => {
|
return (message?.reactions?.results || []).reduce<string[]>((acc, { chosenOrder, reaction }) => {
|
||||||
if (chosenOrder !== undefined) {
|
if (chosenOrder !== undefined) {
|
||||||
@ -108,7 +171,11 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
|
|||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
ref={menuRef}
|
ref={menuRef}
|
||||||
className={buildClassName(styles.menu, 'ReactionPicker')}
|
className={buildClassName(styles.menu, 'ReactionPicker')}
|
||||||
bubbleClassName={buildClassName(styles.menuContent, !withCustomReactions && styles.onlyReactions)}
|
bubbleClassName={buildClassName(
|
||||||
|
styles.menuContent,
|
||||||
|
!withCustomReactions && !renderedStoryId && styles.onlyReactions,
|
||||||
|
renderedStoryId && styles.storyMenu,
|
||||||
|
)}
|
||||||
withPortal
|
withPortal
|
||||||
noCompact
|
noCompact
|
||||||
positionX={positionX}
|
positionX={positionX}
|
||||||
@ -121,20 +188,20 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
|
|||||||
>
|
>
|
||||||
<CustomEmojiPicker
|
<CustomEmojiPicker
|
||||||
idPrefix="message-emoji-set-"
|
idPrefix="message-emoji-set-"
|
||||||
isHidden={!isOpen || !withCustomReactions}
|
isHidden={!isOpen || !(withCustomReactions || renderedStoryId)}
|
||||||
loadAndPlay={Boolean(isOpen && withCustomReactions)}
|
loadAndPlay={Boolean(isOpen && withCustomReactions)}
|
||||||
isReactionPicker
|
isReactionPicker
|
||||||
className={!withCustomReactions ? styles.hidden : undefined}
|
className={!withCustomReactions && !renderedStoryId ? styles.hidden : undefined}
|
||||||
selectedReactionIds={selectedReactionIds}
|
selectedReactionIds={selectedReactionIds}
|
||||||
isTranslucent={isTranslucent}
|
isTranslucent={isTranslucent}
|
||||||
onCustomEmojiSelect={handleToggleCustomReaction}
|
onCustomEmojiSelect={renderedStoryId ? handleStoryReactionSelect : handleToggleCustomReaction}
|
||||||
onReactionSelect={handleToggleReaction}
|
onReactionSelect={renderedStoryId ? handleStoryReactionSelect : handleToggleReaction}
|
||||||
/>
|
/>
|
||||||
{!withCustomReactions && Boolean(renderedChatId) && (
|
{!withCustomReactions && Boolean(renderedChatId) && (
|
||||||
<ReactionPickerLimited
|
<ReactionPickerLimited
|
||||||
chatId={renderedChatId}
|
chatId={renderedChatId}
|
||||||
loadAndPlay={isOpen}
|
loadAndPlay={isOpen}
|
||||||
onReactionSelect={handleToggleReaction}
|
onReactionSelect={renderedStoryId ? handleStoryReactionSelect : handleToggleReaction}
|
||||||
selectedReactionIds={selectedReactionIds}
|
selectedReactionIds={selectedReactionIds}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -144,22 +211,29 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
export default memo(withGlobal<OwnProps>((global): StateProps => {
|
export default memo(withGlobal<OwnProps>((global): StateProps => {
|
||||||
const state = selectTabState(global);
|
const state = selectTabState(global);
|
||||||
const { chatId, messageId, position } = state.reactionPicker || {};
|
const {
|
||||||
|
chatId, messageId, storyUserId, storyId, position,
|
||||||
|
} = state.reactionPicker || {};
|
||||||
|
const story = storyUserId && storyId
|
||||||
|
? selectUserStory(global, storyUserId, storyId) as ApiStory | ApiStorySkipped
|
||||||
|
: undefined;
|
||||||
const chat = chatId ? selectChat(global, chatId) : undefined;
|
const chat = chatId ? selectChat(global, chatId) : undefined;
|
||||||
const chatFullInfo = chatId ? selectChatFullInfo(global, chatId) : undefined;
|
const chatFullInfo = chatId ? selectChatFullInfo(global, chatId) : undefined;
|
||||||
const message = chatId && messageId ? selectChatMessage(global, chatId, messageId) : undefined;
|
const message = chatId && messageId ? selectChatMessage(global, chatId, messageId) : undefined;
|
||||||
const isPrivateChat = chatId ? isUserId(chatId) : false;
|
const isPrivateChat = chatId ? isUserId(chatId) : Boolean(storyUserId);
|
||||||
const areSomeReactionsAllowed = chatFullInfo?.enabledReactions?.type === 'some';
|
const areSomeReactionsAllowed = chatFullInfo?.enabledReactions?.type === 'some';
|
||||||
const areCustomReactionsAllowed = chatFullInfo?.enabledReactions?.type === 'all'
|
const areCustomReactionsAllowed = chatFullInfo?.enabledReactions?.type === 'all'
|
||||||
&& chatFullInfo?.enabledReactions?.areCustomAllowed;
|
&& chatFullInfo?.enabledReactions?.areCustomAllowed;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message,
|
message,
|
||||||
|
story,
|
||||||
position,
|
position,
|
||||||
withCustomReactions: chat?.isForbidden || areSomeReactionsAllowed
|
withCustomReactions: chat?.isForbidden || areSomeReactionsAllowed
|
||||||
? false
|
? false
|
||||||
: areCustomReactionsAllowed || isPrivateChat,
|
: areCustomReactionsAllowed || isPrivateChat,
|
||||||
isTranslucent: selectIsContextMenuTranslucent(global),
|
isTranslucent: selectIsContextMenuTranslucent(global),
|
||||||
|
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||||
};
|
};
|
||||||
})(ReactionPicker));
|
})(ReactionPicker));
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
body:not(.no-menu-blur) & {
|
body:not(.no-menu-blur) & {
|
||||||
background: var(--color-background-compact-menu);
|
background: var(--color-background-compact-menu);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(25px);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.is-safari & {
|
body.is-safari & {
|
||||||
|
|||||||
43
src/components/middle/message/Story.tsx
Normal file
43
src/components/middle/message/Story.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import React, { memo } from '../../../lib/teact/teact';
|
||||||
|
import { withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiMessage, ApiTypeStory,
|
||||||
|
} from '../../../api/types';
|
||||||
|
|
||||||
|
import { selectUserStory } from '../../../global/selectors';
|
||||||
|
|
||||||
|
import BaseStory from './BaseStory';
|
||||||
|
|
||||||
|
interface OwnProps {
|
||||||
|
message: ApiMessage;
|
||||||
|
isProtected?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StateProps {
|
||||||
|
story?: ApiTypeStory;
|
||||||
|
isConnected?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Story({
|
||||||
|
message, story, isProtected, isConnected,
|
||||||
|
}: OwnProps & StateProps) {
|
||||||
|
const { storyData } = message.content;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseStory
|
||||||
|
story={story || storyData}
|
||||||
|
isProtected={isProtected}
|
||||||
|
isConnected={isConnected}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>((global, { message }): StateProps => {
|
||||||
|
const { id, userId } = message.content.storyData!;
|
||||||
|
|
||||||
|
return {
|
||||||
|
story: selectUserStory(global, userId, id),
|
||||||
|
isConnected: global.connectionState === 'connectionStateReady',
|
||||||
|
};
|
||||||
|
})(Story));
|
||||||
102
src/components/middle/message/StoryMention.tsx
Normal file
102
src/components/middle/message/StoryMention.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import React, { memo } from '../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiMessage, ApiTypeStory, ApiUser,
|
||||||
|
} from '../../../api/types';
|
||||||
|
|
||||||
|
import { getStoryMediaHash, getUserFirstOrLastName } from '../../../global/helpers';
|
||||||
|
import {
|
||||||
|
selectUser, selectUserStories, selectUserStory,
|
||||||
|
} from '../../../global/selectors';
|
||||||
|
import renderText from '../../common/helpers/renderText';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
import useEnsureStory from '../../../hooks/useEnsureStory';
|
||||||
|
import useMedia from '../../../hooks/useMedia';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
|
interface OwnProps {
|
||||||
|
message: ApiMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StateProps {
|
||||||
|
story?: ApiTypeStory;
|
||||||
|
user?: ApiUser;
|
||||||
|
targetUser?: ApiUser;
|
||||||
|
isUnread?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function StoryMention({
|
||||||
|
message, story, user, isUnread, targetUser,
|
||||||
|
}: OwnProps & StateProps) {
|
||||||
|
const { openStoryViewer } = getActions();
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const { storyData } = message.content;
|
||||||
|
|
||||||
|
const handleClick = useLastCallback(() => {
|
||||||
|
openStoryViewer({
|
||||||
|
userId: story!.userId,
|
||||||
|
storyId: story!.id,
|
||||||
|
isSingleUser: true,
|
||||||
|
isSingleStory: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const isDeleted = story && 'isDeleted' in story;
|
||||||
|
const isLoaded = story && 'content' in story;
|
||||||
|
const video = isLoaded ? story.content.video : undefined;
|
||||||
|
const imageHash = isLoaded
|
||||||
|
? getStoryMediaHash(story, 'pictogram')
|
||||||
|
: undefined;
|
||||||
|
const imgBlobUrl = useMedia(imageHash);
|
||||||
|
const thumbUrl = imgBlobUrl || video?.thumbnail?.dataUri;
|
||||||
|
|
||||||
|
useEnsureStory(storyData!.userId, storyData!.id, story);
|
||||||
|
|
||||||
|
function getTitle() {
|
||||||
|
if (user?.isSelf) {
|
||||||
|
return isDeleted
|
||||||
|
? lang('ExpiredStoryMentioned', getUserFirstOrLastName(targetUser))
|
||||||
|
: lang('StoryYouMentionedTitle', getUserFirstOrLastName(targetUser));
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDeleted
|
||||||
|
? lang('ExpiredStoryMention')
|
||||||
|
: lang('StoryMentionedTitle', getUserFirstOrLastName(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={buildClassName('action-message-story-mention', isUnread && 'is-unread', isLoaded && 'with-preview')}
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
onClick={isLoaded ? handleClick : undefined}
|
||||||
|
>
|
||||||
|
{isLoaded && (
|
||||||
|
<span className="story-media-wrapper">
|
||||||
|
{thumbUrl && (
|
||||||
|
<img src={thumbUrl} alt="" className="story-media" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span className="story-title">{renderText(getTitle(), ['emoji', 'simple_markdown'])}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>((global, { message }): StateProps => {
|
||||||
|
const { id, userId } = message.content.storyData!;
|
||||||
|
const lastReadId = selectUserStories(global, userId)?.lastReadId;
|
||||||
|
|
||||||
|
return {
|
||||||
|
story: selectUserStory(global, userId, id),
|
||||||
|
user: selectUser(global, userId),
|
||||||
|
targetUser: selectUser(global, message.chatId),
|
||||||
|
isUnread: Boolean(lastReadId && lastReadId < id),
|
||||||
|
};
|
||||||
|
})(StoryMention));
|
||||||
@ -20,6 +20,11 @@
|
|||||||
background: var(--accent-color);
|
background: var(--accent-color);
|
||||||
border-radius: 0.125rem;
|
border-radius: 0.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-story {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--quick-button {
|
&--quick-button {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user