code:
# App artifacts
/_build
/db
/deps
/*.ez

# Generated on crash by the VM
erl_crash.dump

# Generated on crash by NPM
npm-debug.log

# Static artifacts
/assets/node_modules

# Since we are building assets from assets/,
# we ignore priv/static. You may want to comment
# this depending on your deployment strategy.
/priv/static/

# Files matching config/*.secret.exs pattern contain sensitive
# data and you should not commit them into version control.
#
# Alternatively, you may comment the line below and commit the
# secrets files as long as you replace their contents by environment
# variables.
#/config/*.secret.exs
/config/config.exs
/config/dev.exs
/config/prod.exs
/config/prod.secret.exs
/config/test.exs
/uploads
/uploads-temp

repo_name: bye/ass


code:
# Qtfile

To start your Phoenix server:

  * Install dependencies with `mix deps.get`
  * Create and migrate your database with `mix ecto.create && mix ecto.migrate`
  * Install Node.js dependencies with `cd assets && npm install`
  * Start Phoenix endpoint with `mix phx.server`

Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.

Ready to run in production? Please [check our deployment guides](http://www.phoenixframework.org/docs/deployment).

## Learn more

  * Official website: http://www.phoenixframework.org/
  * Guides: http://phoenixframework.org/docs/overview
  * Docs: https://hexdocs.pm/phoenix
  * Mailing list: http://groups.google.com/group/phoenix-talk
  * Source: https://github.com/phoenixframework/phoenix

license: mit


code:
exports.config = {
    // See http://brunch.io/#documentation for docs.
    files: {
        javascripts: {
            // joinTo: "js/app.js"
            joinTo: {
                // "js/uploader.js": "js/uploader.js",
                "js/room-view.js": [
                    "js/uploader.js",
                    "js/room.js",
                    "js/socket.js",
                    "js/file-list.js",
                    "js/upload.js",
                    "js/file.js",
                    "js/file-list-view.js",
                    "js/upload-view.js",
                    "js/file-view.js",
                    "js/presence.js",
                    "js/presence-view.js",
                    "js/room-view.js",
                    "js/async-button-view.js"
                ],
                "js/app.js": /(js\/app\.js|^(?!js))/
            }

            // To use a separate vendor.js bundle, specify two files path
            // http://brunch.io/docs/config#-files-
            // joinTo: {
            //   "js/app.js": /^js/,
            //   "js/vendor.js": /^(?!js)/
            // }
            //
            // To change the order of concatenation of files, explicitly mention here
            // order: {
            //   before: [
            //     "vendor/js/jquery-2.1.1.js",
            //     "vendor/js/bootstrap.min.js"
            //   ]
            // }
        },
        stylesheets: {
            joinTo: "css/app.css"
            /*
              "css/app.css": [
              "css/app.css",
              "css/register.css"
              ]
            */
        },
        templates: {
            joinTo: "js/app.js"
        }
    },

    conventions: {
        // This option sets where we should place non-css and non-js assets in.
        // By default, we set this to "/assets/static". Files in this directory
        // will be copied to `paths.public`, which is "priv/static" by default.
        assets: /^(static)/
    },

    // Phoenix paths configuration
    paths: {
        // Dependencies and current project directories to watch
        watched: ["static", "css", "js", "vendor"],
        // Where to compile files to
        public: "../priv/static"
    },

    // Configure your plugins
    plugins: {
        babel: {
            "presets": [
                ["env", {
                    "useBuiltIns": true
                }]
            ],
            "plugins": [
                ["transform-runtime", {
                    "polyfill": false,
                    "regenerator": true
                }],
                ["transform-object-assign"]
            ]
        }
    },

    modules: {
        autoRequire: {
            "js/app.js": ["js/app"]
        }
    },

    npm: {
        enabled: true,
        whitelist: ["phoenix", "phoenix_html", "vue", "babel-runtime"]
    }
};


