A share can now hold a private text (a password, a key, a short note) with its files or on its own. The upload page's new "Private text" card takes up to 100 KB; the browser encrypts the text and sends it through the same chunk pipeline as a file, flagged is_text on share_files, so it gets the share's password, expiry, download limit and cleanup. The recipient sees the text only after pressing "Show text", which counts as their download, so a messenger link preview cannot use up a share limited to one download. The text is left out of the file list, the ZIP and the file counts; the admin dashboard marks shares that hold one with "Text". The website gains a Private text feature card and a fifth phone screenshot; every screenshot is retaken. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
130 lines
7.9 KiB
PHP
130 lines
7.9 KiB
PHP
{{-- The page a recipient opens. No anchored components (menus, tooltips) on it: it has to work on
|
|
iOS before Safari 18.4, which cannot position them. --}}
|
|
|
|
<x-page brand>
|
|
{{-- Under the page's h1, what the recipient acts on is in cards: the password, or the share's
|
|
private text and its files, each in its own card, with the share's expiry and limit under them. --}}
|
|
@if (! $authenticated)
|
|
<x-card :title="__('Password Required')" :subtitle="__('Enter the password to open this share')" heading="h2" variant="outlined">
|
|
<x-form wire:submit="verifyPassword">
|
|
<x-password
|
|
full
|
|
wire:model="password"
|
|
:label="__('Password')"
|
|
required
|
|
autocomplete="off"
|
|
:placeholder="__('Enter share password')"
|
|
/>
|
|
|
|
<x-slot:actions>
|
|
<x-button type="submit" :label="__('Unlock')" variant="filled" icon="lock_open" spinner="verifyPassword" />
|
|
</x-slot:actions>
|
|
</x-form>
|
|
</x-card>
|
|
@else
|
|
{{-- A download link does not render the page again: the download limit's note switches on
|
|
the first press of any download or "Show text", and the server draws the open window on
|
|
the next visit. --}}
|
|
<x-stack gap="space200" x-data="{ downloaded: false }">
|
|
{{-- The text is fetched only when the recipient asks for it, never with the page: a link
|
|
preview must not use up a share limited to one download. It stays in Alpine and is
|
|
drawn as plain text. --}}
|
|
@if ($hasText)
|
|
<x-card :title="__('Private text')" heading="h2" variant="outlined">
|
|
<x-stack
|
|
gap="space200"
|
|
x-data="{
|
|
text: null,
|
|
async reveal() {
|
|
const text = await $wire.revealText();
|
|
|
|
if (text === null) {
|
|
window.materialToast({{ \Illuminate\Support\Js::from(__('This text is no longer available.')) }}, { type: 'error' });
|
|
|
|
return;
|
|
}
|
|
|
|
this.text = text;
|
|
downloaded = true;
|
|
},
|
|
}"
|
|
>
|
|
<template x-if="text !== null">
|
|
<x-stack gap="space200">
|
|
<div class="share-text md-type-body-lg" x-text="text" data-test="shared-text"></div>
|
|
|
|
<x-row justify="end">
|
|
<x-button
|
|
:label="__('Copy')"
|
|
icon="content_copy"
|
|
variant="tonal"
|
|
x-on:click="navigator.clipboard.writeText(text).then(() => window.materialToast({{ \Illuminate\Support\Js::from(__('Copied to the clipboard')) }}, { type: 'success' }))"
|
|
data-test="copy-text"
|
|
/>
|
|
</x-row>
|
|
</x-stack>
|
|
</template>
|
|
|
|
<x-row justify="end" x-show="text === null">
|
|
<x-button :label="__('Show text')" icon="visibility" variant="filled" x-on:click="reveal()" spinner="revealText" data-test="show-text" />
|
|
</x-row>
|
|
</x-stack>
|
|
</x-card>
|
|
@endif
|
|
|
|
@if ($files->isNotEmpty())
|
|
<x-card :title="__('Shared Files')" heading="h2" variant="outlined">
|
|
<x-stack gap="space200">
|
|
<x-list :label="__('Shared Files')">
|
|
@foreach ($files as $file)
|
|
<x-list-item
|
|
:title="$file->relative_path ?: $file->original_name"
|
|
icon="description"
|
|
wire:key="file-{{ $file->id }}"
|
|
>
|
|
<x-slot:description><span class="md-tabular">{{ Number::fileSize($file->file_size) }}</span></x-slot:description>
|
|
<x-slot:end>
|
|
<x-button icon="download" :link="route('share.download.file', [$share, $file])" no-wire-navigate :aria-label="__('Download :name', ['name' => $file->original_name])" x-on:click="downloaded = true" />
|
|
</x-slot:end>
|
|
</x-list-item>
|
|
@endforeach
|
|
</x-list>
|
|
|
|
<x-row justify="end">
|
|
@if ($files->count() > 1)
|
|
<x-button :label="__('Download All as ZIP')" icon="download" variant="filled" :link="route('share.download.all', $share)" no-wire-navigate x-on:click="downloaded = true" />
|
|
@else
|
|
<x-button :label="__('Download')" icon="download" variant="filled" :link="route('share.download.file', [$share, $files->first()])" no-wire-navigate x-on:click="downloaded = true" />
|
|
@endif
|
|
</x-row>
|
|
</x-stack>
|
|
</x-card>
|
|
@endif
|
|
|
|
@if ($share->expires_at || $share->max_downloads)
|
|
<x-stack gap="space100">
|
|
@if ($share->expires_at)
|
|
<p class="md-type-body-sm md-ink-variant">{{ __('Expires') }}: {{ $share->expires_at->diffForHumans() }}</p>
|
|
@endif
|
|
|
|
@if ($share->max_downloads)
|
|
@if ($hasText)
|
|
@if ($downloadWindowEndsAt)
|
|
<p class="md-type-body-sm md-ink-variant">{{ __('You can open this share for another :time.', ['time' => $downloadWindowEndsAt->diffForHumans(syntax: \Carbon\CarbonInterface::DIFF_ABSOLUTE)]) }}</p>
|
|
@elseif ($remainingDownloads > 0)
|
|
<p class="md-type-body-sm md-ink-variant" x-show="! downloaded">{{ trans_choice('{1} Showing the text or downloading uses the last remaining download. You then have :window to open this share.|[2,*] Showing the text or downloading uses 1 of :count remaining downloads. You then have :window to open this share.', $remainingDownloads, ['window' => $downloadWindow]) }}</p>
|
|
<p class="md-type-body-sm md-ink-variant" x-show="downloaded" x-cloak>{{ __('You have :window to open this share.', ['window' => $downloadWindow]) }}</p>
|
|
@endif
|
|
@elseif ($downloadWindowEndsAt)
|
|
<p class="md-type-body-sm md-ink-variant">{{ __('You can download these files for another :time.', ['time' => $downloadWindowEndsAt->diffForHumans(syntax: \Carbon\CarbonInterface::DIFF_ABSOLUTE)]) }}</p>
|
|
@elseif ($remainingDownloads > 0)
|
|
<p class="md-type-body-sm md-ink-variant" x-show="! downloaded">{{ trans_choice('{1} Downloading uses the last remaining download. You then have :window to download the files.|[2,*] Downloading uses 1 of :count remaining downloads. You then have :window to download the files.', $remainingDownloads, ['window' => $downloadWindow]) }}</p>
|
|
<p class="md-type-body-sm md-ink-variant" x-show="downloaded" x-cloak>{{ __('You have :window to download the files.', ['window' => $downloadWindow]) }}</p>
|
|
@endif
|
|
@endif
|
|
</x-stack>
|
|
@endif
|
|
</x-stack>
|
|
@endif
|
|
</x-page>
|