// The server caps how long a new share link may live (`share_max_ttl_days` in // /api/capabilities, set by the admin under Protection; default 7 days). The // dialogs read it so they only OFFER expiries the server will honour — a // "30 days" option that quietly becomes 7 days is a lie on the screen, and a // "Never" option that becomes a week is a worse one. // // One helper for every surface: the Share / Permissions panel, the standalone // share dialog, the desktop app and the embeds all render the same choices // from the same rule. A surface that clamps differently would mean the same // product behaves two ways. import { formatInstant } from '../composables/useLocale'; /* zaman:z1 */ export interface ExpiryOption { /** Days; 0 = never. */ v: number; l: string; } /** The stock expiry choices before the ceiling is applied (days; 0 = never). */ export const STOCK_EXPIRY_DAYS = [0, 1, 7, 30]; /** * clampExpiryOptions returns the options a dialog may show under a ceiling * of `maxDays` (0/undefined = no ceiling → every option as is). Options past * the ceiling and "never" are dropped; the ceiling itself is added as the * longest choice when the stock list does not already contain it. */ export function clampExpiryOptions( days: number[], maxDays: number | undefined, label: (days: number) => string, ): ExpiryOption[] { const max = maxDays && maxDays > 0 ? Math.floor(maxDays) : 0; let list = max ? days.filter((d) => d > 0 && d <= max) : days.slice(); if (max && !list.includes(max)) list.push(max); list = Array.from(new Set(list)).sort((a, b) => (a === 0 ? -1 : b === 0 ? 1 : a - b)); return list.map((d) => ({ v: d, l: label(d) })); } /** * defaultExpiryDays is what a fresh dialog preselects: the ceiling when there * is one (the server would apply it anyway — showing it up front is honest), * otherwise "never". */ export function defaultExpiryDays(maxDays: number | undefined): number { return maxDays && maxDays > 0 ? Math.floor(maxDays) : 0; } /** * clampExpiryDate pulls a free-form expiry (datetime input) under the ceiling. * Returns the ISO string to send, or null for "never" when there is no * ceiling. `now` is injectable for tests. */ export function clampExpiryDate( chosen: Date | null, maxDays: number | undefined, now: Date = new Date(), ): { iso: string | null; clamped: boolean } { const max = maxDays && maxDays > 0 ? Math.floor(maxDays) : 0; if (!max) return { iso: chosen ? chosen.toISOString() : null, clamped: false }; const limit = new Date(now.getTime() + max * 86400000); if (!chosen || chosen.getTime() > limit.getTime()) return { iso: limit.toISOString(), clamped: true }; return { iso: chosen.toISOString(), clamped: false }; } /** Value for a `` under the ceiling (local time, minute precision). */ export function expiryInputMax(maxDays: number | undefined, now: Date = new Date()): string | undefined { const max = maxDays && maxDays > 0 ? Math.floor(maxDays) : 0; if (!max) return undefined; const limit = new Date(now.getTime() + max * 86400000); const pad = (n: number) => String(n).padStart(2, '0'); return `${limit.getFullYear()}-${pad(limit.getMonth() + 1)}-${pad(limit.getDate())}T${pad(limit.getHours())}:${pad(limit.getMinutes())}`; } /** "Valid until 30 Aug 2026, 14:05" / "Does not expire" from the server's `expires_at`. */ export function validUntilLine( expiresAt: string | null | undefined, locale: 'tr' | 'en', /** The explorer whose clock applies (lib/timezone EXPLORER_CLOCK). */ clock?: symbol, ): string { if (!expiresAt) return locale === 'tr' ? 'Bu bağlantının süresi yoktur.' : 'This link does not expire.'; const d = new Date(expiresAt); // zaman:z1 — the viewer's chosen clock, not the browser's. "Valid until // 14:05" is a deadline, and a deadline printed in somebody else's zone is // the worst kind of wrong: it looks actionable. Both halves of that — the // zone AND the locale tag — come from `formatInstant`, which is the one // place either is decided. This file used to pass the zone correctly and // then map the tag itself, to 'en-GB', while useLocale mapped it to // 'en-US': the same deadline was spelled "20 Sept 2026, 13:53" in the share // dialog and "Sep 20, 2026, 1:53 PM" in the listing behind it. const when = Number.isNaN(d.getTime()) ? expiresAt : formatInstant(d, locale, { dateStyle: 'medium', timeStyle: 'short' }, clock); return locale === 'tr' ? `Bu bağlantı ${when} tarihine kadar geçerli.` : `This link is valid until ${when}.`; } /** The hint under an expiry control: what the server will allow at most. */ export function ttlCeilingHint(maxDays: number | undefined, locale: 'tr' | 'en'): string { const max = maxDays && maxDays > 0 ? Math.floor(maxDays) : 0; if (!max) return ''; return locale === 'tr' ? `Bağlantılar en fazla ${max} gün geçerli olabilir (sunucu ayarı).` : `Links can be valid for at most ${max} day${max === 1 ? '' : 's'} (server setting).`; } /** * The share dialog's muted detail line: its facts joined with " · ". * * ⚠ The first fact is a whole sentence ("This link is valid until …, 10:00 * AM.") and the ones after it are fragments ("3 downloads"), so a plain join * printed "10:00 AM. · 3 downloads" — a full stop in the middle of a line. A * fact that is followed by another loses its closing full stop; the last one * keeps whatever it has. */ export function shareDetailLine(facts: Array): string { const kept = facts.filter((f): f is string => !!f); return kept.map((f, i) => (i < kept.length - 1 ? f.replace(/\.\s*$/, '') : f)).join(' · '); }