From 075d9e979a77bfe529a43393416420338b78d10f Mon Sep 17 00:00:00 2001 From: Andrey Sitnik Date: Sat, 26 Oct 2024 23:29:56 +0000 Subject: [PATCH] Add popups to router --- core/environment.ts | 17 +++++-- core/fast.ts | 1 + core/preview.ts | 8 +++- core/router.ts | 72 +++++++++++++++++++++++------ core/slow.ts | 1 + core/test/fast.test.ts | 1 + core/test/page.test.ts | 4 +- core/test/preview.test.ts | 6 +++ core/test/router.test.ts | 92 ++++++++++++++++++++++++++----------- core/test/two-steps.test.ts | 7 +++ web/pages/feeds/edit.svelte | 1 + web/stores/router.ts | 11 ++++- web/stories/environment.ts | 3 +- web/stories/scene.svelte | 14 ++++-- 14 files changed, 183 insertions(+), 55 deletions(-) diff --git a/core/environment.ts b/core/environment.ts index 093a256b..04709a7f 100644 --- a/core/environment.ts +++ b/core/environment.ts @@ -126,8 +126,17 @@ export function setIsMobile(isSmallScreen: boolean): void { const testRouter = atom() -export function setBaseTestRoute(route: BaseRoute | undefined): void { - testRouter.set(route) +export function addHashToBaseRoute( + route: BaseRoute | Omit | undefined +): BaseRoute | undefined { + if (!route) return undefined + return { hash: '', ...route } as BaseRoute +} + +export function setBaseTestRoute( + route: BaseRoute | Omit | undefined +): void { + testRouter.set(addHashToBaseRoute(route)) } export function getTestEnvironment(): EnvironmentAndStore { @@ -137,7 +146,9 @@ export function getTestEnvironment(): EnvironmentAndStore { locale: atom('en'), logStoreCreator: () => new MemoryStore(), networkType: () => ({ saveData: undefined, type: undefined }), - openRoute: setBaseTestRoute, + openRoute: route => { + setBaseTestRoute({ ...route, hash: '' }) + }, persistentEvents: { addEventListener() {}, removeEventListener() {} }, persistentStore: {}, restartApp: () => {}, diff --git a/core/fast.ts b/core/fast.ts index 320e91dc..f62cca20 100644 --- a/core/fast.ts +++ b/core/fast.ts @@ -228,6 +228,7 @@ onEnvironment(({ openRoute }) => { if (notSynced(router.get())) { openRoute({ params: { category, since }, + popups: [], route: 'fast' }) } diff --git a/core/preview.ts b/core/preview.ts index 3438008a..0c41bdb8 100644 --- a/core/preview.ts +++ b/core/preview.ts @@ -348,7 +348,11 @@ onEnvironment(({ openRoute }) => { previewUrl.listen(link => { let page = router.get() if (page.route === 'add' && page.params.url !== link) { - openRoute({ params: { candidate: undefined, url: link }, route: 'add' }) + openRoute({ + params: { candidate: undefined, url: link }, + popups: [], + route: 'add' + }) } }), router.subscribe(({ params, route }) => { @@ -368,6 +372,7 @@ onEnvironment(({ openRoute }) => { } else { openRoute({ params: { candidate: undefined, url: params.url }, + popups: [], route: 'add' }) } @@ -398,6 +403,7 @@ onEnvironment(({ openRoute }) => { candidate: candidateUrl, url: page.params.url }, + popups: [], route: 'add' }) } diff --git a/core/router.ts b/core/router.ts index 541fe576..52b4eaab 100644 --- a/core/router.ts +++ b/core/router.ts @@ -28,6 +28,10 @@ export interface Routes { welcome: {} } +export const popupNames = { feed: true, feedUrl: true, post: true } + +export type PopupRoute = { param: string; popup: keyof typeof popupNames } + export type RouteName = keyof Routes type EmptyObject = Record @@ -44,7 +48,12 @@ export type ParamlessRouteName = { }[RouteName] export type Route = Name extends string - ? { params: Routes[Name]; redirect?: boolean; route: Name } + ? { + params: Routes[Name] + popups: PopupRoute[] + redirect?: boolean + route: Name + } : never type StringParams = { @@ -52,7 +61,7 @@ type StringParams = { } export type BaseRoute = Name extends string - ? { params: StringParams; route: Name } + ? { hash: string; params: StringParams; route: Name } : never export type BaseRouter = ReadableAtom @@ -71,7 +80,7 @@ const SETTINGS = new Set([ const ORGANIZE = new Set(['add', 'categories']) function open(route: ParamlessRouteName): Route { - return { params: {}, route } + return { params: {}, popups: [], route } } function redirect(route: Route): Route { @@ -91,7 +100,25 @@ function validateNumber( } } -let $router = atom({ params: {}, route: 'home' }) +let $router = atom({ params: {}, popups: [], route: 'home' }) + +function checkPopupName( + popup: string | undefined +): popup is keyof typeof popupNames { + return !!popup && popup in popupNames +} + +function parsePopups(hash: string): PopupRoute[] { + let popups: PopupRoute[] = [] + let parts = hash.split(',') + for (let part of parts) { + let [popup, param] = part.split('=', 2) + if (checkPopupName(popup) && param) { + popups.push({ param, popup }) + } + } + return popups +} export const router = readonlyExport($router) @@ -102,10 +129,17 @@ onEnvironment(({ baseRouter }) => { (route, user, withFeeds, fast, slowUnread) => { if (!route) { return open('notFound') - } else if (user) { + } else if (!user) { + if (!GUEST.has(route.route)) { + return open('start') + } else { + return { params: route.params, popups: [], route: route.route } + } + } else { + let popups = parsePopups(route.hash) if (GUEST.has(route.route) || route.route === 'home') { if (withFeeds) { - return redirect({ params: {}, route: 'slow' }) + return redirect({ params: {}, popups, route: 'slow' }) } else { return redirect(open('welcome')) } @@ -116,12 +150,14 @@ onEnvironment(({ baseRouter }) => { } else if (route.route === 'feeds') { return redirect({ params: { candidate: undefined, url: undefined }, + popups, route: 'add' }) } else if (route.route === 'fast') { if (!route.params.category && !fast.isLoading) { return redirect({ params: { category: fast.categories[0].id }, + popups, route: 'fast' }) } @@ -136,11 +172,12 @@ onEnvironment(({ baseRouter }) => { if (route.params.since) { return validateNumber(route.params.since, since => { return { - ...route, params: { ...route.params, since - } + }, + popups, + route: route.route } }) } @@ -153,6 +190,7 @@ onEnvironment(({ baseRouter }) => { if (feedData) { return redirect({ params: { feed: feedData[0].id || '' }, + popups, route: 'slow' }) } @@ -162,11 +200,12 @@ onEnvironment(({ baseRouter }) => { if (route.params.page) { return validateNumber(route.params.page, page => { return { - ...route, params: { ...route.params, page - } + }, + popups, + route: route.route } }) } else { @@ -175,19 +214,19 @@ onEnvironment(({ baseRouter }) => { ...route.params, page: 1 }, + popups, route: 'slow' } } } - } else if (!GUEST.has(route.route)) { - return open('start') + return { params: route.params, popups, route: route.route } } - return route }, (oldRoute, newRoute) => { return ( oldRoute.route === newRoute.route && - JSON.stringify(oldRoute.params) === JSON.stringify(newRoute.params) + JSON.stringify(oldRoute.params) === JSON.stringify(newRoute.params) && + JSON.stringify(oldRoute.popups) === JSON.stringify(newRoute.popups) ) } ) @@ -215,26 +254,31 @@ export const backRoute = computed( if (route === 'add' && params.candidate) { return { params: { candidate: undefined, url: params.url }, + popups: [], route: 'add' } } else if (route === 'categories' && params.feed) { return { params: {}, + popups: [], route: 'categories' } } else if (route === 'fast' && params.post) { return { params: { category: params.category }, + popups: [], route: 'fast' } } else if (route === 'slow' && params.post) { return { params: { feed: params.feed }, + popups: [], route: 'slow' } } else if (route === 'export' && params.format) { return { params: { format: undefined }, + popups: [], route: 'export' } } diff --git a/core/slow.ts b/core/slow.ts index 3e6abc48..6a5306c1 100644 --- a/core/slow.ts +++ b/core/slow.ts @@ -193,6 +193,7 @@ onEnvironment(({ openRoute }) => { if (notSynced(router.get())) { openRoute({ params: { feed, page }, + popups: [], route: 'slow' }) } diff --git a/core/test/fast.test.ts b/core/test/fast.test.ts index 738c50c2..7978ccca 100644 --- a/core/test/fast.test.ts +++ b/core/test/fast.test.ts @@ -412,6 +412,7 @@ test('syncs fast category and since with URL', async () => { await markReadAndLoadNextFastPosts() deepStrictEqual(router.get(), { params: { category: category1, since: 5000 }, + popups: [], route: 'fast' }) diff --git a/core/test/page.test.ts b/core/test/page.test.ts index 9810d80b..023e7ef9 100644 --- a/core/test/page.test.ts +++ b/core/test/page.test.ts @@ -95,6 +95,7 @@ test('synchronizes params', async () => { await setTimeout(1) deepStrictEqual(router.get(), { params: { candidate: undefined, url: 'https://example.com' }, + popups: [], route: 'add' }) equal(pages.add().url.get(), 'https://example.com') @@ -107,6 +108,7 @@ test('synchronizes params', async () => { await setTimeout(1) deepStrictEqual(router.get(), { params: { candidate: undefined, url: 'https://other.com' }, + popups: [], route: 'add' }) equal(pages.add().url.get(), 'https://other.com') @@ -115,7 +117,7 @@ test('synchronizes params', async () => { setBaseTestRoute({ params: {}, route: 'notFound' }) pages.add().url.set('https://example.com') await setTimeout(1) - deepStrictEqual(router.get(), { params: {}, route: 'notFound' }) + deepStrictEqual(router.get(), { params: {}, popups: [], route: 'notFound' }) }) test('has under construction pages', () => { diff --git a/core/test/preview.test.ts b/core/test/preview.test.ts index 2a21e144..733e2bd7 100644 --- a/core/test/preview.test.ts +++ b/core/test/preview.test.ts @@ -632,6 +632,7 @@ test('changes URL during typing in the field', async () => { test('syncs URL with router', async () => { deepStrictEqual(router.get(), { params: { candidate: undefined, url: undefined }, + popups: [], route: 'add' }) @@ -639,6 +640,7 @@ test('syncs URL with router', async () => { setPreviewUrl('example.com') deepStrictEqual(router.get(), { params: { candidate: undefined, url: 'http://example.com' }, + popups: [], route: 'add' }) @@ -646,6 +648,7 @@ test('syncs URL with router', async () => { setPreviewUrl('https://other.com') deepStrictEqual(router.get(), { params: { candidate: undefined, url: 'https://other.com' }, + popups: [], route: 'add' }) @@ -683,6 +686,7 @@ test('show candidate on wide screen', async () => { deepStrictEqual(router.get(), { params: { candidate: 'https://a.com/atom', url: 'https://a.com/atom' }, + popups: [], route: 'add' }) equal(currentCandidate.get(), undefined) @@ -704,6 +708,7 @@ test('do not show candidate on mobile screen', async () => { deepStrictEqual(router.get(), { params: { candidate: undefined, url: 'https://a.com/atom' }, + popups: [], route: 'add' }) equal(previewCandidate.get(), undefined) @@ -722,6 +727,7 @@ test('redirect to candidates list if no current candidate', async () => { equal(previewCandidate.get(), undefined) deepStrictEqual(router.get(), { params: { candidate: undefined, url: 'https://a.com/atom' }, + popups: [], route: 'add' }) }) diff --git a/core/test/router.test.ts b/core/test/router.test.ts index 49c38eb4..2401fd64 100644 --- a/core/test/router.test.ts +++ b/core/test/router.test.ts @@ -28,31 +28,19 @@ afterEach(async () => { test('opens 404', () => { setBaseTestRoute(undefined) - deepStrictEqual(router.get(), { - params: {}, - route: 'notFound' - }) + deepStrictEqual(router.get(), { params: {}, popups: [], route: 'notFound' }) }) test('transforms routers for guest', () => { userId.set(undefined) setBaseTestRoute({ params: {}, route: 'home' }) - deepStrictEqual(router.get(), { - params: {}, - route: 'start' - }) + deepStrictEqual(router.get(), { params: {}, popups: [], route: 'start' }) setBaseTestRoute({ params: {}, route: 'slow' }) - deepStrictEqual(router.get(), { - params: {}, - route: 'start' - }) + deepStrictEqual(router.get(), { params: {}, popups: [], route: 'start' }) setBaseTestRoute({ params: {}, route: 'signin' }) - deepStrictEqual(router.get(), { - params: {}, - route: 'signin' - }) + deepStrictEqual(router.get(), { params: {}, popups: [], route: 'signin' }) }) test('transforms routers for users', () => { @@ -60,6 +48,7 @@ test('transforms routers for users', () => { setBaseTestRoute({ params: {}, route: 'home' }) deepStrictEqual(router.get(), { params: {}, + popups: [], redirect: true, route: 'welcome' }) @@ -67,12 +56,14 @@ test('transforms routers for users', () => { setBaseTestRoute({ params: { category: 'general' }, route: 'fast' }) deepStrictEqual(router.get(), { params: { category: 'general' }, + popups: [], route: 'fast' }) setBaseTestRoute({ params: {}, route: 'home' }) deepStrictEqual(router.get(), { params: {}, + popups: [], redirect: true, route: 'welcome' }) @@ -80,15 +71,13 @@ test('transforms routers for users', () => { setBaseTestRoute({ params: {}, route: 'signin' }) deepStrictEqual(router.get(), { params: {}, + popups: [], redirect: true, route: 'welcome' }) userId.set(undefined) - deepStrictEqual(router.get(), { - params: {}, - route: 'signin' - }) + deepStrictEqual(router.get(), { params: {}, popups: [], route: 'signin' }) }) test('transforms routers for users with feeds', async () => { @@ -96,6 +85,7 @@ test('transforms routers for users with feeds', async () => { setBaseTestRoute({ params: {}, route: 'home' }) deepStrictEqual(router.get(), { params: {}, + popups: [], redirect: true, route: 'welcome' }) @@ -103,6 +93,7 @@ test('transforms routers for users with feeds', async () => { let id = await addFeed(testFeed()) deepStrictEqual(router.get(), { params: {}, + popups: [], redirect: true, route: 'slow' }) @@ -110,6 +101,7 @@ test('transforms routers for users with feeds', async () => { setBaseTestRoute({ params: {}, route: 'welcome' }) deepStrictEqual(router.get(), { params: {}, + popups: [], redirect: true, route: 'slow' }) @@ -118,6 +110,7 @@ test('transforms routers for users with feeds', async () => { await deleteFeed(id) deepStrictEqual(router.get(), { params: {}, + popups: [], redirect: true, route: 'welcome' }) @@ -129,6 +122,7 @@ test('transforms section to first section page', () => { setBaseTestRoute({ params: {}, route: 'settings' }) deepStrictEqual(router.get(), { params: {}, + popups: [], redirect: true, route: 'interface' }) @@ -136,6 +130,7 @@ test('transforms section to first section page', () => { setBaseTestRoute({ params: {}, route: 'feeds' }) deepStrictEqual(router.get(), { params: { candidate: undefined, url: undefined }, + popups: [], redirect: true, route: 'add' }) @@ -152,6 +147,7 @@ test('transforms routers to first fast category', async () => { await setTimeout(100) deepStrictEqual(router.get(), { params: { category: idA }, + popups: [], redirect: true, route: 'fast' }) @@ -195,12 +191,14 @@ test('converts since to number', async () => { setBaseTestRoute({ params: { category: idA, since: 1000 }, route: 'fast' }) deepStrictEqual(router.get(), { params: { category: idA, since: 1000 }, + popups: [], route: 'fast' }) setBaseTestRoute({ params: { category: idA, since: '1000' }, route: 'fast' }) deepStrictEqual(router.get(), { params: { category: idA, since: 1000 }, + popups: [], route: 'fast' }) @@ -210,15 +208,13 @@ test('converts since to number', async () => { }) deepStrictEqual(router.get(), { params: { category: idA, post, since: 1000 }, + popups: [], route: 'fast' }) await setTimeout(10) setBaseTestRoute({ params: { category: idA, since: '1000k' }, route: 'fast' }) - deepStrictEqual(router.get(), { - params: {}, - route: 'notFound' - }) + deepStrictEqual(router.get(), { params: {}, popups: [], route: 'notFound' }) }) test('checks that category exists', async () => { @@ -231,15 +227,13 @@ test('checks that category exists', async () => { route: 'fast' }) await setTimeout(100) - deepStrictEqual(router.get(), { - params: {}, - route: 'notFound' - }) + deepStrictEqual(router.get(), { params: {}, popups: [], route: 'notFound' }) setBaseTestRoute({ params: { category: idA, since: 100 }, route: 'fast' }) await setTimeout(100) deepStrictEqual(router.get(), { params: { category: idA, since: 100 }, + popups: [], route: 'fast' }) }) @@ -252,6 +246,48 @@ test('backRoute handles export with format', () => { deepStrictEqual(router.get(), { params: { format: undefined }, + popups: [], route: 'export' }) }) + +test('parses popups', () => { + userId.set('10') + setBaseTestRoute({ hash: 'feed=id1,post=id2', params: {}, route: 'profile' }) + deepStrictEqual(router.get(), { + params: {}, + popups: [ + { param: 'id1', popup: 'feed' }, + { param: 'id2', popup: 'post' } + ], + route: 'profile' + }) + + setBaseTestRoute({ hash: 'broken,post=id', params: {}, route: 'profile' }) + deepStrictEqual(router.get(), { + params: {}, + popups: [{ param: 'id', popup: 'post' }], + route: 'profile' + }) + + setBaseTestRoute({ + hash: 'unknown=id1,post=id', + params: {}, + route: 'profile' + }) + deepStrictEqual(router.get(), { + params: {}, + popups: [{ param: 'id', popup: 'post' }], + route: 'profile' + }) +}) + +test('hides popups for guest', () => { + userId.set(undefined) + setBaseTestRoute({ hash: 'feed=id1,post=id2', params: {}, route: 'profile' }) + deepStrictEqual(router.get(), { + params: {}, + popups: [], + route: 'start' + }) +}) diff --git a/core/test/two-steps.test.ts b/core/test/two-steps.test.ts index 7940d197..99992ca5 100644 --- a/core/test/two-steps.test.ts +++ b/core/test/two-steps.test.ts @@ -54,6 +54,7 @@ test('works with adds route on wide screen', async () => { strictEqual(secondStep.get(), true) deepStrictEqual(backRoute.get(), { params: { candidate: undefined, url: 'https://a.com/atom' }, + popups: [], route: 'add' }) }) @@ -91,6 +92,7 @@ test('works with categories route', async () => { strictEqual(secondStep.get(), true) deepStrictEqual(backRoute.get(), { params: {}, + popups: [], route: 'categories' }) }) @@ -111,6 +113,7 @@ test('works with fast route', async () => { strictEqual(secondStep.get(), true) deepStrictEqual(backRoute.get(), { params: { category: idA }, + popups: [], route: 'fast' }) }) @@ -131,6 +134,7 @@ test('works with slow route', async () => { strictEqual(secondStep.get(), true) deepStrictEqual(backRoute.get(), { params: { feed }, + popups: [], route: 'slow' }) }) @@ -143,6 +147,7 @@ test('goes back to first step', async () => { deepStrictEqual(router.get(), { params: { feed }, + popups: [], route: 'categories' }) @@ -150,6 +155,7 @@ test('goes back to first step', async () => { deepStrictEqual(router.get(), { params: {}, + popups: [], route: 'categories' }) @@ -157,6 +163,7 @@ test('goes back to first step', async () => { deepStrictEqual(router.get(), { params: {}, + popups: [], route: 'categories' }) }) diff --git a/web/pages/feeds/edit.svelte b/web/pages/feeds/edit.svelte index 7100fa3e..d048fe8a 100644 --- a/web/pages/feeds/edit.svelte +++ b/web/pages/feeds/edit.svelte @@ -72,6 +72,7 @@ if (page.route === 'categories') { openRoute({ params: {}, + popups: [], route: page.route }) } diff --git a/web/stores/router.ts b/web/stores/router.ts index 9cca7cb3..fc3aedc9 100644 --- a/web/stores/router.ts +++ b/web/stores/router.ts @@ -38,6 +38,7 @@ export const urlRouter = computed(pathRouter, path => { if ('url' in path.params) params.url = path.params.url if ('candidate' in path.search) params.candidate = path.search.candidate return { + hash: path.hash, params, route: path.route } @@ -46,6 +47,7 @@ export const urlRouter = computed(pathRouter, path => { if ('since' in path.search) params.since = Number(path.search.since) if ('post' in path.search) params.post = path.search.post return { + hash: path.hash, params, route: path.route } @@ -56,6 +58,7 @@ export const urlRouter = computed(pathRouter, path => { } if ('post' in path.search) params.post = path.search.post return { + hash: path.hash, params, route: path.route } @@ -92,10 +95,14 @@ function moveToSearch( return getPagePath(pathRouter, page.route, rest, search) } -export function getURL(to: ParamlessRouteName | Route): string { +export function getURL( + to: Omit | ParamlessRouteName | Route +): string { let page: Route if (typeof to === 'string') { - page = { params: {}, route: to } + page = { params: {}, popups: [], route: to } + } else if (!('popups' in to)) { + page = { ...to, popups: [] } as Route } else { page = to } diff --git a/web/stories/environment.ts b/web/stories/environment.ts index 0d32d541..cb81e2d9 100644 --- a/web/stories/environment.ts +++ b/web/stories/environment.ts @@ -9,6 +9,7 @@ import { import { atom } from 'nanostores' export const baseRouter = atom({ + hash: '', params: { category: 'general' }, route: 'fast' }) @@ -37,7 +38,7 @@ setupEnvironment({ return networkType }, openRoute(page) { - baseRouter.set(page) + baseRouter.set({ ...page, hash: '' }) }, persistentEvents: { addEventListener() {}, diff --git a/web/stories/scene.svelte b/web/stories/scene.svelte index f48ea65d..1892d5b3 100644 --- a/web/stories/scene.svelte +++ b/web/stories/scene.svelte @@ -2,6 +2,8 @@ import { addCategory, addFeed, + addHashToBaseRoute, + type BaseRoute, Category, type CategoryValue, clearPreview, @@ -32,7 +34,6 @@ type PostValue, refreshStatistics, type RefreshStatistics, - type Route, selectAllImportedFeeds, slowPosts, type SlowPostsValue, @@ -74,7 +75,7 @@ openedPost, refreshing = false, responses = {}, - route = { params: {}, route: 'slow' }, + route, showPagination = false, slowState = INITIAL_SLOW, unloadedFeeds = [] @@ -91,7 +92,7 @@ openedPost?: PostValue | undefined refreshing?: false | Partial responses?: Record - route?: Route + route?: BaseRoute | Omit showPagination?: boolean slowState?: SlowPostsValue unloadedFeeds?: string[] @@ -125,11 +126,14 @@ if (fast) { baseRouter.set({ + hash: '', params: { category: 'general' }, route: 'fast' }) } else { - baseRouter.set(route) + baseRouter.set( + addHashToBaseRoute(route) ?? { hash: '', params: {}, route: 'slow' } + ) } }) @@ -177,7 +181,7 @@ return () => { forceSet(isRefreshing, false) - baseRouter.set({ params: {}, route: 'slow' }) + baseRouter.set({ hash: '', params: {}, route: 'slow' }) setNetworkType(DEFAULT_NETWORK) cleanLogux() forceSet(slowPosts, INITIAL_SLOW)