code:
/*This file is for your main application css*/
:root {
    --base-text: #C7C7C7; /*Everything from here to...*/
    --blue-text: #3C8FFF;
    --pink-text: #F557FF;
    --green-text: #57FF53;
    --red-text: #FF0000; /*... here is unused*/
    --qtstream: #f1d98d; /*Logo for the site*/
    --dark-background: #202127; /*Background for the whole site*/
    --dark-background2:#1A1B20; /*Used nowhere*/
    --dropzone-bg: #2d2d2d; /*Used nowhere*/
    --lighter-dark-background: #32353e; /*Used for various backgrounds, mainly the header and the login/registration forms*/
    --lighter-dark-background2: #292b32; /*This one effects the even files on the file list*/
    --odd-file: #3e424c; /*Used for the odd file coloring*/
    --del-button:#462b2d; /*Used for the delete button coloring*/
    --l-bg:#54555B; /*Used nowhere*/
    --border-color: #4f5c6d; /*Affects the header's border*/
    --pointless-border: #626262; /*Used nowhere*/
    --dimmer: rgba(255, 255, 255, 0.5); /*Is used when the page is dimmed*/
    --filter-border:#404552; /*This one effects the border of the filter on the file list*/
    --general-text:white; /*Affects the base text of the website*/
    --expire-text: #b1b5c4; /*Affects the file expiration date?*/
    /*These are visited and unvisited links*/
    --new-link: #3961F8;
    --visited-link: #9660FA;
    /*These are for the file list the first two determine the file uploader border and background. The second one effects the color of the text*/
    --uploader-boarder: #58525b;
    --uploader-bg: #273049;
    --file-link: #d2d5e0;
    /*The first one effects the font of the input text on the registration and login page when it's not focused*/
    --input-color: rgb(140,143,148);
    /*These are for the input forms on the login and registration page*/
    --input-underline: #4e588d;
    --input-background: #373743;
    --input-text: #c7c9db;
    /*These affect the upload button*/
    --upload-bg: #2b3246;
    --upload-boarder: #0e1320;
    --upload-text: #e8e8e8;
}

@open-sans {
    font-family: "Open Sans";
    src: url("/font/OpenSans-Regular.ttf")
}

* {
    padding: 0px;
    margin: 0px;
}

a {
    /* color: #4458ED; */
    color: var(--new-link);
}

a:visited {
    color: var(--visited-link);
}

body {
    font-family: Open Sans;
    font-size: 16px;
    color: var(--general-text);
    background: var(--dark-background);
}

.body-smaller {
    font-size: 14px;
}

.content-container {
    text-align: center;
    padding: 20px;
}

.nav-container {
    width: 100%;
    border-color: var(--lighter-dark-background);
    border-bottom: 2px solid var(--border-color);
    background-color: var(--lighter-dark-background);
}

.navigation {
    width: 100%;
    position: relative;
    font-size: 25px;
    line-height: 100px;
}

#room {
    background: var(--dark-background);
    margin-left: 10px;
    margin-right: 10px;
}

.header-icon {
    text-align: center;
    padding: 15px;
    /* background-color: var(--dropzone-bg); */
    background-color: var(--lighter-dark-background);
    /* width: 100%; */
}

#header-image {
    max-width: 40px;
}

.qtstream {
    padding-left: 10px;
    color: var(--qtstream);
    font-weight: bold;
}

.nav-text {
    color: var(--base-text);
    font-weight: normal;
}

.user-profile {
    text-align: center;
    width: 100%;
}

.huge-text {
    font-size: 24px;
    text-align: center;
    padding: 20px;
}

/*
.huge-text a:link {
    color: var(--blue-text);
}

.stream-url-container {
    text-align: center;
    width: 100%;
}

.stream-url-box {
    display: inline-block;
    margin: auto;
    background-color: var(--dropzone-bg);
    padding: 20px;
    border-radius: 5px;
    border: 2px solid #555555;
}

.stream-url {
    color: var(--pink-text);
    font-weight: bold;
}

.live-url {
    color: var(--red-text);
}

.key {
    color: var(--blue-text);
}
*/

hr {
    border-top: 1px solid var(--pointless-border);
    margin-top: 10px;
    margin-bottom: 5px;
}

#file-dropzone {
    background-color: var(--dropzone-bg);
}

#file-list-container {
    box-sizing: border-box;
}

.column, html, body, .container, main {
    display: flex;
    flex-direction: column;
    flex-wrap: nowrap;
    justify-content: flex-start;
    align-items: stretch;
    align-content: stretch;
    max-width: 100%;
    margin: 0;
    padding: 0;
}

