Browse Source

Merge pull request #2 from morethanwords/master

Merge main
master
wang chenyu 4 years ago committed by GitHub
parent
commit
ef77df84ca
  1. 69
      src/components/chat/bubbles.ts
  2. 4
      src/components/chat/contextMenu.ts
  3. 2
      src/components/chat/selection.ts
  4. 2
      src/components/divAndCaption.ts
  5. 11
      src/components/inputField.ts
  6. 1
      src/components/peerTitle.ts
  7. 2
      src/components/row.ts
  8. 1
      src/components/sidebarLeft/tabs/addMembers.ts
  9. 31
      src/components/sidebarRight/tabs/chatType.ts
  10. 199
      src/components/sidebarRight/tabs/editChannel.ts
  11. 134
      src/components/sidebarRight/tabs/editChat.ts
  12. 15
      src/components/sidebarRight/tabs/sharedMedia.ts
  13. 2
      src/config/app.ts
  14. 5
      src/helpers/dom.ts
  15. 151
      src/helpers/slicedArray.ts
  16. 18
      src/lang.ts
  17. 1
      src/lib/appManagers/appDialogsManager.ts
  18. 8
      src/lib/appManagers/appImManager.ts
  19. 176
      src/lib/appManagers/appMessagesManager.ts
  20. 2
      src/lib/appManagers/appPhotosManager.ts
  21. 14
      src/lib/appManagers/appUsersManager.ts
  22. 7
      src/lib/storages/dialogs.ts
  23. 2
      src/scss/partials/_button.scss
  24. 20
      src/scss/partials/_chatBubble.scss
  25. 1
      src/scss/partials/_selector.scss
  26. 4
      src/scss/partials/_sidebar.scss
  27. 2
      src/scss/style.scss
  28. 121
      src/tests/slicedArray.test.ts

69
src/components/chat/bubbles.ts

