UI/Channels: вкладки по тапу + CreateChannel fallback для legacy формата

This commit is contained in:
AidarKC 2026-05-13 02:42:57 +03:00
parent b55fd1571e
commit 76e4a6cba0
4 changed files with 123 additions and 20 deletions

View File

@ -0,0 +1,26 @@
# Каналы: явные вкладки + fallback CreateChannel для legacy-сервера
Статус: `pending`
## Что сделано
- На экране каналов добавлены явные вкладки:
- `Каналы`
- `Чаты`
- `Мои`
- Переключение теперь работает по обычному тапу, без необходимости long-press на кнопке toolbar.
- В `addBlockCreateChannel` добавлен fallback:
- сначала отправляется текущий формат CreateChannel (с description/type/version),
- если сервер возвращает `bad_block_format`, выполняется повтор с legacy-форматом тела (без description/type/version) для совместимости со старым сервером.
## Как проверять
1. Открыть экран каналов и проверить переключение всех трёх вкладок по тапу.
2. Нажимать на строки каналов и убедиться, что переход в канал работает.
3. Создать новый канал и убедиться, что при старом сервере создание не падает с `Некорректный формат блока`.
## Ожидаемый результат
- Вкладки `Каналы/Чаты/Мои` переключаются стабильно.
- Каналы открываются по тапу.
- Создание канала устойчиво к legacy-формату сервера.

View File

@ -1,2 +1,2 @@
client.version=1.2.47 client.version=1.2.48
server.version=1.2.41 server.version=1.2.42

View File

@ -1089,6 +1089,26 @@ export function render({ navigate, route }) {
const contentEl = document.createElement('div'); const contentEl = document.createElement('div');
contentEl.className = 'channels-list-content'; contentEl.className = 'channels-list-content';
const tabsEl = document.createElement('div');
tabsEl.className = 'channels-tabs';
const tabLabels = {
feed: 'Каналы',
dialogs: 'Чаты',
my: 'Мои',
};
TAB_ORDER.forEach((tabKey) => {
const tabBtn = document.createElement('button');
tabBtn.type = 'button';
tabBtn.className = `channels-tab-btn${listState.activeTab === tabKey ? ' is-active' : ''}`;
tabBtn.textContent = tabLabels[tabKey] || tabKey;
tabBtn.addEventListener('click', () => {
if (listState.activeTab === tabKey) return;
listState.activeTab = tabKey;
rerenderList();
});
tabsEl.append(tabBtn);
});
const bottomCta = document.createElement('button'); const bottomCta = document.createElement('button');
bottomCta.type = 'button'; bottomCta.type = 'button';
@ -1123,6 +1143,10 @@ export function render({ navigate, route }) {
onReload: reloadFeed, onReload: reloadFeed,
isTabEmpty, isTabEmpty,
}); });
tabsEl.querySelectorAll('.channels-tab-btn').forEach((btn, idx) => {
const key = TAB_ORDER[idx];
btn.classList.toggle('is-active', key === listState.activeTab);
});
}; };
let touchStartX = 0; let touchStartX = 0;
@ -1146,7 +1170,7 @@ export function render({ navigate, route }) {
rerenderList(); rerenderList();
}, { passive: true }); }, { passive: true });
screen.append(contentEl, bottomCta); screen.append(tabsEl, contentEl, bottomCta);
if (createSuccessFlash) { if (createSuccessFlash) {
showToast(createSuccessFlash); showToast(createSuccessFlash);

View File

@ -456,6 +456,31 @@ function makeCreateChannelBodyBytes({
); );
} }
function makeCreateChannelBodyBytesLegacy({
lineCode,
prevLineNumber,
prevLineHashHex,
thisLineNumber,
channelName,
}) {
const check = validateChannelDisplayName(channelName);
if (!check.ok) throw new Error(channelNameErrorText(check.code));
const cleanName = check.normalized;
const nameBytes = utf8Bytes(cleanName);
if (nameBytes.length < 1 || nameBytes.length > 255) {
throw new Error('Channel name must be 1..255 bytes');
}
return concatBytes(
int32Bytes(lineCode),
int32Bytes(prevLineNumber),
hexToBytes(normalizeHex32(prevLineHashHex)),
int32Bytes(thisLineNumber),
int8Byte(nameBytes.length),
nameBytes,
);
}
function makeTextPostBodyBytes({ lineCode, prevLineNumber, prevLineHashHex, thisLineNumber, text }) { function makeTextPostBodyBytes({ lineCode, prevLineNumber, prevLineHashHex, thisLineNumber, text }) {
const message = String(text || '').trim(); const message = String(text || '').trim();
if (!message) throw new Error('Message text is required'); if (!message) throw new Error('Message text is required');
@ -1075,23 +1100,51 @@ export class AuthService {
thisLineNumber = createdChannels.length + 1; thisLineNumber = createdChannels.length + 1;
} }
const payload = await this.addBlockSigned({ let payload;
login: cleanLogin, try {
storagePwd, payload = await this.addBlockSigned({
msgType: MSG_TYPE_TECH, login: cleanLogin,
msgSubType: MSG_SUBTYPE_TECH_CREATE_CHANNEL, storagePwd,
msgVersion: CREATE_CHANNEL_BODY_VERSION, msgType: MSG_TYPE_TECH,
bodyBytes: makeCreateChannelBodyBytes({ msgSubType: MSG_SUBTYPE_TECH_CREATE_CHANNEL,
lineCode: 0, msgVersion: CREATE_CHANNEL_BODY_VERSION,
prevLineNumber, bodyBytes: makeCreateChannelBodyBytes({
prevLineHashHex, lineCode: 0,
thisLineNumber, prevLineNumber,
channelName: cleanChannelName, prevLineHashHex,
channelDescription: cleanChannelDescription, thisLineNumber,
channelType: typeCode, channelName: cleanChannelName,
channelTypeVersion: typeVersion, channelDescription: cleanChannelDescription,
}), channelType: typeCode,
}); channelTypeVersion: typeVersion,
}),
});
} catch (error) {
const rawCode = String(error?.code || '').toUpperCase();
const rawText = String(error?.message || '').toLowerCase();
const isLegacyFormatMismatch = (
rawCode === 'BAD_BLOCK_FORMAT' ||
rawText.includes('bad_block_format') ||
rawText.includes('некорректный формат блока')
);
if (!isLegacyFormatMismatch) throw error;
// Совместимость со старыми серверами, где CreateChannel body без description/type.
payload = await this.addBlockSigned({
login: cleanLogin,
storagePwd,
msgType: MSG_TYPE_TECH,
msgSubType: MSG_SUBTYPE_TECH_CREATE_CHANNEL,
msgVersion: CREATE_CHANNEL_BODY_VERSION,
bodyBytes: makeCreateChannelBodyBytesLegacy({
lineCode: 0,
prevLineNumber,
prevLineHashHex,
thisLineNumber,
channelName: cleanChannelName,
}),
});
}
const selector = { const selector = {
ownerBlockchainName: blockchainName, ownerBlockchainName: blockchainName,