您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Improves Telegram Web accessibility for screen reader users with UI fixes and feature enhancements.
// ==UserScript== // @name Accessible Telegram Web // @namespace http://tampermonkey.net/ // @version 3.0 // @description Improves Telegram Web accessibility for screen reader users with UI fixes and feature enhancements. // @author mahmood hozhabri // @match https://web.telegram.org/a/* // @grant GM_info // @run-at document-end // ==/UserScript== (function() { 'use strict'; // Simulates a universal click by dispatching multiple event types to ensure reliability. function simulateUniversalClick(element) { if (!element) return; const isTouchDevice = 'ontouchstart' in window; if (isTouchDevice) { element.dispatchEvent(new TouchEvent('touchstart', { bubbles: true, cancelable: true, view: window })); element.dispatchEvent(new TouchEvent('touchend', { bubbles: true, cancelable: true, view: window })); } element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window })); element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window })); element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })); } // ========================================================================= // SECTION: Unified Live Region for Announcements // ... (All other functions from before remain the same) // ... // ========================================================================= let unifiedLiveRegion; let currentChatStatusText = ''; let lastAnnouncedMessageId = null; let announcementTimeout = null; let isInitialPageLoad = true; // Initializes the visually hidden live region element. function createUnifiedLiveRegion() { if (unifiedLiveRegion) return; unifiedLiveRegion = document.createElement('div'); unifiedLiveRegion.setAttribute('aria-live', 'polite'); unifiedLiveRegion.setAttribute('aria-atomic', 'true'); unifiedLiveRegion.style.cssText = 'position: absolute; left: -9999px; top: -9999px; width: 1px; height: 1px; overflow: hidden;'; document.body.appendChild(unifiedLiveRegion); } // Sends text to the live region to be announced by a screen reader. function announceText(text, isMessage = false) { if (!unifiedLiveRegion) return; if (announcementTimeout) { clearTimeout(announcementTimeout); announcementTimeout = null; } if (!text.trim()) { if (unifiedLiveRegion.textContent.trim() !== '' && !unifiedLiveRegion.dataset.isAnnouncingMessage) { unifiedLiveRegion.textContent = ''; } return; } if (isMessage) { unifiedLiveRegion.dataset.isAnnouncingMessage = 'true'; unifiedLiveRegion.textContent = text; announcementTimeout = setTimeout(() => { if (unifiedLiveRegion.textContent === text) { unifiedLiveRegion.textContent = currentChatStatusText ? `User status: ${currentChatStatusText}` : ''; delete unifiedLiveRegion.dataset.isAnnouncingMessage; } }, 3000); } else { delete unifiedLiveRegion.dataset.isAnnouncingMessage; if (unifiedLiveRegion.textContent !== `User status: ${text}`) { unifiedLiveRegion.textContent = `User status: ${text}`; } } } let statusObserverInstance = null; let currentObservedChatInfoContainer = null; let statusUpdateDebounceTimeout = null; // Monitors the chat header for status changes (e.g., "online", "typing") and announces them. function monitorChatStatus() { const chatInfoContainer = document.querySelector('.ChatInfo .info'); if (!chatInfoContainer) { if (statusObserverInstance) { statusObserverInstance.disconnect(); statusObserverInstance = null; currentObservedChatInfoContainer = null; } currentChatStatusText = ''; announceText(''); return; } if (chatInfoContainer === currentObservedChatInfoContainer) { return; } if (statusObserverInstance) { statusObserverInstance.disconnect(); } const extractStatusText = (element) => { const commonExclusions = ['last seen', 'ago', 'connecting', 'updating', 'synchronizing', 'subscriber', 'member', 'bot', 'monthly users']; const checkAndFilter = (text) => { if (!text) return ''; const lowerText = text.toLowerCase(); for (const exclusion of commonExclusions) { if (lowerText.includes(exclusion)) { return ''; } } return text; }; const typingStatusElem = element.querySelector('p.typing-status'); if (typingStatusElem && typingStatusElem.textContent.trim()) { const text = typingStatusElem.textContent.trim().replace(/\.{3}$/, ''); return checkAndFilter(text); } const userStatusSpan = element.querySelector('.status .user-status'); if (userStatusSpan && userStatusSpan.textContent.trim()) { const text = userStatusSpan.textContent.trim(); return checkAndFilter(text); } const generalStatusElem = element.querySelector('.status'); if (generalStatusElem && generalStatusElem.textContent.trim()) { const text = generalStatusElem.textContent.trim(); const fullNameElem = element.querySelector('.fullName'); const fullNameText = fullNameElem ? fullNameElem.textContent.trim() : ''; if (text !== fullNameText) { return checkAndFilter(text); } } return ''; }; const updateStatusAnnouncement = () => { const newStatusText = extractStatusText(chatInfoContainer); if (newStatusText !== currentChatStatusText) { currentChatStatusText = newStatusText; if (currentChatStatusText.trim()) { announceText(currentChatStatusText); } else { announceText(''); } } }; updateStatusAnnouncement(); statusObserverInstance = new MutationObserver((mutations) => { const relevantChange = mutations.some(mutation => mutation.target === chatInfoContainer || (mutation.target.parentElement === chatInfoContainer && ( mutation.target.matches('p.typing-status') || mutation.target.matches('.status') )) || (mutation.type === 'characterData' && mutation.target.parentElement && ( mutation.target.parentElement.matches('p.typing-status') || mutation.target.parentElement.matches('.status') || mutation.target.parentElement.closest('p.typing-status') || mutation.target.parentElement.closest('.status .user-status') )) ); if (relevantChange) { if (statusUpdateDebounceTimeout) clearTimeout(statusUpdateDebounceTimeout); statusUpdateDebounceTimeout = setTimeout(updateStatusAnnouncement, 50); } }); currentObservedChatInfoContainer = chatInfoContainer; const observerOptions = { childList: true, subtree: true, characterData: true }; statusObserverInstance.observe(chatInfoContainer, observerOptions); } // ========================================================================= // SECTION: Chat List Processing // ... // (The function processChatList is unchanged) function processChatList() { const chats = document.querySelectorAll('#LeftColumn .ListItem.Chat'); chats.forEach(chat => { const link = chat.querySelector('a.ListItem-button'); if (!link) return; const nameElem = chat.querySelector('.fullName'), timeElem = chat.querySelector('.time'), messageElem = chat.querySelector('.last-message-summary'), senderElem = chat.querySelector('.sender-name'), unreadBadge = chat.querySelector('.ChatBadge.unread .tgKbsVmz'); if (!nameElem || !timeElem || !messageElem || !nameElem.textContent.trim() || !timeElem.textContent.trim() || !messageElem.textContent.trim()) { return; } const name = nameElem.textContent.trim(); const time = timeElem.textContent.trim(); const message = messageElem.textContent.trim(); const sender = senderElem ? senderElem.textContent.trim() : ''; const unreadCount = unreadBadge ? unreadBadge.textContent.trim() : ''; let chatTypePrefix = ''; if (chat.classList.contains('private')) { chatTypePrefix = `${name}. `; } else if (chat.classList.contains('forum') || senderElem) { chatTypePrefix = `Group: ${name}. `; } else { chatTypePrefix = `Channel: ${name}. `; } let ariaLabel = chatTypePrefix; if (unreadCount) ariaLabel += `${unreadCount} unread messages. `; ariaLabel += sender ? `from ${sender}: ${message}. ` : `${message}. `; ariaLabel += `${time}.`; const lastState = chat.getAttribute('data-last-state-signature'); if (lastState !== ariaLabel) { link.setAttribute('aria-label', ariaLabel); chat.setAttribute('data-last-state-signature', ariaLabel); } }); } // ========================================================================= // SECTION: Custom Accessible Menus // ... // (The functions createAccessibleMenu and showAccessibleContextMenu are unchanged) function createAccessibleMenu(title, items) { const existingMenu = document.getElementById('accessible-context-menu'); if (existingMenu) existingMenu.remove(); const overlay = document.createElement('div'); overlay.id = 'accessible-context-menu'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 9999; display: flex; justify-content: center; align-items: center; `; const menu = document.createElement('div'); menu.setAttribute('role', 'dialog'); menu.setAttribute('aria-modal', 'true'); menu.setAttribute('aria-label', title); menu.style.cssText = ` background-color: white; color: black; border-radius: 12px; padding: 10px; max-width: 80%; box-shadow: 0 4px 12px rgba(0,0,0,0.2); `; const menuList = document.createElement('ul'); menuList.style.cssText = 'list-style: none; padding: 0; margin: 0;'; items.forEach(item => { const listItem = document.createElement('li'); const button = document.createElement('button'); button.textContent = item.text; button.style.cssText = ` width: 100%; padding: 12px 16px; border: none; background: none; text-align: left; font-size: 17px; cursor: pointer; color: ${item.isDestructive ? '#ff3b30' : '#007aff'}; ${item.disabled ? 'color: #8e8e93; cursor: not-allowed;' : ''} `; if (!item.disabled) { button.onclick = () => { item.action(); if (!item.keepMenuOpen) { overlay.remove(); } }; } listItem.appendChild(button); menuList.appendChild(listItem); }); const cancelButton = document.createElement('button'); cancelButton.textContent = 'Cancel'; cancelButton.style.cssText = ` width: 100%; padding: 12px 16px; border: none; background-color: #f0f0f0; border-radius: 8px; margin-top: 10px; font-size: 17px; font-weight: bold; color: #007aff; cursor: pointer; `; cancelButton.onclick = () => overlay.remove(); menu.appendChild(menuList); menu.appendChild(cancelButton); overlay.appendChild(menu); document.body.appendChild(overlay); const firstButton = menu.querySelector('button'); if (firstButton) firstButton.focus(); overlay.onclick = (e) => { if (e.target === overlay) { overlay.remove(); } }; } function showAccessibleContextMenu(nativeMenuItems, title) { const nativeMenuBackdrop = document.querySelector('.Menu.in-portal .backdrop'); if (nativeMenuBackdrop) { nativeMenuBackdrop.click(); } const menuTitle = title || 'Actions'; const items = Array.from(nativeMenuItems).map(item => ({ text: item.textContent.trim(), action: () => simulateUniversalClick(item), isDestructive: item.classList.contains('destructive'), disabled: false })).filter(item => item.text); if (items.length > 0) { createAccessibleMenu(menuTitle, items); } } // ========================================================================= // SECTION: Delegated Event Listeners // ... // (All Delegated Event Listener functions are unchanged) function addDelegatedChatListListeners() { const leftColumn = document.getElementById('LeftColumn'); if (!leftColumn || leftColumn.hasAttribute('data-delegated-chat-listeners-added')) { return; } let longPressTimer; let wasLongPress = false; let touchStartX = 0; let touchStartY = 0; const LONG_PRESS_THRESHOLD = 10; const LONG_PRESS_DELAY = 500; const getTargetChat = (event) => event.target.closest('.ListItem.Chat'); const showMenu = (chat, clientX, clientY) => { if (!chat) return; chat.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true, view: window, clientX, clientY })); setTimeout(() => { const nativeMenuItems = document.querySelectorAll('.ListItem-context-menu .MenuItem'); if (nativeMenuItems.length > 0) { showAccessibleContextMenu(nativeMenuItems, 'Chat Actions'); } }, 50); }; leftColumn.addEventListener('touchstart', (event) => { const targetChat = getTargetChat(event); if (!targetChat) return; wasLongPress = false; touchStartX = event.touches[0].clientX; touchStartY = event.touches[0].clientY; longPressTimer = setTimeout(() => { wasLongPress = true; showMenu(targetChat, touchStartX, touchStartY); }, LONG_PRESS_DELAY); }, { passive: true }); leftColumn.addEventListener('touchmove', (event) => { if (!longPressTimer) return; const deltaX = Math.abs(event.touches[0].clientX - touchStartX); const deltaY = Math.abs(event.touches[0].clientY - touchStartY); if (deltaX > LONG_PRESS_THRESHOLD || deltaY > LONG_PRESS_THRESHOLD) { clearTimeout(longPressTimer); longPressTimer = null; } }, { passive: true }); leftColumn.addEventListener('touchend', (event) => { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } if (wasLongPress) { event.preventDefault(); } }); leftColumn.addEventListener('contextmenu', (e) => { const targetChat = getTargetChat(e); if (!targetChat) return; e.preventDefault(); e.stopPropagation(); showMenu(targetChat, e.clientX, e.clientY); }); leftColumn.setAttribute('data-delegated-chat-listeners-added', 'true'); } function addDelegatedMessageListeners() { const messageList = document.querySelector('.MessageList.custom-scroll'); if (!messageList || messageList.hasAttribute('data-delegated-listeners-added')) { return; } let longPressTimer; let wasLongPress = false; let touchStartX = 0; let touchStartY = 0; const LONG_PRESS_DELAY = 500; const TOUCH_MOVE_THRESHOLD = 10; const getTargetMessage = (event) => event.target.closest('.Message'); messageList.addEventListener('touchstart', (event) => { const targetMessage = getTargetMessage(event); if (!targetMessage) return; wasLongPress = false; touchStartX = event.touches[0].clientX; touchStartY = event.touches[0].clientY; longPressTimer = setTimeout(() => { wasLongPress = true; targetMessage.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true, view: window, clientX: touchStartX, clientY: touchStartY })); setTimeout(() => { const nativeMenuItems = document.querySelectorAll('.ContextMenuContainer .MenuItem'); if (nativeMenuItems.length > 0) { showAccessibleContextMenu(nativeMenuItems, 'Message Actions'); } }, 50); }, LONG_PRESS_DELAY); }, { passive: true }); messageList.addEventListener('touchmove', (event) => { if (!longPressTimer) return; const deltaX = Math.abs(event.touches[0].clientX - touchStartX); const deltaY = Math.abs(event.touches[0].clientY - touchStartY); if (deltaX > TOUCH_MOVE_THRESHOLD || deltaY > TOUCH_MOVE_THRESHOLD) { clearTimeout(longPressTimer); longPressTimer = null; } }, { passive: true }); messageList.addEventListener('touchend', (event) => { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } if (wasLongPress) { event.preventDefault(); } }); messageList.addEventListener('contextmenu', (e) => { const targetMessage = getTargetMessage(e); if (!targetMessage) return; e.preventDefault(); e.stopPropagation(); targetMessage.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true, view: window, clientX: e.clientX, clientY: e.clientY })); setTimeout(() => { const nativeMenuItems = document.querySelectorAll('.ContextMenuContainer .MenuItem'); if (nativeMenuItems.length > 0) showAccessibleContextMenu(nativeMenuItems, 'Message Actions'); }, 50); }); messageList.setAttribute('data-delegated-listeners-added', 'true'); } function showAudioPlayerMenu() { const audioPlayer = document.querySelector('.AudioPlayer'); if (!audioPlayer) return; const playPauseBtn = audioPlayer.querySelector('.toggle-play'); const prevBtn = audioPlayer.querySelector('.player-button[aria-label="Previous track"]'); const nextBtn = audioPlayer.querySelector('.player-button[aria-label="Next track"]'); const rateBtn = audioPlayer.querySelector('.playback-button'); const closeBtn = audioPlayer.querySelector('.player-close'); const createRefreshAction = (element) => () => { if (element) simulateUniversalClick(element); setTimeout(showAudioPlayerMenu, 150); }; const items = [{ text: playPauseBtn && playPauseBtn.classList.contains('pause') ? 'Pause' : 'Play', action: createRefreshAction(playPauseBtn), disabled: !playPauseBtn, keepMenuOpen: true }, { text: 'Previous Track', action: createRefreshAction(prevBtn), disabled: !prevBtn || prevBtn.classList.contains('disabled'), keepMenuOpen: true }, { text: 'Next Track', action: createRefreshAction(nextBtn), disabled: !nextBtn || nextBtn.classList.contains('disabled'), keepMenuOpen: true }, { text: `Playback Rate (${rateBtn ? rateBtn.textContent.trim() : '1X'})`, action: createRefreshAction(rateBtn), disabled: !rateBtn, keepMenuOpen: true }, { text: 'Close Player', action: () => simulateUniversalClick(closeBtn), disabled: !closeBtn, isDestructive: true }]; createAccessibleMenu('Player Controls', items); } // ========================================================================= // SECTION: Message Processing // ... // (All Message Processing functions are unchanged) function handleAudioMessage(message, isVoice) { const originalContentWrapper = message.querySelector('.message-content-wrapper'); if (!originalContentWrapper || message.querySelector('[data-accessible-audio-heading="true"]')) { return; } const playButton = message.querySelector('.Button.toggle-play'); if (!playButton) return; const textContentElem = message.querySelector('.text-content'); let captionText = ''; const extractedLinks = []; if (textContentElem) { const clone = textContentElem.cloneNode(true); clone.querySelectorAll('.Reactions, .MessageMeta').forEach(el => el.remove()); clone.querySelectorAll('img.emoji, .custom-emoji').forEach(emojiEl => { const altText = emojiEl.alt || emojiEl.dataset.alt; if (altText) emojiEl.parentNode.replaceChild(document.createTextNode(altText), emojiEl); }); captionText = clone.innerText.trim(); textContentElem.querySelectorAll('a').forEach(link => extractedLinks.push(link)); } const accessibleHeading = document.createElement('div'); accessibleHeading.setAttribute('role', 'heading'); accessibleHeading.setAttribute('aria-level', '3'); accessibleHeading.setAttribute('tabindex', '0'); accessibleHeading.setAttribute('data-accessible-audio-heading', 'true'); accessibleHeading.style.cssText = 'padding: 12px 16px; cursor: pointer; font-size: 1rem; border: 1px solid transparent; border-radius: 8px; margin: 2px;'; accessibleHeading.onfocus = () => accessibleHeading.style.borderColor = '#3390ec'; accessibleHeading.onblur = () => accessibleHeading.style.borderColor = 'transparent'; const senderTitleElem = message.querySelector('.message-title .sender-title'); const sender = senderTitleElem ? `From ${senderTitleElem.textContent.trim()}.` : (message.classList.contains('own') ? 'Your message.' : ''); let label, text; const controlsContainer = document.createElement('div'); controlsContainer.setAttribute('role', 'toolbar'); controlsContainer.setAttribute('aria-label', 'Audio Message Options'); controlsContainer.style.cssText = 'margin-top: 8px; display: flex; flex-wrap: wrap; gap: 8px; z-index: 6; position: relative;'; if (isVoice) { const voiceDurationElem = message.querySelector('.voice-duration'); const duration = (voiceDurationElem && voiceDurationElem.textContent.trim()) || ''; const timeElem = message.querySelector('.message-time'); const time = (timeElem && timeElem.textContent.trim()) || ''; label = `${sender} Voice message. ${duration ? `Duration: ${duration}.` : ''} ${captionText ? `Caption: ${captionText}.` : ''} ${time ? `Time: ${time}.` : ''} Double tap to play and open player controls.`; text = `Voice message (${duration})`; accessibleHeading.setAttribute('aria-label', label); accessibleHeading.textContent = text; accessibleHeading.onclick = (e) => { e.stopPropagation(); simulateUniversalClick(playButton); setTimeout(showAudioPlayerMenu, 150); }; message.insertBefore(accessibleHeading, originalContentWrapper); } else { // Music const songTitleElem = message.querySelector('.Audio .title'); const songTitle = (songTitleElem && songTitleElem.textContent.trim()) || 'Untitled'; const performerElem = message.querySelector('.Audio .performer'); const performer = (performerElem && performerElem.textContent.trim()) || 'Unknown Artist'; const durationElem = message.querySelector('.Audio .duration'); const duration = (durationElem && durationElem.textContent.trim()); label = `Music file. ${performer} - ${songTitle}. ${duration ? `Duration: ${duration}.` : ''} ${captionText ? `Caption: ${captionText}.` : ''} Double tap to play and open player controls.`; text = `Music: ${songTitle}`; accessibleHeading.setAttribute('aria-label', label); accessibleHeading.textContent = text; accessibleHeading.onclick = (e) => { e.stopPropagation(); simulateUniversalClick(playButton); setTimeout(showAudioPlayerMenu, 150); }; message.insertBefore(accessibleHeading, originalContentWrapper); const originalDownloadContainer = message.querySelector('.Audio .download-button'); if (originalDownloadContainer) { const newDownloadButton = document.createElement('button'); const updateButtonState = () => { const isDownloading = originalDownloadContainer.querySelector('.Progress, .spinner-container'); const nativeCancelButton = originalDownloadContainer.querySelector('.cancel-button, .icon-close'); if (isDownloading || nativeCancelButton) { newDownloadButton.textContent = 'Cancel'; newDownloadButton.onclick = (e) => { e.stopPropagation(); if (nativeCancelButton) simulateUniversalClick(nativeCancelButton); }; } else { newDownloadButton.textContent = `Download ${performer} - ${songTitle}`; newDownloadButton.onclick = (e) => { e.stopPropagation(); simulateUniversalClick(originalDownloadContainer); }; } }; updateButtonState(); const observer = new MutationObserver(updateButtonState); observer.observe(originalDownloadContainer, { childList: true, subtree: true, attributes: true }); controlsContainer.appendChild(newDownloadButton); } } if (extractedLinks.length > 0) { extractedLinks.forEach(link => { const newLinkAnchor = document.createElement('a'); newLinkAnchor.textContent = link.textContent.trim() || link.href; newLinkAnchor.href = link.href; newLinkAnchor.target = '_blank'; newLinkAnchor.rel = 'noopener noreferrer'; newLinkAnchor.style.cssText = 'padding: 8px 12px; background-color: #f0f0f0; border: 1px solid #ddd; border-radius: 8px; text-decoration: none; color: #007aff;'; controlsContainer.appendChild(newLinkAnchor); }); } if (controlsContainer.hasChildNodes()) { message.insertBefore(controlsContainer, accessibleHeading.nextSibling); } originalContentWrapper.querySelectorAll('button, a, [role="button"]').forEach(el => { el.setAttribute('aria-hidden', 'true'); el.setAttribute('tabindex', '-1'); }); message.setAttribute('data-accessible-message', 'true'); if (!isInitialPageLoad) { const announceLabel = accessibleHeading.getAttribute('aria-label'); if (announceLabel) { announceText(announceLabel, true); } } } function processMessages() { const messages = document.querySelectorAll('.Message:not([data-accessible-message="true"])'); messages.forEach(message => { const isVoice = !!message.querySelector('.message-content.voice'); const isMusic = !!message.querySelector('.Audio.inline'); if (isVoice || isMusic) { handleAudioMessage(message, isVoice); return; } const messageContentWrapper = message.querySelector('.message-content-wrapper'); if (!messageContentWrapper) return; message.style.display = 'flex'; message.style.flexDirection = 'column'; message.style.alignItems = 'flex-start'; const overlay = document.createElement('div'); overlay.setAttribute('role', 'heading'); overlay.setAttribute('aria-level', '3'); overlay.style.cssText = 'position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 5; cursor: pointer;'; const labelParts = []; const stickerImage = message.querySelector('.sticker-image'); const gifMedia = message.querySelector('.message-media-gif, .media-inner.is-document.is-animated'); if (stickerImage) { const emojiEl = stickerImage.closest('.Message').querySelector('.emoji-small'); const stickerEmoji = emojiEl ? emojiEl.alt || emojiEl.textContent.trim() : ''; let stickerLabel = 'Sticker'; if (stickerEmoji) { stickerLabel += `: ${stickerEmoji}`; } labelParts.push(stickerLabel); stickerImage.setAttribute('aria-label', stickerLabel); stickerImage.setAttribute('alt', stickerLabel); } else if (gifMedia) { let gifLabel = 'Animated GIF'; const messageTextElem = message.querySelector('.message-text'); const caption = (messageTextElem && messageTextElem.textContent.trim()) || ''; if (caption) { gifLabel += `: ${caption}`; } labelParts.push(gifLabel); const mediaElement = gifMedia.querySelector('img, video'); if (mediaElement) { mediaElement.setAttribute('aria-label', gifLabel); mediaElement.setAttribute('alt', gifLabel); } } const isOwnMessage = message.classList.contains('own'); if (isOwnMessage) { labelParts.push('Your message.'); } else { const senderTitleElem = message.querySelector('.message-title .sender-title'); const isForwarded = !!message.querySelector('.message-title-wrapper .label'); if (senderTitleElem) { labelParts.push(isForwarded ? `Forwarded from ${senderTitleElem.textContent.trim()}.` : `From ${senderTitleElem.textContent.trim()}.`); } } const textContentElem = message.querySelector('.text-content'); if (textContentElem) { const clone = textContentElem.cloneNode(true); clone.querySelectorAll('.Reactions, .MessageMeta').forEach(el => el.remove()); clone.querySelectorAll('img.emoji, .custom-emoji').forEach(emojiEl => { const altText = emojiEl.alt || emojiEl.dataset.alt; if (altText) emojiEl.parentNode.replaceChild(document.createTextNode(altText), emojiEl); }); const cleanText = clone.innerText.trim(); if (cleanText) labelParts.push(cleanText); } const controlsContainer = document.createElement('div'); controlsContainer.setAttribute('role', 'toolbar'); controlsContainer.setAttribute('aria-label', 'Message Options'); controlsContainer.style.cssText = 'margin-top: 8px; display: flex; flex-wrap: wrap; gap: 8px; z-index: 6; position: relative;'; if (textContentElem) { textContentElem.querySelectorAll('a').forEach(link => { const newLinkAnchor = document.createElement('a'); newLinkAnchor.textContent = link.textContent.trim() || link.href; newLinkAnchor.href = link.href; newLinkAnchor.target = '_blank'; newLinkAnchor.rel = 'noopener noreferrer'; controlsContainer.appendChild(newLinkAnchor); }); } const replyHeader = message.querySelector('.message-subheader .EmbeddedMessage'); if (replyHeader) { const embeddedSenderElem = replyHeader.querySelector('.embedded-sender'); const sender = (embeddedSenderElem && embeddedSenderElem.textContent.trim()); const embeddedTextElem = replyHeader.querySelector('.embedded-text-wrapper'); const text = (embeddedTextElem && embeddedTextElem.textContent.trim()); let replyLabel = "In reply to"; if (sender) replyLabel += ` message from ${sender}`; if (text) replyLabel += `: ${text}`; labelParts.push(`${replyLabel}.`); } message.querySelectorAll('.InlineButtons .Button, .reply-markup .Button').forEach(button => { const buttonText = button.textContent.trim(); if (buttonText) { const newInlineButton = document.createElement('button'); newInlineButton.textContent = `Button: ${buttonText}`; newInlineButton.onclick = (e) => { e.stopPropagation(); simulateUniversalClick(button); }; controlsContainer.appendChild(newInlineButton); } }); const originalForwardButton = message.querySelector('.message-action-button[aria-label="Forward"]'); if (originalForwardButton) { const newForwardButton = document.createElement('button'); newForwardButton.textContent = 'Forward'; newForwardButton.onclick = (e) => { e.stopPropagation(); simulateUniversalClick(originalForwardButton); }; controlsContainer.appendChild(newForwardButton); } const originalCommentButton = message.querySelector('.CommentButton'); if (originalCommentButton) { const newCommentButton = document.createElement('button'); const commentCount = originalCommentButton.getAttribute('data-cnt'); const labelElem = originalCommentButton.querySelector('.label'); const labelText = (labelElem && labelElem.textContent.trim()); let buttonText; if (labelText && labelText.toLowerCase().includes('leave a comment')) { buttonText = 'Leave a comment'; } else if (commentCount && parseInt(commentCount, 10) > 0) { buttonText = `${commentCount} ${parseInt(commentCount, 10) === 1 ? 'comment' : 'comments'}`; } else { buttonText = 'Comments'; } newCommentButton.textContent = buttonText; newCommentButton.onclick = (e) => { e.stopPropagation(); simulateUniversalClick(originalCommentButton); }; controlsContainer.appendChild(newCommentButton); } const album = message.querySelector('.Album'); const fileInfo = message.querySelector('.File'); if (album) { const items = Array.from(album.querySelectorAll('.media-inner')); let photoCount = 0, videoCount = 0; items.forEach(item => { if (item.querySelector('video, .message-media-duration')) videoCount++; else photoCount++; }); let albumDescription = `Album with ${items.length} items`; if (photoCount > 0) albumDescription += `: ${photoCount} photos`; if (videoCount > 0) albumDescription += `${photoCount > 0 ? ' and' : ':'} ${videoCount} videos`; albumDescription += '.'; let currentIndex = 0; const getSelectedItemType = () => (items[currentIndex] && items[currentIndex].querySelector('video, .message-media-duration')) ? 'Video' : 'Photo'; const updateMediaFocus = () => { items.forEach((item, index) => item.style.outline = index === currentIndex ? '3px solid #3390ec' : 'none'); const currentItemLabel = `Item ${currentIndex + 1} of ${items.length}: ${getSelectedItemType()}.`; overlay.setAttribute('aria-label', `${currentItemLabel} ${albumDescription} \n${labelParts.join(' \n')}`); }; overlay.onclick = (e) => { e.stopPropagation(); if (items[currentIndex]) simulateUniversalClick(items[currentIndex]); }; const prevButton = document.createElement('button'); prevButton.textContent = 'Previous Media'; prevButton.onclick = () => { currentIndex = (currentIndex - 1 + items.length) % items.length; updateMediaFocus(); }; controlsContainer.appendChild(prevButton); const nextButton = document.createElement('button'); nextButton.textContent = 'Next Media'; nextButton.onclick = () => { currentIndex = (currentIndex + 1) % items.length; updateMediaFocus(); }; controlsContainer.appendChild(nextButton); updateMediaFocus(); } else if (fileInfo) { labelParts.unshift("File."); const fileNameElem = fileInfo.querySelector('.file-title'); const fileName = (fileNameElem && fileNameElem.textContent.trim()); const fileSizeElem = fileInfo.querySelector('.file-subtitle'); const fileSize = (fileSizeElem && fileSizeElem.textContent.trim()); if (fileName) labelParts.push(`File name: ${fileName}.`); if (fileSize) labelParts.push(`File size: ${fileSize}.`); const downloadTrigger = fileInfo.querySelector('.file-icon-container'); if (downloadTrigger) { overlay.onclick = (e) => { e.stopPropagation(); simulateUniversalClick(downloadTrigger); }; const clone = downloadTrigger.cloneNode(true); clone.removeAttribute('aria-hidden'); clone.setAttribute('tabindex', '0'); clone.setAttribute('role', 'button'); clone.setAttribute('aria-label', `Download ${fileName || 'file'}`); clone.style.position = 'relative'; clone.onclick = (e) => { e.stopPropagation(); simulateUniversalClick(downloadTrigger); }; controlsContainer.appendChild(clone); } } else { const hasVideoIndicator = message.querySelector('video, .message-media-duration, .icon-large-play'); const mediaContainer = message.querySelector('.media-inner'); const hasPhotoIndicator = mediaContainer && !hasVideoIndicator; if (hasVideoIndicator) labelParts.unshift("Video."); else if (hasPhotoIndicator) labelParts.unshift("Photo."); if (mediaContainer) { overlay.onclick = (e) => { e.stopPropagation(); const interactiveMedia = message.querySelector('.media-inner.interactive'); if (interactiveMedia) simulateUniversalClick(interactiveMedia); }; } } const timeElem = message.querySelector('.message-time'); if (timeElem) { let metaText = `Time: ${timeElem.textContent.trim()}.`; if (isOwnMessage) { const statusIcon = message.querySelector('.MessageOutgoingStatus i'); if (statusIcon) { if (statusIcon.classList.contains('icon-message-read')) metaText += ' Status: Read.'; else if (statusIcon.classList.contains('icon-message-succeeded')) metaText += ' Status: Sent, not yet seen.'; else if (statusIcon.classList.contains('icon-clock')) metaText += ' Status: Sending.'; else if (statusIcon.classList.contains('icon-error')) metaText += ' Status: Failed to send.'; } } labelParts.push(metaText); } const viewsElem = message.querySelector('.message-views'); if (viewsElem) labelParts.push(`${viewsElem.textContent.trim()} views.`); if (!album) { overlay.setAttribute('aria-label', labelParts.join(' \n')); } messageContentWrapper.style.position = 'relative'; messageContentWrapper.appendChild(overlay); if (controlsContainer.hasChildNodes()) message.appendChild(controlsContainer); const allInnerElements = messageContentWrapper.querySelectorAll('button, a, [role="button"], input, .Reactions, .sticker-image, .message-media-gif, .media-inner.is-document.is-animated'); allInnerElements.forEach(el => { if (el !== overlay && el.closest('.message-content-wrapper') === messageContentWrapper) { el.setAttribute('aria-hidden', 'true'); el.setAttribute('tabindex', '-1'); } }); overlay.removeAttribute('aria-hidden'); overlay.setAttribute('tabindex', '0'); message.setAttribute('data-accessible-message', 'true'); if (!isInitialPageLoad) { const messageList = document.querySelector('.MessageList.custom-scroll'); const allVisibleMessages = messageList ? Array.from(messageList.querySelectorAll('.Message:not(.service-message)')) : []; const latestMessage = allVisibleMessages[allVisibleMessages.length - 1]; if (latestMessage === message && labelParts.length > 0) { const announcementText = labelParts.join(' \n'); const messageId = message.id || message.dataset.messageId; if (messageId && messageId !== lastAnnouncedMessageId) { announceText(announcementText, true); lastAnnouncedMessageId = messageId; } else if (!messageId && announcementText !== unifiedLiveRegion.textContent) { announceText(announcementText, true); lastAnnouncedMessageId = null; } } } }); } // ========================================================================= // SECTION: Other UI Processors // ========================================================================= // NEW FUNCTION to handle confirmation dialogs like the delete message dialog function processConfirmDialog() { const dialog = document.querySelector('.ConfirmDialog'); if (!dialog || dialog.hasAttribute('data-accessible-dialog')) { return; } const titleElem = dialog.querySelector('h3'); dialog.setAttribute('role', 'dialog'); dialog.setAttribute('aria-modal', 'true'); if (titleElem && !dialog.hasAttribute('aria-labelledby')) { const titleId = 'dialog-title-' + Date.now(); titleElem.id = titleId; dialog.setAttribute('aria-labelledby', titleId); } const checkbox = dialog.querySelector('input[type="checkbox"]'); if (checkbox) { const label = checkbox.closest('label'); if (label && !checkbox.hasAttribute('aria-label')) { const labelText = label.textContent.trim(); if (labelText) { checkbox.setAttribute('aria-label', labelText); } } } dialog.setAttribute('data-accessible-dialog', 'true'); // Set focus to the first interactive element (checkbox or delete button) const elementToFocus = checkbox || dialog.querySelector('.Button.danger'); if (elementToFocus) { setTimeout(() => elementToFocus.focus(), 100); } } function processForwardList() { const forwardModal = document.querySelector('.Modal.ChatOrUserPicker'); if (!forwardModal) return; const items = forwardModal.querySelectorAll('.ChatOrUserPicker-item:not([data-accessible-forward-item="true"])'); items.forEach(item => { const nameElem = item.querySelector('.fullName'); const subtitleElem = item.querySelector('.OYmzCSp2'); if (!nameElem) return; const name = nameElem.textContent.trim(); const subtitle = subtitleElem ? subtitleElem.textContent.trim() : ''; const subtitleLower = subtitle.toLowerCase(); const avatar = item.querySelector('.Avatar'); let itemTypePrefix = ''; if (avatar && avatar.classList.contains('saved-messages')) itemTypePrefix = ''; else if (subtitleLower.includes('subscriber')) itemTypePrefix = `Channel: ${name}. `; else if (subtitleLower.includes('member') || (avatar && avatar.classList.contains('forum'))) itemTypePrefix = `Group: ${name}. `; else if (subtitleLower.includes('bot')) itemTypePrefix = `Bot: ${name}. `; else if (subtitleLower.includes('last seen') || subtitleLower.includes('online') || subtitleLower.includes('monthly users') || (avatar && avatar.classList.contains('private'))) itemTypePrefix = `Chat with ${name}. `; else if (subtitle) itemTypePrefix = `Group: ${name}. `; else itemTypePrefix = `Chat with ${name}. `; const newLabel = itemTypePrefix ? `${itemTypePrefix}${subtitle}` : `${name}. ${subtitle}`; item.setAttribute('aria-label', newLabel); item.setAttribute('data-accessible-forward-item', 'true'); }); } function processSearchResults() { const searchContainer = document.querySelector('.LeftSearch--content'); if (!searchContainer) return; const items = searchContainer.querySelectorAll('.ListItem.search-result .ListItem-button:not([data-accessible-search-item="true"])'); items.forEach(item => { const nameElem = item.querySelector('.fullName'); const statusElem = item.querySelector('.status'); if (!nameElem || !statusElem) return; const name = nameElem.textContent.trim(); const statusText = statusElem.textContent.trim(); const statusTextLower = statusText.toLowerCase(); let typePrefix = ''; if (statusTextLower.includes('subscriber')) typePrefix = `Channel: ${name}. `; else if (statusTextLower.includes('member')) typePrefix = `Group: ${name}. `; else if (statusTextLower.includes('bot')) typePrefix = `Bot: ${name}. `; else if (statusTextLower.includes('last seen') || statusTextLower.includes('online')) typePrefix = `User: ${name}. `; else typePrefix = `${name}. `; const ariaLabel = `${typePrefix}${statusText}`; item.setAttribute('aria-label', ariaLabel); item.setAttribute('data-accessible-search-item', 'true'); }); } // ========================================================================= // SECTION: Settings Accessibility // ... // (The function processSettings is unchanged) let settingsHeaderIdCounter = 0; function processSettings() { // Target only the currently visible settings page const activeSettingsContent = document.querySelector('#Settings .Transition_slide-active .settings-content'); if (!activeSettingsContent) return; // Group items with headers (like Data and Storage sections) const settingItems = activeSettingsContent.querySelectorAll('.settings-item:not([data-accessible-group="true"])'); settingItems.forEach(item => { const header = item.querySelector('h4.settings-item-header'); if (header) { const headerId = `settings-header-${settingsHeaderIdCounter++}`; header.id = headerId; item.setAttribute('role', 'group'); item.setAttribute('aria-labelledby', headerId); // Process checkboxes within this group item.querySelectorAll('label.Checkbox').forEach(label => { const input = label.querySelector('input[type="checkbox"]'); const span = label.querySelector('span.label'); if (input && span && !input.hasAttribute('aria-label')) { const fullLabel = `${header.textContent.trim()}: ${span.textContent.trim()}`; input.setAttribute('aria-label', fullLabel); } }); // Process range sliders within this group const rangeSlider = item.querySelector('.RangeSlider'); if (rangeSlider) { const rangeInput = rangeSlider.querySelector('input[type="range"]'); const rangeLabel = rangeSlider.querySelector('.slider-top-row .label'); const rangeValue = rangeSlider.querySelector('.slider-top-row .value'); if (rangeInput && rangeLabel && !rangeInput.hasAttribute('aria-label')) { rangeInput.setAttribute('aria-label', rangeLabel.textContent.trim()); } if (rangeInput && rangeValue) { rangeInput.setAttribute('aria-valuetext', rangeValue.textContent.trim()); } } } item.setAttribute('data-accessible-group', 'true'); }); // Process simple navigation list items in settings (main settings page) const simpleItems = activeSettingsContent.querySelectorAll('.ListItem:not([data-accessible-setting="true"]) .ListItem-button'); simpleItems.forEach(button => { // Only process if it doesn't have a checkbox inside, to avoid duplication. if (button.querySelector('input[type="checkbox"]')) return; const textContent = button.textContent.trim(); const currentValueElem = button.querySelector('.settings-item__current-value'); if (currentValueElem) { const currentValue = currentValueElem.textContent.trim(); const baseText = textContent.replace(currentValue, '').trim(); button.setAttribute('aria-label', `${baseText}: ${currentValue}`); } button.closest('.ListItem').setAttribute('data-accessible-setting', 'true'); }); } // ========================================================================= // Other utility functions are unchanged // ... function processUnreadDivider() { const unreadDividers = document.querySelectorAll('.unread-divider:not([role="heading"])'); unreadDividers.forEach(divider => { divider.setAttribute('role', 'heading'); divider.setAttribute('aria-level', '2'); divider.setAttribute('tabindex', '0'); }); } function cleanupExtraButtons() { const selectToolbar = document.querySelector('.MessageSelectToolbar'); if (selectToolbar && !selectToolbar.hasAttribute('data-hidden-by-script')) { selectToolbar.setAttribute('aria-hidden', 'true'); selectToolbar.setAttribute('data-hidden-by-script', 'true'); } const mentionButton = document.querySelector('button[aria-label="Go to next mention"]'); if (mentionButton) mentionButton.parentElement.setAttribute('aria-hidden', 'true'); const reactionButton = document.querySelector('button[aria-label="Go to next unread reactions"]'); if (reactionButton) reactionButton.parentElement.setAttribute('aria-hidden', 'true'); } function hideInvisibleElements() { const cloneInput = document.querySelector('.custom-scroll.input-scroller.clone'); if (cloneInput && !cloneInput.hasAttribute('aria-hidden')) { cloneInput.setAttribute('aria-hidden', 'true'); cloneInput.setAttribute('tabindex', '-1'); } const placeholderTextElem = document.querySelector('#message-input-text .placeholder-text'); if (placeholderTextElem && !placeholderTextElem.hasAttribute('aria-hidden')) { placeholderTextElem.setAttribute('aria-hidden', 'true'); placeholderTextElem.setAttribute('tabindex', '-1'); } } function addVideoViewerToggleListener() { document.body.addEventListener('click', function(event) { const mediaViewer = event.target.closest('#MediaViewer'); if (!mediaViewer) return; if (event.target.closest('.VideoPlayer') && !event.target.closest('.VideoPlayerControls')) { const video = mediaViewer.querySelector('video#media-viewer-video'); if (video) video.paused ? video.play() : video.pause(); } }); } function addGoToBottomListener() { document.body.addEventListener('click', function(event) { const goToBottomButton = event.target.closest('button[aria-label="Go to bottom"]'); if (!goToBottomButton) return; event.preventDefault(); event.stopPropagation(); const messageList = document.querySelector('.MessageList.custom-scroll'); if (messageList) { messageList.scrollTo({ top: messageList.scrollHeight, behavior: 'smooth' }); } }, true); } // ========================================================================= // SECTION: Main Observer // ========================================================================= function startObserver() { const targetNode = document.getElementById('root'); if (!targetNode) { setTimeout(startObserver, 500); return; } createUnifiedLiveRegion(); const observer = new MutationObserver((mutationsList) => { const processorsToRun = new Set(); for (const mutation of mutationsList) { if (!(mutation.target instanceof Element)) continue; if (mutation.target.closest('#LeftColumn')) { processorsToRun.add(processChatList); processorsToRun.add(addDelegatedChatListListeners); processorsToRun.add(processSearchResults); } if (mutation.target.closest('.MessageList')) { processorsToRun.add(processMessages); processorsToRun.add(processUnreadDivider); } if (mutation.target.closest('.Modal.ChatOrUserPicker')) { processorsToRun.add(processForwardList); } // MODIFIED: Added a check for the confirmation dialog if (mutation.target.closest('.ConfirmDialog')) { processorsToRun.add(processConfirmDialog); } if (mutation.target.closest('.ChatInfo')) { processorsToRun.add(monitorChatStatus); } if (mutation.target.closest('#Settings')) { processorsToRun.add(processSettings); } if (mutation.type === 'childList' && Array.from(mutation.addedNodes).some(node => node instanceof Element && (node.matches('.custom-scroll.input-scroller.clone') || node.matches('.placeholder-text')))) { processorsToRun.add(hideInvisibleElements); } } if (processorsToRun.size > 0) { if (window.accessibilityTimeout) clearTimeout(window.accessibilityTimeout); window.accessibilityTimeout = setTimeout(() => { processorsToRun.forEach(processor => processor()); cleanupExtraButtons(); hideInvisibleElements(); if (processorsToRun.has(processMessages)) { addDelegatedMessageListeners(); } }, 250); } }); // Added `attributes: true` to detect class changes for settings navigation. observer.observe(targetNode, { childList: true, subtree: true, attributes: true }); addVideoViewerToggleListener(); addGoToBottomListener(); // Initial run of all processors on page load. setTimeout(() => { processChatList(); addDelegatedChatListListeners(); isInitialPageLoad = true; processMessages(); addDelegatedMessageListeners(); processUnreadDivider(); isInitialPageLoad = false; cleanupExtraButtons(); monitorChatStatus(); hideInvisibleElements(); processSettings(); }, 1000); } window.addEventListener('load', startObserver); })();