Browse Source

Search multiselect

Fix keyboard inputs on iOS
Alt+ArrowUp/ArrowDown shortcut
master
Eduard Kuzmenko 3 years ago
parent
commit
c2604ec14d
  1. 4
      src/components/appMediaViewer.ts
  2. 238
      src/components/appSearchSuper..ts
  3. 23
      src/components/chat/bubbles.ts
  4. 101
      src/components/chat/contextMenu.ts
  5. 66
      src/components/chat/input.ts
  6. 2
      src/components/chat/replyContainer.ts
  7. 938
      src/components/chat/selection.ts
  8. 8
      src/components/chat/topbar.ts
  9. 9
      src/components/popups/forward.ts
  10. 2
      src/components/rangeSelector.ts
  11. 15
      src/components/row.ts
  12. 86
      src/components/sendingStatus.ts
  13. 20
      src/components/sidebarRight/tabs/sharedMedia.ts
  14. 4
      src/components/singleTransition.ts
  15. 77
      src/components/transition.ts
  16. 3
      src/components/wrappers.ts
  17. 2
      src/config/app.ts
  18. 12
      src/helpers/dom/clickEvent.ts
  19. 2
      src/helpers/dom/fixSafariStickyInputFocusing.ts
  20. 1
      src/lang.ts
  21. 29
      src/lib/appManagers/appDialogsManager.ts
  22. 21
      src/lib/appManagers/appImManager.ts
  23. 2
      src/lib/appManagers/appMessagesManager.ts
  24. 1
      src/lib/rootScope.ts
  25. 5
      src/scss/components/_global.scss
  26. 2
      src/scss/partials/_button.scss
  27. 5
      src/scss/partials/_chat.scss
  28. 45
      src/scss/partials/_chatBubble.scss
  29. 25
      src/scss/partials/_chatlist.scss
  30. 14
      src/scss/partials/_checkbox.scss
  31. 14
      src/scss/partials/_ckin.scss
  32. 4
      src/scss/partials/_document.scss
  33. 4
      src/scss/partials/_profile.scss
  34. 183
      src/scss/partials/_rightSidebar.scss
  35. 4
      src/scss/partials/_row.scss
  36. 34
      src/scss/partials/_transition.scss
  37. 26
      src/scss/style.scss

4
src/components/appMediaViewer.ts

