diff --git a/Dev_Docs/Pending_Features/2026-05-14_1627_мультиаккаунты-и-улучшенный-поиск-каналов.md b/Dev_Docs/Pending_Features/2026-05-14_1627_мультиаккаунты-и-улучшенный-поиск-каналов.md new file mode 100644 index 0000000..bf2a1dd --- /dev/null +++ b/Dev_Docs/Pending_Features/2026-05-14_1627_мультиаккаунты-и-улучшенный-поиск-каналов.md @@ -0,0 +1,32 @@ +# Мультиаккаунты + улучшенный поиск каналов/чатов + +- краткое описание фичи: + - Добавлен long-press на кнопку `Профиль` в нижнем тулбаре. + - По удержанию открывается меню с кнопкой `Сменить профиль`. + - Добавлен экран `Сменить профиль`: + - список уже добавленных аккаунтов; + - пометка активного аккаунта; + - переключение на другой аккаунт; + - кнопки `Добавить аккаунт (Войти)` и `Добавить аккаунт (Регистрация)`. + - Сессии нескольких аккаунтов сохраняются локально; при `authorizeSession` аккаунт добавляется/обновляется в списке. + - Выход из текущей сессии теперь удаляет только текущий аккаунт из списка аккаунтов. + - В `Новый персональный публичный чат` разрешён логин длиной 1 символ (тип канала `100`). + - В `Найти канал` улучшен UX: + - кнопка `Найти`; + - поиск пользователей по началу логина; + - понятные сообщения при отсутствии совпадений/каналов. + +- что именно проверять: + - Удержать кнопку `Профиль` и открыть `Сменить профиль`. + - Проверить отображение активного аккаунта и переключение на другой. + - Проверить сценарий `Добавить аккаунт` (войти/зарегистрироваться) без вылета из текущего аккаунта. + - Проверить создание персонального публичного чата с логином из 1 символа. + - Проверить поиск каналов по префиксу логина и работу кнопки `Найти`. + +- ожидаемый результат: + - Переключение между несколькими аккаунтами работает из UI. + - Поиск каналов стал управляемым и понятным. + - Ограничение 3+ символов для персонального чата снято. + +- статус: + - pending diff --git a/VERSION.properties b/VERSION.properties index dcd79e5..da975c1 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.53 -server.version=1.2.47 +client.version=1.2.54 +server.version=1.2.48 diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js index 225b149..618a081 100644 --- a/shine-UI/js/app.js +++ b/shine-UI/js/app.js @@ -30,6 +30,7 @@ import { markIncomingReadByBaseKey, markOutgoingReadByBaseKey, setContacts, + cancelAddAccountFlow, } from './state.js'; import * as startView from './pages/start-view.js'; @@ -44,6 +45,7 @@ import * as loginPasswordView from './pages/login-password-view.js'; import * as keyStorageView from './pages/key-storage-view.js'; import * as profileView from './pages/profile-view.js'; +import * as accountSwitcherView from './pages/account-switcher-view.js'; import * as profileEditView from './pages/profile-edit-view.js'; import * as walletView from './pages/wallet-view.js'; import * as settingsView from './pages/settings-view.js'; @@ -83,6 +85,7 @@ const routes = { 'login-password-view': loginPasswordView, 'key-storage-view': keyStorageView, 'profile-view': profileView, + 'account-switcher-view': accountSwitcherView, 'profile-edit-view': profileEditView, 'wallet-view': walletView, 'settings-view': settingsView, @@ -676,10 +679,13 @@ function renderApp() { return; } - if (state.session.isAuthorized && PRE_AUTH_PAGES.includes(pageId)) { + if (state.session.isAuthorized && PRE_AUTH_PAGES.includes(pageId) && !state.accountAddingMode) { navigate('messages-list'); return; } + if (state.session.isAuthorized && !PRE_AUTH_PAGES.includes(pageId) && state.accountAddingMode) { + cancelAddAccountFlow(); + } const page = routes[pageId] || routes['start-view']; diff --git a/shine-UI/js/components/toolbar.js b/shine-UI/js/components/toolbar.js index 67ef9dd..f7e9512 100644 --- a/shine-UI/js/components/toolbar.js +++ b/shine-UI/js/components/toolbar.js @@ -9,6 +9,7 @@ const ITEMS = [ { pageId: 'profile-view', label: 'Профиль', icon: '👤' }, ]; const CHANNEL_HOLD_MS = 260; +const PROFILE_HOLD_MS = 320; const CHANNEL_MODES = Object.freeze([ { key: 'feed', label: 'Каналы' }, { key: 'dialogs', label: 'Чаты' }, @@ -62,6 +63,8 @@ export function renderToolbar(currentPageId, navigate) { } if (item.pageId === 'channels-list') { installChannelsHoldSwitcher(btn, navigate); + } else if (item.pageId === 'profile-view') { + installProfileHoldMenu(btn, navigate); } else { btn.addEventListener('click', () => navigate(item.pageId)); } @@ -71,6 +74,73 @@ export function renderToolbar(currentPageId, navigate) { return root; } +function installProfileHoldMenu(button, navigate) { + let holdTimer = 0; + let pressed = false; + let holdActive = false; + let overlay = null; + + const clearTimer = () => { + if (holdTimer) { + window.clearTimeout(holdTimer); + holdTimer = 0; + } + }; + + const closeOverlay = () => { + if (overlay) overlay.remove(); + overlay = null; + holdActive = false; + }; + + const openOverlay = () => { + const rect = button.getBoundingClientRect(); + overlay = document.createElement('div'); + overlay.className = 'toolbar-channels-hold-overlay'; + overlay.style.minWidth = '190px'; + overlay.style.left = `${Math.round(rect.left + rect.width / 2)}px`; + overlay.style.top = `${Math.round(rect.top - 12)}px`; + overlay.innerHTML = ``; + overlay.querySelector('[data-action="switch-profile"]')?.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + closeOverlay(); + navigate('account-switcher-view'); + }); + document.body.append(overlay); + holdActive = true; + }; + + button.addEventListener('pointerdown', () => { + pressed = true; + holdActive = false; + clearTimer(); + holdTimer = window.setTimeout(() => { + if (!pressed) return; + openOverlay(); + }, PROFILE_HOLD_MS); + }); + + button.addEventListener('pointerup', () => { + clearTimer(); + const wasHold = holdActive; + pressed = false; + if (wasHold) { + window.setTimeout(closeOverlay, 80); + return; + } + navigate('profile-view'); + }); + + button.addEventListener('pointercancel', () => { + clearTimer(); + pressed = false; + closeOverlay(); + }); + + button.addEventListener('contextmenu', (event) => event.preventDefault()); +} + function installChannelsHoldSwitcher(button, navigate) { let holdTimer = 0; let pressed = false; diff --git a/shine-UI/js/pages/account-switcher-view.js b/shine-UI/js/pages/account-switcher-view.js new file mode 100644 index 0000000..1f3739c --- /dev/null +++ b/shine-UI/js/pages/account-switcher-view.js @@ -0,0 +1,76 @@ +import { renderHeader } from '../components/header.js'; +import { beginAddAccountFlow, state, switchToAccount } from '../state.js'; +import { toUserMessage } from '../services/ui-error-texts.js'; + +export const pageMeta = { id: 'account-switcher-view', title: 'Сменить профиль' }; + +export function render({ navigate }) { + const screen = document.createElement('section'); + screen.className = 'stack'; + + screen.append( + renderHeader({ + title: 'Сменить профиль', + leftAction: { label: '<', onClick: () => navigate('profile-view') }, + }), + ); + + const list = document.createElement('div'); + list.className = 'stack'; + const status = document.createElement('div'); + status.className = 'meta-muted'; + + const accounts = Array.isArray(state.accounts) ? state.accounts : []; + const activeLogin = String(state.session.login || '').trim().toLowerCase(); + + if (!accounts.length) { + const empty = document.createElement('div'); + empty.className = 'card meta-muted'; + empty.textContent = 'Сохранённых аккаунтов пока нет.'; + list.append(empty); + } else { + accounts.forEach((account) => { + const login = String(account?.login || '').trim(); + if (!login) return; + const card = document.createElement('button'); + card.type = 'button'; + card.className = 'card row'; + card.style.justifyContent = 'space-between'; + card.style.alignItems = 'center'; + card.innerHTML = ` + ${login} + ${login.toLowerCase() === activeLogin ? 'Активный' : 'Переключить'} + `; + card.addEventListener('click', async () => { + if (login.toLowerCase() === activeLogin) return; + status.textContent = 'Переключаем аккаунт...'; + try { + await switchToAccount(login); + status.textContent = ''; + navigate('profile-view'); + } catch (error) { + status.textContent = toUserMessage(error, 'Не удалось переключить аккаунт.'); + } + }); + list.append(card); + }); + } + + const actions = document.createElement('div'); + actions.className = 'form-actions-grid'; + actions.innerHTML = ` + + + `; + actions.querySelector('#account-switcher-add-login')?.addEventListener('click', () => { + beginAddAccountFlow(); + navigate('login-view'); + }); + actions.querySelector('#account-switcher-add-register')?.addEventListener('click', () => { + beginAddAccountFlow(); + navigate('register-view'); + }); + + screen.append(list, actions, status); + return screen; +} diff --git a/shine-UI/js/pages/channels-list.js b/shine-UI/js/pages/channels-list.js index 3df2990..65fae53 100644 --- a/shine-UI/js/pages/channels-list.js +++ b/shine-UI/js/pages/channels-list.js @@ -407,12 +407,13 @@ function openChannelFinderModal({ navigate }) {