Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LiveView Native Clients: Support file uploading #15

Closed
AZholtkevych opened this issue Jun 5, 2023 · 1 comment · Fixed by liveview-native/phoenix-channels-client#78 or #61
Closed
Assignees
Labels
LIVEVIEWNATIVE CLIENTS LiveViewNative Clients Core issues label phoenix_live_view:0.19 https://www.phoenixframework.org/blog/phoenix-liveview-0.19-released

Comments

@AZholtkevych
Copy link

AZholtkevych commented Jun 5, 2023

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.

@AZholtkevych AZholtkevych converted this from a draft issue Jun 5, 2023
@AZholtkevych AZholtkevych changed the title Support file uploading LVN blockers: Support file uploading Jun 5, 2023
@AZholtkevych AZholtkevych changed the title LVN blockers: Support file uploading LiveView Native SwiftUI blockers: Support file uploading Jun 5, 2023
@KronicDeth KronicDeth added the phoenix_live_view:0.19 https://www.phoenixframework.org/blog/phoenix-liveview-0.19-released label Jun 8, 2023
@AZholtkevych AZholtkevych mentioned this issue Jun 12, 2023
26 tasks
@AZholtkevych AZholtkevych changed the title LiveView Native SwiftUI blockers: Support file uploading LiveView Native SwiftUI: Support file uploading Jun 15, 2023
@AZholtkevych AZholtkevych moved this from Todo to In Progress in LiveViewNative Core Aug 8, 2023
@AZholtkevych AZholtkevych assigned simlay and unassigned Bajix Sep 11, 2023
@AZholtkevych AZholtkevych changed the title LiveView Native SwiftUI: Support file uploading LiveView Native Clients: Support file uploading Sep 12, 2023
@AZholtkevych AZholtkevych added the LIVEVIEWNATIVE CLIENTS LiveViewNative Clients Core issues label label Sep 13, 2023
@AZholtkevych AZholtkevych moved this from In Progress to Todo in LiveViewNative Core Sep 27, 2023
@AZholtkevych AZholtkevych moved this from Todo to In Progress in LiveViewNative Core Nov 22, 2023
@AZholtkevych AZholtkevych moved this from In Progress to Blocked in LiveViewNative Core Nov 27, 2023
@AZholtkevych
Copy link
Author

AZholtkevych commented Nov 27, 2023

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

@AZholtkevych AZholtkevych moved this from Done to In Progress in LiveViewNative Core Jan 24, 2024
@simlay simlay closed this as completed in #61 Feb 6, 2024
@github-project-automation github-project-automation bot moved this from In Progress to Done in LiveViewNative Core Feb 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LIVEVIEWNATIVE CLIENTS LiveViewNative Clients Core issues label phoenix_live_view:0.19 https://www.phoenixframework.org/blog/phoenix-liveview-0.19-released
Projects
Status: Done
4 participants