.column-wrap {
    flex-wrap: wrap;
}

.row {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: flex-start;
    align-items: baseline;
    align-content: flex-start;
    max-height: 100%;
    margin: 0;
    padding: 0;
}

.row-right {
    justify-content: flex-end;
}

.row-spaced {
    justify-content: space-between;
}

.grow, .wrapped, body, .container, main {
    flex-grow: 1;
}

html {
    height: 100%;
}

.grow-double {
    flex-grow: 2;
}

.shrink {
    flex-shrink: 1;
}

.no-shrink {
    flex-shrink: 0;
}

.no-grow {
    flex-grow: 0;
}

.shrink-double {
    flex-shrink: 2;
}

.scroll-container {
    overflow-y: auto;
}

.scroll-content {
    min-height: 100%;
    width: 100%;
}

.auto-basis {
    flex-basis: auto;
}

.zero-basis {
    flex-basis: 0;
}

.file-container {
    padding-top: 8px;
    padding-bottom: 8px;
    padding-left: 4px;
    padding-right: 3px;
    background-color: var(--lighter-dark-background);
}

.file-link {
    margin-left: 7px;
    text-decoration: none;
    color: var(--file-link);
    font-weight: 500;
    display: block;
}

.file-uploader {
    background: var(--uploader-bg);
    border-style: solid;
    border-color: var(--uploader-boarder);
    border-width: 1px;
    border-radius: 2px;
    padding-left: 2px;
    padding-right: 2px;
    font-size: 11px;
    top: 50%;
    margin-left: 5px;
    display: block;
}

[v-cloak] {
    visibility: hidden;
}

.hidden {
    visibility: hidden;
}

.form-group {
    margin: 10px;
}

#dim-screen {
    position: fixed;
    padding: 0;
    margin: 0;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: var(--dimmer);
    display: flex;
}

#loading-message {
    margin: auto;
    font-size: 36px;
    text-align: center;
}

.register-form {
    background: var(--lighter-dark-background);
    margin-left: 10%;
    margin-right: 10%;
    margin-top: 2%;
    padding-bottom: 15px;
    padding-top: 8px;
    border: 1px solid var(--form-border);
    border-radius: 5px;
}

.login-form {
    background: var(--lighter-dark-background);
    margin-left: 30%;
    margin-right: 30%;
    margin-top: 1%;
    padding-bottom: 15px;
    padding-top: 5px;
    border: 1px solid var(--form-border);
    border-radius: 5px;
    text-align: center;
}

.register-field input, .login-field input, span.info {
    position: relative;
    padding-top: 15px;
    padding-left: 10px;
}

.register-field input, .login-field input {
    background: var(--lighter-dark-background);
    border-style: none;
    border-bottom-style: solid;
    border-color: var(--lighter-dark-background);
    transition: ease-in-out 0.2s;
    width: 40%;
    max-width: 300px;
    color: var(--input-color);
    font-size: 18px;
}

.login-field input {
    width: 90%;
    max-width: 800px;
}

.register-field input:focus, .login-field input:focus {
    border-color: var(--input-underline);
    background: var(--input-background);
    color: var(--input-text);
    transition: ease-in-out 0.2s;
}

.login button {
    margin-left: 30%;
}

.register button {
    margin-left: 10%;
}

.login button, .register button {
    margin-top: 1%;
}

.register-form h1 {
    padding-left: 10px;
}

main .room, main .room-list {
    text-align: center;
}

.room-list  * {
    display: inline-block;
    text-align: center;
}

.room-list {
    padding-top: 5px;
    padding-bottom: 5px;
    font-size: 18px;
}

.room-list > .room-id {
    width: 10%;
    min-width: 100px;
}

.room-list > .room-name {
    width: 50%;
}

.room-list > .room-owner {
    width: 15%;
}

.room * {
    text-align: center;
    display: inline-block;
}

.room > .room-id {
    width: 10%;
    min-width: 100px;
}

