diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index fb567c1a..671e0f3e 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -15,7 +15,7 @@ import type { AppPeersManager } from "../../lib/appManagers/appPeersManager"; import type sessionStorage from '../../lib/sessionStorage'; import type Chat from "./chat"; 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 { isTouchSupported } from "../../helpers/touchSupport"; import { logger } from "../../lib/logger"; @@ -130,6 +130,8 @@ export default class ChatBubbles { public isFirstLoad = true; private needReflowScroll: boolean; + private fetchNewPromise: Promise; + 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'); @@ -1399,6 +1401,7 @@ export default class ChatBubbles { this.messagesQueuePromise = null; this.getHistoryTopPromise = this.getHistoryBottomPromise = undefined; + this.fetchNewPromise = undefined; if(this.stickyIntersector) { this.stickyIntersector.disconnect(); @@ -1618,8 +1621,7 @@ export default class ChatBubbles { this.chat.dispatchEvent('setPeer', lastMsgId, !isJump); - const isFetchIntervalNeeded = () => peerId < 0 && !this.appChatsManager.isInChat(peerId); - const needFetchInterval = isFetchIntervalNeeded(); + const needFetchInterval = this.appMessagesManager.isFetchIntervalNeeded(peerId); const needFetchNew = savedPosition || needFetchInterval; if(!needFetchNew) { // warning @@ -1627,20 +1629,44 @@ export default class ChatBubbles { this.scrollable.loadedAll.bottom = true; } } else { + const middleware = this.getMiddleware(); Promise.all([setPeerPromise, getHeavyAnimationPromise()]).then(() => { + if(!middleware()) { + return; + } + this.scrollable.checkForTriggers(); if(needFetchInterval) { - const middleware = this.getMiddleware(); - const interval = window.setInterval(() => { - if(!middleware() || !isFetchIntervalNeeded()) { - clearInterval(interval); - return; - } - - this.scrollable.loadedAll.bottom = false; - this.loadMoreHistory(false); - }, 30e3); + const f = () => { + this.fetchNewPromise = new Promise((resolve) => { + if(!middleware() || !this.appMessagesManager.isFetchIntervalNeeded(peerId)) { + resolve(); + return; + } + + this.appMessagesManager.getNewHistory(peerId, this.chat.threadId).then((historyStorage) => { + if(!middleware()) { + 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'); canHaveTail = false; } else { - messageDiv.innerHTML = richText; + setInnerHTML(messageDiv, richText); } /* if(strLength === emojiStrLength) { @@ -1983,7 +2009,7 @@ export default class ChatBubbles { messageDiv.classList.add('message-empty'); } */ } else { - messageDiv.innerHTML = richText; + setInnerHTML(messageDiv, richText); } const timeSpan = MessageRender.setTime(this.chat, message, bubble, bubbleContainer, messageDiv); @@ -2219,21 +2245,21 @@ export default class ChatBubbles { nameEl.classList.add('name'); nameEl.setAttribute('target', '_blank'); nameEl.href = webpage.url || '#'; - nameEl.innerHTML = RichTextProcessor.wrapEmojiText(webpage.site_name); + setInnerHTML(nameEl, RichTextProcessor.wrapEmojiText(webpage.site_name)); quoteTextDiv.append(nameEl); } if(webpage.rTitle) { let titleDiv = document.createElement('div'); titleDiv.classList.add('title'); - titleDiv.innerHTML = webpage.rTitle; + setInnerHTML(titleDiv, webpage.rTitle); quoteTextDiv.append(titleDiv); } if(webpage.rDescription) { let textDiv = document.createElement('div'); textDiv.classList.add('text'); - textDiv.innerHTML = webpage.rDescription; + setInnerHTML(textDiv, webpage.rDescription); quoteTextDiv.append(textDiv); } @@ -2626,9 +2652,15 @@ export default class ChatBubbles { } */ 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; } + + if(lastSlice.isEnd(SliceEnd.Top) && history.includes(lastSlice[lastSlice.length - 1])) { + this.scrollable.loadedAll.top = true; + } //console.time('appImManager render history'); @@ -2818,8 +2850,9 @@ export default class ChatBubbles { additionMsgIds = [additionMsgId]; } else { const historyStorage = this.appMessagesManager.getHistoryStorage(peerId, this.chat.threadId); - if(historyStorage.history.length < loadCount && !historyStorage.history.slice.isEnd(SliceEnd.Both)) { - additionMsgIds = historyStorage.history.slice.slice(); + const slice = historyStorage.history.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 for(let i = additionMsgIds.length - 1; i >= 0; --i) { diff --git a/src/components/chat/contextMenu.ts b/src/components/chat/contextMenu.ts index 04c08961..8d862f22 100644 --- a/src/components/chat/contextMenu.ts +++ b/src/components/chat/contextMenu.ts @@ -158,7 +158,7 @@ export default class ChatContextMenu { private init() { this.buttons = [{ icon: 'send2', - text: 'Chat.Context.Scheduled.SendNow', + text: 'MessageScheduleSend', onClick: this.onSendScheduledClick, verify: () => this.chat.type === 'scheduled' && !this.message.pFlags.is_outgoing }, { @@ -170,7 +170,7 @@ export default class ChatContextMenu { withSelection: true }, { icon: 'schedule', - text: 'Chat.Context.Scheduled.Reschedule', + text: 'MessageScheduleEditTime', onClick: () => { this.chat.input.scheduleSending(() => { this.appMessagesManager.editMessage(this.message, this.message.message, { diff --git a/src/components/chat/selection.ts b/src/components/chat/selection.ts index a085b57a..910f7f3d 100644 --- a/src/components/chat/selection.ts +++ b/src/components/chat/selection.ts @@ -359,7 +359,7 @@ export default class ChatSelection { if(this.chat.type === 'scheduled') { 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', () => { new PopupSendNow(this.bubbles.peerId, [...this.selectedMids], () => { this.cancelSelection(); diff --git a/src/components/divAndCaption.ts b/src/components/divAndCaption.ts index efa903ec..68ac1afb 100644 --- a/src/components/divAndCaption.ts +++ b/src/components/divAndCaption.ts @@ -23,9 +23,11 @@ export default class DivAndCaption { this.title = document.createElement('div'); this.title.classList.add(className + '-title'); + this.title.setAttribute('dir', 'auto'); this.subtitle = document.createElement('div'); this.subtitle.classList.add(className + '-subtitle'); + this.subtitle.setAttribute('dir', 'auto'); this.content.append(this.title, this.subtitle); this.container.append(this.border, this.content); diff --git a/src/components/inputField.ts b/src/components/inputField.ts index c344263a..5b087eea 100644 --- a/src/components/inputField.ts +++ b/src/components/inputField.ts @@ -42,7 +42,8 @@ let init = () => { init = null; }; -const checkAndSetRTL = (input: HTMLElement) => { +// ! it doesn't respect symbols other than strongs +/* const checkAndSetRTL = (input: HTMLElement) => { //const isEmpty = isInputEmpty(input); //console.log('input', isEmpty); @@ -56,7 +57,7 @@ const checkAndSetRTL = (input: HTMLElement) => { //console.log('RTL', direction, char); input.style.direction = direction; -}; +}; */ export enum InputState { Neutral = 0, @@ -112,7 +113,7 @@ class InputField { input = this.container.firstElementChild as HTMLElement; const observer = new MutationObserver(() => { - checkAndSetRTL(input); + //checkAndSetRTL(input); if(processInput) { processInput(); @@ -148,9 +149,11 @@ class InputField { `; input = this.container.firstElementChild as HTMLElement; - input.addEventListener('input', () => checkAndSetRTL(input)); + //input.addEventListener('input', () => checkAndSetRTL(input)); } + input.setAttribute('dir', 'auto'); + if(placeholder) { _i18n(input, placeholder, undefined, 'placeholder'); diff --git a/src/components/peerTitle.ts b/src/components/peerTitle.ts index b6107508..a34ad289 100644 --- a/src/components/peerTitle.ts +++ b/src/components/peerTitle.ts @@ -43,6 +43,7 @@ export default class PeerTitle { constructor(options: PeerTitleOptions) { this.element = document.createElement('span'); this.element.classList.add('peer-title'); + this.element.setAttribute('dir', 'auto'); this.update(options); weakMap.set(this.element, this); diff --git a/src/components/row.ts b/src/components/row.ts index b215340a..d234dc90 100644 --- a/src/components/row.ts +++ b/src/components/row.ts @@ -40,6 +40,7 @@ export default class Row { this.subtitle = document.createElement('div'); this.subtitle.classList.add('row-subtitle'); + this.subtitle.setAttribute('dir', 'auto'); if(options.subtitle) { this.subtitle.innerHTML = options.subtitle; } else if(options.subtitleLangKey) { @@ -89,6 +90,7 @@ export default class Row { this.title = document.createElement('div'); this.title.classList.add('row-title'); + this.title.setAttribute('dir', 'auto'); if(options.title) { this.title.innerHTML = options.title; } else { diff --git a/src/components/sidebarLeft/tabs/addMembers.ts b/src/components/sidebarLeft/tabs/addMembers.ts index 7b530906..55393dc3 100644 --- a/src/components/sidebarLeft/tabs/addMembers.ts +++ b/src/components/sidebarLeft/tabs/addMembers.ts @@ -20,6 +20,7 @@ export default class AppAddMembersTab extends SliderSuperTab { protected init() { this.nextBtn = ButtonCorner({icon: 'arrow_next'}); this.content.append(this.nextBtn); + this.scrollable.container.remove(); this.nextBtn.addEventListener('click', () => { const peerIds = this.selector.getSelected(); diff --git a/src/components/sidebarRight/tabs/groupType.ts b/src/components/sidebarRight/tabs/chatType.ts similarity index 83% rename from src/components/sidebarRight/tabs/groupType.ts rename to src/components/sidebarRight/tabs/chatType.ts index 808adff6..7bde27c7 100644 --- a/src/components/sidebarRight/tabs/groupType.ts +++ b/src/components/sidebarRight/tabs/chatType.ts @@ -22,34 +22,37 @@ import I18n from "../../../lib/langPack"; import PopupPeer from "../../popups/peer"; import ButtonCorner from "../../buttonCorner"; -export default class AppGroupTypeTab extends SliderSuperTabEventable { - public peerId: number; +export default class AppChatTypeTab extends SliderSuperTabEventable { + public chatId: number; public chatFull: ChatFull; protected init() { 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({ - name: 'GroupType' + name: isBroadcast ? 'ChannelType' : 'GroupType' }); const random = randomLong(); const privateRow = new Row({ radioField: new RadioField({ - langKey: 'MegaPrivate', + langKey: isBroadcast ? 'ChannelPrivate' : 'MegaPrivate', name: random, value: 'private' }), - subtitleLangKey: 'MegaPrivateInfo' + subtitleLangKey: isBroadcast ? 'ChannelPrivateInfo' : 'MegaPrivateInfo' }); const publicRow = new Row({ radioField: new RadioField({ - langKey: 'MegaPublic', + langKey: isBroadcast ? 'ChannelPublic' : 'MegaPublic', name: random, value: 'public' }), - subtitleLangKey: 'MegaPublicInfo' + subtitleLangKey: isBroadcast ? 'ChannelPublicInfo' : 'MegaPublicInfo' }); const form = RadioFormFromRows([privateRow, publicRow], (value) => { const a = [privateSection, publicSection]; @@ -61,7 +64,7 @@ export default class AppGroupTypeTab extends SliderSuperTabEventable { onChange(); }); - const chat: Chat = appChatsManager.getChat(-this.peerId); + const chat: Chat = appChatsManager.getChat(this.chatId); section.content.append(form); @@ -70,7 +73,7 @@ export default class AppGroupTypeTab extends SliderSuperTabEventable { //let revoked = false; const linkRow = new Row({ title: (this.chatFull.exported_invite as ExportedChatInvite.chatInviteExported).link, - subtitleLangKey: 'MegaPrivateLinkHelp', + subtitleLangKey: isBroadcast ? 'ChannelPrivateLinkHelp' : 'MegaPrivateLinkHelp', clickable: () => { copyTextToClipboard((this.chatFull.exported_invite as ExportedChatInvite.chatInviteExported).link); toast(I18n.format('LinkCopied', true)); @@ -86,7 +89,7 @@ export default class AppGroupTypeTab extends SliderSuperTabEventable { callback: () => { const toggle = toggleDisability([btnRevoke], true); - appProfileManager.getChatInviteLink(-this.peerId, true).then(link => { + appProfileManager.getChatInviteLink(this.chatId, true).then(link => { toggle(); linkRow.title.innerHTML = link; //revoked = true; @@ -102,7 +105,7 @@ export default class AppGroupTypeTab extends SliderSuperTabEventable { privateSection.content.append(linkRow.container, btnRevoke); const publicSection = new SettingSection({ - caption: 'Channel.UsernameAboutGroup', + caption: isBroadcast ? 'Channel.UsernameAboutChannel' : 'Channel.UsernameAboutGroup', noDelimiter: true }); @@ -126,7 +129,7 @@ export default class AppGroupTypeTab extends SliderSuperTabEventable { invalidText: 'Link.Invalid', takenText: 'Link.Taken', onChange: onChange, - peerId: this.peerId, + peerId: -this.chatId, head: placeholder }); @@ -141,7 +144,7 @@ export default class AppGroupTypeTab extends SliderSuperTabEventable { attachClickEvent(applyBtn, () => { /* const unsetLoader = */setButtonLoader(applyBtn); const username = publicRow.radioField.checked ? linkInputField.getValue() : ''; - appChatsManager.migrateChat(-this.peerId).then(channelId => { + appChatsManager.migrateChat(this.chatId).then(channelId => { return appChatsManager.updateUsername(channelId, username); }).then(() => { //unsetLoader(); diff --git a/src/components/sidebarRight/tabs/editChannel.ts b/src/components/sidebarRight/tabs/editChannel.ts deleted file mode 100644 index e82962f6..00000000 --- a/src/components/sidebarRight/tabs/editChannel.ts +++ /dev/null @@ -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[] = []; - - 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); - } - } -} diff --git a/src/components/sidebarRight/tabs/editGroup.ts b/src/components/sidebarRight/tabs/editChat.ts similarity index 55% rename from src/components/sidebarRight/tabs/editGroup.ts rename to src/components/sidebarRight/tabs/editChat.ts index f92c4304..f753f505 100644 --- a/src/components/sidebarRight/tabs/editGroup.ts +++ b/src/components/sidebarRight/tabs/editChat.ts @@ -13,15 +13,17 @@ import Button from "../../button"; import appChatsManager, { ChatRights } from "../../../lib/appManagers/appChatsManager"; import appProfileManager from "../../../lib/appManagers/appProfileManager"; import { attachClickEvent, toggleDisability } from "../../../helpers/dom"; -import { ChatFull } from "../../../layer"; -import AppGroupTypeTab from "./groupType"; +import { Chat } from "../../../layer"; +import AppChatTypeTab from "./chatType"; import rootScope from "../../../lib/rootScope"; import AppGroupPermissionsTab from "./groupPermissions"; -import { i18n } from "../../../lib/langPack"; +import { i18n, LangPackKey } from "../../../lib/langPack"; import PopupDeleteDialog from "../../popups/deleteDialog"; +import { addCancelButton } from "../../popups"; +import PopupPeer from "../../popups/peer"; -export default class AppEditGroupTab extends SliderSuperTab { - private groupNameInputField: InputField; +export default class AppEditChatTab extends SliderSuperTab { + private chatNameInputField: InputField; private descriptionInputField: InputField; private editPeer: EditPeer; public chatId: number; @@ -33,9 +35,13 @@ export default class AppEditGroupTab extends SliderSuperTab { this.container.classList.add('edit-peer-container', 'edit-group-container'); this.setTitle('Edit'); - + 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 inputFields: InputField[] = []; @@ -43,26 +49,23 @@ export default class AppEditGroupTab extends SliderSuperTab { const inputWrapper = document.createElement('div'); inputWrapper.classList.add('input-wrapper'); - this.groupNameInputField = new InputField({ - label: 'CreateGroup.NameHolder', - name: 'group-name', + this.chatNameInputField = new InputField({ + label: isBroadcast ? 'Channel.ChannelNameHolder' : 'CreateGroup.NameHolder', + name: 'chat-name', maxLength: 255 }); this.descriptionInputField = new InputField({ label: 'DescriptionPlaceholder', - name: 'group-description', + name: 'chat-description', maxLength: 255 }); - - const chat = appChatsManager.getChat(this.chatId); - this.groupNameInputField.setOriginalValue(chat.title); - + this.chatNameInputField.setOriginalValue(chat.title); 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({ peerId: -this.chatId, @@ -74,29 +77,37 @@ export default class AppEditGroupTab extends SliderSuperTab { section.content.append(this.editPeer.avatarEdit.container, inputWrapper); if(appChatsManager.hasRights(this.chatId, 'change_type')) { - const groupTypeRow = new Row({ - titleLangKey: 'GroupType', + const chatTypeRow = new Row({ + titleLangKey: isBroadcast ? 'ChannelType' : 'GroupType', clickable: () => { - const tab = new AppGroupTypeTab(this.slider); - tab.peerId = -this.chatId; + const tab = new AppChatTypeTab(this.slider); + tab.chatId = this.chatId; tab.chatFull = chatFull; tab.open(); - this.listenerSetter.add(tab.eventListener, 'destroy', setGroupTypeSubtitle); + this.listenerSetter.add(tab.eventListener, 'destroy', setChatTypeSubtitle); }, icon: 'lock' }); - const setGroupTypeSubtitle = () => { - groupTypeRow.subtitle.textContent = ''; - groupTypeRow.subtitle.append(i18n(chat.username ? 'TypePublicGroup' : 'TypePrivateGroup')); + const setChatTypeSubtitle = () => { + chatTypeRow.subtitle.textContent = ''; + + 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(); - section.content.append(groupTypeRow.container); + setChatTypeSubtitle(); + section.content.append(chatTypeRow.container); } - if(appChatsManager.hasRights(this.chatId, 'change_permissions')) { + if(appChatsManager.hasRights(this.chatId, 'change_permissions') && !isBroadcast) { const flags = [ 'send_messages', 'send_media', @@ -149,8 +160,8 @@ export default class AppEditGroupTab extends SliderSuperTab { let promises: Promise[] = []; const id = this.chatId; - if(this.groupNameInputField.isValid()) { - promises.push(appChatsManager.editTitle(id, this.groupNameInputField.value)); + if(this.chatNameInputField.isValid()) { + promises.push(appChatsManager.editTitle(id, this.chatNameInputField.value)); } if(this.descriptionInputField.isValid()) { @@ -168,6 +179,36 @@ export default class AppEditGroupTab extends SliderSuperTab { this.close(); }); }, {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({ - titleLangKey: 'GroupMembers', - subtitle: '2 500', + titleLangKey: isBroadcast ? 'PeerInfo.Subscribers' : 'GroupMembers', icon: 'newgroup', clickable: true }); + membersRow.subtitle.append(i18n('Subscribers', [numberThousandSplitter(335356)])); + section.content.append(membersRow.container); if(appChatsManager.hasRights(this.chatId, 'change_permissions')) { @@ -203,17 +245,45 @@ export default class AppEditGroupTab extends SliderSuperTab { if(appChatsManager.hasRights(this.chatId, 'delete_chat')) { 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, () => { - new PopupDeleteDialog(-this.chatId, undefined, (promise) => { - const toggle = toggleDisability([btnDelete], true); - promise.then(() => { - this.close(); - }, () => { - toggle(); + 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) => { + const toggle = toggleDisability([btnDelete], true); + promise.then(() => { + this.close(); + }, () => { + toggle(); + }); }); - }); + } }, {listenerSetter: this.listenerSetter}); section.content.append(btnDelete); @@ -221,13 +291,15 @@ export default class AppEditGroupTab extends SliderSuperTab { this.scrollable.append(section.container); } - // ! 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}) => { - if(-this.chatId === migrateFrom) { - this.chatId = -migrateTo; - this._init(); - } - }); + if(!isChannel) { + // ! 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}) => { + if(-this.chatId === migrateFrom) { + this.chatId = -migrateTo; + this._init(); + } + }); + } } protected init() { diff --git a/src/components/sidebarRight/tabs/sharedMedia.ts b/src/components/sidebarRight/tabs/sharedMedia.ts index bc76348d..e5abd562 100644 --- a/src/components/sidebarRight/tabs/sharedMedia.ts +++ b/src/components/sidebarRight/tabs/sharedMedia.ts @@ -19,9 +19,8 @@ import { attachClickEvent, replaceContent, cancelEvent } from "../../../helpers/ import appSidebarRight from ".."; import { TransitionSlider } from "../../transition"; import appNotificationsManager from "../../../lib/appManagers/appNotificationsManager"; -import AppEditGroupTab from "./editGroup"; +import AppEditChatTab from "./editChat"; import PeerTitle from "../../peerTitle"; -import AppEditChannelTab from "./editChannel"; import AppEditContactTab from "./editContact"; import appChatsManager, { Channel } from "../../../lib/appManagers/appChatsManager"; import { Chat, Message, MessageAction, ChatFull, Photo } from "../../../layer"; @@ -854,17 +853,15 @@ export default class AppSharedMediaTab extends SliderSuperTab { }); attachClickEvent(this.editBtn, (e) => { - let tab: AppEditGroupTab | AppEditChannelTab | AppEditContactTab; - if(appPeersManager.isAnyGroup(this.peerId)) { - tab = new AppEditGroupTab(appSidebarRight); - } else if(this.peerId > 0) { - tab = new AppEditContactTab(appSidebarRight); + let tab: AppEditChatTab | AppEditContactTab; + if(this.peerId < 0) { + tab = new AppEditChatTab(appSidebarRight); } else { - tab = new AppEditChannelTab(appSidebarRight); + tab = new AppEditContactTab(appSidebarRight); } if(tab) { - if(tab instanceof AppEditGroupTab) { + if(tab instanceof AppEditChatTab) { tab.chatId = -this.peerId; } else { tab.peerId = this.peerId; diff --git a/src/config/app.ts b/src/config/app.ts index 4ca77c4b..df266947 100644 --- a/src/config/app.ts +++ b/src/config/app.ts @@ -12,7 +12,7 @@ const App = { id: 1025907, hash: '452b0359b988148995f22ff0f4229750', - version: '0.5.0', + version: '0.5.1', langPackVersion: '0.1.6', langPack: 'macos', langPackCode: 'en', diff --git a/src/helpers/dom.ts b/src/helpers/dom.ts index a151ad44..e12a2dca 100644 --- a/src/helpers/dom.ts +++ b/src/helpers/dom.ts @@ -694,3 +694,8 @@ export function replaceContent(elem: HTMLElement, node: string | Node) { elem.append(node); } } + +export function setInnerHTML(elem: HTMLElement, html: string) { + elem.setAttribute('dir', 'auto'); + elem.innerHTML = html; +} diff --git a/src/helpers/slicedArray.ts b/src/helpers/slicedArray.ts index 5ec02dba..807be1d7 100644 --- a/src/helpers/slicedArray.ts +++ b/src/helpers/slicedArray.ts @@ -4,6 +4,8 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +import { MOUNT_CLASS_TO } from "../config/debug"; + /** * Descend sorted storage */ @@ -14,15 +16,19 @@ export enum SliceEnd { None = 0, Top = 1, Bottom = 2, - Both = 4 + Both = SliceEnd.Top | SliceEnd.Bottom }; export interface Slice extends Array { - slicedArray: SlicedArray; + //slicedArray: SlicedArray; end: SliceEnd; isEnd: (side: SliceEnd) => boolean; 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 { @@ -35,51 +41,84 @@ export default class SlicedArray { private sliceConstructor: SliceConstructor; constructor() { - const self = this; - this.sliceConstructor = class Slice extends Array implements Slice { - slicedArray: SlicedArray; - end: SliceEnd = SliceEnd.None; + // @ts-ignore + this.sliceConstructor = SlicedArray.getSliceConstructor(this); - constructor(...items: ItemType[]) { - super(...items); - this.slicedArray = self; - } + const first = this.constructSlice(); + //first.setEnd(SliceEnd.Bottom); + this.slices = [first]; + } - isEnd(side: SliceEnd) { - if(this.end & side) { + private static getSliceConstructor(slicedArray: SlicedArray) { + return class Slice extends Array implements Slice { + //slicedArray: SlicedArray; + end: SliceEnd = SliceEnd.None; + + /* constructor(...items: ItemType[]) { + super(...items); + //this.slicedArray = slicedArray; + } */ + + isEnd(side: SliceEnd): boolean { + if((this.end & side) === side) { return true; - } - + }/* else if(!this.slicedArray) { + return false; + } */ + + let isEnd = false; if(side === SliceEnd.Top) { - const slice = self.last; - return slice.end & side ? this.includes(slice[slice.length - 1]) || !slice.length : false; + const slice = slicedArray.last; + isEnd = slice.end & side ? this.includes(slice[slice.length - 1])/* || !slice.length */ : false; } else if(side === SliceEnd.Bottom) { - const slice = self.first; - return slice.end & side ? this.includes(slice[0]) || !slice.length : false; - }/* else if(side === SliceEnd.Both) { - - } */ + const slice = slicedArray.first; + isEnd = slice.end & side ? this.includes(slice[0])/* || !slice.length */ : false; + } else if(side === SliceEnd.Both) { + return this.isEnd(SliceEnd.Top) && this.isEnd(SliceEnd.Bottom); + } - return false; + if(isEnd) { + this.setEnd(side); + } + + return isEnd; } - + setEnd(side: SliceEnd) { this.end |= side; + } - if(side !== SliceEnd.Both && this.end & SliceEnd.Top && this.end & SliceEnd.Bottom) { - this.end |= SliceEnd.Both; + unsetEnd(side: SliceEnd) { + 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); + } + } } + + return ret; } } - - const first = this.constructSlice(); - first.setEnd(SliceEnd.Bottom); - this.slices = [first]; } public constructSlice(...items: ItemType[]) { //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; // ! 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) { return; } @@ -136,15 +175,15 @@ export default class SlicedArray { const first = this.slices[0]; if(!first.length) { first.push(...slice); - return; + return first; } const lowerBound = slice[slice.length - 1]; const upperBound = slice[0]; - let foundSlice: Slice, lowerIndex = -1, upperIndex = -1; - for(let i = 0; i < this.slices.length; ++i) { - foundSlice = this.slices[i]; + let foundSlice: Slice, lowerIndex = -1, upperIndex = -1, foundSliceIndex = 0; + for(; foundSliceIndex < this.slices.length; ++foundSliceIndex) { + foundSlice = this.slices[foundSliceIndex]; lowerIndex = foundSlice.indexOf(lowerBound); upperIndex = foundSlice.indexOf(upperBound); @@ -173,29 +212,38 @@ export default class SlicedArray { } this.slices.splice(insertIndex, 0, this.constructSlice(...slice)); + foundSliceIndex = insertIndex; } - this.flatten(); - } - - private flatten() { - if(this.slices.length < 2) { - return; + if(flatten) { + return this.flatten(foundSliceIndex); } + } - for(let i = 0, length = this.slices.length; i < (length - 1); ++i) { - const prevSlice = this.slices[i]; - const nextSlice = this.slices[i + 1]; + private flatten(foundSliceIndex: number) { + if(this.slices.length >= 2) { + for(let i = 0, length = this.slices.length; i < (length - 1); ++i) { + const prevSlice = this.slices[i]; + const nextSlice = this.slices[i + 1]; + + const upperIndex = prevSlice.indexOf(nextSlice[0]); + if(upperIndex !== -1) { + prevSlice.setEnd(nextSlice.end); + this.slices.splice(i + 1, 1); - const upperIndex = prevSlice.indexOf(nextSlice[0]); - if(upperIndex !== -1) { - prevSlice.setEnd(nextSlice.end); - this.slices.splice(i + 1, 1); - length--; + if(i < foundSliceIndex) { + --foundSliceIndex; + } - this.insertSlice(nextSlice); + --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) { - 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 index = slice.indexOf(item); if(index !== -1) { @@ -295,6 +343,8 @@ export default class SlicedArray { const topWasMeantToLoad = add_offset < 0 ? limit + add_offset : limit; 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 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[]) { - 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[]) { - 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) { const found = this.findSlice(item); if(found) { found.slice.splice(found.index, 1); + return true; } + + return false; } } -(window as any).slicedArray = new SlicedArray(); +MOUNT_CLASS_TO && (MOUNT_CLASS_TO.SlicedArray = SlicedArray); diff --git a/src/lang.ts b/src/lang.ts index ba192daf..c81d710c 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -133,6 +133,7 @@ const lang = { "AttachAudio": "Voice message", "AttachRound": "Video message", "AttachGame": "Game", + "Bot": "bot", //"ChannelJoined": "You joined this channel", "ChannelMegaJoined": "You joined this group", "Channel.DescriptionPlaceholder": "Description (optional)", @@ -408,6 +409,14 @@ const lang = { "OpenUrlAlert2": "Do you want to open %1$s?", "FilterNoChatsToDisplay": "Folder is empty", "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 "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.", "Chat.CopySelectedText": "Copy Selected Text", "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.ScheduledUntilOnline": "Scheduled until online", "Chat.Date.ScheduledForToday": "Scheduled for today", @@ -559,12 +566,7 @@ const lang = { "Peer.Activity.Chat.Multi.SendingFile1": "%@ and %d others are sending files", "Peer.ServiceNotifications": "service notifications", "Peer.RepliesNotifications": "Reply Notifications", - "Peer.Status.online": "online", - "Peer.Status.recently": "last seen recently", "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.Yesterday": "yesterday", "Peer.Status.LastSeenAt": "last seen %@ at %@", @@ -614,8 +616,6 @@ const lang = { "one_value": "Send Video", "other_value": "Send %d Videos" }, - "Presence.bot": "bot", - "Presence.Support": "support", "PrivacyAndSecurity.Item.On": "On", "PrivacyAndSecurity.Item.Off": "Off", "PrivacySettings.VoiceCalls": "Calls", diff --git a/src/lib/appManagers/appDialogsManager.ts b/src/lib/appManagers/appDialogsManager.ts index b8b49784..236e773c 100644 --- a/src/lib/appManagers/appDialogsManager.ts +++ b/src/lib/appManagers/appDialogsManager.ts @@ -1357,6 +1357,7 @@ export class AppDialogsManager { const span = document.createElement('span'); span.classList.add('user-last-message'); + span.setAttribute('dir', 'auto'); //captionDiv.append(titleSpan); //captionDiv.append(span); diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 0d22c8b1..bce20395 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -50,6 +50,7 @@ import { copy, getObjectKeysAndSort } from '../../helpers/object'; import { getFilesFromEvent } from '../../helpers/files'; import PeerTitle from '../../components/peerTitle'; import PopupPeer from '../../components/popups/peer'; +import { SliceEnd } from '../../helpers/slicedArray'; //console.log('appImManager included33!'); @@ -445,10 +446,11 @@ export class AppImManager { return; } else if(e.code === 'ArrowUp') { if(!chat.input.editMsgId && chat.input.isInputEmpty()) { - const history = appMessagesManager.getHistoryStorage(chat.peerId, chat.threadId); - if(history.history.length) { + const historyStorage = appMessagesManager.getHistoryStorage(chat.peerId, chat.threadId); + const slice = historyStorage.history.slice; + if(slice.isEnd(SliceEnd.Bottom) && slice.length) { let goodMid: number; - for(const mid of history.history.slice) { + for(const mid of slice) { const message = chat.getMessage(mid); const good = this.myId === chat.peerId ? message.fromId === this.myId : message.pFlags.out; diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index c31a3da6..77ce0277 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -14,7 +14,7 @@ import ProgressivePreloader from "../../components/preloader"; import { CancellablePromise, deferredPromise } from "../../helpers/cancellablePromise"; import { tsNow } from "../../helpers/date"; import { createPosterForVideo } from "../../helpers/files"; -import { copy, defineNotNumerableProperties, getObjectKeysAndSort } from "../../helpers/object"; +import { copy, getObjectKeysAndSort } from "../../helpers/object"; import { randomLong } from "../../helpers/random"; 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"; @@ -168,7 +168,7 @@ export class AppMessagesManager { public migratedFromTo: {[peerId: number]: number} = {}; public migratedToFrom: {[peerId: number]: number} = {}; - public newMessagesHandlePromise = 0; + public newMessagesHandleTimeout = 0; public newMessagesToHandle: {[peerId: string]: Set} = {}; public newDialogsHandlePromise: Promise; public newDialogsToHandle: {[peerId: string]: Dialog} = {}; @@ -1301,14 +1301,17 @@ export class AppMessagesManager { /* if(options.threadId && this.threadsStorage[peerId]) { delete this.threadsStorage[peerId][options.threadId]; } */ - if(options.threadId) { - const historyStorage = this.getHistoryStorage(peerId, options.threadId); - historyStorage.history.unshift(messageId); + const storages: HistoryStorage[] = [ + this.getHistoryStorage(peerId), + options.threadId ? this.getHistoryStorage(peerId, options.threadId) : undefined + ]; + + for(const storage of storages) { + if(storage) { + storage.history.unshift(messageId); + } } - const historyStorage = this.getHistoryStorage(peerId); - historyStorage.history.unshift(messageId); - //if(!options.isGroupedItem) { this.saveMessages([message], {storage, isOutgoing: true}); setTimeout(() => { @@ -1514,7 +1517,6 @@ export class AppMessagesManager { if(pendingData) { const {peerId, tempId, storage} = pendingData; const historyStorage = this.getHistoryStorage(peerId); - const pos = historyStorage.history.findSlice(tempId); apiUpdatesManager.processUpdateMessage({ _: 'updateShort', @@ -1524,9 +1526,7 @@ export class AppMessagesManager { } }); - if(pos) { - pos.slice.splice(pos.index, 1); - } + historyStorage.history.delete(tempId); delete this.pendingByRandomId[randomId]; delete storage[tempId]; @@ -1627,7 +1627,7 @@ export class AppMessagesManager { // ! ВНИМАНИЕ: ОЧЕНЬ СЛОЖНАЯ ЛОГИКА: // ! если делать запрос сначала по папке 0, потом по папке 1, по индексу 0 в массиве будет один и тот же диалог, с dialog.pFlags.pinned, ЛОЛ??? // ! т.е., с запросом folder_id: 1, и exclude_pinned: 0, в результате будут ещё и закреплённые с папки 0 - return apiManager.invokeApi('messages.getDialogs', { + return apiManager.invokeApiSingle('messages.getDialogs', { folder_id: folderId, offset_date: offsetDate, offset_id: offsetId, @@ -3271,7 +3271,7 @@ export class AppMessagesManager { } public getDiscussionMessage(peerId: number, mid: number) { - return apiManager.invokeApi('messages.getDiscussionMessage', { + return apiManager.invokeApiSingle('messages.getDiscussionMessage', { peer: appPeersManager.getInputPeerById(peerId), msg_id: this.getServerMessageId(mid) }).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 = () => { - clearTimeout(this.newMessagesHandlePromise); - this.newMessagesHandlePromise = 0; + clearTimeout(this.newMessagesHandleTimeout); + this.newMessagesHandleTimeout = 0; rootScope.broadcast('history_multiappend', this.newMessagesToHandle); this.newMessagesToHandle = {}; @@ -3404,6 +3415,7 @@ export class AppMessagesManager { return promise; } + // TODO: cancel notification by peer when this function is being called public readHistory(peerId: number, maxId = 0, threadId?: number, force = false) { //return Promise.resolve(); // console.trace('start read') @@ -3452,7 +3464,7 @@ export class AppMessagesManager { _: 'updateReadChannelInbox', max_id: maxId, channel_id: -peerId - } + } as Update.updateReadChannelInbox }); } else { if(!historyStorage.readPromise) { @@ -3477,21 +3489,10 @@ export class AppMessagesManager { _: 'updateReadHistoryInbox', max_id: maxId, 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)); if(historyStorage.readPromise) { @@ -3534,7 +3535,7 @@ export class AppMessagesManager { _: 'updateChannelReadMessagesContents', channel_id: channelId, messages: msgIds - } + } as Update.updateChannelReadMessagesContents }); }); } else { @@ -3548,7 +3549,7 @@ export class AppMessagesManager { messages: msgIds, pts: affectedMessages.pts, pts_count: affectedMessages.pts_count - } + } as Update.updateReadMessagesContents }); }); } @@ -3694,14 +3695,19 @@ export class AppMessagesManager { return false; } - const history = historyStorage.history.slice; - const topMsgId = history[0]; - history.unshift(message.mid); - if(message.mid < topMsgId) { - //this.log.error('this should\'nt have happenned!', message, history); - history.sort((a, b) => { - return b - a; - }); + // * catch situation with disconnect. if message's id is lower than we already have (in bottom end slice), will sort it + const firstSlice = historyStorage.history.first; + if(firstSlice.isEnd(SliceEnd.Bottom)) { + let i = 0; + for(const length = firstSlice.length; i < length; ++i) { + if(message.mid > firstSlice[i]) { + break; + } + } + + firstSlice.splice(i, 0, message.mid); + } else { + historyStorage.history.unshift(message.mid); } if(historyStorage.count !== null) { @@ -3717,14 +3723,7 @@ export class AppMessagesManager { } if(!pendingMessage) { - if(this.newMessagesToHandle[peerId] === undefined) { - this.newMessagesToHandle[peerId] = new Set(); - } - - this.newMessagesToHandle[peerId].add(message.mid); - if(!this.newMessagesHandlePromise) { - this.newMessagesHandlePromise = window.setTimeout(this.handleNewMessages, 0); - } + this.handleNewMessage(peerId, message.mid); } if(isLocalThreadUpdate) { @@ -4471,7 +4470,7 @@ export class AppMessagesManager { return Promise.resolve(Object.keys(storage).map(id => +id)); } - return apiManager.invokeApi('messages.getScheduledHistory', { + return apiManager.invokeApiSingle('messages.getScheduledHistory', { peer: appPeersManager.getInputPeerById(peerId), hash: 0 }).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 */ @@ -4567,7 +4596,7 @@ export class AppMessagesManager { } 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 { count: historyStorage.count, 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 { 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 offsetIdOffset = (historyResult as MessagesMessages.messagesMessagesSlice).offset_id_offset || 0; - const isTopEnd = offsetIdOffset >= (historyStorage.count - limit) || historyStorage.count < (limit + add_offset); + const topWasMeantToLoad = add_offset < 0 ? limit + add_offset : limit; + + const isTopEnd = offsetIdOffset >= (historyStorage.count - topWasMeantToLoad) || historyStorage.count < topWasMeantToLoad; + const isBottomEnd = !offsetIdOffset || (add_offset < 0 && (offsetIdOffset + add_offset) <= 0); /* if(!maxId && historyResult.messages.length) { maxId = this.incrementMessageId((historyResult.messages[0] as MyMessage).mid, 1); @@ -4598,13 +4632,14 @@ export class AppMessagesManager { const wasTotalCount = historyStorage.history.length; */ - historyResult.messages.forEach((message) => { + const mids = messages.map((message) => { if(this.mergeReplyKeyboard(historyStorage, message)) { rootScope.broadcast('history_reply_markup', {peerId}); } + + return (message as MyMessage).mid; }); - const mids = historyResult.messages.map((message) => (message as MyMessage).mid); // * add bound manually. // * 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) { @@ -4618,10 +4653,16 @@ export class AppMessagesManager { mids.splice(i, 0, offset_id); } - historyStorage.history.insertSlice(mids); - - if(isTopEnd) { - historyStorage.history.last.setEnd(SliceEnd.Top); + const slice = historyStorage.history.insertSlice(mids); + if(slice) { + if(isTopEnd) { + slice.setEnd(SliceEnd.Top); + } + + if(isBottomEnd) { + slice.setEnd(SliceEnd.Bottom); + historyStorage.maxId = slice[0]; // ! WARNING + } } /* const isBackLimit = offset < 0 && -offset !== fullLimit; @@ -4681,7 +4722,7 @@ export class AppMessagesManager { options.msg_id = this.getServerMessageId(threadId) || 0; } - const promise: ReturnType = apiManager.invokeApi(threadId ? 'messages.getReplies' : 'messages.getHistory', options, { + const promise: ReturnType = apiManager.invokeApiSingle(threadId ? 'messages.getReplies' : 'messages.getHistory', options, { //timeout: APITIMEOUT, noErrorBox: true }) as any; @@ -4699,21 +4740,24 @@ export class AppMessagesManager { 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) { historyResult.messages.splice(length - 1, 1); length--; - (historyResult as MessagesMessages.messagesMessagesSlice).count--; + count--; } // 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 - if(length && (historyResult.messages[length - 1] as Message.message).grouped_id - && (historyStorage.history.length + historyResult.messages.length) < (historyResult as MessagesMessages.messagesMessagesSlice).count) { - return this.requestHistory(peerId, (historyResult.messages[length - 1] as Message.message).mid, 10, 0, offsetDate, threadId).then((_historyResult) => { - return historyResult; - }); + const historyStorage = this.getHistoryStorage(peerId, threadId); + const oldestMessage: Message.message = historyResult.messages[length - 1] as any; + 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; @@ -4760,12 +4804,12 @@ export class AppMessagesManager { let promise: Promise; if(+peerId < 0 && appPeersManager.isChannel(+peerId)) { - promise = apiManager.invokeApi('channels.getMessages', { + promise = apiManager.invokeApiSingle('channels.getMessages', { channel: appChatsManager.getChannelInput(-+peerId), id: msgIds }); } else { - promise = apiManager.invokeApi('messages.getMessages', { + promise = apiManager.invokeApiSingle('messages.getMessages', { id: msgIds }); } diff --git a/src/lib/appManagers/appPhotosManager.ts b/src/lib/appManagers/appPhotosManager.ts index 7f420204..2e47153b 100644 --- a/src/lib/appManagers/appPhotosManager.ts +++ b/src/lib/appManagers/appPhotosManager.ts @@ -13,7 +13,7 @@ import type { DownloadOptions } from "../mtproto/apiFileManager"; import { bytesFromHex } from "../../helpers/bytes"; import { CancellablePromise } from "../../helpers/cancellablePromise"; import { getFileNameByLocation } from "../../helpers/fileName"; -import { safeReplaceArrayInObject, defineNotNumerableProperties, isObject } from "../../helpers/object"; +import { safeReplaceArrayInObject, isObject } from "../../helpers/object"; import { isSafari } from "../../helpers/userAgent"; import { InputFileLocation, InputMedia, Photo, PhotoSize, PhotosPhotos } from "../../layer"; import apiManager from "../mtproto/mtprotoworker"; diff --git a/src/lib/appManagers/appUsersManager.ts b/src/lib/appManagers/appUsersManager.ts index 0f0c1151..f3d17a5d 100644 --- a/src/lib/appManagers/appUsersManager.ts +++ b/src/lib/appManagers/appUsersManager.ts @@ -431,7 +431,7 @@ export class AppUsersManager { break; default: { if(this.isBot(userId)) { - key = 'Presence.bot'; + key = 'Bot'; break; } @@ -442,23 +442,23 @@ export class AppUsersManager { } if(user.pFlags.support) { - key = 'Presence.Support'; + key = 'SupportStatus'; break; } switch(user.status?._) { case 'userStatusRecently': { - key = 'Peer.Status.recently'; + key = 'Lately'; break; } case 'userStatusLastWeek': { - key = 'Peer.Status.lastWeek'; + key = 'WithinAWeek'; break; } case 'userStatusLastMonth': { - key = 'Peer.Status.lastMonth'; + key = 'WithinAMonth'; break; } @@ -487,12 +487,12 @@ export class AppUsersManager { } case 'userStatusOnline': { - key = 'Peer.Status.online'; + key = 'Online'; break; } default: { - key = 'Peer.Status.longTimeAgo'; + key = 'ALongTimeAgo'; break; } } diff --git a/src/lib/storages/dialogs.ts b/src/lib/storages/dialogs.ts index 7fe9735f..dc2a20a5 100644 --- a/src/lib/storages/dialogs.ts +++ b/src/lib/storages/dialogs.ts @@ -25,6 +25,7 @@ import { forEachReverse, insertInDescendSortedArray } from "../../helpers/array" import rootScope from "../rootScope"; import { safeReplaceObject } from "../../helpers/object"; import { AppStateManager } from "../appManagers/appStateManager"; +import { SliceEnd } from "../../helpers/slicedArray"; export default class DialogsStorage { private storage: AppStateManager['storages']['dialogs']; @@ -490,13 +491,17 @@ export default class DialogsStorage { } const historyStorage = this.appMessagesManager.getHistoryStorage(peerId); + const slice = historyStorage.history.slice; /* if(historyStorage === undefined) { // warning historyStorage.history.push(mid); if(this.mergeReplyKeyboard(historyStorage, message)) { rootScope.broadcast('history_reply_markup', {peerId}); } - } else */if(!historyStorage.history.slice.length) { + } else */if(!slice.length) { 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; diff --git a/src/scss/partials/_button.scss b/src/scss/partials/_button.scss index 6b1e0502..39a20b54 100644 --- a/src/scss/partials/_button.scss +++ b/src/scss/partials/_button.scss @@ -384,7 +384,7 @@ --size: 54px; border-radius: 50%; height: var(--size); - width: var(--size) !important; + width: var(--size); line-height: var(--size); @include respond-to(handhelds) { diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index 4e3de500..e8b310b2 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -995,6 +995,7 @@ $bubble-margin: .25rem; position: relative !important; height: 0px !important; visibility: hidden !important; + float: none; .inner { visibility: hidden !important; @@ -1250,6 +1251,8 @@ $bubble-margin: .25rem; /* display: inline-flex; align-items: center; */ height: 12px; + direction: ltr; + float: right; // * rtl fix i { font-size: 1.15rem; @@ -1284,6 +1287,10 @@ $bubble-margin: .25rem; } } + &.webpage .time { + float: none; + } + .video-time { position: absolute; top: 3px; @@ -1893,7 +1900,7 @@ $bubble-margin: .25rem; margin-left: -4px; .inner { - color: var(--message-out-primary-color); + color: var(--message-out-status-color); bottom: 4px; } @@ -1902,9 +1909,14 @@ $bubble-margin: .25rem; //vertical-align: middle; margin-left: 1px; 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 { margin-bottom: 1px; } */ @@ -1954,7 +1966,7 @@ $bubble-margin: .25rem; } &-time, &-subtitle { - color: var(--message-out-primary-color); + color: var(--message-out-status-color); } &-toggle, &-download { @@ -2002,8 +2014,8 @@ $bubble-margin: .25rem; } } - .audio-subtitle, .contact-number, .document-size { - color: var(--message-out-primary-color); + .contact-number, .document-size { + color: var(--message-out-status-color); } poll-element { diff --git a/src/scss/partials/_selector.scss b/src/scss/partials/_selector.scss index c6186d21..785aeb9a 100644 --- a/src/scss/partials/_selector.scss +++ b/src/scss/partials/_selector.scss @@ -15,6 +15,7 @@ } .selector { + width: 100%; height: 100%; display: flex; flex-direction: column; diff --git a/src/scss/partials/_sidebar.scss b/src/scss/partials/_sidebar.scss index f0036c0a..87d724e4 100644 --- a/src/scss/partials/_sidebar.scss +++ b/src/scss/partials/_sidebar.scss @@ -57,8 +57,8 @@ margin-top: 60px; } */ - > div { + /* > div { width: 100%; - } + } */ } } diff --git a/src/scss/style.scss b/src/scss/style.scss index 6e8ca483..926136da 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -170,6 +170,7 @@ $chat-input-inner-padding-handhelds: .25rem; @include splitColor(message-out-background-color, #eeffde, true, true); --message-out-link-color: var(--link-color); --message-out-primary-color: #4fae4e; + --message-out-status-color: var(--message-out-primary-color); --message-out-audio-play-button-color: #fff; // * Day theme end @@ -215,6 +216,7 @@ html.night { @include splitColor(message-out-background-color, #8774E1, true, true); --message-out-link-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); // * Night theme end } diff --git a/src/tests/slicedArray.test.ts b/src/tests/slicedArray.test.ts new file mode 100644 index 00000000..b5a399bb --- /dev/null +++ b/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)); + }); + }); + }); +});