feat: добавить репосты сообщений в каналах и тредах

This commit is contained in:
AidarKC
2026-05-21 16:16:26 +03:00
parent 5344c42ceb
commit fd99250882
20 changed files with 694 additions and 50 deletions
+190 -1
View File
@@ -359,6 +359,90 @@ function openReplyModal({ onSubmit, navigate }) {
if (textEl) textEl.focus();
}
function openRepostModal({ navigate, channels = [], onSubmit }) {
const root = document.getElementById('modal-root');
const options = (Array.isArray(channels) ? channels : [])
.filter((item) => item?.selector?.ownerBlockchainName && Number.isFinite(Number(item?.selector?.channelRootBlockNumber)))
.map((item, index) => {
const owner = String(item?.ownerLogin || '').trim();
const name = String(item?.channelName || '').trim();
const label = `${owner || 'my'} / ${name || 'stories'}`;
return `<option value="${index}">${label}</option>`;
})
.join('');
root.innerHTML = `
<div class="modal" id="repost-modal">
<div class="modal-card stack">
<h3 class="modal-title">Репост</h3>
<label class="meta-muted" for="repost-channel-select">Канал</label>
<select id="repost-channel-select" class="input">${options}</select>
<label class="meta-muted" for="repost-comment">Комментарий</label>
<textarea id="repost-comment" class="input" rows="5" maxlength="2000" placeholder="Комментарий к репосту"></textarea>
<div class="row wrap-row">
<button class="ghost-btn" id="repost-voice" type="button">🎤 Голосом</button>
</div>
<div class="meta-muted inline-error" id="repost-error"></div>
<div class="form-actions-grid">
<button class="secondary-btn" id="repost-cancel" type="button">Отмена</button>
<button class="primary-btn" id="repost-submit" type="button">Опубликовать репост</button>
</div>
</div>
</div>
`;
const selectEl = root.querySelector('#repost-channel-select');
const textEl = root.querySelector('#repost-comment');
const errorEl = root.querySelector('#repost-error');
const submitEl = root.querySelector('#repost-submit');
let inFlight = false;
const setBusy = (busy) => {
inFlight = !!busy;
if (selectEl) selectEl.disabled = inFlight;
if (textEl) textEl.disabled = inFlight;
if (submitEl) {
submitEl.disabled = inFlight;
submitEl.textContent = inFlight ? 'Публикуем...' : 'Опубликовать репост';
}
};
const close = () => { root.innerHTML = ''; };
root.querySelector('#repost-cancel')?.addEventListener('click', close);
root.querySelector('#repost-voice')?.addEventListener('click', async () => {
await openSpeechInputModal({
navigate,
onTextReady: (text) => {
const prev = String(textEl?.value || '').trim();
if (textEl) textEl.value = prev ? `${prev} ${text}` : text;
},
});
});
submitEl?.addEventListener('click', async () => {
if (inFlight) return;
const idx = Number(selectEl?.value ?? -1);
if (!Number.isFinite(idx) || idx < 0 || idx >= channels.length) {
errorEl.textContent = 'Выберите канал для репоста.';
return;
}
const text = String(textEl?.value || '').trim();
if (!text) {
errorEl.textContent = 'Введите комментарий к репосту.';
return;
}
setBusy(true);
errorEl.textContent = '';
try {
await onSubmit({ channel: channels[idx].selector, text });
close();
} catch (error) {
setBusy(false);
errorEl.textContent = toUserMessage(error, 'Не удалось сделать репост.');
}
});
if (textEl) textEl.focus();
}
function openAddMessageModal({ channelName, onSubmit, navigate }) {
const root = document.getElementById('modal-root');
root.innerHTML = `
@@ -544,6 +628,14 @@ function mapApiMessageToPost(message, selector, localNumber) {
repliesCount: Number(message?.repliesCount || 0),
timestampMs: resolveMessageTimestampMs(message),
messageRef,
msgSubType: Number(message?.msgSubType || 0),
targetRef: message?.targetBlockchainName && Number.isFinite(Number(message?.targetBlockNumber))
? {
blockchainName: String(message.targetBlockchainName).trim(),
blockNumber: Number(message.targetBlockNumber),
blockHash: normalizeMessageHash(message?.targetBlockHash),
}
: null,
reactionState: messageRef ? getMessageReactionState(messageRef) : '',
isOwnMessage: String(message?.authorLogin || '').trim().toLowerCase() === String(state.session.login || '').trim().toLowerCase(),
};
@@ -791,6 +883,7 @@ function renderPostCard(post, {
selector,
onToggleLike,
onReply,
onRepost,
onShare,
onEdit,
}) {
@@ -911,7 +1004,19 @@ function renderPostCard(post, {
onSubmit: async (text) => onReply(post.messageRef, text),
});
});
actions.append(likeButton, replyButton);
const repostButton = document.createElement('button');
repostButton.type = 'button';
repostButton.className = 'channel-action-item channel-action-reply';
repostButton.innerHTML = `
<span class="channel-action-icon" aria-hidden="true">🔁</span>
<span class="channel-action-label">Репост</span>
`;
repostButton.addEventListener('click', (event) => {
event.stopPropagation();
animatePress(event.currentTarget);
onRepost(post.messageRef);
});
actions.append(likeButton, replyButton, repostButton);
const shareButton = document.createElement('button');
shareButton.type = 'button';
@@ -928,6 +1033,28 @@ function renderPostCard(post, {
});
actions.append(shareButton);
if (post.msgSubType === 30 && post.targetRef?.blockchainName && Number.isFinite(post.targetRef?.blockNumber) && post.targetRef?.blockHash) {
const originalBtn = document.createElement('button');
originalBtn.type = 'button';
originalBtn.className = 'channel-action-item';
originalBtn.innerHTML = `
<span class="channel-action-icon" aria-hidden="true">↪</span>
<span class="channel-action-label">Оригинал</span>
`;
originalBtn.addEventListener('click', (event) => {
event.stopPropagation();
const ownerLogin = extractLoginFromBlockchainName(post.targetRef.blockchainName);
if (!ownerLogin) return;
const ok = window.confirm('Перейти к оригинальному сообщению?');
if (!ok) return;
navigate(makeShineMessageRoute({
ownerLogin,
messageBlockchainName: post.targetRef.blockchainName,
messageBlockNumber: post.targetRef.blockNumber,
}));
});
actions.append(originalBtn);
}
if (post.isOwnMessage) {
const editButton = document.createElement('button');
editButton.type = 'button';
@@ -979,6 +1106,7 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
selector: channelData.selector,
onToggleLike: handlers.onToggleLike,
onReply: handlers.onReply,
onRepost: handlers.onRepost,
onShare: handlers.onShare,
onEdit: handlers.onEdit,
});
@@ -1123,6 +1251,59 @@ export function render({ navigate, route }) {
rerender();
};
const loadOwnedChannelsForRepost = async (login) => {
const feed = await authService.listSubscriptionsFeed(login, 1000);
const rows = Array.isArray(feed?.ownedChannels) ? feed.ownedChannels : [];
return rows
.map((row) => {
const selector = {
ownerBlockchainName: String(row?.channel?.ownerBlockchainName || '').trim(),
channelRootBlockNumber: Number(row?.channel?.channelRoot?.blockNumber),
channelRootBlockHash: normalizeRouteHash(row?.channel?.channelRoot?.blockHash),
};
if (!selector.ownerBlockchainName || !Number.isFinite(selector.channelRootBlockNumber) || selector.channelRootBlockNumber < 0) {
return null;
}
return {
ownerLogin: String(row?.channel?.ownerLogin || '').trim(),
channelName: String(row?.channel?.channelName || '').trim(),
selector,
};
})
.filter(Boolean);
};
const isSameChannelSelector = (a, b) => (
String(a?.ownerBlockchainName || '').trim() === String(b?.ownerBlockchainName || '').trim()
&& Number(a?.channelRootBlockNumber) === Number(b?.channelRootBlockNumber)
&& normalizeRouteHash(a?.channelRootBlockHash) === normalizeRouteHash(b?.channelRootBlockHash)
);
const onRepost = async (messageRef) => {
const { login, storagePwd } = requireSigningSession();
const channels = await loadOwnedChannelsForRepost(login);
if (!channels.length) throw new Error('У вас пока нет каналов для репоста.');
openRepostModal({
navigate,
channels,
onSubmit: async ({ channel, text }) => {
await authService.addBlockRepost({
login,
storagePwd,
channel,
message: messageRef,
text,
});
if (isSameChannelSelector(channel, activeSelector)) {
pendingScrollByRoute.set(routeKey, '__LAST__');
rerender();
}
softHaptic(12);
showToast('Репост опубликован');
},
});
};
const onShare = async (routePath) => {
try {
const routeToShare = String(routePath || '').trim();
@@ -1241,6 +1422,14 @@ export function render({ navigate, route }) {
throw new Error(toUserMessage(error, 'Не удалось отправить ответ.'));
}
},
onRepost: async (messageRef) => {
try {
await onRepost(messageRef);
showStatus('');
} catch (error) {
showStatus(toUserMessage(error, 'Не удалось сделать репост.'));
}
},
onAddPost: async (bodyText) => {
try {
await onAddPost(bodyText);