Доработать оглавление канала и отображение дневника

This commit is contained in:
AidarKC
2026-08-10 03:05:23 +04:00
parent 67d63256c2
commit 37e8ea27d0
5 changed files with 260 additions and 18 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
client.version=1.5.11 client.version=1.5.12
server.version=1.4.9 server.version=1.4.9
+155 -13
View File
@@ -53,7 +53,7 @@ const MSG_SUBTYPE_STATUS_IN_STUDY = 130;
const MSG_SUBTYPE_STATUS_ABANDONED = 140; const MSG_SUBTYPE_STATUS_ABANDONED = 140;
const MSG_SUBTYPE_STATUS_COMPLETED = 150; const MSG_SUBTYPE_STATUS_COMPLETED = 150;
const DIARY_CHANNEL_NAME = 'diary'; const DIARY_CHANNEL_NAME = 'diary';
const DIARY_CHANNEL_DISPLAY_NAME = 'Личный дневник'; const DIARY_CHANNEL_DISPLAY_NAME = 'Дневник';
const pendingReactionActions = new Set(); const pendingReactionActions = new Set();
const pendingScrollByRoute = new Map(); const pendingScrollByRoute = new Map();
@@ -319,6 +319,10 @@ function getChannelMessageTypeMeta(msgSubType) {
} }
} }
function isEntrypointSubType(msgSubType) {
return Number(msgSubType || 0) === MSG_SUBTYPE_TEXT_ENTRYPOINT;
}
function isDiarySelector(selector) { function isDiarySelector(selector) {
return String(selector?.channelName || '').trim().toLowerCase() === DIARY_CHANNEL_NAME; return String(selector?.channelName || '').trim().toLowerCase() === DIARY_CHANNEL_NAME;
} }
@@ -340,9 +344,9 @@ function isStatusActionSubType(msgSubType) {
function getStatusActionTypeMeta(statusSubType, targetMsgSubType = 0) { function getStatusActionTypeMeta(statusSubType, targetMsgSubType = 0) {
const status = Number(statusSubType || 0); const status = Number(statusSubType || 0);
const target = Number(targetMsgSubType || 0); const target = Number(targetMsgSubType || 0);
if (status === MSG_SUBTYPE_STATUS_DONE_ONCE && target === MSG_SUBTYPE_TEXT_EXERCISE) return { label: 'Выполнено' }; if (status === MSG_SUBTYPE_STATUS_DONE_ONCE && target === MSG_SUBTYPE_TEXT_EXERCISE) return { label: 'Выполнил упражнение' };
if (status === MSG_SUBTYPE_STATUS_LEARNED && target === MSG_SUBTYPE_TEXT_EXERCISE) return { label: 'Изучено' }; if (status === MSG_SUBTYPE_STATUS_LEARNED && target === MSG_SUBTYPE_TEXT_EXERCISE) return { label: 'Изучил упражнение' };
if (status === MSG_SUBTYPE_STATUS_SERVICE_PASSED && target === MSG_SUBTYPE_TEXT_SERVICE) return { label: 'Пройдено' }; if (status === MSG_SUBTYPE_STATUS_SERVICE_PASSED && target === MSG_SUBTYPE_TEXT_SERVICE) return { label: 'Пройденная процедура' };
if (status === MSG_SUBTYPE_STATUS_INTERESTED) return { label: 'Заинтересовался' }; if (status === MSG_SUBTYPE_STATUS_INTERESTED) return { label: 'Заинтересовался' };
if (status === MSG_SUBTYPE_STATUS_STARTED) return { label: 'Начал' }; if (status === MSG_SUBTYPE_STATUS_STARTED) return { label: 'Начал' };
if (status === MSG_SUBTYPE_STATUS_IN_STUDY) return { label: 'Изучаю' }; if (status === MSG_SUBTYPE_STATUS_IN_STUDY) return { label: 'Изучаю' };
@@ -833,6 +837,100 @@ function openStatusActionMenuModal({ targetLabel, options = [], onSelect }) {
}); });
} }
function sortPostsByTimeDesc(posts = []) {
return [...posts].sort((a, b) => {
const byTime = Number(b?.timestampMs || 0) - Number(a?.timestampMs || 0);
if (byTime !== 0) return byTime;
return Number(b?.messageRef?.blockNumber || 0) - Number(a?.messageRef?.blockNumber || 0);
});
}
function getEntrypointPosts(posts = []) {
return sortPostsByTimeDesc((Array.isArray(posts) ? posts : []).filter((post) => isEntrypointSubType(post?.msgSubType)));
}
function flashAndScrollToMessage(messageRef) {
const key = messageRefKey(messageRef);
if (!key) return false;
const cards = Array.from(document.querySelectorAll('.channel-message-card[data-message-key]'));
const target = cards.find((card) => card.dataset.messageKey === key);
if (!target) return false;
target.classList.remove('is-focus-flash');
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
window.setTimeout(() => target.classList.add('is-focus-flash'), 60);
window.setTimeout(() => target.classList.remove('is-focus-flash'), 1800);
return true;
}
function openEntrypointMenuModal({ onShowHistory }) {
const root = document.getElementById('modal-root');
root.innerHTML = `
<div class="modal" id="channel-entrypoint-menu-modal">
<div class="modal-card stack">
<h3 class="modal-title">Оглавление канала</h3>
<div class="stack">
<button class="channel-menu-item channel-status-action-item" id="channel-entrypoint-history" type="button">Просмотреть историю изменений оглавления</button>
</div>
<div class="form-actions-grid">
<button class="secondary-btn" id="channel-entrypoint-close" type="button">Закрыть</button>
</div>
</div>
</div>
`;
root.querySelector('#channel-entrypoint-close')?.addEventListener('click', () => {
root.innerHTML = '';
});
root.querySelector('#channel-entrypoint-history')?.addEventListener('click', () => {
root.innerHTML = '';
if (typeof onShowHistory === 'function') onShowHistory();
});
}
function openEntrypointHistoryModal({ channelTitle = '', posts = [], onSelect }) {
const root = document.getElementById('modal-root');
const items = getEntrypointPosts(posts);
const cleanTitle = String(channelTitle || '').trim() || 'канала';
root.innerHTML = `
<div class="modal" id="channel-entrypoint-history-modal">
<div class="modal-card stack">
<h3 class="modal-title">История оглавления</h3>
<p class="meta-muted">Показаны все версии оглавления канала ${escapeHtml(cleanTitle)}</p>
<div class="entrypoint-history-list" id="entrypoint-history-list"></div>
<button class="secondary-btn" id="entrypoint-history-close" type="button">Закрыть</button>
</div>
</div>
`;
const list = root.querySelector('#entrypoint-history-list');
if (list) {
if (!items.length) {
const empty = document.createElement('div');
empty.className = 'card meta-muted';
empty.textContent = 'Оглавление в этом канале пока не добавлялось.';
list.append(empty);
} else {
items.forEach((post) => {
const parsed = parseMessageAttachments(post.body);
const item = document.createElement('button');
item.type = 'button';
item.className = 'entrypoint-history-item';
item.innerHTML = `
<strong>${escapeHtml(post.timestampMs ? new Date(post.timestampMs).toLocaleString('ru-RU') : 'Без даты')}</strong>
<span>#${escapeHtml(post.localNumber || '—')}</span>
<p>${escapeHtml(String(parsed.text || '').trim() || 'Без текста')}</p>
`;
item.addEventListener('click', () => {
root.innerHTML = '';
if (typeof onSelect === 'function') onSelect(post);
});
list.append(item);
});
}
}
root.querySelector('#entrypoint-history-close')?.addEventListener('click', () => {
root.innerHTML = '';
});
}
function openRepostModal({ navigate, channels = [], onSubmit }) { function openRepostModal({ navigate, channels = [], onSubmit }) {
const root = document.getElementById('modal-root'); const root = document.getElementById('modal-root');
const options = (Array.isArray(channels) ? channels : []) const options = (Array.isArray(channels) ? channels : [])
@@ -1129,7 +1227,7 @@ function mapApiMessageToPost(message, selector, localNumber) {
messageRef, messageRef,
rawMessage: message, rawMessage: message,
msgSubType, msgSubType,
isRating: msgSubType === MSG_SUBTYPE_TEXT_RATING, isRating: msgSubType === MSG_SUBTYPE_TEXT_RATING && !isStatusAction,
isStatusAction, isStatusAction,
targetRef: message?.targetBlockchainName && Number.isFinite(Number(message?.targetBlockNumber)) targetRef: message?.targetBlockchainName && Number.isFinite(Number(message?.targetBlockNumber))
? { ? {
@@ -1173,7 +1271,7 @@ async function loadFromApi(route, channelId) {
let selector = buildSelectorFromRoute(route, channelId); let selector = buildSelectorFromRoute(route, channelId);
if (selector?.ownerBlockchainName && selector?.channelName && isDiarySelector(selector)) { if (selector?.ownerBlockchainName && selector?.channelName && isDiarySelector(selector)) {
if (!isAuthorized) { if (!isAuthorized) {
throw new Error('Личный дневник доступен только после входа.'); throw new Error('Дневник доступен только после входа.');
} }
const diaryPayload = await authService.getPersonalDiary(currentSessionLogin, 400, 'asc'); const diaryPayload = await authService.getPersonalDiary(currentSessionLogin, 400, 'asc');
const diaryMessages = Array.isArray(diaryPayload?.messages) ? diaryPayload.messages : []; const diaryMessages = Array.isArray(diaryPayload?.messages) ? diaryPayload.messages : [];
@@ -1192,7 +1290,7 @@ async function loadFromApi(route, channelId) {
channel: { channel: {
name: diaryPayload?.channel?.channelName || DIARY_CHANNEL_NAME, name: diaryPayload?.channel?.channelName || DIARY_CHANNEL_NAME,
displayTitle: String(diaryPayload?.channel?.displayName || DIARY_CHANNEL_DISPLAY_NAME).trim(), displayTitle: String(diaryPayload?.channel?.displayName || DIARY_CHANNEL_DISPLAY_NAME).trim(),
displayName: DIARY_CHANNEL_DISPLAY_NAME, displayName: 'Дневника',
description: String(diaryPayload?.channel?.channelDescription || '').trim(), description: String(diaryPayload?.channel?.channelDescription || '').trim(),
avaAr: '', avaAr: '',
avaSha256: '', avaSha256: '',
@@ -1482,6 +1580,7 @@ function renderPostCard(post, {
onReply, onReply,
onRating, onRating,
onStatusAction, onStatusAction,
onOpenEntrypointMenu,
onRepost, onRepost,
onShare, onShare,
onEdit, onEdit,
@@ -1491,6 +1590,7 @@ function renderPostCard(post, {
const card = document.createElement('article'); const card = document.createElement('article');
card.className = 'card stack channel-message-card'; card.className = 'card stack channel-message-card';
if (post.isRating) card.classList.add('is-rating'); if (post.isRating) card.classList.add('is-rating');
if (selector && isDiarySelector(selector)) card.classList.add('is-diary-entry');
const authorTile = document.createElement('button'); const authorTile = document.createElement('button');
authorTile.type = 'button'; authorTile.type = 'button';
@@ -1544,7 +1644,6 @@ function renderPostCard(post, {
const typeButton = document.createElement('button'); const typeButton = document.createElement('button');
typeButton.type = 'button'; typeButton.type = 'button';
typeButton.className = 'channel-message-type-button'; typeButton.className = 'channel-message-type-button';
if (!typeMeta.actionable) typeButton.classList.add('is-static');
typeButton.textContent = typeMeta.label; typeButton.textContent = typeMeta.label;
if (typeMeta.actionable && typeof onStatusAction === 'function') { if (typeMeta.actionable && typeof onStatusAction === 'function') {
typeButton.addEventListener('click', (event) => { typeButton.addEventListener('click', (event) => {
@@ -1552,7 +1651,14 @@ function renderPostCard(post, {
animatePress(event.currentTarget); animatePress(event.currentTarget);
onStatusAction(post); onStatusAction(post);
}); });
} else if (isEntrypointSubType(post.msgSubType) && typeof onOpenEntrypointMenu === 'function') {
typeButton.addEventListener('click', (event) => {
event.stopPropagation();
animatePress(event.currentTarget);
onOpenEntrypointMenu(post);
});
} else { } else {
typeButton.classList.add('is-static');
typeButton.disabled = true; typeButton.disabled = true;
} }
headRow.append(typeButton); headRow.append(typeButton);
@@ -1597,7 +1703,7 @@ function renderPostCard(post, {
ratingBadge.textContent = 'Оценка'; ratingBadge.textContent = 'Оценка';
card.append(ratingBadge); card.append(ratingBadge);
} }
if (post.isStatusAction) { if (post.isStatusAction && selector && isDiarySelector(selector)) {
const statusBadge = document.createElement('span'); const statusBadge = document.createElement('span');
statusBadge.className = 'channel-message-kind-badge channel-message-kind-badge--status'; statusBadge.className = 'channel-message-kind-badge channel-message-kind-badge--status';
statusBadge.textContent = getStatusActionTypeMeta(post.msgSubType, post.targetMsgSubType).label; statusBadge.textContent = getStatusActionTypeMeta(post.msgSubType, post.targetMsgSubType).label;
@@ -1848,6 +1954,7 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
onReply: handlers.onReply, onReply: handlers.onReply,
onRating: handlers.onRating, onRating: handlers.onRating,
onStatusAction: handlers.onStatusAction, onStatusAction: handlers.onStatusAction,
onOpenEntrypointMenu: handlers.onOpenEntrypointMenu,
onRepost: handlers.onRepost, onRepost: handlers.onRepost,
onShare: handlers.onShare, onShare: handlers.onShare,
onEdit: handlers.onEdit, onEdit: handlers.onEdit,
@@ -1862,7 +1969,7 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
const empty = document.createElement('div'); const empty = document.createElement('div');
empty.className = 'card meta-muted'; empty.className = 'card meta-muted';
empty.textContent = channelData.isDiary empty.textContent = channelData.isDiary
? 'К сожалению, у вас пока еще ничего нет в личном дневнике.' ? 'К сожалению, у вас пока еще ничего нет в Дневнике.'
: 'Ждем ваших начинаний'; : 'Ждем ваших начинаний';
feed.append(empty); feed.append(empty);
} }
@@ -1928,13 +2035,20 @@ export function render({ navigate, route }) {
const header = renderHeader({ const header = renderHeader({
title: '', title: '',
leftAction: { label: '<', onClick: () => navigateBack() }, leftAction: { label: '<', onClick: () => navigateBack() },
rightActions: [{ label: 'Канал: ...', onClick: () => {} }], rightActions: [
{ label: 'Канал: ...', className: 'channel-header-route-btn', onClick: () => {} },
{ label: 'Оглавление', className: 'channel-header-entrypoint-btn', onClick: () => {} },
],
}); });
const channelHeaderButton = header.querySelector('.header-actions .icon-btn'); const channelHeaderButton = header.querySelector('.header-actions .channel-header-route-btn');
const channelEntrypointButton = header.querySelector('.header-actions .channel-header-entrypoint-btn');
if (channelHeaderButton) { if (channelHeaderButton) {
channelHeaderButton.classList.add('channel-header-route-btn');
channelHeaderButton.disabled = true; channelHeaderButton.disabled = true;
} }
if (channelEntrypointButton) {
channelEntrypointButton.disabled = true;
channelEntrypointButton.hidden = true;
}
const rerender = () => { const rerender = () => {
const current = document.querySelector('section.channels-screen--channel'); const current = document.querySelector('section.channels-screen--channel');
@@ -2179,6 +2293,18 @@ export function render({ navigate, route }) {
const apiData = await loadFromApi(route, channelId); const apiData = await loadFromApi(route, channelId);
activeSelector = apiData?.selector || null; activeSelector = apiData?.selector || null;
const titleLabel = apiData?.channel?.displayTitle || apiData?.channel?.name || 'channel'; const titleLabel = apiData?.channel?.displayTitle || apiData?.channel?.name || 'channel';
const entrypointPosts = getEntrypointPosts(apiData?.posts);
const openEntrypointHistory = () => {
openEntrypointHistoryModal({
channelTitle: titleLabel,
posts: apiData?.posts,
onSelect: (post) => {
if (!flashAndScrollToMessage(post?.messageRef)) {
showToast('Не удалось найти запись оглавления в ленте');
}
},
});
};
if (channelHeaderButton) { if (channelHeaderButton) {
channelHeaderButton.textContent = titleLabel; channelHeaderButton.textContent = titleLabel;
channelHeaderButton.disabled = false; channelHeaderButton.disabled = false;
@@ -2193,6 +2319,17 @@ export function render({ navigate, route }) {
}); });
}; };
} }
if (channelEntrypointButton) {
const canShowEntrypointButton = !apiData?.isDiary && entrypointPosts.length > 0;
channelEntrypointButton.hidden = !canShowEntrypointButton;
channelEntrypointButton.disabled = !canShowEntrypointButton;
channelEntrypointButton.onclick = () => {
const latestEntrypoint = entrypointPosts[0];
if (!latestEntrypoint?.messageRef || !flashAndScrollToMessage(latestEntrypoint.messageRef)) {
showToast('Не удалось найти актуальное оглавление');
}
};
}
skeleton.remove(); skeleton.remove();
cleanupSeenTracking = renderBody(screen, navigate, routeKey, apiData, { cleanupSeenTracking = renderBody(screen, navigate, routeKey, apiData, {
onAddMessage: () => { onAddMessage: () => {
@@ -2241,6 +2378,11 @@ export function render({ navigate, route }) {
showStatus(toUserMessage(error, 'Не удалось записать действие.')); showStatus(toUserMessage(error, 'Не удалось записать действие.'));
} }
}, },
onOpenEntrypointMenu: () => {
openEntrypointMenuModal({
onShowHistory: openEntrypointHistory,
});
},
onRepost: async (messageRef) => { onRepost: async (messageRef) => {
try { try {
await onRepost(messageRef); await onRepost(messageRef);
+3 -3
View File
@@ -22,7 +22,7 @@ const MENU_OVERLAY_ID = 'channels-context-menu-overlay';
const CHANNEL_TYPE_STORIES = 0; const CHANNEL_TYPE_STORIES = 0;
const CHANNEL_TYPE_PERSONAL = 100; const CHANNEL_TYPE_PERSONAL = 100;
const DIARY_CHANNEL_NAME = 'diary'; const DIARY_CHANNEL_NAME = 'diary';
const DIARY_DISPLAY_NAME = 'Личный дневник'; const DIARY_DISPLAY_NAME = 'Дневник';
function cleanChannelMessagePreview(text) { function cleanChannelMessagePreview(text) {
const parsed = parseMessageAttachments(text); const parsed = parseMessageAttachments(text);
@@ -730,7 +730,7 @@ function buildDiaryChannelRow(diaryPayload, ownRows = [], notificationsState = {
avatar: 'Д', avatar: 'Д',
avaAr: '', avaAr: '',
title: DIARY_DISPLAY_NAME, title: DIARY_DISPLAY_NAME,
technicalLabel: 'Виртуальная лента ваших действий', technicalLabel: 'Виртуальная лента Дневника',
channelName: DIARY_CHANNEL_NAME, channelName: DIARY_CHANNEL_NAME,
displayTitle: DIARY_DISPLAY_NAME, displayTitle: DIARY_DISPLAY_NAME,
channelDescription: 'История действий по упражнениям, услугам и курсам', channelDescription: 'История действий по упражнениям, услугам и курсам',
@@ -738,7 +738,7 @@ function buildDiaryChannelRow(diaryPayload, ownRows = [], notificationsState = {
channelTypeVersion: 1, channelTypeVersion: 1,
messagePreview: String(lastMessage?.text || '').trim() messagePreview: String(lastMessage?.text || '').trim()
? cleanChannelMessagePreview(lastMessage?.text) ? cleanChannelMessagePreview(lastMessage?.text)
: 'Новая запись в личном дневнике', : 'Новая запись в Дневнике',
messagesCount: messages.length, messagesCount: messages.length,
unreadCount: 0, unreadCount: 0,
lastMessageAt: Number(lastMessage?.createdAtMs || 0), lastMessageAt: Number(lastMessage?.createdAtMs || 0),
+19 -1
View File
@@ -278,12 +278,25 @@ function createMediaSlide({ item, url, kind, messageTimestampMs, slide, previewU
openAttachmentViewer({ item, url, kind }); openAttachmentViewer({ item, url, kind });
}); });
const bindImageLayout = (img) => {
img.addEventListener('load', () => {
const naturalWidth = Number(img.naturalWidth || 0);
const naturalHeight = Number(img.naturalHeight || 0);
const isLandscape = naturalWidth > 0 && naturalHeight > 0 && naturalWidth > naturalHeight;
img.classList.toggle('is-landscape', isLandscape);
frame.classList.toggle('is-landscape', isLandscape);
slide.classList.toggle('has-landscape-media', isLandscape);
slide.parentElement?.classList.toggle('has-landscape-media', isLandscape);
}, { once: true });
};
if (kind === 'image') { if (kind === 'image') {
const img = document.createElement('img'); const img = document.createElement('img');
img.className = 'message-attachment-media'; img.className = 'message-attachment-media';
img.src = url; img.src = url;
img.alt = item.name; img.alt = item.name;
img.loading = 'lazy'; img.loading = 'lazy';
bindImageLayout(img);
img.addEventListener('error', () => replaceWithUnavailable(slide, messageTimestampMs), { once: true }); img.addEventListener('error', () => replaceWithUnavailable(slide, messageTimestampMs), { once: true });
frame.append(img); frame.append(img);
} else if (previewUrl) { } else if (previewUrl) {
@@ -292,6 +305,7 @@ function createMediaSlide({ item, url, kind, messageTimestampMs, slide, previewU
img.src = previewUrl; img.src = previewUrl;
img.alt = `${item.name} preview`; img.alt = `${item.name} preview`;
img.loading = 'lazy'; img.loading = 'lazy';
bindImageLayout(img);
img.addEventListener('error', () => replaceWithUnavailable(slide, messageTimestampMs), { once: true }); img.addEventListener('error', () => replaceWithUnavailable(slide, messageTimestampMs), { once: true });
const play = document.createElement('span'); const play = document.createElement('span');
play.className = 'message-attachment-play'; play.className = 'message-attachment-play';
@@ -351,6 +365,7 @@ export function createAttachmentCarouselElement(attachments = [], { gateway = ''
next.setAttribute('aria-label', 'Следующее вложение'); next.setAttribute('aria-label', 'Следующее вложение');
const counter = document.createElement('div'); const counter = document.createElement('div');
counter.className = 'message-attachment-counter'; counter.className = 'message-attachment-counter';
wrap.classList.toggle('has-single-attachment', items.length <= 1);
const render = () => { const render = () => {
stopVideos(wrap); stopVideos(wrap);
@@ -358,6 +373,8 @@ export function createAttachmentCarouselElement(attachments = [], { gateway = ''
const kind = getAttachmentKind(item); const kind = getAttachmentKind(item);
const url = buildArweaveDataUrl({ gateway, txId: item.ar }); const url = buildArweaveDataUrl({ gateway, txId: item.ar });
const previewUrl = item.preview?.ar ? buildArweaveDataUrl({ gateway, txId: item.preview.ar }) : ''; const previewUrl = item.preview?.ar ? buildArweaveDataUrl({ gateway, txId: item.preview.ar }) : '';
viewport.classList.remove('has-landscape-media');
slide.className = 'message-attachment-slide';
slide.dataset.unavailable = '0'; slide.dataset.unavailable = '0';
slide.replaceChildren(); slide.replaceChildren();
slide.append(kind === 'file' slide.append(kind === 'file'
@@ -365,7 +382,8 @@ export function createAttachmentCarouselElement(attachments = [], { gateway = ''
: createMediaSlide({ item, url, kind, messageTimestampMs, slide, previewUrl })); : createMediaSlide({ item, url, kind, messageTimestampMs, slide, previewUrl }));
prev.hidden = items.length <= 1 || index <= 0; prev.hidden = items.length <= 1 || index <= 0;
next.hidden = items.length <= 1 || index >= items.length - 1; next.hidden = items.length <= 1 || index >= items.length - 1;
counter.textContent = `${index + 1} из ${items.length}`; counter.hidden = items.length <= 1;
counter.textContent = items.length > 1 ? `${index + 1} из ${items.length}` : '';
}; };
const move = (delta) => { const move = (delta) => {
+82
View File
@@ -2305,12 +2305,21 @@ textarea.input {
touch-action: pan-y; touch-action: pan-y;
} }
.message-attachment-carousel-viewport.has-landscape-media {
align-items: start;
min-height: 0;
}
.message-attachment-slide { .message-attachment-slide {
align-items: stretch; align-items: stretch;
display: grid; display: grid;
min-height: 12rem; min-height: 12rem;
} }
.message-attachment-slide.has-landscape-media {
min-height: 0;
}
.message-attachment-arrow { .message-attachment-arrow {
align-items: center; align-items: center;
background: rgba(15, 23, 42, 0.62); background: rgba(15, 23, 42, 0.62);
@@ -2363,6 +2372,10 @@ textarea.input {
position: relative; position: relative;
} }
.message-attachment-media-frame.is-landscape {
min-height: 0;
}
.message-attachment-media { .message-attachment-media {
display: block; display: block;
height: min(52vh, 20rem); height: min(52vh, 20rem);
@@ -2371,6 +2384,10 @@ textarea.input {
width: 100%; width: 100%;
} }
.message-attachment-media.is-landscape {
height: auto;
}
.message-attachment-play { .message-attachment-play {
align-items: center; align-items: center;
background: rgba(15, 23, 42, 0.72); background: rgba(15, 23, 42, 0.72);
@@ -3960,6 +3977,14 @@ textarea.input {
margin-bottom: 6px; margin-bottom: 6px;
} }
.channels-screen--channel .channel-message-card.is-diary-entry {
width: min(100%, 28rem);
max-width: min(84vw, 28rem);
padding: 16px 16px 15px;
justify-self: center;
align-self: center;
}
.channel-message-top { .channel-message-top {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -4084,6 +4109,11 @@ textarea.input {
box-shadow: 0 0 38px rgba(181, 136, 42, 0.14); box-shadow: 0 0 38px rgba(181, 136, 42, 0.14);
} }
.channels-screen .channel-message-card.is-focus-flash {
border-color: rgba(255, 214, 117, 0.5);
box-shadow: 0 0 0 1px rgba(255, 214, 117, 0.28), 0 0 34px rgba(255, 214, 117, 0.16);
}
.channel-message-body { .channel-message-body {
color: #ffffff; color: #ffffff;
line-height: 1.5; line-height: 1.5;
@@ -5460,6 +5490,58 @@ textarea.input {
color: #d9e6ff; color: #d9e6ff;
} }
.channels-screen--channel .page-header .channel-header-entrypoint-btn {
border: 1px solid rgba(224, 190, 117, 0.38);
background: linear-gradient(180deg, rgba(57, 74, 119, 0.34), rgba(18, 29, 54, 0.34));
color: #f1d99c;
border-radius: 10px;
min-height: 36px;
padding: 6px 10px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.02em;
text-transform: uppercase;
transform: translateY(-40%);
}
.entrypoint-history-list {
display: grid;
gap: 10px;
max-height: min(58vh, 30rem);
overflow: auto;
}
.entrypoint-history-item {
appearance: none;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 14px;
background: rgba(17, 24, 39, 0.72);
color: #eef3ff;
display: grid;
gap: 4px;
padding: 12px 13px;
text-align: left;
}
.entrypoint-history-item strong {
color: #f1d99c;
font-size: 13px;
}
.entrypoint-history-item span {
color: rgba(188, 208, 244, 0.74);
font-size: 12px;
}
.entrypoint-history-item p {
margin: 0;
color: #f5f8ff;
font-size: 13px;
line-height: 1.4;
white-space: pre-wrap;
word-break: break-word;
}
.channel-head-actions .secondary-btn { .channel-head-actions .secondary-btn {
background: rgba(255, 255, 255, 0.04); background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.12); border: 1px solid rgba(255, 255, 255, 0.12);