From 31fcc959890cdcf00c26d3fa9d88e1a55cf180cb Mon Sep 17 00:00:00 2001 From: morethanwords Date: Mon, 29 Nov 2021 17:51:29 +0400 Subject: [PATCH] Forward options Fixed opening scheduled chat after forwarding --- src/components/buttonMenu.ts | 43 +-- src/components/chat/bubbles.ts | 60 +++-- src/components/chat/input.ts | 310 ++++++++++++++++++---- src/components/chat/replyContainer.ts | 2 +- src/components/chat/selection.ts | 4 +- src/components/checkboxField.ts | 1 + src/components/radioForm.ts | 6 +- src/config/app.ts | 2 +- src/lang.ts | 27 ++ src/lib/appManagers/appMessagesManager.ts | 38 ++- src/lib/mediaPlayer.ts | 4 +- src/scss/partials/_button.scss | 9 + src/scss/partials/_chat.scss | 54 +++- src/scss/partials/_chatBubble.scss | 1 + src/scss/partials/_checkbox.scss | 3 +- src/scss/partials/_poll.scss | 5 +- 16 files changed, 449 insertions(+), 120 deletions(-) diff --git a/src/components/buttonMenu.ts b/src/components/buttonMenu.ts index ba94f015..4984585f 100644 --- a/src/components/buttonMenu.ts +++ b/src/components/buttonMenu.ts @@ -16,10 +16,12 @@ export type ButtonMenuItemOptions = { icon?: string, text?: LangPackKey, regularText?: string, - onClick: (e: MouseEvent | TouchEvent) => void, + onClick: (e: MouseEvent | TouchEvent) => void | boolean, element?: HTMLElement, + textElement?: HTMLElement, options?: AttachClickOptions, checkboxField?: CheckboxField, + noCheckboxClickListener?: boolean, keepOpen?: boolean /* , cancelEvent?: true */ }; @@ -27,34 +29,43 @@ export type ButtonMenuItemOptions = { const ButtonMenuItem = (options: ButtonMenuItemOptions) => { if(options.element) return options.element; - const {icon, text, onClick} = options; + const {icon, text, onClick, checkboxField, noCheckboxClickListener} = options; const el = document.createElement('div'); el.className = 'btn-menu-item' + (icon ? ' tgico-' + icon : ''); ripple(el); - const t = text ? i18n(text) : document.createElement('span'); - if(options.regularText) t.innerHTML = options.regularText; - t.classList.add('btn-menu-item-text'); - el.append(t); - - if(options.checkboxField) { - el.append(options.checkboxField.label); - attachClickEvent(el, () => { - options.checkboxField.checked = !options.checkboxField.checked; - }, options.options); + let textElement = options.textElement; + if(!textElement) { + textElement = options.textElement = text ? i18n(text) : document.createElement('span'); + if(options.regularText) textElement.innerHTML = options.regularText; } + + textElement.classList.add('btn-menu-item-text'); + el.append(textElement); - const keepOpen = !!options.checkboxField || !!options.keepOpen; + const keepOpen = !!checkboxField || !!options.keepOpen; // * cancel mobile keyboard close - attachClickEvent(el, CLICK_EVENT_NAME !== 'click' || keepOpen ? (e) => { + attachClickEvent(el, /* CLICK_EVENT_NAME !== 'click' || keepOpen ? */ (e) => { cancelEvent(e); - onClick(e); + const result = onClick(e); + + if(result === false) { + return; + } if(!keepOpen) { closeBtnMenu(); } - } : onClick, options.options); + + if(checkboxField && !noCheckboxClickListener/* && result !== false */) { + checkboxField.checked = checkboxField.input.type === 'radio' ? true : !checkboxField.checked; + } + }/* : onClick */, options.options); + + if(checkboxField) { + el.append(checkboxField.label); + } return options.element = el; }; diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 1abc2262..9d76ee32 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -1854,12 +1854,18 @@ export default class ChatBubbles { return {cached: true, promise: this.chat.setPeerPromise}; } */ + const chatType = this.chat.type; + + if(chatType === 'scheduled') { + lastMsgId = 0; + } + this.historyStorage = this.appMessagesManager.getHistoryStorage(peerId, this.chat.threadId); - let topMessage = this.chat.type === 'pinned' ? this.appMessagesManager.pinnedMessages[peerId].maxId : this.historyStorage.maxId ?? 0; + let topMessage = chatType === 'pinned' ? this.appMessagesManager.pinnedMessages[peerId].maxId : this.historyStorage.maxId ?? 0; const isTarget = lastMsgId !== undefined; // * this one will fix topMessage for null message in history (e.g. channel comments with only 1 comment and it is a topMessage) - /* if(this.chat.type !== 'pinned' && topMessage && !historyStorage.history.slice.includes(topMessage)) { + /* if(chatType !== 'pinned' && topMessage && !historyStorage.history.slice.includes(topMessage)) { topMessage = 0; } */ @@ -1883,6 +1889,8 @@ export default class ChatBubbles { } const isJump = lastMsgId !== topMessage; + + const {scrollable} = this; if(samePeer) { const mounted = this.getMountedBubble(lastMsgId); @@ -1893,7 +1901,7 @@ export default class ChatBubbles { this.chat.dispatchEvent('setPeer', lastMsgId, false); } else if(topMessage && !isJump) { //this.log('will scroll down', this.scroll.scrollTop, this.scroll.scrollHeight); - this.scrollable.scrollTop = this.scrollable.scrollHeight; + scrollable.scrollTop = scrollable.scrollHeight; this.chat.dispatchEvent('setPeer', lastMsgId, true); } @@ -1909,16 +1917,16 @@ export default class ChatBubbles { this.replyFollowHistory.length = 0; this.passEntities = { - messageEntityBotCommand: this.appPeersManager.isAnyGroup(this.peerId) || this.appUsersManager.isBot(this.peerId) + messageEntityBotCommand: this.appPeersManager.isAnyGroup(peerId) || this.appUsersManager.isBot(peerId) }; } if(DEBUG) { - this.log('setPeer peerId:', this.peerId, this.historyStorage, lastMsgId, topMessage); + this.log('setPeer peerId:', peerId, this.historyStorage, lastMsgId, topMessage); } // add last message, bc in getHistory will load < max_id - const additionMsgId = isJump || this.chat.type === 'scheduled' ? 0 : topMessage; + const additionMsgId = isJump || chatType === 'scheduled' ? 0 : topMessage; /* this.setPeerPromise = null; this.preloader.detach(); @@ -1943,12 +1951,12 @@ export default class ChatBubbles { const oldChatInner = this.chatInner; this.cleanup(); - this.chatInner = document.createElement('div'); + const chatInner = this.chatInner = document.createElement('div'); if(samePeer) { - this.chatInner.className = oldChatInner.className; - this.chatInner.classList.remove('disable-hover', 'is-scrolling'); + chatInner.className = oldChatInner.className; + chatInner.classList.remove('disable-hover', 'is-scrolling'); } else { - this.chatInner.classList.add('bubbles-inner'); + chatInner.classList.add('bubbles-inner'); } this.lazyLoadQueue.lock(); @@ -1970,7 +1978,7 @@ export default class ChatBubbles { // clear if(!cached) { if(!samePeer) { - this.scrollable.container.textContent = ''; + scrollable.container.textContent = ''; //oldChatInner.remove(); this.chat.finishPeerChange(isTarget, isJump, lastMsgId); this.preloader.attach(this.bubblesContainer); @@ -2000,9 +2008,9 @@ export default class ChatBubbles { // this.ladderDeferred.resolve(); - this.scrollable.lastScrollDirection = 0; - this.scrollable.lastScrollTop = 0; - replaceContent(this.scrollable.container, this.chatInner); + scrollable.lastScrollDirection = 0; + scrollable.lastScrollTop = 0; + replaceContent(scrollable.container, chatInner); animationIntersector.unlockGroup(CHAT_ANIMATION_GROUP); animationIntersector.checkAnimations(false, CHAT_ANIMATION_GROUP/* , true */); @@ -2013,7 +2021,7 @@ export default class ChatBubbles { //if(dialog && lastMsgID && lastMsgID !== topMessage && (this.bubbles[lastMsgID] || this.firstUnreadBubble)) { if(savedPosition) { - this.scrollable.scrollTop = savedPosition.top; + scrollable.scrollTop = savedPosition.top; /* const mountedByLastMsgId = this.getMountedBubble(lastMsgId); let bubble: HTMLElement = mountedByLastMsgId?.bubble; if(!bubble?.parentElement) { @@ -2023,15 +2031,15 @@ export default class ChatBubbles { if(bubble) { const top = bubble.getBoundingClientRect().top; const distance = savedPosition.top - top; - this.scrollable.scrollTop += distance; + scrollable.scrollTop += distance; } */ } else if((topMessage && isJump) || isTarget) { const fromUp = maxBubbleId > 0 && (maxBubbleId < lastMsgId || lastMsgId < 0); const followingUnread = readMaxId === lastMsgId && !isTarget; if(!fromUp && samePeer) { - this.scrollable.scrollTop = 99999; + scrollable.scrollTop = 99999; } else if(fromUp/* && (samePeer || forwardingUnread) */) { - this.scrollable.scrollTop = 0; + scrollable.scrollTop = 0; } const mountedByLastMsgId = this.getMountedBubble(lastMsgId); @@ -2048,7 +2056,7 @@ export default class ChatBubbles { } } } else { - this.scrollable.scrollTop = 99999; + scrollable.scrollTop = 99999; } this.onScroll(); @@ -2056,7 +2064,7 @@ export default class ChatBubbles { const middleware = this.getMiddleware(); const afterSetPromise = Promise.all([setPeerPromise, getHeavyAnimationPromise()]); afterSetPromise.then(() => { // check whether list isn't full - this.scrollable.checkForTriggers(); + scrollable.checkForTriggers(); }); this.chat.dispatchEvent('setPeer', lastMsgId, !isJump); @@ -2074,7 +2082,7 @@ export default class ChatBubbles { return; } - this.scrollable.checkForTriggers(); + scrollable.checkForTriggers(); if(needFetchInterval) { const f = () => { @@ -2092,7 +2100,7 @@ export default class ChatBubbles { const slice = historyStorage.history.slice; const isBottomEnd = slice.isEnd(SliceEnd.Bottom); - if(this.scrollable.loadedAll.bottom && this.scrollable.loadedAll.bottom !== isBottomEnd) { + if(scrollable.loadedAll.bottom && scrollable.loadedAll.bottom !== isBottomEnd) { this.setLoaded('bottom', isBottomEnd); this.onScroll(); } @@ -2114,14 +2122,14 @@ export default class ChatBubbles { }); } - this.log('scrolledAllDown:', this.scrollable.loadedAll.bottom); + this.log('scrolledAllDown:', scrollable.loadedAll.bottom); //if(!this.unreaded.length && dialog) { // lol - if(this.scrollable.loadedAll.bottom && topMessage && !this.unreaded.size) { // lol + if(scrollable.loadedAll.bottom && topMessage && !this.unreaded.size) { // lol this.onScrolledAllDown(); } - if(this.chat.type === 'chat') { + if(chatType === 'chat') { const dialog = this.appMessagesManager.getDialogOnly(peerId); if(dialog?.pFlags.unread_mark) { this.appMessagesManager.markDialogUnread(peerId, true); @@ -2624,7 +2632,7 @@ export default class ChatBubbles { if(message.pFlags.unread || isOutgoing) this.unreadOut.add(message.mid); let status = ''; if(isOutgoing) status = 'is-sending'; - else status = message.pFlags.unread ? 'is-sent' : 'is-read'; + else status = message.pFlags.unread || message.pFlags.is_scheduled ? 'is-sent' : 'is-read'; bubble.classList.add(status); } diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index 52e540b3..f58aa25e 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -70,7 +70,7 @@ import ReplyKeyboard from './replyKeyboard'; import InlineHelper from './inlineHelper'; import debounce from '../../helpers/schedulers/debounce'; import noop from '../../helpers/noop'; -import { putPreloader } from '../misc'; +import { openBtnMenu, putPreloader } from '../misc'; import SetTransition from '../singleTransition'; import PeerTitle from '../peerTitle'; import { fastRaf } from '../../helpers/schedulers'; @@ -84,6 +84,10 @@ import { NULL_PEER_ID } from '../../lib/mtproto/mtproto_config'; import setCaretAt from '../../helpers/dom/setCaretAt'; import getKeyFromEvent from '../../helpers/dom/getKeyFromEvent'; import getKeyFromEventCaseInsensitive from '../../helpers/dom/getKeyFromEventCaseInsensitive'; +import CheckboxField from '../checkboxField'; +import DropdownHover from '../../helpers/dropdownHover'; +import RadioForm from '../radioForm'; +import findUpTag from '../../helpers/dom/findUpTag'; const RECORD_MIN_TIME = 500; const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.'; @@ -123,7 +127,17 @@ export default class ChatInput { iconBtn: HTMLButtonElement } = {} as any; - private forwardElements: {} = {} as any; + private forwardElements: { + changePeer: ButtonMenuItemOptions, + showSender: ButtonMenuItemOptions, + hideSender: ButtonMenuItemOptions, + showCaption: ButtonMenuItemOptions, + hideCaption: ButtonMenuItemOptions, + container: HTMLElement, + modifyArgs?: ButtonMenuItemOptions[] + } = {} as any; + private forwardHover: DropdownHover; + private forwardWasDroppingAuthor: boolean; private getWebPagePromise: Promise; private willSendWebPage: WebPage = null; @@ -313,14 +327,119 @@ export default class ChatInput { this.replyElements.container.append(this.replyElements.iconBtn, this.replyElements.cancelBtn); - const forwardBtnMenu = ButtonMenu([], this.listenerSetter); + // + + const onHideAuthorClick = () => { + isChangingAuthor = true; + return this.canToggleHideAuthor(); + }; + + const onHideCaptionClick = () => { + isChangingAuthor = false; + }; + + const forwardElements: ChatInput['forwardElements'] = this.forwardElements = {} as any; + let isChangingAuthor = false; + const forwardButtons: ButtonMenuItemOptions[] = [ + forwardElements.showSender = { + text: 'Chat.Alert.Forward.Action.Show1', + onClick: onHideAuthorClick, + checkboxField: new CheckboxField({checked: true}) + }, + forwardElements.hideSender = { + text: 'Chat.Alert.Forward.Action.Hide1', + onClick: onHideAuthorClick, + checkboxField: new CheckboxField({checked: false}) + }, + forwardElements.showCaption = { + text: 'Chat.Alert.Forward.Action.ShowCaption', + onClick: onHideCaptionClick, + checkboxField: new CheckboxField({checked: true}) + }, + forwardElements.hideCaption = { + text: 'Chat.Alert.Forward.Action.HideCaption', + onClick: onHideCaptionClick, + checkboxField: new CheckboxField({checked: false}) + }, + forwardElements.changePeer = { + text: 'Chat.Alert.Forward.Action.Another', + onClick: () => { + this.changeForwardRecipient(); + }, + icon: 'replace' + } + ]; + const forwardBtnMenu = forwardElements.container = ButtonMenu(forwardButtons, this.listenerSetter); + // forwardBtnMenu.classList.add('top-center'); + + const children = Array.from(forwardBtnMenu.children) as HTMLElement[]; + const groups: { + elements: HTMLElement[], + onChange: (value: string, event: Event) => void + }[] = [{ + elements: children.slice(0, 2), + onChange: (value, e) => { + const checked = !!+value; + if(isChangingAuthor) { + this.forwardWasDroppingAuthor = !checked; + } + + const replyTitle = this.replyElements.container.querySelector('.reply-title'); + if(replyTitle) { + const el = replyTitle.firstElementChild as HTMLElement; + const i = I18n.weakMap.get(el) as I18n.IntlElement; + const langPackKey: LangPackKey = forwardElements.showSender.checkboxField.checked ? 'Chat.Accessory.Forward' : 'Chat.Accessory.Hidden'; + i.key = langPackKey; + i.update(); + } + } + }, { + elements: children.slice(2, 4), + onChange: (value) => { + const checked = !!+value; + let b: ButtonMenuItemOptions; + if(checked && this.forwardWasDroppingAuthor !== undefined) { + b = this.forwardWasDroppingAuthor ? forwardElements.hideSender : forwardElements.showSender; + } else { + b = checked ? forwardElements.showSender : forwardElements.hideSender; + } + + b.checkboxField.checked = true; + } + }]; + groups.forEach(group => { + const container = RadioForm(group.elements.map(e => { + return { + container: e, + input: e.querySelector('input') + }; + }), group.onChange); + + const hr = document.createElement('hr'); + container.append(hr); + forwardBtnMenu.append(container); + }); - this.forwardElements = { - container: forwardBtnMenu - } as any; + forwardBtnMenu.append(forwardElements.changePeer.element); + if(!IS_TOUCH_SUPPORTED) { + const forwardHover = this.forwardHover = new DropdownHover({ + element: forwardBtnMenu + }); + } + + forwardElements.modifyArgs = forwardButtons.slice(0, -1); this.replyElements.container.append(forwardBtnMenu); + forwardElements.modifyArgs.forEach((b, idx) => { + const {input} = b.checkboxField; + input.type = 'radio'; + input.name = idx < 2 ? 'author' : 'caption'; + input.value = '' + +!(idx % 2); + }); + + // + this.newMessageWrapper = document.createElement('div'); this.newMessageWrapper.classList.add('new-message-wrapper'); @@ -467,7 +586,7 @@ export default class ChatInput { openSide: 'top-left', onContextElement: this.btnSend, onOpen: () => { - return !this.isInputEmpty(); + return !this.isInputEmpty() || !!Object.keys(this.forwarding).length; } }); @@ -706,9 +825,15 @@ export default class ChatInput { } public scheduleSending = (callback: () => void = this.sendMessage.bind(this, true), initDate = new Date()) => { - const canSendWhenOnline = rootScope.myId !== this.chat.peerId && this.chat.peerId.isUser() && this.appUsersManager.isUserOnlineVisible(this.chat.peerId); + const {peerId} = this.chat; + const middleware = this.chat.bubbles.getMiddleware(); + const canSendWhenOnline = rootScope.myId !== peerId && peerId.isUser() && this.appUsersManager.isUserOnlineVisible(peerId); new PopupSchedule(initDate, (timestamp) => { + if(!middleware()) { + return; + } + const minTimestamp = (Date.now() / 1000 | 0) + 10; if(timestamp <= minTimestamp) { timestamp = undefined; @@ -718,7 +843,13 @@ export default class ChatInput { callback(); if(this.chat.type !== 'scheduled' && timestamp) { - this.appImManager.openScheduled(this.chat.peerId); + setTimeout(() => { // ! need timeout here because .forwardMessages will be called after timeout + if(!middleware()) { + return; + } + + this.appImManager.openScheduled(peerId); + }, 0); } }, canSendWhenOnline).show(); }; @@ -842,6 +973,12 @@ export default class ChatInput { }/* else if(this.chat.type === 'chat') { } */ + if(this.forwardElements) { + this.forwardWasDroppingAuthor = false; + this.forwardElements.showCaption.checkboxField.setValueSilently(true); + this.forwardElements.showSender.checkboxField.setValueSilently(true); + } + if(this.btnScheduled) { this.btnScheduled.classList.add('hide'); const middleware = this.chat.bubbles.getMiddleware(); @@ -1668,26 +1805,11 @@ export default class ChatInput { private onHelperClick = (e: Event) => { cancelEvent(e); - if(!findUpClassName(e.target, 'reply-wrapper')) return; + if(!findUpClassName(e.target, 'reply')) return; if(this.helperType === 'forward') { - if(this.helperWaitingForward) return; - this.helperWaitingForward = true; - - const helperFunc = this.helperFunc; - this.clearHelper(); - this.updateSendBtn(); - let selected = false; - const popup = new PopupForward(copy(this.forwarding), () => { - selected = true; - }); - - popup.addEventListener('close', () => { - this.helperWaitingForward = false; - - if(!selected) { - helperFunc(); - } - }); + if(IS_TOUCH_SUPPORTED && !this.forwardElements.container.classList.contains('active')) { + openBtnMenu(this.forwardElements.container); + } } else if(this.helperType === 'reply') { this.chat.setMessageId(this.replyToMsgId); } else if(this.helperType === 'edit') { @@ -1695,6 +1817,27 @@ export default class ChatInput { } }; + private changeForwardRecipient() { + if(this.helperWaitingForward) return; + this.helperWaitingForward = true; + + const helperFunc = this.helperFunc; + this.clearHelper(); + this.updateSendBtn(); + let selected = false; + const popup = new PopupForward(copy(this.forwarding), () => { + selected = true; + }); + + popup.addEventListener('close', () => { + this.helperWaitingForward = false; + + if(!selected) { + helperFunc(); + } + }); + } + public clearInput(canSetDraft = true, fireEvent = true, clearValue = '') { if(document.activeElement === this.messageInput && IS_MOBILE_SAFARI) { // fix first char uppercase const i = document.createElement('input'); @@ -1787,37 +1930,41 @@ export default class ChatInput { } public sendMessage(force = false) { - if(this.chat.type === 'scheduled' && !force && !this.editMsgId) { + const {editMsgId, chat} = this; + if(chat.type === 'scheduled' && !force && !editMsgId) { this.scheduleSending(); return; } + const {threadId, peerId} = chat; + const {replyToMsgId, noWebPage, sendSilent, scheduleDate} = this; + const {value, entities} = getRichValue(this.messageInputField.input); //return; - if(this.editMsgId) { + if(editMsgId) { const message = this.editMessage; - if(!!value.trim() || message.media) { + if(value.trim() || message.media) { this.appMessagesManager.editMessage(message, value, { entities, - noWebPage: this.noWebPage + noWebPage: noWebPage }); this.onMessageSent(); } else { - new PopupDeleteMessages(this.chat.peerId, [this.editMsgId], this.chat.type); + new PopupDeleteMessages(peerId, [editMsgId], chat.type); return; } - } else { - this.appMessagesManager.sendText(this.chat.peerId, value, { + } else if(value.trim()) { + this.appMessagesManager.sendText(peerId, value, { entities, - replyToMsgId: this.replyToMsgId, - threadId: this.chat.threadId, - noWebPage: this.noWebPage, + replyToMsgId: replyToMsgId, + threadId: threadId, + noWebPage: noWebPage, webPage: this.getWebPagePromise ? undefined : this.willSendWebPage, - scheduleDate: this.scheduleDate, - silent: this.sendSilent, + scheduleDate: scheduleDate, + silent: sendSilent, clearDraft: true }); @@ -1828,14 +1975,13 @@ export default class ChatInput { // * wait for sendText set messageId for invokeAfterMsg if(this.forwarding) { const forwarding = copy(this.forwarding); - const peerId = this.chat.peerId; - const silent = this.sendSilent; - const scheduleDate = this.scheduleDate; setTimeout(() => { for(const fromPeerId in forwarding) { this.appMessagesManager.forwardMessages(peerId, fromPeerId.toPeerId(), forwarding[fromPeerId], { - silent, - scheduleDate: scheduleDate + silent: sendSilent, + scheduleDate: scheduleDate, + dropAuthor: this.forwardElements.hideSender.checkboxField.checked, + dropCaptions: this.forwardElements.hideCaption.checkboxField.checked }); } @@ -1883,6 +2029,12 @@ export default class ChatInput { return false; } + private canToggleHideAuthor() { + const hideCaptionCheckboxField = this.forwardElements.hideCaption.checkboxField; + return !hideCaptionCheckboxField.checked || + findUpTag(hideCaptionCheckboxField.label, 'FORM').classList.contains('hide'); + } + /* public sendSomething(callback: () => void, force = false) { if(this.chat.type === 'scheduled' && !force) { this.scheduleSending(() => this.sendSomething(callback, true)); @@ -1915,7 +2067,7 @@ export default class ChatInput { //const peerTitles: string[] const fromPeerIds = Object.keys(fromPeerIdsMids).map(fromPeerId => fromPeerId.toPeerId()); const smth: Set = new Set(); - let length = 0; + let length = 0, messagesWithCaptionsLength = 0; fromPeerIds.forEach(fromPeerId => { const mids = fromPeerIdsMids[fromPeerId]; @@ -1926,6 +2078,10 @@ export default class ChatInput { } else { smth.add('P' + message.fromId); } + + if(message.media && message.message) { + ++messagesWithCaptionsLength; + } }); length += mids.length; @@ -1935,19 +2091,25 @@ export default class ChatInput { const peerTitles = [...smth].map(smth => { const type = smth[0]; smth = smth.slice(1); - return type === 'P' ? - new PeerTitle({peerId: smth.toPeerId(), dialog: false, onlyFirstName}).element : - (onlyFirstName ? smth.split(' ')[0] : smth); + if(type === 'P') { + const peerId = smth.toPeerId(); + return peerId === rootScope.myId ? i18n('Chat.Accessory.Forward.You') : new PeerTitle({peerId, dialog: false, onlyFirstName}).element; + } else { + return onlyFirstName ? smth.split(' ')[0] : smth; + } }); - const title = document.createDocumentFragment(); + const titleKey: LangPackKey = this.forwardElements.showSender.checkboxField.checked ? 'Chat.Accessory.Forward' : 'Chat.Accessory.Hidden'; + const title = i18n(titleKey, [length]); + + const senderTitles = document.createDocumentFragment(); if(peerTitles.length < 3) { - title.append(...join(peerTitles, false)); + senderTitles.append(...join(peerTitles, false)); } else { - title.append(peerTitles[0], i18n('AndOther', [peerTitles.length - 1])); + senderTitles.append(peerTitles[0], i18n('AndOther', [peerTitles.length - 1])); } - - let firstMessage: any, usingFullAlbum: boolean; + + let firstMessage: Message.message, usingFullAlbum: boolean; if(fromPeerIds.length === 1) { const fromPeerId = fromPeerIds[0]; const mids = fromPeerIdsMids[fromPeerId]; @@ -1961,13 +2123,45 @@ export default class ChatInput { } } } - + + const subtitleFragment = document.createDocumentFragment(); + const delimiter = ': '; if(usingFullAlbum || length === 1) { const mids = fromPeerIdsMids[fromPeerIds[0]]; const replyFragment = this.appMessagesManager.wrapMessageForReply(firstMessage, undefined, mids); - this.setTopInfo('forward', f, title, replyFragment); + subtitleFragment.append( + senderTitles, + delimiter, + replyFragment + ); } else { - this.setTopInfo('forward', f, title, i18n('ForwardedMessageCount', [length])); + subtitleFragment.append( + i18n('Chat.Accessory.Forward.From'), + delimiter, + senderTitles + ); + } + + let newReply = this.setTopInfo('forward', f, title, subtitleFragment); + + this.forwardElements.modifyArgs.forEach((b, idx) => { + const text = b.textElement; + const intl: I18n.IntlElement = I18n.weakMap.get(text) as any; + intl.args = [idx < 2 ? fromPeerIds.length : messagesWithCaptionsLength]; + intl.update(); + }); + + const form = findUpTag(this.forwardElements.showCaption.checkboxField.label, 'FORM'); + form.classList.toggle('hide', !messagesWithCaptionsLength); + const hideCaption = this.forwardElements.hideCaption.checkboxField.checked; + if(messagesWithCaptionsLength && hideCaption) { + this.forwardElements.hideSender.checkboxField.setValueSilently(true); + } else if(this.forwardWasDroppingAuthor !== undefined) { + (this.forwardWasDroppingAuthor ? this.forwardElements.hideSender : this.forwardElements.showSender).checkboxField.setValueSilently(true); + } + + if(this.forwardHover) { + this.forwardHover.attachButtonListener(newReply, this.listenerSetter); } this.forwarding = fromPeerIdsMids; @@ -2114,6 +2308,8 @@ export default class ChatInput { setTimeout(() => { this.updateSendBtn(); }, 0); + + return newReply; } // public saveScroll() { diff --git a/src/components/chat/replyContainer.ts b/src/components/chat/replyContainer.ts index 9d3d3843..b2f1ee9e 100644 --- a/src/components/chat/replyContainer.ts +++ b/src/components/chat/replyContainer.ts @@ -44,7 +44,7 @@ export function wrapReplyDivAndCaption(options: { let middleware: () => boolean; if(media && mediaEl) { subtitleEl.textContent = ''; - subtitleEl.append(appMessagesManager.wrapMessageForReply(message)); + subtitleEl.append(appMessagesManager.wrapMessageForReply(message, undefined, undefined, undefined, undefined, true)); //console.log('wrap reply', media); diff --git a/src/components/chat/selection.ts b/src/components/chat/selection.ts index 37ab10b4..ac16dace 100644 --- a/src/components/chat/selection.ts +++ b/src/components/chat/selection.ts @@ -622,7 +622,7 @@ export class SearchSelection extends AppSelection { attachClickEvent(this.selectionForwardBtn, () => { const obj: {[fromPeerId: PeerId]: number[]} = {}; for(const [fromPeerId, mids] of this.selectedMids) { - obj[fromPeerId] = Array.from(mids); + obj[fromPeerId] = Array.from(mids).sort((a, b) => a - b); } new PopupForward(obj, () => { @@ -897,7 +897,7 @@ export default class ChatSelection extends AppSelection { attachClickEvent(this.selectionForwardBtn, () => { const obj: {[fromPeerId: PeerId]: number[]} = {}; for(const [fromPeerId, mids] of this.selectedMids) { - obj[fromPeerId] = Array.from(mids); + obj[fromPeerId] = Array.from(mids).sort((a, b) => a - b); } new PopupForward(obj, () => { diff --git a/src/components/checkboxField.ts b/src/components/checkboxField.ts index 41133554..4e9f0385 100644 --- a/src/components/checkboxField.ts +++ b/src/components/checkboxField.ts @@ -45,6 +45,7 @@ export default class CheckboxField { } const input = this.input = document.createElement('input'); + input.classList.add('checkbox-field-input'); input.type = 'checkbox'; if(options.name) { input.id = 'input-' + options.name; diff --git a/src/components/radioForm.ts b/src/components/radioForm.ts index 7dcb3727..87e7ac05 100644 --- a/src/components/radioForm.ts +++ b/src/components/radioForm.ts @@ -4,15 +4,15 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -export default function RadioForm(radios: {container: HTMLElement, input: HTMLInputElement}[], onChange: (value: string) => void) { +export default function RadioForm(radios: {container: HTMLElement, input: HTMLInputElement}[], onChange: (value: string, event: Event) => void) { const form = document.createElement('form'); radios.forEach(r => { const {container, input} = r; form.append(container); - input.addEventListener('change', () => { + input.addEventListener('change', (e) => { if(input.checked) { - onChange(input.value); + onChange(input.value, e); } }); }); diff --git a/src/config/app.ts b/src/config/app.ts index 3d860576..6b7b9a05 100644 --- a/src/config/app.ts +++ b/src/config/app.ts @@ -19,7 +19,7 @@ const App = { version: process.env.VERSION, versionFull: process.env.VERSION_FULL, build: +process.env.BUILD, - langPackVersion: '0.3.6', + langPackVersion: '0.3.7', langPack: 'macos', langPackCode: 'en', domains: [MAIN_DOMAIN] as string[], diff --git a/src/lang.ts b/src/lang.ts index 2af4d217..5621e68e 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -609,6 +609,33 @@ const lang = { "Contacts.PhoneNumber.NotRegistred": "The person with this phone number is not registered on Telegram yet.", "Channel.UsernameAboutChannel": "People can share this link with others and can find your channel using Telegram search.", "Channel.UsernameAboutGroup": "People can share this link with others and find your group using Telegram search.", + "Chat.Accessory.Forward": { + "one_value": "Forward Message", + "other_value": "Forward %d Messages" + }, + "Chat.Accessory.Forward.You": "You", + "Chat.Accessory.Forward.From": "From", + "Chat.Accessory.Hidden": { + "one_value": "Forward Message (sender's name hidden)", + "other_value": "Forward %d Messages (senders' names hidden)" + }, + "Chat.Alert.Forward.Action.Another": "Forward to Another Chat", + "Chat.Alert.Forward.Action.Hide1": { + "one_value": "Hide Sender's Name", + "other_value": "Hide Senders' Names" + }, + "Chat.Alert.Forward.Action.Show1": { + "one_value": "Show Sender's Name", + "other_value": "Show Senders' Names" + }, + "Chat.Alert.Forward.Action.ShowCaption": { + "one_value": "Show Caption", + "other_value": "Show Captions" + }, + "Chat.Alert.Forward.Action.HideCaption": { + "one_value": "Hide Caption", + "other_value": "Hide Captions" + }, "Chat.CopySelectedText": "Copy Selected Text", "Chat.Confirm.Unpin": "Would you like to unpin this message?", "Chat.Date.ScheduledFor": "Scheduled for %@", diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 4948d59e..fb1baa8c 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -445,7 +445,7 @@ export class AppMessagesManager { scheduleDate: number, silent: true }> = {}) { - if(typeof(text) !== 'string' || !text.length) { + if(!text.trim()) { return; } @@ -1538,7 +1538,7 @@ export class AppMessagesManager { } private generateForwardHeader(peerId: PeerId, originalMessage: Message.message) { - const myId = appUsersManager.getSelf().id; + const myId = appUsersManager.getSelf().id.toPeerId(); if(originalMessage.fromId === myId && originalMessage.peerId === myId && !originalMessage.fwd_from) { return; } @@ -1891,24 +1891,44 @@ export class AppMessagesManager { public forwardMessages(peerId: PeerId, fromPeerId: PeerId, mids: number[], options: Partial<{ withMyScore: true, silent: true, - scheduleDate: number + scheduleDate: number, + dropAuthor: boolean, + dropCaptions: boolean }> = {}) { peerId = appPeersManager.getPeerMigratedTo(peerId) || peerId; mids = mids.slice().sort((a, b) => a - b); + if(options.dropCaptions) { + options.dropAuthor = true; + } + const groups: { [groupId: string]: { tempId: string, - messages: any[] + messages: Message.message[] } } = {}; const newMessages = mids.map(mid => { const originalMessage: Message.message = this.getMessageByPeer(fromPeerId, mid); const message: Message.message = this.generateOutgoingMessage(peerId, options); - message.fwd_from = this.generateForwardHeader(peerId, originalMessage); - (['entities', 'forwards', 'message', 'media', 'reply_markup', 'views'] as any as Array).forEach(key => { + const keys: Array = [ + 'entities', + 'media', + // 'reply_markup' + ]; + + if(!options.dropAuthor) { + message.fwd_from = this.generateForwardHeader(peerId, originalMessage); + keys.push('views', 'forwards'); + } + + if(!options.dropCaptions) { + keys.push('message'); + } + + keys.forEach(key => { // @ts-ignore message[key] = originalMessage[key]; }); @@ -1956,7 +1976,9 @@ export class AppMessagesManager { to_peer: appPeersManager.getInputPeerById(peerId), with_my_score: options.withMyScore, silent: options.silent, - schedule_date: options.scheduleDate + schedule_date: options.scheduleDate, + drop_author: options.dropAuthor, + drop_media_captions: options.dropCaptions }, sentRequestOptions).then((updates) => { this.log('forwardMessages updates:', updates); apiUpdatesManager.processUpdateMessage(updates); @@ -2738,7 +2760,7 @@ export class AppMessagesManager { usingFullAlbum = false; } - if(!usingFullAlbum && !withoutMediaType) { + if((!usingFullAlbum && !withoutMediaType) || !text) { const media = message.media; switch(media._) { case 'messageMediaPhoto': diff --git a/src/lib/mediaPlayer.ts b/src/lib/mediaPlayer.ts index 073c6b90..911d3434 100644 --- a/src/lib/mediaPlayer.ts +++ b/src/lib/mediaPlayer.ts @@ -553,7 +553,9 @@ export default class VideoPlayer extends EventListenerBase<{ const buttons: Parameters[0] = [0.25, 0.5, 1, 1.25, 1.5, 2].map((rate) => { return { regularText: rate === 1 ? 'Normal' : '' + rate, - onClick: () => this.video.playbackRate = rate + onClick: () => { + this.video.playbackRate = rate; + } }; }); const btnMenu = ButtonMenu(buttons); diff --git a/src/scss/partials/_button.scss b/src/scss/partials/_button.scss index 752da48b..0d2c06a8 100644 --- a/src/scss/partials/_button.scss +++ b/src/scss/partials/_button.scss @@ -155,6 +155,10 @@ transform-origin: bottom left; } + &.top-center { + transform-origin: bottom center; + } + &.center-left { transform-origin: center right; } @@ -252,6 +256,11 @@ margin-top: -.125rem; } */ } + + hr { + padding: 0; + margin: .5rem 0; + } } .btn-primary { diff --git a/src/scss/partials/_chat.scss b/src/scss/partials/_chat.scss index 8d4ae0cc..12968b83 100644 --- a/src/scss/partials/_chat.scss +++ b/src/scss/partials/_chat.scss @@ -454,6 +454,8 @@ $chat-helper-size: 36px; .reply-wrapper { height: 0 !important; + opacity: 0; + pointer-events: none; } .btn-send { @@ -988,15 +990,61 @@ $chat-helper-size: 36px; order: 0; pointer-events: none; - display: none; + // display: none; } &-cancel { - // order: 2; - order: 0; + order: 2; + // order: 0; } + + &-subtitle { + color: var(--secondary-text-color) !important; + } + + .peer-title { + font-weight: 400; + } } + .btn-menu { + top: auto; + bottom: calc(100% + 1.0625rem); + left: 3.125rem; + transform: scale(1) !important; + + &-item { + padding-right: 1.5rem; + + &-text { + order: 1; + } + + .checkbox { + &-field { + --size: 1.5rem; + order: 0; + margin: 0 2rem 0 0; + } + + &-box { + &-border, + &-background { + display: none; + } + + &-check use { + stroke: var(--primary-color); + } + } + } + } + + @include respond-to(handhelds) { + left: calc(var(--padding-horizontal) * -1); + } + } + /* span.emoji { margin: 0 .125rem; // font-size: .8rem; diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index 54430b78..5ea7284f 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -1620,6 +1620,7 @@ $bubble-beside-button-width: 38px; align-items: center; user-select: none; height: 1.125rem; + line-height: 1; &.can-autoplay:after { content: $tgico-nosound; diff --git a/src/scss/partials/_checkbox.scss b/src/scss/partials/_checkbox.scss index 4ba5dd82..f3e95b4f 100644 --- a/src/scss/partials/_checkbox.scss +++ b/src/scss/partials/_checkbox.scss @@ -87,6 +87,7 @@ stroke-dasharray: 24.19, 24.19; stroke-dashoffset: 0; transition: stroke-dasharray .1s .15s ease-in-out, visibility 0s .15s; + visibility: visible; // fix blinking on parent's transform @include animation-level(0) { transition: none !important; @@ -262,7 +263,7 @@ position: absolute; } -.checkbox-field [type="checkbox"] { +.checkbox-field .checkbox-field-input { &:not(:checked) + .checkbox-box { .checkbox-box-check { use { diff --git a/src/scss/partials/_poll.scss b/src/scss/partials/_poll.scss index 88c94862..53f1d45f 100644 --- a/src/scss/partials/_poll.scss +++ b/src/scss/partials/_poll.scss @@ -292,7 +292,10 @@ poll-element { font-size: 20px; line-height: 16px; animation: none; - transition: opacity .2s ease; + + @include animation-level(2) { + transition: opacity .2s ease; + } } }