@ -1684,7 +1684,9 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
onForwardClick = () => { onForwardClick = () => {
if(this.currentMessageId) { if(this.currentMessageId) {
//appSidebarRight.forwardTab.open([this.currentMessageId]); //appSidebarRight.forwardTab.open([this.currentMessageId]);
new PopupForward(this.currentPeerId, [this.currentMessageId], () => { new PopupForward({
[this.currentPeerId]: [this.currentMessageId]
}, () => {
return this.close(); return this.close();
}); });
} }

238
src/components/appSearchSuper..ts

@ -21,7 +21,7 @@ import AppMediaViewer from "./appMediaViewer";
import { SearchGroup, SearchGroupType } from "./appSearch"; import { SearchGroup, SearchGroupType } from "./appSearch";
import { horizontalMenu } from "./horizontalMenu"; import { horizontalMenu } from "./horizontalMenu";
import LazyLoadQueue from "./lazyLoadQueue"; import LazyLoadQueue from "./lazyLoadQueue";
import { putPreloader } from "./misc"; import { attachContextMenuListener, openBtnMenu, positionMenu, putPreloader } from "./misc";
import { ripple } from "./ripple"; import { ripple } from "./ripple";
import Scrollable, { ScrollableX } from "./scrollable"; import Scrollable, { ScrollableX } from "./scrollable";
import { wrapDocument, wrapPhoto, wrapVideo } from "./wrappers"; import { wrapDocument, wrapPhoto, wrapVideo } from "./wrappers";
@ -42,6 +42,14 @@ import { isTouchSupported } from "../helpers/touchSupport";
import handleTabSwipe from "../helpers/dom/handleTabSwipe"; import handleTabSwipe from "../helpers/dom/handleTabSwipe";
import windowSize from "../helpers/windowSize"; import windowSize from "../helpers/windowSize";
import { formatPhoneNumber } from "../helpers/formatPhoneNumber"; import { formatPhoneNumber } from "../helpers/formatPhoneNumber";
import ButtonMenu, { ButtonMenuItemOptions } from "./buttonMenu";
import PopupForward from "./popups/forward";
import PopupDeleteMessages from "./popups/deleteMessages";
import Row from "./row";
import htmlToDocumentFragment from "../helpers/dom/htmlToDocumentFragment";
import { SearchSelection } from "./chat/selection";
import { cancelEvent } from "../helpers/dom/cancelEvent";
import { attachClickEvent, simulateClickEvent } from "../helpers/dom/clickEvent";
//const testScroll = false; //const testScroll = false;
@ -69,6 +77,149 @@ export type SearchSuperMediaTab = {
scroll?: {scrollTop: number, scrollHeight: number} scroll?: {scrollTop: number, scrollHeight: number}
}; };
class SearchContextMenu {
private buttons: (ButtonMenuItemOptions & {verify?: () => boolean, withSelection?: true})[];
private element: HTMLElement;
private target: HTMLElement;
private peerId: number;
private mid: number;
private isSelected: boolean;
constructor(
private attachTo: HTMLElement,
private searchSuper: AppSearchSuper
) {
const onContextMenu = (e: MouseEvent) => {
if(this.init) {
this.init();
this.init = null;
}
let item: HTMLElement;
try {
item = findUpClassName(e.target, 'search-super-item');
} catch(e) {}
if(!item) return;
if(e instanceof MouseEvent) e.preventDefault();
if(this.element.classList.contains('active')) {
return false;
}
if(e instanceof MouseEvent) e.cancelBubble = true;
this.target = item;
this.peerId = +item.dataset.peerId;
this.mid = +item.dataset.mid;
this.isSelected = searchSuper.selection.isMidSelected(this.peerId, this.mid);
this.buttons.forEach(button => {
let good: boolean;
if(this.isSelected && !button.withSelection) {
good = false;
} else {
good = button.verify ? button.verify() : true;
}
button.element.classList.toggle('hide', !good);
});
item.classList.add('menu-open');
positionMenu(e, this.element);
openBtnMenu(this.element, () => {
item.classList.remove('menu-open');
});
};
if(isTouchSupported) {
} else {
attachContextMenuListener(attachTo, onContextMenu as any);
}
}
private init() {
this.buttons = [{
icon: 'forward',
text: 'Forward',
onClick: this.onForwardClick
}, {
icon: 'forward',
text: 'Message.Context.Selection.Forward',
onClick: this.onForwardClick,
verify: () => this.isSelected &&
!this.searchSuper.selection.selectionForwardBtn.classList.contains('hide'),
withSelection: true
}, {
icon: 'message',
text: 'Message.Context.Goto',
onClick: this.onGotoClick,
withSelection: true
}, {
icon: 'select',
text: 'Message.Context.Select',
onClick: this.onSelectClick
}, {
icon: 'select',
text: 'Message.Context.Selection.Clear',
onClick: this.onClearSelectionClick,
verify: () => this.isSelected,
withSelection: true
}, {
icon: 'delete danger',
text: 'Delete',
onClick: this.onDeleteClick,
verify: () => appMessagesManager.canDeleteMessage(appMessagesManager.getMessageByPeer(this.peerId, this.mid))
}, {
icon: 'delete danger',
text: 'Message.Context.Selection.Delete',
onClick: this.onDeleteClick,
verify: () => this.isSelected && !this.searchSuper.selection.selectionDeleteBtn.classList.contains('hide'),
withSelection: true
}];
this.element = ButtonMenu(this.buttons);
this.element.classList.add('search-contextmenu', 'contextmenu');
document.getElementById('page-chats').append(this.element);
}
private onGotoClick = () => {
rootScope.dispatchEvent('history_focus', {
peerId: this.peerId,
mid: this.mid,
threadId: this.searchSuper.searchContext.threadId
});
};
private onForwardClick = () => {
if(this.searchSuper.selection.isSelecting) {
simulateClickEvent(this.searchSuper.selection.selectionForwardBtn);
} else {
new PopupForward({
[this.peerId]: [this.mid]
});
}
};
private onSelectClick = () => {
this.searchSuper.selection.toggleByElement(this.target);
};
private onClearSelectionClick = () => {
this.searchSuper.selection.cancelSelection();
};
private onDeleteClick = () => {
if(this.searchSuper.selection.isSelecting) {
simulateClickEvent(this.searchSuper.selection.selectionDeleteBtn);
} else {
new PopupDeleteMessages(this.peerId, [this.mid], 'chat');
}
};
}
export default class AppSearchSuper { export default class AppSearchSuper {
public tabs: {[t in SearchSuperType]: HTMLDivElement} = {} as any; public tabs: {[t in SearchSuperType]: HTMLDivElement} = {} as any;
@ -76,8 +227,9 @@ export default class AppSearchSuper {
public container: HTMLElement; public container: HTMLElement;
public nav: HTMLElement; public nav: HTMLElement;
private navScrollableContainer: HTMLDivElement; public navScrollableContainer: HTMLDivElement;
private tabsContainer: HTMLElement; public tabsContainer: HTMLElement;
public navScrollable: ScrollableX;
private tabsMenu: HTMLElement; private tabsMenu: HTMLElement;
private prevTabId = -1; private prevTabId = -1;
@ -88,7 +240,7 @@ export default class AppSearchSuper {
public usedFromHistory: Partial<{[type in SearchSuperType]: number}> = {}; public usedFromHistory: Partial<{[type in SearchSuperType]: number}> = {};
public urlsToRevoke: string[] = []; public urlsToRevoke: string[] = [];
private searchContext: SearchSuperContext; public searchContext: SearchSuperContext;
public loadMutex: Promise<any> = Promise.resolve(); public loadMutex: Promise<any> = Promise.resolve();
private nextRates: Partial<{[type in SearchSuperType]: number}> = {}; private nextRates: Partial<{[type in SearchSuperType]: number}> = {};
@ -127,16 +279,23 @@ export default class AppSearchSuper {
public onChangeTab?: (mediaTab: SearchSuperMediaTab) => void; public onChangeTab?: (mediaTab: SearchSuperMediaTab) => void;
public showSender? = false; public showSender? = false;
private searchContextMenu: SearchContextMenu;
public selection: SearchSelection;
constructor(options: Pick<AppSearchSuper, 'mediaTabs' | 'scrollable' | 'searchGroups' | 'asChatList' | 'groupByMonth' | 'hideEmptyTabs' | 'onChangeTab' | 'showSender'>) { constructor(options: Pick<AppSearchSuper, 'mediaTabs' | 'scrollable' | 'searchGroups' | 'asChatList' | 'groupByMonth' | 'hideEmptyTabs' | 'onChangeTab' | 'showSender'>) {
safeAssign(this, options); safeAssign(this, options);
this.container = document.createElement('div'); this.container = document.createElement('div');
this.container.classList.add('search-super'); this.container.classList.add('search-super');
this.searchContextMenu = new SearchContextMenu(this.container, this);
this.selection = new SearchSelection(this, appMessagesManager);
const navScrollableContainer = this.navScrollableContainer = document.createElement('div'); const navScrollableContainer = this.navScrollableContainer = document.createElement('div');
navScrollableContainer.classList.add('search-super-tabs-scrollable', 'menu-horizontal-scrollable', 'sticky'); navScrollableContainer.classList.add('search-super-tabs-scrollable', 'menu-horizontal-scrollable', 'sticky');
const navScrollable = new ScrollableX(navScrollableContainer); const navScrollable = this.navScrollable = new ScrollableX(navScrollableContainer);
navScrollable.container.classList.add('search-super-nav-scrollable');
const nav = this.nav = document.createElement('nav'); const nav = this.nav = document.createElement('nav');
nav.classList.add('search-super-tabs', 'menu-horizontal-div'); nav.classList.add('search-super-tabs', 'menu-horizontal-div');
@ -285,6 +444,13 @@ export default class AppSearchSuper {
this.onTransitionEnd(); this.onTransitionEnd();
}, undefined, navScrollable); }, undefined, navScrollable);
attachClickEvent(this.tabsContainer, (e) => {
if(this.selection.isSelecting) {
cancelEvent(e);
this.selection.toggleByElement(findUpClassName(e.target, 'search-super-item'));
}
}, {capture: true, passive: false});
const onMediaClick = (className: string, targetClassName: string, inputFilter: MyInputMessagesFilter, e: MouseEvent) => { const onMediaClick = (className: string, targetClassName: string, inputFilter: MyInputMessagesFilter, e: MouseEvent) => {
const target = findUpClassName(e.target as HTMLDivElement, className); const target = findUpClassName(e.target as HTMLDivElement, className);
@ -314,8 +480,20 @@ export default class AppSearchSuper {
.openMedia(message, targets[idx].element, 0, false, targets.slice(0, idx), targets.slice(idx + 1)); .openMedia(message, targets[idx].element, 0, false, targets.slice(0, idx), targets.slice(idx + 1));
}; };
this.tabs.inputMessagesFilterPhotoVideo.addEventListener('click', onMediaClick.bind(null, 'grid-item', 'grid-item', 'inputMessagesFilterPhotoVideo')); attachClickEvent(this.tabs.inputMessagesFilterPhotoVideo, onMediaClick.bind(null, 'grid-item', 'grid-item', 'inputMessagesFilterPhotoVideo'));
this.tabs.inputMessagesFilterDocument.addEventListener('click', onMediaClick.bind(null, 'document-with-thumb', 'media-container', 'inputMessagesFilterDocument')); attachClickEvent(this.tabs.inputMessagesFilterDocument, onMediaClick.bind(null, 'document-with-thumb', 'media-container', 'inputMessagesFilterDocument'));
attachClickEvent(this.tabs.inputMessagesFilterUrl, (e) => {
const target = e.target as HTMLElement;
if(target.tagName === 'A') {
return;
}
try {
const a = findUpClassName(target, 'row').querySelector('.anchor-url:last-child') as HTMLAnchorElement;
a.click();
} catch(err) {}
});
this.mediaTab = this.mediaTabs[0]; this.mediaTab = this.mediaTabs[0];
@ -593,7 +771,7 @@ export default class AppSearchSuper {
let div = document.createElement('div'); let div = document.createElement('div');
let previewDiv = document.createElement('div'); let previewDiv = document.createElement('div');
previewDiv.classList.add('preview'); previewDiv.classList.add('preview', 'row-media');
//this.log('wrapping webpage', webpage); //this.log('wrapping webpage', webpage);
@ -618,7 +796,19 @@ export default class AppSearchSuper {
let title = webpage.rTitle || ''; let title = webpage.rTitle || '';
let subtitle = webpage.rDescription || ''; let subtitle = webpage.rDescription || '';
let url = RichTextProcessor.wrapRichText(webpage.url || '');
const subtitleFragment = htmlToDocumentFragment(subtitle);
const aFragment = htmlToDocumentFragment(RichTextProcessor.wrapRichText(webpage.url || ''));
const a = aFragment.firstElementChild;
if(a instanceof HTMLAnchorElement) {
a.innerText = decodeURIComponent(a.href);
}
if(subtitleFragment.firstChild) {
subtitleFragment.append('\n');
}
subtitleFragment.append(a);
if(!title) { if(!title) {
//title = new URL(webpage.url).hostname; //title = new URL(webpage.url).hostname;
@ -632,18 +822,32 @@ export default class AppSearchSuper {
titleAdditionHTML = `<div class="sent-time">${formatDateAccordingToToday(new Date(message.date * 1000))}</div>`; titleAdditionHTML = `<div class="sent-time">${formatDateAccordingToToday(new Date(message.date * 1000))}</div>`;
} }
const row = new Row({
title,
titleRight: titleAdditionHTML,
subtitle: subtitleFragment,
havePadding: true,
clickable: true,
noRipple: true
});
/* const mediaDiv = document.createElement('div');
mediaDiv.classList.add('row-media'); */
row.container.append(previewDiv);
/* ripple(div);
div.append(previewDiv); div.append(previewDiv);
div.insertAdjacentHTML('beforeend', ` div.insertAdjacentHTML('beforeend', `
<div class="title">${title}${titleAdditionHTML}</div> <div class="title">${title}${titleAdditionHTML}</div>
<div class="subtitle">${subtitle}</div> <div class="subtitle">${subtitle}</div>
<div class="url">${url}</div> <div class="url">${url}</div>
${sender} ${sender}
`); `); */
if(div.innerText.trim().length) { if(row.container.innerText.trim().length) {
elemsToAppend.push({element: div, message}); elemsToAppend.push({element: row.container, message});
} }
} }
break; break;
@ -675,6 +879,10 @@ export default class AppSearchSuper {
element.dataset.mid = '' + message.mid; element.dataset.mid = '' + message.mid;
element.dataset.peerId = '' + message.peerId; element.dataset.peerId = '' + message.peerId;
monthContainer.items[method](element); monthContainer.items[method](element);
if(this.selection.isSelecting) {
this.selection.toggleElementCheckbox(element, true);
}
}); });
} }
@ -1245,6 +1453,10 @@ export default class AppSearchSuper {
this.usedFromHistory[mediaTab.inputFilter] = -1; this.usedFromHistory[mediaTab.inputFilter] = -1;
}); });
if(this.selection.isSelecting) {
this.selection.cancelSelection();
}
// * must go to first tab (это костыль) // * must go to first tab (это костыль)
/* const membersTab = this.mediaTabsMap.get('members'); /* const membersTab = this.mediaTabsMap.get('members');
if(membersTab) { if(membersTab) {

23
src/components/chat/bubbles.ts

@ -439,6 +439,7 @@ export default class ChatBubbles {
}); });
}); });
// attachClickEvent(this.bubblesContainer, this.onBubblesClick, {listenerSetter: this.listenerSetter});
this.listenerSetter.add(this.bubblesContainer)('click', this.onBubblesClick/* , {capture: true, passive: false} */); this.listenerSetter.add(this.bubblesContainer)('click', this.onBubblesClick/* , {capture: true, passive: false} */);
if(DEBUG) { if(DEBUG) {
@ -865,7 +866,7 @@ export default class ChatBubbles {
} }
if(!isTouchSupported && findUpClassName(target, 'time')) { if(!isTouchSupported && findUpClassName(target, 'time')) {
this.chat.selection.toggleByBubble(bubble); this.chat.selection.toggleByElement(bubble);
return; return;
} }
@ -884,7 +885,7 @@ export default class ChatBubbles {
} }
//this.chatSelection.toggleByBubble(bubble); //this.chatSelection.toggleByBubble(bubble);
this.chat.selection.toggleByBubble(findUpClassName(target, 'grouped-item') || bubble); this.chat.selection.toggleByElement(findUpClassName(target, 'grouped-item') || bubble);
return; return;
} }
@ -1076,7 +1077,9 @@ export default class ChatBubbles {
} else if(target.classList.contains('forward')) { } else if(target.classList.contains('forward')) {
const mid = +bubble.dataset.mid; const mid = +bubble.dataset.mid;
const message = this.appMessagesManager.getMessageByPeer(this.peerId, mid); const message = this.appMessagesManager.getMessageByPeer(this.peerId, mid);
new PopupForward(this.peerId, this.appMessagesManager.getMidsByMessage(message)); new PopupForward({
[this.peerId]: this.appMessagesManager.getMidsByMessage(message)
});
//appSidebarRight.forwardTab.open([mid]); //appSidebarRight.forwardTab.open([mid]);
return; return;
} }
@ -1390,7 +1393,7 @@ export default class ChatBubbles {
}); });
if(permanent && this.chat.selection.isSelecting) { if(permanent && this.chat.selection.isSelecting) {
this.chat.selection.deleteSelectedMids(mids); this.chat.selection.deleteSelectedMids(this.peerId, mids);
} }
animationIntersector.checkAnimations(false, CHAT_ANIMATION_GROUP); animationIntersector.checkAnimations(false, CHAT_ANIMATION_GROUP);
@ -2213,6 +2216,7 @@ export default class ChatBubbles {
// ! reset due to album edit or delete item // ! reset due to album edit or delete item
this.bubbles[+message.mid] = bubble; this.bubbles[+message.mid] = bubble;
bubble.dataset.mid = message.mid; bubble.dataset.mid = message.mid;
bubble.dataset.peerId = '' + message.peerId;
bubble.dataset.timestamp = message.date; bubble.dataset.timestamp = message.date;
const loadPromises: Promise<any>[] = []; const loadPromises: Promise<any>[] = [];
@ -2281,7 +2285,7 @@ export default class ChatBubbles {
}); });
let canHaveTail = true; let canHaveTail = true;
let isStandaloneMedia = false;
let needToSetHTML = true; let needToSetHTML = true;
if(totalEntities && !messageMedia) { if(totalEntities && !messageMedia) {
let emojiEntities = totalEntities.filter((e) => e._ === 'messageEntityEmoji'); let emojiEntities = totalEntities.filter((e) => e._ === 'messageEntityEmoji');
@ -2308,6 +2312,7 @@ export default class ChatBubbles {
} }
bubble.classList.add('is-message-empty', 'emoji-big'); bubble.classList.add('is-message-empty', 'emoji-big');
isStandaloneMedia = true;
canHaveTail = false; canHaveTail = false;
needToSetHTML = false; needToSetHTML = false;
} }
@ -2391,7 +2396,9 @@ export default class ChatBubbles {
} }
return new Promise<number>((resolve, reject) => { return new Promise<number>((resolve, reject) => {
new PopupForward(this.peerId, [], (peerId) => { new PopupForward({
[this.peerId]: []
}, (peerId) => {
resolve(peerId); resolve(peerId);
}, () => { }, () => {
reject(); reject();
@ -2468,8 +2475,6 @@ export default class ChatBubbles {
const isOut = our && (!message.fwd_from || this.peerId !== rootScope.myId); const isOut = our && (!message.fwd_from || this.peerId !== rootScope.myId);
let nameContainer: HTMLElement = bubbleContainer; let nameContainer: HTMLElement = bubbleContainer;
let isStandaloneMedia = false;
// media // media
if(messageMedia/* && messageMedia._ === 'messageMediaPhoto' */) { if(messageMedia/* && messageMedia._ === 'messageMediaPhoto' */) {
let attachmentDiv = document.createElement('div'); let attachmentDiv = document.createElement('div');
@ -2856,7 +2861,7 @@ export default class ChatBubbles {
} }
if(this.chat.selection.isSelecting) { if(this.chat.selection.isSelecting) {
this.chat.selection.toggleBubbleCheckbox(bubble, true); this.chat.selection.toggleElementCheckbox(bubble, true);
} }
let savedFrom = ''; let savedFrom = '';

101
src/components/chat/contextMenu.ts

@ -19,11 +19,10 @@ import PopupPinMessage from "../popups/unpinMessage";
import { copyTextToClipboard } from "../../helpers/clipboard"; import { copyTextToClipboard } from "../../helpers/clipboard";
import PopupSendNow from "../popups/sendNow"; import PopupSendNow from "../popups/sendNow";
import { toast } from "../toast"; import { toast } from "../toast";
import I18n, { i18n, LangPackKey } from "../../lib/langPack"; import I18n, { LangPackKey } from "../../lib/langPack";
import findUpClassName from "../../helpers/dom/findUpClassName"; import findUpClassName from "../../helpers/dom/findUpClassName";
import { cancelEvent } from "../../helpers/dom/cancelEvent"; import { cancelEvent } from "../../helpers/dom/cancelEvent";
import cancelSelection from "../../helpers/dom/cancelSelection"; import { attachClickEvent, simulateClickEvent } from "../../helpers/dom/clickEvent";
import { attachClickEvent } from "../../helpers/dom/clickEvent";
import isSelectionEmpty from "../../helpers/dom/isSelectionEmpty"; import isSelectionEmpty from "../../helpers/dom/isSelectionEmpty";
import { Message } from "../../layer"; import { Message } from "../../layer";
import PopupReportMessages from "../popups/reportMessages"; import PopupReportMessages from "../popups/reportMessages";
@ -33,6 +32,7 @@ export default class ChatContextMenu {
private element: HTMLElement; private element: HTMLElement;
private isSelectable: boolean; private isSelectable: boolean;
private isSelected: boolean;
private target: HTMLElement; private target: HTMLElement;
private isTargetAGroupedItem: boolean; private isTargetAGroupedItem: boolean;
private isTextSelected: boolean; private isTextSelected: boolean;
@ -75,17 +75,6 @@ export default class ChatContextMenu {
let mid = +bubble.dataset.mid; let mid = +bubble.dataset.mid;
if(!mid) return; if(!mid) return;
// * если открыть контекстное меню для альбома не по бабблу, и последний элемент не выбран, чтобы показать остальные пункты
if(chat.selection.isSelecting && !contentWrapper) {
const mids = this.chat.getMidsByMid(mid);
if(mids.length > 1) {
const selectedMid = chat.selection.selectedMids.has(mid) ? mid : mids.find(mid => chat.selection.selectedMids.has(mid));
if(selectedMid) {
mid = selectedMid;
}
}
}
this.isSelectable = this.chat.selection.canSelectBubble(bubble); this.isSelectable = this.chat.selection.canSelectBubble(bubble);
this.peerId = this.chat.peerId; this.peerId = this.chat.peerId;
//this.msgID = msgID; //this.msgID = msgID;
@ -97,6 +86,19 @@ export default class ChatContextMenu {
); );
this.isUsernameTarget = this.target.tagName === 'A' && this.target.classList.contains('mention'); this.isUsernameTarget = this.target.tagName === 'A' && this.target.classList.contains('mention');
// * если открыть контекстное меню для альбома не по бабблу, и последний элемент не выбран, чтобы показать остальные пункты
if(chat.selection.isSelecting && !contentWrapper) {
const mids = this.chat.getMidsByMid(mid);
if(mids.length > 1) {
const selectedMid = this.chat.selection.isMidSelected(this.peerId, mid) ?
mid :
mids.find(mid => this.chat.selection.isMidSelected(this.peerId, mid));
if(selectedMid) {
mid = selectedMid;
}
}
}
const groupedItem = findUpClassName(this.target, 'grouped-item'); const groupedItem = findUpClassName(this.target, 'grouped-item');
this.isTargetAGroupedItem = !!groupedItem; this.isTargetAGroupedItem = !!groupedItem;
if(groupedItem) { if(groupedItem) {
@ -105,6 +107,7 @@ export default class ChatContextMenu {
this.mid = mid; this.mid = mid;
} }
this.isSelected = this.chat.selection.isMidSelected(this.peerId, this.mid);
this.message = this.chat.getMessage(this.mid); this.message = this.chat.getMessage(this.mid);
this.buttons.forEach(button => { this.buttons.forEach(button => {
@ -147,29 +150,10 @@ export default class ChatContextMenu {
if(good) { if(good) {
cancelEvent(e); cancelEvent(e);
//onContextMenu((e as TouchEvent).changedTouches[0]); //onContextMenu((e as TouchEvent).changedTouches[0]);
onContextMenu((e as TouchEvent).changedTouches ? (e as TouchEvent).changedTouches[0] : e as MouseEvent); // onContextMenu((e as TouchEvent).changedTouches ? (e as TouchEvent).changedTouches[0] : e as MouseEvent);
onContextMenu(e);
} }
}, {listenerSetter: this.chat.bubbles.listenerSetter}); }, {listenerSetter: this.chat.bubbles.listenerSetter});
attachContextMenuListener(attachTo, (e) => {
if(chat.selection.isSelecting) return;
// * these two lines will fix instant text selection on iOS Safari
document.body.classList.add('no-select'); // * need no-select on body because chat-input transforms in channels
attachTo.addEventListener('touchend', (e) => {
cancelEvent(e); // ! this one will fix propagation to document loader button, etc
document.body.classList.remove('no-select');
//this.chat.bubbles.onBubblesClick(e);
}, {once: true, capture: true});
cancelSelection();
//cancelEvent(e as any);
const bubble = findUpClassName(e.target, 'grouped-item') || findUpClassName(e.target, 'bubble');
if(bubble) {
chat.selection.toggleByBubble(bubble);
}
}, this.chat.bubbles.listenerSetter);
} else attachContextMenuListener(attachTo, onContextMenu, this.chat.bubbles.listenerSetter); } else attachContextMenuListener(attachTo, onContextMenu, this.chat.bubbles.listenerSetter);
} }
@ -183,7 +167,7 @@ export default class ChatContextMenu {
icon: 'send2', icon: 'send2',
text: 'Message.Context.Selection.SendNow', text: 'Message.Context.Selection.SendNow',
onClick: this.onSendScheduledClick, onClick: this.onSendScheduledClick,
verify: () => this.chat.type === 'scheduled' && this.chat.selection.selectedMids.has(this.mid) && !this.chat.selection.selectionSendNowBtn.hasAttribute('disabled'), verify: () => this.chat.type === 'scheduled' && this.isSelected && !this.chat.selection.selectionSendNowBtn.hasAttribute('disabled'),
notDirect: () => true, notDirect: () => true,
withSelection: true withSelection: true
}, { }, {
@ -228,7 +212,21 @@ export default class ChatContextMenu {
icon: 'copy', icon: 'copy',
text: 'Message.Context.Selection.Copy', text: 'Message.Context.Selection.Copy',
onClick: this.onCopyClick, onClick: this.onCopyClick,
verify: () => this.chat.selection.selectedMids.has(this.mid) && !![...this.chat.selection.selectedMids].find(mid => !!this.chat.getMessage(mid).message), verify: () => {
if(!this.isSelected) {
return false;
}
for(const [peerId, mids] of this.chat.selection.selectedMids) {
for(const mid of mids) {
if(!!this.appMessagesManager.getMessageByPeer(peerId, mid).message) {
return true;
}
}
}
return false;
},
notDirect: () => true, notDirect: () => true,
withSelection: true withSelection: true
}, { }, {
@ -319,7 +317,7 @@ export default class ChatContextMenu {
text: 'Message.Context.Selection.Forward', text: 'Message.Context.Selection.Forward',
onClick: this.onForwardClick, onClick: this.onForwardClick,
verify: () => this.chat.selection.selectionForwardBtn && verify: () => this.chat.selection.selectionForwardBtn &&
this.chat.selection.selectedMids.has(this.mid) && this.isSelected &&
!this.chat.selection.selectionForwardBtn.hasAttribute('disabled'), !this.chat.selection.selectionForwardBtn.hasAttribute('disabled'),
notDirect: () => true, notDirect: () => true,
withSelection: true withSelection: true
@ -336,14 +334,14 @@ export default class ChatContextMenu {
icon: 'select', icon: 'select',
text: 'Message.Context.Select', text: 'Message.Context.Select',
onClick: this.onSelectClick, onClick: this.onSelectClick,
verify: () => !this.message.action && !this.chat.selection.selectedMids.has(this.mid) && this.isSelectable, verify: () => !this.message.action && !this.isSelected && this.isSelectable,
notDirect: () => true, notDirect: () => true,
withSelection: true withSelection: true
}, { }, {
icon: 'select', icon: 'select',
text: 'Message.Context.Selection.Clear', text: 'Message.Context.Selection.Clear',
onClick: this.onClearSelectionClick, onClick: this.onClearSelectionClick,
verify: () => this.chat.selection.selectedMids.has(this.mid), verify: () => this.isSelected,
notDirect: () => true, notDirect: () => true,
withSelection: true withSelection: true
}, { }, {
@ -355,7 +353,7 @@ export default class ChatContextMenu {
icon: 'delete danger', icon: 'delete danger',
text: 'Message.Context.Selection.Delete', text: 'Message.Context.Selection.Delete',
onClick: this.onDeleteClick, onClick: this.onDeleteClick,
verify: () => this.chat.selection.selectedMids.has(this.mid) && !this.chat.selection.selectionDeleteBtn.hasAttribute('disabled'), verify: () => this.isSelected && !this.chat.selection.selectionDeleteBtn.hasAttribute('disabled'),
notDirect: () => true, notDirect: () => true,
withSelection: true withSelection: true
}]; }];
@ -364,11 +362,11 @@ export default class ChatContextMenu {
this.element.id = 'bubble-contextmenu'; this.element.id = 'bubble-contextmenu';
this.element.classList.add('contextmenu'); this.element.classList.add('contextmenu');
this.chat.container.append(this.element); this.chat.container.append(this.element);
}; }
private onSendScheduledClick = () => { private onSendScheduledClick = () => {
if(this.chat.selection.isSelecting) { if(this.chat.selection.isSelecting) {
this.chat.selection.selectionSendNowBtn.click(); simulateClickEvent(this.chat.selection.selectionSendNowBtn);
} else { } else {
new PopupSendNow(this.peerId, this.chat.getMidsByMid(this.mid)); new PopupSendNow(this.peerId, this.chat.getMidsByMid(this.mid));
} }
@ -384,11 +382,15 @@ export default class ChatContextMenu {
private onCopyClick = () => { private onCopyClick = () => {
if(isSelectionEmpty()) { if(isSelectionEmpty()) {
const mids = this.chat.selection.isSelecting ? [...this.chat.selection.selectedMids].sort((a, b) => a - b) : [this.mid]; const mids = this.chat.selection.isSelecting ?
[...this.chat.selection.selectedMids.get(this.peerId)].sort((a, b) => a - b) :
[this.mid];
const str = mids.reduce((acc, mid) => { const str = mids.reduce((acc, mid) => {
const message = this.chat.getMessage(mid); const message = this.chat.getMessage(mid);
return acc + (message?.message ? message.message + '\n' : ''); return acc + (message?.message ? message.message + '\n' : '');
}, '').trim(); }, '').trim();
copyTextToClipboard(str); copyTextToClipboard(str);
} else { } else {
document.execCommand('copy'); document.execCommand('copy');
@ -443,14 +445,17 @@ export default class ChatContextMenu {
private onForwardClick = () => { private onForwardClick = () => {
if(this.chat.selection.isSelecting) { if(this.chat.selection.isSelecting) {
this.chat.selection.selectionForwardBtn.click(); simulateClickEvent(this.chat.selection.selectionForwardBtn);
} else { } else {
new PopupForward(this.peerId, this.isTargetAGroupedItem ? [this.mid] : this.chat.getMidsByMid(this.mid)); const mids = this.isTargetAGroupedItem ? [this.mid] : this.chat.getMidsByMid(this.mid);
new PopupForward({
[this.peerId]: mids
});
} }
}; };
private onSelectClick = () => { private onSelectClick = () => {
this.chat.selection.toggleByBubble(findUpClassName(this.target, 'grouped-item') || findUpClassName(this.target, 'bubble')); this.chat.selection.toggleByElement(findUpClassName(this.target, 'grouped-item') || findUpClassName(this.target, 'bubble'));
}; };
private onClearSelectionClick = () => { private onClearSelectionClick = () => {
@ -459,7 +464,7 @@ export default class ChatContextMenu {
private onDeleteClick = () => { private onDeleteClick = () => {
if(this.chat.selection.isSelecting) { if(this.chat.selection.isSelecting) {
this.chat.selection.selectionDeleteBtn.click(); simulateClickEvent(this.chat.selection.selectionDeleteBtn);
} else { } else {
new PopupDeleteMessages(this.peerId, this.isTargetAGroupedItem ? [this.mid] : this.chat.getMidsByMid(this.mid), this.chat.type); new PopupDeleteMessages(this.peerId, this.isTargetAGroupedItem ? [this.mid] : this.chat.getMidsByMid(this.mid), this.chat.type);
} }

66
src/components/chat/input.ts

@ -77,6 +77,7 @@ import PeerTitle from '../peerTitle';
import { fastRaf } from '../../helpers/schedulers'; import { fastRaf } from '../../helpers/schedulers';
import PopupDeleteMessages from '../popups/deleteMessages'; import PopupDeleteMessages from '../popups/deleteMessages';
import fixSafariStickyInputFocusing, { IS_STICKY_INPUT_BUGGED } from '../../helpers/dom/fixSafariStickyInputFocusing'; import fixSafariStickyInputFocusing, { IS_STICKY_INPUT_BUGGED } from '../../helpers/dom/fixSafariStickyInputFocusing';
import { copy } from '../../helpers/object';
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.';
@ -117,8 +118,7 @@ export default class ChatInput {
private getWebPagePromise: Promise<void>; private getWebPagePromise: Promise<void>;
private willSendWebPage: WebPage = null; private willSendWebPage: WebPage = null;
private forwardingMids: number[] = []; private forwarding: {[fromPeerId: number]: number[]};
private forwardingFromPeerId: number = 0;
public replyToMsgId: number; public replyToMsgId: number;
public editMsgId: number; public editMsgId: number;
private noWebPage: true; private noWebPage: true;
@ -1402,7 +1402,7 @@ export default class ChatInput {
private onBtnSendClick = (e: Event) => { private onBtnSendClick = (e: Event) => {
cancelEvent(e); cancelEvent(e);
if(!this.recorder || this.recording || !this.isInputEmpty() || this.forwardingMids.length || this.editMsgId) { if(!this.recorder || this.recording || !this.isInputEmpty() || this.forwarding || this.editMsgId) {
if(this.recording) { if(this.recording) {
if((Date.now() - this.recordStartTime) < RECORD_MIN_TIME) { if((Date.now() - this.recordStartTime) < RECORD_MIN_TIME) {
this.onCancelRecordClick(); this.onCancelRecordClick();
@ -1526,13 +1526,11 @@ export default class ChatInput {
if(this.helperWaitingForward) return; if(this.helperWaitingForward) return;
this.helperWaitingForward = true; this.helperWaitingForward = true;
const fromId = this.forwardingFromPeerId;
const mids = this.forwardingMids.slice();
const helperFunc = this.helperFunc; const helperFunc = this.helperFunc;
this.clearHelper(); this.clearHelper();
this.updateSendBtn(); this.updateSendBtn();
let selected = false; let selected = false;
new PopupForward(fromId, mids, () => { new PopupForward(copy(this.forwarding), () => {
selected = true; selected = true;
}, () => { }, () => {
this.helperWaitingForward = false; this.helperWaitingForward = false;
@ -1593,7 +1591,7 @@ export default class ChatInput {
const isInputEmpty = this.isInputEmpty(); const isInputEmpty = this.isInputEmpty();
if(this.editMsgId) icon = 'edit'; if(this.editMsgId) icon = 'edit';
else if(!this.recorder || this.recording || !isInputEmpty || this.forwardingMids.length) icon = this.chat.type === 'scheduled' ? 'schedule' : 'send'; else if(!this.recorder || this.recording || !isInputEmpty || this.forwarding) icon = this.chat.type === 'scheduled' ? 'schedule' : 'send';
else icon = 'record'; else icon = 'record';
['send', 'record', 'edit', 'schedule'].forEach(i => { ['send', 'record', 'edit', 'schedule'].forEach(i => {
@ -1673,17 +1671,18 @@ export default class ChatInput {
} }
// * wait for sendText set messageId for invokeAfterMsg // * wait for sendText set messageId for invokeAfterMsg
if(this.forwardingMids.length) { if(this.forwarding) {
const mids = this.forwardingMids.slice(); const forwarding = copy(this.forwarding);
const fromPeerId = this.forwardingFromPeerId;
const peerId = this.chat.peerId; const peerId = this.chat.peerId;
const silent = this.sendSilent; const silent = this.sendSilent;
const scheduleDate = this.scheduleDate; const scheduleDate = this.scheduleDate;
setTimeout(() => { setTimeout(() => {
this.appMessagesManager.forwardMessages(peerId, fromPeerId, mids, { for(const fromPeerId in forwarding) {
this.appMessagesManager.forwardMessages(peerId, +fromPeerId, forwarding[fromPeerId], {
silent, silent,
scheduleDate: scheduleDate scheduleDate: scheduleDate
}); });
}
}, 0); }, 0);
} }
@ -1751,17 +1750,26 @@ export default class ChatInput {
f(); f();
} }
public initMessagesForward(fromPeerId: number, mids: number[]) { public initMessagesForward(fromPeerIdsMids: {[fromPeerId: number]: number[]}) {
const f = () => { const f = () => {
//const peerTitles: string[] //const peerTitles: string[]
const smth: Set<string | number> = new Set(mids.map(mid => { const fromPeerIds = Object.keys(fromPeerIdsMids).map(str => +str);
const smth: Set<string | number> = new Set();
let length = 0;
fromPeerIds.forEach(fromPeerId => {
const mids = fromPeerIdsMids[fromPeerId];
mids.forEach(mid => {
const message = this.appMessagesManager.getMessageByPeer(fromPeerId, mid); const message = this.appMessagesManager.getMessageByPeer(fromPeerId, mid);
if(message.fwd_from && message.fwd_from.from_name && !message.fromId && !message.fwdFromId) { if(message.fwd_from?.from_name && !message.fromId && !message.fwdFromId) {
return message.fwd_from.from_name; smth.add(message.fwd_from.from_name);
} else { } else {
return message.fromId; smth.add(message.fromId);
} }
})); });
length += mids.length;
});
const onlyFirstName = smth.size > 2; const onlyFirstName = smth.size > 2;
const peerTitles = [...smth].map(smth => { const peerTitles = [...smth].map(smth => {
@ -1777,25 +1785,30 @@ export default class ChatInput {
title.append(peerTitles[0], i18n('AndOther', [peerTitles.length - 1])); title.append(peerTitles[0], i18n('AndOther', [peerTitles.length - 1]));
} }
const firstMessage = this.appMessagesManager.getMessageByPeer(fromPeerId, mids[0]); let firstMessage: any, usingFullAlbum: boolean;
if(fromPeerIds.length === 1) {
const fromPeerId = fromPeerIds[0];
const mids = fromPeerIdsMids[fromPeerId];
firstMessage = this.appMessagesManager.getMessageByPeer(fromPeerId, mids[0]);
let usingFullAlbum = !!firstMessage.grouped_id; usingFullAlbum = !!firstMessage.grouped_id;
if(firstMessage.grouped_id) { if(usingFullAlbum) {
const albumMids = this.appMessagesManager.getMidsByMessage(firstMessage); const albumMids = this.appMessagesManager.getMidsByMessage(firstMessage);
if(albumMids.length !== mids.length || albumMids.find(mid => !mids.includes(mid))) { if(albumMids.length !== length || albumMids.find(mid => !mids.includes(mid))) {
usingFullAlbum = false; usingFullAlbum = false;
} }
} }
}
if(usingFullAlbum || length === 1) {
const mids = fromPeerIdsMids[fromPeerIds[0]];
const replyFragment = this.appMessagesManager.wrapMessageForReply(firstMessage, undefined, mids); const replyFragment = this.appMessagesManager.wrapMessageForReply(firstMessage, undefined, mids);
if(usingFullAlbum || mids.length === 1) {
this.setTopInfo('forward', f, title, replyFragment); this.setTopInfo('forward', f, title, replyFragment);
} else { } else {
this.setTopInfo('forward', f, title, i18n('ForwardedMessageCount', [mids.length])); this.setTopInfo('forward', f, title, i18n('ForwardedMessageCount', [length]));
} }
this.forwardingMids = mids.slice(); this.forwarding = fromPeerIdsMids;
this.forwardingFromPeerId = fromPeerId;
}; };
f(); f();
@ -1845,8 +1858,7 @@ export default class ChatInput {
} }
this.replyToMsgId = undefined; this.replyToMsgId = undefined;
this.forwardingMids.length = 0; this.forwarding = undefined;
this.forwardingFromPeerId = 0;
this.editMsgId = undefined; this.editMsgId = undefined;
this.helperType = this.helperFunc = undefined; this.helperType = this.helperFunc = undefined;

2
src/components/chat/replyContainer.ts

@ -52,7 +52,7 @@ export function wrapReplyDivAndCaption(options: {
media = media.webpage; media = media.webpage;
} }
if(media.photo || (media.document && ['video', 'sticker', 'gif', 'round'].indexOf(media.document.type) !== -1)) { if(media.photo || (media.document && ['video', 'sticker', 'gif', 'round', 'photo'].indexOf(media.document.type) !== -1)) {
middleware = appImManager.chat.bubbles.getMiddleware(); middleware = appImManager.chat.bubbles.getMiddleware();
const lazyLoadQueue = appImManager.chat.bubbles.lazyLoadQueue; const lazyLoadQueue = appImManager.chat.bubbles.lazyLoadQueue;

938
src/components/chat/selection.ts

File diff suppressed because it is too large Load Diff

8
src/components/chat/topbar.ts

@ -249,13 +249,13 @@ export default class ChatTopbar {
return; return;
} }
const original = selection.toggleByBubble.bind(selection); const original = selection.toggleByElement.bind(selection);
selection.toggleByBubble = (bubble) => { selection.toggleByElement = (bubble) => {
appStateManager.pushToState('chatContextMenuHintWasShown', true); appStateManager.pushToState('chatContextMenuHintWasShown', true);
toast(i18n('Chat.Menu.Hint')); toast(i18n('Chat.Menu.Hint'));
selection.toggleByBubble = original; selection.toggleByElement = original;
selection.toggleByBubble(bubble); selection.toggleByElement(bubble);
}; };
}); });
}, },

9
src/components/popups/forward.ts

@ -8,7 +8,12 @@ import appImManager from "../../lib/appManagers/appImManager";
import PopupPickUser from "./pickUser"; import PopupPickUser from "./pickUser";
export default class PopupForward extends PopupPickUser { export default class PopupForward extends PopupPickUser {
constructor(fromPeerId: number, mids: number[], onSelect?: (peerId: number) => Promise<void> | void, onClose?: () => void, overrideOnSelect = false) { constructor(
peerIdMids: {[fromPeerId: number]: number[]},
onSelect?: (peerId: number) => Promise<void> | void,
onClose?: () => void,
overrideOnSelect = false
) {
super({ super({
peerTypes: ['dialogs', 'contacts'], peerTypes: ['dialogs', 'contacts'],
onSelect: overrideOnSelect ? onSelect : async(peerId) => { onSelect: overrideOnSelect ? onSelect : async(peerId) => {
@ -20,7 +25,7 @@ export default class PopupForward extends PopupPickUser {
} }
appImManager.setInnerPeer(peerId); appImManager.setInnerPeer(peerId);
appImManager.chat.input.initMessagesForward(fromPeerId, mids.slice()); appImManager.chat.input.initMessagesForward(peerIdMids);
}, },
onClose, onClose,
placeholder: 'ShareModal.Search.ForwardPlaceholder', placeholder: 'ShareModal.Search.ForwardPlaceholder',

2
src/components/rangeSelector.ts

@ -73,11 +73,13 @@ export default class RangeSelector {
this.rect = this.container.getBoundingClientRect(); this.rect = this.container.getBoundingClientRect();
this.mousedown = true; this.mousedown = true;
this.scrub(event); this.scrub(event);
this.container.classList.add('is-focused');
this.events?.onMouseDown && this.events.onMouseDown(event); this.events?.onMouseDown && this.events.onMouseDown(event);
}; };
protected onMouseUp = (event: GrabEvent) => { protected onMouseUp = (event: GrabEvent) => {
this.mousedown = false; this.mousedown = false;
this.container.classList.remove('is-focused');
this.events?.onMouseUp && this.events.onMouseUp(event); this.events?.onMouseUp && this.events.onMouseUp(event);
}; };

15
src/components/row.ts

@ -11,6 +11,7 @@ import { SliderSuperTab } from "./slider";
import RadioForm from "./radioForm"; import RadioForm from "./radioForm";
import { i18n, LangPackKey } from "../lib/langPack"; import { i18n, LangPackKey } from "../lib/langPack";
import replaceContent from "../helpers/dom/replaceContent"; import replaceContent from "../helpers/dom/replaceContent";
import setInnerHTML from "../helpers/dom/setInnerHTML";
export default class Row { export default class Row {
public container: HTMLElement; public container: HTMLElement;
@ -24,7 +25,7 @@ export default class Row {
constructor(options: Partial<{ constructor(options: Partial<{
icon: string, icon: string,
subtitle: string, subtitle: string | HTMLElement | DocumentFragment,
subtitleLangKey: LangPackKey, subtitleLangKey: LangPackKey,
subtitleLangArgs: any[], subtitleLangArgs: any[],
radioField: Row['radioField'], radioField: Row['radioField'],
@ -35,7 +36,8 @@ export default class Row {
titleRight: string | HTMLElement, titleRight: string | HTMLElement,
clickable: boolean | ((e: Event) => void), clickable: boolean | ((e: Event) => void),
navigationTab: SliderSuperTab, navigationTab: SliderSuperTab,
havePadding: boolean havePadding: boolean,
noRipple: boolean
}> = {}) { }> = {}) {
this.container = document.createElement(options.radioField || options.checkboxField ? 'label' : 'div'); this.container = document.createElement(options.radioField || options.checkboxField ? 'label' : 'div');
this.container.classList.add('row'); this.container.classList.add('row');
@ -44,7 +46,11 @@ export default class Row {
this.subtitle.classList.add('row-subtitle'); this.subtitle.classList.add('row-subtitle');
this.subtitle.setAttribute('dir', 'auto'); this.subtitle.setAttribute('dir', 'auto');
if(options.subtitle) { if(options.subtitle) {
this.subtitle.innerHTML = options.subtitle; if(typeof(options.subtitle) === 'string') {
setInnerHTML(this.subtitle, options.subtitle);
} else {
this.subtitle.append(options.subtitle);
}
} else if(options.subtitleLangKey) { } else if(options.subtitleLangKey) {
this.subtitle.append(i18n(options.subtitleLangKey, options.subtitleLangArgs)); this.subtitle.append(i18n(options.subtitleLangKey, options.subtitleLangArgs));
} }
@ -137,7 +143,10 @@ export default class Row {
} }
this.container.classList.add('row-clickable', 'hover-effect'); this.container.classList.add('row-clickable', 'hover-effect');
if(!options.noRipple) {
ripple(this.container, undefined, undefined, true); ripple(this.container, undefined, undefined, true);
}
/* if(options.radioField || options.checkboxField) { /* if(options.radioField || options.checkboxField) {
this.container.prepend(this.container.lastElementChild); this.container.prepend(this.container.lastElementChild);

86
src/components/sendingStatus.ts

@ -0,0 +1,86 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { Message } from "../layer";
/* import findUpClassName from "../helpers/dom/findUpClassName";
import rootScope from "../lib/rootScope";
import Transition from "./transition"; */
export enum SENDING_STATUS {
Error = -1,
Pending,
Sent,
Read
}
export function getSendingStatus(message: Message.message | Message.messageService) {
return message.pFlags.is_outgoing ?
SENDING_STATUS.Pending : (
message.pFlags.unread ?
SENDING_STATUS.Sent :
SENDING_STATUS.Read
);
}
export function setSendingStatus(
container: HTMLElement,
message?: Message.message | Message.messageService,
disableAnimationIfRippleFound?: boolean
) {
let className: 'check' | 'checks' | 'sending';
if(message?.pFlags.out) {
if(message.pFlags.is_outgoing) {
className = 'sending';
} else if(message.pFlags.unread) {
className = 'check';
} else {
className = 'checks';
}
}
if(!className) {
container.textContent = '';
return;
}
const iconClassName = 'tgico-' + className;
const lastElement = container.lastElementChild as HTMLElement;
if(lastElement && lastElement.classList.contains(iconClassName)) {
return;
}
const element = document.createElement('i');
element.classList.add('sending-status-icon', /* 'transition-item', */ iconClassName);
container.append(element);
if(lastElement) {
lastElement.remove();
}
/* if(!lastElement) {
element.classList.add('active');
return;
}
const select = Transition(container, undefined, 350, () => {
lastElement.remove();
}, false, true, false);
let animate = rootScope.settings.animationsEnabled && className !== 'sending' && !lastElement.classList.contains('tgico-sending');
if(disableAnimationIfRippleFound && animate) {
const parent = findUpClassName(container, 'rp');
if(parent.querySelector('.c-ripple__circle') || parent.matches(':hover')) {
animate = false;
}
}
select(element, animate, lastElement); */
/* SetTransition(lastElement, 'is-visible', false, 350, () => {
// lastElement.remove();
}, 2);
SetTransition(element, 'is-visible', true, 350, undefined, 2); */
}

20
src/components/sidebarRight/tabs/sharedMedia.ts

@ -245,7 +245,8 @@ class PeerProfileAvatars {
const rect = this.container.getBoundingClientRect(); const rect = this.container.getBoundingClientRect();
const e = (_e as TouchEvent).touches ? (_e as TouchEvent).touches[0] : _e as MouseEvent; // const e = (_e as TouchEvent).touches ? (_e as TouchEvent).touches[0] : _e as MouseEvent;
const e = _e;
const x = e.pageX; const x = e.pageX;
const clickX = x - rect.left; const clickX = x - rect.left;
@ -1055,16 +1056,15 @@ export default class AppSharedMediaTab extends SliderSuperTab {
const inputFilter = mediaTab.inputFilter; const inputFilter = mediaTab.inputFilter;
const filtered = this.searchSuper.filterMessagesByType(mids.map(mid => appMessagesManager.getMessageByPeer(peerId, mid)), inputFilter); const filtered = this.searchSuper.filterMessagesByType(mids.map(mid => appMessagesManager.getMessageByPeer(peerId, mid)), inputFilter);
if(filtered.length) { if(filtered.length) {
if(this.historiesStorage[peerId][inputFilter]) { const history = this.historiesStorage[peerId][inputFilter];
this.historiesStorage[peerId][inputFilter].unshift(...filtered.map(message => ({mid: message.mid, peerId: message.peerId}))); if(history) {
history.unshift(...filtered.map(message => ({mid: message.mid, peerId: message.peerId})));
} }
if(this.peerId === peerId && this.searchSuper.usedFromHistory[inputFilter] !== -1) { if(this.peerId === peerId && this.searchSuper.usedFromHistory[inputFilter] !== -1) {
this.searchSuper.usedFromHistory[inputFilter] += filtered.length; this.searchSuper.usedFromHistory[inputFilter] += filtered.length;
this.searchSuper.performSearchResult(filtered, mediaTab, false); this.searchSuper.performSearchResult(filtered, mediaTab, false);
} }
break;
} }
} }
} }
@ -1078,17 +1078,21 @@ export default class AppSharedMediaTab extends SliderSuperTab {
for(const type of this.searchSuper.mediaTabs) { for(const type of this.searchSuper.mediaTabs) {
const inputFilter = type.inputFilter; const inputFilter = type.inputFilter;
if(!this.historiesStorage[peerId][inputFilter]) continue;
const history = this.historiesStorage[peerId][inputFilter]; const history = this.historiesStorage[peerId][inputFilter];
if(!history) continue;
const idx = history.findIndex(m => m.mid === mid); const idx = history.findIndex(m => m.mid === mid);
if(idx !== -1) { if(idx !== -1) {
history.splice(idx, 1); history.splice(idx, 1);
if(this.peerId === peerId) { if(this.peerId === peerId) {
const container = this.searchSuper.tabs[inputFilter]; const container = this.searchSuper.tabs[inputFilter];
const div = container.querySelector(`div[data-mid="${mid}"][data-peer-id="${peerId}"]`); const div = container.querySelector(`div[data-mid="${mid}"][data-peer-id="${peerId}"]`) as HTMLElement;
if(div) { if(div) {
if(this.searchSuper.selection.isSelecting) {
this.searchSuper.selection.toggleByElement(div);
}
div.remove(); div.remove();
} }

4
src/components/singleTransition.ts

@ -26,6 +26,10 @@ const SetTransition = (
} }
} }
// if(forwards && className && element.classList.contains(className) && !element.classList.contains('animating')) {
// return;
// }
if(useRafs && rootScope.settings.animationsEnabled && duration) { if(useRafs && rootScope.settings.animationsEnabled && duration) {
element.dataset.raf = '' + window.requestAnimationFrame(() => { element.dataset.raf = '' + window.requestAnimationFrame(() => {
delete element.dataset.raf; delete element.dataset.raf;

77
src/components/transition.ts

@ -10,6 +10,7 @@ import { dispatchHeavyAnimationEvent } from "../hooks/useHeavyAnimationCheck";
import whichChild from "../helpers/dom/whichChild"; import whichChild from "../helpers/dom/whichChild";
import findUpClassName from "../helpers/dom/findUpClassName"; import findUpClassName from "../helpers/dom/findUpClassName";
import { isSafari } from "../helpers/userAgent"; import { isSafari } from "../helpers/userAgent";
import { cancelEvent } from "../helpers/dom/cancelEvent";
function slideNavigation(tabContent: HTMLElement, prevTabContent: HTMLElement, toRight: boolean) { function slideNavigation(tabContent: HTMLElement, prevTabContent: HTMLElement, toRight: boolean) {
const width = prevTabContent.getBoundingClientRect().width; const width = prevTabContent.getBoundingClientRect().width;
@ -80,7 +81,13 @@ function slideTabs(tabContent: HTMLElement, prevTabContent: HTMLElement, toRight
}; };
} }
export const TransitionSlider = (content: HTMLElement, type: 'tabs' | 'navigation' | 'zoom-fade' | 'slide-fade' | 'none'/* | 'counter' */, transitionTime: number, onTransitionEnd?: (id: number) => void, isHeavy = true) => { export const TransitionSlider = (
content: HTMLElement,
type: 'tabs' | 'navigation' | 'zoom-fade' | 'slide-fade' | 'none'/* | 'counter' */,
transitionTime: number,
onTransitionEnd?: (id: number) => void,
isHeavy = true
) => {
let animationFunction: TransitionFunction = null; let animationFunction: TransitionFunction = null;
switch(type) { switch(type) {
@ -101,14 +108,26 @@ export const TransitionSlider = (content: HTMLElement, type: 'tabs' | 'navigatio
type TransitionFunction = (tabContent: HTMLElement, prevTabContent: HTMLElement, toRight: boolean) => void | (() => void); type TransitionFunction = (tabContent: HTMLElement, prevTabContent: HTMLElement, toRight: boolean) => void | (() => void);
const Transition = (content: HTMLElement, animationFunction: TransitionFunction, transitionTime: number, onTransitionEnd?: (id: number) => void, isHeavy = true) => { const Transition = (
content: HTMLElement,
animationFunction: TransitionFunction,
transitionTime: number,
onTransitionEnd?: (id: number) => void,
isHeavy = true,
once = false,
withAnimationListener = true
) => {
const onTransitionEndCallbacks: Map<HTMLElement, Function> = new Map(); const onTransitionEndCallbacks: Map<HTMLElement, Function> = new Map();
let animationDeferred: CancellablePromise<void>; let animationDeferred: CancellablePromise<void>;
let animationStarted = 0; // let animationStarted = 0;
let from: HTMLElement = null; let from: HTMLElement = null;
// TODO: check for transition type (transform, etc) using by animationFunction if(withAnimationListener) {
content.addEventListener(animationFunction ? 'transitionend' : 'animationend', (e) => { const listenerName = animationFunction ? 'transitionend' : 'animationend';
const onEndEvent = (e: TransitionEvent | AnimationEvent) => {
cancelEvent(e);
if((e.target as HTMLElement).parentElement !== content) { if((e.target as HTMLElement).parentElement !== content) {
return; return;
} }
@ -134,29 +153,49 @@ const Transition = (content: HTMLElement, animationFunction: TransitionFunction,
} }
content.classList.remove('animating', 'backwards', 'disable-hover'); content.classList.remove('animating', 'backwards', 'disable-hover');
});
function selectTab(id: number | HTMLElement, animate = true) { if(once) {
const self = selectTab; content.removeEventListener(listenerName, onEndEvent/* , {capture: false} */);
from = animationDeferred = undefined;
onTransitionEndCallbacks.clear();
}
};
// TODO: check for transition type (transform, etc) using by animationFunction
content.addEventListener(listenerName, onEndEvent/* , {passive: true, capture: false} */);
}
function selectTab(id: number | HTMLElement, animate = true, overrideFrom?: typeof from) {
if(overrideFrom) {
from = overrideFrom;
}
if(id instanceof HTMLElement) { if(id instanceof HTMLElement) {
id = whichChild(id); id = whichChild(id);
} }
const prevId = self.prevId(); const prevId = selectTab.prevId();
if(id === prevId) return false; if(id === prevId) return false;
//console.log('selectTab id:', id); //console.log('selectTab id:', id);
const _from = from;
const to = content.children[id] as HTMLElement; const to = content.children[id] as HTMLElement;
if(!rootScope.settings.animationsEnabled || prevId === -1) { if(!rootScope.settings.animationsEnabled || prevId === -1) {
animate = false; animate = false;
} }
if(!withAnimationListener) {
const timeout = content.dataset.timeout;
if(timeout !== undefined) {
clearTimeout(+timeout);
}
delete content.dataset.timeout;
}
if(!animate) { if(!animate) {
if(_from) _from.classList.remove('active', 'to', 'from'); if(from) from.classList.remove('active', 'to', 'from');
if(to) { if(to) {
to.classList.remove('to', 'from'); to.classList.remove('to', 'from');
to.classList.add('active'); to.classList.add('active');
@ -170,12 +209,21 @@ const Transition = (content: HTMLElement, animationFunction: TransitionFunction,
return; return;
} }
if(!withAnimationListener) {
content.dataset.timeout = '' + window.setTimeout(() => {
to.classList.remove('to');
from && from.classList.remove('from');
content.classList.remove('animating', 'backwards', 'disable-hover');
delete content.dataset.timeout;
}, transitionTime);
}
if(from) { if(from) {
from.classList.remove('to'); from.classList.remove('to');
from.classList.add('from'); from.classList.add('from');
} }
content.classList.add('animating', 'disable-hover'); content.classList.add('animating'/* , 'disable-hover' */);
const toRight = prevId < id; const toRight = prevId < id;
content.classList.toggle('backwards', !toRight); content.classList.toggle('backwards', !toRight);
@ -200,7 +248,8 @@ const Transition = (content: HTMLElement, animationFunction: TransitionFunction,
}); });
} }
if(_from/* && false */) { if(from/* && false */) {
const _from = from;
const callback = () => { const callback = () => {
_from.classList.remove('active', 'from'); _from.classList.remove('active', 'from');
@ -223,7 +272,7 @@ const Transition = (content: HTMLElement, animationFunction: TransitionFunction,
if(isHeavy) { if(isHeavy) {
if(!animationDeferred) { if(!animationDeferred) {
animationDeferred = deferredPromise<void>(); animationDeferred = deferredPromise<void>();
animationStarted = performance.now(); // animationStarted = performance.now();
} }
dispatchHeavyAnimationEvent(animationDeferred, transitionTime * 2); dispatchHeavyAnimationEvent(animationDeferred, transitionTime * 2);

3
src/components/wrappers.ts

@ -550,7 +550,7 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS
icoDiv.classList.add('document-ico'); icoDiv.classList.add('document-ico');
const cacheContext = appDownloadManager.getCacheContext(doc); const cacheContext = appDownloadManager.getCacheContext(doc);
if(doc.thumbs?.length || (message.pFlags.is_outgoing && cacheContext.url && doc.type === 'photo')) { if((doc.thumbs?.length || (message.pFlags.is_outgoing && cacheContext.url && doc.type === 'photo')) && doc.mime_type !== 'image/gif') {
docDiv.classList.add('document-with-thumb'); docDiv.classList.add('document-with-thumb');
let imgs: HTMLImageElement[] = []; let imgs: HTMLImageElement[] = [];
@ -1564,6 +1564,7 @@ export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble,
const container = document.createElement('div'); const container = document.createElement('div');
container.classList.add('document-container'); container.classList.add('document-container');
container.dataset.mid = '' + mid; container.dataset.mid = '' + mid;
container.dataset.peerId = '' + message.peerId;
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.classList.add('document-wrapper'); wrapper.classList.add('document-wrapper');

2
src/config/app.ts

@ -16,7 +16,7 @@ export const MAIN_DOMAIN = 'web.telegram.org';
const App = { const App = {
id: 1025907, id: 1025907,
hash: '452b0359b988148995f22ff0f4229750', hash: '452b0359b988148995f22ff0f4229750',
version: '0.8.2', version: '0.8.3',
langPackVersion: '0.3.3', langPackVersion: '0.3.3',
langPack: 'macos', langPack: 'macos',
langPackCode: 'en', langPackCode: 'en',

12
src/helpers/dom/clickEvent.ts

@ -8,9 +8,9 @@ import type ListenerSetter from "../listenerSetter";
import { isTouchSupported } from "../touchSupport"; import { isTouchSupported } from "../touchSupport";
import simulateEvent from "./dispatchEvent"; import simulateEvent from "./dispatchEvent";
export const CLICK_EVENT_NAME: 'mousedown' | 'touchend' | 'click' = (isTouchSupported ? 'mousedown' : 'click') as any; export const CLICK_EVENT_NAME: 'mousedown' /* | 'touchend' */ | 'click' = (isTouchSupported ? 'mousedown' : 'click') as any;
export type AttachClickOptions = AddEventListenerOptions & Partial<{listenerSetter: ListenerSetter, touchMouseDown: true}>; export type AttachClickOptions = AddEventListenerOptions & Partial<{listenerSetter: ListenerSetter, touchMouseDown: true}>;
export function attachClickEvent(elem: HTMLElement, callback: (e: TouchEvent | MouseEvent) => void, options: AttachClickOptions = {}) { export function attachClickEvent(elem: HTMLElement | Window, callback: (e: /* TouchEvent | */MouseEvent) => void, options: AttachClickOptions = {}) {
const add = options.listenerSetter ? options.listenerSetter.add(elem) : elem.addEventListener.bind(elem); const add = options.listenerSetter ? options.listenerSetter.add(elem) : elem.addEventListener.bind(elem);
// const remove = options.listenerSetter ? options.listenerSetter.removeManual.bind(options.listenerSetter, elem) : elem.removeEventListener.bind(elem); // const remove = options.listenerSetter ? options.listenerSetter.removeManual.bind(options.listenerSetter, elem) : elem.removeEventListener.bind(elem);
@ -46,11 +46,11 @@ export function attachClickEvent(elem: HTMLElement, callback: (e: TouchEvent | M
} }
export function detachClickEvent(elem: HTMLElement, callback: (e: TouchEvent | MouseEvent) => void, options?: AddEventListenerOptions) { export function detachClickEvent(elem: HTMLElement, callback: (e: TouchEvent | MouseEvent) => void, options?: AddEventListenerOptions) {
if(CLICK_EVENT_NAME === 'touchend') { // if(CLICK_EVENT_NAME === 'touchend') {
elem.removeEventListener('touchstart', callback, options); // elem.removeEventListener('touchstart', callback, options);
} else { // } else {
elem.removeEventListener(CLICK_EVENT_NAME, callback, options); elem.removeEventListener(CLICK_EVENT_NAME, callback, options);
} // }
} }
export function simulateClickEvent(elem: HTMLElement) { export function simulateClickEvent(elem: HTMLElement) {

2
src/helpers/dom/fixSafariStickyInputFocusing.ts

@ -56,7 +56,7 @@ if(IS_STICKY_INPUT_BUGGED) {
// let hasFocus = false; // let hasFocus = false;
let lastFocusOutTimeStamp = 0; let lastFocusOutTimeStamp = 0;
document.addEventListener('focusin', (e) => { document.addEventListener('focusin', (e) => {
if((e.timeStamp - lastFocusOutTimeStamp) < 50/* && document.activeElement === input */) { if(!(e.target as HTMLElement).classList.contains('is-sticky-input-bugged') || (e.timeStamp - lastFocusOutTimeStamp) < 50/* && document.activeElement === input */) {
return; return;
} }

1
src/lang.ts

@ -793,6 +793,7 @@ const lang = {
"Message.Context.Select": "Select", "Message.Context.Select": "Select",
"Message.Context.Pin": "Pin", "Message.Context.Pin": "Pin",
"Message.Context.Unpin": "Unpin", "Message.Context.Unpin": "Unpin",
"Message.Context.Goto": "Show Message",
"MessageContext.CopyMessageLink1": "Copy Message Link", "MessageContext.CopyMessageLink1": "Copy Message Link",
"NewPoll.Anonymous": "Anonymous Voting", "NewPoll.Anonymous": "Anonymous Voting",
"NewPoll.Explanation.Placeholder": "Add a Comment (Optional)", "NewPoll.Explanation.Placeholder": "Add a Comment (Optional)",

29
src/lib/appManagers/appDialogsManager.ts

@ -22,7 +22,7 @@ import rootScope from "../rootScope";
import apiUpdatesManager from "./apiUpdatesManager"; import apiUpdatesManager from "./apiUpdatesManager";
import appPeersManager from './appPeersManager'; import appPeersManager from './appPeersManager';
import appImManager from "./appImManager"; import appImManager from "./appImManager";
import appMessagesManager, { Dialog } from "./appMessagesManager"; import appMessagesManager, { Dialog, MyMessage } from "./appMessagesManager";
import appStateManager, { State } from "./appStateManager"; import appStateManager, { State } from "./appStateManager";
import appUsersManager from "./appUsersManager"; import appUsersManager from "./appUsersManager";
import Button from "../../components/button"; import Button from "../../components/button";
@ -51,6 +51,7 @@ import windowSize from "../../helpers/windowSize";
import isInDOM from "../../helpers/dom/isInDOM"; import isInDOM from "../../helpers/dom/isInDOM";
import appPhotosManager, { MyPhoto } from "./appPhotosManager"; import appPhotosManager, { MyPhoto } from "./appPhotosManager";
import { MyDocument } from "./appDocsManager"; import { MyDocument } from "./appDocsManager";
import { setSendingStatus } from "../../components/sendingStatus";
export type DialogDom = { export type DialogDom = {
avatarEl: AvatarElement, avatarEl: AvatarElement,
@ -440,6 +441,7 @@ export class AppDialogsManager {
public setFilterId(filterId: number) { public setFilterId(filterId: number) {
this.filterId = filterId; this.filterId = filterId;
this.indexKey = appMessagesManager.dialogsStorage ? appMessagesManager.dialogsStorage.getDialogIndexKey(this.filterId) : 'index'; this.indexKey = appMessagesManager.dialogsStorage ? appMessagesManager.dialogsStorage.getDialogIndexKey(this.filterId) : 'index';
rootScope.filterId = filterId;
} }
private async onStateLoaded(state: State) { private async onStateLoaded(state: State) {
@ -1433,22 +1435,15 @@ export class AppDialogsManager {
} }
} }
const lastMessage = dialog.draft?._ === 'draftMessage' ? let setStatusMessage: MyMessage;
dialog.draft : if(dialog.draft?._ !== 'draftMessage') {
appMessagesManager.getMessageByPeer(dialog.peerId, dialog.top_message); const lastMessage: MyMessage = appMessagesManager.getMessageByPeer(dialog.peerId, dialog.top_message);
if(!lastMessage.deleted && lastMessage.pFlags.out && lastMessage.peerId !== rootScope.myId/* && if(!lastMessage.deleted && lastMessage.pFlags.out && lastMessage.peerId !== rootScope.myId) {
dialog.read_outbox_max_id */) { // maybe comment, 06.20.2020 setStatusMessage = lastMessage;
const isUnread = !!lastMessage.pFlags?.unread
/* && dialog.read_outbox_max_id !== 0 */; // maybe uncomment, 31.01.2020
if(isUnread) {
dom.statusSpan.classList.remove('tgico-checks');
dom.statusSpan.classList.add('tgico-check');
} else {
dom.statusSpan.classList.remove('tgico-check');
dom.statusSpan.classList.add('tgico-checks');
} }
} else dom.statusSpan.classList.remove('tgico-check', 'tgico-checks'); }
setSendingStatus(dom.statusSpan, setStatusMessage, true);
const filter = appMessagesManager.filtersStorage.getFilter(this.filterId); const filter = appMessagesManager.filtersStorage.getFilter(this.filterId);
let isPinned: boolean; let isPinned: boolean;
@ -1672,7 +1667,7 @@ export class AppDialogsManager {
li.dataset.peerId = '' + peerId; li.dataset.peerId = '' + peerId;
const statusSpan = document.createElement('span'); const statusSpan = document.createElement('span');
statusSpan.classList.add('message-status'); statusSpan.classList.add('message-status', 'sending-status'/* , 'transition', 'reveal' */);
const lastTimeSpan = document.createElement('span'); const lastTimeSpan = document.createElement('span');
lastTimeSpan.classList.add('message-time'); lastTimeSpan.classList.add('message-time');

21
src/lib/appManagers/appImManager.ts

@ -806,6 +806,16 @@ export class AppImManager {
if(e.code === 'KeyC' && (e.ctrlKey || e.metaKey) && target.tagName !== 'INPUT') { if(e.code === 'KeyC' && (e.ctrlKey || e.metaKey) && target.tagName !== 'INPUT') {
return; return;
} else if(e.altKey && (e.code === 'ArrowUp' || e.code === 'ArrowDown') && rootScope.peerId) {
const folder = appMessagesManager.dialogsStorage.getFolder(rootScope.filterId, true);
const idx = folder.findIndex(dialog => dialog.peerId === rootScope.peerId);
if(idx !== -1) {
const nextIndex = e.code === 'ArrowUp' ? idx - 1 : idx + 1;
const nextDialog = folder[nextIndex];
if(nextDialog) {
this.setPeer(nextDialog.peerId);
}
}
} else if(e.code === 'ArrowUp') { } else if(e.code === 'ArrowUp') {
if(!chat.input.editMsgId && chat.input.isInputEmpty()) { if(!chat.input.editMsgId && chat.input.isInputEmpty()) {
const historyStorage = appMessagesManager.getHistoryStorage(chat.peerId, chat.threadId); const historyStorage = appMessagesManager.getHistoryStorage(chat.peerId, chat.threadId);
@ -854,23 +864,18 @@ export class AppImManager {
document.body.addEventListener('keydown', onKeyDown); document.body.addEventListener('keydown', onKeyDown);
rootScope.addEventListener('history_multiappend', (e) => { rootScope.addEventListener('history_multiappend', (msgIdsByPeer) => {
const msgIdsByPeer = e;
for(const peerId in msgIdsByPeer) { for(const peerId in msgIdsByPeer) {
appSidebarRight.sharedMediaTab.renderNewMessages(+peerId, Array.from(msgIdsByPeer[peerId])); appSidebarRight.sharedMediaTab.renderNewMessages(+peerId, Array.from(msgIdsByPeer[peerId]));
} }
}); });
rootScope.addEventListener('history_delete', (e) => { rootScope.addEventListener('history_delete', ({peerId, msgs}) => {
const {peerId, msgs} = e;
appSidebarRight.sharedMediaTab.deleteDeletedMessages(peerId, Array.from(msgs)); appSidebarRight.sharedMediaTab.deleteDeletedMessages(peerId, Array.from(msgs));
}); });
// Calls when message successfully sent and we have an id // Calls when message successfully sent and we have an id
rootScope.addEventListener('message_sent', (e) => { rootScope.addEventListener('message_sent', ({storage, tempId, mid}) => {
const {storage, tempId, mid} = e;
const message = appMessagesManager.getMessageFromStorage(storage, mid); const message = appMessagesManager.getMessageFromStorage(storage, mid);
appSidebarRight.sharedMediaTab.renderNewMessages(message.peerId, [mid]); appSidebarRight.sharedMediaTab.renderNewMessages(message.peerId, [mid]);
}); });

2
src/lib/appManagers/appMessagesManager.ts

@ -2171,7 +2171,7 @@ export class AppMessagesManager {
//return Object.keys(this.groupedMessagesStorage[grouped_id]).map(id => +id).sort((a, b) => a - b); //return Object.keys(this.groupedMessagesStorage[grouped_id]).map(id => +id).sort((a, b) => a - b);
} }
public getMidsByMessage(message: any) { public getMidsByMessage(message: Message.message) {
if(message?.grouped_id) return this.getMidsByAlbum(message.grouped_id); if(message?.grouped_id) return this.getMidsByAlbum(message.grouped_id);
else return [message.mid]; else return [message.mid];
} }

1
src/lib/rootScope.ts

@ -147,6 +147,7 @@ export class RootScope extends EventListenerBase<{
public connectionStatus: {[name: string]: ConnectionStatusChange} = {}; public connectionStatus: {[name: string]: ConnectionStatusChange} = {};
public settings: State['settings']; public settings: State['settings'];
public peerId = 0; public peerId = 0;
public filterId = 0;
public systemTheme: Theme['name']; public systemTheme: Theme['name'];
public config: Partial<Config.config> = { public config: Partial<Config.config> = {
forwarded_count_max: 100, forwarded_count_max: 100,

5
src/scss/components/_global.scss

@ -155,6 +155,11 @@ Utility Classes
border-radius: 0 !important; border-radius: 0 !important;
} }
.disable-hover/* ,
.disable-hover * */ {
pointer-events: none !important;
}
/* .flex-grow { /* .flex-grow {
flex-grow: 1; flex-grow: 1;
} }

2
src/scss/partials/_button.scss

@ -212,7 +212,7 @@
right: 0; right: 0;
top: 0; top: 0;
bottom: 0; bottom: 0;
z-index: 1; z-index: 3;
cursor: default; cursor: default;
user-select: none; user-select: none;
//background-color: rgba(0, 0, 0, .2); //background-color: rgba(0, 0, 0, .2);

5
src/scss/partials/_chat.scss

@ -1147,11 +1147,6 @@ $chat-helper-size: 36px;
@include respond-to(handhelds) { @include respond-to(handhelds) {
padding: 0 $chat-padding-handhelds; padding: 0 $chat-padding-handhelds;
html.is-ios & {
-webkit-user-select: none;
-webkit-touch-callout: none;
}
} }
&.is-chat { &.is-chat {

45
src/scss/partials/_chatBubble.scss

@ -221,7 +221,8 @@ $bubble-margin: .25rem;
// ! hide context menu for media on android // ! hide context menu for media on android
.bubbles.is-selecting & { .bubbles.is-selecting & {
img, img,
video { video,
a {
pointer-events: none; pointer-events: none;
} }
} }
@ -1031,7 +1032,7 @@ $bubble-margin: .25rem;
&-title, &-title,
&-subtitle, &-subtitle,
i { i {
color: #fff; color: #fff !important;
} }
&-border { &-border {
@ -1412,31 +1413,31 @@ $bubble-margin: .25rem;
position: absolute; position: absolute;
/* position: relative; /* position: relative;
width: max-content; */ width: max-content; */
bottom: .1rem; bottom: .1875rem;
right: .2rem; right: .1875rem;
border-radius: 12px; border-radius: .75rem;
background-color: var(--message-time-background); background-color: var(--message-time-background);
padding: 0 .2rem; padding: 0 .3125rem;
z-index: 2; z-index: 2;
.time { .time {
margin-left: 0; margin-left: 0;
color: #fff;
visibility: visible;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 2.5px; padding: 0;
line-height: 18px; margin: 0;
pointer-events: all; // show title
white-space: nowrap; white-space: nowrap;
height: 18px; height: 18px;
.inner {
right: unset;
bottom: unset;
color: #fff;
margin: inherit;
&:after { &:after {
color: #fff; color: #fff;
} }
.inner {
display: none;
} }
} }
} }
@ -2102,6 +2103,10 @@ $bubble-margin: .25rem;
.reply, .name { .reply, .name {
right: calc(100% + 10px); right: calc(100% + 10px);
} }
.message {
right: 0;
}
} }
&:not(.just-media) { &:not(.just-media) {
@ -2117,14 +2122,6 @@ $bubble-margin: .25rem;
} }
} }
&.sticker,
&.round,
&.emoji-big {
.message {
right: 0;
}
}
.quote:before { .quote:before {
background-color: var(--message-out-primary-color); background-color: var(--message-out-primary-color);
} }
@ -2152,10 +2149,6 @@ $bubble-margin: .25rem;
} }
} }
&.is-message-empty .time .inner {
color: var(--message-out-primary-color);
}
/* &.is-message-empty .time:after { /* &.is-message-empty .time:after {
margin-bottom: 1px; margin-bottom: 1px;
} */ } */

25
src/scss/partials/_chatlist.scss

@ -109,6 +109,7 @@ ul.chatlist {
} */ } */
li { li {
--background: unset;
//height: var(--height); //height: var(--height);
height: 72px; height: 72px;
//max-height: var(--height); //max-height: var(--height);
@ -124,12 +125,14 @@ ul.chatlist {
padding-right: 8.5px; padding-right: 8.5px;
padding-left: 8.5px; */ padding-left: 8.5px; */
overflow: hidden; overflow: hidden;
background: var(--background);
@include respond-to(handhelds) { @include respond-to(handhelds) {
border-radius: 0; border-radius: 0;
} }
@include hover-background-effect(); //@include hover-background-effect();
@include hover(gray, --background, false);
&.is-muted { &.is-muted {
.user-title { .user-title {
@ -183,13 +186,13 @@ ul.chatlist {
} */ } */
&.menu-open { &.menu-open {
background: var(--light-secondary-text-color); --background: var(--light-secondary-text-color);
} }
@include respond-to(not-handhelds) { @include respond-to(not-handhelds) {
&.active { &.active {
--background: var(--primary-color) !important;
//background: var(--light-secondary-text-color); //background: var(--light-secondary-text-color);
background: var(--primary-color) !important;
.user-caption, .user-caption,
.tgico-chatspinned:before, .tgico-chatspinned:before,
@ -405,25 +408,25 @@ ul.chatlist {
} }
.message-status { .message-status {
margin-right: .1rem; margin-right: 0.125rem;
//margin-top: .3rem;
margin-top: -.3rem;
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
&[class*=" tgico-"] {
color: var(--chatlist-status-color); color: var(--chatlist-status-color);
line-height: 1;
width: 1.25rem;
height: 1.25rem;
font-size: 1.25rem; font-size: 1.25rem;
} position: relative;
margin-top: -.0625rem;
&:before { &:before {
vertical-align: middle; vertical-align: middle;
} }
} }
.message-time { /* .message-time {
vertical-align: middle; vertical-align: middle;
} } */
.tgico-chatspinned { .tgico-chatspinned {
background: transparent; background: transparent;

14
src/scss/partials/_checkbox.scss

@ -56,10 +56,12 @@
} }
&-background { &-background {
// it is needed for circle scale animation
top: -15%; top: -15%;
right: -15%; right: -15%;
bottom: -15%; bottom: -15%;
left: -15%; left: -15%;
background-color: var(--primary-color); background-color: var(--primary-color);
transform: scale(1); transform: scale(1);
border-radius: 50%; border-radius: 50%;
@ -138,6 +140,14 @@
.checkbox-box { .checkbox-box {
border-radius: 50%; border-radius: 50%;
overflow: auto;
&-background {
top: 0;
right: 0;
bottom: 0;
left: 0;
}
&-border { &-border {
border: 2px solid var(--secondary-color); border: 2px solid var(--secondary-color);
@ -147,6 +157,10 @@
&-check { &-check {
--offset: calc(var(--size) - (var(--size) / 2 + .125rem)); --offset: calc(var(--size) - (var(--size) / 2 + .125rem));
} }
html.is-safari & {
-webkit-mask-image: none;
}
} }
} }

14
src/scss/partials/_ckin.scss

@ -287,6 +287,9 @@ video::-webkit-media-controls-enclosure {
margin: 0; margin: 0;
outline: none; outline: none;
caret-color: var(--color); caret-color: var(--color);
position: absolute;
top: -.5rem;
bottom: -.5rem;
&:focus { &:focus {
outline: none; outline: none;
@ -348,17 +351,24 @@ video::-webkit-media-controls-enclosure {
position: absolute; position: absolute;
right: 0; right: 0;
top: 50%; top: 50%;
transform: translate(calc(var(--thumb-size) / 2), -50%); transform: translate(calc(var(--thumb-size) / 2), -50%) scale(1);
@include animation-level(2) {
transition: transform .125s ease-in-out;
} }
} }
} }
}
&.is-focused .progress-line__filled:not(.progress-line__loaded):after {
transform: translate(calc(var(--thumb-size) / 2), -50%) scale(1.25);
}
&__loaded, &:before { &__loaded, &:before {
opacity: .3; opacity: .3;
background-color: var(--secondary-color); background-color: var(--secondary-color);
} }
&__seek,
&__filled, &__filled,
&__loaded { &__loaded {
border-radius: var(--border-radius); border-radius: var(--border-radius);

4
src/scss/partials/_document.scss

@ -10,6 +10,10 @@
padding-left: 4.25rem; padding-left: 4.25rem;
height: 70px; height: 70px;
.media-photo {
border-radius: inherit;
}
&-ico { &-ico {
background-color: var(--background-color); background-color: var(--background-color);
border-radius: $border-radius; border-radius: $border-radius;

4
src/scss/partials/_profile.scss

@ -194,7 +194,7 @@
position: relative; position: relative;
width: 100%; width: 100%;
.checkbox-field { /* .checkbox-field {
margin: 0; margin: 0;
padding: 0; padding: 0;
margin-left: -54px; margin-left: -54px;
@ -202,7 +202,7 @@
.checkbox-caption { .checkbox-caption {
padding-left: 54px; padding-left: 54px;
} } */
&-wrapper { &-wrapper {
flex: 1 1 auto; flex: 1 1 auto;

183
src/scss/partials/_rightSidebar.scss

@ -310,7 +310,9 @@
display: none; display: none;
} }
.document-name, .audio-title, .title { .document-name,
.audio-title,
.title {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
@ -347,6 +349,11 @@
overflow: hidden; overflow: hidden;
} }
.checkbox-field {
right: .25rem;
top: .25rem;
}
/* span.video-play { /* span.video-play {
background-color: var(--message-time-background); background-color: var(--message-time-background);
color: #fff; color: #fff;
@ -357,6 +364,37 @@
} */ } */
} }
.checkbox {
&-box {
box-shadow: 0px 0px 3px 0px rgb(0 0 0 / 40%);
&-border {
border-color: var(--message-checkbox-border-color);
}
&-background {
background-color: var(--message-checkbox-color);
}
}
&-field {
position: absolute;
z-index: 2;
margin: 0;
}
}
.document,
.audio {
.checkbox-field {
top: 50%;
left: 0;
margin-left: 2rem;
margin-top: 1rem;
transform: translateY(-50%);
}
}
&-content-media &-month { &-content-media &-month {
&-items { &-items {
width: 100%; width: 100%;
@ -385,7 +423,8 @@
//height: 54px; //height: 54px;
height: calc(48px + 1.5rem); height: calc(48px + 1.5rem);
&-ico, &-download { &-ico,
&-download {
width: 48px; width: 48px;
height: 48px; height: 48px;
border-radius: 5px !important; border-radius: 5px !important;
@ -412,24 +451,21 @@
.search-super-item { .search-super-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-top: 20px; padding-left: 4.4375rem;
margin-left: 5px;
padding-bottom: 2px;
//padding-bottom: 10px;
position: relative; position: relative;
padding-left: 60px;
overflow: hidden; overflow: hidden;
//min-height: 48px; min-height: 4.375rem;
min-height: 58px; cursor: pointer;
justify-content: flex-start;
}
.preview { .row-media {
height: 3rem; height: 3rem;
width: 3rem; width: 3rem;
border-radius: .375rem; border-radius: .375rem;
overflow: hidden; overflow: hidden;
position: absolute; position: absolute;
left: 0; left: .6875rem;
top: 0;
&.empty { &.empty {
display: flex; display: flex;
@ -440,45 +476,62 @@
text-transform: uppercase; text-transform: uppercase;
background-color: var(--primary-color); background-color: var(--primary-color);
} }
}
/* .anchor-url {
&:before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
content: " ";
cursor: pointer;
}
} */
.media-photo { .media-photo {
object-fit: cover; object-fit: cover;
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: inherit;
} }
.row-title {
margin-top: .1875rem;
} }
.url { .row-subtitle {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
font-size: 14px; white-space: pre-wrap;
margin-top: -1px; text-overflow: ellipsis;
word-break: break-word;
&.sender {
margin-top: .125rem;
} }
} }
.title { .sent-time {
font-size: 16px; margin: 1px 0 0;
margin-top: 2px;
} }
.subtitle { .checkbox-field {
font-size: 14px; padding: 0 !important;
max-width: 310px; margin: 2rem 0 0 -1.75rem !important;
&.sender {
margin-top: 2px;
}
} }
@include respond-to(not-handhelds) {
.search-super-month-items { .search-super-month-items {
padding: 0 24px 15px 15px; margin: .5625rem;
}
}
@include respond-to(handhelds) { @include respond-to(handhelds) {
padding: 0 16px 15px 7px; .search-super-month-name {
padding: .875rem 1rem;
} }
} }
} }
&-content-music, &-content-voice { &-content-music, &-content-voice {
@ -528,6 +581,76 @@
} }
} }
} }
&-tabs-scrollable {
.search-super-nav-scrollable {
opacity: 1;
}
.search-super-nav-scrollable,
.search-super-selection-container {
@include animation-level(2) {
transition: opacity .2s ease-in-out;
}
}
&.is-selecting {
&:not(.backwards) {
.search-super-nav-scrollable {
opacity: 0;
}
.search-super-selection-container {
opacity: 1;
}
}
}
}
&.is-selecting {
a {
pointer-events: none;
}
.row {
&:not(.menu-open) {
background-color: transparent !important;
}
}
}
&-selection {
&-container {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1rem;
opacity: 0;
.btn-icon + .btn-icon {
margin-left: .5rem;
}
@include respond-to(handhelds) {
padding: 0 .5rem;
}
}
&-count {
flex-grow: 1;
font-weight: 500;
color: var(--primary-text-color);
white-space: nowrap;
text-transform: capitalize;
margin-left: 1.5rem;
}
}
} }
#search-container { #search-container {

4
src/scss/partials/_row.scss

@ -115,4 +115,8 @@
margin: 0 !important; margin: 0 !important;
left: .5rem; left: .5rem;
} }
&.menu-open {
background-color: var(--light-secondary-text-color);
}
} }

34
src/scss/partials/_transition.scss

@ -1,14 +1,7 @@
/* // * Jolly Cobra's transition
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
.transition { .transition {
--easeOutSine: cubic-bezier(.39, .575, .565, 1); > .transition-item {
--easeInSine: cubic-bezier(.47, 0, .745, .715);
.transition-item {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@ -71,6 +64,8 @@
* slide-fade * slide-fade
*/ */
&.slide-fade { &.slide-fade {
--easeOutSine: cubic-bezier(.39, .575, .565, 1);
--easeInSine: cubic-bezier(.47, 0, .745, .715);
position: relative; position: relative;
> .from { > .from {
@ -117,6 +112,18 @@
} }
} }
} }
/* &.reveal {
> .to {
clip-path: inset(0 100% 0 0);
}
&.animating {
> .to {
animation: reveal-in 350ms ease-in;
}
}
} */
} }
/* /*
@ -188,6 +195,15 @@
} }
} }
/* @keyframes reveal-in {
0% {
clip-path: inset(0 100% 0 0);
}
100% {
clip-path: inset(0 0 0 0);
}
} */
/* .zoom-fade { /* .zoom-fade {
transition: .15s ease-in-out opacity, .15s ease-in-out transform; transition: .15s ease-in-out opacity, .15s ease-in-out transform;
transform: scale3d(1.1, 1.1, 1); transform: scale3d(1.1, 1.1, 1);

26
src/scss/style.scss

@ -406,6 +406,10 @@ html.is-ios {
//&, body { //&, body {
position: fixed; // fix iOS fullscreen scroll position: fixed; // fix iOS fullscreen scroll
//} //}
// disable image longtapping
-webkit-user-select: none;
-webkit-touch-callout: none;
} }
@supports(padding: unquote('max(0px)')) { @supports(padding: unquote('max(0px)')) {
@ -490,11 +494,6 @@ input, textarea, button, select, a, div {
//} //}
} }
.disable-hover/* ,
.disable-hover * */ {
pointer-events: none !important;
}
@include respond-to(not-handhelds) { @include respond-to(not-handhelds) {
.only-handhelds { .only-handhelds {
display: none !important; display: none !important;
@ -1359,3 +1358,20 @@ middle-ellipsis-element {
content: "W"; content: "W";
} }
} }
.sending-status {
&:empty {
display: none;
}
/* &.animating {
.sending-status-icon {
background: var(--background);
}
} */
&-icon {
position: absolute;
line-height: 1 !important;
}
}

Loading…
Cancel
Save