liveview-native / liveview-native-core

Provides core language-agnostic functionality for LiveView Native across platforms
MIT License
146 stars 10 forks source link

LiveView Native Clients: Support file uploading #15

Closed AZholtkevych closed 9 months ago

AZholtkevych commented 1 year ago

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

This will be a port of https://github.com/phoenixframework/phoenix_live_view/pull/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.

AZholtkevych commented 11 months ago

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 https://github.com/liveview-native/liveview-native-core/issues/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 https://github.com/liveview-native/liveview-native-core/issues/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