From df814f2a68f710c4f1586cb5cac862c7225d4df9 Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Thu, 18 Aug 2022 13:21:19 +0200 Subject: [PATCH] Favorite stickers Stickers context menu --- src/components/chat/autocompleteHelper.ts | 4 +- src/components/chat/bubbles.ts | 9 +- src/components/chat/contextMenu.ts | 30 +++ src/components/chat/input.ts | 18 +- src/components/chat/stickersHelper.ts | 4 +- src/components/emoticonsDropdown/index.ts | 53 ++++-- .../emoticonsDropdown/tabs/stickers.ts | 173 ++++++++++++++---- src/components/popups/payment.ts | 2 +- src/components/popups/stickers.ts | 16 +- src/components/ripple.ts | 2 +- src/components/wrappers/sticker.ts | 14 +- src/helpers/dom/attachListNavigation.ts | 16 +- src/helpers/dom/createContextMenu.ts | 113 ++++++++++++ src/helpers/dom/createStickersContextMenu.ts | 76 ++++++++ src/helpers/dom/findUpAsChild.ts | 2 +- src/helpers/dropdownHover.ts | 13 +- src/helpers/overlayClickHandler.ts | 8 +- src/lang.ts | 11 ++ src/lib/appManagers/appDocsManager.ts | 4 +- src/lib/appManagers/appImManager.ts | 54 ++++-- src/lib/appManagers/appMessagesManager.ts | 22 +-- src/lib/appManagers/appStickersManager.ts | 140 +++++++++++--- .../utils/docs/getDocumentDownloadOptions.ts | 4 +- .../utils/docs/getDocumentInput.ts | 28 ++- .../docs/getDocumentInputFileLocation.ts | 17 ++ .../utils/docs/getDocumentInputFileName.ts | 4 +- src/lib/mtproto/apiManager.ts | 2 +- src/lib/mtproto/tl_utils.ts | 13 +- src/lib/rootScope.ts | 3 +- src/scss/partials/_button.scss | 6 +- src/scss/partials/_emojiDropdown.scss | 2 +- src/scss/partials/popups/_peer.scss | 1 + 32 files changed, 690 insertions(+), 174 deletions(-) create mode 100644 src/helpers/dom/createContextMenu.ts create mode 100644 src/helpers/dom/createStickersContextMenu.ts create mode 100644 src/lib/appManagers/utils/docs/getDocumentInputFileLocation.ts diff --git a/src/components/chat/autocompleteHelper.ts b/src/components/chat/autocompleteHelper.ts index b7dc97bd..c6d90125 100644 --- a/src/components/chat/autocompleteHelper.ts +++ b/src/components/chat/autocompleteHelper.ts @@ -4,7 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import attachListNavigation from '../../helpers/dom/attachListNavigation'; +import attachListNavigation, {ListNavigationOptions} from '../../helpers/dom/attachListNavigation'; import EventListenerBase from '../../helpers/eventListenerBase'; import {IS_MOBILE} from '../../environment/userAgent'; import rootScope from '../../lib/rootScope'; @@ -28,7 +28,7 @@ export default class AutocompleteHelper extends EventListenerBase<{ protected controller: AutocompleteHelperController; protected listType: 'xy' | 'x' | 'y'; - protected onSelect: (target: Element) => boolean | void; + protected onSelect: ListNavigationOptions['onSelect']; protected waitForKey?: string[]; protected navigationItem: NavigationItem; diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 2351d585..b5fcc4f9 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -945,7 +945,7 @@ export default class ChatBubbles { return; } - awaited.forEach(({bubble, message}) => { + awaited.filter(Boolean).forEach(({bubble, message}) => { if(this.bubbles[message.mid] !== bubble) { return; } @@ -4011,8 +4011,9 @@ export default class ChatBubbles { } const sizes = mediaSizes.active; - const size = bubble.classList.contains('emoji-big') ? sizes.emojiSticker : (doc.animated ? sizes.animatedSticker : sizes.staticSticker); - setAttachmentSize(doc, attachmentDiv, size.width, size.height); + const isEmoji = bubble.classList.contains('emoji-big'); + const boxSize = isEmoji ? sizes.emojiSticker : (doc.animated ? sizes.animatedSticker : sizes.staticSticker); + setAttachmentSize(doc, attachmentDiv, boxSize.width, boxSize.height); // let preloader = new ProgressivePreloader(attachmentDiv, false); bubbleContainer.style.minWidth = attachmentDiv.style.width; bubbleContainer.style.minHeight = attachmentDiv.style.height; @@ -4026,7 +4027,7 @@ export default class ChatBubbles { // play: !!message.pending || !multipleRender, play: true, loop: true, - emoji: bubble.classList.contains('emoji-big') ? messageMessage : undefined, + emoji: isEmoji ? messageMessage : undefined, withThumb: true, loadPromises, isOut, diff --git a/src/components/chat/contextMenu.ts b/src/components/chat/contextMenu.ts index c6785a11..38fe724b 100644 --- a/src/components/chat/contextMenu.ts +++ b/src/components/chat/contextMenu.ts @@ -281,6 +281,21 @@ export default class ChatContextMenu { } private setButtons() { + const verifyFavoriteSticker = async(toAdd: boolean) => { + const doc = ((this.message as Message.message).media as MessageMedia.messageMediaDocument)?.document; + if(!(doc as MyDocument)?.sticker) { + return false; + } + + const favedStickers = await this.managers.acknowledged.appStickersManager.getFavedStickersStickers(); + if(!favedStickers.cached) { + return false; + } + + const found = (await favedStickers.result).some((_doc) => _doc.id === doc.id); + return toAdd ? !found : found; + }; + this.buttons = [{ icon: 'send2', text: 'MessageScheduleSend', @@ -317,6 +332,16 @@ export default class ChatContextMenu { !!this.chat.input.messageInput && this.chat.type !== 'scheduled'/* , cancelEvent: true */ + }, { + icon: 'favourites', + text: 'AddToFavorites', + onClick: this.onFaveStickerClick.bind(this, false), + verify: () => verifyFavoriteSticker(true) + }, { + icon: 'favourites', + text: 'DeleteFromFavorites', + onClick: this.onFaveStickerClick.bind(this, true), + verify: () => verifyFavoriteSticker(false) }, { icon: 'edit', text: 'Edit', @@ -682,6 +707,11 @@ export default class ChatContextMenu { this.chat.input.initMessageReply(this.mid); }; + private onFaveStickerClick = (unfave?: boolean) => { + const docId = ((this.message as Message.message).media as MessageMedia.messageMediaDocument).document.id; + this.managers.appStickersManager.faveSticker(docId, unfave); + }; + private onEditClick = () => { this.chat.input.initMessageEditing(this.mid); }; diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index 10f185b8..537c15fa 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -95,6 +95,7 @@ import ChatSendAs from './sendAs'; import filterAsync from '../../helpers/array/filterAsync'; import InputFieldAnimated from '../inputFieldAnimated'; import getStickerEffectThumb from '../../lib/appManagers/utils/stickers/getStickerEffectThumb'; +import PopupStickers from '../popups/stickers'; const RECORD_MIN_TIME = 500; const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.'; @@ -1059,6 +1060,9 @@ export default class ChatInput { return; } + const popups = PopupElement.getPopups(PopupStickers); + popups.forEach((popup) => popup.hide()); + this.appImManager.openScheduled(peerId); }, 0); } @@ -2435,7 +2439,12 @@ export default class ChatInput { // this.onMessageSent(); } - public async sendMessageWithDocument(document: MyDocument | string, force = false, clearDraft = false) { + public async sendMessageWithDocument( + document: MyDocument | DocId, + force = false, + clearDraft = false, + silent = false + ) { document = await this.managers.appDocsManager.getDoc(document); const flag = document.type === 'sticker' ? 'send_stickers' : (document.type === 'gif' ? 'send_gifs' : 'send_media'); @@ -2445,7 +2454,7 @@ export default class ChatInput { } if(this.chat.type === 'scheduled' && !force) { - this.scheduleSending(() => this.sendMessageWithDocument(document, true, clearDraft)); + this.scheduleSending(() => this.sendMessageWithDocument(document, true, clearDraft, silent)); return false; } @@ -2460,12 +2469,13 @@ export default class ChatInput { this.managers.appMessagesManager.sendFile(this.chat.peerId, document, { ...this.chat.getMessageSendingParams(), isMedia: true, - clearDraft: clearDraft || undefined + clearDraft: clearDraft || undefined, + silent }); this.onMessageSent(clearDraft, true); if(document.type === 'sticker') { - emoticonsDropdown.stickersTab?.pushRecentSticker(document); + emoticonsDropdown.stickersTab?.unshiftRecentSticker(document); } return true; diff --git a/src/components/chat/stickersHelper.ts b/src/components/chat/stickersHelper.ts index 529d9cfb..c5539600 100644 --- a/src/components/chat/stickersHelper.ts +++ b/src/components/chat/stickersHelper.ts @@ -32,8 +32,8 @@ export default class StickersHelper extends AutocompleteHelper { appendTo, controller, listType: 'xy', - onSelect: (target) => { - return !EmoticonsDropdown.onMediaClick({target}, true); + onSelect: async(target) => { + return !(await EmoticonsDropdown.onMediaClick({target}, true)); }, waitForKey: ['ArrowUp', 'ArrowDown'] }); diff --git a/src/components/emoticonsDropdown/index.ts b/src/components/emoticonsDropdown/index.ts index 33975f8c..d1fe8e97 100644 --- a/src/components/emoticonsDropdown/index.ts +++ b/src/components/emoticonsDropdown/index.ts @@ -30,6 +30,7 @@ import {IS_APPLE_MOBILE} from '../../environment/userAgent'; import {AppManagers} from '../../lib/appManagers/managers'; import type LazyLoadQueueIntersector from '../lazyLoadQueueIntersector'; import {simulateClickEvent} from '../../helpers/dom/clickEvent'; +import overlayCounter from '../../helpers/overlayCounter'; export const EMOTICONSSTICKERGROUP: AnimationItemGroup = 'emoticons-dropdown'; @@ -88,7 +89,7 @@ export class EmoticonsDropdown extends DropdownHover { EmoticonsDropdown.lazyLoadQueue.unlock(); EmoticonsDropdown.lazyLoadQueue.refresh(); - this.container.classList.remove('disable-hover'); + // this.container.classList.remove('disable-hover'); }); this.addEventListener('close', () => { @@ -106,7 +107,7 @@ export class EmoticonsDropdown extends DropdownHover { EmoticonsDropdown.lazyLoadQueue.unlock(); EmoticonsDropdown.lazyLoadQueue.refresh(); - this.container.classList.remove('disable-hover'); + // this.container.classList.remove('disable-hover'); this.savedRange = undefined; }); @@ -182,6 +183,26 @@ export class EmoticonsDropdown extends DropdownHover { this.tabs[INIT_TAB_ID].init(); // onTransitionEnd не вызовется, т.к. это первая открытая вкладка } + if(!IS_TOUCH_SUPPORTED) { + let lastMouseMoveEvent: MouseEvent, mouseMoveEventAttached = false; + const onMouseMove = (e: MouseEvent) => { + lastMouseMoveEvent = e; + }; + overlayCounter.addEventListener('change', (isActive) => { + if(isActive) { + if(!mouseMoveEventAttached) { + document.body.addEventListener('mousemove', onMouseMove); + mouseMoveEventAttached = true; + } + } else if(mouseMoveEventAttached) { + document.body.removeEventListener('mousemove', onMouseMove); + if(lastMouseMoveEvent) { + this.onMouseOut(lastMouseMoveEvent); + } + } + }); + } + appImManager.addEventListener('peer_changed', this.checkRights); this.checkRights(); @@ -204,15 +225,17 @@ export class EmoticonsDropdown extends DropdownHover { this.deleteBtn.classList.toggle('hide', this.tabId !== 0); }; - private checkRights = () => { + private checkRights = async() => { const {peerId, threadId} = appImManager.chat; const children = this.tabsEl.children; const tabsElements = Array.from(children) as HTMLElement[]; - const canSendStickers = this.managers.appMessagesManager.canSendToPeer(peerId, threadId, 'send_stickers'); - tabsElements[2].toggleAttribute('disabled', !canSendStickers); + const [canSendStickers, canSendGifs] = await Promise.all([ + this.managers.appMessagesManager.canSendToPeer(peerId, threadId, 'send_stickers'), + this.managers.appMessagesManager.canSendToPeer(peerId, threadId, 'send_gifs') + ]); - const canSendGifs = this.managers.appMessagesManager.canSendToPeer(peerId, threadId, 'send_gifs'); + tabsElements[2].toggleAttribute('disabled', !canSendStickers); tabsElements[3].toggleAttribute('disabled', !canSendGifs); const active = this.tabsEl.querySelector('.active'); @@ -290,30 +313,34 @@ export class EmoticonsDropdown extends DropdownHover { return {stickyIntersector, setActive}; }; - public static onMediaClick = (e: {target: EventTarget | Element}, clearDraft = false) => { + public static onMediaClick = async(e: {target: EventTarget | Element}, clearDraft = false, silent?: boolean) => { let target = e.target as HTMLElement; target = findUpTag(target, 'DIV'); if(!target) return false; - const fileId = target.dataset.docId; - if(!fileId) return false; + const docId = target.dataset.docId; + if(!docId) return false; - if(appImManager.chat.input.sendMessageWithDocument(fileId, undefined, clearDraft)) { + return this.sendDocId(docId, clearDraft, silent); + }; + + public static async sendDocId(docId: DocId, clearDraft?: boolean, silent?: boolean) { + if(await appImManager.chat.input.sendMessageWithDocument(docId, undefined, clearDraft, silent)) { /* dropdown.classList.remove('active'); toggleEl.classList.remove('active'); */ if(emoticonsDropdown.container) { emoticonsDropdown.forceClose = true; - emoticonsDropdown.container.classList.add('disable-hover'); + // emoticonsDropdown.container.classList.add('disable-hover'); emoticonsDropdown.toggle(false); } return true; } else { - console.warn('got no doc by id:', fileId); + console.warn('got no doc by id:', docId); return false; } - }; + } public addLazyLoadQueueRepeat(lazyLoadQueue: LazyLoadQueueIntersector, processInvisibleDiv: (div: HTMLElement) => void) { this.addEventListener('close', () => { diff --git a/src/components/emoticonsDropdown/tabs/stickers.ts b/src/components/emoticonsDropdown/tabs/stickers.ts index a9732f45..08363d58 100644 --- a/src/components/emoticonsDropdown/tabs/stickers.ts +++ b/src/components/emoticonsDropdown/tabs/stickers.ts @@ -28,6 +28,10 @@ import noop from '../../../helpers/noop'; import ButtonIcon from '../../buttonIcon'; import confirmationPopup from '../../confirmationPopup'; import VisibilityIntersector, {OnVisibilityChange} from '../../visibilityIntersector'; +import createStickersContextMenu from '../../../helpers/dom/createStickersContextMenu'; +import findUpAsChild from '../../../helpers/dom/findUpAsChild'; +import forEachReverse from '../../../helpers/array/forEachReverse'; +import {MTAppConfig} from '../../../lib/mtproto/appConfig'; export class SuperStickerRenderer { public lazyLoadQueue: LazyLoadQueueRepeat; @@ -158,18 +162,18 @@ type StickersTabCategory = { document: MyDocument, element: HTMLElement }[], - pos?: number + mounted?: boolean, + id: string, + limit?: number }; -const RECENT_STICKERS_COUNT = 20; - export default class StickersTab implements EmoticonsTab { private content: HTMLElement; private categories: {[id: string]: StickersTabCategory}; private categoriesMap: Map; private categoriesIntersector: VisibilityIntersector; - private localCategoryIndex: number; + private localCategories: StickersTabCategory[]; private scroll: Scrollable; private menu: HTMLElement; @@ -180,7 +184,13 @@ export default class StickersTab implements EmoticonsTab { constructor(private managers: AppManagers) { this.categories = {}; this.categoriesMap = new Map(); - this.localCategoryIndex = 0; + this.localCategories = []; + } + + private setFavedLimit(appConfig: MTAppConfig) { + const limit = rootScope.premium ? appConfig.stickers_faved_limit_premium : appConfig.stickers_faved_limit_default; + const category = this.categories['faved']; + category.limit = limit; } private createCategory(stickerSet: StickerSet.stickerSet, _title: HTMLElement | DocumentFragment) { @@ -211,7 +221,8 @@ export default class StickersTab implements EmoticonsTab { menuTabPadding }, set: stickerSet, - items: [] + items: [], + id: '' + stickerSet.id }; container.append(title, items); @@ -267,7 +278,7 @@ export default class StickersTab implements EmoticonsTab { const category = this.createCategory(set, wrapEmojiText(set.title)); const {menuTab, menuTabPadding, container} = category.elements; - const pos = prepend ? this.localCategoryIndex : 0xFFFF; + const pos = prepend ? this.localCategories.filter((category) => category.mounted).length : 0xFFFF; positionElementByIndex(menuTab, this.menu, pos); const promise = this.managers.appStickersManager.getStickerSet(set); @@ -373,16 +384,44 @@ export default class StickersTab implements EmoticonsTab { const createLocalCategory = (id: string, title: LangPackKey, icon?: string) => { const category = this.createCategory({id} as any, i18n(title)); + this.localCategories.push(category); category.elements.title.classList.add('disable-hover'); icon && category.elements.menuTab.classList.add('tgico-' + icon); category.elements.menuTabPadding.remove(); - category.pos = this.localCategoryIndex++; this.toggleLocalCategory(category, false); return category; }; + const onCategoryStickers = (category: StickersTabCategory, stickers: MyDocument[]) => { + // if(category.id === 'faved' && category.limit && category.limit < stickers.length) { + // category.limit = stickers.length; + // } + + if(category.limit) { + stickers = stickers.slice(0, category.limit); + } + + const ids = new Set(stickers.map((doc) => doc.id)); + forEachReverse(category.items, (item) => { + if(!ids.has(item.document.id)) { + this.deleteSticker(category, item.document, true); + } + }); + + this.toggleLocalCategory(category, !!stickers.length); + forEachReverse(stickers, (doc, idx) => { + this.unshiftSticker(category, doc, true, idx); + }); + this.spliceExceed(category); + category.elements.container.classList.remove('hide'); + }; + + const favedCategory = createLocalCategory('faved', 'FavoriteStickers', 'saved'); + favedCategory.elements.menuTab.classList.add('active'); + const recentCategory = createLocalCategory('recent', 'Stickers.Recent', 'recent'); - recentCategory.elements.menuTab.classList.add('active'); + recentCategory.limit = 20; + // recentCategory.elements.menuTab.classList.add('active'); const clearButton = ButtonIcon('close', {noRipple: true}); recentCategory.elements.title.append(clearButton); @@ -398,22 +437,22 @@ export default class StickersTab implements EmoticonsTab { }, noop); }); - const onRecentStickers = (stickers: MyDocument[]) => { - const sliced = stickers.slice(0, RECENT_STICKERS_COUNT) as MyDocument[]; - - clearCategoryItems(recentCategory); - this.toggleLocalCategory(recentCategory, !!sliced.length); - this.categoryAppendStickers(recentCategory, Promise.resolve(sliced)); - }; - const premiumCategory = createLocalCategory('premium', 'PremiumStickersShort'); const s = document.createElement('span'); s.classList.add('tgico-star', 'color-premium'); premiumCategory.elements.menuTab.append(s); const promises = [ - this.managers.appStickersManager.getRecentStickers().then((stickers) => { - onRecentStickers(stickers.stickers as MyDocument[]); + Promise.all([ + this.managers.apiManager.getAppConfig(), + this.managers.appStickersManager.getFavedStickersStickers() + ]).then(([appConfig, stickers]) => { + this.setFavedLimit(appConfig); + onCategoryStickers(favedCategory, stickers); + }), + + this.managers.appStickersManager.getRecentStickersStickers().then((stickers) => { + onCategoryStickers(recentCategory, stickers); }), this.managers.appStickersManager.getAllStickers().then((res) => { @@ -492,12 +531,32 @@ export default class StickersTab implements EmoticonsTab { } }); - rootScope.addEventListener('stickers_recent', (stickers) => { + rootScope.addEventListener('sticker_updated', ({type, document, faved}) => { + // if(type === 'faved') { + // return; + // } + + const category = this.categories[type === 'faved' ? 'faved' : 'recent']; + if(category) { + if(faved) { + this.unshiftSticker(category, document); + } else { + this.deleteSticker(category, document); + } + } + }); + + rootScope.addEventListener('stickers_updated', ({type, stickers}) => { if(this.mounted) { - onRecentStickers(stickers); + const category = this.categories[type === 'faved' ? 'faved' : 'recent']; + onCategoryStickers(category, stickers); } }); + rootScope.addEventListener('app_config', (appConfig) => { + this.setFavedLimit(appConfig); + }); + const resizeCategories = () => { for(const [container, category] of this.categoriesMap) { this.setCategoryItemsHeight(category); @@ -508,6 +567,17 @@ export default class StickersTab implements EmoticonsTab { emoticonsDropdown.addEventListener('opened', resizeCategories); + createStickersContextMenu({ + listenTo: this.content, + verifyRecent: (target) => !!findUpAsChild(target, this.categories['recent'].elements.items), + onOpen: () => { + emoticonsDropdown.setIgnoreMouseOut(true); + }, + onClose: () => { + emoticonsDropdown.setIgnoreMouseOut(false); + } + }); + this.init = null; } @@ -516,23 +586,50 @@ export default class StickersTab implements EmoticonsTab { category.elements.menuTab.remove(); category.elements.container.remove(); } else { - const pos = category.pos; - positionElementByIndex(category.elements.menuTab, this.menu, pos); - positionElementByIndex(category.elements.container, this.scroll.container, pos); + let idx = this.localCategories.indexOf(category); + const notMounted = this.localCategories.slice(0, idx).filter((category) => !category.mounted); + idx -= notMounted.length; + positionElementByIndex(category.elements.menuTab, this.menu, idx); + positionElementByIndex(category.elements.container, this.scroll.container, idx); } + category.mounted = visible; // category.elements.container.classList.toggle('hide', !visible); } - public pushRecentSticker(doc: MyDocument) { - this.managers.appStickersManager.pushRecentSticker(doc.id); + private onLocalCategoryUpdate(category: StickersTabCategory) { + this.setCategoryItemsHeight(category); + this.toggleLocalCategory(category, !!category.items.length); + } + + public deleteSticker(category: StickersTabCategory, doc: MyDocument, batch?: boolean) { + const item = findAndSplice(category.items, (item) => item.document.id === doc.id); + if(item) { + item.element.remove(); - const category = this.categories['recent']; - if(!category) { - return; + if(!batch) { + this.onLocalCategoryUpdate(category); + } + } + } + + private spliceExceed(category: StickersTabCategory) { + const {items, limit} = category; + items.splice(limit, items.length - limit).forEach(({element}) => { + element.remove(); + }); + + this.onLocalCategoryUpdate(category); + } + + public unshiftSticker(category: StickersTabCategory, doc: MyDocument, batch?: boolean, idx?: number) { + if(idx !== undefined) { + const i = category.items[idx]; + if(i && i.document.id === doc.id) { + return; + } } - const items = category.elements.items; let item = findAndSplice(category.items, (item) => item.document.id === doc.id); if(!item) { item = { @@ -542,13 +639,19 @@ export default class StickersTab implements EmoticonsTab { } category.items.unshift(item); - if(items.childElementCount) items.prepend(item.element); - if(items.childElementCount > RECENT_STICKERS_COUNT) { - (Array.from(items.children) as HTMLElement[]).slice(RECENT_STICKERS_COUNT).forEach((el) => el.remove()); + category.elements.items.prepend(item.element); + + if(!batch) { + this.spliceExceed(category); } + } - this.setCategoryItemsHeight(category); - this.toggleLocalCategory(category, true); + public unshiftRecentSticker(doc: MyDocument) { + this.managers.appStickersManager.saveRecentSticker(doc.id); + } + + public deleteRecentSticker(doc: MyDocument) { + this.managers.appStickersManager.saveRecentSticker(doc.id, true); } onClose() { diff --git a/src/components/popups/payment.ts b/src/components/popups/payment.ts index fc1484f5..55ba313e 100644 --- a/src/components/popups/payment.ts +++ b/src/components/popups/payment.ts @@ -351,7 +351,7 @@ export default class PopupPayment extends PopupElement { } tipsLabel.label.addEventListener('mousedown', (e) => { - if(!findUpAsChild(e.target, input)) { + if(!findUpAsChild(e.target as HTMLElement, input)) { placeCaretAtEnd(input); } }); diff --git a/src/components/popups/stickers.ts b/src/components/popups/stickers.ts index 865d70fa..d9e1da3b 100644 --- a/src/components/popups/stickers.ts +++ b/src/components/popups/stickers.ts @@ -20,6 +20,7 @@ import {attachClickEvent} from '../../helpers/dom/clickEvent'; import {toastNew} from '../toast'; import setInnerHTML from '../../helpers/dom/setInnerHTML'; import wrapEmojiText from '../../lib/richTextProcessor/wrapEmojiText'; +import createStickersContextMenu from '../../helpers/dom/createStickersContextMenu'; const ANIMATION_GROUP: AnimationItemGroup = 'STICKERS-POPUP'; @@ -34,6 +35,7 @@ export default class PopupStickers extends PopupElement { this.addEventListener('close', () => { animationIntersector.setOnlyOnePlayableGroup(); + destroy(); }); const div = document.createElement('div'); @@ -57,10 +59,10 @@ export default class PopupStickers extends PopupElement { this.scrollable.append(div); this.body.append(this.stickersFooter); - // const editButton = document.createElement('button'); - // editButton.classList.add('btn-primary'); - - // this.stickersFooter.append(editButton); + const {destroy} = createStickersContextMenu({ + listenTo: this.stickersDiv, + isStickerPack: true + }); this.loadStickerSet(); } @@ -69,11 +71,9 @@ export default class PopupStickers extends PopupElement { const target = findUpClassName(e.target, 'sticker-set-sticker'); if(!target) return; - const fileId = target.dataset.docId; - if(appImManager.chat.input.sendMessageWithDocument(fileId)) { + const docId = target.dataset.docId; + if(appImManager.chat.input.sendMessageWithDocument(docId)) { this.hide(); - } else { - console.warn('got no doc by id:', fileId); } }; diff --git a/src/components/ripple.ts b/src/components/ripple.ts index 27340ec2..de8d30d9 100644 --- a/src/components/ripple.ts +++ b/src/components/ripple.ts @@ -141,7 +141,7 @@ export default function ripple( findUpClassName(e.target as HTMLElement, 'c-ripple') !== r ) && ( attachListenerTo === elem || - !findUpAsChild(e.target, attachListenerTo) + !findUpAsChild(e.target as HTMLElement, attachListenerTo) ); // TODO: rename this variable diff --git a/src/components/wrappers/sticker.ts b/src/components/wrappers/sticker.ts index fdc33588..10614eff 100644 --- a/src/components/wrappers/sticker.ts +++ b/src/components/wrappers/sticker.ts @@ -17,6 +17,8 @@ import renderImageFromUrl from '../../helpers/dom/renderImageFromUrl'; import getImageFromStrippedThumb from '../../helpers/getImageFromStrippedThumb'; import getPreviewURLFromThumb from '../../helpers/getPreviewURLFromThumb'; import makeError from '../../helpers/makeError'; +import {makeMediaSize} from '../../helpers/mediaSize'; +import mediaSizes from '../../helpers/mediaSizes'; import onMediaLoad from '../../helpers/onMediaLoad'; import {isSavingLottiePreview, saveLottiePreview} from '../../helpers/saveLottiePreview'; import throttle from '../../helpers/schedulers/throttle'; @@ -79,12 +81,12 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, asStatic = true; } - if(!width) { - width = !emoji ? 200 : undefined; - } - - if(!height) { - height = !emoji ? 200 : undefined; + if(!width && !height) { + const sizes = mediaSizes.active; + const boxSize = emoji ? sizes.emojiSticker : (doc.animated ? sizes.animatedSticker : sizes.staticSticker); + const size = makeMediaSize(doc.w, doc.h).aspectFitted(boxSize); + width = size.width; + height = size.height; } if(stickerType === 2) { diff --git a/src/helpers/dom/attachListNavigation.ts b/src/helpers/dom/attachListNavigation.ts index 5c42b965..07115aa7 100644 --- a/src/helpers/dom/attachListNavigation.ts +++ b/src/helpers/dom/attachListNavigation.ts @@ -17,13 +17,15 @@ const ACTIVE_CLASS_NAME = 'active'; const AXIS_Y_KEYS: ArrowKey[] = ['ArrowUp', 'ArrowDown']; const AXIS_X_KEYS: ArrowKey[] = ['ArrowLeft', 'ArrowRight']; -export default function attachListNavigation({list, type, onSelect, once, waitForKey}: { +export type ListNavigationOptions = { list: HTMLElement, type: 'xy' | 'x' | 'y', - onSelect: (target: Element) => void | boolean, + onSelect: (target: Element) => void | boolean | Promise, once: boolean, waitForKey?: string[] -}) { +}; + +export default function attachListNavigation({list, type, onSelect, once, waitForKey}: ListNavigationOptions) { let waitForKeySet = waitForKey?.length ? new Set(waitForKey) : undefined; const keyNames = new Set(type === 'xy' ? AXIS_Y_KEYS.concat(AXIS_X_KEYS) : (type === 'x' ? AXIS_X_KEYS : AXIS_Y_KEYS)); @@ -118,7 +120,7 @@ export default function attachListNavigation({list, type, onSelect, once, waitFo list.classList.add('navigable-list'); const onMouseMove = (e: MouseEvent) => { - const target = findUpAsChild(e.target, list) as HTMLElement; + const target = findUpAsChild(e.target as HTMLElement, list) as HTMLElement; if(!target) { return; } @@ -129,7 +131,7 @@ export default function attachListNavigation({list, type, onSelect, once, waitFo const onClick = (e: Event) => { cancelEvent(e); // cancel keyboard closening - const target = findUpAsChild(e.target, list) as HTMLElement; + const target = findUpAsChild(e.target as HTMLElement, list) as HTMLElement; if(!target) { return; } @@ -138,8 +140,8 @@ export default function attachListNavigation({list, type, onSelect, once, waitFo fireSelect(getCurrentTarget()); }; - const fireSelect = (target: Element) => { - const canContinue = onSelect(target); + const fireSelect = async(target: Element) => { + const canContinue = await onSelect(target); if(canContinue !== undefined ? !canContinue : once) { detach(); } diff --git a/src/helpers/dom/createContextMenu.ts b/src/helpers/dom/createContextMenu.ts new file mode 100644 index 00000000..59d0726d --- /dev/null +++ b/src/helpers/dom/createContextMenu.ts @@ -0,0 +1,113 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import ButtonMenu, {ButtonMenuItemOptions} from '../../components/buttonMenu'; +import filterAsync from '../array/filterAsync'; +import contextMenuController from '../contextMenuController'; +import ListenerSetter from '../listenerSetter'; +import {getMiddleware} from '../middleware'; +import positionMenu from '../positionMenu'; +import {attachContextMenuListener} from './attachContextMenuListener'; + +export default function createContextMenu boolean | Promise}>({ + buttons, + findElement, + listenTo, + appendTo, + filterButtons, + onOpen, + onClose +}: { + buttons: T[], + findElement: (e: MouseEvent) => HTMLElement, + listenTo: HTMLElement, + appendTo?: HTMLElement, + filterButtons?: (buttons: T[]) => Promise, + onOpen?: (target: HTMLElement) => any, + onClose?: () => any +}) { + appendTo ??= document.body; + + const attachListenerSetter = new ListenerSetter(); + const listenerSetter = new ListenerSetter(); + const middleware = getMiddleware(); + let element: HTMLElement; + + attachContextMenuListener(listenTo, (e) => { + const target = findElement(e as any); + if(!target) { + return; + } + + let _element = element; + if(e instanceof MouseEvent || e.hasOwnProperty('preventDefault')) (e as any).preventDefault(); + if(_element && _element.classList.contains('active')) { + return false; + } + if(e instanceof MouseEvent || e.hasOwnProperty('cancelBubble')) (e as any).cancelBubble = true; + + const r = async() => { + await onOpen?.(target); + + const initResult = await init(); + if(!initResult) { + return; + } + + _element = initResult.element; + const {cleanup, destroy} = initResult; + + positionMenu(e, _element); + contextMenuController.openBtnMenu(_element, () => { + onClose?.(); + cleanup(); + + setTimeout(() => { + destroy(); + }, 300); + }); + }; + + r(); + }, attachListenerSetter); + + const cleanup = () => { + listenerSetter.removeAll(); + middleware.clean(); + }; + + const destroy = () => { + cleanup(); + attachListenerSetter.removeAll(); + }; + + const init = async() => { + cleanup(); + + buttons.forEach((button) => button.element = undefined); + const f = filterButtons || ((buttons: T[]) => filterAsync(buttons, (button) => button?.verify?.() ?? true)); + + const filteredButtons = await f(buttons); + if(!filteredButtons.length) { + return; + } + + const _element = element = ButtonMenu(filteredButtons, listenerSetter); + _element.classList.add('contextmenu'); + + appendTo.append(_element); + + return { + element: _element, + cleanup, + destroy: () => { + _element.remove(); + } + }; + }; + + return {element, destroy}; +} diff --git a/src/helpers/dom/createStickersContextMenu.ts b/src/helpers/dom/createStickersContextMenu.ts new file mode 100644 index 00000000..fdfa4cae --- /dev/null +++ b/src/helpers/dom/createStickersContextMenu.ts @@ -0,0 +1,76 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import type {MyDocument} from '../../lib/appManagers/appDocsManager'; +import PopupStickers from '../../components/popups/stickers'; +import appImManager from '../../lib/appManagers/appImManager'; +import rootScope from '../../lib/rootScope'; +import createContextMenu from './createContextMenu'; +import findUpClassName from './findUpClassName'; +import emoticonsDropdown, {EmoticonsDropdown} from '../../components/emoticonsDropdown'; + +export default function createStickersContextMenu(options: { + listenTo: HTMLElement, + isStickerPack?: boolean, + verifyRecent?: (target: HTMLElement) => boolean, + appendTo?: HTMLElement, + onOpen?: () => any, + onClose?: () => any +}) { + const {listenTo, isStickerPack, verifyRecent, appendTo, onOpen, onClose} = options; + let target: HTMLElement, doc: MyDocument; + const verifyFavoriteSticker = async(toAdd: boolean) => { + const favedStickers = await rootScope.managers.acknowledged.appStickersManager.getFavedStickersStickers(); + if(!favedStickers.cached) { + return false; + } + + const found = (await favedStickers.result).some((_doc) => _doc.id === doc.id); + return toAdd ? !found : found; + }; + + return createContextMenu({ + listenTo: listenTo, + appendTo, + findElement: (e) => target = findUpClassName(e.target, 'media-sticker-wrapper'), + onOpen: async() => { + doc = await rootScope.managers.appDocsManager.getDoc(target.dataset.docId); + return onOpen?.(); + }, + onClose, + buttons: [{ + icon: 'stickers', + text: 'Context.ViewStickerSet', + onClick: () => new PopupStickers(doc.stickerSetInput).show(), + verify: () => !isStickerPack + }, { + icon: 'favourites', + text: 'AddToFavorites', + onClick: () => rootScope.managers.appStickersManager.faveSticker(doc.id, false), + verify: () => verifyFavoriteSticker(true) + }, { + icon: 'favourites', + text: 'DeleteFromFavorites', + onClick: () => rootScope.managers.appStickersManager.faveSticker(doc.id, true), + verify: () => verifyFavoriteSticker(false) + }, { + icon: 'delete', + text: 'DeleteFromRecent', + onClick: () => emoticonsDropdown.stickersTab.deleteRecentSticker(doc), + verify: () => verifyRecent?.(target) ?? false + }, { + icon: 'mute', + text: 'Chat.Send.WithoutSound', + onClick: () => EmoticonsDropdown.sendDocId(doc.id, false, true), + verify: () => !!(appImManager.chat.peerId && appImManager.chat.peerId !== rootScope.myId) + }, { + icon: 'schedule', + text: 'Chat.Send.ScheduledMessage', + onClick: () => appImManager.chat.input.scheduleSending(() => appImManager.chat.input.sendMessageWithDocument(doc)), + verify: () => !!appImManager.chat.peerId + }] + }); +} diff --git a/src/helpers/dom/findUpAsChild.ts b/src/helpers/dom/findUpAsChild.ts index e278e601..b3672dfb 100644 --- a/src/helpers/dom/findUpAsChild.ts +++ b/src/helpers/dom/findUpAsChild.ts @@ -4,7 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -export default function findUpAsChild(el: any, parent: any) { +export default function findUpAsChild(el: HTMLElement, parent: HTMLElement): HTMLElement { if(el.parentElement === parent) return el; while(el.parentElement) { diff --git a/src/helpers/dropdownHover.ts b/src/helpers/dropdownHover.ts index 45c0ba96..da596da8 100644 --- a/src/helpers/dropdownHover.ts +++ b/src/helpers/dropdownHover.ts @@ -25,6 +25,7 @@ export default class DropdownHover extends EventListenerBase<{ protected displayTimeout: number; protected forceClose = false; protected inited = false; + protected ignoreMouseOut = false; constructor(options: { element: DropdownHover['element'] @@ -63,11 +64,15 @@ export default class DropdownHover extends EventListenerBase<{ } } - private onMouseOut = (e: MouseEvent) => { + protected onMouseOut = (e: MouseEvent) => { if(KEEP_OPEN || !this.isActive()) return; clearTimeout(this.displayTimeout); - const toElement = (e as any).toElement as Element; + if(this.ignoreMouseOut) { + return; + } + + const toElement = (e as any).toElement as HTMLElement; if(toElement && findUpAsChild(toElement, this.element)) { return; } @@ -162,4 +167,8 @@ export default class DropdownHover extends EventListenerBase<{ public isActive() { return this.element.classList.contains('active'); } + + public setIgnoreMouseOut(ignore: boolean) { + this.ignoreMouseOut = ignore; + } } diff --git a/src/helpers/overlayClickHandler.ts b/src/helpers/overlayClickHandler.ts index 781576af..fce75d74 100644 --- a/src/helpers/overlayClickHandler.ts +++ b/src/helpers/overlayClickHandler.ts @@ -24,11 +24,11 @@ export default class OverlayClickHandler extends EventListenerBase<{ protected withOverlay?: boolean ) { super(false); - this.listenerOptions = withOverlay ? undefined : {capture: true}; + this.listenerOptions = withOverlay ? {} : {capture: true}; } protected onClick = (e: MouseEvent | TouchEvent) => { - if(this.element && findUpAsChild(e.target, this.element)) { + if(this.element && findUpAsChild(e.target as HTMLElement, this.element)) { return; } @@ -48,7 +48,7 @@ export default class OverlayClickHandler extends EventListenerBase<{ if(!IS_TOUCH_SUPPORTED) { // window.removeEventListener('keydown', onKeyDown, {capture: true}); - window.removeEventListener('contextmenu', this.onClick); + window.removeEventListener('contextmenu', this.onClick, this.listenerOptions); } document.removeEventListener(CLICK_EVENT_NAME, this.onClick, this.listenerOptions); @@ -89,7 +89,7 @@ export default class OverlayClickHandler extends EventListenerBase<{ if(!IS_TOUCH_SUPPORTED) { // window.addEventListener('keydown', onKeyDown, {capture: true}); - window.addEventListener('contextmenu', this.onClick, {once: true}); + window.addEventListener('contextmenu', this.onClick, {...this.listenerOptions, once: true}); } /* // ! because this event must be canceled, and can't cancel on menu click (below) diff --git a/src/lang.ts b/src/lang.ts index 2adabccf..2a834ca3 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -749,6 +749,16 @@ const lang = { 'ClearRecentStickersAlertTitle': 'Clear recent stickers', 'ClearRecentStickersAlertMessage': 'Do you want to clear all your recent stickers?', 'PremiumStickersShort': 'Premium', + 'FavoriteStickers': 'Favorites', + 'AddToFavorites': 'Add to Favorites', + 'AddedToFavorites': 'Sticker added to Favorites.', + 'RemovedFromFavorites': 'Sticker was removed from Favorites', + 'RemovedFromRecent': 'Sticker was removed from Recent', + 'DeleteFromFavorites': 'Delete from Favorites', + 'DeleteFromRecent': 'Remove from Recent', + 'NewChatsFromNonContacts': 'New chats from unknown users', + 'ArchiveAndMute': 'Archive and Mute', + 'ArchiveAndMuteInfo': 'Automatically archive and mute new chats, groups and channels from non-contacts.', // * macos 'AccountSettings.Filters': 'Chat Folders', @@ -949,6 +959,7 @@ const lang = { 'ChatList.Mute.3Days': 'For 3 Days', 'ChatList.Mute.Forever': 'Forever', 'Channel.DescriptionHolderDescrpiton': 'You can provide an optional description for your channel.', + 'Context.ViewStickerSet': 'View Sticker Set', 'CreateGroup.NameHolder': 'Group Name', 'Date.Today': 'Today', 'DeleteChat.DeleteGroupForAll': 'Delete for all members', diff --git a/src/lib/appManagers/appDocsManager.ts b/src/lib/appManagers/appDocsManager.ts index 524bbfbb..0f4f03d8 100644 --- a/src/lib/appManagers/appDocsManager.ts +++ b/src/lib/appManagers/appDocsManager.ts @@ -20,7 +20,7 @@ import assumeType from '../../helpers/assumeType'; import {getEnvironment} from '../../environment/utils'; import {isServiceWorkerOnline} from '../mtproto/mtproto.worker'; import MTProtoMessagePort from '../mtproto/mtprotoMessagePort'; -import getDocumentInput from './utils/docs/getDocumentInput'; +import getDocumentInputFileLocation from './utils/docs/getDocumentInputFileLocation'; import getDocumentURL from './utils/docs/getDocumentURL'; import type {ThumbCache} from '../storages/thumbs'; import makeError from '../../helpers/makeError'; @@ -405,6 +405,6 @@ export class AppDocsManager extends AppManager { public requestDocPart(docId: DocId, dcId: number, offset: number, limit: number) { const doc = this.getDoc(docId); if(!doc) return Promise.reject(makeError('NO_DOC')); - return this.apiFileManager.requestFilePart(dcId, getDocumentInput(doc), offset, limit); + return this.apiFileManager.requestFilePart(dcId, getDocumentInputFileLocation(doc), offset, limit); } } diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 3507259a..1848f960 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -52,7 +52,7 @@ import confirmationPopup from '../../components/confirmationPopup'; import IS_GROUP_CALL_SUPPORTED from '../../environment/groupCallSupport'; import IS_CALL_SUPPORTED from '../../environment/callSupport'; import {CallType} from '../calls/types'; -import {Modify, SendMessageEmojiInteractionData} from '../../types'; +import {Awaited, Modify, SendMessageEmojiInteractionData} from '../../types'; import htmlToSpan from '../../helpers/dom/htmlToSpan'; import getVisibleRect from '../../helpers/dom/getVisibleRect'; import {attachClickEvent, simulateClickEvent} from '../../helpers/dom/clickEvent'; @@ -579,12 +579,20 @@ export class AppImManager extends EventListenerBase<{ const doc = await this.managers.appDocsManager.getDoc(docId); if(!middleware()) return; - const {ready, transformer} = await doThatSticker({ - doc, - mediaContainer, - middleware, - lockGroups: true - }); + let result: Awaited>; + try { + result = await doThatSticker({ + doc, + mediaContainer, + middleware, + lockGroups: true + }); + if(!result) return; + } catch(err) { + return; + } + + const {ready, transformer} = result; previousTransformer = transformer; @@ -614,12 +622,20 @@ export class AppImManager extends EventListenerBase<{ const doc = await this.managers.appDocsManager.getDoc(docId); if(!middleware()) return; - const {ready, transformer} = await doThatSticker({ - doc, - mediaContainer, - middleware, - isSwitching: true - }); + let r: Awaited>; + try { + r = await doThatSticker({ + doc, + mediaContainer, + middleware, + isSwitching: true + }); + if(!r) return; + } catch(err) { + return; + } + + const {ready, transformer} = r; const _previousTransformer = previousTransformer; SetTransition(_previousTransformer, 'is-switching', true, switchDuration, () => { @@ -660,6 +676,18 @@ export class AppImManager extends EventListenerBase<{ document.addEventListener('mouseup', onMouseUp, {once: true}); }); + rootScope.addEventListener('sticker_updated', ({type, faved}) => { + if(type === 'faved') { + toastNew({ + langPackKey: faved ? 'AddedToFavorites' : 'RemovedFromFavorites' + }); + } else if(!faved) { + toastNew({ + langPackKey: 'RemovedFromRecent' + }); + } + }); + apiManagerProxy.addEventListener('notificationBuild', (options) => { if(this.chat.peerId === options.message.peerId && !idleController.isIdle) { return; diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 49d7938f..65ae9290 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -61,6 +61,7 @@ import getAlbumText from './utils/messages/getAlbumText'; import pause from '../../helpers/schedulers/pause'; import makeError from '../../helpers/makeError'; import getStickerEffectThumb from './utils/stickers/getStickerEffectThumb'; +import getDocumentInput from './utils/docs/getDocumentInput'; // console.trace('include'); // TODO: если удалить диалог находясь в папке, то он не удалится из папки и будет виден в настройках @@ -682,9 +683,9 @@ export class AppMessagesManager extends AppManager { size: MediaSize }, duration: number, - background: true, - silent: true, - clearDraft: true, + background: boolean, + silent: boolean, + clearDraft: boolean, scheduleDate: number, noSound: boolean, @@ -930,16 +931,9 @@ export class AppMessagesManager extends AppManager { message.send = () => { if(isDocument) { - const {id, access_hash, file_reference} = file as MyDocument; - const inputMedia: InputMedia = { _: 'inputMediaDocument', - id: { - _: 'inputDocument', - id, - access_hash, - file_reference - } + id: getDocumentInput(file) }; sentDeferred.resolve(inputMedia); @@ -1465,10 +1459,10 @@ export class AppMessagesManager extends AppManager { } */ private beforeMessageSending(message: Message.message, options: Partial<{ - isGroupedItem: true, - isScheduled: true, + isGroupedItem: boolean, + isScheduled: boolean, threadId: number, - clearDraft: true, + clearDraft: boolean, sequential: boolean, processAfter?: (cb: () => void) => void }> = {}) { diff --git a/src/lib/appManagers/appStickersManager.ts b/src/lib/appManagers/appStickersManager.ts index edf7fb6c..13aaf84c 100644 --- a/src/lib/appManagers/appStickersManager.ts +++ b/src/lib/appManagers/appStickersManager.ts @@ -5,7 +5,7 @@ */ import type {MyDocument} from './appDocsManager'; -import {Document, InputFileLocation, InputStickerSet, MessagesAllStickers, MessagesFeaturedStickers, MessagesFoundStickerSets, MessagesRecentStickers, MessagesStickers, MessagesStickerSet, PhotoSize, StickerPack, StickerSet, StickerSetCovered} from '../../layer'; +import {Document, InputFileLocation, InputStickerSet, MessagesAllStickers, MessagesFavedStickers, MessagesFeaturedStickers, MessagesFoundStickerSets, MessagesRecentStickers, MessagesStickers, MessagesStickerSet, PhotoSize, StickerPack, StickerSet, StickerSetCovered, Update} from '../../layer'; import {Modify} from '../../types'; import AppStorage from '../storage'; import DATABASE_STATE from '../../config/databases/state'; @@ -17,6 +17,7 @@ import {AppManager} from './manager'; import fixEmoji from '../richTextProcessor/fixEmoji'; import ctx from '../../environment/ctx'; import {getEnvironment} from '../../environment/utils'; +import getDocumentInput from './utils/docs/getDocumentInput'; const CACHE_TIME = 3600e3; @@ -49,6 +50,9 @@ export class AppStickersManager extends AppManager { private sounds: Record; private getAnimatedEmojiSoundsPromise: Promise; + private favedStickers: MyDocument[]; + private recentStickers: MyDocument[]; + protected after() { this.getStickerSetPromises = {}; this.getStickersByEmoticonsPromises = {}; @@ -57,6 +61,7 @@ export class AppStickersManager extends AppManager { this.rootScope.addEventListener('user_auth', () => { setTimeout(() => { this.getAnimatedEmojiStickerSet(); + this.getFavedStickersStickers(); }, 1000); if(!this.getGreetingStickersPromise && this.getGreetingStickersTimeout === undefined) { @@ -67,6 +72,8 @@ export class AppStickersManager extends AppManager { } }); + this.rootScope.addEventListener('app_config', () => this.onStickersUpdated('faved', true)); + this.apiUpdatesManager.addMultipleEventsListeners({ updateNewStickerSet: (update) => { const stickerSet = update.stickerset as MyMessagesStickerSet; @@ -74,11 +81,17 @@ export class AppStickersManager extends AppManager { this.rootScope.dispatchEvent('stickers_installed', stickerSet.set); }, - updateRecentStickers: () => { - this.getRecentStickers().then(({stickers}) => { - this.rootScope.dispatchEvent('stickers_recent', stickers as MyDocument[]); - }); - } + updateRecentStickers: () => this.onStickersUpdated('recent', true), + + updateFavedStickers: () => this.onStickersUpdated('faved', true) + }); + } + + private async onStickersUpdated(type: 'faved' | 'recent', overwrite: boolean) { + const stickers = await (type === 'faved' ? this.getFavedStickersStickers(overwrite) : this.getRecentStickersStickers(overwrite)); + this.rootScope.dispatchEvent('stickers_updated', { + type, + stickers }); } @@ -233,6 +246,7 @@ export class AppStickersManager extends AppManager { processResult: (res) => { assumeType(res); + this.recentStickers = res.stickers as MyDocument[]; this.saveStickers(res.stickers); return res; } @@ -241,6 +255,46 @@ export class AppStickersManager extends AppManager { return res; } + public getRecentStickersStickers(overwrite?: boolean) { + if(overwrite) this.recentStickers = undefined; + else if(this.recentStickers) return this.recentStickers; + return this.getRecentStickers().then(() => this.recentStickers); + } + + public saveRecentSticker(docId: DocId, unsave?: boolean, attached?: boolean) { + const doc = this.appDocsManager.getDoc(docId); + + findAndSplice(this.recentStickers, (_doc) => _doc.id === docId); + if(!unsave) { + this.recentStickers.unshift(doc); + + const docEmoticon = fixEmoji(doc.stickerEmojiRaw); + for(const emoticon in this.getStickersByEmoticonsPromises) { + const promise = this.getStickersByEmoticonsPromises[emoticon]; + promise.then((stickers) => { + const _doc = findAndSplice(stickers, (_doc) => _doc.id === doc.id); + if(_doc) { + stickers.unshift(_doc); + } else if(emoticon.includes(docEmoticon)) { + stickers.unshift(doc); + } + }); + } + } + + this.rootScope.dispatchEvent('sticker_updated', {type: 'recent', faved: !unsave, document: doc}); + + if(unsave) { + this.onStickersUpdated('recent', false); + } + + return this.apiManager.invokeApi('messages.saveRecentSticker', { + id: getDocumentInput(doc), + unsave, + attached + }); + } + private cleanEmoji(emoji: string) { return emoji.replace(/\ufe0f/g, '').replace(/🏻|🏼|🏽|🏾|🏿/g, ''); } @@ -409,14 +463,64 @@ export class AppStickersManager extends AppManager { return res.sets; } - public async getPromoPremiumStickers() { + public getPromoPremiumStickers() { return this.getStickersByEmoticon('⭐️⭐️', false); } - public async getPremiumStickers() { + public getPremiumStickers() { return this.getStickersByEmoticon('📂⭐️', false); } + public getFavedStickers() { + return this.apiManager.invokeApiHashable({ + method: 'messages.getFavedStickers', + processResult: (favedStickers) => { + assumeType(favedStickers); + this.saveStickers(favedStickers.stickers); + this.favedStickers = favedStickers.stickers as MyDocument[]; + return favedStickers; + } + }); + } + + public getFavedStickersStickers(overwrite?: boolean) { + if(overwrite) this.favedStickers = undefined; + else if(this.favedStickers) return this.favedStickers; + return this.getFavedStickers().then(() => this.favedStickers); + } + + public async getFavedStickersLimit() { + const appConfig = await this.apiManager.getAppConfig(); + return this.rootScope.premium ? appConfig.stickers_faved_limit_premium : appConfig.stickers_faved_limit_default; + } + + public async faveSticker(docId: DocId, unfave?: boolean) { + if(!this.favedStickers) { + await this.getFavedStickersStickers(); + } + + const limit = await this.getFavedStickersLimit(); + + const doc = this.appDocsManager.getDoc(docId); + findAndSplice(this.favedStickers, (_doc) => _doc.id === doc.id); + + if(!unfave) { + this.favedStickers.unshift(doc); + const spliced = this.favedStickers.splice(limit, this.favedStickers.length - limit); + } + + this.rootScope.dispatchEvent('sticker_updated', {type: 'faved', faved: !unfave, document: doc}); + + return this.apiManager.invokeApi('messages.faveSticker', { + id: getDocumentInput(doc), + unfave + }).then(() => { + if(unfave) { + this.onStickersUpdated('faved', true); + } + }); + } + public async toggleStickerSet(set: StickerSet.stickerSet) { set = this.storage.getFromCache(set.id).set; @@ -578,24 +682,12 @@ export class AppStickersManager extends AppManager { }); } - public pushRecentSticker(docId: DocId) { - const doc = this.appDocsManager.getDoc(docId); - const docEmoticon = fixEmoji(doc.stickerEmojiRaw); - for(const emoticon in this.getStickersByEmoticonsPromises) { - const promise = this.getStickersByEmoticonsPromises[emoticon]; - promise.then((stickers) => { - const _doc = findAndSplice(stickers, _doc => _doc.id === doc.id); - if(_doc) { - stickers.unshift(_doc); - } else if(emoticon.includes(docEmoticon)) { - stickers.unshift(doc); - } - }); + public clearRecentStickers() { + if(this.recentStickers) { + this.recentStickers.length = 0; + this.onStickersUpdated('recent', false); } - } - public clearRecentStickers() { - this.rootScope.dispatchEvent('stickers_recent', []); return this.apiManager.invokeApi('messages.clearRecentStickers'); } } diff --git a/src/lib/appManagers/utils/docs/getDocumentDownloadOptions.ts b/src/lib/appManagers/utils/docs/getDocumentDownloadOptions.ts index a6f1672c..070c6be9 100644 --- a/src/lib/appManagers/utils/docs/getDocumentDownloadOptions.ts +++ b/src/lib/appManagers/utils/docs/getDocumentDownloadOptions.ts @@ -6,10 +6,10 @@ import type {Document, PhotoSize, VideoSize} from '../../../../layer'; import type {DownloadOptions} from '../../../mtproto/apiFileManager'; -import getDocumentInput from './getDocumentInput'; +import getDocumentInputFileLocation from './getDocumentInputFileLocation'; export default function getDocumentDownloadOptions(doc: Document.document, thumb?: PhotoSize.photoSize | VideoSize, queueId?: number, onlyCache?: boolean): DownloadOptions { - const inputFileLocation = getDocumentInput(doc, thumb?.type); + const inputFileLocation = getDocumentInputFileLocation(doc, thumb?.type); let mimeType: string; if(thumb?._ === 'photoSize') { diff --git a/src/lib/appManagers/utils/docs/getDocumentInput.ts b/src/lib/appManagers/utils/docs/getDocumentInput.ts index 67ef1d1d..2eae4306 100644 --- a/src/lib/appManagers/utils/docs/getDocumentInput.ts +++ b/src/lib/appManagers/utils/docs/getDocumentInput.ts @@ -1,17 +1,11 @@ -/* - * https://github.com/morethanwords/tweb - * Copyright (C) 2019-2021 Eduard Kuzmenko - * https://github.com/morethanwords/tweb/blob/master/LICENSE - */ - -import {Document, InputFileLocation} from '../../../../layer'; - -export default function getInput(doc: Document.document, thumbSize?: string): InputFileLocation.inputDocumentFileLocation { - return { - _: 'inputDocumentFileLocation', - id: doc.id, - access_hash: doc.access_hash, - file_reference: doc.file_reference, - thumb_size: thumbSize - }; -} +import {InputDocument} from '../../../../layer'; +import type {MyDocument} from '../../appDocsManager'; + +export default function getDocumentInput(doc: MyDocument): InputDocument { + return { + _: 'inputDocument', + id: doc.id, + access_hash: doc.access_hash, + file_reference: doc.file_reference + }; +} diff --git a/src/lib/appManagers/utils/docs/getDocumentInputFileLocation.ts b/src/lib/appManagers/utils/docs/getDocumentInputFileLocation.ts new file mode 100644 index 00000000..6e773cf2 --- /dev/null +++ b/src/lib/appManagers/utils/docs/getDocumentInputFileLocation.ts @@ -0,0 +1,17 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import {Document, InputFileLocation} from '../../../../layer'; + +export default function getDocumentInputFileLocation(doc: Document.document, thumbSize?: string): InputFileLocation.inputDocumentFileLocation { + return { + _: 'inputDocumentFileLocation', + id: doc.id, + access_hash: doc.access_hash, + file_reference: doc.file_reference, + thumb_size: thumbSize + }; +} diff --git a/src/lib/appManagers/utils/docs/getDocumentInputFileName.ts b/src/lib/appManagers/utils/docs/getDocumentInputFileName.ts index 36e49087..d5c86c10 100644 --- a/src/lib/appManagers/utils/docs/getDocumentInputFileName.ts +++ b/src/lib/appManagers/utils/docs/getDocumentInputFileName.ts @@ -6,8 +6,8 @@ import {getFileNameByLocation} from '../../../../helpers/fileName'; import {Document} from '../../../../layer'; -import getDocumentInput from './getDocumentInput'; +import getDocumentInputFileLocation from './getDocumentInputFileLocation'; export default function getDocumentInputFileName(doc: Document.document, thumbSize?: string) { - return getFileNameByLocation(getDocumentInput(doc, thumbSize), {fileName: doc.file_name}); + return getFileNameByLocation(getDocumentInputFileLocation(doc, thumbSize), {fileName: doc.file_name}); } diff --git a/src/lib/mtproto/apiManager.ts b/src/lib/mtproto/apiManager.ts index 62a337ac..7ec52595 100644 --- a/src/lib/mtproto/apiManager.ts +++ b/src/lib/mtproto/apiManager.ts @@ -103,7 +103,7 @@ export class ApiManager extends ApiManagerMethods { protected after() { this.apiUpdatesManager.addMultipleEventsListeners({ updateConfig: () => { - this.getConfig(); + this.getConfig(true); this.getAppConfig(true); } }); diff --git a/src/lib/mtproto/tl_utils.ts b/src/lib/mtproto/tl_utils.ts index 37fdaf53..87b61186 100644 --- a/src/lib/mtproto/tl_utils.ts +++ b/src/lib/mtproto/tl_utils.ts @@ -17,14 +17,17 @@ import isObject from '../../helpers/object/isObject'; import gzipUncompress from '../../helpers/gzipUncompress'; import bigInt from 'big-integer'; import ulongFromInts from '../../helpers/long/ulongFromInts'; -import { safeBigInt } from '../../helpers/bigInt/bigIntConstants'; -import { bigIntToSigned, bigIntToUnsigned } from '../../helpers/bigInt/bigIntConversion'; +import {safeBigInt} from '../../helpers/bigInt/bigIntConstants'; +import {bigIntToSigned, bigIntToUnsigned} from '../../helpers/bigInt/bigIntConversion'; const boolFalse = +Schema.API.constructors.find((c) => c.predicate === 'boolFalse').id; const boolTrue = +Schema.API.constructors.find((c) => c.predicate === 'boolTrue').id; const vector = +Schema.API.constructors.find((c) => c.predicate === 'vector').id; const gzipPacked = +Schema.MTProto.constructors.find((c) => c.predicate === 'gzip_packed').id; +// * using slice to have a new buffer, otherwise the buffer will be copied to main thread +const sliceMethod: 'slice' | 'subarray' = 'slice'; // subarray + class TLSerialization { private maxLength = 2048; // 2Kb private offset = 0; // in bytes @@ -565,7 +568,7 @@ class TLDeserialization { (this.byteView[this.offset++] << 16); } - const bytes = this.byteView.subarray(this.offset, this.offset + len); + const bytes = this.byteView[sliceMethod](this.offset, this.offset + len); this.offset += len; // Padding @@ -587,7 +590,7 @@ class TLDeserialization { const len = bits / 8; if(typed) { - const result = this.byteView.subarray(this.offset, this.offset + len); + const result = this.byteView[sliceMethod](this.offset, this.offset + len); this.offset += len; return result; } @@ -614,7 +617,7 @@ class TLDeserialization { if(typed) { const bytes = new Uint8Array(len); - bytes.set(this.byteView.subarray(this.offset, this.offset + len)); + bytes.set(this.byteView[sliceMethod](this.offset, this.offset + len)); this.offset += len; return bytes; } diff --git a/src/lib/rootScope.ts b/src/lib/rootScope.ts index 8abbec02..859f44fa 100644 --- a/src/lib/rootScope.ts +++ b/src/lib/rootScope.ts @@ -87,7 +87,8 @@ export type BroadcastEvents = { 'stickers_installed': StickerSet.stickerSet, 'stickers_deleted': StickerSet.stickerSet, - 'stickers_recent': MyDocument[], + 'stickers_updated': {type: 'recent' | 'faved', stickers: MyDocument[]}, + 'sticker_updated': {type: 'recent' | 'faved', document: MyDocument, faved: boolean}, 'state_cleared': void, 'state_synchronized': ChatId | void, diff --git a/src/scss/partials/_button.scss b/src/scss/partials/_button.scss index 79068f64..b4f02fed 100644 --- a/src/scss/partials/_button.scss +++ b/src/scss/partials/_button.scss @@ -4,6 +4,8 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +$btn-menu-z-index: 4; + .btn, .btn-icon { background: none; @@ -97,7 +99,7 @@ visibility: hidden; position: absolute; background-color: var(--menu-background-color); - z-index: 3; + z-index: $btn-menu-z-index; top: 100%; padding: .3125rem 0; border-radius: $border-radius-medium; @@ -324,7 +326,7 @@ right: 0; top: 0; bottom: 0; - z-index: 3; + z-index: $btn-menu-z-index; cursor: default; user-select: none; //background-color: rgba(0, 0, 0, .2); diff --git a/src/scss/partials/_emojiDropdown.scss b/src/scss/partials/_emojiDropdown.scss index 840149e2..832e31f0 100644 --- a/src/scss/partials/_emojiDropdown.scss +++ b/src/scss/partials/_emojiDropdown.scss @@ -188,7 +188,7 @@ } &.active { - &:not(.tgico-recent) { + &:not(.tgico-recent):not(.tgico-saved) { background-color: var(--light-secondary-text-color); } } diff --git a/src/scss/partials/popups/_peer.scss b/src/scss/partials/popups/_peer.scss index 76125b9a..721f92a4 100644 --- a/src/scss/partials/popups/_peer.scss +++ b/src/scss/partials/popups/_peer.scss @@ -18,6 +18,7 @@ &-container { padding: .75rem .5rem; + min-width: 17.5rem; max-width: unquote('min(400px, 100%)'); }