Browse Source

Spoilers

master
Eduard Kuzmenko 2 years ago
parent
commit
5f6463b870
  1. 26
      src/components/chat/bubbles.ts
  2. 6
      src/components/chat/input.ts
  3. 2
      src/components/chat/replyContainer.ts
  4. 8
      src/helpers/dom/getRichElementValue.ts
  5. 24
      src/lib/appManagers/appMessagesManager.ts
  6. 150
      src/lib/richtextprocessor.ts
  7. 66
      src/scss/partials/_spoiler.scss
  8. 5
      src/scss/style.scss

26
src/components/chat/bubbles.ts

@ -1048,6 +1048,32 @@ export default class ChatBubbles { @@ -1048,6 +1048,32 @@ export default class ChatBubbles {
return;
}
const spoiler: HTMLElement = findUpClassName(target, 'spoiler');
if(spoiler) {
const messageDiv = findUpClassName(spoiler, 'message');
const className = 'is-spoiler-visible';
const isVisible = messageDiv.classList.contains(className);
if(!isVisible) {
cancelEvent(e);
}
const duration = 400 / 2;
const showDuration = 5000;
const useRafs = !isVisible ? 1 : 0;
if(useRafs) {
messageDiv.classList.add('will-change');
}
SetTransition(messageDiv, className, true, duration + showDuration, () => {
SetTransition(messageDiv, className, false, duration, () => {
messageDiv.classList.remove('will-change');
});
}, useRafs);
return;
}
const commentsDiv: HTMLElement = findUpClassName(target, 'replies');
if(commentsDiv) {
const bubbleMid = +bubble.dataset.mid;

6
src/components/chat/input.ts

@ -1368,7 +1368,8 @@ export default class ChatInput { @@ -1368,7 +1368,8 @@ export default class ChatInput {
underline: 'Underline',
strikethrough: 'Strikethrough',
monospace: () => document.execCommand('fontName', false, 'monospace'),
link: href ? () => document.execCommand('createLink', false, href) : () => document.execCommand('unlink', false, null)
link: href ? () => document.execCommand('createLink', false, href) : () => document.execCommand('unlink', false, null),
spoiler: () => document.execCommand('fontName', false, 'spoiler')
};
if(!commandsMap[type]) {
@ -1463,7 +1464,8 @@ export default class ChatInput { @@ -1463,7 +1464,8 @@ export default class ChatInput {
'KeyI': 'italic',
'KeyU': 'underline',
'KeyS': 'strikethrough',
'KeyM': 'monospace'
'KeyM': 'monospace',
'KeyP': 'spoiler'
};
if(this.appImManager.markupTooltip) {

2
src/components/chat/replyContainer.ts

@ -96,7 +96,7 @@ export function wrapReplyDivAndCaption(options: { @@ -96,7 +96,7 @@ export function wrapReplyDivAndCaption(options: {
} else {
if(message) {
subtitleEl.textContent = '';
subtitleEl.append(appMessagesManager.wrapMessageForReply(message, message.message && limitSymbols(message.message, 140)));
subtitleEl.append(appMessagesManager.wrapMessageForReply(message));
} else {
if(typeof(subtitle) === 'string') {
subtitle = limitSymbols(subtitle, 140);

8
src/helpers/dom/getRichElementValue.ts

@ -11,10 +11,10 @@ @@ -11,10 +11,10 @@
import { MessageEntity } from "../../layer";
export type MarkdownType = 'bold' | 'italic' | 'underline' | 'strikethrough' | 'monospace' | 'link' | 'mentionName';
export type MarkdownType = 'bold' | 'italic' | 'underline' | 'strikethrough' | 'monospace' | 'link' | 'mentionName' | 'spoiler';
export type MarkdownTag = {
match: string,
entityName: 'messageEntityBold' | 'messageEntityUnderline' | 'messageEntityItalic' | 'messageEntityPre' | 'messageEntityStrike' | 'messageEntityTextUrl' | 'messageEntityMentionName';
entityName: Extract<MessageEntity['_'], 'messageEntityBold' | 'messageEntityUnderline' | 'messageEntityItalic' | 'messageEntityPre' | 'messageEntityStrike' | 'messageEntityTextUrl' | 'messageEntityMentionName' | 'messageEntitySpoiler'>;
};
// https://core.telegram.org/bots/api#html-style
@ -46,6 +46,10 @@ export const markdownTags: {[type in MarkdownType]: MarkdownTag} = { @@ -46,6 +46,10 @@ export const markdownTags: {[type in MarkdownType]: MarkdownTag} = {
mentionName: {
match: 'A.follow',
entityName: 'messageEntityMentionName'
},
spoiler: {
match: '[style*="spoiler"]',
entityName: 'messageEntitySpoiler'
}
};

24
src/lib/appManagers/appMessagesManager.ts

@ -2531,12 +2531,12 @@ export class AppMessagesManager { @@ -2531,12 +2531,12 @@ export class AppMessagesManager {
messageId: mid
};
if(isMessage) {
/* if(isMessage) {
const entities = message.entities;
if(entities && entities.find(entity => entity._ === 'messageEntitySpoiler')) {
message.media = {_: 'messageMediaUnsupported'};
}
}
} */
if(isMessage && message.media) {
switch(message.media._) {
@ -2802,6 +2802,7 @@ export class AppMessagesManager { @@ -2802,6 +2802,7 @@ export class AppMessagesManager {
}
};
let entities = (message as Message.message).totalEntities;
if((message as Message.message).media) {
assumeType<Message.message>(message);
let usingFullAlbum = true;
@ -2821,7 +2822,9 @@ export class AppMessagesManager { @@ -2821,7 +2822,9 @@ export class AppMessagesManager {
}
if(usingFullAlbum) {
text = this.getAlbumText(message.grouped_id).message;
const albumText = this.getAlbumText(message.grouped_id);
text = albumText.message;
entities = albumText.totalEntities;
if(!withoutMediaType) {
addPart('AttachAlbum');
@ -2859,8 +2862,8 @@ export class AppMessagesManager { @@ -2859,8 +2862,8 @@ export class AppMessagesManager {
addPart('AttachContact');
break;
case 'messageMediaGame': {
const prefix = '🎮' + ' ';
text = prefix + media.game.title;
const f = '🎮' + ' ' + media.game.title;
addPart(undefined, plain ? f : RichTextProcessor.wrapEmojiText(f));
break;
}
case 'messageMediaDocument': {
@ -2924,14 +2927,17 @@ export class AppMessagesManager { @@ -2924,14 +2927,17 @@ export class AppMessagesManager {
if(text) {
text = limitSymbols(text, 100);
if(!entities) {
entities = [];
}
if(plain) {
parts.push(text);
parts.push(RichTextProcessor.wrapPlainText(text, entities));
} else {
let entities = RichTextProcessor.parseEntities(text.replace(/\n/g, ' '));
// let entities = RichTextProcessor.parseEntities(text.replace(/\n/g, ' '));
if(highlightWord) {
highlightWord = highlightWord.trim();
if(!entities) entities = [];
let found = false;
let match: any;
let regExp = new RegExp(escapeRegExp(highlightWord), 'gi');
@ -2941,7 +2947,7 @@ export class AppMessagesManager { @@ -2941,7 +2947,7 @@ export class AppMessagesManager {
}
if(found) {
entities.sort((a, b) => a.offset - b.offset);
RichTextProcessor.sortEntities(entities);
}
}

150
src/lib/richtextprocessor.ts

@ -18,6 +18,7 @@ import { encodeEntities } from '../helpers/string'; @@ -18,6 +18,7 @@ import { encodeEntities } from '../helpers/string';
import { IS_SAFARI } from '../environment/userAgent';
import { MOUNT_CLASS_TO } from '../config/debug';
import IS_EMOJI_SUPPORTED from '../environment/emojiSupport';
import { copy } from '../helpers/object';
const EmojiHelper = {
emojiMap: (code: string) => { return code; },
@ -82,7 +83,7 @@ const botCommandRegExp = '\\/([a-zA-Z\\d_]{1,32})(?:@(' + usernameRegExp + '))?( @@ -82,7 +83,7 @@ const botCommandRegExp = '\\/([a-zA-Z\\d_]{1,32})(?:@(' + usernameRegExp + '))?(
const fullRegExp = new RegExp('(^| )(@)(' + usernameRegExp + ')|(' + urlRegExp + ')|(\\n)|(' + emojiRegExp + ')|(^|[\\s\\(\\]])(#[' + alphaNumericRegExp + ']{2,64})|(^|\\s)' + botCommandRegExp, 'i');
const emailRegExp = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
//const markdownTestRegExp = /[`_*@~]/;
const markdownRegExp = /(^|\s|\n)(````?)([\s\S]+?)(````?)([\s\n\.,:?!;]|$)|(^|\s|\x01)(`|~~|\*\*|__|_-_)([^\n]+?)\7([\x01\s\.,:?!;]|$)|@(\d+)\s*\((.+?)\)|(\[(.+?)\]\((.+?)\))/m;
const markdownRegExp = /(^|\s|\n)(````?)([\s\S]+?)(````?)([\s\n\.,:?!;]|$)|(^|\s|\x01)(`|~~|\*\*|__|_-_|\|\|)([^\n]+?)\7([\x01\s\.,:?!;]|$)|@(\d+)\s*\((.+?)\)|(\[(.+?)\]\((.+?)\))/m;
const siteHashtags: {[siteName: string]: string} = {
Telegram: 'tg://search_hashtag?hashtag={1}',
Twitter: 'https://twitter.com/hashtag/{1}',
@ -102,7 +103,8 @@ const markdownEntities: {[markdown: string]: MessageEntity['_']} = { @@ -102,7 +103,8 @@ const markdownEntities: {[markdown: string]: MessageEntity['_']} = {
'**': 'messageEntityBold',
'__': 'messageEntityItalic',
'~~': 'messageEntityStrike',
'_-_': 'messageEntityUnderline'
'_-_': 'messageEntityUnderline',
'||': 'messageEntitySpoiler'
};
const passConflictingEntities: Set<MessageEntity['_']> = new Set([
@ -287,7 +289,7 @@ namespace RichTextProcessor { @@ -287,7 +289,7 @@ namespace RichTextProcessor {
const isSOH = match[6] === '\x01';
entity = {
_: markdownEntities[match[7]] as (MessageEntity.messageEntityBold | MessageEntity.messageEntityCode | MessageEntity.messageEntityItalic)['_'],
_: markdownEntities[match[7]] as (MessageEntity.messageEntityBold | MessageEntity.messageEntityCode | MessageEntity.messageEntityItalic | MessageEntity.messageEntitySpoiler)['_'],
//offset: matchIndex + match[6].length,
offset: matchIndex + (isSOH ? 0 : match[6].length),
length: text.length
@ -407,7 +409,9 @@ namespace RichTextProcessor { @@ -407,7 +409,9 @@ namespace RichTextProcessor {
// currentEntities.sort((a, b) => a.offset - b.offset);
// currentEntities.sort((a, b) => (a.offset - b.offset) || (a._ === 'messageEntityCaret' && -1));
if(!IS_EMOJI_SUPPORTED) { // fix splitted emoji. messageEntityTextUrl can split the emoji if starts before its end (e.g. on fe0f)
// * fix splitted emoji. messageEntityTextUrl can split the emoji if starts before its end (e.g. on fe0f)
// * have to fix even if emoji supported since it's being wrapped in span
// if(!IS_EMOJI_SUPPORTED) {
for(let i = 0; i < currentEntities.length; ++i) {
const entity = currentEntities[i];
if(entity._ === 'messageEntityEmoji') {
@ -417,7 +421,7 @@ namespace RichTextProcessor { @@ -417,7 +421,7 @@ namespace RichTextProcessor {
}
}
}
}
// }
return currentEntities;
}
@ -466,16 +470,17 @@ namespace RichTextProcessor { @@ -466,16 +470,17 @@ namespace RichTextProcessor {
entities: MessageEntity[],
contextSite: string,
highlightUsername: string,
noLinks: true,
noLinebreaks: true,
noCommands: true,
noLinks: boolean,
noLinebreaks: boolean,
noCommands: boolean,
wrappingDraft: boolean,
//mustWrapEmoji: boolean,
fromBot: boolean,
noTextFormat: true,
noTextFormat: boolean,
passEntities: Partial<{
[_ in MessageEntity['_']]: boolean
}>,
noEncoding: boolean,
contextHashtag?: string,
}> = {}) {
@ -495,17 +500,50 @@ namespace RichTextProcessor { @@ -495,17 +500,50 @@ namespace RichTextProcessor {
const contextExternal = contextSite !== 'Telegram';
const insertPart = (entity: MessageEntity, startPart: string, endPart?: string/* , priority = 0 */) => {
lol.push({part: startPart, offset: entity.offset/* , priority */});
const startOffset = entity.offset, endOffset = endPart ? entity.offset + entity.length : undefined;
let startIndex: number, endIndex: number, length = lol.length;
for(let i = length - 1; i >= 0; --i) {
const offset = lol[i].offset;
if(startIndex === undefined && startOffset >= offset) {
startIndex = i + 1;
}
if(endOffset !== undefined) {
if(endOffset <= offset) {
endIndex = i;
}
}
if(startOffset > offset && (endOffset === undefined || endOffset < offset)) {
break;
}
}
startIndex ??= 0;
lol.splice(startIndex, 0, {part: startPart, offset: entity.offset/* , priority */});
if(endPart) {
lol.push({part: endPart, offset: entity.offset + entity.length/* , priority */});
if(endOffset !== undefined) {
endIndex ??= startIndex;
++endIndex;
lol.splice(endIndex, 0, {part: endPart, offset: entity.offset + entity.length/* , priority */});
}
};
const pushPartsAfterSort: typeof lol = [];
const textLength = text.length;
for(let i = 0, length = entities.length; i < length; ++i) {
const entity = entities[i];
let entity = entities[i];
// * check whether text was sliced
// TODO: consider about moving it to other function
if(entity.offset >= textLength) {
continue;
} else if((entity.offset + entity.length) > textLength) {
entity = copy(entity);
entity.length = entity.offset + entity.length - textLength;
}
switch(entity._) {
case 'messageEntityBold': {
if(!options.noTextFormat) {
@ -535,7 +573,7 @@ namespace RichTextProcessor { @@ -535,7 +573,7 @@ namespace RichTextProcessor {
if(options.wrappingDraft) {
const styleName = IS_SAFARI ? 'text-decoration' : 'text-decoration-line';
insertPart(entity, `<span style="${styleName}: line-through;">`, '</span>');
} else {
} else if(!options.noTextFormat) {
insertPart(entity, '<del>', '</del>');
}
@ -546,7 +584,7 @@ namespace RichTextProcessor { @@ -546,7 +584,7 @@ namespace RichTextProcessor {
if(options.wrappingDraft) {
const styleName = IS_SAFARI ? 'text-decoration' : 'text-decoration-line';
insertPart(entity, `<span style="${styleName}: underline;">`, '</span>');
} else {
} else if(!options.noTextFormat) {
insertPart(entity, '<u>', '</u>');
}
@ -556,7 +594,7 @@ namespace RichTextProcessor { @@ -556,7 +594,7 @@ namespace RichTextProcessor {
case 'messageEntityCode': {
if(options.wrappingDraft) {
insertPart(entity, '<span style="font-family: monospace;">', '</span>');
} else {
} else if(!options.noTextFormat) {
insertPart(entity, '<code>', '</code>');
}
@ -732,14 +770,28 @@ namespace RichTextProcessor { @@ -732,14 +770,28 @@ namespace RichTextProcessor {
break;
}
case 'messageEntitySpoiler': {
if(options.noTextFormat) {
const before = text.slice(0, entity.offset);
const after = text.slice(entity.offset + entity.length);
text = before + '▚'.repeat(entity.length) + after;
} else if(options.wrappingDraft) {
insertPart(entity, '<span style="font-family: spoiler;">', '</span>');
} else {
insertPart(entity, '<span class="spoiler"><span class="spoiler-text">', '</span></span>');
}
break;
}
}
}
// lol.sort((a, b) => (a.offset - b.offset) || (a.priority - b.priority));
lol.sort((a, b) => a.offset - b.offset); // have to sort because of nested entities
// lol.sort((a, b) => a.offset - b.offset); // have to sort because of nested entities
let partsLength = lol.length, partsAfterSortLength = pushPartsAfterSort.length;
for(let i = 0; i < partsAfterSortLength; ++i) {
let partsLength = lol.length, pushPartsAfterSortLength = pushPartsAfterSort.length;
for(let i = 0; i < pushPartsAfterSortLength; ++i) {
const part = pushPartsAfterSort[i];
let insertAt = 0;
while(insertAt < partsLength) {
@ -751,14 +803,15 @@ namespace RichTextProcessor { @@ -751,14 +803,15 @@ namespace RichTextProcessor {
lol.splice(insertAt, 0, part);
}
partsLength += partsAfterSortLength;
partsLength += pushPartsAfterSortLength;
const arr: string[] = [];
let usedLength = 0;
for(let i = 0; i < partsLength; ++i) {
const {part, offset} = lol[i];
if(offset > usedLength) {
arr.push(encodeEntities(text.slice(usedLength, offset)));
const sliced = text.slice(usedLength, offset);
arr.push(options.noEncoding ? sliced : encodeEntities(sliced));
usedLength = offset;
}
@ -766,7 +819,8 @@ namespace RichTextProcessor { @@ -766,7 +819,8 @@ namespace RichTextProcessor {
}
if(usedLength < text.length) {
arr.push(encodeEntities(text.slice(usedLength)));
const sliced = text.slice(usedLength);
arr.push(options.noEncoding ? sliced : encodeEntities(sliced));
}
return arr.join('');
@ -834,7 +888,7 @@ namespace RichTextProcessor { @@ -834,7 +888,7 @@ namespace RichTextProcessor {
return url;
}
export function replaceUrlEncodings(urlWithEncoded: string) {
/* export function replaceUrlEncodings(urlWithEncoded: string) {
return urlWithEncoded.replace(/(%[A-Z\d]{2})+/g, (str) => {
try {
return decodeURIComponent(str);
@ -842,43 +896,23 @@ namespace RichTextProcessor { @@ -842,43 +896,23 @@ namespace RichTextProcessor {
return str;
}
});
}
export function wrapPlainText(text: string) {
if(IS_EMOJI_SUPPORTED) {
return text;
}
} */
if(!text || !text.length) {
return '';
/**
* ! This function is still unsafe to use with .innerHTML
*/
export function wrapPlainText(text: string, entities?: MessageEntity[]) {
if(entities?.length) {
entities = entities.filter(entity => entity._ === 'messageEntitySpoiler');
}
text = text.replace(/\ufe0f/g, '');
var match;
var raw = text;
const arr: string[] = [];
let emojiTitle;
fullRegExp.lastIndex = 0;
while((match = raw.match(fullRegExp))) {
arr.push(raw.substr(0, match.index))
if(match[8]) {
// @ts-ignore
const emojiCode = EmojiHelper.emojiMap[match[8]];
if(emojiCode &&
// @ts-ignore
(emojiTitle = emojiData[emojiCode][1][0])) {
arr.push(':' + emojiTitle + ':');
} else {
arr.push(match[0]);
}
} else {
arr.push(match[0]);
}
raw = raw.substr(match.index + match[0].length);
}
arr.push(raw);
return arr.join('');
return wrapRichText(text, {
entities,
noEncoding: true,
noTextFormat: true,
noLinebreaks: true,
noLinks: true
});
}
export function wrapEmojiText(text: string, isDraft = false) {

66
src/scss/partials/_spoiler.scss

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
.spoiler {
--anim: .4s ease;
position: relative;
background-color: var(--spoiler-background-color);
&-text {
opacity: 0;
}
/* &-draft {
background-color: var(--spoiler-draft-background-color);
} */
}
[style*="spoiler"] {
background-color: var(--spoiler-draft-background-color);
font-family: inherit !important;
}
.message {
&.will-change {
.spoiler {
// box-shadow: 0 0 var(--spoiler-background-color);
&-text {
filter: blur(6px);
}
}
}
&.is-spoiler-visible {
&.animating {
.spoiler {
transition: /* box-shadow var(--anim), */ background-color var(--anim);
&-text {
transition: opacity var(--anim), filter var(--anim);
}
}
}
&:not(.backwards) {
.spoiler {
background-color: transparent;
// box-shadow: 0 0 30px 30px transparent;
&-text {
filter: blur(0);
opacity: 1;
}
}
}
&.backwards {
.spoiler-text {
filter: blur(3px);
}
}
}
}

5
src/scss/style.scss

@ -175,6 +175,8 @@ $chat-input-inner-padding-handhelds: .25rem; @@ -175,6 +175,8 @@ $chat-input-inner-padding-handhelds: .25rem;
--link-color: #00488f;
--ripple-color: rgba(0, 0, 0, .08);
--poll-circle-color: var(--border-color);
--spoiler-background-color: #e3e5e8;
--spoiler-draft-background-color: #d9d9d9;
--message-background-color: var(--surface-color);
--message-checkbox-color: #61c642;
@ -241,6 +243,8 @@ $chat-input-inner-padding-handhelds: .25rem; @@ -241,6 +243,8 @@ $chat-input-inner-padding-handhelds: .25rem;
--link-color: var(--primary-color);
--ripple-color: rgba(255, 255, 255, .08);
--poll-circle-color: #fff;
--spoiler-background-color: #373e4e;
--spoiler-draft-background-color: #484848;
--message-background-color: var(--surface-color);
--message-checkbox-color: var(--primary-color);
@ -300,6 +304,7 @@ $chat-input-inner-padding-handhelds: .25rem; @@ -300,6 +304,7 @@ $chat-input-inner-padding-handhelds: .25rem;
@import "partials/colorPicker";
@import "partials/replyKeyboard";
@import "partials/peopleNearby";
@import "partials/spoiler";
@import "partials/popups/popup";
@import "partials/popups/editAvatar";

Loading…
Cancel
Save