(function() { 'use strict'; const root = document.getElementById('root'); const header = document.getElementById('header'); const frameShell = document.getElementById('frame-shell'); const frameWrap = document.getElementById('frame-wrap'); const disclosure = document.getElementById('disclosure'); const footer = document.getElementById('footer'); const buttonsWrap = document.getElementById('buttons-wrap'); const buttons = document.getElementById('buttons'); const badge = document.getElementById('badge'); const menuBackdrop = document.getElementById('menu-backdrop'); const menu = document.getElementById('menu'); const menuList = document.getElementById('menu-list'); const blocker = document.getElementById('blocker'); const resizeHandles = Array.prototype.slice.call( document.querySelectorAll('.resize-handle')); const title = document.getElementById('title'); const controls = { back: document.getElementById('back'), menu: document.getElementById('menu-toggle'), menuIcon: document.getElementById('menu-toggle-icon'), close: document.getElementById('close') }; const shellState = { backVisible: false, menuVisible: false, badgeVisible: false, bottomText: '', isFullscreen: false, blocked: false, menuOpen: false, menuItems: [], buttons: { main: null, secondary: null } }; if (window.TelegramDesktopWindowAlphaSupported === false) { root.classList.add('no-window-alpha'); } const shellAssets = { icons: Object.create(null), titleMenuIcon: null, verifiedBadge: null, menuPalette: null }; const shellToken = TDESKTOP_SHELL_TOKEN_PLACEHOLDER; const nativeMessageType = 'tdesktop_external_bot_webapp'; const maxPendingEvents = 64; let iframe = null; let frameLoaded = false; let frameUrl = 'about:blank'; let frameOrigin = ''; let sameOrigin = false; let frameGeneration = 0; let reloadSupported = false; let reloadTimeout = null; let viewportScheduled = false; let resizeObserver = null; const pendingEvents = []; function normalizeEventData(eventData) { if (typeof eventData === 'string') { try { const parsed = JSON.parse(eventData); return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}; } catch (e) { return {}; } } return eventData && typeof eventData === 'object' && !Array.isArray(eventData) ? eventData : {}; } function isShellOrigin() { return window.location.protocol === 'https:' && window.location.hostname === 'web.telegram.org' && (!window.location.port || window.location.port === '443'); } function originFromUrl(url) { try { const origin = new URL(url, window.location.href).origin; return origin && origin !== 'null' ? origin : ''; } catch (e) { return ''; } } function invokeNative(source, eventType, eventData, origin) { if (!window.external || typeof window.external.invoke !== 'function' || typeof shellToken !== 'string' || !shellToken || typeof eventType !== 'string') { return; } if (source === 'shell' && !isShellOrigin()) { return; } window.external.invoke(JSON.stringify({ type: nativeMessageType, source: source, token: shellToken, origin: origin || window.location.origin, eventType: eventType, eventData: normalizeEventData(eventData) })); } function invokeShell(eventType, eventData) { invokeNative('shell', eventType, eventData); } function invokeWebApp(eventType, eventData, origin) { invokeNative('webapp', eventType, eventData, origin); } function sendToFrame(eventType, eventData, generation) { if (!iframe || !iframe.contentWindow || generation !== frameGeneration) { return; } if (sameOrigin && !frameOrigin) { return; } const targetOrigin = sameOrigin ? frameOrigin : '*'; iframe.contentWindow.postMessage(JSON.stringify({ eventType: eventType, eventData: eventData || {} }), targetOrigin); } function postToFrame(eventType, eventData) { const generation = frameGeneration; if (!iframe || !iframe.contentWindow || !frameLoaded) { pendingEvents.push({ generation: generation, eventType: eventType, eventData: eventData || {} }); while (pendingEvents.length > maxPendingEvents) { pendingEvents.shift(); } return; } sendToFrame(eventType, eventData, generation); } function shellPointerPayload(event, extra) { const payload = { button: event.button, x: event.clientX, y: event.clientY, rootX: event.screenX, rootY: event.screenY, timeStamp: Math.round(event.timeStamp || 0) }; if (extra && typeof extra === 'object') { for (const key in extra) { payload[key] = extra[key]; } } return payload; } function beginShellControl(command, event, extra) { if (shellState.blocked || shellState.isFullscreen || event.defaultPrevented || !event.isTrusted || event.button !== 0) { return; } closeMenu(); invokeShell(command, shellPointerPayload(event, extra)); event.preventDefault(); } function beginShellMove(event) { const target = event.target; if (target && target.closest && target.closest('.title-control, #menu')) { return; } beginShellControl('shell_begin_move', event); } function beginShellResize(edge, event) { beginShellControl('shell_begin_resize', event, { edge: edge }); } function flushPendingEvents() { const pending = pendingEvents.splice(0); for (const event of pending) { if (event.generation === frameGeneration) { sendToFrame(event.eventType, event.eventData, event.generation); } } } function sendViewportChanged() { if (!iframe) { return; } const height = Math.max( 0, Math.round(frameShell.getBoundingClientRect().height)); postToFrame('viewport_changed', { height: height, is_state_stable: true, is_expanded: true }); } function scheduleViewport() { if (viewportScheduled) { return; } viewportScheduled = true; window.requestAnimationFrame(function() { viewportScheduled = false; sendViewportChanged(); }); } function setMetric(name, value) { if (typeof value === 'number' && Number.isFinite(value)) { root.style.setProperty(name, String(value) + 'px'); } } function applyMetrics(data) { if (!data || typeof data !== 'object') { return; } setMetric('--shell-radius', data.shellRadius); setMetric('--shell-pad-top', data.shellPaddingTop); setMetric('--shell-pad-right', data.shellPaddingRight); setMetric('--shell-pad-bottom', data.shellPaddingBottom); setMetric('--shell-pad-left', data.shellPaddingLeft); setMetric('--shadow-pad-top', data.shadowPaddingTop); setMetric('--shadow-pad-right', data.shadowPaddingRight); setMetric('--shadow-pad-bottom', data.shadowPaddingBottom); setMetric('--shadow-pad-left', data.shadowPaddingLeft); setMetric('--header-height', data.headerHeight); setMetric('--title-pad-top', data.titlePaddingTop); setMetric('--title-pad-right', data.titlePaddingRight); setMetric('--title-pad-bottom', data.titlePaddingBottom); setMetric('--title-pad-left', data.titlePaddingLeft); setMetric('--badge-skip', data.badgeSkip); setMetric('--frame-radius', data.frameRadius); setMetric('--control-width', data.controlWidth); setMetric('--control-height', data.controlHeight); setMetric('--button-height', data.buttonHeight); setMetric('--button-gap-x', data.buttonGapX); setMetric('--button-gap-y', data.buttonGapY); setMetric('--disclosure-skip', data.disclosureSkip); setMetric('--footer-button-skip', data.footerButtonSkip); setMetric('--fullscreen-control-width', data.fullscreenControlWidth); setMetric('--fullscreen-control-height', data.fullscreenControlHeight); setMetric('--fullscreen-control-top', data.fullscreenControlTop); setMetric('--fullscreen-control-right', data.fullscreenControlRight); setMetric('--fullscreen-control-gap', data.fullscreenControlGap); } function colorForBackground(value) { if (!/^#[0-9a-f]{6}$/i.test(value || '')) { return null; } const red = parseInt(value.slice(1, 3), 16) / 255; const green = parseInt(value.slice(3, 5), 16) / 255; const blue = parseInt(value.slice(5, 7), 16) / 255; const luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue; return luminance > 0.5 ? '#000000' : '#ffffff'; } function footerColorForBackground(value) { if (!/^#[0-9a-f]{6}$/i.test(value || '')) { return null; } const red = parseInt(value.slice(1, 3), 16) / 255; const green = parseInt(value.slice(3, 5), 16) / 255; const blue = parseInt(value.slice(5, 7), 16) / 255; const luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue; const contrast = 2.5; const textLuminance = (luminance > 0.5) ? 0 : 1; const adaptiveOpacity = (luminance - textLuminance + contrast) / contrast; const opacity = Math.max(0.5, Math.min(0.64, adaptiveOpacity)); const channel = (luminance > 0.5) ? 0 : 255; return 'rgba(' + String(channel) + ', ' + String(channel) + ', ' + String(channel) + ', ' + String(opacity) + ')'; } function titleControlColorsForBackground(value) { if (!/^#[0-9a-f]{6}$/i.test(value || '')) { return null; } const red = parseInt(value.slice(1, 3), 16) / 255; const green = parseInt(value.slice(3, 5), 16) / 255; const blue = parseInt(value.slice(5, 7), 16) / 255; const luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue; const contrast = 2.5; const textLuminance = (luminance > 0.5) ? 0 : 1; const adaptiveOpacity = (luminance - textLuminance + contrast) / contrast; const opacity = Math.max(0.5, Math.min(0.64, adaptiveOpacity)); const channel = (luminance > 0.5) ? 0 : 255; return { fg: 'rgba(' + String(channel) + ', ' + String(channel) + ', ' + String(channel) + ', ' + String(opacity) + ')', ripple: 'rgba(' + String(channel) + ', ' + String(channel) + ', ' + String(channel) + ', ' + String(opacity * 0.1) + ')' }; } function hexByte(value) { const text = Math.max( 0, Math.min(255, Math.round(value))).toString(16); return (text.length < 2 ? '0' : '') + text; } function buttonRippleForBackground(value) { if (!/^#[0-9a-f]{6}$/i.test(value || '')) { return null; } const red = parseInt(value.slice(1, 3), 16); const green = parseInt(value.slice(3, 5), 16); const blue = parseInt(value.slice(5, 7), 16); const maximum = Math.max(red, green, blue); const minimum = Math.min(red, green, blue); const delta = maximum - minimum; let hue = 0; if (delta !== 0 && maximum === red) { hue = 60 * (((green - blue) / delta) % 6); } else if (delta !== 0 && maximum === green) { hue = 60 * ((blue - red) / delta + 2); } else if (delta !== 0) { hue = 60 * ((red - green) / delta + 4); } if (hue < 0) { hue += 360; } const saturation = maximum === 0 ? 0 : delta / maximum; const nextValue = Math.max( 0, Math.min(255, maximum + (maximum > 128 ? -32 : 32))); const chroma = nextValue * saturation; const x = chroma * (1 - Math.abs((hue / 60) % 2 - 1)); const m = nextValue - chroma; let nextRed = 0; let nextGreen = 0; let nextBlue = 0; if (hue < 60) { nextRed = chroma; nextGreen = x; } else if (hue < 120) { nextRed = x; nextGreen = chroma; } else if (hue < 180) { nextGreen = chroma; nextBlue = x; } else if (hue < 240) { nextGreen = x; nextBlue = chroma; } else if (hue < 300) { nextRed = x; nextBlue = chroma; } else { nextRed = chroma; nextBlue = x; } return '#' + hexByte(nextRed + m) + hexByte(nextGreen + m) + hexByte(nextBlue + m); } function applyColors(data) { const next = (data && data.colors) ? data.colors : data; if (!next || typeof next !== 'object') { return; } if (next.bodyBg) { root.style.setProperty('--body-bg', next.bodyBg); const footerFg = footerColorForBackground(next.bodyBg); if (footerFg) { root.style.setProperty('--footer-fg', footerFg); } } if (next.titleBg) { root.style.setProperty('--title-bg', next.titleBg); const titleFg = colorForBackground(next.titleBg); if (titleFg) { root.style.setProperty('--title-fg', titleFg); } const titleControl = titleControlColorsForBackground(next.titleBg); if (titleControl) { root.style.setProperty('--title-control-fg', titleControl.fg); root.style.setProperty( '--title-control-ripple', titleControl.ripple); } } if (next.bottomBg) { root.style.setProperty('--bottom-bg', next.bottomBg); } } function applyAssets(data) { if (!data || typeof data !== 'object') { return; } if (data.icons && typeof data.icons === 'object') { shellAssets.icons = data.icons; } if (data.titleMenuIcon && typeof data.titleMenuIcon === 'object') { shellAssets.titleMenuIcon = data.titleMenuIcon; } if (data.verifiedBadge && typeof data.verifiedBadge === 'object') { shellAssets.verifiedBadge = data.verifiedBadge; } if (data.menuPalette && typeof data.menuPalette === 'object') { shellAssets.menuPalette = data.menuPalette; if (data.menuPalette.bg) { root.style.setProperty('--menu-bg', data.menuPalette.bg); } if (data.menuPalette.fg) { root.style.setProperty('--menu-fg', data.menuPalette.fg); } if (data.menuPalette.hoverBg) { root.style.setProperty( '--menu-hover-bg', data.menuPalette.hoverBg); } if (data.menuPalette.ripple) { root.style.setProperty('--menu-ripple', data.menuPalette.ripple); } if (data.menuPalette.separator) { root.style.setProperty( '--menu-separator', data.menuPalette.separator); } if (data.menuPalette.attention) { root.style.setProperty( '--menu-attention', data.menuPalette.attention); } } if (shellAssets.titleMenuIcon && shellAssets.titleMenuIcon.url) { controls.menuIcon.style.webkitMaskImage = 'url(' + shellAssets.titleMenuIcon.url + ')'; controls.menuIcon.style.maskImage = 'url(' + shellAssets.titleMenuIcon.url + ')'; if (shellAssets.titleMenuIcon.width) { controls.menuIcon.style.width = String(shellAssets.titleMenuIcon.width) + 'px'; } if (shellAssets.titleMenuIcon.height) { controls.menuIcon.style.height = String(shellAssets.titleMenuIcon.height) + 'px'; } } if (shellAssets.verifiedBadge && shellAssets.verifiedBadge.url) { badge.style.backgroundImage = 'url(' + shellAssets.verifiedBadge.url + ')'; if (shellAssets.verifiedBadge.width) { badge.style.width = String(shellAssets.verifiedBadge.width) + 'px'; } if (shellAssets.verifiedBadge.height) { badge.style.height = String(shellAssets.verifiedBadge.height) + 'px'; } if (shellAssets.verifiedBadge.alt) { badge.setAttribute('aria-label', shellAssets.verifiedBadge.alt); } } else { badge.style.backgroundImage = ''; badge.removeAttribute('aria-label'); } applyChrome({}); renderMenu(); } function applyChrome(data) { if (!data || typeof data !== 'object') { return; } if (Object.prototype.hasOwnProperty.call(data, 'backVisible')) { shellState.backVisible = !!data.backVisible; } if (Object.prototype.hasOwnProperty.call(data, 'menuVisible')) { shellState.menuVisible = !!data.menuVisible; } if (Object.prototype.hasOwnProperty.call(data, 'badgeVisible')) { shellState.badgeVisible = !!data.badgeVisible; } controls.back.classList.toggle('hidden', !shellState.backVisible); controls.menu.classList.toggle('hidden', !shellState.menuVisible); controls.menu.disabled = !shellState.menuVisible || !shellState.menuItems.length; badge.classList.toggle( 'hidden', !shellState.badgeVisible || !(shellAssets.verifiedBadge && shellAssets.verifiedBadge.url)); badge.setAttribute( 'aria-hidden', badge.classList.contains('hidden') ? 'true' : 'false'); if (controls.menu.disabled) { closeMenu(); } } function visibleButtons() { const main = shellState.buttons.main && shellState.buttons.main.visible ? shellState.buttons.main : null; const secondary = shellState.buttons.secondary && shellState.buttons.secondary.visible ? shellState.buttons.secondary : null; const result = { layout: 'single', buttons: [] }; if (main && secondary) { const position = secondary.position || 'left'; if (position === 'top') { result.layout = 'vertical'; result.buttons = [secondary, main]; } else if (position === 'bottom') { result.layout = 'vertical'; result.buttons = [main, secondary]; } else if (position === 'left') { result.layout = 'horizontal'; result.buttons = [secondary, main]; } else { result.layout = 'horizontal'; result.buttons = [main, secondary]; } } else if (main) { result.buttons = [main]; } else if (secondary) { result.buttons = [secondary]; } return result; } function requestButtonIcon(state) { if (!state || !state.visible || !state.iconCustomEmojiId || state.iconResolvedGeneration === state.iconGeneration || state.iconRequestGeneration === state.iconGeneration) { return; } state.iconRequestGeneration = state.iconGeneration; invokeShell('shell_request_button_icon', { name: state.name }); } function updateFooter() { const visible = visibleButtons(); const hasButtons = !!visible.buttons.length; disclosure.textContent = ''; disclosure.classList.remove('visible'); buttonsWrap.classList.toggle('visible', hasButtons); footer.classList.toggle('visible', hasButtons); root.style.setProperty( '--footer-gap', shellState.isFullscreen ? '0px' : hasButtons ? 'var(--footer-button-skip)' : 'var(--disclosure-skip)'); scheduleViewport(); } function renderButtons() { const visible = visibleButtons(); buttons.textContent = ''; buttons.dataset.layout = visible.layout; for (const state of visible.buttons) { const button = document.createElement('button'); button.type = 'button'; button.className = 'shell-button'; button.disabled = !state.active; const buttonColor = state.color || '#40a7e3'; button.style.background = buttonColor; button.style.color = state.textColor || '#ffffff'; const rippleColor = buttonRippleForBackground(buttonColor); if (rippleColor) { button.style.setProperty('--button-ripple', rippleColor); } const icon = document.createElement('span'); icon.className = 'button-icon'; const iconReady = state.iconResolvedGeneration === state.iconGeneration; if (state.iconCustomEmojiId && state.iconUrl) { icon.classList.add('visible'); icon.style.backgroundImage = 'url(' + state.iconUrl + ')'; } else if (state.iconCustomEmojiId && !iconReady) { icon.classList.add('visible', 'pending'); requestButtonIcon(state); } button.appendChild(icon); const label = document.createElement('span'); label.className = 'button-label'; label.textContent = state.text || ''; button.appendChild(label); const spinner = document.createElement('span'); spinner.className = 'button-spinner'; if (state.progress) { spinner.classList.add('visible'); } button.appendChild(spinner); button.addEventListener('click', function() { if (!button.disabled) { postToFrame(state.name + '_button_pressed', {}); } }); setupRipple(button); buttons.appendChild(button); } updateFooter(); } function menuIconUrl(name) { const asset = shellAssets.icons && name ? shellAssets.icons[name] : null; return (asset && asset.url) ? asset.url : ''; } function createMenuNode(item, className) { const clickable = !!item.id && item.enabled !== false; const node = document.createElement(clickable ? 'button' : 'div'); node.className = className + (item.attention ? ' attention' : '') + (clickable ? '' : ' disabled'); if (clickable) { node.type = 'button'; setupRipple(node); node.addEventListener('click', function(event) { if (!shellState.blocked && event.isTrusted) { invokeShell('shell_menu_action', { id: item.id }); closeMenu(); } }); } return node; } function createMenuCopy(item) { const copy = document.createElement('span'); copy.className = 'menu-item-copy'; const titleNode = document.createElement('span'); titleNode.className = 'menu-item-title'; titleNode.textContent = item.text || ''; copy.appendChild(titleNode); if (item.subtitle) { const subtitleNode = document.createElement('span'); subtitleNode.className = 'menu-item-subtitle'; subtitleNode.textContent = item.subtitle; copy.appendChild(subtitleNode); } return copy; } function renderMenu() { menuList.textContent = ''; for (const item of shellState.menuItems) { if (item.separator) { const separator = document.createElement('div'); separator.className = 'menu-separator'; menuList.appendChild(separator); continue; } if (Array.isArray(item.children) && item.children.length) { const group = document.createElement('div'); group.className = 'menu-group'; const header = document.createElement('div'); header.className = 'menu-item menu-group-title'; const headerIcon = document.createElement('span'); headerIcon.className = 'menu-item-icon'; const headerIconUrl = menuIconUrl(item.icon); if (headerIconUrl) { headerIcon.style.backgroundImage = 'url(' + headerIconUrl + ')'; } header.appendChild(headerIcon); header.appendChild(createMenuCopy(item)); group.appendChild(header); const children = document.createElement('div'); children.className = 'menu-group-children'; for (const child of item.children) { if (child.separator) { const separator = document.createElement('div'); separator.className = 'menu-separator'; children.appendChild(separator); continue; } const row = createMenuNode(child, 'menu-download'); row.appendChild(createMenuCopy(child)); if (child.actionLabel) { const action = document.createElement('span'); action.className = 'menu-item-action'; action.textContent = child.actionLabel; row.appendChild(action); } children.appendChild(row); } group.appendChild(children); menuList.appendChild(group); continue; } const row = createMenuNode(item, 'menu-item'); const icon = document.createElement('span'); icon.className = 'menu-item-icon'; const iconUrl = menuIconUrl(item.icon); if (iconUrl) { icon.style.backgroundImage = 'url(' + iconUrl + ')'; } row.appendChild(icon); row.appendChild(createMenuCopy(item)); menuList.appendChild(row); } menu.classList.toggle( 'visible', shellState.menuOpen && shellState.menuVisible && !!shellState.menuItems.length); menuBackdrop.classList.toggle( 'visible', menu.classList.contains('visible')); controls.menu.classList.toggle( 'active', menu.classList.contains('visible')); } function closeMenu() { if (!shellState.menuOpen) { return; } shellState.menuOpen = false; renderMenu(); } function toggleMenu(event) { if (event && !event.isTrusted) { return; } if (shellState.blocked) { return; } if (shellState.menuOpen) { closeMenu(); return; } invokeShell('shell_menu_request', {}); shellState.menuOpen = true; renderMenu(); } function parseFrameMessage(data) { if (typeof data === 'string') { try { return JSON.parse(data); } catch (e) { return null; } } return data && typeof data === 'object' && !Array.isArray(data) ? data : null; } function addRipple(button, x, y) { if (!button || button.disabled) { return; } const rect = button.getBoundingClientRect(); const ripple = document.createElement('span'); ripple.className = 'ripple'; const inner = document.createElement('span'); inner.className = 'inner'; const size = 2 * Math.hypot( Math.max(x, rect.width - x), Math.max(y, rect.height - y)); inner.style.width = String(size) + 'px'; inner.style.height = String(size) + 'px'; inner.style.left = String(x - size / 2) + 'px'; inner.style.top = String(y - size / 2) + 'px'; ripple.appendChild(inner); button.appendChild(ripple); } function stopRipples(button) { const ripples = Array.prototype.slice.call( button.querySelectorAll('.ripple:not(.hiding)')); for (const ripple of ripples) { ripple.classList.add('hiding'); window.setTimeout(function() { ripple.remove(); }, 200); } } function setupRipple(button) { button.addEventListener('mousedown', function(event) { if (event.button !== 0) { return; } const rect = button.getBoundingClientRect(); addRipple(button, event.clientX - rect.left, event.clientY - rect.top); }); button.addEventListener('mouseup', function() { stopRipples(button); }); button.addEventListener('mouseleave', function() { stopRipples(button); }); } window.addEventListener('message', function(event) { if (!iframe || !iframe.contentWindow || event.source !== iframe.contentWindow) { return; } if (sameOrigin && (!frameOrigin || event.origin !== frameOrigin)) { return; } const message = parseFrameMessage(event.data); if (!message || typeof message.eventType !== 'string') { return; } if (message.eventType === 'iframe_ready') { reloadSupported = !!(message.eventData && message.eventData.reload_supported); return; } else if (message.eventType === 'iframe_will_reload') { if (reloadTimeout) { window.clearTimeout(reloadTimeout); reloadTimeout = null; } frameLoaded = false; return; } invokeWebApp(message.eventType, message.eventData, event.origin); }); menuBackdrop.addEventListener('mousedown', closeMenu); blocker.addEventListener('click', function(event) { if (!event.isTrusted || !shellState.blocked) { return; } invokeShell('shell_close_layer', {}); event.preventDefault(); event.stopPropagation(); }); document.addEventListener('mousedown', function(event) { if (!shellState.menuOpen) { return; } const target = event.target; if (menu.contains(target) || controls.menu.contains(target) || menuBackdrop.contains(target)) { return; } closeMenu(); }); window.addEventListener('keydown', function(event) { if (event.key === 'Escape' && shellState.menuOpen) { event.preventDefault(); closeMenu(); } }); window.addEventListener('resize', scheduleViewport); if (window.ResizeObserver) { resizeObserver = new window.ResizeObserver(scheduleViewport); resizeObserver.observe(frameShell); resizeObserver.observe(root); } controls.close.addEventListener('click', function(event) { if (event.isTrusted) { invokeShell('shell_close', {}); } }); controls.back.addEventListener('click', function() { postToFrame('back_button_pressed', {}); }); controls.menu.addEventListener('click', toggleMenu); setupRipple(controls.close); setupRipple(controls.back); setupRipple(controls.menu); header.addEventListener('selectstart', function(event) { event.preventDefault(); }); header.addEventListener('mousedown', beginShellMove); for (const handle of resizeHandles) { handle.addEventListener('mousedown', function(event) { beginShellResize(handle.getAttribute('data-resize-edge'), event); }); } function createIframe(url) { closeMenu(); if (reloadTimeout) { window.clearTimeout(reloadTimeout); reloadTimeout = null; } const generation = ++frameGeneration; pendingEvents.splice(0); const next = document.createElement('iframe'); next.setAttribute('allow', 'clipboard-read; clipboard-write; fullscreen'); next.referrerPolicy = 'no-referrer'; next.addEventListener('load', function() { if (iframe !== next || generation !== frameGeneration) { return; } frameLoaded = true; flushPendingEvents(); scheduleViewport(); }); frameLoaded = false; reloadSupported = false; if (iframe) { iframe.remove(); } iframe = next; iframe.src = url || 'about:blank'; frameWrap.appendChild(iframe); } function fallbackReloadFrame() { createIframe(frameUrl || 'about:blank'); } function reloadFrame() { if (!iframe) { return; } if (reloadSupported && frameLoaded && iframe.contentWindow) { sendToFrame('reload_iframe', {}, frameGeneration); if (reloadTimeout) { window.clearTimeout(reloadTimeout); } reloadTimeout = window.setTimeout(function() { reloadTimeout = null; fallbackReloadFrame(); }, 500); return; } fallbackReloadFrame(); } function isNativeToken(token) { return !!shellToken && token === shellToken; } const api = { bootstrap: function(data, token) { if (!isNativeToken(token)) { return; } applyMetrics(data && data.metrics); applyColors(data && data.colors); applyChrome(data || {}); shellState.bottomText = ''; title.textContent = (data && data.title) || ''; document.title = (data && data.title) || 'Telegram'; sameOrigin = !!(data && data.sameOrigin); frameUrl = (data && data.url) || 'about:blank'; frameOrigin = sameOrigin ? originFromUrl(frameUrl) : ''; createIframe(frameUrl); renderButtons(); renderMenu(); }, nativeEvent: function(eventType, eventData, token) { if (!isNativeToken(token) || typeof eventType !== 'string') { return; } if (eventType === 'fullscreen_changed' && eventData) { shellState.isFullscreen = !!eventData.is_fullscreen; root.classList.toggle('fullscreen', shellState.isFullscreen); updateFooter(); } postToFrame(eventType, eventData || {}); }, setTitle: function(data, token) { if (!isNativeToken(token)) { return; } title.textContent = (data && data.title) || ''; document.title = (data && data.title) || 'Telegram'; }, setChrome: function(data, token) { if (!isNativeToken(token)) { return; } applyChrome(data || {}); }, setColors: function(data, token) { if (!isNativeToken(token)) { return; } applyColors(data || {}); }, setAssets: function(data, token) { if (!isNativeToken(token)) { return; } applyAssets(data || {}); }, setMenu: function(data, token) { if (!isNativeToken(token)) { return; } shellState.menuItems = Array.isArray(data && data.items) ? data.items : []; applyChrome({}); renderMenu(); }, setBottomText: function(data, token) { if (!isNativeToken(token)) { return; } shellState.bottomText = ''; updateFooter(); }, setButton: function(data, token) { if (!isNativeToken(token)) { return; } if (!data || !data.name) { return; } const previous = shellState.buttons[data.name] || {}; const next = Object.assign({}, previous, data); if (next.iconGeneration !== previous.iconGeneration || next.iconCustomEmojiId !== previous.iconCustomEmojiId) { next.iconRequestGeneration = ''; next.iconResolvedGeneration = ''; next.iconUrl = ''; } if (!next.iconCustomEmojiId) { next.iconRequestGeneration = ''; next.iconResolvedGeneration = next.iconGeneration || ''; next.iconUrl = ''; } shellState.buttons[data.name] = next; renderButtons(); }, setButtonIcon: function(data, token) { if (!isNativeToken(token)) { return; } if (!data || !data.name) { return; } const state = shellState.buttons[data.name]; if (!state || state.iconGeneration !== (data.generation || '')) { return; } const icon = (data.icon && typeof data.icon === 'object') ? data.icon : null; state.iconRequestGeneration = ''; state.iconResolvedGeneration = state.iconGeneration; state.iconUrl = (icon && icon.url) ? icon.url : ''; renderButtons(); }, setBlocked: function(data, token) { if (!isNativeToken(token)) { return; } shellState.blocked = !!(data && data.blocked); root.classList.toggle('blocked', shellState.blocked); if (shellState.blocked) { closeMenu(); } }, setProgress: function(data, token) { if (!isNativeToken(token)) { return; } root.classList.toggle('loading', !!(data && data.shown)); }, reloadFrame: function(data, token) { if (!isNativeToken(token)) { return; } reloadFrame(); }, sendViewport: function(data, token) { if (!isNativeToken(token)) { return; } scheduleViewport(); } }; Object.defineProperty(window, 'TelegramDesktopShell', { value: Object.freeze(api), configurable: false, writable: false }); })();