SHA256
feat: добавить репосты сообщений в каналах и тредах
This commit is contained in:
@@ -283,6 +283,14 @@ function buildTargetFromNode(node) {
|
||||
return { blockchainName, blockNumber, blockHash };
|
||||
}
|
||||
|
||||
function buildRepostTargetFromNode(node) {
|
||||
const blockchainName = String(node?.targetBlockchainName || '').trim();
|
||||
const blockNumber = Number(node?.targetBlockNumber);
|
||||
const blockHash = normalizeMessageHash(node?.targetBlockHash);
|
||||
if (!blockchainName || !Number.isFinite(blockNumber) || blockNumber < 0 || !blockHash) return null;
|
||||
return { blockchainName, blockNumber, blockHash };
|
||||
}
|
||||
|
||||
function firstNonEmptyText(...candidates) {
|
||||
for (const candidate of candidates) {
|
||||
if (typeof candidate !== 'string') continue;
|
||||
@@ -379,6 +387,95 @@ 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="thread-repost-modal">
|
||||
<div class="modal-card stack">
|
||||
<h3 class="modal-title">Репост</h3>
|
||||
<label class="meta-muted" for="thread-repost-channel-select">Канал</label>
|
||||
<select id="thread-repost-channel-select" class="input">${options}</select>
|
||||
<label class="meta-muted" for="thread-repost-comment">Комментарий</label>
|
||||
<textarea id="thread-repost-comment" class="input" rows="5" maxlength="2000" placeholder="Комментарий к репосту"></textarea>
|
||||
<div class="row wrap-row">
|
||||
<button class="ghost-btn" id="thread-repost-voice" type="button">🎤 Голосом</button>
|
||||
</div>
|
||||
<div class="meta-muted inline-error" id="thread-repost-error"></div>
|
||||
<div class="form-actions-grid">
|
||||
<button class="secondary-btn" id="thread-repost-cancel" type="button">Отмена</button>
|
||||
<button class="primary-btn" id="thread-repost-submit" type="button">Опубликовать репост</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const selectEl = root.querySelector('#thread-repost-channel-select');
|
||||
const textEl = root.querySelector('#thread-repost-comment');
|
||||
const errorEl = root.querySelector('#thread-repost-error');
|
||||
const submitEl = root.querySelector('#thread-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('#thread-repost-cancel')?.addEventListener('click', close);
|
||||
root.querySelector('#thread-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 openMessageHistoryModal({ versions = [], title = 'История изменений' }) {
|
||||
const root = document.getElementById('modal-root');
|
||||
const rows = Array.isArray(versions) ? versions : [];
|
||||
@@ -477,6 +574,8 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
const replies = Number(node?.repliesCount || 0);
|
||||
const isOwnMessage = String(node?.authorLogin || '').trim().toLowerCase() === String(state.session.login || '').trim().toLowerCase();
|
||||
const isChannelPost = Number(node?.channelInfo?.channelRoot?.blockNumber) >= 0;
|
||||
const msgSubType = Number(node?.msgSubType || 0);
|
||||
const repostTarget = msgSubType === 30 ? buildRepostTargetFromNode(node) : null;
|
||||
|
||||
const headingText = String(heading || '').trim();
|
||||
if (headingText) {
|
||||
@@ -610,7 +709,46 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
await handlers.onShare(target);
|
||||
});
|
||||
|
||||
actions.append(likeButton, replyButton, shareButton);
|
||||
const repostButton = document.createElement('button');
|
||||
repostButton.type = 'button';
|
||||
repostButton.className = 'channel-action-item thread-reply-btn';
|
||||
repostButton.innerHTML = `
|
||||
<span class="channel-action-icon" aria-hidden="true">🔁</span>
|
||||
<span class="channel-action-label">Репост</span>
|
||||
`;
|
||||
repostButton.addEventListener('click', async (event) => {
|
||||
event.stopPropagation();
|
||||
animatePress(event.currentTarget);
|
||||
try {
|
||||
await handlers.onRepost(target);
|
||||
} catch (error) {
|
||||
handlers?.onActionError?.(error, 'repost');
|
||||
}
|
||||
});
|
||||
|
||||
actions.append(likeButton, replyButton, repostButton, shareButton);
|
||||
if (repostTarget) {
|
||||
const originalButton = document.createElement('button');
|
||||
originalButton.type = 'button';
|
||||
originalButton.className = 'channel-action-item';
|
||||
originalButton.innerHTML = `
|
||||
<span class="channel-action-icon" aria-hidden="true">↪</span>
|
||||
<span class="channel-action-label">Оригинал</span>
|
||||
`;
|
||||
originalButton.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
const ok = window.confirm('Перейти к оригинальному сообщению?');
|
||||
if (!ok) return;
|
||||
const ownerLogin = extractLoginFromBlockchainName(repostTarget.blockchainName);
|
||||
if (!ownerLogin) return;
|
||||
handlers.navigate(makeShineMessageRoute({
|
||||
ownerLogin,
|
||||
messageBlockchainName: repostTarget.blockchainName,
|
||||
messageBlockNumber: repostTarget.blockNumber,
|
||||
}));
|
||||
});
|
||||
actions.append(originalButton);
|
||||
}
|
||||
if (isOwnMessage) {
|
||||
const editButton = document.createElement('button');
|
||||
editButton.type = 'button';
|
||||
@@ -797,6 +935,45 @@ export function render({ navigate, route }) {
|
||||
showStatus('');
|
||||
rerender();
|
||||
},
|
||||
onRepost: async (target) => {
|
||||
const { login, storagePwd } = requireSigningSession();
|
||||
const feed = await authService.listSubscriptionsFeed(login, 1000);
|
||||
const channels = (Array.isArray(feed?.ownedChannels) ? feed.ownedChannels : [])
|
||||
.map((row) => {
|
||||
const selectorRow = {
|
||||
ownerBlockchainName: String(row?.channel?.ownerBlockchainName || '').trim(),
|
||||
channelRootBlockNumber: Number(row?.channel?.channelRoot?.blockNumber),
|
||||
channelRootBlockHash: normalizeRouteHash(row?.channel?.channelRoot?.blockHash),
|
||||
};
|
||||
if (!selectorRow.ownerBlockchainName || !Number.isFinite(selectorRow.channelRootBlockNumber) || selectorRow.channelRootBlockNumber < 0) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
ownerLogin: String(row?.channel?.ownerLogin || '').trim(),
|
||||
channelName: String(row?.channel?.channelName || '').trim(),
|
||||
selector: selectorRow,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (!channels.length) throw new Error('У вас пока нет каналов для репоста.');
|
||||
|
||||
openRepostModal({
|
||||
navigate,
|
||||
channels,
|
||||
onSubmit: async ({ channel, text }) => {
|
||||
await authService.addBlockRepost({
|
||||
login,
|
||||
storagePwd,
|
||||
channel,
|
||||
message: target,
|
||||
text,
|
||||
});
|
||||
softHaptic(12);
|
||||
showToast('Репост опубликован');
|
||||
showStatus('');
|
||||
},
|
||||
});
|
||||
},
|
||||
onShare: async (target) => {
|
||||
try {
|
||||
const routePath = buildThreadRouteFromTarget(target, selector);
|
||||
@@ -824,7 +1001,9 @@ export function render({ navigate, route }) {
|
||||
onActionError: (error, action) => {
|
||||
const fallback = action === 'unlike'
|
||||
? 'Не удалось убрать лайк.'
|
||||
: 'Не удалось поставить лайк.';
|
||||
: action === 'repost'
|
||||
? 'Не удалось сделать репост.'
|
||||
: 'Не удалось поставить лайк.';
|
||||
showStatus(toUserMessage(error, fallback));
|
||||
},
|
||||
onEdit: async (target, textValue, meta = {}) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -40,6 +40,7 @@ const MSG_SUBTYPE_TEXT_POST = 10;
|
||||
const MSG_SUBTYPE_TEXT_EDIT_POST = 11;
|
||||
const MSG_SUBTYPE_TEXT_REPLY = 20;
|
||||
const MSG_SUBTYPE_TEXT_EDIT_REPLY = 21;
|
||||
const MSG_SUBTYPE_TEXT_REPOST = 30;
|
||||
const MSG_SUBTYPE_REACTION_LIKE = 1;
|
||||
const MSG_SUBTYPE_REACTION_UNLIKE = 2;
|
||||
const MSG_SUBTYPE_CONNECTION_FOLLOW = 30;
|
||||
@@ -371,6 +372,50 @@ function makeTextReplyBodyBytes({ toBlockchainName, toBlockNumber, toBlockHashHe
|
||||
);
|
||||
}
|
||||
|
||||
function makeTextRepostBodyBytes({
|
||||
lineCode,
|
||||
prevLineNumber,
|
||||
prevLineHashHex,
|
||||
thisLineNumber,
|
||||
toBlockchainName,
|
||||
toBlockNumber,
|
||||
toBlockHashHex,
|
||||
text,
|
||||
}) {
|
||||
const message = String(text || '').trim();
|
||||
if (!message) throw new Error('Комментарий к репосту обязателен');
|
||||
|
||||
const bch = String(toBlockchainName || '').trim();
|
||||
if (!bch) throw new Error('toBlockchainName is required for repost');
|
||||
const bchBytes = utf8Bytes(bch);
|
||||
if (bchBytes.length < 1 || bchBytes.length > 255) {
|
||||
throw new Error('toBlockchainName must be 1..255 bytes');
|
||||
}
|
||||
|
||||
const blockNumber = Number(toBlockNumber);
|
||||
if (!Number.isFinite(blockNumber) || blockNumber < 0) {
|
||||
throw new Error('Invalid toBlockNumber for repost');
|
||||
}
|
||||
|
||||
const textBytes = utf8Bytes(message);
|
||||
if (textBytes.length < 1 || textBytes.length > 65535) {
|
||||
throw new Error('Repost comment must be 1..65535 UTF-8 bytes');
|
||||
}
|
||||
|
||||
return concatBytes(
|
||||
int32Bytes(lineCode),
|
||||
int32Bytes(prevLineNumber),
|
||||
hexToBytes(normalizeHex32(prevLineHashHex)),
|
||||
int32Bytes(thisLineNumber),
|
||||
int8Byte(bchBytes.length),
|
||||
bchBytes,
|
||||
int32Bytes(blockNumber),
|
||||
hexToBytes(normalizeHex32(toBlockHashHex)),
|
||||
int16Bytes(textBytes.length),
|
||||
textBytes,
|
||||
);
|
||||
}
|
||||
|
||||
function makeTextEditPostBodyBytes({
|
||||
lineCode,
|
||||
prevLineNumber,
|
||||
@@ -1010,6 +1055,77 @@ export class AuthService {
|
||||
});
|
||||
}
|
||||
|
||||
async addBlockRepost({ login, channel, message, text, storagePwd }) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
if (!cleanLogin) throw new Error('Missing login');
|
||||
const cleanText = String(text || '').trim();
|
||||
if (!cleanText) throw new Error('Комментарий к репосту обязателен');
|
||||
const target = normalizeMessageRefTarget(message, 'repost');
|
||||
const selector = channel || {};
|
||||
const owner = String(selector?.ownerBlockchainName || '').trim();
|
||||
const root = Number(selector?.channelRootBlockNumber);
|
||||
const key = `repost:${cleanLogin}:${owner}:${root}:${target.blockchainName}:${target.blockNumber}:${target.blockHash}:${cleanText}`;
|
||||
|
||||
return this.runWriteLocked(key, async () => {
|
||||
const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd);
|
||||
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
||||
if (!owner || !Number.isFinite(root) || root < 0) throw new Error('Invalid channel selector');
|
||||
if (owner !== blockchainName) throw new Error('Repost is allowed only to your own channels');
|
||||
|
||||
let rootHashHex = normalizeHex32(selector?.channelRootBlockHash, ZERO64);
|
||||
if (rootHashHex === ZERO64) {
|
||||
const ownChannels = await this.listOwnChannelsForBlockchain(cleanLogin, blockchainName);
|
||||
const rootChannel = ownChannels.find((item) => item.rootBlockNumber === root);
|
||||
if (!rootChannel) throw new Error('Channel root not found');
|
||||
rootHashHex = normalizeHex32(rootChannel.rootBlockHash, ZERO64);
|
||||
}
|
||||
|
||||
let prevLineNumber = root;
|
||||
let prevLineHashHex = rootHashHex;
|
||||
let thisLineNumber = 0;
|
||||
try {
|
||||
const latestPayload = await this.getChannelMessages({
|
||||
ownerBlockchainName: owner,
|
||||
channelRootBlockNumber: root,
|
||||
channelRootBlockHash: rootHashHex,
|
||||
}, 1, 'desc', cleanLogin);
|
||||
const latestMessage = Array.isArray(latestPayload?.messages) ? latestPayload.messages[0] : null;
|
||||
const latestBlockNumber = Number(latestMessage?.messageRef?.blockNumber);
|
||||
const latestBlockHash = normalizeHex32(latestMessage?.messageRef?.blockHash, '');
|
||||
const latestVersionsTotal = Number(latestMessage?.versionsTotal);
|
||||
if (Number.isFinite(latestBlockNumber) && latestBlockNumber >= 0 && latestBlockHash) {
|
||||
prevLineNumber = latestBlockNumber;
|
||||
prevLineHashHex = latestBlockHash;
|
||||
thisLineNumber = Number.isFinite(latestVersionsTotal) && latestVersionsTotal > 0
|
||||
? Math.max(0, latestVersionsTotal)
|
||||
: 1;
|
||||
}
|
||||
} catch {
|
||||
// fallback to root anchor
|
||||
}
|
||||
|
||||
const bodyBytes = makeTextRepostBodyBytes({
|
||||
lineCode: root,
|
||||
prevLineNumber,
|
||||
prevLineHashHex,
|
||||
thisLineNumber,
|
||||
toBlockchainName: target.blockchainName,
|
||||
toBlockNumber: target.blockNumber,
|
||||
toBlockHashHex: target.blockHash,
|
||||
text: cleanText,
|
||||
});
|
||||
|
||||
return this.addBlockSigned({
|
||||
login: cleanLogin,
|
||||
storagePwd,
|
||||
msgType: MSG_TYPE_TEXT,
|
||||
msgSubType: MSG_SUBTYPE_TEXT_REPOST,
|
||||
msgVersion: 1,
|
||||
bodyBytes,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async addBlockEditMessage({
|
||||
login,
|
||||
message,
|
||||
|
||||
Reference in New Issue
Block a user