Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web): assets now have a permanent URL #8532

Merged
merged 29 commits into from
Apr 24, 2024

Conversation

midzelis
Copy link
Contributor

@midzelis midzelis commented Apr 5, 2024

Add stable urls for all assets. Navigating within app will update browser location. Reloading the page will properly load the asset within the context.

#2906
#7017

@alextran1502
Copy link
Contributor

Hello, I just finished the first round of testing; one issue I am seeing is using the mouse clicking on the back button from the asset-viewer component, the URL isn't updated, and then when you click on a different asset to open, the first click is to clear the URL, the second click then opens the asset-viewer

@midzelis
Copy link
Contributor Author

midzelis commented Apr 5, 2024

Yeah, something slipped in late last night. Took a look with fresh eyes today and fixed the problem. Extracted the url manipulation into the navigation.ts util. Force pushed to resolve some conflicts with main. Not sure if this project prefers merge commits or rebases in PRs.

@jrasm91
Copy link
Contributor

jrasm91 commented Apr 6, 2024

It doesn't matter because we'll squash it into a single commit on main.

@@ -1,11 +0,0 @@
import { authenticate } from '$lib/utils/auth';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we remove this file?

@alextran1502
Copy link
Contributor

There are a few +page.ts file with the following content get removed, why is that?

import type { PageLoad } from './$types';	

export const load = (async () => {	
  await authenticate();	
  return {	
    meta: {	
      title: 'Search',	
    },	
  };	
}) satisfies PageLoad;	

@klejejs
Copy link
Contributor

klejejs commented Apr 7, 2024

Should fix #8238 as well. Thanks for working on this!

@danieldietzler
Copy link
Member

Will also close #4746

@midzelis
Copy link
Contributor Author

midzelis commented Apr 9, 2024

I've split up this PR into very small, hopefully easy to understand, commit.

I've tried hard to not update all the paths, but ultimately failed. However, I was able to remove the [...rest] approach, and use optional path params instead. However, because of the way sveltekit does routing, if there is common functionality that cross-cuts multiple routes (i.e. the <routeBase>/photos/:assetId part) - you have to add that loader to each +page.ts. If the route had a common prefix, parent +page.tsloaders would run in parallel with child +page.ts loaders. Based on the immich route structure, assets come at the end of the path, so there are no common paths, and so each leaf +page.ts loader needs to run the load function itself. I have at least factored the load function into a util.

By using optional params, I don't need to parse the URL anymore. However, I still need to do that when I need to handle previous/next and open-asset navigation. That is because sveltekit doesn't expose https://github.com/sveltejs/kit/blob/main/packages/kit/src/utils/routing.js which would make it easier to parse, find/replace route segments. i.e. I could find the current route, and call resolve_route with the new route to create the new URL - alas, I have to resort to doing the regexp find/replace, which isn't bad, but it does create a very fragile connection between the routes (which in sveltekit are represented by paths (!!) - and the references in code to these routes.) This however, is not a new problem - any call to goto() in the codebase will need to refer to a route by url, and not by route.id. This is just a weirdness with sveltekit routing.

Ok - now on to the actual meat of this PR - updating the URL with the new path when opening/closing and prev/next buttons are pushed.

Previously, I had the navigation (the goto() call) performed directly in the asset-viewer/asset-grid. In this PR, I wanted to try to remove knowledge of the Routes from the components. So that only the sources in under /src/routes would be aware of the routing-specific navigation paths. To do this, instead of performing the goto call directly in the component, instead I use events. Again, ran into weirdness: Svelte component events don't bubble, and they can't be made to.

Here, I created some custom DOM events, and have the asestviewer/grid send these events at the right time. Then, once these events work they're way back up to the layout (which is part of /src/routes) it will figure out the route to navigate to.

Now - as I was typing this, I googled a bit about bubbling events in svelte - and it turns out there is way they want you to do this - thats to forward them: https://svelte.dev/examples/event-forwarding

In light of this guidance, I can rework the custom events to just use forwarding of the standard events. But before I do that, my question to ya'll is - do you want the components to directly call goto() or do you want them to use events that get forwarded up to either +page.svelte or +layout.svelte and get handled there? Note, there already are several places where goto is called from component code. If its agreed to go down this road, there should be a refactoring in a future PR to fix up goto() with events, and have them handled buy the routes components.

@midzelis
Copy link
Contributor Author

midzelis commented Apr 9, 2024

There are a few +page.ts file with the following content get removed, why is that?

