Media permissions

Check for rights for forward
This commit is contained in:
Eduard Kuzmenko 2023-02-27 17:11:37 +04:00
parent 5fdffa3b18
commit 820885dbd9
69 changed files with 1614 additions and 543 deletions

View File

@ -245,7 +245,7 @@ export class AppMediaPlaybackController extends EventListenerBase<{
return media;
}
const doc = getMediaFromMessage(message) as Document.document;
const doc = getMediaFromMessage(message, true) as Document.document;
storage.set(mid, media = document.createElement(doc.type === 'round' || doc.type === 'video' ? 'video' : 'audio'));
// const source = document.createElement('source');
// source.type = doc.type === 'voice' && !opusDecodeController.isPlaySupported() ? 'audio/wav' : doc.mime_type;
@ -431,7 +431,7 @@ export class AppMediaPlaybackController extends EventListenerBase<{
await onMediaLoad(playingMedia, undefined, false); // have to wait for load, otherwise on macOS won't set
const doc = getMediaFromMessage(message) as MyDocument;
const doc = getMediaFromMessage(message, true) as MyDocument;
const artwork: MediaImage[] = [];
@ -554,7 +554,7 @@ export class AppMediaPlaybackController extends EventListenerBase<{
const message = this.getMessageByMedia(playingMedia);
return {
doc: getMediaFromMessage(message) as MyDocument,
doc: getMediaFromMessage(message, true) as MyDocument,
message,
media: playingMedia,
playbackParams: this.getPlaybackParams()
@ -858,7 +858,7 @@ export class AppMediaPlaybackController extends EventListenerBase<{
}
private getPlaybackMediaTypeFromMessage(message: Message.message) {
const doc = getMediaFromMessage(message) as MyDocument;
const doc = getMediaFromMessage(message, true) as MyDocument;
let mediaType: PlaybackMediaType = 'audio';
if(doc?.type) {
if(doc.type === 'voice' || doc.type === 'round') {

View File

@ -51,7 +51,7 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
processItem: (item) => {
const isForDocument = this.searchContext.inputFilter._ === 'inputMessagesFilterDocument';
const {mid, peerId} = item;
const media: MyPhoto | MyDocument = getMediaFromMessage(item);
const media = getMediaFromMessage(item, true);
if(!media) return;
@ -226,7 +226,7 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
onDownloadClick = () => {
const {message} = this.target;
const media = getMediaFromMessage(message);
const media = getMediaFromMessage(message, true);
if(!media) return;
appDownloadManager.downloadToDisc({media, queueId: appImManager.chat.bubbles.lazyLoadQueue.queueId});
};
@ -258,7 +258,7 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
const mid = message.mid;
const fromId = (message as Message.message).fwd_from && !message.fromId ? (message as Message.message).fwd_from.from_name : message.fromId;
const media = getMediaFromMessage(message);
const media = getMediaFromMessage(message, true);
const noForwards = await this.managers.appPeersManager.noForwards(message.peerId);
const isServiceMessage = message._ === 'messageService';
@ -283,7 +283,7 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
this.wholeDiv.classList.toggle('no-forwards', cantDownloadMessage);
this.setCaption(message);
const promise = super._openMedia(media, message.date, fromId, fromRight, target, reverse, prevTargets, nextTargets, message/* , needLoadMore */);
const promise = super._openMedia(media as MyPhoto | MyDocument, message.date, fromId, fromRight, target, reverse, prevTargets, nextTargets, message/* , needLoadMore */);
this.target.mid = mid;
this.target.peerId = message.peerId;
this.target.message = message;

View File

@ -42,7 +42,7 @@ import EventListenerBase from '../helpers/eventListenerBase';
import {MyMessage} from '../lib/appManagers/appMessagesManager';
import {NULL_PEER_ID} from '../lib/mtproto/mtproto_config';
import {isFullScreen} from '../helpers/dom/fullScreen';
import {attachClickEvent} from '../helpers/dom/clickEvent';
import {attachClickEvent, hasMouseMovedSinceDown} from '../helpers/dom/clickEvent';
import SearchListLoader from '../helpers/searchListLoader';
import createVideo from '../helpers/dom/createVideo';
import {AppManagers} from '../lib/appManagers/managers';
@ -57,6 +57,7 @@ import {toastNew} from './toast';
import clamp from '../helpers/number/clamp';
import debounce from '../helpers/schedulers/debounce';
import isBetween from '../helpers/number/isBetween';
import findUpAsChild from '../helpers/dom/findUpAsChild';
const ZOOM_STEP = 0.5;
const ZOOM_INITIAL_VALUE = 1;
@ -137,6 +138,7 @@ export default class AppMediaViewerBase<
protected lastDragDelta: {x: number, y: number} = this.transform;
protected lastGestureTime: number;
protected clampZoomDebounced: ReturnType<typeof debounce<() => void>>;
ignoreNextClick: boolean;
get target() {
return this.listLoader.current;
@ -228,13 +230,25 @@ export default class AppMediaViewerBase<
}, ZOOM_INITIAL_VALUE);
this.zoomElements.rangeSelector.setListeners();
this.zoomElements.rangeSelector.setHandlers({
onScrub: this.setZoomValue,
onMouseUp: () => this.setZoomValue()
onScrub: (value) => {
const add = value - this.transform.scale;
this.addZoom(add);
this.clampZoomDebounced?.clearTimeout();
},
onMouseDown: () => {
this.moversContainer.classList.add('no-transition');
this.zoomElements.rangeSelector.container.classList.remove('with-transition');
},
onMouseUp: () => {
this.moversContainer.classList.remove('no-transition');
this.zoomElements.rangeSelector.container.classList.add('with-transition');
this.setZoomValue();
}
});
this.zoomElements.container.append(this.zoomElements.btnOut, this.zoomElements.rangeSelector.container, this.zoomElements.btnIn);
if(!IS_TOUCH_SUPPORTED && false) {
if(!IS_TOUCH_SUPPORTED) {
this.wholeDiv.append(this.zoomElements.container);
}
@ -349,7 +363,7 @@ export default class AppMediaViewerBase<
this.swipeHandler = new SwipeHandler({
element: this.wholeDiv,
onReset: this.onSwipeReset,
onFirstSwipe: this.onSwipeFirst,
onFirstSwipe: this.onSwipeFirst as any,
onSwipe: (xDiff, yDiff, e, cancelDrag) => {
if(isFullScreen()) {
return;
@ -400,6 +414,7 @@ export default class AppMediaViewerBase<
verifyTouchTarget: (e) => {
// * Fix for seek input
if(isFullScreen() ||
findUpAsChild(e.target as HTMLElement, this.zoomElements.container) ||
findUpClassName(e.target, 'ckin__controls') ||
findUpClassName(e.target, 'media-viewer-caption') ||
(findUpClassName(e.target, 'media-viewer-topbar') && e.type !== 'wheel')) {
@ -413,10 +428,13 @@ export default class AppMediaViewerBase<
});
}
protected onSwipeFirst = () => {
protected onSwipeFirst = (e: MouseEvent | TouchEvent | WheelEvent) => {
this.lastDragOffset = this.lastDragDelta = {x: 0, y: 0};
this.lastTransform = {...this.transform};
this.moversContainer.classList.add('no-transition');
if(e.type !== 'wheel' || !this.ctrlKeyDown) { // keep transition for real mouse wheel
this.moversContainer.classList.add('no-transition');
this.zoomElements.rangeSelector.container.classList.remove('with-transition');
}
this.isGesturingNow = true;
this.lastGestureTime = Date.now();
this.clampZoomDebounced.clearTimeout();
@ -426,11 +444,16 @@ export default class AppMediaViewerBase<
}
};
protected onSwipeReset = () => {
protected onSwipeReset = (e?: Event) => {
// move
this.moversContainer.classList.remove('no-transition');
this.zoomElements.rangeSelector.container.classList.add('with-transition');
this.clampZoomDebounced.clearTimeout();
if(e?.type === 'mouseup' && this.draggingType === 'mousemove') {
this.ignoreNextClick = true;
}
const {draggingType} = this;
this.isZoomingNow = false;
this.isGesturingNow = false;
@ -492,7 +515,7 @@ export default class AppMediaViewerBase<
this.isZoomingNow = true;
const zoomMaxBounceValue = ZOOM_MAX_VALUE * 3;
const scale = zoomAdd ? clamp(this.lastTransform.scale + zoomAdd, ZOOM_MIN_VALUE, zoomMaxBounceValue) : (zoom ?? clamp(this.lastTransform.scale * zoomFactor, ZOOM_MIN_VALUE, zoomMaxBounceValue));
const scale = zoomAdd !== undefined ? clamp(this.lastTransform.scale + zoomAdd, ZOOM_MIN_VALUE, zoomMaxBounceValue) : (zoom ?? clamp(this.lastTransform.scale * zoomFactor, ZOOM_MIN_VALUE, zoomMaxBounceValue));
const scaleFactor = scale / this.lastTransform.scale;
const offsetX = Math.abs(Math.min(this.lastTransform.x, 0));
const offsetY = Math.abs(Math.min(this.lastTransform.y, 0));
@ -661,8 +684,8 @@ export default class AppMediaViewerBase<
this.moversContainer.style.transform = `translate3d(${this.transform.x.toFixed(3)}px, ${this.transform.y.toFixed(3)}px, 0px) scale(${value.toFixed(3)})`;
this.zoomElements.btnOut.classList.toggle('inactive', value === ZOOM_MIN_VALUE);
this.zoomElements.btnIn.classList.toggle('inactive', value === ZOOM_MAX_VALUE);
this.zoomElements.btnOut.classList.toggle('inactive', value <= ZOOM_MIN_VALUE);
this.zoomElements.btnIn.classList.toggle('inactive', value >= ZOOM_MAX_VALUE);
this.toggleZoom(value !== ZOOM_INITIAL_VALUE);
};
@ -735,6 +758,11 @@ export default class AppMediaViewerBase<
}
onClick = (e: MouseEvent) => {
if(this.ignoreNextClick) {
this.ignoreNextClick = undefined;
return;
}
if(this.setMoverAnimationPromise) return;
const target = e.target as HTMLElement;
@ -756,7 +784,11 @@ export default class AppMediaViewerBase<
return;
}
const isZooming = this.isZooming;
if(hasMouseMovedSinceDown(e)) {
return;
}
const isZooming = this.isZooming && false;
let mover: HTMLElement = null;
const classNames = ['ckin__player', 'media-viewer-buttons', 'media-viewer-author', 'media-viewer-caption', 'zoom-container'];
if(isZooming) {
@ -1850,8 +1882,9 @@ export default class AppMediaViewerBase<
const cancellablePromise = isDocument ? appDownloadManager.downloadMediaURL({media}) : appDownloadManager.downloadMediaURL({media, thumb: size});
const photoSizes = !isDocument && media.sizes.slice().filter((size) => (size as PhotoSize.photoSize).w) as PhotoSize.photoSize[];
photoSizes?.sort((a, b) => b.size - a.size);
const cancellableFullPromise = !isDocument && appDownloadManager.downloadMediaURL({media, thumb: photoSizes?.[0]});
photoSizes && photoSizes.sort((a, b) => b.size - a.size);
const fullPhotoSize = photoSizes?.[0];
const cancellableFullPromise = !isDocument && fullPhotoSize !== size && appDownloadManager.downloadMediaURL({media, thumb: fullPhotoSize});
onAnimationEnd.then(async() => {
if(!(await getCacheContext()).url) {
@ -1866,8 +1899,6 @@ export default class AppMediaViewerBase<
return;
}
// /////this.log('indochina', blob);
const url = (await getCacheContext()).url;
if(target instanceof SVGSVGElement) {
this.updateMediaSource(target, url, 'img');
@ -1886,8 +1917,6 @@ export default class AppMediaViewerBase<
const image = new Image();
image.classList.add('thumbnail');
// this.log('will renderImageFromUrl:', image, div, target);
renderImageFromUrl(image, url, () => {
fastRaf(() => {
this.updateMediaSource(target, url, 'img');
@ -1902,7 +1931,7 @@ export default class AppMediaViewerBase<
});
}, false);
cancellableFullPromise?.then((url) => {
cancellableFullPromise && cancellableFullPromise.then((url) => {
const fullImage = new Image();
fullImage.classList.add('thumbnail');
renderImageFromUrl(fullImage, url, () => {

View File

@ -648,7 +648,7 @@ export default class AppSearchSuper {
}
private async processPhotoVideoFilter({message, promises, middleware}: ProcessSearchSuperResult) {
const media = getMediaFromMessage(message);
const media = getMediaFromMessage(message, true);
const div = document.createElement('div');
div.classList.add('grid-item');
@ -711,7 +711,7 @@ export default class AppSearchSuper {
}
private async processDocumentFilter({message, inputFilter}: ProcessSearchSuperResult) {
const document = getMediaFromMessage(message) as Document.document;
const document = getMediaFromMessage(message, true) as Document.document;
const showSender = this.showSender || (['voice', 'round'] as MyDocument['type'][]).includes(document.type);
const div = await wrapDocument({

View File

@ -31,7 +31,7 @@ import filterAsync from '../helpers/array/filterAsync';
import getParticipantPeerId from '../lib/appManagers/utils/chats/getParticipantPeerId';
import getChatMembersString from './wrappers/getChatMembersString';
import getUserStatusString from './wrappers/getUserStatusString';
import {Chat, User} from '../layer';
import {ChannelsChannelParticipants, Chat, User} from '../layer';
import canSendToUser from '../lib/appManagers/utils/users/canSendToUser';
import hasRights from '../lib/appManagers/utils/chats/hasRights';
import getDialogIndex from '../lib/appManagers/utils/dialogs/getDialogIndex';
@ -75,7 +75,7 @@ export default class AppSelectPeers {
private onChange: (length: number) => void;
private peerType: SelectSearchPeerType[] = ['dialogs'];
private renderResultsFunc: (peerIds: PeerId[]) => void | Promise<void>;
private chatRightsAction: ChatRights;
private chatRightsActions: ChatRights[];
private multiSelect = true;
private rippleEnabled = true;
private avatarSize: DialogElementSize = 'abitbigger';
@ -104,7 +104,7 @@ export default class AppSelectPeers {
peerId?: AppSelectPeers['peerId'],
onFirstRender?: () => void,
renderResultsFunc?: AppSelectPeers['renderResultsFunc'],
chatRightsAction?: AppSelectPeers['chatRightsAction'],
chatRightsActions?: AppSelectPeers['chatRightsActions'],
multiSelect?: AppSelectPeers['multiSelect'],
rippleEnabled?: AppSelectPeers['rippleEnabled'],
avatarSize?: AppSelectPeers['avatarSize'],
@ -347,7 +347,7 @@ export default class AppSelectPeers {
dialogs = dialogs.slice();
findAndSplice(dialogs, d => d.peerId === rootScope.myId); // no my account
if(this.chatRightsAction) {
if(this.chatRightsActions) {
dialogs = await filterAsync(dialogs, (d) => this.filterByRights(d.peerId));
if(!middleware()) {
return;
@ -389,8 +389,8 @@ export default class AppSelectPeers {
private async filterByRights(peerId: PeerId) {
const peer: User | Chat = await this.managers.appPeersManager.getPeer(peerId);
if(peerId.isUser()) {
return this.chatRightsAction !== 'send_messages' || canSendToUser(peer as User.user);
} else if(hasRights(peer as Chat.chat, this.chatRightsAction)) {
return this.chatRightsActions[0] !== 'send_plain' || canSendToUser(peer as User.user);
} else if(this.chatRightsActions.every((action) => hasRights(peer as Chat.chat, action))) {
return true;
}
}
@ -433,7 +433,7 @@ export default class AppSelectPeers {
// do not add global result if only dialogs needed
let resultPeerIds = isGlobalSearch ? searchResult.my_results.concat(searchResult.results) : searchResult.my_results;
if(this.chatRightsAction) {
if(this.chatRightsActions) {
resultPeerIds = await filterAsync(resultPeerIds, (peerId) => this.filterByRights(peerId));
if(!middleware()) {
return;
@ -474,7 +474,7 @@ export default class AppSelectPeers {
const pageCount = 50; // same as in group permissions to use cache
const {middleware} = this.getTempId('channelParticipants');
const promise = this.managers.appProfileManager.getChannelParticipants(
const promise = this.managers.appProfileManager.getParticipants(
this.peerId.toChatId(),
{
_: 'channelParticipantsSearch',
@ -492,18 +492,20 @@ export default class AppSelectPeers {
this.loadedWhat.channelParticipants = true;
});
const participants = await promise;
const chatParticipants = await promise;
if(!middleware()) {
return;
}
const peerIds = participants.participants.map((participant) => {
return getParticipantPeerId(participant);
});
const {participants} = chatParticipants;
const peerIds = participants.map((participant) => getParticipantPeerId(participant));
indexOfAndSplice(peerIds, rootScope.myId);
this.renderResultsFunc(peerIds);
if(this.list.childElementCount >= participants.count || participants.participants.length < pageCount) {
const count = (chatParticipants as ChannelsChannelParticipants.channelsChannelParticipants).count ?? participants.length;
if(this.list.childElementCount >= count || participants.length < pageCount) {
this.loadedWhat.channelParticipants = true;
}
}

View File

@ -123,7 +123,7 @@ function createWaveformBars(waveform: Uint8Array, duration: number) {
const bar_value = Math.max(((maxValue * maxDelta) + ((normValue + 1) / 2)) / (normValue + 1), barHeightMin);
const h = `
<rect x="${barX}" y="${barHeightMax - bar_value}" width="${barWidth}" height="${bar_value}" rx="1" ry="1"></rect>
<rect class="audio-waveform-bar" x="${barX}" y="${barHeightMax - bar_value}" width="${barWidth}" height="${bar_value}" rx="1" ry="1"></rect>
`;
html += h;
@ -197,6 +197,11 @@ async function wrapVoiceMessage(audioEl: AudioElement) {
// TODO: State to enum
audioEl.transcriptionState = 2;
} else {
const message = audioEl.message;
if(message.pFlags.is_outgoing) {
return;
}
audioEl.transcriptionState = 1;
!speechRecognitionLoader.parentElement && speechRecognitionDiv.append(speechRecognitionLoader);
doubleRaf().then(() => {

View File

@ -6,7 +6,7 @@
import contextMenuController from '../helpers/contextMenuController';
import cancelEvent from '../helpers/dom/cancelEvent';
import {AttachClickOptions, CLICK_EVENT_NAME} from '../helpers/dom/clickEvent';
import {AttachClickOptions, CLICK_EVENT_NAME, hasMouseMovedSinceDown} from '../helpers/dom/clickEvent';
import ListenerSetter from '../helpers/listenerSetter';
import ButtonIcon from './buttonIcon';
import ButtonMenu, {ButtonMenuItemOptionsVerifiable} from './buttonMenu';
@ -28,7 +28,7 @@ export function ButtonMenuToggleHandler({
const add = options?.listenerSetter ? options.listenerSetter.add(el) : el.addEventListener.bind(el);
add(CLICK_EVENT_NAME, (e: Event) => {
if(!el.classList.contains('btn-menu-toggle')) return false;
if(!el.classList.contains('btn-menu-toggle') || hasMouseMovedSinceDown(e)) return false;
cancelEvent(e);

View File

@ -656,11 +656,11 @@ export default class ChatBubbles {
dots?.remove();
}
if(!text && !pending && !transcribedText.classList.contains('has-some-text')) {
transcribedText.append(i18n('Chat.Voice.Transribe.Error'));
if(!text && !pending/* && !transcribedText.classList.contains('has-some-text') */) {
transcribedText.replaceChildren(i18n('Chat.Voice.Transribe.Error'));
transcribedText.classList.add('is-error');
} else {
transcribedText.classList.add('has-some-text');
} else if(text) {
// transcribedText.classList.add('has-some-text');
transcribedText.firstChild.textContent = text;
}
@ -1101,9 +1101,14 @@ export default class ChatBubbles {
const chat = await this.managers.appChatsManager.getChat(chatId);
const hadRights = this.chatInner.classList.contains('has-rights');
const hasRights = await this.chat.canSend();
const hadPlainRights = this.chat.input.canSendPlain();
const [hasRights, hasPlainRights, canEmbedLinks] = await Promise.all([
this.chat.canSend('send_messages'),
this.chat.canSend('send_plain'),
this.chat.canSend('embed_links')
]);
if(hadRights !== hasRights) {
if(hadRights !== hasRights || hadPlainRights !== hasPlainRights) {
const callbacks = await Promise.all([
this.finishPeerChange(),
this.chat.input.finishPeerChange()
@ -1112,6 +1117,12 @@ export default class ChatBubbles {
callbacks.forEach((callback) => callback());
}
// reset webpage
if((canEmbedLinks && !this.chat.input.willSendWebPage) || (!canEmbedLinks && this.chat.input.willSendWebPage)) {
this.chat.input.lastUrl = '';
this.chat.input.onMessageInput();
}
if(!!(chat as MTChat.channel).pFlags.forum !== this.chat.isForum && this.chat.type === 'chat') {
this.chat.peerId = 0;
this.chat.appImManager.setPeer({peerId});

View File

@ -19,7 +19,7 @@ import findUpClassName from '../../helpers/dom/findUpClassName';
import cancelEvent from '../../helpers/dom/cancelEvent';
import {attachClickEvent, simulateClickEvent} from '../../helpers/dom/clickEvent';
import isSelectionEmpty from '../../helpers/dom/isSelectionEmpty';
import {Message, Poll, Chat as MTChat, MessageMedia, AvailableReaction, MessageEntity, InputStickerSet, StickerSet, Document, Reaction} from '../../layer';
import {Message, Poll, Chat as MTChat, MessageMedia, AvailableReaction, MessageEntity, InputStickerSet, StickerSet, Document, Reaction, Photo} from '../../layer';
import PopupReportMessages from '../popups/reportMessages';
import assumeType from '../../helpers/assumeType';
import PopupSponsored from '../popups/sponsored';
@ -39,7 +39,7 @@ import {attachContextMenuListener} from '../../helpers/dom/attachContextMenuList
import filterAsync from '../../helpers/array/filterAsync';
import appDownloadManager from '../../lib/appManagers/appDownloadManager';
import {SERVICE_PEER_ID} from '../../lib/mtproto/mtproto_config';
import {MessagesStorageKey} from '../../lib/appManagers/appMessagesManager';
import {MessagesStorageKey, MyMessage} from '../../lib/appManagers/appMessagesManager';
import filterUnique from '../../helpers/array/filterUnique';
import replaceContent from '../../helpers/dom/replaceContent';
import wrapEmojiText from '../../lib/richTextProcessor/wrapEmojiText';
@ -79,6 +79,7 @@ export default class ChatContextMenu {
private albumMessages: Message.message[];
private linkToMessage: Awaited<ReturnType<ChatContextMenu['getUrlToMessage']>>;
private selectedMessagesText: string;
private selectedMessages: MyMessage[];
constructor(
private chat: Chat,
@ -199,6 +200,7 @@ export default class ChatContextMenu {
this.canOpenReactedList = undefined;
this.linkToMessage = await this.getUrlToMessage();
this.selectedMessagesText = await this.getSelectedMessagesText();
this.selectedMessages = this.chat.selection.isSelecting ? await this.chat.selection.getSelectedMessages() : undefined;
const initResult = await this.init();
if(!initResult) {
@ -445,31 +447,9 @@ export default class ChatContextMenu {
icon: 'download',
text: 'MediaViewer.Context.Download',
onClick: () => {
appDownloadManager.downloadToDisc({media: getMediaFromMessage(this.message)});
appDownloadManager.downloadToDisc({media: getMediaFromMessage(this.message, true)});
},
verify: () => {
if(!canSaveMessageMedia(this.message) || this.noForwards) {
return false;
}
const isPhoto: boolean = !!((this.message as Message.message).media as MessageMedia.messageMediaPhoto)?.photo;
let isGoodType = false
if(isPhoto) {
isGoodType = true;
} else {
const doc: MyDocument = ((this.message as Message.message).media as MessageMedia.messageMediaDocument)?.document as any;
if(!doc) return false;
// isGoodType = doc.type && (['gif', 'video', 'audio', 'voice', 'sticker'] as MyDocument['type'][]).includes(doc.type)
isGoodType = true;
}
let hasTarget = !!IS_TOUCH_SUPPORTED;
if(isGoodType) hasTarget ||= !!findUpClassName(this.target, 'document') || !!findUpClassName(this.target, 'audio') || !!findUpClassName(this.target, 'media-sticker-wrapper') || !!findUpClassName(this.target, 'media-photo') || !!findUpClassName(this.target, 'media-video');
return isGoodType && hasTarget;
}
verify: () => this.canDownload(this.message, true)
}, {
icon: 'checkretract',
text: 'Chat.Poll.Unvote',
@ -502,6 +482,16 @@ export default class ChatContextMenu {
!this.chat.selection.selectionForwardBtn.hasAttribute('disabled'),
notDirect: () => true,
withSelection: true
}, {
icon: 'download',
text: 'Message.Context.Selection.Download',
onClick: () => {
this.selectedMessages.forEach((message) => {
appDownloadManager.downloadToDisc({media: getMediaFromMessage(message, true)});
});
},
verify: () => this.selectedMessages ? this.selectedMessages.some((message) => this.canDownload(message, false)) : false,
withSelection: true
}, {
icon: 'flag',
text: 'ReportChat',
@ -574,6 +564,30 @@ export default class ChatContextMenu {
}];
}
private canDownload(message: MyMessage, withTarget?: boolean) {
if(!canSaveMessageMedia(message) || this.noForwards) {
return false;
}
const isPhoto: boolean = !!((message as Message.message).media as MessageMedia.messageMediaPhoto)?.photo;
let isGoodType = false
if(isPhoto) {
isGoodType = true;
} else {
const doc: MyDocument = ((message as Message.message).media as MessageMedia.messageMediaDocument)?.document as any;
if(!doc) return false;
// isGoodType = doc.type && (['gif', 'video', 'audio', 'voice', 'sticker'] as MyDocument['type'][]).includes(doc.type)
isGoodType = true;
}
let hasTarget = !withTarget || !!IS_TOUCH_SUPPORTED;
if(isGoodType) hasTarget ||= !!findUpClassName(this.target, 'document') || !!findUpClassName(this.target, 'audio') || !!findUpClassName(this.target, 'media-sticker-wrapper') || !!findUpClassName(this.target, 'media-photo') || !!findUpClassName(this.target, 'media-video');
return isGoodType && hasTarget;
}
private getMessageWithText() {
return (this.albumMessages && getAlbumText(this.albumMessages)) || this.message;
}
@ -932,7 +946,7 @@ export default class ChatContextMenu {
} else {
const peerId = this.peerId;
const mids = this.isTargetAGroupedItem ? [this.mid] : await this.chat.getMidsByMid(this.mid);
new PopupForward({
PopupForward.create({
[peerId]: mids
});
}

View File

@ -29,6 +29,8 @@ import generateQId from '../../lib/appManagers/utils/inlineBots/generateQId';
import appDownloadManager from '../../lib/appManagers/appDownloadManager';
import {AnimationItemGroup} from '../animationIntersector';
import wrapPhoto from '../wrappers/photo';
import {i18n} from '../../lib/langPack';
import {POSTING_NOT_ALLOWED_MAP} from './input';
const ANIMATION_GROUP: AnimationItemGroup = 'INLINE-HELPER';
// const GRID_ITEMS = 5;
@ -39,7 +41,7 @@ export default class InlineHelper extends AutocompleteHelper {
private gifsMasonry: GifsMasonry;
private superStickerRenderer: SuperStickerRenderer;
private onChangeScreen: () => void;
public checkQuery: (peerId: PeerId, username: string, query: string) => ReturnType<InlineHelper['_checkQuery']>;
public checkQuery: ReturnType<typeof debounce<InlineHelper['_checkQuery']>>;
constructor(
appendTo: HTMLElement,
@ -85,7 +87,7 @@ export default class InlineHelper extends AutocompleteHelper {
});
}
public _checkQuery = async(peerId: PeerId, username: string, query: string) => {
public _checkQuery = async(peerId: PeerId, username: string, query: string, canSendInline: boolean) => {
const middleware = this.controller.getMiddleware();
const peer = await this.managers.appUsersManager.resolveUsername(username);
@ -97,6 +99,21 @@ export default class InlineHelper extends AutocompleteHelper {
throw 'NOT_A_BOT';
}
if(!canSendInline) {
if(!middleware()) {
throw 'PEER_CHANGED';
}
if(this.init) {
this.init();
this.init = null;
}
this.container.classList.add('cant-send');
this.toggle(false);
throw 'NO_INLINES';
}
const renderPromise = this.managers.appInlineBotsManager.getInlineResults(peerId, peer.id, query).then((botResults) => {
if(!middleware()) {
throw 'PEER_CHANGED';
@ -252,10 +269,9 @@ export default class InlineHelper extends AutocompleteHelper {
parent.append(btnSwitchToPM);
}
parent.append(this.list = list);
this.container.classList.remove('cant-send');
if(this.gifsMasonry) {
this.gifsMasonry.detach();
}
this.gifsMasonry?.detach();
this.gifsMasonry = gifsMasonry;
gifsMasonry.attach();
@ -290,5 +306,9 @@ export default class InlineHelper extends AutocompleteHelper {
this.scrollable = new Scrollable(this.container);
this.lazyLoadQueue = new LazyLoadQueue();
this.superStickerRenderer = new SuperStickerRenderer(this.lazyLoadQueue, ANIMATION_GROUP, this.managers);
const span = i18n(POSTING_NOT_ALLOWED_MAP['send_inline']);
span.classList.add('inline-helper-cant-send');
this.container.append(span);
}
}

View File

@ -108,9 +108,21 @@ import getAttachMenuBotIcon from '../../lib/appManagers/utils/attachMenuBots/get
import TelegramWebView from '../telegramWebView';
import forEachReverse from '../../helpers/array/forEachReverse';
import {MARKDOWN_ENTITIES} from '../../lib/richTextProcessor';
import IMAGE_MIME_TYPES_SUPPORTED from '../../environment/imageMimeTypesSupport';
import VIDEO_MIME_TYPES_SUPPORTED from '../../environment/videoMimeTypesSupport';
import {ChatRights} from '../../lib/appManagers/appChatsManager';
const RECORD_MIN_TIME = 500;
const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.';
export const POSTING_NOT_ALLOWED_MAP: {[action in ChatRights]?: LangPackKey} = {
send_voices: 'GlobalAttachVoiceRestricted',
send_stickers: 'GlobalAttachStickersRestricted',
send_gifs: 'GlobalAttachGifRestricted',
send_media: 'GlobalAttachMediaRestricted',
send_plain: 'GlobalSendMessageRestricted',
send_polls: 'ErrorSendRestrictedPollsAll',
send_inline: 'GlobalAttachInlineRestricted'
};
type ChatInputHelperType = 'edit' | 'webpage' | 'forward' | 'reply';
@ -123,7 +135,7 @@ export default class ChatInput {
private inputMessageContainer: HTMLDivElement;
private btnSend: HTMLButtonElement;
private btnCancelRecord: HTMLButtonElement;
private lastUrl = '';
public lastUrl = '';
private lastTimeType = 0;
public chatInput: HTMLElement;
@ -160,7 +172,7 @@ export default class ChatInput {
private forwardWasDroppingAuthor: boolean;
private getWebPagePromise: Promise<void>;
private willSendWebPage: WebPage = null;
public willSendWebPage: WebPage = null;
private forwarding: {[fromPeerId: PeerId]: number[]};
public replyToMsgId: number;
public editMsgId: number;
@ -585,18 +597,37 @@ export default class ChatInput {
icon.classList.remove('state-back');
});
// const getSendMediaRights = () => Promise.all([this.chat.canSend('send_photos'), this.chat.canSend('send_videos')]).then(([photos, videos]) => ({photos, videos}));
const onAttachMediaClick = (photos: boolean, videos: boolean) => {
this.fileInput.value = '';
const accept = [
...(photos ? IMAGE_MIME_TYPES_SUPPORTED : []),
...(videos ? VIDEO_MIME_TYPES_SUPPORTED : [])
].join(', ');
this.fileInput.setAttribute('accept', accept);
this.willAttachType = 'media';
this.fileInput.click();
};
this.attachMenuButtons = [{
icon: 'image',
text: 'Chat.Input.Attach.PhotoOrVideo',
onClick: () => {
this.fileInput.value = '';
const accept = [...MEDIA_MIME_TYPES_SUPPORTED].join(', ');
this.fileInput.setAttribute('accept', accept);
this.willAttachType = 'media';
this.fileInput.click();
},
verify: () => this.chat.canSend('send_media')
onClick: () => onAttachMediaClick(true, true)
// verify: () => getSendMediaRights().then(({photos, videos}) => photos && videos)
}, /* {
icon: 'image',
text: 'AttachPhoto',
onClick: () => onAttachMediaClick(true, false),
verify: () => getSendMediaRights().then(({photos, videos}) => photos && !videos)
}, {
icon: 'image',
text: 'AttachVideo',
onClick: () => onAttachMediaClick(false, true),
verify: () => getSendMediaRights().then(({photos, videos}) => !photos && videos)
}, */ {
icon: 'document',
text: 'Chat.Input.Attach.Document',
onClick: () => {
@ -604,17 +635,26 @@ export default class ChatInput {
this.fileInput.removeAttribute('accept');
this.willAttachType = 'document';
this.fileInput.click();
},
verify: () => this.chat.canSend('send_media')
}
// verify: () => this.chat.canSend('send_docs')
}, {
icon: 'poll',
text: 'Poll',
onClick: () => {
onClick: async() => {
const action: ChatRights = 'send_polls';
if(!(await this.chat.canSend(action))) {
toastNew({langPackKey: POSTING_NOT_ALLOWED_MAP[action]});
return;
}
PopupElement.createPopup(PopupCreatePoll, this.chat).show();
},
verify: () => (this.chat.peerId.isAnyChat() || this.chat.isBot) && this.chat.canSend('send_polls')
verify: () => this.chat.peerId.isAnyChat() || this.chat.isBot
}];
// preload the bots
this.managers.appAttachMenuBotsManager.getAttachMenuBots();
const attachMenuButtons = this.attachMenuButtons.slice();
this.attachMenu = ButtonMenuToggle({
buttonOptions: {noRipple: true},
@ -1368,6 +1408,7 @@ export default class ChatInput {
canPinMessage,
isBot,
canSend,
canSendPlain,
neededFakeContainer,
ackedPeerFull,
ackedScheduledMids,
@ -1377,7 +1418,8 @@ export default class ChatInput {
this.managers.appPeersManager.isBroadcast(peerId),
this.managers.appPeersManager.canPinMessage(peerId),
this.managers.appPeersManager.isBot(peerId),
this.chat.canSend(),
this.chat.canSend('send_messages'),
this.chat.canSend('send_plain'),
this.getNeededFakeContainer(startParam),
modifyAckedPromise(this.managers.acknowledged.appProfileManager.getProfileByPeerId(peerId)),
btnScheduled ? modifyAckedPromise(this.managers.acknowledged.appMessagesManager.getScheduledMessages(peerId)) : undefined,
@ -1385,7 +1427,7 @@ export default class ChatInput {
this.filterAttachMenuButtons()
]);
const placeholderKey = this.messageInput ? await this.getPlaceholderKey() : undefined;
const placeholderKey = this.messageInput ? await this.getPlaceholderKey(canSendPlain) : undefined;
return () => {
// console.warn('[input] finishpeerchange start');
@ -1394,7 +1436,6 @@ export default class ChatInput {
goDownBtn.classList.toggle('is-broadcast', isBroadcast);
goDownBtn.classList.remove('hide');
this.messageInputField?.onFakeInput();
if(this.goDownUnreadBadge) {
this.setUnreadCount();
@ -1439,27 +1480,18 @@ export default class ChatInput {
}
}
if(previousSendAs) {
previousSendAs.destroy();
}
if(setSendAsCallback) {
setSendAsCallback();
}
if(replyKeyboard) {
replyKeyboard.setPeer(peerId);
}
if(sendMenu) {
sendMenu.setPeerId(peerId);
}
previousSendAs?.destroy();
setSendAsCallback?.();
replyKeyboard?.setPeer(peerId);
sendMenu?.setPeerId(peerId);
if(this.messageInput) {
this.updateMessageInput(canSend, placeholderKey, filteredAttachMenuButtons);
this.updateMessageInput(canSend, canSendPlain, placeholderKey, filteredAttachMenuButtons);
this.messageInput.dataset.peerId = '' + peerId;
}
this.messageInputField?.onFakeInput(undefined, true);
let haveSomethingInControl = false;
if(this.pinnedControlBtn) {
const good = this.chat.type === 'pinned';
@ -1530,10 +1562,13 @@ export default class ChatInput {
this.updateOffset('commands', forwards, skipAnimation, useRafs);
}
private async getPlaceholderKey() {
private async getPlaceholderKey(canSend?: boolean) {
canSend ??= await this.chat.canSend('send_plain');
const {peerId, threadId, isForum} = this.chat;
let key: LangPackKey;
if(threadId && !isForum) {
if(!canSend) {
key = 'Channel.Persmission.MessageBlock';
} else if(threadId && !isForum) {
key = 'Comment';
} else if(await this.managers.appPeersManager.isBroadcast(peerId)) {
key = 'ChannelBroadcast';
@ -1562,13 +1597,17 @@ export default class ChatInput {
private filterAttachMenuButtons() {
if(!this.attachMenuButtons) return;
return filterAsync(this.attachMenuButtons, (button) => {
return button.verify();
return button.verify ? button.verify() : true;
});
}
public updateMessageInput(canSend: boolean, placeholderKey: LangPackKey, visible: ChatInput['attachMenuButtons']) {
public updateMessageInput(
canSend: boolean,
canSendPlain: boolean,
placeholderKey: LangPackKey,
visible: ChatInput['attachMenuButtons']
) {
const {chatInput, attachMenu, messageInput} = this;
const {peerId, threadId} = this.chat;
const isHidden = chatInput.classList.contains('is-hidden');
const willBeHidden = !canSend;
if(isHidden !== willBeHidden) {
@ -1580,14 +1619,18 @@ export default class ChatInput {
this.updateMessageInputPlaceholder(placeholderKey);
if(!canSend) {
messageInput.contentEditable = 'inherit';
if(!canSend || !canSendPlain) {
messageInput.contentEditable = 'false';
if(!canSendPlain) {
this.messageInputField.onFakeInput(undefined, true);
}
} else {
messageInput.contentEditable = 'true';
this.setDraft(undefined, false);
if(!messageInput.innerHTML) {
this.messageInputField.onFakeInput();
this.messageInputField.onFakeInput(undefined, true);
}
}
@ -1651,6 +1694,14 @@ export default class ChatInput {
}
});
attachClickEvent(this.messageInput, (e) => {
if(!this.canSendPlain()) {
toastNew({
langPackKey: POSTING_NOT_ALLOWED_MAP['send_plain']
});
}
}, {listenerSetter: this.listenerSetter});
if(IS_TOUCH_SUPPORTED) {
attachClickEvent(this.messageInput, (e) => {
if(emoticonsDropdown.isActive()) {
@ -1706,6 +1757,10 @@ export default class ChatInput {
});
}
public canSendPlain() {
return !(!this.messageInput.isContentEditable && !this.chatInput.classList.contains('is-hidden'));
}
private prepareDocumentExecute = () => {
this.executedHistory.push(this.messageInput.innerHTML);
return () => this.canUndoFromHTML = this.messageInput.innerHTML;
@ -1877,9 +1932,7 @@ export default class ChatInput {
// checkForSingle();
// saveExecuted();
if(this.appImManager.markupTooltip) {
this.appImManager.markupTooltip.setActiveMarkupButton();
}
this.appImManager.markupTooltip?.setActiveMarkupButton();
if(textNode) {
(textNode.parentElement === this.messageInput ? textNode : textNode.parentElement).remove();
@ -1957,7 +2010,7 @@ export default class ChatInput {
}
};
private onMessageInput = (e?: Event) => {
public onMessageInput = (e?: Event) => {
// * validate due to manual formatting through browser's context menu
/* const inputType = (e as InputEvent).inputType;
console.log('message input event', e);
@ -1999,17 +2052,15 @@ export default class ChatInput {
}
}
// console.log('messageInput url:', url);
if(this.lastUrl !== url) {
this.lastUrl = url;
// this.willSendWebPage = null;
const promise = this.getWebPagePromise = this.managers.appWebPagesManager.getWebPage(url).then((webpage) => {
const promise = this.getWebPagePromise = Promise.all([
this.managers.appWebPagesManager.getWebPage(url),
this.chat.canSend('embed_links')
]).then(([webpage, canEmbedLinks]) => {
if(this.getWebPagePromise === promise) this.getWebPagePromise = undefined;
if(this.lastUrl !== url) return;
if(webpage._ === 'webPage') {
// console.log('got webpage: ', webpage);
if(webpage._ === 'webPage' && canEmbedLinks) {
this.setTopInfo('webpage', () => {}, webpage.site_name || webpage.title || 'Webpage', webpage.description || webpage.url || '');
delete this.noWebPage;
this.willSendWebPage = webpage;
@ -2039,31 +2090,27 @@ export default class ChatInput {
this.managers.appMessagesManager.setTyping(this.chat.peerId, {_: 'sendMessageCancelAction'}, undefined, this.chat.threadId);
}
if(this.appImManager.markupTooltip) {
this.appImManager.markupTooltip.hide();
}
this.appImManager.markupTooltip?.hide();
// * Chrome has a bug - it will preserve the formatting if the input with monospace text is cleared
// * so have to reset formatting
if(document.activeElement === this.messageInput) {
// document.execCommand('styleWithCSS', false, 'true');
if(document.activeElement === this.messageInput && !IS_MOBILE) {
setTimeout(() => {
if(document.activeElement === this.messageInput) {
this.resetCurrentFontFormatting();
this.messageInput.textContent = '1';
placeCaretAtEnd(this.messageInput);
this.messageInput.textContent = '';
}
}, 0);
// document.execCommand('styleWithCSS', false, 'false');
}
} else {
const time = Date.now();
if((time - this.lastTimeType) >= 6000) {
if((time - this.lastTimeType) >= 6000 && e?.isTrusted) {
this.lastTimeType = time;
this.managers.appMessagesManager.setTyping(this.chat.peerId, {_: 'sendMessageTypingAction'}, undefined, this.chat.threadId);
}
if(this.botCommands) {
this.botCommands.toggle(true);
}
this.botCommands?.toggle(true);
}
if(this.botCommands) {
@ -2080,6 +2127,13 @@ export default class ChatInput {
};
public insertAtCaret(insertText: string, insertEntity?: MessageEntity, isHelper = true) {
if(!this.canSendPlain()) {
toastNew({
langPackKey: POSTING_NOT_ALLOWED_MAP['send_plain']
});
return;
}
RichInputHandler.getInstance().makeFocused(this.messageInput);
const {value: fullValue, caretPos, entities} = getRichValueWithCaret(this.messageInput);
@ -2192,7 +2246,12 @@ export default class ChatInput {
}
public onEmojiSelected = (emoji: ReturnType<typeof getEmojiFromElement>, autocomplete: boolean) => {
const entity: MessageEntity = emoji.docId ? {_: 'messageEntityCustomEmoji', document_id: emoji.docId, length: emoji.emoji.length, offset: 0} : getEmojiEntityFromEmoji(emoji.emoji);
const entity: MessageEntity = emoji.docId ? {
_: 'messageEntityCustomEmoji',
document_id: emoji.docId,
length: emoji.emoji.length,
offset: 0
} : getEmojiEntityFromEmoji(emoji.emoji);
this.insertAtCaret(emoji.emoji, entity, autocomplete);
};
@ -2255,12 +2314,17 @@ export default class ChatInput {
}
}
foundHelper = this.checkInlineAutocomplete(value, foundHelper);
let canSendInline: boolean;
if(!foundHelper) {
canSendInline = await this.chat.canSend('send_inline');
}
foundHelper = this.checkInlineAutocomplete(value, canSendInline, foundHelper);
this.autocompleteHelperController.hideOtherHelpers(foundHelper);
}
private checkInlineAutocomplete(value: string, foundHelper?: AutocompleteHelper): AutocompleteHelper {
private checkInlineAutocomplete(value: string, canSendInline: boolean, foundHelper?: AutocompleteHelper): AutocompleteHelper {
let needPlaceholder = false;
const setPreloaderShow = (show: boolean) => {
@ -2268,6 +2332,10 @@ export default class ChatInput {
return;
}
if(show && !canSendInline) {
show = false;
}
SetTransition({
element: this.btnPreloader,
className: 'show',
@ -2293,7 +2361,7 @@ export default class ChatInput {
setPreloaderShow(true);
}
this.inlineHelper.checkQuery(this.chat.peerId, username, query).then(({user, renderPromise}) => {
this.inlineHelper.checkQuery(this.chat.peerId, username, query, canSendInline).then(({user, renderPromise}) => {
if(needPlaceholder && user.bot_inline_placeholder) {
this.messageInput.dataset.inlinePlaceholder = user.bot_inline_placeholder;
}
@ -2348,8 +2416,9 @@ export default class ChatInput {
}
} else {
const isAnyChat = this.chat.peerId.isAnyChat();
if(isAnyChat && !(await this.chat.canSend('send_media'))) {
toast(POSTING_MEDIA_NOT_ALLOWED);
const flag: ChatRights = 'send_voices';
if(isAnyChat && !(await this.chat.canSend(flag))) {
toastNew({langPackKey: POSTING_NOT_ALLOWED_MAP[flag]});
return;
}
@ -2477,7 +2546,7 @@ export default class ChatInput {
}
};
private onHelperCancel = async(e?: Event, force?: boolean) => {
public onHelperCancel = async(e?: Event, force?: boolean) => {
if(e) {
cancelEvent(e);
}
@ -2770,7 +2839,7 @@ export default class ChatInput {
const flag = document.type === 'sticker' ? 'send_stickers' : (document.type === 'gif' ? 'send_gifs' : 'send_media');
if(this.chat.peerId.isAnyChat() && !(await this.chat.canSend(flag))) {
toast(POSTING_MEDIA_NOT_ALLOWED);
toastNew({langPackKey: POSTING_NOT_ALLOWED_MAP[flag]});
return false;
}
@ -3111,31 +3180,4 @@ export default class ChatInput {
return container;
}
// public saveScroll() {
// this.scrollTop = this.chat.bubbles.scrollable.container.scrollTop;
// this.scrollOffsetTop = this.chatInput.offsetTop;
// }
// public restoreScroll() {
// if(this.chatInput.style.display) return;
// //console.log('input resize', offsetTop, this.chatInput.offsetTop);
// let newOffsetTop = this.chatInput.offsetTop;
// let container = this.chat.bubbles.scrollable.container;
// let scrollTop = container.scrollTop;
// let clientHeight = container.clientHeight;
// let maxScrollTop = container.scrollHeight;
// if(newOffsetTop < this.scrollOffsetTop) {
// this.scrollDiff = this.scrollOffsetTop - newOffsetTop;
// container.scrollTop += this.scrollDiff;
// } else if(scrollTop !== this.scrollTop) {
// let endDiff = maxScrollTop - (scrollTop + clientHeight);
// if(endDiff < this.scrollDiff/* && false */) {
// //container.scrollTop -= endDiff;
// } else {
// container.scrollTop -= this.scrollDiff;
// }
// }
// }
}

View File

@ -7,7 +7,7 @@
import replaceContent from '../../helpers/dom/replaceContent';
import {Middleware} from '../../helpers/middleware';
import limitSymbols from '../../helpers/string/limitSymbols';
import {Document, Message, MessageMedia, Photo, WebPage} from '../../layer';
import {Document, Message, MessageMedia, Photo, WebPage, VideoSize} from '../../layer';
import appImManager from '../../lib/appManagers/appImManager';
import choosePhotoSize from '../../lib/appManagers/utils/photos/choosePhotoSize';
import wrapEmojiText from '../../lib/richTextProcessor/wrapEmojiText';
@ -85,7 +85,7 @@ export async function wrapReplyDivAndCaption(options: {
middleware,
loadPromises,
withoutPreloader: true,
videoSize: document.video_thumbs[0],
videoSize: document.video_thumbs[0] as Extract<VideoSize, VideoSize.videoSize>,
group: animationGroup
});
} else {

View File

@ -37,6 +37,7 @@ import {AppManagers} from '../../lib/appManagers/managers';
import {attachContextMenuListener} from '../../helpers/dom/attachContextMenuListener';
import filterUnique from '../../helpers/array/filterUnique';
import appImManager from '../../lib/appManagers/appImManager';
import {Message} from '../../layer';
const accumulateMapSet = (map: Map<any, Set<number>>) => {
return [...map.values()].reduce((acc, v) => acc + v.size, 0);
@ -360,7 +361,7 @@ class AppSelection extends EventListenerBase<{
cantDelete = !size;
const cantSend = !size;
for(const [peerId, mids] of this.selectedMids) {
const storageKey: MessagesStorageKey = `${peerId}_${this.isScheduled ? 'scheduled' : 'history'}`;
const storageKey = this.getStorageKey(peerId);
const r = await this.managers.appMessagesManager.cantForwardDeleteMids(storageKey, Array.from(mids));
cantForward = r.cantForward;
cantDelete = r.cantDelete;
@ -371,6 +372,20 @@ class AppSelection extends EventListenerBase<{
this.onUpdateContainer?.(cantForward, cantDelete, cantSend);
}
private getStorageKey(peerId: PeerId): MessagesStorageKey {
return `${peerId}_${this.isScheduled ? 'scheduled' : 'history'}`;
}
public getSelectedMessages() {
const selectedMessagesPromises: Promise<Message.message | Message.messageService>[] = [];
this.selectedMids.forEach((mids, peerId) => {
const storageKey = this.getStorageKey(peerId);
const p = Array.from(mids).map((mid) => this.managers.appMessagesManager.getMessageFromStorage(storageKey, mid));
selectedMessagesPromises.push(...p);
});
return Promise.all(selectedMessagesPromises);
}
public toggleSelection(toggleCheckboxes = true, forceSelection = false) {
const wasSelecting = this.isSelecting;
const size = this.selectedMids.size;
@ -988,9 +1003,10 @@ export default class ChatSelection extends AppSelection {
void this.selectionInputWrapper.offsetLeft; // reflow
// background.style.opacity = '';
this.selectionInputWrapper.style.opacity = '';
left.style.transform = '';
right.style.transform = '';
}
this.selectionLeft.style.transform = '';
this.selectionRight.style.transform = '';
} else if(this.selectionLeft && translateButtonsX !== undefined) {
this.selectionLeft.style.transform = `translateX(-${translateButtonsX}px)`;
this.selectionRight.style.transform = `translateX(${translateButtonsX}px)`;

View File

@ -448,7 +448,7 @@ export default class ChatTopbar {
});
},
placeholder: 'ShareModal.Search.Placeholder',
chatRightsAction: 'send_messages',
chatRightsActions: ['send_plain'],
selfPresence: 'ChatYourSelf'
});
},

View File

@ -36,7 +36,7 @@ export default class CheckboxField {
const label = this.label = document.createElement('label');
label.classList.add('checkbox-field');
if(options.restriction) {
if(options.restriction && !options.toggle) {
label.classList.add('checkbox-field-restriction');
}
@ -109,6 +109,10 @@ export default class CheckboxField {
if(options.toggle) {
label.classList.add('checkbox-field-toggle');
if(options.restriction) {
label.classList.add('checkbox-field-toggle-restriction');
}
const toggle = document.createElement('div');
toggle.classList.add('checkbox-toggle');
label.append(toggle);

View File

@ -37,6 +37,9 @@ import BezierEasing from '../../vendor/bezier-easing';
import RichInputHandler from '../../helpers/dom/richInputHandler';
import {getCaretPosF} from '../../helpers/dom/getCaretPosNew';
import ListenerSetter from '../../helpers/listenerSetter';
import {ChatRights} from '../../lib/appManagers/appChatsManager';
import {toastNew} from '../toast';
import {POSTING_NOT_ALLOWED_MAP} from '../chat/input';
export const EMOTICONSSTICKERGROUP: AnimationItemGroup = 'emoticons-dropdown';
@ -79,12 +82,19 @@ export class EmoticonsDropdown extends DropdownHover {
private savedRange: Range;
private managers: AppManagers;
private rights: {[action in ChatRights]?: boolean};
constructor() {
super({
element: document.getElementById('emoji-dropdown') as HTMLDivElement,
ignoreOutClickClassName: 'input-message-input'
});
this.rights = {
send_gifs: undefined,
send_stickers: undefined
};
this.addEventListener('open', async() => {
if(IS_TOUCH_SUPPORTED) {
// appImManager.chat.input.saveScroll();
@ -322,6 +332,17 @@ export class EmoticonsDropdown extends DropdownHover {
return;
}
const rights: {[tabId: number]: ChatRights} = {
[this.stickersTab.tabId]: 'send_stickers',
[this.gifsTab.tabId]: 'send_gifs'
};
const action = rights[id];
if(action && !this.rights[action]) {
toastNew({langPackKey: POSTING_NOT_ALLOWED_MAP[action]});
return false;
}
animationIntersector.checkAnimations(true, EMOTICONSSTICKERGROUP);
this.tabId = id;
@ -331,19 +352,19 @@ export class EmoticonsDropdown extends DropdownHover {
private checkRights = async() => {
const {peerId, threadId} = appImManager.chat;
const children = this.tabsEl.children;
const tabsElements = Array.from(children) as HTMLElement[];
const [canSendStickers, canSendGifs] = await Promise.all([
this.managers.appMessagesManager.canSendToPeer(peerId, threadId, 'send_stickers'),
this.managers.appMessagesManager.canSendToPeer(peerId, threadId, 'send_gifs')
]);
const actions = Object.keys(this.rights) as ChatRights[];
tabsElements[this.stickersTab.tabId + 1].toggleAttribute('disabled', !canSendStickers);
tabsElements[this.gifsTab.tabId + 1].toggleAttribute('disabled', !canSendGifs);
const rights = await Promise.all(actions.map((action) => {
return this.managers.appMessagesManager.canSendToPeer(peerId, threadId, action);
}));
actions.forEach((action, idx) => {
this.rights[action] = rights[idx];
});
const active = this.tabsEl.querySelector('.active');
if(active && whichChild(active) !== (this.emojiTab.tabId + 1) && (!canSendStickers || !canSendGifs)) {
if(active && whichChild(active) !== (this.emojiTab.tabId + 1) && (!this.rights['send_stickers'] || !this.rights['send_gifs'])) {
this.selectTab(this.emojiTab.tabId, false);
}
};

View File

@ -177,7 +177,7 @@ let init = () => {
const peerId = (input.dataset.peerId || NULL_PEER_ID).toPeerId();
if(html.trim()) {
console.log(html.replace(/ (style|class|id)=".+?"/g, ''));
// console.log(html.replace(/ (style|class|id)=".+?"/g, ''));
html = html.replace(/<style([\s\S]*)<\/style>/, '');
html = html.replace(/<!--([\s\S]*)-->/, '');
@ -260,7 +260,7 @@ let init = () => {
mergeEntities(entities, entities2);
}
console.log('usePlainText', usePlainText);
// console.log('usePlainText', usePlainText);
}
if(usePlainText) {

View File

@ -42,20 +42,22 @@ export default class InputFieldAnimated extends InputField {
this.inputFake.className = this.input.className + ' input-field-input-fake';
}
public onFakeInput(setHeight = true) {
public onFakeInput(setHeight = true, noAnimation?: boolean) {
const {scrollHeight: newHeight/* , clientHeight */} = this.inputFake;
/* if(this.wasInputFakeClientHeight && this.wasInputFakeClientHeight !== clientHeight) {
this.input.classList.add('no-scrollbar'); // ! в сафари может вообще не появиться скролл после анимации, так как ему нужен полный reflow блока с overflow.
this.showScrollDebounced();
} */
noAnimation ??= !this.input.isContentEditable;
const currentHeight = +this.input.style.height.replace('px', '');
if(currentHeight === newHeight) {
return;
}
const TRANSITION_DURATION_FACTOR = 50;
const transitionDuration = Math.round(
const transitionDuration = noAnimation ? 0 : Math.round(
TRANSITION_DURATION_FACTOR * Math.log(Math.abs(newHeight - currentHeight))
);

View File

@ -4,15 +4,19 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import type {ChatRights} from '../../lib/appManagers/appChatsManager';
import flatten from '../../helpers/array/flatten';
import appImManager from '../../lib/appManagers/appImManager';
import rootScope from '../../lib/rootScope';
import {toastNew} from '../toast';
import PopupPickUser from './pickUser';
import getMediaFromMessage from '../../lib/appManagers/utils/messages/getMediaFromMessage';
export default class PopupForward extends PopupPickUser {
constructor(
peerIdMids?: {[fromPeerId: PeerId]: number[]},
onSelect?: (peerId: PeerId) => Promise<void> | void
onSelect?: (peerId: PeerId) => Promise<void> | void,
chatRightsAction: ChatRights[] = ['send_plain']
) {
super({
peerTypes: ['dialogs', 'contacts'],
@ -43,8 +47,71 @@ export default class PopupForward extends PopupPickUser {
appImManager.chat.input.initMessagesForward(peerIdMids);
},
placeholder: 'ShareModal.Search.ForwardPlaceholder',
chatRightsAction: 'send_messages',
chatRightsActions: chatRightsAction,
selfPresence: 'ChatYourSelf'
});
}
public static async create(...args: ConstructorParameters<typeof PopupForward>) {
const [peerIdMids] = args;
const messagesPromises = Object.keys(peerIdMids).map((peerId) => {
const mids = peerIdMids[peerId as any as number];
return mids.map((mid) => {
return rootScope.managers.appMessagesManager.getMessageByPeer(peerId.toPeerId(), mid);
});
});
const messages = await Promise.all(flatten(messagesPromises));
const actions: Set<ChatRights> = new Set();
messages.forEach((message) => {
if(!message) {
return;
}
const media = getMediaFromMessage(message);
let action: ChatRights;
if(!media) {
if(message.viaBotId) {
action = 'send_inline';
} else {
action = 'send_plain';
}
} else {
if(media._ === 'webPage') {
action = 'embed_links';
} else if(media._ === 'photo') {
action = 'send_photos';
} else if(media._ === 'game') {
action = 'send_games';
} else {
switch(media.type) {
case 'audio':
action = 'send_audios';
break;
case 'gif':
action = 'send_gifs';
break;
case 'round':
action = 'send_roundvideos';
break;
case 'sticker':
action = 'send_stickers';
break;
case 'voice':
action = 'send_voices';
break;
default:
action = 'send_docs';
break;
}
}
}
if(action) {
actions.add(action);
}
});
new PopupForward(args[0], args[1], Array.from(actions));
}
}

View File

@ -6,9 +6,10 @@
import type Chat from '../chat/chat';
import type {SendFileDetails} from '../../lib/appManagers/appMessagesManager';
import type {ChatRights} from '../../lib/appManagers/appChatsManager';
import PopupElement from '.';
import Scrollable from '../scrollable';
import {toast} from '../toast';
import {toast, toastNew} from '../toast';
import SendContextMenu from '../chat/sendContextMenu';
import {createPosterFromMedia, createPosterFromVideo} from '../../helpers/createPoster';
import {MyDocument} from '../../lib/appManagers/appDocsManager';
@ -41,6 +42,11 @@ import {renderImageFromUrlPromise} from '../../helpers/dom/renderImageFromUrl';
import ButtonMenuToggle from '../buttonMenuToggle';
import partition from '../../helpers/array/partition';
import InputFieldAnimated from '../inputFieldAnimated';
import IMAGE_MIME_TYPES_SUPPORTED from '../../environment/imageMimeTypesSupport';
import VIDEO_MIME_TYPES_SUPPORTED from '../../environment/videoMimeTypesSupport';
import rootScope from '../../lib/rootScope';
import shake from '../../helpers/dom/shake';
import AUDIO_MIME_TYPES_SUPPORTED from '../../environment/audioMimeTypeSupport';
type SendFileParams = SendFileDetails & {
file?: File,
@ -96,6 +102,29 @@ export default class PopupNewMedia extends PopupElement {
this.construct(willAttachType);
}
public static async canSend(peerId: PeerId, onlyVisible?: boolean) {
const actions: ChatRights[] = [
'send_photos',
'send_videos',
'send_docs',
'send_audios',
'send_gifs'
];
const actionsPromises = actions.map((action) => {
return peerId.isAnyChat() && !onlyVisible ? rootScope.managers.appChatsManager.hasRights(peerId.toChatId(), action) : true;
});
const out: {[action in ChatRights]?: boolean} = {};
const results = await Promise.all(actionsPromises);
actions.forEach((action, idx) => {
out[action] = results[idx];
})
return out;
}
private async construct(willAttachType: PopupNewMedia['willAttach']['type']) {
this.willAttach = {
type: willAttachType,
@ -106,6 +135,12 @@ export default class PopupNewMedia extends PopupElement {
const captionMaxLength = await this.managers.apiManager.getLimit('caption');
this.captionLengthMax = captionMaxLength;
const canSend = await PopupNewMedia.canSend(this.chat.peerId, true);
const canSendPhotos = canSend.send_photos;
const canSendVideos = canSend.send_videos;
const canSendDocs = canSend.send_docs;
attachClickEvent(this.btnConfirm, () => this.send(), {listenerSetter: this.listenerSetter});
const btnMenu = await ButtonMenuToggle({
@ -115,17 +150,35 @@ export default class PopupNewMedia extends PopupElement {
icon: 'image',
text: 'Popup.Attach.AsMedia',
onClick: () => this.changeType('media'),
verify: () => this.hasAnyMedia() && this.willAttach.type === 'document'
verify: () => {
if(!this.hasAnyMedia() || this.willAttach.type !== 'document') {
return false;
}
if(!canSendPhotos && !canSendVideos) {
return false;
}
if(!canSendPhotos || !canSendVideos) {
const mimeTypes = canSendPhotos ? IMAGE_MIME_TYPES_SUPPORTED : VIDEO_MIME_TYPES_SUPPORTED;
const {media, files} = this.partition(mimeTypes);
if(files.length) {
return false;
}
}
return true;
}
}, {
icon: 'document',
text: 'SendAsFile',
onClick: () => this.changeType('document'),
verify: () => this.files.length === 1 && this.willAttach.type !== 'document'
verify: () => this.files.length === 1 && this.willAttach.type !== 'document' && canSendDocs
}, {
icon: 'document',
text: 'SendAsFiles',
onClick: () => this.changeType('document'),
verify: () => this.files.length > 1 && this.willAttach.type !== 'document'
verify: () => this.files.length > 1 && this.willAttach.type !== 'document' && canSendDocs
}, {
icon: 'groupmedia',
text: 'Popup.Attach.GroupMedia',
@ -365,8 +418,8 @@ export default class PopupNewMedia extends PopupElement {
this.willAttach.type = type;
}
private partition() {
const [media, files] = partition(this.willAttach.sendFileDetails, (d) => MEDIA_MIME_TYPES_SUPPORTED.has(d.file.type));
private partition(mimeTypes = MEDIA_MIME_TYPES_SUPPORTED) {
const [media, files] = partition(this.willAttach.sendFileDetails, (d) => mimeTypes.has(d.file.type));
return {
media,
files
@ -455,7 +508,69 @@ export default class PopupNewMedia extends PopupElement {
}
};
private send(force = false) {
private async send(force = false) {
let caption = this.messageInputField.value;
if(caption.length > this.captionLengthMax) {
toast(I18n.format('Error.PreviewSender.CaptionTooLong', true));
return;
}
const {peerId, input} = this.chat;
const canSend = await PopupNewMedia.canSend(peerId);
const willAttach = this.willAttach;
willAttach.isMedia = willAttach.type === 'media' || undefined;
const {sendFileDetails, isMedia} = willAttach;
let foundBad = false;
this.iterate((sendFileParams) => {
if(foundBad) {
return;
}
const isBad: (LangPackKey | boolean)[] = sendFileParams.map((params) => {
const a: [Set<string> | (() => boolean), LangPackKey, ChatRights][] = [
[AUDIO_MIME_TYPES_SUPPORTED, 'GlobalAttachAudioRestricted', 'send_audios'],
[() => !MEDIA_MIME_TYPES_SUPPORTED.has(params.file.type), 'GlobalAttachDocumentsRestricted', 'send_docs']
];
if(isMedia) {
a.unshift(
[IMAGE_MIME_TYPES_SUPPORTED, 'GlobalAttachPhotoRestricted', 'send_photos'],
[() => VIDEO_MIME_TYPES_SUPPORTED.has(params.file.type as any) && params.noSound, 'GlobalAttachGifRestricted', 'send_gifs'],
[VIDEO_MIME_TYPES_SUPPORTED, 'GlobalAttachVideoRestricted', 'send_videos']
);
}
const found = a.find(([verify]) => {
return typeof(verify) === 'function' ? verify() : verify.has(params.file.type);
});
if(found) {
return canSend[found[2]] ? undefined : found[1];
}
return (!isMedia && !canSend.send_docs && 'GlobalAttachDocumentsRestricted') || undefined;
});
const key = isBad.find((i) => typeof(i) === 'string') as LangPackKey;
if(key) {
toastNew({
langPackKey: key
});
if(rootScope.settings.animationsEnabled) {
shake(this.body);
}
}
foundBad ||= !!key;
});
if(foundBad) {
return;
}
if(this.chat.type === 'scheduled' && !force) {
this.chat.input.scheduleSending(() => {
this.send(true);
@ -464,18 +579,6 @@ export default class PopupNewMedia extends PopupElement {
return;
}
let caption = this.messageInputField.value;
if(caption.length > this.captionLengthMax) {
toast(I18n.format('Error.PreviewSender.CaptionTooLong', true));
return;
}
const willAttach = this.willAttach;
willAttach.isMedia = willAttach.type === 'media' || undefined;
const {sendFileDetails, isMedia} = willAttach;
const {peerId, input} = this.chat;
const {length} = sendFileDetails;
const sendingParams = this.chat.getMessageSendingParams();
this.iterate((sendFileParams) => {
@ -631,7 +734,7 @@ export default class PopupNewMedia extends PopupElement {
const file = params.file;
const isPhoto = file.type.startsWith('image/');
const isAudio = file.type.startsWith('audio/');
const isAudio = AUDIO_MIME_TYPES_SUPPORTED.has(file.type as any);
if(isPhoto || isAudio || file.size < 20e6) {
params.objectURL ||= await apiManagerProxy.invoke('createObjectURL', file);
}
@ -860,3 +963,5 @@ export default class PopupNewMedia extends PopupElement {
});
}
}
(window as any).PopupNewMedia = PopupNewMedia;

View File

@ -16,7 +16,7 @@ export default class PopupPickUser extends PopupElement {
peerTypes: AppSelectPeers['peerType'],
onSelect?: (peerId: PeerId) => Promise<void> | void,
placeholder: LangPackKey,
chatRightsAction?: AppSelectPeers['chatRightsAction'],
chatRightsActions?: AppSelectPeers['chatRightsActions'],
peerId?: number,
selfPresence?: LangPackKey
}) {
@ -51,7 +51,7 @@ export default class PopupPickUser extends PopupElement {
this.selector.input.focus();
}
},
chatRightsAction: options.chatRightsAction,
chatRightsActions: options.chatRightsActions,
multiSelect: false,
rippleEnabled: false,
avatarSize: 'abitbigger',

View File

@ -149,13 +149,15 @@ export default function ripple(
// });
};
const isRippleUnneeded = (e: Event) => e.target !== elem && (
['BUTTON', 'A'].includes((e.target as HTMLElement).tagName) ||
findUpClassName(e.target as HTMLElement, 'c-ripple') !== r
) && (
attachListenerTo === elem ||
!findUpAsChild(e.target as HTMLElement, attachListenerTo)
);
const isRippleUnneeded = (e: Event) => {
return e.target !== elem && (
['BUTTON', 'A'].includes((e.target as HTMLElement).tagName) ||
findUpClassName(e.target as HTMLElement, 'c-ripple') !== r
) && (
attachListenerTo === elem ||
!findUpAsChild(e.target as HTMLElement, attachListenerTo)
) && !findUpClassName(e.target, 'checkbox-field');
};
// TODO: rename this variable
let touchStartFired = false;

View File

@ -181,10 +181,15 @@ export default class AppEditChatTab extends SliderSuperTab {
if(canChangePermissions && !isBroadcast) {
const flags = [
'send_messages',
'send_media',
'send_stickers',
'send_polls',
'send_photos',
'send_videos',
'send_roundvideos',
'send_audios',
'send_voices',
'send_docs',
'send_plain',
'embed_links',
'invite_users',
'pin_messages',

View File

@ -4,13 +4,15 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import type {ChatRights} from '../../../lib/appManagers/appChatsManager';
import flatten from '../../../helpers/array/flatten';
import cancelEvent from '../../../helpers/dom/cancelEvent';
import {attachClickEvent} from '../../../helpers/dom/clickEvent';
import findUpTag from '../../../helpers/dom/findUpTag';
import replaceContent from '../../../helpers/dom/replaceContent';
import ListenerSetter from '../../../helpers/listenerSetter';
import ScrollableLoader from '../../../helpers/scrollableLoader';
import {ChannelParticipant, Chat, ChatBannedRights, Update} from '../../../layer';
import {ChatRights} from '../../../lib/appManagers/appChatsManager';
import {ChannelParticipant, Chat, ChatBannedRights} from '../../../layer';
import appDialogsManager, {DialogDom, DIALOG_LIST_ELEMENT_TAG} from '../../../lib/appManagers/appDialogsManager';
import {AppManagers} from '../../../lib/appManagers/managers';
import combineParticipantBannedRights from '../../../lib/appManagers/utils/chats/combineParticipantBannedRights';
@ -21,20 +23,32 @@ import I18n, {i18n, join, LangPackKey} from '../../../lib/langPack';
import rootScope from '../../../lib/rootScope';
import CheckboxField from '../../checkboxField';
import PopupPickUser from '../../popups/pickUser';
import Row, {CreateRowFromCheckboxField} from '../../row';
import Row from '../../row';
import SettingSection from '../../settingSection';
import {SliderSuperTabEventable} from '../../sliderTab';
import {toast} from '../../toast';
import AppUserPermissionsTab from './userPermissions';
import findUpAsChild from '../../../helpers/dom/findUpAsChild';
type T = {
flags: ChatRights[],
text: LangPackKey,
exceptionText: LangPackKey,
checkboxField?: CheckboxField,
nested?: T[],
nestedTo?: T,
nestedCounter?: HTMLElement,
setNestedCounter?: (count: number) => void,
toggleWith?: {checked?: ChatRights[], unchecked?: ChatRights[]}
};
export class ChatPermissions {
public v: Array<{
flags: ChatRights[],
text: LangPackKey,
exceptionText: LangPackKey,
checkboxField?: CheckboxField,
}>;
private toggleWith: Partial<{[chatRight in ChatRights]: ChatRights[]}>;
public v: Array<T>;
protected chat: Chat.chat | Chat.channel;
protected rights: ChatBannedRights.chatBannedRights;
protected defaultBannedRights: ChatBannedRights.chatBannedRights;
protected restrictionText: LangPackKey;
constructor(private options: {
chatId: ChatId,
@ -46,79 +60,158 @@ export class ChatPermissions {
}
public async construct() {
this.v = [
{flags: ['send_messages'], text: 'UserRestrictionsSend', exceptionText: 'UserRestrictionsNoSend'},
{flags: ['send_media'], text: 'UserRestrictionsSendMedia', exceptionText: 'UserRestrictionsNoSendMedia'},
const mediaNested: T[] = [
{flags: ['send_photos'], text: 'UserRestrictionsSendPhotos', exceptionText: 'UserRestrictionsNoSendPhotos'},
{flags: ['send_videos'], text: 'UserRestrictionsSendVideos', exceptionText: 'UserRestrictionsNoSendVideos'},
{flags: ['send_stickers', 'send_gifs'], text: 'UserRestrictionsSendStickers', exceptionText: 'UserRestrictionsNoSendStickers'},
{flags: ['send_polls'], text: 'UserRestrictionsSendPolls', exceptionText: 'UserRestrictionsNoSendPolls'},
{flags: ['embed_links'], text: 'UserRestrictionsEmbedLinks', exceptionText: 'UserRestrictionsNoEmbedLinks'},
{flags: ['send_audios'], text: 'UserRestrictionsSendMusic', exceptionText: 'UserRestrictionsNoSendMusic'},
{flags: ['send_docs'], text: 'UserRestrictionsSendFiles', exceptionText: 'UserRestrictionsNoSendDocs'},
{flags: ['send_voices'], text: 'UserRestrictionsSendVoices', exceptionText: 'UserRestrictionsNoSendVoice'},
{flags: ['send_roundvideos'], text: 'UserRestrictionsSendRound', exceptionText: 'UserRestrictionsNoSendRound'},
{flags: ['embed_links'], text: 'UserRestrictionsEmbedLinks', exceptionText: 'UserRestrictionsNoEmbedLinks', toggleWith: {checked: ['send_plain']}},
{flags: ['send_polls'], text: 'UserRestrictionsSendPolls', exceptionText: 'UserRestrictionsNoSendPolls'}
];
const mediaToggleWith = flatten(mediaNested.map(({flags}) => flags));
const media: T = {flags: ['send_media'], text: 'UserRestrictionsSendMedia', exceptionText: 'UserRestrictionsNoSendMedia', nested: mediaNested, toggleWith: {unchecked: mediaToggleWith, checked: mediaToggleWith}};
this.v = [
{flags: ['send_plain'], text: 'UserRestrictionsSend', exceptionText: 'UserRestrictionsNoSend', toggleWith: {unchecked: ['embed_links']}},
media,
{flags: ['invite_users'], text: 'UserRestrictionsInviteUsers', exceptionText: 'UserRestrictionsNoInviteUsers'},
{flags: ['pin_messages'], text: 'UserRestrictionsPinMessages', exceptionText: 'UserRestrictionsNoPinMessages'},
{flags: ['change_info'], text: 'UserRestrictionsChangeInfo', exceptionText: 'UserRestrictionsNoChangeInfo'}
];
this.toggleWith = {
'send_messages': ['send_media', 'send_stickers', 'send_polls', 'embed_links']
};
mediaNested.forEach((info) => info.nestedTo = media);
const options = this.options;
const chat = await this.managers.appChatsManager.getChat(options.chatId) as Chat.chat | Chat.channel;
const defaultBannedRights = chat.default_banned_rights;
const rights = options.participant ? combineParticipantBannedRights(chat as Chat.channel, options.participant.banned_rights) : defaultBannedRights;
const chat = this.chat = await this.managers.appChatsManager.getChat(options.chatId) as Chat.chat | Chat.channel;
const defaultBannedRights = this.defaultBannedRights = chat.default_banned_rights;
const rights = this.rights = options.participant ? combineParticipantBannedRights(chat as Chat.channel, options.participant.banned_rights) : defaultBannedRights;
const restrictionText: LangPackKey = options.participant ? 'UserRestrictionsDisabled' : 'EditCantEditPermissionsPublic';
for(const info of this.v) {
const mainFlag = info.flags[0];
const row = CreateRowFromCheckboxField(
info.checkboxField = new CheckboxField({
text: info.text,
checked: hasRights(chat, mainFlag, rights),
restriction: true,
listenerSetter: options.listenerSetter
})
);
const {nodes} = this.createRow(info);
options.appendTo.append(...nodes);
}
if((
options.participant &&
defaultBannedRights.pFlags[mainFlag as keyof typeof defaultBannedRights['pFlags']]
) || (
getPeerActiveUsernames(chat as Chat.channel)[0] &&
(
info.flags.includes('pin_messages') ||
info.flags.includes('change_info')
)
)
) {
info.checkboxField.input.disabled = true;
this.v.push(...mediaNested);
}
/* options.listenerSetter.add(info.checkboxField.input)('change', (e) => {
if(!e.isTrusted) {
return;
protected createRow(info: T, isNested?: boolean) {
const {defaultBannedRights, chat, rights, restrictionText} = this;
const mainFlag = info.flags[0];
const row = new Row({
titleLangKey: isNested ? undefined : info.text,
checkboxField: info.checkboxField = new CheckboxField({
text: isNested ? info.text : undefined,
checked: info.nested ? false : hasRights(chat, mainFlag, rights),
toggle: !isNested,
listenerSetter: this.options.listenerSetter,
restriction: !isNested
}),
listenerSetter: this.options.listenerSetter,
clickable: info.nested ? (e) => {
if(findUpAsChild(e.target as HTMLElement, row.checkboxField.label)) {
return;
}
cancelEvent(e);
row.container.classList.toggle('accordion-toggler-expanded');
accordion.classList.toggle('is-expanded');
} : undefined
});
if((
this.options.participant &&
defaultBannedRights.pFlags[mainFlag as keyof typeof defaultBannedRights['pFlags']]
) || (
getPeerActiveUsernames(chat as Chat.channel)[0] &&
(
info.flags.includes('pin_messages') ||
info.flags.includes('change_info')
)
)
) {
info.checkboxField.input.disabled = true;
attachClickEvent(info.checkboxField.label, (e) => {
toast(I18n.format(restrictionText, true));
}, {listenerSetter: this.options.listenerSetter});
}
if(info.toggleWith || info.nestedTo) {
const processToggleWith = info.toggleWith ? (info: T) => {
const {toggleWith, nested} = info;
const value = info.checkboxField.checked;
const arr = value ? toggleWith.checked : toggleWith.unchecked;
if(!arr) {
return;
}
const other = this.v.filter((i) => arr.includes(i.flags[0]));
other.forEach((info) => {
info.checkboxField.setValueSilently(value);
if(info.nestedTo && !nested) {
this.setNestedCounter(info.nestedTo);
}
cancelEvent(e);
toast('This option is disabled for all members in Group Permissions.');
info.checkboxField.checked = false;
}); */
attachClickEvent(info.checkboxField.label, (e) => {
toast(I18n.format(restrictionText, true));
}, {listenerSetter: options.listenerSetter});
}
if(this.toggleWith[mainFlag]) {
options.listenerSetter.add(info.checkboxField.input)('change', () => {
if(!info.checkboxField.checked) {
const other = this.v.filter((i) => this.toggleWith[mainFlag].includes(i.flags[0]));
other.forEach((info) => {
info.checkboxField.checked = false;
});
if(info.toggleWith) {
processToggleWith(info);
}
});
}
options.appendTo.append(row.container);
if(info.nested) {
this.setNestedCounter(info);
}
} : undefined;
const processNestedTo = info.nestedTo ? () => {
const length = this.getNestedCheckedLength(info.nestedTo);
info.nestedTo.checkboxField.setValueSilently(length === info.nestedTo.nested.length);
this.setNestedCounter(info.nestedTo, length);
} : undefined;
this.options.listenerSetter.add(info.checkboxField.input)('change', () => {
processToggleWith?.(info);
processNestedTo?.();
});
}
const nodes: HTMLElement[] = [row.container];
let accordion: HTMLElement, nestedCounter: HTMLElement;
if(info.nested) {
const container = accordion = document.createElement('div');
container.classList.add('accordion');
container.style.setProperty('--max-height', info.nested.length * 48 + 'px');
info.nested.forEach((info) => {
container.append(...this.createRow(info, true).nodes);
});
nodes.push(container);
const span = document.createElement('span');
span.classList.add('tgico-down', 'accordion-icon');
nestedCounter = info.nestedCounter = document.createElement('b');
this.setNestedCounter(info);
row.title.append(' ', nestedCounter, ' ', span);
row.container.classList.add('accordion-toggler');
row.titleRow.classList.add('with-delimiter');
row.checkboxField.setValueSilently(this.getNestedCheckedLength(info) === info.nested.length);
}
return {row, nodes};
}
protected getNestedCheckedLength(info: T) {
return info.nested.reduce((acc, v) => acc + +v.checkboxField.checked, 0);
}
protected setNestedCounter(info: T, count = this.getNestedCheckedLength(info)) {
info.nestedCounter.textContent = `${count}/${info.nested.length}`;
}
public takeOut() {
@ -128,14 +221,23 @@ export class ChatPermissions {
pFlags: {}
};
const IGNORE_FLAGS: Set<ChatRights> = new Set([
'send_media'
]);
for(const info of this.v) {
const banned = !info.checkboxField.checked;
if(banned) {
info.flags.forEach((flag) => {
// @ts-ignore
rights.pFlags[flag] = true;
});
if(!banned) {
continue;
}
info.flags.forEach((flag) => {
if(IGNORE_FLAGS.has(flag)) {
return;
}
// @ts-ignore
rights.pFlags[flag] = true;
});
}
return rights;
@ -195,7 +297,7 @@ export default class AppGroupPermissionsTab extends SliderSuperTabEventable {
const openPermissions = async(peerId: PeerId) => {
let participant: AppUserPermissionsTab['participant'];
try {
participant = await this.managers.appProfileManager.getChannelParticipant(this.chatId, peerId) as any;
participant = await this.managers.appProfileManager.getParticipant(this.chatId, peerId);
} catch(err) {
toast('User is no longer participant');
return;
@ -270,37 +372,35 @@ export default class AppGroupPermissionsTab extends SliderSuperTabEventable {
append
});
setSubtitle(dom, participant);
(dom.listEl as any).dialogDom = dom;
// dom.titleSpan.innerHTML = 'Chinaza Akachi';
// dom.lastMessageSpan.innerHTML = 'Can Add Users and Pin Messages';
setSubtitle(dom, participant);
};
// this.listenerSetter.add(rootScope)('updateChannelParticipant', (update: Update.updateChannelParticipant) => {
// const needAdd = update.new_participant?._ === 'channelParticipantBanned' && !update.new_participant.banned_rights.pFlags.view_messages;
// const li = list.querySelector(`[data-peer-id="${update.user_id}"]`);
// if(needAdd) {
// if(!li) {
// add(update.new_participant as ChannelParticipant.channelParticipantBanned, false);
// } else {
// setSubtitle(li, update.new_participant as ChannelParticipant.channelParticipantBanned);
// }
this.listenerSetter.add(rootScope)('chat_participant', (update) => {
const needAdd = update.new_participant?._ === 'channelParticipantBanned' &&
!update.new_participant.banned_rights.pFlags.view_messages;
const li = list.querySelector(`[data-peer-id="${update.user_id}"]`);
if(needAdd) {
if(!li) {
add(update.new_participant as ChannelParticipant.channelParticipantBanned, false);
} else {
setSubtitle((li as any).dialogDom, update.new_participant as ChannelParticipant.channelParticipantBanned);
}
// if(update.prev_participant?._ !== 'channelParticipantBanned') {
// ++exceptionsCount;
// }
// } else {
// if(li) {
// li.remove();
// }
if(update.prev_participant?._ !== 'channelParticipantBanned') {
++exceptionsCount;
}
} else {
li?.remove();
// if(update.prev_participant?._ === 'channelParticipantBanned') {
// --exceptionsCount;
// }
// }
if(update.prev_participant?._ === 'channelParticipantBanned') {
--exceptionsCount;
}
}
// setLength();
// });
setLength();
});
const setLength = () => {
replaceContent(addExceptionRow.subtitle, i18n(exceptionsCount ? 'Permissions.ExceptionsCount' : 'Permissions.NoExceptions', [exceptionsCount]));

View File

@ -7,16 +7,18 @@
import {attachClickEvent} from '../../../helpers/dom/clickEvent';
import toggleDisability from '../../../helpers/dom/toggleDisability';
import deepEqual from '../../../helpers/object/deepEqual';
import {ChannelParticipant} from '../../../layer';
import {ChannelParticipant, ChatParticipant} from '../../../layer';
import appDialogsManager from '../../../lib/appManagers/appDialogsManager';
import Button from '../../button';
import confirmationPopup from '../../confirmationPopup';
import SettingSection from '../../settingSection';
import {SliderSuperTabEventable} from '../../sliderTab';
import getUserStatusString from '../../wrappers/getUserStatusString';
import wrapPeerTitle from '../../wrappers/peerTitle';
import {ChatPermissions} from './groupPermissions';
export default class AppUserPermissionsTab extends SliderSuperTabEventable {
public participant: ChannelParticipant;
public participant: ChannelParticipant | ChatParticipant;
public chatId: ChatId;
public userId: UserId;
@ -26,6 +28,8 @@ export default class AppUserPermissionsTab extends SliderSuperTabEventable {
let destroyListener: () => void;
const isChannel = await this.managers.appChatsManager.isChannel(this.chatId);
{
const section = new SettingSection({
name: 'UserRestrictionsCanDo'
@ -55,7 +59,6 @@ export default class AppUserPermissionsTab extends SliderSuperTabEventable {
}, this.managers);
destroyListener = () => {
// appChatsManager.editChatDefaultBannedRights(this.chatId, p.takeOut());
const rights = p.takeOut();
if(this.participant._ === 'channelParticipantBanned' && deepEqual(this.participant.banned_rights.pFlags, rights.pFlags)) {
return;
@ -77,7 +80,10 @@ export default class AppUserPermissionsTab extends SliderSuperTabEventable {
attachClickEvent(btnDeleteException, () => {
const toggle = toggleDisability([btnDeleteException], true);
this.managers.appChatsManager.clearChannelParticipantBannedRights(this.chatId, this.participant).then(() => {
this.managers.appChatsManager.clearChannelParticipantBannedRights(
this.chatId,
this.participant as ChannelParticipant.channelParticipantBanned
).then(() => {
this.eventListener.removeEventListener('destroy', destroyListener);
this.close();
}, () => {
@ -90,31 +96,34 @@ export default class AppUserPermissionsTab extends SliderSuperTabEventable {
const btnDelete = Button('btn-primary btn-transparent danger', {icon: 'deleteuser', text: 'UserRestrictionsBlock'});
attachClickEvent(btnDelete, () => {
attachClickEvent(btnDelete, async() => {
const toggle = toggleDisability([btnDelete], true);
this.managers.appChatsManager.kickFromChannel(this.chatId, this.participant).then(() => {
this.eventListener.removeEventListener('destroy', destroyListener);
this.close();
});
/* new PopupPeer('popup-group-kick-user', {
peerId: -this.chatId,
title: 'Ban User?',
description: `Are you sure you want to ban <b>${appPeersManager.getPeerTitle(this.userId)}</b>`,
buttons: addCancelButton([{
text: 'BAN',
callback: () => {
const toggle = toggleDisability([btnDelete], true);
appChatsManager.kickFromChannel(this.chatId, this.participant).then(() => {
this.eventListener.removeEventListener('destroy', destroyListener);
this.close();
}, () => {
toggle();
});
},
isDanger: true
}])
}).show(); */
try {
const peerId = this.userId.toPeerId();
await confirmationPopup({
peerId: this.chatId.toPeerId(true),
descriptionLangKey: 'Permissions.RemoveFromGroup',
descriptionLangArgs: [await wrapPeerTitle({peerId: peerId})],
titleLangKey: 'ChannelBlockUser',
button: {
langKey: 'Remove',
isDanger: true
}
});
if(!isChannel) {
await this.managers.appChatsManager.kickFromChat(this.chatId, this.participant);
} else {
await this.managers.appChatsManager.kickFromChannel(this.chatId, this.participant as ChannelParticipant);
}
} catch(err) {
toggle();
return;
}
this.eventListener.removeEventListener('destroy', destroyListener);
this.close();
}, {listenerSetter: this.listenerSetter});
section.content.append(btnDelete);

View File

@ -98,7 +98,7 @@ export default class SwipeHandler {
private onSwipe: (xDiff: number, yDiff: number, e: EE, cancelDrag?: (x: boolean, y: boolean) => void) => boolean | void;
private verifyTouchTarget: (evt: EE) => boolean | Promise<boolean>;
private onFirstSwipe: (e: EE) => void;
private onReset: () => void;
private onReset: (e?: Event) => void;
private onStart: () => void;
private onZoom: (details: ZoomDetails) => void;
private onDrag: (e: EE, captureEvent: E, details: {dragOffsetX: number, dragOffsetY: number}, cancelDrag: (x: boolean, y: boolean) => void) => void;
@ -252,7 +252,7 @@ export default class SwipeHandler {
}
if(this.hadMove) {
this.onReset?.();
this.onReset?.(e);
}
this.releaseWheelDrag?.clearTimeout();
@ -291,10 +291,14 @@ export default class SwipeHandler {
}
const e = getEvent(_e);
if(Math.max(0, e.button ?? 0) !== 0) {
if(![0, 1].includes(Math.max(0, e.button ?? 0))) {
return;
}
if(e.button === 1) {
cancelEvent(_e as any);
}
if(isSwipingBackSafari(_e as any)) {
return;
}

View File

@ -37,7 +37,7 @@ export default function wrapAlbum({messages, attachmentDiv, middleware, uploadin
// !lowest msgID will be the FIRST in album
for(const message of messages) {
const media: Photo.photo | Document.document = getMediaFromMessage(message);
const media = getMediaFromMessage(message, true);
const size: any = media._ === 'photo' ? choosePhotoSize(media, 480, 480) : {w: media.w, h: media.h};
items.push({size, media, message});

View File

@ -35,7 +35,7 @@ export default async function wrapPhoto({photo, message, container, boxWidth, bo
isOut?: boolean,
lazyLoadQueue?: LazyLoadQueue,
middleware?: Middleware,
size?: PhotoSize | VideoSize,
size?: PhotoSize | Extract<VideoSize, VideoSize.videoSize>,
withoutPreloader?: boolean,
loadPromises?: Promise<any>[],
autoDownloadSize?: number,

View File

@ -85,7 +85,7 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd
skipRatio?: number,
static?: boolean,
managers?: AppManagers,
fullThumb?: PhotoSize | VideoSize,
fullThumb?: PhotoSize | Extract<VideoSize, VideoSize.videoSize>,
isOut?: boolean,
noPremium?: boolean,
withLock?: boolean,

View File

@ -40,7 +40,7 @@ export default function wrapStickerAnimation({
skipRatio?: number,
play: boolean,
managers?: AppManagers,
fullThumb?: PhotoSize | VideoSize,
fullThumb?: PhotoSize | Extract<VideoSize, VideoSize.videoSize>,
withRandomOffset?: boolean,
relativeEffect?: boolean,
loopEffect?: boolean

View File

@ -82,7 +82,7 @@ export default async function wrapVideo({doc, container, message, boxWidth, boxH
loadPromises?: Promise<any>[],
autoDownload?: ChatAutoDownloadSettings,
photoSize?: PhotoSize,
videoSize?: VideoSize,
videoSize?: Extract<VideoSize, VideoSize.videoSize>,
searchContext?: MediaSearchContext,
managers?: AppManagers,
noAutoplayAttribute?: boolean

View File

@ -21,7 +21,7 @@ const App = {
version: process.env.VERSION,
versionFull: process.env.VERSION_FULL,
build: +process.env.BUILD,
langPackVersion: '0.8.6',
langPackVersion: '0.9.3',
langPack: 'webk',
langPackCode: 'en',
domains: MAIN_DOMAINS,

View File

@ -0,0 +1,8 @@
export type AUDIO_MIME_TYPE = 'audio/mpeg' | 'audio/aac' | 'audio/wav';
const AUDIO_MIME_TYPES_SUPPORTED: Set<AUDIO_MIME_TYPE> = new Set([
'audio/mpeg',
'audio/aac',
'audio/wav'
]);
export default AUDIO_MIME_TYPES_SUPPORTED;

5
src/global.d.ts vendored
View File

@ -56,7 +56,7 @@ declare global {
type LocalFileError = ApiFileManagerError | ReferenceError | StorageError;
type LocalErrorType = LocalFileError | NetworkerError | FiltersError |
'UNKNOWN' | 'NO_DOC' | 'MIDDLEWARE' | 'PORT_DISCONNECTED' | 'NO_AUTO_DOWNLOAD';
'UNKNOWN' | 'NO_DOC' | 'MIDDLEWARE' | 'PORT_DISCONNECTED' | 'NO_AUTO_DOWNLOAD' | 'CHAT_PRIVATE';
type ServerErrorType = 'FILE_REFERENCE_EXPIRED' | 'SESSION_REVOKED' | 'AUTH_KEY_DUPLICATED' |
'SESSION_PASSWORD_NEEDED' | 'CONNECTION_NOT_INITED' | 'ERROR_EMPTY' | 'MTPROTO_CLUSTER_INVALID' |
@ -64,7 +64,8 @@ declare global {
'VOICE_MESSAGES_FORBIDDEN' | 'PHOTO_INVALID_DIMENSIONS' | 'PHOTO_SAVE_FILE_INVALID' |
'USER_ALREADY_PARTICIPANT' | 'USERNAME_INVALID' | 'USERNAME_PURCHASE_AVAILABLE' | 'USERNAMES_ACTIVE_TOO_MUCH' |
'BOT_INVALID' | 'USERNAME_NOT_OCCUPIED' | 'PINNED_TOO_MUCH' | 'LOCATION_INVALID' |
'FILE_ID_INVALID' | 'CHANNEL_FORUM_MISSING' | 'TRANSCRIPTION_FAILED';
'FILE_ID_INVALID' | 'CHANNEL_FORUM_MISSING' | 'TRANSCRIPTION_FAILED' | 'USER_NOT_PARTICIPANT' |
'PEER_ID_INVALID';
type ErrorType = LocalErrorType | ServerErrorType;

View File

@ -8,6 +8,17 @@ import type ListenerSetter from '../listenerSetter';
import IS_TOUCH_SUPPORTED from '../../environment/touchSupport';
import simulateEvent from './dispatchEvent';
let lastMouseDownElement: HTMLElement;
document.addEventListener('mousedown', (e) => {
lastMouseDownElement = e.target as HTMLElement;
});
export function hasMouseMovedSinceDown(e: Event) {
if(e.isTrusted && e.type === 'click' && e.target !== lastMouseDownElement) {
return true;
}
}
export const CLICK_EVENT_NAME: 'mousedown' /* | 'touchend' */ | 'click' = (IS_TOUCH_SUPPORTED ? 'mousedown' : 'click') as any;
export type AttachClickOptions = AddEventListenerOptions & Partial<{listenerSetter: ListenerSetter, touchMouseDown: true}>;
export function attachClickEvent(elem: HTMLElement | Window, callback: (e: /* TouchEvent | */MouseEvent) => void, options: AttachClickOptions = {}) {
@ -42,6 +53,18 @@ export function attachClickEvent(elem: HTMLElement | Window, callback: (e: /* To
} else {
add(CLICK_EVENT_NAME, callback, options);
} */
if(CLICK_EVENT_NAME === 'click') {
const cb = callback;
callback = (e) => {
if(hasMouseMovedSinceDown(e)) {
return;
}
cb(e);
};
}
add(CLICK_EVENT_NAME, callback, options);
// @ts-ignore

View File

@ -8,7 +8,7 @@ import appNavigationController, {NavigationItem} from '../components/appNavigati
import IS_TOUCH_SUPPORTED from '../environment/touchSupport';
import {IS_MOBILE_SAFARI} from '../environment/userAgent';
import cancelEvent from './dom/cancelEvent';
import {CLICK_EVENT_NAME} from './dom/clickEvent';
import {CLICK_EVENT_NAME, hasMouseMovedSinceDown} from './dom/clickEvent';
import findUpAsChild from './dom/findUpAsChild';
import EventListenerBase from './eventListenerBase';
@ -28,6 +28,10 @@ export default class OverlayClickHandler extends EventListenerBase<{
}
protected onClick = (e: MouseEvent | TouchEvent) => {
if(hasMouseMovedSinceDown(e)) {
return;
}
if(this.element && findUpAsChild(e.target as HTMLElement, this.element)) {
return;
}

View File

@ -82,6 +82,7 @@ const lang = {
'Message.Context.Selection.Clear': 'Clear selection',
'Message.Context.Selection.Delete': 'Delete selected',
'Message.Context.Selection.Forward': 'Forward selected',
'Message.Context.Selection.Download': 'Download selected',
'Message.Context.Selection.SendNow': 'Send Now selected',
'Message.Unsupported.Desktop': '__This message is currently not supported on Telegram Web. Try [getdesktop.telegram.org](https://getdesktop.telegram.org/)__',
'Message.Unsupported.Mobile': '__This message is currently not supported on Telegram Web. Try [telegram.org/dl](https://telegram.org/dl/)__',
@ -109,6 +110,7 @@ const lang = {
'one_value': '%d exception',
'other_value': '%d exceptions'
},
'Permissions.RemoveFromGroup': 'Are you sure you want to remove **%s** from the group?',
'PWA.Install': 'Install App',
'Link.Available': 'Link is available',
'Link.Taken': 'Link is already taken',
@ -400,6 +402,12 @@ const lang = {
'UserRestrictionsSendMedia': 'Send Media',
'UserRestrictionsSendPolls': 'Send Polls',
'UserRestrictionsSendStickers': 'Send Stickers and GIFs',
'UserRestrictionsSendPhotos': 'Send Photos',
'UserRestrictionsSendVideos': 'Send Videos',
'UserRestrictionsSendMusic': 'Send Music',
'UserRestrictionsSendFiles': 'Send Files',
'UserRestrictionsSendVoices': 'Send Voice Messages',
'UserRestrictionsSendRound': 'Send Video Messages',
'UserRestrictionsEmbedLinks': 'Embed Links',
'UserRestrictionsChangeInfo': 'Change Chat Info',
'UserRestrictionsPinMessages': 'Pin Messages',
@ -409,6 +417,12 @@ const lang = {
'UserRestrictionsNoSendMedia': 'no media',
'UserRestrictionsNoSendPolls': 'no polls',
'UserRestrictionsNoSendStickers': 'no stickers & GIFs',
'UserRestrictionsNoSendPhotos': 'no photos',
'UserRestrictionsNoSendVideos': 'no videos',
'UserRestrictionsNoSendMusic': 'no music',
'UserRestrictionsNoSendDocs': 'no files',
'UserRestrictionsNoSendVoice': 'no voice',
'UserRestrictionsNoSendRound': 'no round',
'UserRestrictionsNoEmbedLinks': 'no embed links',
'UserRestrictionsNoChangeInfo': 'can\'t change Info',
'UserRestrictionsNoPinMessages': 'no pins',
@ -892,6 +906,22 @@ const lang = {
'ThemeDay': 'Day',
'ThemeNight': 'Night',
'AutoNightSystemDefault': 'System Default',
'GlobalAttachPlainRestricted': 'Sending text messages isn\'t allowed in this group.',
'GlobalAttachDocumentsRestricted': 'Sending documents isn\'t allowed in this group.',
'GlobalAttachMediaRestricted': 'Sending media isn\'t allowed in this group.',
'GlobalAttachAudioRestricted': 'Sending music isn\'t allowed in this group.',
'GlobalAttachPhotoRestricted': 'Sending photos isn\'t allowed in this group.',
'GlobalAttachVideoRestricted': 'Sending videos isn\'t allowed in this group.',
'GlobalAttachVoiceRestricted': 'Sending voice isn\'t allowed in this group.',
'GlobalAttachRoundRestricted': 'Sending round videos isn\'t allowed in this group.',
'GlobalAttachInlineRestricted': 'Sending inline content isn\'t allowed in this group.',
'GlobalAttachStickersRestricted': 'Stickers aren\'t allowed in this group.',
'GlobalAttachGifRestricted': 'Sending GIFs is not allowed in this group.',
'GlobalAttachEmojiRestricted': 'Text messages aren\'t allowed in this group.',
'GlobalSendMessageRestricted': 'Sending messages is not allowed in this group.',
'ErrorSendRestrictedPollsAll': 'Sorry, sending polls is not allowed in this group.',
'Remove': 'Remove',
'ChannelBlockUser': 'Remove User',
// * macos
'AccountSettings.Filters': 'Chat Folders',
@ -1110,6 +1140,7 @@ const lang = {
'ChatList.Mute.3Days': 'For 3 Days',
'ChatList.Mute.Forever': 'Forever',
'Channel.DescriptionHolderDescrpiton': 'You can provide an optional description for your channel.',
'Channel.Persmission.MessageBlock': 'Text is not Allowed',
'ChannelVisibility.Confirm.MakePrivate.Channel': 'If you make this channel private, the name @%@ will be removed. Anyone else will be able to take it for their public groups or channels.',
'ChannelVisibility.Confirm.MakePrivate.Group': 'If you make this group private, the name @%@ will be removed. Anyone else will be able to take it for their public groups or channels.',
'Context.ViewStickerSet': 'View Sticker Set',

376
src/layer.d.ts vendored
View File

@ -285,7 +285,8 @@ export namespace InputChatPhoto {
flags?: number,
file?: InputFile,
video?: InputFile,
video_start_ts?: number
video_start_ts?: number,
video_emoji_markup?: VideoSize
};
export type inputChatPhoto = {
@ -699,6 +700,7 @@ export namespace ChatFull {
pFlags: Partial<{
can_set_username?: true,
has_scheduled?: true,
translations_disabled?: true,
}>,
id: string | number,
about: string,
@ -733,6 +735,7 @@ export namespace ChatFull {
can_delete_channel?: true,
antispam?: true,
participants_hidden?: true,
translations_disabled?: true,
}>,
flags2?: number,
id: string | number,
@ -1071,7 +1074,7 @@ export namespace MessageMedia {
/**
* @link https://core.telegram.org/type/MessageAction
*/
export type MessageAction = MessageAction.messageActionEmpty | MessageAction.messageActionChatCreate | MessageAction.messageActionChatEditTitle | MessageAction.messageActionChatEditPhoto | MessageAction.messageActionChatDeletePhoto | MessageAction.messageActionChatAddUser | MessageAction.messageActionChatDeleteUser | MessageAction.messageActionChatJoinedByLink | MessageAction.messageActionChannelCreate | MessageAction.messageActionChatMigrateTo | MessageAction.messageActionChannelMigrateFrom | MessageAction.messageActionPinMessage | MessageAction.messageActionHistoryClear | MessageAction.messageActionGameScore | MessageAction.messageActionPaymentSentMe | MessageAction.messageActionPaymentSent | MessageAction.messageActionPhoneCall | MessageAction.messageActionScreenshotTaken | MessageAction.messageActionCustomAction | MessageAction.messageActionBotAllowed | MessageAction.messageActionSecureValuesSentMe | MessageAction.messageActionSecureValuesSent | MessageAction.messageActionContactSignUp | MessageAction.messageActionGeoProximityReached | MessageAction.messageActionGroupCall | MessageAction.messageActionInviteToGroupCall | MessageAction.messageActionSetMessagesTTL | MessageAction.messageActionGroupCallScheduled | MessageAction.messageActionSetChatTheme | MessageAction.messageActionChatJoinedByRequest | MessageAction.messageActionWebViewDataSentMe | MessageAction.messageActionWebViewDataSent | MessageAction.messageActionGiftPremium | MessageAction.messageActionTopicCreate | MessageAction.messageActionTopicEdit | MessageAction.messageActionSuggestProfilePhoto | MessageAction.messageActionAttachMenuBotAllowed | MessageAction.messageActionDiscussionStarted | MessageAction.messageActionChatLeave | MessageAction.messageActionChannelDeletePhoto | MessageAction.messageActionChannelEditTitle | MessageAction.messageActionChannelEditPhoto | MessageAction.messageActionChannelEditVideo | MessageAction.messageActionChatEditVideo | MessageAction.messageActionChatAddUsers | MessageAction.messageActionChatJoined | MessageAction.messageActionChatReturn | MessageAction.messageActionChatJoinedYou | MessageAction.messageActionChatReturnYou;
export type MessageAction = MessageAction.messageActionEmpty | MessageAction.messageActionChatCreate | MessageAction.messageActionChatEditTitle | MessageAction.messageActionChatEditPhoto | MessageAction.messageActionChatDeletePhoto | MessageAction.messageActionChatAddUser | MessageAction.messageActionChatDeleteUser | MessageAction.messageActionChatJoinedByLink | MessageAction.messageActionChannelCreate | MessageAction.messageActionChatMigrateTo | MessageAction.messageActionChannelMigrateFrom | MessageAction.messageActionPinMessage | MessageAction.messageActionHistoryClear | MessageAction.messageActionGameScore | MessageAction.messageActionPaymentSentMe | MessageAction.messageActionPaymentSent | MessageAction.messageActionPhoneCall | MessageAction.messageActionScreenshotTaken | MessageAction.messageActionCustomAction | MessageAction.messageActionBotAllowed | MessageAction.messageActionSecureValuesSentMe | MessageAction.messageActionSecureValuesSent | MessageAction.messageActionContactSignUp | MessageAction.messageActionGeoProximityReached | MessageAction.messageActionGroupCall | MessageAction.messageActionInviteToGroupCall | MessageAction.messageActionSetMessagesTTL | MessageAction.messageActionGroupCallScheduled | MessageAction.messageActionSetChatTheme | MessageAction.messageActionChatJoinedByRequest | MessageAction.messageActionWebViewDataSentMe | MessageAction.messageActionWebViewDataSent | MessageAction.messageActionGiftPremium | MessageAction.messageActionTopicCreate | MessageAction.messageActionTopicEdit | MessageAction.messageActionSuggestProfilePhoto | MessageAction.messageActionAttachMenuBotAllowed | MessageAction.messageActionRequestedPeer | MessageAction.messageActionDiscussionStarted | MessageAction.messageActionChatLeave | MessageAction.messageActionChannelDeletePhoto | MessageAction.messageActionChannelEditTitle | MessageAction.messageActionChannelEditPhoto | MessageAction.messageActionChannelEditVideo | MessageAction.messageActionChatEditVideo | MessageAction.messageActionChatAddUsers | MessageAction.messageActionChatJoined | MessageAction.messageActionChatReturn | MessageAction.messageActionChatJoinedYou | MessageAction.messageActionChatReturnYou;
export namespace MessageAction {
export type messageActionEmpty = {
@ -1296,6 +1299,12 @@ export namespace MessageAction {
_: 'messageActionAttachMenuBotAllowed'
};
export type messageActionRequestedPeer = {
_: 'messageActionRequestedPeer',
button_id: number,
peer: Peer
};
export type messageActionDiscussionStarted = {
_: 'messageActionDiscussionStarted'
};
@ -1527,7 +1536,7 @@ export namespace GeoPoint {
/**
* @link https://core.telegram.org/type/auth.SentCode
*/
export type AuthSentCode = AuthSentCode.authSentCode;
export type AuthSentCode = AuthSentCode.authSentCode | AuthSentCode.authSentCodeSuccess;
export namespace AuthSentCode {
export type authSentCode = {
@ -1539,6 +1548,11 @@ export namespace AuthSentCode {
timeout?: number,
phone_number?: string
};
export type authSentCodeSuccess = {
_: 'auth.sentCodeSuccess',
authorization: AuthAuthorization
};
}
/**
@ -1555,6 +1569,7 @@ export namespace AuthAuthorization {
}>,
otherwise_relogin_days?: number,
tmp_sessions?: number,
future_auth_token?: Uint8Array,
user: User
};
@ -1766,6 +1781,7 @@ export namespace UserFull {
has_scheduled?: true,
video_calls_available?: true,
voice_messages_forbidden?: true,
translations_disabled?: true,
}>,
id: string | number,
about?: string,
@ -2086,7 +2102,7 @@ export namespace MessagesFilter {
/**
* @link https://core.telegram.org/type/Update
*/
export type Update = Update.updateNewMessage | Update.updateMessageID | Update.updateDeleteMessages | Update.updateUserTyping | Update.updateChatUserTyping | Update.updateChatParticipants | Update.updateUserStatus | Update.updateUserName | Update.updateNewEncryptedMessage | Update.updateEncryptedChatTyping | Update.updateEncryption | Update.updateEncryptedMessagesRead | Update.updateChatParticipantAdd | Update.updateChatParticipantDelete | Update.updateDcOptions | Update.updateNotifySettings | Update.updateServiceNotification | Update.updatePrivacy | Update.updateUserPhone | Update.updateReadHistoryInbox | Update.updateReadHistoryOutbox | Update.updateWebPage | Update.updateReadMessagesContents | Update.updateChannelTooLong | Update.updateChannel | Update.updateNewChannelMessage | Update.updateReadChannelInbox | Update.updateDeleteChannelMessages | Update.updateChannelMessageViews | Update.updateChatParticipantAdmin | Update.updateNewStickerSet | Update.updateStickerSetsOrder | Update.updateStickerSets | Update.updateSavedGifs | Update.updateBotInlineQuery | Update.updateBotInlineSend | Update.updateEditChannelMessage | Update.updateBotCallbackQuery | Update.updateEditMessage | Update.updateInlineBotCallbackQuery | Update.updateReadChannelOutbox | Update.updateDraftMessage | Update.updateReadFeaturedStickers | Update.updateRecentStickers | Update.updateConfig | Update.updatePtsChanged | Update.updateChannelWebPage | Update.updateDialogPinned | Update.updatePinnedDialogs | Update.updateBotWebhookJSON | Update.updateBotWebhookJSONQuery | Update.updateBotShippingQuery | Update.updateBotPrecheckoutQuery | Update.updatePhoneCall | Update.updateLangPackTooLong | Update.updateLangPack | Update.updateFavedStickers | Update.updateChannelReadMessagesContents | Update.updateContactsReset | Update.updateChannelAvailableMessages | Update.updateDialogUnreadMark | Update.updateMessagePoll | Update.updateChatDefaultBannedRights | Update.updateFolderPeers | Update.updatePeerSettings | Update.updatePeerLocated | Update.updateNewScheduledMessage | Update.updateDeleteScheduledMessages | Update.updateTheme | Update.updateGeoLiveViewed | Update.updateLoginToken | Update.updateMessagePollVote | Update.updateDialogFilter | Update.updateDialogFilterOrder | Update.updateDialogFilters | Update.updatePhoneCallSignalingData | Update.updateChannelMessageForwards | Update.updateReadChannelDiscussionInbox | Update.updateReadChannelDiscussionOutbox | Update.updatePeerBlocked | Update.updateChannelUserTyping | Update.updatePinnedMessages | Update.updatePinnedChannelMessages | Update.updateChat | Update.updateGroupCallParticipants | Update.updateGroupCall | Update.updatePeerHistoryTTL | Update.updateChatParticipant | Update.updateChannelParticipant | Update.updateBotStopped | Update.updateGroupCallConnection | Update.updateBotCommands | Update.updatePendingJoinRequests | Update.updateBotChatInviteRequester | Update.updateMessageReactions | Update.updateAttachMenuBots | Update.updateWebViewResultSent | Update.updateBotMenuButton | Update.updateSavedRingtones | Update.updateTranscribedAudio | Update.updateReadFeaturedEmojiStickers | Update.updateUserEmojiStatus | Update.updateRecentEmojiStatuses | Update.updateRecentReactions | Update.updateMoveStickerSetToTop | Update.updateMessageExtendedMedia | Update.updateChannelPinnedTopic | Update.updateChannelPinnedTopics | Update.updateUser | Update.updateNewDiscussionMessage | Update.updateDeleteDiscussionMessages | Update.updateChannelReload;
export type Update = Update.updateNewMessage | Update.updateMessageID | Update.updateDeleteMessages | Update.updateUserTyping | Update.updateChatUserTyping | Update.updateChatParticipants | Update.updateUserStatus | Update.updateUserName | Update.updateNewEncryptedMessage | Update.updateEncryptedChatTyping | Update.updateEncryption | Update.updateEncryptedMessagesRead | Update.updateChatParticipantAdd | Update.updateChatParticipantDelete | Update.updateDcOptions | Update.updateNotifySettings | Update.updateServiceNotification | Update.updatePrivacy | Update.updateUserPhone | Update.updateReadHistoryInbox | Update.updateReadHistoryOutbox | Update.updateWebPage | Update.updateReadMessagesContents | Update.updateChannelTooLong | Update.updateChannel | Update.updateNewChannelMessage | Update.updateReadChannelInbox | Update.updateDeleteChannelMessages | Update.updateChannelMessageViews | Update.updateChatParticipantAdmin | Update.updateNewStickerSet | Update.updateStickerSetsOrder | Update.updateStickerSets | Update.updateSavedGifs | Update.updateBotInlineQuery | Update.updateBotInlineSend | Update.updateEditChannelMessage | Update.updateBotCallbackQuery | Update.updateEditMessage | Update.updateInlineBotCallbackQuery | Update.updateReadChannelOutbox | Update.updateDraftMessage | Update.updateReadFeaturedStickers | Update.updateRecentStickers | Update.updateConfig | Update.updatePtsChanged | Update.updateChannelWebPage | Update.updateDialogPinned | Update.updatePinnedDialogs | Update.updateBotWebhookJSON | Update.updateBotWebhookJSONQuery | Update.updateBotShippingQuery | Update.updateBotPrecheckoutQuery | Update.updatePhoneCall | Update.updateLangPackTooLong | Update.updateLangPack | Update.updateFavedStickers | Update.updateChannelReadMessagesContents | Update.updateContactsReset | Update.updateChannelAvailableMessages | Update.updateDialogUnreadMark | Update.updateMessagePoll | Update.updateChatDefaultBannedRights | Update.updateFolderPeers | Update.updatePeerSettings | Update.updatePeerLocated | Update.updateNewScheduledMessage | Update.updateDeleteScheduledMessages | Update.updateTheme | Update.updateGeoLiveViewed | Update.updateLoginToken | Update.updateMessagePollVote | Update.updateDialogFilter | Update.updateDialogFilterOrder | Update.updateDialogFilters | Update.updatePhoneCallSignalingData | Update.updateChannelMessageForwards | Update.updateReadChannelDiscussionInbox | Update.updateReadChannelDiscussionOutbox | Update.updatePeerBlocked | Update.updateChannelUserTyping | Update.updatePinnedMessages | Update.updatePinnedChannelMessages | Update.updateChat | Update.updateGroupCallParticipants | Update.updateGroupCall | Update.updatePeerHistoryTTL | Update.updateChatParticipant | Update.updateChannelParticipant | Update.updateBotStopped | Update.updateGroupCallConnection | Update.updateBotCommands | Update.updatePendingJoinRequests | Update.updateBotChatInviteRequester | Update.updateMessageReactions | Update.updateAttachMenuBots | Update.updateWebViewResultSent | Update.updateBotMenuButton | Update.updateSavedRingtones | Update.updateTranscribedAudio | Update.updateReadFeaturedEmojiStickers | Update.updateUserEmojiStatus | Update.updateRecentEmojiStatuses | Update.updateRecentReactions | Update.updateMoveStickerSetToTop | Update.updateMessageExtendedMedia | Update.updateChannelPinnedTopic | Update.updateChannelPinnedTopics | Update.updateUser | Update.updateAutoSaveSettings | Update.updateNewDiscussionMessage | Update.updateDeleteDiscussionMessages | Update.updateChannelReload;
export namespace Update {
export type updateNewMessage = {
@ -2868,6 +2884,10 @@ export namespace Update {
user_id: string | number
};
export type updateAutoSaveSettings = {
_: 'updateAutoSaveSettings'
};
export type updateNewDiscussionMessage = {
_: 'updateNewDiscussionMessage',
message?: Message
@ -4325,7 +4345,7 @@ export namespace BotInfo {
/**
* @link https://core.telegram.org/type/KeyboardButton
*/
export type KeyboardButton = KeyboardButton.keyboardButton | KeyboardButton.keyboardButtonUrl | KeyboardButton.keyboardButtonCallback | KeyboardButton.keyboardButtonRequestPhone | KeyboardButton.keyboardButtonRequestGeoLocation | KeyboardButton.keyboardButtonSwitchInline | KeyboardButton.keyboardButtonGame | KeyboardButton.keyboardButtonBuy | KeyboardButton.keyboardButtonUrlAuth | KeyboardButton.inputKeyboardButtonUrlAuth | KeyboardButton.keyboardButtonRequestPoll | KeyboardButton.inputKeyboardButtonUserProfile | KeyboardButton.keyboardButtonUserProfile | KeyboardButton.keyboardButtonWebView | KeyboardButton.keyboardButtonSimpleWebView;
export type KeyboardButton = KeyboardButton.keyboardButton | KeyboardButton.keyboardButtonUrl | KeyboardButton.keyboardButtonCallback | KeyboardButton.keyboardButtonRequestPhone | KeyboardButton.keyboardButtonRequestGeoLocation | KeyboardButton.keyboardButtonSwitchInline | KeyboardButton.keyboardButtonGame | KeyboardButton.keyboardButtonBuy | KeyboardButton.keyboardButtonUrlAuth | KeyboardButton.inputKeyboardButtonUrlAuth | KeyboardButton.keyboardButtonRequestPoll | KeyboardButton.inputKeyboardButtonUserProfile | KeyboardButton.keyboardButtonUserProfile | KeyboardButton.keyboardButtonWebView | KeyboardButton.keyboardButtonSimpleWebView | KeyboardButton.keyboardButtonRequestPeer;
export namespace KeyboardButton {
export type keyboardButton = {
@ -4430,6 +4450,13 @@ export namespace KeyboardButton {
text: string,
url: string
};
export type keyboardButtonRequestPeer = {
_: 'keyboardButtonRequestPeer',
text: string,
button_id: number,
peer_type: RequestPeerType
};
}
/**
@ -5268,7 +5295,7 @@ export namespace AuthCodeType {
/**
* @link https://core.telegram.org/type/auth.SentCodeType
*/
export type AuthSentCodeType = AuthSentCodeType.authSentCodeTypeApp | AuthSentCodeType.authSentCodeTypeSms | AuthSentCodeType.authSentCodeTypeCall | AuthSentCodeType.authSentCodeTypeFlashCall | AuthSentCodeType.authSentCodeTypeMissedCall | AuthSentCodeType.authSentCodeTypeEmailCode | AuthSentCodeType.authSentCodeTypeSetUpEmailRequired | AuthSentCodeType.authSentCodeTypeFragmentSms;
export type AuthSentCodeType = AuthSentCodeType.authSentCodeTypeApp | AuthSentCodeType.authSentCodeTypeSms | AuthSentCodeType.authSentCodeTypeCall | AuthSentCodeType.authSentCodeTypeFlashCall | AuthSentCodeType.authSentCodeTypeMissedCall | AuthSentCodeType.authSentCodeTypeEmailCode | AuthSentCodeType.authSentCodeTypeSetUpEmailRequired | AuthSentCodeType.authSentCodeTypeFragmentSms | AuthSentCodeType.authSentCodeTypeFirebaseSms;
export namespace AuthSentCodeType {
export type authSentCodeTypeApp = {
@ -5323,6 +5350,15 @@ export namespace AuthSentCodeType {
url: string,
length: number
};
export type authSentCodeTypeFirebaseSms = {
_: 'auth.sentCodeTypeFirebaseSms',
flags?: number,
nonce?: Uint8Array,
receipt?: string,
push_timeout?: number,
length: number
};
}
/**
@ -8090,6 +8126,13 @@ export namespace ChatBannedRights {
invite_users?: true,
pin_messages?: true,
manage_topics?: true,
send_photos?: true,
send_videos?: true,
send_roundvideos?: true,
send_audios?: true,
send_voices?: true,
send_docs?: true,
send_plain?: true,
}>,
until_date: number
};
@ -8149,8 +8192,11 @@ export namespace CodeSettings {
current_number?: true,
allow_app_hash?: true,
allow_missed_call?: true,
allow_firebase?: true,
}>,
logout_tokens?: Array<Uint8Array>
logout_tokens?: Array<Uint8Array>,
token?: string,
app_sandbox?: boolean
};
}
@ -8857,7 +8903,7 @@ export namespace HelpPromoData {
/**
* @link https://core.telegram.org/type/VideoSize
*/
export type VideoSize = VideoSize.videoSize;
export type VideoSize = VideoSize.videoSize | VideoSize.videoSizeEmojiMarkup | VideoSize.videoSizeStickerMarkup;
export namespace VideoSize {
export type videoSize = {
@ -8869,6 +8915,19 @@ export namespace VideoSize {
size: number,
video_start_ts?: number
};
export type videoSizeEmojiMarkup = {
_: 'videoSizeEmojiMarkup',
emoji_id: string | number,
background_colors: Array<number>
};
export type videoSizeStickerMarkup = {
_: 'videoSizeStickerMarkup',
stickerset: InputStickerSet,
sticker_id: string | number,
background_colors: Array<number>
};
}
/**
@ -9798,22 +9857,6 @@ export namespace MessagesAvailableReactions {
};
}
/**
* @link https://core.telegram.org/type/messages.TranslatedText
*/
export type MessagesTranslatedText = MessagesTranslatedText.messagesTranslateNoResult | MessagesTranslatedText.messagesTranslateResultText;
export namespace MessagesTranslatedText {
export type messagesTranslateNoResult = {
_: 'messages.translateNoResult'
};
export type messagesTranslateResultText = {
_: 'messages.translateResultText',
text: string
};
}
/**
* @link https://core.telegram.org/type/MessagePeerReaction
*/
@ -10171,6 +10214,7 @@ export namespace InputStorePaymentPurpose {
flags?: number,
pFlags: Partial<{
restore?: true,
upgrade?: true,
}>
};
@ -10212,6 +10256,19 @@ export namespace PaymentFormMethod {
};
}
/**
* @link https://core.telegram.org/type/bots.PremiumGiftsOptions
*/
export type BotsPremiumGiftsOptions = BotsPremiumGiftsOptions.botsPremiumGiftsOptions;
export namespace BotsPremiumGiftsOptions {
export type botsPremiumGiftsOptions = {
_: 'bots.premiumGiftsOptions',
name: string,
gifts: Array<PremiumGiftOption>
};
}
/**
* @link https://core.telegram.org/type/EmojiStatus
*/
@ -10387,6 +10444,7 @@ export namespace PremiumSubscriptionOption {
current?: true,
can_purchase_upgrade?: true,
}>,
transaction?: string,
months: number,
currency: string,
amount: string | number,
@ -10549,6 +10607,164 @@ export namespace ExportedContactToken {
};
}
/**
* @link https://core.telegram.org/type/RequestPeerType
*/
export type RequestPeerType = RequestPeerType.requestPeerTypeUser | RequestPeerType.requestPeerTypeChat | RequestPeerType.requestPeerTypeBroadcast;
export namespace RequestPeerType {
export type requestPeerTypeUser = {
_: 'requestPeerTypeUser',
flags?: number,
bot?: boolean,
premium?: boolean
};
export type requestPeerTypeChat = {
_: 'requestPeerTypeChat',
flags?: number,
pFlags: Partial<{
creator?: true,
bot_participant?: true,
}>,
has_username?: boolean,
forum?: boolean,
user_admin_rights?: ChatAdminRights,
bot_admin_rights?: ChatAdminRights
};
export type requestPeerTypeBroadcast = {
_: 'requestPeerTypeBroadcast',
flags?: number,
pFlags: Partial<{
creator?: true,
}>,
has_username?: boolean,
user_admin_rights?: ChatAdminRights,
bot_admin_rights?: ChatAdminRights
};
}
/**
* @link https://core.telegram.org/type/EmojiList
*/
export type EmojiList = EmojiList.emojiListNotModified | EmojiList.emojiList;
export namespace EmojiList {
export type emojiListNotModified = {
_: 'emojiListNotModified'
};
export type emojiList = {
_: 'emojiList',
hash: string | number,
document_id: Array<string | number>
};
}
/**
* @link https://core.telegram.org/type/EmojiGroup
*/
export type EmojiGroup = EmojiGroup.emojiGroup;
export namespace EmojiGroup {
export type emojiGroup = {
_: 'emojiGroup',
title: string,
icon_emoji_id: string | number,
emoticons: Array<string>
};
}
/**
* @link https://core.telegram.org/type/messages.EmojiGroups
*/
export type MessagesEmojiGroups = MessagesEmojiGroups.messagesEmojiGroupsNotModified | MessagesEmojiGroups.messagesEmojiGroups;
export namespace MessagesEmojiGroups {
export type messagesEmojiGroupsNotModified = {
_: 'messages.emojiGroupsNotModified'
};
export type messagesEmojiGroups = {
_: 'messages.emojiGroups',
hash: number,
groups: Array<EmojiGroup>
};
}
/**
* @link https://core.telegram.org/type/TextWithEntities
*/
export type TextWithEntities = TextWithEntities.textWithEntities;
export namespace TextWithEntities {
export type textWithEntities = {
_: 'textWithEntities',
text: string,
entities: Array<MessageEntity>
};
}
/**
* @link https://core.telegram.org/type/messages.TranslatedText
*/
export type MessagesTranslatedText = MessagesTranslatedText.messagesTranslateResult;
export namespace MessagesTranslatedText {
export type messagesTranslateResult = {
_: 'messages.translateResult',
result: Array<TextWithEntities>
};
}
/**
* @link https://core.telegram.org/type/AutoSaveSettings
*/
export type AutoSaveSettings = AutoSaveSettings.autoSaveSettings;
export namespace AutoSaveSettings {
export type autoSaveSettings = {
_: 'autoSaveSettings',
flags?: number,
pFlags: Partial<{
photos?: true,
videos?: true,
}>,
video_max_size?: string | number
};
}
/**
* @link https://core.telegram.org/type/AutoSaveException
*/
export type AutoSaveException = AutoSaveException.autoSaveException;
export namespace AutoSaveException {
export type autoSaveException = {
_: 'autoSaveException',
peer: Peer,
settings: AutoSaveSettings
};
}
/**
* @link https://core.telegram.org/type/account.AutoSaveSettings
*/
export type AccountAutoSaveSettings = AccountAutoSaveSettings.accountAutoSaveSettings;
export namespace AccountAutoSaveSettings {
export type accountAutoSaveSettings = {
_: 'account.autoSaveSettings',
users_settings: AutoSaveSettings,
chats_settings: AutoSaveSettings,
broadcasts_settings: AutoSaveSettings,
exceptions: Array<AutoSaveException>,
chats: Array<Chat>,
users: Array<User>
};
}
export interface ConstructorDeclMap {
'error': Error.error,
'inputPeerEmpty': InputPeer.inputPeerEmpty,
@ -11495,8 +11711,6 @@ export interface ConstructorDeclMap {
'messages.availableReactions': MessagesAvailableReactions.messagesAvailableReactions,
'messageEntitySpoiler': MessageEntity.messageEntitySpoiler,
'channelAdminLogEventActionChangeAvailableReactions': ChannelAdminLogEventAction.channelAdminLogEventActionChangeAvailableReactions,
'messages.translateNoResult': MessagesTranslatedText.messagesTranslateNoResult,
'messages.translateResultText': MessagesTranslatedText.messagesTranslateResultText,
'messagePeerReaction': MessagePeerReaction.messagePeerReaction,
'groupCallStreamChannel': GroupCallStreamChannel.groupCallStreamChannel,
'phone.groupCallStreamChannels': PhoneGroupCallStreamChannels.phoneGroupCallStreamChannels,
@ -11557,6 +11771,7 @@ export interface ConstructorDeclMap {
'privacyKeyVoiceMessages': PrivacyKey.privacyKeyVoiceMessages,
'paymentFormMethod': PaymentFormMethod.paymentFormMethod,
'inputWebFileAudioAlbumThumbLocation': InputWebFileLocation.inputWebFileAudioAlbumThumbLocation,
'bots.premiumGiftsOptions': BotsPremiumGiftsOptions.botsPremiumGiftsOptions,
'emojiStatusEmpty': EmojiStatus.emojiStatusEmpty,
'emojiStatus': EmojiStatus.emojiStatus,
'emojiStatusUntil': EmojiStatus.emojiStatusUntil,
@ -11619,6 +11834,26 @@ export interface ConstructorDeclMap {
'messageActionAttachMenuBotAllowed': MessageAction.messageActionAttachMenuBotAllowed,
'stickerSetNoCovered': StickerSetCovered.stickerSetNoCovered,
'updateUser': Update.updateUser,
'auth.sentCodeSuccess': AuthSentCode.authSentCodeSuccess,
'messageActionRequestedPeer': MessageAction.messageActionRequestedPeer,
'requestPeerTypeUser': RequestPeerType.requestPeerTypeUser,
'requestPeerTypeChat': RequestPeerType.requestPeerTypeChat,
'requestPeerTypeBroadcast': RequestPeerType.requestPeerTypeBroadcast,
'keyboardButtonRequestPeer': KeyboardButton.keyboardButtonRequestPeer,
'emojiListNotModified': EmojiList.emojiListNotModified,
'emojiList': EmojiList.emojiList,
'auth.sentCodeTypeFirebaseSms': AuthSentCodeType.authSentCodeTypeFirebaseSms,
'emojiGroup': EmojiGroup.emojiGroup,
'messages.emojiGroupsNotModified': MessagesEmojiGroups.messagesEmojiGroupsNotModified,
'messages.emojiGroups': MessagesEmojiGroups.messagesEmojiGroups,
'videoSizeEmojiMarkup': VideoSize.videoSizeEmojiMarkup,
'videoSizeStickerMarkup': VideoSize.videoSizeStickerMarkup,
'textWithEntities': TextWithEntities.textWithEntities,
'messages.translateResult': MessagesTranslatedText.messagesTranslateResult,
'autoSaveSettings': AutoSaveSettings.autoSaveSettings,
'autoSaveException': AutoSaveException.autoSaveException,
'account.autoSaveSettings': AccountAutoSaveSettings.accountAutoSaveSettings,
'updateAutoSaveSettings': Update.updateAutoSaveSettings,
'messageEntityEmoji': MessageEntity.messageEntityEmoji,
'messageEntityHighlight': MessageEntity.messageEntityHighlight,
'messageEntityLinebreak': MessageEntity.messageEntityLinebreak,
@ -11998,7 +12233,8 @@ export type PhotosUploadProfilePhoto = {
fallback?: boolean,
file?: InputFile,
video?: InputFile,
video_start_ts?: number
video_start_ts?: number,
video_emoji_markup?: VideoSize
};
export type PhotosDeletePhotos = {
@ -12353,6 +12589,7 @@ export type ChannelsCreateChannel = {
broadcast?: boolean,
megagroup?: boolean,
for_import?: boolean,
forum?: boolean,
title: string,
about: string,
geo_point?: InputGeoPoint,
@ -12807,6 +13044,8 @@ export type StickersCreateStickerSet = {
masks?: boolean,
animated?: boolean,
videos?: boolean,
emojis?: boolean,
text_color?: boolean,
user_id: InputUser,
title: string,
short_name: string,
@ -14034,9 +14273,8 @@ export type MessagesSetDefaultReaction = {
export type MessagesTranslateText = {
flags?: number,
peer?: InputPeer,
msg_id?: number,
text?: string,
from_lang?: string,
id?: Array<number>,
text?: Array<TextWithEntities>,
to_lang: string
};
@ -14392,7 +14630,8 @@ export type PhotosUploadContactProfilePhoto = {
user_id: InputUser,
file?: InputFile,
video?: InputFile,
video_start_ts?: number
video_start_ts?: number,
video_emoji_markup?: VideoSize
};
export type ChannelsToggleParticipantsHidden = {
@ -14400,6 +14639,69 @@ export type ChannelsToggleParticipantsHidden = {
enabled: boolean
};
export type MessagesSendBotRequestedPeer = {
peer: InputPeer,
msg_id: number,
button_id: number,
requested_peer: InputPeer
};
export type AccountGetDefaultProfilePhotoEmojis = {
hash: string | number
};
export type AccountGetDefaultGroupPhotoEmojis = {
hash: string | number
};
export type AuthRequestFirebaseSms = {
flags?: number,
phone_number: string,
phone_code_hash: string,
safety_net_token?: string,
ios_push_secret?: string
};
export type MessagesGetEmojiGroups = {
hash: number
};
export type MessagesGetEmojiStatusGroups = {
hash: number
};
export type MessagesGetEmojiProfilePhotoGroups = {
hash: number
};
export type MessagesSearchCustomEmoji = {
emoticon: string,
hash: string | number
};
export type MessagesTogglePeerTranslations = {
flags?: number,
disabled?: boolean,
peer: InputPeer
};
export type AccountGetAutoSaveSettings = {
};
export type AccountSaveAutoSaveSettings = {
flags?: number,
users?: boolean,
chats?: boolean,
broadcasts?: boolean,
peer?: InputPeer,
settings: AutoSaveSettings
};
export type AccountDeleteAutoSaveExceptions = {
};
export interface MethodDeclMap {
'invokeAfterMsg': {req: InvokeAfterMsg, res: any},
'invokeAfterMsgs': {req: InvokeAfterMsgs, res: any},
@ -14879,5 +15181,17 @@ export interface MethodDeclMap {
'contacts.importContactToken': {req: ContactsImportContactToken, res: User},
'photos.uploadContactProfilePhoto': {req: PhotosUploadContactProfilePhoto, res: PhotosPhoto},
'channels.toggleParticipantsHidden': {req: ChannelsToggleParticipantsHidden, res: Updates},
'messages.sendBotRequestedPeer': {req: MessagesSendBotRequestedPeer, res: Updates},
'account.getDefaultProfilePhotoEmojis': {req: AccountGetDefaultProfilePhotoEmojis, res: EmojiList},
'account.getDefaultGroupPhotoEmojis': {req: AccountGetDefaultGroupPhotoEmojis, res: EmojiList},
'auth.requestFirebaseSms': {req: AuthRequestFirebaseSms, res: boolean},
'messages.getEmojiGroups': {req: MessagesGetEmojiGroups, res: MessagesEmojiGroups},
'messages.getEmojiStatusGroups': {req: MessagesGetEmojiStatusGroups, res: MessagesEmojiGroups},
'messages.getEmojiProfilePhotoGroups': {req: MessagesGetEmojiProfilePhotoGroups, res: MessagesEmojiGroups},
'messages.searchCustomEmoji': {req: MessagesSearchCustomEmoji, res: EmojiList},
'messages.togglePeerTranslations': {req: MessagesTogglePeerTranslations, res: boolean},
'account.getAutoSaveSettings': {req: AccountGetAutoSaveSettings, res: AccountAutoSaveSettings},
'account.saveAutoSaveSettings': {req: AccountSaveAutoSaveSettings, res: boolean},
'account.deleteAutoSaveExceptions': {req: AccountDeleteAutoSaveExceptions, res: boolean},
}

View File

@ -20,6 +20,7 @@ import parseMarkdown from '../richTextProcessor/parseMarkdown';
import ctx from '../../environment/ctx';
import EventListenerBase from '../../helpers/eventListenerBase';
import applyMixins from '../../helpers/applyMixins';
import tsNow from '../../helpers/tsNow';
type UpdatesState = {
pendingPtsUpdates: (Update & {pts: number, pts_count: number})[],
@ -311,7 +312,7 @@ class ApiUpdatesManager {
updatesState.date = nextState.date;
} else {
updatesState.pts = differenceResult.pts;
updatesState.date = (Date.now() / 1000 | 0) + this.timeManager.getServerTimeOffset();
updatesState.date = tsNow(true) + this.timeManager.getServerTimeOffset();
delete updatesState.seq;
this.channelStates = {};
@ -740,7 +741,7 @@ class ApiUpdatesManager {
message,
type: 'local',
pFlags: {},
inbox_date: Date.now() / 1000 | 0,
inbox_date: tsNow(true),
media: undefined
};

View File

@ -12,7 +12,7 @@
import deepEqual from '../../helpers/object/deepEqual';
import isObject from '../../helpers/object/isObject';
import safeReplaceObject from '../../helpers/object/safeReplaceObject';
import {ChannelParticipant, ChannelsCreateChannel, Chat, ChatAdminRights, ChatBannedRights, ChatInvite, ChatPhoto, ChatReactions, InputChannel, InputChatPhoto, InputFile, InputPeer, MessagesSponsoredMessages, SponsoredMessage, Update, Updates} from '../../layer';
import {ChannelParticipant, ChannelsCreateChannel, Chat, ChatAdminRights, ChatBannedRights, ChatInvite, ChatParticipant, ChatPhoto, ChatReactions, InputChannel, InputChatPhoto, InputFile, InputPeer, MessagesSponsoredMessages, SponsoredMessage, Update, Updates} from '../../layer';
import {isRestricted} from '../../helpers/restrictions';
import {AppManager} from './manager';
import hasRights from './utils/chats/hasRights';
@ -21,6 +21,7 @@ import {AppStoragesManager} from './appStoragesManager';
import getServerMessageId from './utils/messageId/getServerMessageId';
import {randomLong} from '../../helpers/random';
import generateMessageId from './utils/messageId/generateMessageId';
import tsNow from '../../helpers/tsNow';
export type Channel = Chat.channel;
export type ChatRights = keyof ChatBannedRights['pFlags'] | keyof ChatAdminRights['pFlags'] | 'change_type' | 'change_permissions' | 'delete_chat' | 'view_participants';
@ -37,26 +38,9 @@ export class AppChatsManager extends AppManager {
this.clear(true);
this.apiUpdatesManager.addMultipleEventsListeners({
/* updateChannel: (update) => {
const channelId = update.channel_id;
//console.log('updateChannel:', update);
rootScope.broadcast('channel_settings', {channelId});
}, */
updateChannelParticipant: this.onUpdateChannelParticipant,
updateChannelParticipant: (update) => {
this.apiManager.clearCache('channels.getParticipants', (params) => {
return (params.channel as InputChannel.inputChannel).channel_id === update.channel_id;
});
},
updateChatDefaultBannedRights: (update) => {
const chatId = this.appPeersManager.getPeerId(update.peer).toChatId();
const chat = this.chats[chatId] as Chat.chat;
if(chat) {
chat.default_banned_rights = update.default_banned_rights;
this.rootScope.dispatchEvent('chat_update', chatId);
}
}
updateChatDefaultBannedRights: this.onUpdateChatDefaultBannedRights
});
return Promise.all([
@ -535,8 +519,14 @@ export class AppChatsManager extends AppManager {
});
}
public editBanned(id: ChatId, participant: PeerId | ChannelParticipant, banned_rights: ChatBannedRights) {
public async editBanned(id: ChatId, participant: PeerId | ChannelParticipant | ChatParticipant, banned_rights: ChatBannedRights) {
const peerId = typeof(participant) !== 'object' ? participant : getParticipantPeerId(participant);
const wasChannel = this.isChannel(id);
if(!wasChannel) {
const channelId = await this.migrateChat(id);
id = channelId;
}
return this.apiManager.invokeApi('channels.editBanned', {
channel: this.getChannelInput(id),
participant: this.appPeersManager.getInputPeerById(peerId),
@ -545,7 +535,7 @@ export class AppChatsManager extends AppManager {
this.onChatUpdated(id, updates);
if(typeof(participant) === 'object') {
const timestamp = Date.now() / 1000 | 0;
const timestamp = tsNow(true);
this.apiUpdatesManager.processLocalUpdate({
_: 'updateChannelParticipant',
channel_id: id,
@ -553,7 +543,7 @@ export class AppChatsManager extends AppManager {
actor_id: undefined,
qts: undefined,
user_id: peerId,
prev_participant: participant,
prev_participant: wasChannel ? participant as ChannelParticipant : undefined,
new_participant: Object.keys(banned_rights.pFlags).length ? {
_: 'channelParticipantBanned',
date: timestamp,
@ -585,9 +575,9 @@ export class AppChatsManager extends AppManager {
});
}
public kickFromChat(id: ChatId, participant: PeerId | ChannelParticipant) {
if(this.isChannel(id)) return this.kickFromChannel(id, participant);
else return this.deleteChatUser(id, (participant as PeerId).toUserId());
public kickFromChat(id: ChatId, participant: PeerId | ChannelParticipant | ChatParticipant) {
if(this.isChannel(id)) return this.kickFromChannel(id, participant as ChannelParticipant);
else return this.deleteChatUser(id, isObject(participant) ? getParticipantPeerId(participant) : (participant as PeerId).toUserId());
}
public resolveChannel(id: ChatId) {
@ -904,4 +894,21 @@ export class AppChatsManager extends AppManager {
pinned
}).then(this.onChatUpdated.bind(this, chatId));
}
private onUpdateChannelParticipant = (update: Update.updateChannelParticipant) => {
this.apiManager.clearCache('channels.getParticipants', (params) => {
return (params.channel as InputChannel.inputChannel).channel_id === update.channel_id;
});
this.rootScope.dispatchEvent('chat_participant', update);
};
private onUpdateChatDefaultBannedRights = (update: Update.updateChatDefaultBannedRights) => {
const chatId = this.appPeersManager.getPeerId(update.peer).toChatId();
const chat = this.chats[chatId] as Chat.chat;
if(chat) {
chat.default_banned_rights = update.default_banned_rights;
this.rootScope.dispatchEvent('chat_update', chatId);
}
};
}

View File

@ -221,7 +221,7 @@ type DialogElementOptions = {
wrapOptions?: WrapSomethingOptions,
isMainList?: boolean
};
class DialogElement extends Row {
export class DialogElement extends Row {
public dom: DialogDom;
constructor({
@ -2748,7 +2748,7 @@ export class AppDialogsManager {
let mediaContainer: HTMLElement;
const willPrepend: (Promise<any> | HTMLElement)[] = [];
if(lastMessage && !draftMessage && !isRestricted) {
const media: MyDocument | MyPhoto = getMediaFromMessage(lastMessage);
const media = getMediaFromMessage(lastMessage, true);
const videoTypes: Set<MyDocument['type']> = new Set(['video', 'gif', 'round']);
if(media && (media._ === 'photo' || videoTypes.has(media.type))) {
const size = choosePhotoSize(media, 20, 20);

View File

@ -26,6 +26,7 @@ import getDocumentURL from './utils/docs/getDocumentURL';
import makeError from '../../helpers/makeError';
import {EXTENSION_MIME_TYPE_MAP} from '../../environment/mimeTypeMap';
import {THUMB_TYPE_FULL} from '../mtproto/mtproto_config';
import tsNow from '../../helpers/tsNow';
export type MyDocument = Document.document;
@ -331,7 +332,7 @@ export class AppDocsManager extends AppManager {
id,
mime_type: file.type as MTMimeType,
size: file.size,
date: Date.now() / 1000,
date: tsNow(true),
pFlags: {},
thumbs: [thumb],
file_name: file.name

View File

@ -239,7 +239,7 @@ export class AppDraftsManager extends AppManager {
public setDraft(peerId: PeerId, threadId: number, message: string, entities?: MessageEntity[]) {
const draft: DraftMessage.draftMessage = {
_: 'draftMessage',
date: Date.now() / 1000 | 0,
date: tsNow(true),
message,
pFlags: {},
entities

View File

@ -45,6 +45,7 @@ import debounce from '../../helpers/schedulers/debounce';
import pause from '../../helpers/schedulers/pause';
import {InternalLink, InternalLinkTypeMap, INTERNAL_LINK_TYPE} from './internalLink';
import MEDIA_MIME_TYPES_SUPPORTED from '../../environment/mediaMimeTypesSupport';
import IMAGE_MIME_TYPES_SUPPORTED from '../../environment/imageMimeTypesSupport';
import {NULL_PEER_ID} from '../mtproto/mtproto_config';
import telegramMeWebManager from '../mtproto/telegramMeWebManager';
import {ONE_DAY} from '../../helpers/date';
@ -102,6 +103,8 @@ import findUpTag from '../../helpers/dom/findUpTag';
import {MTAppConfig} from '../mtproto/appConfig';
import PopupForward from '../../components/popups/forward';
import AppBackgroundTab from '../../components/sidebarLeft/tabs/background';
import partition from '../../helpers/array/partition';
import indexOfAndSplice from '../../helpers/array/indexOfAndSplice';
export type ChatSavedPosition = {
mids: number[],
@ -1728,24 +1731,43 @@ export class AppImManager extends EventListenerBase<{
return;
}
const rights = await PopupNewMedia.canSend(this.chat.peerId, true);
const _dropsContainer = newMediaPopup ? mediaDropsContainer : dropsContainer;
const _drops = newMediaPopup ? mediaDrops : drops;
if(mount && !_drops.length) {
const force = isFiles && !types.length; // * can't get file items not from 'drop' on Safari
const foundMedia = types.filter((t) => MEDIA_MIME_TYPES_SUPPORTED.has(t)).length;
// const foundDocuments = types.length - foundMedia;
const [foundMedia, foundDocuments] = partition(types, (t) => MEDIA_MIME_TYPES_SUPPORTED.has(t));
const [foundPhotos, foundVideos] = partition(foundMedia, (t) => IMAGE_MIME_TYPES_SUPPORTED.has(t));
this.log('drag files', types);
if(!rights.send_docs) {
foundDocuments.length = 0;
} else {
foundDocuments.push(...foundMedia);
}
if(!rights.send_photos) {
foundPhotos.forEach((mimeType) => indexOfAndSplice(foundMedia, mimeType));
foundPhotos.length = 0;
}
if(!rights.send_videos) {
foundVideos.forEach((mimeType) => indexOfAndSplice(foundMedia, mimeType));
foundVideos.length = 0;
}
this.log('drag files', types, foundMedia, foundDocuments, foundPhotos, foundVideos);
if(newMediaPopup) {
newMediaPopup.appendDrops(_dropsContainer);
if(types.length || force) {
const length = (rights.send_docs ? [foundDocuments] : [foundPhotos, foundVideos]).reduce((acc, v) => acc + v.length, 0);
if(length || force) {
_drops.push(new ChatDragAndDrop(_dropsContainer, {
header: 'Preview.Dragging.AddItems',
headerArgs: [types.length],
headerArgs: [length],
onDrop: (e: DragEvent) => {
toggle(e, false);
this.log('drop', e);
@ -1754,7 +1776,7 @@ export class AppImManager extends EventListenerBase<{
}));
}
} else {
if(types.length || force) {
if(foundDocuments.length || force) {
_drops.push(new ChatDragAndDrop(_dropsContainer, {
icon: 'dragfiles',
header: 'Chat.DropTitle',
@ -1767,8 +1789,7 @@ export class AppImManager extends EventListenerBase<{
}));
}
// if((foundMedia && !foundDocuments) || force) {
if(foundMedia || force) {
if(foundMedia.length || force) {
_drops.push(new ChatDragAndDrop(_dropsContainer, {
icon: 'dragmedia',
header: 'Chat.DropTitle',

View File

@ -524,7 +524,7 @@ export class AppMessagesManager extends AppManager {
});
}
public transcribeAudio(message: Message.message): Promise<MessagesTranscribedAudio> {
public async transcribeAudio(message: Message.message): Promise<MessagesTranscribedAudio> {
const {id, peerId} = message;
const process = (result: MessagesTranscribedAudio) => {
@ -3916,16 +3916,12 @@ export class AppMessagesManager extends AppManager {
});
}
public async deleteMessages(peerId: PeerId, mids: number[], revoke?: boolean) {
public async deleteMessages(peerId: PeerId, mids: number[], revoke?: boolean, isRecursion?: boolean) {
let promise: Promise<any>;
const config = await this.apiManager.getConfig();
const overflowMids = mids.splice(config.forwarded_count_max, mids.length - config.forwarded_count_max);
const localMessageIds = mids.map((mid) => getServerMessageId(mid));
if(peerId.isAnyChat() && this.appPeersManager.isChannel(peerId)) {
const channelId = peerId.toChatId();
const isChannel = this.appPeersManager.isChannel(peerId);
const channelId = isChannel && peerId.toChatId();
if(isChannel && !isRecursion) {
const channel = this.appChatsManager.getChat(channelId) as Chat.channel;
if(!channel.pFlags.creator && !channel.admin_rights?.pFlags?.delete_messages) {
mids = mids.filter((mid) => {
@ -3937,10 +3933,21 @@ export class AppMessagesManager extends AppManager {
return;
}
}
}
const config = await this.apiManager.getConfig();
const overflowMids = mids.splice(config.forwarded_count_max, mids.length - config.forwarded_count_max);
const serverMessageIds = mids.map((mid) => {
const messageId = getServerMessageId(mid);
// filter outgoing messages
return generateMessageId(messageId) === mid && messageId;
}).filter(Boolean);
if(isChannel) {
promise = this.apiManager.invokeApi('channels.deleteMessages', {
channel: this.appChatsManager.getChannelInput(channelId),
id: localMessageIds
id: serverMessageIds
}).then((affectedMessages) => {
this.apiUpdatesManager.processLocalUpdate({
_: 'updateDeleteChannelMessages',
@ -3953,7 +3960,7 @@ export class AppMessagesManager extends AppManager {
} else {
promise = this.apiManager.invokeApi('messages.deleteMessages', {
revoke,
id: localMessageIds
id: serverMessageIds
}).then((affectedMessages) => {
this.apiUpdatesManager.processLocalUpdate({
_: 'updateDeleteMessages',
@ -3966,7 +3973,7 @@ export class AppMessagesManager extends AppManager {
const promises: (typeof promise)[] = [promise];
if(overflowMids.length) {
promises.push(this.deleteMessages(peerId, overflowMids, revoke));
promises.push(this.deleteMessages(peerId, overflowMids, revoke, true));
}
return Promise.all(promises).then(noop);

View File

@ -20,6 +20,8 @@ import getParticipantPeerId from './utils/chats/getParticipantPeerId';
import ctx from '../../environment/ctx';
import {ReferenceContext} from '../mtproto/referenceDatabase';
import generateMessageId from './utils/messageId/generateMessageId';
import assumeType from '../../helpers/assumeType';
import makeError from '../../helpers/makeError';
export type UserTyping = Partial<{userId: UserId, action: SendMessageAction, timeout: number}>;
@ -34,56 +36,11 @@ export class AppProfileManager extends AppManager {
protected after() {
this.apiUpdatesManager.addMultipleEventsListeners({
updateChatParticipants: (update) => {
const participants = update.participants;
if(participants._ === 'chatParticipants') {
const chatId = participants.chat_id;
const chatFull = this.chatsFull[chatId] as ChatFull.chatFull;
if(chatFull !== undefined) {
chatFull.participants = participants;
this.rootScope.dispatchEvent('chat_full_update', chatId);
}
}
},
updateChatParticipants: this.onUpdateChatParticipants,
updateChatParticipantAdd: (update) => {
const chatFull = this.chatsFull[update.chat_id] as ChatFull.chatFull;
if(chatFull !== undefined) {
const _participants = chatFull.participants as ChatParticipants.chatParticipants;
const participants = _participants.participants || [];
for(let i = 0, length = participants.length; i < length; i++) {
if(participants[i].user_id === update.user_id) {
return;
}
}
updateChatParticipantAdd: this.onUpdateChatParticipantAdd,
participants.push({
_: 'chatParticipant',
user_id: update.user_id,
inviter_id: update.inviter_id,
date: tsNow(true)
});
_participants.version = update.version;
this.rootScope.dispatchEvent('chat_full_update', update.chat_id);
}
},
updateChatParticipantDelete: (update) => {
const chatFull = this.chatsFull[update.chat_id] as ChatFull.chatFull;
if(chatFull !== undefined) {
const _participants = chatFull.participants as ChatParticipants.chatParticipants;
const participants = _participants.participants || [];
for(let i = 0, length = participants.length; i < length; i++) {
if(participants[i].user_id === update.user_id) {
participants.splice(i, 1);
_participants.version = update.version;
this.rootScope.dispatchEvent('chat_full_update', update.chat_id);
return;
}
}
}
},
updateChatParticipantDelete: this.onUpdateChatParticipantDelete,
updateUserTyping: this.onUpdateUserTyping,
updateChatUserTyping: this.onUpdateUserTyping,
@ -332,7 +289,68 @@ export class AppProfileManager extends AppManager {
});
}
public getChannelParticipants(id: ChatId, filter: ChannelParticipantsFilter = {_: 'channelParticipantsRecent'}, limit = 200, offset = 0) {
public getParticipants(
id: ChatId,
filter: ChannelParticipantsFilter = {_: 'channelParticipantsRecent'},
limit = 200,
offset = 0
) {
if(this.appChatsManager.isChannel(id)) {
return this.getChannelParticipants(id, filter, limit, offset);
}
return Promise.resolve(this.getChatFull(id)).then((chatFull) => {
const chatParticipants = (chatFull as ChatFull.chatFull).participants;
if(chatParticipants._ !== 'chatParticipants') {
throw makeError('CHAT_PRIVATE');
}
if(filter._ === 'channelParticipantsSearch' && filter.q.trim()) {
const index = this.appUsersManager.createSearchIndex();
chatParticipants.participants.forEach((chatParticipant) => {
const userId = chatParticipant.user_id;
index.indexObject(userId, this.appUsersManager.getUserSearchText(userId));
});
const found = index.search(filter.q);
const filteredParticipants = chatParticipants.participants.filter((chatParticipant) => {
return found.has(chatParticipant.user_id);
});
return {...chatParticipants, participants: filteredParticipants};
}
return chatParticipants;
});
}
public getParticipant(id: ChatId, peerId: PeerId) {
if(this.appChatsManager.isChannel(id)) {
return this.getChannelParticipant(id, peerId);
}
return this.getParticipants(id).then((chatParticipants) => {
assumeType<ChatParticipants.chatParticipants>(chatParticipants);
const found = chatParticipants.participants.find((chatParticipant) => {
if(getParticipantPeerId(chatParticipant) === peerId) {
return chatParticipant;
}
});
if(!found) {
throw makeError('USER_NOT_PARTICIPANT');
}
return found;
});
}
public getChannelParticipants(
id: ChatId,
filter: ChannelParticipantsFilter = {_: 'channelParticipantsRecent'},
limit = 200,
offset = 0
) {
if(filter._ === 'channelParticipantsRecent') {
const chat = this.appChatsManager.getChat(id);
if(chat?.pFlags && (
@ -647,6 +665,67 @@ export class AppProfileManager extends AppManager {
return peerId + (threadId ? `_${threadId}` : '');
}
public getPeerTypings(peerId: PeerId, threadId?: number) {
return this.typingsInPeer[this.getTypingsKey(peerId, threadId)];
}
private onUpdateChatParticipants = (update: Update.updateChatParticipants) => {
const participants = update.participants;
if(participants._ !== 'chatParticipants') {
return;
}
const chatId = participants.chat_id;
const chatFull = this.chatsFull[chatId] as ChatFull.chatFull;
if(chatFull !== undefined) {
chatFull.participants = participants;
this.rootScope.dispatchEvent('chat_full_update', chatId);
}
};
private onUpdateChatParticipantAdd = (update: Update.updateChatParticipantAdd) => {
const chatFull = this.chatsFull[update.chat_id] as ChatFull.chatFull;
if(chatFull === undefined) {
return;
}
const _participants = chatFull.participants as ChatParticipants.chatParticipants;
const participants = _participants.participants || [];
for(let i = 0, length = participants.length; i < length; i++) {
if(participants[i].user_id === update.user_id) {
return;
}
}
participants.push({
_: 'chatParticipant',
user_id: update.user_id,
inviter_id: update.inviter_id,
date: tsNow(true)
});
_participants.version = update.version;
this.rootScope.dispatchEvent('chat_full_update', update.chat_id);
};
private onUpdateChatParticipantDelete = (update: Update.updateChatParticipantDelete) => {
const chatFull = this.chatsFull[update.chat_id] as ChatFull.chatFull;
if(chatFull === undefined) {
return;
}
const _participants = chatFull.participants as ChatParticipants.chatParticipants;
const participants = _participants.participants || [];
for(let i = 0, length = participants.length; i < length; i++) {
if(participants[i].user_id === update.user_id) {
participants.splice(i, 1);
_participants.version = update.version;
this.rootScope.dispatchEvent('chat_full_update', update.chat_id);
return;
}
}
};
private onUpdateUserTyping = (update: Update.updateUserTyping | Update.updateChatUserTyping | Update.updateChannelUserTyping) => {
const fromId = (update as Update.updateUserTyping).user_id ?
(update as Update.updateUserTyping).user_id.toPeerId() :
@ -741,8 +820,4 @@ export class AppProfileManager extends AppManager {
this.rootScope.dispatchEvent('peer_block', {peerId, blocked: update.blocked});
};
public getPeerTypings(peerId: PeerId, threadId?: number) {
return this.typingsInPeer[this.getTypingsKey(peerId, threadId)];
}
}

View File

@ -6,7 +6,7 @@
import type {MyDocument} from './appDocsManager';
import type {DownloadOptions} from '../mtproto/apiFileManager';
import {Document, InputFileLocation, InputStickerSet, MessagesAllStickers, MessagesFavedStickers, MessagesFeaturedStickers, MessagesFoundStickerSets, MessagesRecentStickers, MessagesStickers, MessagesStickerSet, PhotoSize, StickerPack, StickerSet, StickerSetCovered, Update} from '../../layer';
import {Document, InputFileLocation, InputStickerSet, MessagesAllStickers, MessagesFavedStickers, MessagesFeaturedStickers, MessagesFoundStickerSets, MessagesRecentStickers, MessagesStickers, MessagesStickerSet, PhotoSize, StickerPack, StickerSet, StickerSetCovered, Update, VideoSize} from '../../layer';
import {Modify} from '../../types';
import AppStorage from '../storage';
import DATABASE_STATE from '../../config/databases/state';
@ -20,6 +20,7 @@ import ctx from '../../environment/ctx';
import {getEnvironment} from '../../environment/utils';
import getDocumentInput from './utils/docs/getDocumentInput';
import getStickerEffectThumb from './utils/stickers/getStickerEffectThumb';
import tsNow from '../../helpers/tsNow';
const CACHE_TIME = 3600e3;
@ -406,7 +407,7 @@ export class AppStickersManager extends AppManager {
public preloadSticker(docId: DocId, isPremiumEffect?: boolean) {
const doc = this.appDocsManager.getDoc(docId);
return this.apiFileManager.downloadMedia({media: doc, thumb: isPremiumEffect ? doc.video_thumbs?.[0] : undefined});
return this.apiFileManager.downloadMedia({media: doc, thumb: isPremiumEffect ? doc.video_thumbs?.[0] as Extract<VideoSize, VideoSize.videoSize> : undefined});
}
private saveStickerSet(res: Omit<MessagesStickerSet.messagesStickerSet, '_'>, id: DocId) {
@ -592,7 +593,7 @@ export class AppStickersManager extends AppManager {
});
if(res) {
set.installed_date = Date.now() / 1000 | 0;
set.installed_date = tsNow(true);
this.rootScope.dispatchEvent('stickers_installed', set);
return true;
}

View File

@ -431,7 +431,7 @@ export class AppUsersManager extends AppManager {
return index.search(query).has(user.id);
}
private createSearchIndex() {
public createSearchIndex() {
return new SearchIndex<UserId>({
clearBadChars: true,
ignoreCase: true,

View File

@ -35,8 +35,8 @@ import DEBUG from '../../config/debug';
// const key2 = [('00000' + sentCount).slice(-5), key].join('-');
// let byManager = stats[manager] ??= {};
// let byMethod = byManager[method] ??= {times: [], byArgs: {}};
// const byManager = stats[manager] ??= {};
// const byMethod = byManager[method] ??= {times: [], byArgs: {}};
// const perf = performance.now();
// promise.catch(noop).finally(() => {
@ -53,8 +53,9 @@ import DEBUG from '../../config/debug';
// }
// setInterval(() => {
// // console.log(dT(), '[PROXY] stats', stats, sentCount, sentMethods, sentMethods2);
// console.log(dT(), '[PROXY] stats', stats, sentCount, sentMethods, sentMethods2);
// sentCount = 0;
// stats = {};
// sentMethods = {};
// sentMethods2 = {};
// }, 2000);

View File

@ -365,7 +365,7 @@ export class UiNotificationsManager {
avatarContext.textBaseline = 'middle';
avatarContext.textAlign = 'center';
avatarContext.fillStyle = 'white';
avatarContext.fillText(abbreviation.text, avatarCanvas.width / 2, avatarCanvas.height * (window.devicePixelRatio > 1 ? .5625 : 5));
avatarContext.fillText(abbreviation.text, avatarCanvas.width / 2, avatarCanvas.height * (window.devicePixelRatio > 1 || true ? .5625 : .5));
notification.image = avatarCanvas.toDataURL();
}

View File

@ -63,7 +63,14 @@ export default function hasRights(chat: Exclude<Chat, Chat.chatEmpty>, action: C
case 'send_media':
case 'send_messages':
case 'send_polls':
case 'send_stickers': {
case 'send_stickers':
case 'send_photos':
case 'send_videos':
case 'send_roundvideos':
case 'send_audios':
case 'send_voices':
case 'send_docs':
case 'send_plain': {
if(!isThread && chat.pFlags.left) {
return false;
}

View File

@ -8,7 +8,12 @@ import type {Document, PhotoSize, VideoSize} from '../../../../layer';
import type {DownloadOptions} from '../../../mtproto/apiFileManager';
import getDocumentInputFileLocation from './getDocumentInputFileLocation';
export default function getDocumentDownloadOptions(doc: Document.document, thumb?: PhotoSize.photoSize | VideoSize, queueId?: number, onlyCache?: boolean): DownloadOptions {
export default function getDocumentDownloadOptions(
doc: Document.document,
thumb?: PhotoSize.photoSize | Extract<VideoSize, VideoSize.videoSize>,
queueId?: number,
onlyCache?: boolean
): DownloadOptions {
const inputFileLocation = getDocumentInputFileLocation(doc, thumb?.type);
let mimeType: MTMimeType;

View File

@ -1,6 +1,8 @@
import {Document, Message, MessageAction, MessageExtendedMedia, MessageMedia, Photo, WebPage} from '../../../../layer';
import {Document, Game, Message, MessageAction, MessageExtendedMedia, MessageMedia, Photo, WebPage} from '../../../../layer';
export default function getMediaFromMessage(message: Message) {
export default function getMediaFromMessage(message: Message, onlyInner: true): Photo.photo | Document.document;
export default function getMediaFromMessage(message: Message, onlyInner?: false): Photo.photo | Document.document | Game.game | WebPage.webPage;
export default function getMediaFromMessage(message: Message, onlyInner = false): Photo.photo | Document.document | Game.game | WebPage.webPage {
if(!message) return;
let media: any;
@ -15,8 +17,9 @@ export default function getMediaFromMessage(message: Message) {
}
media = (messageMedia as MessageMedia.messageMediaDocument).document ||
(messageMedia as MessageMedia.messageMediaPhoto).photo;
(messageMedia as MessageMedia.messageMediaPhoto).photo ||
(onlyInner ? undefined : (messageMedia as MessageMedia.messageMediaGame).game || messageMedia);
}
return media as Photo.photo | Document.document;
return media as any;
}

View File

@ -1,5 +1,6 @@
import {MyDocument} from '../../appDocsManager';
import {VideoSize} from '../../../../layer';
export default function getStickerEffectThumb(doc: MyDocument) {
return doc.video_thumbs?.[0];
return doc.video_thumbs?.[0] as Extract<VideoSize, VideoSize.videoSize>;
}

View File

@ -64,7 +64,7 @@ export type DownloadOptions = {
export type DownloadMediaOptions = {
media: Photo.photo | Document.document | WebDocument,
thumb?: PhotoSize | VideoSize,
thumb?: PhotoSize | Extract<VideoSize, VideoSize.videoSize>,
queueId?: number,
onlyCache?: boolean,
downloadId?: string

View File

@ -37,6 +37,7 @@ import pause from '../../helpers/schedulers/pause';
import ApiManagerMethods from './api_methods';
import {getEnvironment} from '../../environment/utils';
import toggleStorages from '../../helpers/toggleStorages';
import tsNow from '../../helpers/tsNow';
/* class RotatableArray<T> {
public array: Array<T> = [];
@ -250,7 +251,7 @@ export class ApiManager extends ApiManagerMethods {
public async setUserAuth(userAuth: UserAuth | UserId) {
if(typeof(userAuth) === 'string' || typeof(userAuth) === 'number') {
userAuth = {dcID: 0, date: Date.now() / 1000 | 0, id: userAuth.toPeerId(false)};
userAuth = {dcID: 0, date: tsNow(true), id: userAuth.toPeerId(false)};
}
this.rootScope.dispatchEvent('user_auth', userAuth);

File diff suppressed because one or more lines are too long

View File

@ -28,6 +28,7 @@ export type BroadcastEvents = {
'chat_full_update': ChatId,
'chat_update': ChatId,
'chat_toggle_forum': {chatId: ChatId, enabled: boolean},
'chat_participant': Update.updateChannelParticipant,
'channel_update': ChatId,
@ -179,10 +180,7 @@ export class RootScope extends EventListenerBase<BroadcastEventsListeners> {
this.addEventListener('premium_toggle_private', ({isNew, isPremium}) => {
this.premium = isPremium;
if(!isNew) {
this.dispatchEventSingle('premium_toggle', isPremium);
}
this.dispatchEventSingle('premium_toggle', isPremium);
});
this.addEventListener('connection_status_change', (status) => {

View File

@ -163,6 +163,17 @@ const onFirstMount = () => {
}).then((code) => {
// console.log('got code', code);
if(code._ === 'auth.sentCodeSuccess') {
const {authorization} = code;
if(authorization._ === 'auth.authorization') {
rootScope.managers.apiManager.setUser(authorization.user);
import('./pageIm').then((m) => {
m.default.mount();
});
}
}
import('./pageAuthCode').then((m) => m.default.mount(Object.assign(code, {phone_number: phone_number})));
}).catch((err) => {
toggle();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -382,7 +382,7 @@
height: 100%;
&-background {
rect {
.audio-waveform-bar {
opacity: .3;
@include hover() {
@ -408,7 +408,7 @@
margin-top: 1px;
}
rect {
&-bar {
fill: var(--primary-color);
//overflow: visible!important;
}
@ -599,7 +599,7 @@
}
}
&:not(.is-out) .audio-toggle:not(.playing) + .audio-waveform-container .audio-waveform-background rect {
&:not(.is-out) .audio-toggle:not(.playing) + .audio-waveform-container .audio-waveform-background .audio-waveform-bar {
opacity: 1;
}
}

View File

@ -2437,6 +2437,7 @@ $bubble-border-radius-big: 12px;
}
.audio-transcribed-text {
margin-bottom: .75rem;
margin-top: .25rem;
&.is-error {
@ -2464,9 +2465,9 @@ $bubble-border-radius-big: 12px;
}
}
.document-message + .audio-transcribed-text {
margin-bottom: .75rem;
}
// .document-message + .audio-transcribed-text {
// margin-bottom: .75rem;
// }
}
@keyframes audio-dots {
@ -2949,7 +2950,7 @@ $bubble-border-radius-big: 12px;
.audio {
&-waveform {
rect {
&-bar {
fill: var(--message-out-primary-color);
&.active {
@ -2974,7 +2975,7 @@ $bubble-border-radius-big: 12px;
}
&.is-unread {
rect {
.audio-waveform-bar {
fill: var(--message-out-primary-color);
}

View File

@ -61,7 +61,9 @@
border-radius: inherit;
}
.media-photo, .media-video, .media-poster {
.media-photo,
.media-video,
.media-poster {
object-fit: cover;
width: 100%;
height: 100%;
@ -114,4 +116,23 @@
white-space: pre-wrap;
}
}
&-cant-send {
display: none;
margin: 0 auto;
color: var(--secondary-text-color);
}
&.cant-send {
width: 100% !important;
height: 3rem;
.scrollable-y {
display: none;
}
.inline-helper-cant-send {
display: block;
}
}
}

View File

@ -107,6 +107,8 @@
pointer-events: none;
line-height: var(--line-height);
@include text-overflow(false);
@include animation-level(0) {
transition: none;
}
@ -385,7 +387,17 @@
position: absolute;
@include animation-level(2) {
transition: border-color .1s, transform .1s cubic-bezier(.22, .75, .7, 1.44);
transition: border-color .1s, transform .1s cubic-bezier(.22, .75, .7, 1.3);
}
}
}
&-restriction {
.checkbox-toggle {
background-color: var(--danger-color);
&:before {
border-color: var(--danger-color);
}
}
}

View File

@ -91,6 +91,14 @@ $row-border-radius: $border-radius-medium;
color: var(--secondary-text-color);
}
}
&-row {
&.with-delimiter {
.row-title:first-child {
border-right: 1px solid var(--border-color);
}
}
}
}
&-title-right,
@ -321,4 +329,10 @@ $row-border-radius: $border-radius-medium;
color: var(--primary-color);
}
}
&.accordion-toggler {
.checkbox-field-toggle {
pointer-events: all !important;
}
}
}

View File

@ -274,6 +274,18 @@
}
.popup-create-contact {
.popup-container {
padding: 0 1rem 1rem !important;
}
.popup-header {
padding: 0 !important;
}
.btn-primary {
margin-bottom: 0 !important;
}
.name-fields {
display: flex;
flex-direction: column;

View File

@ -50,9 +50,10 @@
.checkbox-field {
display: flex;
align-items: center;
height: 3rem;
min-height: 3rem;
height: auto;
margin: 0;
padding: 0 1.125rem;
padding: .25rem 1.125rem;
.checkbox-box {
left: auto;

View File

@ -2102,3 +2102,34 @@ hr {
height: 100%;
}
}
.accordion {
overflow: hidden;
height: 0;
&.is-expanded {
height: var(--max-height);
}
@include animation-level(2) {
transition: height var(--transition-standard-in);
}
&-icon {
transform: rotate(0deg) translateY(4px);
color: var(--secondary-text-color);
font-size: 1.25rem;
display: inline-block;
line-height: 0;
@include animation-level(2) {
transition: transform var(--transition-standard-in);
}
}
&-toggler-expanded {
.accordion-icon {
transform: translateY(4px) rotate(180deg);
}
}
}