Голосовой ввод
Идёт запись...
00:00
-
`;
+ root.append(host);
- const statusEl = root.querySelector('#speech-input-status');
- const timeEl = root.querySelector('#speech-input-time');
- const levelEl = root.querySelector('#speech-level-fill');
- const errorEl = root.querySelector('#speech-input-error');
- const cancelBtn = root.querySelector('#speech-cancel');
- const okBtn = root.querySelector('#speech-ok');
+ const statusEl = host.querySelector('#speech-input-status');
+ const timeEl = host.querySelector('#speech-input-time');
+ const levelEl = host.querySelector('#speech-level-fill');
+ const errorEl = host.querySelector('#speech-input-error');
+ const cancelBtn = host.querySelector('#speech-cancel');
+ const sendNowBtn = host.querySelector('#speech-send-now');
+ const okBtn = host.querySelector('#speech-ok');
const recorder = createMicrophoneRecorder();
let closed = false;
let busy = false;
@@ -55,14 +59,16 @@ export async function openSpeechInputModal({ navigate, onTextReady }) {
const close = () => {
if (closed) return;
closed = true;
- root.innerHTML = '';
+ host.remove();
};
const setBusy = (flag) => {
busy = !!flag;
cancelBtn.disabled = busy;
+ sendNowBtn.disabled = busy;
okBtn.disabled = busy;
okBtn.textContent = busy ? 'Распознаю...' : 'OK';
+ sendNowBtn.textContent = busy ? 'Распознаю...' : 'Распознать и сразу отправить сообщение';
};
try {
@@ -84,10 +90,16 @@ export async function openSpeechInputModal({ navigate, onTextReady }) {
okBtn.addEventListener('click', async () => {
if (busy) return;
setBusy(true);
- errorEl.textContent = '';
- statusEl.textContent = 'Распознаю речь...';
try {
const audioBlob = await recorder.stop();
+ host.innerHTML = `
+
+ `;
const text = await transcribeAudioBySettings(audioBlob, state.entrySettings);
if (typeof onTextReady === 'function') onTextReady(text);
close();
@@ -97,4 +109,24 @@ export async function openSpeechInputModal({ navigate, onTextReady }) {
errorEl.textContent = `Ошибка распознавания: ${error?.message || 'unknown'}`;
}
});
+
+ sendNowBtn.addEventListener('click', async () => {
+ if (busy) return;
+ setBusy(true);
+ try {
+ const audioBlob = await recorder.stop();
+ close();
+ if (typeof onSendQueued === 'function') onSendQueued();
+ const text = await transcribeAudioBySettings(audioBlob, state.entrySettings);
+ if (typeof onSendText === 'function') {
+ await onSendText(text);
+ } else if (typeof onTextReady === 'function') {
+ onTextReady(text);
+ }
+ } catch (error) {
+ setBusy(false);
+ close();
+ window.alert(`Ошибка распознавания: ${error?.message || 'unknown'}`);
+ }
+ });
}
diff --git a/shine-UI/js/pages/channel-thread-view.js b/shine-UI/js/pages/channel-thread-view.js
index c81ed54..74f2f46 100644
--- a/shine-UI/js/pages/channel-thread-view.js
+++ b/shine-UI/js/pages/channel-thread-view.js
@@ -207,6 +207,27 @@ function buildThreadRouteFromTarget(target, selector) {
].join('/');
}
+function buildChannelRouteFromThread(selector, resolvedChannelLabel = '') {
+ if (selector?.short?.ownerBlockchainName && selector?.short?.channelName) {
+ return [
+ 'channel',
+ encodeRoutePart(selector.short.ownerBlockchainName),
+ encodeRoutePart(selector.short.channelName),
+ ].join('/');
+ }
+
+ const ownerBch = String(selector?.channel?.ownerBlockchainName || '').trim();
+ const label = String(resolvedChannelLabel || '').trim();
+ const slashIndex = label.indexOf('/');
+ const channelName = slashIndex >= 0 ? label.slice(slashIndex + 1).trim() : '';
+ if (!ownerBch || !channelName) return '';
+ return [
+ 'channel',
+ encodeRoutePart(ownerBch),
+ encodeRoutePart(channelName),
+ ].join('/');
+}
+
function buildTargetFromNode(node) {
const blockchainName = String(node?.authorBlockchainName || '').trim();
const blockNumber = Number(node?.messageRef?.blockNumber);
@@ -315,6 +336,7 @@ function openReplyModal({ onSubmit, navigate }) {
function renderNodeCard(node, heading, handlers, localNumber) {
const card = document.createElement('article');
card.className = 'card stack thread-node-card channel-message-card';
+ card.classList.add('is-counters-visible');
const author = node?.authorLogin || 'автор';
const text = resolveNodeText(node) || '(пусто)';
@@ -532,9 +554,16 @@ export function render({ navigate, route }) {
const appScreen = document.getElementById('app-screen');
appScreen?.classList.add('channels-scroll-clean');
- const channelIndicator = document.createElement('div');
- channelIndicator.className = 'card channels-user-chip';
- channelIndicator.textContent = `Канал: ${channelDisplayName || selector?.channel?.ownerBlockchainName || 'неизвестно'}`;
+ const header = renderHeader({
+ title: '',
+ leftAction: { label: '<', onClick: () => navigateBack() },
+ rightActions: [{ label: 'Тред в канале: ...', onClick: () => {} }],
+ });
+ const threadHeaderButton = header.querySelector('.header-actions .icon-btn');
+ if (threadHeaderButton) {
+ threadHeaderButton.classList.add('channel-header-route-btn');
+ threadHeaderButton.disabled = true;
+ }
const statusBox = document.createElement('div');
statusBox.className = 'card status-line is-unavailable channels-status';
@@ -643,13 +672,7 @@ export function render({ navigate, route }) {
},
};
- screen.append(
- renderHeader({
- title: 'Тред',
- leftAction: { label: '<', onClick: () => navigateBack() },
- }),
- );
- screen.append(channelIndicator, statusBox);
+ screen.append(header, statusBox);
if (!selector) {
const invalid = document.createElement('div');
@@ -758,7 +781,18 @@ export function render({ navigate, route }) {
resolvedChannelLabel = await resolveChannelDisplayNameFromServer(selector.channel);
}
const fallbackChannel = String(selector?.channel?.ownerBlockchainName || '').trim() || 'неизвестно';
- channelIndicator.textContent = `Канал: ${resolvedChannelLabel || fallbackChannel}`;
+ const resolvedChannelTitle = resolvedChannelLabel || fallbackChannel;
+ if (threadHeaderButton) {
+ threadHeaderButton.textContent = `Тред в канале: ${resolvedChannelTitle}`;
+ threadHeaderButton.disabled = false;
+ threadHeaderButton.onclick = (event) => {
+ event.preventDefault();
+ animatePress(event.currentTarget);
+ const routeToChannel = buildChannelRouteFromThread(selector, resolvedChannelLabel);
+ if (routeToChannel) navigate(routeToChannel);
+ else navigate('channels-list');
+ };
+ }
let seq = 0;
const nextNumber = () => {
@@ -766,8 +800,9 @@ export function render({ navigate, route }) {
return seq;
};
+ let ancestorsWrap = null;
if (ancestors.length) {
- const ancestorsWrap = document.createElement('div');
+ ancestorsWrap = document.createElement('div');
ancestorsWrap.className = 'stack thread-block thread-block--ancestors';
const title = document.createElement('h3');
title.className = 'section-title';
@@ -776,18 +811,17 @@ export function render({ navigate, route }) {
ancestors.forEach((node, index) => {
ancestorsWrap.append(renderNodeCard(node, `Предок ${index + 1}`, handlers, nextNumber()));
});
- screen.append(ancestorsWrap);
}
+ let focusWrap = null;
if (focus) {
- const focusWrap = document.createElement('div');
+ focusWrap = document.createElement('div');
focusWrap.className = 'stack thread-block thread-block--focus';
const focusTitle = document.createElement('h3');
focusTitle.className = 'section-title';
focusTitle.textContent = 'Текущее сообщение';
focusWrap.append(focusTitle);
focusWrap.append(renderNodeCard(focus, '', handlers, nextNumber()));
- screen.append(focusWrap);
}
const descendantsWrap = document.createElement('div');
@@ -806,8 +840,23 @@ export function render({ navigate, route }) {
descendantsWrap.append(empty);
}
+ if (ancestorsWrap) {
+ screen.append(ancestorsWrap);
+ const divider = document.createElement('div');
+ divider.className = 'thread-history-divider';
+ screen.append(divider);
+ }
+
+ if (focusWrap) screen.append(focusWrap);
screen.append(descendantsWrap);
+
applyPendingScroll(screen, routeKey);
+ const hasPendingScroll = pendingThreadScroll.has(routeKey);
+ if (!hasPendingScroll && focusWrap) {
+ setTimeout(() => {
+ focusWrap.scrollIntoView({ behavior: 'auto', block: 'start' });
+ }, 20);
+ }
} catch (error) {
skeleton.remove();
const failed = document.createElement('div');
diff --git a/shine-UI/js/pages/channel-view.js b/shine-UI/js/pages/channel-view.js
index 58b0364..b6996d4 100644
--- a/shine-UI/js/pages/channel-view.js
+++ b/shine-UI/js/pages/channel-view.js
@@ -871,7 +871,7 @@ export function render({ navigate, route }) {
const header = renderHeader({
title: '',
leftAction: { label: '<', onClick: () => navigateBack() },
- rightActions: [{ label: 'Канал', onClick: () => {} }],
+ rightActions: [{ label: 'Канал: ...', onClick: () => {} }],
});
const channelHeaderButton = header.querySelector('.header-actions .icon-btn');
if (channelHeaderButton) {
@@ -987,7 +987,7 @@ export function render({ navigate, route }) {
try {
const apiData = await loadFromApi(route, channelId);
activeSelector = apiData?.selector || null;
- const channelRouteLabel = `${apiData?.channel?.ownerName || 'owner'}/${apiData?.channel?.name || 'channel'}`;
+ const channelRouteLabel = `Канал: ${apiData?.channel?.ownerName || 'owner'}/${apiData?.channel?.name || 'channel'}`;
if (channelHeaderButton) {
channelHeaderButton.textContent = channelRouteLabel;
channelHeaderButton.disabled = false;
diff --git a/shine-UI/js/pages/chat-view.js b/shine-UI/js/pages/chat-view.js
index 8ab1b20..e5bc3a8 100644
--- a/shine-UI/js/pages/chat-view.js
+++ b/shine-UI/js/pages/chat-view.js
@@ -10,14 +10,108 @@ import {
markOutgoingSent,
markReadReceiptSentByBaseKey,
authService,
+ setContacts,
state,
} from '../state.js';
import { startOutgoingCall } from '../services/call-service.js';
import { openSpeechInputModal } from '../components/speech-input-modal.js';
import { isTextToSpeechConfigured, speakTextBySettings } from '../services/speech-tools-service.js';
+import { showToast } from '../services/channels-ux.js';
export const pageMeta = { id: 'chat-view', title: 'Чат' };
+function openMessageActionsModal({ messageText = '', onReadAloud }) {
+ const root = document.getElementById('modal-root');
+ if (!root) return;
+ root.innerHTML = `
+
+
+
+ `;
+
+ const close = () => {
+ root.innerHTML = '';
+ };
+
+ root.querySelector('#chat-message-actions-modal-overlay')?.addEventListener('click', (event) => {
+ if (event.target?.id === 'chat-message-actions-modal-overlay') close();
+ });
+ root.querySelector('#msg-action-copy')?.addEventListener('click', async () => {
+ try {
+ if (navigator?.clipboard?.writeText) {
+ await navigator.clipboard.writeText(String(messageText || ''));
+ }
+ showToast('Сообщение скопированно', { timeoutMs: 1000 });
+ } catch {
+ showToast('Не удалось скопировать сообщение', { kind: 'error', timeoutMs: 1200 });
+ } finally {
+ close();
+ }
+ });
+ root.querySelector('#msg-action-read')?.addEventListener('click', async () => {
+ close();
+ if (typeof onReadAloud === 'function') await onReadAloud();
+ });
+}
+
+function showTtsMissingConfigDialog(navigate) {
+ const root = document.getElementById('modal-root');
+ if (!root) return;
+ root.innerHTML = `
+
+
+
Озвучка не настроена
+
Перейти в настройки инструментов?
+
+ Нет
+ Да
+
+
+
+ `;
+ const close = () => { root.innerHTML = ''; };
+ root.querySelector('#chat-tts-no')?.addEventListener('click', close);
+ root.querySelector('#chat-tts-yes')?.addEventListener('click', () => {
+ close();
+ navigate('tools-settings-view');
+ });
+}
+
+function autoResizeComposer(textarea) {
+ if (!textarea) return;
+ textarea.style.height = 'auto';
+ textarea.style.height = `${Math.min(180, Math.max(42, textarea.scrollHeight))}px`;
+}
+
+function openConfirmContactModal(targetLogin = '') {
+ const root = document.getElementById('modal-root');
+ if (!root) return Promise.resolve(false);
+ return new Promise((resolve) => {
+ root.innerHTML = `
+
+ `;
+
+ const close = (answer) => {
+ root.innerHTML = '';
+ resolve(!!answer);
+ };
+ root.querySelector('#contact-confirm-no')?.addEventListener('click', () => close(false));
+ root.querySelector('#contact-confirm-yes')?.addEventListener('click', () => close(true));
+ });
+}
+
function parseBaseKey(baseKey) {
const raw = String(baseKey || '').trim();
const parts = raw.split('|');
@@ -82,7 +176,7 @@ function scrollToLatestMessage(list) {
window.setTimeout(apply, 120);
}
-function renderLog(list, chatId) {
+function renderLog(list, chatId, { onOpenActions } = {}) {
list.innerHTML = '';
const messages = getChatMessages(chatId);
let unreadSeparatorInserted = false;
@@ -122,6 +216,9 @@ function renderLog(list, chatId) {
}
bubble.append(textNode, metaNode);
+ bubble.addEventListener('click', () => {
+ if (typeof onOpenActions === 'function') onOpenActions(msg);
+ });
list.append(bubble);
});
scrollToLatestMessage(list);
@@ -142,20 +239,42 @@ export function render({ navigate, route }) {
screen.append(
renderHeader({
- title: `Чат: ${contact.name}`,
+ title: `Чат с ${contact.name}`,
leftAction: { label: '←', onClick: () => navigate('messages-list') },
rightActions: [{
label: 'Позвонить',
onClick: async () => {
try {
await startOutgoingCall(chatId);
- renderLog(log, chatId);
+ renderLog(log, chatId, {
+ onOpenActions: (msg) => openMessageActionsModal({
+ messageText: msg?.text || '',
+ onReadAloud: async () => {
+ if (!isTextToSpeechConfigured(state.entrySettings)) {
+ showTtsMissingConfigDialog(navigate);
+ return;
+ }
+ await speakTextBySettings(String(msg?.text || ''), state.entrySettings);
+ },
+ }),
+ });
} catch (e) {
addSystemChatMessage(chatId, `[Звонок] Ошибка запуска: ${e.message || 'unknown'}`, {
from: 'out',
kind: 'call-tech',
});
- renderLog(log, chatId);
+ renderLog(log, chatId, {
+ onOpenActions: (msg) => openMessageActionsModal({
+ messageText: msg?.text || '',
+ onReadAloud: async () => {
+ if (!isTextToSpeechConfigured(state.entrySettings)) {
+ showTtsMissingConfigDialog(navigate);
+ return;
+ }
+ await speakTextBySettings(String(msg?.text || ''), state.entrySettings);
+ },
+ }),
+ });
}
},
}],
@@ -168,18 +287,26 @@ export function render({ navigate, route }) {
const btn = document.createElement('button');
btn.className = 'secondary-btn';
btn.type = 'button';
- btn.textContent = 'Добавить в контакты';
+ btn.textContent = 'Добавить собеседника в контакты';
btn.addEventListener('click', async () => {
try {
- await authService.addCloseFriend(chatId);
- state.contacts = [...new Set([...(state.contacts || []), chatId])];
+ const approved = await openConfirmContactModal(chatId);
+ if (!approved) return;
+ await authService.setUserRelation({
+ login: state.session.login,
+ toLogin: chatId,
+ kind: 'contact',
+ enabled: true,
+ storagePwd: state.session.storagePwdInMemory,
+ });
+ const contactsPayload = await authService.listContacts();
+ setContacts(contactsPayload?.contacts || []);
addAppLogEntry({
level: 'info',
source: 'contacts',
message: `Пользователь ${chatId} добавлен в контакты`,
});
- btn.disabled = true;
- btn.textContent = 'Добавлено';
+ card.remove();
} catch (e) {
addAppLogEntry({
level: 'warn',
@@ -202,51 +329,29 @@ export function render({ navigate, route }) {
const form = document.createElement('form');
form.className = 'chat-input dm-chat-input';
form.innerHTML = `
-
-
🎤
-
🔊
-
Отправить
+
+
+ 🎤
+ ➤
+
`;
- form.querySelector('#chat-voice-input')?.addEventListener('click', async () => {
- const input = form.elements.message;
- await openSpeechInputModal({
- navigate,
- onTextReady: (text) => {
- const prev = String(input.value || '').trim();
- input.value = prev ? `${prev} ${text}` : text;
- },
- });
- });
-
- form.querySelector('#chat-read-aloud')?.addEventListener('click', async () => {
- const input = form.elements.message;
- const text = String(input.value || '').trim();
- if (!text) {
- window.alert('Введите текст для озвучки.');
- return;
- }
- if (!isTextToSpeechConfigured(state.entrySettings)) {
- const goSettings = window.confirm('Озвучка не настроена. Перейти в настройки инструментов?');
- if (goSettings) navigate('tools-settings-view');
- return;
- }
- try {
- await speakTextBySettings(text, state.entrySettings);
- } catch (error) {
- window.alert(`Ошибка озвучки: ${error?.message || 'unknown'}`);
- }
- });
-
- form.addEventListener('submit', async (event) => {
- event.preventDefault();
- const input = form.elements.message;
- const text = input.value.trim();
+ const sendTextMessage = async (rawText) => {
+ const text = String(rawText || '').trim();
if (!text) return;
-
const tempId = addOutgoingPendingMessage(chatId, text);
- input.value = '';
- renderLog(log, chatId);
+ renderLog(log, chatId, {
+ onOpenActions: (msg) => openMessageActionsModal({
+ messageText: msg?.text || '',
+ onReadAloud: async () => {
+ if (!isTextToSpeechConfigured(state.entrySettings)) {
+ showTtsMissingConfigDialog(navigate);
+ return;
+ }
+ await speakTextBySettings(String(msg?.text || ''), state.entrySettings);
+ },
+ }),
+ });
try {
const result = await authService.sendDirectMessage({
@@ -259,7 +364,18 @@ export function render({ navigate, route }) {
messageKey: result?.outgoingKey || '',
baseKey: result?.baseKey || result?.localBaseKey || '',
});
- renderLog(log, chatId);
+ renderLog(log, chatId, {
+ onOpenActions: (msg) => openMessageActionsModal({
+ messageText: msg?.text || '',
+ onReadAloud: async () => {
+ if (!isTextToSpeechConfigured(state.entrySettings)) {
+ showTtsMissingConfigDialog(navigate);
+ return;
+ }
+ await speakTextBySettings(String(msg?.text || ''), state.entrySettings);
+ },
+ }),
+ });
addAppLogEntry({
level: 'info',
source: 'outgoing-dm',
@@ -282,13 +398,63 @@ export function render({ navigate, route }) {
error: e?.message || 'unknown',
},
});
- renderLog(log, chatId);
+ renderLog(log, chatId, {
+ onOpenActions: (msg) => openMessageActionsModal({
+ messageText: msg?.text || '',
+ onReadAloud: async () => {
+ if (!isTextToSpeechConfigured(state.entrySettings)) {
+ showTtsMissingConfigDialog(navigate);
+ return;
+ }
+ await speakTextBySettings(String(msg?.text || ''), state.entrySettings);
+ },
+ }),
+ });
}
+ };
+
+ const input = form.elements.message;
+ autoResizeComposer(input);
+ input?.addEventListener('input', () => autoResizeComposer(input));
+
+ form.querySelector('#chat-voice-input')?.addEventListener('click', async () => {
+ await openSpeechInputModal({
+ navigate,
+ onTextReady: (text) => {
+ const prev = String(input.value || '').trim();
+ input.value = prev ? `${prev} ${text}` : text;
+ autoResizeComposer(input);
+ },
+ onSendText: async (text) => sendTextMessage(text),
+ onSendQueued: () => {
+ showToast('Сообщение будет отправлено автоматически после распознавания', { timeoutMs: 1000 });
+ },
+ });
+ });
+
+ form.addEventListener('submit', async (event) => {
+ event.preventDefault();
+ const text = input.value.trim();
+ if (!text) return;
+ input.value = '';
+ autoResizeComposer(input);
+ await sendTextMessage(text);
});
wrap.append(log, form);
screen.append(wrap);
- renderLog(log, chatId);
+ renderLog(log, chatId, {
+ onOpenActions: (msg) => openMessageActionsModal({
+ messageText: msg?.text || '',
+ onReadAloud: async () => {
+ if (!isTextToSpeechConfigured(state.entrySettings)) {
+ showTtsMissingConfigDialog(navigate);
+ return;
+ }
+ await speakTextBySettings(String(msg?.text || ''), state.entrySettings);
+ },
+ }),
+ });
window.requestAnimationFrame(() => scrollToLatestMessage(log));
void sendReadReceiptsForVisible(chatId);
return screen;
diff --git a/shine-UI/js/pages/profile-view.js b/shine-UI/js/pages/profile-view.js
index 91d61f3..4204d46 100644
--- a/shine-UI/js/pages/profile-view.js
+++ b/shine-UI/js/pages/profile-view.js
@@ -38,7 +38,7 @@ export function render({ navigate }) {
const topActions = document.createElement('div');
topActions.className = 'profile-top-actions';
topActions.innerHTML = `
-
Изменить профиль
+
Редактировать профиль
Кошелёк
Настройки
`;
@@ -61,13 +61,6 @@ export function render({ navigate }) {
`;
- const statusRow = document.createElement('div');
- statusRow.className = 'row profile-status-row';
- statusRow.innerHTML = `
-
Загрузка параметров...
- `;
-
const badgesRow = document.createElement('div');
badgesRow.className = 'row';
badgesRow.innerHTML = `
@@ -78,8 +71,6 @@ export function render({ navigate }) {
const listWrap = document.createElement('div');
listWrap.className = 'stack profile-param-list';
- const reloadBtn = statusRow.querySelector('[data-reload="true"]');
- const statusLineEl = statusRow.querySelector('[data-profile-status-line="true"]');
const officialBtn = badgesRow.querySelector('[data-toggle="official"]');
const shineBtn = badgesRow.querySelector('[data-toggle="shine"]');
const identityEl = topRow.querySelector('[data-profile-identity="true"]');
@@ -155,10 +146,6 @@ export function render({ navigate }) {
async function refreshProfileSnapshot() {
try {
- if (statusLineEl instanceof HTMLElement) {
- statusLineEl.className = 'status-line';
- statusLineEl.textContent = 'Загрузка параметров...';
- }
const snapshot = await loadProfileSnapshot(login);
currentFields = Array.isArray(snapshot.fields) ? snapshot.fields : [];
currentToggles = Array.isArray(snapshot.toggles) ? snapshot.toggles : [];
@@ -168,39 +155,12 @@ export function render({ navigate }) {
updateAvatarUi();
updateTogglesUi();
renderFields(currentFields);
- if (statusLineEl instanceof HTMLElement) {
- statusLineEl.className = 'status-line is-available';
- statusLineEl.textContent = 'Профиль обновлён.';
- }
} catch (error) {
- if (statusLineEl instanceof HTMLElement) {
- statusLineEl.className = 'status-line is-unavailable';
- statusLineEl.textContent = `Ошибка загрузки профиля: ${error?.message || 'unknown'}`;
- }
+ // ignore status row in profile-view
}
}
- const showToggleInfo = (toggleKey) => {
- const item = currentToggles.find((entry) => entry.key === toggleKey);
- const isEnabled = Boolean(item?.enabled);
- if (toggleKey === 'official') {
- if (statusLineEl instanceof HTMLElement) statusLineEl.className = 'status-line is-available';
- if (statusLineEl instanceof HTMLElement) statusLineEl.textContent = isEnabled
- ? 'Аккаунт является официальным.'
- : 'Аккаунт не является официальным.';
- return;
- }
- if (statusLineEl instanceof HTMLElement) statusLineEl.className = 'status-line is-available';
- if (statusLineEl instanceof HTMLElement) statusLineEl.textContent = isEnabled
- ? 'Аккаунт является сияющим.'
- : 'Аккаунт не является сияющим.';
- };
-
- reloadBtn?.addEventListener('click', refreshProfileSnapshot);
- officialBtn?.addEventListener('click', () => showToggleInfo('official'));
- shineBtn?.addEventListener('click', () => showToggleInfo('shine'));
-
- card.append(topRow, badgesRow, listWrap, statusRow);
+ card.append(topRow, badgesRow, listWrap);
screen.append(card);
updateAvatarUi();
diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css
index 2208573..13b8467 100644
--- a/shine-UI/styles/components.css
+++ b/shine-UI/styles/components.css
@@ -2353,7 +2353,7 @@ textarea.input {
position: absolute;
left: 50%;
top: 50%;
- transform: translate(-50%, -66%);
+ transform: translate(-50%, -66%) !important;
max-width: 72vw;
overflow: hidden;
text-overflow: ellipsis;
@@ -2366,7 +2366,12 @@ textarea.input {
.channels-screen .page-header .channel-header-route-btn:hover,
.channels-screen .page-header .channel-header-route-btn:focus-visible {
- transform: translate(-50%, -66%);
+ transform: translate(-50%, -66%) !important;
+}
+
+.channels-screen .page-header .channel-header-route-btn:active,
+.channels-screen .page-header .channel-header-route-btn.is-springing {
+ transform: translate(-50%, -66%) !important;
}
.thread-node-heading {
@@ -3458,7 +3463,8 @@ textarea.input {
.dm-chat-input {
gap: 10px;
- grid-template-columns: 1fr auto auto auto;
+ grid-template-columns: 1fr auto;
+ align-items: end;
}
.dm-voice-btn {
@@ -3466,6 +3472,21 @@ textarea.input {
padding: 0 10px;
}
+.dm-actions-col {
+ display: grid;
+ grid-template-rows: auto auto;
+ gap: 6px;
+}
+
+.dm-chat-input .dm-input {
+ min-height: 42px;
+ max-height: 180px;
+ resize: none;
+ overflow-y: auto;
+ line-height: 1.35;
+ padding: 10px 12px;
+}
+
.voice-level-wrap {
width: 100%;
height: 8px;
@@ -3503,6 +3524,35 @@ textarea.input {
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
}
+.dm-send-icon-btn {
+ min-width: 42px;
+ width: 42px;
+ padding: 0;
+ font-size: 15px;
+ line-height: 1;
+}
+
+.dm-message-actions-menu {
+ width: min(52vw, 240px);
+ padding: 8px;
+ gap: 6px;
+}
+
+.dm-message-action-btn {
+ width: 100%;
+ justify-content: flex-start;
+}
+
+.speech-actions-top {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 8px;
+}
+
+.speech-send-now-btn {
+ width: 100%;
+}
+
/* DM messages-list status + empty block as full glass buttons */
.dm-screen .dm-status-line {
display: block;