I believe these are the redirects (before this pr, navigations to /album/:id/photos/:id would redirect to /album/:id/photos, etc. This new force-push has easier to follow step-by-step commits that should help you understand whats happening, if you view each commit 1 by 1.

@danieldietzler
Copy link
Member

danieldietzler commented Apr 9, 2024

@midzelis Firstly, thanks a lot for this write-up and for investing the time and efforts! :)
Regarding the event topic: I personally would prefer one event different pages can emit. To fix your issue with them (and for some other reasons) Svelte 5 will actually deprecate the emit/dispatch stuff in favor of simply passing through functions basically. In fact, we're slowly transitioning to that already so that we're prepared to switch to svelte 5 once available. It would be awesome if you could switch to that new model as well already; especially I think it could even fix some of the issues you have with it. For reference: https://svelte-5-preview.vercel.app/docs/event-handlers

@alextran1502
Copy link
Contributor

alextran1502 commented Apr 9, 2024

There are a few +page.ts file with the following content get removed, why is that?

I believe these are the redirects (before this pr, navigations to /album/:id/photos/:id would redirect to /album/:id/photos, etc. This new force-push has easier to follow step-by-step commits that should help you understand whats happening, if you view each commit 1 by 1.

From what I see, for pages that don't have +page.ts, you cannot navigate to those pages directly from the address bar, or ctrl + f5 won't take effect. It will have to be done by clicking on the navigation button

edit: I commented before reading your write up, thank you

@mertalev mertalev changed the title Assets now have permament URL Assets now have permanent URL Apr 10, 2024
@alextran1502
Copy link
Contributor

alextran1502 commented Apr 10, 2024

Hello @midzelis, I was running some functional testing of this PR. There are a few things/bugs I noticed

  1. Clicking on Explore/Map loads the same route
  2. Go to Explore > click on a Place > click on an asset to enter the detail view > Ctrl + R will show an error prompt.
  3. Search for an asset > enter the detail view > click on the close button on upper top left corner > the detail view will not be close
  4. Search for an asset > enter the detail view > Ctrl + F5 > the detail view disappear, return to the search result grid but no result is shown because the path is now having the following value /search/photos/80a7129a-26f1-4dbf-a533-71192ffb69fd?query=%7B"query"%3A"test"%7D
  5. When navigating between assets, I can see the loading bar on top, I would like this to be removed when navigating between assets.

Let's fix those first before another round of testing 😄

Thanks again for the amazing work!

@danieldietzler danieldietzler changed the title Assets now have permanent URL feat(web): assets now have a permanent URL Apr 10, 2024
@benmccann
Copy link
Contributor

nice! switching from [..rest] to [[optional]] made this much cleaner!

However, because of the way sveltekit does routing, if there is common functionality that cross-cuts multiple routes (i.e. the /photos/:assetId part) - you have to add that loader to each +page.ts.

I haven't checked if you could apply it in this PR, but you can share functionality without sharing URL paths with groups: https://kit.svelte.dev/docs/advanced-routing#advanced-layouts-group. However, it applies all or none to a directory still and so if you wanted to apply it to a subset of routes, as I think may be the case here, it may not be of use

sveltekit doesn't expose https://github.com/sveltejs/kit/blob/main/packages/kit/src/utils/routing.js

I've tried 😄 It's contentious though. I think the concerns are that exposing an API would reduce our flexibility to make changes. Now that things are a bit more stable and we've had multiple major releases maybe it could be revisited at some point - though everyone's pretty busy with other things so I doubt it'd happen soon

Copy link
Member

@danieldietzler danieldietzler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm really liking the code so far! :)


$: albumId = ($page.route?.id === '/(user)/albums/[albumId]' || undefined) && $page.params.albumId;
$: isShare = $page.route?.id === '/(user)/share/[key]' || undefined;
$: albumId = isAlbumsRoute($page.route?.id) ? $page.params.albumId : undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could probably simplify this

Suggested change
$: albumId = isAlbumsRoute($page.route?.id) ? $page.params.albumId : undefined;
$: albumId = isAlbumsRoute($page.route?.id) && $page.params.albumId;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this not work?

Comment on lines 12 to 16
export const isPhotosRoute = (route?: string | null) => route?.startsWith('/(user)/photos/[[assetId=id]]') || false;
export const isSharedLinkRoute = (route?: string | null) => route?.startsWith('/(user)/share/[key]') || false;
export const isSearchRoute = (route?: string | null) => route?.startsWith('/(user)/search') || false;
export const isAlbumsRoute = (route?: string | null) => route?.startsWith('/(user)/albums/[albumId=id]') || false;
export const isPeopleRoute = (route?: string | null) => route?.startsWith('/(user)/people/[personId]') || false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could remove all those || false and instead write !!route?.startsWith(...). Probably up to personal preference though

