Stories: New features and fixes (#3773)
This commit is contained in:
parent
25e6a8f72f
commit
a7c7c8d95c
@ -16,7 +16,7 @@ import type {
|
|||||||
|
|
||||||
import { pick } from '../../../util/iteratees';
|
import { pick } from '../../../util/iteratees';
|
||||||
import { buildApiPhoto, buildApiThumbnailFromStripped } from './common';
|
import { buildApiPhoto, buildApiThumbnailFromStripped } from './common';
|
||||||
import { buildApiDocument, buildApiWebDocument, buildVideoFromDocument } from './messages';
|
import { buildApiDocument, buildApiWebDocument, buildVideoFromDocument } from './messageContent';
|
||||||
import { buildStickerFromDocument } from './symbols';
|
import { buildStickerFromDocument } from './symbols';
|
||||||
import localDb from '../localDb';
|
import localDb from '../localDb';
|
||||||
import { buildApiPeerId } from './peers';
|
import { buildApiPeerId } from './peers';
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import {
|
|||||||
} from './peers';
|
} from './peers';
|
||||||
import { omitVirtualClassFields } from './helpers';
|
import { omitVirtualClassFields } from './helpers';
|
||||||
import { getServerTime, getServerTimeOffset } from '../../../util/serverTime';
|
import { getServerTime, getServerTimeOffset } from '../../../util/serverTime';
|
||||||
import { buildApiReaction } from './messages';
|
import { buildApiReaction } from './reactions';
|
||||||
import { buildApiUsernames } from './common';
|
import { buildApiUsernames } from './common';
|
||||||
|
|
||||||
type PeerEntityApiChatFields = Omit<ApiChat, (
|
type PeerEntityApiChatFields = Omit<ApiChat, (
|
||||||
|
|||||||
@ -1,12 +1,24 @@
|
|||||||
import { Api as GramJs } from '../../../lib/gramjs';
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
import { strippedPhotoToJpg } from '../../../lib/gramjs/Utils';
|
import { strippedPhotoToJpg } from '../../../lib/gramjs/Utils';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ApiPhoto, ApiPhotoSize, ApiThumbnail, ApiVideoSize, ApiUsername,
|
ApiPhoto,
|
||||||
|
ApiPhotoSize,
|
||||||
|
ApiThumbnail,
|
||||||
|
ApiVideoSize,
|
||||||
|
ApiUsername,
|
||||||
|
ApiFormattedText,
|
||||||
|
ApiMessageEntity,
|
||||||
|
ApiMessageEntityDefault,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
import {
|
||||||
|
ApiMessageEntityTypes,
|
||||||
|
} from '../../types';
|
||||||
|
import type { ApiPrivacySettings, PrivacyVisibility } from '../../../types';
|
||||||
|
|
||||||
import { bytesToDataUri } from './helpers';
|
import { bytesToDataUri } from './helpers';
|
||||||
import { pathBytesToSvg } from './pathBytesToSvg';
|
import { pathBytesToSvg } from './pathBytesToSvg';
|
||||||
import { compact } from '../../../util/iteratees';
|
import { compact } from '../../../util/iteratees';
|
||||||
|
import { buildApiPeerId } from './peers';
|
||||||
|
|
||||||
const DEFAULT_THUMB_SIZE = { w: 100, h: 100 };
|
const DEFAULT_THUMB_SIZE = { w: 100, h: 100 };
|
||||||
|
|
||||||
@ -132,3 +144,104 @@ export function buildApiUsernames(mtpPeer: GramJs.User | GramJs.Channel | GramJs
|
|||||||
|
|
||||||
return usernames;
|
return usernames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildApiFormattedText(textWithEntities: GramJs.TextWithEntities): ApiFormattedText {
|
||||||
|
const { text, entities } = textWithEntities;
|
||||||
|
|
||||||
|
return {
|
||||||
|
text,
|
||||||
|
entities: entities.map(buildApiMessageEntity),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildApiMessageEntity(entity: GramJs.TypeMessageEntity): ApiMessageEntity {
|
||||||
|
const {
|
||||||
|
className: type, offset, length,
|
||||||
|
} = entity;
|
||||||
|
|
||||||
|
if (entity instanceof GramJs.MessageEntityMentionName) {
|
||||||
|
return {
|
||||||
|
type: ApiMessageEntityTypes.MentionName,
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
userId: buildApiPeerId(entity.userId, 'user'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity instanceof GramJs.MessageEntityTextUrl) {
|
||||||
|
return {
|
||||||
|
type: ApiMessageEntityTypes.TextUrl,
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
url: entity.url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity instanceof GramJs.MessageEntityPre) {
|
||||||
|
return {
|
||||||
|
type: ApiMessageEntityTypes.Pre,
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
language: entity.language,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity instanceof GramJs.MessageEntityCustomEmoji) {
|
||||||
|
return {
|
||||||
|
type: ApiMessageEntityTypes.CustomEmoji,
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
documentId: entity.documentId.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: type as `${ApiMessageEntityDefault['type']}`,
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
644
src/api/gramjs/apiBuilders/messageContent.ts
Normal file
644
src/api/gramjs/apiBuilders/messageContent.ts
Normal file
@ -0,0 +1,644 @@
|
|||||||
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiFormattedText,
|
||||||
|
ApiMessage,
|
||||||
|
ApiWebPage,
|
||||||
|
ApiWebPageStoryData,
|
||||||
|
ApiWebDocument,
|
||||||
|
ApiMessageExtendedMediaPreview,
|
||||||
|
ApiPoll,
|
||||||
|
ApiInvoice,
|
||||||
|
ApiGame,
|
||||||
|
ApiLocation,
|
||||||
|
ApiContact,
|
||||||
|
ApiDocument,
|
||||||
|
ApiVoice,
|
||||||
|
ApiAudio,
|
||||||
|
ApiVideo,
|
||||||
|
ApiPhoto,
|
||||||
|
ApiSticker,
|
||||||
|
ApiMessageStoryData,
|
||||||
|
} from '../../types';
|
||||||
|
import type { UniversalMessage } from './messages';
|
||||||
|
|
||||||
|
import { SUPPORTED_IMAGE_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES, VIDEO_WEBM_TYPE } from '../../../config';
|
||||||
|
import { pick } from '../../../util/iteratees';
|
||||||
|
import { addStoryToLocalDb, serializeBytes } from '../helpers';
|
||||||
|
import {
|
||||||
|
buildApiMessageEntity,
|
||||||
|
buildApiPhoto,
|
||||||
|
buildApiPhotoSize,
|
||||||
|
buildApiThumbnailFromPath,
|
||||||
|
buildApiThumbnailFromStripped,
|
||||||
|
} from './common';
|
||||||
|
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
|
||||||
|
import { buildStickerFromDocument } from './symbols';
|
||||||
|
|
||||||
|
export function buildMessageContent(
|
||||||
|
mtpMessage: UniversalMessage | GramJs.UpdateServiceNotification,
|
||||||
|
) {
|
||||||
|
let content: ApiMessage['content'] = {};
|
||||||
|
|
||||||
|
if (mtpMessage.media) {
|
||||||
|
content = {
|
||||||
|
...buildMessageMediaContent(mtpMessage.media),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasUnsupportedMedia = mtpMessage.media instanceof GramJs.MessageMediaUnsupported;
|
||||||
|
|
||||||
|
if (mtpMessage.message && !hasUnsupportedMedia
|
||||||
|
&& !content.sticker && !content.poll && !content.contact && !(content.video?.isRound)) {
|
||||||
|
content = {
|
||||||
|
...content,
|
||||||
|
text: buildMessageTextContent(mtpMessage.message, mtpMessage.entities),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMessageTextContent(
|
||||||
|
message: string,
|
||||||
|
entities?: GramJs.TypeMessageEntity[],
|
||||||
|
): ApiFormattedText {
|
||||||
|
return {
|
||||||
|
text: message,
|
||||||
|
...(entities && { entities: entities.map(buildApiMessageEntity) }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): ApiMessage['content'] | undefined {
|
||||||
|
if ('ttlSeconds' in media && media.ttlSeconds) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('extendedMedia' in media && media.extendedMedia instanceof GramJs.MessageExtendedMedia) {
|
||||||
|
return buildMessageMediaContent(media.extendedMedia.media);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sticker = buildSticker(media);
|
||||||
|
if (sticker) return { sticker };
|
||||||
|
|
||||||
|
const photo = buildPhoto(media);
|
||||||
|
if (photo) return { photo };
|
||||||
|
|
||||||
|
const video = buildVideo(media);
|
||||||
|
const altVideo = buildAltVideo(media);
|
||||||
|
if (video) return { video, altVideo };
|
||||||
|
|
||||||
|
const audio = buildAudio(media);
|
||||||
|
if (audio) return { audio };
|
||||||
|
|
||||||
|
const voice = buildVoice(media);
|
||||||
|
if (voice) return { voice };
|
||||||
|
|
||||||
|
const document = buildDocumentFromMedia(media);
|
||||||
|
if (document) return { document };
|
||||||
|
|
||||||
|
const contact = buildContact(media);
|
||||||
|
if (contact) return { contact };
|
||||||
|
|
||||||
|
const poll = buildPollFromMedia(media);
|
||||||
|
if (poll) return { poll };
|
||||||
|
|
||||||
|
const webPage = buildWebPage(media);
|
||||||
|
if (webPage) return { webPage };
|
||||||
|
|
||||||
|
const invoice = buildInvoiceFromMedia(media);
|
||||||
|
if (invoice) return { invoice };
|
||||||
|
|
||||||
|
const location = buildLocationFromMedia(media);
|
||||||
|
if (location) return { location };
|
||||||
|
|
||||||
|
const game = buildGameFromMedia(media);
|
||||||
|
if (game) return { game };
|
||||||
|
|
||||||
|
const storyData = buildMessageStoryData(media);
|
||||||
|
if (storyData) return { storyData };
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSticker(media: GramJs.TypeMessageMedia): ApiSticker | undefined {
|
||||||
|
if (
|
||||||
|
!(media instanceof GramJs.MessageMediaDocument)
|
||||||
|
|| !media.document
|
||||||
|
|| !(media.document instanceof GramJs.Document)
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildStickerFromDocument(media.document, media.nopremium);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPhoto(media: GramJs.TypeMessageMedia): ApiPhoto | undefined {
|
||||||
|
if (!(media instanceof GramJs.MessageMediaPhoto) || !media.photo || !(media.photo instanceof GramJs.Photo)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildApiPhoto(media.photo, media.spoiler);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildVideoFromDocument(document: GramJs.Document, isSpoiler?: boolean): ApiVideo | undefined {
|
||||||
|
if (document instanceof GramJs.DocumentEmpty) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
id, mimeType, thumbs, size, attributes,
|
||||||
|
} = document;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
|
if (mimeType === VIDEO_WEBM_TYPE && !(self as any).isWebmSupported) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoAttr = attributes
|
||||||
|
.find((a: any): a is GramJs.DocumentAttributeVideo => a instanceof GramJs.DocumentAttributeVideo);
|
||||||
|
|
||||||
|
if (!videoAttr) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gifAttr = attributes
|
||||||
|
.find((a: any): a is GramJs.DocumentAttributeAnimated => a instanceof GramJs.DocumentAttributeAnimated);
|
||||||
|
|
||||||
|
const {
|
||||||
|
duration,
|
||||||
|
w: width,
|
||||||
|
h: height,
|
||||||
|
supportsStreaming = false,
|
||||||
|
roundMessage: isRound = false,
|
||||||
|
nosound,
|
||||||
|
} = videoAttr;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: String(id),
|
||||||
|
mimeType,
|
||||||
|
duration,
|
||||||
|
fileName: getFilenameFromDocument(document, 'video'),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
supportsStreaming,
|
||||||
|
isRound,
|
||||||
|
isGif: Boolean(gifAttr),
|
||||||
|
thumbnail: buildApiThumbnailFromStripped(thumbs),
|
||||||
|
size: size.toJSNumber(),
|
||||||
|
isSpoiler,
|
||||||
|
...(nosound && { noSound: true }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildVideo(media: GramJs.TypeMessageMedia): ApiVideo | undefined {
|
||||||
|
if (
|
||||||
|
!(media instanceof GramJs.MessageMediaDocument)
|
||||||
|
|| !(media.document instanceof GramJs.Document)
|
||||||
|
|| !media.document.mimeType.startsWith('video')
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if (
|
||||||
|
!(media instanceof GramJs.MessageMediaDocument)
|
||||||
|
|| !media.document
|
||||||
|
|| !(media.document instanceof GramJs.Document)
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioAttribute = media.document.attributes
|
||||||
|
.find((attr: any): attr is GramJs.DocumentAttributeAudio => (
|
||||||
|
attr instanceof GramJs.DocumentAttributeAudio
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!audioAttribute || audioAttribute.voice) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const thumbnailSizes = media.document.thumbs && media.document.thumbs
|
||||||
|
.filter((thumb): thumb is GramJs.PhotoSize => thumb instanceof GramJs.PhotoSize)
|
||||||
|
.map((thumb) => buildApiPhotoSize(thumb));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: String(media.document.id),
|
||||||
|
fileName: getFilenameFromDocument(media.document, 'audio'),
|
||||||
|
thumbnailSizes,
|
||||||
|
size: media.document.size.toJSNumber(),
|
||||||
|
...pick(media.document, ['mimeType']),
|
||||||
|
...pick(audioAttribute, ['duration', 'performer', 'title']),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildVoice(media: GramJs.TypeMessageMedia): ApiVoice | undefined {
|
||||||
|
if (
|
||||||
|
!(media instanceof GramJs.MessageMediaDocument)
|
||||||
|
|| !media.document
|
||||||
|
|| !(media.document instanceof GramJs.Document)
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioAttribute = media.document.attributes
|
||||||
|
.find((attr: any): attr is GramJs.DocumentAttributeAudio => (
|
||||||
|
attr instanceof GramJs.DocumentAttributeAudio
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!audioAttribute || !audioAttribute.voice) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { duration, waveform } = audioAttribute;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: String(media.document.id),
|
||||||
|
duration,
|
||||||
|
waveform: waveform ? Array.from(waveform) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDocumentFromMedia(media: GramJs.TypeMessageMedia) {
|
||||||
|
if (!(media instanceof GramJs.MessageMediaDocument) || !media.document) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildApiDocument(media.document);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildApiDocument(document: GramJs.TypeDocument): ApiDocument | undefined {
|
||||||
|
if (!(document instanceof GramJs.Document)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
id, size, mimeType, date, thumbs, attributes,
|
||||||
|
} = document;
|
||||||
|
|
||||||
|
const photoSize = thumbs && thumbs.find((s: any): s is GramJs.PhotoSize => s instanceof GramJs.PhotoSize);
|
||||||
|
let thumbnail = thumbs && buildApiThumbnailFromStripped(thumbs);
|
||||||
|
if (!thumbnail && thumbs && photoSize) {
|
||||||
|
const photoPath = thumbs.find((s: any): s is GramJs.PhotoPathSize => s instanceof GramJs.PhotoPathSize);
|
||||||
|
if (photoPath) {
|
||||||
|
thumbnail = buildApiThumbnailFromPath(photoPath, photoSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mediaType: ApiDocument['mediaType'] | undefined;
|
||||||
|
let mediaSize: ApiDocument['mediaSize'] | undefined;
|
||||||
|
if (photoSize) {
|
||||||
|
mediaSize = {
|
||||||
|
width: photoSize.w,
|
||||||
|
height: photoSize.h,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (SUPPORTED_IMAGE_CONTENT_TYPES.has(mimeType)) {
|
||||||
|
mediaType = 'photo';
|
||||||
|
|
||||||
|
const imageAttribute = attributes
|
||||||
|
.find((a: any): a is GramJs.DocumentAttributeImageSize => a instanceof GramJs.DocumentAttributeImageSize);
|
||||||
|
|
||||||
|
if (imageAttribute) {
|
||||||
|
const { w: width, h: height } = imageAttribute;
|
||||||
|
mediaSize = {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (SUPPORTED_VIDEO_CONTENT_TYPES.has(mimeType)) {
|
||||||
|
mediaType = 'video';
|
||||||
|
const videoAttribute = attributes
|
||||||
|
.find((a: any): a is GramJs.DocumentAttributeVideo => a instanceof GramJs.DocumentAttributeVideo);
|
||||||
|
|
||||||
|
if (videoAttribute) {
|
||||||
|
const { w: width, h: height } = videoAttribute;
|
||||||
|
mediaSize = {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: String(id),
|
||||||
|
size: size.toJSNumber(),
|
||||||
|
mimeType,
|
||||||
|
timestamp: date,
|
||||||
|
fileName: getFilenameFromDocument(document),
|
||||||
|
thumbnail,
|
||||||
|
mediaType,
|
||||||
|
mediaSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildContact(media: GramJs.TypeMessageMedia): ApiContact | undefined {
|
||||||
|
if (!(media instanceof GramJs.MessageMediaContact)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
firstName, lastName, phoneNumber, userId,
|
||||||
|
} = media;
|
||||||
|
|
||||||
|
return {
|
||||||
|
firstName, lastName, phoneNumber, userId: buildApiPeerId(userId, 'user'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPollFromMedia(media: GramJs.TypeMessageMedia): ApiPoll | undefined {
|
||||||
|
if (!(media instanceof GramJs.MessageMediaPoll)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildPoll(media.poll, media.results);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInvoiceFromMedia(media: GramJs.TypeMessageMedia): ApiInvoice | undefined {
|
||||||
|
if (!(media instanceof GramJs.MessageMediaInvoice)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildInvoice(media);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLocationFromMedia(media: GramJs.TypeMessageMedia): ApiLocation | undefined {
|
||||||
|
if (media instanceof GramJs.MessageMediaGeo) {
|
||||||
|
return buildGeo(media);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media instanceof GramJs.MessageMediaVenue) {
|
||||||
|
return buildVenue(media);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media instanceof GramJs.MessageMediaGeoLive) {
|
||||||
|
return buildGeoLive(media);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGeo(media: GramJs.MessageMediaGeo): ApiLocation | undefined {
|
||||||
|
const point = buildGeoPoint(media.geo);
|
||||||
|
return point && { type: 'geo', geo: point };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildVenue(media: GramJs.MessageMediaVenue): ApiLocation | undefined {
|
||||||
|
const {
|
||||||
|
geo, title, provider, address, venueId, venueType,
|
||||||
|
} = media;
|
||||||
|
const point = buildGeoPoint(geo);
|
||||||
|
return point && {
|
||||||
|
type: 'venue',
|
||||||
|
geo: point,
|
||||||
|
title,
|
||||||
|
provider,
|
||||||
|
address,
|
||||||
|
venueId,
|
||||||
|
venueType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGeoLive(media: GramJs.MessageMediaGeoLive): ApiLocation | undefined {
|
||||||
|
const { geo, period, heading } = media;
|
||||||
|
const point = buildGeoPoint(geo);
|
||||||
|
return point && {
|
||||||
|
type: 'geoLive',
|
||||||
|
geo: point,
|
||||||
|
period,
|
||||||
|
heading,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildGeoPoint(geo: GramJs.TypeGeoPoint): ApiLocation['geo'] | undefined {
|
||||||
|
if (geo instanceof GramJs.GeoPointEmpty) return undefined;
|
||||||
|
const {
|
||||||
|
long, lat, accuracyRadius, accessHash,
|
||||||
|
} = geo;
|
||||||
|
return {
|
||||||
|
long,
|
||||||
|
lat,
|
||||||
|
accessHash: accessHash.toString(),
|
||||||
|
accuracyRadius,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGameFromMedia(media: GramJs.TypeMessageMedia): ApiGame | undefined {
|
||||||
|
if (!(media instanceof GramJs.MessageMediaGame)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildGame(media);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGame(media: GramJs.MessageMediaGame): ApiGame | undefined {
|
||||||
|
const {
|
||||||
|
id, accessHash, shortName, title, description, photo: apiPhoto, document: apiDocument,
|
||||||
|
} = media.game;
|
||||||
|
|
||||||
|
const photo = apiPhoto instanceof GramJs.Photo ? buildApiPhoto(apiPhoto) : undefined;
|
||||||
|
const document = apiDocument instanceof GramJs.Document ? buildApiDocument(apiDocument) : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id.toString(),
|
||||||
|
accessHash: accessHash.toString(),
|
||||||
|
shortName,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
photo,
|
||||||
|
document,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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 }) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPoll(poll: GramJs.Poll, pollResults: GramJs.PollResults): ApiPoll {
|
||||||
|
const { id, answers: rawAnswers } = poll;
|
||||||
|
const answers = rawAnswers.map((answer) => ({
|
||||||
|
text: answer.text,
|
||||||
|
option: serializeBytes(answer.option),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: String(id),
|
||||||
|
summary: {
|
||||||
|
isPublic: poll.publicVoters,
|
||||||
|
...pick(poll, [
|
||||||
|
'closed',
|
||||||
|
'multipleChoice',
|
||||||
|
'quiz',
|
||||||
|
'question',
|
||||||
|
'closePeriod',
|
||||||
|
'closeDate',
|
||||||
|
]),
|
||||||
|
answers,
|
||||||
|
},
|
||||||
|
results: buildPollResults(pollResults),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildInvoice(media: GramJs.MessageMediaInvoice): ApiInvoice {
|
||||||
|
const {
|
||||||
|
description: text, title, photo, test, totalAmount, currency, receiptMsgId, extendedMedia,
|
||||||
|
} = media;
|
||||||
|
|
||||||
|
const preview = extendedMedia instanceof GramJs.MessageExtendedMediaPreview
|
||||||
|
? buildApiMessageExtendedMediaPreview(extendedMedia) : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
photo: buildApiWebDocument(photo),
|
||||||
|
receiptMsgId,
|
||||||
|
amount: Number(totalAmount),
|
||||||
|
currency,
|
||||||
|
isTest: test,
|
||||||
|
extendedMedia: preview,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPollResults(pollResults: GramJs.PollResults): ApiPoll['results'] {
|
||||||
|
const {
|
||||||
|
results: rawResults, totalVoters, recentVoters, solution, solutionEntities: entities, min,
|
||||||
|
} = pollResults;
|
||||||
|
const results = rawResults?.map(({
|
||||||
|
option, chosen, correct, voters,
|
||||||
|
}) => ({
|
||||||
|
isChosen: chosen,
|
||||||
|
isCorrect: correct,
|
||||||
|
option: serializeBytes(option),
|
||||||
|
votersCount: voters,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
isMin: min,
|
||||||
|
totalVoters,
|
||||||
|
recentVoterIds: recentVoters?.map((peer) => getApiChatIdFromMtpPeer(peer)),
|
||||||
|
results,
|
||||||
|
solution,
|
||||||
|
...(entities && { solutionEntities: entities.map(buildApiMessageEntity) }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undefined {
|
||||||
|
if (
|
||||||
|
!(media instanceof GramJs.MessageMediaWebPage)
|
||||||
|
|| !(media.webpage instanceof GramJs.WebPage)
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
id, photo, document, attributes,
|
||||||
|
} = media.webpage;
|
||||||
|
|
||||||
|
let video;
|
||||||
|
if (document instanceof GramJs.Document && document.mimeType.startsWith('video/')) {
|
||||||
|
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 {
|
||||||
|
id: Number(id),
|
||||||
|
...pick(media.webpage, [
|
||||||
|
'url',
|
||||||
|
'displayUrl',
|
||||||
|
'type',
|
||||||
|
'siteName',
|
||||||
|
'title',
|
||||||
|
'description',
|
||||||
|
'duration',
|
||||||
|
]),
|
||||||
|
photo: photo instanceof GramJs.Photo ? buildApiPhoto(photo) : undefined,
|
||||||
|
document: !video && document ? buildApiDocument(document) : undefined,
|
||||||
|
video,
|
||||||
|
story,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilenameFromDocument(document: GramJs.Document, defaultBase = 'file') {
|
||||||
|
const { mimeType, attributes } = document;
|
||||||
|
const filenameAttribute = attributes
|
||||||
|
.find((a: any): a is GramJs.DocumentAttributeFilename => a instanceof GramJs.DocumentAttributeFilename);
|
||||||
|
|
||||||
|
if (filenameAttribute) {
|
||||||
|
return filenameAttribute.fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = mimeType.split('/')[1];
|
||||||
|
|
||||||
|
return `${defaultBase}${String(document.id)}.${extension}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildApiMessageExtendedMediaPreview(
|
||||||
|
preview: GramJs.MessageExtendedMediaPreview,
|
||||||
|
): ApiMessageExtendedMediaPreview {
|
||||||
|
const {
|
||||||
|
w, h, thumb, videoDuration,
|
||||||
|
} = preview;
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
duration: videoDuration,
|
||||||
|
thumbnail: thumb ? buildApiThumbnailFromStripped([thumb]) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildApiWebDocument(document?: GramJs.TypeWebDocument): ApiWebDocument | undefined {
|
||||||
|
if (!document) return undefined;
|
||||||
|
|
||||||
|
const {
|
||||||
|
url, size, mimeType,
|
||||||
|
} = document;
|
||||||
|
const accessHash = document instanceof GramJs.WebDocument ? document.accessHash.toString() : undefined;
|
||||||
|
const sizeAttr = document.attributes.find((attr): attr is GramJs.DocumentAttributeImageSize => (
|
||||||
|
attr instanceof GramJs.DocumentAttributeImageSize
|
||||||
|
));
|
||||||
|
const dimensions = sizeAttr && { width: sizeAttr.w, height: sizeAttr.h };
|
||||||
|
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
accessHash,
|
||||||
|
size,
|
||||||
|
mimeType,
|
||||||
|
dimensions,
|
||||||
|
};
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,8 @@ import type {
|
|||||||
} from '../../types';
|
} from '../../types';
|
||||||
import type { ApiPrivacyKey } from '../../../types';
|
import type { ApiPrivacyKey } from '../../../types';
|
||||||
|
|
||||||
import { buildApiDocument, buildApiReaction } from './messages';
|
import { buildApiReaction } from './reactions';
|
||||||
|
import { buildApiDocument } from './messageContent';
|
||||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
|
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
|
||||||
import { omit, pick } from '../../../util/iteratees';
|
import { omit, pick } from '../../../util/iteratees';
|
||||||
import { getServerTime } from '../../../util/serverTime';
|
import { getServerTime } from '../../../util/serverTime';
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import type {
|
|||||||
ApiPaymentForm, ApiReceipt, ApiLabeledPrice, ApiPaymentCredentials,
|
ApiPaymentForm, ApiReceipt, ApiLabeledPrice, ApiPaymentCredentials,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
|
||||||
import { buildApiDocument, buildApiMessageEntity, buildApiWebDocument } from './messages';
|
import { buildApiDocument, buildApiWebDocument } from './messageContent';
|
||||||
|
import { buildApiMessageEntity } from './common';
|
||||||
import { omitVirtualClassFields } from './helpers';
|
import { omitVirtualClassFields } from './helpers';
|
||||||
|
|
||||||
export function buildShippingOptions(shippingOptions: GramJs.ShippingOption[] | undefined) {
|
export function buildShippingOptions(shippingOptions: GramJs.ShippingOption[] | undefined) {
|
||||||
|
|||||||
103
src/api/gramjs/apiBuilders/reactions.ts
Normal file
103
src/api/gramjs/apiBuilders/reactions.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
|
import type {
|
||||||
|
ApiAvailableReaction,
|
||||||
|
ApiPeerReaction,
|
||||||
|
ApiReaction,
|
||||||
|
ApiReactionCount,
|
||||||
|
ApiReactionEmoji,
|
||||||
|
ApiReactions,
|
||||||
|
} from '../../types';
|
||||||
|
import { buildApiDocument } from './messageContent';
|
||||||
|
import { getApiChatIdFromMtpPeer } from './peers';
|
||||||
|
|
||||||
|
export function buildMessageReactions(reactions: GramJs.MessageReactions): ApiReactions {
|
||||||
|
const {
|
||||||
|
recentReactions, results, canSeeList,
|
||||||
|
} = reactions;
|
||||||
|
|
||||||
|
return {
|
||||||
|
canSeeList,
|
||||||
|
results: results.map(buildReactionCount).filter(Boolean).sort(reactionCountComparator),
|
||||||
|
recentReactions: recentReactions?.map(buildMessagePeerReaction).filter(Boolean),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function reactionCountComparator(a: ApiReactionCount, b: ApiReactionCount) {
|
||||||
|
const diff = b.count - a.count;
|
||||||
|
if (diff) return diff;
|
||||||
|
if (a.chosenOrder !== undefined && b.chosenOrder !== undefined) {
|
||||||
|
return a.chosenOrder - b.chosenOrder;
|
||||||
|
}
|
||||||
|
if (a.chosenOrder !== undefined) return 1;
|
||||||
|
if (b.chosenOrder !== undefined) return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildReactionCount(reactionCount: GramJs.ReactionCount): ApiReactionCount | undefined {
|
||||||
|
const { chosenOrder, count, reaction } = reactionCount;
|
||||||
|
|
||||||
|
const apiReaction = buildApiReaction(reaction);
|
||||||
|
if (!apiReaction) return undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
chosenOrder,
|
||||||
|
count,
|
||||||
|
reaction: apiReaction,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMessagePeerReaction(userReaction: GramJs.MessagePeerReaction): ApiPeerReaction | undefined {
|
||||||
|
const {
|
||||||
|
peerId, reaction, big, unread, date, my,
|
||||||
|
} = userReaction;
|
||||||
|
|
||||||
|
const apiReaction = buildApiReaction(reaction);
|
||||||
|
if (!apiReaction) return undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
peerId: getApiChatIdFromMtpPeer(peerId),
|
||||||
|
reaction: apiReaction,
|
||||||
|
addedDate: date,
|
||||||
|
isUnread: unread,
|
||||||
|
isBig: big,
|
||||||
|
isOwn: my,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildApiReaction(reaction: GramJs.TypeReaction): ApiReaction | undefined {
|
||||||
|
if (reaction instanceof GramJs.ReactionEmoji) {
|
||||||
|
return {
|
||||||
|
emoticon: reaction.emoticon,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reaction instanceof GramJs.ReactionCustomEmoji) {
|
||||||
|
return {
|
||||||
|
documentId: reaction.documentId.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildApiAvailableReaction(availableReaction: GramJs.AvailableReaction): ApiAvailableReaction {
|
||||||
|
const {
|
||||||
|
selectAnimation, staticIcon, reaction, title, appearAnimation,
|
||||||
|
inactive, aroundAnimation, centerIcon, effectAnimation, activateAnimation,
|
||||||
|
premium,
|
||||||
|
} = availableReaction;
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectAnimation: buildApiDocument(selectAnimation),
|
||||||
|
appearAnimation: buildApiDocument(appearAnimation),
|
||||||
|
activateAnimation: buildApiDocument(activateAnimation),
|
||||||
|
effectAnimation: buildApiDocument(effectAnimation),
|
||||||
|
staticIcon: buildApiDocument(staticIcon),
|
||||||
|
aroundAnimation: aroundAnimation ? buildApiDocument(aroundAnimation) : undefined,
|
||||||
|
centerIcon: centerIcon ? buildApiDocument(centerIcon) : undefined,
|
||||||
|
reaction: { emoticon: reaction } as ApiReactionEmoji,
|
||||||
|
title,
|
||||||
|
isInactive: inactive,
|
||||||
|
isPremium: premium,
|
||||||
|
};
|
||||||
|
}
|
||||||
144
src/api/gramjs/apiBuilders/stories.ts
Normal file
144
src/api/gramjs/apiBuilders/stories.ts
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiMediaArea, ApiMediaAreaCoordinates, ApiMessage, ApiStealthMode, ApiStoryView, ApiTypeStory,
|
||||||
|
} from '../../types';
|
||||||
|
|
||||||
|
import { buildCollectionByCallback } from '../../../util/iteratees';
|
||||||
|
import { buildApiPeerId } from './peers';
|
||||||
|
import { buildPrivacyRules } from './common';
|
||||||
|
import { buildGeoPoint, buildMessageMediaContent, buildMessageTextContent } from './messageContent';
|
||||||
|
import { buildApiReaction } from './reactions';
|
||||||
|
|
||||||
|
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,
|
||||||
|
mediaAreas, sentReaction,
|
||||||
|
} = 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?.reactionsCount && { reactionsCount: views.reactionsCount }),
|
||||||
|
...(views?.recentViewers && {
|
||||||
|
recentViewerIds: views.recentViewers.map((viewerId) => buildApiPeerId(viewerId, 'user')),
|
||||||
|
}),
|
||||||
|
...(privacy && { visibility: buildPrivacyRules(privacy) }),
|
||||||
|
...(mediaAreas && { mediaAreas: mediaAreas.map(buildApiMediaArea).filter(Boolean) }),
|
||||||
|
...(sentReaction && { sentReaction: buildApiReaction(sentReaction) }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildApiStoryView(view: GramJs.TypeStoryView): ApiStoryView {
|
||||||
|
const {
|
||||||
|
userId, date, reaction, blockedMyStoriesFrom, blocked,
|
||||||
|
} = view;
|
||||||
|
return {
|
||||||
|
userId: userId.toString(),
|
||||||
|
date,
|
||||||
|
...(reaction && { reaction: buildApiReaction(reaction) }),
|
||||||
|
areStoriesBlocked: blocked || blockedMyStoriesFrom,
|
||||||
|
isUserBlocked: blocked,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildApiStealthMode(stealthMode: GramJs.TypeStoriesStealthMode): ApiStealthMode {
|
||||||
|
return {
|
||||||
|
activeUntil: stealthMode.activeUntilDate,
|
||||||
|
cooldownUntil: stealthMode.cooldownUntilDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildApiMediaAreaCoordinates(coordinates: GramJs.TypeMediaAreaCoordinates): ApiMediaAreaCoordinates {
|
||||||
|
const {
|
||||||
|
x, y, w, h, rotation,
|
||||||
|
} = coordinates;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
rotation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildApiMediaArea(area: GramJs.TypeMediaArea): ApiMediaArea | undefined {
|
||||||
|
if (area instanceof GramJs.MediaAreaVenue) {
|
||||||
|
const { geo, title, coordinates } = area;
|
||||||
|
const point = buildGeoPoint(geo);
|
||||||
|
|
||||||
|
if (!point) return undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'venue',
|
||||||
|
coordinates: buildApiMediaAreaCoordinates(coordinates),
|
||||||
|
geo: point,
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (area instanceof GramJs.MediaAreaGeoPoint) {
|
||||||
|
const { geo, coordinates } = area;
|
||||||
|
const point = buildGeoPoint(geo);
|
||||||
|
|
||||||
|
if (!point) return undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'geoPoint',
|
||||||
|
coordinates: buildApiMediaAreaCoordinates(coordinates),
|
||||||
|
geo: point,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildApiUsersStories(userStories: GramJs.UserStories) {
|
||||||
|
const userId = buildApiPeerId(userStories.userId, 'user');
|
||||||
|
|
||||||
|
return buildCollectionByCallback(userStories.stories, (story) => [story.id, buildApiStory(userId, story)]);
|
||||||
|
}
|
||||||
@ -667,11 +667,14 @@ export function buildInputPrivacyRules(
|
|||||||
|
|
||||||
switch (visibility) {
|
switch (visibility) {
|
||||||
case 'everybody':
|
case 'everybody':
|
||||||
rules.push(new GramJs.InputPrivacyValueAllowAll());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'contacts': {
|
case 'contacts': {
|
||||||
rules.push(new GramJs.InputPrivacyValueAllowContacts());
|
if (visibility === 'contacts') {
|
||||||
|
rules.push(new GramJs.InputPrivacyValueAllowContacts());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visibility === 'everybody') {
|
||||||
|
rules.push(new GramJs.InputPrivacyValueAllowAll());
|
||||||
|
}
|
||||||
|
|
||||||
const users = deniedUserList?.reduce<GramJs.InputUser[]>((acc, { id, accessHash }) => {
|
const users = deniedUserList?.reduce<GramJs.InputUser[]>((acc, { id, accessHash }) => {
|
||||||
acc.push(new GramJs.InputPeerUser({
|
acc.push(new GramJs.InputPeerUser({
|
||||||
|
|||||||
@ -58,7 +58,7 @@ export {
|
|||||||
} from './management';
|
} from './management';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
updateProfile, checkUsername, updateUsername, fetchBlockedContacts, blockContact, unblockContact,
|
updateProfile, checkUsername, updateUsername, fetchBlockedUsers, blockUser, unblockUser,
|
||||||
updateProfilePhoto, uploadProfilePhoto, deleteProfilePhotos, fetchWallpapers, uploadWallpaper,
|
updateProfilePhoto, uploadProfilePhoto, deleteProfilePhotos, fetchWallpapers, uploadWallpaper,
|
||||||
fetchAuthorizations, terminateAuthorization, terminateAllAuthorizations,
|
fetchAuthorizations, terminateAuthorization, terminateAllAuthorizations,
|
||||||
fetchWebAuthorizations, terminateWebAuthorization, terminateAllWebAuthorizations,
|
fetchWebAuthorizations, terminateWebAuthorization, terminateAllWebAuthorizations,
|
||||||
@ -109,8 +109,4 @@ export {
|
|||||||
broadcastLocalDbUpdateFull,
|
broadcastLocalDbUpdateFull,
|
||||||
} from '../localDb';
|
} from '../localDb';
|
||||||
|
|
||||||
export {
|
export * from './stories';
|
||||||
fetchAllStories, fetchUserStories, fetchUserPinnedStories, fetchUserStoriesByIds, viewStory, markStoryRead,
|
|
||||||
deleteStory, toggleStoryPinned, fetchStorySeenBy, fetchStoryLink, fetchStoriesArchive, reportStory, editStoryPrivacy,
|
|
||||||
toggleStoriesHidden, fetchStoriesMaxIds,
|
|
||||||
} from './stories';
|
|
||||||
|
|||||||
@ -20,6 +20,8 @@ const MEDIA_ENTITY_TYPES = new Set([
|
|||||||
'msg', 'sticker', 'gif', 'wallpaper', 'photo', 'webDocument', 'document', 'videoAvatar',
|
'msg', 'sticker', 'gif', 'wallpaper', 'photo', 'webDocument', 'document', 'videoAvatar',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const JPEG_SIZE_TYPES = new Set(['s', 'm', 'x', 'y', 'w', 'a', 'b', 'c', 'd']);
|
||||||
|
|
||||||
export default async function downloadMedia(
|
export default async function downloadMedia(
|
||||||
{
|
{
|
||||||
url, mediaFormat, start, end, isHtmlAllowed,
|
url, mediaFormat, start, end, isHtmlAllowed,
|
||||||
@ -178,7 +180,11 @@ async function download(
|
|||||||
mimeType = (entity as GramJs.TypeWebDocument).mimeType;
|
mimeType = (entity as GramJs.TypeWebDocument).mimeType;
|
||||||
fullSize = (entity as GramJs.TypeWebDocument).size;
|
fullSize = (entity as GramJs.TypeWebDocument).size;
|
||||||
} else {
|
} else {
|
||||||
mimeType = (entity as GramJs.Document).mimeType;
|
if (JPEG_SIZE_TYPES.has(sizeType || '')) {
|
||||||
|
mimeType = 'image/jpeg';
|
||||||
|
} else {
|
||||||
|
mimeType = (entity as GramJs.Document).mimeType;
|
||||||
|
}
|
||||||
fullSize = (entity as GramJs.Document).size.toJSNumber();
|
fullSize = (entity as GramJs.Document).size.toJSNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -38,12 +38,12 @@ import {
|
|||||||
buildApiMessage,
|
buildApiMessage,
|
||||||
buildLocalForwardedMessage,
|
buildLocalForwardedMessage,
|
||||||
buildLocalMessage,
|
buildLocalMessage,
|
||||||
buildWebPage,
|
|
||||||
buildApiSponsoredMessage,
|
buildApiSponsoredMessage,
|
||||||
buildApiFormattedText,
|
|
||||||
buildMessageTextContent,
|
|
||||||
buildMessageMediaContent,
|
|
||||||
} from '../apiBuilders/messages';
|
} from '../apiBuilders/messages';
|
||||||
|
import {
|
||||||
|
buildMessageMediaContent, buildMessageTextContent, buildWebPage,
|
||||||
|
} from '../apiBuilders/messageContent';
|
||||||
|
import { buildApiFormattedText } from '../apiBuilders/common';
|
||||||
import { buildApiUser } from '../apiBuilders/users';
|
import { buildApiUser } from '../apiBuilders/users';
|
||||||
import {
|
import {
|
||||||
buildInputEntity,
|
buildInputEntity,
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import type { ApiChat, ApiReaction } from '../../types';
|
|||||||
import { REACTION_LIST_LIMIT, RECENT_REACTIONS_LIMIT, TOP_REACTIONS_LIMIT } from '../../../config';
|
import { REACTION_LIST_LIMIT, RECENT_REACTIONS_LIMIT, TOP_REACTIONS_LIMIT } from '../../../config';
|
||||||
import { buildInputPeer, buildInputReaction } from '../gramjsBuilders';
|
import { buildInputPeer, buildInputReaction } from '../gramjsBuilders';
|
||||||
import { buildApiUser } from '../apiBuilders/users';
|
import { buildApiUser } from '../apiBuilders/users';
|
||||||
import { buildApiAvailableReaction, buildApiReaction, buildMessagePeerReaction } from '../apiBuilders/messages';
|
import { buildApiAvailableReaction, buildApiReaction, buildMessagePeerReaction } from '../apiBuilders/reactions';
|
||||||
import { invokeRequest } from './client';
|
import { invokeRequest } from './client';
|
||||||
import localDb from '../localDb';
|
import localDb from '../localDb';
|
||||||
import { addEntitiesToLocalDb } from '../helpers';
|
import { addEntitiesToLocalDb } from '../helpers';
|
||||||
|
|||||||
@ -23,8 +23,7 @@ import {
|
|||||||
buildApiWallpaper,
|
buildApiWallpaper,
|
||||||
buildApiWebSession, buildLangPack, buildLangPackString,
|
buildApiWebSession, buildLangPack, buildLangPackString,
|
||||||
} from '../apiBuilders/misc';
|
} from '../apiBuilders/misc';
|
||||||
import { buildPrivacyRules } from '../apiBuilders/messages';
|
import { buildApiPhoto, buildPrivacyRules } 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';
|
||||||
import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
|
import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
|
||||||
@ -224,8 +223,13 @@ export async function uploadWallpaper(file: File) {
|
|||||||
return { wallpaper };
|
return { wallpaper };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchBlockedContacts() {
|
export async function fetchBlockedUsers({
|
||||||
|
isOnlyStories,
|
||||||
|
}: {
|
||||||
|
isOnlyStories?: true;
|
||||||
|
}) {
|
||||||
const result = await invokeRequest(new GramJs.contacts.GetBlocked({
|
const result = await invokeRequest(new GramJs.contacts.GetBlocked({
|
||||||
|
myStoriesFrom: isOnlyStories,
|
||||||
limit: BLOCKED_LIST_LIMIT,
|
limit: BLOCKED_LIST_LIMIT,
|
||||||
}));
|
}));
|
||||||
if (!result) {
|
if (!result) {
|
||||||
@ -242,15 +246,29 @@ export async function fetchBlockedContacts() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function blockContact(chatOrUserId: string, accessHash?: string) {
|
export function blockUser({
|
||||||
|
user,
|
||||||
|
isOnlyStories,
|
||||||
|
} : {
|
||||||
|
user: ApiUser;
|
||||||
|
isOnlyStories?: true;
|
||||||
|
}) {
|
||||||
return invokeRequest(new GramJs.contacts.Block({
|
return invokeRequest(new GramJs.contacts.Block({
|
||||||
id: buildInputPeer(chatOrUserId, accessHash),
|
id: buildInputPeer(user.id, user.accessHash),
|
||||||
|
myStoriesFrom: isOnlyStories,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unblockContact(chatOrUserId: string, accessHash?: string) {
|
export function unblockUser({
|
||||||
|
user,
|
||||||
|
isOnlyStories,
|
||||||
|
} : {
|
||||||
|
user: ApiUser;
|
||||||
|
isOnlyStories?: true;
|
||||||
|
}) {
|
||||||
return invokeRequest(new GramJs.contacts.Unblock({
|
return invokeRequest(new GramJs.contacts.Unblock({
|
||||||
id: buildInputPeer(chatOrUserId, accessHash),
|
id: buildInputPeer(user.id, user.accessHash),
|
||||||
|
myStoriesFrom: isOnlyStories,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,21 @@
|
|||||||
import BigInt from 'big-integer';
|
|
||||||
import { invokeRequest } from './client';
|
import { invokeRequest } from './client';
|
||||||
import type {
|
import type {
|
||||||
ApiUser, ApiUserStories, ApiReportReason, ApiTypeStory,
|
ApiUser, ApiUserStories, ApiReportReason, ApiTypeStory, ApiReaction, ApiStealthMode,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import type { PrivacyVisibility } from '../../../types';
|
import type { PrivacyVisibility } from '../../../types';
|
||||||
import { Api as GramJs } from '../../../lib/gramjs';
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
import { addEntitiesToLocalDb, addStoryToLocalDb } from '../helpers';
|
import { addEntitiesToLocalDb, addStoryToLocalDb } from '../helpers';
|
||||||
import { buildApiUser } from '../apiBuilders/users';
|
import { buildApiUser } from '../apiBuilders/users';
|
||||||
import { buildApiStory, buildApiUsersStories } from '../apiBuilders/messages';
|
|
||||||
import { buildApiPeerId } from '../apiBuilders/peers';
|
import { buildApiPeerId } from '../apiBuilders/peers';
|
||||||
import {
|
import {
|
||||||
|
buildApiStoryView, buildApiStory, buildApiUsersStories, buildApiStealthMode,
|
||||||
|
} from '../apiBuilders/stories';
|
||||||
|
import {
|
||||||
|
buildInputEntity,
|
||||||
buildInputPeer,
|
buildInputPeer,
|
||||||
buildInputPeerFromLocalDb,
|
buildInputPeerFromLocalDb,
|
||||||
buildInputPrivacyRules,
|
buildInputPrivacyRules,
|
||||||
|
buildInputReaction,
|
||||||
buildInputReportReason,
|
buildInputReportReason,
|
||||||
} from '../gramjsBuilders';
|
} from '../gramjsBuilders';
|
||||||
import { STORY_LIST_LIMIT } from '../../../config';
|
import { STORY_LIST_LIMIT } from '../../../config';
|
||||||
@ -28,8 +31,14 @@ export async function fetchAllStories({
|
|||||||
isHidden?: boolean;
|
isHidden?: boolean;
|
||||||
}): Promise<
|
}): Promise<
|
||||||
undefined
|
undefined
|
||||||
| { state: string }
|
| { state: string; stealthMode: ApiStealthMode }
|
||||||
| { users: ApiUser[]; userStories: Record<string, ApiUserStories>; hasMore?: true; state: string }> {
|
| {
|
||||||
|
users: ApiUser[];
|
||||||
|
userStories: Record<string, ApiUserStories>;
|
||||||
|
hasMore?: true;
|
||||||
|
state: string;
|
||||||
|
stealthMode: ApiStealthMode;
|
||||||
|
}> {
|
||||||
const params: ConstructorParameters<typeof GramJs.stories.GetAllStories>[0] = isFirstRequest
|
const params: ConstructorParameters<typeof GramJs.stories.GetAllStories>[0] = isFirstRequest
|
||||||
? (isHidden ? { hidden: true } : {})
|
? (isHidden ? { hidden: true } : {})
|
||||||
: { state: stateHash, next: true, ...(isHidden && { hidden: true }) };
|
: { state: stateHash, next: true, ...(isHidden && { hidden: true }) };
|
||||||
@ -42,6 +51,7 @@ export async function fetchAllStories({
|
|||||||
if (result instanceof GramJs.stories.AllStoriesNotModified) {
|
if (result instanceof GramJs.stories.AllStoriesNotModified) {
|
||||||
return {
|
return {
|
||||||
state: result.state,
|
state: result.state,
|
||||||
|
stealthMode: buildApiStealthMode(result.stealthMode),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,6 +105,7 @@ export async function fetchAllStories({
|
|||||||
userStories: allUserStories,
|
userStories: allUserStories,
|
||||||
hasMore: result.hasMore,
|
hasMore: result.hasMore,
|
||||||
state: result.state,
|
state: result.state,
|
||||||
|
stealthMode: buildApiStealthMode(result.stealthMode),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,19 +226,28 @@ export function toggleStoryPinned({ storyId, isPinned }: { storyId: number; isPi
|
|||||||
return invokeRequest(new GramJs.stories.TogglePinned({ id: [storyId], pinned: isPinned }));
|
return invokeRequest(new GramJs.stories.TogglePinned({ id: [storyId], pinned: isPinned }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchStorySeenBy({
|
export async function fetchStoryViewList({
|
||||||
storyId, limit = STORY_LIST_LIMIT, offsetDate = 0, offsetId = 0,
|
storyId,
|
||||||
|
areJustContacts,
|
||||||
|
query,
|
||||||
|
areReactionsFirst,
|
||||||
|
limit = STORY_LIST_LIMIT,
|
||||||
|
offset = '',
|
||||||
}: {
|
}: {
|
||||||
storyId: number;
|
storyId: number;
|
||||||
|
areJustContacts?: true;
|
||||||
|
areReactionsFirst?: true;
|
||||||
|
query?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offsetDate?: number;
|
offset?: string;
|
||||||
offsetId?: number;
|
|
||||||
}) {
|
}) {
|
||||||
const result = await invokeRequest(new GramJs.stories.GetStoryViewsList({
|
const result = await invokeRequest(new GramJs.stories.GetStoryViewsList({
|
||||||
id: storyId,
|
id: storyId,
|
||||||
|
justContacts: areJustContacts,
|
||||||
|
q: query,
|
||||||
|
reactionsFirst: areReactionsFirst,
|
||||||
limit,
|
limit,
|
||||||
offsetDate,
|
offset,
|
||||||
offsetId: BigInt(offsetId),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
@ -236,13 +256,15 @@ export async function fetchStorySeenBy({
|
|||||||
|
|
||||||
addEntitiesToLocalDb(result.users);
|
addEntitiesToLocalDb(result.users);
|
||||||
const users = result.users.map(buildApiUser).filter(Boolean);
|
const users = result.users.map(buildApiUser).filter(Boolean);
|
||||||
const seenByDates = result.views.reduce<Record<string, number>>((acc, view) => {
|
const views = result.views.map(buildApiStoryView);
|
||||||
acc[buildApiPeerId(view.userId, 'user')] = view.date;
|
|
||||||
|
|
||||||
return acc;
|
return {
|
||||||
}, {});
|
users,
|
||||||
|
views,
|
||||||
return { users, seenByDates, count: result.count };
|
nextOffset: result.nextOffset,
|
||||||
|
reactionsCount: result.reactionsCount,
|
||||||
|
viewsCount: result.count,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchStoryLink({ userId, storyId }: { userId: string; storyId: number }) {
|
export async function fetchStoryLink({ userId, storyId }: { userId: string; storyId: number }) {
|
||||||
@ -341,3 +363,36 @@ async function fetchCommonStoriesRequest({ method, userId }: {
|
|||||||
stories,
|
stories,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sendStoryReaction({
|
||||||
|
user, storyId, reaction, shouldAddToRecent,
|
||||||
|
}: {
|
||||||
|
user: ApiUser;
|
||||||
|
storyId: number;
|
||||||
|
reaction?: ApiReaction;
|
||||||
|
shouldAddToRecent?: boolean;
|
||||||
|
}) {
|
||||||
|
return invokeRequest(new GramJs.stories.SendReaction({
|
||||||
|
reaction: reaction ? buildInputReaction(reaction) : new GramJs.ReactionEmpty(),
|
||||||
|
userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser,
|
||||||
|
storyId,
|
||||||
|
...(shouldAddToRecent && { addToRecent: true }),
|
||||||
|
}), {
|
||||||
|
shouldReturnTrue: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function activateStealthMode({
|
||||||
|
isForPast,
|
||||||
|
isForFuture,
|
||||||
|
}: {
|
||||||
|
isForPast?: true;
|
||||||
|
isForFuture?: true;
|
||||||
|
}) {
|
||||||
|
return invokeRequest(new GramJs.stories.ActivateStealthMode({
|
||||||
|
past: isForPast,
|
||||||
|
future: isForFuture,
|
||||||
|
}), {
|
||||||
|
shouldReturnTrue: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
} from '../apiBuilders/symbols';
|
} from '../apiBuilders/symbols';
|
||||||
import { buildApiUserEmojiStatus } from '../apiBuilders/users';
|
import { buildApiUserEmojiStatus } from '../apiBuilders/users';
|
||||||
import { buildInputStickerSet, buildInputDocument, buildInputStickerSetShortName } from '../gramjsBuilders';
|
import { buildInputStickerSet, buildInputDocument, buildInputStickerSetShortName } from '../gramjsBuilders';
|
||||||
import { buildVideoFromDocument } from '../apiBuilders/messages';
|
import { buildVideoFromDocument } from '../apiBuilders/messageContent';
|
||||||
import { DEFAULT_GIF_SEARCH_BOT_USERNAME, RECENT_STATUS_LIMIT, RECENT_STICKERS_LIMIT } from '../../../config';
|
import { DEFAULT_GIF_SEARCH_BOT_USERNAME, RECENT_STATUS_LIMIT, RECENT_STICKERS_LIMIT } from '../../../config';
|
||||||
|
|
||||||
import localDb from '../localDb';
|
import localDb from '../localDb';
|
||||||
|
|||||||
@ -5,23 +5,33 @@ import type {
|
|||||||
ApiStory, ApiStorySkipped,
|
ApiStory, ApiStorySkipped,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
|
import localDb from './localDb';
|
||||||
import { DEBUG, GENERAL_TOPIC_ID } from '../../config';
|
import { DEBUG, GENERAL_TOPIC_ID } from '../../config';
|
||||||
import { omit, pick } from '../../util/iteratees';
|
import { omit, pick } from '../../util/iteratees';
|
||||||
import { getServerTimeOffset, setServerTimeOffset } from '../../util/serverTime';
|
import { getServerTimeOffset, setServerTimeOffset } from '../../util/serverTime';
|
||||||
|
import {
|
||||||
|
addMessageToLocalDb,
|
||||||
|
addEntitiesToLocalDb,
|
||||||
|
addPhotoToLocalDb,
|
||||||
|
resolveMessageApiChatId,
|
||||||
|
serializeBytes,
|
||||||
|
log,
|
||||||
|
swapLocalInvoiceMedia,
|
||||||
|
isChatFolder,
|
||||||
|
addStoryToLocalDb,
|
||||||
|
} from './helpers';
|
||||||
|
import { scheduleMutedTopicUpdate, scheduleMutedChatUpdate } from './scheduleUnmute';
|
||||||
import {
|
import {
|
||||||
buildApiMessage,
|
buildApiMessage,
|
||||||
buildApiMessageFromShort,
|
buildApiMessageFromShort,
|
||||||
buildApiMessageFromShortChat,
|
buildApiMessageFromShortChat,
|
||||||
buildMessageMediaContent,
|
|
||||||
buildPoll,
|
|
||||||
buildPollResults,
|
|
||||||
buildApiMessageFromNotification,
|
buildApiMessageFromNotification,
|
||||||
buildMessageDraft,
|
buildMessageDraft,
|
||||||
buildMessageReactions,
|
|
||||||
buildApiMessageExtendedMediaPreview,
|
|
||||||
buildApiStory,
|
|
||||||
buildPrivacyRules,
|
|
||||||
} from './apiBuilders/messages';
|
} from './apiBuilders/messages';
|
||||||
|
import {
|
||||||
|
buildApiReaction,
|
||||||
|
buildMessageReactions,
|
||||||
|
} from './apiBuilders/reactions';
|
||||||
import {
|
import {
|
||||||
buildChatMember,
|
buildChatMember,
|
||||||
buildChatMembers,
|
buildChatMembers,
|
||||||
@ -36,30 +46,20 @@ import {
|
|||||||
buildApiUserEmojiStatus,
|
buildApiUserEmojiStatus,
|
||||||
buildApiUserStatus,
|
buildApiUserStatus,
|
||||||
} from './apiBuilders/users';
|
} from './apiBuilders/users';
|
||||||
import {
|
|
||||||
buildMessageFromUpdate,
|
|
||||||
isMessageWithMedia,
|
|
||||||
buildChatPhotoForLocalDb,
|
|
||||||
} from './gramjsBuilders';
|
|
||||||
import localDb from './localDb';
|
|
||||||
import { omitVirtualClassFields } from './apiBuilders/helpers';
|
import { omitVirtualClassFields } from './apiBuilders/helpers';
|
||||||
import {
|
|
||||||
addMessageToLocalDb,
|
|
||||||
addEntitiesToLocalDb,
|
|
||||||
addPhotoToLocalDb,
|
|
||||||
resolveMessageApiChatId,
|
|
||||||
serializeBytes,
|
|
||||||
log,
|
|
||||||
swapLocalInvoiceMedia,
|
|
||||||
isChatFolder,
|
|
||||||
addStoryToLocalDb,
|
|
||||||
} from './helpers';
|
|
||||||
import {
|
import {
|
||||||
buildApiNotifyException,
|
buildApiNotifyException,
|
||||||
buildApiNotifyExceptionTopic,
|
buildApiNotifyExceptionTopic,
|
||||||
buildPrivacyKey,
|
buildPrivacyKey,
|
||||||
} from './apiBuilders/misc';
|
} from './apiBuilders/misc';
|
||||||
import { buildApiPhoto, buildApiUsernames } from './apiBuilders/common';
|
import {
|
||||||
|
buildApiMessageExtendedMediaPreview,
|
||||||
|
buildMessageMediaContent,
|
||||||
|
buildPoll,
|
||||||
|
buildPollResults,
|
||||||
|
} from './apiBuilders/messageContent';
|
||||||
|
import { buildApiStealthMode, buildApiStory } from './apiBuilders/stories';
|
||||||
|
import { buildApiPhoto, buildApiUsernames, buildPrivacyRules } from './apiBuilders/common';
|
||||||
import {
|
import {
|
||||||
buildApiGroupCall,
|
buildApiGroupCall,
|
||||||
buildApiGroupCallParticipant,
|
buildApiGroupCallParticipant,
|
||||||
@ -69,7 +69,11 @@ import {
|
|||||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './apiBuilders/peers';
|
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './apiBuilders/peers';
|
||||||
import { buildApiEmojiInteraction, buildStickerSet } from './apiBuilders/symbols';
|
import { buildApiEmojiInteraction, buildStickerSet } from './apiBuilders/symbols';
|
||||||
import { buildApiBotMenuButton } from './apiBuilders/bots';
|
import { buildApiBotMenuButton } from './apiBuilders/bots';
|
||||||
import { scheduleMutedTopicUpdate, scheduleMutedChatUpdate } from './scheduleUnmute';
|
import {
|
||||||
|
buildMessageFromUpdate,
|
||||||
|
isMessageWithMedia,
|
||||||
|
buildChatPhotoForLocalDb,
|
||||||
|
} from './gramjsBuilders';
|
||||||
|
|
||||||
export type Update = (
|
export type Update = (
|
||||||
(GramJs.TypeUpdate | GramJs.TypeUpdates) & { _entities?: (GramJs.TypeUser | GramJs.TypeChat)[] }
|
(GramJs.TypeUpdate | GramJs.TypeUpdates) & { _entities?: (GramJs.TypeUser | GramJs.TypeChat)[] }
|
||||||
@ -879,6 +883,7 @@ export function updater(update: Update) {
|
|||||||
'@type': 'updatePeerBlocked',
|
'@type': 'updatePeerBlocked',
|
||||||
id: getApiChatIdFromMtpPeer(update.peerId),
|
id: getApiChatIdFromMtpPeer(update.peerId),
|
||||||
isBlocked: update.blocked,
|
isBlocked: update.blocked,
|
||||||
|
isBlockedFromStories: update.blockedMyStoriesFrom,
|
||||||
});
|
});
|
||||||
} else if (update instanceof GramJs.UpdatePrivacy) {
|
} else if (update instanceof GramJs.UpdatePrivacy) {
|
||||||
const key = buildPrivacyKey(update.key);
|
const key = buildPrivacyKey(update.key);
|
||||||
@ -1086,6 +1091,18 @@ export function updater(update: Update) {
|
|||||||
userId: buildApiPeerId(update.userId, 'user'),
|
userId: buildApiPeerId(update.userId, 'user'),
|
||||||
lastReadId: update.maxId,
|
lastReadId: update.maxId,
|
||||||
});
|
});
|
||||||
|
} else if (update instanceof GramJs.UpdateSentStoryReaction) {
|
||||||
|
onUpdate({
|
||||||
|
'@type': 'updateSentStoryReaction',
|
||||||
|
userId: buildApiPeerId(update.userId, 'user'),
|
||||||
|
storyId: update.storyId,
|
||||||
|
reaction: buildApiReaction(update.reaction),
|
||||||
|
});
|
||||||
|
} else if (update instanceof GramJs.UpdateStoriesStealthMode) {
|
||||||
|
onUpdate({
|
||||||
|
'@type': 'updateStealthMode',
|
||||||
|
stealthMode: buildApiStealthMode(update.stealthMode),
|
||||||
|
});
|
||||||
} 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);
|
||||||
|
|||||||
@ -212,7 +212,7 @@ export interface ApiPaymentCredentials {
|
|||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ApiGeoPoint {
|
export interface ApiGeoPoint {
|
||||||
long: number;
|
long: number;
|
||||||
lat: number;
|
lat: number;
|
||||||
accessHash: string;
|
accessHash: string;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { ApiMessage } from './messages';
|
import type { ApiGeoPoint, ApiMessage, ApiReaction } from './messages';
|
||||||
import type { ApiPrivacySettings } from '../../types';
|
import type { ApiPrivacySettings } from '../../types';
|
||||||
|
|
||||||
export interface ApiStory {
|
export interface ApiStory {
|
||||||
@ -16,8 +16,11 @@ export interface ApiStory {
|
|||||||
isPublic?: boolean;
|
isPublic?: boolean;
|
||||||
noForwards?: boolean;
|
noForwards?: boolean;
|
||||||
viewsCount?: number;
|
viewsCount?: number;
|
||||||
|
reactionsCount?: number;
|
||||||
recentViewerIds?: string[];
|
recentViewerIds?: string[];
|
||||||
visibility?: ApiPrivacySettings;
|
visibility?: ApiPrivacySettings;
|
||||||
|
sentReaction?: ApiReaction;
|
||||||
|
mediaAreas?: ApiMediaArea[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiStorySkipped {
|
export interface ApiStorySkipped {
|
||||||
@ -57,3 +60,39 @@ export type ApiWebPageStoryData = {
|
|||||||
id: number;
|
id: number;
|
||||||
userId: string;
|
userId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ApiStoryView = {
|
||||||
|
userId: string;
|
||||||
|
date: number;
|
||||||
|
reaction?: ApiReaction;
|
||||||
|
isUserBlocked?: true;
|
||||||
|
areStoriesBlocked?: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiStealthMode = {
|
||||||
|
activeUntil?: number;
|
||||||
|
cooldownUntil?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiMediaAreaCoordinates = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
rotation: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiMediaAreaVenue = {
|
||||||
|
type: 'venue';
|
||||||
|
coordinates: ApiMediaAreaCoordinates;
|
||||||
|
geo: ApiGeoPoint;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiMediaAreaGeoPoint = {
|
||||||
|
type: 'geoPoint';
|
||||||
|
coordinates: ApiMediaAreaCoordinates;
|
||||||
|
geo: ApiGeoPoint;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiMediaArea = ApiMediaAreaVenue | ApiMediaAreaGeoPoint;
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import type {
|
|||||||
ApiMessageExtendedMediaPreview,
|
ApiMessageExtendedMediaPreview,
|
||||||
ApiPhoto,
|
ApiPhoto,
|
||||||
ApiPoll,
|
ApiPoll,
|
||||||
|
ApiReaction,
|
||||||
ApiReactions,
|
ApiReactions,
|
||||||
ApiStickerSet,
|
ApiStickerSet,
|
||||||
ApiThreadInfo,
|
ApiThreadInfo,
|
||||||
@ -33,7 +34,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';
|
import type { ApiStealthMode, ApiStory, ApiStorySkipped } from './stories';
|
||||||
|
|
||||||
export type ApiUpdateReady = {
|
export type ApiUpdateReady = {
|
||||||
'@type': 'updateApiReady';
|
'@type': 'updateApiReady';
|
||||||
@ -462,7 +463,8 @@ export type ApiUpdateTwoFaStateWaitCode = {
|
|||||||
export type ApiUpdatePeerBlocked = {
|
export type ApiUpdatePeerBlocked = {
|
||||||
'@type': 'updatePeerBlocked';
|
'@type': 'updatePeerBlocked';
|
||||||
id: string;
|
id: string;
|
||||||
isBlocked: boolean;
|
isBlocked?: boolean;
|
||||||
|
isBlockedFromStories?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ApiUpdatePaymentVerificationNeeded = {
|
export type ApiUpdatePaymentVerificationNeeded = {
|
||||||
@ -643,6 +645,18 @@ export type ApiUpdateReadStories = {
|
|||||||
lastReadId: number;
|
lastReadId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ApiUpdateSentStoryReaction = {
|
||||||
|
'@type': 'updateSentStoryReaction';
|
||||||
|
userId: string;
|
||||||
|
storyId: number;
|
||||||
|
reaction?: ApiReaction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiUpdateStealthMode = {
|
||||||
|
'@type': 'updateStealthMode';
|
||||||
|
stealthMode: ApiStealthMode;
|
||||||
|
};
|
||||||
|
|
||||||
export type ApiRequestSync = {
|
export type ApiRequestSync = {
|
||||||
'@type': 'requestSync';
|
'@type': 'requestSync';
|
||||||
};
|
};
|
||||||
@ -673,8 +687,9 @@ 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 | ApiUpdateStory | ApiUpdateReadStories | ApiUpdateDeleteStory |
|
ApiUpdateRecentReactions | ApiUpdateStory | ApiUpdateReadStories | ApiUpdateDeleteStory | ApiUpdateSentStoryReaction |
|
||||||
ApiRequestReconnectApi | ApiRequestSync | ApiUpdateFetchingDifference | ApiUpdateChannelMessages
|
ApiRequestReconnectApi | ApiRequestSync | ApiUpdateFetchingDifference | ApiUpdateChannelMessages |
|
||||||
|
ApiUpdateStealthMode
|
||||||
);
|
);
|
||||||
|
|
||||||
export type OnApiUpdate = (update: ApiUpdate) => void;
|
export type OnApiUpdate = (update: ApiUpdate) => void;
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -7,6 +7,7 @@ export { default as AttachBotRecipientPicker } from '../components/main/AttachBo
|
|||||||
export { default as Dialogs } from '../components/main/Dialogs';
|
export { default as Dialogs } from '../components/main/Dialogs';
|
||||||
export { default as Notifications } from '../components/main/Notifications';
|
export { default as Notifications } from '../components/main/Notifications';
|
||||||
export { default as SafeLinkModal } from '../components/main/SafeLinkModal';
|
export { default as SafeLinkModal } from '../components/main/SafeLinkModal';
|
||||||
|
export { default as MapModal } from '../components/modals/mapModal/MapModal';
|
||||||
export { default as UrlAuthModal } from '../components/main/UrlAuthModal';
|
export { default as UrlAuthModal } from '../components/main/UrlAuthModal';
|
||||||
export { default as HistoryCalendar } from '../components/main/HistoryCalendar';
|
export { default as HistoryCalendar } from '../components/main/HistoryCalendar';
|
||||||
export { default as NewContactModal } from '../components/main/NewContactModal';
|
export { default as NewContactModal } from '../components/main/NewContactModal';
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import Button from '../../ui/Button';
|
|||||||
import OutlinedMicrophoneIcon from './OutlinedMicrophoneIcon';
|
import OutlinedMicrophoneIcon from './OutlinedMicrophoneIcon';
|
||||||
import FullNameTitle from '../../common/FullNameTitle';
|
import FullNameTitle from '../../common/FullNameTitle';
|
||||||
import GroupCallParticipantMenu from './GroupCallParticipantMenu';
|
import GroupCallParticipantMenu from './GroupCallParticipantMenu';
|
||||||
import Skeleton from '../../ui/Skeleton';
|
import Skeleton from '../../ui/placeholder/Skeleton';
|
||||||
|
|
||||||
import styles from './GroupCallParticipantVideo.module.scss';
|
import styles from './GroupCallParticipantVideo.module.scss';
|
||||||
|
|
||||||
|
|||||||
@ -151,8 +151,7 @@ const MicrophoneButton: FC<OwnProps & StateProps> = ({
|
|||||||
play={playSegment.toString()}
|
play={playSegment.toString()}
|
||||||
playSegment={playSegment}
|
playSegment={playSegment}
|
||||||
className={styles.icon}
|
className={styles.icon}
|
||||||
forceOnHeavyAnimation
|
forceAlways
|
||||||
forceInBackground
|
|
||||||
/>
|
/>
|
||||||
<Spinner className={buildClassName(styles.spinner, isConnecting && styles.spinnerVisible)} color="white" />
|
<Spinner className={buildClassName(styles.spinner, isConnecting && styles.spinnerVisible)} color="white" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -81,8 +81,7 @@ const OutlinedMicrophoneIcon: FC<OwnProps> = ({
|
|||||||
size={28}
|
size={28}
|
||||||
color={microphoneColor}
|
color={microphoneColor}
|
||||||
className={className}
|
className={className}
|
||||||
forceOnHeavyAnimation
|
forceAlways
|
||||||
forceInBackground
|
|
||||||
nonInteractive
|
nonInteractive
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -41,8 +41,8 @@ export type OwnProps = {
|
|||||||
quality?: number;
|
quality?: number;
|
||||||
color?: string;
|
color?: string;
|
||||||
isLowPriority?: boolean;
|
isLowPriority?: boolean;
|
||||||
|
forceAlways?: boolean;
|
||||||
forceOnHeavyAnimation?: boolean;
|
forceOnHeavyAnimation?: boolean;
|
||||||
forceInBackground?: boolean;
|
|
||||||
sharedCanvas?: HTMLCanvasElement;
|
sharedCanvas?: HTMLCanvasElement;
|
||||||
sharedCanvasCoords?: { x: number; y: number };
|
sharedCanvasCoords?: { x: number; y: number };
|
||||||
onClick?: NoneToVoidFunction;
|
onClick?: NoneToVoidFunction;
|
||||||
@ -67,8 +67,8 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
quality,
|
quality,
|
||||||
isLowPriority,
|
isLowPriority,
|
||||||
color,
|
color,
|
||||||
|
forceAlways,
|
||||||
forceOnHeavyAnimation,
|
forceOnHeavyAnimation,
|
||||||
forceInBackground,
|
|
||||||
sharedCanvas,
|
sharedCanvas,
|
||||||
sharedCanvasCoords,
|
sharedCanvasCoords,
|
||||||
onClick,
|
onClick,
|
||||||
@ -181,7 +181,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
if (
|
if (
|
||||||
!animation
|
!animation
|
||||||
|| !(playRef.current || playSegmentRef.current)
|
|| !(playRef.current || playSegmentRef.current)
|
||||||
|| isFrozen(forceOnHeavyAnimation, forceInBackground)
|
|| isFrozen(forceAlways)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -221,13 +221,13 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (playKey) {
|
if (playKey) {
|
||||||
if (!isFrozen(forceOnHeavyAnimation, forceInBackground)) {
|
if (!isFrozen(forceAlways, forceOnHeavyAnimation)) {
|
||||||
playAnimation(noLoop);
|
playAnimation(noLoop);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
pauseAnimation();
|
pauseAnimation();
|
||||||
}
|
}
|
||||||
}, [animation, playKey, noLoop, playAnimation, pauseAnimation, forceOnHeavyAnimation, forceInBackground]);
|
}, [animation, playKey, noLoop, playAnimation, pauseAnimation, forceAlways, forceOnHeavyAnimation]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (animation) {
|
if (animation) {
|
||||||
@ -240,12 +240,12 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
}
|
}
|
||||||
}, [playAnimation, animation, tgsUrl]);
|
}, [playAnimation, animation, tgsUrl]);
|
||||||
|
|
||||||
useHeavyAnimationCheck(pauseAnimation, playAnimation, !playKey || forceOnHeavyAnimation);
|
useHeavyAnimationCheck(pauseAnimation, playAnimation, !playKey || forceAlways || forceOnHeavyAnimation);
|
||||||
usePriorityPlaybackCheck(pauseAnimation, playAnimation, !playKey);
|
usePriorityPlaybackCheck(pauseAnimation, playAnimation, !playKey || forceAlways);
|
||||||
// Pausing frame may not happen in background,
|
// Pausing frame may not happen in background,
|
||||||
// so we need to make sure it happens right after focusing,
|
// so we need to make sure it happens right after focusing,
|
||||||
// then we can play again.
|
// then we can play again.
|
||||||
useBackgroundMode(pauseAnimation, playAnimationOnRaf, !playKey || forceInBackground);
|
useBackgroundMode(pauseAnimation, playAnimationOnRaf, !playKey || forceAlways);
|
||||||
|
|
||||||
if (sharedCanvas) {
|
if (sharedCanvas) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -268,8 +268,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
|
|
||||||
export default memo(AnimatedSticker);
|
export default memo(AnimatedSticker);
|
||||||
|
|
||||||
function isFrozen(forceOnHeavyAnimation = false, forceInBackground = false) {
|
function isFrozen(forceAlways = false, forceOnHeavyAnimation = false) {
|
||||||
return (!forceOnHeavyAnimation && isHeavyAnimating())
|
if (forceAlways) return false;
|
||||||
|| isPriorityPlaybackActive()
|
return (!forceOnHeavyAnimation && isHeavyAnimating()) || isPriorityPlaybackActive() || isBackgroundModeActive();
|
||||||
|| (!forceInBackground && isBackgroundModeActive());
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -118,6 +118,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.size-giant {
|
||||||
|
width: 5rem;
|
||||||
|
height: 5rem;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
|
||||||
|
.emoji {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.size-jumbo {
|
&.size-jumbo {
|
||||||
width: 7.5rem;
|
width: 7.5rem;
|
||||||
height: 7.5rem;
|
height: 7.5rem;
|
||||||
|
|||||||
@ -36,7 +36,7 @@ import './Avatar.scss';
|
|||||||
|
|
||||||
const LOOP_COUNT = 3;
|
const LOOP_COUNT = 3;
|
||||||
|
|
||||||
export type AvatarSize = 'micro' | 'tiny' | 'mini' | 'small' | 'small-mobile' | 'medium' | 'large' | 'jumbo';
|
export type AvatarSize = 'micro' | 'tiny' | 'mini' | 'small' | 'small-mobile' | 'medium' | 'large' | 'giant' | 'jumbo';
|
||||||
|
|
||||||
const cn = createClassNameBuilder('Avatar');
|
const cn = createClassNameBuilder('Avatar');
|
||||||
cn.media = cn('media');
|
cn.media = cn('media');
|
||||||
@ -51,6 +51,7 @@ type OwnProps = {
|
|||||||
isSavedMessages?: boolean;
|
isSavedMessages?: boolean;
|
||||||
withVideo?: boolean;
|
withVideo?: boolean;
|
||||||
withStory?: boolean;
|
withStory?: boolean;
|
||||||
|
forPremiumPromo?: boolean;
|
||||||
withStoryGap?: boolean;
|
withStoryGap?: boolean;
|
||||||
withStorySolid?: boolean;
|
withStorySolid?: boolean;
|
||||||
storyViewerOrigin?: StoryViewerOrigin;
|
storyViewerOrigin?: StoryViewerOrigin;
|
||||||
@ -70,6 +71,7 @@ const Avatar: FC<OwnProps> = ({
|
|||||||
isSavedMessages,
|
isSavedMessages,
|
||||||
withVideo,
|
withVideo,
|
||||||
withStory,
|
withStory,
|
||||||
|
forPremiumPromo,
|
||||||
withStoryGap,
|
withStoryGap,
|
||||||
withStorySolid,
|
withStorySolid,
|
||||||
storyViewerOrigin,
|
storyViewerOrigin,
|
||||||
@ -211,7 +213,7 @@ 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',
|
((withStory && user?.hasStories) || forPremiumPromo) && 'with-story-circle',
|
||||||
withStorySolid && user?.hasStories && 'with-story-solid',
|
withStorySolid && user?.hasStories && 'with-story-solid',
|
||||||
withStorySolid && user?.hasUnreadStories && 'has-unread-story',
|
withStorySolid && user?.hasUnreadStories && 'has-unread-story',
|
||||||
onClick && 'interactive',
|
onClick && 'interactive',
|
||||||
|
|||||||
@ -34,11 +34,13 @@ const SIZES: Record<AvatarSize, number> = {
|
|||||||
'small-mobile': 2.625 * DPR * REM,
|
'small-mobile': 2.625 * DPR * REM,
|
||||||
medium: 2.875 * DPR * REM,
|
medium: 2.875 * DPR * REM,
|
||||||
large: 3.5 * DPR * REM,
|
large: 3.5 * DPR * REM,
|
||||||
|
giant: 5.125 * DPR * REM,
|
||||||
jumbo: 7.625 * DPR * REM,
|
jumbo: 7.625 * DPR * REM,
|
||||||
};
|
};
|
||||||
|
|
||||||
const BLUE = ['#34C578', '#3CA3F3'];
|
const BLUE = ['#34C578', '#3CA3F3'];
|
||||||
const GREEN = ['#C9EB38', '#09C167'];
|
const GREEN = ['#C9EB38', '#09C167'];
|
||||||
|
const PURPLE = ['#A667FF', '#55A5FF'];
|
||||||
const GRAY = '#C4C9CC';
|
const GRAY = '#C4C9CC';
|
||||||
const DARK_GRAY = '#737373';
|
const DARK_GRAY = '#737373';
|
||||||
const STROKE_WIDTH = 0.125 * DPR * REM;
|
const STROKE_WIDTH = 0.125 * DPR * REM;
|
||||||
@ -119,7 +121,7 @@ export default memo(withGlobal<OwnProps>((global, { userId }): StateProps => {
|
|||||||
};
|
};
|
||||||
})(AvatarStoryCircle));
|
})(AvatarStoryCircle));
|
||||||
|
|
||||||
function drawGradientCircle({
|
export function drawGradientCircle({
|
||||||
canvas,
|
canvas,
|
||||||
size,
|
size,
|
||||||
color,
|
color,
|
||||||
@ -142,6 +144,8 @@ function drawGradientCircle({
|
|||||||
segmentsCount = SEGMENTS_MAX;
|
segmentsCount = SEGMENTS_MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const strokeModifier = Math.max(Math.max(size - SIZES.large, 0) / DPR / REM / 1.5, 1);
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
return;
|
return;
|
||||||
@ -150,7 +154,7 @@ function drawGradientCircle({
|
|||||||
canvas.width = size;
|
canvas.width = size;
|
||||||
canvas.height = size;
|
canvas.height = size;
|
||||||
const centerCoordinate = size / 2;
|
const centerCoordinate = size / 2;
|
||||||
const radius = (size - STROKE_WIDTH) / 2;
|
const radius = (size - STROKE_WIDTH * strokeModifier) / 2;
|
||||||
const segmentAngle = (2 * Math.PI) / segmentsCount;
|
const segmentAngle = (2 * Math.PI) / segmentsCount;
|
||||||
const gapSize = (GAP_PERCENT / 100) * (2 * Math.PI);
|
const gapSize = (GAP_PERCENT / 100) * (2 * Math.PI);
|
||||||
const gradient = ctx.createLinearGradient(
|
const gradient = ctx.createLinearGradient(
|
||||||
@ -160,7 +164,7 @@ function drawGradientCircle({
|
|||||||
Math.ceil(size * Math.sin(Math.PI / 2)),
|
Math.ceil(size * Math.sin(Math.PI / 2)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const colorStops = color === 'green' ? GREEN : BLUE;
|
const colorStops = color === 'purple' ? PURPLE : color === 'green' ? GREEN : BLUE;
|
||||||
colorStops.forEach((colorStop, index) => {
|
colorStops.forEach((colorStop, index) => {
|
||||||
gradient.addColorStop(index / (colorStops.length - 1), colorStop);
|
gradient.addColorStop(index / (colorStops.length - 1), colorStop);
|
||||||
});
|
});
|
||||||
@ -174,7 +178,7 @@ function drawGradientCircle({
|
|||||||
let endAngle = startAngle + segmentAngle - (segmentsCount > 1 ? gapSize : 0);
|
let endAngle = startAngle + segmentAngle - (segmentsCount > 1 ? gapSize : 0);
|
||||||
|
|
||||||
ctx.strokeStyle = isRead ? readSegmentColor : gradient;
|
ctx.strokeStyle = isRead ? readSegmentColor : gradient;
|
||||||
ctx.lineWidth = isRead ? STROKE_WIDTH_READ : STROKE_WIDTH;
|
ctx.lineWidth = (isRead ? STROKE_WIDTH_READ : STROKE_WIDTH) * strokeModifier;
|
||||||
|
|
||||||
if (withExtraGap) {
|
if (withExtraGap) {
|
||||||
if (startAngle >= EXTRA_GAP_START && endAngle <= EXTRA_GAP_END) { // Segment is inside extra gap
|
if (startAngle >= EXTRA_GAP_START && endAngle <= EXTRA_GAP_END) { // Segment is inside extra gap
|
||||||
|
|||||||
@ -427,6 +427,14 @@
|
|||||||
right: 0.5rem;
|
right: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.story-reaction-button {
|
||||||
|
--custom-emoji-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-reaction-heart {
|
||||||
|
color: var(--color-heart);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .input-group {
|
> .input-group {
|
||||||
@ -718,6 +726,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#story-input-text .placeholder-text {
|
||||||
|
bottom: 0.875rem;
|
||||||
|
left: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
.composer-tooltip {
|
.composer-tooltip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: calc(100% + 0.5rem);
|
bottom: calc(100% + 0.5rem);
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import type {
|
|||||||
ApiMessageEntity,
|
ApiMessageEntity,
|
||||||
ApiNewPoll,
|
ApiNewPoll,
|
||||||
ApiReaction,
|
ApiReaction,
|
||||||
|
ApiStealthMode,
|
||||||
ApiSticker,
|
ApiSticker,
|
||||||
ApiUser,
|
ApiUser,
|
||||||
ApiVideo,
|
ApiVideo,
|
||||||
@ -68,6 +69,7 @@ import {
|
|||||||
selectTheme,
|
selectTheme,
|
||||||
selectUser,
|
selectUser,
|
||||||
selectUserFullInfo,
|
selectUserFullInfo,
|
||||||
|
selectUserStory,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
import {
|
import {
|
||||||
getAllowedAttachmentOptions,
|
getAllowedAttachmentOptions,
|
||||||
@ -146,6 +148,7 @@ import StickerTooltip from '../middle/composer/StickerTooltip.async';
|
|||||||
import EmojiTooltip from '../middle/composer/EmojiTooltip.async';
|
import EmojiTooltip from '../middle/composer/EmojiTooltip.async';
|
||||||
import CustomSendMenu from '../middle/composer/CustomSendMenu.async';
|
import CustomSendMenu from '../middle/composer/CustomSendMenu.async';
|
||||||
import ReactionSelector from '../middle/message/ReactionSelector';
|
import ReactionSelector from '../middle/message/ReactionSelector';
|
||||||
|
import ReactionStaticEmoji from './ReactionStaticEmoji';
|
||||||
|
|
||||||
import './Composer.scss';
|
import './Composer.scss';
|
||||||
|
|
||||||
@ -230,6 +233,8 @@ type StateProps =
|
|||||||
canPlayAnimatedEmojis?: boolean;
|
canPlayAnimatedEmojis?: boolean;
|
||||||
canBuyPremium?: boolean;
|
canBuyPremium?: boolean;
|
||||||
shouldCollectDebugLogs?: boolean;
|
shouldCollectDebugLogs?: boolean;
|
||||||
|
sentStoryReaction?: ApiReaction;
|
||||||
|
stealthMode?: ApiStealthMode;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum MainButtonState {
|
enum MainButtonState {
|
||||||
@ -254,6 +259,10 @@ const MESSAGE_MAX_LENGTH = 4096;
|
|||||||
const SENDING_ANIMATION_DURATION = 350;
|
const SENDING_ANIMATION_DURATION = 350;
|
||||||
const MOUNT_ANIMATION_DURATION = 430;
|
const MOUNT_ANIMATION_DURATION = 430;
|
||||||
|
|
||||||
|
const HEART_REACTION: ApiReaction = {
|
||||||
|
emoticon: '❤',
|
||||||
|
};
|
||||||
|
|
||||||
const Composer: FC<OwnProps & StateProps> = ({
|
const Composer: FC<OwnProps & StateProps> = ({
|
||||||
type,
|
type,
|
||||||
isOnActiveTab,
|
isOnActiveTab,
|
||||||
@ -328,6 +337,8 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
canBuyPremium,
|
canBuyPremium,
|
||||||
canPlayAnimatedEmojis,
|
canPlayAnimatedEmojis,
|
||||||
shouldCollectDebugLogs,
|
shouldCollectDebugLogs,
|
||||||
|
sentStoryReaction,
|
||||||
|
stealthMode,
|
||||||
onForward,
|
onForward,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
@ -349,6 +360,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
showAllowedMessageTypesNotification,
|
showAllowedMessageTypesNotification,
|
||||||
openStoryReactionPicker,
|
openStoryReactionPicker,
|
||||||
closeReactionPicker,
|
closeReactionPicker,
|
||||||
|
sendStoryReaction,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
@ -356,6 +368,9 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const inputRef = useRef<HTMLDivElement>(null);
|
const inputRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const storyReactionRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
const [getHtml, setHtml] = useSignal('');
|
const [getHtml, setHtml] = useSignal('');
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
const getSelectionRange = useGetSelectionRange(editableInputCssSelector);
|
const getSelectionRange = useGetSelectionRange(editableInputCssSelector);
|
||||||
@ -374,6 +389,9 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
const [isInputHasFocus, markInputHasFocus, unmarkInputHasFocus] = useFlag();
|
const [isInputHasFocus, markInputHasFocus, unmarkInputHasFocus] = useFlag();
|
||||||
const [isAttachMenuOpen, onAttachMenuOpen, onAttachMenuClose] = useFlag();
|
const [isAttachMenuOpen, onAttachMenuOpen, onAttachMenuClose] = useFlag();
|
||||||
|
|
||||||
|
const isSentStoryReactionHeart = sentStoryReaction && 'emoticon' in sentStoryReaction
|
||||||
|
? sentStoryReaction.emoticon === HEART_REACTION.emoticon : false;
|
||||||
|
|
||||||
useEffect(processMessageInputForCustomEmoji, [getHtml]);
|
useEffect(processMessageInputForCustomEmoji, [getHtml]);
|
||||||
|
|
||||||
const customEmojiNotificationNumber = useRef(0);
|
const customEmojiNotificationNumber = useRef(0);
|
||||||
@ -398,10 +416,10 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
}, [chatId]);
|
}, [chatId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chatId && isReady) {
|
if (chatId && isReady && !isInStoryViewer) {
|
||||||
loadScheduledHistory({ chatId });
|
loadScheduledHistory({ chatId });
|
||||||
}
|
}
|
||||||
}, [isReady, chatId, threadId]);
|
}, [isReady, chatId, threadId, isInStoryViewer]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chatId && chat && !sendAsPeerIds && isReady && isChatSuperGroup(chat)) {
|
if (chatId && chat && !sendAsPeerIds && isReady && isChatSuperGroup(chat)) {
|
||||||
@ -521,22 +539,25 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
startRecordTimeRef,
|
startRecordTimeRef,
|
||||||
} = useVoiceRecording();
|
} = useVoiceRecording();
|
||||||
|
|
||||||
|
const shouldSendRecordingStatus = isForCurrentMessageList && !isInStoryViewer;
|
||||||
useInterval(() => {
|
useInterval(() => {
|
||||||
sendMessageAction({ type: 'recordAudio' });
|
sendMessageAction({ type: 'recordAudio' });
|
||||||
}, activeVoiceRecording && SEND_MESSAGE_ACTION_INTERVAL);
|
}, shouldSendRecordingStatus ? activeVoiceRecording && SEND_MESSAGE_ACTION_INTERVAL : undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isForCurrentMessageList || isInStoryViewer) return;
|
||||||
if (!activeVoiceRecording) {
|
if (!activeVoiceRecording) {
|
||||||
sendMessageAction({ type: 'cancel' });
|
sendMessageAction({ type: 'cancel' });
|
||||||
}
|
}
|
||||||
}, [activeVoiceRecording, sendMessageAction]);
|
}, [activeVoiceRecording, isForCurrentMessageList, isInStoryViewer, sendMessageAction]);
|
||||||
|
|
||||||
const isEditingRef = useStateRef(Boolean(editingMessage));
|
const isEditingRef = useStateRef(Boolean(editingMessage));
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isForCurrentMessageList || isInStoryViewer) return;
|
||||||
if (getHtml() && !isEditingRef.current) {
|
if (getHtml() && !isEditingRef.current) {
|
||||||
sendMessageAction({ type: 'typing' });
|
sendMessageAction({ type: 'typing' });
|
||||||
}
|
}
|
||||||
}, [getHtml, isEditingRef, sendMessageAction]);
|
}, [getHtml, isEditingRef, isForCurrentMessageList, isInStoryViewer, sendMessageAction]);
|
||||||
|
|
||||||
const isAdmin = chat && isChatAdmin(chat);
|
const isAdmin = chat && isChatAdmin(chat);
|
||||||
|
|
||||||
@ -736,8 +757,28 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
handleContextMenuHide,
|
handleContextMenuHide,
|
||||||
} = useContextMenuHandlers(mainButtonRef, !(mainButtonState === MainButtonState.Send && canShowCustomSendMenu));
|
} = useContextMenuHandlers(mainButtonRef, !(mainButtonState === MainButtonState.Send && canShowCustomSendMenu));
|
||||||
|
|
||||||
|
const {
|
||||||
|
contextMenuPosition: storyReactionPickerPosition,
|
||||||
|
handleContextMenu: handleStoryPickerContextMenu,
|
||||||
|
handleBeforeContextMenu: handleBeforeStoryPickerContextMenu,
|
||||||
|
handleContextMenuHide: handleStoryPickerContextMenuHide,
|
||||||
|
} = useContextMenuHandlers(storyReactionRef, !isInStoryViewer);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isReactionPickerOpen) return;
|
||||||
|
|
||||||
|
if (storyReactionPickerPosition) {
|
||||||
|
openStoryReactionPicker({
|
||||||
|
storyUserId: chatId,
|
||||||
|
storyId: storyId!,
|
||||||
|
position: storyReactionPickerPosition,
|
||||||
|
});
|
||||||
|
handleStoryPickerContextMenuHide();
|
||||||
|
}
|
||||||
|
}, [chatId, handleStoryPickerContextMenuHide, isReactionPickerOpen, storyId, storyReactionPickerPosition]);
|
||||||
|
|
||||||
useClipboardPaste(
|
useClipboardPaste(
|
||||||
isForCurrentMessageList,
|
isForCurrentMessageList || isInStoryViewer,
|
||||||
insertFormattedTextAndUpdateCursor,
|
insertFormattedTextAndUpdateCursor,
|
||||||
handleSetAttachments,
|
handleSetAttachments,
|
||||||
setNextText,
|
setNextText,
|
||||||
@ -1226,6 +1267,18 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
return withBotMenuButton && !getHtml() && !activeVoiceRecording;
|
return withBotMenuButton && !getHtml() && !activeVoiceRecording;
|
||||||
}, [withBotMenuButton, getHtml, activeVoiceRecording]);
|
}, [withBotMenuButton, getHtml, activeVoiceRecording]);
|
||||||
|
|
||||||
|
const [timedPlaceholderLangKey, timedPlaceholderDate] = useMemo(() => {
|
||||||
|
if (slowMode?.nextSendDate) {
|
||||||
|
return ['SlowModeWait', slowMode.nextSendDate];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stealthMode?.activeUntil && isInStoryViewer) {
|
||||||
|
return ['StealthModeActiveHint', stealthMode.activeUntil];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}, [isInStoryViewer, slowMode?.nextSendDate, stealthMode?.activeUntil]);
|
||||||
|
|
||||||
const isComposerHasFocus = isBotKeyboardOpen || isSymbolMenuOpen || isEmojiTooltipOpen || isSendAsMenuOpen
|
const isComposerHasFocus = isBotKeyboardOpen || isSymbolMenuOpen || isEmojiTooltipOpen || isSendAsMenuOpen
|
||||||
|| isMentionTooltipOpen || isInlineBotTooltipOpen || isDeleteModalOpen || isBotCommandMenuOpen || isAttachMenuOpen
|
|| isMentionTooltipOpen || isInlineBotTooltipOpen || isDeleteModalOpen || isBotCommandMenuOpen || isAttachMenuOpen
|
||||||
|| isStickerTooltipOpen || isBotCommandTooltipOpen || isCustomEmojiTooltipOpen || isBotMenuButtonOpen
|
|| isStickerTooltipOpen || isBotCommandTooltipOpen || isCustomEmojiTooltipOpen || isBotMenuButtonOpen
|
||||||
@ -1348,7 +1401,17 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleReactionPickerOpen = useLastCallback((position: IAnchorPosition) => {
|
const handleReactionPickerOpen = useLastCallback((position: IAnchorPosition) => {
|
||||||
openStoryReactionPicker({ storyUserId: chatId, storyId: storyId!, position });
|
openStoryReactionPicker({
|
||||||
|
storyUserId: chatId,
|
||||||
|
storyId: storyId!,
|
||||||
|
position,
|
||||||
|
sendAsMessage: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleLikeStory = useLastCallback(() => {
|
||||||
|
const reaction = sentStoryReaction ? undefined : HEART_REACTION;
|
||||||
|
sendStoryReaction({ userId: chatId, storyId: storyId!, reaction });
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSendScheduled = useLastCallback(() => {
|
const handleSendScheduled = useLastCallback(() => {
|
||||||
@ -1586,6 +1649,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
canSendPlainText={!isComposerBlocked}
|
canSendPlainText={!isComposerBlocked}
|
||||||
inputCssSelector={editableInputCssSelector}
|
inputCssSelector={editableInputCssSelector}
|
||||||
idPrefix={type}
|
idPrefix={type}
|
||||||
|
forceDarkTheme={isInStoryViewer}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<MessageInput
|
<MessageInput
|
||||||
@ -1607,6 +1671,8 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
? (botKeyboardPlaceholder || inputPlaceholder || lang('Message'))
|
? (botKeyboardPlaceholder || inputPlaceholder || lang('Message'))
|
||||||
: lang('Chat.PlaceholderTextNotAllowed'))
|
: lang('Chat.PlaceholderTextNotAllowed'))
|
||||||
}
|
}
|
||||||
|
timedPlaceholderDate={timedPlaceholderDate}
|
||||||
|
timedPlaceholderLangKey={timedPlaceholderLangKey}
|
||||||
forcedPlaceholder={inlineBotHelp}
|
forcedPlaceholder={inlineBotHelp}
|
||||||
canAutoFocus={isReady && isForCurrentMessageList && !hasAttachments && isInMessageList}
|
canAutoFocus={isReady && isForCurrentMessageList && !hasAttachments && isInMessageList}
|
||||||
noFocusInterception={hasAttachments}
|
noFocusInterception={hasAttachments}
|
||||||
@ -1618,6 +1684,33 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
onFocus={markInputHasFocus}
|
onFocus={markInputHasFocus}
|
||||||
onBlur={unmarkInputHasFocus}
|
onBlur={unmarkInputHasFocus}
|
||||||
/>
|
/>
|
||||||
|
{isInStoryViewer && !activeVoiceRecording && (
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
className="story-reaction-button"
|
||||||
|
color="translucent"
|
||||||
|
onClick={handleLikeStory}
|
||||||
|
onContextMenu={handleStoryPickerContextMenu}
|
||||||
|
onMouseDown={handleBeforeStoryPickerContextMenu}
|
||||||
|
ariaLabel={lang('AccDescrLike')}
|
||||||
|
ref={storyReactionRef}
|
||||||
|
>
|
||||||
|
{sentStoryReaction && !isSentStoryReactionHeart && (
|
||||||
|
<ReactionStaticEmoji
|
||||||
|
reaction={sentStoryReaction}
|
||||||
|
availableReactions={availableReactions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<i
|
||||||
|
className={buildClassName(
|
||||||
|
'icon',
|
||||||
|
!sentStoryReaction && 'icon-heart-outline',
|
||||||
|
isSentStoryReactionHeart && 'icon-heart story-reaction-heart',
|
||||||
|
)}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{isInMessageList && (
|
{isInMessageList && (
|
||||||
<>
|
<>
|
||||||
{isInlineBotLoading && Boolean(inlineBotId) && (
|
{isInlineBotLoading && Boolean(inlineBotId) && (
|
||||||
@ -1730,7 +1823,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
ref={mainButtonRef}
|
ref={mainButtonRef}
|
||||||
round
|
round
|
||||||
color={isInMessageList ? 'secondary' : undefined}
|
color="secondary"
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
mainButtonState,
|
mainButtonState,
|
||||||
'main-button',
|
'main-button',
|
||||||
@ -1772,7 +1865,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global, {
|
(global, {
|
||||||
chatId, threadId, messageListType, isMobile, type,
|
chatId, threadId, storyId, messageListType, isMobile, type,
|
||||||
}): StateProps => {
|
}): StateProps => {
|
||||||
const chat = selectChat(global, chatId);
|
const chat = selectChat(global, chatId);
|
||||||
const chatBot = chatId !== REPLIES_USER_ID ? selectBot(global, chatId) : undefined;
|
const chatBot = chatId !== REPLIES_USER_ID ? selectBot(global, chatId) : undefined;
|
||||||
@ -1802,10 +1895,15 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const sendAsChat = !sendAsUser && sendAsId ? selectChat(global, sendAsId) : undefined;
|
const sendAsChat = !sendAsUser && sendAsId ? selectChat(global, sendAsId) : undefined;
|
||||||
const requestedDraftText = selectRequestedDraftText(global, chatId);
|
const requestedDraftText = selectRequestedDraftText(global, chatId);
|
||||||
const requestedDraftFiles = selectRequestedDraftFiles(global, chatId);
|
const requestedDraftFiles = selectRequestedDraftFiles(global, chatId);
|
||||||
|
|
||||||
|
const tabState = selectTabState(global);
|
||||||
|
const isStoryViewerOpen = Boolean(tabState.storyViewer.storyId);
|
||||||
|
|
||||||
const currentMessageList = selectCurrentMessageList(global);
|
const currentMessageList = selectCurrentMessageList(global);
|
||||||
const isForCurrentMessageList = chatId === currentMessageList?.chatId
|
const isForCurrentMessageList = chatId === currentMessageList?.chatId
|
||||||
&& threadId === currentMessageList?.threadId
|
&& threadId === currentMessageList?.threadId
|
||||||
&& messageListType === currentMessageList?.type;
|
&& messageListType === currentMessageList?.type
|
||||||
|
&& !isStoryViewerOpen;
|
||||||
const user = selectUser(global, chatId);
|
const user = selectUser(global, chatId);
|
||||||
const canSendVoiceByPrivacy = (user && !selectUserFullInfo(global, user.id)?.noVoiceMessages) ?? true;
|
const canSendVoiceByPrivacy = (user && !selectUserFullInfo(global, user.id)?.noVoiceMessages) ?? true;
|
||||||
const slowMode = chatFullInfo?.slowMode;
|
const slowMode = chatFullInfo?.slowMode;
|
||||||
@ -1817,7 +1915,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
|
|
||||||
const replyingToId = selectReplyingToId(global, chatId, threadId);
|
const replyingToId = selectReplyingToId(global, chatId, threadId);
|
||||||
|
|
||||||
const tabState = selectTabState(global);
|
const story = storyId && selectUserStory(global, chatId, storyId);
|
||||||
|
const sentStoryReaction = story && 'sentReaction' in story ? story.sentReaction : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
availableReactions: type === 'story' ? global.availableReactions : undefined,
|
availableReactions: type === 'story' ? global.availableReactions : undefined,
|
||||||
@ -1880,6 +1979,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
canBuyPremium: !isCurrentUserPremium && !selectIsPremiumPurchaseBlocked(global),
|
canBuyPremium: !isCurrentUserPremium && !selectIsPremiumPurchaseBlocked(global),
|
||||||
canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global),
|
canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global),
|
||||||
shouldCollectDebugLogs: global.settings.byKey.shouldCollectDebugLogs,
|
shouldCollectDebugLogs: global.settings.byKey.shouldCollectDebugLogs,
|
||||||
|
sentStoryReaction,
|
||||||
|
stealthMode: global.stories.stealthMode,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(Composer));
|
)(Composer));
|
||||||
|
|||||||
@ -36,6 +36,7 @@ type OwnProps = {
|
|||||||
withTranslucentThumb?: boolean;
|
withTranslucentThumb?: boolean;
|
||||||
shouldPreloadPreview?: boolean;
|
shouldPreloadPreview?: boolean;
|
||||||
forceOnHeavyAnimation?: boolean;
|
forceOnHeavyAnimation?: boolean;
|
||||||
|
forceAlways?: boolean;
|
||||||
observeIntersectionForLoading?: ObserveFn;
|
observeIntersectionForLoading?: ObserveFn;
|
||||||
observeIntersectionForPlaying?: ObserveFn;
|
observeIntersectionForPlaying?: ObserveFn;
|
||||||
onClick?: NoneToVoidFunction;
|
onClick?: NoneToVoidFunction;
|
||||||
@ -58,6 +59,7 @@ const CustomEmoji: FC<OwnProps> = ({
|
|||||||
sharedCanvasHqRef,
|
sharedCanvasHqRef,
|
||||||
withTranslucentThumb,
|
withTranslucentThumb,
|
||||||
shouldPreloadPreview,
|
shouldPreloadPreview,
|
||||||
|
forceAlways,
|
||||||
forceOnHeavyAnimation,
|
forceOnHeavyAnimation,
|
||||||
observeIntersectionForLoading,
|
observeIntersectionForLoading,
|
||||||
observeIntersectionForPlaying,
|
observeIntersectionForPlaying,
|
||||||
@ -137,6 +139,7 @@ const CustomEmoji: FC<OwnProps> = ({
|
|||||||
loopLimit={loopLimit}
|
loopLimit={loopLimit}
|
||||||
shouldPreloadPreview={shouldPreloadPreview || noPlay || !canPlay}
|
shouldPreloadPreview={shouldPreloadPreview || noPlay || !canPlay}
|
||||||
forceOnHeavyAnimation={forceOnHeavyAnimation}
|
forceOnHeavyAnimation={forceOnHeavyAnimation}
|
||||||
|
forceAlways={forceAlways}
|
||||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||||
withSharedAnimation={withSharedAnimation}
|
withSharedAnimation={withSharedAnimation}
|
||||||
|
|||||||
@ -322,6 +322,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
<StickerSetCover
|
<StickerSetCover
|
||||||
stickerSet={stickerSet as ApiStickerSet}
|
stickerSet={stickerSet as ApiStickerSet}
|
||||||
noPlay={!canAnimate || !loadAndPlay}
|
noPlay={!canAnimate || !loadAndPlay}
|
||||||
|
forcePlayback
|
||||||
observeIntersection={observeIntersectionForCovers}
|
observeIntersection={observeIntersectionForCovers}
|
||||||
sharedCanvasRef={withSharedCanvas ? (isHq ? sharedCanvasHqRef : sharedCanvasRef) : undefined}
|
sharedCanvasRef={withSharedCanvas ? (isHq ? sharedCanvasHqRef : sharedCanvasRef) : undefined}
|
||||||
/>
|
/>
|
||||||
@ -345,6 +346,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
withTranslucentThumb={isTranslucent}
|
withTranslucentThumb={isTranslucent}
|
||||||
onClick={selectStickerSet}
|
onClick={selectStickerSet}
|
||||||
clickArg={index}
|
clickArg={index}
|
||||||
|
forcePlayback
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -422,6 +424,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
onContextMenuOpen={onContextMenuOpen}
|
onContextMenuOpen={onContextMenuOpen}
|
||||||
onContextMenuClose={onContextMenuClose}
|
onContextMenuClose={onContextMenuClose}
|
||||||
onContextMenuClick={onContextMenuClick}
|
onContextMenuClick={onContextMenuClick}
|
||||||
|
forcePlayback
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -63,7 +63,7 @@ const DeleteChatModal: FC<OwnProps & StateProps> = ({
|
|||||||
deleteHistory,
|
deleteHistory,
|
||||||
deleteChannel,
|
deleteChannel,
|
||||||
deleteChatUser,
|
deleteChatUser,
|
||||||
blockContact,
|
blockUser,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
@ -77,10 +77,10 @@ const DeleteChatModal: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const handleDeleteAndStop = useCallback(() => {
|
const handleDeleteAndStop = useCallback(() => {
|
||||||
deleteHistory({ chatId: chat.id, shouldDeleteForAll: true });
|
deleteHistory({ chatId: chat.id, shouldDeleteForAll: true });
|
||||||
blockContact({ contactId: chat.id, accessHash: chat.accessHash! });
|
blockUser({ userId: chat.id });
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
}, [deleteHistory, chat.id, chat.accessHash, blockContact, onClose]);
|
}, [chat.id, onClose]);
|
||||||
|
|
||||||
const handleDeleteChat = useCallback(() => {
|
const handleDeleteChat = useCallback(() => {
|
||||||
if (isPrivateChat) {
|
if (isPrivateChat) {
|
||||||
|
|||||||
@ -70,7 +70,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
font-size: 1.25rem;
|
font-size: 1.125rem;
|
||||||
|
margin-inline-end: 0.125rem;
|
||||||
line-height: 0.9375rem;
|
line-height: 0.9375rem;
|
||||||
vertical-align: -0.1875rem;
|
vertical-align: -0.1875rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -81,11 +81,14 @@ const EmbeddedStory: FC<OwnProps> = ({
|
|||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
>
|
>
|
||||||
{pictogramUrl && renderPictogram(pictogramUrl, isProtected)}
|
{pictogramUrl && renderPictogram(pictogramUrl, isProtected)}
|
||||||
<div className={buildClassName('message-text', isExpiredStory && 'with-message-color')}>
|
<div className="message-text with-message-color">
|
||||||
<p dir="auto">
|
<p dir="auto">
|
||||||
{isExpiredStory && (
|
{isExpiredStory && (
|
||||||
<i className="icon icon-story-expired" aria-hidden />
|
<i className="icon icon-story-expired" aria-hidden />
|
||||||
)}
|
)}
|
||||||
|
{isFullStory && (
|
||||||
|
<i className="icon icon-story-reply" aria-hidden />
|
||||||
|
)}
|
||||||
{lang(title)}
|
{lang(title)}
|
||||||
</p>
|
</p>
|
||||||
<div className="message-title" dir="auto">{renderText(senderTitle || NBSP)}</div>
|
<div className="message-title" dir="auto">{renderText(senderTitle || NBSP)}</div>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
.root {
|
.root {
|
||||||
display: flex;
|
display: flex !important;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,7 @@ interface OwnProps {
|
|||||||
withTranslucentThumbs?: boolean;
|
withTranslucentThumbs?: boolean;
|
||||||
shouldRenderAsHtml?: boolean;
|
shouldRenderAsHtml?: boolean;
|
||||||
inChatList?: boolean;
|
inChatList?: boolean;
|
||||||
|
forcePlayback?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIN_CUSTOM_EMOJIS_FOR_SHARED_CANVAS = 3;
|
const MIN_CUSTOM_EMOJIS_FOR_SHARED_CANVAS = 3;
|
||||||
@ -43,6 +44,7 @@ function MessageText({
|
|||||||
withTranslucentThumbs,
|
withTranslucentThumbs,
|
||||||
shouldRenderAsHtml,
|
shouldRenderAsHtml,
|
||||||
inChatList,
|
inChatList,
|
||||||
|
forcePlayback,
|
||||||
}: OwnProps) {
|
}: OwnProps) {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const sharedCanvasRef = useRef<HTMLCanvasElement>(null);
|
const sharedCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
@ -94,6 +96,7 @@ function MessageText({
|
|||||||
sharedCanvasRef,
|
sharedCanvasRef,
|
||||||
sharedCanvasHqRef,
|
sharedCanvasHqRef,
|
||||||
cacheBuster: textCacheBusterRef.current.toString(),
|
cacheBuster: textCacheBusterRef.current.toString(),
|
||||||
|
forcePlayback,
|
||||||
}),
|
}),
|
||||||
].flat().filter(Boolean)}
|
].flat().filter(Boolean)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -29,6 +29,7 @@ type OwnProps = {
|
|||||||
observeIntersection?: ObserveFn;
|
observeIntersection?: ObserveFn;
|
||||||
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
||||||
sharedCanvasHqRef?: React.RefObject<HTMLCanvasElement>;
|
sharedCanvasHqRef?: React.RefObject<HTMLCanvasElement>;
|
||||||
|
forcePlayback?: boolean;
|
||||||
onClick: (reaction: ApiReaction) => void;
|
onClick: (reaction: ApiReaction) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -40,6 +41,7 @@ const ReactionEmoji: FC<OwnProps> = ({
|
|||||||
observeIntersection,
|
observeIntersection,
|
||||||
sharedCanvasRef,
|
sharedCanvasRef,
|
||||||
sharedCanvasHqRef,
|
sharedCanvasHqRef,
|
||||||
|
forcePlayback,
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -84,6 +86,7 @@ const ReactionEmoji: FC<OwnProps> = ({
|
|||||||
sharedCanvasRef={sharedCanvasRef}
|
sharedCanvasRef={sharedCanvasRef}
|
||||||
sharedCanvasHqRef={sharedCanvasHqRef}
|
sharedCanvasHqRef={sharedCanvasHqRef}
|
||||||
withTranslucentThumb
|
withTranslucentThumb
|
||||||
|
forceAlways={forcePlayback}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<AnimatedIconWithPreview
|
<AnimatedIconWithPreview
|
||||||
|
|||||||
@ -1,4 +1,14 @@
|
|||||||
.ReactionStaticEmoji {
|
.ReactionStaticEmoji {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
// Unicorn reaction preview is too small
|
||||||
|
&.with-unicorn-fix {
|
||||||
|
transform: scale(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.icon-heart {
|
||||||
|
color: var(--color-heart) !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ type OwnProps = {
|
|||||||
availableReactions?: ApiAvailableReaction[];
|
availableReactions?: ApiAvailableReaction[];
|
||||||
className?: string;
|
className?: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
|
withIconHeart?: boolean;
|
||||||
observeIntersection?: ObserveFn;
|
observeIntersection?: ObserveFn;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -29,6 +30,7 @@ const ReactionStaticEmoji: FC<OwnProps> = ({
|
|||||||
availableReactions,
|
availableReactions,
|
||||||
className,
|
className,
|
||||||
size,
|
size,
|
||||||
|
withIconHeart,
|
||||||
observeIntersection,
|
observeIntersection,
|
||||||
}) => {
|
}) => {
|
||||||
const isCustom = 'documentId' in reaction;
|
const isCustom = 'documentId' in reaction;
|
||||||
@ -40,6 +42,9 @@ const ReactionStaticEmoji: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const transitionClassNames = useMediaTransition(mediaData);
|
const transitionClassNames = useMediaTransition(mediaData);
|
||||||
|
|
||||||
|
const shouldApplySizeFix = 'emoticon' in reaction && reaction.emoticon === '🦄';
|
||||||
|
const shouldReplaceWithHeartIcon = withIconHeart && 'emoticon' in reaction && reaction.emoticon === '❤';
|
||||||
|
|
||||||
if (isCustom) {
|
if (isCustom) {
|
||||||
return (
|
return (
|
||||||
<CustomEmoji
|
<CustomEmoji
|
||||||
@ -51,9 +56,20 @@ const ReactionStaticEmoji: FC<OwnProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldReplaceWithHeartIcon) {
|
||||||
|
return (
|
||||||
|
<i className="ReactionStaticEmoji icon icon-heart" style={`font-size: ${size}px; width: ${size}px`} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
className={buildClassName('ReactionStaticEmoji', transitionClassNames, className)}
|
className={buildClassName(
|
||||||
|
'ReactionStaticEmoji',
|
||||||
|
shouldApplySizeFix && 'with-unicorn-fix',
|
||||||
|
transitionClassNames,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
style={size ? `width: ${size}px; height: ${size}px` : undefined}
|
style={size ? `width: ${size}px; height: ${size}px` : undefined}
|
||||||
src={mediaData || blankUrl}
|
src={mediaData || blankUrl}
|
||||||
alt={availableReaction?.title}
|
alt={availableReaction?.title}
|
||||||
|
|||||||
@ -41,6 +41,7 @@ type OwnProps<T> = {
|
|||||||
isCurrentUserPremium?: boolean;
|
isCurrentUserPremium?: boolean;
|
||||||
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
||||||
withTranslucentThumb?: boolean;
|
withTranslucentThumb?: boolean;
|
||||||
|
forcePlayback?: boolean;
|
||||||
observeIntersection: ObserveFn;
|
observeIntersection: ObserveFn;
|
||||||
observeIntersectionForShowing?: ObserveFn;
|
observeIntersectionForShowing?: ObserveFn;
|
||||||
noShowPremium?: boolean;
|
noShowPremium?: boolean;
|
||||||
@ -79,6 +80,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
noShowPremium,
|
noShowPremium,
|
||||||
sharedCanvasRef,
|
sharedCanvasRef,
|
||||||
withTranslucentThumb,
|
withTranslucentThumb,
|
||||||
|
forcePlayback,
|
||||||
onClick,
|
onClick,
|
||||||
clickArg,
|
clickArg,
|
||||||
onFaveClick,
|
onFaveClick,
|
||||||
@ -301,6 +303,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
sharedCanvasRef={sharedCanvasRef}
|
sharedCanvasRef={sharedCanvasRef}
|
||||||
withTranslucentThumb={withTranslucentThumb}
|
withTranslucentThumb={withTranslucentThumb}
|
||||||
customColor={customColor}
|
customColor={customColor}
|
||||||
|
forceAlways={forcePlayback}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!noShowPremium && isLocked && (
|
{!noShowPremium && isLocked && (
|
||||||
|
|||||||
@ -54,6 +54,7 @@ type OwnProps = {
|
|||||||
withDefaultStatusIcon?: boolean;
|
withDefaultStatusIcon?: boolean;
|
||||||
isTranslucent?: boolean;
|
isTranslucent?: boolean;
|
||||||
noContextMenus?: boolean;
|
noContextMenus?: boolean;
|
||||||
|
forcePlayback?: boolean;
|
||||||
observeIntersection?: ObserveFn;
|
observeIntersection?: ObserveFn;
|
||||||
observeIntersectionForPlayingItems: ObserveFn;
|
observeIntersectionForPlayingItems: ObserveFn;
|
||||||
observeIntersectionForShowingItems: ObserveFn;
|
observeIntersectionForShowingItems: ObserveFn;
|
||||||
@ -92,6 +93,7 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
withDefaultStatusIcon,
|
withDefaultStatusIcon,
|
||||||
isTranslucent,
|
isTranslucent,
|
||||||
noContextMenus,
|
noContextMenus,
|
||||||
|
forcePlayback,
|
||||||
observeIntersection,
|
observeIntersection,
|
||||||
observeIntersectionForPlayingItems,
|
observeIntersectionForPlayingItems,
|
||||||
observeIntersectionForShowingItems,
|
observeIntersectionForShowingItems,
|
||||||
@ -222,8 +224,7 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
}
|
}
|
||||||
}, [shouldRender, loadStickers, stickerSet]);
|
}, [shouldRender, loadStickers, stickerSet]);
|
||||||
|
|
||||||
const isLocked = !isSavedMessages && !isRecent && isEmoji && !isCurrentUserPremium
|
const isLocked = !isSavedMessages && !isCurrentUserPremium && isPremiumSet;
|
||||||
&& stickerSet.stickers?.some(({ isFree }) => !isFree);
|
|
||||||
|
|
||||||
const isInstalled = stickerSet.installedDate && !stickerSet.isArchived;
|
const isInstalled = stickerSet.installedDate && !stickerSet.isArchived;
|
||||||
const canCut = !isInstalled && stickerSet.id !== RECENT_SYMBOL_SET_ID && stickerSet.id !== POPULAR_SYMBOL_SET_ID;
|
const canCut = !isInstalled && stickerSet.id !== RECENT_SYMBOL_SET_ID && stickerSet.id !== POPULAR_SYMBOL_SET_ID;
|
||||||
@ -239,7 +240,16 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
const favoriteStickerIdsSet = useMemo(() => (
|
const favoriteStickerIdsSet = useMemo(() => (
|
||||||
favoriteStickers ? new Set(favoriteStickers.map(({ id }) => id)) : undefined
|
favoriteStickers ? new Set(favoriteStickers.map(({ id }) => id)) : undefined
|
||||||
), [favoriteStickers]);
|
), [favoriteStickers]);
|
||||||
const withAddSetButton = !shouldHideHeader && !isRecent && isEmoji && !isInstalled && !isPopular;
|
const withAddSetButton = !shouldHideHeader && !isRecent && isEmoji && !isPopular
|
||||||
|
&& (!isInstalled || (!isCurrentUserPremium && !isSavedMessages));
|
||||||
|
const addSetButtonText = useMemo(() => {
|
||||||
|
if (isLocked) {
|
||||||
|
if (isInstalled) return lang('lng_emoji_premium_restore');
|
||||||
|
return lang('Unlock');
|
||||||
|
}
|
||||||
|
|
||||||
|
return lang('Add');
|
||||||
|
}, [isLocked, lang, isInstalled]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -256,7 +266,9 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
{isLocked && <i className="symbol-set-locked-icon icon icon-lock-badge" />}
|
{isLocked && <i className="symbol-set-locked-icon icon icon-lock-badge" />}
|
||||||
{stickerSet.title}
|
{stickerSet.title}
|
||||||
{withAddSetButton && Boolean(stickerSet.stickers) && (
|
{withAddSetButton && Boolean(stickerSet.stickers) && (
|
||||||
<span className="symbol-set-amount">{lang('Stickers', stickerSet.stickers.length, 'i')}</span>
|
<span className="symbol-set-amount">
|
||||||
|
{lang(isEmoji ? 'EmojiCount' : 'Stickers', stickerSet.stickers.length, 'i')}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
{isRecent && (
|
{isRecent && (
|
||||||
@ -271,7 +283,7 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
size="tiny"
|
size="tiny"
|
||||||
fluid
|
fluid
|
||||||
>
|
>
|
||||||
{isPremiumSet && isLocked ? lang('Unlock') : lang('Add')}
|
{addSetButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -321,6 +333,7 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
onClick={onReactionSelect!}
|
onClick={onReactionSelect!}
|
||||||
sharedCanvasRef={sharedCanvasRef}
|
sharedCanvasRef={sharedCanvasRef}
|
||||||
sharedCanvasHqRef={sharedCanvasHqRef}
|
sharedCanvasHqRef={sharedCanvasHqRef}
|
||||||
|
forcePlayback={forcePlayback}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -358,6 +371,7 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
onContextMenuOpen={onContextMenuOpen}
|
onContextMenuOpen={onContextMenuOpen}
|
||||||
onContextMenuClose={onContextMenuClose}
|
onContextMenuClose={onContextMenuClose}
|
||||||
onContextMenuClick={onContextMenuClick}
|
onContextMenuClick={onContextMenuClick}
|
||||||
|
forcePlayback={forcePlayback}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -38,6 +38,7 @@ type OwnProps = {
|
|||||||
loopLimit?: number;
|
loopLimit?: number;
|
||||||
shouldLoop?: boolean;
|
shouldLoop?: boolean;
|
||||||
shouldPreloadPreview?: boolean;
|
shouldPreloadPreview?: boolean;
|
||||||
|
forceAlways?: boolean;
|
||||||
forceOnHeavyAnimation?: boolean;
|
forceOnHeavyAnimation?: boolean;
|
||||||
observeIntersectionForLoading?: ObserveFn;
|
observeIntersectionForLoading?: ObserveFn;
|
||||||
observeIntersectionForPlaying?: ObserveFn;
|
observeIntersectionForPlaying?: ObserveFn;
|
||||||
@ -65,6 +66,7 @@ const StickerView: FC<OwnProps> = ({
|
|||||||
loopLimit,
|
loopLimit,
|
||||||
shouldLoop = false,
|
shouldLoop = false,
|
||||||
shouldPreloadPreview,
|
shouldPreloadPreview,
|
||||||
|
forceAlways,
|
||||||
forceOnHeavyAnimation,
|
forceOnHeavyAnimation,
|
||||||
observeIntersectionForLoading,
|
observeIntersectionForLoading,
|
||||||
observeIntersectionForPlaying,
|
observeIntersectionForPlaying,
|
||||||
@ -161,7 +163,8 @@ const StickerView: FC<OwnProps> = ({
|
|||||||
tgsUrl={fullMediaData}
|
tgsUrl={fullMediaData}
|
||||||
play={shouldPlay}
|
play={shouldPlay}
|
||||||
noLoop={!shouldLoop}
|
noLoop={!shouldLoop}
|
||||||
forceOnHeavyAnimation={forceOnHeavyAnimation}
|
forceOnHeavyAnimation={forceAlways || forceOnHeavyAnimation}
|
||||||
|
forceAlways={forceAlways}
|
||||||
isLowPriority={isSmall && !selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSetInfo)}
|
isLowPriority={isSmall && !selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSetInfo)}
|
||||||
sharedCanvas={sharedCanvasRef?.current || undefined}
|
sharedCanvas={sharedCanvasRef?.current || undefined}
|
||||||
sharedCanvasCoords={coords}
|
sharedCanvasCoords={coords}
|
||||||
@ -178,6 +181,7 @@ const StickerView: FC<OwnProps> = ({
|
|||||||
playsInline
|
playsInline
|
||||||
muted
|
muted
|
||||||
loop={!loopLimit}
|
loop={!loopLimit}
|
||||||
|
isPriority={forceAlways}
|
||||||
disablePictureInPicture
|
disablePictureInPicture
|
||||||
onReady={markPlayerReady}
|
onReady={markPlayerReady}
|
||||||
onEnded={onVideoEnded}
|
onEnded={onVideoEnded}
|
||||||
|
|||||||
@ -14,15 +14,25 @@ import renderText from './renderText';
|
|||||||
import { renderTextWithEntities } from './renderTextWithEntities';
|
import { renderTextWithEntities } from './renderTextWithEntities';
|
||||||
import trimText from '../../../util/trimText';
|
import trimText from '../../../util/trimText';
|
||||||
|
|
||||||
export function renderMessageText(
|
export function renderMessageText({
|
||||||
message: ApiMessage,
|
message,
|
||||||
highlight?: string,
|
highlight,
|
||||||
emojiSize?: number,
|
emojiSize,
|
||||||
isSimple?: boolean,
|
isSimple,
|
||||||
truncateLength?: number,
|
truncateLength,
|
||||||
isProtected?: boolean,
|
isProtected,
|
||||||
shouldRenderAsHtml?: boolean,
|
forcePlayback,
|
||||||
) {
|
shouldRenderAsHtml,
|
||||||
|
} : {
|
||||||
|
message: ApiMessage;
|
||||||
|
highlight?: string;
|
||||||
|
emojiSize?: number;
|
||||||
|
isSimple?: boolean;
|
||||||
|
truncateLength?: number;
|
||||||
|
isProtected?: boolean;
|
||||||
|
forcePlayback?: boolean;
|
||||||
|
shouldRenderAsHtml?: boolean;
|
||||||
|
}) {
|
||||||
const { text, entities } = message.content.text || {};
|
const { text, entities } = message.content.text || {};
|
||||||
|
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@ -39,6 +49,7 @@ export function renderMessageText(
|
|||||||
messageId: message.id,
|
messageId: message.id,
|
||||||
isSimple,
|
isSimple,
|
||||||
isProtected,
|
isProtected,
|
||||||
|
forcePlayback,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,7 +78,9 @@ export function renderMessageSummary(
|
|||||||
const emoji = !noEmoji && getMessageSummaryEmoji(message);
|
const emoji = !noEmoji && getMessageSummaryEmoji(message);
|
||||||
const emojiWithSpace = emoji ? `${emoji} ` : '';
|
const emojiWithSpace = emoji ? `${emoji} ` : '';
|
||||||
|
|
||||||
const text = renderMessageText(message, highlight, undefined, true, truncateLength);
|
const text = renderMessageText({
|
||||||
|
message, highlight, isSimple: true, truncateLength,
|
||||||
|
});
|
||||||
const description = getMessageSummaryDescription(lang, message, text);
|
const description = getMessageSummaryDescription(lang, message, text);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@ -42,6 +42,7 @@ export function renderTextWithEntities({
|
|||||||
sharedCanvasRef,
|
sharedCanvasRef,
|
||||||
sharedCanvasHqRef,
|
sharedCanvasHqRef,
|
||||||
cacheBuster,
|
cacheBuster,
|
||||||
|
forcePlayback,
|
||||||
}: {
|
}: {
|
||||||
text: string;
|
text: string;
|
||||||
entities?: ApiMessageEntity[];
|
entities?: ApiMessageEntity[];
|
||||||
@ -57,8 +58,9 @@ export function renderTextWithEntities({
|
|||||||
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
||||||
sharedCanvasHqRef?: React.RefObject<HTMLCanvasElement>;
|
sharedCanvasHqRef?: React.RefObject<HTMLCanvasElement>;
|
||||||
cacheBuster?: string;
|
cacheBuster?: string;
|
||||||
|
forcePlayback?: boolean;
|
||||||
}) {
|
}) {
|
||||||
if (!entities || !entities.length) {
|
if (!entities?.length) {
|
||||||
return renderMessagePart(text, highlight, emojiSize, shouldRenderAsHtml, isSimple);
|
return renderMessagePart(text, highlight, emojiSize, shouldRenderAsHtml, isSimple);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,7 +133,7 @@ export function renderTextWithEntities({
|
|||||||
// Render the entity itself
|
// Render the entity itself
|
||||||
const newEntity = shouldRenderAsHtml
|
const newEntity = shouldRenderAsHtml
|
||||||
? processEntityAsHtml(entity, entityContent, nestedEntityContent)
|
? processEntityAsHtml(entity, entityContent, nestedEntityContent)
|
||||||
: processEntity(
|
: processEntity({
|
||||||
entity,
|
entity,
|
||||||
entityContent,
|
entityContent,
|
||||||
nestedEntityContent,
|
nestedEntityContent,
|
||||||
@ -146,7 +148,8 @@ export function renderTextWithEntities({
|
|||||||
sharedCanvasRef,
|
sharedCanvasRef,
|
||||||
sharedCanvasHqRef,
|
sharedCanvasHqRef,
|
||||||
cacheBuster,
|
cacheBuster,
|
||||||
);
|
forcePlayback,
|
||||||
|
});
|
||||||
|
|
||||||
if (Array.isArray(newEntity)) {
|
if (Array.isArray(newEntity)) {
|
||||||
renderResult.push(...newEntity);
|
renderResult.push(...newEntity);
|
||||||
@ -312,22 +315,39 @@ function organizeEntity(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function processEntity(
|
function processEntity({
|
||||||
entity: ApiMessageEntity,
|
entity,
|
||||||
entityContent: TextPart,
|
entityContent,
|
||||||
nestedEntityContent: TextPart[],
|
nestedEntityContent,
|
||||||
highlight?: string,
|
highlight,
|
||||||
messageId?: number,
|
messageId,
|
||||||
isSimple?: boolean,
|
isSimple,
|
||||||
isProtected?: boolean,
|
isProtected,
|
||||||
observeIntersectionForLoading?: ObserveFn,
|
observeIntersectionForLoading,
|
||||||
observeIntersectionForPlaying?: ObserveFn,
|
observeIntersectionForPlaying,
|
||||||
withTranslucentThumbs?: boolean,
|
withTranslucentThumbs,
|
||||||
emojiSize?: number,
|
emojiSize,
|
||||||
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>,
|
sharedCanvasRef,
|
||||||
sharedCanvasHqRef?: React.RefObject<HTMLCanvasElement>,
|
sharedCanvasHqRef,
|
||||||
cacheBuster?: string,
|
cacheBuster,
|
||||||
) {
|
forcePlayback,
|
||||||
|
} : {
|
||||||
|
entity: ApiMessageEntity;
|
||||||
|
entityContent: TextPart;
|
||||||
|
nestedEntityContent: TextPart[];
|
||||||
|
highlight?: string;
|
||||||
|
messageId?: number;
|
||||||
|
isSimple?: boolean;
|
||||||
|
isProtected?: boolean;
|
||||||
|
observeIntersectionForLoading?: ObserveFn;
|
||||||
|
observeIntersectionForPlaying?: ObserveFn;
|
||||||
|
withTranslucentThumbs?: boolean;
|
||||||
|
emojiSize?: number;
|
||||||
|
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
||||||
|
sharedCanvasHqRef?: React.RefObject<HTMLCanvasElement>;
|
||||||
|
cacheBuster?: string;
|
||||||
|
forcePlayback?: boolean;
|
||||||
|
}) {
|
||||||
const entityText = typeof entityContent === 'string' && entityContent;
|
const entityText = typeof entityContent === 'string' && entityContent;
|
||||||
const renderedContent = nestedEntityContent.length ? nestedEntityContent : entityContent;
|
const renderedContent = nestedEntityContent.length ? nestedEntityContent : entityContent;
|
||||||
|
|
||||||
@ -360,6 +380,7 @@ function processEntity(
|
|||||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||||
withTranslucentThumb={withTranslucentThumbs}
|
withTranslucentThumb={withTranslucentThumbs}
|
||||||
|
forceAlways={forcePlayback}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -485,6 +506,7 @@ function processEntity(
|
|||||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||||
withTranslucentThumb={withTranslucentThumbs}
|
withTranslucentThumb={withTranslucentThumbs}
|
||||||
|
forceAlways={forcePlayback}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -319,11 +319,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.colon, .forward {
|
.colon, .chat-prefix-icon {
|
||||||
margin-inline-end: 0.1875rem;
|
margin-inline-end: 0.1875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forward {
|
.chat-prefix-icon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: var(--color-list-icon);
|
color: var(--color-list-icon);
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
|
|||||||
@ -151,7 +151,8 @@ export default function useChatListEntry({
|
|||||||
<span className="colon">:</span>
|
<span className="colon">:</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{lastMessage.forwardInfo && (<i className="icon icon-share-filled forward" />)}
|
{lastMessage.forwardInfo && (<i className="icon icon-share-filled chat-prefix-icon" />)}
|
||||||
|
{Boolean(lastMessage.replyToStoryId) && (<i className="icon icon-story-reply chat-prefix-icon" />)}
|
||||||
{renderSummary(lang, lastMessage, observeIntersection, mediaBlobUrl || mediaThumbnail, isRoundVideo)}
|
{renderSummary(lang, lastMessage, observeIntersection, mediaBlobUrl || mediaThumbnail, isRoundVideo)}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -37,7 +37,7 @@ const BlockUserModal: FC<OwnProps & StateProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
setUserSearchQuery,
|
setUserSearchQuery,
|
||||||
blockContact,
|
blockUser,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
@ -65,13 +65,9 @@ const BlockUserModal: FC<OwnProps & StateProps> = ({
|
|||||||
}, [blockedIds, contactIds, currentUserId, search, localContactIds, usersById]);
|
}, [blockedIds, contactIds, currentUserId, search, localContactIds, usersById]);
|
||||||
|
|
||||||
const handleRemoveUser = useCallback((userId: string) => {
|
const handleRemoveUser = useCallback((userId: string) => {
|
||||||
const { id: contactId, accessHash } = usersById[userId] || {};
|
blockUser({ userId });
|
||||||
if (!contactId || !accessHash) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
blockContact({ contactId, accessHash });
|
|
||||||
onClose();
|
onClose();
|
||||||
}, [blockContact, onClose, usersById]);
|
}, [onClose]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChatOrUserPicker
|
<ChatOrUserPicker
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.subtitle) {
|
.subtitle {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|||||||
@ -117,7 +117,7 @@ const SettingsActiveWebsites: FC<OwnProps & StateProps> = ({
|
|||||||
<span className={buildClassName('subtitle', 'black', 'tight', styles.platform)}>
|
<span className={buildClassName('subtitle', 'black', 'tight', styles.platform)}>
|
||||||
{session.domain}, {session.browser}, {session.platform}
|
{session.domain}, {session.browser}, {session.platform}
|
||||||
</span>
|
</span>
|
||||||
<span className="subtitle">{session.ip} {session.region}</span>
|
<span className={buildClassName('subtitle', styles.subtitle)}>{session.ip} {session.region}</span>
|
||||||
</div>
|
</div>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -67,7 +67,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
loadPrivacySettings,
|
loadPrivacySettings,
|
||||||
loadBlockedContacts,
|
loadBlockedUsers,
|
||||||
loadAuthorizations,
|
loadAuthorizations,
|
||||||
loadContentSettings,
|
loadContentSettings,
|
||||||
updateContentSettings,
|
updateContentSettings,
|
||||||
@ -79,12 +79,12 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
|||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadBlockedContacts();
|
loadBlockedUsers();
|
||||||
loadAuthorizations();
|
loadAuthorizations();
|
||||||
loadPrivacySettings();
|
loadPrivacySettings();
|
||||||
loadContentSettings();
|
loadContentSettings();
|
||||||
loadWebAuthorizations();
|
loadWebAuthorizations();
|
||||||
}, [loadBlockedContacts, loadAuthorizations, loadPrivacySettings, loadContentSettings, loadWebAuthorizations]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
|
|||||||
@ -39,13 +39,13 @@ const SettingsPrivacyBlockedUsers: FC<OwnProps & StateProps> = ({
|
|||||||
blockedIds,
|
blockedIds,
|
||||||
phoneCodeList,
|
phoneCodeList,
|
||||||
}) => {
|
}) => {
|
||||||
const { unblockContact } = getActions();
|
const { unblockUser } = getActions();
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const [isBlockUserModalOpen, openBlockUserModal, closeBlockUserModal] = useFlag();
|
const [isBlockUserModalOpen, openBlockUserModal, closeBlockUserModal] = useFlag();
|
||||||
const handleUnblockClick = useCallback((contactId: string) => {
|
const handleUnblockClick = useCallback((userId: string) => {
|
||||||
unblockContact({ contactId });
|
unblockUser({ userId });
|
||||||
}, [unblockContact]);
|
}, [unblockUser]);
|
||||||
|
|
||||||
useHistoryBack({
|
useHistoryBack({
|
||||||
isActive,
|
isActive,
|
||||||
@ -53,13 +53,13 @@ const SettingsPrivacyBlockedUsers: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const blockedUsernamesById = useMemo(() => {
|
const blockedUsernamesById = useMemo(() => {
|
||||||
return blockedIds.reduce((acc, contactId) => {
|
return blockedIds.reduce((acc, userId) => {
|
||||||
const isPrivate = isUserId(contactId);
|
const isPrivate = isUserId(userId);
|
||||||
const user = isPrivate ? usersByIds[contactId] : undefined;
|
const user = isPrivate ? usersByIds[userId] : undefined;
|
||||||
const mainUsername = user && !user.phoneNumber && getMainUsername(user);
|
const mainUsername = user && !user.phoneNumber && getMainUsername(user);
|
||||||
|
|
||||||
if (mainUsername) {
|
if (mainUsername) {
|
||||||
acc[contactId] = mainUsername;
|
acc[userId] = mainUsername;
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import type {
|
|||||||
ApiAttachBot,
|
ApiAttachBot,
|
||||||
ApiChat,
|
ApiChat,
|
||||||
ApiChatFolder,
|
ApiChatFolder,
|
||||||
|
ApiGeoPoint,
|
||||||
ApiMessage,
|
ApiMessage,
|
||||||
ApiUser,
|
ApiUser,
|
||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
@ -95,6 +96,7 @@ 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 StoryViewer from '../story/StoryViewer.async';
|
||||||
|
import MapModal from '../modals/mapModal/MapModal.async';
|
||||||
|
|
||||||
import './Main.scss';
|
import './Main.scss';
|
||||||
|
|
||||||
@ -115,6 +117,8 @@ type StateProps = {
|
|||||||
hasDialogs: boolean;
|
hasDialogs: boolean;
|
||||||
audioMessage?: ApiMessage;
|
audioMessage?: ApiMessage;
|
||||||
safeLinkModalUrl?: string;
|
safeLinkModalUrl?: string;
|
||||||
|
mapModalGeoPoint?: ApiGeoPoint;
|
||||||
|
mapModalZoom?: number;
|
||||||
isHistoryCalendarOpen: boolean;
|
isHistoryCalendarOpen: boolean;
|
||||||
shouldSkipHistoryAnimations?: boolean;
|
shouldSkipHistoryAnimations?: boolean;
|
||||||
openedStickerSetShortName?: string;
|
openedStickerSetShortName?: string;
|
||||||
@ -171,6 +175,8 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
audioMessage,
|
audioMessage,
|
||||||
activeGroupCallId,
|
activeGroupCallId,
|
||||||
safeLinkModalUrl,
|
safeLinkModalUrl,
|
||||||
|
mapModalGeoPoint,
|
||||||
|
mapModalZoom,
|
||||||
isHistoryCalendarOpen,
|
isHistoryCalendarOpen,
|
||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
limitReached,
|
limitReached,
|
||||||
@ -245,6 +251,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
loadRecentReactions,
|
loadRecentReactions,
|
||||||
loadFeaturedEmojiStickers,
|
loadFeaturedEmojiStickers,
|
||||||
setIsAppUpdateAvailable,
|
setIsAppUpdateAvailable,
|
||||||
|
loadPremiumSetStickers,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
if (DEBUG && !DEBUG_isLogged) {
|
if (DEBUG && !DEBUG_isLogged) {
|
||||||
@ -326,6 +333,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
if (isMasterTab && isCurrentUserPremium) {
|
if (isMasterTab && isCurrentUserPremium) {
|
||||||
loadDefaultStatusIcons();
|
loadDefaultStatusIcons();
|
||||||
loadRecentEmojiStatuses();
|
loadRecentEmojiStatuses();
|
||||||
|
loadPremiumSetStickers();
|
||||||
}
|
}
|
||||||
}, [isCurrentUserPremium, isMasterTab]);
|
}, [isCurrentUserPremium, isMasterTab]);
|
||||||
|
|
||||||
@ -517,6 +525,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
<Dialogs isOpen={hasDialogs} />
|
<Dialogs isOpen={hasDialogs} />
|
||||||
{audioMessage && <AudioPlayer key={audioMessage.id} message={audioMessage} noUi />}
|
{audioMessage && <AudioPlayer key={audioMessage.id} message={audioMessage} noUi />}
|
||||||
<SafeLinkModal url={safeLinkModalUrl} />
|
<SafeLinkModal url={safeLinkModalUrl} />
|
||||||
|
<MapModal geoPoint={mapModalGeoPoint} zoom={mapModalZoom} />
|
||||||
<UrlAuthModal urlAuth={urlAuth} currentUserName={currentUserName} />
|
<UrlAuthModal urlAuth={urlAuth} currentUserName={currentUserName} />
|
||||||
<HistoryCalendar isOpen={isHistoryCalendarOpen} />
|
<HistoryCalendar isOpen={isHistoryCalendarOpen} />
|
||||||
<StickerSetModal
|
<StickerSetModal
|
||||||
@ -579,6 +588,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
urlAuth,
|
urlAuth,
|
||||||
webApp,
|
webApp,
|
||||||
safeLinkModalUrl,
|
safeLinkModalUrl,
|
||||||
|
mapModal,
|
||||||
openedStickerSetShortName,
|
openedStickerSetShortName,
|
||||||
openedCustomEmojiSetIds,
|
openedCustomEmojiSetIds,
|
||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
@ -623,6 +633,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
hasDialogs: Boolean(dialogs.length),
|
hasDialogs: Boolean(dialogs.length),
|
||||||
audioMessage,
|
audioMessage,
|
||||||
safeLinkModalUrl,
|
safeLinkModalUrl,
|
||||||
|
mapModalGeoPoint: mapModal?.point,
|
||||||
|
mapModalZoom: mapModal?.zoom,
|
||||||
isHistoryCalendarOpen: Boolean(historyCalendarSelectedAt),
|
isHistoryCalendarOpen: Boolean(historyCalendarSelectedAt),
|
||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
openedStickerSetShortName,
|
openedStickerSetShortName,
|
||||||
|
|||||||
@ -18,7 +18,6 @@
|
|||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
line-height: 1.25rem;
|
line-height: 1.25rem;
|
||||||
min-height: 2.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
@ -29,3 +28,11 @@
|
|||||||
background: var(--item-color, #000);
|
background: var(--item-color, #000);
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-icon {
|
||||||
|
font-size: 2rem !important;
|
||||||
|
align-self: center;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
margin-left: 1rem;
|
||||||
|
color: var(--item-color, #000) !important;
|
||||||
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import React, { memo } from '../../../lib/teact/teact';
|
|||||||
|
|
||||||
import renderText from '../../common/helpers/renderText';
|
import renderText from '../../common/helpers/renderText';
|
||||||
import { hexToRgb, lerpRgb } from '../../../util/switchTheme';
|
import { hexToRgb, lerpRgb } from '../../../util/switchTheme';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
|
||||||
import ListItem from '../../ui/ListItem';
|
import ListItem from '../../ui/ListItem';
|
||||||
|
|
||||||
@ -10,11 +11,12 @@ import styles from './PremiumFeatureItem.module.scss';
|
|||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
icon: string;
|
icon: string;
|
||||||
|
isFontIcon?: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
text: string;
|
text: string;
|
||||||
onClick: VoidFunction;
|
|
||||||
index: number;
|
index: number;
|
||||||
count: number;
|
count: number;
|
||||||
|
onClick?: VoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
const COLORS = [
|
const COLORS = [
|
||||||
@ -24,6 +26,7 @@ const COLORS = [
|
|||||||
|
|
||||||
const PremiumFeatureItem: FC<OwnProps> = ({
|
const PremiumFeatureItem: FC<OwnProps> = ({
|
||||||
icon,
|
icon,
|
||||||
|
isFontIcon,
|
||||||
title,
|
title,
|
||||||
text,
|
text,
|
||||||
index,
|
index,
|
||||||
@ -33,11 +36,19 @@ const PremiumFeatureItem: FC<OwnProps> = ({
|
|||||||
const newIndex = (index / count) * COLORS.length;
|
const newIndex = (index / count) * COLORS.length;
|
||||||
const colorA = COLORS[Math.floor(newIndex)];
|
const colorA = COLORS[Math.floor(newIndex)];
|
||||||
const colorB = COLORS[Math.ceil(newIndex)] ?? colorA;
|
const colorB = COLORS[Math.ceil(newIndex)] ?? colorA;
|
||||||
const { r, g, b } = lerpRgb(colorA, colorB, 1);
|
const { r, g, b } = lerpRgb(colorA, colorB, 0.5);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem buttonClassName={styles.root} onClick={onClick}>
|
<ListItem buttonClassName={styles.root} onClick={onClick} inactive={!onClick}>
|
||||||
<img src={icon} className={styles.icon} alt="" style={`--item-color: rgb(${r},${g},${b})`} />
|
{isFontIcon ? (
|
||||||
|
<i
|
||||||
|
className={buildClassName(styles.fontIcon, `icon icon-${icon}`)}
|
||||||
|
aria-hidden
|
||||||
|
style={`--item-color: rgb(${r},${g},${b})`}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<img src={icon} className={styles.icon} alt="" style={`--item-color: rgb(${r},${g},${b})`} />
|
||||||
|
)}
|
||||||
<div className={styles.text}>
|
<div className={styles.text}>
|
||||||
<div className={styles.title}>{renderText(title, ['br'])}</div>
|
<div className={styles.title}>{renderText(title, ['br'])}</div>
|
||||||
<div className={styles.description}>{text}</div>
|
<div className={styles.description}>{text}</div>
|
||||||
|
|||||||
@ -52,7 +52,7 @@
|
|||||||
margin-bottom: 7.5rem;
|
margin-bottom: 7.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.limits {
|
.limits, .stories {
|
||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,6 +63,10 @@
|
|||||||
height: calc(var(--vh) * 55 + 41px);
|
height: calc(var(--vh) * 55 + 41px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stories {
|
||||||
|
height: calc(var(--vh) * 55 + 100px);
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
padding-left: 4rem;
|
padding-left: 4rem;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
@ -140,4 +144,8 @@
|
|||||||
.limits-content {
|
.limits-content {
|
||||||
height: calc(var(--vh) * 100 - 193px);
|
height: calc(var(--vh) * 100 - 193px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stories {
|
||||||
|
height: calc(var(--vh) * 100 - 135px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import PremiumLimitPreview from './common/PremiumLimitPreview';
|
|||||||
import PremiumFeaturePreviewVideo from './previews/PremiumFeaturePreviewVideo';
|
import PremiumFeaturePreviewVideo from './previews/PremiumFeaturePreviewVideo';
|
||||||
import SliderDots from '../../common/SliderDots';
|
import SliderDots from '../../common/SliderDots';
|
||||||
import PremiumFeaturePreviewStickers from './previews/PremiumFeaturePreviewStickers';
|
import PremiumFeaturePreviewStickers from './previews/PremiumFeaturePreviewStickers';
|
||||||
|
import PremiumFeaturePreviewStories from './previews/PremiumFeaturePreviewStories';
|
||||||
|
|
||||||
import styles from './PremiumFeatureModal.module.scss';
|
import styles from './PremiumFeatureModal.module.scss';
|
||||||
|
|
||||||
@ -36,6 +37,7 @@ export const PREMIUM_FEATURE_TITLES: Record<string, string> = {
|
|||||||
animated_userpics: 'PremiumPreviewAnimatedProfiles',
|
animated_userpics: 'PremiumPreviewAnimatedProfiles',
|
||||||
emoji_status: 'PremiumPreviewEmojiStatus',
|
emoji_status: 'PremiumPreviewEmojiStatus',
|
||||||
translations: 'PremiumPreviewTranslations',
|
translations: 'PremiumPreviewTranslations',
|
||||||
|
stories: 'PremiumPreviewStories',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PREMIUM_FEATURE_DESCRIPTIONS: Record<string, string> = {
|
export const PREMIUM_FEATURE_DESCRIPTIONS: Record<string, string> = {
|
||||||
@ -52,9 +54,11 @@ export const PREMIUM_FEATURE_DESCRIPTIONS: Record<string, string> = {
|
|||||||
animated_userpics: 'PremiumPreviewAnimatedProfilesDescription',
|
animated_userpics: 'PremiumPreviewAnimatedProfilesDescription',
|
||||||
emoji_status: 'PremiumPreviewEmojiStatusDescription',
|
emoji_status: 'PremiumPreviewEmojiStatusDescription',
|
||||||
translations: 'PremiumPreviewTranslationsDescription',
|
translations: 'PremiumPreviewTranslationsDescription',
|
||||||
|
stories: 'PremiumPreviewStoriesDescription',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PREMIUM_FEATURE_SECTIONS = [
|
export const PREMIUM_FEATURE_SECTIONS = [
|
||||||
|
'stories',
|
||||||
'double_limits',
|
'double_limits',
|
||||||
'more_upload',
|
'more_upload',
|
||||||
'faster_download',
|
'faster_download',
|
||||||
@ -267,6 +271,14 @@ const PremiumFeatureModal: FC<OwnProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (section === 'stories') {
|
||||||
|
return (
|
||||||
|
<div className={buildClassName(styles.slide, styles.stories)}>
|
||||||
|
<PremiumFeaturePreviewStories />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const i = promo.videoSections.indexOf(section);
|
const i = promo.videoSections.indexOf(section);
|
||||||
if (i === -1) return undefined;
|
if (i === -1) return undefined;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -34,7 +34,7 @@
|
|||||||
.main {
|
.main {
|
||||||
padding: 1rem 0.5rem;
|
padding: 1rem 0.5rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: scroll;
|
overflow-y: scroll;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -57,6 +57,7 @@ const LIMIT_ACCOUNTS = 4;
|
|||||||
const STATUS_EMOJI_SIZE = 8 * REM;
|
const STATUS_EMOJI_SIZE = 8 * REM;
|
||||||
|
|
||||||
const PREMIUM_FEATURE_COLOR_ICONS: Record<string, string> = {
|
const PREMIUM_FEATURE_COLOR_ICONS: Record<string, string> = {
|
||||||
|
stories: PremiumStatus,
|
||||||
double_limits: PremiumLimits,
|
double_limits: PremiumLimits,
|
||||||
infinite_reactions: PremiumReactions,
|
infinite_reactions: PremiumReactions,
|
||||||
premium_stickers: PremiumStickers,
|
premium_stickers: PremiumStickers,
|
||||||
|
|||||||
@ -0,0 +1,43 @@
|
|||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
align-self: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 500;
|
||||||
|
align-self: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features {
|
||||||
|
min-height: 5rem;
|
||||||
|
overflow-y: scroll;
|
||||||
|
border-top: 0.0625rem solid transparent;
|
||||||
|
transition: 0.2s ease-in-out border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile {
|
||||||
|
padding: 1rem 1rem 1rem 2rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrolled {
|
||||||
|
border-color: var(--color-borders);
|
||||||
|
}
|
||||||
@ -0,0 +1,123 @@
|
|||||||
|
import React, { memo, useLayoutEffect, useRef } from '../../../../lib/teact/teact';
|
||||||
|
import { withGlobal } from '../../../../global';
|
||||||
|
|
||||||
|
import type { ApiUser } from '../../../../api/types';
|
||||||
|
|
||||||
|
import { selectUser } from '../../../../global/selectors';
|
||||||
|
import { REM } from '../../../common/helpers/mediaDimensions';
|
||||||
|
import { DPR } from '../../../../util/windowEnvironment';
|
||||||
|
import { drawGradientCircle } from '../../../common/AvatarStoryCircle';
|
||||||
|
import buildClassName from '../../../../util/buildClassName';
|
||||||
|
|
||||||
|
import useLang from '../../../../hooks/useLang';
|
||||||
|
import useScrolledState from '../../../../hooks/useScrolledState';
|
||||||
|
|
||||||
|
import Avatar from '../../../common/Avatar';
|
||||||
|
import PremiumFeatureItem from '../PremiumFeatureItem';
|
||||||
|
|
||||||
|
import styles from './PremiumFeaturePreviewStories.module.scss';
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
currentUser: ApiUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
const STORY_FEATURE_TITLES = {
|
||||||
|
stories_order: 'PremiumStoriesPriority',
|
||||||
|
stories_stealth: 'PremiumStoriesStealth',
|
||||||
|
stories_views: 'PremiumStoriesViews',
|
||||||
|
stories_timer: 'lng_premium_stories_subtitle_expiration',
|
||||||
|
stories_save: 'PremiumStoriesSaveToGallery',
|
||||||
|
stories_caption: 'lng_premium_stories_subtitle_caption',
|
||||||
|
stories_link: 'lng_premium_stories_subtitle_links',
|
||||||
|
};
|
||||||
|
|
||||||
|
const STORY_FEATURE_DESCRIPTIONS = {
|
||||||
|
stories_order: 'PremiumStoriesPriorityDescription',
|
||||||
|
stories_stealth: 'PremiumStoriesStealthDescription',
|
||||||
|
stories_views: 'PremiumStoriesViewsDescription',
|
||||||
|
stories_timer: 'PremiumStoriesExpirationDescription',
|
||||||
|
stories_save: 'PremiumStoriesSaveToGalleryDescription',
|
||||||
|
stories_caption: 'PremiumStoriesCaptionDescription',
|
||||||
|
stories_link: 'PremiumStoriesFormattingDescription',
|
||||||
|
};
|
||||||
|
|
||||||
|
const STORY_FEATURE_ICONS = {
|
||||||
|
stories_order: 'story-priority',
|
||||||
|
stories_stealth: 'eye-closed-outline',
|
||||||
|
stories_views: 'eye-outline',
|
||||||
|
stories_timer: 'timer',
|
||||||
|
stories_save: 'arrow-down-circle',
|
||||||
|
stories_caption: 'story-caption',
|
||||||
|
stories_link: 'link-badge',
|
||||||
|
};
|
||||||
|
|
||||||
|
const STORY_FEATURE_ORDER = Object.keys(STORY_FEATURE_TITLES) as (keyof typeof STORY_FEATURE_TITLES)[];
|
||||||
|
|
||||||
|
const CIRCLE_SIZE = 5.25 * DPR * REM;
|
||||||
|
const CIRCLE_SEGMENTS = 8;
|
||||||
|
const CIRCLE_READ_SEGMENTS = 0;
|
||||||
|
|
||||||
|
const PremiumFeaturePreviewVideo = ({
|
||||||
|
currentUser,
|
||||||
|
}: StateProps) => {
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const circleRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!circleRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawGradientCircle({
|
||||||
|
canvas: circleRef.current,
|
||||||
|
size: CIRCLE_SIZE,
|
||||||
|
segmentsCount: CIRCLE_SEGMENTS,
|
||||||
|
color: 'purple',
|
||||||
|
readSegmentsCount: CIRCLE_READ_SEGMENTS,
|
||||||
|
readSegmentColor: 'transparent',
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { handleScroll, isAtBeginning } = useScrolledState();
|
||||||
|
|
||||||
|
const maxSize = CIRCLE_SIZE / DPR;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<Avatar forPremiumPromo peer={currentUser} size="giant" />
|
||||||
|
<canvas className={styles.circle} ref={circleRef} style={`max-width: ${maxSize}px; max-height: ${maxSize}px`} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.title}>{lang('UpgradedStories')}</div>
|
||||||
|
<div
|
||||||
|
className={buildClassName(styles.features, !isAtBeginning && styles.scrolled, 'custom-scroll')}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
>
|
||||||
|
{STORY_FEATURE_ORDER.map((section, index) => {
|
||||||
|
return (
|
||||||
|
<PremiumFeatureItem
|
||||||
|
key={section}
|
||||||
|
title={lang(STORY_FEATURE_TITLES[section])}
|
||||||
|
text={lang(STORY_FEATURE_DESCRIPTIONS[section])}
|
||||||
|
icon={STORY_FEATURE_ICONS[section]}
|
||||||
|
isFontIcon
|
||||||
|
index={index}
|
||||||
|
count={STORY_FEATURE_ORDER.length}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div className={styles.mobile}>{lang('lng_premium_stories_about_mobile')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal(
|
||||||
|
(global): StateProps => {
|
||||||
|
return {
|
||||||
|
currentUser: selectUser(global, global.currentUserId!)!,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(PremiumFeaturePreviewVideo));
|
||||||
@ -207,7 +207,7 @@ const MediaViewer: FC<StateProps> = ({
|
|||||||
const prevMediaId = usePrevious(mediaId);
|
const prevMediaId = usePrevious(mediaId);
|
||||||
const prevAvatarOwner = usePrevious<ApiChat | ApiUser | undefined>(avatarOwner);
|
const prevAvatarOwner = usePrevious<ApiChat | ApiUser | undefined>(avatarOwner);
|
||||||
const prevBestImageData = usePrevious(bestImageData);
|
const prevBestImageData = usePrevious(bestImageData);
|
||||||
const textParts = message ? renderMessageText(message) : undefined;
|
const textParts = message ? renderMessageText({ message, forcePlayback: true }) : undefined;
|
||||||
const hasFooter = Boolean(textParts);
|
const hasFooter = Boolean(textParts);
|
||||||
const shouldAnimateOpening = prevIsHidden && prevMediaId !== mediaId;
|
const shouldAnimateOpening = prevIsHidden && prevMediaId !== mediaId;
|
||||||
|
|
||||||
|
|||||||
@ -147,7 +147,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
|||||||
if (!message) return undefined;
|
if (!message) return undefined;
|
||||||
const textParts = message.content.action?.type === 'suggestProfilePhoto'
|
const textParts = message.content.action?.type === 'suggestProfilePhoto'
|
||||||
? lang('Conversation.SuggestedPhotoTitle')
|
? lang('Conversation.SuggestedPhotoTitle')
|
||||||
: renderMessageText(message);
|
: renderMessageText({ message, forcePlayback: true });
|
||||||
|
|
||||||
const hasFooter = Boolean(textParts);
|
const hasFooter = Boolean(textParts);
|
||||||
const posterSize = message && calculateMediaViewerDimensions(dimensions!, hasFooter, isVideo);
|
const posterSize = message && calculateMediaViewerDimensions(dimensions!, hasFooter, isVideo);
|
||||||
|
|||||||
@ -37,7 +37,7 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
openAddContactDialog,
|
openAddContactDialog,
|
||||||
blockContact,
|
blockUser,
|
||||||
reportSpam,
|
reportSpam,
|
||||||
deleteChat,
|
deleteChat,
|
||||||
leaveChannel,
|
leaveChannel,
|
||||||
@ -51,7 +51,6 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
|
|||||||
const [isBlockUserModalOpen, openBlockUserModal, closeBlockUserModal] = useFlag();
|
const [isBlockUserModalOpen, openBlockUserModal, closeBlockUserModal] = useFlag();
|
||||||
const [shouldReportSpam, setShouldReportSpam] = useState<boolean>(true);
|
const [shouldReportSpam, setShouldReportSpam] = useState<boolean>(true);
|
||||||
const [shouldDeleteChat, setShouldDeleteChat] = useState<boolean>(true);
|
const [shouldDeleteChat, setShouldDeleteChat] = useState<boolean>(true);
|
||||||
const { accessHash } = chat || {};
|
|
||||||
const {
|
const {
|
||||||
isAutoArchived, canReportSpam, canAddContact, canBlockContact,
|
isAutoArchived, canReportSpam, canAddContact, canBlockContact,
|
||||||
} = settings || {};
|
} = settings || {};
|
||||||
@ -66,7 +65,7 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const handleConfirmBlock = useLastCallback(() => {
|
const handleConfirmBlock = useLastCallback(() => {
|
||||||
closeBlockUserModal();
|
closeBlockUserModal();
|
||||||
blockContact({ contactId: chatId, accessHash: accessHash! });
|
blockUser({ userId: chatId });
|
||||||
if (canReportSpam && shouldReportSpam) {
|
if (canReportSpam && shouldReportSpam) {
|
||||||
reportSpam({ chatId });
|
reportSpam({ chatId });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -108,7 +108,7 @@ const EmojiInteractionAnimation: FC<OwnProps & StateProps> = ({
|
|||||||
tgsUrl={effectTgsUrl}
|
tgsUrl={effectTgsUrl}
|
||||||
play
|
play
|
||||||
quality={IS_ANDROID ? 0.5 : undefined}
|
quality={IS_ANDROID ? 0.5 : undefined}
|
||||||
forceOnHeavyAnimation
|
forceAlways
|
||||||
noLoop
|
noLoop
|
||||||
onLoad={startPlaying}
|
onLoad={startPlaying}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import useMedia from '../../hooks/useMedia';
|
|||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
|
|
||||||
import OptimizedVideo from '../ui/OptimizedVideo';
|
import OptimizedVideo from '../ui/OptimizedVideo';
|
||||||
import Skeleton from '../ui/Skeleton';
|
import Skeleton from '../ui/placeholder/Skeleton';
|
||||||
|
|
||||||
import styles from './MessageListBotInfo.module.scss';
|
import styles from './MessageListBotInfo.module.scss';
|
||||||
|
|
||||||
|
|||||||
@ -592,6 +592,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
|||||||
canSendPlainText
|
canSendPlainText
|
||||||
className="attachment-modal-symbol-menu with-menu-transitions"
|
className="attachment-modal-symbol-menu with-menu-transitions"
|
||||||
idPrefix="attachment"
|
idPrefix="attachment"
|
||||||
|
forceDarkTheme={forceDarkTheme}
|
||||||
/>
|
/>
|
||||||
<MessageInput
|
<MessageInput
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import useAppLayout from '../../../hooks/useAppLayout';
|
|||||||
import useDerivedState from '../../../hooks/useDerivedState';
|
import useDerivedState from '../../../hooks/useDerivedState';
|
||||||
|
|
||||||
import TextFormatter from './TextFormatter';
|
import TextFormatter from './TextFormatter';
|
||||||
|
import TextTimer from '../../ui/TextTimer';
|
||||||
|
|
||||||
const CONTEXT_MENU_CLOSE_DELAY_MS = 100;
|
const CONTEXT_MENU_CLOSE_DELAY_MS = 100;
|
||||||
// Focus slows down animation, also it breaks transition layout in Chrome
|
// Focus slows down animation, also it breaks transition layout in Chrome
|
||||||
@ -54,6 +55,8 @@ type OwnProps = {
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
getHtml: Signal<string>;
|
getHtml: Signal<string>;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
|
timedPlaceholderLangKey?: string;
|
||||||
|
timedPlaceholderDate?: number;
|
||||||
forcedPlaceholder?: string;
|
forcedPlaceholder?: string;
|
||||||
noFocusInterception?: boolean;
|
noFocusInterception?: boolean;
|
||||||
canAutoFocus: boolean;
|
canAutoFocus: boolean;
|
||||||
@ -114,6 +117,8 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
|||||||
isActive,
|
isActive,
|
||||||
getHtml,
|
getHtml,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
timedPlaceholderLangKey,
|
||||||
|
timedPlaceholderDate,
|
||||||
forcedPlaceholder,
|
forcedPlaceholder,
|
||||||
canSendPlainText,
|
canSendPlainText,
|
||||||
canAutoFocus,
|
canAutoFocus,
|
||||||
@ -165,6 +170,16 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
|||||||
const { isMobile } = useAppLayout();
|
const { isMobile } = useAppLayout();
|
||||||
const isMobileDevice = isMobile && (IS_IOS || IS_ANDROID);
|
const isMobileDevice = isMobile && (IS_IOS || IS_ANDROID);
|
||||||
|
|
||||||
|
const [shouldDisplayTimer, setShouldDisplayTimer] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShouldDisplayTimer(Boolean(timedPlaceholderLangKey && timedPlaceholderDate));
|
||||||
|
}, [timedPlaceholderDate, timedPlaceholderLangKey]);
|
||||||
|
|
||||||
|
const handleTimerEnd = useLastCallback(() => {
|
||||||
|
setShouldDisplayTimer(false);
|
||||||
|
});
|
||||||
|
|
||||||
useInputCustomEmojis(
|
useInputCustomEmojis(
|
||||||
getHtml,
|
getHtml,
|
||||||
inputRef,
|
inputRef,
|
||||||
@ -572,7 +587,9 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
|||||||
>
|
>
|
||||||
{!isAttachmentModalInput && !canSendPlainText
|
{!isAttachmentModalInput && !canSendPlainText
|
||||||
&& <i className="icon icon-lock-badge placeholder-icon" />}
|
&& <i className="icon icon-lock-badge placeholder-icon" />}
|
||||||
{placeholder}
|
{shouldDisplayTimer ? (
|
||||||
|
<TextTimer langKey={timedPlaceholderLangKey!} endsAt={timedPlaceholderDate!} onEnd={handleTimerEnd} />
|
||||||
|
) : placeholder}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<canvas ref={sharedCanvasRef} className="shared-canvas" />
|
<canvas ref={sharedCanvasRef} className="shared-canvas" />
|
||||||
|
|||||||
@ -294,6 +294,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
noPlay={!canAnimate || !loadAndPlay}
|
noPlay={!canAnimate || !loadAndPlay}
|
||||||
observeIntersection={observeIntersectionForCovers}
|
observeIntersection={observeIntersectionForCovers}
|
||||||
sharedCanvasRef={withSharedCanvas ? sharedCanvasRef : undefined}
|
sharedCanvasRef={withSharedCanvas ? sharedCanvasRef : undefined}
|
||||||
|
forcePlayback
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
@ -314,6 +315,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
withTranslucentThumb={isTranslucent}
|
withTranslucentThumb={isTranslucent}
|
||||||
onClick={selectStickerSet}
|
onClick={selectStickerSet}
|
||||||
clickArg={index}
|
clickArg={index}
|
||||||
|
forcePlayback
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -375,6 +377,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
onStickerUnfave={handleStickerUnfave}
|
onStickerUnfave={handleStickerUnfave}
|
||||||
onStickerFave={handleStickerFave}
|
onStickerFave={handleStickerFave}
|
||||||
onStickerRemoveRecent={handleRemoveRecentSticker}
|
onStickerRemoveRecent={handleRemoveRecentSticker}
|
||||||
|
forcePlayback
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -29,6 +29,7 @@ type OwnProps = {
|
|||||||
stickerSet: ApiStickerSet;
|
stickerSet: ApiStickerSet;
|
||||||
size?: number;
|
size?: number;
|
||||||
noPlay?: boolean;
|
noPlay?: boolean;
|
||||||
|
forcePlayback?: boolean;
|
||||||
observeIntersection: ObserveFn;
|
observeIntersection: ObserveFn;
|
||||||
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
||||||
};
|
};
|
||||||
@ -37,6 +38,7 @@ const StickerSetCover: FC<OwnProps> = ({
|
|||||||
stickerSet,
|
stickerSet,
|
||||||
size = STICKER_SIZE_PICKER_HEADER,
|
size = STICKER_SIZE_PICKER_HEADER,
|
||||||
noPlay,
|
noPlay,
|
||||||
|
forcePlayback,
|
||||||
observeIntersection,
|
observeIntersection,
|
||||||
sharedCanvasRef,
|
sharedCanvasRef,
|
||||||
}) => {
|
}) => {
|
||||||
@ -90,6 +92,7 @@ const StickerSetCover: FC<OwnProps> = ({
|
|||||||
isLowPriority={!selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSet)}
|
isLowPriority={!selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSet)}
|
||||||
sharedCanvas={sharedCanvasRef?.current || undefined}
|
sharedCanvas={sharedCanvasRef?.current || undefined}
|
||||||
sharedCanvasCoords={coords}
|
sharedCanvasCoords={coords}
|
||||||
|
forceAlways={forcePlayback}
|
||||||
/>
|
/>
|
||||||
) : (isVideo && !shouldFallbackToStatic) ? (
|
) : (isVideo && !shouldFallbackToStatic) ? (
|
||||||
<OptimizedVideo
|
<OptimizedVideo
|
||||||
@ -97,6 +100,7 @@ const StickerSetCover: FC<OwnProps> = ({
|
|||||||
src={mediaData}
|
src={mediaData}
|
||||||
canPlay={shouldPlay}
|
canPlay={shouldPlay}
|
||||||
style={colorFilter}
|
style={colorFilter}
|
||||||
|
isPriority={forcePlayback}
|
||||||
loop
|
loop
|
||||||
disablePictureInPicture
|
disablePictureInPicture
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import React, {
|
|||||||
memo, useEffect, useLayoutEffect, useRef, useState,
|
memo, useEffect, useLayoutEffect, useRef, useState,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||||
import { getActions, withGlobal } from '../../../global';
|
import { withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import type { ApiSticker, ApiVideo } from '../../../api/types';
|
import type { ApiSticker, ApiVideo } from '../../../api/types';
|
||||||
@ -10,7 +10,7 @@ import type { GlobalActions } from '../../../global';
|
|||||||
|
|
||||||
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { selectTabState, selectIsCurrentUserPremium, selectIsContextMenuTranslucent } from '../../../global/selectors';
|
import { selectTabState, selectIsContextMenuTranslucent } from '../../../global/selectors';
|
||||||
|
|
||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
@ -69,7 +69,6 @@ export type OwnProps = {
|
|||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
isLeftColumnShown: boolean;
|
isLeftColumnShown: boolean;
|
||||||
isCurrentUserPremium?: boolean;
|
|
||||||
isBackgroundTranslucent?: boolean;
|
isBackgroundTranslucent?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -83,7 +82,6 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
|||||||
canSendGifs,
|
canSendGifs,
|
||||||
isMessageComposer,
|
isMessageComposer,
|
||||||
isLeftColumnShown,
|
isLeftColumnShown,
|
||||||
isCurrentUserPremium,
|
|
||||||
idPrefix,
|
idPrefix,
|
||||||
isAttachmentModal,
|
isAttachmentModal,
|
||||||
canSendPlainText,
|
canSendPlainText,
|
||||||
@ -105,7 +103,6 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
|||||||
addRecentEmoji,
|
addRecentEmoji,
|
||||||
addRecentCustomEmoji,
|
addRecentCustomEmoji,
|
||||||
}) => {
|
}) => {
|
||||||
const { loadPremiumSetStickers } = getActions();
|
|
||||||
const [activeTab, setActiveTab] = useState<number>(0);
|
const [activeTab, setActiveTab] = useState<number>(0);
|
||||||
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
||||||
const [recentCustomEmojis, setRecentCustomEmojis] = useState<string[]>([]);
|
const [recentCustomEmojis, setRecentCustomEmojis] = useState<string[]>([]);
|
||||||
@ -130,12 +127,6 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
|||||||
setActiveTab(STICKERS_TAB_INDEX);
|
setActiveTab(STICKERS_TAB_INDEX);
|
||||||
}, [canSendPlainText]);
|
}, [canSendPlainText]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isCurrentUserPremium) {
|
|
||||||
loadPremiumSetStickers();
|
|
||||||
}
|
|
||||||
}, [isCurrentUserPremium, loadPremiumSetStickers]);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!isMobile || !isOpen || isAttachmentModal) {
|
if (!isMobile || !isOpen || isAttachmentModal) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -356,7 +347,6 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
(global): StateProps => {
|
(global): StateProps => {
|
||||||
return {
|
return {
|
||||||
isLeftColumnShown: selectTabState(global).isLeftColumnShown,
|
isLeftColumnShown: selectTabState(global).isLeftColumnShown,
|
||||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
|
||||||
isBackgroundTranslucent: selectIsContextMenuTranslucent(global),
|
isBackgroundTranslucent: selectIsContextMenuTranslucent(global),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -29,6 +29,7 @@ type OwnProps = {
|
|||||||
canSendStickers?: boolean;
|
canSendStickers?: boolean;
|
||||||
isMessageComposer?: boolean;
|
isMessageComposer?: boolean;
|
||||||
idPrefix: string;
|
idPrefix: string;
|
||||||
|
forceDarkTheme?: boolean;
|
||||||
openSymbolMenu: VoidFunction;
|
openSymbolMenu: VoidFunction;
|
||||||
closeSymbolMenu: VoidFunction;
|
closeSymbolMenu: VoidFunction;
|
||||||
onCustomEmojiSelect: (emoji: ApiSticker) => void;
|
onCustomEmojiSelect: (emoji: ApiSticker) => void;
|
||||||
@ -65,6 +66,7 @@ const SymbolMenuButton: FC<OwnProps> = ({
|
|||||||
canSendPlainText,
|
canSendPlainText,
|
||||||
isSymbolMenuForced,
|
isSymbolMenuForced,
|
||||||
className,
|
className,
|
||||||
|
forceDarkTheme,
|
||||||
inputCssSelector = EDITABLE_INPUT_CSS_SELECTOR,
|
inputCssSelector = EDITABLE_INPUT_CSS_SELECTOR,
|
||||||
openSymbolMenu,
|
openSymbolMenu,
|
||||||
closeSymbolMenu,
|
closeSymbolMenu,
|
||||||
@ -196,7 +198,7 @@ const SymbolMenuButton: FC<OwnProps> = ({
|
|||||||
addRecentCustomEmoji={addRecentCustomEmoji}
|
addRecentCustomEmoji={addRecentCustomEmoji}
|
||||||
isAttachmentModal={isAttachmentModal}
|
isAttachmentModal={isAttachmentModal}
|
||||||
canSendPlainText={canSendPlainText}
|
canSendPlainText={canSendPlainText}
|
||||||
className={className}
|
className={buildClassName(className, forceDarkTheme && 'component-theme-dark')}
|
||||||
positionX={isAttachmentModal ? positionX : undefined}
|
positionX={isAttachmentModal ? positionX : undefined}
|
||||||
positionY={isAttachmentModal ? positionY : undefined}
|
positionY={isAttachmentModal ? positionY : undefined}
|
||||||
transformOriginX={isAttachmentModal ? transformOriginX : undefined}
|
transformOriginX={isAttachmentModal ? transformOriginX : undefined}
|
||||||
|
|||||||
@ -5,7 +5,9 @@ import type { ApiAttachment, ApiFormattedText, ApiMessage } from '../../../../ap
|
|||||||
import { ApiMessageEntityTypes } from '../../../../api/types';
|
import { ApiMessageEntityTypes } from '../../../../api/types';
|
||||||
|
|
||||||
import buildAttachment from '../helpers/buildAttachment';
|
import buildAttachment from '../helpers/buildAttachment';
|
||||||
import { DEBUG, EDITABLE_INPUT_ID, EDITABLE_INPUT_MODAL_ID } from '../../../../config';
|
import {
|
||||||
|
DEBUG, EDITABLE_INPUT_ID, EDITABLE_INPUT_MODAL_ID, EDITABLE_STORY_INPUT_ID,
|
||||||
|
} from '../../../../config';
|
||||||
import getFilesFromDataTransferItems from '../helpers/getFilesFromDataTransferItems';
|
import getFilesFromDataTransferItems from '../helpers/getFilesFromDataTransferItems';
|
||||||
import parseMessageInput, { ENTITY_CLASS_BY_NODE_NAME } from '../../../../util/parseMessageInput';
|
import parseMessageInput, { ENTITY_CLASS_BY_NODE_NAME } from '../../../../util/parseMessageInput';
|
||||||
import cleanDocsHtml from '../../../../lib/cleanDocsHtml';
|
import cleanDocsHtml from '../../../../lib/cleanDocsHtml';
|
||||||
@ -86,7 +88,7 @@ const useClipboardPaste = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const input = document.activeElement;
|
const input = document.activeElement;
|
||||||
if (input && ![EDITABLE_INPUT_ID, EDITABLE_INPUT_MODAL_ID].includes(input.id)) {
|
if (input && ![EDITABLE_INPUT_ID, EDITABLE_INPUT_MODAL_ID, EDITABLE_STORY_INPUT_ID].includes(input.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -66,7 +66,7 @@ const AnimatedEmoji: FC<OwnProps & StateProps> = ({
|
|||||||
noLoad={!isIntersecting}
|
noLoad={!isIntersecting}
|
||||||
forcePreview={forceLoadPreview}
|
forcePreview={forceLoadPreview}
|
||||||
play={isIntersecting}
|
play={isIntersecting}
|
||||||
forceOnHeavyAnimation
|
forceAlways
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={buildClassName('AnimatedEmoji media-inner', sticker?.id === LIKE_STICKER_ID && 'like-sticker-thumb')}
|
className={buildClassName('AnimatedEmoji media-inner', sticker?.id === LIKE_STICKER_ID && 'like-sticker-thumb')}
|
||||||
style={style}
|
style={style}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { getGamePreviewPhotoHash, getGamePreviewVideoHash, getMessageText } from
|
|||||||
|
|
||||||
import useMedia from '../../../hooks/useMedia';
|
import useMedia from '../../../hooks/useMedia';
|
||||||
|
|
||||||
import Skeleton from '../../ui/Skeleton';
|
import Skeleton from '../../ui/placeholder/Skeleton';
|
||||||
|
|
||||||
import './Game.scss';
|
import './Game.scss';
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import useLang from '../../../hooks/useLang';
|
|||||||
import useMedia from '../../../hooks/useMedia';
|
import useMedia from '../../../hooks/useMedia';
|
||||||
import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef';
|
import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef';
|
||||||
|
|
||||||
import Skeleton from '../../ui/Skeleton';
|
import Skeleton from '../../ui/placeholder/Skeleton';
|
||||||
|
|
||||||
import './Invoice.scss';
|
import './Invoice.scss';
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import {
|
|||||||
} from '../../../global/helpers';
|
} from '../../../global/helpers';
|
||||||
import { formatCountdownShort, formatLastUpdated } from '../../../util/dateFormat';
|
import { formatCountdownShort, formatLastUpdated } from '../../../util/dateFormat';
|
||||||
import {
|
import {
|
||||||
getMetersPerPixel, getVenueColor, getVenueIconUrl, prepareMapUrl,
|
getMetersPerPixel, getVenueColor, getVenueIconUrl,
|
||||||
} from '../../../util/map';
|
} from '../../../util/map';
|
||||||
import { getServerTime } from '../../../util/serverTime';
|
import { getServerTime } from '../../../util/serverTime';
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ import usePrevious from '../../../hooks/usePrevious';
|
|||||||
import useInterval from '../../../hooks/useInterval';
|
import useInterval from '../../../hooks/useInterval';
|
||||||
|
|
||||||
import Avatar from '../../common/Avatar';
|
import Avatar from '../../common/Avatar';
|
||||||
import Skeleton from '../../ui/Skeleton';
|
import Skeleton from '../../ui/placeholder/Skeleton';
|
||||||
|
|
||||||
import './Location.scss';
|
import './Location.scss';
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ const Location: FC<OwnProps> = ({
|
|||||||
message,
|
message,
|
||||||
peer,
|
peer,
|
||||||
}) => {
|
}) => {
|
||||||
const { openUrl } = getActions();
|
const { openMapModal } = 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);
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -95,8 +95,7 @@ const Location: FC<OwnProps> = ({
|
|||||||
}, [type, point, zoom]);
|
}, [type, point, zoom]);
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
const url = prepareMapUrl(point.lat, point.long, zoom);
|
openMapModal({ geoPoint: point, zoom });
|
||||||
openUrl({ url });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateCountdown = useLastCallback((countdownEl: HTMLDivElement) => {
|
const updateCountdown = useLastCallback((countdownEl: HTMLDivElement) => {
|
||||||
@ -194,6 +193,7 @@ const Location: FC<OwnProps> = ({
|
|||||||
className="full-media map"
|
className="full-media map"
|
||||||
src={mapBlobUrl}
|
src={mapBlobUrl}
|
||||||
alt="Location on a map"
|
alt="Location on a map"
|
||||||
|
draggable={false}
|
||||||
style={`width: ${DEFAULT_MAP_CONFIG.width}px; height: ${DEFAULT_MAP_CONFIG.height}px;`}
|
style={`width: ${DEFAULT_MAP_CONFIG.width}px; height: ${DEFAULT_MAP_CONFIG.height}px;`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -224,14 +224,14 @@ const Location: FC<OwnProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className={pinClassName} style={`--pin-color: ${color}`}>
|
<div className={pinClassName} style={`--pin-color: ${color}`}>
|
||||||
<PinSvg />
|
<PinSvg />
|
||||||
<img src={iconSrc} className="venue-icon" alt="" />
|
<img src={iconSrc} draggable={false} className="venue-icon" alt="" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<img className={pinClassName} src={mapPin} alt="" />
|
<img className={pinClassName} draggable={false} src={mapPin} alt="" />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,7 +33,7 @@ import useAppLayout from '../../../hooks/useAppLayout';
|
|||||||
import Menu from '../../ui/Menu';
|
import Menu from '../../ui/Menu';
|
||||||
import MenuItem from '../../ui/MenuItem';
|
import MenuItem from '../../ui/MenuItem';
|
||||||
import MenuSeparator from '../../ui/MenuSeparator';
|
import MenuSeparator from '../../ui/MenuSeparator';
|
||||||
import Skeleton from '../../ui/Skeleton';
|
import Skeleton from '../../ui/placeholder/Skeleton';
|
||||||
import ReactionSelector from './ReactionSelector';
|
import ReactionSelector from './ReactionSelector';
|
||||||
import AvatarList from '../../common/AvatarList';
|
import AvatarList from '../../common/AvatarList';
|
||||||
|
|
||||||
|
|||||||
@ -135,7 +135,7 @@ const ReactionAnimatedEmoji: FC<OwnProps> = ({
|
|||||||
tgsUrl={mediaDataEffect}
|
tgsUrl={mediaDataEffect}
|
||||||
play={isIntersecting}
|
play={isIntersecting}
|
||||||
noLoop
|
noLoop
|
||||||
forceOnHeavyAnimation
|
forceAlways
|
||||||
onEnded={handleEnded}
|
onEnded={handleEnded}
|
||||||
/>
|
/>
|
||||||
{isCustom ? (
|
{isCustom ? (
|
||||||
@ -148,7 +148,7 @@ const ReactionAnimatedEmoji: FC<OwnProps> = ({
|
|||||||
tgsUrl={mediaDataCenterIcon}
|
tgsUrl={mediaDataCenterIcon}
|
||||||
play={isIntersecting}
|
play={isIntersecting}
|
||||||
noLoop
|
noLoop
|
||||||
forceOnHeavyAnimation
|
forceAlways
|
||||||
onLoad={markAnimationLoaded}
|
onLoad={markAnimationLoaded}
|
||||||
onEnded={unmarkAnimationLoaded}
|
onEnded={unmarkAnimationLoaded}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -41,6 +41,7 @@ interface StateProps {
|
|||||||
isCurrentUserPremium?: boolean;
|
isCurrentUserPremium?: boolean;
|
||||||
position?: IAnchorPosition;
|
position?: IAnchorPosition;
|
||||||
isTranslucent?: boolean;
|
isTranslucent?: boolean;
|
||||||
|
sendAsMessage?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FULL_PICKER_SHIFT_DELTA = { x: -23, y: -64 };
|
const FULL_PICKER_SHIFT_DELTA = { x: -23, y: -64 };
|
||||||
@ -55,9 +56,10 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
|
|||||||
isTranslucent,
|
isTranslucent,
|
||||||
isCurrentUserPremium,
|
isCurrentUserPremium,
|
||||||
withCustomReactions,
|
withCustomReactions,
|
||||||
|
sendAsMessage,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
toggleReaction, closeReactionPicker, sendMessage, showNotification,
|
toggleReaction, closeReactionPicker, sendMessage, showNotification, sendStoryReaction,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
@ -121,33 +123,42 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
|
|||||||
closeReactionPicker();
|
closeReactionPicker();
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleStoryReactionSelect = useLastCallback((reaction: ApiReaction | ApiSticker) => {
|
const handleStoryReactionSelect = useLastCallback((item: ApiReaction | ApiSticker) => {
|
||||||
|
const reaction = 'id' in item ? { documentId: item.id } : item;
|
||||||
|
|
||||||
|
const sticker = 'documentId' in item
|
||||||
|
? getGlobal().customEmojis.byId[item.documentId] : 'emoticon' in item ? undefined : item;
|
||||||
|
|
||||||
|
if (sticker && !sticker.isFree && !isCurrentUserPremium) {
|
||||||
|
showNotification({
|
||||||
|
message: lang('UnlockPremiumEmojiHint'),
|
||||||
|
action: {
|
||||||
|
action: 'openPremiumModal',
|
||||||
|
payload: { initialSection: 'animated_emoji' },
|
||||||
|
},
|
||||||
|
actionText: lang('PremiumMore'),
|
||||||
|
});
|
||||||
|
|
||||||
|
closeReactionPicker();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sendAsMessage) {
|
||||||
|
sendStoryReaction({
|
||||||
|
userId: renderedStoryUserId!, storyId: renderedStoryId!, reaction, shouldAddToRecent: true,
|
||||||
|
});
|
||||||
|
closeReactionPicker();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let text: string | undefined;
|
let text: string | undefined;
|
||||||
let entities: ApiMessageEntity[] | undefined;
|
let entities: ApiMessageEntity[] | undefined;
|
||||||
|
|
||||||
if ('emoticon' in reaction) {
|
if ('emoticon' in item) {
|
||||||
text = reaction.emoticon;
|
text = item.emoticon;
|
||||||
} else {
|
} else {
|
||||||
const sticker = 'documentId' in reaction ? getGlobal().customEmojis.byId[reaction.documentId] : reaction;
|
const customEmojiMessage = parseMessageInput(buildCustomEmojiHtml(sticker!));
|
||||||
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;
|
text = customEmojiMessage.text;
|
||||||
entities = customEmojiMessage.entities;
|
entities = customEmojiMessage.entities;
|
||||||
}
|
}
|
||||||
@ -212,7 +223,7 @@ 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 {
|
const {
|
||||||
chatId, messageId, storyUserId, storyId, position,
|
chatId, messageId, storyUserId, storyId, position, sendAsMessage,
|
||||||
} = state.reactionPicker || {};
|
} = state.reactionPicker || {};
|
||||||
const story = storyUserId && storyId
|
const story = storyUserId && storyId
|
||||||
? selectUserStory(global, storyUserId, storyId) as ApiStory | ApiStorySkipped
|
? selectUserStory(global, storyUserId, storyId) as ApiStory | ApiStorySkipped
|
||||||
@ -234,6 +245,7 @@ export default memo(withGlobal<OwnProps>((global): StateProps => {
|
|||||||
: areCustomReactionsAllowed || isPrivateChat,
|
: areCustomReactionsAllowed || isPrivateChat,
|
||||||
isTranslucent: selectIsContextMenuTranslucent(global),
|
isTranslucent: selectIsContextMenuTranslucent(global),
|
||||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||||
|
sendAsMessage,
|
||||||
};
|
};
|
||||||
})(ReactionPicker));
|
})(ReactionPicker));
|
||||||
|
|
||||||
|
|||||||
@ -64,6 +64,7 @@ const ReactionSelectorReaction: FC<OwnProps> = ({
|
|||||||
noLoop
|
noLoop
|
||||||
size={REACTION_SIZE}
|
size={REACTION_SIZE}
|
||||||
onEnded={unmarkIsFirstPlay}
|
onEnded={unmarkIsFirstPlay}
|
||||||
|
forceAlways
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isFirstPlay && !noAppearAnimation && (
|
{!isFirstPlay && !noAppearAnimation && (
|
||||||
@ -75,6 +76,7 @@ const ReactionSelectorReaction: FC<OwnProps> = ({
|
|||||||
size={REACTION_SIZE}
|
size={REACTION_SIZE}
|
||||||
onLoad={markAnimationLoaded}
|
onLoad={markAnimationLoaded}
|
||||||
onEnded={deactivate}
|
onEnded={deactivate}
|
||||||
|
forceAlways
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -88,7 +88,7 @@ export function getMessageCopyOptions(
|
|||||||
document.execCommand('copy');
|
document.execCommand('copy');
|
||||||
} else {
|
} else {
|
||||||
const clipboardText = renderMessageText(
|
const clipboardText = renderMessageText(
|
||||||
message, undefined, undefined, undefined, undefined, undefined, true,
|
{ message, shouldRenderAsHtml: true },
|
||||||
);
|
);
|
||||||
if (clipboardText) copyHtmlToClipboard(clipboardText.join(''), getMessageTextWithSpoilers(message)!);
|
if (clipboardText) copyHtmlToClipboard(clipboardText.join(''), getMessageTextWithSpoilers(message)!);
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/components/modals/mapModal/MapModal.async.tsx
Normal file
17
src/components/modals/mapModal/MapModal.async.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import React from '../../../lib/teact/teact';
|
||||||
|
import { Bundles } from '../../../util/moduleLoader';
|
||||||
|
|
||||||
|
import type { OwnProps } from './MapModal';
|
||||||
|
|
||||||
|
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||||
|
|
||||||
|
const MapModalAsync: FC<OwnProps> = (props) => {
|
||||||
|
const { geoPoint } = props;
|
||||||
|
const MapModal = useModuleLoader(Bundles.Extra, 'MapModal', !geoPoint);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return MapModal ? <MapModal {...props} /> : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MapModalAsync;
|
||||||
10
src/components/modals/mapModal/MapModal.module.scss
Normal file
10
src/components/modals/mapModal/MapModal.module.scss
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
100
src/components/modals/mapModal/MapModal.tsx
Normal file
100
src/components/modals/mapModal/MapModal.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import React, { memo, useMemo } from '../../../lib/teact/teact';
|
||||||
|
import { getActions } from '../../../global';
|
||||||
|
|
||||||
|
import type { ApiGeoPoint } from '../../../api/types';
|
||||||
|
|
||||||
|
import { IS_IOS, IS_MAC_OS } from '../../../util/windowEnvironment';
|
||||||
|
import { prepareMapUrl } from '../../../util/map';
|
||||||
|
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
|
||||||
|
import Modal from '../../ui/Modal';
|
||||||
|
import Button from '../../ui/Button';
|
||||||
|
|
||||||
|
import styles from './MapModal.module.scss';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
geoPoint?: ApiGeoPoint;
|
||||||
|
zoom?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const OpenMapModal = ({ geoPoint, zoom }: OwnProps) => {
|
||||||
|
const { closeMapModal } = getActions();
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const isOpen = Boolean(geoPoint);
|
||||||
|
|
||||||
|
const handleClose = useLastCallback(() => {
|
||||||
|
closeMapModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
const [googleUrl, bingUrl, appleUrl, osmUrl] = useMemo(() => {
|
||||||
|
if (!geoPoint) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const google = prepareMapUrl('google', geoPoint, zoom);
|
||||||
|
const bing = prepareMapUrl('bing', geoPoint, zoom);
|
||||||
|
const osm = prepareMapUrl('osm', geoPoint, zoom);
|
||||||
|
const apple = prepareMapUrl('apple', geoPoint, zoom);
|
||||||
|
|
||||||
|
return [google, bing, apple, osm];
|
||||||
|
}, [geoPoint, zoom]);
|
||||||
|
|
||||||
|
const openUrl = useLastCallback((url: string) => {
|
||||||
|
closeMapModal();
|
||||||
|
window.open(url, '_blank', 'noopener');
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleGoogleClick = useLastCallback(() => {
|
||||||
|
openUrl(googleUrl!);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleBingClick = useLastCallback(() => {
|
||||||
|
openUrl(bingUrl!);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAppleClick = useLastCallback(() => {
|
||||||
|
openUrl(appleUrl!);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleOsmClick = useLastCallback(() => {
|
||||||
|
openUrl(osmUrl!);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
contentClassName={styles.root}
|
||||||
|
title={lang('OpenMapWith')}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
isSlim
|
||||||
|
>
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
{(IS_IOS || IS_MAC_OS) && (
|
||||||
|
<Button fluid size="smaller" onClick={handleAppleClick}>
|
||||||
|
Apple Maps
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button fluid size="smaller" onClick={handleGoogleClick}>
|
||||||
|
Google Maps
|
||||||
|
</Button>
|
||||||
|
<Button fluid size="smaller" onClick={handleBingClick}>
|
||||||
|
Bing Maps
|
||||||
|
</Button>
|
||||||
|
<Button fluid size="smaller" onClick={handleOsmClick}>
|
||||||
|
Open Street Maps
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="dialog-buttons mt-2">
|
||||||
|
<Button className="confirm-dialog-button" isText onClick={handleClose}>
|
||||||
|
{lang('Cancel')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(OpenMapModal);
|
||||||
@ -16,7 +16,7 @@ import useLang from '../../hooks/useLang';
|
|||||||
import useMedia from '../../hooks/useMedia';
|
import useMedia from '../../hooks/useMedia';
|
||||||
|
|
||||||
import Checkbox from '../ui/Checkbox';
|
import Checkbox from '../ui/Checkbox';
|
||||||
import Skeleton from '../ui/Skeleton';
|
import Skeleton from '../ui/placeholder/Skeleton';
|
||||||
import SafeLink from '../common/SafeLink';
|
import SafeLink from '../common/SafeLink';
|
||||||
import ListItem from '../ui/ListItem';
|
import ListItem from '../ui/ListItem';
|
||||||
|
|
||||||
|
|||||||
@ -38,6 +38,7 @@ import {
|
|||||||
selectTabState,
|
selectTabState,
|
||||||
selectTheme,
|
selectTheme,
|
||||||
selectUser,
|
selectUser,
|
||||||
|
selectUserFullInfo,
|
||||||
selectUserStories,
|
selectUserStories,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
import { captureEvents, SwipeDirection } from '../../util/captureEvents';
|
import { captureEvents, SwipeDirection } from '../../util/captureEvents';
|
||||||
@ -610,8 +611,9 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
if (isUserId(chatId)) {
|
if (isUserId(chatId)) {
|
||||||
resolvedUserId = chatId;
|
resolvedUserId = chatId;
|
||||||
user = selectUser(global, resolvedUserId);
|
user = selectUser(global, resolvedUserId);
|
||||||
|
const userFullInfo = selectUserFullInfo(global, chatId);
|
||||||
hasCommonChatsTab = user && !user.isSelf && !isUserBot(user);
|
hasCommonChatsTab = user && !user.isSelf && !isUserBot(user);
|
||||||
hasStoriesTab = user?.isSelf || (user?.hasStories && !user.areStoriesHidden);
|
hasStoriesTab = user && (user.isSelf || (!user.areStoriesHidden && userFullInfo?.hasPinnedStories));
|
||||||
const userStories = hasStoriesTab ? selectUserStories(global, user!.id) : undefined;
|
const userStories = hasStoriesTab ? selectUserStories(global, user!.id) : undefined;
|
||||||
storyIds = userStories?.pinnedIds;
|
storyIds = userStories?.pinnedIds;
|
||||||
storyByIds = userStories?.byId;
|
storyByIds = userStories?.byId;
|
||||||
|
|||||||
55
src/components/story/MediaAreaOverlay.tsx
Normal file
55
src/components/story/MediaAreaOverlay.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import React, { memo } from '../../lib/teact/teact';
|
||||||
|
import { getActions } from '../../global';
|
||||||
|
|
||||||
|
import type { ApiMediaArea } from '../../api/types';
|
||||||
|
import type { IDimensions } from '../../global/types';
|
||||||
|
|
||||||
|
import buildStyle from '../../util/buildStyle';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
|
||||||
|
import styles from './StoryViewer.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
mediaAreas?: ApiMediaArea[];
|
||||||
|
mediaDimensions: IDimensions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MediaAreaOverlay = ({ mediaAreas, mediaDimensions }: OwnProps) => {
|
||||||
|
const { openMapModal } = getActions();
|
||||||
|
const handleMediaAreaClick = (mediaArea: ApiMediaArea) => {
|
||||||
|
if (mediaArea.geo) {
|
||||||
|
openMapModal({ geoPoint: mediaArea.geo });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={buildClassName(styles.mediaAreaOverlay, styles.media)}
|
||||||
|
style={buildStyle(`aspect-ratio: ${mediaDimensions.width} / ${mediaDimensions.height}`)}
|
||||||
|
>
|
||||||
|
{mediaAreas?.map((mediaArea) => (
|
||||||
|
<div
|
||||||
|
className={styles.mediaArea}
|
||||||
|
style={prepareStyle(mediaArea)}
|
||||||
|
onClick={() => handleMediaAreaClick(mediaArea)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function prepareStyle(mediaArea: ApiMediaArea) {
|
||||||
|
const {
|
||||||
|
x, y, width, height, rotation,
|
||||||
|
} = mediaArea.coordinates;
|
||||||
|
|
||||||
|
return buildStyle(
|
||||||
|
`left: ${x}%`,
|
||||||
|
`top: ${y}%`,
|
||||||
|
`width: ${width}%`,
|
||||||
|
`height: ${height}%`,
|
||||||
|
`transform: rotate(${rotation}deg) translate(-50%, -50%)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(MediaAreaOverlay);
|
||||||
53
src/components/story/StealthModeModal.module.scss
Normal file
53
src/components/story/StealthModeModal.module.scss
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stealthIcon {
|
||||||
|
width: 5rem;
|
||||||
|
height: 5rem;
|
||||||
|
font-size: 3rem;
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.listItem {
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: var(--color-primary) !important;
|
||||||
|
margin-right: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
line-height: 1.25rem !important;
|
||||||
|
}
|
||||||
135
src/components/story/StealthModeModal.tsx
Normal file
135
src/components/story/StealthModeModal.tsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import React, { memo, useEffect, useState } from '../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
|
import type { ApiStealthMode } from '../../api/types';
|
||||||
|
|
||||||
|
import { selectIsCurrentUserPremium, selectTabState } from '../../global/selectors';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import { getServerTime } from '../../util/serverTime';
|
||||||
|
|
||||||
|
import useLang from '../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../hooks/useLastCallback';
|
||||||
|
|
||||||
|
import Modal from '../ui/Modal';
|
||||||
|
import Button from '../ui/Button';
|
||||||
|
import ListItem from '../ui/ListItem';
|
||||||
|
import TextTimer from '../ui/TextTimer';
|
||||||
|
|
||||||
|
import styles from './StealthModeModal.module.scss';
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
isOpen?: boolean;
|
||||||
|
stealthMode?: ApiStealthMode;
|
||||||
|
isCurrentUserPremium?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StealthModeModal = ({ isOpen, stealthMode, isCurrentUserPremium } : StateProps) => {
|
||||||
|
const {
|
||||||
|
toggleStealthModal,
|
||||||
|
activateStealthMode,
|
||||||
|
showNotification,
|
||||||
|
openPremiumModal,
|
||||||
|
} = getActions();
|
||||||
|
const [isOnCooldown, setIsOnCooldown] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!stealthMode) return;
|
||||||
|
const serverTime = getServerTime();
|
||||||
|
if (stealthMode.cooldownUntil && stealthMode.cooldownUntil > serverTime) {
|
||||||
|
setIsOnCooldown(true);
|
||||||
|
}
|
||||||
|
}, [stealthMode, isOpen]);
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const handleTimerEnds = useLastCallback(() => {
|
||||||
|
setIsOnCooldown(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = useLastCallback(() => {
|
||||||
|
toggleStealthModal({ isOpen: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleActivate = useLastCallback(() => {
|
||||||
|
if (!isCurrentUserPremium) {
|
||||||
|
openPremiumModal({ initialSection: 'stories' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activateStealthMode();
|
||||||
|
showNotification({
|
||||||
|
title: lang('StealthModeOn'),
|
||||||
|
message: lang('StealthModeOnHint'),
|
||||||
|
});
|
||||||
|
toggleStealthModal({ isOpen: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className="component-theme-dark"
|
||||||
|
contentClassName={styles.root}
|
||||||
|
isOpen={isOpen}
|
||||||
|
isSlim
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
color="translucent"
|
||||||
|
size="smaller"
|
||||||
|
className={styles.close}
|
||||||
|
ariaLabel={lang('Close')}
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
<i className="icon icon-close" />
|
||||||
|
</Button>
|
||||||
|
<div className={styles.stealthIcon}>
|
||||||
|
<i className="icon icon-eye-closed-outline" />
|
||||||
|
</div>
|
||||||
|
<div className={styles.title}>{lang('StealthMode')}</div>
|
||||||
|
<div className={styles.description}>
|
||||||
|
{lang(isCurrentUserPremium ? 'StealthModeHint' : 'StealthModePremiumHint')}
|
||||||
|
</div>
|
||||||
|
<ListItem
|
||||||
|
className={buildClassName(styles.listItem, 'smaller-icon')}
|
||||||
|
multiline
|
||||||
|
inactive
|
||||||
|
leftElement={<i className={buildClassName('icon icon-stealth-past', styles.icon)} />}
|
||||||
|
>
|
||||||
|
<span className="title">{lang('HideRecentViews')}</span>
|
||||||
|
<span className={buildClassName('subtitle', styles.subtitle)}>{lang('HideRecentViewsDescription')}</span>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
className={buildClassName(styles.listItem, 'smaller-icon')}
|
||||||
|
multiline
|
||||||
|
inactive
|
||||||
|
leftElement={<i className={buildClassName('icon icon-stealth-future', styles.icon)} aria-hidden />}
|
||||||
|
>
|
||||||
|
<span className="title">{lang('HideNextViews')}</span>
|
||||||
|
<span className={buildClassName('subtitle', styles.subtitle)}>{lang('HideNextViewsDescription')}</span>
|
||||||
|
</ListItem>
|
||||||
|
<Button
|
||||||
|
className={styles.button}
|
||||||
|
size="smaller"
|
||||||
|
disabled={isOnCooldown}
|
||||||
|
isShiny={!isCurrentUserPremium}
|
||||||
|
withPremiumGradient={!isCurrentUserPremium}
|
||||||
|
onClick={handleActivate}
|
||||||
|
>
|
||||||
|
{!isCurrentUserPremium ? lang('UnlockStealthMode')
|
||||||
|
: isOnCooldown
|
||||||
|
? (<TextTimer langKey="AvailableIn" endsAt={stealthMode!.cooldownUntil!} onEnd={handleTimerEnds} />)
|
||||||
|
: lang('EnableStealthMode')}
|
||||||
|
</Button>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal((global): StateProps => {
|
||||||
|
const tabState = selectTabState(global);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isOpen: tabState.storyViewer?.isStealthModalOpen,
|
||||||
|
stealthMode: global.stories.stealthMode,
|
||||||
|
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||||
|
};
|
||||||
|
})(StealthModeModal));
|
||||||
@ -4,20 +4,24 @@ import React, {
|
|||||||
import { getActions, getGlobal, 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 { ApiStory, ApiTypeStory, ApiUser } from '../../api/types';
|
import type {
|
||||||
|
ApiStealthMode, ApiStory, ApiTypeStory, ApiUser,
|
||||||
|
} from '../../api/types';
|
||||||
import type { IDimensions } from '../../global/types';
|
import type { IDimensions } from '../../global/types';
|
||||||
import type { Signal } from '../../util/signals';
|
import type { Signal } from '../../util/signals';
|
||||||
|
|
||||||
import { MAIN_THREAD_ID } from '../../api/types';
|
import { MAIN_THREAD_ID } from '../../api/types';
|
||||||
|
|
||||||
|
import { EDITABLE_STORY_INPUT_CSS_SELECTOR, EDITABLE_STORY_INPUT_ID } from '../../config';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import renderText from '../common/helpers/renderText';
|
import renderText from '../common/helpers/renderText';
|
||||||
|
import { formatMediaDuration, formatRelativeTime } from '../../util/dateFormat';
|
||||||
import { getUserFirstOrLastName } from '../../global/helpers';
|
import { getUserFirstOrLastName } from '../../global/helpers';
|
||||||
import { formatRelativeTime } from '../../util/dateFormat';
|
|
||||||
import { getServerTime } from '../../util/serverTime';
|
import { getServerTime } from '../../util/serverTime';
|
||||||
import {
|
import {
|
||||||
selectChat, selectTabState, selectUserStory, selectUserStories, selectIsCurrentUserPremium,
|
selectChat, selectTabState, selectUserStory, selectUserStories, selectIsCurrentUserPremium,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
import captureKeyboardListeners from '../../util/captureKeyboardListeners';
|
import captureKeyboardListeners from '../../util/captureKeyboardListeners';
|
||||||
|
import download from '../../util/download';
|
||||||
|
|
||||||
import useAppLayout, { getIsMobile } from '../../hooks/useAppLayout';
|
import useAppLayout, { getIsMobile } from '../../hooks/useAppLayout';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
@ -33,7 +37,7 @@ import useLongPress from '../../hooks/useLongPress';
|
|||||||
import useUnsupportedMedia from '../../hooks/media/useUnsupportedMedia';
|
import useUnsupportedMedia from '../../hooks/media/useUnsupportedMedia';
|
||||||
import useCanvasBlur from '../../hooks/useCanvasBlur';
|
import useCanvasBlur from '../../hooks/useCanvasBlur';
|
||||||
import useMediaTransition from '../../hooks/useMediaTransition';
|
import useMediaTransition from '../../hooks/useMediaTransition';
|
||||||
import { useStoryProps } from './hooks/useStoryProps';
|
import useStoryProps from './hooks/useStoryProps';
|
||||||
|
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import Avatar from '../common/Avatar';
|
import Avatar from '../common/Avatar';
|
||||||
@ -42,9 +46,10 @@ import StoryProgress from './StoryProgress';
|
|||||||
import Composer from '../common/Composer';
|
import Composer from '../common/Composer';
|
||||||
import MenuItem from '../ui/MenuItem';
|
import MenuItem from '../ui/MenuItem';
|
||||||
import DropdownMenu from '../ui/DropdownMenu';
|
import DropdownMenu from '../ui/DropdownMenu';
|
||||||
import Skeleton from '../ui/Skeleton';
|
import Skeleton from '../ui/placeholder/Skeleton';
|
||||||
import StoryCaption from './StoryCaption';
|
import StoryCaption from './StoryCaption';
|
||||||
import AvatarList from '../common/AvatarList';
|
import AvatarList from '../common/AvatarList';
|
||||||
|
import MediaAreaOverlay from './MediaAreaOverlay';
|
||||||
|
|
||||||
import styles from './StoryViewer.module.scss';
|
import styles from './StoryViewer.module.scss';
|
||||||
|
|
||||||
@ -77,6 +82,7 @@ interface StateProps {
|
|||||||
isChatExist?: boolean;
|
isChatExist?: boolean;
|
||||||
areChatSettingsLoaded?: boolean;
|
areChatSettingsLoaded?: boolean;
|
||||||
isCurrentUserPremium?: boolean;
|
isCurrentUserPremium?: boolean;
|
||||||
|
stealthMode: ApiStealthMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const VIDEO_MIN_READY_STATE = 4;
|
const VIDEO_MIN_READY_STATE = 4;
|
||||||
@ -85,6 +91,8 @@ const SPACEBAR_CODE = 32;
|
|||||||
const PRIMARY_VIDEO_MIME = 'video/mp4; codecs=hvc1.1.6.L63.00';
|
const PRIMARY_VIDEO_MIME = 'video/mp4; codecs=hvc1.1.6.L63.00';
|
||||||
const SECONDARY_VIDEO_MIME = 'video/mp4; codecs=avc1.64001E';
|
const SECONDARY_VIDEO_MIME = 'video/mp4; codecs=avc1.64001E';
|
||||||
|
|
||||||
|
const STEALTH_MODE_NOTIFICATION_DURATION = 4000;
|
||||||
|
|
||||||
function Story({
|
function Story({
|
||||||
isSelf,
|
isSelf,
|
||||||
userId,
|
userId,
|
||||||
@ -104,6 +112,7 @@ function Story({
|
|||||||
areChatSettingsLoaded,
|
areChatSettingsLoaded,
|
||||||
getIsAnimating,
|
getIsAnimating,
|
||||||
isCurrentUserPremium,
|
isCurrentUserPremium,
|
||||||
|
stealthMode,
|
||||||
onDelete,
|
onDelete,
|
||||||
onClose,
|
onClose,
|
||||||
onReport,
|
onReport,
|
||||||
@ -115,7 +124,7 @@ function Story({
|
|||||||
openNextStory,
|
openNextStory,
|
||||||
loadUserSkippedStories,
|
loadUserSkippedStories,
|
||||||
openForwardMenu,
|
openForwardMenu,
|
||||||
openStorySeenBy,
|
openStoryViewModal,
|
||||||
copyStoryLink,
|
copyStoryLink,
|
||||||
toggleStoryPinned,
|
toggleStoryPinned,
|
||||||
openChat,
|
openChat,
|
||||||
@ -123,7 +132,8 @@ function Story({
|
|||||||
openStoryPrivacyEditor,
|
openStoryPrivacyEditor,
|
||||||
loadChatSettings,
|
loadChatSettings,
|
||||||
fetchChat,
|
fetchChat,
|
||||||
loadStorySeenBy,
|
loadStoryViews,
|
||||||
|
toggleStealthModal,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
const serverTime = getServerTime();
|
const serverTime = getServerTime();
|
||||||
|
|
||||||
@ -137,6 +147,7 @@ function Story({
|
|||||||
const [isCaptionExpanded, expandCaption, foldCaption] = useFlag(false);
|
const [isCaptionExpanded, expandCaption, foldCaption] = useFlag(false);
|
||||||
const [isPausedBySpacebar, setIsPausedBySpacebar] = useState(false);
|
const [isPausedBySpacebar, setIsPausedBySpacebar] = useState(false);
|
||||||
const [isPausedByLongPress, markIsPausedByLongPress, unmarkIsPausedByLongPress] = useFlag(false);
|
const [isPausedByLongPress, markIsPausedByLongPress, unmarkIsPausedByLongPress] = useFlag(false);
|
||||||
|
const [isDropdownMenuOpen, markDropdownMenuOpen, unmarkDropdownMenuOpen] = useFlag(false);
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
const {
|
const {
|
||||||
@ -151,10 +162,15 @@ function Story({
|
|||||||
altMediaData,
|
altMediaData,
|
||||||
hasFullData,
|
hasFullData,
|
||||||
hasThumb,
|
hasThumb,
|
||||||
} = useStoryProps(story);
|
mediaAreas,
|
||||||
|
canDownload,
|
||||||
|
downloadMediaData,
|
||||||
|
} = useStoryProps(story, isCurrentUserPremium, isDropdownMenuOpen);
|
||||||
|
|
||||||
const isLoadedStory = story && 'content' in story;
|
const isLoadedStory = story && 'content' in story;
|
||||||
|
|
||||||
|
const isChangelog = userId === storyChangelogUserId;
|
||||||
|
|
||||||
const canPinToProfile = useCurrentOrPrev(
|
const canPinToProfile = useCurrentOrPrev(
|
||||||
isSelf && isLoadedStory ? !story.isPinned : undefined,
|
isSelf && isLoadedStory ? !story.isPinned : undefined,
|
||||||
true,
|
true,
|
||||||
@ -164,12 +180,12 @@ function Story({
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
const areViewsExpired = Boolean(
|
const areViewsExpired = Boolean(
|
||||||
isSelf && !isCurrentUserPremium && isLoadedStory && (story!.date + viewersExpirePeriod) < getServerTime(),
|
isSelf && isLoadedStory && (story!.date + viewersExpirePeriod) < getServerTime(),
|
||||||
);
|
);
|
||||||
const canCopyLink = Boolean(
|
const canCopyLink = Boolean(
|
||||||
isLoadedStory
|
isLoadedStory
|
||||||
&& story.isPublic
|
&& story.isPublic
|
||||||
&& userId !== storyChangelogUserId
|
&& !isChangelog
|
||||||
&& user?.usernames?.length,
|
&& user?.usernames?.length,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -177,17 +193,24 @@ function Story({
|
|||||||
isLoadedStory
|
isLoadedStory
|
||||||
&& story.isPublic
|
&& story.isPublic
|
||||||
&& !story.noForwards
|
&& !story.noForwards
|
||||||
&& userId !== storyChangelogUserId
|
&& !isChangelog
|
||||||
&& !isCaptionExpanded,
|
&& !isCaptionExpanded,
|
||||||
);
|
);
|
||||||
|
const canShareOwn = Boolean(
|
||||||
|
isSelf
|
||||||
|
&& isLoadedStory
|
||||||
|
&& story.isPublic
|
||||||
|
&& !story.noForwards,
|
||||||
|
);
|
||||||
|
|
||||||
const canPlayStory = Boolean(
|
const canPlayStory = Boolean(
|
||||||
hasFullData && !shouldForcePause && isAppFocused && !isComposerHasFocus && !isCaptionExpanded
|
hasFullData && !shouldForcePause && isAppFocused && !isComposerHasFocus && !isCaptionExpanded
|
||||||
&& !isPausedBySpacebar && !isPausedByLongPress,
|
&& !isPausedBySpacebar && !isPausedByLongPress,
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
shouldRender: shouldRenderSkeleton, transitionClassNames: skeletonTransitionClassNames,
|
shouldRender: shouldRenderSkeleton, transitionClassNames: skeletonTransitionClassNames,
|
||||||
} = useShowTransition((isVideo && !hasFullData) || (!isVideo && !previewBlobUrl));
|
} = useShowTransition(!hasFullData);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
transitionClassNames: mediaTransitionClassNames,
|
transitionClassNames: mediaTransitionClassNames,
|
||||||
@ -199,7 +222,7 @@ function Story({
|
|||||||
const {
|
const {
|
||||||
shouldRender: shouldRenderComposer,
|
shouldRender: shouldRenderComposer,
|
||||||
transitionClassNames: composerAppearanceAnimationClassNames,
|
transitionClassNames: composerAppearanceAnimationClassNames,
|
||||||
} = useShowTransition(!isSelf);
|
} = useShowTransition(!isSelf && !isChangelog);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
shouldRender: shouldRenderCaptionBackdrop,
|
shouldRender: shouldRenderCaptionBackdrop,
|
||||||
@ -255,6 +278,16 @@ function Story({
|
|||||||
unmarkIsPausedByLongPress();
|
unmarkIsPausedByLongPress();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleDropdownMenuOpen = useLastCallback(() => {
|
||||||
|
markDropdownMenuOpen();
|
||||||
|
handlePauseStory();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDropdownMenuClose = useLastCallback(() => {
|
||||||
|
unmarkDropdownMenuOpen();
|
||||||
|
handlePlayStory();
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
onMouseDown: handleLongPressMouseDown,
|
onMouseDown: handleLongPressMouseDown,
|
||||||
onMouseUp: handleLongPressMouseUp,
|
onMouseUp: handleLongPressMouseUp,
|
||||||
@ -279,7 +312,7 @@ function Story({
|
|||||||
if (!isSelf || isDeletedStory || areViewsExpired) return;
|
if (!isSelf || isDeletedStory || areViewsExpired) return;
|
||||||
|
|
||||||
// Refresh recent viewers list each time
|
// Refresh recent viewers list each time
|
||||||
loadStorySeenBy({ storyId });
|
loadStoryViews({ storyId, isPreload: true });
|
||||||
}, [isDeletedStory, areViewsExpired, isSelf, storyId]);
|
}, [isDeletedStory, areViewsExpired, isSelf, storyId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -394,8 +427,8 @@ function Story({
|
|||||||
handlePauseStory();
|
handlePauseStory();
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleOpenStorySeenBy = useLastCallback(() => {
|
const handleOpenStoryViewModal = useLastCallback(() => {
|
||||||
openStorySeenBy({ storyId });
|
openStoryViewModal({ storyId });
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleInfoPrivacyEdit = useLastCallback(() => {
|
const handleInfoPrivacyEdit = useLastCallback(() => {
|
||||||
@ -437,6 +470,25 @@ function Story({
|
|||||||
setStoryViewerMuted({ isMuted: !isMuted });
|
setStoryViewerMuted({ isMuted: !isMuted });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleOpenStealthModal = useLastCallback(() => {
|
||||||
|
if (stealthMode.activeUntil && getServerTime() < stealthMode.activeUntil) {
|
||||||
|
const diff = stealthMode.activeUntil - getServerTime();
|
||||||
|
showNotification({
|
||||||
|
title: lang('StealthModeOn'),
|
||||||
|
message: lang('Story.ToastStealthModeActiveText', formatMediaDuration(diff)),
|
||||||
|
duration: STEALTH_MODE_NOTIFICATION_DURATION,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleStealthModal({ isOpen: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDownload = useLastCallback(() => {
|
||||||
|
if (!downloadMediaData) return;
|
||||||
|
download(downloadMediaData, `story-${userId}-${storyId}.${isVideo ? 'mp4' : 'jpg'}`);
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDeletedStory) return;
|
if (!isDeletedStory) return;
|
||||||
|
|
||||||
@ -457,7 +509,7 @@ function Story({
|
|||||||
onClick={onTrigger}
|
onClick={onTrigger}
|
||||||
ariaLabel={lang('AccDescrOpenMenu2')}
|
ariaLabel={lang('AccDescrOpenMenu2')}
|
||||||
>
|
>
|
||||||
<i className="icon icon-more" aria-hidden />
|
<i className={buildClassName('icon icon-more', styles.topIcon)} aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -571,6 +623,7 @@ function Story({
|
|||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
'icon',
|
'icon',
|
||||||
isMuted || noSound ? 'icon-speaker-muted-story' : 'icon-speaker-story',
|
isMuted || noSound ? 'icon-speaker-muted-story' : 'icon-speaker-story',
|
||||||
|
styles.topIcon,
|
||||||
)}
|
)}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
@ -580,8 +633,8 @@ function Story({
|
|||||||
className={buildClassName(styles.button, styles.buttonMenu)}
|
className={buildClassName(styles.button, styles.buttonMenu)}
|
||||||
trigger={MenuButton}
|
trigger={MenuButton}
|
||||||
positionX="right"
|
positionX="right"
|
||||||
onOpen={handlePauseStory}
|
onOpen={handleDropdownMenuOpen}
|
||||||
onClose={handlePlayStory}
|
onClose={handleDropdownMenuClose}
|
||||||
>
|
>
|
||||||
{canCopyLink && <MenuItem icon="copy" onClick={handleCopyStoryLink}>{lang('CopyLink')}</MenuItem>}
|
{canCopyLink && <MenuItem icon="copy" onClick={handleCopyStoryLink}>{lang('CopyLink')}</MenuItem>}
|
||||||
{canPinToProfile && (
|
{canPinToProfile && (
|
||||||
@ -590,8 +643,14 @@ function Story({
|
|||||||
{canUnpinFromProfile && (
|
{canUnpinFromProfile && (
|
||||||
<MenuItem icon="delete" onClick={handleUnpinClick}>{lang('ArchiveStory')}</MenuItem>
|
<MenuItem icon="delete" onClick={handleUnpinClick}>{lang('ArchiveStory')}</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
{canDownload && (
|
||||||
|
<MenuItem icon="download" disabled={!downloadMediaData} onClick={handleDownload}>
|
||||||
|
{lang('lng_media_download')}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
<MenuItem icon="eye-closed-outline" onClick={handleOpenStealthModal}>{lang('StealthMode')}</MenuItem>
|
||||||
|
{!isSelf && <MenuItem icon="flag" onClick={handleReportStoryClick}>{lang('lng_report_story')}</MenuItem>}
|
||||||
{isSelf && <MenuItem icon="delete" destructive onClick={handleDeleteStoryClick}>{lang('Delete')}</MenuItem>}
|
{isSelf && <MenuItem icon="delete" destructive onClick={handleDeleteStoryClick}>{lang('Delete')}</MenuItem>}
|
||||||
{!isSelf && <MenuItem icon="flag" onClick={handleReportStoryClick}>{lang('Report')}</MenuItem>}
|
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -608,7 +667,7 @@ function Story({
|
|||||||
}, [story]);
|
}, [story]);
|
||||||
|
|
||||||
function renderRecentViewers() {
|
function renderRecentViewers() {
|
||||||
const { viewsCount } = story as ApiStory;
|
const { viewsCount, reactionsCount } = story as ApiStory;
|
||||||
|
|
||||||
if (!viewsCount) {
|
if (!viewsCount) {
|
||||||
return (
|
return (
|
||||||
@ -625,16 +684,23 @@ function Story({
|
|||||||
styles.recentViewersInteractive,
|
styles.recentViewersInteractive,
|
||||||
appearanceAnimationClassNames,
|
appearanceAnimationClassNames,
|
||||||
)}
|
)}
|
||||||
onClick={handleOpenStorySeenBy}
|
onClick={handleOpenStoryViewModal}
|
||||||
>
|
>
|
||||||
{!areViewsExpired && Boolean(recentViewers?.length) && (
|
{!areViewsExpired && Boolean(recentViewers?.length) && (
|
||||||
<AvatarList
|
<AvatarList
|
||||||
size="small"
|
size="small"
|
||||||
peers={recentViewers}
|
peers={recentViewers}
|
||||||
|
className={styles.recentViewersAvatars}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<span className={styles.recentViewersCount}>{lang('Views', viewsCount, 'i')}</span>
|
<span>{lang('Views', viewsCount, 'i')}</span>
|
||||||
|
{Boolean(reactionsCount) && (
|
||||||
|
<span className={styles.reactionCount}>
|
||||||
|
<i className={buildClassName(styles.reactionCountHeart, 'icon icon-heart')} />
|
||||||
|
{reactionsCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -662,11 +728,7 @@ function Story({
|
|||||||
<img src={previewBlobUrl} alt="" className={buildClassName(styles.media, previewTransitionClassNames)} />
|
<img src={previewBlobUrl} alt="" className={buildClassName(styles.media, previewTransitionClassNames)} />
|
||||||
)}
|
)}
|
||||||
{shouldRenderSkeleton && (
|
{shouldRenderSkeleton && (
|
||||||
<Skeleton
|
<Skeleton className={buildClassName(skeletonTransitionClassNames, styles.fullSize)} />
|
||||||
width={dimensions.width}
|
|
||||||
height={dimensions.height}
|
|
||||||
className={buildClassName(skeletonTransitionClassNames, styles.skeleton)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{!isVideo && fullMediaData && (
|
{!isVideo && fullMediaData && (
|
||||||
<img
|
<img
|
||||||
@ -713,9 +775,22 @@ function Story({
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<MediaAreaOverlay mediaAreas={mediaAreas} mediaDimensions={dimensions} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isSelf && renderRecentViewers()}
|
{isSelf && renderRecentViewers()}
|
||||||
|
{canShareOwn && (
|
||||||
|
<Button
|
||||||
|
className={styles.ownForward}
|
||||||
|
color="translucent"
|
||||||
|
size="smaller"
|
||||||
|
round
|
||||||
|
onClick={handleForwardClick}
|
||||||
|
ariaLabel={lang('Forward')}
|
||||||
|
>
|
||||||
|
<i className="icon icon-forward" aria-hidden />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{shouldRenderCaptionBackdrop && (
|
{shouldRenderCaptionBackdrop && (
|
||||||
<div
|
<div
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@ -725,12 +800,14 @@ function Story({
|
|||||||
aria-label={lang('Close')}
|
aria-label={lang('Close')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{hasText && <div className={styles.captionGradient} />}
|
||||||
{hasText && (
|
{hasText && (
|
||||||
<StoryCaption
|
<StoryCaption
|
||||||
key={`caption-${storyId}-${userId}`}
|
key={`caption-${storyId}-${userId}`}
|
||||||
story={story as ApiStory}
|
story={story as ApiStory}
|
||||||
isExpanded={isCaptionExpanded}
|
isExpanded={isCaptionExpanded}
|
||||||
onExpand={expandCaption}
|
onExpand={expandCaption}
|
||||||
|
onFold={foldCaption}
|
||||||
className={appearanceAnimationClassNames}
|
className={appearanceAnimationClassNames}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -743,8 +820,8 @@ function Story({
|
|||||||
isReady={!isSelf}
|
isReady={!isSelf}
|
||||||
messageListType="thread"
|
messageListType="thread"
|
||||||
isMobile={getIsMobile()}
|
isMobile={getIsMobile()}
|
||||||
editableInputCssSelector="#editable-story-input-text"
|
editableInputCssSelector={EDITABLE_STORY_INPUT_CSS_SELECTOR}
|
||||||
editableInputId="editable-story-input-text"
|
editableInputId={EDITABLE_STORY_INPUT_ID}
|
||||||
inputId="story-input-text"
|
inputId="story-input-text"
|
||||||
className={buildClassName(styles.composer, composerAppearanceAnimationClassNames)}
|
className={buildClassName(styles.composer, composerAppearanceAnimationClassNames)}
|
||||||
inputPlaceholder={lang('ReplyPrivately')}
|
inputPlaceholder={lang('ReplyPrivately')}
|
||||||
@ -765,16 +842,23 @@ export default memo(withGlobal<OwnProps>((global, {
|
|||||||
const chat = selectChat(global, userId);
|
const chat = selectChat(global, userId);
|
||||||
const tabState = selectTabState(global);
|
const tabState = selectTabState(global);
|
||||||
const {
|
const {
|
||||||
storyViewer: { isMuted, storyIdSeenBy, isPrivacyModalOpen },
|
storyViewer: {
|
||||||
|
isMuted,
|
||||||
|
viewModal,
|
||||||
|
isPrivacyModalOpen,
|
||||||
|
isStealthModalOpen,
|
||||||
|
},
|
||||||
forwardMessages: { storyId: forwardedStoryId },
|
forwardMessages: { storyId: forwardedStoryId },
|
||||||
premiumModal,
|
premiumModal,
|
||||||
|
safeLinkModalUrl,
|
||||||
|
mapModal,
|
||||||
} = tabState;
|
} = tabState;
|
||||||
const { isOpen: isPremiumModalOpen } = premiumModal || {};
|
const { isOpen: isPremiumModalOpen } = premiumModal || {};
|
||||||
const { orderedIds, pinnedIds, archiveIds } = selectUserStories(global, userId) || {};
|
const { orderedIds, pinnedIds, archiveIds } = selectUserStories(global, userId) || {};
|
||||||
const story = selectUserStory(global, userId, storyId);
|
const story = selectUserStory(global, userId, storyId);
|
||||||
const shouldForcePause = Boolean(
|
const shouldForcePause = Boolean(
|
||||||
storyIdSeenBy || forwardedStoryId || tabState.reactionPicker?.storyId || isReportModalOpen || isPrivacyModalOpen
|
viewModal || forwardedStoryId || tabState.reactionPicker?.storyId || isReportModalOpen || isPrivacyModalOpen
|
||||||
|| isPremiumModalOpen || isDeleteModalOpen,
|
|| isPremiumModalOpen || isDeleteModalOpen || safeLinkModalUrl || isStealthModalOpen || mapModal,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -789,5 +873,6 @@ export default memo(withGlobal<OwnProps>((global, {
|
|||||||
viewersExpirePeriod: appConfig!.storyExpirePeriod + appConfig!.storyViewersExpirePeriod,
|
viewersExpirePeriod: appConfig!.storyExpirePeriod + appConfig!.storyViewersExpirePeriod,
|
||||||
isChatExist: Boolean(chat),
|
isChatExist: Boolean(chat),
|
||||||
areChatSettingsLoaded: Boolean(chat?.settings),
|
areChatSettingsLoaded: Boolean(chat?.settings),
|
||||||
|
stealthMode: global.stories.stealthMode,
|
||||||
};
|
};
|
||||||
})(Story));
|
})(Story));
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
import React, {
|
import React, {
|
||||||
memo, useEffect, useRef, useState,
|
memo, useEffect, useRef, useState,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
|
import { requestMutation } from '../../lib/fasterdom/fasterdom';
|
||||||
|
import { addExtraClass, removeExtraClass } from '../../lib/teact/teact-dom';
|
||||||
|
|
||||||
import type { ApiStory } from '../../api/types';
|
import type { ApiStory } from '../../api/types';
|
||||||
|
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import { requestMutation } from '../../lib/fasterdom/fasterdom';
|
import { REM } from '../common/helpers/mediaDimensions';
|
||||||
import { addExtraClass, removeExtraClass } from '../../lib/teact/teact-dom';
|
|
||||||
|
|
||||||
import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation';
|
import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
|
import useShowTransition from '../../hooks/useShowTransition';
|
||||||
|
|
||||||
import MessageText from '../common/MessageText';
|
import MessageText from '../common/MessageText';
|
||||||
|
|
||||||
@ -18,34 +20,39 @@ import styles from './StoryViewer.module.scss';
|
|||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
story: ApiStory;
|
story: ApiStory;
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
onExpand: NoneToVoidFunction;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
|
onExpand: NoneToVoidFunction;
|
||||||
|
onFold?: NoneToVoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EXPAND_ANIMATION_DURATION_MS = 400;
|
const EXPAND_ANIMATION_DURATION_MS = 400;
|
||||||
const OVERFLOW_THRESHOLD_PX = 4;
|
const OVERFLOW_THRESHOLD_PX = 5.75 * REM;
|
||||||
|
|
||||||
function StoryCaption({
|
function StoryCaption({
|
||||||
story, isExpanded, className, onExpand,
|
story, isExpanded, className, onExpand, onFold,
|
||||||
}: OwnProps) {
|
}: OwnProps) {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
// 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);
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
const [hasOverflow, setHasOverflow] = useState<boolean>(false);
|
// eslint-disable-next-line no-null/no-null
|
||||||
const [height, setHeight] = useState<number>(0);
|
const showMoreButtonRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const caption = story.content.text;
|
||||||
|
|
||||||
|
const [hasOverflow, setHasOverflow] = useState(false);
|
||||||
const prevIsExpanded = usePrevDuringAnimation(isExpanded || undefined, EXPAND_ANIMATION_DURATION_MS);
|
const prevIsExpanded = usePrevDuringAnimation(isExpanded || undefined, EXPAND_ANIMATION_DURATION_MS);
|
||||||
|
const isInExpandedState = isExpanded || prevIsExpanded;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current) {
|
if (!ref.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { scrollHeight, clientHeight } = ref.current;
|
const { clientHeight } = ref.current;
|
||||||
setHasOverflow(scrollHeight - clientHeight > OVERFLOW_THRESHOLD_PX);
|
setHasOverflow(clientHeight > OVERFLOW_THRESHOLD_PX);
|
||||||
setHeight(scrollHeight - clientHeight);
|
}, [caption]);
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
requestMutation(() => {
|
requestMutation(() => {
|
||||||
@ -59,14 +66,38 @@ function StoryCaption({
|
|||||||
removeExtraClass(contentRef.current, styles.animate);
|
removeExtraClass(contentRef.current, styles.animate);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [height, isExpanded]);
|
}, [isExpanded]);
|
||||||
|
|
||||||
|
const canExpand = hasOverflow && !isInExpandedState;
|
||||||
|
const { shouldRender: shouldRenderShowMore, transitionClassNames } = useShowTransition(
|
||||||
|
canExpand, undefined, true, 'slow', true,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showMoreButtonRef.current || !contentRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const button = showMoreButtonRef.current;
|
||||||
|
const container = contentRef.current;
|
||||||
|
|
||||||
|
const { offsetWidth } = button;
|
||||||
|
requestMutation(() => {
|
||||||
|
container.style.setProperty('--expand-button-width', `${offsetWidth}px`);
|
||||||
|
});
|
||||||
|
}, [canExpand]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isExpanded) {
|
||||||
|
ref.current?.scrollTo({ top: 0 });
|
||||||
|
}
|
||||||
|
}, [isExpanded]);
|
||||||
|
|
||||||
const canExpand = hasOverflow && !isExpanded;
|
|
||||||
const fullClassName = buildClassName(
|
const fullClassName = buildClassName(
|
||||||
styles.captionContent,
|
styles.captionContent,
|
||||||
hasOverflow && !isExpanded && styles.hasOverflow,
|
hasOverflow && !isExpanded && styles.hasOverflow,
|
||||||
(isExpanded || prevIsExpanded) && styles.expanded,
|
isInExpandedState && styles.expanded,
|
||||||
canExpand && styles.captionInteractive,
|
shouldRenderShowMore && styles.withShowMore,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -75,22 +106,28 @@ function StoryCaption({
|
|||||||
ref={contentRef}
|
ref={contentRef}
|
||||||
className={fullClassName}
|
className={fullClassName}
|
||||||
role={canExpand ? 'button' : undefined}
|
role={canExpand ? 'button' : undefined}
|
||||||
style={`--scroll-height: ${isExpanded ? height : 0}px;`}
|
onClick={canExpand ? onExpand : onFold}
|
||||||
onClick={canExpand ? () => onExpand() : undefined}
|
|
||||||
>
|
>
|
||||||
<div ref={ref} className={buildClassName(styles.captionInner, 'allow-selection', 'custom-scroll')}>
|
<div
|
||||||
{hasOverflow && (
|
ref={ref}
|
||||||
<div className={buildClassName(styles.captionExpand, isExpanded && styles.hidden)}>
|
className={buildClassName(styles.captionInner, 'allow-selection', 'custom-scroll')}
|
||||||
{lang('Story.CaptionShowMore')}
|
>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<MessageText
|
<MessageText
|
||||||
messageOrStory={story}
|
messageOrStory={story}
|
||||||
withTranslucentThumbs
|
withTranslucentThumbs
|
||||||
|
forcePlayback
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{shouldRenderShowMore && (
|
||||||
|
<div
|
||||||
|
ref={showMoreButtonRef}
|
||||||
|
className={buildClassName(styles.captionShowMore, transitionClassNames)}
|
||||||
|
onClick={onExpand}
|
||||||
|
>
|
||||||
|
{lang('Story.CaptionShowMore')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,7 +64,6 @@
|
|||||||
.option {
|
.option {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
@ -147,11 +146,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.action {
|
.action {
|
||||||
|
width: 100%;
|
||||||
color: #8774E1;
|
color: #8774E1;
|
||||||
cursor: var(--custom-cursor, pointer);
|
cursor: var(--custom-cursor, pointer);
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
transition: opacity 200ms;
|
transition: opacity 200ms;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
> :global(.icon) {
|
> :global(.icon) {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@ -170,6 +174,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
|
|||||||
@ -38,7 +38,7 @@ interface StateProps {
|
|||||||
currentUserId: string;
|
currentUserId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PrivacyAction = 'blockUserIds' | 'closeFriends' | 'allowUserIds';
|
type PrivacyAction = 'blockUserIds' | 'closeFriends' | 'blockContactUserIds' | 'allowUserIds';
|
||||||
|
|
||||||
interface PrivacyOption {
|
interface PrivacyOption {
|
||||||
name: string;
|
name: string;
|
||||||
@ -53,13 +53,13 @@ const OPTIONS: PrivacyOption[] = [{
|
|||||||
value: 'everybody',
|
value: 'everybody',
|
||||||
color: ['#50ABFF', '#007AFF'],
|
color: ['#50ABFF', '#007AFF'],
|
||||||
icon: 'channel-filled',
|
icon: 'channel-filled',
|
||||||
actions: undefined,
|
actions: 'blockUserIds',
|
||||||
}, {
|
}, {
|
||||||
name: 'StoryPrivacyOptionContacts',
|
name: 'StoryPrivacyOptionContacts',
|
||||||
value: 'contacts',
|
value: 'contacts',
|
||||||
color: ['#C36EFF', '#8B60FA'],
|
color: ['#C36EFF', '#8B60FA'],
|
||||||
icon: 'user-filled',
|
icon: 'user-filled',
|
||||||
actions: 'blockUserIds',
|
actions: 'blockContactUserIds',
|
||||||
}, {
|
}, {
|
||||||
name: 'StoryPrivacyOptionCloseFriends',
|
name: 'StoryPrivacyOptionCloseFriends',
|
||||||
value: 'closeFriends',
|
value: 'closeFriends',
|
||||||
@ -82,7 +82,13 @@ enum Screens {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function StorySettings({
|
function StorySettings({
|
||||||
isOpen, story, visibility, contactListIds, usersById, currentUserId, onClose,
|
isOpen,
|
||||||
|
story,
|
||||||
|
visibility,
|
||||||
|
contactListIds,
|
||||||
|
usersById,
|
||||||
|
currentUserId,
|
||||||
|
onClose,
|
||||||
}: OwnProps & StateProps) {
|
}: OwnProps & StateProps) {
|
||||||
const { editStoryPrivacy, toggleStoryPinned } = getActions();
|
const { editStoryPrivacy, toggleStoryPinned } = getActions();
|
||||||
|
|
||||||
@ -91,6 +97,7 @@ function StorySettings({
|
|||||||
const [privacy, setPrivacy] = useState<ApiPrivacySettings | undefined>(visibility);
|
const [privacy, setPrivacy] = useState<ApiPrivacySettings | undefined>(visibility);
|
||||||
const [isPinned, setIsPinned] = useState(story?.isPinned);
|
const [isPinned, setIsPinned] = useState(story?.isPinned);
|
||||||
const [activeKey, setActiveKey] = useState<Screens>(Screens.privacy);
|
const [activeKey, setActiveKey] = useState<Screens>(Screens.privacy);
|
||||||
|
const [editingBlockingCategory, setEditingBlockingCategory] = useState<PrivacyVisibility>('everybody');
|
||||||
const isBackButton = activeKey !== Screens.privacy;
|
const isBackButton = activeKey !== Screens.privacy;
|
||||||
|
|
||||||
const closeFriendIds = useMemo(() => {
|
const closeFriendIds = useMemo(() => {
|
||||||
@ -107,6 +114,11 @@ function StorySettings({
|
|||||||
return undefined;
|
return undefined;
|
||||||
}, [activeKey, currentUserId, privacy?.allowUserIds]);
|
}, [activeKey, currentUserId, privacy?.allowUserIds]);
|
||||||
|
|
||||||
|
const selectedBlockedIds = useMemo(() => {
|
||||||
|
if (editingBlockingCategory !== privacy?.visibility) return [];
|
||||||
|
return privacy?.blockUserIds || [];
|
||||||
|
}, [editingBlockingCategory, privacy?.blockUserIds, privacy?.visibility]);
|
||||||
|
|
||||||
const handleAllowUserIdsChange = useLastCallback((newIds: string[]) => {
|
const handleAllowUserIdsChange = useLastCallback((newIds: string[]) => {
|
||||||
setPrivacy({
|
setPrivacy({
|
||||||
...privacy!,
|
...privacy!,
|
||||||
@ -118,6 +130,7 @@ function StorySettings({
|
|||||||
setPrivacy({
|
setPrivacy({
|
||||||
...privacy!,
|
...privacy!,
|
||||||
blockUserIds: newIds,
|
blockUserIds: newIds,
|
||||||
|
visibility: editingBlockingCategory,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -160,6 +173,12 @@ function StorySettings({
|
|||||||
break;
|
break;
|
||||||
case 'blockUserIds':
|
case 'blockUserIds':
|
||||||
setActiveKey(Screens.denyList);
|
setActiveKey(Screens.denyList);
|
||||||
|
setEditingBlockingCategory('everybody');
|
||||||
|
break;
|
||||||
|
case 'blockContactUserIds':
|
||||||
|
setActiveKey(Screens.denyList);
|
||||||
|
setEditingBlockingCategory('contacts');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,13 +212,14 @@ function StorySettings({
|
|||||||
return lang('StoryPrivacyOptionPeople', closeFriendIds.length, 'i');
|
return lang('StoryPrivacyOptionPeople', closeFriendIds.length, 'i');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === 'blockUserIds') {
|
if ((action === 'blockUserIds' && privacy?.visibility === 'everybody')
|
||||||
if (!privacy?.blockUserIds || privacy.blockUserIds.length === 0) {
|
|| (action === 'blockContactUserIds' && privacy?.visibility === 'contacts')) {
|
||||||
|
if (!privacy?.blockUserIds?.length) {
|
||||||
return lang('StoryPrivacyOptionContactsDetail');
|
return lang('StoryPrivacyOptionContactsDetail');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (privacy.blockUserIds.length === 1) {
|
if (privacy.blockUserIds.length === 1) {
|
||||||
return lang('StoryPrivacyOptionExcludePerson', getUserFullName(usersById[closeFriendIds[0]]));
|
return lang('StoryPrivacyOptionExcludePerson', getUserFullName(usersById[privacy.blockUserIds[0]]));
|
||||||
}
|
}
|
||||||
|
|
||||||
return lang('StoryPrivacyOptionExcludePeople', privacy.blockUserIds.length, 'i');
|
return lang('StoryPrivacyOptionExcludePeople', privacy.blockUserIds.length, 'i');
|
||||||
@ -254,7 +274,7 @@ function StorySettings({
|
|||||||
contactListIds={contactListIds}
|
contactListIds={contactListIds}
|
||||||
currentUserId={currentUserId}
|
currentUserId={currentUserId}
|
||||||
usersById={usersById}
|
usersById={usersById}
|
||||||
selectedIds={privacy?.blockUserIds}
|
selectedIds={selectedBlockedIds}
|
||||||
onSelect={handleDenyUserIdsChange}
|
onSelect={handleDenyUserIdsChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -309,7 +329,7 @@ function StorySettings({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="button"
|
role="button"
|
||||||
className={styles.action}
|
className={styles.action}
|
||||||
aria-label={lang('Change List')}
|
aria-label={lang('Edit')}
|
||||||
onClick={(e) => { handleActionClick(e, option.actions!); }}
|
onClick={(e) => { handleActionClick(e, option.actions!); }}
|
||||||
>
|
>
|
||||||
<span className={styles.actionInner}>{renderActionName(option.actions)}</span>
|
<span className={styles.actionInner}>{renderActionName(option.actions)}</span>
|
||||||
|
|||||||
@ -16,7 +16,8 @@ import useLastCallback from '../../hooks/useLastCallback';
|
|||||||
import usePrevious from '../../hooks/usePrevious';
|
import usePrevious from '../../hooks/usePrevious';
|
||||||
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||||
import useSignal from '../../hooks/useSignal';
|
import useSignal from '../../hooks/useSignal';
|
||||||
import { useSlideSizes } from './hooks/useSlideSizes';
|
import useSlideSizes from './hooks/useSlideSizes';
|
||||||
|
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||||
|
|
||||||
import Story from './Story';
|
import Story from './Story';
|
||||||
import StoryPreview from './StoryPreview';
|
import StoryPreview from './StoryPreview';
|
||||||
@ -24,6 +25,7 @@ import StoryPreview from './StoryPreview';
|
|||||||
import styles from './StoryViewer.module.scss';
|
import styles from './StoryViewer.module.scss';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
|
isOpen?: boolean;
|
||||||
isReportModalOpen?: boolean;
|
isReportModalOpen?: boolean;
|
||||||
isDeleteModalOpen?: boolean;
|
isDeleteModalOpen?: boolean;
|
||||||
onDelete: (storyId: number) => void;
|
onDelete: (storyId: number) => void;
|
||||||
@ -49,8 +51,20 @@ const ANIMATION_TO_ACTIVE_SCALE = '3';
|
|||||||
const ANIMATION_FROM_ACTIVE_SCALE = `${FROM_ACTIVE_SCALE_VALUE}`;
|
const ANIMATION_FROM_ACTIVE_SCALE = `${FROM_ACTIVE_SCALE_VALUE}`;
|
||||||
|
|
||||||
function StorySlides({
|
function StorySlides({
|
||||||
userIds, currentUserId, currentStoryId, isSingleUser, isSingleStory, isPrivate, isArchive, byUserId,
|
userIds,
|
||||||
isReportModalOpen, isDeleteModalOpen, onDelete, onClose, onReport,
|
currentUserId,
|
||||||
|
currentStoryId,
|
||||||
|
isOpen,
|
||||||
|
isSingleUser,
|
||||||
|
isSingleStory,
|
||||||
|
isPrivate,
|
||||||
|
isArchive,
|
||||||
|
byUserId,
|
||||||
|
isReportModalOpen,
|
||||||
|
isDeleteModalOpen,
|
||||||
|
onDelete,
|
||||||
|
onClose,
|
||||||
|
onReport,
|
||||||
}: OwnProps & StateProps) {
|
}: OwnProps & StateProps) {
|
||||||
const [renderingUserId, setRenderingUserId] = useState(currentUserId);
|
const [renderingUserId, setRenderingUserId] = useState(currentUserId);
|
||||||
const [renderingStoryId, setRenderingStoryId] = useState(currentStoryId);
|
const [renderingStoryId, setRenderingStoryId] = useState(currentStoryId);
|
||||||
@ -64,6 +78,12 @@ function StorySlides({
|
|||||||
const rendersRef = useRef<Record<string, { current: HTMLDivElement | null }>>({});
|
const rendersRef = useRef<Record<string, { current: HTMLDivElement | null }>>({});
|
||||||
const [getIsAnimating, setIsAnimating] = useSignal(false);
|
const [getIsAnimating, setIsAnimating] = useSignal(false);
|
||||||
|
|
||||||
|
useHistoryBack({
|
||||||
|
isActive: isOpen,
|
||||||
|
onBack: onClose,
|
||||||
|
shouldBeReplaced: true,
|
||||||
|
});
|
||||||
|
|
||||||
function setRef(ref: HTMLDivElement | null, userId: string) {
|
function setRef(ref: HTMLDivElement | null, userId: string) {
|
||||||
if (!ref) {
|
if (!ref) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
152
src/components/story/StoryView.tsx
Normal file
152
src/components/story/StoryView.tsx
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import React, { memo, useMemo } from '../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
|
import type { ApiAvailableReaction, ApiStoryView, ApiUser } from '../../api/types';
|
||||||
|
|
||||||
|
import { selectUser } from '../../global/selectors';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import { REM } from '../common/helpers/mediaDimensions';
|
||||||
|
import { formatDateAtTime } from '../../util/dateFormat';
|
||||||
|
import { getUserFullName } from '../../global/helpers';
|
||||||
|
|
||||||
|
import useLastCallback from '../../hooks/useLastCallback';
|
||||||
|
import useLang from '../../hooks/useLang';
|
||||||
|
|
||||||
|
import ListItem from '../ui/ListItem';
|
||||||
|
import ReactionStaticEmoji from '../common/ReactionStaticEmoji';
|
||||||
|
import PrivateChatInfo from '../common/PrivateChatInfo';
|
||||||
|
|
||||||
|
import styles from './StoryViewModal.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
storyView: ApiStoryView;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
user?: ApiUser;
|
||||||
|
availableReactions?: ApiAvailableReaction[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const CLOSE_ANIMATION_DURATION = 100;
|
||||||
|
const DEFAULT_REACTION_SIZE = 1.5 * REM;
|
||||||
|
|
||||||
|
const StoryView = ({
|
||||||
|
storyView,
|
||||||
|
user,
|
||||||
|
availableReactions,
|
||||||
|
}: OwnProps & StateProps) => {
|
||||||
|
const {
|
||||||
|
openChat, closeStoryViewer, unblockUser, blockUser, deleteContact, updateStoryView,
|
||||||
|
} = getActions();
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const handleClick = useLastCallback(() => {
|
||||||
|
closeStoryViewer();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
openChat({ id: storyView.userId });
|
||||||
|
}, CLOSE_ANIMATION_DURATION);
|
||||||
|
});
|
||||||
|
|
||||||
|
const contextActions = useMemo(() => {
|
||||||
|
const { userId, areStoriesBlocked, isUserBlocked } = storyView;
|
||||||
|
const { isContact } = user || {};
|
||||||
|
const fullName = getUserFullName(user);
|
||||||
|
|
||||||
|
const actions = [];
|
||||||
|
|
||||||
|
if (!isUserBlocked) {
|
||||||
|
if (!areStoriesBlocked) {
|
||||||
|
actions.push({
|
||||||
|
handler: () => {
|
||||||
|
blockUser({ userId, isOnlyStories: true });
|
||||||
|
updateStoryView({ userId, areStoriesBlocked: true });
|
||||||
|
},
|
||||||
|
title: lang('StoryHideFrom', fullName),
|
||||||
|
icon: 'hand-stop',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
actions.push({
|
||||||
|
handler: () => {
|
||||||
|
unblockUser({ userId, isOnlyStories: true });
|
||||||
|
updateStoryView({ userId, areStoriesBlocked: false });
|
||||||
|
},
|
||||||
|
title: lang('StoryShowBackTo', fullName),
|
||||||
|
icon: 'play-story',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isContact) {
|
||||||
|
actions.push({
|
||||||
|
handler: () => {
|
||||||
|
deleteContact({ userId });
|
||||||
|
},
|
||||||
|
title: lang('DeleteContact'),
|
||||||
|
icon: 'delete-user',
|
||||||
|
destructive: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
actions.push({
|
||||||
|
handler: () => {
|
||||||
|
if (isUserBlocked) {
|
||||||
|
unblockUser({ userId });
|
||||||
|
updateStoryView({ userId, isUserBlocked: false });
|
||||||
|
} else {
|
||||||
|
blockUser({ userId });
|
||||||
|
updateStoryView({ userId, isUserBlocked: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: lang(isUserBlocked ? 'Unblock' : 'BlockUser'),
|
||||||
|
icon: isUserBlocked ? 'user' : 'delete-user',
|
||||||
|
destructive: !isUserBlocked,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}, [lang, storyView, user]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
key={storyView.userId}
|
||||||
|
className={buildClassName(
|
||||||
|
'chat-item-clickable small-icon',
|
||||||
|
styles.opacityFadeIn,
|
||||||
|
(storyView.isUserBlocked || storyView.areStoriesBlocked) && styles.blocked,
|
||||||
|
)}
|
||||||
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
|
onClick={() => handleClick()}
|
||||||
|
rightElement={storyView.reaction ? (
|
||||||
|
<ReactionStaticEmoji
|
||||||
|
reaction={storyView.reaction}
|
||||||
|
className={styles.viewReaction}
|
||||||
|
size={DEFAULT_REACTION_SIZE}
|
||||||
|
availableReactions={availableReactions}
|
||||||
|
withIconHeart
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
contextActions={contextActions}
|
||||||
|
withPortalForMenu
|
||||||
|
menuBubbleClassName={styles.menuBubble}
|
||||||
|
>
|
||||||
|
<PrivateChatInfo
|
||||||
|
userId={storyView.userId}
|
||||||
|
noStatusOrTyping
|
||||||
|
status={formatDateAtTime(lang, storyView.date * 1000)}
|
||||||
|
statusIcon="icon-message-read"
|
||||||
|
withStory
|
||||||
|
forceShowSelf
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>((global, { storyView }) => {
|
||||||
|
const user = selectUser(global, storyView.userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
availableReactions: global.availableReactions,
|
||||||
|
};
|
||||||
|
})(StoryView));
|
||||||
125
src/components/story/StoryViewModal.module.scss
Normal file
125
src/components/story/StoryViewModal.module.scss
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
.views-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
|
||||||
|
height: 35rem;
|
||||||
|
@supports (height: min(80vh, 35rem)) {
|
||||||
|
height: min(80vh, 35rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.views-list-loading {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.centeredInfo {
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-reaction {
|
||||||
|
--custom-emoji-size: 1.5rem;
|
||||||
|
margin-inline-start: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
min-height: 17rem;
|
||||||
|
overflow-y: scroll;
|
||||||
|
position: relative;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-button {
|
||||||
|
height: 2rem !important;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-filter {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
pointer-events: none;
|
||||||
|
color: #FFFFFF !important;
|
||||||
|
background-color: var(--color-interactive-element-hover) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-button {
|
||||||
|
padding-inline: 0.5rem !important;
|
||||||
|
border-radius: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-sort {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #FFFFFF;
|
||||||
|
margin-inline-end: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-down {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
margin-block: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-info {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: 0.25rem 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrolled {
|
||||||
|
border-bottom: 0.0625rem solid var(--color-borders);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
border-top: 0.0625rem solid var(--color-borders);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
margin-block: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-fade-in {
|
||||||
|
animation: fadeIn 0.2s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocked {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check {
|
||||||
|
margin: 0 0 0 auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuBubble {
|
||||||
|
max-width: min(25rem, 80vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
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