Browse Source

Call incompatibility notification

Music replay & loop
Fix incrementing notification by reaction
master
Eduard Kuzmenko 2 years ago
parent
commit
bab69c170a
  1. 79
      src/components/appMediaPlaybackController.ts
  2. 10
      src/components/audio.ts
  3. 40
      src/components/chat/audio.ts
  4. 2
      src/components/chat/input.ts
  5. 2
      src/components/peerProfileAvatars.ts
  6. 2
      src/components/wrappers.ts
  7. 2
      src/helpers/avatarListLoader.ts
  8. 10
      src/helpers/compareValue.ts
  9. 65
      src/helpers/listLoader.ts
  10. 25
      src/helpers/long/compareLong.ts
  11. 152
      src/helpers/searchListLoader.ts
  12. 54
      src/helpers/slicedArray.ts
  13. 7
      src/lib/appManagers/appImManager.ts
  14. 22
      src/lib/appManagers/appMessagesManager.ts
  15. 6
      src/lib/appManagers/appNotificationsManager.ts
  16. 7
      src/lib/calls/callInstance.ts
  17. 6
      src/lib/rootScope.ts
  18. 4
      src/scss/partials/_chat.scss
  19. 51
      src/scss/tgico/_style.scss
  20. 181
      src/scss/tgico/_variables.scss
  21. 22
      src/tests/slicedArray.test.ts

79
src/components/appMediaPlaybackController.ts

