UI/Channels: вкладки по тапу + CreateChannel fallback для legacy формата
This commit is contained in:
parent
b55fd1571e
commit
76e4a6cba0
@ -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-формату сервера.
|
||||||
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.47
|
client.version=1.2.48
|
||||||
server.version=1.2.41
|
server.version=1.2.42
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user