diff --git a/src/components/appMediaViewer.ts b/src/components/appMediaViewer.ts index 165f4f72..bce86973 100644 --- a/src/components/appMediaViewer.ts +++ b/src/components/appMediaViewer.ts @@ -29,6 +29,8 @@ import { months, ONE_DAY } from "../helpers/date"; import { SearchSuperContext } from "./appSearchSuper."; import DEBUG from "../config/debug"; import appNavigationController from "./appNavigationController"; +import { Message } from "../layer"; +import { forEachReverse } from "../helpers/array"; // TODO: масштабирование картинок (не SVG) при ресайзе, и правильный возврат на исходную позицию // TODO: картинки "обрезаются" если возвращаются или появляются с места, где есть их перекрытие (топбар, поле ввода) @@ -1338,8 +1340,8 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet //} } - const method = older ? value.history.forEach : value.history.forEachReverse; - method.call(value.history, message => { + const method: any = older ? value.history.forEach.bind(value.history) : forEachReverse.bind(null, value.history); + method((message: Message.message) => { const {mid, peerId} = message; const media = this.getMediaFromMessage(message); diff --git a/src/components/appMediaViewerNew.ts b/src/components/appMediaViewerNew.ts index 66a55855..a9e5ff28 100644 --- a/src/components/appMediaViewerNew.ts +++ b/src/components/appMediaViewerNew.ts @@ -27,7 +27,8 @@ import appSidebarRight, { AppSidebarRight } from "./sidebarRight"; import SwipeHandler from "./swipeHandler"; import { months, ONE_DAY } from "../helpers/date"; import { SearchSuperContext } from "./appSearchSuper."; -import { PhotoSize } from "../layer"; +import { Message, PhotoSize } from "../layer"; +import { forEachReverse } from "../helpers/array"; // TODO: масштабирование картинок (не SVG) при ресайзе, и правильный возврат на исходную позицию // TODO: картинки "обрезаются" если возвращаются или появляются с места, где есть их перекрытие (топбар, поле ввода) @@ -1241,8 +1242,8 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet //} } - const method = older ? value.history.forEach : value.history.forEachReverse; - method.call(value.history, message => { + const method: any = older ? value.history.forEach.bind(value.history) : forEachReverse.bind(null, value.history); + method((message: Message.message) => { const {mid, peerId} = message; const media = this.getMediaFromMessage(message); diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 13e3ec6e..f934f86f 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -48,6 +48,7 @@ import DEBUG from "../../config/debug"; import { SliceEnd } from "../../helpers/slicedArray"; import serverTimeManager from "../../lib/mtproto/serverTimeManager"; import PeerTitle from "../peerTitle"; +import { forEachReverse } from "../../helpers/array"; const USE_MEDIA_TAILS = false; const IGNORE_ACTIONS: Message.messageService['action']['_'][] = [/* 'messageActionHistoryClear' */]; @@ -341,7 +342,7 @@ export default class ChatBubbles { promise.then(() => { }); */ - this.needUpdate.forEachReverse((obj, idx) => { + forEachReverse(this.needUpdate, (obj, idx) => { if(obj.replyMid === mid && obj.replyToPeerId === peerId) { const {mid, replyMid} = this.needUpdate.splice(idx, 1)[0]; @@ -888,7 +889,7 @@ export default class ChatBubbles { public onGoDownClick() { if(this.replyFollowHistory.length) { - this.replyFollowHistory.forEachReverse((mid, idx) => { + forEachReverse(this.replyFollowHistory, (mid, idx) => { const bubble = this.bubbles[mid]; let bad = true; if(bubble) { diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index 34154ed7..806f2b76 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -1386,10 +1386,11 @@ export default class ChatInput { let input = RichTextProcessor.wrapDraftText(message.message, {entities: message.totalEntities}); const f = () => { // ! костыль - const replyText = this.appMessagesManager.getRichReplyText(message, undefined, [message.mid]); + const replyFragment = this.appMessagesManager.wrapMessageForReply(message, undefined, [message.mid]); this.setTopInfo('edit', f, 'Editing', undefined, input, message); const subtitleEl = this.replyElements.container.querySelector('.reply-subtitle'); - subtitleEl.innerHTML = replyText; + subtitleEl.textContent = ''; + subtitleEl.append(replyFragment); this.editMsgId = mid; input = undefined; @@ -1419,13 +1420,22 @@ export default class ChatInput { const title = peerTitles.length < 3 ? peerTitles.join(' and ') : peerTitles[0] + ' and ' + (peerTitles.length - 1) + ' others'; const firstMessage = this.appMessagesManager.getMessageByPeer(fromPeerId, mids[0]); - const replyText = this.appMessagesManager.getRichReplyText(firstMessage, undefined, mids); - if(replyText.includes('Album') || mids.length === 1) { + let usingFullAlbum = true; + if(firstMessage.grouped_id) { + const albumMids = this.appMessagesManager.getMidsByMessage(firstMessage); + if(albumMids.length !== mids.length || albumMids.find(mid => !mids.includes(mid))) { + usingFullAlbum = false; + } + } + + const replyFragment = this.appMessagesManager.wrapMessageForReply(firstMessage, undefined, mids); + if(usingFullAlbum || mids.length === 1) { this.setTopInfo('forward', f, title); // ! костыль const subtitleEl = this.replyElements.container.querySelector('.reply-subtitle'); - subtitleEl.innerHTML = replyText; + subtitleEl.textContent = ''; + subtitleEl.append(replyFragment); } else { this.setTopInfo('forward', f, title, mids.length + ' ' + (mids.length > 1 ? 'forwarded messages' : 'forwarded message')); } diff --git a/src/components/chat/replyContainer.ts b/src/components/chat/replyContainer.ts index b2c47854..32e8d7b3 100644 --- a/src/components/chat/replyContainer.ts +++ b/src/components/chat/replyContainer.ts @@ -1,5 +1,6 @@ import { limitSymbols } from "../../helpers/string"; import appImManager, { CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager"; +import appMessagesManager from "../../lib/appManagers/appMessagesManager"; import appPhotosManager from "../../lib/appManagers/appPhotosManager"; import { RichTextProcessor } from "../../lib/richtextprocessor"; import DivAndCaption from "../divAndCaption"; @@ -22,12 +23,11 @@ export function wrapReplyDivAndCaption(options: { titleEl.innerHTML = title; } - subtitle = limitSymbols(subtitle, 140); - const media = message && message.media; let setMedia = false; if(media && mediaEl) { - subtitle = message.rReply; + subtitleEl.textContent = ''; + subtitleEl.append(appMessagesManager.wrapMessageForReply(message)); //console.log('wrap reply', media); @@ -81,10 +81,11 @@ export function wrapReplyDivAndCaption(options: { } } } else { + subtitle = limitSymbols(subtitle, 140); subtitle = subtitle ? RichTextProcessor.wrapEmojiText(subtitle) : ''; + subtitleEl.innerHTML = subtitle; } - - subtitleEl.innerHTML = subtitle; + return setMedia; } diff --git a/src/components/sidebarRight/tabs/stickers.ts b/src/components/sidebarRight/tabs/stickers.ts index 061df7c8..8443e9d6 100644 --- a/src/components/sidebarRight/tabs/stickers.ts +++ b/src/components/sidebarRight/tabs/stickers.ts @@ -10,6 +10,7 @@ import { RichTextProcessor } from "../../../lib/richtextprocessor"; import { wrapSticker } from "../../wrappers"; import appSidebarRight from ".."; import { StickerSet, StickerSetCovered } from "../../../layer"; +import { forEachReverse } from "../../../helpers/array"; export default class AppStickersTab extends SliderSuperTab { private inputSearch: InputSearch; @@ -202,7 +203,7 @@ export default class AppStickersTab extends SliderSuperTab { coveredSets = coveredSets.slice(); const children = Array.from(this.setsDiv.children) as HTMLElement[]; - children.forEachReverse(el => { + forEachReverse(children, el => { const id = el.dataset.stickerSet; const index = coveredSets.findIndex(covered => covered.set.id === id); diff --git a/src/config/app.ts b/src/config/app.ts index 7bc5f770..a009b0c6 100644 --- a/src/config/app.ts +++ b/src/config/app.ts @@ -2,7 +2,7 @@ const App = { id: 1025907, hash: '452b0359b988148995f22ff0f4229750', version: '0.4.0', - langPackVersion: '0.0.1', + langPackVersion: '0.0.2', domains: [] as string[], baseDcId: 2 }; diff --git a/src/helpers/array.ts b/src/helpers/array.ts index e1aa7d0b..26796cf8 100644 --- a/src/helpers/array.ts +++ b/src/helpers/array.ts @@ -1,4 +1,4 @@ -import { copy } from "./object"; +/* import { copy } from "./object"; export function listMergeSorted(list1: any[] = [], list2: any[] = []) { const result = copy(list1); @@ -11,7 +11,7 @@ export function listMergeSorted(list1: any[] = [], list2: any[] = []) { } return result; -} +} */ export const accumulate = (arr: number[], initialValue: number) => arr.reduce((acc, value) => acc + value, initialValue); @@ -23,4 +23,10 @@ export function findAndSpliceAll(array: Array, verify: (value: T, index: n } return out; -} \ No newline at end of file +} + +export function forEachReverse(array: Array, callback: (value: T, index?: number, array?: Array) => void) { + for(let length = array.length, i = length - 1; i >= 0; --i) { + callback(array[i], i, array); + } +}; diff --git a/src/helpers/dom.ts b/src/helpers/dom.ts index 4421aa48..4ec7e41d 100644 --- a/src/helpers/dom.ts +++ b/src/helpers/dom.ts @@ -798,3 +798,10 @@ export function toggleDisability(elements: HTMLElement[], disable: boolean) { export function canFocus(isFirstInput: boolean) { return !isMobileSafari || !isFirstInput; } + +export function htmlToDocumentFragment(html: string) { + var template = document.createElement('template'); + html = html.trim(); // Never return a text node of whitespace as the result + template.innerHTML = html; + return template.content; +} diff --git a/src/lang.ts b/src/lang.ts index c59f0957..b73e59ac 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -49,13 +49,15 @@ const lang = { "ActionChangedVideo": "un1 changed the group video", "ActionAddUser": "un1 added un2", "ActionAddUserSelf": "un1 returned to the group", - "ActionAddUserSelfMega": "un1 joined the group", "ActionAddUserSelfYou": "You returned to the group", + "ActionAddUserSelfMega": "un1 joined the group", "ActionLeftUser": "un1 left the group", "ActionKickUser": "un1 removed un2", "ActionInviteUser": "un1 joined the group via invite link", "ActionPinnedNoText": "un1 pinned a message", "ActionMigrateFromGroup": "This group was upgraded to a supergroup", + //"ChannelJoined": "You joined this channel", + "ChannelMegaJoined": "You joined this group", "FilterAlwaysShow": "Include Chats", "FilterNeverShow": "Exclude Chats", "FilterInclude": "Included Chats", @@ -155,6 +157,7 @@ const lang = { "Chat.Service.Channel.UpdatedTitle": "Channel renamed to \"%@\"", "Chat.Service.Channel.UpdatedPhoto": "Channel photo updated", "Chat.Service.Channel.RemovedPhoto": "Channel photo removed", + "Chat.Service.Channel.UpdatedVideo": "Channel video updated", "Chat.Service.BotPermissionAllowed": "You allowed this bot to message you when you logged in on %@", "ChatList.Service.Call.incoming": "Incoming Call (%@)", "ChatList.Service.Call.outgoing": "Outgoing Call (%@)", diff --git a/src/layer.d.ts b/src/layer.d.ts index 8c4737c5..3ccaff39 100644 --- a/src/layer.d.ts +++ b/src/layer.d.ts @@ -967,7 +967,7 @@ export namespace MessageMedia { /** * @link https://core.telegram.org/type/MessageAction */ -export type MessageAction = MessageAction.messageActionEmpty | MessageAction.messageActionChatCreate | MessageAction.messageActionChatEditTitle | MessageAction.messageActionChatEditPhoto | MessageAction.messageActionChatDeletePhoto | MessageAction.messageActionChatAddUser | MessageAction.messageActionChatDeleteUser | MessageAction.messageActionChatJoinedByLink | MessageAction.messageActionChannelCreate | MessageAction.messageActionChatMigrateTo | MessageAction.messageActionChannelMigrateFrom | MessageAction.messageActionPinMessage | MessageAction.messageActionHistoryClear | MessageAction.messageActionGameScore | MessageAction.messageActionPaymentSentMe | MessageAction.messageActionPaymentSent | MessageAction.messageActionPhoneCall | MessageAction.messageActionScreenshotTaken | MessageAction.messageActionCustomAction | MessageAction.messageActionBotAllowed | MessageAction.messageActionSecureValuesSentMe | MessageAction.messageActionSecureValuesSent | MessageAction.messageActionContactSignUp | MessageAction.messageActionGeoProximityReached; +export type MessageAction = MessageAction.messageActionEmpty | MessageAction.messageActionChatCreate | MessageAction.messageActionChatEditTitle | MessageAction.messageActionChatEditPhoto | MessageAction.messageActionChatDeletePhoto | MessageAction.messageActionChatAddUser | MessageAction.messageActionChatDeleteUser | MessageAction.messageActionChatJoinedByLink | MessageAction.messageActionChannelCreate | MessageAction.messageActionChatMigrateTo | MessageAction.messageActionChannelMigrateFrom | MessageAction.messageActionPinMessage | MessageAction.messageActionHistoryClear | MessageAction.messageActionGameScore | MessageAction.messageActionPaymentSentMe | MessageAction.messageActionPaymentSent | MessageAction.messageActionPhoneCall | MessageAction.messageActionScreenshotTaken | MessageAction.messageActionCustomAction | MessageAction.messageActionBotAllowed | MessageAction.messageActionSecureValuesSentMe | MessageAction.messageActionSecureValuesSent | MessageAction.messageActionContactSignUp | MessageAction.messageActionGeoProximityReached | MessageAction.messageActionChatLeave | MessageAction.messageActionChannelDeletePhoto | MessageAction.messageActionChannelEditTitle | MessageAction.messageActionChannelEditPhoto | MessageAction.messageActionChannelEditVideo | MessageAction.messageActionChatEditVideo | MessageAction.messageActionChatAddUsers | MessageAction.messageActionChatJoined | MessageAction.messageActionChatReturn | MessageAction.messageActionChatJoinedYou | MessageAction.messageActionChatReturnYou; export namespace MessageAction { export type messageActionEmpty = { @@ -1102,6 +1102,60 @@ export namespace MessageAction { to_id: Peer, distance: number }; + + export type messageActionChatLeave = { + _: 'messageActionChatLeave', + user_id?: number + }; + + export type messageActionChannelDeletePhoto = { + _: 'messageActionChannelDeletePhoto' + }; + + export type messageActionChannelEditTitle = { + _: 'messageActionChannelEditTitle', + title?: string + }; + + export type messageActionChannelEditPhoto = { + _: 'messageActionChannelEditPhoto', + photo?: Photo + }; + + export type messageActionChannelEditVideo = { + _: 'messageActionChannelEditVideo', + photo?: Photo + }; + + export type messageActionChatEditVideo = { + _: 'messageActionChatEditVideo', + photo?: Photo + }; + + export type messageActionChatAddUsers = { + _: 'messageActionChatAddUsers', + users?: Array + }; + + export type messageActionChatJoined = { + _: 'messageActionChatJoined', + users?: Array + }; + + export type messageActionChatReturn = { + _: 'messageActionChatReturn', + users?: Array + }; + + export type messageActionChatJoinedYou = { + _: 'messageActionChatJoinedYou', + users?: Array + }; + + export type messageActionChatReturnYou = { + _: 'messageActionChatReturnYou', + users?: Array + }; } /** @@ -9018,6 +9072,17 @@ export interface ConstructorDeclMap { 'messageEntityEmoji': MessageEntity.messageEntityEmoji, 'messageEntityHighlight': MessageEntity.messageEntityHighlight, 'messageEntityLinebreak': MessageEntity.messageEntityLinebreak, + 'messageActionChatLeave': MessageAction.messageActionChatLeave, + 'messageActionChannelDeletePhoto': MessageAction.messageActionChannelDeletePhoto, + 'messageActionChannelEditTitle': MessageAction.messageActionChannelEditTitle, + 'messageActionChannelEditPhoto': MessageAction.messageActionChannelEditPhoto, + 'messageActionChannelEditVideo': MessageAction.messageActionChannelEditVideo, + 'messageActionChatEditVideo': MessageAction.messageActionChatEditVideo, + 'messageActionChatAddUsers': MessageAction.messageActionChatAddUsers, + 'messageActionChatJoined': MessageAction.messageActionChatJoined, + 'messageActionChatReturn': MessageAction.messageActionChatReturn, + 'messageActionChatJoinedYou': MessageAction.messageActionChatJoinedYou, + 'messageActionChatReturnYou': MessageAction.messageActionChatReturnYou, } export type InvokeAfterMsg = { diff --git a/src/lib/appManagers/appDialogsManager.ts b/src/lib/appManagers/appDialogsManager.ts index d845afdd..ba82e0b8 100644 --- a/src/lib/appManagers/appDialogsManager.ts +++ b/src/lib/appManagers/appDialogsManager.ts @@ -1056,38 +1056,13 @@ export class AppDialogsManager { /* if(!dom.lastMessageSpan.classList.contains('user-typing')) */ { + dom.lastMessageSpan.textContent = ''; if(highlightWord && lastMessage.message) { - let lastMessageText = appMessagesManager.getRichReplyText(lastMessage, ''); - - let messageText = lastMessage.message; - let entities = RichTextProcessor.parseEntities(messageText.replace(/\n/g, ' ')); - let regExp = new RegExp(escapeRegExp(highlightWord), 'gi'); - let match: any; - - if(!entities) entities = []; - let found = false; - while((match = regExp.exec(messageText)) !== null) { - entities.push({_: 'messageEntityHighlight', length: highlightWord.length, offset: match.index}); - found = true; - } - - if(found) { - entities.sort((a, b) => a.offset - b.offset); - } - - let messageWrapped = RichTextProcessor.wrapRichText(messageText, { - noLinebreaks: true, - entities: entities, - noTextFormat: true - }); - - dom.lastMessageSpan.innerHTML = lastMessageText + messageWrapped; + dom.lastMessageSpan.append(appMessagesManager.wrapMessageForReply(lastMessage, undefined, undefined, false, highlightWord)); } else if(draftMessage) { - dom.lastMessageSpan.innerHTML = draftMessage.rReply; + dom.lastMessageSpan.append(appMessagesManager.wrapMessageForReply(draftMessage)); } else if(!lastMessage.deleted) { - dom.lastMessageSpan.innerHTML = lastMessage.rReply; - } else { - dom.lastMessageSpan.innerHTML = ''; + dom.lastMessageSpan.append(appMessagesManager.wrapMessageForReply(lastMessage)); } /* if(lastMessage.from_id === auth.id) { // You: */ diff --git a/src/lib/appManagers/appDraftsManager.ts b/src/lib/appManagers/appDraftsManager.ts index f4d8247b..238e9868 100644 --- a/src/lib/appManagers/appDraftsManager.ts +++ b/src/lib/appManagers/appDraftsManager.ts @@ -163,7 +163,7 @@ export class AppDraftsManager { const totalEntities = RichTextProcessor.mergeEntities(apiEntities, myEntities); // ! only in this order, otherwise bold and emoji formatting won't work draft.rMessage = RichTextProcessor.wrapDraftText(draft.message, {entities: totalEntities}); - draft.rReply = appMessagesManager.getRichReplyText(draft); + //draft.rReply = appMessagesManager.getRichReplyText(draft); if(draft.reply_to_msg_id) { draft.reply_to_msg_id = appMessagesManager.generateMessageId(draft.reply_to_msg_id); } diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index b3a86435..19b017c5 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -5,10 +5,10 @@ import { tsNow } from "../../helpers/date"; import { createPosterForVideo } from "../../helpers/files"; import { copy, defineNotNumerableProperties, getObjectKeysAndSort } from "../../helpers/object"; import { randomLong } from "../../helpers/random"; -import { splitStringByLength, limitSymbols } from "../../helpers/string"; +import { splitStringByLength, limitSymbols, escapeRegExp } from "../../helpers/string"; import { Chat, ChatFull, Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessageFwdHeader, MessageReplies, MessageReplyHeader, MessagesDialogs, MessagesFilter, MessagesMessages, MessagesPeerDialogs, MethodDeclMap, NotifyPeer, PeerNotifySettings, PhotoSize, SendMessageAction, Update } from "../../layer"; import { InvokeApiOptions } from "../../types"; -import I18n, { langPack, LangPackKey, _i18n } from "../langPack"; +import I18n, { join, langPack, LangPackKey, _i18n } from "../langPack"; import { logger, LogLevels } from "../logger"; import type { ApiFileManager } from '../mtproto/apiFileManager'; //import apiManager from '../mtproto/apiManager'; @@ -39,6 +39,8 @@ import DEBUG, { MOUNT_CLASS_TO } from "../../config/debug"; import SlicedArray, { Slice, SliceEnd } from "../../helpers/slicedArray"; import appNotificationsManager, { NotifyOptions } from "./appNotificationsManager"; import PeerTitle from "../../components/peerTitle"; +import { forEachReverse } from "../../helpers/array"; +import { htmlToDocumentFragment } from "../../helpers/dom"; //console.trace('include'); // TODO: если удалить сообщение в непрогруженном диалоге, то при обновлении, из-за стейта, последнего сообщения в чатлисте не будет @@ -92,6 +94,8 @@ export type MessagesStorage = { [mid: string]: any }; +export type MyMessageActionType = Message.messageService['action']['_']; + export class AppMessagesManager { public static MESSAGE_ID_INCREMENT = 0x10000; public static MESSAGE_ID_OFFSET = 0xFFFFFFFF; @@ -292,7 +296,7 @@ export class AppMessagesManager { } if(state.dialogs) { - state.dialogs.forEachReverse(dialog => { + forEachReverse(state.dialogs, dialog => { dialog.top_message = this.getServerMessageId(dialog.top_message); // * fix outgoing message to avoid copying dialog this.saveConversation(dialog); @@ -1755,7 +1759,7 @@ export class AppMessagesManager { let maxSeenIdIncremented = offsetDate ? true : false; let hasPrepend = false; const noIdsDialogs: {[peerId: number]: Dialog} = {}; - (dialogsResult.dialogs as Dialog[]).forEachReverse(dialog => { + forEachReverse((dialogsResult.dialogs as Dialog[]), dialog => { //const d = Object.assign({}, dialog); // ! нужно передавать folderId, так как по папке !== 0 нет свойства folder_id this.saveConversation(dialog, dialog.folder_id ?? folderId); @@ -2221,7 +2225,7 @@ export class AppMessagesManager { isScheduled: true, isOutgoing: true }> = {}) { - let groups: Set; + //let groups: Set; messages.forEach((message) => { if(message.pFlags === undefined) { message.pFlags = {}; @@ -2384,8 +2388,12 @@ export class AppMessagesManager { //case 'messageActionChannelEditPhoto': case 'messageActionChatEditPhoto': message.action.photo = appPhotosManager.savePhoto(message.action.photo, mediaContext); - if(isBroadcast) { // ! messageActionChannelEditPhoto не существует в принципе, это используется для перевода. - message.action._ = 'messageActionChannelEditPhoto'; + if(message.action.photo.video_sizes) { + message.action._ = isBroadcast ? 'messageActionChannelEditVideo' : 'messageActionChatEditVideo'; + } else { + if(isBroadcast) { // ! messageActionChannelEditPhoto не существует в принципе, это используется для перевода. + message.action._ = 'messageActionChannelEditPhoto'; + } } break; @@ -2405,10 +2413,11 @@ export class AppMessagesManager { if(message.action.users.length === 1) { message.action.user_id = message.action.users[0]; if(message.fromId === message.action.user_id) { + let suffix = message.fromId === appUsersManager.getSelf().id ? 'You' : ''; if(isChannel) { - message.action._ = 'messageActionChatJoined'; + message.action._ = 'messageActionChatJoined' + suffix; } else { - message.action._ = 'messageActionChatReturn'; + message.action._ = 'messageActionChatReturn' + suffix; } } } else if(message.action.users.length > 1) { @@ -2460,7 +2469,7 @@ export class AppMessagesManager { } } - if(message.grouped_id) { + /* if(message.grouped_id) { if(!groups) { groups = new Set(); } @@ -2468,7 +2477,7 @@ export class AppMessagesManager { groups.add(message.grouped_id); } else { message.rReply = this.getRichReplyText(message); - } + } */ if(message.message && message.message.length && !message.totalEntities) { const myEntities = RichTextProcessor.parseEntities(message.message); @@ -2479,7 +2488,7 @@ export class AppMessagesManager { storage[mid] = message; }); - if(groups) { + /* if(groups) { for(const groupId of groups) { const mids = this.groupedMessagesStorage[groupId]; for(const mid in mids) { @@ -2487,11 +2496,28 @@ export class AppMessagesManager { message.rReply = this.getRichReplyText(message); } } - } + } */ } - public getRichReplyText(message: any, text: string = message.message, usingMids?: number[], plain = false) { - let messageText = ''; + public wrapMessageForReply(message: any, text: string, usingMids: number[], plain: true, highlightWord?: string): string; + public wrapMessageForReply(message: any, text?: string, usingMids?: number[], plain?: false, highlightWord?: string): DocumentFragment; + public wrapMessageForReply(message: any, text: string = message.message, usingMids?: number[], plain?: boolean, highlightWord?: string): DocumentFragment | string { + const parts: (HTMLElement | string)[] = []; + + const addPart = (part: string | HTMLElement, text?: string) => { + if(text) { + part += ', '; + } + + if(plain) { + parts.push(part); + } else { + const el = document.createElement('i'); + if(typeof(part) === 'string') el.innerHTML = part; + else el.append(part); + parts.push(el); + } + }; if(message.media) { let usingFullAlbum = true; @@ -2512,7 +2538,7 @@ export class AppMessagesManager { if(usingFullAlbum) { text = this.getAlbumText(message.grouped_id).message; - messageText += 'Album' + (text ? ', ' : '') + ''; + addPart('Album', text); } } else { usingFullAlbum = false; @@ -2522,36 +2548,36 @@ export class AppMessagesManager { const media = message.media; switch(media._) { case 'messageMediaPhoto': - messageText += 'Photo' + (message.message ? ', ' : '') + ''; + addPart('Photo', message.message); break; case 'messageMediaDice': - messageText += plain ? media.emoticon : RichTextProcessor.wrapEmojiText(media.emoticon); + addPart(plain ? media.emoticon : RichTextProcessor.wrapEmojiText(media.emoticon)); break; case 'messageMediaGeo': - messageText += 'Geolocation'; + addPart('Geolocation'); break; case 'messageMediaPoll': - messageText += '' + (plain ? '📊' + ' ' + media.poll.question || 'poll' : media.poll.rReply) + ''; + addPart(plain ? '📊' + ' ' + (media.poll.question || 'poll') : media.poll.rReply); break; case 'messageMediaContact': - messageText += 'Contact'; + addPart('Contact'); break; case 'messageMediaDocument': let document = media.document; if(document.type === 'video') { - messageText = 'Video' + (message.message ? ', ' : '') + ''; + addPart('Video', message.message); } else if(document.type === 'voice') { - messageText = 'Voice message' + (message.message ? ', ' : '') + ''; + addPart('Voice message', message.message); } else if(document.type === 'gif') { - messageText = 'GIF' + (message.message ? ', ' : '') + ''; + addPart('GIF', message.message); } else if(document.type === 'round') { - messageText = 'Video message' + (message.message ? ', ' : '') + ''; + addPart('Video message', message.message); } else if(document.type === 'sticker') { - messageText = (document.stickerEmoji || '') + 'Sticker'; + addPart(((plain ? document.stickerEmojiRaw : document.stickerEmoji) || '') + 'Sticker'); text = ''; } else { - messageText = '' + document.file_name + (message.message ? ', ' : '') + ''; + addPart(document.file_name, message.message); } break; @@ -2565,30 +2591,53 @@ export class AppMessagesManager { } if(message.action) { - const str = this.wrapMessageActionText(message, plain); - - messageText = str ? '' + str + '' : ''; + const actionWrapped = this.wrapMessageActionTextNew(message, plain); + if(actionWrapped) { + addPart(actionWrapped); + } } - let messageWrapped = ''; if(text) { text = limitSymbols(text, 100); - const entities = RichTextProcessor.parseEntities(text.replace(/\n/g, ' ')); + if(plain) { + parts.push(text); + } else { + let entities = RichTextProcessor.parseEntities(text.replace(/\n/g, ' ')); + + if(highlightWord) { + if(!entities) entities = []; + let found = false; + let match: any; + let regExp = new RegExp(escapeRegExp(highlightWord), 'gi'); + while((match = regExp.exec(text)) !== null) { + entities.push({_: 'messageEntityHighlight', length: highlightWord.length, offset: match.index}); + found = true; + } + + if(found) { + entities.sort((a, b) => a.offset - b.offset); + } + } - messageWrapped = plain ? text : RichTextProcessor.wrapRichText(text, { - noLinebreaks: true, - entities, - noLinks: true, - noTextFormat: true - }); + const messageWrapped = RichTextProcessor.wrapRichText(text, { + noLinebreaks: true, + entities, + noLinks: true, + noTextFormat: true + }); + + parts.push(htmlToDocumentFragment(messageWrapped) as any); + } } if(plain) { - messageText = messageText.replace(//g, '').replace(/<\/i>/g, ''); + return parts.join(''); + } else { + const fragment = document.createDocumentFragment(); + fragment.append(...parts); + return fragment; } - - return messageText + messageWrapped; } public getSenderToPeerText(message: MyMessage) { @@ -2606,84 +2655,9 @@ export class AppMessagesManager { return senderTitle; } - public wrapMessageActionText(message: any, plain?: boolean) { - const action = message.action as MessageAction; - - let str = ''; - if((action as MessageAction.messageActionCustomAction).message) { - str = RichTextProcessor.wrapRichText((action as MessageAction.messageActionCustomAction).message, {noLinebreaks: true}); - } else { - let _ = action._; - let suffix = ''; - let l = ''; - - const getNameDivHTML = (peerId: number) => { - const title = appPeersManager.getPeerTitle(peerId); - return title ? (plain ? title + ' ' : `
${title}
`) : ''; - }; - - switch(action._) { - case "messageActionPhoneCall": { - _ += '.' + (action as any).type; - - const duration = action.duration; - if(duration) { - const d: string[] = []; - - d.push(duration % 60 + ' s'); - if(duration >= 60) d.push((duration / 60 | 0) + ' min'); - //if(duration >= 3600) d.push((duration / 3600 | 0) + ' h'); - suffix = ' (' + d.reverse().join(' ') + ')'; - } - - return langPack[_] + suffix; - } - - case 'messageActionChatDeleteUser': - // @ts-ignore - case 'messageActionChatAddUsers': - case 'messageActionChatAddUser': { - const users: number[] = (action as MessageAction.messageActionChatAddUser).users || [(action as MessageAction.messageActionChatDeleteUser).user_id]; - - l = langPack[_].replace('{}', users.map((userId: number) => getNameDivHTML(userId).trim()).join(', ')); - break; - } - - case 'messageActionBotAllowed': { - const anchorHTML = RichTextProcessor.wrapRichText(action.domain, { - entities: [{ - _: 'messageEntityUrl', - length: action.domain.length, - offset: 0 - }] - }); - - l = langPack[_].replace('{}', anchorHTML); - break; - } - - default: - str = langPack[_] || `[${action._}]`; - break; - } - - if(!l) { - l = langPack[_]; - if(l === undefined) { - l = '[' + _ + ']'; - } - } - - str = !l || l[0].toUpperCase() === l[0] ? l : getNameDivHTML(message.fromId) + l + (suffix ? ' ' : ''); - } - - //this.log('message action:', action); - - return str; - } - public wrapMessageActionTextNew(message: any, plain: true): string; public wrapMessageActionTextNew(message: any, plain?: false): HTMLElement; + public wrapMessageActionTextNew(message: any, plain: boolean): HTMLElement | string; public wrapMessageActionTextNew(message: any, plain?: boolean): HTMLElement | string { const element: HTMLElement = plain ? undefined : document.createElement('span'); const action = message.action as MessageAction; @@ -2705,7 +2679,7 @@ export class AppMessagesManager { let args: any[]; const getNameDivHTML = (peerId: number, plain: boolean) => { - return plain ? appPeersManager.getPeerTitle(peerId) + ' ' : (new PeerTitle({peerId})).element; + return plain ? appPeersManager.getPeerTitle(peerId, plain) + ' ' : (new PeerTitle({peerId})).element; }; switch(action._) { @@ -2724,27 +2698,61 @@ export class AppMessagesManager { break; } + case 'messageActionPinMessage': + case 'messageActionContactSignUp': + case 'messageActionChatLeave': + case 'messageActionChatJoined': case 'messageActionChatCreate': - case 'messageActionChatJoinedByLink': { + case 'messageActionChatEditPhoto': + case 'messageActionChatDeletePhoto': + case 'messageActionChatEditVideo': + case 'messageActionChatJoinedByLink': + case 'messageActionChannelEditVideo': + case 'messageActionChannelDeletePhoto': { langPackKey = langPack[_]; args = [getNameDivHTML(message.fromId, plain)]; break; } + case 'messageActionChannelEditTitle': + case 'messageActionChatEditTitle': { + langPackKey = langPack[_]; + + args = []; + if(action._ === 'messageActionChatEditTitle') { + args.push(getNameDivHTML(message.fromId, plain)); + } + + args.push(plain ? action.title : RichTextProcessor.wrapEmojiText(action.title)); + break; + } + case 'messageActionChatDeleteUser': - // @ts-ignore case 'messageActionChatAddUsers': case 'messageActionChatAddUser': { const users: number[] = (action as MessageAction.messageActionChatAddUser).users || [(action as MessageAction.messageActionChatDeleteUser).user_id]; langPackKey = langPack[_]; - args = [ - getNameDivHTML(message.fromId, plain), - users.length > 1 ? - users.map((userId: number) => (getNameDivHTML(userId, true) as string).trim()).join(', ') : - getNameDivHTML(users[0], plain) - ]; + args = [getNameDivHTML(message.fromId, plain)]; + + if(users.length > 1) { + if(plain) { + args.push(...users.map((userId: number) => (getNameDivHTML(userId, true) as string).trim()).join(', ')); + } else { + const fragment = document.createElement('span'); + fragment.append( + ...join( + users.map((userId: number) => getNameDivHTML(userId, false)) as HTMLElement[], + false + ) + ); + args.push(fragment); + } + } else { + args.push(getNameDivHTML(users[0], plain)); + } + break; } @@ -2756,9 +2764,11 @@ export class AppMessagesManager { offset: 0 }] }); + + const node = htmlToDocumentFragment(anchorHTML); langPackKey = langPack[_]; - args = [anchorHTML]; + args = [node]; break; } @@ -2927,7 +2937,7 @@ export class AppMessagesManager { // * В эту функцию попадут только те диалоги, в которых есть read_inbox_max_id и read_outbox_max_id, в отличие от тех, что будут в getTopMessages // ! fix 'dialogFolder', maybe there is better way to do it, this only can happen by 'messages.getPinnedDialogs' by folder_id: 0 - dialogsResult.dialogs.forEachReverse((dialog, idx) => { + forEachReverse(dialogsResult.dialogs, (dialog, idx) => { if(dialog._ === 'dialogFolder') { dialogsResult.dialogs.splice(idx, 1); } @@ -4779,7 +4789,7 @@ export class AppMessagesManager { if(message._ === 'message' && message.fwd_from && options.fwdCount) { notificationMessage = 'Forwarded ' + options.fwdCount + ' messages';//fwdMessagesPluralize(options.fwd_count); } else { - notificationMessage = this.getRichReplyText(message, undefined, undefined, true); + notificationMessage = this.wrapMessageForReply(message, undefined, undefined, true); } } else { notificationMessage = 'New notification'; diff --git a/src/lib/appManagers/appStateManager.ts b/src/lib/appManagers/appStateManager.ts index 7abda329..cd59e282 100644 --- a/src/lib/appManagers/appStateManager.ts +++ b/src/lib/appManagers/appStateManager.ts @@ -1,15 +1,15 @@ import type { Dialog } from './appMessagesManager'; -import { UserAuth } from '../mtproto/mtproto_config'; -import EventListenerBase from '../../helpers/eventListenerBase'; -import rootScope from '../rootScope'; -import sessionStorage from '../sessionStorage'; -import { logger } from '../logger'; +import type { UserAuth } from '../mtproto/mtproto_config'; import type { AppUsersManager } from './appUsersManager'; import type { AppChatsManager } from './appChatsManager'; import type { AuthState } from '../../types'; import type FiltersStorage from '../storages/filters'; import type DialogsStorage from '../storages/dialogs'; import type { AppDraftsManager } from './appDraftsManager'; +import EventListenerBase from '../../helpers/eventListenerBase'; +import rootScope from '../rootScope'; +import sessionStorage from '../sessionStorage'; +import { logger } from '../logger'; import { copy, setDeepProperty, validateInitObject } from '../../helpers/object'; import { getHeavyAnimationPromise } from '../../hooks/useHeavyAnimationCheck'; import App from '../../config/app'; diff --git a/src/lib/appManagers/appStickersManager.ts b/src/lib/appManagers/appStickersManager.ts index 90775a0d..36a75a51 100644 --- a/src/lib/appManagers/appStickersManager.ts +++ b/src/lib/appManagers/appStickersManager.ts @@ -5,6 +5,7 @@ import rootScope from '../rootScope'; import appDocsManager from './appDocsManager'; import AppStorage from '../storage'; import { MOUNT_CLASS_TO } from '../../config/debug'; +import { forEachReverse } from '../../helpers/array'; // TODO: если пак будет сохранён и потом обновлён, то недостающие стикеры не подгрузит @@ -33,7 +34,7 @@ export class AppStickersManager { } public saveStickers(docs: Document[]) { - docs.forEachReverse((doc, idx) => { + forEachReverse(docs, (doc, idx) => { doc = appDocsManager.saveDoc(doc); if(!doc) docs.splice(idx, 1); diff --git a/src/lib/langPack.ts b/src/lib/langPack.ts index 6afff72a..cc8eea27 100644 --- a/src/lib/langPack.ts +++ b/src/lib/langPack.ts @@ -1,4 +1,4 @@ -import { MOUNT_CLASS_TO } from "../config/debug"; +import DEBUG, { MOUNT_CLASS_TO } from "../config/debug"; import { safeAssign } from "../helpers/object"; import { capitalizeFirstLetter } from "../helpers/string"; import type lang from "../lang"; @@ -11,9 +11,12 @@ export const langPack: {[actionType: string]: LangPackKey} = { "messageActionChatCreate": "ActionCreateGroup", "messageActionChatEditTitle": "ActionChangedTitle", "messageActionChatEditPhoto": "ActionChangedPhoto", + "messageActionChatEditVideo": "ActionChangedVideo", "messageActionChatDeletePhoto": "ActionRemovedPhoto", "messageActionChatReturn": "ActionAddUserSelf", + "messageActionChatReturnYou": "ActionAddUserSelfYou", "messageActionChatJoined": "ActionAddUserSelfMega", + "messageActionChatJoinedYou": "ChannelMegaJoined", "messageActionChatAddUser": "ActionAddUser", "messageActionChatAddUsers": "ActionAddUser", "messageActionChatLeave": "ActionLeftUser", @@ -24,6 +27,7 @@ export const langPack: {[actionType: string]: LangPackKey} = { "messageActionChannelCreate": "ActionCreateChannel", "messageActionChannelEditTitle": "Chat.Service.Channel.UpdatedTitle", "messageActionChannelEditPhoto": "Chat.Service.Channel.UpdatedPhoto", + "messageActionChannelEditVideo": "Chat.Service.Channel.UpdatedVideo", "messageActionChannelDeletePhoto": "Chat.Service.Channel.RemovedPhoto", "messageActionHistoryClear": "HistoryCleared", @@ -51,6 +55,8 @@ namespace I18n { ]).then(([langPack]) => { if(!langPack/* || true */) { return getLangPack('en'); + } else if(DEBUG) { + return getLangPack(langPack.lang_code); } else if(langPack.appVersion !== App.langPackVersion) { return getLangPack(langPack.lang_code); } @@ -177,7 +183,7 @@ namespace I18n { return ''; }); - if(lastIndex !== (input.length - 1)) { + if(lastIndex !== input.length) { out.push(input.slice(lastIndex)); } diff --git a/src/lib/mtproto/networker.ts b/src/lib/mtproto/networker.ts index 64f9615a..9bb6f162 100644 --- a/src/lib/mtproto/networker.ts +++ b/src/lib/mtproto/networker.ts @@ -21,6 +21,7 @@ import HTTP from './transports/http'; import type TcpObfuscated from './transports/tcpObfuscated'; import { bigInt2str, cmp, rightShift_, str2bigInt } from '../../vendor/leemon'; +import { forEachReverse } from '../../helpers/array'; //console.error('networker included!', new Error().stack); @@ -196,7 +197,7 @@ export default class MTPNetworker { } if(sentMessage.container) { - sentMessage.inner.forEachReverse((innerSentMessageId, idx) => { + forEachReverse(sentMessage.inner, (innerSentMessageId, idx) => { const innerSentMessage = this.updateSentMessage(innerSentMessageId); if(!innerSentMessage) { sentMessage.inner.splice(idx, 1); diff --git a/src/lib/polyfill.ts b/src/lib/polyfill.ts index 3cb6d319..e349d7d1 100644 --- a/src/lib/polyfill.ts +++ b/src/lib/polyfill.ts @@ -36,13 +36,6 @@ Uint8Array.prototype.toJSON = function() { //return {type: 'bytes', value: [...this]}; }; -Array.prototype.forEachReverse = function(callback: (value: T, index?: number, array?: Array) => void) { - let length = this.length; - for(var i = length - 1; i >= 0; --i) { - callback(this[i], i, this); - } -}; - Array.prototype.findAndSplice = function(verify: (value: T, index?: number, array?: Array) => boolean) { let index = this.findIndex(verify); return index !== -1 ? this.splice(index, 1)[0] : undefined; @@ -88,7 +81,6 @@ declare global { } interface Array { - forEachReverse(callback: (value: T, index?: number, array?: Array) => void): void; findAndSplice(verify: (value: T, index?: number, array?: Array) => boolean): T; } diff --git a/src/lib/rootScope.ts b/src/lib/rootScope.ts index 241b7b74..d07126df 100644 --- a/src/lib/rootScope.ts +++ b/src/lib/rootScope.ts @@ -6,10 +6,10 @@ import type { MyDialogFilter } from "./storages/filters"; import type { ConnectionStatusChange } from "../types"; import type { UserTyping } from "./appManagers/appChatsManager"; import type Chat from "../components/chat/chat"; -import { UserAuth } from "./mtproto/mtproto_config"; -import { State } from "./appManagers/appStateManager"; +import type { UserAuth } from "./mtproto/mtproto_config"; +import type { State } from "./appManagers/appStateManager"; +import type { MyDraftMessage } from "./appManagers/appDraftsManager"; import EventListenerBase from "../helpers/eventListenerBase"; -import { MyDraftMessage } from "./appManagers/appDraftsManager"; import { MOUNT_CLASS_TO } from "../config/debug"; export type BroadcastEvents = { diff --git a/src/lib/storages/filters.ts b/src/lib/storages/filters.ts index c0b7608e..cde3bf1d 100644 --- a/src/lib/storages/filters.ts +++ b/src/lib/storages/filters.ts @@ -7,6 +7,7 @@ import type { AppUsersManager } from "../appManagers/appUsersManager"; import type _rootScope from "../rootScope"; import type {Dialog} from '../appManagers/appMessagesManager'; import apiManager from "../mtproto/mtprotoworker"; +import { forEachReverse } from "../../helpers/array"; export type MyDialogFilter = Modify this.appPeersManager.getInputPeerById(peerId)); }); - c.include_peers.forEachReverse((peerId, idx) => { + forEachReverse(c.include_peers, (peerId, idx) => { if(c.pinned_peers.includes(peerId)) { c.include_peers.splice(idx, 1); } @@ -229,7 +230,7 @@ export default class FiltersStorage { filter[key] = filter[key].map((peer: any) => this.appPeersManager.getPeerId(peer)); }); - filter.include_peers.forEachReverse((peerId, idx) => { + forEachReverse(filter.include_peers, (peerId, idx) => { if(filter.pinned_peers.includes(peerId)) { filter.include_peers.splice(idx, 1); } diff --git a/src/scripts/in/schema_additional_params.json b/src/scripts/in/schema_additional_params.json index 0c8f4bc9..21bc4263 100644 --- a/src/scripts/in/schema_additional_params.json +++ b/src/scripts/in/schema_additional_params.json @@ -144,4 +144,68 @@ "params": [ {"name": "rTitle", "type": "string"} ] +}, { + "predicate": "messageActionChatLeave", + "params": [ + {"name": "user_id", "type": "number"} + ], + "type": "MessageAction" +}, { + "predicate": "messageActionChannelDeletePhoto", + "params": [], + "type": "MessageAction" +}, { + "predicate": "messageActionChannelEditTitle", + "params": [ + {"name": "title", "type": "string"} + ], + "type": "MessageAction" +}, { + "predicate": "messageActionChannelEditPhoto", + "params": [ + {"name": "photo", "type": "Photo"} + ], + "type": "MessageAction" +}, { + "predicate": "messageActionChannelEditVideo", + "params": [ + {"name": "photo", "type": "Photo"} + ], + "type": "MessageAction" +}, { + "predicate": "messageActionChatEditVideo", + "params": [ + {"name": "photo", "type": "Photo"} + ], + "type": "MessageAction" +}, { + "predicate": "messageActionChatAddUsers", + "params": [ + {"name": "users", "type": "Array"} + ], + "type": "MessageAction" +}, { + "predicate": "messageActionChatJoined", + "params": [ + {"name": "users", "type": "Array"} + ], + "type": "MessageAction" +}, { + "predicate": "messageActionChatReturn", + "params": [ + {"name": "users", "type": "Array"} + ], + "type": "MessageAction" +}, { + "predicate": "messageActionChatJoinedYou", + "params": [ + {"name": "users", "type": "Array"} + ], + "type": "MessageAction" +}, { + "predicate": "messageActionChatReturnYou", + "params": [ + {"name": "users", "type": "Array"} + ], + "type": "MessageAction" }] \ No newline at end of file diff --git a/src/scss/partials/_chatlist.scss b/src/scss/partials/_chatlist.scss index 6ac1919a..ae88f703 100644 --- a/src/scss/partials/_chatlist.scss +++ b/src/scss/partials/_chatlist.scss @@ -156,7 +156,6 @@ ul.chatlist { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - vertical-align: middle; //margin: .1rem 0; line-height: 27px; } @@ -286,6 +285,8 @@ ul.chatlist { margin-right: .1rem; //margin-top: .3rem; margin-top: -.3rem; + display: inline-block; + vertical-align: middle; &[class*=" tgico-"] { color: $color-green; @@ -297,6 +298,10 @@ ul.chatlist { } } + .message-time { + vertical-align: middle; + } + .dialog-subtitle-badge { margin-top: 4px; margin-right: -3px;