diff --git a/src/components/appMediaPlaybackController.ts b/src/components/appMediaPlaybackController.ts index 2f3ede3c..ee8745ca 100644 --- a/src/components/appMediaPlaybackController.ts +++ b/src/components/appMediaPlaybackController.ts @@ -58,7 +58,7 @@ type MediaDetails = { export type PlaybackMediaType = 'voice' | 'video' | 'audio'; -class AppMediaPlaybackController { +export class AppMediaPlaybackController { private container: HTMLElement; private media: Map> = 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 = { 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); } diff --git a/src/components/audio.ts b/src/components/audio.ts index 12dc0423..dfa47842 100644 --- a/src/components/audio.ts +++ b/src/components/audio.ts @@ -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); } diff --git a/src/components/chat/audio.ts b/src/components/chat/audio.ts index 6ee6e4aa..c015fcda 100644 --- a/src/components/chat/audio.ts +++ b/src/components/chat/audio.ts @@ -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) => { + 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); diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index 93bdcb3e..ed4c433c 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -682,7 +682,7 @@ export default class ChatInput { - + `); this.btnSendContainer.append(this.recordRippleEl, this.btnSend); diff --git a/src/components/peerProfileAvatars.ts b/src/components/peerProfileAvatars.ts index 8543d797..0e6aff20 100644 --- a/src/components/peerProfileAvatars.ts +++ b/src/components/peerProfileAvatars.ts @@ -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, diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index dbb694c4..cc132814 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -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); } diff --git a/src/helpers/avatarListLoader.ts b/src/helpers/avatarListLoader.ts index 14b0e010..af7da474 100644 --- a/src/helpers/avatarListLoader.ts +++ b/src/helpers/avatarListLoader.ts @@ -17,7 +17,7 @@ export default class AvatarListLoader 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; diff --git a/src/helpers/compareValue.ts b/src/helpers/compareValue.ts new file mode 100644 index 00000000..e008c997 --- /dev/null +++ b/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); +} diff --git a/src/helpers/listLoader.ts b/src/helpers/listLoader.ts index 1c579b50..e0f14837 100644 --- a/src/helpers/listLoader.ts +++ b/src/helpers/listLoader.ts @@ -66,8 +66,8 @@ export default class ListLoader { 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 { 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 { 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 { 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) { diff --git a/src/helpers/long/compareLong.ts b/src/helpers/long/compareLong.ts new file mode 100644 index 00000000..b2368934 --- /dev/null +++ b/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; +} diff --git a/src/helpers/searchListLoader.ts b/src/helpers/searchListLoader.ts index a87e5893..de0bcb32 100644 --- a/src/helpers/searchListLoader.ts +++ b/src/helpers/searchListLoader.ts @@ -18,14 +18,16 @@ export default class SearchListLoader void; - constructor(options: Omit, 'loadMore'> & {onEmptied?: () => void} = {}) { + private otherSideLoader: SearchListLoader; + + constructor(options: Omit, '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 { + + // }; + } } protected filterMids(mids: number[]) { @@ -116,7 +129,26 @@ export default class SearchListLoader 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 0) return this.go(-this.previous.length); + else return this.go(this.next.length); + } + + public goRound(length: number, dispatchJump?: boolean) { + let ret: ReturnType['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 { +export interface Slice extends Array { //slicedArray: SlicedArray; end: SliceEnd; @@ -27,17 +28,18 @@ export interface Slice extends Array { 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; + splice: (start: number, deleteCount: number, ...items: ItemType[]) => Slice; } -export interface SliceConstructor { - new(...items: ItemType[]): Slice; +export interface SliceConstructor { + // new(...items: T[]): Slice; + new(length: number): Slice; } -export default class SlicedArray { - private slices: Slice[]/* = [[7,6,5],[4,3,2],[1,0,-1]] */; - private sliceConstructor: SliceConstructor; +export default class SlicedArray { + private slices: Slice[]/* = [[7,6,5],[4,3,2],[1,0,-1]] */; + private sliceConstructor: SliceConstructor; constructor() { // @ts-ignore @@ -48,8 +50,8 @@ export default class SlicedArray { this.slices = [first]; } - private static getSliceConstructor(slicedArray: SlicedArray) { - return class Slice extends Array implements Slice { + private static getSliceConstructor(slicedArray: SlicedArray) { + return class Slice extends Array implements Slice { //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, 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; 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; 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); diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index e2949f44..f3cfc85c 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -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 || {}); }); diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index a7d83e3f..a2f92e71 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -80,7 +80,7 @@ const DO_NOT_READ_HISTORY = false; export type HistoryStorage = { count: number | null, - history: SlicedArray, + history: SlicedArray, maxId?: number, readPromise?: Promise, @@ -94,7 +94,7 @@ export type HistoryStorage = { export type HistoryResult = { count: number, - history: Slice, + history: Slice, offsetIdOffset?: number, }; @@ -220,7 +220,7 @@ export class AppMessagesManager { private middleware: ReturnType; - private unreadMentions: {[peerId: PeerId]: SlicedArray} = {}; + private unreadMentions: {[peerId: PeerId]: SlicedArray} = {}; private goToNextMentionPromises: {[peerId: PeerId]: Promise} = {}; private batchUpdates: { @@ -3901,7 +3901,7 @@ export class AppMessagesManager { let storage: { count?: number; - history: SlicedArray; + history: SlicedArray; }; // * костыль для limit 1, если нужно и получить сообщение, и узнать количество сообщений @@ -4314,7 +4314,7 @@ export class AppMessagesManager { } } - private fixUnreadMentionsCountIfNeeded(peerId: PeerId, slicedArray: SlicedArray) { + private fixUnreadMentionsCountIfNeeded(peerId: PeerId, slicedArray: SlicedArray) { 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, historyResult: Parameters[0], offset_id: number, limit: number, diff --git a/src/lib/appManagers/appNotificationsManager.ts b/src/lib/appManagers/appNotificationsManager.ts index 40a341e3..91dd3871 100644 --- a/src/lib/appManagers/appNotificationsManager.ts +++ b/src/lib/appManagers/appNotificationsManager.ts @@ -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(); } diff --git a/src/lib/calls/callInstance.ts b/src/lib/calls/callInstance.ts index a9dcebdd..3cf38b04 100644 --- a/src/lib/calls/callInstance.ts +++ b/src/lib/calls/callInstance.ts @@ -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'); }); } diff --git a/src/lib/rootScope.ts b/src/lib/rootScope.ts index 7cc62154..d07175f1 100644 --- a/src/lib/rootScope.ts +++ b/src/lib/rootScope.ts @@ -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<{ diff --git a/src/scss/partials/_chat.scss b/src/scss/partials/_chat.scss index 5dba219f..ffe006f9 100644 --- a/src/scss/partials/_chat.scss +++ b/src/scss/partials/_chat.scss @@ -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; diff --git a/src/scss/tgico/_style.scss b/src/scss/tgico/_style.scss index 12d5fc2a..8b664540 100644 --- a/src/scss/tgico/_style.scss +++ b/src/scss/tgico/_style.scss @@ -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; diff --git a/src/scss/tgico/_variables.scss b/src/scss/tgico/_variables.scss index b4463fea..8dcc0ac3 100644 --- a/src/scss/tgico/_variables.scss +++ b/src/scss/tgico/_variables.scss @@ -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"; diff --git a/src/tests/slicedArray.test.ts b/src/tests/slicedArray.test.ts index 574073aa..28181faf 100644 --- a/src/tests/slicedArray.test.ts +++ b/src/tests/slicedArray.test.ts @@ -7,23 +7,27 @@ test('Slicing returns new Slice', () => { }); describe('Inserting', () => { - const sliced = new SlicedArray(); + const sliced = new SlicedArray(); // @ts-ignore const slices = sliced.slices; + + const toSomething = (v: number) => { + return '' + v; + }; - 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 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 = 90; - const values: number[] = []; + 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; test('Insert arrays with gap & join them', () => { slices[0].length = 0;