.room > .room-name {
    width: 50%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.room > .room-owner {
    width: 15%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

h1, .register-field > span, .red, .presence {
    color: #c9c9c9;
}

.presence-container, .presence-container-small {
    max-height: 20vh;
}

.control-bar {
}

.presence-container-medium {
    max-height: 50vh;
}

.presence-container-big {
    max-height: 75vh;
}

.collapse {
    visibility: collapse;
}

.button, .custom-file-upload, button {
    position: relative;
    background: var(--upload-bg);
    border-radius: 3px;
    border: 1px solid var(--upload-border);
    display: block;
    cursor: pointer;
    color: var(--upload-text);
    font-family: Arial;
    font-size: 15px;
    font-weight: bold;
    padding: 11px 23px;
    text-decoration: none;
    transition: 0.1s;
    text-align: center;
}

button:hover, .custom-file-upload:hover, #delete-button:hover {
    filter: brightness(120%);
    transition: 0.1s;
}

.delete.button {
    background: transparent;
    border-style: none;
}

#upload-button, #delete-button, .custom-file-upload {
    vertical-align: middle;
}

#delete-button {
    background: var(--del-button);
}

.small.delete.button, .mark{
    margin: 1px 3px 1px 3px;
    padding: 0;
    vertical-align: middle;
    display: block;
}

.row-centre {
    align-content: center;
}

.file-even {
    background: var(--lighter-dark-background);
}

.file-odd {
    background: var(--odd-file);
}

.file-expiration {
    font-size: 10px;
    color: var(--expire-text);
    margin: auto 6px auto 6px;
    display: block;
}

.nav-text {
    text-decoration: none
}

.register-form h1 {
    padding-left: 10px;
}

.header-icon, .red {
    background: var(--lighter-dark-background);
}

#file-dropzone input {
    border-style: solid;
    border-radius: 3px;
    border-color: var(--filter-border);
}

input[type="file"] {
    display: none;
}

/*#file-dropzone input {
    background: #2b2e36;
    border-top-style: none;
    border-bottom-style: none;
    border-width: 1px;
    border-color: rgb(100,100,100);
    border-left-style: solid;
    border-right-style: solid;
    padding-left: 5px;
    padding-top: 2px;
    padding-bottom: 2px;
}*/


code:
// Brunch automatically concatenates all files in your
// watched paths. Those paths can be configured at
// config.paths.watched in "brunch-config.js".
//
// However, those files will only be executed if
// explicitly imported. The only exception are files
// in vendor, which are never wrapped in imports and
// therefore are always executed.

// Import dependencies
//
// If you no longer want to use a dependency, remember
// to also remove its path from "config.paths.watched".
import * as phoenix from "phoenix_html";

// Import local files
//
// Local files can be imported directly using relative
// paths "./socket" or full ones "web/static/js/socket".

// import socket from "./socket"


code:
"use strict";

function sleep(time) {
    return new Promise(resolve => setTimeout(resolve, time));
}

export default {
    name: "async-button",
    template: "#async-button-template",
    props: ["action", "defaultIcon"],
    data() {
        return {
            failCount: 0,
            status: "ready"
        };
    },
    methods: {
        async click() {
            if (this.status == "ready") {
                this.status = "waiting";
                const result = await this.action();
                if (result.success) {
                    this.status = "succeeded";
                } else {
                    this.status = "failed";
                    this.failCount += 1;
                    await sleep((2 ** this.failCount) * 1000);
                    this.status = "ready";
                }
            }
        }
    }
};


code:
"use strict";

import file from "./file-view";
import upload from "./upload-view";

export default function(room) {
    return {
        name: "fileList",
        template: "#file-list-template",
        props: ["role", "filter"],
        data() {
            return room.fileList;
        },
        components: {
            file: file(room),
            upload: upload(room)
        },
        computed: {
            filteredFiles() {
                return this.files.filter((f) => {
                    return f.filename.toUpperCase().search(this.filter.toUpperCase()) >= 0;
                });
            },
            filesLength() {
                return this.files.length;
            },
            filteredFilesLength() {
                return this.filteredFiles.length;
            }
        }
    };
};


code:
"use strict";

import Upload from "./upload";
import File from "./file";

export default class FileList {
    constructor(room_id) {
        this.uploads = [];
        this.files = [];
        this.status = 0;
        this.room_id = room_id;
    }

    async wakeUploader() {
        if (this.status) {
            return;
        }

        this.status += 1;

        try {
            await this.performUploads();
        } finally {
            // todo: catch
            this.status -= 1;
        }
    }

