SHA256
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:
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user