UI: каналы 1..32, публичный type=1 и актуальный prevLine перед записью
This commit is contained in:
parent
01b38952e5
commit
ab31ccf6d8
@ -14,6 +14,9 @@
|
|||||||
- fallback по `ownerLogin + channelName`;
|
- fallback по `ownerLogin + channelName`;
|
||||||
- дополнительный fallback через `GetUser(owner)` с сопоставлением `blockchainName`.
|
- дополнительный fallback через `GetUser(owner)` с сопоставлением `blockchainName`.
|
||||||
Это снижает число ложных `Канал не найден` при открытии сторис/каналов других пользователей.
|
Это снижает число ложных `Канал не найден` при открытии сторис/каналов других пользователей.
|
||||||
|
- В форме «Создать канал» (вкладка «Мои») удалён выбор типа канала: создаётся только публичный канал `type=1` с полями «название + описание».
|
||||||
|
- Минимальная длина названия канала изменена с `3` на `1` (новый диапазон: `1..32`).
|
||||||
|
- Перед записью сообщения в канал UI теперь получает актуальное состояние линии канала (последний блок в линии) и строит `TEXT_POST` от свежего `prevLine`, что убирает постоянные конфликты состояния (`bad_prev_line_hash` / `line_err_prev_hash_mismatch`) при добавлении в свои сторис/каналы.
|
||||||
|
|
||||||
- что именно проверять:
|
- что именно проверять:
|
||||||
- Создать персональный публичный чат через UI (`Каналы -> Чаты -> Новый персональный публичный чат`) и убедиться, что ошибка `BAD_BLOCK_FORMAT` больше не появляется.
|
- Создать персональный публичный чат через UI (`Каналы -> Чаты -> Новый персональный публичный чат`) и убедиться, что ошибка `BAD_BLOCK_FORMAT` больше не появляется.
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.56
|
client.version=1.2.57
|
||||||
server.version=1.2.50
|
server.version=1.2.51
|
||||||
|
|||||||
@ -12,8 +12,6 @@ export const pageMeta = { id: 'add-channel-view', title: 'Создать кан
|
|||||||
|
|
||||||
const CREATE_CHANNEL_FLASH_KEY = 'shine-channels-create-success';
|
const CREATE_CHANNEL_FLASH_KEY = 'shine-channels-create-success';
|
||||||
const CHANNEL_TYPE_PUBLIC = 1;
|
const CHANNEL_TYPE_PUBLIC = 1;
|
||||||
const CHANNEL_TYPE_PERSONAL = 100;
|
|
||||||
const CHANNEL_TYPE_GROUP = 200;
|
|
||||||
|
|
||||||
function persistCreateSuccessFlash(message) {
|
function persistCreateSuccessFlash(message) {
|
||||||
try {
|
try {
|
||||||
@ -48,15 +46,8 @@ export function render({ navigate }) {
|
|||||||
form.innerHTML = `
|
form.innerHTML = `
|
||||||
<strong class="channel-head-title">Создание канала</strong>
|
<strong class="channel-head-title">Создание канала</strong>
|
||||||
<p class="channel-head-meta">Можно использовать только латиницу, цифры, _ и -.</p>
|
<p class="channel-head-meta">Можно использовать только латиницу, цифры, _ и -.</p>
|
||||||
<p class="channel-head-meta">Длина названия: от 3 до 32 символов.</p>
|
<p class="channel-head-meta">Длина названия: от 1 до 32 символов.</p>
|
||||||
|
<div class="meta-muted">Тип канала фиксирован: публичный (1).</div>
|
||||||
<label for="channel-type">Тип канала</label>
|
|
||||||
<select id="channel-type" class="input">
|
|
||||||
<option value="${CHANNEL_TYPE_PUBLIC}">Публичный (1)</option>
|
|
||||||
<option value="${CHANNEL_TYPE_PERSONAL}">Персональный (100)</option>
|
|
||||||
<option value="${CHANNEL_TYPE_GROUP}">Групповой (200)</option>
|
|
||||||
</select>
|
|
||||||
<div id="channel-type-hint" class="meta-muted">Публичный канал видят все. Писать может только владелец.</div>
|
|
||||||
|
|
||||||
<label for="channel-name">Название канала</label>
|
<label for="channel-name">Название канала</label>
|
||||||
<input id="channel-name" class="input" maxlength="32" placeholder="Например: my_channel-1" required />
|
<input id="channel-name" class="input" maxlength="32" placeholder="Например: my_channel-1" required />
|
||||||
@ -75,8 +66,6 @@ export function render({ navigate }) {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const nameEl = form.querySelector('#channel-name');
|
const nameEl = form.querySelector('#channel-name');
|
||||||
const typeEl = form.querySelector('#channel-type');
|
|
||||||
const typeHintEl = form.querySelector('#channel-type-hint');
|
|
||||||
const descriptionEl = form.querySelector('#channel-description');
|
const descriptionEl = form.querySelector('#channel-description');
|
||||||
const nameErrorEl = form.querySelector('#channel-name-error');
|
const nameErrorEl = form.querySelector('#channel-name-error');
|
||||||
const descriptionErrorEl = form.querySelector('#channel-description-error');
|
const descriptionErrorEl = form.querySelector('#channel-description-error');
|
||||||
@ -92,27 +81,10 @@ export function render({ navigate }) {
|
|||||||
submitEl.disabled = submitInFlight;
|
submitEl.disabled = submitInFlight;
|
||||||
cancelEl.disabled = submitInFlight;
|
cancelEl.disabled = submitInFlight;
|
||||||
nameEl.disabled = submitInFlight;
|
nameEl.disabled = submitInFlight;
|
||||||
typeEl.disabled = submitInFlight;
|
|
||||||
descriptionEl.disabled = submitInFlight;
|
descriptionEl.disabled = submitInFlight;
|
||||||
submitEl.textContent = submitInFlight ? 'Создаём...' : 'Создать';
|
submitEl.textContent = submitInFlight ? 'Создаём...' : 'Создать';
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateTypeHint = () => {
|
|
||||||
const typeCode = Number(typeEl.value || CHANNEL_TYPE_PUBLIC);
|
|
||||||
if (typeCode === CHANNEL_TYPE_PERSONAL) {
|
|
||||||
typeHintEl.textContent = 'Для персонального канала название должно быть login собеседника.';
|
|
||||||
nameEl.placeholder = 'Например: aidar';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeCode === CHANNEL_TYPE_GROUP) {
|
|
||||||
typeHintEl.textContent = 'Для группового канала участников добавляют командами /.add и /.remove.';
|
|
||||||
nameEl.placeholder = 'Например: team_room';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
typeHintEl.textContent = 'Публичный канал видят все. Писать может только владелец.';
|
|
||||||
nameEl.placeholder = 'Например: my_channel-1';
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateValidation = () => {
|
const updateValidation = () => {
|
||||||
const nameCheck = validateChannelDisplayName(nameEl.value);
|
const nameCheck = validateChannelDisplayName(nameEl.value);
|
||||||
const descriptionCheck = validateDescription(descriptionEl.value);
|
const descriptionCheck = validateDescription(descriptionEl.value);
|
||||||
@ -154,21 +126,12 @@ export function render({ navigate }) {
|
|||||||
errorEl.textContent = '';
|
errorEl.textContent = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const channelType = Number(typeEl.value || CHANNEL_TYPE_PUBLIC);
|
|
||||||
if (channelType === CHANNEL_TYPE_PERSONAL) {
|
|
||||||
const targetLogin = normalizeChannelDisplayName(check.name);
|
|
||||||
const foundUser = await authService.getUser(targetLogin);
|
|
||||||
if (!foundUser?.exists) {
|
|
||||||
throw new Error('Логин для персонального канала не найден.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await authService.addBlockCreateChannel({
|
await authService.addBlockCreateChannel({
|
||||||
login,
|
login,
|
||||||
storagePwd,
|
storagePwd,
|
||||||
channelName: normalizeChannelDisplayName(check.name),
|
channelName: normalizeChannelDisplayName(check.name),
|
||||||
channelDescription: normalizeChannelDescription(check.description),
|
channelDescription: normalizeChannelDescription(check.description),
|
||||||
channelType,
|
channelType: CHANNEL_TYPE_PUBLIC,
|
||||||
channelTypeVersion: 1,
|
channelTypeVersion: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -182,11 +145,9 @@ export function render({ navigate }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
cancelEl.addEventListener('click', () => navigate('channels-list'));
|
cancelEl.addEventListener('click', () => navigate('channels-list'));
|
||||||
typeEl.addEventListener('change', updateTypeHint);
|
|
||||||
|
|
||||||
screen.append(form);
|
screen.append(form);
|
||||||
nameEl.focus();
|
nameEl.focus();
|
||||||
updateTypeHint();
|
|
||||||
updateValidation();
|
updateValidation();
|
||||||
return screen;
|
return screen;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -791,14 +791,21 @@ export class AuthService {
|
|||||||
if (!cleanLogin) throw new Error('Missing login for AddBlock');
|
if (!cleanLogin) throw new Error('Missing login for AddBlock');
|
||||||
if (!storagePwd) throw new Error('Missing storagePwd for AddBlock signing');
|
if (!storagePwd) throw new Error('Missing storagePwd for AddBlock signing');
|
||||||
|
|
||||||
const user = await this.getUser(cleanLogin);
|
const resolveFreshCursor = async () => {
|
||||||
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
const user = await this.getUser(cleanLogin);
|
||||||
const freshNum = Number(user?.serverLastGlobalNumber);
|
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
||||||
const freshHash = normalizeHex32(user?.serverLastGlobalHash, ZERO64);
|
const freshNum = Number(user?.serverLastGlobalNumber);
|
||||||
const freshCursor = {
|
const freshHash = normalizeHex32(user?.serverLastGlobalHash, ZERO64);
|
||||||
serverLastGlobalNumber: Number.isFinite(freshNum) ? freshNum : -1,
|
return {
|
||||||
serverLastGlobalHash: freshHash,
|
blockchainName,
|
||||||
|
cursor: {
|
||||||
|
serverLastGlobalNumber: Number.isFinite(freshNum) ? freshNum : -1,
|
||||||
|
serverLastGlobalHash: freshHash,
|
||||||
|
},
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
const freshState = await resolveFreshCursor();
|
||||||
|
const blockchainName = freshState.blockchainName;
|
||||||
|
|
||||||
const savedKeys = await loadEncryptedUserSecrets(cleanLogin, storagePwd);
|
const savedKeys = await loadEncryptedUserSecrets(cleanLogin, storagePwd);
|
||||||
const blockchainPrivatePkcs8 = savedKeys?.blockchainKey;
|
const blockchainPrivatePkcs8 = savedKeys?.blockchainKey;
|
||||||
@ -832,7 +839,7 @@ export class AuthService {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let cursor = freshCursor;
|
let cursor = freshState.cursor;
|
||||||
let response = await tryAdd(cursor);
|
let response = await tryAdd(cursor);
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
const knownNum = Number(response?.payload?.serverLastGlobalNumber);
|
const knownNum = Number(response?.payload?.serverLastGlobalNumber);
|
||||||
@ -840,6 +847,10 @@ export class AuthService {
|
|||||||
if (Number.isFinite(knownNum) && /^[0-9a-fA-F]{64}$/.test(knownHash)) {
|
if (Number.isFinite(knownNum) && /^[0-9a-fA-F]{64}$/.test(knownHash)) {
|
||||||
cursor = { serverLastGlobalNumber: knownNum, serverLastGlobalHash: knownHash.toLowerCase() };
|
cursor = { serverLastGlobalNumber: knownNum, serverLastGlobalHash: knownHash.toLowerCase() };
|
||||||
response = await tryAdd(cursor);
|
response = await tryAdd(cursor);
|
||||||
|
} else {
|
||||||
|
const refreshed = await resolveFreshCursor();
|
||||||
|
cursor = refreshed.cursor;
|
||||||
|
response = await tryAdd(cursor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1211,11 +1222,32 @@ export class AuthService {
|
|||||||
rootHashHex = normalizeHex32(rootChannel.rootBlockHash, ZERO64);
|
rootHashHex = normalizeHex32(rootChannel.rootBlockHash, ZERO64);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let prevLineNumber = lineCode;
|
||||||
|
let prevLineHashHex = rootHashHex;
|
||||||
|
let thisLineNumber = 0;
|
||||||
|
try {
|
||||||
|
const latestPayload = await this.getChannelMessages({
|
||||||
|
ownerBlockchainName,
|
||||||
|
channelRootBlockNumber: lineCode,
|
||||||
|
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, '');
|
||||||
|
if (Number.isFinite(latestBlockNumber) && latestBlockNumber >= 0 && latestBlockHash) {
|
||||||
|
prevLineNumber = latestBlockNumber;
|
||||||
|
prevLineHashHex = latestBlockHash;
|
||||||
|
thisLineNumber = latestBlockNumber + 1;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// fallback to root anchor
|
||||||
|
}
|
||||||
|
|
||||||
const bodyBytes = makeTextPostBodyBytes({
|
const bodyBytes = makeTextPostBodyBytes({
|
||||||
lineCode,
|
lineCode,
|
||||||
prevLineNumber: lineCode,
|
prevLineNumber,
|
||||||
prevLineHashHex: rootHashHex,
|
prevLineHashHex,
|
||||||
thisLineNumber: 0,
|
thisLineNumber,
|
||||||
text: cleanText,
|
text: cleanText,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
const MIN_LEN = 3;
|
const MIN_LEN = 1;
|
||||||
const MAX_LEN = 32;
|
const MAX_LEN = 32;
|
||||||
const ALLOWED_CHARS_RE = /^[A-Za-z0-9_-]+$/;
|
const ALLOWED_CHARS_RE = /^[A-Za-z0-9_-]+$/;
|
||||||
|
|
||||||
@ -54,7 +54,7 @@ export function channelNameErrorText(code) {
|
|||||||
case 'blank':
|
case 'blank':
|
||||||
return 'Введите название канала.';
|
return 'Введите название канала.';
|
||||||
case 'too_short':
|
case 'too_short':
|
||||||
return 'Название слишком короткое: минимум 3 символа.';
|
return 'Название слишком короткое: минимум 1 символ.';
|
||||||
case 'too_long':
|
case 'too_long':
|
||||||
return 'Название слишком длинное: максимум 32 символа.';
|
return 'Название слишком длинное: максимум 32 символа.';
|
||||||
case 'bad_chars':
|
case 'bad_chars':
|
||||||
|
|||||||
@ -55,9 +55,10 @@ export function toUserMessage(error, fallback = 'Действие не выпо
|
|||||||
text.includes('channel name must match') ||
|
text.includes('channel name must match') ||
|
||||||
text.includes('channelname contains unsupported') ||
|
text.includes('channelname contains unsupported') ||
|
||||||
text.includes('channelname length must be 3..32') ||
|
text.includes('channelname length must be 3..32') ||
|
||||||
|
text.includes('channelname length must be 1..32') ||
|
||||||
text.includes('bad_channel_name')
|
text.includes('bad_channel_name')
|
||||||
) {
|
) {
|
||||||
return 'Некорректное название канала. Разрешены кириллица, латиница, цифры, пробел, _ и - (3..32 символа).';
|
return 'Некорректное название канала. Разрешены кириллица, латиница, цифры, пробел, _ и - (1..32 символа).';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (text.includes('channel name is required') || text.includes('введите имя канала')) {
|
if (text.includes('channel name is required') || text.includes('введите имя канала')) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user