    async performUploads() {
        while (this.uploads.length) {
            await this.upload(this.uploads[0]);
            this.uploads.shift();
        }
    }

    upload(upload) {
        return new Promise((resolve, reject) => {
            const req = new XMLHttpRequest();
            var query = "room_id=" + this.room_id + "&filename=" + upload.file.name;

            if (upload.file.type) {
                query += "&content_type=" + upload.file.type;
            }

            req.open("POST", "/api/upload?" + query, true);
            req.setRequestHeader("Content-Type", "application/octet-stream");

            req.upload.addEventListener("progress", (ev) => {
                if (ev.lengthComputable) {
                    upload.uploaded = ev.loaded;
                }
            });

            req.addEventListener("load", (ev) => {
                resolve(req.response);
            });

            req.addEventListener("error", (ev) => {
                reject(req.response);
            });

            req.addEventListener("abort", (ev) => {
                resolve("aborted xhr");
            });

            req.send(upload.file);
        });
    }

    async addUpload(id, name) {
        const upload = new Upload(id, name);
        this.uploads.push(upload);
        await this.wakeUploader();
    }

    static search(idKey, items, id, cont) {
        for (let i = 0; i < items.length; i++) {
            if (items[i][idKey] == id) {
                return cont(items[i], i);
            }
        }
        return null;
    }

    searchUploads(id, cont) {
        return FileList.search("id", this.uploads, id, cont);
    }

    searchFiles(id, cont) {
        return FileList.search("uuid", this.files, id, cont);
    }

    setFileDeletionFailed(uuids) {
        for (let file of this.files.reverse()) {
            if (uuids[uuids.length - 1] == file.uuid) {
                file.deleteStatus = "failed";
                uuids.pop();
            }
        }
    }

    setFileDeletionResults(uuids_original, results_original) {
        let uuids = uuids_original.slice(0);
        let results = results_original.slice(0);
        for (let file of this.files.reverse()) {
            if (uuids[uuids.length - 1] == file.uuid) {
                if (results[results.length - 1] == "ok") {
                    file.deleteStatus = "succeeded";
                } else {
                    file.deleteStatus = "failed";
                }
                results.pop();
                uuids.pop();
            }
        }
    }

    getDeletedFiles() {
        const result = [];
        for (let file of this.files) {
            if (file.marked) {
                file.deleteStatus = "waiting";
                result.push(file.uuid);
            }
        }
        return result;
    }

    progressUpload(id, uploaded, total) {
        this.searchUploads(id, (upload, i) => {
            upload.uploaded = uploaded;
            upload.total = total;
        });
    }

    completeUpload(id) {
        this.searchUploads(id, (upload, i) => {
            this.uploads.splice(i, 1);
        });
    }

    removeFile(uuid) {
        this.searchFiles(uuid, (file, i) => {
            this.files.splice(i, 1);
        });
    }

    addFile(data) {
        const file = File.create(data);
        this.files.unshift(file);
    }
}


code:
"use strict";

import asyncButton from "./async-button-view";

export default function(room) {
    return {
        data() {
            const x = room.fileList.searchFiles(this.uuid, (file, i) => {
                return file;
            });
            return x;
        },
        name: "file",
        template: "#file-template",
        props: ["role", "uuid", "index", "filesLength"],
        computed: {
            domId() {
                return "file-" + this.uuid;
            },
            link() {
                return `/get/${this.uuid}/${this.filename}`;
            },
            mod() {
                return this.role == "mod" || this.role == "admin";
            },
            isOdd() {
                return (this.index + this.filesLength) % 2;
            },
            isEven() {
                return (this.index + this.filesLength + 1) % 2;
            },
            formattedExpirationDate() {
                return (new Date(this.expiration_date)).toLocaleString();
            },
            shrunken_ip() {
                return this.ip_address.substring(0, 22);
            }
        },
        methods: {
            async deleteMe() {
                this.deleteStatus = "waiting";
                const files = [this.uuid];
                let result = await room.deleteFiles(files);
                result.success = result.results.length == 1 && result.results[0] == "ok";
                return result;
            },
            async banMe() {
                const date = new Date(
                    new Date().setFullYear(new Date().getFullYear() + 10)
                );
                const ban = {
                    file_bans: [
                        {
                            hash: this.hash
                        }
                    ],
                    reason: "quick ban",
                    end: Math.round(date.getTime() / 1000)
                };
                const result = await room.ban(ban);
                return result;
            },

            async banUploader() {
                const date = new Date(
                    new Date().setHours(new Date().getHours() + 1)
                );

                const ban = {
                    user_bans: [
                        {
                            bannee_id: this.uploader_id,
                            hell: false,
                            ip_bans: [
                                {
                                    ip_address: this.ip_address
                                }
                            ]
                        }
                    ],
                    reason: "quick ban",
                    end: Math.round(date.getTime() / 1000)
                };
                const result = await room.ban(ban);
                return result;
            }
        },

        components: {
            asyncButton: asyncButton
        }
    };
};


