WIP: эмодзи-пикер (Telegram-набор) — чекпоинт незавершённой работы

- Эмодзи-пикер: js/components/emoji-picker.js + каталог telegram-emoji-*.js
  (smileys/people/animals_nature/food_drink/travel_places/activity/objects/
  symbols/flags) + 631 ассет в assets/emoji (7.2 МБ).
- Интеграция пикера: chat-view.js, app.js, index.html, styles/components.css.
- Дев-скрипты генерации превью + THIRD_PARTY_NOTICES.
- Работа НЕ завершена (WIP), сохранено как чекпоинт. С origin (+56) пока НЕ
  мержится — большой мерж будет после завершения, отдельным шагом.
- VERSION: client 1.2.263.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pixel
2026-07-15 21:28:45 +03:00
co-authored by Claude Opus 4.8
parent 8c4997ee2e
commit 266a74ef79
650 changed files with 3333 additions and 13 deletions
+89 -2
View File
@@ -17,6 +17,13 @@ import {
} from '../state.js';
import { startOutgoingCall } from '../services/call-service.js';
import { openSpeechInputModal } from '../components/speech-input-modal.js';
import {
createEmojiPicker,
createTelegramAnimatedEmoji,
appendTelegramAnimatedText,
isTelegramAnimatedEmoji,
stopAllTelegramEmojiAnimations,
} from '../components/emoji-picker.js?v=202607152130';
import { isTextToSpeechConfigured, speakTextBySettings } from '../services/speech-tools-service.js';
import { showToast } from '../services/channels-ux.js';
@@ -275,6 +282,19 @@ function scrollToLatestMessage(list) {
window.setTimeout(apply, 260);
}
function renderMessageText(textNode, messageText) {
const text = String(messageText || '');
const emoji = text.trim();
if (text === emoji && isTelegramAnimatedEmoji(emoji)) {
textNode.classList.add('bubble-text--animated-emoji');
textNode.append(createTelegramAnimatedEmoji(emoji, { className: 'chat-telegram-emoji' }));
return;
}
if (appendTelegramAnimatedText(textNode, text, { className: 'chat-telegram-emoji chat-telegram-emoji--inline' })) {
textNode.classList.add('bubble-text--telegram-emoji-inline');
}
}
function renderLog(list, chatId, { onOpenActions } = {}) {
list.innerHTML = '';
const messages = getChatMessages(chatId);
@@ -296,7 +316,7 @@ function renderLog(list, chatId, { onOpenActions } = {}) {
const textNode = document.createElement('div');
textNode.className = 'bubble-text';
textNode.textContent = msg.text || '';
renderMessageText(textNode, msg.text);
bubble.append(textNode);
const metaNode = document.createElement('div');
@@ -429,18 +449,61 @@ export function render({ navigate, route }) {
<div class="dm-edit-banner__text" id="chat-edit-banner-text"></div>
<button class="ghost-btn dm-edit-banner__close" type="button" id="chat-edit-cancel" title="Отменить редактирование">✕</button>
</div>
<div class="emoji-picker-slot" id="chat-emoji-picker-slot" hidden></div>
<textarea class="input dm-input" name="message" rows="1" placeholder="Введите сообщение" maxlength="12000"></textarea>
<div class="dm-actions-col">
<button class="ghost-btn dm-emoji-btn" type="button" id="chat-emoji-toggle" aria-label="Эмодзи" title="Эмодзи">☺</button>
<button class="ghost-btn dm-voice-btn" type="button" id="chat-voice-input" title="Голосовой ввод">🎤</button>
<button class="primary-btn dm-send-btn dm-send-icon-btn" type="submit" title="Отправить">➤</button>
</div>
`;
const input = form.elements.message;
const emojiSlot = form.querySelector('#chat-emoji-picker-slot');
const emojiToggle = form.querySelector('#chat-emoji-toggle');
const editBanner = form.querySelector('#chat-edit-banner');
const editBannerText = form.querySelector('#chat-edit-banner-text');
const editCancelBtn = form.querySelector('#chat-edit-cancel');
let activeEdit = null;
let emojiPickerOpen = false;
let emojiSelection = null;
const rememberEmojiSelection = () => {
if (!input) return;
const valueLength = String(input.value || '').length;
const start = Math.max(0, Math.min(valueLength, Number(input.selectionStart ?? valueLength)));
const end = Math.max(start, Math.min(valueLength, Number(input.selectionEnd ?? start)));
emojiSelection = { start, end };
};
const closeEmojiPicker = () => {
if (!emojiPickerOpen) return;
emojiPickerOpen = false;
stopAllTelegramEmojiAnimations();
emojiSlot.hidden = true;
emojiToggle?.setAttribute('aria-expanded', 'false');
};
const insertEmoji = (emoji) => {
if (!input) return;
const value = String(input.value || '');
const start = emojiSelection?.start ?? Number(input.selectionStart ?? value.length);
const end = emojiSelection?.end ?? Number(input.selectionEnd ?? value.length);
input.value = `${value.slice(0, start)}${emoji}${value.slice(end)}`;
const nextPosition = start + emoji.length;
emojiSelection = { start: nextPosition, end: nextPosition };
input.focus();
try {
input.setSelectionRange(nextPosition, nextPosition);
} catch {
// Ignore browsers that do not expose textarea selection during focus changes.
}
autoResizeComposer(input);
};
if (emojiSlot) {
emojiSlot.append(createEmojiPicker({ onSelect: insertEmoji }));
}
const syncEditBanner = () => {
if (!editBanner || !editBannerText) return;
@@ -646,10 +709,31 @@ export function render({ navigate, route }) {
});
autoResizeComposer(input);
input?.addEventListener('input', () => autoResizeComposer(input));
input?.addEventListener('input', () => {
rememberEmojiSelection();
autoResizeComposer(input);
});
input?.addEventListener('select', rememberEmojiSelection);
input?.addEventListener('keyup', rememberEmojiSelection);
input?.addEventListener('click', rememberEmojiSelection);
input?.addEventListener('focus', () => {
rememberEmojiSelection();
scrollToLatestMessage(log);
});
emojiToggle?.setAttribute('aria-expanded', 'false');
emojiToggle?.addEventListener('pointerdown', (event) => {
rememberEmojiSelection();
event.preventDefault();
});
emojiToggle?.addEventListener('click', () => {
emojiPickerOpen = !emojiPickerOpen;
emojiSlot.hidden = !emojiPickerOpen;
emojiToggle.setAttribute('aria-expanded', String(emojiPickerOpen));
});
screen.addEventListener('pointerdown', (event) => {
if (!emojiPickerOpen || form.contains(event.target)) return;
closeEmojiPicker();
});
input?.addEventListener('keydown', async (event) => {
if (event.key !== 'Enter') return;
if (event.ctrlKey) {
@@ -672,10 +756,12 @@ export function render({ navigate, route }) {
if (!text) return;
input.value = '';
autoResizeComposer(input);
closeEmojiPicker();
await sendTextMessage(text);
});
form.querySelector('#chat-voice-input')?.addEventListener('click', async () => {
closeEmojiPicker();
await openSpeechInputModal({
navigate,
onTextReady: (text) => {
@@ -696,6 +782,7 @@ export function render({ navigate, route }) {
if (!text) return;
input.value = '';
autoResizeComposer(input);
closeEmojiPicker();
await sendTextMessage(text);
});