@ -15,7 +15,7 @@ import type { AppPeersManager } from "../../lib/appManagers/appPeersManager";
import type sessionStorage from '../../lib/sessionStorage'; import type sessionStorage from '../../lib/sessionStorage';
import type Chat from "./chat"; import type Chat from "./chat";
import { CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager"; import { CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager";
import { cancelEvent, whichChild, attachClickEvent, positionElementByIndex, reflowScrollableElement, replaceContent, htmlToDocumentFragment } from "../../helpers/dom"; import { cancelEvent, whichChild, attachClickEvent, positionElementByIndex, reflowScrollableElement, replaceContent, htmlToDocumentFragment, setInnerHTML } from "../../helpers/dom";
import { getObjectKeysAndSort } from "../../helpers/object"; import { getObjectKeysAndSort } from "../../helpers/object";
import { isTouchSupported } from "../../helpers/touchSupport"; import { isTouchSupported } from "../../helpers/touchSupport";
import { logger } from "../../lib/logger"; import { logger } from "../../lib/logger";
@ -130,6 +130,8 @@ export default class ChatBubbles {
public isFirstLoad = true; public isFirstLoad = true;
private needReflowScroll: boolean; private needReflowScroll: boolean;
private fetchNewPromise: Promise<void>;
constructor(private chat: Chat, private appMessagesManager: AppMessagesManager, private appStickersManager: AppStickersManager, private appUsersManager: AppUsersManager, private appInlineBotsManager: AppInlineBotsManager, private appPhotosManager: AppPhotosManager, private appDocsManager: AppDocsManager, private appPeersManager: AppPeersManager, private appChatsManager: AppChatsManager, private storage: typeof sessionStorage) { constructor(private chat: Chat, private appMessagesManager: AppMessagesManager, private appStickersManager: AppStickersManager, private appUsersManager: AppUsersManager, private appInlineBotsManager: AppInlineBotsManager, private appPhotosManager: AppPhotosManager, private appDocsManager: AppDocsManager, private appPeersManager: AppPeersManager, private appChatsManager: AppChatsManager, private storage: typeof sessionStorage) {
//this.chat.log.error('Bubbles construction'); //this.chat.log.error('Bubbles construction');
@ -1399,6 +1401,7 @@ export default class ChatBubbles {
this.messagesQueuePromise = null; this.messagesQueuePromise = null;
this.getHistoryTopPromise = this.getHistoryBottomPromise = undefined; this.getHistoryTopPromise = this.getHistoryBottomPromise = undefined;
this.fetchNewPromise = undefined;
if(this.stickyIntersector) { if(this.stickyIntersector) {
this.stickyIntersector.disconnect(); this.stickyIntersector.disconnect();
@ -1618,8 +1621,7 @@ export default class ChatBubbles {
this.chat.dispatchEvent('setPeer', lastMsgId, !isJump); this.chat.dispatchEvent('setPeer', lastMsgId, !isJump);
const isFetchIntervalNeeded = () => peerId < 0 && !this.appChatsManager.isInChat(peerId); const needFetchInterval = this.appMessagesManager.isFetchIntervalNeeded(peerId);
const needFetchInterval = isFetchIntervalNeeded();
const needFetchNew = savedPosition || needFetchInterval; const needFetchNew = savedPosition || needFetchInterval;
if(!needFetchNew) { if(!needFetchNew) {
// warning // warning
@ -1627,20 +1629,44 @@ export default class ChatBubbles {
this.scrollable.loadedAll.bottom = true; this.scrollable.loadedAll.bottom = true;
} }
} else { } else {
const middleware = this.getMiddleware();
Promise.all([setPeerPromise, getHeavyAnimationPromise()]).then(() => { Promise.all([setPeerPromise, getHeavyAnimationPromise()]).then(() => {
if(!middleware()) {
return;
}
this.scrollable.checkForTriggers(); this.scrollable.checkForTriggers();
if(needFetchInterval) { if(needFetchInterval) {
const middleware = this.getMiddleware(); const f = () => {
const interval = window.setInterval(() => { this.fetchNewPromise = new Promise<void>((resolve) => {
if(!middleware() || !isFetchIntervalNeeded()) { if(!middleware() || !this.appMessagesManager.isFetchIntervalNeeded(peerId)) {
clearInterval(interval); resolve();
return; return;
} }
this.scrollable.loadedAll.bottom = false; this.appMessagesManager.getNewHistory(peerId, this.chat.threadId).then((historyStorage) => {
this.loadMoreHistory(false); if(!middleware()) {
}, 30e3); resolve();
return;
}
const slice = historyStorage.history.slice;
const isBottomEnd = slice.isEnd(SliceEnd.Bottom);
if(this.scrollable.loadedAll.bottom !== isBottomEnd) {
this.scrollable.loadedAll.bottom = isBottomEnd;
this.onScroll();
}
setTimeout(f, 30e3);
resolve();
});
}).finally(() => {
this.fetchNewPromise = undefined;
});
};
f();
} }
}); });
} }
@ -1975,7 +2001,7 @@ export default class ChatBubbles {
bubble.classList.add('is-message-empty', 'emoji-big'); bubble.classList.add('is-message-empty', 'emoji-big');
canHaveTail = false; canHaveTail = false;
} else { } else {
messageDiv.innerHTML = richText; setInnerHTML(messageDiv, richText);
} }
/* if(strLength === emojiStrLength) { /* if(strLength === emojiStrLength) {
@ -1983,7 +2009,7 @@ export default class ChatBubbles {
messageDiv.classList.add('message-empty'); messageDiv.classList.add('message-empty');
} */ } */
} else { } else {
messageDiv.innerHTML = richText; setInnerHTML(messageDiv, richText);
} }
const timeSpan = MessageRender.setTime(this.chat, message, bubble, bubbleContainer, messageDiv); const timeSpan = MessageRender.setTime(this.chat, message, bubble, bubbleContainer, messageDiv);
@ -2219,21 +2245,21 @@ export default class ChatBubbles {
nameEl.classList.add('name'); nameEl.classList.add('name');
nameEl.setAttribute('target', '_blank'); nameEl.setAttribute('target', '_blank');
nameEl.href = webpage.url || '#'; nameEl.href = webpage.url || '#';
nameEl.innerHTML = RichTextProcessor.wrapEmojiText(webpage.site_name); setInnerHTML(nameEl, RichTextProcessor.wrapEmojiText(webpage.site_name));
quoteTextDiv.append(nameEl); quoteTextDiv.append(nameEl);
} }
if(webpage.rTitle) { if(webpage.rTitle) {
let titleDiv = document.createElement('div'); let titleDiv = document.createElement('div');
titleDiv.classList.add('title'); titleDiv.classList.add('title');
titleDiv.innerHTML = webpage.rTitle; setInnerHTML(titleDiv, webpage.rTitle);
quoteTextDiv.append(titleDiv); quoteTextDiv.append(titleDiv);
} }
if(webpage.rDescription) { if(webpage.rDescription) {
let textDiv = document.createElement('div'); let textDiv = document.createElement('div');
textDiv.classList.add('text'); textDiv.classList.add('text');
textDiv.innerHTML = webpage.rDescription; setInnerHTML(textDiv, webpage.rDescription);
quoteTextDiv.append(textDiv); quoteTextDiv.append(textDiv);
} }
@ -2626,10 +2652,16 @@ export default class ChatBubbles {
} */ } */
const historyStorage = this.appMessagesManager.getHistoryStorage(this.peerId, this.chat.threadId); const historyStorage = this.appMessagesManager.getHistoryStorage(this.peerId, this.chat.threadId);
if(history.includes(historyStorage.maxId)) { const firstSlice = historyStorage.history.first;
const lastSlice = historyStorage.history.last;
if(firstSlice.isEnd(SliceEnd.Bottom) && history.includes(firstSlice[0])) {
this.scrollable.loadedAll.bottom = true; this.scrollable.loadedAll.bottom = true;
} }
if(lastSlice.isEnd(SliceEnd.Top) && history.includes(lastSlice[lastSlice.length - 1])) {
this.scrollable.loadedAll.top = true;
}
//console.time('appImManager render history'); //console.time('appImManager render history');
return new Promise<boolean>((resolve, reject) => { return new Promise<boolean>((resolve, reject) => {
@ -2818,8 +2850,9 @@ export default class ChatBubbles {
additionMsgIds = [additionMsgId]; additionMsgIds = [additionMsgId];
} else { } else {
const historyStorage = this.appMessagesManager.getHistoryStorage(peerId, this.chat.threadId); const historyStorage = this.appMessagesManager.getHistoryStorage(peerId, this.chat.threadId);
if(historyStorage.history.length < loadCount && !historyStorage.history.slice.isEnd(SliceEnd.Both)) { const slice = historyStorage.history.slice;
additionMsgIds = historyStorage.history.slice.slice(); if(slice.length < loadCount && !slice.isEnd(SliceEnd.Both)) {
additionMsgIds = slice.slice();
// * filter last album, because we don't know is it the last item // * filter last album, because we don't know is it the last item
for(let i = additionMsgIds.length - 1; i >= 0; --i) { for(let i = additionMsgIds.length - 1; i >= 0; --i) {

4
src/components/chat/contextMenu.ts

@ -158,7 +158,7 @@ export default class ChatContextMenu {
private init() { private init() {
this.buttons = [{ this.buttons = [{
icon: 'send2', icon: 'send2',
text: 'Chat.Context.Scheduled.SendNow', text: 'MessageScheduleSend',
onClick: this.onSendScheduledClick, onClick: this.onSendScheduledClick,
verify: () => this.chat.type === 'scheduled' && !this.message.pFlags.is_outgoing verify: () => this.chat.type === 'scheduled' && !this.message.pFlags.is_outgoing
}, { }, {
@ -170,7 +170,7 @@ export default class ChatContextMenu {
withSelection: true withSelection: true
}, { }, {
icon: 'schedule', icon: 'schedule',
text: 'Chat.Context.Scheduled.Reschedule', text: 'MessageScheduleEditTime',
onClick: () => { onClick: () => {
this.chat.input.scheduleSending(() => { this.chat.input.scheduleSending(() => {
this.appMessagesManager.editMessage(this.message, this.message.message, { this.appMessagesManager.editMessage(this.message, this.message.message, {

2
src/components/chat/selection.ts

@ -359,7 +359,7 @@ export default class ChatSelection {
if(this.chat.type === 'scheduled') { if(this.chat.type === 'scheduled') {
this.selectionSendNowBtn = Button('btn-primary btn-transparent btn-short text-bold selection-container-send', {icon: 'send2'}); this.selectionSendNowBtn = Button('btn-primary btn-transparent btn-short text-bold selection-container-send', {icon: 'send2'});
this.selectionSendNowBtn.append(i18n('Chat.Context.Scheduled.SendNow')); this.selectionSendNowBtn.append(i18n('MessageScheduleSend'));
this.listenerSetter.add(this.selectionSendNowBtn, 'click', () => { this.listenerSetter.add(this.selectionSendNowBtn, 'click', () => {
new PopupSendNow(this.bubbles.peerId, [...this.selectedMids], () => { new PopupSendNow(this.bubbles.peerId, [...this.selectedMids], () => {
this.cancelSelection(); this.cancelSelection();

2
src/components/divAndCaption.ts

@ -23,9 +23,11 @@ export default class DivAndCaption<T> {
this.title = document.createElement('div'); this.title = document.createElement('div');
this.title.classList.add(className + '-title'); this.title.classList.add(className + '-title');
this.title.setAttribute('dir', 'auto');
this.subtitle = document.createElement('div'); this.subtitle = document.createElement('div');
this.subtitle.classList.add(className + '-subtitle'); this.subtitle.classList.add(className + '-subtitle');
this.subtitle.setAttribute('dir', 'auto');
this.content.append(this.title, this.subtitle); this.content.append(this.title, this.subtitle);
this.container.append(this.border, this.content); this.container.append(this.border, this.content);

11
src/components/inputField.ts

@ -42,7 +42,8 @@ let init = () => {
init = null; init = null;
}; };
const checkAndSetRTL = (input: HTMLElement) => { // ! it doesn't respect symbols other than strongs
/* const checkAndSetRTL = (input: HTMLElement) => {
//const isEmpty = isInputEmpty(input); //const isEmpty = isInputEmpty(input);
//console.log('input', isEmpty); //console.log('input', isEmpty);
@ -56,7 +57,7 @@ const checkAndSetRTL = (input: HTMLElement) => {
//console.log('RTL', direction, char); //console.log('RTL', direction, char);
input.style.direction = direction; input.style.direction = direction;
}; }; */
export enum InputState { export enum InputState {
Neutral = 0, Neutral = 0,
@ -112,7 +113,7 @@ class InputField {
input = this.container.firstElementChild as HTMLElement; input = this.container.firstElementChild as HTMLElement;
const observer = new MutationObserver(() => { const observer = new MutationObserver(() => {
checkAndSetRTL(input); //checkAndSetRTL(input);
if(processInput) { if(processInput) {
processInput(); processInput();
@ -148,9 +149,11 @@ class InputField {
`; `;
input = this.container.firstElementChild as HTMLElement; input = this.container.firstElementChild as HTMLElement;
input.addEventListener('input', () => checkAndSetRTL(input)); //input.addEventListener('input', () => checkAndSetRTL(input));
} }
input.setAttribute('dir', 'auto');
if(placeholder) { if(placeholder) {
_i18n(input, placeholder, undefined, 'placeholder'); _i18n(input, placeholder, undefined, 'placeholder');

1
src/components/peerTitle.ts

@ -43,6 +43,7 @@ export default class PeerTitle {
constructor(options: PeerTitleOptions) { constructor(options: PeerTitleOptions) {
this.element = document.createElement('span'); this.element = document.createElement('span');
this.element.classList.add('peer-title'); this.element.classList.add('peer-title');
this.element.setAttribute('dir', 'auto');
this.update(options); this.update(options);
weakMap.set(this.element, this); weakMap.set(this.element, this);

2
src/components/row.ts

@ -40,6 +40,7 @@ export default class Row {
this.subtitle = document.createElement('div'); this.subtitle = document.createElement('div');
this.subtitle.classList.add('row-subtitle'); this.subtitle.classList.add('row-subtitle');
this.subtitle.setAttribute('dir', 'auto');
if(options.subtitle) { if(options.subtitle) {
this.subtitle.innerHTML = options.subtitle; this.subtitle.innerHTML = options.subtitle;
} else if(options.subtitleLangKey) { } else if(options.subtitleLangKey) {
@ -89,6 +90,7 @@ export default class Row {
this.title = document.createElement('div'); this.title = document.createElement('div');
this.title.classList.add('row-title'); this.title.classList.add('row-title');
this.title.setAttribute('dir', 'auto');
if(options.title) { if(options.title) {
this.title.innerHTML = options.title; this.title.innerHTML = options.title;
} else { } else {

1
src/components/sidebarLeft/tabs/addMembers.ts

@ -20,6 +20,7 @@ export default class AppAddMembersTab extends SliderSuperTab {
protected init() { protected init() {
this.nextBtn = ButtonCorner({icon: 'arrow_next'}); this.nextBtn = ButtonCorner({icon: 'arrow_next'});
this.content.append(this.nextBtn); this.content.append(this.nextBtn);
this.scrollable.container.remove();
this.nextBtn.addEventListener('click', () => { this.nextBtn.addEventListener('click', () => {
const peerIds = this.selector.getSelected(); const peerIds = this.selector.getSelected();

31
src/components/sidebarRight/tabs/groupType.ts → src/components/sidebarRight/tabs/chatType.ts

@ -22,34 +22,37 @@ import I18n from "../../../lib/langPack";
import PopupPeer from "../../popups/peer"; import PopupPeer from "../../popups/peer";
import ButtonCorner from "../../buttonCorner"; import ButtonCorner from "../../buttonCorner";
export default class AppGroupTypeTab extends SliderSuperTabEventable { export default class AppChatTypeTab extends SliderSuperTabEventable {
public peerId: number; public chatId: number;
public chatFull: ChatFull; public chatFull: ChatFull;
protected init() { protected init() {
this.container.classList.add('edit-peer-container', 'group-type-container'); this.container.classList.add('edit-peer-container', 'group-type-container');
this.setTitle('GroupType');
const isBroadcast = appChatsManager.isBroadcast(this.chatId);
this.setTitle(isBroadcast ? 'ChannelType' : 'GroupType');
const section = new SettingSection({ const section = new SettingSection({
name: 'GroupType' name: isBroadcast ? 'ChannelType' : 'GroupType'
}); });
const random = randomLong(); const random = randomLong();
const privateRow = new Row({ const privateRow = new Row({
radioField: new RadioField({ radioField: new RadioField({
langKey: 'MegaPrivate', langKey: isBroadcast ? 'ChannelPrivate' : 'MegaPrivate',
name: random, name: random,
value: 'private' value: 'private'
}), }),
subtitleLangKey: 'MegaPrivateInfo' subtitleLangKey: isBroadcast ? 'ChannelPrivateInfo' : 'MegaPrivateInfo'
}); });
const publicRow = new Row({ const publicRow = new Row({
radioField: new RadioField({ radioField: new RadioField({
langKey: 'MegaPublic', langKey: isBroadcast ? 'ChannelPublic' : 'MegaPublic',
name: random, name: random,
value: 'public' value: 'public'
}), }),
subtitleLangKey: 'MegaPublicInfo' subtitleLangKey: isBroadcast ? 'ChannelPublicInfo' : 'MegaPublicInfo'
}); });
const form = RadioFormFromRows([privateRow, publicRow], (value) => { const form = RadioFormFromRows([privateRow, publicRow], (value) => {
const a = [privateSection, publicSection]; const a = [privateSection, publicSection];
@ -61,7 +64,7 @@ export default class AppGroupTypeTab extends SliderSuperTabEventable {
onChange(); onChange();
}); });
const chat: Chat = appChatsManager.getChat(-this.peerId); const chat: Chat = appChatsManager.getChat(this.chatId);
section.content.append(form); section.content.append(form);
@ -70,7 +73,7 @@ export default class AppGroupTypeTab extends SliderSuperTabEventable {
//let revoked = false; //let revoked = false;
const linkRow = new Row({ const linkRow = new Row({
title: (this.chatFull.exported_invite as ExportedChatInvite.chatInviteExported).link, title: (this.chatFull.exported_invite as ExportedChatInvite.chatInviteExported).link,
subtitleLangKey: 'MegaPrivateLinkHelp', subtitleLangKey: isBroadcast ? 'ChannelPrivateLinkHelp' : 'MegaPrivateLinkHelp',
clickable: () => { clickable: () => {
copyTextToClipboard((this.chatFull.exported_invite as ExportedChatInvite.chatInviteExported).link); copyTextToClipboard((this.chatFull.exported_invite as ExportedChatInvite.chatInviteExported).link);
toast(I18n.format('LinkCopied', true)); toast(I18n.format('LinkCopied', true));
@ -86,7 +89,7 @@ export default class AppGroupTypeTab extends SliderSuperTabEventable {
callback: () => { callback: () => {
const toggle = toggleDisability([btnRevoke], true); const toggle = toggleDisability([btnRevoke], true);
appProfileManager.getChatInviteLink(-this.peerId, true).then(link => { appProfileManager.getChatInviteLink(this.chatId, true).then(link => {
toggle(); toggle();
linkRow.title.innerHTML = link; linkRow.title.innerHTML = link;
//revoked = true; //revoked = true;
@ -102,7 +105,7 @@ export default class AppGroupTypeTab extends SliderSuperTabEventable {
privateSection.content.append(linkRow.container, btnRevoke); privateSection.content.append(linkRow.container, btnRevoke);
const publicSection = new SettingSection({ const publicSection = new SettingSection({
caption: 'Channel.UsernameAboutGroup', caption: isBroadcast ? 'Channel.UsernameAboutChannel' : 'Channel.UsernameAboutGroup',
noDelimiter: true noDelimiter: true
}); });
@ -126,7 +129,7 @@ export default class AppGroupTypeTab extends SliderSuperTabEventable {
invalidText: 'Link.Invalid', invalidText: 'Link.Invalid',
takenText: 'Link.Taken', takenText: 'Link.Taken',
onChange: onChange, onChange: onChange,
peerId: this.peerId, peerId: -this.chatId,
head: placeholder head: placeholder
}); });
@ -141,7 +144,7 @@ export default class AppGroupTypeTab extends SliderSuperTabEventable {
attachClickEvent(applyBtn, () => { attachClickEvent(applyBtn, () => {
/* const unsetLoader = */setButtonLoader(applyBtn); /* const unsetLoader = */setButtonLoader(applyBtn);
const username = publicRow.radioField.checked ? linkInputField.getValue() : ''; const username = publicRow.radioField.checked ? linkInputField.getValue() : '';
appChatsManager.migrateChat(-this.peerId).then(channelId => { appChatsManager.migrateChat(this.chatId).then(channelId => {
return appChatsManager.updateUsername(channelId, username); return appChatsManager.updateUsername(channelId, username);
}).then(() => { }).then(() => {
//unsetLoader(); //unsetLoader();

199
src/components/sidebarRight/tabs/editChannel.ts

@ -1,199 +0,0 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { SliderSuperTab } from "../../slider"
import InputField from "../../inputField";
import EditPeer from "../../editPeer";
import { SettingSection } from "../../sidebarLeft";
import Row from "../../row";
import CheckboxField from "../../checkboxField";
import Button from "../../button";
import appChatsManager from "../../../lib/appManagers/appChatsManager";
import appProfileManager from "../../../lib/appManagers/appProfileManager";
import { attachClickEvent, toggleDisability } from "../../../helpers/dom";
import PopupPeer from "../../popups/peer";
import { addCancelButton } from "../../popups";
import { i18n } from "../../../lib/langPack";
import { numberThousandSplitter } from "../../../helpers/number";
export default class AppEditChannelTab extends SliderSuperTab {
private nameInputField: InputField;
private descriptionInputField: InputField;
private editPeer: EditPeer;
public peerId: number;
protected async init() {
this.container.classList.add('edit-peer-container', 'edit-channel-container');
this.setTitle('Edit');
const chatFull = await appProfileManager.getChannelFull(-this.peerId, true);
{
const section = new SettingSection({noDelimiter: true});
if(appChatsManager.hasRights(-this.peerId, 'change_info')) {
const inputFields: InputField[] = [];
const inputWrapper = document.createElement('div');
inputWrapper.classList.add('input-wrapper');
this.nameInputField = new InputField({
label: 'Channel.ChannelNameHolder',
name: 'channel-name',
maxLength: 255
});
this.descriptionInputField = new InputField({
label: 'DescriptionPlaceholder',
name: 'channel-description',
maxLength: 255
});
this.nameInputField.setOriginalValue(appChatsManager.getChat(-this.peerId).title);
this.descriptionInputField.setOriginalValue(chatFull.about);
inputWrapper.append(this.nameInputField.container, this.descriptionInputField.container);
inputFields.push(this.nameInputField, this.descriptionInputField);
this.editPeer = new EditPeer({
peerId: this.peerId,
inputFields,
listenerSetter: this.listenerSetter
});
this.content.append(this.editPeer.nextBtn);
section.content.append(this.editPeer.avatarEdit.container, inputWrapper);
attachClickEvent(this.editPeer.nextBtn, () => {
this.editPeer.nextBtn.disabled = true;
let promises: Promise<any>[] = [];
const id = -this.peerId;
if(this.nameInputField.isValid()) {
promises.push(appChatsManager.editTitle(id, this.nameInputField.value));
}
if(this.descriptionInputField.isValid()) {
promises.push(appChatsManager.editAbout(id, this.descriptionInputField.value));
}
if(this.editPeer.uploadAvatar) {
promises.push(this.editPeer.uploadAvatar().then(inputFile => {
return appChatsManager.editPhoto(id, inputFile);
}));
}
Promise.race(promises).finally(() => {
this.editPeer.nextBtn.removeAttribute('disabled');
this.close();
});
}, {listenerSetter: this.listenerSetter});
}
/* if(appChatsManager.hasRights(-this.peerId, 'change_type')) {
const channelTypeRow = new Row({
titleLangKey: 'ChannelType',
subtitleLangKey: 'TypePrivate',
clickable: true,
icon: 'lock'
});
section.content.append(channelTypeRow.container);
}
if(appChatsManager.hasRights(-this.peerId, 'change_info')) {
const discussionRow = new Row({
titleLangKey: 'PeerInfo.Discussion',
subtitleLangKey: 'PeerInfo.Discussion.Add',
clickable: true,
icon: 'message'
});
section.content.append(discussionRow.container);
}
const administratorsRow = new Row({
titleLangKey: 'PeerInfo.Administrators',
subtitle: '' + chatFull.admins_count,
icon: 'admin',
clickable: true
});
section.content.append(administratorsRow.container);
if(appChatsManager.hasRights(-this.peerId, 'change_info')) {
const signMessagesCheckboxField = new CheckboxField({
text: 'PeerInfo.SignMessages',
checked: false
});
section.content.append(signMessagesCheckboxField.label);
} */
this.scrollable.append(section.container);
}
/* {
const section = new SettingSection({
});
const subscribersRow = new Row({
titleLangKey: 'PeerInfo.Subscribers',
icon: 'newgroup',
clickable: true
});
subscribersRow.subtitle.append(i18n('Subscribers', [numberThousandSplitter(335356)]));
section.content.append(subscribersRow.container);
this.scrollable.append(section.container);
} */
if(appChatsManager.hasRights(-this.peerId, 'delete_chat')) {
const section = new SettingSection({
});
const btnDelete = Button('btn-primary btn-transparent danger', {icon: 'delete', text: 'PeerInfo.DeleteChannel'});
attachClickEvent(btnDelete, () => {
new PopupPeer('popup-delete-channel', {
peerId: this.peerId,
titleLangKey: 'ChannelDeleteMenu',
descriptionLangKey: 'AreYouSureDeleteAndExitChannel',
buttons: addCancelButton([{
langKey: 'ChannelDeleteMenu',
callback: () => {
const toggle = toggleDisability([btnDelete], true);
},
isDanger: true
}, {
langKey: 'DeleteChannelForAll',
callback: () => {
const toggle = toggleDisability([btnDelete], true);
appChatsManager.deleteChannel(-this.peerId).then(() => {
this.close();
}, () => {
toggle();
});
},
isDanger: true
}])
}).show();
}, {listenerSetter: this.listenerSetter});
section.content.append(btnDelete);
this.scrollable.append(section.container);
}
}
}

134
src/components/sidebarRight/tabs/editGroup.ts → src/components/sidebarRight/tabs/editChat.ts

@ -13,15 +13,17 @@ import Button from "../../button";
import appChatsManager, { ChatRights } from "../../../lib/appManagers/appChatsManager"; import appChatsManager, { ChatRights } from "../../../lib/appManagers/appChatsManager";
import appProfileManager from "../../../lib/appManagers/appProfileManager"; import appProfileManager from "../../../lib/appManagers/appProfileManager";
import { attachClickEvent, toggleDisability } from "../../../helpers/dom"; import { attachClickEvent, toggleDisability } from "../../../helpers/dom";
import { ChatFull } from "../../../layer"; import { Chat } from "../../../layer";
import AppGroupTypeTab from "./groupType"; import AppChatTypeTab from "./chatType";
import rootScope from "../../../lib/rootScope"; import rootScope from "../../../lib/rootScope";
import AppGroupPermissionsTab from "./groupPermissions"; import AppGroupPermissionsTab from "./groupPermissions";
import { i18n } from "../../../lib/langPack"; import { i18n, LangPackKey } from "../../../lib/langPack";
import PopupDeleteDialog from "../../popups/deleteDialog"; import PopupDeleteDialog from "../../popups/deleteDialog";
import { addCancelButton } from "../../popups";
import PopupPeer from "../../popups/peer";
export default class AppEditGroupTab extends SliderSuperTab { export default class AppEditChatTab extends SliderSuperTab {
private groupNameInputField: InputField; private chatNameInputField: InputField;
private descriptionInputField: InputField; private descriptionInputField: InputField;
private editPeer: EditPeer; private editPeer: EditPeer;
public chatId: number; public chatId: number;
@ -36,6 +38,10 @@ export default class AppEditGroupTab extends SliderSuperTab {
const chatFull = await appProfileManager.getChatFull(this.chatId, true); const chatFull = await appProfileManager.getChatFull(this.chatId, true);
const chat: Chat.chat | Chat.channel = appChatsManager.getChat(this.chatId);
const isBroadcast = appChatsManager.isBroadcast(this.chatId);
const isChannel = appChatsManager.isChannel(this.chatId);
{ {
const section = new SettingSection({noDelimiter: true}); const section = new SettingSection({noDelimiter: true});
const inputFields: InputField[] = []; const inputFields: InputField[] = [];
@ -43,26 +49,23 @@ export default class AppEditGroupTab extends SliderSuperTab {
const inputWrapper = document.createElement('div'); const inputWrapper = document.createElement('div');
inputWrapper.classList.add('input-wrapper'); inputWrapper.classList.add('input-wrapper');
this.groupNameInputField = new InputField({ this.chatNameInputField = new InputField({
label: 'CreateGroup.NameHolder', label: isBroadcast ? 'Channel.ChannelNameHolder' : 'CreateGroup.NameHolder',
name: 'group-name', name: 'chat-name',
maxLength: 255 maxLength: 255
}); });
this.descriptionInputField = new InputField({ this.descriptionInputField = new InputField({
label: 'DescriptionPlaceholder', label: 'DescriptionPlaceholder',
name: 'group-description', name: 'chat-description',
maxLength: 255 maxLength: 255
}); });
const chat = appChatsManager.getChat(this.chatId); this.chatNameInputField.setOriginalValue(chat.title);
this.groupNameInputField.setOriginalValue(chat.title);
this.descriptionInputField.setOriginalValue(chatFull.about); this.descriptionInputField.setOriginalValue(chatFull.about);
inputWrapper.append(this.groupNameInputField.container, this.descriptionInputField.container); inputWrapper.append(this.chatNameInputField.container, this.descriptionInputField.container);
inputFields.push(this.groupNameInputField, this.descriptionInputField); inputFields.push(this.chatNameInputField, this.descriptionInputField);
this.editPeer = new EditPeer({ this.editPeer = new EditPeer({
peerId: -this.chatId, peerId: -this.chatId,
@ -74,29 +77,37 @@ export default class AppEditGroupTab extends SliderSuperTab {
section.content.append(this.editPeer.avatarEdit.container, inputWrapper); section.content.append(this.editPeer.avatarEdit.container, inputWrapper);
if(appChatsManager.hasRights(this.chatId, 'change_type')) { if(appChatsManager.hasRights(this.chatId, 'change_type')) {
const groupTypeRow = new Row({ const chatTypeRow = new Row({
titleLangKey: 'GroupType', titleLangKey: isBroadcast ? 'ChannelType' : 'GroupType',
clickable: () => { clickable: () => {
const tab = new AppGroupTypeTab(this.slider); const tab = new AppChatTypeTab(this.slider);
tab.peerId = -this.chatId; tab.chatId = this.chatId;
tab.chatFull = chatFull; tab.chatFull = chatFull;
tab.open(); tab.open();
this.listenerSetter.add(tab.eventListener, 'destroy', setGroupTypeSubtitle); this.listenerSetter.add(tab.eventListener, 'destroy', setChatTypeSubtitle);
}, },
icon: 'lock' icon: 'lock'
}); });
const setGroupTypeSubtitle = () => { const setChatTypeSubtitle = () => {
groupTypeRow.subtitle.textContent = ''; chatTypeRow.subtitle.textContent = '';
groupTypeRow.subtitle.append(i18n(chat.username ? 'TypePublicGroup' : 'TypePrivateGroup'));
let key: LangPackKey;
if(isBroadcast) {
key = (chat as Chat.channel).username ? 'TypePublic' : 'TypePrivate';
} else {
key = (chat as Chat.channel).username ? 'TypePublicGroup' : 'TypePrivateGroup';
}
chatTypeRow.subtitle.append(i18n(key));
}; };
setGroupTypeSubtitle(); setChatTypeSubtitle();
section.content.append(groupTypeRow.container); section.content.append(chatTypeRow.container);
} }
if(appChatsManager.hasRights(this.chatId, 'change_permissions')) { if(appChatsManager.hasRights(this.chatId, 'change_permissions') && !isBroadcast) {
const flags = [ const flags = [
'send_messages', 'send_messages',
'send_media', 'send_media',
@ -149,8 +160,8 @@ export default class AppEditGroupTab extends SliderSuperTab {
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
const id = this.chatId; const id = this.chatId;
if(this.groupNameInputField.isValid()) { if(this.chatNameInputField.isValid()) {
promises.push(appChatsManager.editTitle(id, this.groupNameInputField.value)); promises.push(appChatsManager.editTitle(id, this.chatNameInputField.value));
} }
if(this.descriptionInputField.isValid()) { if(this.descriptionInputField.isValid()) {
@ -168,6 +179,36 @@ export default class AppEditGroupTab extends SliderSuperTab {
this.close(); this.close();
}); });
}, {listenerSetter: this.listenerSetter}); }, {listenerSetter: this.listenerSetter});
/*
if(appChatsManager.hasRights(-this.peerId, 'change_info')) {
const discussionRow = new Row({
titleLangKey: 'PeerInfo.Discussion',
subtitleLangKey: 'PeerInfo.Discussion.Add',
clickable: true,
icon: 'message'
});
section.content.append(discussionRow.container);
}
const administratorsRow = new Row({
titleLangKey: 'PeerInfo.Administrators',
subtitle: '' + chatFull.admins_count,
icon: 'admin',
clickable: true
});
section.content.append(administratorsRow.container);
if(appChatsManager.hasRights(-this.peerId, 'change_info')) {
const signMessagesCheckboxField = new CheckboxField({
text: 'PeerInfo.SignMessages',
checked: false
});
section.content.append(signMessagesCheckboxField.label);
} */
} }
/* { /* {
@ -176,12 +217,13 @@ export default class AppEditGroupTab extends SliderSuperTab {
}); });
const membersRow = new Row({ const membersRow = new Row({
titleLangKey: 'GroupMembers', titleLangKey: isBroadcast ? 'PeerInfo.Subscribers' : 'GroupMembers',
subtitle: '2 500',
icon: 'newgroup', icon: 'newgroup',
clickable: true clickable: true
}); });
membersRow.subtitle.append(i18n('Subscribers', [numberThousandSplitter(335356)]));
section.content.append(membersRow.container); section.content.append(membersRow.container);
if(appChatsManager.hasRights(this.chatId, 'change_permissions')) { if(appChatsManager.hasRights(this.chatId, 'change_permissions')) {
@ -203,9 +245,36 @@ export default class AppEditGroupTab extends SliderSuperTab {
if(appChatsManager.hasRights(this.chatId, 'delete_chat')) { if(appChatsManager.hasRights(this.chatId, 'delete_chat')) {
const section = new SettingSection({}); const section = new SettingSection({});
const btnDelete = Button('btn-primary btn-transparent danger', {icon: 'delete', text: 'DeleteMega'}); const btnDelete = Button('btn-primary btn-transparent danger', {icon: 'delete', text: isBroadcast ? 'PeerInfo.DeleteChannel' : 'DeleteMega'});
attachClickEvent(btnDelete, () => { attachClickEvent(btnDelete, () => {
if(isBroadcast) {
new PopupPeer('popup-delete-channel', {
peerId: -this.chatId,
titleLangKey: 'ChannelDeleteMenu',
descriptionLangKey: 'AreYouSureDeleteAndExitChannel',
buttons: addCancelButton([{
langKey: 'ChannelDeleteMenu',
callback: () => {
const toggle = toggleDisability([btnDelete], true);
},
isDanger: true
}, {
langKey: 'DeleteChannelForAll',
callback: () => {
const toggle = toggleDisability([btnDelete], true);
appChatsManager.deleteChannel(this.chatId).then(() => {
this.close();
}, () => {
toggle();
});
},
isDanger: true
}])
}).show();
} else {
new PopupDeleteDialog(-this.chatId, undefined, (promise) => { new PopupDeleteDialog(-this.chatId, undefined, (promise) => {
const toggle = toggleDisability([btnDelete], true); const toggle = toggleDisability([btnDelete], true);
promise.then(() => { promise.then(() => {
@ -214,6 +283,7 @@ export default class AppEditGroupTab extends SliderSuperTab {
toggle(); toggle();
}); });
}); });
}
}, {listenerSetter: this.listenerSetter}); }, {listenerSetter: this.listenerSetter});
section.content.append(btnDelete); section.content.append(btnDelete);
@ -221,6 +291,7 @@ export default class AppEditGroupTab extends SliderSuperTab {
this.scrollable.append(section.container); this.scrollable.append(section.container);
} }
if(!isChannel) {
// ! this one will fire earlier than tab's closeAfterTimeout (destroy) event and listeners will be erased, so destroy won't fire // ! this one will fire earlier than tab's closeAfterTimeout (destroy) event and listeners will be erased, so destroy won't fire
this.listenerSetter.add(rootScope, 'dialog_migrate', ({migrateFrom, migrateTo}) => { this.listenerSetter.add(rootScope, 'dialog_migrate', ({migrateFrom, migrateTo}) => {
if(-this.chatId === migrateFrom) { if(-this.chatId === migrateFrom) {
@ -229,6 +300,7 @@ export default class AppEditGroupTab extends SliderSuperTab {
} }
}); });
} }
}
protected init() { protected init() {
return this._init(); return this._init();

15
src/components/sidebarRight/tabs/sharedMedia.ts

@ -19,9 +19,8 @@ import { attachClickEvent, replaceContent, cancelEvent } from "../../../helpers/
import appSidebarRight from ".."; import appSidebarRight from "..";
import { TransitionSlider } from "../../transition"; import { TransitionSlider } from "../../transition";
import appNotificationsManager from "../../../lib/appManagers/appNotificationsManager"; import appNotificationsManager from "../../../lib/appManagers/appNotificationsManager";
import AppEditGroupTab from "./editGroup"; import AppEditChatTab from "./editChat";
import PeerTitle from "../../peerTitle"; import PeerTitle from "../../peerTitle";
import AppEditChannelTab from "./editChannel";
import AppEditContactTab from "./editContact"; import AppEditContactTab from "./editContact";
import appChatsManager, { Channel } from "../../../lib/appManagers/appChatsManager"; import appChatsManager, { Channel } from "../../../lib/appManagers/appChatsManager";
import { Chat, Message, MessageAction, ChatFull, Photo } from "../../../layer"; import { Chat, Message, MessageAction, ChatFull, Photo } from "../../../layer";
@ -854,17 +853,15 @@ export default class AppSharedMediaTab extends SliderSuperTab {
}); });
attachClickEvent(this.editBtn, (e) => { attachClickEvent(this.editBtn, (e) => {
let tab: AppEditGroupTab | AppEditChannelTab | AppEditContactTab; let tab: AppEditChatTab | AppEditContactTab;
if(appPeersManager.isAnyGroup(this.peerId)) { if(this.peerId < 0) {
tab = new AppEditGroupTab(appSidebarRight); tab = new AppEditChatTab(appSidebarRight);
} else if(this.peerId > 0) {
tab = new AppEditContactTab(appSidebarRight);
} else { } else {
tab = new AppEditChannelTab(appSidebarRight); tab = new AppEditContactTab(appSidebarRight);
} }
if(tab) { if(tab) {
if(tab instanceof AppEditGroupTab) { if(tab instanceof AppEditChatTab) {
tab.chatId = -this.peerId; tab.chatId = -this.peerId;
} else { } else {
tab.peerId = this.peerId; tab.peerId = this.peerId;

2
src/config/app.ts

@ -12,7 +12,7 @@
const App = { const App = {
id: 1025907, id: 1025907,
hash: '452b0359b988148995f22ff0f4229750', hash: '452b0359b988148995f22ff0f4229750',
version: '0.5.0', version: '0.5.1',
langPackVersion: '0.1.6', langPackVersion: '0.1.6',
langPack: 'macos', langPack: 'macos',
langPackCode: 'en', langPackCode: 'en',

5
src/helpers/dom.ts

@ -694,3 +694,8 @@ export function replaceContent(elem: HTMLElement, node: string | Node) {
elem.append(node); elem.append(node);
} }
} }
export function setInnerHTML(elem: HTMLElement, html: string) {
elem.setAttribute('dir', 'auto');
elem.innerHTML = html;
}

151
src/helpers/slicedArray.ts

@ -4,6 +4,8 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import { MOUNT_CLASS_TO } from "../config/debug";
/** /**
* Descend sorted storage * Descend sorted storage
*/ */
@ -14,15 +16,19 @@ export enum SliceEnd {
None = 0, None = 0,
Top = 1, Top = 1,
Bottom = 2, Bottom = 2,
Both = 4 Both = SliceEnd.Top | SliceEnd.Bottom
}; };
export interface Slice extends Array<ItemType> { export interface Slice extends Array<ItemType> {
slicedArray: SlicedArray; //slicedArray: SlicedArray;
end: SliceEnd; end: SliceEnd;
isEnd: (side: SliceEnd) => boolean; isEnd: (side: SliceEnd) => boolean;
setEnd: (side: SliceEnd) => void; setEnd: (side: SliceEnd) => void;
unsetEnd: (side: SliceEnd) => void;
slice: (from?: number, to?: number) => Slice;
splice: (start: number, deleteCount: number, ...items: ItemType[]) => Slice;
} }
export interface SliceConstructor { export interface SliceConstructor {
@ -35,51 +41,84 @@ export default class SlicedArray {
private sliceConstructor: SliceConstructor; private sliceConstructor: SliceConstructor;
constructor() { constructor() {
const self = this; // @ts-ignore
this.sliceConstructor = class Slice extends Array<ItemType> implements Slice { this.sliceConstructor = SlicedArray.getSliceConstructor(this);
slicedArray: SlicedArray;
const first = this.constructSlice();
//first.setEnd(SliceEnd.Bottom);
this.slices = [first];
}
private static getSliceConstructor(slicedArray: SlicedArray) {
return class Slice extends Array<ItemType> implements Slice {
//slicedArray: SlicedArray;
end: SliceEnd = SliceEnd.None; end: SliceEnd = SliceEnd.None;
constructor(...items: ItemType[]) { /* constructor(...items: ItemType[]) {
super(...items); super(...items);
this.slicedArray = self; //this.slicedArray = slicedArray;
} } */
isEnd(side: SliceEnd) { isEnd(side: SliceEnd): boolean {
if(this.end & side) { if((this.end & side) === side) {
return true; return true;
} }/* else if(!this.slicedArray) {
return false;
} */
let isEnd = false;
if(side === SliceEnd.Top) { if(side === SliceEnd.Top) {
const slice = self.last; const slice = slicedArray.last;
return slice.end & side ? this.includes(slice[slice.length - 1]) || !slice.length : false; isEnd = slice.end & side ? this.includes(slice[slice.length - 1])/* || !slice.length */ : false;
} else if(side === SliceEnd.Bottom) { } else if(side === SliceEnd.Bottom) {
const slice = self.first; const slice = slicedArray.first;
return slice.end & side ? this.includes(slice[0]) || !slice.length : false; isEnd = slice.end & side ? this.includes(slice[0])/* || !slice.length */ : false;
}/* else if(side === SliceEnd.Both) { } else if(side === SliceEnd.Both) {
return this.isEnd(SliceEnd.Top) && this.isEnd(SliceEnd.Bottom);
}
} */ if(isEnd) {
this.setEnd(side);
}
return false; return isEnd;
} }
setEnd(side: SliceEnd) { setEnd(side: SliceEnd) {
this.end |= side; this.end |= side;
}
if(side !== SliceEnd.Both && this.end & SliceEnd.Top && this.end & SliceEnd.Bottom) { unsetEnd(side: SliceEnd) {
this.end |= SliceEnd.Both; this.end ^= side;
}
splice(start: number, deleteCount: number, ...items: ItemType[]) {
const ret = super.splice(start, deleteCount, ...items);
if(!this.length) {
const slices = slicedArray.slices as number[][];
const idx = slices.indexOf(this);
if(idx !== -1) {
if(slices.length === 1) { // left empty slice without ends
this.unsetEnd(SliceEnd.Both);
} else { // delete this slice
slices.splice(idx, 1);
} }
} }
} }
const first = this.constructSlice(); return ret;
first.setEnd(SliceEnd.Bottom); }
this.slices = [first]; }
} }
public constructSlice(...items: ItemType[]) { public constructSlice(...items: ItemType[]) {
//const slice = new Slice(this, ...items); //const slice = new Slice(this, ...items);
const slice = new this.sliceConstructor(...items); // can't pass items directly to constructor because first argument is length
const slice = new this.sliceConstructor(items.length);
for(let i = 0, length = items.length; i < length; ++i) {
slice[i] = items[i];
}
return slice; return slice;
// ! code below will slow execution in 15 times // ! code below will slow execution in 15 times
@ -128,7 +167,7 @@ export default class SlicedArray {
*/ */
} }
public insertSlice(slice: ItemType[]) { public insertSlice(slice: ItemType[], flatten = true) {
if(!slice.length) { if(!slice.length) {
return; return;
} }
@ -136,15 +175,15 @@ export default class SlicedArray {
const first = this.slices[0]; const first = this.slices[0];
if(!first.length) { if(!first.length) {
first.push(...slice); first.push(...slice);
return; return first;
} }
const lowerBound = slice[slice.length - 1]; const lowerBound = slice[slice.length - 1];
const upperBound = slice[0]; const upperBound = slice[0];
let foundSlice: Slice, lowerIndex = -1, upperIndex = -1; let foundSlice: Slice, lowerIndex = -1, upperIndex = -1, foundSliceIndex = 0;
for(let i = 0; i < this.slices.length; ++i) { for(; foundSliceIndex < this.slices.length; ++foundSliceIndex) {
foundSlice = this.slices[i]; foundSlice = this.slices[foundSliceIndex];
lowerIndex = foundSlice.indexOf(lowerBound); lowerIndex = foundSlice.indexOf(lowerBound);
upperIndex = foundSlice.indexOf(upperBound); upperIndex = foundSlice.indexOf(upperBound);
@ -173,16 +212,16 @@ export default class SlicedArray {
} }
this.slices.splice(insertIndex, 0, this.constructSlice(...slice)); this.slices.splice(insertIndex, 0, this.constructSlice(...slice));
foundSliceIndex = insertIndex;
} }
this.flatten(); if(flatten) {
return this.flatten(foundSliceIndex);
} }
private flatten() {
if(this.slices.length < 2) {
return;
} }
private flatten(foundSliceIndex: number) {
if(this.slices.length >= 2) {
for(let i = 0, length = this.slices.length; i < (length - 1); ++i) { for(let i = 0, length = this.slices.length; i < (length - 1); ++i) {
const prevSlice = this.slices[i]; const prevSlice = this.slices[i];
const nextSlice = this.slices[i + 1]; const nextSlice = this.slices[i + 1];
@ -191,11 +230,20 @@ export default class SlicedArray {
if(upperIndex !== -1) { if(upperIndex !== -1) {
prevSlice.setEnd(nextSlice.end); prevSlice.setEnd(nextSlice.end);
this.slices.splice(i + 1, 1); this.slices.splice(i + 1, 1);
length--;
this.insertSlice(nextSlice); if(i < foundSliceIndex) {
--foundSliceIndex;
}
--length; // respect array bounds
--i; // repeat from the same place
this.insertSlice(nextSlice, false);
}
} }
} }
return this.slices[foundSliceIndex];
} }
// * // *
@ -217,7 +265,7 @@ export default class SlicedArray {
} }
public findSlice(item: ItemType) { public findSlice(item: ItemType) {
for(let i = 0; i < this.slices.length; ++i) { for(let i = 0, length = this.slices.length; i < length; ++i) {
const slice = this.slices[i]; const slice = this.slices[i];
const index = slice.indexOf(item); const index = slice.indexOf(item);
if(index !== -1) { if(index !== -1) {
@ -295,6 +343,8 @@ export default class SlicedArray {
const topWasMeantToLoad = add_offset < 0 ? limit + add_offset : limit; const topWasMeantToLoad = add_offset < 0 ? limit + add_offset : limit;
const bottomWasMeantToLoad = Math.abs(add_offset); const bottomWasMeantToLoad = Math.abs(add_offset);
// can use 'slice' here to check because if it's end, then 'sliced' is out of 'slice'
// useful when there is only 1 message in chat on its reopening
const topFulfilled = (slice.length - sliceOffset) >= topWasMeantToLoad || (slice.isEnd(SliceEnd.Top) ? (sliced.setEnd(SliceEnd.Top), true) : false); const topFulfilled = (slice.length - sliceOffset) >= topWasMeantToLoad || (slice.isEnd(SliceEnd.Top) ? (sliced.setEnd(SliceEnd.Top), true) : false);
const bottomFulfilled = (sliceOffset - bottomWasMeantToLoad) >= 0 || (slice.isEnd(SliceEnd.Bottom) ? (sliced.setEnd(SliceEnd.Bottom), true) : false); const bottomFulfilled = (sliceOffset - bottomWasMeantToLoad) >= 0 || (slice.isEnd(SliceEnd.Bottom) ? (sliced.setEnd(SliceEnd.Bottom), true) : false);
@ -308,19 +358,40 @@ export default class SlicedArray {
} }
public unshift(...items: ItemType[]) { public unshift(...items: ItemType[]) {
this.first.unshift(...items); let slice = this.first;
if(!slice.length) {
slice.setEnd(SliceEnd.Bottom);
} else if(!slice.isEnd(SliceEnd.Bottom)) {
slice = this.constructSlice();
slice.setEnd(SliceEnd.Bottom);
this.slices.unshift(slice);
}
slice.unshift(...items);
} }
public push(...items: ItemType[]) { public push(...items: ItemType[]) {
this.last.push(...items); let slice = this.last;
if(!slice.length) {
slice.setEnd(SliceEnd.Top);
} else if(!slice.isEnd(SliceEnd.Top)) {
slice = this.constructSlice();
slice.setEnd(SliceEnd.Top);
this.slices.push(slice);
}
slice.push(...items);
} }
public delete(item: ItemType) { public delete(item: ItemType) {
const found = this.findSlice(item); const found = this.findSlice(item);
if(found) { if(found) {
found.slice.splice(found.index, 1); found.slice.splice(found.index, 1);
return true;
} }
return false;
} }
} }
(window as any).slicedArray = new SlicedArray(); MOUNT_CLASS_TO && (MOUNT_CLASS_TO.SlicedArray = SlicedArray);

18
src/lang.ts

@ -133,6 +133,7 @@ const lang = {
"AttachAudio": "Voice message", "AttachAudio": "Voice message",
"AttachRound": "Video message", "AttachRound": "Video message",
"AttachGame": "Game", "AttachGame": "Game",
"Bot": "bot",
//"ChannelJoined": "You joined this channel", //"ChannelJoined": "You joined this channel",
"ChannelMegaJoined": "You joined this group", "ChannelMegaJoined": "You joined this group",
"Channel.DescriptionPlaceholder": "Description (optional)", "Channel.DescriptionPlaceholder": "Description (optional)",
@ -408,6 +409,14 @@ const lang = {
"OpenUrlAlert2": "Do you want to open %1$s?", "OpenUrlAlert2": "Do you want to open %1$s?",
"FilterNoChatsToDisplay": "Folder is empty", "FilterNoChatsToDisplay": "Folder is empty",
"FilterNoChatsToDisplayInfo": "No chats currently belong to this folder.", "FilterNoChatsToDisplayInfo": "No chats currently belong to this folder.",
"SupportStatus": "support",
"Lately": "last seen recently",
"WithinAWeek": "last seen within a week",
"WithinAMonth": "last seen within a month",
"ALongTimeAgo": "last seen a long time ago",
"Online": "online",
"MessageScheduleSend": "Send Now",
"MessageScheduleEditTime": "Reschedule",
// * macos // * macos
"AccountSettings.Filters": "Chat Folders", "AccountSettings.Filters": "Chat Folders",
@ -420,8 +429,6 @@ const lang = {
"Channel.UsernameAboutGroup": "People can share this link with others and find your group using Telegram search.", "Channel.UsernameAboutGroup": "People can share this link with others and find your group using Telegram search.",
"Chat.CopySelectedText": "Copy Selected Text", "Chat.CopySelectedText": "Copy Selected Text",
"Chat.Confirm.Unpin": "Would you like to unpin this message?", "Chat.Confirm.Unpin": "Would you like to unpin this message?",
"Chat.Context.Scheduled.SendNow": "Send Now",
"Chat.Context.Scheduled.Reschedule": "Re-schedule",
"Chat.Date.ScheduledFor": "Scheduled for %@", "Chat.Date.ScheduledFor": "Scheduled for %@",
//"Chat.Date.ScheduledUntilOnline": "Scheduled until online", //"Chat.Date.ScheduledUntilOnline": "Scheduled until online",
"Chat.Date.ScheduledForToday": "Scheduled for today", "Chat.Date.ScheduledForToday": "Scheduled for today",
@ -559,12 +566,7 @@ const lang = {
"Peer.Activity.Chat.Multi.SendingFile1": "%@ and %d others are sending files", "Peer.Activity.Chat.Multi.SendingFile1": "%@ and %d others are sending files",
"Peer.ServiceNotifications": "service notifications", "Peer.ServiceNotifications": "service notifications",
"Peer.RepliesNotifications": "Reply Notifications", "Peer.RepliesNotifications": "Reply Notifications",
"Peer.Status.online": "online",
"Peer.Status.recently": "last seen recently",
"Peer.Status.justNow": "last seen just now", "Peer.Status.justNow": "last seen just now",
"Peer.Status.lastWeek": "last seen within a week",
"Peer.Status.lastMonth": "last seen within a month",
"Peer.Status.longTimeAgo": "last seen a long time ago",
"Peer.Status.Today": "today", "Peer.Status.Today": "today",
"Peer.Status.Yesterday": "yesterday", "Peer.Status.Yesterday": "yesterday",
"Peer.Status.LastSeenAt": "last seen %@ at %@", "Peer.Status.LastSeenAt": "last seen %@ at %@",
@ -614,8 +616,6 @@ const lang = {
"one_value": "Send Video", "one_value": "Send Video",
"other_value": "Send %d Videos" "other_value": "Send %d Videos"
}, },
"Presence.bot": "bot",
"Presence.Support": "support",
"PrivacyAndSecurity.Item.On": "On", "PrivacyAndSecurity.Item.On": "On",
"PrivacyAndSecurity.Item.Off": "Off", "PrivacyAndSecurity.Item.Off": "Off",
"PrivacySettings.VoiceCalls": "Calls", "PrivacySettings.VoiceCalls": "Calls",

1
src/lib/appManagers/appDialogsManager.ts

@ -1357,6 +1357,7 @@ export class AppDialogsManager {
const span = document.createElement('span'); const span = document.createElement('span');
span.classList.add('user-last-message'); span.classList.add('user-last-message');
span.setAttribute('dir', 'auto');
//captionDiv.append(titleSpan); //captionDiv.append(titleSpan);
//captionDiv.append(span); //captionDiv.append(span);

8
src/lib/appManagers/appImManager.ts

@ -50,6 +50,7 @@ import { copy, getObjectKeysAndSort } from '../../helpers/object';
import { getFilesFromEvent } from '../../helpers/files'; import { getFilesFromEvent } from '../../helpers/files';
import PeerTitle from '../../components/peerTitle'; import PeerTitle from '../../components/peerTitle';
import PopupPeer from '../../components/popups/peer'; import PopupPeer from '../../components/popups/peer';
import { SliceEnd } from '../../helpers/slicedArray';
//console.log('appImManager included33!'); //console.log('appImManager included33!');
@ -445,10 +446,11 @@ export class AppImManager {
return; return;
} else if(e.code === 'ArrowUp') { } else if(e.code === 'ArrowUp') {
if(!chat.input.editMsgId && chat.input.isInputEmpty()) { if(!chat.input.editMsgId && chat.input.isInputEmpty()) {
const history = appMessagesManager.getHistoryStorage(chat.peerId, chat.threadId); const historyStorage = appMessagesManager.getHistoryStorage(chat.peerId, chat.threadId);
if(history.history.length) { const slice = historyStorage.history.slice;
if(slice.isEnd(SliceEnd.Bottom) && slice.length) {
let goodMid: number; let goodMid: number;
for(const mid of history.history.slice) { for(const mid of slice) {
const message = chat.getMessage(mid); const message = chat.getMessage(mid);
const good = this.myId === chat.peerId ? message.fromId === this.myId : message.pFlags.out; const good = this.myId === chat.peerId ? message.fromId === this.myId : message.pFlags.out;

176
src/lib/appManagers/appMessagesManager.ts

@ -14,7 +14,7 @@ import ProgressivePreloader from "../../components/preloader";
import { CancellablePromise, deferredPromise } from "../../helpers/cancellablePromise"; import { CancellablePromise, deferredPromise } from "../../helpers/cancellablePromise";
import { tsNow } from "../../helpers/date"; import { tsNow } from "../../helpers/date";
import { createPosterForVideo } from "../../helpers/files"; import { createPosterForVideo } from "../../helpers/files";
import { copy, defineNotNumerableProperties, getObjectKeysAndSort } from "../../helpers/object"; import { copy, getObjectKeysAndSort } from "../../helpers/object";
import { randomLong } from "../../helpers/random"; import { randomLong } from "../../helpers/random";
import { splitStringByLength, limitSymbols, escapeRegExp } from "../../helpers/string"; import { splitStringByLength, limitSymbols, escapeRegExp } from "../../helpers/string";
import { Chat, ChatFull, Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessageFwdHeader, MessageMedia, MessageReplies, MessageReplyHeader, MessagesDialogs, MessagesFilter, MessagesMessages, MethodDeclMap, NotifyPeer, PeerNotifySettings, PhotoSize, SendMessageAction, Update, Photo } from "../../layer"; import { Chat, ChatFull, Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessageFwdHeader, MessageMedia, MessageReplies, MessageReplyHeader, MessagesDialogs, MessagesFilter, MessagesMessages, MethodDeclMap, NotifyPeer, PeerNotifySettings, PhotoSize, SendMessageAction, Update, Photo } from "../../layer";
@ -168,7 +168,7 @@ export class AppMessagesManager {
public migratedFromTo: {[peerId: number]: number} = {}; public migratedFromTo: {[peerId: number]: number} = {};
public migratedToFrom: {[peerId: number]: number} = {}; public migratedToFrom: {[peerId: number]: number} = {};
public newMessagesHandlePromise = 0; public newMessagesHandleTimeout = 0;
public newMessagesToHandle: {[peerId: string]: Set<number>} = {}; public newMessagesToHandle: {[peerId: string]: Set<number>} = {};
public newDialogsHandlePromise: Promise<any>; public newDialogsHandlePromise: Promise<any>;
public newDialogsToHandle: {[peerId: string]: Dialog} = {}; public newDialogsToHandle: {[peerId: string]: Dialog} = {};
@ -1301,13 +1301,16 @@ export class AppMessagesManager {
/* if(options.threadId && this.threadsStorage[peerId]) { /* if(options.threadId && this.threadsStorage[peerId]) {
delete this.threadsStorage[peerId][options.threadId]; delete this.threadsStorage[peerId][options.threadId];
} */ } */
if(options.threadId) { const storages: HistoryStorage[] = [
const historyStorage = this.getHistoryStorage(peerId, options.threadId); this.getHistoryStorage(peerId),
historyStorage.history.unshift(messageId); options.threadId ? this.getHistoryStorage(peerId, options.threadId) : undefined
} ];
const historyStorage = this.getHistoryStorage(peerId); for(const storage of storages) {
historyStorage.history.unshift(messageId); if(storage) {
storage.history.unshift(messageId);
}
}
//if(!options.isGroupedItem) { //if(!options.isGroupedItem) {
this.saveMessages([message], {storage, isOutgoing: true}); this.saveMessages([message], {storage, isOutgoing: true});
@ -1514,7 +1517,6 @@ export class AppMessagesManager {
if(pendingData) { if(pendingData) {
const {peerId, tempId, storage} = pendingData; const {peerId, tempId, storage} = pendingData;
const historyStorage = this.getHistoryStorage(peerId); const historyStorage = this.getHistoryStorage(peerId);
const pos = historyStorage.history.findSlice(tempId);
apiUpdatesManager.processUpdateMessage({ apiUpdatesManager.processUpdateMessage({
_: 'updateShort', _: 'updateShort',
@ -1524,9 +1526,7 @@ export class AppMessagesManager {
} }
}); });
if(pos) { historyStorage.history.delete(tempId);
pos.slice.splice(pos.index, 1);
}
delete this.pendingByRandomId[randomId]; delete this.pendingByRandomId[randomId];
delete storage[tempId]; delete storage[tempId];
@ -1627,7 +1627,7 @@ export class AppMessagesManager {
// ! ВНИМАНИЕ: ОЧЕНЬ СЛОЖНАЯ ЛОГИКА: // ! ВНИМАНИЕ: ОЧЕНЬ СЛОЖНАЯ ЛОГИКА:
// ! если делать запрос сначала по папке 0, потом по папке 1, по индексу 0 в массиве будет один и тот же диалог, с dialog.pFlags.pinned, ЛОЛ??? // ! если делать запрос сначала по папке 0, потом по папке 1, по индексу 0 в массиве будет один и тот же диалог, с dialog.pFlags.pinned, ЛОЛ???
// ! т.е., с запросом folder_id: 1, и exclude_pinned: 0, в результате будут ещё и закреплённые с папки 0 // ! т.е., с запросом folder_id: 1, и exclude_pinned: 0, в результате будут ещё и закреплённые с папки 0
return apiManager.invokeApi('messages.getDialogs', { return apiManager.invokeApiSingle('messages.getDialogs', {
folder_id: folderId, folder_id: folderId,
offset_date: offsetDate, offset_date: offsetDate,
offset_id: offsetId, offset_id: offsetId,
@ -3271,7 +3271,7 @@ export class AppMessagesManager {
} }
public getDiscussionMessage(peerId: number, mid: number) { public getDiscussionMessage(peerId: number, mid: number) {
return apiManager.invokeApi('messages.getDiscussionMessage', { return apiManager.invokeApiSingle('messages.getDiscussionMessage', {
peer: appPeersManager.getInputPeerById(peerId), peer: appPeersManager.getInputPeerById(peerId),
msg_id: this.getServerMessageId(mid) msg_id: this.getServerMessageId(mid)
}).then(result => { }).then(result => {
@ -3295,9 +3295,20 @@ export class AppMessagesManager {
}); });
} }
private handleNewMessage(peerId: number, mid: number) {
if(this.newMessagesToHandle[peerId] === undefined) {
this.newMessagesToHandle[peerId] = new Set();
}
this.newMessagesToHandle[peerId].add(mid);
if(!this.newMessagesHandleTimeout) {
this.newMessagesHandleTimeout = window.setTimeout(this.handleNewMessages, 0);
}
}
handleNewMessages = () => { handleNewMessages = () => {
clearTimeout(this.newMessagesHandlePromise); clearTimeout(this.newMessagesHandleTimeout);
this.newMessagesHandlePromise = 0; this.newMessagesHandleTimeout = 0;
rootScope.broadcast('history_multiappend', this.newMessagesToHandle); rootScope.broadcast('history_multiappend', this.newMessagesToHandle);
this.newMessagesToHandle = {}; this.newMessagesToHandle = {};
@ -3404,6 +3415,7 @@ export class AppMessagesManager {
return promise; return promise;
} }
// TODO: cancel notification by peer when this function is being called
public readHistory(peerId: number, maxId = 0, threadId?: number, force = false) { public readHistory(peerId: number, maxId = 0, threadId?: number, force = false) {
//return Promise.resolve(); //return Promise.resolve();
// console.trace('start read') // console.trace('start read')
@ -3452,7 +3464,7 @@ export class AppMessagesManager {
_: 'updateReadChannelInbox', _: 'updateReadChannelInbox',
max_id: maxId, max_id: maxId,
channel_id: -peerId channel_id: -peerId
} } as Update.updateReadChannelInbox
}); });
} else { } else {
if(!historyStorage.readPromise) { if(!historyStorage.readPromise) {
@ -3477,21 +3489,10 @@ export class AppMessagesManager {
_: 'updateReadHistoryInbox', _: 'updateReadHistoryInbox',
max_id: maxId, max_id: maxId,
peer: appPeersManager.getOutputPeer(peerId) peer: appPeersManager.getOutputPeer(peerId)
} } as Update.updateReadHistoryInbox
}); });
} }
if(!threadId && historyStorage && historyStorage.history.length) {
const slice = historyStorage.history.slice;
for(const mid of slice) {
const message = this.getMessageByPeer(peerId, mid);
if(message && !message.pFlags.out) {
message.pFlags.unread = false;
appNotificationsManager.cancel('msg' + mid);
}
}
}
appNotificationsManager.soundReset(appPeersManager.getPeerString(peerId)); appNotificationsManager.soundReset(appPeersManager.getPeerString(peerId));
if(historyStorage.readPromise) { if(historyStorage.readPromise) {
@ -3534,7 +3535,7 @@ export class AppMessagesManager {
_: 'updateChannelReadMessagesContents', _: 'updateChannelReadMessagesContents',
channel_id: channelId, channel_id: channelId,
messages: msgIds messages: msgIds
} } as Update.updateChannelReadMessagesContents
}); });
}); });
} else { } else {
@ -3548,7 +3549,7 @@ export class AppMessagesManager {
messages: msgIds, messages: msgIds,
pts: affectedMessages.pts, pts: affectedMessages.pts,
pts_count: affectedMessages.pts_count pts_count: affectedMessages.pts_count
} } as Update.updateReadMessagesContents
}); });
}); });
} }
@ -3694,14 +3695,19 @@ export class AppMessagesManager {
return false; return false;
} }
const history = historyStorage.history.slice; // * catch situation with disconnect. if message's id is lower than we already have (in bottom end slice), will sort it
const topMsgId = history[0]; const firstSlice = historyStorage.history.first;
history.unshift(message.mid); if(firstSlice.isEnd(SliceEnd.Bottom)) {
if(message.mid < topMsgId) { let i = 0;
//this.log.error('this should\'nt have happenned!', message, history); for(const length = firstSlice.length; i < length; ++i) {
history.sort((a, b) => { if(message.mid > firstSlice[i]) {
return b - a; break;
}); }
}
firstSlice.splice(i, 0, message.mid);
} else {
historyStorage.history.unshift(message.mid);
} }
if(historyStorage.count !== null) { if(historyStorage.count !== null) {
@ -3717,14 +3723,7 @@ export class AppMessagesManager {
} }
if(!pendingMessage) { if(!pendingMessage) {
if(this.newMessagesToHandle[peerId] === undefined) { this.handleNewMessage(peerId, message.mid);
this.newMessagesToHandle[peerId] = new Set();
}
this.newMessagesToHandle[peerId].add(message.mid);
if(!this.newMessagesHandlePromise) {
this.newMessagesHandlePromise = window.setTimeout(this.handleNewMessages, 0);
}
} }
if(isLocalThreadUpdate) { if(isLocalThreadUpdate) {
@ -4471,7 +4470,7 @@ export class AppMessagesManager {
return Promise.resolve(Object.keys(storage).map(id => +id)); return Promise.resolve(Object.keys(storage).map(id => +id));
} }
return apiManager.invokeApi('messages.getScheduledHistory', { return apiManager.invokeApiSingle('messages.getScheduledHistory', {
peer: appPeersManager.getInputPeerById(peerId), peer: appPeersManager.getInputPeerById(peerId),
hash: 0 hash: 0
}).then(historyResult => { }).then(historyResult => {
@ -4506,6 +4505,36 @@ export class AppMessagesManager {
}); });
} }
public isFetchIntervalNeeded(peerId: number) {
return peerId < 0 && !appChatsManager.isInChat(peerId);
}
public async getNewHistory(peerId: number, threadId?: number) {
if(!this.isFetchIntervalNeeded(peerId)) {
return;
}
const historyStorage = this.getHistoryStorage(peerId, threadId);
const slice = historyStorage.history.slice;
if(!slice.isEnd(SliceEnd.Bottom)) {
return;
}
delete historyStorage.maxId;
slice.unsetEnd(SliceEnd.Bottom);
let historyResult = this.getHistory(peerId, slice[0], 0, 50, threadId);
if(historyResult instanceof Promise) {
historyResult = await historyResult;
}
for(let i = 0, length = historyResult.history.length; i < length; ++i) {
this.handleNewMessage(peerId, historyResult.history[i]);
}
return historyStorage;
}
/** /**
* * https://core.telegram.org/api/offsets, offset_id is inclusive * * https://core.telegram.org/api/offsets, offset_id is inclusive
*/ */
@ -4567,7 +4596,7 @@ export class AppMessagesManager {
} }
const haveSlice = historyStorage.history.sliceMe(maxId, offset, limit); const haveSlice = historyStorage.history.sliceMe(maxId, offset, limit);
if(haveSlice && (haveSlice.slice.length === limit || (haveSlice.fulfilled & SliceEnd.Both))) { if(haveSlice && (haveSlice.slice.length === limit || (haveSlice.fulfilled & SliceEnd.Both) === SliceEnd.Both)) {
return { return {
count: historyStorage.count, count: historyStorage.count,
history: haveSlice.slice, history: haveSlice.slice,
@ -4587,10 +4616,15 @@ export class AppMessagesManager {
public fillHistoryStorage(peerId: number, offset_id: number, limit: number, add_offset: number, historyStorage: HistoryStorage, threadId?: number): Promise<void> { public fillHistoryStorage(peerId: number, offset_id: number, limit: number, add_offset: number, historyStorage: HistoryStorage, threadId?: number): Promise<void> {
return this.requestHistory(peerId, offset_id, limit, add_offset, undefined, threadId).then((historyResult) => { return this.requestHistory(peerId, offset_id, limit, add_offset, undefined, threadId).then((historyResult) => {
historyStorage.count = (historyResult as MessagesMessages.messagesMessagesSlice).count || historyResult.messages.length; const {offset_id_offset, count, messages} = historyResult as MessagesMessages.messagesMessagesSlice;
historyStorage.count = count || messages.length;
const offsetIdOffset = offset_id_offset || 0;
const topWasMeantToLoad = add_offset < 0 ? limit + add_offset : limit;
const offsetIdOffset = (historyResult as MessagesMessages.messagesMessagesSlice).offset_id_offset || 0; const isTopEnd = offsetIdOffset >= (historyStorage.count - topWasMeantToLoad) || historyStorage.count < topWasMeantToLoad;
const isTopEnd = offsetIdOffset >= (historyStorage.count - limit) || historyStorage.count < (limit + add_offset); const isBottomEnd = !offsetIdOffset || (add_offset < 0 && (offsetIdOffset + add_offset) <= 0);
/* if(!maxId && historyResult.messages.length) { /* if(!maxId && historyResult.messages.length) {
maxId = this.incrementMessageId((historyResult.messages[0] as MyMessage).mid, 1); maxId = this.incrementMessageId((historyResult.messages[0] as MyMessage).mid, 1);
@ -4598,13 +4632,14 @@ export class AppMessagesManager {
const wasTotalCount = historyStorage.history.length; */ const wasTotalCount = historyStorage.history.length; */
historyResult.messages.forEach((message) => { const mids = messages.map((message) => {
if(this.mergeReplyKeyboard(historyStorage, message)) { if(this.mergeReplyKeyboard(historyStorage, message)) {
rootScope.broadcast('history_reply_markup', {peerId}); rootScope.broadcast('history_reply_markup', {peerId});
} }
return (message as MyMessage).mid;
}); });
const mids = historyResult.messages.map((message) => (message as MyMessage).mid);
// * add bound manually. // * add bound manually.
// * offset_id will be inclusive only if there is 'add_offset' <= -1 (-1 - will only include the 'offset_id') // * offset_id will be inclusive only if there is 'add_offset' <= -1 (-1 - will only include the 'offset_id')
if(offset_id && !mids.includes(offset_id) && offsetIdOffset < historyStorage.count) { if(offset_id && !mids.includes(offset_id) && offsetIdOffset < historyStorage.count) {
@ -4618,10 +4653,16 @@ export class AppMessagesManager {
mids.splice(i, 0, offset_id); mids.splice(i, 0, offset_id);
} }
historyStorage.history.insertSlice(mids); const slice = historyStorage.history.insertSlice(mids);
if(slice) {
if(isTopEnd) { if(isTopEnd) {
historyStorage.history.last.setEnd(SliceEnd.Top); slice.setEnd(SliceEnd.Top);
}
if(isBottomEnd) {
slice.setEnd(SliceEnd.Bottom);
historyStorage.maxId = slice[0]; // ! WARNING
}
} }
/* const isBackLimit = offset < 0 && -offset !== fullLimit; /* const isBackLimit = offset < 0 && -offset !== fullLimit;
@ -4681,7 +4722,7 @@ export class AppMessagesManager {
options.msg_id = this.getServerMessageId(threadId) || 0; options.msg_id = this.getServerMessageId(threadId) || 0;
} }
const promise: ReturnType<AppMessagesManager['requestHistory']> = apiManager.invokeApi(threadId ? 'messages.getReplies' : 'messages.getHistory', options, { const promise: ReturnType<AppMessagesManager['requestHistory']> = apiManager.invokeApiSingle(threadId ? 'messages.getReplies' : 'messages.getHistory', options, {
//timeout: APITIMEOUT, //timeout: APITIMEOUT,
noErrorBox: true noErrorBox: true
}) as any; }) as any;
@ -4699,22 +4740,25 @@ export class AppMessagesManager {
apiUpdatesManager.addChannelState(-peerId, (historyResult as MessagesMessages.messagesChannelMessages).pts); apiUpdatesManager.addChannelState(-peerId, (historyResult as MessagesMessages.messagesChannelMessages).pts);
} }
let length = historyResult.messages.length; let length = historyResult.messages.length, count = (historyResult as MessagesMessages.messagesMessagesSlice).count;
if(length && historyResult.messages[length - 1].deleted) { if(length && historyResult.messages[length - 1].deleted) {
historyResult.messages.splice(length - 1, 1); historyResult.messages.splice(length - 1, 1);
length--; length--;
(historyResult as MessagesMessages.messagesMessagesSlice).count--; count--;
} }
// will load more history if last message is album grouped (because it can be not last item) // will load more history if last message is album grouped (because it can be not last item)
const historyStorage = this.getHistoryStorage(peerId, threadId);
// historyResult.messages: desc sorted // historyResult.messages: desc sorted
if(length && (historyResult.messages[length - 1] as Message.message).grouped_id const historyStorage = this.getHistoryStorage(peerId, threadId);
&& (historyStorage.history.length + historyResult.messages.length) < (historyResult as MessagesMessages.messagesMessagesSlice).count) { const oldestMessage: Message.message = historyResult.messages[length - 1] as any;
return this.requestHistory(peerId, (historyResult.messages[length - 1] as Message.message).mid, 10, 0, offsetDate, threadId).then((_historyResult) => { if(length && oldestMessage.grouped_id) {
const foundSlice = historyStorage.history.findSlice(oldestMessage.mid);
if(foundSlice && (foundSlice.slice.length + historyResult.messages.length) < count) {
return this.requestHistory(peerId, oldestMessage.mid, 10, 0, offsetDate, threadId).then((_historyResult) => {
return historyResult; return historyResult;
}); });
} }
}
return historyResult; return historyResult;
}, (error) => { }, (error) => {
@ -4760,12 +4804,12 @@ export class AppMessagesManager {
let promise: Promise<MethodDeclMap['channels.getMessages']['res'] | MethodDeclMap['messages.getMessages']['res']>; let promise: Promise<MethodDeclMap['channels.getMessages']['res'] | MethodDeclMap['messages.getMessages']['res']>;
if(+peerId < 0 && appPeersManager.isChannel(+peerId)) { if(+peerId < 0 && appPeersManager.isChannel(+peerId)) {
promise = apiManager.invokeApi('channels.getMessages', { promise = apiManager.invokeApiSingle('channels.getMessages', {
channel: appChatsManager.getChannelInput(-+peerId), channel: appChatsManager.getChannelInput(-+peerId),
id: msgIds id: msgIds
}); });
} else { } else {
promise = apiManager.invokeApi('messages.getMessages', { promise = apiManager.invokeApiSingle('messages.getMessages', {
id: msgIds id: msgIds
}); });
} }

2
src/lib/appManagers/appPhotosManager.ts

@ -13,7 +13,7 @@ import type { DownloadOptions } from "../mtproto/apiFileManager";
import { bytesFromHex } from "../../helpers/bytes"; import { bytesFromHex } from "../../helpers/bytes";
import { CancellablePromise } from "../../helpers/cancellablePromise"; import { CancellablePromise } from "../../helpers/cancellablePromise";
import { getFileNameByLocation } from "../../helpers/fileName"; import { getFileNameByLocation } from "../../helpers/fileName";
import { safeReplaceArrayInObject, defineNotNumerableProperties, isObject } from "../../helpers/object"; import { safeReplaceArrayInObject, isObject } from "../../helpers/object";
import { isSafari } from "../../helpers/userAgent"; import { isSafari } from "../../helpers/userAgent";
import { InputFileLocation, InputMedia, Photo, PhotoSize, PhotosPhotos } from "../../layer"; import { InputFileLocation, InputMedia, Photo, PhotoSize, PhotosPhotos } from "../../layer";
import apiManager from "../mtproto/mtprotoworker"; import apiManager from "../mtproto/mtprotoworker";

14
src/lib/appManagers/appUsersManager.ts

@ -431,7 +431,7 @@ export class AppUsersManager {
break; break;
default: { default: {
if(this.isBot(userId)) { if(this.isBot(userId)) {
key = 'Presence.bot'; key = 'Bot';
break; break;
} }
@ -442,23 +442,23 @@ export class AppUsersManager {
} }
if(user.pFlags.support) { if(user.pFlags.support) {
key = 'Presence.Support'; key = 'SupportStatus';
break; break;
} }
switch(user.status?._) { switch(user.status?._) {
case 'userStatusRecently': { case 'userStatusRecently': {
key = 'Peer.Status.recently'; key = 'Lately';
break; break;
} }
case 'userStatusLastWeek': { case 'userStatusLastWeek': {
key = 'Peer.Status.lastWeek'; key = 'WithinAWeek';
break; break;
} }
case 'userStatusLastMonth': { case 'userStatusLastMonth': {
key = 'Peer.Status.lastMonth'; key = 'WithinAMonth';
break; break;
} }
@ -487,12 +487,12 @@ export class AppUsersManager {
} }
case 'userStatusOnline': { case 'userStatusOnline': {
key = 'Peer.Status.online'; key = 'Online';
break; break;
} }
default: { default: {
key = 'Peer.Status.longTimeAgo'; key = 'ALongTimeAgo';
break; break;
} }
} }

7
src/lib/storages/dialogs.ts

@ -25,6 +25,7 @@ import { forEachReverse, insertInDescendSortedArray } from "../../helpers/array"
import rootScope from "../rootScope"; import rootScope from "../rootScope";
import { safeReplaceObject } from "../../helpers/object"; import { safeReplaceObject } from "../../helpers/object";
import { AppStateManager } from "../appManagers/appStateManager"; import { AppStateManager } from "../appManagers/appStateManager";
import { SliceEnd } from "../../helpers/slicedArray";
export default class DialogsStorage { export default class DialogsStorage {
private storage: AppStateManager['storages']['dialogs']; private storage: AppStateManager['storages']['dialogs'];
@ -490,13 +491,17 @@ export default class DialogsStorage {
} }
const historyStorage = this.appMessagesManager.getHistoryStorage(peerId); const historyStorage = this.appMessagesManager.getHistoryStorage(peerId);
const slice = historyStorage.history.slice;
/* if(historyStorage === undefined) { // warning /* if(historyStorage === undefined) { // warning
historyStorage.history.push(mid); historyStorage.history.push(mid);
if(this.mergeReplyKeyboard(historyStorage, message)) { if(this.mergeReplyKeyboard(historyStorage, message)) {
rootScope.broadcast('history_reply_markup', {peerId}); rootScope.broadcast('history_reply_markup', {peerId});
} }
} else */if(!historyStorage.history.slice.length) { } else */if(!slice.length) {
historyStorage.history.unshift(mid); historyStorage.history.unshift(mid);
} else if(!slice.isEnd(SliceEnd.Bottom)) { // * this will probably never happen, however, if it does, then it will fix slice with top_message
const slice = historyStorage.history.insertSlice([mid]);
slice.setEnd(SliceEnd.Bottom);
} }
historyStorage.maxId = mid; historyStorage.maxId = mid;

2
src/scss/partials/_button.scss

@ -384,7 +384,7 @@
--size: 54px; --size: 54px;
border-radius: 50%; border-radius: 50%;
height: var(--size); height: var(--size);
width: var(--size) !important; width: var(--size);
line-height: var(--size); line-height: var(--size);
@include respond-to(handhelds) { @include respond-to(handhelds) {

20
src/scss/partials/_chatBubble.scss

@ -995,6 +995,7 @@ $bubble-margin: .25rem;
position: relative !important; position: relative !important;
height: 0px !important; height: 0px !important;
visibility: hidden !important; visibility: hidden !important;
float: none;
.inner { .inner {
visibility: hidden !important; visibility: hidden !important;
@ -1250,6 +1251,8 @@ $bubble-margin: .25rem;
/* display: inline-flex; /* display: inline-flex;
align-items: center; */ align-items: center; */
height: 12px; height: 12px;
direction: ltr;
float: right; // * rtl fix
i { i {
font-size: 1.15rem; font-size: 1.15rem;
@ -1284,6 +1287,10 @@ $bubble-margin: .25rem;
} }
} }
&.webpage .time {
float: none;
}
.video-time { .video-time {
position: absolute; position: absolute;
top: 3px; top: 3px;
@ -1893,7 +1900,7 @@ $bubble-margin: .25rem;
margin-left: -4px; margin-left: -4px;
.inner { .inner {
color: var(--message-out-primary-color); color: var(--message-out-status-color);
bottom: 4px; bottom: 4px;
} }
@ -1902,9 +1909,14 @@ $bubble-margin: .25rem;
//vertical-align: middle; //vertical-align: middle;
margin-left: 1px; margin-left: 1px;
line-height: 16px; // of message line-height: 16px; // of message
color: var(--message-out-primary-color);
} }
} }
&.is-message-empty .time .inner {
color: var(--message-out-primary-color);
}
/* &.is-message-empty .time:after { /* &.is-message-empty .time:after {
margin-bottom: 1px; margin-bottom: 1px;
} */ } */
@ -1954,7 +1966,7 @@ $bubble-margin: .25rem;
} }
&-time, &-subtitle { &-time, &-subtitle {
color: var(--message-out-primary-color); color: var(--message-out-status-color);
} }
&-toggle, &-download { &-toggle, &-download {
@ -2002,8 +2014,8 @@ $bubble-margin: .25rem;
} }
} }
.audio-subtitle, .contact-number, .document-size { .contact-number, .document-size {
color: var(--message-out-primary-color); color: var(--message-out-status-color);
} }
poll-element { poll-element {

1
src/scss/partials/_selector.scss

@ -15,6 +15,7 @@
} }
.selector { .selector {
width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

4
src/scss/partials/_sidebar.scss

@ -57,8 +57,8 @@
margin-top: 60px; margin-top: 60px;
} */ } */
> div { /* > div {
width: 100%; width: 100%;
} } */
} }
} }

2
src/scss/style.scss

@ -170,6 +170,7 @@ $chat-input-inner-padding-handhelds: .25rem;
@include splitColor(message-out-background-color, #eeffde, true, true); @include splitColor(message-out-background-color, #eeffde, true, true);
--message-out-link-color: var(--link-color); --message-out-link-color: var(--link-color);
--message-out-primary-color: #4fae4e; --message-out-primary-color: #4fae4e;
--message-out-status-color: var(--message-out-primary-color);
--message-out-audio-play-button-color: #fff; --message-out-audio-play-button-color: #fff;
// * Day theme end // * Day theme end
@ -215,6 +216,7 @@ html.night {
@include splitColor(message-out-background-color, #8774E1, true, true); @include splitColor(message-out-background-color, #8774E1, true, true);
--message-out-link-color: #fff; --message-out-link-color: #fff;
--message-out-primary-color: #fff; --message-out-primary-color: #fff;
--message-out-status-color: rgba(255, 255, 255, .6);
--message-out-audio-play-button-color: var(--message-out-background-color); --message-out-audio-play-button-color: var(--message-out-background-color);
// * Night theme end // * Night theme end
} }

121
src/tests/slicedArray.test.ts

@ -0,0 +1,121 @@
import SlicedArray, { Slice } from "../helpers/slicedArray";
test('Slicing returns new Slice', () => {
const sliced = new SlicedArray();
const newSlice = sliced.slice.slice();
expect(newSlice.isEnd).toBeDefined();
});
describe('Inserting', () => {
const sliced = new SlicedArray();
// @ts-ignore
const slices = sliced.slices;
const arr = [100, 99, 98, 97, 96, 95];
const distantArr = arr.slice(-2).map(v => v - 2);
const missingArr = [arr[arr.length - 1], arr[arr.length - 1] - 1, distantArr[0]];
const startValue = 90;
const values: number[] = [];
const valuesPerArray = 3;
const totalArrays = 10;
for(let i = 0, length = valuesPerArray * totalArrays; i < length; ++i) {
values.push(startValue - i);
}
const arrays: number[][] = [];
for(let i = 0; i < totalArrays; ++i) {
arrays.push(values.slice(valuesPerArray * i, valuesPerArray * (i + 1)));
}
test('Insert & flatten', () => {
const idx = 2;
sliced.insertSlice(arr.slice(0, idx + 1));
sliced.insertSlice(arr.slice(idx));
expect([...sliced.first]).toEqual(arr);
});
test('Insert inner values', () => {
sliced.insertSlice(arr.slice(1, -1));
expect([...sliced.first]).toEqual(arr);
});
test('Insert distant slice', () => {
const length = slices.length;
sliced.insertSlice(distantArr);
expect(slices.length).toEqual(length + 1);
});
test('Insert intersection & join them', () => {
const length = slices.length;
sliced.insertSlice(missingArr);
expect(slices.length).toEqual(length - 1);
});
let returnedSlice: Slice;
test('Insert arrays with gap & join them', () => {
slices[0].length = 0;
for(const arr of arrays) {
sliced.insertSlice(arr);
}
expect(slices.length).toEqual(totalArrays);
returnedSlice = sliced.insertSlice(values.slice(0, -valuesPerArray + 1));
expect(slices.length).toEqual(1);
});
test('Return inserted & flattened slice', () => {
expect(slices[0]).toEqual(returnedSlice);
});
});
describe('Slicing', () => {
const sliced = new SlicedArray();
// @ts-ignore
const slices = sliced.slices;
const VALUES_LENGTH = 100;
const INCREMENTOR = 0xFFFF;
const values: number[] = [];
for(let i = 0; i < VALUES_LENGTH; ++i) {
values[i] = i + INCREMENTOR * i;
}
values.sort((a, b) => b - a);
sliced.insertSlice(values);
const addOffset = 40;
const limit = 40;
const r = (func: (idx: number) => void) => {
const max = VALUES_LENGTH * 3;
for(let i = 0; i < max; ++i) {
const idx = Math.random() * max | 0;
func(idx);
}
};
describe('Positive addOffset', () => {
test('From the start', () => {
const {slice} = sliced.sliceMe(0, addOffset, limit);
expect([...slice]).toEqual(values.slice(addOffset, addOffset + limit));
});
test('From existing offsetId', () => {
r((idx) => {
const value = values[idx] || 1;
idx += 1; // because is it inclusive
const {slice} = sliced.sliceMe(value, addOffset, limit);
expect([...slice]).toEqual(values.slice(idx + addOffset, idx + addOffset + limit));
});
});
});
});
Loading…
Cancel
Save