code:
"use strict";

export default class File {
    static create(data) {
        const file = new File();
        return Object.assign(file, data);
    }

    constructor() {
        this.marked = false;
        this.deleteStatus = "ready";
        this.banStatus = "ready";
    }
}


code:
"use strict";

export default function(room) {
    return {
        name: "presence",
        template: "#presence-template",
        props: ["role", "presenceHidden"],
        data() {
            return room.presence;
        },
        methods: {
            togglePresence() {
                this.$emit("toggle-presence");
            }
        },
        computed: {
            hidden() {
                return this.presenceHidden;
            }
        }
    };
};


code:
"use strict";

import {Presence as PhoenixPresence} from "phoenix";

export default class Presence {
    constructor() {
        this.presences = {};
    }

    syncState(state) {
        this.presences = PhoenixPresence.syncState(this.presences, state);
    }

    diffState(diff) {
        this.presences = PhoenixPresence.syncDiff(this.presences, diff);
    }
}


code:
"use strict";

import socket from "./socket";
import Room from "./room";
import Vue from "vue/dist/vue.common.js";
import fileListView from "./file-list-view";
import presenceView from "./presence-view";
const room = new Room(socket);

// A room with a vue
export default new Vue({
    name: "room",
    data: room,
    el: "#room",
    components: {
        fileList: fileListView(room),
        presence: presenceView(room)
    },
    mounted() {
        room.initialiseUploader();
    },
    methods: {
        async deleteFiles() {
            const files = room.fileList.getDeletedFiles();
            await room.deleteFiles(files);
        },
        togglePresence() {
            this.presenceSize = (this.presenceSize + 1) % 4;
        }
    },
    computed: {
        mod() {
            return this.role == "mod" || this.role == "admin";
        },
        presenceBig() {
            return this.presenceSize == 3;
        },
        presenceMedium() {
            return this.presenceSize == 2;
        },
        presenceSmall() {
            return this.presenceSize == 1;
        },
        presenceHidden() {
            return this.presenceSize == 0;
        }
    }
});


code:
//import "js/uploader.js";
import FileList from "./file-list";
import Presence from "./presence";

export default class Room {
    constructor(socket) {
        this.room_id = window.config.room_id;
        this.fileList = new FileList(this.room_id);
        this.presence = new Presence();
        const self = this;
        Room.createChannel(socket, this.room_id, this).then(channel => {
            self.channel = channel;
        });
        this.role = "user";
        this.filter = "";
        this.topid = 0;
        this.presenceSize = 0;
    }

    push(method, data) {
        return new Promise((resolve, reject) => {
            this.channel.push(method, data)
                .receive("ok", resolve)
                .receive("error", reject)
            ;
        });
    }

    async deleteFiles(files) {
        try {
            const results = await this.push("delete", {files: files})
            this.fileList.setFileDeletionResults(files, results.results);
            console.log("files deleted: " + results.results);
            return results;
        } catch (e) {
            this.fileList.setFileDeletionFailed(files);
            console.log("failed to delete files: " + e);
            throw e;
        }
   }

    async ban(data) {
        try {
            const result = await this.push("ban", data)
            return result;
        } catch (e) {
            console.log("failed to ban: " + e);
            throw e;
        }
   }

