Call incompatibility notification
Music replay & loop Fix incrementing notification by reaction
This commit is contained in:
parent
0753649f02
commit
bab69c170a
@ -58,7 +58,7 @@ type MediaDetails = {
|
||||
|
||||
export type PlaybackMediaType = 'voice' | 'video' | 'audio';
|
||||
|
||||
class AppMediaPlaybackController {
|
||||
export class AppMediaPlaybackController {
|
||||
private container: HTMLElement;
|
||||
private media: Map<PeerId, Map<number, HTMLMediaElement>> = new Map();
|
||||
private scheduled: AppMediaPlaybackController['media'] = new Map();
|
||||
@ -78,9 +78,13 @@ class AppMediaPlaybackController {
|
||||
public volume: number;
|
||||
public muted: boolean;
|
||||
public playbackRate: number;
|
||||
public loop: boolean;
|
||||
public round: boolean;
|
||||
private _volume = 1;
|
||||
private _muted = false;
|
||||
private _playbackRate = 1;
|
||||
private _loop = false;
|
||||
private _round = false;
|
||||
private lockedSwitchers: boolean;
|
||||
private playbackRates: Record<PlaybackMediaType, number> = {
|
||||
voice: 1,
|
||||
@ -128,7 +132,9 @@ class AppMediaPlaybackController {
|
||||
const keys = [
|
||||
'volume' as const,
|
||||
'muted' as const,
|
||||
'playbackRate' as const
|
||||
'playbackRate' as const,
|
||||
'loop' as const,
|
||||
'round' as const
|
||||
];
|
||||
keys.forEach(key => {
|
||||
const _key = ('_' + key) as `_${typeof key}`;
|
||||
@ -141,7 +147,7 @@ class AppMediaPlaybackController {
|
||||
|
||||
// @ts-ignore
|
||||
this[_key] = value;
|
||||
if(this.playingMedia) {
|
||||
if(this.playingMedia && (key !== 'loop' || this.playingMediaType === 'audio') && key !== 'round') {
|
||||
// @ts-ignore
|
||||
this.playingMedia[key] = value;
|
||||
}
|
||||
@ -158,12 +164,20 @@ class AppMediaPlaybackController {
|
||||
}
|
||||
|
||||
private dispatchPlaybackParams() {
|
||||
const {volume, muted, playbackRate} = this;
|
||||
rootScope.dispatchEvent('media_playback_params', {
|
||||
volume, muted, playbackRate
|
||||
});
|
||||
rootScope.dispatchEvent('media_playback_params', this.getPlaybackParams());
|
||||
}
|
||||
|
||||
public getPlaybackParams() {
|
||||
const {volume, muted, playbackRate, loop, round} = this;
|
||||
return {
|
||||
volume,
|
||||
muted,
|
||||
playbackRate,
|
||||
loop,
|
||||
round
|
||||
};
|
||||
}
|
||||
|
||||
public seekBackward = (details: MediaSessionActionDetails) => {
|
||||
const media = this.playingMedia;
|
||||
if(media) {
|
||||
@ -302,6 +316,10 @@ class AppMediaPlaybackController {
|
||||
|
||||
if(this.playingMedia === media) {
|
||||
media.playbackRate = this.playbackRate;
|
||||
|
||||
if(doc.type === 'audio') {
|
||||
media.loop = this.loop;
|
||||
}
|
||||
}
|
||||
// }, doc.supportsStreaming ? 500e3 : 0);
|
||||
|
||||
@ -494,15 +512,19 @@ class AppMediaPlaybackController {
|
||||
const previousMedia = this.playingMedia;
|
||||
if(previousMedia !== media) {
|
||||
this.stop();
|
||||
this.setMedia(media, message);
|
||||
|
||||
const verify = (element: MediaItem) => element.mid === mid && element.peerId === peerId;
|
||||
if(!this.listLoader.current || !verify(this.listLoader.current)) {
|
||||
let idx = this.listLoader.previous.findIndex(verify);
|
||||
const current = this.listLoader.getCurrent();
|
||||
if(!current || !verify(current)) {
|
||||
const previous = this.listLoader.getPrevious();
|
||||
|
||||
let idx = previous.findIndex(verify);
|
||||
let jumpLength: number;
|
||||
if(idx !== -1) {
|
||||
jumpLength = -(this.listLoader.previous.length - idx);
|
||||
jumpLength = -(previous.length - idx);
|
||||
} else {
|
||||
idx = this.listLoader.next.findIndex(verify);
|
||||
idx = this.listLoader.getNext().findIndex(verify);
|
||||
if(idx !== -1) {
|
||||
jumpLength = idx + 1;
|
||||
}
|
||||
@ -510,14 +532,12 @@ class AppMediaPlaybackController {
|
||||
|
||||
if(idx !== -1) {
|
||||
if(jumpLength) {
|
||||
this.listLoader.go(jumpLength, false);
|
||||
this.go(jumpLength, false);
|
||||
}
|
||||
} else {
|
||||
this.setTargets({peerId, mid});
|
||||
}
|
||||
}
|
||||
|
||||
this.setMedia(media, message);
|
||||
}
|
||||
|
||||
// audio_pause не успеет сработать без таймаута
|
||||
@ -540,7 +560,8 @@ class AppMediaPlaybackController {
|
||||
return {
|
||||
doc: appMessagesManager.getMediaFromMessage(message),
|
||||
message,
|
||||
media: playingMedia
|
||||
media: playingMedia,
|
||||
playbackParams: this.getPlaybackParams()
|
||||
};
|
||||
}
|
||||
|
||||
@ -564,7 +585,10 @@ class AppMediaPlaybackController {
|
||||
|
||||
//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();
|
||||
rootScope.dispatchEvent('media_stop');
|
||||
}
|
||||
@ -654,19 +678,32 @@ class AppMediaPlaybackController {
|
||||
}, 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 = () => {
|
||||
return !this.lockedSwitchers && this.listLoader.go(1);
|
||||
return this.go(1);
|
||||
};
|
||||
|
||||
public previous = () => {
|
||||
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;
|
||||
this.toggle(true);
|
||||
return;
|
||||
}
|
||||
|
||||
return !this.lockedSwitchers && this.listLoader.go(-1);
|
||||
return this.go(-1);
|
||||
};
|
||||
|
||||
public willBePlayed(media: HTMLMediaElement) {
|
||||
@ -746,6 +783,10 @@ class AppMediaPlaybackController {
|
||||
this.playingMedia.muted = this.muted;
|
||||
this.playingMedia.playbackRate = this.playbackRate;
|
||||
|
||||
if(mediaType === 'audio') {
|
||||
this.playingMedia.loop = this.loop;
|
||||
}
|
||||
|
||||
if('mediaSession' in navigator) {
|
||||
this.setNewMediadata(message);
|
||||
}
|
||||
|
@ -362,7 +362,7 @@ function constructDownloadPreloader(tryAgainOnFail = true) {
|
||||
return preloader;
|
||||
}
|
||||
|
||||
export const findMediaTargets = (anchor: HTMLElement/* , useSearch: boolean */) => {
|
||||
export const findMediaTargets = (anchor: HTMLElement, anchorMid: number/* , useSearch: boolean */) => {
|
||||
let prev: MediaItem[], next: MediaItem[];
|
||||
// if(anchor.classList.contains('search-super-item') || !useSearch) {
|
||||
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];
|
||||
};
|
||||
|
||||
@ -490,7 +496,7 @@ export default class AudioElement extends HTMLElement {
|
||||
inputFilter: {_: 'inputMessagesFilterEmpty'},
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager";
|
||||
import type ChatTopbar from "./topbar";
|
||||
import rootScope from "../../lib/rootScope";
|
||||
import appMediaPlaybackController from "../appMediaPlaybackController";
|
||||
import appMediaPlaybackController, { AppMediaPlaybackController } from "../appMediaPlaybackController";
|
||||
import DivAndCaption from "../divAndCaption";
|
||||
import PinnedContainer from "./pinnedContainer";
|
||||
import Chat from "./chat";
|
||||
@ -27,6 +27,7 @@ export default class ChatAudio extends PinnedContainer {
|
||||
private progressLine: MediaProgressLine;
|
||||
private volumeSelector: VolumeSelector;
|
||||
private fasterEl: HTMLElement;
|
||||
private repeatEl: HTMLButtonElement;
|
||||
|
||||
constructor(protected topbar: ChatTopbar, protected chat: Chat, protected appMessagesManager: AppMessagesManager) {
|
||||
super({
|
||||
@ -84,12 +85,25 @@ export default class ChatAudio extends PinnedContainer {
|
||||
this.volumeSelector.btn.prepend(tunnel);
|
||||
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});
|
||||
attachClick(fasterEl, () => {
|
||||
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');
|
||||
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_pause', this.onPause);
|
||||
this.topbar.listenerSetter.add(rootScope)('media_stop', this.onStop);
|
||||
this.topbar.listenerSetter.add(rootScope)('media_playback_params', ({playbackRate}) => {
|
||||
this.onPlaybackRateChange(playbackRate);
|
||||
});
|
||||
this.topbar.listenerSetter.add(rootScope)('media_playback_params', this.onPlaybackParams);
|
||||
|
||||
const playingDetails = appMediaPlaybackController.getPlayingDetails();
|
||||
if(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) => {
|
||||
this.fasterEl.classList.toggle('active', playbackRate > 1);
|
||||
private onPlaybackParams = (playbackParams: ReturnType<AppMediaPlaybackController['getPlaybackParams']>) => {
|
||||
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 = () => {
|
||||
@ -137,18 +153,20 @@ export default class ChatAudio extends PinnedContainer {
|
||||
media: HTMLMediaElement
|
||||
}) => {
|
||||
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;
|
||||
|
||||
//subtitle = 'Voice message';
|
||||
subtitle = formatFullSentTime(message.date);
|
||||
this.fasterEl.classList.remove('hide');
|
||||
} else {
|
||||
title = doc.audioTitle || doc.fileName;
|
||||
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.fill(title, subtitle, message);
|
||||
|
@ -682,7 +682,7 @@ export default class ChatInput {
|
||||
<span class="tgico tgico-send"></span>
|
||||
<span class="tgico tgico-schedule"></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);
|
||||
|
@ -257,7 +257,7 @@ export default class PeerProfileAvatars {
|
||||
if(!older) return Promise.resolve({count: undefined, items: []});
|
||||
|
||||
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 {
|
||||
count: value.count,
|
||||
|
@ -328,7 +328,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
|
||||
inputFilter: {_: 'inputMessagesFilterEmpty'},
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@ export default class AvatarListLoader<Item extends {photoId: Photo.photo['id']}>
|
||||
loadMore: (anchor, older, loadCount) => {
|
||||
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 => {
|
||||
const items = value.photos.map(photoId => {
|
||||
return {element: null as HTMLElement, photoId} as any;
|
||||
|
10
src/helpers/compareValue.ts
Normal file
10
src/helpers/compareValue.ts
Normal file
@ -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);
|
||||
}
|
@ -66,8 +66,8 @@ export default class ListLoader<T extends {}, P extends {}> {
|
||||
this.current = undefined;
|
||||
this.previous = [];
|
||||
this.next = [];
|
||||
this.loadedAllUp = this.loadedAllDown = loadedAll;
|
||||
this.loadPromiseUp = this.loadPromiseDown = null;
|
||||
this.setLoaded(true, loadedAll);
|
||||
this.setLoaded(false, loadedAll);
|
||||
}
|
||||
|
||||
public go(length: number, dispatchJump = true) {
|
||||
@ -79,15 +79,17 @@ export default class ListLoader<T extends {}, P extends {}> {
|
||||
return;
|
||||
}
|
||||
|
||||
this.previous.push(this.current, ...items);
|
||||
if(this.current !== undefined) items.unshift(this.current);
|
||||
this.previous.push(...items);
|
||||
} 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();
|
||||
if(!item) {
|
||||
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) {
|
||||
@ -103,13 +105,50 @@ export default class ListLoader<T extends {}, P extends {}> {
|
||||
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
|
||||
public load(older: boolean) {
|
||||
if(older && this.loadedAllDown) return Promise.resolve();
|
||||
else if(!older && this.loadedAllUp) return Promise.resolve();
|
||||
if(older ? this.loadedAllDown : this.loadedAllUp) return Promise.resolve();
|
||||
|
||||
if(older && this.loadPromiseDown) return this.loadPromiseDown;
|
||||
else if(!older && this.loadPromiseUp) return this.loadPromiseUp;
|
||||
let promise = older ? this.loadPromiseDown : this.loadPromiseUp;
|
||||
if(promise) return promise;
|
||||
|
||||
let anchor: T;
|
||||
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];
|
||||
}
|
||||
|
||||
const promise = this.loadMore(anchor, older, this.loadCount).then(result => {
|
||||
if((older && this.loadPromiseDown !== promise) || (!older && this.loadPromiseUp !== promise)) {
|
||||
anchor ??= this.current;
|
||||
promise = this.loadMore(anchor, older, this.loadCount).then(result => {
|
||||
if((older ? this.loadPromiseDown : this.loadPromiseUp) !== promise) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(result.items.length < this.loadCount) {
|
||||
if(older) this.loadedAllDown = true;
|
||||
else this.loadedAllUp = true;
|
||||
this.setLoaded(older, true);
|
||||
}
|
||||
|
||||
if(this.count === undefined) {
|
||||
|
25
src/helpers/long/compareLong.ts
Normal file
25
src/helpers/long/compareLong.ts
Normal file
@ -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;
|
||||
}
|
@ -18,14 +18,16 @@ export default class SearchListLoader<Item extends {mid: number, peerId: PeerId}
|
||||
public searchContext: MediaSearchContext;
|
||||
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({
|
||||
...options,
|
||||
loadMore: (anchor, older, 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);
|
||||
|
||||
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_multiappend', this.onHistoryMultiappend);
|
||||
rootScope.addEventListener('message_sent', this.onMessageSent);
|
||||
|
||||
if(!options.isInner) {
|
||||
this.otherSideLoader = new SearchListLoader({
|
||||
...options,
|
||||
isInner: true
|
||||
});
|
||||
|
||||
// this.otherSideLoader.onLoadedMore = () => {
|
||||
|
||||
// };
|
||||
}
|
||||
}
|
||||
|
||||
protected filterMids(mids: number[]) {
|
||||
@ -116,7 +129,26 @@ export default class SearchListLoader<Item extends {mid: number, peerId: PeerId}
|
||||
const filtered = this.filterMids(sorted);
|
||||
const targets = filtered.map(message => this.processItem(message)).filter(Boolean);
|
||||
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;
|
||||
}
|
||||
|
||||
if(!this.searchContext.useSearch) {
|
||||
this.loadedAllDown = this.loadedAllUp = true;
|
||||
// if(!this.searchContext.useSearch) {
|
||||
// this.loadedAllDown = this.loadedAllUp = true;
|
||||
// }
|
||||
|
||||
if(this.otherSideLoader) {
|
||||
this.otherSideLoader.setSearchContext(context);
|
||||
}
|
||||
}
|
||||
|
||||
public reset() {
|
||||
super.reset();
|
||||
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() {
|
||||
@ -157,5 +292,10 @@ export default class SearchListLoader<Item extends {mid: number, peerId: PeerId}
|
||||
rootScope.removeEventListener('history_multiappend', this.onHistoryMultiappend);
|
||||
rootScope.removeEventListener('message_sent', this.onMessageSent);
|
||||
this.onEmptied = undefined;
|
||||
|
||||
if(this.otherSideLoader) {
|
||||
this.otherSideLoader.cleanup();
|
||||
this.otherSideLoader = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,12 +5,13 @@
|
||||
*/
|
||||
|
||||
import { MOUNT_CLASS_TO } from "../config/debug";
|
||||
import compareValue from "./compareValue";
|
||||
|
||||
/**
|
||||
* Descend sorted storage
|
||||
*/
|
||||
|
||||
type ItemType = number;
|
||||
type ItemType = number | string;
|
||||
|
||||
export enum SliceEnd {
|
||||
None = 0,
|
||||
@ -19,7 +20,7 @@ export enum SliceEnd {
|
||||
Both = SliceEnd.Top | SliceEnd.Bottom
|
||||
};
|
||||
|
||||
export interface Slice extends Array<ItemType> {
|
||||
export interface Slice<T extends ItemType> extends Array<T> {
|
||||
//slicedArray: SlicedArray;
|
||||
end: SliceEnd;
|
||||
|
||||
@ -27,17 +28,18 @@ export interface Slice extends Array<ItemType> {
|
||||
setEnd: (side: SliceEnd) => void;
|
||||
unsetEnd: (side: SliceEnd) => void;
|
||||
|
||||
slice: (from?: number, to?: number) => Slice;
|
||||
splice: (start: number, deleteCount: number, ...items: ItemType[]) => Slice;
|
||||
slice: (from?: number, to?: number) => Slice<T>;
|
||||
splice: (start: number, deleteCount: number, ...items: ItemType[]) => Slice<T>;
|
||||
}
|
||||
|
||||
export interface SliceConstructor {
|
||||
new(...items: ItemType[]): Slice;
|
||||
export interface SliceConstructor<T extends ItemType> {
|
||||
// new(...items: T[]): Slice<T>;
|
||||
new(length: number): Slice<T>;
|
||||
}
|
||||
|
||||
export default class SlicedArray {
|
||||
private slices: Slice[]/* = [[7,6,5],[4,3,2],[1,0,-1]] */;
|
||||
private sliceConstructor: SliceConstructor;
|
||||
export default class SlicedArray<T extends ItemType> {
|
||||
private slices: Slice<T>[]/* = [[7,6,5],[4,3,2],[1,0,-1]] */;
|
||||
private sliceConstructor: SliceConstructor<T>;
|
||||
|
||||
constructor() {
|
||||
// @ts-ignore
|
||||
@ -48,8 +50,8 @@ export default class SlicedArray {
|
||||
this.slices = [first];
|
||||
}
|
||||
|
||||
private static getSliceConstructor(slicedArray: SlicedArray) {
|
||||
return class Slice extends Array<ItemType> implements Slice {
|
||||
private static getSliceConstructor(slicedArray: SlicedArray<ItemType>) {
|
||||
return class Slice<T> extends Array<ItemType> implements Slice<T> {
|
||||
//slicedArray: SlicedArray;
|
||||
end: SliceEnd = SliceEnd.None;
|
||||
|
||||
@ -95,7 +97,7 @@ export default class SlicedArray {
|
||||
const ret = super.splice(start, deleteCount, ...items);
|
||||
|
||||
if(!this.length) {
|
||||
const slices = slicedArray.slices as number[][];
|
||||
const slices = slicedArray.slices as ItemType[][];
|
||||
const idx = slices.indexOf(this);
|
||||
if(idx !== -1) {
|
||||
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);
|
||||
// can't pass items directly to constructor because first argument is 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) {
|
||||
return;
|
||||
}
|
||||
@ -180,7 +182,7 @@ export default class SlicedArray {
|
||||
const lowerBound = slice[slice.length - 1];
|
||||
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) {
|
||||
foundSlice = this.slices[foundSliceIndex];
|
||||
lowerIndex = foundSlice.indexOf(lowerBound);
|
||||
@ -205,7 +207,7 @@ export default class SlicedArray {
|
||||
let insertIndex = 0;
|
||||
for(const length = this.slices.length; insertIndex < length; ++insertIndex) { // * maybe should iterate from the end, could be faster ?
|
||||
const s = this.slices[insertIndex];
|
||||
if(slice[0] > s[0]) {
|
||||
if(compareValue(slice[0], s[0]) === 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -263,7 +265,7 @@ export default class SlicedArray {
|
||||
return this.slice.length;
|
||||
}
|
||||
|
||||
public findSlice(item: ItemType) {
|
||||
public findSlice(item: T) {
|
||||
for(let i = 0, length = this.slices.length; i < length; ++i) {
|
||||
const slice = this.slices[i];
|
||||
const index = slice.indexOf(item);
|
||||
@ -275,8 +277,8 @@ export default class SlicedArray {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public findSliceOffset(maxId: number) {
|
||||
let slice: Slice;
|
||||
public findSliceOffset(maxId: T) {
|
||||
let slice: Slice<T>;
|
||||
for(let i = 0; i < this.slices.length; ++i) {
|
||||
let offset = 0;
|
||||
slice = this.slices[i];
|
||||
@ -284,8 +286,8 @@ export default class SlicedArray {
|
||||
continue;
|
||||
}
|
||||
|
||||
for(; offset < slice.length; offset++) {
|
||||
if(maxId >= slice[offset]) {
|
||||
for(; offset < slice.length; ++offset) {
|
||||
if(compareValue(maxId, slice[offset]) >= 0) {
|
||||
/* if(!offset) { // because can't find 3 in [[5,4], [2,1]]
|
||||
return undefined;
|
||||
} */
|
||||
@ -309,7 +311,7 @@ export default class SlicedArray {
|
||||
}
|
||||
|
||||
// * 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 offset = 0;
|
||||
let sliceOffset = 0;
|
||||
@ -337,7 +339,7 @@ export default class SlicedArray {
|
||||
//const fixHalfBackLimit = add_offset && !(limit / add_offset % 2) && (sliceEnd % 2) ? 1 : 0;
|
||||
//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 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;
|
||||
if(!slice.length) {
|
||||
slice.setEnd(SliceEnd.Bottom);
|
||||
@ -369,7 +371,7 @@ export default class SlicedArray {
|
||||
slice.unshift(...items);
|
||||
}
|
||||
|
||||
public push(...items: ItemType[]) {
|
||||
public push(...items: T[]) {
|
||||
let slice = this.last;
|
||||
if(!slice.length) {
|
||||
slice.setEnd(SliceEnd.Top);
|
||||
@ -382,7 +384,7 @@ export default class SlicedArray {
|
||||
slice.push(...items);
|
||||
}
|
||||
|
||||
public delete(item: ItemType) {
|
||||
public delete(item: T) {
|
||||
const found = this.findSlice(item);
|
||||
if(found) {
|
||||
found.slice.splice(found.index, 1);
|
||||
|
@ -345,6 +345,13 @@ export class AppImManager {
|
||||
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.setToCache('chatPositions', c || {});
|
||||
});
|
||||
|
@ -80,7 +80,7 @@ const DO_NOT_READ_HISTORY = false;
|
||||
|
||||
export type HistoryStorage = {
|
||||
count: number | null,
|
||||
history: SlicedArray,
|
||||
history: SlicedArray<number>,
|
||||
|
||||
maxId?: number,
|
||||
readPromise?: Promise<void>,
|
||||
@ -94,7 +94,7 @@ export type HistoryStorage = {
|
||||
|
||||
export type HistoryResult = {
|
||||
count: number,
|
||||
history: Slice,
|
||||
history: Slice<number>,
|
||||
offsetIdOffset?: number,
|
||||
};
|
||||
|
||||
@ -220,7 +220,7 @@ export class AppMessagesManager {
|
||||
|
||||
private middleware: ReturnType<typeof getMiddleware>;
|
||||
|
||||
private unreadMentions: {[peerId: PeerId]: SlicedArray} = {};
|
||||
private unreadMentions: {[peerId: PeerId]: SlicedArray<number>} = {};
|
||||
private goToNextMentionPromises: {[peerId: PeerId]: Promise<any>} = {};
|
||||
|
||||
private batchUpdates: {
|
||||
@ -3901,7 +3901,7 @@ export class AppMessagesManager {
|
||||
|
||||
let storage: {
|
||||
count?: number;
|
||||
history: SlicedArray;
|
||||
history: SlicedArray<number>;
|
||||
};
|
||||
|
||||
// * костыль для 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);
|
||||
if(!slicedArray.length && dialog?.unread_mentions_count) {
|
||||
this.reloadConversation(peerId);
|
||||
@ -5117,6 +5117,11 @@ export class AppMessagesManager {
|
||||
|
||||
private onUpdateServiceNotification = (update: Update.updateServiceNotification) => {
|
||||
//this.log('updateServiceNotification', update);
|
||||
if(update.pFlags?.popup) {
|
||||
rootScope.dispatchEvent('service_notification', update);
|
||||
return;
|
||||
}
|
||||
|
||||
const fromId = SERVICE_PEER_ID;
|
||||
const peerId = fromId;
|
||||
const messageId = this.generateTempMessageId(peerId);
|
||||
@ -5613,6 +5618,11 @@ export class AppMessagesManager {
|
||||
notificationMessage = I18n.format('Notifications.New', true);
|
||||
}
|
||||
|
||||
if(options.userReaction) {
|
||||
notification.noIncrement = true;
|
||||
notification.silent = true;
|
||||
}
|
||||
|
||||
notification.title = appPeersManager.getPeerTitle(peerId, true);
|
||||
if(isAnyChat && message.fromId !== message.peerId) {
|
||||
notification.title = appPeersManager.getPeerTitle(message.fromId, true) +
|
||||
@ -5848,7 +5858,7 @@ export class AppMessagesManager {
|
||||
return {count, offsetIdOffset, isTopEnd, isBottomEnd};
|
||||
}
|
||||
|
||||
public mergeHistoryResult(slicedArray: SlicedArray,
|
||||
public mergeHistoryResult(slicedArray: SlicedArray<number>,
|
||||
historyResult: Parameters<AppMessagesManager['isHistoryResultEnd']>[0],
|
||||
offset_id: number,
|
||||
limit: number,
|
||||
|
@ -45,6 +45,7 @@ export type NotifyOptions = Partial<{
|
||||
message: string;
|
||||
silent: boolean;
|
||||
onclick: () => void;
|
||||
noIncrement: boolean;
|
||||
}>;
|
||||
|
||||
export type NotificationSettings = {
|
||||
@ -595,7 +596,10 @@ export class AppNotificationsManager {
|
||||
}
|
||||
// console.log('notify image', data.image)
|
||||
|
||||
this.notificationsCount++;
|
||||
if(!data.noIncrement) {
|
||||
++this.notificationsCount;
|
||||
}
|
||||
|
||||
if(!this.titleInterval) {
|
||||
this.toggleToggler();
|
||||
}
|
||||
|
@ -357,6 +357,13 @@ export default class CallInstance extends CallInstanceBase<{
|
||||
});
|
||||
}).then(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');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -96,7 +96,7 @@ export type BroadcastEvents = {
|
||||
|
||||
'media_play': {doc: MyDocument, message: Message.message, media: HTMLMediaElement},
|
||||
'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,
|
||||
|
||||
'state_cleared': void,
|
||||
@ -164,7 +164,9 @@ export type BroadcastEvents = {
|
||||
|
||||
'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<{
|
||||
|
@ -354,7 +354,7 @@ $background-transition-total-time: #{$input-transition-time - $background-transi
|
||||
// }
|
||||
|
||||
&.send .tgico-send,
|
||||
&.record .tgico-microphone,
|
||||
&.record .tgico-microphone_filled,
|
||||
&.edit .tgico-check,
|
||||
&.schedule .tgico-schedule {
|
||||
visibility: visible !important;
|
||||
@ -368,7 +368,7 @@ $background-transition-total-time: #{$input-transition-time - $background-transi
|
||||
|
||||
@include animation-level(2) {
|
||||
&.send .tgico-send,
|
||||
&.record .tgico-microphone,
|
||||
&.record .tgico-microphone_filled,
|
||||
&.edit .tgico-check,
|
||||
&.schedule .tgico-schedule {
|
||||
animation: grow-icon .4s forwards ease-in-out !important;
|
||||
|
@ -3,9 +3,9 @@
|
||||
@font-face {
|
||||
font-family: '#{$tgico-font-family}';
|
||||
src:
|
||||
url('#{$tgico-font-path}/#{$tgico-font-family}.ttf?cyy67r') format('truetype'),
|
||||
url('#{$tgico-font-path}/#{$tgico-font-family}.woff?cyy67r') format('woff'),
|
||||
url('#{$tgico-font-path}/#{$tgico-font-family}.svg?cyy67r##{$tgico-font-family}') format('svg');
|
||||
url('#{$tgico-font-path}/#{$tgico-font-family}.ttf?1mzumm') format('truetype'),
|
||||
url('#{$tgico-font-path}/#{$tgico-font-family}.woff?1mzumm') format('woff'),
|
||||
url('#{$tgico-font-path}/#{$tgico-font-family}.svg?1mzumm##{$tgico-font-family}') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
@ -332,11 +332,6 @@
|
||||
content: $tgico-email;
|
||||
}
|
||||
}
|
||||
.tgico-endcall {
|
||||
&:before {
|
||||
content: $tgico-endcall;
|
||||
}
|
||||
}
|
||||
.tgico-endcall_filled {
|
||||
&:before {
|
||||
content: $tgico-endcall_filled;
|
||||
@ -377,6 +372,11 @@
|
||||
content: $tgico-flag;
|
||||
}
|
||||
}
|
||||
.tgico-flip {
|
||||
&:before {
|
||||
content: $tgico-flip;
|
||||
}
|
||||
}
|
||||
.tgico-folder {
|
||||
&:before {
|
||||
content: $tgico-folder;
|
||||
@ -547,6 +547,21 @@
|
||||
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 {
|
||||
&:before {
|
||||
content: $tgico-minus;
|
||||
@ -737,6 +752,16 @@
|
||||
content: $tgico-rightpanel;
|
||||
}
|
||||
}
|
||||
.tgico-rotate_left {
|
||||
&:before {
|
||||
content: $tgico-rotate_left;
|
||||
}
|
||||
}
|
||||
.tgico-rotate_right {
|
||||
&:before {
|
||||
content: $tgico-rotate_right;
|
||||
}
|
||||
}
|
||||
.tgico-saved {
|
||||
&:before {
|
||||
content: $tgico-saved;
|
||||
@ -802,6 +827,11 @@
|
||||
content: $tgico-sharescreen_filled;
|
||||
}
|
||||
}
|
||||
.tgico-shuffle {
|
||||
&:before {
|
||||
content: $tgico-shuffle;
|
||||
}
|
||||
}
|
||||
.tgico-smallscreen {
|
||||
&:before {
|
||||
content: $tgico-smallscreen;
|
||||
@ -897,6 +927,11 @@
|
||||
content: $tgico-videocamera;
|
||||
}
|
||||
}
|
||||
.tgico-videocamera_crossed_filled {
|
||||
&:before {
|
||||
content: $tgico-videocamera_crossed_filled;
|
||||
}
|
||||
}
|
||||
.tgico-videocamera_filled {
|
||||
&:before {
|
||||
content: $tgico-videocamera_filled;
|
||||
|
@ -58,15 +58,15 @@ $tgico-dragmedia: "\e938";
|
||||
$tgico-eats: "\e939";
|
||||
$tgico-edit: "\e93a";
|
||||
$tgico-email: "\e93b";
|
||||
$tgico-endcall: "\e93c";
|
||||
$tgico-endcall_filled: "\e93d";
|
||||
$tgico-enter: "\e93e";
|
||||
$tgico-eye1: "\e93f";
|
||||
$tgico-eye2: "\e940";
|
||||
$tgico-fast_forward: "\e941";
|
||||
$tgico-fast_rewind: "\e942";
|
||||
$tgico-favourites: "\e943";
|
||||
$tgico-flag: "\e944";
|
||||
$tgico-endcall_filled: "\e93c";
|
||||
$tgico-enter: "\e93d";
|
||||
$tgico-eye1: "\e93e";
|
||||
$tgico-eye2: "\e93f";
|
||||
$tgico-fast_forward: "\e940";
|
||||
$tgico-fast_rewind: "\e941";
|
||||
$tgico-favourites: "\e942";
|
||||
$tgico-flag: "\e943";
|
||||
$tgico-flip: "\e944";
|
||||
$tgico-folder: "\e945";
|
||||
$tgico-fontsize: "\e946";
|
||||
$tgico-forward: "\e947";
|
||||
@ -101,82 +101,89 @@ $tgico-menu: "\e963";
|
||||
$tgico-message: "\e964";
|
||||
$tgico-messageunread: "\e965";
|
||||
$tgico-microphone: "\e966";
|
||||
$tgico-minus: "\e967";
|
||||
$tgico-monospace: "\e968";
|
||||
$tgico-more: "\e969";
|
||||
$tgico-mute: "\e96a";
|
||||
$tgico-muted: "\e96b";
|
||||
$tgico-newchannel: "\e96c";
|
||||
$tgico-newchat_filled: "\e96d";
|
||||
$tgico-newgroup: "\e96e";
|
||||
$tgico-newprivate: "\e96f";
|
||||
$tgico-next: "\e970";
|
||||
$tgico-noncontacts: "\e971";
|
||||
$tgico-nosound: "\e972";
|
||||
$tgico-passwordoff: "\e973";
|
||||
$tgico-pause: "\e974";
|
||||
$tgico-permissions: "\e975";
|
||||
$tgico-phone: "\e976";
|
||||
$tgico-pin: "\e977";
|
||||
$tgico-pinlist: "\e978";
|
||||
$tgico-pinned_filled: "\e979";
|
||||
$tgico-pinnedchat: "\e97a";
|
||||
$tgico-pip: "\e97b";
|
||||
$tgico-play: "\e97c";
|
||||
$tgico-playback_05: "\e97d";
|
||||
$tgico-playback_15: "\e97e";
|
||||
$tgico-playback_1x: "\e97f";
|
||||
$tgico-playback_2x: "\e980";
|
||||
$tgico-plus: "\e981";
|
||||
$tgico-poll: "\e982";
|
||||
$tgico-previous: "\e983";
|
||||
$tgico-radiooff: "\e984";
|
||||
$tgico-radioon: "\e985";
|
||||
$tgico-reactions: "\e986";
|
||||
$tgico-readchats: "\e987";
|
||||
$tgico-recent: "\e988";
|
||||
$tgico-replace: "\e989";
|
||||
$tgico-reply: "\e98a";
|
||||
$tgico-reply_filled: "\e98b";
|
||||
$tgico-rightpanel: "\e98c";
|
||||
$tgico-saved: "\e98d";
|
||||
$tgico-savedmessages: "\e98e";
|
||||
$tgico-schedule: "\e98f";
|
||||
$tgico-scheduled: "\e990";
|
||||
$tgico-search: "\e991";
|
||||
$tgico-select: "\e992";
|
||||
$tgico-send: "\e993";
|
||||
$tgico-send2: "\e994";
|
||||
$tgico-sending: "\e995";
|
||||
$tgico-sendingerror: "\e996";
|
||||
$tgico-settings: "\e997";
|
||||
$tgico-settings_filled: "\e998";
|
||||
$tgico-sharescreen_filled: "\e999";
|
||||
$tgico-smallscreen: "\e99a";
|
||||
$tgico-smile: "\e99b";
|
||||
$tgico-spoiler: "\e99c";
|
||||
$tgico-sport: "\e99d";
|
||||
$tgico-stickers: "\e99e";
|
||||
$tgico-stop: "\e99f";
|
||||
$tgico-strikethrough: "\e9a0";
|
||||
$tgico-textedit: "\e9a1";
|
||||
$tgico-tip: "\e9a2";
|
||||
$tgico-tools: "\e9a3";
|
||||
$tgico-unarchive: "\e9a4";
|
||||
$tgico-underline: "\e9a5";
|
||||
$tgico-unmute: "\e9a6";
|
||||
$tgico-unpin: "\e9a7";
|
||||
$tgico-unread: "\e9a8";
|
||||
$tgico-up: "\e9a9";
|
||||
$tgico-user: "\e9aa";
|
||||
$tgico-username: "\e9ab";
|
||||
$tgico-videocamera: "\e9ac";
|
||||
$tgico-videocamera_filled: "\e9ad";
|
||||
$tgico-videochat: "\e9ae";
|
||||
$tgico-volume_down: "\e9af";
|
||||
$tgico-volume_mute: "\e9b0";
|
||||
$tgico-volume_off: "\e9b1";
|
||||
$tgico-volume_up: "\e9b2";
|
||||
$tgico-zoomin: "\e9b3";
|
||||
$tgico-zoomout: "\e9b4";
|
||||
$tgico-microphone_crossed: "\e967";
|
||||
$tgico-microphone_crossed_filled: "\e968";
|
||||
$tgico-microphone_filled: "\e969";
|
||||
$tgico-minus: "\e96a";
|
||||
$tgico-monospace: "\e96b";
|
||||
$tgico-more: "\e96c";
|
||||
$tgico-mute: "\e96d";
|
||||
$tgico-muted: "\e96e";
|
||||
$tgico-newchannel: "\e96f";
|
||||
$tgico-newchat_filled: "\e970";
|
||||
$tgico-newgroup: "\e971";
|
||||
$tgico-newprivate: "\e972";
|
||||
$tgico-next: "\e973";
|
||||
$tgico-noncontacts: "\e974";
|
||||
$tgico-nosound: "\e975";
|
||||
$tgico-passwordoff: "\e976";
|
||||
$tgico-pause: "\e977";
|
||||
$tgico-permissions: "\e978";
|
||||
$tgico-phone: "\e979";
|
||||
$tgico-pin: "\e97a";
|
||||
$tgico-pinlist: "\e97b";
|
||||
$tgico-pinned_filled: "\e97c";
|
||||
$tgico-pinnedchat: "\e97d";
|
||||
$tgico-pip: "\e97e";
|
||||
$tgico-play: "\e97f";
|
||||
$tgico-playback_05: "\e980";
|
||||
$tgico-playback_15: "\e981";
|
||||
$tgico-playback_1x: "\e982";
|
||||
$tgico-playback_2x: "\e983";
|
||||
$tgico-plus: "\e984";
|
||||
$tgico-poll: "\e985";
|
||||
$tgico-previous: "\e986";
|
||||
$tgico-radiooff: "\e987";
|
||||
$tgico-radioon: "\e988";
|
||||
$tgico-reactions: "\e989";
|
||||
$tgico-readchats: "\e98a";
|
||||
$tgico-recent: "\e98b";
|
||||
$tgico-replace: "\e98c";
|
||||
$tgico-reply: "\e98d";
|
||||
$tgico-reply_filled: "\e98e";
|
||||
$tgico-rightpanel: "\e98f";
|
||||
$tgico-rotate_left: "\e990";
|
||||
$tgico-rotate_right: "\e991";
|
||||
$tgico-saved: "\e992";
|
||||
$tgico-savedmessages: "\e993";
|
||||
$tgico-schedule: "\e994";
|
||||
$tgico-scheduled: "\e995";
|
||||
$tgico-search: "\e996";
|
||||
$tgico-select: "\e997";
|
||||
$tgico-send: "\e998";
|
||||
$tgico-send2: "\e999";
|
||||
$tgico-sending: "\e99a";
|
||||
$tgico-sendingerror: "\e99b";
|
||||
$tgico-settings: "\e99c";
|
||||
$tgico-settings_filled: "\e99d";
|
||||
$tgico-sharescreen_filled: "\e99e";
|
||||
$tgico-shuffle: "\e99f";
|
||||
$tgico-smallscreen: "\e9a0";
|
||||
$tgico-smile: "\e9a1";
|
||||
$tgico-spoiler: "\e9a2";
|
||||
$tgico-sport: "\e9a3";
|
||||
$tgico-stickers: "\e9a4";
|
||||
$tgico-stop: "\e9a5";
|
||||
$tgico-strikethrough: "\e9a6";
|
||||
$tgico-textedit: "\e9a7";
|
||||
$tgico-tip: "\e9a8";
|
||||
$tgico-tools: "\e9a9";
|
||||
$tgico-unarchive: "\e9aa";
|
||||
$tgico-underline: "\e9ab";
|
||||
$tgico-unmute: "\e9ac";
|
||||
$tgico-unpin: "\e9ad";
|
||||
$tgico-unread: "\e9ae";
|
||||
$tgico-up: "\e9af";
|
||||
$tgico-user: "\e9b0";
|
||||
$tgico-username: "\e9b1";
|
||||
$tgico-videocamera: "\e9b2";
|
||||
$tgico-videocamera_crossed_filled: "\e9b3";
|
||||
$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";
|
||||
|
||||
|
@ -7,23 +7,27 @@ test('Slicing returns new Slice', () => {
|
||||
});
|
||||
|
||||
describe('Inserting', () => {
|
||||
const sliced = new SlicedArray();
|
||||
const sliced = new SlicedArray<typeof arr[0]>();
|
||||
|
||||
// @ts-ignore
|
||||
const slices = sliced.slices;
|
||||
|
||||
const arr = [100, 99, 98, 97, 96, 95];
|
||||
const distantArr = arr.slice(-2).map(v => v - 2);
|
||||
const missingArr = [arr[arr.length - 1], arr[arr.length - 1] - 1, distantArr[0]];
|
||||
|
||||
const startValue = 90;
|
||||
const values: number[] = [];
|
||||
const toSomething = (v: number) => {
|
||||
return '' + v;
|
||||
};
|
||||
|
||||
const arr = [100, 99, 98, 97, 96, 95].map(toSomething);
|
||||
const distantArr = arr.slice(-2).map(v => toSomething(+v - 2));
|
||||
const missingArr = [arr[arr.length - 1], toSomething(+arr[arr.length - 1] - 1), distantArr[1]];
|
||||
|
||||
const startValue = toSomething(90);
|
||||
const values: typeof arr = [];
|
||||
const valuesPerArray = 3;
|
||||
const totalArrays = 10;
|
||||
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) {
|
||||
arrays.push(values.slice(valuesPerArray * i, valuesPerArray * (i + 1)));
|
||||
}
|
||||
@ -57,7 +61,7 @@ describe('Inserting', () => {
|
||||
expect(slices.length).toEqual(length - 1);
|
||||
});
|
||||
|
||||
let returnedSlice: Slice;
|
||||
let returnedSlice: Slice<typeof arr[0]>;
|
||||
test('Insert arrays with gap & join them', () => {
|
||||
slices[0].length = 0;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user