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. 28
      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 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * 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 EventListenerBase from '../../helpers/eventListenerBase';
import {IS_MOBILE} from '../../environment/userAgent'; import {IS_MOBILE} from '../../environment/userAgent';
import rootScope from '../../lib/rootScope'; import rootScope from '../../lib/rootScope';
@ -28,7 +28,7 @@ export default class AutocompleteHelper extends EventListenerBase<{
protected controller: AutocompleteHelperController; protected controller: AutocompleteHelperController;
protected listType: 'xy' | 'x' | 'y'; protected listType: 'xy' | 'x' | 'y';
protected onSelect: (target: Element) => boolean | void; protected onSelect: ListNavigationOptions['onSelect'];
protected waitForKey?: string[]; protected waitForKey?: string[];
protected navigationItem: NavigationItem; protected navigationItem: NavigationItem;

9
src/components/chat/bubbles.ts

@ -945,7 +945,7 @@ export default class ChatBubbles {
return; return;
} }
awaited.forEach(({bubble, message}) => { awaited.filter(Boolean).forEach(({bubble, message}) => {
if(this.bubbles[message.mid] !== bubble) { if(this.bubbles[message.mid] !== bubble) {
return; return;
} }
@ -4011,8 +4011,9 @@ export default class ChatBubbles {
} }
const sizes = mediaSizes.active; const sizes = mediaSizes.active;
const size = bubble.classList.contains('emoji-big') ? sizes.emojiSticker : (doc.animated ? sizes.animatedSticker : sizes.staticSticker); const isEmoji = bubble.classList.contains('emoji-big');
setAttachmentSize(doc, attachmentDiv, size.width, size.height); const boxSize = isEmoji ? sizes.emojiSticker : (doc.animated ? sizes.animatedSticker : sizes.staticSticker);
setAttachmentSize(doc, attachmentDiv, boxSize.width, boxSize.height);
// let preloader = new ProgressivePreloader(attachmentDiv, false); // let preloader = new ProgressivePreloader(attachmentDiv, false);
bubbleContainer.style.minWidth = attachmentDiv.style.width; bubbleContainer.style.minWidth = attachmentDiv.style.width;
bubbleContainer.style.minHeight = attachmentDiv.style.height; bubbleContainer.style.minHeight = attachmentDiv.style.height;
@ -4026,7 +4027,7 @@ export default class ChatBubbles {
// play: !!message.pending || !multipleRender, // play: !!message.pending || !multipleRender,
play: true, play: true,
loop: true, loop: true,
emoji: bubble.classList.contains('emoji-big') ? messageMessage : undefined, emoji: isEmoji ? messageMessage : undefined,
withThumb: true, withThumb: true,
loadPromises, loadPromises,
isOut, isOut,

30
src/components/chat/contextMenu.ts

@ -281,6 +281,21 @@ export default class ChatContextMenu {
} }
private setButtons() { 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 = [{ this.buttons = [{
icon: 'send2', icon: 'send2',
text: 'MessageScheduleSend', text: 'MessageScheduleSend',
@ -317,6 +332,16 @@ export default class ChatContextMenu {
!!this.chat.input.messageInput && !!this.chat.input.messageInput &&
this.chat.type !== 'scheduled'/* , this.chat.type !== 'scheduled'/* ,
cancelEvent: true */ 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', icon: 'edit',
text: 'Edit', text: 'Edit',
@ -682,6 +707,11 @@ export default class ChatContextMenu {
this.chat.input.initMessageReply(this.mid); 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 = () => { private onEditClick = () => {
this.chat.input.initMessageEditing(this.mid); this.chat.input.initMessageEditing(this.mid);
}; };

18
src/components/chat/input.ts

@ -95,6 +95,7 @@ import ChatSendAs from './sendAs';
import filterAsync from '../../helpers/array/filterAsync'; import filterAsync from '../../helpers/array/filterAsync';
import InputFieldAnimated from '../inputFieldAnimated'; import InputFieldAnimated from '../inputFieldAnimated';
import getStickerEffectThumb from '../../lib/appManagers/utils/stickers/getStickerEffectThumb'; import getStickerEffectThumb from '../../lib/appManagers/utils/stickers/getStickerEffectThumb';
import PopupStickers from '../popups/stickers';
const RECORD_MIN_TIME = 500; const RECORD_MIN_TIME = 500;
const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.'; const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.';
@ -1059,6 +1060,9 @@ export default class ChatInput {
return; return;
} }
const popups = PopupElement.getPopups(PopupStickers);
popups.forEach((popup) => popup.hide());
this.appImManager.openScheduled(peerId); this.appImManager.openScheduled(peerId);
}, 0); }, 0);
} }
@ -2435,7 +2439,12 @@ export default class ChatInput {
// this.onMessageSent(); // 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); document = await this.managers.appDocsManager.getDoc(document);
const flag = document.type === 'sticker' ? 'send_stickers' : (document.type === 'gif' ? 'send_gifs' : 'send_media'); 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) { if(this.chat.type === 'scheduled' && !force) {
this.scheduleSending(() => this.sendMessageWithDocument(document, true, clearDraft)); this.scheduleSending(() => this.sendMessageWithDocument(document, true, clearDraft, silent));
return false; return false;
} }
@ -2460,12 +2469,13 @@ export default class ChatInput {
this.managers.appMessagesManager.sendFile(this.chat.peerId, document, { this.managers.appMessagesManager.sendFile(this.chat.peerId, document, {
...this.chat.getMessageSendingParams(), ...this.chat.getMessageSendingParams(),
isMedia: true, isMedia: true,
clearDraft: clearDraft || undefined clearDraft: clearDraft || undefined,
silent
}); });
this.onMessageSent(clearDraft, true); this.onMessageSent(clearDraft, true);
if(document.type === 'sticker') { if(document.type === 'sticker') {
emoticonsDropdown.stickersTab?.pushRecentSticker(document); emoticonsDropdown.stickersTab?.unshiftRecentSticker(document);
} }
return true; return true;

4
src/components/chat/stickersHelper.ts

@ -32,8 +32,8 @@ export default class StickersHelper extends AutocompleteHelper {
appendTo, appendTo,
controller, controller,
listType: 'xy', listType: 'xy',
onSelect: (target) => { onSelect: async(target) => {
return !EmoticonsDropdown.onMediaClick({target}, true); return !(await EmoticonsDropdown.onMediaClick({target}, true));
}, },
waitForKey: ['ArrowUp', 'ArrowDown'] waitForKey: ['ArrowUp', 'ArrowDown']
}); });

53
src/components/emoticonsDropdown/index.ts

@ -30,6 +30,7 @@ import {IS_APPLE_MOBILE} from '../../environment/userAgent';
import {AppManagers} from '../../lib/appManagers/managers'; import {AppManagers} from '../../lib/appManagers/managers';
import type LazyLoadQueueIntersector from '../lazyLoadQueueIntersector'; import type LazyLoadQueueIntersector from '../lazyLoadQueueIntersector';
import {simulateClickEvent} from '../../helpers/dom/clickEvent'; import {simulateClickEvent} from '../../helpers/dom/clickEvent';
import overlayCounter from '../../helpers/overlayCounter';
export const EMOTICONSSTICKERGROUP: AnimationItemGroup = 'emoticons-dropdown'; export const EMOTICONSSTICKERGROUP: AnimationItemGroup = 'emoticons-dropdown';
@ -88,7 +89,7 @@ export class EmoticonsDropdown extends DropdownHover {
EmoticonsDropdown.lazyLoadQueue.unlock(); EmoticonsDropdown.lazyLoadQueue.unlock();
EmoticonsDropdown.lazyLoadQueue.refresh(); EmoticonsDropdown.lazyLoadQueue.refresh();
this.container.classList.remove('disable-hover'); // this.container.classList.remove('disable-hover');
}); });
this.addEventListener('close', () => { this.addEventListener('close', () => {
@ -106,7 +107,7 @@ export class EmoticonsDropdown extends DropdownHover {
EmoticonsDropdown.lazyLoadQueue.unlock(); EmoticonsDropdown.lazyLoadQueue.unlock();
EmoticonsDropdown.lazyLoadQueue.refresh(); EmoticonsDropdown.lazyLoadQueue.refresh();
this.container.classList.remove('disable-hover'); // this.container.classList.remove('disable-hover');
this.savedRange = undefined; this.savedRange = undefined;
}); });
@ -182,6 +183,26 @@ export class EmoticonsDropdown extends DropdownHover {
this.tabs[INIT_TAB_ID].init(); // onTransitionEnd не вызовется, т.к. это первая открытая вкладка 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); appImManager.addEventListener('peer_changed', this.checkRights);
this.checkRights(); this.checkRights();
@ -204,15 +225,17 @@ export class EmoticonsDropdown extends DropdownHover {
this.deleteBtn.classList.toggle('hide', this.tabId !== 0); this.deleteBtn.classList.toggle('hide', this.tabId !== 0);
}; };
private checkRights = () => { private checkRights = async() => {
const {peerId, threadId} = appImManager.chat; const {peerId, threadId} = appImManager.chat;
const children = this.tabsEl.children; const children = this.tabsEl.children;
const tabsElements = Array.from(children) as HTMLElement[]; const tabsElements = Array.from(children) as HTMLElement[];
const canSendStickers = this.managers.appMessagesManager.canSendToPeer(peerId, threadId, 'send_stickers'); const [canSendStickers, canSendGifs] = await Promise.all([
tabsElements[2].toggleAttribute('disabled', !canSendStickers); 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); tabsElements[3].toggleAttribute('disabled', !canSendGifs);
const active = this.tabsEl.querySelector('.active'); const active = this.tabsEl.querySelector('.active');
@ -290,30 +313,34 @@ export class EmoticonsDropdown extends DropdownHover {
return {stickyIntersector, setActive}; 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; let target = e.target as HTMLElement;
target = findUpTag(target, 'DIV'); target = findUpTag(target, 'DIV');
if(!target) return false; if(!target) return false;
const fileId = target.dataset.docId; const docId = target.dataset.docId;
if(!fileId) return false; 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'); /* dropdown.classList.remove('active');
toggleEl.classList.remove('active'); */ toggleEl.classList.remove('active'); */
if(emoticonsDropdown.container) { if(emoticonsDropdown.container) {
emoticonsDropdown.forceClose = true; emoticonsDropdown.forceClose = true;
emoticonsDropdown.container.classList.add('disable-hover'); // emoticonsDropdown.container.classList.add('disable-hover');
emoticonsDropdown.toggle(false); emoticonsDropdown.toggle(false);
} }
return true; return true;
} else { } else {
console.warn('got no doc by id:', fileId); console.warn('got no doc by id:', docId);
return false; return false;
} }
}; }
public addLazyLoadQueueRepeat(lazyLoadQueue: LazyLoadQueueIntersector, processInvisibleDiv: (div: HTMLElement) => void) { public addLazyLoadQueueRepeat(lazyLoadQueue: LazyLoadQueueIntersector, processInvisibleDiv: (div: HTMLElement) => void) {
this.addEventListener('close', () => { this.addEventListener('close', () => {

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

@ -28,6 +28,10 @@ import noop from '../../../helpers/noop';
import ButtonIcon from '../../buttonIcon'; import ButtonIcon from '../../buttonIcon';
import confirmationPopup from '../../confirmationPopup'; import confirmationPopup from '../../confirmationPopup';
import VisibilityIntersector, {OnVisibilityChange} from '../../visibilityIntersector'; 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 { export class SuperStickerRenderer {
public lazyLoadQueue: LazyLoadQueueRepeat; public lazyLoadQueue: LazyLoadQueueRepeat;
@ -158,18 +162,18 @@ type StickersTabCategory = {
document: MyDocument, document: MyDocument,
element: HTMLElement element: HTMLElement
}[], }[],
pos?: number mounted?: boolean,
id: string,
limit?: number
}; };
const RECENT_STICKERS_COUNT = 20;
export default class StickersTab implements EmoticonsTab { export default class StickersTab implements EmoticonsTab {
private content: HTMLElement; private content: HTMLElement;
private categories: {[id: string]: StickersTabCategory}; private categories: {[id: string]: StickersTabCategory};
private categoriesMap: Map<HTMLElement, StickersTabCategory>; private categoriesMap: Map<HTMLElement, StickersTabCategory>;
private categoriesIntersector: VisibilityIntersector; private categoriesIntersector: VisibilityIntersector;
private localCategoryIndex: number; private localCategories: StickersTabCategory[];
private scroll: Scrollable; private scroll: Scrollable;
private menu: HTMLElement; private menu: HTMLElement;
@ -180,7 +184,13 @@ export default class StickersTab implements EmoticonsTab {
constructor(private managers: AppManagers) { constructor(private managers: AppManagers) {
this.categories = {}; this.categories = {};
this.categoriesMap = new Map(); 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) { private createCategory(stickerSet: StickerSet.stickerSet, _title: HTMLElement | DocumentFragment) {
@ -211,7 +221,8 @@ export default class StickersTab implements EmoticonsTab {
menuTabPadding menuTabPadding
}, },
set: stickerSet, set: stickerSet,
items: [] items: [],
id: '' + stickerSet.id
}; };
container.append(title, items); container.append(title, items);
@ -267,7 +278,7 @@ export default class StickersTab implements EmoticonsTab {
const category = this.createCategory(set, wrapEmojiText(set.title)); const category = this.createCategory(set, wrapEmojiText(set.title));
const {menuTab, menuTabPadding, container} = category.elements; 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); positionElementByIndex(menuTab, this.menu, pos);
const promise = this.managers.appStickersManager.getStickerSet(set); 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 createLocalCategory = (id: string, title: LangPackKey, icon?: string) => {
const category = this.createCategory({id} as any, i18n(title)); const category = this.createCategory({id} as any, i18n(title));
this.localCategories.push(category);
category.elements.title.classList.add('disable-hover'); category.elements.title.classList.add('disable-hover');
icon && category.elements.menuTab.classList.add('tgico-' + icon); icon && category.elements.menuTab.classList.add('tgico-' + icon);
category.elements.menuTabPadding.remove(); category.elements.menuTabPadding.remove();
category.pos = this.localCategoryIndex++;
this.toggleLocalCategory(category, false); this.toggleLocalCategory(category, false);
return category; 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'); 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}); const clearButton = ButtonIcon('close', {noRipple: true});
recentCategory.elements.title.append(clearButton); recentCategory.elements.title.append(clearButton);
@ -398,22 +437,22 @@ export default class StickersTab implements EmoticonsTab {
}, noop); }, 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 premiumCategory = createLocalCategory('premium', 'PremiumStickersShort');
const s = document.createElement('span'); const s = document.createElement('span');
s.classList.add('tgico-star', 'color-premium'); s.classList.add('tgico-star', 'color-premium');
premiumCategory.elements.menuTab.append(s); premiumCategory.elements.menuTab.append(s);
const promises = [ const promises = [
this.managers.appStickersManager.getRecentStickers().then((stickers) => { Promise.all([
onRecentStickers(stickers.stickers as MyDocument[]); 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) => { 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) { 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 = () => { const resizeCategories = () => {
for(const [container, category] of this.categoriesMap) { for(const [container, category] of this.categoriesMap) {
this.setCategoryItemsHeight(category); this.setCategoryItemsHeight(category);
@ -508,6 +567,17 @@ export default class StickersTab implements EmoticonsTab {
emoticonsDropdown.addEventListener('opened', resizeCategories); 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; this.init = null;
} }
@ -516,23 +586,50 @@ export default class StickersTab implements EmoticonsTab {
category.elements.menuTab.remove(); category.elements.menuTab.remove();
category.elements.container.remove(); category.elements.container.remove();
} else { } else {
const pos = category.pos; let idx = this.localCategories.indexOf(category);
positionElementByIndex(category.elements.menuTab, this.menu, pos); const notMounted = this.localCategories.slice(0, idx).filter((category) => !category.mounted);
positionElementByIndex(category.elements.container, this.scroll.container, pos); 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); // category.elements.container.classList.toggle('hide', !visible);
} }
public pushRecentSticker(doc: MyDocument) { private onLocalCategoryUpdate(category: StickersTabCategory) {
this.managers.appStickersManager.pushRecentSticker(doc.id); 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(!batch) {
if(!category) { this.onLocalCategoryUpdate(category);
return; }
}
}
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); let item = findAndSplice(category.items, (item) => item.document.id === doc.id);
if(!item) { if(!item) {
item = { item = {
@ -542,13 +639,19 @@ export default class StickersTab implements EmoticonsTab {
} }
category.items.unshift(item); category.items.unshift(item);
if(items.childElementCount) items.prepend(item.element); category.elements.items.prepend(item.element);
if(items.childElementCount > RECENT_STICKERS_COUNT) {
(Array.from(items.children) as HTMLElement[]).slice(RECENT_STICKERS_COUNT).forEach((el) => el.remove()); if(!batch) {
this.spliceExceed(category);
} }
}
this.setCategoryItemsHeight(category); public unshiftRecentSticker(doc: MyDocument) {
this.toggleLocalCategory(category, true); this.managers.appStickersManager.saveRecentSticker(doc.id);
}
public deleteRecentSticker(doc: MyDocument) {
this.managers.appStickersManager.saveRecentSticker(doc.id, true);
} }
onClose() { onClose() {

2
src/components/popups/payment.ts

@ -351,7 +351,7 @@ export default class PopupPayment extends PopupElement {
} }
tipsLabel.label.addEventListener('mousedown', (e) => { tipsLabel.label.addEventListener('mousedown', (e) => {
if(!findUpAsChild(e.target, input)) { if(!findUpAsChild(e.target as HTMLElement, input)) {
placeCaretAtEnd(input); placeCaretAtEnd(input);
} }
}); });

16
src/components/popups/stickers.ts

@ -20,6 +20,7 @@ import {attachClickEvent} from '../../helpers/dom/clickEvent';
import {toastNew} from '../toast'; import {toastNew} from '../toast';
import setInnerHTML from '../../helpers/dom/setInnerHTML'; import setInnerHTML from '../../helpers/dom/setInnerHTML';
import wrapEmojiText from '../../lib/richTextProcessor/wrapEmojiText'; import wrapEmojiText from '../../lib/richTextProcessor/wrapEmojiText';
import createStickersContextMenu from '../../helpers/dom/createStickersContextMenu';
const ANIMATION_GROUP: AnimationItemGroup = 'STICKERS-POPUP'; const ANIMATION_GROUP: AnimationItemGroup = 'STICKERS-POPUP';
@ -34,6 +35,7 @@ export default class PopupStickers extends PopupElement {
this.addEventListener('close', () => { this.addEventListener('close', () => {
animationIntersector.setOnlyOnePlayableGroup(); animationIntersector.setOnlyOnePlayableGroup();
destroy();
}); });
const div = document.createElement('div'); const div = document.createElement('div');
@ -57,10 +59,10 @@ export default class PopupStickers extends PopupElement {
this.scrollable.append(div); this.scrollable.append(div);
this.body.append(this.stickersFooter); this.body.append(this.stickersFooter);
// const editButton = document.createElement('button'); const {destroy} = createStickersContextMenu({
// editButton.classList.add('btn-primary'); listenTo: this.stickersDiv,
isStickerPack: true
// this.stickersFooter.append(editButton); });
this.loadStickerSet(); this.loadStickerSet();
} }
@ -69,11 +71,9 @@ export default class PopupStickers extends PopupElement {
const target = findUpClassName(e.target, 'sticker-set-sticker'); const target = findUpClassName(e.target, 'sticker-set-sticker');
if(!target) return; if(!target) return;
const fileId = target.dataset.docId; const docId = target.dataset.docId;
if(appImManager.chat.input.sendMessageWithDocument(fileId)) { if(appImManager.chat.input.sendMessageWithDocument(docId)) {
this.hide(); this.hide();
} else {
console.warn('got no doc by id:', fileId);
} }
}; };

2
src/components/ripple.ts

@ -141,7 +141,7 @@ export default function ripple(
findUpClassName(e.target as HTMLElement, 'c-ripple') !== r findUpClassName(e.target as HTMLElement, 'c-ripple') !== r
) && ( ) && (
attachListenerTo === elem || attachListenerTo === elem ||
!findUpAsChild(e.target, attachListenerTo) !findUpAsChild(e.target as HTMLElement, attachListenerTo)
); );
// TODO: rename this variable // TODO: rename this variable

14
src/components/wrappers/sticker.ts

@ -17,6 +17,8 @@ import renderImageFromUrl from '../../helpers/dom/renderImageFromUrl';
import getImageFromStrippedThumb from '../../helpers/getImageFromStrippedThumb'; import getImageFromStrippedThumb from '../../helpers/getImageFromStrippedThumb';
import getPreviewURLFromThumb from '../../helpers/getPreviewURLFromThumb'; import getPreviewURLFromThumb from '../../helpers/getPreviewURLFromThumb';
import makeError from '../../helpers/makeError'; import makeError from '../../helpers/makeError';
import {makeMediaSize} from '../../helpers/mediaSize';
import mediaSizes from '../../helpers/mediaSizes';
import onMediaLoad from '../../helpers/onMediaLoad'; import onMediaLoad from '../../helpers/onMediaLoad';
import {isSavingLottiePreview, saveLottiePreview} from '../../helpers/saveLottiePreview'; import {isSavingLottiePreview, saveLottiePreview} from '../../helpers/saveLottiePreview';
import throttle from '../../helpers/schedulers/throttle'; import throttle from '../../helpers/schedulers/throttle';
@ -79,12 +81,12 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
asStatic = true; asStatic = true;
} }
if(!width) { if(!width && !height) {
width = !emoji ? 200 : undefined; const sizes = mediaSizes.active;
} const boxSize = emoji ? sizes.emojiSticker : (doc.animated ? sizes.animatedSticker : sizes.staticSticker);
const size = makeMediaSize(doc.w, doc.h).aspectFitted(boxSize);
if(!height) { width = size.width;
height = !emoji ? 200 : undefined; height = size.height;
} }
if(stickerType === 2) { if(stickerType === 2) {

16
src/helpers/dom/attachListNavigation.ts

@ -17,13 +17,15 @@ const ACTIVE_CLASS_NAME = 'active';
const AXIS_Y_KEYS: ArrowKey[] = ['ArrowUp', 'ArrowDown']; const AXIS_Y_KEYS: ArrowKey[] = ['ArrowUp', 'ArrowDown'];
const AXIS_X_KEYS: ArrowKey[] = ['ArrowLeft', 'ArrowRight']; const AXIS_X_KEYS: ArrowKey[] = ['ArrowLeft', 'ArrowRight'];
export default function attachListNavigation({list, type, onSelect, once, waitForKey}: { export type ListNavigationOptions = {
list: HTMLElement, list: HTMLElement,
type: 'xy' | 'x' | 'y', type: 'xy' | 'x' | 'y',
onSelect: (target: Element) => void | boolean, onSelect: (target: Element) => void | boolean | Promise<boolean>,
once: boolean, once: boolean,
waitForKey?: string[] waitForKey?: string[]
}) { };
export default function attachListNavigation({list, type, onSelect, once, waitForKey}: ListNavigationOptions) {
let waitForKeySet = waitForKey?.length ? new Set(waitForKey) : undefined; 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)); 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'); list.classList.add('navigable-list');
const onMouseMove = (e: MouseEvent) => { const onMouseMove = (e: MouseEvent) => {
const target = findUpAsChild(e.target, list) as HTMLElement; const target = findUpAsChild(e.target as HTMLElement, list) as HTMLElement;
if(!target) { if(!target) {
return; return;
} }
@ -129,7 +131,7 @@ export default function attachListNavigation({list, type, onSelect, once, waitFo
const onClick = (e: Event) => { const onClick = (e: Event) => {
cancelEvent(e); // cancel keyboard closening 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) { if(!target) {
return; return;
} }
@ -138,8 +140,8 @@ export default function attachListNavigation({list, type, onSelect, once, waitFo
fireSelect(getCurrentTarget()); fireSelect(getCurrentTarget());
}; };
const fireSelect = (target: Element) => { const fireSelect = async(target: Element) => {
const canContinue = onSelect(target); const canContinue = await onSelect(target);
if(canContinue !== undefined ? !canContinue : once) { if(canContinue !== undefined ? !canContinue : once) {
detach(); detach();
} }

113
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<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 @@
/*
* 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 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * 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; if(el.parentElement === parent) return el;
while(el.parentElement) { while(el.parentElement) {

13
src/helpers/dropdownHover.ts

@ -25,6 +25,7 @@ export default class DropdownHover extends EventListenerBase<{
protected displayTimeout: number; protected displayTimeout: number;
protected forceClose = false; protected forceClose = false;
protected inited = false; protected inited = false;
protected ignoreMouseOut = false;
constructor(options: { constructor(options: {
element: DropdownHover['element'] 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; if(KEEP_OPEN || !this.isActive()) return;
clearTimeout(this.displayTimeout); 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)) { if(toElement && findUpAsChild(toElement, this.element)) {
return; return;
} }
@ -162,4 +167,8 @@ export default class DropdownHover extends EventListenerBase<{
public isActive() { public isActive() {
return this.element.classList.contains('active'); 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<{
protected withOverlay?: boolean protected withOverlay?: boolean
) { ) {
super(false); super(false);
this.listenerOptions = withOverlay ? undefined : {capture: true}; this.listenerOptions = withOverlay ? {} : {capture: true};
} }
protected onClick = (e: MouseEvent | TouchEvent) => { protected onClick = (e: MouseEvent | TouchEvent) => {
if(this.element && findUpAsChild(e.target, this.element)) { if(this.element && findUpAsChild(e.target as HTMLElement, this.element)) {
return; return;
} }
@ -48,7 +48,7 @@ export default class OverlayClickHandler extends EventListenerBase<{
if(!IS_TOUCH_SUPPORTED) { if(!IS_TOUCH_SUPPORTED) {
// window.removeEventListener('keydown', onKeyDown, {capture: true}); // 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); document.removeEventListener(CLICK_EVENT_NAME, this.onClick, this.listenerOptions);
@ -89,7 +89,7 @@ export default class OverlayClickHandler extends EventListenerBase<{
if(!IS_TOUCH_SUPPORTED) { if(!IS_TOUCH_SUPPORTED) {
// window.addEventListener('keydown', onKeyDown, {capture: true}); // 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) /* // ! because this event must be canceled, and can't cancel on menu click (below)

11
src/lang.ts

@ -749,6 +749,16 @@ const lang = {
'ClearRecentStickersAlertTitle': 'Clear recent stickers', 'ClearRecentStickersAlertTitle': 'Clear recent stickers',
'ClearRecentStickersAlertMessage': 'Do you want to clear all your recent stickers?', 'ClearRecentStickersAlertMessage': 'Do you want to clear all your recent stickers?',
'PremiumStickersShort': 'Premium', '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 // * macos
'AccountSettings.Filters': 'Chat Folders', 'AccountSettings.Filters': 'Chat Folders',
@ -949,6 +959,7 @@ const lang = {
'ChatList.Mute.3Days': 'For 3 Days', 'ChatList.Mute.3Days': 'For 3 Days',
'ChatList.Mute.Forever': 'Forever', 'ChatList.Mute.Forever': 'Forever',
'Channel.DescriptionHolderDescrpiton': 'You can provide an optional description for your channel.', 'Channel.DescriptionHolderDescrpiton': 'You can provide an optional description for your channel.',
'Context.ViewStickerSet': 'View Sticker Set',
'CreateGroup.NameHolder': 'Group Name', 'CreateGroup.NameHolder': 'Group Name',
'Date.Today': 'Today', 'Date.Today': 'Today',
'DeleteChat.DeleteGroupForAll': 'Delete for all members', 'DeleteChat.DeleteGroupForAll': 'Delete for all members',

4
src/lib/appManagers/appDocsManager.ts

@ -20,7 +20,7 @@ import assumeType from '../../helpers/assumeType';
import {getEnvironment} from '../../environment/utils'; import {getEnvironment} from '../../environment/utils';
import {isServiceWorkerOnline} from '../mtproto/mtproto.worker'; import {isServiceWorkerOnline} from '../mtproto/mtproto.worker';
import MTProtoMessagePort from '../mtproto/mtprotoMessagePort'; import MTProtoMessagePort from '../mtproto/mtprotoMessagePort';
import getDocumentInput from './utils/docs/getDocumentInput'; import getDocumentInputFileLocation from './utils/docs/getDocumentInputFileLocation';
import getDocumentURL from './utils/docs/getDocumentURL'; import getDocumentURL from './utils/docs/getDocumentURL';
import type {ThumbCache} from '../storages/thumbs'; import type {ThumbCache} from '../storages/thumbs';
import makeError from '../../helpers/makeError'; import makeError from '../../helpers/makeError';
@ -405,6 +405,6 @@ export class AppDocsManager extends AppManager {
public requestDocPart(docId: DocId, dcId: number, offset: number, limit: number) { public requestDocPart(docId: DocId, dcId: number, offset: number, limit: number) {
const doc = this.getDoc(docId); const doc = this.getDoc(docId);
if(!doc) return Promise.reject(makeError('NO_DOC')); 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';
import IS_GROUP_CALL_SUPPORTED from '../../environment/groupCallSupport'; import IS_GROUP_CALL_SUPPORTED from '../../environment/groupCallSupport';
import IS_CALL_SUPPORTED from '../../environment/callSupport'; import IS_CALL_SUPPORTED from '../../environment/callSupport';
import {CallType} from '../calls/types'; import {CallType} from '../calls/types';
import {Modify, SendMessageEmojiInteractionData} from '../../types'; import {Awaited, Modify, SendMessageEmojiInteractionData} from '../../types';
import htmlToSpan from '../../helpers/dom/htmlToSpan'; import htmlToSpan from '../../helpers/dom/htmlToSpan';
import getVisibleRect from '../../helpers/dom/getVisibleRect'; import getVisibleRect from '../../helpers/dom/getVisibleRect';
import {attachClickEvent, simulateClickEvent} from '../../helpers/dom/clickEvent'; import {attachClickEvent, simulateClickEvent} from '../../helpers/dom/clickEvent';
@ -579,12 +579,20 @@ export class AppImManager extends EventListenerBase<{
const doc = await this.managers.appDocsManager.getDoc(docId); const doc = await this.managers.appDocsManager.getDoc(docId);
if(!middleware()) return; if(!middleware()) return;
const {ready, transformer} = await doThatSticker({ let result: Awaited<ReturnType<typeof doThatSticker>>;
doc, try {
mediaContainer, result = await doThatSticker({
middleware, doc,
lockGroups: true mediaContainer,
}); middleware,
lockGroups: true
});
if(!result) return;
} catch(err) {
return;
}
const {ready, transformer} = result;
previousTransformer = transformer; previousTransformer = transformer;
@ -614,12 +622,20 @@ export class AppImManager extends EventListenerBase<{
const doc = await this.managers.appDocsManager.getDoc(docId); const doc = await this.managers.appDocsManager.getDoc(docId);
if(!middleware()) return; if(!middleware()) return;
const {ready, transformer} = await doThatSticker({ let r: Awaited<ReturnType<typeof doThatSticker>>;
doc, try {
mediaContainer, r = await doThatSticker({
middleware, doc,
isSwitching: true mediaContainer,
}); middleware,
isSwitching: true
});
if(!r) return;
} catch(err) {
return;
}
const {ready, transformer} = r;
const _previousTransformer = previousTransformer; const _previousTransformer = previousTransformer;
SetTransition(_previousTransformer, 'is-switching', true, switchDuration, () => { SetTransition(_previousTransformer, 'is-switching', true, switchDuration, () => {
@ -660,6 +676,18 @@ export class AppImManager extends EventListenerBase<{
document.addEventListener('mouseup', onMouseUp, {once: true}); 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) => { apiManagerProxy.addEventListener('notificationBuild', (options) => {
if(this.chat.peerId === options.message.peerId && !idleController.isIdle) { if(this.chat.peerId === options.message.peerId && !idleController.isIdle) {
return; return;

22
src/lib/appManagers/appMessagesManager.ts

@ -61,6 +61,7 @@ import getAlbumText from './utils/messages/getAlbumText';
import pause from '../../helpers/schedulers/pause'; import pause from '../../helpers/schedulers/pause';
import makeError from '../../helpers/makeError'; import makeError from '../../helpers/makeError';
import getStickerEffectThumb from './utils/stickers/getStickerEffectThumb'; import getStickerEffectThumb from './utils/stickers/getStickerEffectThumb';
import getDocumentInput from './utils/docs/getDocumentInput';
// console.trace('include'); // console.trace('include');
// TODO: если удалить диалог находясь в папке, то он не удалится из папки и будет виден в настройках // TODO: если удалить диалог находясь в папке, то он не удалится из папки и будет виден в настройках
@ -682,9 +683,9 @@ export class AppMessagesManager extends AppManager {
size: MediaSize size: MediaSize
}, },
duration: number, duration: number,
background: true, background: boolean,
silent: true, silent: boolean,
clearDraft: true, clearDraft: boolean,
scheduleDate: number, scheduleDate: number,
noSound: boolean, noSound: boolean,
@ -930,16 +931,9 @@ export class AppMessagesManager extends AppManager {
message.send = () => { message.send = () => {
if(isDocument) { if(isDocument) {
const {id, access_hash, file_reference} = file as MyDocument;
const inputMedia: InputMedia = { const inputMedia: InputMedia = {
_: 'inputMediaDocument', _: 'inputMediaDocument',
id: { id: getDocumentInput(file)
_: 'inputDocument',
id,
access_hash,
file_reference
}
}; };
sentDeferred.resolve(inputMedia); sentDeferred.resolve(inputMedia);
@ -1465,10 +1459,10 @@ export class AppMessagesManager extends AppManager {
} */ } */
private beforeMessageSending(message: Message.message, options: Partial<{ private beforeMessageSending(message: Message.message, options: Partial<{
isGroupedItem: true, isGroupedItem: boolean,
isScheduled: true, isScheduled: boolean,
threadId: number, threadId: number,
clearDraft: true, clearDraft: boolean,
sequential: boolean, sequential: boolean,
processAfter?: (cb: () => void) => void processAfter?: (cb: () => void) => void
}> = {}) { }> = {}) {

140
src/lib/appManagers/appStickersManager.ts

@ -5,7 +5,7 @@
*/ */
import type {MyDocument} from './appDocsManager'; 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 {Modify} from '../../types';
import AppStorage from '../storage'; import AppStorage from '../storage';
import DATABASE_STATE from '../../config/databases/state'; import DATABASE_STATE from '../../config/databases/state';
@ -17,6 +17,7 @@ import {AppManager} from './manager';
import fixEmoji from '../richTextProcessor/fixEmoji'; import fixEmoji from '../richTextProcessor/fixEmoji';
import ctx from '../../environment/ctx'; import ctx from '../../environment/ctx';
import {getEnvironment} from '../../environment/utils'; import {getEnvironment} from '../../environment/utils';
import getDocumentInput from './utils/docs/getDocumentInput';
const CACHE_TIME = 3600e3; const CACHE_TIME = 3600e3;
@ -49,6 +50,9 @@ export class AppStickersManager extends AppManager {
private sounds: Record<string, MyDocument>; private sounds: Record<string, MyDocument>;
private getAnimatedEmojiSoundsPromise: Promise<void>; private getAnimatedEmojiSoundsPromise: Promise<void>;
private favedStickers: MyDocument[];
private recentStickers: MyDocument[];
protected after() { protected after() {
this.getStickerSetPromises = {}; this.getStickerSetPromises = {};
this.getStickersByEmoticonsPromises = {}; this.getStickersByEmoticonsPromises = {};
@ -57,6 +61,7 @@ export class AppStickersManager extends AppManager {
this.rootScope.addEventListener('user_auth', () => { this.rootScope.addEventListener('user_auth', () => {
setTimeout(() => { setTimeout(() => {
this.getAnimatedEmojiStickerSet(); this.getAnimatedEmojiStickerSet();
this.getFavedStickersStickers();
}, 1000); }, 1000);
if(!this.getGreetingStickersPromise && this.getGreetingStickersTimeout === undefined) { 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({ this.apiUpdatesManager.addMultipleEventsListeners({
updateNewStickerSet: (update) => { updateNewStickerSet: (update) => {
const stickerSet = update.stickerset as MyMessagesStickerSet; const stickerSet = update.stickerset as MyMessagesStickerSet;
@ -74,11 +81,17 @@ export class AppStickersManager extends AppManager {
this.rootScope.dispatchEvent('stickers_installed', stickerSet.set); this.rootScope.dispatchEvent('stickers_installed', stickerSet.set);
}, },
updateRecentStickers: () => { updateRecentStickers: () => this.onStickersUpdated('recent', true),
this.getRecentStickers().then(({stickers}) => {
this.rootScope.dispatchEvent('stickers_recent', stickers as MyDocument[]); 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) => { processResult: (res) => {
assumeType<MessagesRecentStickers.messagesRecentStickers>(res); assumeType<MessagesRecentStickers.messagesRecentStickers>(res);
this.recentStickers = res.stickers as MyDocument[];
this.saveStickers(res.stickers); this.saveStickers(res.stickers);
return res; return res;
} }
@ -241,6 +255,46 @@ export class AppStickersManager extends AppManager {
return res; 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) { private cleanEmoji(emoji: string) {
return emoji.replace(/\ufe0f/g, '').replace(/🏻|🏼|🏽|🏾|🏿/g, ''); return emoji.replace(/\ufe0f/g, '').replace(/🏻|🏼|🏽|🏾|🏿/g, '');
} }
@ -409,14 +463,64 @@ export class AppStickersManager extends AppManager {
return res.sets; return res.sets;
} }
public async getPromoPremiumStickers() { public getPromoPremiumStickers() {
return this.getStickersByEmoticon('⭐', false); return this.getStickersByEmoticon('⭐', false);
} }
public async getPremiumStickers() { public getPremiumStickers() {
return this.getStickersByEmoticon('📂⭐', false); 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) { public async toggleStickerSet(set: StickerSet.stickerSet) {
set = this.storage.getFromCache(set.id).set; set = this.storage.getFromCache(set.id).set;
@ -578,24 +682,12 @@ export class AppStickersManager extends AppManager {
}); });
} }
public pushRecentSticker(docId: DocId) { public clearRecentStickers() {
const doc = this.appDocsManager.getDoc(docId); if(this.recentStickers) {
const docEmoticon = fixEmoji(doc.stickerEmojiRaw); this.recentStickers.length = 0;
for(const emoticon in this.getStickersByEmoticonsPromises) { this.onStickersUpdated('recent', false);
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() {
this.rootScope.dispatchEvent('stickers_recent', []);
return this.apiManager.invokeApi('messages.clearRecentStickers'); return this.apiManager.invokeApi('messages.clearRecentStickers');
} }
} }

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

@ -6,10 +6,10 @@
import type {Document, PhotoSize, VideoSize} from '../../../../layer'; import type {Document, PhotoSize, VideoSize} from '../../../../layer';
import type {DownloadOptions} from '../../../mtproto/apiFileManager'; 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 { 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; let mimeType: string;
if(thumb?._ === 'photoSize') { if(thumb?._ === 'photoSize') {

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

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

17
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
};
}

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

@ -6,8 +6,8 @@
import {getFileNameByLocation} from '../../../../helpers/fileName'; import {getFileNameByLocation} from '../../../../helpers/fileName';
import {Document} from '../../../../layer'; import {Document} from '../../../../layer';
import getDocumentInput from './getDocumentInput'; import getDocumentInputFileLocation from './getDocumentInputFileLocation';
export default function getDocumentInputFileName(doc: Document.document, thumbSize?: string) { 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 {
protected after() { protected after() {
this.apiUpdatesManager.addMultipleEventsListeners({ this.apiUpdatesManager.addMultipleEventsListeners({
updateConfig: () => { updateConfig: () => {
this.getConfig(); this.getConfig(true);
this.getAppConfig(true); this.getAppConfig(true);
} }
}); });

13
src/lib/mtproto/tl_utils.ts

@ -17,14 +17,17 @@ import isObject from '../../helpers/object/isObject';
import gzipUncompress from '../../helpers/gzipUncompress'; import gzipUncompress from '../../helpers/gzipUncompress';
import bigInt from 'big-integer'; import bigInt from 'big-integer';
import ulongFromInts from '../../helpers/long/ulongFromInts'; import ulongFromInts from '../../helpers/long/ulongFromInts';
import { safeBigInt } from '../../helpers/bigInt/bigIntConstants'; import {safeBigInt} from '../../helpers/bigInt/bigIntConstants';
import { bigIntToSigned, bigIntToUnsigned } from '../../helpers/bigInt/bigIntConversion'; import {bigIntToSigned, bigIntToUnsigned} from '../../helpers/bigInt/bigIntConversion';
const boolFalse = +Schema.API.constructors.find((c) => c.predicate === 'boolFalse').id; const boolFalse = +Schema.API.constructors.find((c) => c.predicate === 'boolFalse').id;
const boolTrue = +Schema.API.constructors.find((c) => c.predicate === 'boolTrue').id; const boolTrue = +Schema.API.constructors.find((c) => c.predicate === 'boolTrue').id;
const vector = +Schema.API.constructors.find((c) => c.predicate === 'vector').id; const vector = +Schema.API.constructors.find((c) => c.predicate === 'vector').id;
const gzipPacked = +Schema.MTProto.constructors.find((c) => c.predicate === 'gzip_packed').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 { class TLSerialization {
private maxLength = 2048; // 2Kb private maxLength = 2048; // 2Kb
private offset = 0; // in bytes private offset = 0; // in bytes
@ -565,7 +568,7 @@ class TLDeserialization<FetchLongAs extends Long> {
(this.byteView[this.offset++] << 16); (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; this.offset += len;
// Padding // Padding
@ -587,7 +590,7 @@ class TLDeserialization<FetchLongAs extends Long> {
const len = bits / 8; const len = bits / 8;
if(typed) { 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; this.offset += len;
return result; return result;
} }
@ -614,7 +617,7 @@ class TLDeserialization<FetchLongAs extends Long> {
if(typed) { if(typed) {
const bytes = new Uint8Array(len); 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; this.offset += len;
return bytes; return bytes;
} }

3
src/lib/rootScope.ts

@ -87,7 +87,8 @@ export type BroadcastEvents = {
'stickers_installed': StickerSet.stickerSet, 'stickers_installed': StickerSet.stickerSet,
'stickers_deleted': 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_cleared': void,
'state_synchronized': ChatId | void, 'state_synchronized': ChatId | void,

6
src/scss/partials/_button.scss

@ -4,6 +4,8 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
$btn-menu-z-index: 4;
.btn, .btn,
.btn-icon { .btn-icon {
background: none; background: none;
@ -97,7 +99,7 @@
visibility: hidden; visibility: hidden;
position: absolute; position: absolute;
background-color: var(--menu-background-color); background-color: var(--menu-background-color);
z-index: 3; z-index: $btn-menu-z-index;
top: 100%; top: 100%;
padding: .3125rem 0; padding: .3125rem 0;
border-radius: $border-radius-medium; border-radius: $border-radius-medium;
@ -324,7 +326,7 @@
right: 0; right: 0;
top: 0; top: 0;
bottom: 0; bottom: 0;
z-index: 3; z-index: $btn-menu-z-index;
cursor: default; cursor: default;
user-select: none; user-select: none;
//background-color: rgba(0, 0, 0, .2); //background-color: rgba(0, 0, 0, .2);

2
src/scss/partials/_emojiDropdown.scss

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

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

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

Loading…
Cancel
Save