    initialiseUploader() {
        const uploadButton = document.getElementById("upload-button");
        const self = this;
        uploadButton.addEventListener("change", function() {
            const files = this.files;
            for (const file of files) {
                self.fileList.addUpload(self.topid, file);
                self.topid += 1;
            }
            this.value = "";
        });

        window.addEventListener("drop", (ev) => {
            ev.preventDefault();
            ev.stopPropagation();
            return false;
        });

        window.addEventListener("dragover", (ev) => {
            ev.preventDefault();
            ev.stopPropagation();
            return false;
        });

        const dropzone = document.getElementById("file-dropzone");
        dropzone.addEventListener("drop", (ev) => {
            ev.preventDefault();
            ev.stopPropagation();
            const dt = ev.dataTransfer;
            if (dt.items) {
                for (let item of dt.items) {
                    if (item.kind == "file") {
                        this.fileList.addUpload(this.topid, item.getAsFile());
                        this.topid += 1;
                    }
                }
            } else {
                // version that works in palememe
                for (let item of dt.files) {
                    this.fileList.addUpload(this.topid, item);
                    this.topid += 1;
                }
            }
            return false;
        }, true);
    }

    static createChannel(socket, room_id, self) {
        return new Promise((resolve, reject) => {
            const channel = socket.channel("room:" + room_id, {});

            channel.on("files", payload => {
                payload.body.forEach(file => {
                    self.fileList.addFile(file);
                });
            });

            channel.on("role", payload => {
                self.role = payload.body;
            });

            channel.on("deleted", payload => {
                self.fileList.removeFile(payload.body);
            });

            channel.on("presence_state", payload => {
                self.presence.syncState(payload);
            });

            channel.on("presence_diff", payload => {
                self.presence.diffState(payload);
            });

            channel.join()
                .receive("ok", resp => {
                    resolve(channel);
                })
                .receive("error", reject)
            ;
        });
    }
}


code:
// NOTE: The contents of this file will only be executed if
// you uncomment its entry in "assets/js/app.js".

// To use Phoenix channels, the first step is to import Socket
// and connect at the socket path in "lib/web/endpoint.ex":
import {Socket} from "phoenix";

let channel_token = document.querySelector("#channel_token").content;
let socket = new Socket("/socket", {params: {token: channel_token}});

// When you connect, you'll often need to authenticate the client.
// For example, imagine you have an authentication plug, `MyAuth`,
// which authenticates the session and assigns a `:current_user`.
// If the current user exists you can assign the user's token in
// the connection for use in the layout.
//
// In your "lib/web/router.ex":
//
//     pipeline :browser do
//       ...
//       plug MyAuth
//       plug :put_user_token
//     end
//
//     defp put_user_token(conn, _) do
//       if current_user = conn.assigns[:current_user] do
//         token = Phoenix.Token.sign(conn, "user socket", current_user.id)
//         assign(conn, :user_token, token)
//       else
//         conn
//       end
//     end
//
// Now you need to pass this token to JavaScript. You can do so
// inside a script tag in "lib/web/templates/layout/app.html.eex":
//
//     <script>window.userToken = "<%= assigns[:user_token] %>";</script>
//
// You will need to verify the user token in the "connect/2" function
// in "lib/web/channels/user_socket.ex":
//
//     def connect(%{"token" => token}, socket) do
//       # max_age: 1209600 is equivalent to two weeks in seconds
//       case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
//         {:ok, user_id} ->
//           {:ok, assign(socket, :user, user_id)}
//         {:error, reason} ->
//           :error
//       end
//     end
//
// Finally, pass the token on connect as below. Or remove it
// from connect if you don't care about authentication.

socket.connect();

// Now that you are connected, you can join channels with a topic:

export default socket;


code:
"use strict";

import asyncButton from "./async-button-view";

export default function(room) {
    return {
        name: "upload",
        template: "#upload-template",
        props: ["role", "index", "id"],
        data() {
            return room.fileList.searchUploads(this.id, (upload, i) => {
                return upload;
            });
        },
        computed: {
            progress() {
                return Math.round((this.uploaded / this.file.size) * 100);
            },
            domId() {
                return "upload-" + this.id;
            },
            isOdd() {
                return this.index % 2;
            },
            isEven() {
                return (this.index + 1) % 2;
            }
        },
        components: {
            asyncButton: asyncButton
        }
    };
};


code:
"use strict";

export default class Upload {
    constructor(id, file) {
        this.id = id;
        this.file = file;
        this.uploaded = 0;
    }
};