mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-28 00:47:07 +00:00
Keep the share email's button link within what mail clients tolerate
The "Open Puter" link names every item in the mail, capped at twenty. But a single item's parameter is ~150 characters once the owner, uuid and an encoded name are in it, so twenty of them run to several kilobytes — past the ~2000 characters where older mail clients cut a URL off or stop making it clickable — and the count alone couldn't do what its comment promised for the mail's primary button. Add a length budget beside the count: items go in, in digest order, only while the whole link stays under it, so the button always works and the first items are the ones highlighted. A single item always fits.
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
||||
maskedSharePath,
|
||||
ownerFromSharePath,
|
||||
SHARE_DEEP_LINK_ITEMS_LIMIT,
|
||||
SHARE_DEEP_LINK_MAX_LENGTH,
|
||||
shareDeepLink,
|
||||
sharedViewLink,
|
||||
shareTargetLink,
|
||||
@@ -137,6 +138,26 @@ describe('sharedViewLink', () => {
|
||||
).searchParams.getAll('shared');
|
||||
expect(shared).toEqual(paths.slice(0, SHARE_DEEP_LINK_ITEMS_LIMIT));
|
||||
});
|
||||
|
||||
// Twenty ordinary names already run to several kilobytes once encoded, so
|
||||
// the count alone is no guard; the link itself has to stay short enough.
|
||||
it('stops adding items before the link outgrows what mail clients tolerate', () => {
|
||||
const paths = Array.from(
|
||||
{ length: SHARE_DEEP_LINK_ITEMS_LIMIT },
|
||||
(_, i) => `/alice/${UID}/${'quarterly report '.repeat(8)}${i}.pdf`,
|
||||
);
|
||||
const link = sharedViewLink('https://puter.com', paths);
|
||||
expect(link.length).toBeLessThanOrEqual(SHARE_DEEP_LINK_MAX_LENGTH);
|
||||
const shared = new URL(link).searchParams.getAll('shared');
|
||||
// Only a leading run made it — the first items, none skipped.
|
||||
expect(shared.length).toBeGreaterThan(1);
|
||||
expect(shared.length).toBeLessThan(paths.length);
|
||||
expect(shared).toEqual(paths.slice(0, shared.length));
|
||||
// The next item would not have fit.
|
||||
expect(
|
||||
link.length + `&shared=${encodeURIComponent(paths[shared.length])}`.length,
|
||||
).toBeGreaterThan(SHARE_DEEP_LINK_MAX_LENGTH);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shareTargetLink', () => {
|
||||
|
||||
@@ -54,12 +54,20 @@ export const maskedSharePath = (target: ShareTarget): string | null => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Items one link will highlight. Past this the link still opens Shared, just
|
||||
* without picking the rest out — a query string of several kilobytes is where
|
||||
* mail clients start truncating or refusing to make it clickable.
|
||||
* Items one link will highlight, at most. Past this the link still opens
|
||||
* Shared, just without picking the rest out.
|
||||
*/
|
||||
export const SHARE_DEEP_LINK_ITEMS_LIMIT = 20;
|
||||
|
||||
/**
|
||||
* How long a link may run, in characters. Somewhere past two thousand, older
|
||||
* mail clients cut a URL off or stop making it clickable — and this is the
|
||||
* button — so items are added only while the whole link stays within this. A
|
||||
* single item always fits: a username, a uuid and a filename come to a few
|
||||
* hundred, and it takes long names for the count above to matter first.
|
||||
*/
|
||||
export const SHARE_DEEP_LINK_MAX_LENGTH = 2000;
|
||||
|
||||
/**
|
||||
* A link that opens the recipient's Shared view with `paths` highlighted, once
|
||||
* they are signed in. Only masked paths travel — each one's second segment is
|
||||
@@ -67,18 +75,23 @@ export const SHARE_DEEP_LINK_ITEMS_LIMIT = 20;
|
||||
* with the first. With no paths the link still lands on Shared.
|
||||
*/
|
||||
export const sharedViewLink = (origin: string, paths: string[]): string => {
|
||||
const base = origin.replace(/\/+$/, '');
|
||||
const unique = [...new Set(paths)].slice(0, SHARE_DEEP_LINK_ITEMS_LIMIT);
|
||||
const query =
|
||||
unique.length === 0
|
||||
? `${SHARE_DEEP_LINK_PARAM}=`
|
||||
: unique
|
||||
.map(
|
||||
(path) =>
|
||||
`${SHARE_DEEP_LINK_PARAM}=${encodeURIComponent(path)}`,
|
||||
)
|
||||
.join('&');
|
||||
return `${base}/?${query}`;
|
||||
const base = `${origin.replace(/\/+$/, '')}/?`;
|
||||
// The first items that fit, in order — never a later one over an
|
||||
// earlier, so what is highlighted reads as the top of the list.
|
||||
const params: string[] = [];
|
||||
let length = base.length;
|
||||
for (const path of new Set(paths)) {
|
||||
if (params.length === SHARE_DEEP_LINK_ITEMS_LIMIT) break;
|
||||
const param = `${SHARE_DEEP_LINK_PARAM}=${encodeURIComponent(path)}`;
|
||||
const added = param.length + (params.length === 0 ? 0 : '&'.length);
|
||||
if (length + added > SHARE_DEEP_LINK_MAX_LENGTH) break;
|
||||
params.push(param);
|
||||
length += added;
|
||||
}
|
||||
return (
|
||||
base +
|
||||
(params.length === 0 ? `${SHARE_DEEP_LINK_PARAM}=` : params.join('&'))
|
||||
);
|
||||
};
|
||||
|
||||
/** A link that opens `path`: the Shared view with that one item highlighted. */
|
||||
|
||||
Reference in New Issue
Block a user