morethanwords
3 years ago
57 changed files with 4112 additions and 3107 deletions
@ -0,0 +1,6 @@ |
|||||||
|
### 0.8.6 |
||||||
|
* Added changelogs. |
||||||
|
* Audio player improvements: seek, next/previous buttons, volume controls. Changing volume in the video player will affect audio as well. |
||||||
|
* Fixed inability to delete multiple items from pending scheduled messages. |
||||||
|
* Fixed accidentally deleting media messages after removing their captions. |
||||||
|
* Fixed editing album captions. |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,57 @@ |
|||||||
|
/* |
||||||
|
* https://github.com/morethanwords/tweb
|
||||||
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||||
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||||
|
*/ |
||||||
|
|
||||||
|
import AvatarListLoader from "../helpers/avatarListLoader"; |
||||||
|
import appImManager from "../lib/appManagers/appImManager"; |
||||||
|
import appPhotosManager from "../lib/appManagers/appPhotosManager"; |
||||||
|
import AppMediaViewerBase from "./appMediaViewerBase"; |
||||||
|
|
||||||
|
type AppMediaViewerAvatarTargetType = {element: HTMLElement, photoId: string}; |
||||||
|
export default class AppMediaViewerAvatar extends AppMediaViewerBase<'', 'delete', AppMediaViewerAvatarTargetType> { |
||||||
|
public peerId: number; |
||||||
|
|
||||||
|
constructor(peerId: number) { |
||||||
|
super(new AvatarListLoader({peerId}), [/* 'delete' */]); |
||||||
|
|
||||||
|
this.peerId = peerId; |
||||||
|
|
||||||
|
this.setBtnMenuToggle([{ |
||||||
|
icon: 'download', |
||||||
|
text: 'MediaViewer.Context.Download', |
||||||
|
onClick: this.onDownloadClick |
||||||
|
}/* , { |
||||||
|
icon: 'delete danger btn-disabled', |
||||||
|
text: 'Delete', |
||||||
|
onClick: () => {} |
||||||
|
} */]); |
||||||
|
|
||||||
|
// * constructing html end
|
||||||
|
|
||||||
|
this.setListeners(); |
||||||
|
} |
||||||
|
|
||||||
|
onPrevClick = (target: AppMediaViewerAvatarTargetType) => { |
||||||
|
this.openMedia(target.photoId, target.element, -1); |
||||||
|
}; |
||||||
|
|
||||||
|
onNextClick = (target: AppMediaViewerAvatarTargetType) => { |
||||||
|
this.openMedia(target.photoId, target.element, 1); |
||||||
|
}; |
||||||
|
|
||||||
|
onDownloadClick = () => { |
||||||
|
appPhotosManager.savePhotoFile(appPhotosManager.getPhoto(this.target.photoId), appImManager.chat.bubbles.lazyLoadQueue.queueId); |
||||||
|
}; |
||||||
|
|
||||||
|
public async openMedia(photoId: string, target?: HTMLElement, fromRight = 0, prevTargets?: AppMediaViewerAvatarTargetType[], nextTargets?: AppMediaViewerAvatarTargetType[]) { |
||||||
|
if(this.setMoverPromise) return this.setMoverPromise; |
||||||
|
|
||||||
|
const photo = appPhotosManager.getPhoto(photoId); |
||||||
|
const ret = super._openMedia(photo, photo.date, this.peerId, fromRight, target, false, prevTargets, nextTargets); |
||||||
|
this.target.photoId = photo.id; |
||||||
|
|
||||||
|
return ret; |
||||||
|
} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,357 @@ |
|||||||
|
/* |
||||||
|
* https://github.com/morethanwords/tweb
|
||||||
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||||
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||||
|
*/ |
||||||
|
|
||||||
|
import PARALLAX_SUPPORTED from "../environment/parallaxSupport"; |
||||||
|
import { copyTextToClipboard } from "../helpers/clipboard"; |
||||||
|
import replaceContent from "../helpers/dom/replaceContent"; |
||||||
|
import { fastRaf } from "../helpers/schedulers"; |
||||||
|
import { User } from "../layer"; |
||||||
|
import { Channel } from "../lib/appManagers/appChatsManager"; |
||||||
|
import appImManager from "../lib/appManagers/appImManager"; |
||||||
|
import appMessagesManager from "../lib/appManagers/appMessagesManager"; |
||||||
|
import appNotificationsManager from "../lib/appManagers/appNotificationsManager"; |
||||||
|
import appPeersManager from "../lib/appManagers/appPeersManager"; |
||||||
|
import appProfileManager from "../lib/appManagers/appProfileManager"; |
||||||
|
import appUsersManager from "../lib/appManagers/appUsersManager"; |
||||||
|
import I18n from "../lib/langPack"; |
||||||
|
import RichTextProcessor from "../lib/richtextprocessor"; |
||||||
|
import rootScope from "../lib/rootScope"; |
||||||
|
import AvatarElement from "./avatar"; |
||||||
|
import CheckboxField from "./checkboxField"; |
||||||
|
import generateVerifiedIcon from "./generateVerifiedIcon"; |
||||||
|
import PeerProfileAvatars from "./peerProfileAvatars"; |
||||||
|
import PeerTitle from "./peerTitle"; |
||||||
|
import Row from "./row"; |
||||||
|
import Scrollable from "./scrollable"; |
||||||
|
import { SettingSection, generateDelimiter } from "./sidebarLeft"; |
||||||
|
import { toast } from "./toast"; |
||||||
|
|
||||||
|
let setText = (text: string, row: Row) => { |
||||||
|
//fastRaf(() => {
|
||||||
|
row.title.innerHTML = text; |
||||||
|
row.container.style.display = ''; |
||||||
|
//});
|
||||||
|
}; |
||||||
|
|
||||||
|
export default class PeerProfile { |
||||||
|
public element: HTMLElement; |
||||||
|
public avatars: PeerProfileAvatars; |
||||||
|
private avatar: AvatarElement; |
||||||
|
private section: SettingSection; |
||||||
|
private name: HTMLDivElement; |
||||||
|
private subtitle: HTMLDivElement; |
||||||
|
private bio: Row; |
||||||
|
private username: Row; |
||||||
|
private phone: Row; |
||||||
|
private notifications: Row; |
||||||
|
|
||||||
|
private cleaned: boolean; |
||||||
|
private setBioTimeout: number; |
||||||
|
private setPeerStatusInterval: number; |
||||||
|
|
||||||
|
private peerId = 0; |
||||||
|
private threadId: number; |
||||||
|
|
||||||
|
constructor(public scrollable: Scrollable) { |
||||||
|
if(!PARALLAX_SUPPORTED) { |
||||||
|
this.scrollable.container.classList.add('no-parallax'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public init() { |
||||||
|
this.init = null; |
||||||
|
|
||||||
|
this.element = document.createElement('div'); |
||||||
|
this.element.classList.add('profile-content'); |
||||||
|
|
||||||
|
this.section = new SettingSection({ |
||||||
|
noDelimiter: true |
||||||
|
}); |
||||||
|
|
||||||
|
this.avatar = new AvatarElement(); |
||||||
|
this.avatar.classList.add('profile-avatar', 'avatar-120'); |
||||||
|
this.avatar.setAttribute('dialog', '1'); |
||||||
|
this.avatar.setAttribute('clickable', ''); |
||||||
|
|
||||||
|
this.name = document.createElement('div'); |
||||||
|
this.name.classList.add('profile-name'); |
||||||
|
|
||||||
|
this.subtitle = document.createElement('div'); |
||||||
|
this.subtitle.classList.add('profile-subtitle'); |
||||||
|
|
||||||
|
this.bio = new Row({ |
||||||
|
title: ' ', |
||||||
|
subtitleLangKey: 'UserBio', |
||||||
|
icon: 'info', |
||||||
|
clickable: (e) => { |
||||||
|
if((e.target as HTMLElement).tagName === 'A') { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
appProfileManager.getProfileByPeerId(this.peerId).then(full => { |
||||||
|
copyTextToClipboard(full.about); |
||||||
|
toast(I18n.format('BioCopied', true)); |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
this.bio.title.classList.add('pre-wrap'); |
||||||
|
|
||||||
|
this.username = new Row({ |
||||||
|
title: ' ', |
||||||
|
subtitleLangKey: 'Username', |
||||||
|
icon: 'username', |
||||||
|
clickable: () => { |
||||||
|
const peer: Channel | User.user = appPeersManager.getPeer(this.peerId); |
||||||
|
copyTextToClipboard('@' + peer.username); |
||||||
|
toast(I18n.format('UsernameCopied', true)); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
this.phone = new Row({ |
||||||
|
title: ' ', |
||||||
|
subtitleLangKey: 'Phone', |
||||||
|
icon: 'phone', |
||||||
|
clickable: () => { |
||||||
|
const peer: User = appUsersManager.getUser(this.peerId); |
||||||
|
copyTextToClipboard('+' + peer.phone); |
||||||
|
toast(I18n.format('PhoneCopied', true)); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
this.notifications = new Row({ |
||||||
|
checkboxField: new CheckboxField({toggle: true}), |
||||||
|
titleLangKey: 'Notifications', |
||||||
|
icon: 'unmute' |
||||||
|
}); |
||||||
|
|
||||||
|
this.section.content.append(this.phone.container, this.username.container, this.bio.container, this.notifications.container); |
||||||
|
|
||||||
|
this.element.append(this.section.container, generateDelimiter()); |
||||||
|
|
||||||
|
this.notifications.checkboxField.input.addEventListener('change', (e) => { |
||||||
|
if(!e.isTrusted) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
//let checked = this.notificationsCheckbox.checked;
|
||||||
|
appMessagesManager.mutePeer(this.peerId); |
||||||
|
}); |
||||||
|
|
||||||
|
rootScope.addEventListener('dialog_notify_settings', (dialog) => { |
||||||
|
if(this.peerId === dialog.peerId) { |
||||||
|
const muted = appNotificationsManager.isPeerLocalMuted(this.peerId, false); |
||||||
|
this.notifications.checkboxField.checked = !muted; |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
rootScope.addEventListener('peer_typings', ({peerId}) => { |
||||||
|
if(this.peerId === peerId) { |
||||||
|
this.setPeerStatus(); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
rootScope.addEventListener('peer_bio_edit', (peerId) => { |
||||||
|
if(peerId === this.peerId) { |
||||||
|
this.setBio(true); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
rootScope.addEventListener('user_update', (userId) => { |
||||||
|
if(this.peerId === userId) { |
||||||
|
this.setPeerStatus(); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
rootScope.addEventListener('contacts_update', (userId) => { |
||||||
|
if(this.peerId === userId) { |
||||||
|
const user = appUsersManager.getUser(userId); |
||||||
|
if(!user.pFlags.self) { |
||||||
|
if(user.phone) { |
||||||
|
setText(appUsersManager.formatUserPhone(user.phone), this.phone); |
||||||
|
} else { |
||||||
|
this.phone.container.style.display = 'none'; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
this.setPeerStatusInterval = window.setInterval(this.setPeerStatus, 60e3); |
||||||
|
} |
||||||
|
|
||||||
|
public setPeerStatus = (needClear = false) => { |
||||||
|
if(!this.peerId) return; |
||||||
|
|
||||||
|
const peerId = this.peerId; |
||||||
|
appImManager.setPeerStatus(this.peerId, this.subtitle, needClear, true, () => peerId === this.peerId); |
||||||
|
}; |
||||||
|
|
||||||
|
public cleanupHTML() { |
||||||
|
this.bio.container.style.display = 'none'; |
||||||
|
this.phone.container.style.display = 'none'; |
||||||
|
this.username.container.style.display = 'none'; |
||||||
|
this.notifications.container.style.display = ''; |
||||||
|
this.notifications.checkboxField.checked = true; |
||||||
|
if(this.setBioTimeout) { |
||||||
|
window.clearTimeout(this.setBioTimeout); |
||||||
|
this.setBioTimeout = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public setAvatar() { |
||||||
|
if(this.peerId !== rootScope.myId) { |
||||||
|
const photo = appPeersManager.getPeerPhoto(this.peerId); |
||||||
|
|
||||||
|
if(photo) { |
||||||
|
const oldAvatars = this.avatars; |
||||||
|
this.avatars = new PeerProfileAvatars(this.scrollable); |
||||||
|
this.avatars.setPeer(this.peerId); |
||||||
|
this.avatars.info.append(this.name, this.subtitle); |
||||||
|
|
||||||
|
this.avatar.remove(); |
||||||
|
|
||||||
|
if(oldAvatars) oldAvatars.container.replaceWith(this.avatars.container); |
||||||
|
else this.element.prepend(this.avatars.container); |
||||||
|
|
||||||
|
if(PARALLAX_SUPPORTED) { |
||||||
|
this.scrollable.container.classList.add('parallax'); |
||||||
|
} |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if(PARALLAX_SUPPORTED) { |
||||||
|
this.scrollable.container.classList.remove('parallax'); |
||||||
|
} |
||||||
|
|
||||||
|
if(this.avatars) { |
||||||
|
this.avatars.container.remove(); |
||||||
|
this.avatars = undefined; |
||||||
|
} |
||||||
|
|
||||||
|
this.avatar.setAttribute('peer', '' + this.peerId); |
||||||
|
|
||||||
|
this.section.content.prepend(this.avatar, this.name, this.subtitle); |
||||||
|
} |
||||||
|
|
||||||
|
public fillProfileElements() { |
||||||
|
if(!this.cleaned) return; |
||||||
|
this.cleaned = false; |
||||||
|
|
||||||
|
const peerId = this.peerId; |
||||||
|
|
||||||
|
this.cleanupHTML(); |
||||||
|
|
||||||
|
this.setAvatar(); |
||||||
|
|
||||||
|
// username
|
||||||
|
if(peerId !== rootScope.myId) { |
||||||
|
let username = appPeersManager.getPeerUsername(peerId); |
||||||
|
if(username) { |
||||||
|
setText(appPeersManager.getPeerUsername(peerId), this.username); |
||||||
|
} |
||||||
|
|
||||||
|
const muted = appNotificationsManager.isPeerLocalMuted(peerId, false); |
||||||
|
this.notifications.checkboxField.checked = !muted; |
||||||
|
} else { |
||||||
|
fastRaf(() => { |
||||||
|
this.notifications.container.style.display = 'none'; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
//let membersLi = this.profileTabs.firstElementChild.children[0] as HTMLLIElement;
|
||||||
|
if(peerId > 0) { |
||||||
|
//membersLi.style.display = 'none';
|
||||||
|
|
||||||
|
let user = appUsersManager.getUser(peerId); |
||||||
|
if(user.phone && peerId !== rootScope.myId) { |
||||||
|
setText(appUsersManager.formatUserPhone(user.phone), this.phone); |
||||||
|
} |
||||||
|
}/* else { |
||||||
|
//membersLi.style.display = appPeersManager.isBroadcast(peerId) ? 'none' : '';
|
||||||
|
} */ |
||||||
|
|
||||||
|
this.setBio(); |
||||||
|
|
||||||
|
replaceContent(this.name, new PeerTitle({ |
||||||
|
peerId, |
||||||
|
dialog: true, |
||||||
|
}).element); |
||||||
|
|
||||||
|
const peer = appPeersManager.getPeer(peerId); |
||||||
|
if(peer?.pFlags?.verified) { |
||||||
|
this.name.append(generateVerifiedIcon()); |
||||||
|
} |
||||||
|
|
||||||
|
this.setPeerStatus(true); |
||||||
|
} |
||||||
|
|
||||||
|
public setBio(override?: true) { |
||||||
|
if(this.setBioTimeout) { |
||||||
|
window.clearTimeout(this.setBioTimeout); |
||||||
|
this.setBioTimeout = 0; |
||||||
|
} |
||||||
|
|
||||||
|
const peerId = this.peerId; |
||||||
|
const threadId = this.threadId; |
||||||
|
|
||||||
|
if(!peerId) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
let promise: Promise<boolean>; |
||||||
|
if(peerId > 0) { |
||||||
|
promise = appProfileManager.getProfile(peerId, override).then(userFull => { |
||||||
|
if(this.peerId !== peerId || this.threadId !== threadId) { |
||||||
|
//this.log.warn('peer changed');
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
if(userFull.rAbout && peerId !== rootScope.myId) { |
||||||
|
setText(userFull.rAbout, this.bio); |
||||||
|
} |
||||||
|
|
||||||
|
//this.log('userFull', userFull);
|
||||||
|
return true; |
||||||
|
}); |
||||||
|
} else { |
||||||
|
promise = appProfileManager.getChatFull(-peerId, override).then((chatFull) => { |
||||||
|
if(this.peerId !== peerId || this.threadId !== threadId) { |
||||||
|
//this.log.warn('peer changed');
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
//this.log('chatInfo res 2:', chatFull);
|
||||||
|
|
||||||
|
if(chatFull.about) { |
||||||
|
setText(RichTextProcessor.wrapRichText(chatFull.about), this.bio); |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
promise.then((canSetNext) => { |
||||||
|
if(canSetNext) { |
||||||
|
this.setBioTimeout = window.setTimeout(() => this.setBio(true), 60e3); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public setPeer(peerId: number, threadId = 0) { |
||||||
|
if(this.peerId === peerId && this.threadId === peerId) return; |
||||||
|
|
||||||
|
if(this.init) { |
||||||
|
this.init(); |
||||||
|
} |
||||||
|
|
||||||
|
this.peerId = peerId; |
||||||
|
this.threadId = threadId; |
||||||
|
|
||||||
|
this.cleaned = true; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,332 @@ |
|||||||
|
import PARALLAX_SUPPORTED from "../environment/parallaxSupport"; |
||||||
|
import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport"; |
||||||
|
import { cancelEvent } from "../helpers/dom/cancelEvent"; |
||||||
|
import { attachClickEvent } from "../helpers/dom/clickEvent"; |
||||||
|
import renderImageFromUrl from "../helpers/dom/renderImageFromUrl"; |
||||||
|
import filterChatPhotosMessages from "../helpers/filterChatPhotosMessages"; |
||||||
|
import ListLoader from "../helpers/listLoader"; |
||||||
|
import { fastRaf } from "../helpers/schedulers"; |
||||||
|
import { Message, ChatFull, MessageAction, Photo } from "../layer"; |
||||||
|
import appAvatarsManager from "../lib/appManagers/appAvatarsManager"; |
||||||
|
import appDownloadManager from "../lib/appManagers/appDownloadManager"; |
||||||
|
import appMessagesManager, { AppMessagesManager } from "../lib/appManagers/appMessagesManager"; |
||||||
|
import appPeersManager from "../lib/appManagers/appPeersManager"; |
||||||
|
import appPhotosManager from "../lib/appManagers/appPhotosManager"; |
||||||
|
import appProfileManager from "../lib/appManagers/appProfileManager"; |
||||||
|
import { openAvatarViewer } from "./avatar"; |
||||||
|
import Scrollable from "./scrollable"; |
||||||
|
import SwipeHandler from "./swipeHandler"; |
||||||
|
|
||||||
|
export default class PeerProfileAvatars { |
||||||
|
private static BASE_CLASS = 'profile-avatars'; |
||||||
|
private static SCALE = PARALLAX_SUPPORTED ? 2 : 1; |
||||||
|
private static TRANSLATE_TEMPLATE = PARALLAX_SUPPORTED ? `translate3d({x}, 0, -1px) scale(${PeerProfileAvatars.SCALE})` : 'translate({x}, 0)'; |
||||||
|
public container: HTMLElement; |
||||||
|
public avatars: HTMLElement; |
||||||
|
public gradient: HTMLElement; |
||||||
|
public info: HTMLElement; |
||||||
|
public arrowPrevious: HTMLElement; |
||||||
|
public arrowNext: HTMLElement; |
||||||
|
private tabs: HTMLDivElement; |
||||||
|
private listLoader: ListLoader<string | Message.messageService, string | Message.messageService>; |
||||||
|
private peerId: number; |
||||||
|
|
||||||
|
constructor(public scrollable: Scrollable) { |
||||||
|
this.container = document.createElement('div'); |
||||||
|
this.container.classList.add(PeerProfileAvatars.BASE_CLASS + '-container'); |
||||||
|
|
||||||
|
this.avatars = document.createElement('div'); |
||||||
|
this.avatars.classList.add(PeerProfileAvatars.BASE_CLASS + '-avatars'); |
||||||
|
|
||||||
|
this.gradient = document.createElement('div'); |
||||||
|
this.gradient.classList.add(PeerProfileAvatars.BASE_CLASS + '-gradient'); |
||||||
|
|
||||||
|
this.info = document.createElement('div'); |
||||||
|
this.info.classList.add(PeerProfileAvatars.BASE_CLASS + '-info'); |
||||||
|
|
||||||
|
this.tabs = document.createElement('div'); |
||||||
|
this.tabs.classList.add(PeerProfileAvatars.BASE_CLASS + '-tabs'); |
||||||
|
|
||||||
|
this.arrowPrevious = document.createElement('div'); |
||||||
|
this.arrowPrevious.classList.add(PeerProfileAvatars.BASE_CLASS + '-arrow'); |
||||||
|
|
||||||
|
/* const previousIcon = document.createElement('i'); |
||||||
|
previousIcon.classList.add(PeerProfileAvatars.BASE_CLASS + '-arrow-icon', 'tgico-previous'); |
||||||
|
this.arrowBack.append(previousIcon); */ |
||||||
|
|
||||||
|
this.arrowNext = document.createElement('div'); |
||||||
|
this.arrowNext.classList.add(PeerProfileAvatars.BASE_CLASS + '-arrow', PeerProfileAvatars.BASE_CLASS + '-arrow-next'); |
||||||
|
|
||||||
|
/* const nextIcon = document.createElement('i'); |
||||||
|
nextIcon.classList.add(PeerProfileAvatars.BASE_CLASS + '-arrow-icon', 'tgico-next'); |
||||||
|
this.arrowNext.append(nextIcon); */ |
||||||
|
|
||||||
|
this.container.append(this.avatars, this.gradient, this.info, this.tabs, this.arrowPrevious, this.arrowNext); |
||||||
|
|
||||||
|
const checkScrollTop = () => { |
||||||
|
if(this.scrollable.scrollTop !== 0) { |
||||||
|
this.scrollable.scrollIntoViewNew(this.scrollable.container.firstElementChild as HTMLElement, 'start'); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
}; |
||||||
|
|
||||||
|
const SWITCH_ZONE = 1 / 3; |
||||||
|
let cancel = false; |
||||||
|
let freeze = false; |
||||||
|
attachClickEvent(this.container, async(_e) => { |
||||||
|
if(freeze) { |
||||||
|
cancelEvent(_e); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if(cancel) { |
||||||
|
cancel = false; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if(!checkScrollTop()) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const rect = this.container.getBoundingClientRect(); |
||||||
|
|
||||||
|
// const e = (_e as TouchEvent).touches ? (_e as TouchEvent).touches[0] : _e as MouseEvent;
|
||||||
|
const e = _e; |
||||||
|
const x = e.pageX; |
||||||
|
|
||||||
|
const clickX = x - rect.left; |
||||||
|
if((!this.listLoader.previous.length && !this.listLoader.next.length) |
||||||
|
|| (clickX > (rect.width * SWITCH_ZONE) && clickX < (rect.width - rect.width * SWITCH_ZONE))) { |
||||||
|
const peerId = this.peerId; |
||||||
|
|
||||||
|
const targets: {element: HTMLElement, item: string | Message.messageService}[] = []; |
||||||
|
this.listLoader.previous.concat(this.listLoader.current, this.listLoader.next).forEach((item, idx) => { |
||||||
|
targets.push({ |
||||||
|
element: /* null */this.avatars.children[idx] as HTMLElement, |
||||||
|
item |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
const prevTargets = targets.slice(0, this.listLoader.previous.length); |
||||||
|
const nextTargets = targets.slice(this.listLoader.previous.length + 1); |
||||||
|
|
||||||
|
const target = this.avatars.children[this.listLoader.previous.length] as HTMLElement; |
||||||
|
freeze = true; |
||||||
|
openAvatarViewer(target, peerId, () => peerId === this.peerId, this.listLoader.current, prevTargets, nextTargets); |
||||||
|
freeze = false; |
||||||
|
} else { |
||||||
|
const centerX = rect.right - (rect.width / 2); |
||||||
|
const toRight = x > centerX; |
||||||
|
|
||||||
|
// this.avatars.classList.remove('no-transition');
|
||||||
|
// fastRaf(() => {
|
||||||
|
this.avatars.classList.add('no-transition'); |
||||||
|
void this.avatars.offsetLeft; // reflow
|
||||||
|
|
||||||
|
let distance: number; |
||||||
|
if(this.listLoader.index === 0 && !toRight) distance = this.listLoader.count - 1; |
||||||
|
else if(this.listLoader.index === (this.listLoader.count - 1) && toRight) distance = -(this.listLoader.count - 1); |
||||||
|
else distance = toRight ? 1 : -1; |
||||||
|
this.listLoader.go(distance); |
||||||
|
|
||||||
|
fastRaf(() => { |
||||||
|
this.avatars.classList.remove('no-transition'); |
||||||
|
}); |
||||||
|
// });
|
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
const cancelNextClick = () => { |
||||||
|
cancel = true; |
||||||
|
document.body.addEventListener(IS_TOUCH_SUPPORTED ? 'touchend' : 'click', (e) => { |
||||||
|
cancel = false; |
||||||
|
}, {once: true}); |
||||||
|
}; |
||||||
|
|
||||||
|
let width = 0, x = 0, lastDiffX = 0, lastIndex = 0, minX = 0; |
||||||
|
const swipeHandler = new SwipeHandler({ |
||||||
|
element: this.avatars, |
||||||
|
onSwipe: (xDiff, yDiff) => { |
||||||
|
lastDiffX = xDiff; |
||||||
|
let lastX = x + xDiff * -PeerProfileAvatars.SCALE; |
||||||
|
if(lastX > 0) lastX = 0; |
||||||
|
else if(lastX < minX) lastX = minX; |
||||||
|
|
||||||
|
this.avatars.style.transform = PeerProfileAvatars.TRANSLATE_TEMPLATE.replace('{x}', lastX + 'px'); |
||||||
|
//console.log(xDiff, yDiff);
|
||||||
|
return false; |
||||||
|
}, |
||||||
|
verifyTouchTarget: (e) => { |
||||||
|
if(!checkScrollTop()) { |
||||||
|
cancelNextClick(); |
||||||
|
cancelEvent(e); |
||||||
|
return false; |
||||||
|
} else if(this.container.classList.contains('is-single') || freeze) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
}, |
||||||
|
onFirstSwipe: () => { |
||||||
|
const rect = this.avatars.getBoundingClientRect(); |
||||||
|
width = rect.width; |
||||||
|
minX = -width * (this.tabs.childElementCount - 1); |
||||||
|
|
||||||
|
/* lastIndex = whichChild(this.tabs.querySelector('.active')); |
||||||
|
x = -width * lastIndex; */ |
||||||
|
x = rect.left - this.container.getBoundingClientRect().left; |
||||||
|
|
||||||
|
this.avatars.style.transform = PeerProfileAvatars.TRANSLATE_TEMPLATE.replace('{x}', x + 'px'); |
||||||
|
|
||||||
|
this.container.classList.add('is-swiping'); |
||||||
|
this.avatars.classList.add('no-transition'); |
||||||
|
void this.avatars.offsetLeft; // reflow
|
||||||
|
}, |
||||||
|
onReset: () => { |
||||||
|
const addIndex = Math.ceil(Math.abs(lastDiffX) / (width / PeerProfileAvatars.SCALE)) * (lastDiffX >= 0 ? 1 : -1); |
||||||
|
cancelNextClick(); |
||||||
|
|
||||||
|
//console.log(addIndex);
|
||||||
|
|
||||||
|
this.avatars.classList.remove('no-transition'); |
||||||
|
fastRaf(() => { |
||||||
|
this.listLoader.go(addIndex); |
||||||
|
this.container.classList.remove('is-swiping'); |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public setPeer(peerId: number) { |
||||||
|
this.peerId = peerId; |
||||||
|
|
||||||
|
const photo = appPeersManager.getPeerPhoto(peerId); |
||||||
|
if(!photo) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const listLoader: PeerProfileAvatars['listLoader'] = this.listLoader = new ListLoader({ |
||||||
|
loadCount: 50, |
||||||
|
loadMore: (anchor, older, loadCount) => { |
||||||
|
if(!older) return Promise.resolve({count: undefined, items: []}); |
||||||
|
|
||||||
|
if(peerId > 0) { |
||||||
|
const maxId: string = (anchor || listLoader.current) as any; |
||||||
|
return appPhotosManager.getUserPhotos(peerId, maxId, loadCount).then(value => { |
||||||
|
return { |
||||||
|
count: value.count, |
||||||
|
items: value.photos |
||||||
|
}; |
||||||
|
}); |
||||||
|
} else { |
||||||
|
const promises: [Promise<ChatFull>, ReturnType<AppMessagesManager['getSearch']>] = [] as any; |
||||||
|
if(!listLoader.current) { |
||||||
|
promises.push(appProfileManager.getChatFull(-peerId)); |
||||||
|
} |
||||||
|
|
||||||
|
promises.push(appMessagesManager.getSearch({ |
||||||
|
peerId, |
||||||
|
maxId: Number.MAX_SAFE_INTEGER, |
||||||
|
inputFilter: { |
||||||
|
_: 'inputMessagesFilterChatPhotos' |
||||||
|
}, |
||||||
|
limit: loadCount, |
||||||
|
backLimit: 0 |
||||||
|
})); |
||||||
|
|
||||||
|
return Promise.all(promises).then((result) => { |
||||||
|
const value = result.pop() as typeof result[1]; |
||||||
|
|
||||||
|
filterChatPhotosMessages(value); |
||||||
|
|
||||||
|
if(!listLoader.current) { |
||||||
|
const chatFull = result[0]; |
||||||
|
const message = value.history.findAndSplice(m => { |
||||||
|
return ((m as Message.messageService).action as MessageAction.messageActionChannelEditPhoto).photo.id === chatFull.chat_photo.id; |
||||||
|
}) as Message.messageService; |
||||||
|
|
||||||
|
listLoader.current = message || appMessagesManager.generateFakeAvatarMessage(this.peerId, chatFull.chat_photo); |
||||||
|
} |
||||||
|
|
||||||
|
//console.log('avatars loaded:', value);
|
||||||
|
return { |
||||||
|
count: value.count, |
||||||
|
items: value.history |
||||||
|
}; |
||||||
|
}); |
||||||
|
} |
||||||
|
}, |
||||||
|
processItem: this.processItem, |
||||||
|
onJump: (item, older) => { |
||||||
|
const id = this.listLoader.index; |
||||||
|
//const nextId = Math.max(0, id);
|
||||||
|
const x = 100 * PeerProfileAvatars.SCALE * id; |
||||||
|
this.avatars.style.transform = PeerProfileAvatars.TRANSLATE_TEMPLATE.replace('{x}', `-${x}%`); |
||||||
|
|
||||||
|
const activeTab = this.tabs.querySelector('.active'); |
||||||
|
if(activeTab) activeTab.classList.remove('active'); |
||||||
|
|
||||||
|
const tab = this.tabs.children[id] as HTMLElement; |
||||||
|
tab.classList.add('active'); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
if(photo._ === 'userProfilePhoto') { |
||||||
|
listLoader.current = photo.photo_id; |
||||||
|
} |
||||||
|
|
||||||
|
this.processItem(listLoader.current); |
||||||
|
|
||||||
|
// listLoader.loaded
|
||||||
|
listLoader.load(true); |
||||||
|
} |
||||||
|
|
||||||
|
public addTab() { |
||||||
|
const tab = document.createElement('div'); |
||||||
|
tab.classList.add(PeerProfileAvatars.BASE_CLASS + '-tab'); |
||||||
|
this.tabs.append(tab); |
||||||
|
|
||||||
|
if(this.tabs.childElementCount === 1) { |
||||||
|
tab.classList.add('active'); |
||||||
|
} |
||||||
|
|
||||||
|
this.container.classList.toggle('is-single', this.tabs.childElementCount <= 1); |
||||||
|
} |
||||||
|
|
||||||
|
public processItem = (photoId: string | Message.messageService) => { |
||||||
|
const avatar = document.createElement('div'); |
||||||
|
avatar.classList.add(PeerProfileAvatars.BASE_CLASS + '-avatar'); |
||||||
|
|
||||||
|
let photo: Photo.photo; |
||||||
|
if(photoId) { |
||||||
|
photo = typeof(photoId) === 'string' ? |
||||||
|
appPhotosManager.getPhoto(photoId) : |
||||||
|
(photoId.action as MessageAction.messageActionChannelEditPhoto).photo as Photo.photo; |
||||||
|
} |
||||||
|
|
||||||
|
const img = new Image(); |
||||||
|
img.classList.add(PeerProfileAvatars.BASE_CLASS + '-avatar-image'); |
||||||
|
img.draggable = false; |
||||||
|
|
||||||
|
if(photo) { |
||||||
|
const size = appPhotosManager.choosePhotoSize(photo, 420, 420, false); |
||||||
|
appPhotosManager.preloadPhoto(photo, size).then(() => { |
||||||
|
const cacheContext = appDownloadManager.getCacheContext(photo, size.type); |
||||||
|
renderImageFromUrl(img, cacheContext.url, () => { |
||||||
|
avatar.append(img); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} else { |
||||||
|
const photo = appPeersManager.getPeerPhoto(this.peerId); |
||||||
|
appAvatarsManager.putAvatar(avatar, this.peerId, photo, 'photo_big', img); |
||||||
|
} |
||||||
|
|
||||||
|
this.avatars.append(avatar); |
||||||
|
|
||||||
|
this.addTab(); |
||||||
|
|
||||||
|
return photoId; |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
import { IS_FIREFOX } from "./userAgent"; |
||||||
|
|
||||||
|
const PARALLAX_SUPPORTED = !IS_FIREFOX && false; |
||||||
|
|
||||||
|
export default PARALLAX_SUPPORTED; |
@ -0,0 +1,33 @@ |
|||||||
|
/* |
||||||
|
* https://github.com/morethanwords/tweb
|
||||||
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||||
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||||
|
*/ |
||||||
|
|
||||||
|
import appPhotosManager from "../lib/appManagers/appPhotosManager"; |
||||||
|
import ListLoader, { ListLoaderOptions } from "./listLoader"; |
||||||
|
|
||||||
|
export default class AvatarListLoader<Item extends {photoId: string}> extends ListLoader<Item, any> { |
||||||
|
private peerId: number; |
||||||
|
|
||||||
|
constructor(options: Omit<ListLoaderOptions<Item, any>, 'loadMore'> & {peerId: number}) { |
||||||
|
super({ |
||||||
|
...options, |
||||||
|
loadMore: (anchor, older, loadCount) => { |
||||||
|
if(this.peerId < 0 || !older) return Promise.resolve({count: 0, items: []}); // ! это значит, что открыло аватар чата, но следующих фотографий нет.
|
||||||
|
|
||||||
|
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; |
||||||
|
}); |
||||||
|
|
||||||
|
return {count: value.count, items}; |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
this.loadedAllUp = true; |
||||||
|
this.peerId = options.peerId; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,25 @@ |
|||||||
|
/* |
||||||
|
* https://github.com/morethanwords/tweb
|
||||||
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||||
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||||
|
*/ |
||||||
|
|
||||||
|
import type { Message, MessageAction } from "../layer"; |
||||||
|
import type { MyMessage } from "../lib/appManagers/appMessagesManager"; |
||||||
|
import { forEachReverse } from "./array"; |
||||||
|
|
||||||
|
export default function filterChatPhotosMessages(value: { |
||||||
|
count: number; |
||||||
|
next_rate: number; |
||||||
|
offset_id_offset: number; |
||||||
|
history: MyMessage[]; |
||||||
|
}) { |
||||||
|
forEachReverse(value.history, (message, idx, arr) => { |
||||||
|
if(!((message as Message.messageService).action as MessageAction.messageActionChatEditPhoto).photo) { |
||||||
|
arr.splice(idx, 1); |
||||||
|
if(value.count !== undefined) { |
||||||
|
--value.count; |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
@ -0,0 +1,161 @@ |
|||||||
|
/* |
||||||
|
* https://github.com/morethanwords/tweb
|
||||||
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||||
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||||
|
*/ |
||||||
|
|
||||||
|
import type { MediaSearchContext } from "../components/appMediaPlaybackController"; |
||||||
|
import type { SearchSuperContext } from "../components/appSearchSuper."; |
||||||
|
import type { Message } from "../layer"; |
||||||
|
import appMessagesIdsManager from "../lib/appManagers/appMessagesIdsManager"; |
||||||
|
import appMessagesManager from "../lib/appManagers/appMessagesManager"; |
||||||
|
import rootScope from "../lib/rootScope"; |
||||||
|
import { forEachReverse } from "./array"; |
||||||
|
import filterChatPhotosMessages from "./filterChatPhotosMessages"; |
||||||
|
import ListLoader, { ListLoaderOptions } from "./listLoader"; |
||||||
|
|
||||||
|
export default class SearchListLoader<Item extends {mid: number, peerId: number}> extends ListLoader<Item, Message.message> { |
||||||
|
public searchContext: MediaSearchContext; |
||||||
|
public onEmptied: () => void; |
||||||
|
|
||||||
|
constructor(options: Omit<ListLoaderOptions<Item, Message.message>, 'loadMore'> & {onEmptied?: () => void} = {}) { |
||||||
|
super({ |
||||||
|
...options, |
||||||
|
loadMore: (anchor, older, loadCount) => { |
||||||
|
const backLimit = older ? 0 : loadCount; |
||||||
|
let maxId = this.current?.mid; |
||||||
|
|
||||||
|
if(anchor) maxId = anchor.mid; |
||||||
|
if(!older) maxId = appMessagesIdsManager.incrementMessageId(maxId, 1); |
||||||
|
|
||||||
|
return appMessagesManager.getSearch({ |
||||||
|
...this.searchContext, |
||||||
|
peerId: this.searchContext.peerId || anchor?.peerId, |
||||||
|
maxId, |
||||||
|
limit: backLimit ? 0 : loadCount, |
||||||
|
backLimit |
||||||
|
}).then(value => { |
||||||
|
/* if(DEBUG) { |
||||||
|
this.log('loaded more media by maxId:', maxId, value, older, this.reverse); |
||||||
|
} */ |
||||||
|
|
||||||
|
if(this.searchContext.inputFilter._ === 'inputMessagesFilterChatPhotos') { |
||||||
|
filterChatPhotosMessages(value); |
||||||
|
} |
||||||
|
|
||||||
|
if(value.next_rate) { |
||||||
|
this.searchContext.nextRate = value.next_rate; |
||||||
|
} |
||||||
|
|
||||||
|
return {count: value.count, items: value.history}; |
||||||
|
}); |
||||||
|
}, |
||||||
|
processItem: (message) => { |
||||||
|
const filtered = this.filterMids([message.mid]); |
||||||
|
if(!filtered.length) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
return options.processItem(message); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
rootScope.addEventListener('history_delete', this.onHistoryDelete); |
||||||
|
rootScope.addEventListener('history_multiappend', this.onHistoryMultiappend); |
||||||
|
rootScope.addEventListener('message_sent', this.onMessageSent); |
||||||
|
} |
||||||
|
|
||||||
|
protected filterMids(mids: number[]) { |
||||||
|
const storage = this.searchContext.isScheduled ? |
||||||
|
appMessagesManager.getScheduledMessagesStorage(this.searchContext.peerId) : |
||||||
|
appMessagesManager.getMessagesStorage(this.searchContext.peerId); |
||||||
|
const filtered = appMessagesManager.filterMessagesByInputFilter(this.searchContext.inputFilter._, mids, storage, mids.length) as Message.message[]; |
||||||
|
return filtered; |
||||||
|
} |
||||||
|
|
||||||
|
protected onHistoryDelete = ({peerId, msgs}: {peerId: number, msgs: Set<number>}) => { |
||||||
|
const shouldBeDeleted = (item: Item) => item.peerId === peerId && msgs.has(item.mid); |
||||||
|
const filter = (item: Item, idx: number, arr: Item[]) => { |
||||||
|
if(shouldBeDeleted(item)) { |
||||||
|
arr.splice(idx, 1); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
forEachReverse(this.previous, filter); |
||||||
|
forEachReverse(this.next, filter); |
||||||
|
|
||||||
|
if(this.current && shouldBeDeleted(this.current)) { |
||||||
|
if(this.go(1)) { |
||||||
|
this.previous.splice(this.previous.length - 1, 1); |
||||||
|
} else if(this.go(-1)) { |
||||||
|
this.next.splice(0, 1); |
||||||
|
} else if(this.onEmptied) { |
||||||
|
this.onEmptied(); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
protected onHistoryMultiappend = (obj: { |
||||||
|
[peerId: string]: Set<number>; |
||||||
|
}) => { |
||||||
|
if(this.searchContext.folderId !== undefined) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// because it's reversed
|
||||||
|
if(!this.loadedAllUp || this.loadPromiseUp) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const mids = obj[this.searchContext.peerId]; |
||||||
|
if(!mids) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const sorted = Array.from(mids).sort((a, b) => a - b); |
||||||
|
const filtered = this.filterMids(sorted); |
||||||
|
const targets = filtered.map(message => this.processItem(message)).filter(Boolean); |
||||||
|
if(targets.length) { |
||||||
|
this.next.push(...targets); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
protected onMessageSent = ({message}: {message: Message.message}) => { |
||||||
|
this.onHistoryMultiappend({ |
||||||
|
[message.peerId]: new Set([message.mid]) |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
public setSearchContext(context: SearchSuperContext) { |
||||||
|
this.searchContext = context; |
||||||
|
|
||||||
|
if(this.searchContext.folderId !== undefined) { |
||||||
|
this.loadedAllUp = true; |
||||||
|
|
||||||
|
if(this.searchContext.nextRate === undefined) { |
||||||
|
this.loadedAllDown = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if(this.searchContext.inputFilter._ === 'inputMessagesFilterChatPhotos') { |
||||||
|
this.loadedAllUp = true; |
||||||
|
} |
||||||
|
|
||||||
|
if(!this.searchContext.useSearch) { |
||||||
|
this.loadedAllDown = this.loadedAllUp = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public reset() { |
||||||
|
super.reset(); |
||||||
|
this.searchContext = undefined; |
||||||
|
} |
||||||
|
|
||||||
|
public cleanup() { |
||||||
|
this.reset(); |
||||||
|
rootScope.removeEventListener('history_delete', this.onHistoryDelete); |
||||||
|
rootScope.removeEventListener('history_multiappend', this.onHistoryMultiappend); |
||||||
|
rootScope.removeEventListener('message_sent', this.onMessageSent); |
||||||
|
this.onEmptied = undefined; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
/* |
||||||
|
* https://github.com/morethanwords/tweb
|
||||||
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||||
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||||
|
*/ |
||||||
|
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
const fs = require('fs'); |
||||||
|
const text = fs.readFileSync('./CHANGELOG.md').toString('utf-8'); |
||||||
|
|
||||||
|
const writeTo = `./public/changelogs/{VERSION}.md`; |
||||||
|
|
||||||
|
const splitted = text.split('\n\n'); |
||||||
|
splitted.forEach(text => { |
||||||
|
text = text.replace(/^\*/gm, '•'); |
||||||
|
const splitted = text.split('\n'); |
||||||
|
const firstLine = splitted.shift(); |
||||||
|
fs.writeFileSync(writeTo.replace('{VERSION}', firstLine.substr(4)), splitted.join('\n')); |
||||||
|
}); |
Loading…
Reference in new issue