Release 2.3.0
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
202813a1e6
commit
f9a7839ad3
@@ -10,6 +10,10 @@
|
||||
* A failed request is retried after 1, 2, 4, 8 and 16 seconds; after that the file waits for its
|
||||
* Retry button, which picks up from the chunk the server last confirmed. The server answers 409
|
||||
* with its own count when a chunk skips ahead, and acknowledges a chunk it already has.
|
||||
*
|
||||
* The private text stays in this component (Alpine only, never a Livewire property) until the share
|
||||
* is created: `submit()` registers its size, sends it through the same queue as a file named
|
||||
* text.txt, and only then asks the component to create the share.
|
||||
*/
|
||||
const RETRY_DELAYS = [1000, 2000, 4000, 8000, 16000]
|
||||
|
||||
@@ -33,6 +37,14 @@ document.addEventListener('alpine:init', () => {
|
||||
/** The request on its way, so Cancel and Remove can abort it. */
|
||||
request: null,
|
||||
|
||||
/** The private text, as typed. */
|
||||
text: '',
|
||||
|
||||
/** The private text's size as the server counts it, in UTF-8 bytes. */
|
||||
get textBytes() {
|
||||
return new TextEncoder().encode(this.text).length
|
||||
},
|
||||
|
||||
get progress() {
|
||||
const unfinished = Object.values(this.uploads).filter((upload) => upload.state !== 'failed')
|
||||
const size = unfinished.reduce((total, upload) => total + upload.size, 0)
|
||||
@@ -223,6 +235,43 @@ document.addEventListener('alpine:init', () => {
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Send the private text, if there is one, then create the share. The text replaces any the
|
||||
* server kept from an earlier attempt, so what is shared is what the field holds now.
|
||||
*/
|
||||
async submit() {
|
||||
if (this.busy) {
|
||||
return
|
||||
}
|
||||
|
||||
const text = this.text.trim() === '' ? '' : this.text
|
||||
const target = await this.$wire.registerText(text === '' ? 0 : this.textBytes)
|
||||
|
||||
if (text !== '') {
|
||||
if (! target) {
|
||||
return
|
||||
}
|
||||
|
||||
const item = { id: target.id, file: new File([text], 'text.txt', { type: 'text/plain' }), target, nextIndex: 0 }
|
||||
|
||||
this.uploads[target.id] = { state: 'queued', sent: 0, size: item.file.size }
|
||||
this.queue.push(item)
|
||||
await this.run()
|
||||
|
||||
const sent = this.uploads[target.id]?.state === 'uploaded'
|
||||
|
||||
this.forget([target.id])
|
||||
|
||||
if (! sent) {
|
||||
window.materialToast(messages.textFailed, { type: 'error' })
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await this.$wire.createShare()
|
||||
},
|
||||
|
||||
retry(id) {
|
||||
const item = this.failed[id]
|
||||
|
||||
@@ -283,7 +332,7 @@ document.addEventListener('alpine:init', () => {
|
||||
},
|
||||
|
||||
warnBeforeLeaving(event) {
|
||||
if (this.busy) {
|
||||
if (this.busy || this.text !== '') {
|
||||
event.preventDefault()
|
||||
event.returnValue = ''
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user