export const isAssetViewerRoute = (target?: NavigationTarget | null) =>
(target?.route.id?.endsWith('/[[assetId=id]]') && 'assetId' in (target?.params || {})) || false;

export async function getAssetInfoFromParam(params: { assetId?: string }) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export async function getAssetInfoFromParam(params: { assetId?: string }) {
export async function getAssetInfoFromParam({ assetId }: { assetId?: string }) {

const { assetId } = route;
const next = assetId ? currentUrlReplaceAssetId(assetId) : currentUrlWithoutAsset();
if (next !== currentUrl()) {
void goto(next, { replaceState: false });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't this function async and we're awaiting the goto?

web/src/params/id.ts Show resolved Hide resolved
on:close={() => assetViewingStore.showAssetViewer(false)}
isShared={false}
/>
{#await import('../../../../../lib/components/asset-viewer/asset-viewer.svelte') then AssetViewer}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we... not do that? lol
An absolute import would be much nicer here haha. Also, why do even need to import it inline?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it was imported via this await block in order to speed initial page load

Copy link
Member

@danieldietzler danieldietzler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

Comment on lines 394 to 403
if (!bucketInfo) {
let date = fromLocalDateTime(localDateTime);
if (this.options.size == TimeBucketSize.Month) {
date = date.set({ day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 });
} else if (this.options.size == TimeBucketSize.Day) {
date = date.set({ hour: 0, minute: 0, second: 0, millisecond: 0 });
}
await this.loadBucket(date.toISO()!, BucketPosition.Unknown);
}
return this.assetToBucket[id] || null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!bucketInfo) {
let date = fromLocalDateTime(localDateTime);
if (this.options.size == TimeBucketSize.Month) {
date = date.set({ day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 });
} else if (this.options.size == TimeBucketSize.Day) {
date = date.set({ hour: 0, minute: 0, second: 0, millisecond: 0 });
}
await this.loadBucket(date.toISO()!, BucketPosition.Unknown);
}
return this.assetToBucket[id] || null;
if (bucketInfo) {
return bucketInfo;
}
let date = fromLocalDateTime(localDateTime);
if (this.options.size == TimeBucketSize.Month) {
date = date.set({ day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 });
} else if (this.options.size == TimeBucketSize.Day) {
date = date.set({ hour: 0, minute: 0, second: 0, millisecond: 0 });
}
await this.loadBucket(date.toISO()!, BucketPosition.Unknown);
return this.assetToBucket[id] || null;

web/src/params/id.ts Show resolved Hide resolved
@midzelis
Copy link
Contributor Author

I was also thinking that maybe we don't want to use the raw UUID in the url.

If you're messing around in the database (for things like development or troubleshooting) it's really nice to be able to just slap a UUID straight into your browser, so IMO we should just keep them raw.

@bo0tzz
fyi @alextran1502

After thinking about it, I would vote for a move away from UUIDs entirely. I would say, use nanoid to generate the primary key - and store that nanoid id in the db.

Personally, I don't think we need 340 undecillion records (340,282,366,920,938,000,000,000,000,000,000,000,000) in the db ;-)

Using nanoid, with just 12 characters (which is generous), we get 9B ids before repetition, or if you are uploading 1000 pictures/hour, it will take 1,000 years to exhaust. You can play with a calculator here: https://zelark.github.io/nano-id-cc/ (Honestly, 8chars which is 2M ids is probably enough)

And you could make it dynamic - if you run out of ids, just bump the length, and generate new ids with the new size.

To handle collisions, there would need to be a retry mechanism on insert. Try to insert with generated id, if collision, then generate a new id and try again.

I think I'll prototype this in another PR to see how it works. The beauty is that we don't need to update any of the existing ids - they can still work. But new assets will have smaller ids. Or, we could run a script to update all the existing ids to the new format, since nobody really has links to assets by id anyways, but there could be other API integrations out there - so I would make it a total optional thing, like the storage template migration job.

@midzelis midzelis mentioned this pull request Apr 15, 2024
@alextran1502 alextran1502 enabled auto-merge (squash) April 24, 2024 19:08
Copy link
Member

@danieldietzler danieldietzler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one! :)

server/test/assets Outdated Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants