liveview-native / liveview-native-core

Provides core language-agnostic functionality for LiveView Native across platforms

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

LiveView Native Clients: Support file uploading

AZholtkevych opened this issue · comments

Handle uploads as described in https://hexdocs.pm/phoenix_live_view/uploads.html.

This will be a port of phoenixframework/phoenix_live_view#1184 and any subsequent fixes.

From analysis of the phoenix_live_view js, it appears that the uploads are chunked and chunks are sent to a special lvu:* namespace

https://github.com/phoenixframework/phoenix_live_view/blob/45bd9bd23dcd4524a328950f511ff30f8382c4dd/assets/js/phoenix_live_view/entry_uploader.js#L12

 this.uploadChannel = liveSocket.channel(`lvu:${entry.ref}`, {token: entry.metadata()})

On joining this channel, the next chunk is read

https://github.com/phoenixframework/phoenix_live_view/blob/45bd9bd23dcd4524a328950f511ff30f8382c4dd/assets/js/phoenix_live_view/entry_uploader.js#L24-L42

      .receive("ok", _data => this.readNextChunk())
      .receive("error", reason => this.error(reason))
  }

  isDone(){ return this.offset >= this.entry.file.size }

  readNextChunk(){
    let reader = new window.FileReader()
    let blob = this.entry.file.slice(this.offset, this.chunkSize + this.offset)
    reader.onload = (e) => {
      if(e.target.error === null){
        this.offset += e.target.result.byteLength
        this.pushChunk(e.target.result)
      } else {
        return logError("Read error: " + e.target.error)
      }
    }
    reader.readAsArrayBuffer(blob)
  }

And the chunk is pushed

https://github.com/phoenixframework/phoenix_live_view/blob/45bd9bd23dcd4524a328950f511ff30f8382c4dd/assets/js/phoenix_live_view/entry_uploader.js#L36-L54

        this.pushChunk(e.target.result)
      } else {
        return logError("Read error: " + e.target.error)
      }
    }
    reader.readAsArrayBuffer(blob)
  }

  pushChunk(chunk){
    if(!this.uploadChannel.isJoined()){ return }
    this.uploadChannel.push("chunk", chunk)
      .receive("ok", () => {
        this.entry.progress((this.offset / this.entry.file.size) * 100)
        if(!this.isDone()){
          this.chunkTimer = setTimeout(() => this.readNextChunk(), this.liveSocket.getLatencySim() || 0)
        }
      })
  }
}

The percentage progress is pushed by the calle to this.entry.progress if the push of the "chunk" event is OK

https://github.com/phoenixframework/phoenix_live_view/blob/45bd9bd23dcd4524a328950f511ff30f8382c4dd/assets/js/phoenix_live_view/upload_entry.js#L45-L61

  progress(progress){
    this._progress = Math.floor(progress)
    if(this._progress > this._lastProgressSent){
      if(this._progress >= 100){
        this._progress = 100
        this._lastProgressSent = 100
        this._isDone = true
        this.view.pushFileProgress(this.fileEl, this.ref, 100, () => {
          LiveUploader.untrackFile(this.fileEl, this.file)
          this._onDone()
        })
      } else {
        this._lastProgressSent = this._progress
        this.view.pushFileProgress(this.fileEl, this.ref, this._progress)
      }
    }
  }

The pushFileProgress both updates the element locally and pushes the progress to the server, allowing server-side changes of the progress

https://github.com/phoenixframework/phoenix_live_view/blob/45bd9bd23dcd4524a328950f511ff30f8382c4dd/assets/js/phoenix_live_view/view.js#L877-L887

  pushFileProgress(fileEl, entryRef, progress, onReply = function (){ }){
    this.liveSocket.withinOwners(fileEl.form, (view, targetCtx) => {
      view.pushWithReply(null, "progress", {
        event: fileEl.getAttribute(view.binding(PHX_PROGRESS)),
        ref: fileEl.getAttribute(PHX_UPLOAD_REF),
        entry_ref: entryRef,
        progress: progress,
        cid: view.targetComponentID(fileEl.form, targetCtx)
      }, onReply)
    })
  }

This server-side channels and event handling of those channels does and should not change for native. While the framework lvu:* channels handle the chunks, any "progress" events can be optionally handled by the LiveView on the server, but it is not required as shown in the guide.

According to @simlay and @bcardarella it is blocked by #16 :
https://dockyard.slack.com/archives/C02E1GA5THB/p1701103552151419?thread_ts=1700864205.927549&cid=C02E1GA5THB

