diff --git a/public/assets/audio/notification.mp3 b/public/assets/audio/notification.mp3 new file mode 100644 index 000000000..0a9f875f2 Binary files /dev/null and b/public/assets/audio/notification.mp3 differ diff --git a/public/assets/img/PinnedMessages.png b/public/assets/img/PinnedMessages.png new file mode 100644 index 000000000..5fdbc4636 Binary files /dev/null and b/public/assets/img/PinnedMessages.png differ diff --git a/public/assets/img/favicon_unread.ico b/public/assets/img/favicon_unread.ico new file mode 100644 index 000000000..7640cc82a Binary files /dev/null and b/public/assets/img/favicon_unread.ico differ diff --git a/src/components/appSelectPeers.ts b/src/components/appSelectPeers.ts index 85daf28ac..6494adb86 100644 --- a/src/components/appSelectPeers.ts +++ b/src/components/appSelectPeers.ts @@ -8,7 +8,7 @@ import rootScope from "../lib/rootScope"; import { cancelEvent, findUpAttribute, findUpClassName } from "../helpers/dom"; import Scrollable from "./scrollable"; import { FocusDirection } from "../helpers/fastSmoothScroll"; -import CheckboxField from "./checkbox"; +import CheckboxField from "./checkboxField"; type PeerType = 'contacts' | 'dialogs'; @@ -346,7 +346,7 @@ export default class AppSelectPeers { if(this.multiSelect) { const selected = this.selected.has(peerId); - const checkboxField = CheckboxField(); + const checkboxField = new CheckboxField(); if(selected) { dom.listEl.classList.add('active'); diff --git a/src/components/chat/selection.ts b/src/components/chat/selection.ts index 60dec7cf2..04e143534 100644 --- a/src/components/chat/selection.ts +++ b/src/components/chat/selection.ts @@ -6,7 +6,7 @@ import { isTouchSupported } from "../../helpers/touchSupport"; import { blurActiveElement, cancelEvent, cancelSelection, findUpClassName, getSelectedText } from "../../helpers/dom"; import Button from "../button"; import ButtonIcon from "../buttonIcon"; -import CheckboxField from "../checkbox"; +import CheckboxField from "../checkboxField"; import PopupDeleteMessages from "../popups/deleteMessages"; import PopupForward from "../popups/forward"; import { toast } from "../toast"; @@ -161,7 +161,7 @@ export default class ChatSelection { if(show) { if(hasCheckbox) return; - const checkboxField = CheckboxField({ + const checkboxField = new CheckboxField({ name: bubble.dataset.mid, round: true }); diff --git a/src/components/checkbox.ts b/src/components/checkbox.ts deleted file mode 100644 index 63afb795f..000000000 --- a/src/components/checkbox.ts +++ /dev/null @@ -1,78 +0,0 @@ -import appStateManager from "../lib/appManagers/appStateManager"; -import { getDeepProperty } from "../helpers/object"; - -const CheckboxField = (options: { - text?: string, - name?: string, - round?: boolean, - stateKey?: string, - disabled?: boolean -} = {}) => { - const label = document.createElement('label'); - label.classList.add('checkbox-field'); - - if(options.round) { - label.classList.add('checkbox-field-round'); - } - - if(options.disabled) { - label.classList.add('checkbox-disabled'); - } - - const input = document.createElement('input'); - input.type = 'checkbox'; - if(options.name) { - input.id = 'input-' + name; - } - - if(options.stateKey) { - appStateManager.getState().then(state => { - input.checked = getDeepProperty(state, options.stateKey); - }); - - input.addEventListener('change', () => { - appStateManager.setByKey(options.stateKey, input.checked); - }); - } - - let span: HTMLSpanElement; - if(options.text) { - span = document.createElement('span'); - span.classList.add('checkbox-caption'); - - if(options.text) { - span.innerText = options.text; - } - } else { - label.classList.add('checkbox-without-caption'); - } - - const box = document.createElement('div'); - box.classList.add('checkbox-box'); - - const checkSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - checkSvg.classList.add('checkbox-box-check'); - checkSvg.setAttributeNS(null, 'viewBox', '0 0 24 24'); - const use = document.createElementNS("http://www.w3.org/2000/svg", "use"); - use.setAttributeNS(null, 'href', '#check'); - use.setAttributeNS(null, 'x', '-1'); - checkSvg.append(use); - - const bg = document.createElement('div'); - bg.classList.add('checkbox-box-background'); - - const border = document.createElement('div'); - border.classList.add('checkbox-box-border'); - - box.append(border, bg, checkSvg); - - label.append(input, box); - - if(span) { - label.append(span); - } - - return {label, input, span}; -}; - -export default CheckboxField; \ No newline at end of file diff --git a/src/components/checkboxField.ts b/src/components/checkboxField.ts new file mode 100644 index 000000000..55db92451 --- /dev/null +++ b/src/components/checkboxField.ts @@ -0,0 +1,100 @@ +import appStateManager from "../lib/appManagers/appStateManager"; +import { getDeepProperty } from "../helpers/object"; + +export default class CheckboxField { + public input: HTMLInputElement; + public label: HTMLLabelElement; + public span: HTMLSpanElement; + + constructor(options: { + text?: string, + name?: string, + round?: boolean, + stateKey?: string, + disabled?: boolean, + checked?: boolean, + } = {}) { + const label = this.label = document.createElement('label'); + label.classList.add('checkbox-field'); + + if(options.round) { + label.classList.add('checkbox-field-round'); + } + + if(options.disabled) { + label.classList.add('checkbox-disabled'); + } + + const input = this.input = document.createElement('input'); + input.type = 'checkbox'; + if(options.name) { + input.id = 'input-' + name; + } + + if(options.checked) { + input.checked = true; + } + + if(options.stateKey) { + appStateManager.getState().then(state => { + this.value = getDeepProperty(state, options.stateKey); + + input.addEventListener('change', () => { + appStateManager.setByKey(options.stateKey, input.checked); + }); + }); + } + + let span: HTMLSpanElement; + if(options.text) { + span = this.span = document.createElement('span'); + span.classList.add('checkbox-caption'); + + if(options.text) { + span.innerText = options.text; + } + } else { + label.classList.add('checkbox-without-caption'); + } + + const box = document.createElement('div'); + box.classList.add('checkbox-box'); + + const checkSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + checkSvg.classList.add('checkbox-box-check'); + checkSvg.setAttributeNS(null, 'viewBox', '0 0 24 24'); + const use = document.createElementNS("http://www.w3.org/2000/svg", "use"); + use.setAttributeNS(null, 'href', '#check'); + use.setAttributeNS(null, 'x', '-1'); + checkSvg.append(use); + + const bg = document.createElement('div'); + bg.classList.add('checkbox-box-background'); + + const border = document.createElement('div'); + border.classList.add('checkbox-box-border'); + + box.append(border, bg, checkSvg); + + label.append(input, box); + + if(span) { + label.append(span); + } + } + + get value() { + return this.input.checked; + } + + set value(value: boolean) { + this.setValueSilently(value); + + const event = new Event('change', {bubbles: true, cancelable: true}); + this.input.dispatchEvent(event); + } + + public setValueSilently(value: boolean) { + this.input.checked = value; + } +} diff --git a/src/components/inputField.ts b/src/components/inputField.ts index c72dc8949..60de2250f 100644 --- a/src/components/inputField.ts +++ b/src/components/inputField.ts @@ -206,4 +206,4 @@ class InputField { } } -export default InputField; \ No newline at end of file +export default InputField; diff --git a/src/components/middleEllipsis.ts b/src/components/middleEllipsis.ts index 1a9975ec6..2ffab696f 100644 --- a/src/components/middleEllipsis.ts +++ b/src/components/middleEllipsis.ts @@ -23,7 +23,7 @@ const map: Map = new Map(); const testQueue: Set = new Set(); -const fontFamily = 'Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif'; +export const fontFamily = 'Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif'; const fontSize = '16px'; let timeoutId: number; diff --git a/src/components/popups/createPoll.ts b/src/components/popups/createPoll.ts index 031063621..0aaa9a519 100644 --- a/src/components/popups/createPoll.ts +++ b/src/components/popups/createPoll.ts @@ -2,7 +2,7 @@ import type { Poll } from "../../lib/appManagers/appPollsManager"; import type Chat from "../chat/chat"; import PopupElement from "."; import { cancelEvent, findUpTag, getRichValue, isInputEmpty, whichChild } from "../../helpers/dom"; -import CheckboxField from "../checkbox"; +import CheckboxField from "../checkboxField"; import InputField from "../inputField"; import RadioField from "../radioField"; import Scrollable from "../scrollable"; @@ -20,7 +20,7 @@ export default class PopupCreatePoll extends PopupElement { private scrollable: Scrollable; private tempId = 0; - private anonymousCheckboxField: ReturnType; + private anonymousCheckboxField: CheckboxField; private multipleCheckboxField: PopupCreatePoll['anonymousCheckboxField']; private quizCheckboxField: PopupCreatePoll['anonymousCheckboxField']; @@ -77,7 +77,7 @@ export default class PopupCreatePoll extends PopupElement { settingsCaption.innerText = 'Settings'; if(!this.chat.appPeersManager.isBroadcast(this.chat.peerId)) { - this.anonymousCheckboxField = CheckboxField({ + this.anonymousCheckboxField = new CheckboxField({ text: 'Anonymous Voting', name: 'anonymous' }); @@ -85,11 +85,11 @@ export default class PopupCreatePoll extends PopupElement { dd.append(this.anonymousCheckboxField.label); } - this.multipleCheckboxField = CheckboxField({ + this.multipleCheckboxField = new CheckboxField({ text: 'Multiple Answers', name: 'multiple' }); - this.quizCheckboxField = CheckboxField({ + this.quizCheckboxField = new CheckboxField({ text: 'Quiz Mode', name: 'quiz' }); @@ -303,7 +303,7 @@ export default class PopupCreatePoll extends PopupElement { }); questionField.input.addEventListener('input', this.onInput); - const radioField = RadioField('', 'question'); + const radioField = new RadioField('', 'question'); radioField.main.append(questionField.container); questionField.input.addEventListener('click', cancelEvent); radioField.label.classList.add('hidden-widget'); diff --git a/src/components/popups/newMedia.ts b/src/components/popups/newMedia.ts index 058d4bdea..510c59095 100644 --- a/src/components/popups/newMedia.ts +++ b/src/components/popups/newMedia.ts @@ -6,7 +6,7 @@ import PopupElement from "."; import Scrollable from "../scrollable"; import { toast } from "../toast"; import { prepareAlbum, wrapDocument } from "../wrappers"; -import CheckboxField from "../checkbox"; +import CheckboxField from "../checkboxField"; import SendContextMenu from "../chat/sendContextMenu"; import { createPosterForVideo, createPosterFromVideo, onVideoLoad } from "../../helpers/files"; import { MyDocument } from "../../lib/appManagers/appDocsManager"; @@ -28,7 +28,7 @@ const MAX_LENGTH_CAPTION = 1024; export default class PopupNewMedia extends PopupElement { private input: HTMLElement; private mediaContainer: HTMLElement; - private groupCheckboxField: { label: HTMLLabelElement; input: HTMLInputElement; span: HTMLSpanElement; }; + private groupCheckboxField: CheckboxField; private wasInputValue = ''; private willAttach: Partial<{ @@ -89,7 +89,7 @@ export default class PopupNewMedia extends PopupElement { this.container.append(scrollable.container); if(files.length > 1) { - this.groupCheckboxField = CheckboxField({ + this.groupCheckboxField = new CheckboxField({ text: 'Group items', name: 'group-items' }); diff --git a/src/components/privacySection.ts b/src/components/privacySection.ts index 4de642625..70c3c4475 100644 --- a/src/components/privacySection.ts +++ b/src/components/privacySection.ts @@ -62,7 +62,7 @@ export default class PrivacySection { const random = randomLong(); r.forEach(({type, text}) => { - this.radioRows.set(type, new Row({radioField: RadioField(text, random, '' + type)})); + this.radioRows.set(type, new Row({radioField: new RadioField(text, random, '' + type)})); }); const form = RadioFormFromRows([...this.radioRows.values()], this.onRadioChange); diff --git a/src/components/radioField.ts b/src/components/radioField.ts index a1d0c1470..891f0aa2c 100644 --- a/src/components/radioField.ts +++ b/src/components/radioField.ts @@ -1,48 +1,50 @@ import appStateManager from "../lib/appManagers/appStateManager"; import { getDeepProperty } from "../helpers/object"; -const RadioField = (text: string, name: string, value?: string, stateKey?: string) => { - const label = document.createElement('label'); - label.classList.add('radio-field'); +export default class RadioField { + public input: HTMLInputElement; + public label: HTMLLabelElement; + public main: HTMLElement; - const input = document.createElement('input'); - input.type = 'radio'; - /* input.id = */input.name = 'input-radio-' + name; - - if(value) { - input.value = value; - - if(stateKey) { - appStateManager.getState().then(state => { - input.checked = getDeepProperty(state, stateKey) === value; - }); + constructor(text: string, name: string, value?: string, stateKey?: string) { + const label = this.label = document.createElement('label'); + label.classList.add('radio-field'); - input.addEventListener('change', () => { - appStateManager.setByKey(stateKey, value); - }); + const input = this.input = document.createElement('input'); + input.type = 'radio'; + /* input.id = */input.name = 'input-radio-' + name; + + if(value) { + input.value = value; + + if(stateKey) { + appStateManager.getState().then(state => { + input.checked = getDeepProperty(state, stateKey) === value; + }); + + input.addEventListener('change', () => { + appStateManager.setByKey(stateKey, value); + }); + } } - } - - const main = document.createElement('div'); - main.classList.add('radio-field-main'); - - if(text) { - main.innerHTML = text; - /* const caption = document.createElement('div'); - caption.classList.add('radio-field-main-caption'); - caption.innerHTML = text; - - if(subtitle) { - label.classList.add('radio-field-with-subtitle'); - caption.insertAdjacentHTML('beforeend', `
${subtitle}
`); + + const main = this.main = document.createElement('div'); + main.classList.add('radio-field-main'); + + if(text) { + main.innerHTML = text; + /* const caption = document.createElement('div'); + caption.classList.add('radio-field-main-caption'); + caption.innerHTML = text; + + if(subtitle) { + label.classList.add('radio-field-with-subtitle'); + caption.insertAdjacentHTML('beforeend', `
${subtitle}
`); + } + + main.append(caption); */ } - - main.append(caption); */ + + label.append(input, main); } - - label.append(input, main); - - return {label, input, main}; }; - -export default RadioField; \ No newline at end of file diff --git a/src/components/row.ts b/src/components/row.ts index cdd1d3d92..9d199ae9d 100644 --- a/src/components/row.ts +++ b/src/components/row.ts @@ -1,4 +1,4 @@ -import CheckboxField from "./checkbox"; +import CheckboxField from "./checkboxField"; import RadioField from "./radioField"; import { ripple } from "./ripple"; import { SliderSuperTab } from "./slider"; @@ -9,8 +9,8 @@ export default class Row { public title: HTMLDivElement; public subtitle: HTMLElement; - public checkboxField: ReturnType; - public radioField: ReturnType; + public checkboxField: CheckboxField; + public radioField: RadioField; public freezed = false; @@ -44,6 +44,10 @@ export default class Row { if(options.checkboxField) { this.checkboxField = options.checkboxField; this.container.append(this.checkboxField.label); + + this.checkboxField.input.addEventListener('change', () => { + this.subtitle.innerHTML = this.checkboxField.input.checked ? 'Enabled' : 'Disabled'; + }); } } else { if(options.title) { diff --git a/src/components/sidebarLeft/tabs/background.ts b/src/components/sidebarLeft/tabs/background.ts index b658e5593..a1caffa51 100644 --- a/src/components/sidebarLeft/tabs/background.ts +++ b/src/components/sidebarLeft/tabs/background.ts @@ -12,7 +12,7 @@ import appStateManager from "../../../lib/appManagers/appStateManager"; import apiManager from "../../../lib/mtproto/mtprotoworker"; import rootScope from "../../../lib/rootScope"; import Button from "../../button"; -import CheckboxField from "../../checkbox"; +import CheckboxField from "../../checkboxField"; import ProgressivePreloader from "../../preloader"; import { SliderSuperTab } from "../../slider"; import { wrapPhoto } from "../../wrappers"; @@ -28,7 +28,7 @@ export default class AppBackgroundTab extends SliderSuperTab { const uploadButton = Button('btn-primary btn-transparent', {icon: 'cameraadd', text: 'Upload Wallpaper', disabled: true}); const colorButton = Button('btn-primary btn-transparent', {icon: 'colorize', text: 'Set a Color', disabled: true}); - const blurCheckboxField = CheckboxField({ + const blurCheckboxField = new CheckboxField({ text: 'Blur Wallpaper Image', name: 'blur', stateKey: 'settings.background.blur' diff --git a/src/components/sidebarLeft/tabs/generalSettings.ts b/src/components/sidebarLeft/tabs/generalSettings.ts index 3ae5d8e48..ade5d5f3d 100644 --- a/src/components/sidebarLeft/tabs/generalSettings.ts +++ b/src/components/sidebarLeft/tabs/generalSettings.ts @@ -2,7 +2,7 @@ import { SliderSuperTab } from "../../slider" import { generateSection } from ".."; import RangeSelector from "../../rangeSelector"; import Button from "../../button"; -import CheckboxField from "../../checkbox"; +import CheckboxField from "../../checkboxField"; import RadioField from "../../radioField"; import appStateManager from "../../../lib/appManagers/appStateManager"; import rootScope from "../../../lib/rootScope"; @@ -73,7 +73,7 @@ export default class AppGeneralSettingsTab extends SliderSuperTab { new AppBackgroundTab(this.slider).open(); }); - const animationsCheckboxField = CheckboxField({ + const animationsCheckboxField = new CheckboxField({ text: 'Enable Animations', name: 'animations', stateKey: 'settings.animationsEnabled' @@ -88,12 +88,12 @@ export default class AppGeneralSettingsTab extends SliderSuperTab { const form = document.createElement('form'); const enterRow = new Row({ - radioField: RadioField('Send by Enter', 'send-shortcut', 'enter', 'settings.sendShortcut'), + radioField: new RadioField('Send by Enter', 'send-shortcut', 'enter', 'settings.sendShortcut'), subtitle: 'New line by Shift + Enter', }); const ctrlEnterRow = new Row({ - radioField: RadioField(`Send by ${isApple ? '⌘' : 'Ctrl'} + Enter`, 'send-shortcut', 'ctrlEnter', 'settings.sendShortcut'), + radioField: new RadioField(`Send by ${isApple ? '⌘' : 'Ctrl'} + Enter`, 'send-shortcut', 'ctrlEnter', 'settings.sendShortcut'), subtitle: 'New line by Enter', }); @@ -105,22 +105,22 @@ export default class AppGeneralSettingsTab extends SliderSuperTab { const container = section('Auto-Download Media'); container.classList.add('sidebar-left-section-disabled'); - const contactsCheckboxField = CheckboxField({ + const contactsCheckboxField = new CheckboxField({ text: 'Contacts', name: 'contacts', stateKey: 'settings.autoDownload.contacts' }); - const privateCheckboxField = CheckboxField({ + const privateCheckboxField = new CheckboxField({ text: 'Private Chats', name: 'private', stateKey: 'settings.autoDownload.private' }); - const groupsCheckboxField = CheckboxField({ + const groupsCheckboxField = new CheckboxField({ text: 'Group Chats', name: 'groups', stateKey: 'settings.autoDownload.groups' }); - const channelsCheckboxField = CheckboxField({ + const channelsCheckboxField = new CheckboxField({ text: 'Channels', name: 'channels', stateKey: 'settings.autoDownload.channels' @@ -133,12 +133,12 @@ export default class AppGeneralSettingsTab extends SliderSuperTab { const container = section('Auto-Play Media'); container.classList.add('sidebar-left-section-disabled'); - const gifsCheckboxField = CheckboxField({ + const gifsCheckboxField = new CheckboxField({ text: 'GIFs', name: 'gifs', stateKey: 'settings.autoPlay.gifs' }); - const videosCheckboxField = CheckboxField({ + const videosCheckboxField = new CheckboxField({ text: 'Videos', name: 'videos', stateKey: 'settings.autoPlay.videos' @@ -150,12 +150,12 @@ export default class AppGeneralSettingsTab extends SliderSuperTab { { const container = section('Stickers'); - const suggestCheckboxField = CheckboxField({ + const suggestCheckboxField = new CheckboxField({ text: 'Suggest Stickers by Emoji', name: 'suggest', stateKey: 'settings.stickers.suggest' }); - const loopCheckboxField = CheckboxField({ + const loopCheckboxField = new CheckboxField({ text: 'Loop Animated Stickers', name: 'loop', stateKey: 'settings.stickers.loop' diff --git a/src/components/sidebarLeft/tabs/includedChats.ts b/src/components/sidebarLeft/tabs/includedChats.ts index 9d5ada0bd..b7eb226cd 100644 --- a/src/components/sidebarLeft/tabs/includedChats.ts +++ b/src/components/sidebarLeft/tabs/includedChats.ts @@ -7,7 +7,7 @@ import { MyDialogFilter as DialogFilter } from "../../../lib/storages/filters"; import rootScope from "../../../lib/rootScope"; import { copy } from "../../../helpers/object"; import ButtonIcon from "../../buttonIcon"; -import CheckboxField from "../../checkbox"; +import CheckboxField from "../../checkboxField"; import Button from "../../button"; import AppEditFolderTab from "./editFolder"; @@ -94,7 +94,7 @@ export default class AppIncludedChatsTab extends SliderSuperTab { } checkbox(selected?: boolean) { - const checkboxField = CheckboxField({ + const checkboxField = new CheckboxField({ round: true }); if(selected) { diff --git a/src/components/sidebarLeft/tabs/notifications.ts b/src/components/sidebarLeft/tabs/notifications.ts new file mode 100644 index 000000000..89834a645 --- /dev/null +++ b/src/components/sidebarLeft/tabs/notifications.ts @@ -0,0 +1,128 @@ +import { SettingSection } from ".."; +import Row from "../../row"; +import CheckboxField from "../../checkboxField"; +import { InputNotifyPeer, PeerNotifySettings, Update } from "../../../layer"; +import appNotificationsManager from "../../../lib/appManagers/appNotificationsManager"; +import { SliderSuperTabEventable } from "../../sliderTab"; +import { copy } from "../../../helpers/object"; +import rootScope from "../../../lib/rootScope"; +import { convertKeyToInputKey } from "../../../helpers/string"; + +type InputNotifyKey = Exclude; + +export default class AppNotificationsTab extends SliderSuperTabEventable { + protected init() { + this.container.classList.add('notifications-container'); + this.title.innerText = 'Notifications'; + + const NotifySection = (options: { + name: string, + typeText: string, + inputKey: InputNotifyKey, + }) => { + const section = new SettingSection({ + name: options.name + }); + + const enabledRow = new Row({ + checkboxField: new CheckboxField({text: options.typeText, checked: true}), + subtitle: 'Loading...', + }); + + const previewEnabledRow = new Row({ + checkboxField: new CheckboxField({text: 'Message preview', checked: true}), + subtitle: 'Loading...', + }); + + section.content.append(enabledRow.container, previewEnabledRow.container); + + this.scrollable.append(section.container); + + const inputNotifyPeer = {_: options.inputKey}; + appNotificationsManager.getNotifySettings(inputNotifyPeer).then((notifySettings) => { + const applySettings = () => { + const muted = appNotificationsManager.isMuted(notifySettings); + enabledRow.checkboxField.value = !muted; + previewEnabledRow.checkboxField.value = notifySettings.show_previews; + + return muted; + }; + + applySettings(); + + this.eventListener.addListener('destroy', () => { + const mute = !enabledRow.checkboxField.value; + const showPreviews = previewEnabledRow.checkboxField.value; + + if(mute === appNotificationsManager.isMuted(notifySettings) && showPreviews === notifySettings.show_previews) { + return; + } + + const inputSettings: any = copy(notifySettings); + inputSettings._ = 'inputPeerNotifySettings'; + inputSettings.mute_until = mute ? 2147483647 : 0; + inputSettings.show_previews = showPreviews; + + appNotificationsManager.updateNotifySettings(inputNotifyPeer, inputSettings); + }, true); + + this.listenerSetter.add(rootScope, 'notify_settings', (update: Update.updateNotifySettings) => { + const inputKey = convertKeyToInputKey(update.peer._) as any; + if(options.inputKey === inputKey) { + notifySettings = update.notify_settings; + applySettings(); + } + }); + }); + }; + + NotifySection({ + name: 'Private Chats', + typeText: 'Notifications for private chats', + inputKey: 'inputNotifyUsers' + }); + + NotifySection({ + name: 'Groups', + typeText: 'Notifications for groups', + inputKey: 'inputNotifyChats' + }); + + NotifySection({ + name: 'Channels', + typeText: 'Notifications for channels', + inputKey: 'inputNotifyBroadcasts' + }); + + { + const section = new SettingSection({ + name: 'Other' + }); + + const contactsSignUpRow = new Row({ + checkboxField: new CheckboxField({text: 'Contacts joined Telegram', checked: true}), + subtitle: 'Loading...', + }); + + const soundRow = new Row({ + checkboxField: new CheckboxField({text: 'Notification sound', checked: true, stateKey: 'settings.notifications.sound'}), + subtitle: 'Enabled', + }); + + section.content.append(contactsSignUpRow.container, soundRow.container); + + this.scrollable.append(section.container); + + appNotificationsManager.getContactSignUpNotification().then(enabled => { + contactsSignUpRow.checkboxField.value = enabled; + + this.eventListener.addListener('destroy', () => { + const _enabled = contactsSignUpRow.checkboxField.value; + if(enabled !== _enabled) { + appNotificationsManager.setContactSignUpNotification(!_enabled); + } + }, true); + }); + } + } +} diff --git a/src/components/sidebarLeft/tabs/privacyAndSecurity.ts b/src/components/sidebarLeft/tabs/privacyAndSecurity.ts index d017347d9..a9d4c8eca 100644 --- a/src/components/sidebarLeft/tabs/privacyAndSecurity.ts +++ b/src/components/sidebarLeft/tabs/privacyAndSecurity.ts @@ -18,6 +18,7 @@ import apiManager from "../../../lib/mtproto/mtprotoworker"; import AppBlockedUsersTab from "./blockedUsers"; import appUsersManager from "../../../lib/appManagers/appUsersManager"; import rootScope from "../../../lib/rootScope"; +import { convertKeyToInputKey } from "../../../helpers/string"; export default class AppPrivacyAndSecurityTab extends SliderSuperTab { private activeSessionsRow: Row; @@ -204,11 +205,7 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTab { } rootScope.on('privacy_update', (update) => { - let key: string = update.key._; - key = key[0].toUpperCase() + key.slice(1); - key = 'input' + key; - - updatePrivacyRow(key as any); + updatePrivacyRow(convertKeyToInputKey(update.key._) as any); }); container.append(numberVisibilityRow.container, lastSeenTimeRow.container, photoVisibilityRow.container, callRow.container, linkAccountRow.container, groupChatsAddRow.container); diff --git a/src/components/sidebarLeft/tabs/settings.ts b/src/components/sidebarLeft/tabs/settings.ts index a9d78dde6..a647e33d8 100644 --- a/src/components/sidebarLeft/tabs/settings.ts +++ b/src/components/sidebarLeft/tabs/settings.ts @@ -8,6 +8,7 @@ import AppPrivacyAndSecurityTab from "./privacyAndSecurity"; import AppGeneralSettingsTab from "./generalSettings"; import AppEditProfileTab from "./editProfile"; import AppChatFoldersTab from "./chatFolders"; +import AppNotificationsTab from "./notifications"; //import AppMediaViewer from "../../appMediaViewerNew"; export default class AppSettingsTab extends SliderSuperTab { @@ -97,7 +98,7 @@ export default class AppSettingsTab extends SliderSuperTab { buttonsDiv.append(this.buttons.edit = Button(className, {icon: 'edit', text: 'Edit Profile'})); buttonsDiv.append(this.buttons.folders = Button(className, {icon: 'folder', text: 'Chat Folders'})); buttonsDiv.append(this.buttons.general = Button(className, {icon: 'settings', text: 'General Settings'})); - buttonsDiv.append(this.buttons.notifications = Button(className, {icon: 'unmute', text: 'Notifications', disabled: true})); + buttonsDiv.append(this.buttons.notifications = Button(className, {icon: 'unmute', text: 'Notifications'})); buttonsDiv.append(this.buttons.privacy = Button(className, {icon: 'lock', text: 'Privacy and Security'})); buttonsDiv.append(this.buttons.language = Button(className, {icon: 'language', text: 'Language', disabled: true})); @@ -122,6 +123,10 @@ export default class AppSettingsTab extends SliderSuperTab { new AppGeneralSettingsTab(this.slider as any).open(); }); + this.buttons.notifications.addEventListener('click', () => { + new AppNotificationsTab(this.slider).open(); + }); + this.buttons.privacy.addEventListener('click', () => { new AppPrivacyAndSecurityTab(this.slider).open(); }); diff --git a/src/components/sidebarRight/tabs/sharedMedia.ts b/src/components/sidebarRight/tabs/sharedMedia.ts index b0be4f8d3..b0f09c1d8 100644 --- a/src/components/sidebarRight/tabs/sharedMedia.ts +++ b/src/components/sidebarRight/tabs/sharedMedia.ts @@ -10,7 +10,7 @@ import AppSearchSuper, { SearchSuperType } from "../../appSearchSuper."; import AvatarElement from "../../avatar"; import Scrollable from "../../scrollable"; import { SliderTab } from "../../slider"; -import CheckboxField from "../../checkbox"; +import CheckboxField from "../../checkboxField"; import { attachClickEvent, cancelEvent } from "../../../helpers/dom"; import appSidebarRight from ".."; import { TransitionSlider } from "../../transition"; @@ -81,7 +81,7 @@ export default class AppSharedMediaTab implements SliderTab { notificationsStatus: this.profileContentEl.querySelector('.profile-row-notifications > p') }; - const checkboxField = CheckboxField({ + const checkboxField = new CheckboxField({ text: 'Notifications', name: 'notifications' }); diff --git a/src/helpers/string.ts b/src/helpers/string.ts index 59f22cf5b..9387756f1 100644 --- a/src/helpers/string.ts +++ b/src/helpers/string.ts @@ -87,3 +87,14 @@ export const checkRTL = (s: string) => { }; //(window as any).checkRTL = checkRTL; + +export function convertInputKeyToKey(inputKey: string) { + const str = inputKey.replace('input', ''); + return (str[0].toLowerCase() + str.slice(1)) as string; +} + +export function convertKeyToInputKey(key: string) { + key = key[0].toUpperCase() + key.slice(1); + key = 'input' + key; + return key; +} diff --git a/src/helpers/userAgent.ts b/src/helpers/userAgent.ts index fef748211..a7c30f7a8 100644 --- a/src/helpers/userAgent.ts +++ b/src/helpers/userAgent.ts @@ -21,3 +21,5 @@ export const isAppleMobile = (/iPad|iPhone|iPod/.test(navigator.platform) || export const isSafari = !!('safari' in ctx) || !!(userAgent && (/\b(iPad|iPhone|iPod)\b/.test(userAgent) || (!!userAgent.match('Safari') && !userAgent.match('Chrome'))))/* || true */; export const isMobileSafari = isSafari && isAppleMobile; + +export const isMobile = /* screen.width && screen.width < 480 || */navigator.userAgent.search(/iOS|iPhone OS|Android|BlackBerry|BB10|Series ?[64]0|J2ME|MIDP|opera mini|opera mobi|mobi.+Gecko|Windows Phone/i) != -1; diff --git a/src/lib/appManagers/apiUpdatesManager.ts b/src/lib/appManagers/apiUpdatesManager.ts index 81e6477b8..f66bd8c4e 100644 --- a/src/lib/appManagers/apiUpdatesManager.ts +++ b/src/lib/appManagers/apiUpdatesManager.ts @@ -1,5 +1,5 @@ //import apiManager from '../mtproto/apiManager'; -import { MOUNT_CLASS_TO } from '../../config/debug'; +import DEBUG, { MOUNT_CLASS_TO } from '../../config/debug'; import { logger, LogLevels } from '../logger'; import apiManager from '../mtproto/mtprotoworker'; import rootScope from '../rootScope'; @@ -10,14 +10,14 @@ import appStateManager from './appStateManager'; import appUsersManager from "./appUsersManager"; type UpdatesState = { - pendingPtsUpdates: any[], - pendingSeqUpdates?: any, + pendingPtsUpdates: {pts: number, pts_count: number}[], + pendingSeqUpdates?: {[seq: number]: {seq: number, date: number, updates: any[]}}, syncPending: { seqAwaiting?: number, ptsAwaiting?: true, timeout: number }, - syncLoading: boolean, + syncLoading: Promise, seq?: number, pts?: number, @@ -32,13 +32,14 @@ export class ApiUpdatesManager { pendingPtsUpdates: [], pendingSeqUpdates: {}, syncPending: null, - syncLoading: true + syncLoading: null }; public channelStates: {[channelId: number]: UpdatesState} = {}; private attached = false; private log = logger('UPDATES', LogLevels.error | LogLevels.log | LogLevels.warn | LogLevels.debug); + private debug = DEBUG; constructor() { // * false for test purposes @@ -53,31 +54,33 @@ export class ApiUpdatesManager { } public popPendingSeqUpdate() { - const nextSeq = this.updatesState.seq + 1; - const pendingUpdatesData = this.updatesState.pendingSeqUpdates[nextSeq]; + const state = this.updatesState; + const nextSeq = state.seq + 1; + const pendingUpdatesData = state.pendingSeqUpdates[nextSeq]; if(!pendingUpdatesData) { return false; } const updates = pendingUpdatesData.updates; - for(let i = 0, length = updates.length; i < length; i++) { + for(let i = 0, length = updates.length; i < length; ++i) { this.saveUpdate(updates[i]); } - this.updatesState.seq = pendingUpdatesData.seq; - if(pendingUpdatesData.date && this.updatesState.date < pendingUpdatesData.date) { - this.updatesState.date = pendingUpdatesData.date; + + state.seq = pendingUpdatesData.seq; + if(pendingUpdatesData.date && state.date < pendingUpdatesData.date) { + state.date = pendingUpdatesData.date; } - delete this.updatesState.pendingSeqUpdates[nextSeq]; + delete state.pendingSeqUpdates[nextSeq]; if(!this.popPendingSeqUpdate() && - this.updatesState.syncPending && - this.updatesState.syncPending.seqAwaiting && - this.updatesState.seq >= this.updatesState.syncPending.seqAwaiting) { - if(!this.updatesState.syncPending.ptsAwaiting) { - clearTimeout(this.updatesState.syncPending.timeout); - this.updatesState.syncPending = null; + state.syncPending && + state.syncPending.seqAwaiting && + state.seq >= state.syncPending.seqAwaiting) { + if(!state.syncPending.ptsAwaiting) { + clearTimeout(state.syncPending.timeout); + state.syncPending = null; } else { - delete this.updatesState.syncPending.seqAwaiting; + delete state.syncPending.seqAwaiting; } } @@ -89,7 +92,8 @@ export class ApiUpdatesManager { if(!curState.pendingPtsUpdates.length) { return false; } - curState.pendingPtsUpdates.sort((a: any, b: any) => { + + curState.pendingPtsUpdates.sort((a, b) => { return a.pts - b.pts; }); // this.log('pop update', channelId, curState.pendingPtsUpdates) @@ -97,7 +101,7 @@ export class ApiUpdatesManager { let curPts = curState.pts; let goodPts = 0; let goodIndex = 0; - for(let i = 0, length = curState.pendingPtsUpdates.length; i < length; i++) { + for(let i = 0, length = curState.pendingPtsUpdates.length; i < length; ++i) { const update = curState.pendingPtsUpdates[i]; curPts += update.pts_count; if(curPts >= update.pts) { @@ -110,10 +114,10 @@ export class ApiUpdatesManager { return false; } - this.log('pop pending pts updates', goodPts, curState.pendingPtsUpdates.slice(0, goodIndex + 1)); + this.debug && this.log('pop pending pts updates', goodPts, curState.pendingPtsUpdates.slice(0, goodIndex + 1)); curState.pts = goodPts; - for(let i = 0; i <= goodIndex; i++) { + for(let i = 0; i <= goodIndex; ++i) { const update = curState.pendingPtsUpdates[i]; this.saveUpdate(update); } @@ -137,18 +141,18 @@ export class ApiUpdatesManager { } } - public processUpdateMessage = (updateMessage: any, options: Partial<{ + public processUpdateMessage = (updateMessage: any/* , options: Partial<{ ignoreSyncLoading: boolean - }> = {}) => { + }> = {} */) => { // return forceGetDifference() const processOpts = { date: updateMessage.date, seq: updateMessage.seq, seqStart: updateMessage.seq_start, - ignoreSyncLoading: options.ignoreSyncLoading + //ignoreSyncLoading: options.ignoreSyncLoading }; - this.log('processUpdateMessage', updateMessage); + this.debug && this.log('processUpdateMessage', updateMessage); switch(updateMessage._) { case 'updatesTooLong': @@ -162,7 +166,7 @@ export class ApiUpdatesManager { case 'updateShortMessage': case 'updateShortChatMessage': { - this.log('updateShortMessage | updateShortChatMessage', {...updateMessage}); + this.debug && this.log('updateShortMessage | updateShortChatMessage', {...updateMessage}); const isOut = updateMessage.pFlags.out; const fromId = updateMessage.from_id || (isOut ? rootScope.myId : updateMessage.user_id); const toId = updateMessage.chat_id @@ -204,37 +208,34 @@ export class ApiUpdatesManager { } }; - public getDifference(first = false) { + public getDifference(first = false): Promise { // this.trace('Get full diff') const updatesState = this.updatesState; - if(!updatesState.syncLoading) { - updatesState.syncLoading = true; + let wasSyncing = updatesState.syncLoading; + if(!wasSyncing) { updatesState.pendingSeqUpdates = {}; updatesState.pendingPtsUpdates = []; - rootScope.broadcast('state_synchronizing'); } if(updatesState.syncPending) { clearTimeout(updatesState.syncPending.timeout); updatesState.syncPending = null; } - - return apiManager.invokeApi('updates.getDifference', { + + const promise = apiManager.invokeApi('updates.getDifference', { pts: updatesState.pts, date: updatesState.date, qts: -1 }, { timeout: 0x7fffffff }).then((differenceResult) => { - this.log('Get diff result', differenceResult); + this.debug && this.log('Get diff result', differenceResult); if(differenceResult._ === 'updates.differenceEmpty') { - this.log('apply empty diff', differenceResult.seq); + this.debug && this.log('apply empty diff', differenceResult.seq); updatesState.date = differenceResult.date; updatesState.seq = differenceResult.seq; - updatesState.syncLoading = false; - rootScope.broadcast('state_synchronized'); - return false; + return; } // ! SORRY I'M SORRY I'M SORRY @@ -284,23 +285,24 @@ export class ApiUpdatesManager { // this.log('apply diff', updatesState.seq, updatesState.pts) if(differenceResult._ === 'updates.differenceSlice') { - this.getDifference(); + return this.getDifference(); } else { - // this.log('finished get diff') - rootScope.broadcast('state_synchronized'); - updatesState.syncLoading = false; + this.debug && this.log('finished get diff'); } - }, () => { - updatesState.syncLoading = false; }); + + if(!wasSyncing) { + this.justAName(updatesState, promise); + } + + return promise; } - public getChannelDifference(channelId: number) { + public getChannelDifference(channelId: number): Promise { const channelState = this.getChannelState(channelId); - if(!channelState.syncLoading) { - channelState.syncLoading = true; + const wasSyncing = channelState.syncLoading; + if(!wasSyncing) { channelState.pendingPtsUpdates = []; - rootScope.broadcast('state_synchronizing', channelId); } if(channelState.syncPending) { @@ -309,40 +311,37 @@ export class ApiUpdatesManager { } //this.log.trace('Get channel diff', appChatsManager.getChat(channelId), channelState.pts); - apiManager.invokeApi('updates.getChannelDifference', { + const promise = apiManager.invokeApi('updates.getChannelDifference', { channel: appChatsManager.getChannelInput(channelId), filter: {_: 'channelMessagesFilterEmpty'}, pts: channelState.pts, limit: 30 }, {timeout: 0x7fffffff}).then((differenceResult) => { - this.log('Get channel diff result', differenceResult) + this.debug && this.log('Get channel diff result', differenceResult) channelState.pts = 'pts' in differenceResult ? differenceResult.pts : undefined; if(differenceResult._ === 'updates.channelDifferenceEmpty') { - this.log('apply channel empty diff', differenceResult); - channelState.syncLoading = false; - rootScope.broadcast('state_synchronized', channelId); - return false; + this.debug && this.log('apply channel empty diff', differenceResult); + return; } if(differenceResult._ === 'updates.channelDifferenceTooLong') { - this.log('channel diff too long', differenceResult); - channelState.syncLoading = false; + this.debug && this.log('channel diff too long', differenceResult); delete this.channelStates[channelId]; this.saveUpdate({_: 'updateChannelReload', channel_id: channelId}); - return false; + return; } appUsersManager.saveApiUsers(differenceResult.users); appChatsManager.saveApiChats(differenceResult.chats); // Should be first because of updateMessageID - this.log('applying', differenceResult.other_updates.length, 'channel other updates'); + this.debug && this.log('applying', differenceResult.other_updates.length, 'channel other updates'); differenceResult.other_updates.forEach((update) => { this.saveUpdate(update); }); - this.log('applying', differenceResult.new_messages.length, 'channel new messages'); + this.debug && this.log('applying', differenceResult.new_messages.length, 'channel new messages'); differenceResult.new_messages.forEach((apiMessage) => { this.saveUpdate({ _: 'updateNewChannelMessage', @@ -352,18 +351,32 @@ export class ApiUpdatesManager { }); }); - this.log('apply channel diff', channelState.pts); + this.debug && this.log('apply channel diff', channelState.pts); if(differenceResult._ === 'updates.channelDifference' && !differenceResult.pFlags['final']) { - this.getChannelDifference(channelId); + return this.getChannelDifference(channelId); } else { - this.log('finished channel get diff'); - rootScope.broadcast('state_synchronized', channelId); - channelState.syncLoading = false; + this.debug && this.log('finished channel get diff'); } + }); + + if(!wasSyncing) { + this.justAName(channelState, promise, channelId); + } + + return promise; + } + + private justAName(state: UpdatesState, promise: UpdatesState['syncLoading'], channelId?: number) { + state.syncLoading = promise; + rootScope.broadcast('state_synchronizing', channelId); + + promise.then(() => { + state.syncLoading = null; + rootScope.broadcast('state_synchronized', channelId); }, () => { - channelState.syncLoading = false; + state.syncLoading = null; }); } @@ -377,7 +390,7 @@ export class ApiUpdatesManager { pts, pendingPtsUpdates: [], syncPending: null, - syncLoading: false + syncLoading: null }; return true; @@ -397,8 +410,8 @@ export class ApiUpdatesManager { public processUpdate(update: any, options: Partial<{ date: number, seq: number, - seqStart: number, - ignoreSyncLoading: boolean + seqStart: number/* , + ignoreSyncLoading: boolean */ }> = {}) { let channelId = 0; switch(update._) { @@ -421,7 +434,7 @@ export class ApiUpdatesManager { // this.log.log('process', channelId, curState.pts, update) - if(curState.syncLoading && !options.ignoreSyncLoading) { + if(curState.syncLoading/* && !options.ignoreSyncLoading */) { return false; } @@ -466,18 +479,24 @@ export class ApiUpdatesManager { if(update.pts) { const newPts = curState.pts + (update.pts_count || 0); if(newPts < update.pts) { - this.log.warn('Pts hole', curState, update, channelId && appChatsManager.getChat(channelId)); + this.debug && this.log.warn('Pts hole', curState, update, channelId && appChatsManager.getChat(channelId)); curState.pendingPtsUpdates.push(update); - if(!curState.syncPending) { + if(!curState.syncPending && !curState.syncLoading) { curState.syncPending = { timeout: window.setTimeout(() => { + curState.syncPending = null; + + if(curState.syncLoading) { + return; + } + if(channelId) { this.getChannelDifference(channelId); } else { this.getDifference(); } }, SYNC_DELAY) - } + }; } curState.syncPending.ptsAwaiting = true; @@ -503,7 +522,7 @@ export class ApiUpdatesManager { if(seqStart !== curState.seq + 1) { if(seqStart > curState.seq) { - this.log.warn('Seq hole', curState, curState.syncPending && curState.syncPending.seqAwaiting); + this.debug && this.log.warn('Seq hole', curState, curState.syncPending && curState.syncPending.seqAwaiting); if(curState.pendingSeqUpdates[seqStart] === undefined) { curState.pendingSeqUpdates[seqStart] = {seq, date: options.date, updates: []}; @@ -513,9 +532,15 @@ export class ApiUpdatesManager { if(!curState.syncPending) { curState.syncPending = { timeout: window.setTimeout(() => { + curState.syncPending = null; + + if(curState.syncLoading) { + return; + } + this.getDifference(); }, SYNC_DELAY) - } + }; } if(!curState.syncPending.seqAwaiting || @@ -563,20 +588,23 @@ export class ApiUpdatesManager { //rootScope.broadcast('state_synchronizing'); if(!state || !state.pts || !state.date || !state.seq) { - apiManager.invokeApi('updates.getState', {}, {noErrorBox: true}).then((stateResult) => { - this.updatesState.seq = stateResult.seq; - this.updatesState.pts = stateResult.pts; - this.updatesState.date = stateResult.date; - //setTimeout(() => { - this.updatesState.syncLoading = false; - //rootScope.broadcast('state_synchronized'); - //}, 1000); - - // ! for testing - // updatesState.seq = 1 - // updatesState.pts = stateResult.pts - 5000 - // updatesState.date = 1 - // getDifference() + this.updatesState.syncLoading = new Promise((resolve) => { + apiManager.invokeApi('updates.getState', {}, {noErrorBox: true}).then((stateResult) => { + this.updatesState.seq = stateResult.seq; + this.updatesState.pts = stateResult.pts; + this.updatesState.date = stateResult.date; + //setTimeout(() => { + this.updatesState.syncLoading = null; + resolve(); + //rootScope.broadcast('state_synchronized'); + //}, 1000); + + // ! for testing + // updatesState.seq = 1 + // updatesState.pts = stateResult.pts - 5000 + // updatesState.date = 1 + // getDifference() + }); }); } else { // ! for testing diff --git a/src/lib/appManagers/appChatsManager.ts b/src/lib/appManagers/appChatsManager.ts index 1e1d2d066..210d49571 100644 --- a/src/lib/appManagers/appChatsManager.ts +++ b/src/lib/appManagers/appChatsManager.ts @@ -326,13 +326,13 @@ export class AppChatsManager { return this.cachedPhotoLocations[id]; } - /* public getChatString(id: number) { + public getChatString(id: number) { const chat = this.getChat(id); if(this.isChannel(id)) { return (this.isMegagroup(id) ? 's' : 'c') + id + '_' + chat.access_hash; } return 'g' + id; - } */ + } public getChatMembersString(id: number) { const chat = this.getChat(id); diff --git a/src/lib/appManagers/appDialogsManager.ts b/src/lib/appManagers/appDialogsManager.ts index 6c2606f9d..1a1305cc7 100644 --- a/src/lib/appManagers/appDialogsManager.ts +++ b/src/lib/appManagers/appDialogsManager.ts @@ -5,7 +5,6 @@ import { attachContextMenuListener, putPreloader } from "../../components/misc"; import { ripple } from "../../components/ripple"; //import Scrollable from "../../components/scrollable"; import Scrollable, { ScrollableX, SliceSides } from "../../components/scrollable"; -import appSidebarLeft from "../../components/sidebarLeft"; import { formatDateAccordingToToday } from "../../helpers/date"; import { escapeRegExp } from "../../helpers/string"; import { isSafari } from "../../helpers/userAgent"; @@ -165,8 +164,8 @@ class ConnectionStatusComponent { DEBUG && this.log('setState: isShown:', this.connecting || this.updating); }; - //this.setStateTimeout = window.setTimeout(cb, timeout); - cb(); + this.setStateTimeout = window.setTimeout(cb, timeout); + //cb(); /* if(timeout) this.setStateTimeout = window.setTimeout(cb, timeout); else cb(); */ }); @@ -499,9 +498,19 @@ export class AppDialogsManager { return this.loadDialogs(); }).then(() => { - appMessagesManager.getConversationsAll('', 0).finally(() => { - appMessagesManager.getConversationsAll('', 1).then(() => { + const allDialogsLoaded = appMessagesManager.dialogsStorage.allDialogsLoaded; + const wasLoaded = allDialogsLoaded[0] || allDialogsLoaded[1]; + const a: Promise = allDialogsLoaded[0] ? Promise.resolve() : appMessagesManager.getConversationsAll('', 0); + const b: Promise = allDialogsLoaded[1] ? Promise.resolve() : appMessagesManager.getConversationsAll('', 1); + a.finally(() => { + b.then(() => { this.accumulateArchivedUnread(); + + if(wasLoaded) { + (apiUpdatesManager.updatesState.syncLoading || Promise.resolve()).then(() => { + appMessagesManager.refreshConversations(); + }); + } }); }); }); diff --git a/src/lib/appManagers/appDraftsManager.ts b/src/lib/appManagers/appDraftsManager.ts index 32b349e7f..f5ba0dad7 100644 --- a/src/lib/appManagers/appDraftsManager.ts +++ b/src/lib/appManagers/appDraftsManager.ts @@ -69,7 +69,11 @@ export class AppDraftsManager { public getAllDrafts() { return this.getAllDraftPromise || (this.getAllDraftPromise = new Promise((resolve) => { apiManager.invokeApi('messages.getAllDrafts').then((updates) => { - apiUpdatesManager.processUpdateMessage(updates, {ignoreSyncLoading: true}); + const p = apiUpdatesManager.updatesState.syncLoading || Promise.resolve(); + p.then(() => { + apiUpdatesManager.processUpdateMessage(updates); + }); + resolve(); }); })); diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 9c41c0999..6cc056e46 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -86,14 +86,17 @@ export class AppImManager { this.offline = rootScope.idle.isIDLE = true; this.updateStatus(); clearInterval(this.updateStatusInterval); + rootScope.broadcast('idle', true); window.addEventListener('focus', () => { this.offline = rootScope.idle.isIDLE = false; this.updateStatus(); this.updateStatusInterval = window.setInterval(() => this.updateStatus(), 50e3); - + // в обратном порядке animationIntersector.checkAnimations(false); + + rootScope.broadcast('idle', false); }, {once: true}); }); @@ -101,6 +104,8 @@ export class AppImManager { window.addEventListener(FOCUS_EVENT_NAME, () => { this.updateStatusInterval = window.setInterval(() => this.updateStatus(), 50e3); this.updateStatus(); + + rootScope.broadcast('idle', false); }, {once: true, passive: true}); this.chatsContainer = document.createElement('div'); diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 724699f58..8672d6d6e 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -6,7 +6,7 @@ import { createPosterForVideo } from "../../helpers/files"; import { copy, defineNotNumerableProperties, getObjectKeysAndSort } from "../../helpers/object"; import { randomLong } from "../../helpers/random"; import { splitStringByLength, limitSymbols } from "../../helpers/string"; -import { ChatFull, Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputNotifyPeer, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessageFwdHeader, MessageMedia, MessageReplies, MessageReplyHeader, MessagesDialogs, MessagesFilter, MessagesMessages, MessagesPeerDialogs, MethodDeclMap, NotifyPeer, PhotoSize, SendMessageAction, Update } from "../../layer"; +import { ChatFull, Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessageFwdHeader, MessageReplies, MessageReplyHeader, MessagesDialogs, MessagesFilter, MessagesMessages, MessagesPeerDialogs, MethodDeclMap, NotifyPeer, PeerNotifySettings, PhotoSize, SendMessageAction, Update } from "../../layer"; import { InvokeApiOptions } from "../../types"; import { langPack } from "../langPack"; import { logger, LogLevels } from "../logger"; @@ -37,6 +37,7 @@ import { getFileNameByLocation } from "../../helpers/fileName"; import appProfileManager from "./appProfileManager"; import DEBUG, { MOUNT_CLASS_TO } from "../../config/debug"; import SlicedArray, { Slice, SliceEnd } from "../../helpers/slicedArray"; +import appNotificationsManager, { NotifyOptions } from "./appNotificationsManager"; //console.trace('include'); // TODO: если удалить сообщение в непрогруженном диалоге, то при обновлении, из-за стейта, последнего сообщения в чатлисте не будет @@ -155,7 +156,14 @@ export class AppMessagesManager { public newMessagesToHandle: {[peerId: string]: number[]} = {}; public newDialogsHandlePromise = 0; public newDialogsToHandle: {[peerId: string]: {reload: true} | Dialog} = {}; - public newUpdatesAfterReloadToHandle: any = {}; + public newUpdatesAfterReloadToHandle: {[peerId: string]: any[]} = {}; + + private notificationsHandlePromise = 0; + private notificationsToHandle: {[peerId: string]: { + fwdCount: number, + fromId: number, + topMessage?: MyMessage + }} = {}; private reloadConversationsPromise: Promise; private reloadConversationsPeers: number[] = []; @@ -342,6 +350,8 @@ export class AppMessagesManager { }); } }); + + appNotificationsManager.start(); } public getInputEntities(entities: MessageEntity[]) { @@ -1543,6 +1553,32 @@ export class AppMessagesManager { return false; } + public async refreshConversations() { + const limit = 100, outDialogs: Dialog[] = []; + for(let folderId = 0; folderId < 2; ++folderId) { + let offsetDate = 0; + for(;;) { + const {dialogs} = await appMessagesManager.getTopMessages(limit, folderId, offsetDate); + + if(dialogs.length) { + outDialogs.push(...dialogs as Dialog[]); + const dialog = dialogs[dialogs.length - 1]; + offsetDate = this.getMessageByPeer(dialog.peerId, dialog.top_message).date; + } else { + break; + } + } + } + + let obj: {[peerId: string]: Dialog} = {}; + outDialogs.forEach(dialog => { + obj[dialog.peerId] = dialog; + }); + rootScope.broadcast('dialogs_multiupdate', obj); + + return outDialogs; + } + public async getConversationsAll(query = '', folderId = 0) { const limit = 100, outDialogs: Dialog[] = []; for(; folderId < 2; ++folderId) { @@ -1609,7 +1645,7 @@ export class AppMessagesManager { }); } - return this.getTopMessages(limit, realFolderId).then(totalCount => { + return this.getTopMessages(limit, realFolderId).then(messagesDialogs => { //const curDialogStorage = this.dialogsStorage[folderId]; offset = 0; @@ -1625,7 +1661,7 @@ export class AppMessagesManager { return { dialogs: curDialogStorage.slice(offset, offset + limit), - count: totalCount, + count: messagesDialogs._ === 'messages.dialogs' ? messagesDialogs.dialogs.length : messagesDialogs.count, isEnd: this.dialogsStorage.allDialogsLoaded[realFolderId] && (offset + limit) >= curDialogStorage.length }; }); @@ -1645,16 +1681,19 @@ export class AppMessagesManager { } } - public getTopMessages(limit: number, folderId: number): Promise { + public getTopMessages(limit: number, folderId: number, offsetDate?: number) { const dialogs = this.dialogsStorage.getFolder(folderId); let offsetId = 0; - let offsetDate = 0; let offsetPeerId = 0; let offsetIndex = 0; - if(this.dialogsStorage.dialogsOffsetDate[folderId]) { - offsetDate = this.dialogsStorage.dialogsOffsetDate[folderId] + serverTimeManager.serverTimeOffset; - offsetIndex = this.dialogsStorage.dialogsOffsetDate[folderId] * 0x10000; + if(offsetDate === undefined) { + offsetDate = this.dialogsStorage.getOffsetDate(folderId); + } + + if(offsetDate) { + offsetIndex = offsetDate * 0x10000; + offsetDate += serverTimeManager.serverTimeOffset; } // ! ВНИМАНИЕ: ОЧЕНЬ СЛОЖНАЯ ЛОГИКА: @@ -1747,7 +1786,7 @@ export class AppMessagesManager { rootScope.broadcast('dialogs_multiupdate', {}); } - return count; + return dialogsResult; }); } @@ -2766,6 +2805,7 @@ export class AppMessagesManager { (dialogsResult.dialogs as Dialog[]).forEach((dialog) => { const peerId = appPeersManager.getPeerId(dialog.peer); let topMessage = dialog.top_message; + const topPendingMessage = this.pendingTopMsgs[peerId]; if(topPendingMessage) { if(!topMessage @@ -2781,34 +2821,17 @@ export class AppMessagesManager { } */ if(topMessage || (dialog.draft && dialog.draft._ === 'draftMessage')) { - //const wasDialogBefore = this.getDialogByPeerID(peerId)[0]; - - // here need to just replace, not FULL replace dialog! WARNING - /* if(wasDialogBefore?.pFlags?.pinned && !dialog?.pFlags?.pinned) { - this.log.error('here need to just replace, not FULL replace dialog! WARNING', wasDialogBefore, dialog); - if(!dialog.pFlags) dialog.pFlags = {}; - dialog.pFlags.pinned = true; - } */ - this.saveConversation(dialog); - - /* if(wasDialogBefore) { - rootScope.$broadcast('dialog_top', dialog); - } else { */ - //if(wasDialogBefore?.top_message !== topMessage) { - updatedDialogs[peerId] = dialog; - //} - //} + updatedDialogs[peerId] = dialog; } else { const dropped = this.dialogsStorage.dropDialog(peerId); if(dropped.length) { - rootScope.broadcast('dialog_drop', {peerId: peerId, dialog: dropped[0]}); + rootScope.broadcast('dialog_drop', {peerId, dialog: dropped[0]}); } } if(this.newUpdatesAfterReloadToHandle[peerId] !== undefined) { - for(const i in this.newUpdatesAfterReloadToHandle[peerId]) { - const update = this.newUpdatesAfterReloadToHandle[peerId][i]; + for(const update of this.newUpdatesAfterReloadToHandle[peerId]) { this.handleUpdate(update); } @@ -2925,6 +2948,8 @@ export class AppMessagesManager { historyStorage.readMaxId = dialog.read_inbox_max_id; historyStorage.readOutboxMaxId = dialog.read_outbox_max_id; + appNotificationsManager.savePeerSettings(peerId, dialog.notify_settings) + if(channelId && dialog.pts) { apiUpdatesManager.addChannelState(channelId, dialog.pts); } @@ -3534,6 +3559,19 @@ export class AppMessagesManager { }); } + 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) { return historyStorage.readPromise; } @@ -3602,6 +3640,40 @@ export class AppMessagesManager { return this.historiesStorage[peerId] ?? (this.historiesStorage[peerId] = {count: null, history: new SlicedArray()}); } + private handleNotifications = () => { + window.clearTimeout(this.notificationsHandlePromise); + this.notificationsHandlePromise = 0; + + //var timeout = $rootScope.idle.isIDLE && StatusManager.isOtherDeviceActive() ? 30000 : 1000; + //const timeout = 1000; + + for(const _peerId in this.notificationsToHandle) { + const peerId = +_peerId; + const notifyPeerToHandle = this.notificationsToHandle[peerId]; + + Promise.all([ + appNotificationsManager.getPeerMuted(peerId), + appNotificationsManager.getNotifySettings(appPeersManager.getInputNotifyPeerById(peerId, true)) + ]).then(([muted, peerTypeNotifySettings]) => { + const topMessage = notifyPeerToHandle.topMessage; + if(muted || !topMessage.pFlags.unread) { + return; + } + + //setTimeout(() => { + if(topMessage.pFlags.unread) { + this.notifyAboutMessage(topMessage, { + fwdCount: notifyPeerToHandle.fwdCount, + peerTypeNotifySettings + }); + } + //}, timeout); + }); + } + + this.notificationsToHandle = {}; + }; + public handleUpdate(update: Update) { /* if(DEBUG) { this.log.debug('handleUpdate', update._, update); @@ -3703,14 +3775,40 @@ export class AppMessagesManager { } const dialog = foundDialog[0]; + const inboxUnread = !message.pFlags.out && message.pFlags.unread; if(dialog) { - const inboxUnread = !message.pFlags.out && message.pFlags.unread; this.setDialogTopMessage(message, dialog); if(inboxUnread) { dialog.unread_count++; } } + if(inboxUnread/* && ($rootScope.selectedPeerID != peerID || $rootScope.idle.isIDLE) */) { + const notifyPeer = message.peerId; + let notifyPeerToHandle = this.notificationsToHandle[notifyPeer]; + if(notifyPeerToHandle === undefined) { + notifyPeerToHandle = this.notificationsToHandle[notifyPeer] = { + fwdCount: 0, + fromId: 0 + }; + } + + if(notifyPeerToHandle.fromId !== message.fromId) { + notifyPeerToHandle.fromId = message.fromId; + notifyPeerToHandle.fwdCount = 0; + } + + if((message as Message.message).fwd_from) { + notifyPeerToHandle.fwdCount++; + } + + notifyPeerToHandle.topMessage = message; + + if(!this.notificationsHandlePromise) { + this.notificationsHandlePromise = window.setTimeout(this.handleNotifications, 0); + } + } + break; } @@ -3986,8 +4084,9 @@ export class AppMessagesManager { if(!message.pFlags.out && !threadId && stillUnreadCount === undefined) { newUnreadCount = --foundDialog.unread_count; - //NotificationsManager.cancel('msg' + messageId); // warning } + + appNotificationsManager.cancel('msg' + messageId); } } @@ -4194,7 +4293,7 @@ export class AppMessagesManager { this.pendingTopMsgs[peerId] = messageId; this.handleUpdate({ _: 'updateNewMessage', - message: message + message } as any); } @@ -4261,13 +4360,14 @@ export class AppMessagesManager { case 'updateNotifySettings': { const {peer, notify_settings} = update; + if(peer._ === 'notifyPeer') { + const peerId = appPeersManager.getPeerId((peer as NotifyPeer.notifyPeer).peer); - const peerId = appPeersManager.getPeerId((peer as NotifyPeer.notifyPeer).peer); - - const dialog = this.getDialogByPeerId(peerId)[0]; - if(dialog) { - dialog.notify_settings = notify_settings; - rootScope.broadcast('dialog_notify_settings', peerId); + const dialog = this.getDialogByPeerId(peerId)[0]; + if(dialog) { + dialog.notify_settings = notify_settings; + rootScope.broadcast('dialog_notify_settings', peerId); + } } /////this.log('updateNotifySettings', peerId, notify_settings); @@ -4379,17 +4479,11 @@ export class AppMessagesManager { } public mutePeer(peerId: number) { - let inputPeer = appPeersManager.getInputPeerById(peerId); - let inputNotifyPeer: InputNotifyPeer.inputNotifyPeer = { - _: 'inputNotifyPeer', - peer: inputPeer - }; - - let settings: InputPeerNotifySettings = { + const settings: InputPeerNotifySettings = { _: 'inputPeerNotifySettings' }; - let dialog = appMessagesManager.getDialogByPeerId(peerId)[0]; + const dialog = appMessagesManager.getDialogByPeerId(peerId)[0]; let muted = true; if(dialog && dialog.notify_settings) { muted = dialog.notify_settings.mute_until > (Date.now() / 1000 | 0); @@ -4398,25 +4492,11 @@ export class AppMessagesManager { if(!muted) { settings.mute_until = 2147483647; } - - apiManager.invokeApi('account.updateNotifySettings', { - peer: inputNotifyPeer, - settings: settings - }).then(bool => { - if(bool) { - this.handleUpdate({ - _: 'updateNotifySettings', - peer: { - _: 'notifyPeer', - peer: appPeersManager.getOutputPeer(peerId) - }, - notify_settings: { // ! WOW, IT WORKS ! - ...settings, - _: 'peerNotifySettings', - } - }); - } - }); + + return appNotificationsManager.updateNotifySettings({ + _: 'inputNotifyPeer', + peer: appPeersManager.getInputPeerById(peerId) + }, settings); } public canWriteToPeer(peerId: number) { @@ -4530,6 +4610,226 @@ export class AppMessagesManager { }); } + private notifyAboutMessage(message: MyMessage, options: Partial<{ + fwdCount: number, + peerTypeNotifySettings: PeerNotifySettings + }> = {}) { + const peerId = this.getMessagePeer(message); + let peerString: string; + const notification: NotifyOptions = {}; + var notificationMessage: string, + notificationPhoto: any; + + const _ = (str: string) => str; + + const localSettings = appNotificationsManager.getLocalSettings(); + if(options.peerTypeNotifySettings.show_previews) { + if(message._ === 'message') { + if(message.fwd_from && options.fwdCount) { + notificationMessage = 'Forwarded ' + options.fwdCount + ' messages';//fwdMessagesPluralize(options.fwd_count); + } else if(message.message) { + if(localSettings.nopreview) { + notificationMessage = _('conversation_message_sent'); + } else { + notificationMessage = RichTextProcessor.wrapPlainText(message.message); + } + } else if(message.media) { + var captionEmoji: string; + switch (message.media._) { + case 'messageMediaPhoto': + notificationMessage = _('conversation_media_photo_raw'); + captionEmoji = '🖼'; + break; + case 'messageMediaDocument': + if(message.media.document._ === 'documentEmpty') break; + switch(message.media.document.type) { + case 'gif': + notificationMessage = _('conversation_media_gif_raw'); + captionEmoji = '🎬'; + break; + case 'sticker': + notificationMessage = _('conversation_media_sticker'); + var stickerEmoji = message.media.document.stickerEmojiRaw; + if(stickerEmoji !== undefined) { + notificationMessage = RichTextProcessor.wrapPlainText(stickerEmoji) + ' ' + notificationMessage; + } + break; + case 'video': + notificationMessage = _('conversation_media_video_raw'); + captionEmoji = '📹'; + break; + case 'round': + notificationMessage = _('conversation_media_round_raw'); + captionEmoji = '📹'; + break; + case 'voice': + case 'audio': + notificationMessage = _('conversation_media_audio_raw'); + break; + default: + if(message.media.document.file_name) { + notificationMessage = RichTextProcessor.wrapPlainText('📎 ' + message.media.document.file_name); + } else { + notificationMessage = _('conversation_media_document_raw'); + captionEmoji = '📎'; + } + break; + } + break; + + case 'messageMediaGeo': + case 'messageMediaVenue': + notificationMessage = _('conversation_media_location_raw'); + captionEmoji = '📍'; + break; + case 'messageMediaContact': + notificationMessage = _('conversation_media_contact_raw'); + break; + case 'messageMediaGame': + notificationMessage = RichTextProcessor.wrapPlainText('🎮 ' + message.media.game.title); + break; + case 'messageMediaUnsupported': + notificationMessage = _('conversation_media_unsupported_raw'); + break; + default: + notificationMessage = _('conversation_media_attachment_raw'); + break; + } + + if(captionEmoji !== undefined && (message.media as any).caption) { + notificationMessage = RichTextProcessor.wrapPlainText(captionEmoji + ' ' + (message.media as any).caption); + } + } + } else { + switch(message.action._) { + case 'messageActionChatCreate': + notificationMessage = _('conversation_group_created_raw') + break + case 'messageActionChatEditTitle': + notificationMessage = _('conversation_group_renamed_raw') + break + case 'messageActionChatEditPhoto': + notificationMessage = _('conversation_group_photo_updated_raw') + break + case 'messageActionChatDeletePhoto': + notificationMessage = _('conversation_group_photo_removed_raw') + break + case 'messageActionChatAddUser': + // @ts-ignore + case 'messageActionChatAddUsers': + notificationMessage = _('conversation_invited_user_message_raw') + break + /* case 'messageActionChatReturn': + notificationMessage = _('conversation_returned_to_group_raw') + break + case 'messageActionChatJoined': + notificationMessage = _('conversation_joined_group_raw') + break */ + case 'messageActionChatDeleteUser': + notificationMessage = _('conversation_kicked_user_message_raw') + break + /* case 'messageActionChatLeave': + notificationMessage = _('conversation_left_group_raw') + break */ + case 'messageActionChatJoinedByLink': + notificationMessage = _('conversation_joined_by_link_raw') + break + case 'messageActionChannelCreate': + notificationMessage = _('conversation_created_channel_raw') + break + /* case 'messageActionChannelEditTitle': + notificationMessage = _('conversation_changed_channel_name_raw') + break + case 'messageActionChannelEditPhoto': + notificationMessage = _('conversation_changed_channel_photo_raw') + break + case 'messageActionChannelDeletePhoto': + notificationMessage = _('conversation_removed_channel_photo_raw') + break */ + case 'messageActionPinMessage': + notificationMessage = _('conversation_pinned_message_raw') + break + /* case 'messageActionGameScore': + notificationMessage = gameScorePluralize(message.action.score) + break */ + + case 'messageActionPhoneCall': + switch((message.action as any).type) { + case 'out_missed': + notificationMessage = _('message_service_phonecall_canceled_raw') + break + case 'in_missed': + notificationMessage = _('message_service_phonecall_missed_raw') + break + case 'out_ok': + notificationMessage = _('message_service_phonecall_outgoing_raw') + break + case 'in_ok': + notificationMessage = _('message_service_phonecall_incoming_raw') + break + } + break + } + } + } else { + notificationMessage = 'New notification'; + } + + if(peerId > 0) { + const fromUser = appUsersManager.getUser(message.fromId); + const fromPhoto = appUsersManager.getUserPhoto(message.fromId); + + notification.title = (fromUser.first_name || '') + + (fromUser.first_name && fromUser.last_name ? ' ' : '') + + (fromUser.last_name || ''); + if(!notification.title) { + notification.title = fromUser.phone || _('conversation_unknown_user_raw'); + } + + notificationPhoto = fromPhoto; + + peerString = appUsersManager.getUserString(peerId); + } else { + notification.title = appChatsManager.getChat(-peerId).title || _('conversation_unknown_chat_raw'); + + if(message.fromId) { + var fromUser = appUsersManager.getUser(message.fromId); + notification.title = (fromUser.first_name || fromUser.last_name || _('conversation_unknown_user_raw')) + + ' @ ' + + notification.title; + } + + notificationPhoto = appChatsManager.getChatPhoto(-peerId); + + peerString = appChatsManager.getChatString(-peerId); + } + + notification.title = RichTextProcessor.wrapPlainText(notification.title); + + notification.onclick = () => { + /* rootScope.broadcast('history_focus', { + peerString: peerString, + messageID: message.flags & 16 ? message.mid : 0 + }); */ + }; + + notification.message = notificationMessage; + notification.key = 'msg' + message.mid; + notification.tag = peerString; + notification.silent = true;//message.pFlags.silent || false; + + /* if(notificationPhoto.location && !notificationPhoto.location.empty) { + apiManager.downloadSmallFile(notificationPhoto.location, notificationPhoto.size).then(function (blob) { + if (message.pFlags.unread) { + notification.image = blob + NotificationsManager.notify(notification) + } + }) + } else { */ + appNotificationsManager.notify(notification); + //} + } + public getScheduledMessagesStorage(peerId: number) { return this.scheduledMessagesStorage[peerId] ?? (this.scheduledMessagesStorage[peerId] = this.createMessageStorage()); } @@ -4915,6 +5215,7 @@ export class AppMessagesManager { if(!message.pFlags.out && !message.pFlags.is_outgoing && message.pFlags.unread) { history.unread++; + appNotificationsManager.cancel('msg' + mid); } history.count++; history.msgs[mid] = true; diff --git a/src/lib/appManagers/appNotificationsManager.ts b/src/lib/appManagers/appNotificationsManager.ts new file mode 100644 index 000000000..f9a9bed61 --- /dev/null +++ b/src/lib/appManagers/appNotificationsManager.ts @@ -0,0 +1,675 @@ +import { fontFamily } from "../../components/middleEllipsis"; +import { MOUNT_CLASS_TO } from "../../config/debug"; +import { CancellablePromise, deferredPromise } from "../../helpers/cancellablePromise"; +import { tsNow } from "../../helpers/date"; +import { copy, deepEqual } from "../../helpers/object"; +import { convertInputKeyToKey } from "../../helpers/string"; +import { isMobile } from "../../helpers/userAgent"; +import { InputNotifyPeer, InputPeerNotifySettings, NotifyPeer, PeerNotifySettings, Update } from "../../layer"; +import Config from "../config"; +import apiManager from "../mtproto/mtprotoworker"; +import rootScope from "../rootScope"; +import sessionStorage from "../sessionStorage"; +import apiUpdatesManager from "./apiUpdatesManager"; +import appChatsManager from "./appChatsManager"; +import appPeersManager from "./appPeersManager"; +import appStateManager from "./appStateManager"; +import appUsersManager from "./appUsersManager"; + +type MyNotification = Notification & { + hidden?: boolean, + show?: () => void, +}; + +export type NotifyOptions = Partial<{ + tag: string; + image: string; + key: string; + title: string; + message: string; + silent: boolean; + onclick: () => void; +}>; + +export class AppNotificationsManager { + private notificationsUiSupport: boolean; + private notificationsShown: {[key: string]: MyNotification} = {}; + private notificationIndex = 0; + private notificationsCount = 0; + private soundsPlayed: {[tag: string]: number} = {}; + private vibrateSupport = !!navigator.vibrate; + private nextSoundAt: number; + private prevSoundVolume: number; + private peerSettings = { + notifyPeer: {} as {[peerId: number]: Promise}, + notifyUsers: null as Promise, + notifyChats: null as Promise, + notifyBroadcasts: null as Promise + }; + private exceptions: {[peerId: string]: PeerNotifySettings} = {}; + private notifyContactsSignUp: Promise; + private faviconEl: HTMLLinkElement = document.head.querySelector('link[rel="icon"]'); + private langNotificationsPluralize = 'notifications';//_.pluralize('page_title_pluralize_notifications'); + + private titleBackup = document.title; + private titleChanged = false; + private titleInterval: number; + private prevFavicon: string; + private stopped = false; + + private settings: Partial<{ + nodesktop: boolean, + volume: number, + novibrate: boolean, + nopreview: boolean, + nopush: boolean, + nosound: boolean, + }> = {}; + + private registeredDevice: any; + private pushInited = false; + + private topMessagesDeferred: CancellablePromise; + + private notifySoundEl: HTMLElement; + + constructor() { + // @ts-ignore + navigator.vibrate = navigator.vibrate || navigator.mozVibrate || navigator.webkitVibrate; + + this.notificationsUiSupport = ('Notification' in window) || ('mozNotification' in navigator); + + this.topMessagesDeferred = deferredPromise(); + + this.notifySoundEl = document.createElement('div'); + this.notifySoundEl.id = 'notify-sound'; + document.body.append(this.notifySoundEl); + + /* rootScope.on('idle.deactivated', (newVal) => { + if(newVal) { + stop(); + } + });*/ + + rootScope.on('idle', (newVal) => { + if(this.stopped) { + return; + } + + if(!newVal) { + this.clear(); + } + + this.toggleToggler(); + }); + + rootScope.on('apiUpdate', (update) => { + // console.log('on apiUpdate', update) + switch(update._) { + case 'updateNotifySettings': { + this.savePeerSettings(update.peer._ === 'notifyPeer' ? appPeersManager.getPeerId(update.peer.peer) : update.peer._, update.notify_settings); + rootScope.broadcast('notify_settings', update); + break; + } + } + }); + + /* rootScope.on('push_init', (tokenData) => { + this.pushInited = true + if(!this.settings.nodesktop && !this.settings.nopush) { + if(tokenData) { + this.registerDevice(tokenData); + } else { + WebPushApiManager.subscribe(); + } + } else { + this.unregisterDevice(tokenData); + } + }); + rootScope.on('push_subscribe', (tokenData) => { + this.registerDevice(tokenData); + }); + rootScope.on('push_unsubscribe', (tokenData) => { + this.unregisterDevice(tokenData); + }); */ + + rootScope.addListener('dialogs_multiupdate', () => { + //unregisterTopMsgs() + this.topMessagesDeferred.resolve(); + }, true); + + /* rootScope.on('push_notification_click', (notificationData) => { + if(notificationData.action === 'push_settings') { + this.topMessagesDeferred.then(() => { + $modal.open({ + templateUrl: templateUrl('settings_modal'), + controller: 'SettingsModalController', + windowClass: 'settings_modal_window mobile_modal', + backdrop: 'single' + }) + }); + return; + } + + if(notificationData.action === 'mute1d') { + apiManager.invokeApi('account.updateDeviceLocked', { + period: 86400 + }).then(() => { + // var toastData = toaster.pop({ + // type: 'info', + // body: _('push_action_mute1d_success'), + // bodyOutputType: 'trustedHtml', + // clickHandler: () => { + // toaster.clear(toastData) + // }, + // showCloseButton: false + // }) + }); + + return; + } + + const peerId = notificationData.custom && notificationData.custom.peerId; + console.log('click', notificationData, peerId); + if(peerId) { + this.topMessagesDeferred.then(() => { + if(notificationData.custom.channel_id && + !appChatsManager.hasChat(notificationData.custom.channel_id)) { + return; + } + + if(peerId > 0 && !appUsersManager.hasUser(peerId)) { + return; + } + + // rootScope.broadcast('history_focus', { + // peerString: appPeersManager.getPeerString(peerId) + // }); + }); + } + }); */ + } + + private toggleToggler(enable = rootScope.idle.isIDLE) { + if(isMobile) return; + + const resetTitle = () => { + this.titleChanged = false; + document.title = this.titleBackup; + this.setFavicon(); + }; + + window.clearInterval(this.titleInterval); + this.titleInterval = 0; + + if(!enable) { + resetTitle(); + } else { + this.titleInterval = window.setInterval(() => { + if(!this.notificationsCount) { + this.toggleToggler(false); + } else if(this.titleChanged) { + resetTitle(); + } else { + this.titleChanged = true; + document.title = this.notificationsCount + ' ' + this.langNotificationsPluralize; + //this.setFavicon('assets/img/favicon_unread.ico'); + + // fetch('assets/img/favicon.ico') + // .then(res => res.blob()) + // .then(blob => { + // const img = document.createElement('img'); + // img.src = URL.createObjectURL(blob); + + const canvas = document.createElement('canvas'); + canvas.width = 32 * window.devicePixelRatio; + canvas.height = canvas.width; + + const ctx = canvas.getContext('2d'); + ctx.beginPath(); + ctx.arc(canvas.width / 2, canvas.height / 2, canvas.width / 2, 0, 2 * Math.PI, false); + ctx.fillStyle = '#5b8af1'; + ctx.fill(); + + let fontSize = 24; + let str = '' + this.notificationsCount; + if(this.notificationsCount < 10) { + fontSize = 22; + } else if(this.notificationsCount < 100) { + fontSize = 20; + } else { + str = '99+'; + fontSize = 18; + } + + ctx.font = `700 ${fontSize}px ${fontFamily}`; + ctx.textBaseline = 'middle'; + ctx.textAlign = 'center'; + ctx.fillStyle = 'white'; + ctx.fillText('' + this.notificationsCount, canvas.width / 2, canvas.height * .5625); + + /* const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); */ + + this.setFavicon(canvas.toDataURL()); + // }); + } + }, 1000); + } + } + + public updateLocalSettings() { + Promise.all(['notify_nodesktop', 'notify_volume', 'notify_novibrate', 'notify_nopreview', 'notify_nopush'].map(k => sessionStorage.get(k as any))) + .then((updSettings) => { + this.settings.nodesktop = updSettings[0]; + this.settings.volume = updSettings[1] === undefined ? 0.5 : updSettings[1]; + this.settings.novibrate = updSettings[2]; + this.settings.nopreview = updSettings[3]; + this.settings.nopush = updSettings[4]; + + /* if(this.pushInited) { + const needPush = !this.settings.nopush && !this.settings.nodesktop && WebPushApiManager.isAvailable || false; + const hasPush = this.registeredDevice !== false; + if(needPush !== hasPush) { + if(needPush) { + WebPushApiManager.subscribe(); + } else { + WebPushApiManager.unsubscribe(); + } + } + } + + WebPushApiManager.setSettings(this.settings); */ + }); + + appStateManager.getState().then(state => { + this.settings.nosound = !state.settings.notifications.sound; + }); + } + + public getLocalSettings() { + return this.settings; + } + + public getNotifySettings(peer: InputNotifyPeer): Promise { + let key: any = convertInputKeyToKey(peer._); + let obj: any = this.peerSettings[key as NotifyPeer['_']]; + + if(peer._ === 'inputNotifyPeer') { + key = appPeersManager.getPeerId(peer.peer); + obj = obj[key]; + } + + if(obj) { + return obj; + } + + return (obj || this.peerSettings)[key] = apiManager.invokeApi('account.getNotifySettings', {peer})/* .then(settings => { + return settings; + }) */; + } + + public updateNotifySettings(peer: InputNotifyPeer, settings: InputPeerNotifySettings) { + //this.savePeerSettings(peerId, settings); + + /* const inputSettings: InputPeerNotifySettings = copy(settings) as any; + inputSettings._ = 'inputPeerNotifySettings'; */ + + return apiManager.invokeApi('account.updateNotifySettings', { + peer, + settings + }).then(value => { + if(value) { + apiUpdatesManager.processUpdateMessage({ + _: 'updateShort', + update: { + _: 'updateNotifySettings', + peer: { + ...peer, + _: convertInputKeyToKey(peer._) + }, + notify_settings: { // ! WOW, IT WORKS ! + ...settings, + _: 'peerNotifySettings', + } + } as Update.updateNotifySettings + }); + } + }); + } + + public getNotifyExceptions() { + apiManager.invokeApi('account.getNotifyExceptions', {compare_sound: true}) + .then((updates) => { + apiUpdatesManager.processUpdateMessage(updates); + }); + } + + public getContactSignUpNotification() { + if(this.notifyContactsSignUp) return this.notifyContactsSignUp; + return this.notifyContactsSignUp = apiManager.invokeApi('account.getContactSignUpNotification'); + } + + public setContactSignUpNotification(silent: boolean) { + apiManager.invokeApi('account.setContactSignUpNotification', {silent}) + .then(value => { + this.notifyContactsSignUp = Promise.resolve(!silent); + }); + } + + private setFavicon(href: string = 'assets/img/favicon.ico') { + if(this.prevFavicon === href) { + return; + } + + const link = this.faviconEl.cloneNode() as HTMLLinkElement; + link.href = href; + this.faviconEl.parentNode.replaceChild(link, this.faviconEl); + this.faviconEl = link; + + this.prevFavicon = href; + } + + public savePeerSettings(key: number | NotifyPeer['_'], settings: PeerNotifySettings) { + const p = Promise.resolve(settings); + let obj: any; + if(typeof(key) === 'number') { + obj = this.peerSettings['notifyPeer']; + } + + (obj || this.peerSettings)[key] = p; + + //rootScope.broadcast('notify_settings', {peerId: peerId}); + } + + public isMuted(peerNotifySettings: PeerNotifySettings) { + return peerNotifySettings._ === 'peerNotifySettings' && + (peerNotifySettings.mute_until * 1000) > tsNow(); + } + + public getPeerMuted(peerId: number) { + return this.getNotifySettings({_: 'inputNotifyPeer', peer: appPeersManager.getInputPeerById(peerId)}) + .then((peerNotifySettings) => this.isMuted(peerNotifySettings)); + } + + public start() { + this.updateLocalSettings(); + //rootScope.on('settings_changed', this.updateNotifySettings); + //WebPushApiManager.start(); + + if(!this.notificationsUiSupport) { + return false; + } + + if('Notification' in window && Notification.permission !== 'granted' && Notification.permission !== 'denied') { + window.addEventListener('click', this.requestPermission); + } + + try { + if('onbeforeunload' in window) { + window.addEventListener('beforeunload', this.clear); + } + } catch (e) {} + } + + private stop() { + this.clear(); + window.clearInterval(this.titleInterval); + this.titleInterval = 0; + this.setFavicon(); + this.stopped = true; + } + + private requestPermission = () => { + Notification.requestPermission(); + window.removeEventListener('click', this.requestPermission); + }; + + public notify(data: NotifyOptions) { + console.log('notify', data, rootScope.idle.isIDLE, this.notificationsUiSupport, this.stopped); + if(this.stopped) { + return; + } + + // FFOS Notification blob src bug workaround + /* if(Config.Navigator.ffos && !Config.Navigator.ffos2p) { + data.image = 'https://telegram.org/img/t_logo.png' + } + else if (data.image && !angular.isString(data.image)) { + if (Config.Navigator.ffos2p) { + FileManager.getDataUrl(data.image, 'image/jpeg').then(function (url) { + data.image = url + notify(data) + }) + return false + } else { + data.image = FileManager.getUrl(data.image, 'image/jpeg') + } + } + else if (!data.image) */ { + data.image = 'assets/img/logo.svg'; + } + // console.log('notify image', data.image) + + this.notificationsCount++; + if(!this.titleInterval) { + this.toggleToggler(); + } + + const now = tsNow(); + if(this.settings.volume > 0 && !this.settings.nosound/* && + ( + !data.tag || + !this.soundsPlayed[data.tag] || + now > this.soundsPlayed[data.tag] + 60000 + ) */ + ) { + this.testSound(this.settings.volume); + this.soundsPlayed[data.tag] = now; + } + + if(!this.notificationsUiSupport || + 'Notification' in window && Notification.permission !== 'granted') { + return false; + } + + if(this.settings.nodesktop) { + if(this.vibrateSupport && !this.settings.novibrate) { + navigator.vibrate([200, 100, 200]); + return; + } + + return; + } + + const idx = ++this.notificationIndex; + const key = data.key || 'k' + idx; + let notification: MyNotification; + + if('Notification' in window) { + try { + if(data.tag) { + for(let i in this.notificationsShown) { + const notification = this.notificationsShown[i]; + if(notification && + notification.tag === data.tag) { + notification.hidden = true + } + } + } + + notification = new Notification(data.title, { + icon: data.image || '', + body: data.message || '', + tag: data.tag || '', + silent: data.silent || false + }); + } catch(e) { + this.notificationsUiSupport = false; + //WebPushApiManager.setLocalNotificationsDisabled(); + return + } + } /* else if('mozNotification' in navigator) { + notification = navigator.mozNotification.createNotification(data.title, data.message || '', data.image || '') + } else if(notificationsMsSiteMode) { + window.external.msSiteModeClearIconOverlay() + window.external.msSiteModeSetIconOverlay('img/icons/icon16.png', data.title) + window.external.msSiteModeActivate() + notification = { + index: idx + } + } */ else { + return; + } + + notification.onclick = () => { + notification.close(); + //AppRuntimeManager.focus(); + this.clear(); + if(data.onclick) { + data.onclick(); + } + }; + + notification.onclose = () => { + if(!notification.hidden) { + delete this.notificationsShown[key]; + this.clear(); + } + }; + + if(notification.show) { + notification.show(); + } + this.notificationsShown[key] = notification; + + if(!isMobile) { + setTimeout(() => { + this.hide(key); + }, 8000); + } + } + + public testSound(volume: number) { + const now = tsNow(); + if(this.nextSoundAt && now < this.nextSoundAt && this.prevSoundVolume === volume) { + return; + } + + this.nextSoundAt = now + 1000; + this.prevSoundVolume = volume; + const filename = 'assets/audio/notification.mp3'; + const audio = document.createElement('audio'); + audio.autoplay = true; + audio.setAttribute('mozaudiochannel', 'notification'); + audio.volume = volume; + audio.innerHTML = ` + + + `; + this.notifySoundEl.append(audio); + + audio.addEventListener('ended', () => { + audio.remove(); + }, {once: true}); + } + + public cancel(key: string) { + const notification = this.notificationsShown[key]; + if(notification) { + if(this.notificationsCount > 0) { + this.notificationsCount--; + } + + try { + if(notification.close) { + notification.hidden = true; + notification.close(); + }/* else if(notificationsMsSiteMode && + notification.index === notificationIndex) { + window.external.msSiteModeClearIconOverlay() + } */ + } catch (e) {} + + delete this.notificationsShown[key]; + } + } + + private hide(key: string) { + const notification = this.notificationsShown[key]; + if(notification) { + try { + if(notification.close) { + notification.hidden = true; + notification.close(); + } + } catch (e) {} + } + } + + public soundReset(tag: string) { + delete this.soundsPlayed[tag]; + } + + public clear() { + /* if(notificationsMsSiteMode) { + window.external.msSiteModeClearIconOverlay() + } else { */ + for(let i in this.notificationsShown) { + const notification = this.notificationsShown[i]; + try { + if(notification.close) { + notification.close(); + } + } catch (e) {} + } + /* } */ + this.notificationsShown = {}; + this.notificationsCount = 0; + + //WebPushApiManager.hidePushNotifications(); + } + + private registerDevice(tokenData: any) { + if(this.registeredDevice && + deepEqual(this.registeredDevice, tokenData)) { + return false; + } + + apiManager.invokeApi('account.registerDevice', { + token_type: tokenData.tokenType, + token: tokenData.tokenValue, + other_uids: [], + app_sandbox: false, + secret: new Uint8Array() + }).then(() => { + this.registeredDevice = tokenData; + }, (error) => { + error.handled = true; + }) + } + + private unregisterDevice(tokenData: any) { + if(!this.registeredDevice) { + return false; + } + + apiManager.invokeApi('account.unregisterDevice', { + token_type: tokenData.tokenType, + token: tokenData.tokenValue, + other_uids: [] + }).then(() => { + this.registeredDevice = false + }, (error) => { + error.handled = true + }) + } + + public getVibrateSupport() { + return this.vibrateSupport + } +} + +const appNotificationsManager = new AppNotificationsManager(); +MOUNT_CLASS_TO && (MOUNT_CLASS_TO.appNotificationsManager = appNotificationsManager); +export default appNotificationsManager; diff --git a/src/lib/appManagers/appPeersManager.ts b/src/lib/appManagers/appPeersManager.ts index ae970338a..35f8b2d82 100644 --- a/src/lib/appManagers/appPeersManager.ts +++ b/src/lib/appManagers/appPeersManager.ts @@ -1,6 +1,6 @@ import { MOUNT_CLASS_TO } from "../../config/debug"; import { isObject } from "../../helpers/object"; -import { DialogPeer, InputDialogPeer, InputPeer, Peer, Update } from "../../layer"; +import { DialogPeer, InputDialogPeer, InputNotifyPeer, InputPeer, Peer, Update } from "../../layer"; import { RichTextProcessor } from "../richtextprocessor"; import rootScope from "../rootScope"; import appChatsManager from "./appChatsManager"; @@ -105,12 +105,12 @@ export class AppPeersManager { return {_: 'peerChat', chat_id: chatId}; } - /* public getPeerString(peerId: number) { + public getPeerString(peerId: number) { if(peerId > 0) { return appUsersManager.getUserString(peerId); } return appChatsManager.getChatString(-peerId); - } */ + } public getPeerUsername(peerId: number): string { if(peerId > 0) { @@ -163,6 +163,13 @@ export class AppPeersManager { return (peerId > 0) && appUsersManager.isBot(peerId); } + /** + * Will check notification exceptions by type too + */ + public isPeerMuted(peerId: number) { + + } + /* public getInputPeer(peerString: string): InputPeer { var firstChar = peerString.charAt(0); var peerParams = peerString.substr(1).split('_'); @@ -195,6 +202,25 @@ export class AppPeersManager { } } */ + public getInputNotifyPeerById(peerId: number, ignorePeerId = false): InputNotifyPeer { + if(ignorePeerId) { + if(peerId > 0) { + return {_: 'inputNotifyUsers'}; + } else { + if(appPeersManager.isBroadcast(peerId)) { + return {_: 'inputNotifyBroadcasts'}; + } else { + return {_: 'inputNotifyChats'}; + } + } + } else { + return { + _: 'inputNotifyPeer', + peer: this.getInputPeerById(peerId) + }; + } + } + public getInputPeerById(peerId: number): InputPeer { if(!peerId) { return {_: 'inputPeerEmpty'}; diff --git a/src/lib/appManagers/appPrivacyManager.ts b/src/lib/appManagers/appPrivacyManager.ts index 7fa50af46..c5528b7a5 100644 --- a/src/lib/appManagers/appPrivacyManager.ts +++ b/src/lib/appManagers/appPrivacyManager.ts @@ -5,6 +5,7 @@ import appChatsManager from "./appChatsManager"; import appUsersManager from "./appUsersManager"; import apiUpdatesManager from "./apiUpdatesManager"; import rootScope from "../rootScope"; +import { convertInputKeyToKey } from "../../helpers/string"; export enum PrivacyType { Everybody = 2, @@ -31,11 +32,6 @@ export class AppPrivacyManager { }); } - public convertInputKeyToKey(inputKey: string) { - let str = inputKey.replace('input', ''); - return (str[0].toLowerCase() + str.slice(1)) as string; - } - public setPrivacy(inputKey: InputPrivacyKey['_'], rules: InputPrivacyRule[]) { return apiManager.invokeApi('account.setPrivacy', { key: { @@ -51,12 +47,12 @@ export class AppPrivacyManager { update: { _: 'updatePrivacy', key: { - _: this.convertInputKeyToKey(inputKey) + _: convertInputKeyToKey(inputKey) }, rules: rules.map(inputRule => { const rule: PrivacyRule = {} as any; Object.assign(rule, inputRule); - rule._ = this.convertInputKeyToKey(rule._) as any; + rule._ = convertInputKeyToKey(rule._) as any; return rule; }) } as Update.updatePrivacy @@ -69,7 +65,7 @@ export class AppPrivacyManager { } public getPrivacy(inputKey: InputPrivacyKey['_']) { - const privacyKey: PrivacyKey['_'] = this.convertInputKeyToKey(inputKey) as any; + const privacyKey: PrivacyKey['_'] = convertInputKeyToKey(inputKey) as any; const rules = this.privacy[privacyKey]; if(rules) { return Promise.resolve(rules); diff --git a/src/lib/appManagers/appStateManager.ts b/src/lib/appManagers/appStateManager.ts index 866cdaed1..c70ec3d2d 100644 --- a/src/lib/appManagers/appStateManager.ts +++ b/src/lib/appManagers/appStateManager.ts @@ -63,6 +63,9 @@ export type State = Partial<{ highlightningColor?: string, color?: string, slug?: string, + }, + notifications: { + sound: boolean } }, drafts: AppDraftsManager['drafts'] @@ -109,6 +112,9 @@ export const STATE_INIT: State = { type: 'image', blur: false, slug: 'ByxGo2lrMFAIAAAAmkJxZabh8eM', // * new blurred camomile + }, + notifications: { + sound: true } }, drafts: {} diff --git a/src/lib/appManagers/appUsersManager.ts b/src/lib/appManagers/appUsersManager.ts index 285e97a2f..280d8f8d0 100644 --- a/src/lib/appManagers/appUsersManager.ts +++ b/src/lib/appManagers/appUsersManager.ts @@ -469,10 +469,10 @@ export class AppUsersManager { return this.cachedPhotoLocations[id]; } - /* public getUserString(id: number) { + public getUserString(id: number) { const user = this.getUser(id); return 'u' + id + (user.access_hash ? '_' + user.access_hash : ''); - } */ + } public getUserInput(id: number): InputUser { const user = this.getUser(id); diff --git a/src/lib/crypto/crypto_utils.ts b/src/lib/crypto/crypto_utils.ts index 8759a4889..356d6f319 100644 --- a/src/lib/crypto/crypto_utils.ts +++ b/src/lib/crypto/crypto_utils.ts @@ -9,10 +9,7 @@ import {str2bigInt, bpe, equalsInt, greater, copy_, eGCD_, add_, rightShift_, sub_, copyInt_, isZero, divide_, one, bigInt2str, powMod, bigInt2bytes} from '../../vendor/leemon';//from 'leemon'; -// @ts-ignore -import {BigInteger} from 'jsbn'; - -import { addPadding, bytesFromBigInt } from '../mtproto/bin_utils'; +import { addPadding } from '../mtproto/bin_utils'; import { bytesToWordss, bytesFromWordss, bytesToHex, bytesFromHex } from '../../helpers/bytes'; import { nextRandomInt } from '../../helpers/random'; @@ -148,21 +145,20 @@ export async function hash_pbkdf2(/* hasher: 'string', */buffer: any, salt: any return bits; } -export function pqPrimeFactorization(pqBytes: any) { - var what = new BigInteger(pqBytes); - var result: any = false; +export function pqPrimeFactorization(pqBytes: number[]) { + let result: ReturnType; - //console.log(dT(), 'PQ start', pqBytes, what.toString(16), what.bitLength()) + //console.log('PQ start', pqBytes, bytesToHex(pqBytes)); try { //console.time('PQ leemon'); - result = pqPrimeLeemon(str2bigInt(what.toString(16), 16, Math.ceil(64 / bpe) + 1)); + result = pqPrimeLeemon(str2bigInt(bytesToHex(pqBytes), 16, Math.ceil(64 / bpe) + 1)); //console.timeEnd('PQ leemon'); } catch (e) { console.error('Pq leemon Exception', e); } - //console.log(dT(), 'PQ finish'); + //console.log('PQ finish', result); return result; } @@ -253,11 +249,11 @@ export function bytesModPow(x: any, y: any, m: any) { var resBigInt = powMod(xBigInt, yBigInt, mBigInt); return bytesFromHex(bigInt2str(resBigInt, 16)); - } catch (e) { + } catch(e) { console.error('mod pow error', e); } - return bytesFromBigInt(new BigInteger(x).modPow(new BigInteger(y), new BigInteger(m)), 256); + //return bytesFromBigInt(new BigInteger(x).modPow(new BigInteger(y), new BigInteger(m)), 256); } export function gzipUncompress(bytes: ArrayBuffer, toString: true): string; diff --git a/src/lib/mtproto/authorizer.ts b/src/lib/mtproto/authorizer.ts index 472b7ecff..ba0560a40 100644 --- a/src/lib/mtproto/authorizer.ts +++ b/src/lib/mtproto/authorizer.ts @@ -3,15 +3,12 @@ import dcConfigurator from "./dcConfigurator"; import rsaKeysManager from "./rsaKeysManager"; import timeManager from "./timeManager"; -// @ts-ignore -import { BigInteger } from "jsbn"; - import CryptoWorker from "../crypto/cryptoworker"; import { logger, LogLevels } from "../logger"; import { bytesCmp, bytesToHex, bytesFromHex, bytesXor } from "../../helpers/bytes"; import DEBUG from "../../config/debug"; -//import { bigInt2str, greater, int2bigInt, one, powMod, str2bigInt, sub } from "../../vendor/leemon"; +import { cmp, int2bigInt, one, pow, str2bigInt, sub } from "../../vendor/leemon"; /* let fNewNonce: any = bytesFromHex('8761970c24cb2329b5b2459752c502f3057cb7e8dbab200e526e8767fdc73b3c').reverse(); let fNonce: any = bytesFromHex('b597720d11faa5914ef485c529cde414').reverse(); @@ -401,21 +398,21 @@ export class Authorizer { this.log('dhPrime cmp OK'); } - var gABigInt = new BigInteger(bytesToHex(gA), 16); - //const _gABigInt = str2bigInt(bytesToHex(gA), 16); - var dhPrimeBigInt = new BigInteger(dhPrimeHex, 16); - //const _dhPrimeBigInt = str2bigInt(dhPrimeHex, 16); + //var gABigInt = new BigInteger(bytesToHex(gA), 16); + const _gABigInt = str2bigInt(bytesToHex(gA), 16); + //var dhPrimeBigInt = new BigInteger(dhPrimeHex, 16); + const _dhPrimeBigInt = str2bigInt(dhPrimeHex, 16); //this.log('gABigInt.compareTo(BigInteger.ONE) <= 0', gABigInt.compareTo(BigInteger.ONE), BigInteger.ONE.compareTo(BigInteger.ONE), greater(_gABigInt, one)); - if(gABigInt.compareTo(BigInteger.ONE) <= 0) { - //if(!greater(_gABigInt, one)) { + //if(gABigInt.compareTo(BigInteger.ONE) <= 0) { + if(cmp(_gABigInt, one) <= 0) { throw new Error('[MT] DH params are not verified: gA <= 1'); } /* this.log('gABigInt.compareTo(dhPrimeBigInt.subtract(BigInteger.ONE)) >= 0', gABigInt.compareTo(dhPrimeBigInt.subtract(BigInteger.ONE)), greater(gABigInt, sub(_dhPrimeBigInt, one))); */ - if(gABigInt.compareTo(dhPrimeBigInt.subtract(BigInteger.ONE)) >= 0) { - //if(greater(gABigInt, sub(_dhPrimeBigInt, one))) { + //if(gABigInt.compareTo(dhPrimeBigInt.subtract(BigInteger.ONE)) >= 0) { + if(cmp(_gABigInt, sub(_dhPrimeBigInt, one)) >= 0) { throw new Error('[MT] DH params are not verified: gA >= dhPrime - 1'); } @@ -424,19 +421,25 @@ export class Authorizer { } - var two = new BigInteger(/* null */''); - two.fromInt(2); - //const _two = int2bigInt(2, 10, 0); + //var two = new BigInteger(/* null */''); + //two.fromInt(2); + const _two = int2bigInt(2, 32, 0); //this.log('_two:', bigInt2str(_two, 16), two.toString(16)); - var twoPow = two.pow(2048 - 64); - //const _twoPow = powMod(_two, int2bigInt(2048 - 64, 10, 0), null); + let perf = performance.now(); + //var twoPow = two.pow(2048 - 64); + //console.log('jsbn pow', performance.now() - perf); + perf = performance.now(); + const _twoPow = pow(_two, 2048 - 64); + //console.log('leemon pow', performance.now() - perf); //this.log('twoPow:', twoPow.toString(16), bigInt2str(_twoPow, 16)); - // this.log('gABigInt.compareTo(twoPow) < 0'); - if(gABigInt.compareTo(twoPow) < 0) { + // this.log('gABigInt.compareTo(twoPow) < 0'); + //if(gABigInt.compareTo(twoPow) < 0) { + if(cmp(_gABigInt, _twoPow) < 0) { throw new Error('[MT] DH params are not verified: gA < 2^{2048-64}'); } - if(gABigInt.compareTo(dhPrimeBigInt.subtract(twoPow)) >= 0) { + //if(gABigInt.compareTo(dhPrimeBigInt.subtract(twoPow)) >= 0) { + if(cmp(_gABigInt, sub(_dhPrimeBigInt, _twoPow)) >= 0) { throw new Error('[MT] DH params are not verified: gA > dhPrime - 2^{2048-64}'); } diff --git a/src/lib/mtproto/bin_utils.ts b/src/lib/mtproto/bin_utils.ts index d56500268..c4a19d12c 100644 --- a/src/lib/mtproto/bin_utils.ts +++ b/src/lib/mtproto/bin_utils.ts @@ -1,8 +1,6 @@ -//import {str2bigInt, divInt_, int2bigInt, bigInt2str, bigInt2bytes} from '../vendor/leemon'; - -// @ts-ignore -import {BigInteger, SecureRandom} from 'jsbn'; -import { bufferConcat, bufferConcats } from '../../helpers/bytes'; +import { bufferConcats } from '../../helpers/bytes'; +import { add_, bigInt2str, cmp, leftShift_, str2bigInt } from '../../vendor/leemon'; +import { nextRandomInt } from '../../helpers/random'; /// #if !MTPROTO_WORKER // @ts-ignore @@ -22,13 +20,13 @@ export function isObject(object: any) { return typeof(object) === 'object' && object !== null; } -export function bigint(num: number) { +/* export function bigint(num: number) { return new BigInteger(num.toString(16), 16); -} +} */ -export function bigStringInt(strNum: string) { +/* export function bigStringInt(strNum: string) { return new BigInteger(strNum, 10); -} +} */ /* export function base64ToBlob(base64str: string, mimeType: string) { var sliceSize = 1024; @@ -62,7 +60,7 @@ export function dataUrlToBlob(url: string) { return blob; } */ -export function bytesFromBigInt(bigInt: BigInteger, len?: number) { +/* export function bytesFromBigInt(bigInt: BigInteger, len?: number) { var bytes = bigInt.toByteArray(); if(len && bytes.length < len) { @@ -82,10 +80,36 @@ export function bytesFromBigInt(bigInt: BigInteger, len?: number) { } return bytes; +} */ + +export function longFromInts(high: number, low: number): string { + //let perf = performance.now(); + //let str = bigint(high).shiftLeft(32).add(bigint(low)).toString(10); + //console.log('longFromInts jsbn', performance.now() - perf); + + //perf = performance.now(); + const bigInt = str2bigInt(high.toString(16), 16, 32);//int2bigInt(high, 64, 64); + //console.log('longFromInts construct high', bigint(high).toString(10), bigInt2str(bigInt, 10)); + leftShift_(bigInt, 32); + //console.log('longFromInts shiftLeft', bigint(high).shiftLeft(32).toString(10), bigInt2str(bigInt, 10)); + add_(bigInt, str2bigInt(low.toString(16), 16, 32)); + const _str = bigInt2str(bigInt, 10); + + //console.log('longFromInts leemon', performance.now() - perf); + + //console.log('longFromInts', high, low, str, _str, str === _str); + + return _str; } -export function longFromInts(high: number, low: number) { - return bigint(high).shiftLeft(32).add(bigint(low)).toString(10); +export function sortLongsArray(arr: string[]) { + return arr.map(long => { + return str2bigInt(long, 10); + }).sort((a, b) => { + return cmp(a, b); + }).map(bigInt => { + return bigInt2str(bigInt, 10); + }); } export function addPadding(bytes: any, blockSize: number = 16, zeroes?: boolean, full = false, prepend = false) { @@ -99,7 +123,9 @@ export function addPadding(bytes: any, blockSize: number = 16, zeroes?: boolean, padding[i] = 0; } } else { - (new SecureRandom()).nextBytes(padding); + for(let i = 0; i < padding.length; ++i) { + padding[i] = nextRandomInt(255); + } } if(bytes instanceof ArrayBuffer) { @@ -112,4 +138,4 @@ export function addPadding(bytes: any, blockSize: number = 16, zeroes?: boolean, } return bytes; -} \ No newline at end of file +} diff --git a/src/lib/mtproto/mtprotoworker.ts b/src/lib/mtproto/mtprotoworker.ts index ac89cb4b7..9ad4c92b3 100644 --- a/src/lib/mtproto/mtprotoworker.ts +++ b/src/lib/mtproto/mtprotoworker.ts @@ -55,7 +55,7 @@ export class ApiManagerProxy extends CryptoWorkerMethods { private isSWRegistered = true; - private debug = DEBUG; + private debug = DEBUG && false; private sockets: Map = new Map(); @@ -140,7 +140,7 @@ export class ApiManagerProxy extends CryptoWorkerMethods { } private onWorkerMessage = (e: MessageEvent) => { - this.log('got message from worker:', e.data); + //this.log('got message from worker:', e.data); const task = e.data; diff --git a/src/lib/mtproto/networker.ts b/src/lib/mtproto/networker.ts index 560e60e8f..e1aa34e95 100644 --- a/src/lib/mtproto/networker.ts +++ b/src/lib/mtproto/networker.ts @@ -1,5 +1,4 @@ -import {isObject} from './bin_utils'; -import {bigStringInt} from './bin_utils'; +import {isObject, sortLongsArray} from './bin_utils'; import {TLDeserialization, TLSerialization} from './tl_utils'; import CryptoWorker from '../crypto/cryptoworker'; import sessionStorage from '../sessionStorage'; @@ -21,6 +20,7 @@ import HTTP from './transports/http'; /// #endif import type TcpObfuscated from './transports/tcpObfuscated'; +import { bigInt2str, cmp, rightShift_, str2bigInt } from '../../vendor/leemon'; //console.error('networker included!', new Error().stack); @@ -793,11 +793,16 @@ export default class MTPNetworker { const currentTime = Date.now(); let messagesByteLen = 0; + + /// #if MTPROTO_HTTP || MTPROTO_HTTP_UPLOAD let hasApiCall = false; let hasHttpWait = false; + /// #endif + let lengthOverflow = false; - for(const messageId in this.pendingMessages) { + const keys = sortLongsArray(Object.keys(this.pendingMessages)); + for(const messageId of keys) { const value = this.pendingMessages[messageId]; if(!value || value <= currentTime) { @@ -808,25 +813,26 @@ export default class MTPNetworker { } */ const messageByteLength = message.body.length + 32; - if(!message.notContentRelated && lengthOverflow) { - continue; // maybe break here - } - if(!message.notContentRelated && - messagesByteLen && - messagesByteLen + messageByteLength > 655360) { // 640 Kb + if((messagesByteLen + messageByteLength) > 655360) { // 640 Kb this.log.warn('lengthOverflow', message, messages); lengthOverflow = true; - continue; // maybe break here + + if(outMessage) { // if it is a first message + break; + } } messages.push(message); messagesByteLen += messageByteLength; + + /// #if MTPROTO_HTTP || MTPROTO_HTTP_UPLOAD if(message.isAPI) { hasApiCall = true; } else if(message.longPoll) { hasHttpWait = true; } + /// #endif outMessage = message; } else { @@ -1426,7 +1432,10 @@ export default class MTPNetworker { this.log.error('Bad msg notification', message); if(message.error_code === 16 || message.error_code === 17) { - const changedOffset = timeManager.applyServerTime(bigStringInt(messageId).shiftRight(32).toString(10)); + //const changedOffset = timeManager.applyServerTime(bigStringInt(messageId).shiftRight(32).toString(10)); + const bigInt = str2bigInt(messageId, 10); + rightShift_(bigInt, 32); + const changedOffset = timeManager.applyServerTime(+bigInt2str(bigInt, 10)); if(message.error_code === 17 || changedOffset) { this.log('Update session'); this.updateSession(); diff --git a/src/lib/mtproto/rsaKeysManager.ts b/src/lib/mtproto/rsaKeysManager.ts index 5500027e0..ccd63a59e 100644 --- a/src/lib/mtproto/rsaKeysManager.ts +++ b/src/lib/mtproto/rsaKeysManager.ts @@ -1,7 +1,7 @@ import { TLSerialization } from "./tl_utils"; -import { bigStringInt } from "./bin_utils"; import CryptoWorker from '../crypto/cryptoworker'; import { bytesFromArrayBuffer, bytesFromHex, bytesToHex } from "../../helpers/bytes"; +import { bigInt2str, str2bigInt } from "../../vendor/leemon"; export class RSAKeysManager { @@ -106,7 +106,7 @@ export class RSAKeysManager { let fingerprintBytes = bytesFromArrayBuffer(hash).slice(-8); fingerprintBytes.reverse(); - this.publicKeysParsed[bytesToHex(fingerprintBytes)] = { + this.publicKeysParsed[bytesToHex(fingerprintBytes).toLowerCase()] = { modulus: keyParsed.modulus, exponent: keyParsed.exponent }; @@ -125,7 +125,8 @@ export class RSAKeysManager { var fingerprintHex, foundKey, i; for(i = 0; i < fingerprints.length; i++) { - fingerprintHex = bigStringInt(fingerprints[i]).toString(16); + //fingerprintHex = bigStringInt(fingerprints[i]).toString(16); + fingerprintHex = bigInt2str(str2bigInt(fingerprints[i], 10), 16).toLowerCase(); if(fingerprintHex.length < 16) { fingerprintHex = new Array(16 - fingerprintHex.length).fill('0').join('') + fingerprintHex; diff --git a/src/lib/mtproto/tl_utils.ts b/src/lib/mtproto/tl_utils.ts index 06405f6b0..9800cc597 100644 --- a/src/lib/mtproto/tl_utils.ts +++ b/src/lib/mtproto/tl_utils.ts @@ -1,11 +1,12 @@ import { bytesToHex } from '../../helpers/bytes'; -import { bigint, bigStringInt, isObject } from './bin_utils'; +import { isObject, longFromInts } from './bin_utils'; import { MOUNT_CLASS_TO } from '../../config/debug'; +import { str2bigInt, dup, divide_, bigInt2str } from '../../vendor/leemon'; +import Schema, { MTProtoConstructor } from './schema'; + /// #if MTPROTO_WORKER // @ts-ignore import { gzipUncompress } from '../crypto/crypto_utils'; -import Schema, { MTProtoConstructor } from './schema'; - /// #endif const boolFalse = +Schema.API.constructors.find(c => c.predicate === 'boolFalse').id >>> 0; @@ -138,10 +139,29 @@ class TLSerialization { if(typeof sLong !== 'string') { sLong = sLong ? sLong.toString() : '0'; } - const divRem = bigStringInt(sLong).divideAndRemainder(bigint(0x100000000)); + + const R = 0x100000000; + //const divRem = bigStringInt(sLong).divideAndRemainder(bigint(R)); + + const a = str2bigInt(sLong, 10, 64); + const q = dup(a); + const r = dup(a); + divide_(a, str2bigInt((R).toString(16), 16, 64), q, r); + //divInt_(a, R); + + const high = +bigInt2str(q, 10); + let low = +bigInt2str(r, 10); + + if(high < low) { + low -= R; + } + + //console.log('storeLong', sLong, divRem[0].intValue(), divRem[1].intValue(), high, low); - this.writeInt(divRem[1].intValue(), (field || '') + ':long[low]'); - this.writeInt(divRem[0].intValue(), (field || '') + ':long[high]'); + //this.writeInt(divRem[1].intValue(), (field || '') + ':long[low]'); + //this.writeInt(divRem[0].intValue(), (field || '') + ':long[high]'); + this.writeInt(low, (field || '') + ':long[low]'); + this.writeInt(high, (field || '') + ':long[high]'); } public storeDouble(f: any, field?: string) { @@ -480,7 +500,8 @@ class TLDeserialization { const iLow = this.readInt((field || '') + ':long[low]'); const iHigh = this.readInt((field || '') + ':long[high]'); - const longDec = bigint(iHigh).shiftLeft(32).add(bigint(iLow)).toString(); + //const longDec = bigint(iHigh).shiftLeft(32).add(bigint(iLow)).toString(); + const longDec = longFromInts(iHigh, iLow); return longDec; } diff --git a/src/lib/polyfill.ts b/src/lib/polyfill.ts index c65dcc8b8..3cb6d3199 100644 --- a/src/lib/polyfill.ts +++ b/src/lib/polyfill.ts @@ -1,5 +1,3 @@ -// @ts-ignore -//import {SecureRandom} from 'jsbn'; import { bytesToHex, bytesFromHex, bufferConcats } from '../helpers/bytes'; import { nextRandomInt } from '../helpers/random'; diff --git a/src/lib/rootScope.ts b/src/lib/rootScope.ts index f9b30f5ac..bb02c5ce0 100644 --- a/src/lib/rootScope.ts +++ b/src/lib/rootScope.ts @@ -89,18 +89,22 @@ type BroadcastEvents = { 'im_mount': void, 'im_tab_change': number, + 'idle': boolean, + 'overlay_toggle': boolean, 'background_change': void, - 'privacy_update': Update.updatePrivacy + 'privacy_update': Update.updatePrivacy, + + 'notify_settings': Update.updateNotifySettings }; class RootScope extends EventListenerBase { private _overlayIsActive: boolean = false; public myId = 0; public idle = { - isIDLE: false + isIDLE: true }; public connectionStatus: {[name: string]: ConnectionStatusChange} = {}; public settings: State['settings']; diff --git a/src/lib/storages/dialogs.ts b/src/lib/storages/dialogs.ts index 0e41b69a9..280e166ec 100644 --- a/src/lib/storages/dialogs.ts +++ b/src/lib/storages/dialogs.ts @@ -9,16 +9,27 @@ export default class DialogsStorage { public dialogs: {[peerId: string]: Dialog} = {}; public byFolders: {[folderId: number]: Dialog[]} = {}; - public allDialogsLoaded: {[folder_id: number]: boolean} = {}; - public dialogsOffsetDate: {[folder_id: number]: number} = {}; - public pinnedOrders: {[folder_id: number]: number[]} = { - 0: [], - 1: [] - }; - public dialogsNum = 0; + public allDialogsLoaded: {[folder_id: number]: boolean}; + private dialogsOffsetDate: {[folder_id: number]: number}; + public pinnedOrders: {[folder_id: number]: number[]}; + private dialogsNum: number; constructor(private appMessagesManager: AppMessagesManager, private appChatsManager: AppChatsManager, private appPeersManager: AppPeersManager, private serverTimeManager: ServerTimeManager) { + this.reset(); + } + public reset() { + this.allDialogsLoaded = {}; + this.dialogsOffsetDate = {}; + this.pinnedOrders = { + 0: [], + 1: [] + }; + this.dialogsNum = 0; + } + + public getOffsetDate(folderId: number) { + return this.dialogsOffsetDate[folderId] || 0; } public getFolder(id: number) { @@ -181,4 +192,4 @@ export default class DialogsStorage { return foundDialog; } -} \ No newline at end of file +} diff --git a/src/lib/storages/filters.ts b/src/lib/storages/filters.ts index 354213986..c0b7608e8 100644 --- a/src/lib/storages/filters.ts +++ b/src/lib/storages/filters.ts @@ -259,4 +259,4 @@ export default class FiltersStorage { filter.orderIndex = this.orderIndex++; } } -} \ No newline at end of file +} diff --git a/src/pages/pageSignIn.ts b/src/pages/pageSignIn.ts index fa59df64a..5ff4b21d6 100644 --- a/src/pages/pageSignIn.ts +++ b/src/pages/pageSignIn.ts @@ -9,7 +9,7 @@ import Page from "./page"; import pageAuthCode from "./pageAuthCode"; import pageSignQR from './pageSignQR'; import InputField from "../components/inputField"; -import CheckboxField from "../components/checkbox"; +import CheckboxField from "../components/checkboxField"; import Button from "../components/button"; import { isAppleMobile } from "../helpers/userAgent"; import fastSmoothScroll from "../helpers/fastSmoothScroll"; @@ -299,7 +299,7 @@ let onFirstMount = () => { this.removeAttribute('readonly'); // fix autocomplete });*/ - const signedCheckboxField = CheckboxField({ + const signedCheckboxField = new CheckboxField({ text: 'Keep me signed in', name: 'keepSession' }); diff --git a/src/scss/partials/_leftSidebar.scss b/src/scss/partials/_leftSidebar.scss index 5b0a2d2b3..97cb2ae1c 100644 --- a/src/scss/partials/_leftSidebar.scss +++ b/src/scss/partials/_leftSidebar.scss @@ -1040,6 +1040,12 @@ } } +.notifications-container { + .sidebar-left-section { + padding-bottom: 0; + } +} + .range-setting-selector { padding: 1rem .875rem; diff --git a/src/scss/style.scss b/src/scss/style.scss index cdf6db6e6..872f83f93 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -1140,10 +1140,17 @@ middle-ellipsis-element { overflow: hidden; } - .radio-field { - &-main { - padding-left: 3.375rem; - margin-left: -3.375rem; + .radio-field-main, .checkbox-field { + padding-left: 3.375rem; + margin-left: -3.375rem; + } + + .checkbox-field { + margin-right: 0; + height: auto; + + .checkbox-caption { + padding-left: 0; } } diff --git a/src/vendor/leemon.ts b/src/vendor/leemon.ts index fc8365021..1b8a5bf93 100644 --- a/src/vendor/leemon.ts +++ b/src/vendor/leemon.ts @@ -494,6 +494,20 @@ export function powMod(x: number[], y: number[], n: number[]): number[] { return trim(ans, 1) } +/** + * Simple pow with no optimizations (in 40x times slower than jsbn's pow) + * @param x bigInt + * @param e + */ +export function pow(x: number[], e: number) { + let ans = dup(x); + e -= 1; + for(let i = 0; i < e; ++i) { + ans = mult(ans, x); + } + return trim(ans, 1); +} + /** * return (x-y) for bigInts x and y * @@ -1482,6 +1496,11 @@ export function bigInt2str(x: number[], base: number): string { return s } +/** + * Convert a bigInt into bytes + * @param x bigInt + * @param littleEndian byte order by default + */ export function bigInt2bytes(x: number[], littleEndian = true) { if(s6.length !== x.length) s6 = dup(x); else copy_(s6, x); @@ -1504,6 +1523,27 @@ export function bigInt2bytes(x: number[], littleEndian = true) { return out; } +/** + * Compare two bigInts and return -1 if x is less, 0 if equals, 1 if greater + * @param x bigInt + * @param y bigInt + */ +export function cmp(x: number[], y: number[]) { + return greater(x, y) ? 1 : (equals(x, y) ? 0 : -1); +} + +/* Object.assign(self, { + cmp, + str2bigInt, + int2bigInt, + bigInt2str, + one, + divide_, + divInt_, + dup, + negative +}); */ + /** * Returns a duplicate of bigInt x * diff --git a/tweb design/Tour6/2FA_1.png b/tweb design/Tour6/Tour 6 Done/2FA_1.png similarity index 100% rename from tweb design/Tour6/2FA_1.png rename to tweb design/Tour6/Tour 6 Done/2FA_1.png diff --git a/tweb design/Tour6/2FA_2.png b/tweb design/Tour6/Tour 6 Done/2FA_2.png similarity index 100% rename from tweb design/Tour6/2FA_2.png rename to tweb design/Tour6/Tour 6 Done/2FA_2.png diff --git a/tweb design/Tour6/2FA_3.png b/tweb design/Tour6/Tour 6 Done/2FA_3.png similarity index 100% rename from tweb design/Tour6/2FA_3.png rename to tweb design/Tour6/Tour 6 Done/2FA_3.png diff --git a/tweb design/Tour6/2FA_4.png b/tweb design/Tour6/Tour 6 Done/2FA_4.png similarity index 100% rename from tweb design/Tour6/2FA_4.png rename to tweb design/Tour6/Tour 6 Done/2FA_4.png diff --git a/tweb design/Tour6/2FA_5.png b/tweb design/Tour6/Tour 6 Done/2FA_5.png similarity index 100% rename from tweb design/Tour6/2FA_5.png rename to tweb design/Tour6/Tour 6 Done/2FA_5.png diff --git a/tweb design/Tour6/2FA_6.png b/tweb design/Tour6/Tour 6 Done/2FA_6.png similarity index 100% rename from tweb design/Tour6/2FA_6.png rename to tweb design/Tour6/Tour 6 Done/2FA_6.png diff --git a/tweb design/Tour6/2FA_7.png b/tweb design/Tour6/Tour 6 Done/2FA_7.png similarity index 100% rename from tweb design/Tour6/2FA_7.png rename to tweb design/Tour6/Tour 6 Done/2FA_7.png diff --git a/tweb design/Tour6/2FA_8.png b/tweb design/Tour6/Tour 6 Done/2FA_8.png similarity index 100% rename from tweb design/Tour6/2FA_8.png rename to tweb design/Tour6/Tour 6 Done/2FA_8.png diff --git a/tweb design/Tour6/2FA_9.png b/tweb design/Tour6/Tour 6 Done/2FA_9.png similarity index 100% rename from tweb design/Tour6/2FA_9.png rename to tweb design/Tour6/Tour 6 Done/2FA_9.png diff --git a/tweb design/Tour6/Comments_1.png b/tweb design/Tour6/Tour 6 Done/Comments_1.png similarity index 100% rename from tweb design/Tour6/Comments_1.png rename to tweb design/Tour6/Tour 6 Done/Comments_1.png diff --git a/tweb design/Tour6/Comments_2.png b/tweb design/Tour6/Tour 6 Done/Comments_2.png similarity index 100% rename from tweb design/Tour6/Comments_2.png rename to tweb design/Tour6/Tour 6 Done/Comments_2.png diff --git a/tweb design/Tour6/MobileCalendar.png b/tweb design/Tour6/Tour 6 Done/MobileCalendar.png similarity index 100% rename from tweb design/Tour6/MobileCalendar.png rename to tweb design/Tour6/Tour 6 Done/MobileCalendar.png diff --git a/tweb design/Tour6/MobileComments.png b/tweb design/Tour6/Tour 6 Done/MobileComments.png similarity index 100% rename from tweb design/Tour6/MobileComments.png rename to tweb design/Tour6/Tour 6 Done/MobileComments.png diff --git a/tweb design/Tour6/Schedule_1.png b/tweb design/Tour6/Tour 6 Done/Schedule_1.png similarity index 100% rename from tweb design/Tour6/Schedule_1.png rename to tweb design/Tour6/Tour 6 Done/Schedule_1.png diff --git a/tweb design/Tour6/Schedule_2.png b/tweb design/Tour6/Tour 6 Done/Schedule_2.png similarity index 100% rename from tweb design/Tour6/Schedule_2.png rename to tweb design/Tour6/Tour 6 Done/Schedule_2.png diff --git a/tweb design/Tour6/Schedule_3.png b/tweb design/Tour6/Tour 6 Done/Schedule_3.png similarity index 100% rename from tweb design/Tour6/Schedule_3.png rename to tweb design/Tour6/Tour 6 Done/Schedule_3.png diff --git a/tweb design/Tour6/Schedule_4.png b/tweb design/Tour6/Tour 6 Done/Schedule_4.png similarity index 100% rename from tweb design/Tour6/Schedule_4.png rename to tweb design/Tour6/Tour 6 Done/Schedule_4.png