@ -58,7 +58,7 @@ type MediaDetails = {
export type PlaybackMediaType = 'voice' | 'video' | 'audio'; export type PlaybackMediaType = 'voice' | 'video' | 'audio';
class AppMediaPlaybackController { export class AppMediaPlaybackController {
private container: HTMLElement; private container: HTMLElement;
private media: Map<PeerId, Map<number, HTMLMediaElement>> = new Map(); private media: Map<PeerId, Map<number, HTMLMediaElement>> = new Map();
private scheduled: AppMediaPlaybackController['media'] = new Map(); private scheduled: AppMediaPlaybackController['media'] = new Map();
@ -78,9 +78,13 @@ class AppMediaPlaybackController {
public volume: number; public volume: number;
public muted: boolean; public muted: boolean;
public playbackRate: number; public playbackRate: number;
public loop: boolean;
public round: boolean;
private _volume = 1; private _volume = 1;
private _muted = false; private _muted = false;
private _playbackRate = 1; private _playbackRate = 1;
private _loop = false;
private _round = false;
private lockedSwitchers: boolean; private lockedSwitchers: boolean;
private playbackRates: Record<PlaybackMediaType, number> = { private playbackRates: Record<PlaybackMediaType, number> = {
voice: 1, voice: 1,
@ -128,7 +132,9 @@ class AppMediaPlaybackController {
const keys = [ const keys = [
'volume' as const, 'volume' as const,
'muted' as const, 'muted' as const,
'playbackRate' as const 'playbackRate' as const,
'loop' as const,
'round' as const
]; ];
keys.forEach(key => { keys.forEach(key => {
const _key = ('_' + key) as `_${typeof key}`; const _key = ('_' + key) as `_${typeof key}`;
@ -141,7 +147,7 @@ class AppMediaPlaybackController {
// @ts-ignore // @ts-ignore
this[_key] = value; this[_key] = value;
if(this.playingMedia) { if(this.playingMedia && (key !== 'loop' || this.playingMediaType === 'audio') && key !== 'round') {
// @ts-ignore // @ts-ignore
this.playingMedia[key] = value; this.playingMedia[key] = value;
} }
@ -158,12 +164,20 @@ class AppMediaPlaybackController {
} }
private dispatchPlaybackParams() { private dispatchPlaybackParams() {
const {volume, muted, playbackRate} = this; rootScope.dispatchEvent('media_playback_params', this.getPlaybackParams());
rootScope.dispatchEvent('media_playback_params', {
volume, muted, playbackRate
});
} }
public getPlaybackParams() {
const {volume, muted, playbackRate, loop, round} = this;
return {
volume,
muted,
playbackRate,
loop,
round
};
}
public seekBackward = (details: MediaSessionActionDetails) => { public seekBackward = (details: MediaSessionActionDetails) => {
const media = this.playingMedia; const media = this.playingMedia;
if(media) { if(media) {
@ -302,6 +316,10 @@ class AppMediaPlaybackController {
if(this.playingMedia === media) { if(this.playingMedia === media) {
media.playbackRate = this.playbackRate; media.playbackRate = this.playbackRate;
if(doc.type === 'audio') {
media.loop = this.loop;
}
} }
// }, doc.supportsStreaming ? 500e3 : 0); // }, doc.supportsStreaming ? 500e3 : 0);
@ -494,15 +512,19 @@ class AppMediaPlaybackController {
const previousMedia = this.playingMedia; const previousMedia = this.playingMedia;
if(previousMedia !== media) { if(previousMedia !== media) {
this.stop(); this.stop();
this.setMedia(media, message);
const verify = (element: MediaItem) => element.mid === mid && element.peerId === peerId; const verify = (element: MediaItem) => element.mid === mid && element.peerId === peerId;
if(!this.listLoader.current || !verify(this.listLoader.current)) { const current = this.listLoader.getCurrent();
let idx = this.listLoader.previous.findIndex(verify); if(!current || !verify(current)) {
const previous = this.listLoader.getPrevious();
let idx = previous.findIndex(verify);
let jumpLength: number; let jumpLength: number;
if(idx !== -1) { if(idx !== -1) {
jumpLength = -(this.listLoader.previous.length - idx); jumpLength = -(previous.length - idx);
} else { } else {
idx = this.listLoader.next.findIndex(verify); idx = this.listLoader.getNext().findIndex(verify);
if(idx !== -1) { if(idx !== -1) {
jumpLength = idx + 1; jumpLength = idx + 1;
} }
@ -510,14 +532,12 @@ class AppMediaPlaybackController {
if(idx !== -1) { if(idx !== -1) {
if(jumpLength) { if(jumpLength) {
this.listLoader.go(jumpLength, false); this.go(jumpLength, false);
} }
} else { } else {
this.setTargets({peerId, mid}); this.setTargets({peerId, mid});
} }
} }
this.setMedia(media, message);
} }
// audio_pause не успеет сработать без таймаута // audio_pause не успеет сработать без таймаута
@ -540,7 +560,8 @@ class AppMediaPlaybackController {
return { return {
doc: appMessagesManager.getMediaFromMessage(message), doc: appMessagesManager.getMediaFromMessage(message),
message, message,
media: playingMedia media: playingMedia,
playbackParams: this.getPlaybackParams()
}; };
} }
@ -564,7 +585,10 @@ class AppMediaPlaybackController {
//console.log('on media end'); //console.log('on media end');
if(!this.next()) { if(this.lockedSwitchers ||
(!this.round && this.listLoader.current && !this.listLoader.next.length) ||
!this.listLoader.getNext().length ||
!this.next()) {
this.stop(); this.stop();
rootScope.dispatchEvent('media_stop'); rootScope.dispatchEvent('media_stop');
} }
@ -654,19 +678,32 @@ class AppMediaPlaybackController {
}, 0); }, 0);
}; };
public go = (length: number, dispatchJump?: boolean) => {
if(this.lockedSwitchers) {
return;
}
if(this.playingMediaType === 'audio') {
return this.listLoader.goRound(length, dispatchJump);
} else {
return this.listLoader.go(length, dispatchJump);
}
};
public next = () => { public next = () => {
return !this.lockedSwitchers && this.listLoader.go(1); return this.go(1);
}; };
public previous = () => { public previous = () => {
const media = this.playingMedia; const media = this.playingMedia;
if(media && (media.currentTime > 5 || !this.listLoader.previous.length)) { // if(media && (media.currentTime > 5 || !this.listLoader.getPrevious().length)) {
if(media && media.currentTime > 5) {
media.currentTime = 0; media.currentTime = 0;
this.toggle(true); this.toggle(true);
return; return;
} }
return !this.lockedSwitchers && this.listLoader.go(-1); return this.go(-1);
}; };
public willBePlayed(media: HTMLMediaElement) { public willBePlayed(media: HTMLMediaElement) {
@ -746,6 +783,10 @@ class AppMediaPlaybackController {
this.playingMedia.muted = this.muted; this.playingMedia.muted = this.muted;
this.playingMedia.playbackRate = this.playbackRate; this.playingMedia.playbackRate = this.playbackRate;
if(mediaType === 'audio') {
this.playingMedia.loop = this.loop;
}
if('mediaSession' in navigator) { if('mediaSession' in navigator) {
this.setNewMediadata(message); this.setNewMediadata(message);
} }

10
src/components/audio.ts

@ -362,7 +362,7 @@ function constructDownloadPreloader(tryAgainOnFail = true) {
return preloader; return preloader;
} }
export const findMediaTargets = (anchor: HTMLElement/* , useSearch: boolean */) => { export const findMediaTargets = (anchor: HTMLElement, anchorMid: number/* , useSearch: boolean */) => {
let prev: MediaItem[], next: MediaItem[]; let prev: MediaItem[], next: MediaItem[];
// if(anchor.classList.contains('search-super-item') || !useSearch) { // if(anchor.classList.contains('search-super-item') || !useSearch) {
const isBubbles = !anchor.classList.contains('search-super-item'); const isBubbles = !anchor.classList.contains('search-super-item');
@ -394,6 +394,12 @@ export const findMediaTargets = (anchor: HTMLElement/* , useSearch: boolean */)
} }
// } // }
if((next.length && next[0].mid < anchorMid) || (prev.length && prev[prev.length - 1].mid > anchorMid)) {
[prev, next] = [next.reverse(), prev.reverse()];
}
// prev = next = undefined;
return [prev, next]; return [prev, next];
}; };
@ -490,7 +496,7 @@ export default class AudioElement extends HTMLElement {
inputFilter: {_: 'inputMessagesFilterEmpty'}, inputFilter: {_: 'inputMessagesFilterEmpty'},
useSearch: false useSearch: false
})) { })) {
const [prev, next] = !hadSearchContext ? [] : findMediaTargets(this/* , this.searchContext.useSearch */); const [prev, next] = !hadSearchContext ? [] : findMediaTargets(this, this.message.mid/* , this.searchContext.useSearch */);
appMediaPlaybackController.setTargets({peerId: this.message.peerId, mid: this.message.mid}, prev, next); appMediaPlaybackController.setTargets({peerId: this.message.peerId, mid: this.message.mid}, prev, next);
} }

40
src/components/chat/audio.ts

@ -7,7 +7,7 @@
import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager"; import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager";
import type ChatTopbar from "./topbar"; import type ChatTopbar from "./topbar";
import rootScope from "../../lib/rootScope"; import rootScope from "../../lib/rootScope";
import appMediaPlaybackController from "../appMediaPlaybackController"; import appMediaPlaybackController, { AppMediaPlaybackController } from "../appMediaPlaybackController";
import DivAndCaption from "../divAndCaption"; import DivAndCaption from "../divAndCaption";
import PinnedContainer from "./pinnedContainer"; import PinnedContainer from "./pinnedContainer";
import Chat from "./chat"; import Chat from "./chat";
@ -27,6 +27,7 @@ export default class ChatAudio extends PinnedContainer {
private progressLine: MediaProgressLine; private progressLine: MediaProgressLine;
private volumeSelector: VolumeSelector; private volumeSelector: VolumeSelector;
private fasterEl: HTMLElement; private fasterEl: HTMLElement;
private repeatEl: HTMLButtonElement;
constructor(protected topbar: ChatTopbar, protected chat: Chat, protected appMessagesManager: AppMessagesManager) { constructor(protected topbar: ChatTopbar, protected chat: Chat, protected appMessagesManager: AppMessagesManager) {
super({ super({
@ -84,12 +85,25 @@ export default class ChatAudio extends PinnedContainer {
this.volumeSelector.btn.prepend(tunnel); this.volumeSelector.btn.prepend(tunnel);
this.volumeSelector.btn.append(volumeProgressLineContainer); this.volumeSelector.btn.append(volumeProgressLineContainer);
this.repeatEl = ButtonIcon('audio_repeat', {noRipple: true});
attachClick(this.repeatEl, () => {
const params = appMediaPlaybackController.getPlaybackParams();
if(!params.round) {
appMediaPlaybackController.round = true;
} else if(params.loop) {
appMediaPlaybackController.round = false;
appMediaPlaybackController.loop = false;
} else {
appMediaPlaybackController.loop = !appMediaPlaybackController.loop;
}
});
const fasterEl = this.fasterEl = ButtonIcon('playback_2x', {noRipple: true}); const fasterEl = this.fasterEl = ButtonIcon('playback_2x', {noRipple: true});
attachClick(fasterEl, () => { attachClick(fasterEl, () => {
appMediaPlaybackController.playbackRate = fasterEl.classList.contains('active') ? 1 : 1.75; appMediaPlaybackController.playbackRate = fasterEl.classList.contains('active') ? 1 : 1.75;
}); });
this.wrapperUtils.prepend(this.volumeSelector.btn, fasterEl); this.wrapperUtils.prepend(this.volumeSelector.btn, fasterEl, this.repeatEl);
const progressWrapper = document.createElement('div'); const progressWrapper = document.createElement('div');
progressWrapper.classList.add('pinned-audio-progress-wrapper'); progressWrapper.classList.add('pinned-audio-progress-wrapper');
@ -102,14 +116,12 @@ export default class ChatAudio extends PinnedContainer {
this.topbar.listenerSetter.add(rootScope)('media_play', this.onMediaPlay); this.topbar.listenerSetter.add(rootScope)('media_play', this.onMediaPlay);
this.topbar.listenerSetter.add(rootScope)('media_pause', this.onPause); this.topbar.listenerSetter.add(rootScope)('media_pause', this.onPause);
this.topbar.listenerSetter.add(rootScope)('media_stop', this.onStop); this.topbar.listenerSetter.add(rootScope)('media_stop', this.onStop);
this.topbar.listenerSetter.add(rootScope)('media_playback_params', ({playbackRate}) => { this.topbar.listenerSetter.add(rootScope)('media_playback_params', this.onPlaybackParams);
this.onPlaybackRateChange(playbackRate);
});
const playingDetails = appMediaPlaybackController.getPlayingDetails(); const playingDetails = appMediaPlaybackController.getPlayingDetails();
if(playingDetails) { if(playingDetails) {
this.onMediaPlay(playingDetails); this.onMediaPlay(playingDetails);
this.onPlaybackRateChange(appMediaPlaybackController.playbackRate); this.onPlaybackParams(playingDetails.playbackParams);
} }
} }
@ -119,8 +131,12 @@ export default class ChatAudio extends PinnedContainer {
} }
} }
private onPlaybackRateChange = (playbackRate: number) => { private onPlaybackParams = (playbackParams: ReturnType<AppMediaPlaybackController['getPlaybackParams']>) => {
this.fasterEl.classList.toggle('active', playbackRate > 1); this.fasterEl.classList.toggle('active', playbackParams.playbackRate > 1);
this.repeatEl.classList.remove('tgico-audio_repeat', 'tgico-audio_repeat_single');
this.repeatEl.classList.add(playbackParams.loop ? 'tgico-audio_repeat_single' : 'tgico-audio_repeat');
this.repeatEl.classList.toggle('active', playbackParams.loop || playbackParams.round);
}; };
private onPause = () => { private onPause = () => {
@ -137,18 +153,20 @@ export default class ChatAudio extends PinnedContainer {
media: HTMLMediaElement media: HTMLMediaElement
}) => { }) => {
let title: string | HTMLElement, subtitle: string | HTMLElement | DocumentFragment; let title: string | HTMLElement, subtitle: string | HTMLElement | DocumentFragment;
if(doc.type === 'voice' || doc.type === 'round') { const isMusic = doc.type !== 'voice' && doc.type !== 'round';
if(!isMusic) {
title = new PeerTitle({peerId: message.fromId, fromName: message.fwd_from?.from_name}).element; title = new PeerTitle({peerId: message.fromId, fromName: message.fwd_from?.from_name}).element;
//subtitle = 'Voice message'; //subtitle = 'Voice message';
subtitle = formatFullSentTime(message.date); subtitle = formatFullSentTime(message.date);
this.fasterEl.classList.remove('hide');
} else { } else {
title = doc.audioTitle || doc.fileName; title = doc.audioTitle || doc.fileName;
subtitle = doc.audioPerformer || i18n('AudioUnknownArtist'); subtitle = doc.audioPerformer || i18n('AudioUnknownArtist');
this.fasterEl.classList.add('hide');
} }
this.fasterEl.classList.toggle('hide', isMusic);
this.repeatEl.classList.toggle('hide', !isMusic);
this.progressLine.setMedia(media); this.progressLine.setMedia(media);
this.fill(title, subtitle, message); this.fill(title, subtitle, message);

2
src/components/chat/input.ts

@ -682,7 +682,7 @@ export default class ChatInput {
<span class="tgico tgico-send"></span> <span class="tgico tgico-send"></span>
<span class="tgico tgico-schedule"></span> <span class="tgico tgico-schedule"></span>
<span class="tgico tgico-check"></span> <span class="tgico tgico-check"></span>
<span class="tgico tgico-microphone"></span> <span class="tgico tgico-microphone_filled"></span>
`); `);
this.btnSendContainer.append(this.recordRippleEl, this.btnSend); this.btnSendContainer.append(this.recordRippleEl, this.btnSend);

2
src/components/peerProfileAvatars.ts

@ -257,7 +257,7 @@ export default class PeerProfileAvatars {
if(!older) return Promise.resolve({count: undefined, items: []}); if(!older) return Promise.resolve({count: undefined, items: []});
if(peerId.isUser()) { if(peerId.isUser()) {
const maxId: Photo.photo['id'] = (anchor || listLoader.current) as any; const maxId: Photo.photo['id'] = anchor as any;
return appPhotosManager.getUserPhotos(peerId, maxId, loadCount).then(value => { return appPhotosManager.getUserPhotos(peerId, maxId, loadCount).then(value => {
return { return {
count: value.count, count: value.count,

2
src/components/wrappers.ts

@ -328,7 +328,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
inputFilter: {_: 'inputMessagesFilterEmpty'}, inputFilter: {_: 'inputMessagesFilterEmpty'},
useSearch: false useSearch: false
})) { })) {
const [prev, next] = !hadSearchContext ? [] : findMediaTargets(divRound/* , searchContext.useSearch */); const [prev, next] = !hadSearchContext ? [] : findMediaTargets(divRound, message.mid/* , searchContext.useSearch */);
appMediaPlaybackController.setTargets({peerId: message.peerId, mid: message.mid}, prev, next); appMediaPlaybackController.setTargets({peerId: message.peerId, mid: message.mid}, prev, next);
} }

2
src/helpers/avatarListLoader.ts

@ -17,7 +17,7 @@ export default class AvatarListLoader<Item extends {photoId: Photo.photo['id']}>
loadMore: (anchor, older, loadCount) => { loadMore: (anchor, older, loadCount) => {
if(this.peerId.isAnyChat() || !older) return Promise.resolve({count: 0, items: []}); // ! это значит, что открыло аватар чата, но следующих фотографий нет. if(this.peerId.isAnyChat() || !older) return Promise.resolve({count: 0, items: []}); // ! это значит, что открыло аватар чата, но следующих фотографий нет.
const maxId = anchor?.photoId || this.current?.photoId; const maxId = anchor?.photoId;
return appPhotosManager.getUserPhotos(this.peerId, maxId, loadCount).then(value => { return appPhotosManager.getUserPhotos(this.peerId, maxId, loadCount).then(value => {
const items = value.photos.map(photoId => { const items = value.photos.map(photoId => {
return {element: null as HTMLElement, photoId} as any; return {element: null as HTMLElement, photoId} as any;

10
src/helpers/compareValue.ts

@ -0,0 +1,10 @@
import compareLong from "./long/compareLong";
export default function compareValue(val1: string | number, val2: typeof val1) {
if((val1 as number).toExponential) {
const diff = (val1 as number) - (val2 as number);
return diff < 0 ? -1 : (diff > 0 ? 1 : 0);
}
return compareLong(val1 as string, val2 as string);
}

65
src/helpers/listLoader.ts

@ -66,8 +66,8 @@ export default class ListLoader<T extends {}, P extends {}> {
this.current = undefined; this.current = undefined;
this.previous = []; this.previous = [];
this.next = []; this.next = [];
this.loadedAllUp = this.loadedAllDown = loadedAll; this.setLoaded(true, loadedAll);
this.loadPromiseUp = this.loadPromiseDown = null; this.setLoaded(false, loadedAll);
} }
public go(length: number, dispatchJump = true) { public go(length: number, dispatchJump = true) {
@ -79,15 +79,17 @@ export default class ListLoader<T extends {}, P extends {}> {
return; return;
} }
this.previous.push(this.current, ...items); if(this.current !== undefined) items.unshift(this.current);
this.previous.push(...items);
} else { } else {
items = this.previous.splice(this.previous.length + length, -length); items = this.previous.splice(Math.max(0, this.previous.length + length), -length);
item = items.shift(); item = items.shift();
if(!item) { if(!item) {
return; return;
} }
this.next.unshift(...items, this.current); if(this.current !== undefined) items.push(this.current);
this.next.unshift(...items);
} }
if(this.next.length < this.loadWhenLeft) { if(this.next.length < this.loadWhenLeft) {
@ -103,13 +105,50 @@ export default class ListLoader<T extends {}, P extends {}> {
return this.current; return this.current;
} }
protected unsetCurrent(toPrevious: boolean) {
if(toPrevious) this.previous.push(this.current);
else this.next.unshift(this.current);
this.current = undefined;
}
public goUnsafe(length: number, dispatchJump?: boolean) {
const leftLength = length > 0 ? Math.max(0, length - this.next.length) : Math.min(0, length + this.previous.length);
const item = this.go(length, leftLength ? false : dispatchJump);
/* if(length > 0 ? this.loadedAllUp : this.loadedAllDown) {
this.unsetCurrent(length > 0);
} */
return {
item: !leftLength ? item : undefined,
leftLength
};
}
protected setLoaded(down: boolean, value: boolean) {
const isChanged = (down ? this.loadedAllDown : this.loadedAllUp) !== value;
if(!isChanged) {
return false;
}
if(down) this.loadedAllDown = value;
else this.loadedAllUp = value;
if(!value) {
if(down) this.loadPromiseDown = null;
else this.loadPromiseUp = null;
}
return true;
}
// нет смысла делать проверку для reverse и loadMediaPromise // нет смысла делать проверку для reverse и loadMediaPromise
public load(older: boolean) { public load(older: boolean) {
if(older && this.loadedAllDown) return Promise.resolve(); if(older ? this.loadedAllDown : this.loadedAllUp) return Promise.resolve();
else if(!older && this.loadedAllUp) return Promise.resolve();
if(older && this.loadPromiseDown) return this.loadPromiseDown; let promise = older ? this.loadPromiseDown : this.loadPromiseUp;
else if(!older && this.loadPromiseUp) return this.loadPromiseUp; if(promise) return promise;
let anchor: T; let anchor: T;
if(older) { if(older) {
@ -118,14 +157,14 @@ export default class ListLoader<T extends {}, P extends {}> {
anchor = this.reverse ? this.next[this.next.length - 1] : this.previous[0]; anchor = this.reverse ? this.next[this.next.length - 1] : this.previous[0];
} }
const promise = this.loadMore(anchor, older, this.loadCount).then(result => { anchor ??= this.current;
if((older && this.loadPromiseDown !== promise) || (!older && this.loadPromiseUp !== promise)) { promise = this.loadMore(anchor, older, this.loadCount).then(result => {
if((older ? this.loadPromiseDown : this.loadPromiseUp) !== promise) {
return; return;
} }
if(result.items.length < this.loadCount) { if(result.items.length < this.loadCount) {
if(older) this.loadedAllDown = true; this.setLoaded(older, true);
else this.loadedAllUp = true;
} }
if(this.count === undefined) { if(this.count === undefined) {

25
src/helpers/long/compareLong.ts

@ -0,0 +1,25 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
export default function compareLong(str1: string, str2: string) {
const str1Length = str1.length;
if(str1Length !== str2.length) {
const diff = str1Length - str2.length;
return diff < 0 ? -1 : (diff > 0 ? 1 : 0);
}
const maxPartLength = 15;
for(let i = 0; i < str1Length; i += maxPartLength) {
const v1 = +str1.slice(i, i + maxPartLength);
const v2 = +str2.slice(i, i + maxPartLength);
const diff = v1 - v2;
if(diff) {
return diff;
}
}
return 0;
}

152
src/helpers/searchListLoader.ts

@ -18,14 +18,16 @@ export default class SearchListLoader<Item extends {mid: number, peerId: PeerId}
public searchContext: MediaSearchContext; public searchContext: MediaSearchContext;
public onEmptied: () => void; public onEmptied: () => void;
constructor(options: Omit<ListLoaderOptions<Item, Message.message>, 'loadMore'> & {onEmptied?: () => void} = {}) { private otherSideLoader: SearchListLoader<Item>;
constructor(options: Omit<ListLoaderOptions<Item, Message.message>, 'loadMore'> & {onEmptied?: () => void, isInner?: boolean} = {}) {
super({ super({
...options, ...options,
loadMore: (anchor, older, loadCount) => { loadMore: (anchor, older, loadCount) => {
const backLimit = older ? 0 : loadCount; const backLimit = older ? 0 : loadCount;
let maxId = this.current?.mid; let maxId = anchor?.mid;
if(anchor) maxId = anchor.mid; if(maxId === undefined) maxId = this.searchContext.maxId;
if(!older) maxId = appMessagesIdsManager.incrementMessageId(maxId, 1); if(!older) maxId = appMessagesIdsManager.incrementMessageId(maxId, 1);
return appMessagesManager.getSearch({ return appMessagesManager.getSearch({
@ -63,6 +65,17 @@ export default class SearchListLoader<Item extends {mid: number, peerId: PeerId}
rootScope.addEventListener('history_delete', this.onHistoryDelete); rootScope.addEventListener('history_delete', this.onHistoryDelete);
rootScope.addEventListener('history_multiappend', this.onHistoryMultiappend); rootScope.addEventListener('history_multiappend', this.onHistoryMultiappend);
rootScope.addEventListener('message_sent', this.onMessageSent); rootScope.addEventListener('message_sent', this.onMessageSent);
if(!options.isInner) {
this.otherSideLoader = new SearchListLoader({
...options,
isInner: true
});
// this.otherSideLoader.onLoadedMore = () => {
// };
}
} }
protected filterMids(mids: number[]) { protected filterMids(mids: number[]) {
@ -116,7 +129,26 @@ export default class SearchListLoader<Item extends {mid: number, peerId: PeerId}
const filtered = this.filterMids(sorted); const filtered = this.filterMids(sorted);
const targets = filtered.map(message => this.processItem(message)).filter(Boolean); const targets = filtered.map(message => this.processItem(message)).filter(Boolean);
if(targets.length) { if(targets.length) {
this.next.push(...targets); /* const {previous, current, next} = this;
const targets = previous.concat(current, next);
const currentIdx = targets.length;
const mid = targets[0].mid;
let i = 0, length = targets.length;
for(; i < length; ++i) {
const target = targets[i];
if(!target || mid < target.mid) {
break;
}
}
if(i < currentIdx) previous.push(...targets);
else next. */
if(!this.current) {
this.previous.push(...targets);
} else {
this.next.push(...targets);
}
} }
}; };
@ -141,14 +173,117 @@ export default class SearchListLoader<Item extends {mid: number, peerId: PeerId}
this.loadedAllUp = true; this.loadedAllUp = true;
} }
if(!this.searchContext.useSearch) { // if(!this.searchContext.useSearch) {
this.loadedAllDown = this.loadedAllUp = true; // this.loadedAllDown = this.loadedAllUp = true;
// }
if(this.otherSideLoader) {
this.otherSideLoader.setSearchContext(context);
} }
} }
public reset() { public reset() {
super.reset(); super.reset();
this.searchContext = undefined; this.searchContext = undefined;
if(this.otherSideLoader) {
this.otherSideLoader.reset();
}
}
public getPrevious() {
let previous = this.previous;
if(this.otherSideLoader) {
previous = previous.concat(this.otherSideLoader.previous);
}
return previous;
}
public getNext() {
let next = this.next;
if(this.otherSideLoader) {
next = next.concat(this.otherSideLoader.next);
}
return next;
}
public getCurrent() {
return this.current || this.otherSideLoader?.current;
}
private goToOtherEnd(length: number) {
if(length > 0) return this.go(-this.previous.length);
else return this.go(this.next.length);
}
public goRound(length: number, dispatchJump?: boolean) {
let ret: ReturnType<SearchListLoader<Item>['goUnsafe']>;
if(this.otherSideLoader?.current) {
ret = this.otherSideLoader.goUnsafe(length, dispatchJump);
if(ret.item) {
return ret.item;
}
length = ret.leftLength;
if(!(length > 0 ? this.otherSideLoader.next : this.otherSideLoader.previous).length) {
const loaded = length > 0 ? this.otherSideLoader.loadedAllUp : this.otherSideLoader.loadedAllDown;
if(!loaded) { // do not reset anything until it's loaded
return;
}
// if other side is loaded too will start from its begin
if((length > 0 && (this.otherSideLoader.searchContext.maxId === 1 || this.otherSideLoader.loadedAllDown)) ||
(length < 0 && (this.otherSideLoader.searchContext.maxId === 0 || this.otherSideLoader.loadedAllUp))) {
return this.otherSideLoader.goToOtherEnd(length);
}
this.otherSideLoader.unsetCurrent(length > 0);
}
}
ret = this.goUnsafe(length, dispatchJump);
if(!ret.item) {
if(this.loadedAllUp && this.loadedAllDown) { // just use the same loader if the list is too short
return this.goToOtherEnd(length);
} else if(this.otherSideLoader) {
length = ret.leftLength;
ret = this.otherSideLoader.goUnsafe(length, dispatchJump);
if(ret.item) {
this.unsetCurrent(length > 0);
}
}
}
return ret?.item;
}
// public setTargets(previous: Item[], next: Item[], reverse: boolean) {
// super.setTargets(previous, next, reverse);
// }
protected setLoaded(down: boolean, value: boolean) {
const changed = super.setLoaded(down, value);
if(changed && this.otherSideLoader && value/* && (this.reverse ? this.loadedAllUp : this.loadedAllDown) */) {
const reverse = this.loadedAllUp;
this.otherSideLoader.setSearchContext({
...this.searchContext,
maxId: reverse ? 1 : 0
});
// these 'reverse' are different, not a mistake here.
this.otherSideLoader.reverse = this.reverse;
this.otherSideLoader.setLoaded(reverse, true);
this.otherSideLoader.load(!reverse);
}
return changed;
} }
public cleanup() { public cleanup() {
@ -157,5 +292,10 @@ export default class SearchListLoader<Item extends {mid: number, peerId: PeerId}
rootScope.removeEventListener('history_multiappend', this.onHistoryMultiappend); rootScope.removeEventListener('history_multiappend', this.onHistoryMultiappend);
rootScope.removeEventListener('message_sent', this.onMessageSent); rootScope.removeEventListener('message_sent', this.onMessageSent);
this.onEmptied = undefined; this.onEmptied = undefined;
if(this.otherSideLoader) {
this.otherSideLoader.cleanup();
this.otherSideLoader = undefined;
}
} }
} }

54
src/helpers/slicedArray.ts

@ -5,12 +5,13 @@
*/ */
import { MOUNT_CLASS_TO } from "../config/debug"; import { MOUNT_CLASS_TO } from "../config/debug";
import compareValue from "./compareValue";
/** /**
* Descend sorted storage * Descend sorted storage
*/ */
type ItemType = number; type ItemType = number | string;
export enum SliceEnd { export enum SliceEnd {
None = 0, None = 0,
@ -19,7 +20,7 @@ export enum SliceEnd {
Both = SliceEnd.Top | SliceEnd.Bottom Both = SliceEnd.Top | SliceEnd.Bottom
}; };
export interface Slice extends Array<ItemType> { export interface Slice<T extends ItemType> extends Array<T> {
//slicedArray: SlicedArray; //slicedArray: SlicedArray;
end: SliceEnd; end: SliceEnd;
@ -27,17 +28,18 @@ export interface Slice extends Array<ItemType> {
setEnd: (side: SliceEnd) => void; setEnd: (side: SliceEnd) => void;
unsetEnd: (side: SliceEnd) => void; unsetEnd: (side: SliceEnd) => void;
slice: (from?: number, to?: number) => Slice; slice: (from?: number, to?: number) => Slice<T>;
splice: (start: number, deleteCount: number, ...items: ItemType[]) => Slice; splice: (start: number, deleteCount: number, ...items: ItemType[]) => Slice<T>;
} }
export interface SliceConstructor { export interface SliceConstructor<T extends ItemType> {
new(...items: ItemType[]): Slice; // new(...items: T[]): Slice<T>;
new(length: number): Slice<T>;
} }
export default class SlicedArray { export default class SlicedArray<T extends ItemType> {
private slices: Slice[]/* = [[7,6,5],[4,3,2],[1,0,-1]] */; private slices: Slice<T>[]/* = [[7,6,5],[4,3,2],[1,0,-1]] */;
private sliceConstructor: SliceConstructor; private sliceConstructor: SliceConstructor<T>;
constructor() { constructor() {
// @ts-ignore // @ts-ignore
@ -48,8 +50,8 @@ export default class SlicedArray {
this.slices = [first]; this.slices = [first];
} }
private static getSliceConstructor(slicedArray: SlicedArray) { private static getSliceConstructor(slicedArray: SlicedArray<ItemType>) {
return class Slice extends Array<ItemType> implements Slice { return class Slice<T> extends Array<ItemType> implements Slice<T> {
//slicedArray: SlicedArray; //slicedArray: SlicedArray;
end: SliceEnd = SliceEnd.None; end: SliceEnd = SliceEnd.None;
@ -95,7 +97,7 @@ export default class SlicedArray {
const ret = super.splice(start, deleteCount, ...items); const ret = super.splice(start, deleteCount, ...items);
if(!this.length) { if(!this.length) {
const slices = slicedArray.slices as number[][]; const slices = slicedArray.slices as ItemType[][];
const idx = slices.indexOf(this); const idx = slices.indexOf(this);
if(idx !== -1) { if(idx !== -1) {
if(slices.length === 1) { // left empty slice without ends if(slices.length === 1) { // left empty slice without ends
@ -111,7 +113,7 @@ export default class SlicedArray {
} }
} }
public constructSlice(...items: ItemType[]) { public constructSlice(...items: T[]) {
//const slice = new Slice(this, ...items); //const slice = new Slice(this, ...items);
// can't pass items directly to constructor because first argument is length // can't pass items directly to constructor because first argument is length
const slice = new this.sliceConstructor(items.length); const slice = new this.sliceConstructor(items.length);
@ -166,7 +168,7 @@ export default class SlicedArray {
*/ */
} }
public insertSlice(slice: ItemType[], flatten = true) { public insertSlice(slice: T[], flatten = true) {
if(!slice.length) { if(!slice.length) {
return; return;
} }
@ -180,7 +182,7 @@ export default class SlicedArray {
const lowerBound = slice[slice.length - 1]; const lowerBound = slice[slice.length - 1];
const upperBound = slice[0]; const upperBound = slice[0];
let foundSlice: Slice, lowerIndex = -1, upperIndex = -1, foundSliceIndex = 0; let foundSlice: Slice<T>, lowerIndex = -1, upperIndex = -1, foundSliceIndex = 0;
for(; foundSliceIndex < this.slices.length; ++foundSliceIndex) { for(; foundSliceIndex < this.slices.length; ++foundSliceIndex) {
foundSlice = this.slices[foundSliceIndex]; foundSlice = this.slices[foundSliceIndex];
lowerIndex = foundSlice.indexOf(lowerBound); lowerIndex = foundSlice.indexOf(lowerBound);
@ -205,7 +207,7 @@ export default class SlicedArray {
let insertIndex = 0; let insertIndex = 0;
for(const length = this.slices.length; insertIndex < length; ++insertIndex) { // * maybe should iterate from the end, could be faster ? for(const length = this.slices.length; insertIndex < length; ++insertIndex) { // * maybe should iterate from the end, could be faster ?
const s = this.slices[insertIndex]; const s = this.slices[insertIndex];
if(slice[0] > s[0]) { if(compareValue(slice[0], s[0]) === 1) {
break; break;
} }
} }
@ -263,7 +265,7 @@ export default class SlicedArray {
return this.slice.length; return this.slice.length;
} }
public findSlice(item: ItemType) { public findSlice(item: T) {
for(let i = 0, length = this.slices.length; i < length; ++i) { for(let i = 0, length = this.slices.length; i < length; ++i) {
const slice = this.slices[i]; const slice = this.slices[i];
const index = slice.indexOf(item); const index = slice.indexOf(item);
@ -275,8 +277,8 @@ export default class SlicedArray {
return undefined; return undefined;
} }
public findSliceOffset(maxId: number) { public findSliceOffset(maxId: T) {
let slice: Slice; let slice: Slice<T>;
for(let i = 0; i < this.slices.length; ++i) { for(let i = 0; i < this.slices.length; ++i) {
let offset = 0; let offset = 0;
slice = this.slices[i]; slice = this.slices[i];
@ -284,8 +286,8 @@ export default class SlicedArray {
continue; continue;
} }
for(; offset < slice.length; offset++) { for(; offset < slice.length; ++offset) {
if(maxId >= slice[offset]) { if(compareValue(maxId, slice[offset]) >= 0) {
/* if(!offset) { // because can't find 3 in [[5,4], [2,1]] /* if(!offset) { // because can't find 3 in [[5,4], [2,1]]
return undefined; return undefined;
} */ } */
@ -309,7 +311,7 @@ export default class SlicedArray {
} }
// * https://core.telegram.org/api/offsets // * https://core.telegram.org/api/offsets
public sliceMe(offsetId: number, add_offset: number, limit: number) { public sliceMe(offsetId: T, add_offset: number, limit: number) {
let slice = this.slice; let slice = this.slice;
let offset = 0; let offset = 0;
let sliceOffset = 0; let sliceOffset = 0;
@ -337,7 +339,7 @@ export default class SlicedArray {
//const fixHalfBackLimit = add_offset && !(limit / add_offset % 2) && (sliceEnd % 2) ? 1 : 0; //const fixHalfBackLimit = add_offset && !(limit / add_offset % 2) && (sliceEnd % 2) ? 1 : 0;
//sliceEnd += fixHalfBackLimit; //sliceEnd += fixHalfBackLimit;
const sliced = slice.slice(sliceStart, sliceEnd) as Slice; const sliced = slice.slice(sliceStart, sliceEnd) as Slice<T>;
const topWasMeantToLoad = add_offset < 0 ? limit + add_offset : limit; const topWasMeantToLoad = add_offset < 0 ? limit + add_offset : limit;
const bottomWasMeantToLoad = Math.abs(add_offset); const bottomWasMeantToLoad = Math.abs(add_offset);
@ -356,7 +358,7 @@ export default class SlicedArray {
}; };
} }
public unshift(...items: ItemType[]) { public unshift(...items: T[]) {
let slice = this.first; let slice = this.first;
if(!slice.length) { if(!slice.length) {
slice.setEnd(SliceEnd.Bottom); slice.setEnd(SliceEnd.Bottom);
@ -369,7 +371,7 @@ export default class SlicedArray {
slice.unshift(...items); slice.unshift(...items);
} }
public push(...items: ItemType[]) { public push(...items: T[]) {
let slice = this.last; let slice = this.last;
if(!slice.length) { if(!slice.length) {
slice.setEnd(SliceEnd.Top); slice.setEnd(SliceEnd.Top);
@ -382,7 +384,7 @@ export default class SlicedArray {
slice.push(...items); slice.push(...items);
} }
public delete(item: ItemType) { public delete(item: T) {
const found = this.findSlice(item); const found = this.findSlice(item);
if(found) { if(found) {
found.slice.splice(found.index, 1); found.slice.splice(found.index, 1);

7
src/lib/appManagers/appImManager.ts

@ -345,6 +345,13 @@ export class AppImManager {
this.toggleChatGradientAnimation(to); this.toggleChatGradientAnimation(to);
}); });
rootScope.addEventListener('service_notification', (update) => {
confirmationPopup({
button: {langKey: 'OK', isCancel: true},
description: RichTextProcessor.wrapRichText(update.message)
});
});
stateStorage.get('chatPositions').then((c) => { stateStorage.get('chatPositions').then((c) => {
stateStorage.setToCache('chatPositions', c || {}); stateStorage.setToCache('chatPositions', c || {});
}); });

22
src/lib/appManagers/appMessagesManager.ts

@ -80,7 +80,7 @@ const DO_NOT_READ_HISTORY = false;
export type HistoryStorage = { export type HistoryStorage = {
count: number | null, count: number | null,
history: SlicedArray, history: SlicedArray<number>,
maxId?: number, maxId?: number,
readPromise?: Promise<void>, readPromise?: Promise<void>,
@ -94,7 +94,7 @@ export type HistoryStorage = {
export type HistoryResult = { export type HistoryResult = {
count: number, count: number,
history: Slice, history: Slice<number>,
offsetIdOffset?: number, offsetIdOffset?: number,
}; };
@ -220,7 +220,7 @@ export class AppMessagesManager {
private middleware: ReturnType<typeof getMiddleware>; private middleware: ReturnType<typeof getMiddleware>;
private unreadMentions: {[peerId: PeerId]: SlicedArray} = {}; private unreadMentions: {[peerId: PeerId]: SlicedArray<number>} = {};
private goToNextMentionPromises: {[peerId: PeerId]: Promise<any>} = {}; private goToNextMentionPromises: {[peerId: PeerId]: Promise<any>} = {};
private batchUpdates: { private batchUpdates: {
@ -3901,7 +3901,7 @@ export class AppMessagesManager {
let storage: { let storage: {
count?: number; count?: number;
history: SlicedArray; history: SlicedArray<number>;
}; };
// * костыль для limit 1, если нужно и получить сообщение, и узнать количество сообщений // * костыль для limit 1, если нужно и получить сообщение, и узнать количество сообщений
@ -4314,7 +4314,7 @@ export class AppMessagesManager {
} }
} }
private fixUnreadMentionsCountIfNeeded(peerId: PeerId, slicedArray: SlicedArray) { private fixUnreadMentionsCountIfNeeded(peerId: PeerId, slicedArray: SlicedArray<number>) {
const dialog = this.getDialogOnly(peerId); const dialog = this.getDialogOnly(peerId);
if(!slicedArray.length && dialog?.unread_mentions_count) { if(!slicedArray.length && dialog?.unread_mentions_count) {
this.reloadConversation(peerId); this.reloadConversation(peerId);
@ -5117,6 +5117,11 @@ export class AppMessagesManager {
private onUpdateServiceNotification = (update: Update.updateServiceNotification) => { private onUpdateServiceNotification = (update: Update.updateServiceNotification) => {
//this.log('updateServiceNotification', update); //this.log('updateServiceNotification', update);
if(update.pFlags?.popup) {
rootScope.dispatchEvent('service_notification', update);
return;
}
const fromId = SERVICE_PEER_ID; const fromId = SERVICE_PEER_ID;
const peerId = fromId; const peerId = fromId;
const messageId = this.generateTempMessageId(peerId); const messageId = this.generateTempMessageId(peerId);
@ -5613,6 +5618,11 @@ export class AppMessagesManager {
notificationMessage = I18n.format('Notifications.New', true); notificationMessage = I18n.format('Notifications.New', true);
} }
if(options.userReaction) {
notification.noIncrement = true;
notification.silent = true;
}
notification.title = appPeersManager.getPeerTitle(peerId, true); notification.title = appPeersManager.getPeerTitle(peerId, true);
if(isAnyChat && message.fromId !== message.peerId) { if(isAnyChat && message.fromId !== message.peerId) {
notification.title = appPeersManager.getPeerTitle(message.fromId, true) + notification.title = appPeersManager.getPeerTitle(message.fromId, true) +
@ -5848,7 +5858,7 @@ export class AppMessagesManager {
return {count, offsetIdOffset, isTopEnd, isBottomEnd}; return {count, offsetIdOffset, isTopEnd, isBottomEnd};
} }
public mergeHistoryResult(slicedArray: SlicedArray, public mergeHistoryResult(slicedArray: SlicedArray<number>,
historyResult: Parameters<AppMessagesManager['isHistoryResultEnd']>[0], historyResult: Parameters<AppMessagesManager['isHistoryResultEnd']>[0],
offset_id: number, offset_id: number,
limit: number, limit: number,

6
src/lib/appManagers/appNotificationsManager.ts

@ -45,6 +45,7 @@ export type NotifyOptions = Partial<{
message: string; message: string;
silent: boolean; silent: boolean;
onclick: () => void; onclick: () => void;
noIncrement: boolean;
}>; }>;
export type NotificationSettings = { export type NotificationSettings = {
@ -595,7 +596,10 @@ export class AppNotificationsManager {
} }
// console.log('notify image', data.image) // console.log('notify image', data.image)
this.notificationsCount++; if(!data.noIncrement) {
++this.notificationsCount;
}
if(!this.titleInterval) { if(!this.titleInterval) {
this.toggleToggler(); this.toggleToggler();
} }

7
src/lib/calls/callInstance.ts

@ -357,6 +357,13 @@ export default class CallInstance extends CallInstanceBase<{
}); });
}).then(phonePhoneCall => { }).then(phonePhoneCall => {
this.appCallsManager.savePhonePhoneCall(phonePhoneCall); this.appCallsManager.savePhonePhoneCall(phonePhoneCall);
}).catch(err => {
this.log.error('accept call error', err);
// if(err.type === 'CALL_PROTOCOL_COMPAT_LAYER_INVALID') {
// }
this.hangUp('phoneCallDiscardReasonHangup');
}); });
} }

6
src/lib/rootScope.ts

@ -96,7 +96,7 @@ export type BroadcastEvents = {
'media_play': {doc: MyDocument, message: Message.message, media: HTMLMediaElement}, 'media_play': {doc: MyDocument, message: Message.message, media: HTMLMediaElement},
'media_pause': void, 'media_pause': void,
'media_playback_params': {volume: number, muted: boolean, playbackRate: number}, 'media_playback_params': {volume: number, muted: boolean, playbackRate: number, loop: boolean, round: boolean},
'media_stop': void, 'media_stop': void,
'state_cleared': void, 'state_cleared': void,
@ -164,7 +164,9 @@ export type BroadcastEvents = {
'quick_reaction': string, 'quick_reaction': string,
'missed_reactions_element': {message: Message.message, changedResults: ReactionCount[]} 'missed_reactions_element': {message: Message.message, changedResults: ReactionCount[]},
'service_notification': Update.updateServiceNotification
}; };
export class RootScope extends EventListenerBase<{ export class RootScope extends EventListenerBase<{

4
src/scss/partials/_chat.scss

@ -354,7 +354,7 @@ $background-transition-total-time: #{$input-transition-time - $background-transi
// } // }
&.send .tgico-send, &.send .tgico-send,
&.record .tgico-microphone, &.record .tgico-microphone_filled,
&.edit .tgico-check, &.edit .tgico-check,
&.schedule .tgico-schedule { &.schedule .tgico-schedule {
visibility: visible !important; visibility: visible !important;
@ -368,7 +368,7 @@ $background-transition-total-time: #{$input-transition-time - $background-transi
@include animation-level(2) { @include animation-level(2) {
&.send .tgico-send, &.send .tgico-send,
&.record .tgico-microphone, &.record .tgico-microphone_filled,
&.edit .tgico-check, &.edit .tgico-check,
&.schedule .tgico-schedule { &.schedule .tgico-schedule {
animation: grow-icon .4s forwards ease-in-out !important; animation: grow-icon .4s forwards ease-in-out !important;

51
src/scss/tgico/_style.scss

@ -3,9 +3,9 @@
@font-face { @font-face {
font-family: '#{$tgico-font-family}'; font-family: '#{$tgico-font-family}';
src: src:
url('#{$tgico-font-path}/#{$tgico-font-family}.ttf?cyy67r') format('truetype'), url('#{$tgico-font-path}/#{$tgico-font-family}.ttf?1mzumm') format('truetype'),
url('#{$tgico-font-path}/#{$tgico-font-family}.woff?cyy67r') format('woff'), url('#{$tgico-font-path}/#{$tgico-font-family}.woff?1mzumm') format('woff'),
url('#{$tgico-font-path}/#{$tgico-font-family}.svg?cyy67r##{$tgico-font-family}') format('svg'); url('#{$tgico-font-path}/#{$tgico-font-family}.svg?1mzumm##{$tgico-font-family}') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: block; font-display: block;
@ -332,11 +332,6 @@
content: $tgico-email; content: $tgico-email;
} }
} }
.tgico-endcall {
&:before {
content: $tgico-endcall;
}
}
.tgico-endcall_filled { .tgico-endcall_filled {
&:before { &:before {
content: $tgico-endcall_filled; content: $tgico-endcall_filled;
@ -377,6 +372,11 @@
content: $tgico-flag; content: $tgico-flag;
} }
} }
.tgico-flip {
&:before {
content: $tgico-flip;
}
}
.tgico-folder { .tgico-folder {
&:before { &:before {
content: $tgico-folder; content: $tgico-folder;
@ -547,6 +547,21 @@
content: $tgico-microphone; content: $tgico-microphone;
} }
} }
.tgico-microphone_crossed {
&:before {
content: $tgico-microphone_crossed;
}
}
.tgico-microphone_crossed_filled {
&:before {
content: $tgico-microphone_crossed_filled;
}
}
.tgico-microphone_filled {
&:before {
content: $tgico-microphone_filled;
}
}
.tgico-minus { .tgico-minus {
&:before { &:before {
content: $tgico-minus; content: $tgico-minus;
@ -737,6 +752,16 @@
content: $tgico-rightpanel; content: $tgico-rightpanel;
} }
} }
.tgico-rotate_left {
&:before {
content: $tgico-rotate_left;
}
}
.tgico-rotate_right {
&:before {
content: $tgico-rotate_right;
}
}
.tgico-saved { .tgico-saved {
&:before { &:before {
content: $tgico-saved; content: $tgico-saved;
@ -802,6 +827,11 @@
content: $tgico-sharescreen_filled; content: $tgico-sharescreen_filled;
} }
} }
.tgico-shuffle {
&:before {
content: $tgico-shuffle;
}
}
.tgico-smallscreen { .tgico-smallscreen {
&:before { &:before {
content: $tgico-smallscreen; content: $tgico-smallscreen;
@ -897,6 +927,11 @@
content: $tgico-videocamera; content: $tgico-videocamera;
} }
} }
.tgico-videocamera_crossed_filled {
&:before {
content: $tgico-videocamera_crossed_filled;
}
}
.tgico-videocamera_filled { .tgico-videocamera_filled {
&:before { &:before {
content: $tgico-videocamera_filled; content: $tgico-videocamera_filled;

181
src/scss/tgico/_variables.scss

@ -58,15 +58,15 @@ $tgico-dragmedia: "\e938";
$tgico-eats: "\e939"; $tgico-eats: "\e939";
$tgico-edit: "\e93a"; $tgico-edit: "\e93a";
$tgico-email: "\e93b"; $tgico-email: "\e93b";
$tgico-endcall: "\e93c"; $tgico-endcall_filled: "\e93c";
$tgico-endcall_filled: "\e93d"; $tgico-enter: "\e93d";
$tgico-enter: "\e93e"; $tgico-eye1: "\e93e";
$tgico-eye1: "\e93f"; $tgico-eye2: "\e93f";
$tgico-eye2: "\e940"; $tgico-fast_forward: "\e940";
$tgico-fast_forward: "\e941"; $tgico-fast_rewind: "\e941";
$tgico-fast_rewind: "\e942"; $tgico-favourites: "\e942";
$tgico-favourites: "\e943"; $tgico-flag: "\e943";
$tgico-flag: "\e944"; $tgico-flip: "\e944";
$tgico-folder: "\e945"; $tgico-folder: "\e945";
$tgico-fontsize: "\e946"; $tgico-fontsize: "\e946";
$tgico-forward: "\e947"; $tgico-forward: "\e947";
@ -101,82 +101,89 @@ $tgico-menu: "\e963";
$tgico-message: "\e964"; $tgico-message: "\e964";
$tgico-messageunread: "\e965"; $tgico-messageunread: "\e965";
$tgico-microphone: "\e966"; $tgico-microphone: "\e966";
$tgico-minus: "\e967"; $tgico-microphone_crossed: "\e967";
$tgico-monospace: "\e968"; $tgico-microphone_crossed_filled: "\e968";
$tgico-more: "\e969"; $tgico-microphone_filled: "\e969";
$tgico-mute: "\e96a"; $tgico-minus: "\e96a";
$tgico-muted: "\e96b"; $tgico-monospace: "\e96b";
$tgico-newchannel: "\e96c"; $tgico-more: "\e96c";
$tgico-newchat_filled: "\e96d"; $tgico-mute: "\e96d";
$tgico-newgroup: "\e96e"; $tgico-muted: "\e96e";
$tgico-newprivate: "\e96f"; $tgico-newchannel: "\e96f";
$tgico-next: "\e970"; $tgico-newchat_filled: "\e970";
$tgico-noncontacts: "\e971"; $tgico-newgroup: "\e971";
$tgico-nosound: "\e972"; $tgico-newprivate: "\e972";
$tgico-passwordoff: "\e973"; $tgico-next: "\e973";
$tgico-pause: "\e974"; $tgico-noncontacts: "\e974";
$tgico-permissions: "\e975"; $tgico-nosound: "\e975";
$tgico-phone: "\e976"; $tgico-passwordoff: "\e976";
$tgico-pin: "\e977"; $tgico-pause: "\e977";
$tgico-pinlist: "\e978"; $tgico-permissions: "\e978";
$tgico-pinned_filled: "\e979"; $tgico-phone: "\e979";
$tgico-pinnedchat: "\e97a"; $tgico-pin: "\e97a";
$tgico-pip: "\e97b"; $tgico-pinlist: "\e97b";
$tgico-play: "\e97c"; $tgico-pinned_filled: "\e97c";
$tgico-playback_05: "\e97d"; $tgico-pinnedchat: "\e97d";
$tgico-playback_15: "\e97e"; $tgico-pip: "\e97e";
$tgico-playback_1x: "\e97f"; $tgico-play: "\e97f";
$tgico-playback_2x: "\e980"; $tgico-playback_05: "\e980";
$tgico-plus: "\e981"; $tgico-playback_15: "\e981";
$tgico-poll: "\e982"; $tgico-playback_1x: "\e982";
$tgico-previous: "\e983"; $tgico-playback_2x: "\e983";
$tgico-radiooff: "\e984"; $tgico-plus: "\e984";
$tgico-radioon: "\e985"; $tgico-poll: "\e985";
$tgico-reactions: "\e986"; $tgico-previous: "\e986";
$tgico-readchats: "\e987"; $tgico-radiooff: "\e987";
$tgico-recent: "\e988"; $tgico-radioon: "\e988";
$tgico-replace: "\e989"; $tgico-reactions: "\e989";
$tgico-reply: "\e98a"; $tgico-readchats: "\e98a";
$tgico-reply_filled: "\e98b"; $tgico-recent: "\e98b";
$tgico-rightpanel: "\e98c"; $tgico-replace: "\e98c";
$tgico-saved: "\e98d"; $tgico-reply: "\e98d";
$tgico-savedmessages: "\e98e"; $tgico-reply_filled: "\e98e";
$tgico-schedule: "\e98f"; $tgico-rightpanel: "\e98f";
$tgico-scheduled: "\e990"; $tgico-rotate_left: "\e990";
$tgico-search: "\e991"; $tgico-rotate_right: "\e991";
$tgico-select: "\e992"; $tgico-saved: "\e992";
$tgico-send: "\e993"; $tgico-savedmessages: "\e993";
$tgico-send2: "\e994"; $tgico-schedule: "\e994";
$tgico-sending: "\e995"; $tgico-scheduled: "\e995";
$tgico-sendingerror: "\e996"; $tgico-search: "\e996";
$tgico-settings: "\e997"; $tgico-select: "\e997";
$tgico-settings_filled: "\e998"; $tgico-send: "\e998";
$tgico-sharescreen_filled: "\e999"; $tgico-send2: "\e999";
$tgico-smallscreen: "\e99a"; $tgico-sending: "\e99a";
$tgico-smile: "\e99b"; $tgico-sendingerror: "\e99b";
$tgico-spoiler: "\e99c"; $tgico-settings: "\e99c";
$tgico-sport: "\e99d"; $tgico-settings_filled: "\e99d";
$tgico-stickers: "\e99e"; $tgico-sharescreen_filled: "\e99e";
$tgico-stop: "\e99f"; $tgico-shuffle: "\e99f";
$tgico-strikethrough: "\e9a0"; $tgico-smallscreen: "\e9a0";
$tgico-textedit: "\e9a1"; $tgico-smile: "\e9a1";
$tgico-tip: "\e9a2"; $tgico-spoiler: "\e9a2";
$tgico-tools: "\e9a3"; $tgico-sport: "\e9a3";
$tgico-unarchive: "\e9a4"; $tgico-stickers: "\e9a4";
$tgico-underline: "\e9a5"; $tgico-stop: "\e9a5";
$tgico-unmute: "\e9a6"; $tgico-strikethrough: "\e9a6";
$tgico-unpin: "\e9a7"; $tgico-textedit: "\e9a7";
$tgico-unread: "\e9a8"; $tgico-tip: "\e9a8";
$tgico-up: "\e9a9"; $tgico-tools: "\e9a9";
$tgico-user: "\e9aa"; $tgico-unarchive: "\e9aa";
$tgico-username: "\e9ab"; $tgico-underline: "\e9ab";
$tgico-videocamera: "\e9ac"; $tgico-unmute: "\e9ac";
$tgico-videocamera_filled: "\e9ad"; $tgico-unpin: "\e9ad";
$tgico-videochat: "\e9ae"; $tgico-unread: "\e9ae";
$tgico-volume_down: "\e9af"; $tgico-up: "\e9af";
$tgico-volume_mute: "\e9b0"; $tgico-user: "\e9b0";
$tgico-volume_off: "\e9b1"; $tgico-username: "\e9b1";
$tgico-volume_up: "\e9b2"; $tgico-videocamera: "\e9b2";
$tgico-zoomin: "\e9b3"; $tgico-videocamera_crossed_filled: "\e9b3";
$tgico-zoomout: "\e9b4"; $tgico-videocamera_filled: "\e9b4";
$tgico-videochat: "\e9b5";
$tgico-volume_down: "\e9b6";
$tgico-volume_mute: "\e9b7";
$tgico-volume_off: "\e9b8";
$tgico-volume_up: "\e9b9";
$tgico-zoomin: "\e9ba";
$tgico-zoomout: "\e9bb";

22
src/tests/slicedArray.test.ts

@ -7,23 +7,27 @@ test('Slicing returns new Slice', () => {
}); });
describe('Inserting', () => { describe('Inserting', () => {
const sliced = new SlicedArray(); const sliced = new SlicedArray<typeof arr[0]>();
// @ts-ignore // @ts-ignore
const slices = sliced.slices; const slices = sliced.slices;
const toSomething = (v: number) => {
return '' + v;
};
const arr = [100, 99, 98, 97, 96, 95]; const arr = [100, 99, 98, 97, 96, 95].map(toSomething);
const distantArr = arr.slice(-2).map(v => v - 2); const distantArr = arr.slice(-2).map(v => toSomething(+v - 2));
const missingArr = [arr[arr.length - 1], arr[arr.length - 1] - 1, distantArr[0]]; const missingArr = [arr[arr.length - 1], toSomething(+arr[arr.length - 1] - 1), distantArr[1]];
const startValue = 90; const startValue = toSomething(90);
const values: number[] = []; const values: typeof arr = [];
const valuesPerArray = 3; const valuesPerArray = 3;
const totalArrays = 10; const totalArrays = 10;
for(let i = 0, length = valuesPerArray * totalArrays; i < length; ++i) { for(let i = 0, length = valuesPerArray * totalArrays; i < length; ++i) {
values.push(startValue - i); values.push(toSomething(+startValue - i));
} }
const arrays: number[][] = []; const arrays: (typeof values)[] = [];
for(let i = 0; i < totalArrays; ++i) { for(let i = 0; i < totalArrays; ++i) {
arrays.push(values.slice(valuesPerArray * i, valuesPerArray * (i + 1))); arrays.push(values.slice(valuesPerArray * i, valuesPerArray * (i + 1)));
} }
@ -57,7 +61,7 @@ describe('Inserting', () => {
expect(slices.length).toEqual(length - 1); expect(slices.length).toEqual(length - 1);
}); });
let returnedSlice: Slice; let returnedSlice: Slice<typeof arr[0]>;
test('Insert arrays with gap & join them', () => { test('Insert arrays with gap & join them', () => {
slices[0].length = 0; slices[0].length = 0;

Loading…
Cancel
Save