Browse Source

Favorite stickers

Stickers context menu
master
Eduard Kuzmenko 2 years ago
parent
commit
df814f2a68
  1. 4
      src/components/chat/autocompleteHelper.ts
  2. 9
      src/components/chat/bubbles.ts
  3. 30
      src/components/chat/contextMenu.ts
  4. 18
      src/components/chat/input.ts
  5. 4
      src/components/chat/stickersHelper.ts
  6. 53
      src/components/emoticonsDropdown/index.ts
  7. 173
      src/components/emoticonsDropdown/tabs/stickers.ts
  8. 2
      src/components/popups/payment.ts
  9. 16
      src/components/popups/stickers.ts
  10. 2
      src/components/ripple.ts
  11. 14
      src/components/wrappers/sticker.ts
  12. 16
      src/helpers/dom/attachListNavigation.ts
  13. 113
      src/helpers/dom/createContextMenu.ts
  14. 76
      src/helpers/dom/createStickersContextMenu.ts
  15. 2
      src/helpers/dom/findUpAsChild.ts
  16. 13
      src/helpers/dropdownHover.ts
  17. 8
      src/helpers/overlayClickHandler.ts
  18. 11
      src/lang.ts
  19. 4
      src/lib/appManagers/appDocsManager.ts
  20. 54
      src/lib/appManagers/appImManager.ts
  21. 22
      src/lib/appManagers/appMessagesManager.ts
  22. 140
      src/lib/appManagers/appStickersManager.ts
  23. 4
      src/lib/appManagers/utils/docs/getDocumentDownloadOptions.ts
  24. 16
      src/lib/appManagers/utils/docs/getDocumentInput.ts
  25. 17
      src/lib/appManagers/utils/docs/getDocumentInputFileLocation.ts
  26. 4
      src/lib/appManagers/utils/docs/getDocumentInputFileName.ts
  27. 2
      src/lib/mtproto/apiManager.ts
  28. 13
      src/lib/mtproto/tl_utils.ts
  29. 3
      src/lib/rootScope.ts
  30. 6
      src/scss/partials/_button.scss
  31. 2
      src/scss/partials/_emojiDropdown.scss
  32. 1
      src/scss/partials/popups/_peer.scss

4
src/components/chat/autocompleteHelper.ts

@ -4,7 +4,7 @@ @@ -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<{ @@ -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;

9
src/components/chat/bubbles.ts

@ -945,7 +945,7 @@ export default class ChatBubbles { @@ -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 { @@ -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 { @@ -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,

30
src/components/chat/contextMenu.ts

@ -281,6 +281,21 @@ export default class ChatContextMenu { @@ -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 { @@ -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 { @@ -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);
};

18
src/components/chat/input.ts

@ -95,6 +95,7 @@ import ChatSendAs from './sendAs'; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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;

4
src/components/chat/stickersHelper.ts

@ -32,8 +32,8 @@ export default class StickersHelper extends AutocompleteHelper { @@ -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']
});

53
src/components/emoticonsDropdown/index.ts

@ -30,6 +30,7 @@ import {IS_APPLE_MOBILE} from '../../environment/userAgent'; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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', () => {

173
src/components/emoticonsDropdown/tabs/stickers.ts

@ -28,6 +28,10 @@ import noop from '../../../helpers/noop'; @@ -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 = { @@ -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<HTMLElement, StickersTabCategory>;
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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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() {

2
src/components/popups/payment.ts

@ -351,7 +351,7 @@ export default class PopupPayment extends PopupElement { @@ -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);
}
});

16
src/components/popups/stickers.ts

@ -20,6 +20,7 @@ import {attachClickEvent} from '../../helpers/dom/clickEvent'; @@ -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 { @@ -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 { @@ -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 { @@ -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);
}
};

2
src/components/ripple.ts

@ -141,7 +141,7 @@ export default function ripple( @@ -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

14
src/components/wrappers/sticker.ts

@ -17,6 +17,8 @@ import renderImageFromUrl from '../../helpers/dom/renderImageFromUrl'; @@ -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, @@ -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) {

16
src/helpers/dom/attachListNavigation.ts

@ -17,13 +17,15 @@ const ACTIVE_CLASS_NAME = 'active'; @@ -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<boolean>,
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 @@ -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 @@ -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 @@ -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();
}

113
src/helpers/dom/createContextMenu.ts

@ -0,0 +1,113 @@ @@ -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<T extends ButtonMenuItemOptions & {verify?: () => boolean | Promise<boolean>}>({
buttons,
findElement,
listenTo,
appendTo,
filterButtons,
onOpen,
onClose
}: {
buttons: T[],
findElement: (e: MouseEvent) => HTMLElement,
listenTo: HTMLElement,
appendTo?: HTMLElement,
filterButtons?: (buttons: T[]) => Promise<T[]>,
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};
}

76
src/helpers/dom/createStickersContextMenu.ts

@ -0,0 +1,76 @@ @@ -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
}]
});
}

2
src/helpers/dom/findUpAsChild.ts