replied to a thread:
So, I’ve been working on #15 and am somewhat unclear what a liveview native template (with an upload) will look like. Following https://hexdocs.pm/phoenix_live_view/uploads.html#allow-uploads and then reverse engineering what’s happening. The <.live_file_input upload={@uploads.avatar} /> bit adds a data-phx-upload-ref key on the dom, this is then used as a message on the channel to get the actual token for the upload. It’s a bit unclear on how to get the value for the data-phx-upload-refkey.
After my research on 15 (Support file uploading), I I think it’s blocked by #16
@alex.zholtkevych
#16 LiveView Native Clients: LiveView Native Core to use Phoenix.Channels client
https://github.com/liveview-native/liveview-client-swiftui/blob/4e81900adff68228d3c5ef2b657ef0c37f95723e/Sources/LiveViewNative/LiveView.swift#L52 - example
Assignees
@KronicDeth
Labels
enhancement, ffi:swift, ffi:kotlin, LIVEVIEWNATIVE CLIENTS
https://github.com/[liveview-native/liveview-native-core](https://github.com/liveview-native/liveview-native-core)|liveview-native/liveview-native-coreliveview-native/liveview-native-core | Jun 5th | Added by GitHub
View newer replies

bcardarella
11:55 AM
This is most likely correct, sorry I owed you a response and got pulled into family stuff over the past few days

sebastian.imlay
11:55 AM
Stupid holidays. No one likes family time (I kid)

bcardarella
11:56 AM
it definitely gets in the way

sebastian.imlay
11:57 AM
I don’t know enough about elixir’s channels to say what fully needs to happen but I could see a need for <.live_native_file_input upload={@uploads.avatar} /> (I dunno how to do this) in the template which DOM attributes needed to know what channel(s) to send the file over.

bcardarella
11:58 AM
so here is a breakdown of the uploader as I'm reading it in the JS
11:58
UploadEntry is part of the LV client: https://github.com/phoenixframework/phoenix_live_view/blob/main/assets/js/phoenix_live_view/upload_entry.js
upload_entry.js
import {
PHX_ACTIVE_ENTRY_REFS,
PHX_LIVE_FILE_UPDATED,
PHX_PREFLIGHTED_REFS
} from "./constants"
Show more
https://github.com/[phoenixframework/phoenix_live_view](https://github.com/phoenixframework/phoenix_live_view)|phoenixframework/phoenix_live_viewphoenixframework/phoenix_live_view | Added by GitHub
11:58
it will then import LiveUploader from https://github.com/phoenixframework/phoenix_live_view/blob/main/assets/js/phoenix_live_view/live_uploader.js
live_uploader.js
import {
PHX_DONE_REFS,
PHX_PREFLIGHTED_REFS,
PHX_UPLOAD_REF
} from "./constants"
Show more
https://github.com/[phoenixframework/phoenix_live_view](https://github.com/phoenixframework/phoenix_live_view)|phoenixframework/phoenix_live_viewphoenixframework/phoenix_live_view | Added by GitHub
11:58
and that will then import EntryUploader https://github.com/phoenixframework/phoenix_live_view/blob/main/assets/js/phoenix_live_view/entry_uploader.js and this is the one that interacts with Channels
entry_uploader.js
import {
logError
} from "./utils"

export default class EntryUploader {
Show more
https://github.com/[phoenixframework/phoenix_live_view](https://github.com/phoenixframework/phoenix_live_view)|phoenixframework/phoenix_live_viewphoenixframework/phoenix_live_view | Added by GitHub
11:59
specifically https://github.com/phoenixframework/phoenix_live_view/blob/main/assets/js/phoenix_live_view/entry_uploader.js#L13
entry_uploader.js
this.uploadChannel = liveSocket.channel(lvu:${entry.ref}, {token: entry.metadata()})
https://github.com/[phoenixframework/phoenix_live_view](https://github.com/phoenixframework/phoenix_live_view)|phoenixframework/phoenix_live_viewphoenixframework/phoenix_live_view | Added by GitHub
11:59
the lvu:${entry.ref} could be a special channels for uploads

sebastian.imlay
11:59 AM
Yes

bcardarella
12:00 PM
confirm it is: https://github.com/phoenixframework/phoenix_live_view/blob/3fe7ddbbed7a841038b229683befb045fda87b63/lib/phoenix_live_view/socket.ex#L113
socket.ex
channel "lvu:*", Phoenix.LiveView.UploadChannel
https://github.com/[phoenixframework/phoenix_live_view](https://github.com/phoenixframework/phoenix_live_view)|phoenixframework/phoenix_live_viewphoenixframework/phoenix_live_view | Added by GitHub

bcardarella
12:00 PM
so it is a completely separate channel from the LiveView channel... so it doesn't block the primary LV channel I'm guessing and when the file upload is complete I bet it just sends the message to the primary LV channel

3 replies
Last reply today at 12:45 PMView thread

bcardarella
12:02 PM
but I agree that LVN Core Channels is the blocker here