@ -4,7 +4,7 @@ @@ -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) {

13
src/helpers/dropdownHover.ts

@ -25,6 +25,7 @@ export default class DropdownHover extends EventListenerBase<{ @@ -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<{ @@ -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<{ @@ -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;
}
}

8
src/helpers/overlayClickHandler.ts

@ -24,11 +24,11 @@ export default class OverlayClickHandler extends EventListenerBase<{ @@ -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<{ @@ -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<{ @@ -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)

11
src/lang.ts

@ -749,6 +749,16 @@ const lang = { @@ -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 = { @@ -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',

4
src/lib/appManagers/appDocsManager.ts

@ -20,7 +20,7 @@ import assumeType from '../../helpers/assumeType'; @@ -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 { @@ -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);
}
}

54
src/lib/appManagers/appImManager.ts

@ -52,7 +52,7 @@ import confirmationPopup from '../../components/confirmationPopup'; @@ -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<{ @@ -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<ReturnType<typeof doThatSticker>>;
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<{ @@ -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<ReturnType<typeof doThatSticker>>;
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<{ @@ -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;

22
src/lib/appManagers/appMessagesManager.ts

@ -61,6 +61,7 @@ import getAlbumText from './utils/messages/getAlbumText'; @@ -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 { @@ -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 { @@ -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 { @@ -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
}> = {}) {

140
src/lib/appManagers/appStickersManager.ts

@ -5,7 +5,7 @@ @@ -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'; @@ -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 { @@ -49,6 +50,9 @@ export class AppStickersManager extends AppManager {
private sounds: Record<string, MyDocument>;
private getAnimatedEmojiSoundsPromise: Promise<void>;
private favedStickers: MyDocument[];
private recentStickers: MyDocument[];
protected after() {
this.getStickerSetPromises = {};
this.getStickersByEmoticonsPromises = {};
@ -57,6 +61,7 @@ export class AppStickersManager extends AppManager { @@ -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 { @@ -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 { @@ -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 { @@ -233,6 +246,7 @@ export class AppStickersManager extends AppManager {
processResult: (res) => {
assumeType<MessagesRecentStickers.messagesRecentStickers>(res);
this.recentStickers = res.stickers as MyDocument[];
this.saveStickers(res.stickers);
return res;
}
@ -241,6 +255,46 @@ export class AppStickersManager extends AppManager { @@ -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 { @@ -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<MessagesFavedStickers.messagesFavedStickers>(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 { @@ -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');
}
}

4
src/lib/appManagers/utils/docs/getDocumentDownloadOptions.ts

@ -6,10 +6,10 @@ @@ -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') {

16
src/lib/appManagers/utils/docs/getDocumentInput.ts

@ -1,17 +1,11 @@ @@ -1,17 +1,11 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import {InputDocument} from '../../../../layer';
import type {MyDocument} from '../../appDocsManager';
import {Document, InputFileLocation} from '../../../../layer';
export default function getInput(doc: Document.document, thumbSize?: string): InputFileLocation.inputDocumentFileLocation {
export default function getDocumentInput(doc: MyDocument): InputDocument {
return {
_: 'inputDocumentFileLocation',
_: 'inputDocument',
id: doc.id,
access_hash: doc.access_hash,
file_reference: doc.file_reference,
thumb_size: thumbSize
file_reference: doc.file_reference
};
}

17
src/lib/appManagers/utils/docs/getDocumentInputFileLocation.ts

@ -0,0 +1,17 @@ @@ -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
};
}

4
src/lib/appManagers/utils/docs/getDocumentInputFileName.ts

@ -6,8 +6,8 @@ @@ -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});
}

2
src/lib/mtproto/apiManager.ts

@ -103,7 +103,7 @@ export class ApiManager extends ApiManagerMethods { @@ -103,7 +103,7 @@ export class ApiManager extends ApiManagerMethods {
protected after() {
this.apiUpdatesManager.addMultipleEventsListeners({
updateConfig: () => {
this.getConfig();
this.getConfig(true);
this.getAppConfig(true);
}
});

13
src/lib/mtproto/tl_utils.ts

@ -17,14 +17,17 @@ import isObject from '../../helpers/object/isObject'; @@ -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<FetchLongAs extends Long> { @@ -565,7 +568,7 @@ class TLDeserialization<FetchLongAs extends Long> {
(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<FetchLongAs extends Long> { @@ -587,7 +590,7 @@ class TLDeserialization<FetchLongAs extends Long> {
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<FetchLongAs extends Long> { @@ -614,7 +617,7 @@ class TLDeserialization<FetchLongAs extends Long> {
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;
}

3
src/lib/rootScope.ts

@ -87,7 +87,8 @@ export type BroadcastEvents = { @@ -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,

6
src/scss/partials/_button.scss

@ -4,6 +4,8 @@ @@ -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 @@ @@ -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 @@ @@ -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);

2
src/scss/partials/_emojiDropdown.scss

@ -188,7 +188,7 @@ @@ -188,7 +188,7 @@
}
&.active {
&:not(.tgico-recent) {
&:not(.tgico-recent):not(.tgico-saved) {
background-color: var(--light-secondary-text-color);
}
}

1
src/scss/partials/popups/_peer.scss

@ -18,6 +18,7 @@ @@ -18,6 +18,7 @@
&-container {
padding: .75rem .5rem;
min-width: 17.5rem;
max-width: unquote('min(400px, 100%)');
}

Loading…
Cancel
Save