hunk.code_window:       getItemName: this.plugin.getItemName,
         getItemIcon: this.plugin.getItemIcon,
         handleScroll: this.handleScroll,
         done: this.donePicking,
         title: this.plugin.title,
         viewType: this.opts.viewType
       })
       return Browser(browserProps)
     }
   }
hunk.labels: keep keep keep keep keep replace keep keep keep keep keep
hunk.after_edit:       viewType: this.opts.viewType,
         showTitles: this.opts.showTitles
  
hunk.file_path: src/plugins/Provider/view/index.js
hunk.type: replace
file: const AuthView = require('./AuthView')
  const Browser = require('./Browser')
  const LoaderView = require('./Loader')
  const Utils = require('../../../core/Utils')
  const { h } = require('preact')
  
  /**
   * Class to easily generate generic views for plugins
   *
   *
   * This class expects the plugin instance using it to have the following
   * accessor methods.
   * Each method takes the item whose property is to be accessed
   * as a param
   *
   * isFolder
   *    @return {Boolean} for if the item is a folder or not
   * getItemData
   *    @return {Object} that is format ready for uppy upload/download
   * getItemIcon
   *    @return {Object} html instance of the item's icon
   * getItemSubList
   *    @return {Array} sub-items in the item. e.g a folder may contain sub-items
   * getItemName
   *    @return {String} display friendly name of the item
   * getMimeType
   *    @return {String} mime type of the item
   * getItemId
   *    @return {String} unique id of the item
   * getItemRequestPath
   *    @return {String} unique request path of the item when making calls to uppy server
   * getItemModifiedDate
   *    @return {object} or {String} date of when last the item was modified
   * getItemThumbnailUrl
   *    @return {String}
   */
  module.exports = class View {
    /**
     * @param {object} instance of the plugin
     */
    constructor (plugin, opts) {
      this.plugin = plugin
      this.Provider = plugin[plugin.id]
  
      // set default options
      const defaultOptions = {
        viewType: 'list'
      }
  
      // merge default options with the ones set by user
      this.opts = Object.assign({}, defaultOptions, opts)
  
      // Logic
      this.updateFolderState = this.updateFolderState.bind(this)
      this.addFile = this.addFile.bind(this)
      this.filterItems = this.filterItems.bind(this)
      this.filterQuery = this.filterQuery.bind(this)
      this.toggleSearch = this.toggleSearch.bind(this)
      this.getFolder = this.getFolder.bind(this)
      this.getNextFolder = this.getNextFolder.bind(this)
      this.logout = this.logout.bind(this)
      this.checkAuth = this.checkAuth.bind(this)
      this.handleAuth = this.handleAuth.bind(this)
      this.handleDemoAuth = this.handleDemoAuth.bind(this)
      this.sortByTitle = this.sortByTitle.bind(this)
      this.sortByDate = this.sortByDate.bind(this)
      this.isActiveRow = this.isActiveRow.bind(this)
      this.isChecked = this.isChecked.bind(this)
      this.toggleCheckbox = this.toggleCheckbox.bind(this)
      this.handleError = this.handleError.bind(this)
      this.handleScroll = this.handleScroll.bind(this)
      this.donePicking = this.donePicking.bind(this)
  
      this.plugin.uppy.on('file-removed', this.updateFolderState)
  
      // Visual
      this.render = this.render.bind(this)
    }
  
    tearDown () {
      this.plugin.uppy.off('file-removed', this.updateFolderState)
    }
  
    _updateFilesAndFolders (res, files, folders) {
      this.plugin.getItemSubList(res).forEach((item) => {
        if (this.plugin.isFolder(item)) {
          folders.push(item)
        } else {
          files.push(item)
        }
      })
  
      this.plugin.setPluginState({ folders, files })
    }
  
    checkAuth () {
      this.plugin.setPluginState({ checkAuthInProgress: true })
      this.Provider.checkAuth()
        .then((authenticated) => {
          this.plugin.setPluginState({ checkAuthInProgress: false })
          this.plugin.onAuth(authenticated)
        })
        .catch((err) => {
          this.plugin.setPluginState({ checkAuthInProgress: false })
          this.handleError(err)
        })
    }
  
    /**
     * Based on folder ID, fetch a new folder and update it to state
     * @param  {String} id Folder id
     * @return {Promise}   Folders/files in folder
     */
    getFolder (id, name) {
      return this._loaderWrapper(
        this.Provider.list(id),
        (res) => {
          let folders = []
          let files = []
          let updatedDirectories
  
          const state = this.plugin.getPluginState()
          const index = state.directories.findIndex((dir) => id === dir.id)
  
          if (index !== -1) {
            updatedDirectories = state.directories.slice(0, index + 1)
          } else {
            updatedDirectories = state.directories.concat([{id, title: name || this.plugin.getItemName(res)}])
          }
  
          this._updateFilesAndFolders(res, files, folders)
          this.plugin.setPluginState({ directories: updatedDirectories })
        },
        this.handleError)
    }
  
    /**
     * Fetches new folder
     * @param  {Object} Folder
     * @param  {String} title Folder title
     */
    getNextFolder (folder) {
      let id = this.plugin.getItemRequestPath(folder)
      this.getFolder(id, this.plugin.getItemName(folder))
      this.lastCheckbox = undefined
    }
  
    addFile (file, isCheckbox = false) {
      const tagFile = {
        source: this.plugin.id,
        data: this.plugin.getItemData(file),
        name: this.plugin.getItemName(file) || this.plugin.getItemId(file),
        type: this.plugin.getMimeType(file),
        isRemote: true,
        body: {
          fileId: this.plugin.getItemId(file)
        },
        remote: {
          host: this.plugin.opts.host,
          url: `${this.Provider.fileUrl(this.plugin.getItemRequestPath(file))}`,
          body: {
            fileId: this.plugin.getItemId(file)
          }
        }
      }
  
      Utils.getFileType(tagFile).then(fileType => {
        if (fileType && Utils.isPreviewSupported(fileType)) {
          tagFile.preview = this.plugin.getItemThumbnailUrl(file)
        }
        this.plugin.uppy.log('Adding remote file')
        this.plugin.uppy.addFile(tagFile).catch(() => {
          // Ignore
        })
        if (!isCheckbox) {
          this.donePicking()
        }
      })
    }
  
    /**
     * Removes session token on client side.
     */
    logout () {
      this.Provider.logout(location.href)
        .then((res) => res.json())
        .then((res) => {
          if (res.ok) {
            const newState = {
              authenticated: false,
              files: [],
              folders: [],
              directories: []
            }
            this.plugin.setPluginState(newState)
          }
        }).catch(this.handleError)
    }
  
    filterQuery (e) {
      const state = this.plugin.getPluginState()
      this.plugin.setPluginState(Object.assign({}, state, {
        filterInput: e.target.value
      }))
    }
  
    toggleSearch (inputEl) {
      const state = this.plugin.getPluginState()
  
      this.plugin.setPluginState({
        isSearchVisible: !state.isSearchVisible,
        filterInput: ''
      })
    }
  
    filterItems (items) {
      const state = this.plugin.getPluginState()
      return items.filter((folder) => {
        return this.plugin.getItemName(folder).toLowerCase().indexOf(state.filterInput.toLowerCase()) !== -1
      })
    }
  
    sortByTitle () {
      const state = Object.assign({}, this.plugin.getPluginState())
      const {files, folders, sorting} = state
  
      let sortedFiles = files.sort((fileA, fileB) => {
        if (sorting === 'titleDescending') {
          return this.plugin.getItemName(fileB).localeCompare(this.plugin.getItemName(fileA))
        }
        return this.plugin.getItemName(fileA).localeCompare(this.plugin.getItemName(fileB))
      })
  
      let sortedFolders = folders.sort((folderA, folderB) => {
        if (sorting === 'titleDescending') {
          return this.plugin.getItemName(folderB).localeCompare(this.plugin.getItemName(folderA))
        }
        return this.plugin.getItemName(folderA).localeCompare(this.plugin.getItemName(folderB))
      })
  
      this.plugin.setPluginState(Object.assign({}, state, {
        files: sortedFiles,
        folders: sortedFolders,
        sorting: (sorting === 'titleDescending') ? 'titleAscending' : 'titleDescending'
      }))
    }
  
    sortByDate () {
      const state = Object.assign({}, this.plugin.getPluginState())
      const {files, folders, sorting} = state
  
      let sortedFiles = files.sort((fileA, fileB) => {
        let a = new Date(this.plugin.getItemModifiedDate(fileA))
        let b = new Date(this.plugin.getItemModifiedDate(fileB))
  
        if (sorting === 'dateDescending') {
          return a > b ? -1 : a < b ? 1 : 0
        }
        return a > b ? 1 : a < b ? -1 : 0
      })
  
      let sortedFolders = folders.sort((folderA, folderB) => {
        let a = new Date(this.plugin.getItemModifiedDate(folderA))
        let b = new Date(this.plugin.getItemModifiedDate(folderB))
  
        if (sorting === 'dateDescending') {
          return a > b ? -1 : a < b ? 1 : 0
        }
  
        return a > b ? 1 : a < b ? -1 : 0
      })
  
      this.plugin.setPluginState(Object.assign({}, state, {
        files: sortedFiles,
        folders: sortedFolders,
        sorting: (sorting === 'dateDescending') ? 'dateAscending' : 'dateDescending'
      }))
    }
  
    sortBySize () {
      const state = Object.assign({}, this.plugin.getPluginState())
      const {files, sorting} = state
  
      // check that plugin supports file sizes
      if (!files.length || !this.plugin.getItemData(files[0]).size) {
        return
      }
  
      let sortedFiles = files.sort((fileA, fileB) => {
        let a = this.plugin.getItemData(fileA).size
        let b = this.plugin.getItemData(fileB).size
  
        if (sorting === 'sizeDescending') {
          return a > b ? -1 : a < b ? 1 : 0
        }
        return a > b ? 1 : a < b ? -1 : 0
      })
  
      this.plugin.setPluginState(Object.assign({}, state, {
        files: sortedFiles,
        sorting: (sorting === 'sizeDescending') ? 'sizeAscending' : 'sizeDescending'
      }))
    }
  
    isActiveRow (file) {
      return this.plugin.getPluginState().activeRow === this.plugin.getItemId(file)
    }
  
    isChecked (item) {
      const itemId = this.providerFileToId(item)
      if (this.plugin.isFolder(item)) {
        const state = this.plugin.getPluginState()
        const folders = state.selectedFolders || {}
        if (itemId in folders) {
          return folders[itemId]
        }
        return false
      }
      return (itemId in this.plugin.uppy.getState().files)
    }
  
    /**
     * Adds all files found inside of specified folder.
     *
     * Uses separated state while folder contents are being fetched and
     * mantains list of selected folders, which are separated from files.
     */
    addFolder (folder) {
      const folderId = this.providerFileToId(folder)
      let state = this.plugin.getPluginState()
      let folders = state.selectedFolders || {}
      if (folderId in folders && folders[folderId].loading) {
        return
      }
      folders[folderId] = {loading: true, files: []}
      this.plugin.setPluginState({selectedFolders: folders})
      this.Provider.list(this.plugin.getItemRequestPath(folder)).then((res) => {
        let files = []
        this.plugin.getItemSubList(res).forEach((item) => {
          if (!this.plugin.isFolder(item)) {
            this.addFile(item, true)
            files.push(this.providerFileToId(item))
          }
        })
        state = this.plugin.getPluginState()
        state.selectedFolders[folderId] = {loading: false, files: files}
        this.plugin.setPluginState({selectedFolders: folders})
        const dashboard = this.plugin.uppy.getPlugin('Dashboard')
        let message
        if (files.length) {
          message = dashboard.i18n('folderAdded', {
            smart_count: files.length, folder: this.plugin.getItemName(folder)
          })
        } else {
          message = dashboard.i18n('emptyFolderAdded')
        }
        this.plugin.uppy.info(message)
      }).catch((e) => {
        state = this.plugin.getPluginState()
        delete state.selectedFolders[folderId]
        this.plugin.setPluginState({selectedFolders: state.selectedFolders})
        this.handleError(e)
      })
    }
  
    removeFolder (folderId) {
      let state = this.plugin.getPluginState()
      let folders = state.selectedFolders || {}
      if (!(folderId in folders)) {
        return
      }
      let folder = folders[folderId]
      if (folder.loading) {
        return
      }
      // deepcopy the files before iteration because the
      // original array constantly gets mutated during
      // the iteration by updateFolderState as each file
      // is removed and 'core:file-removed' is emitted.
      const files = folder.files.concat([])
      for (const fileId of files) {
        if (fileId in this.plugin.uppy.getState().files) {
          this.plugin.uppy.removeFile(fileId)
        }
      }
      delete folders[folderId]
      this.plugin.setPluginState({selectedFolders: folders})
    }
  
    /**
     * Updates selected folders state everytime file is being removed.
     *
     * Note that this is only important when files are getting removed from the
     * main screen, and will do nothing when you uncheck folder directly, since
     * it's already been done in removeFolder method.
     */
    updateFolderState (file) {
      let state = this.plugin.getPluginState()
      let folders = state.selectedFolders || {}
      for (let folderId in folders) {
        let folder = folders[folderId]
        if (folder.loading) {
          continue
        }
        let i = folder.files.indexOf(file.id)
        if (i > -1) {
          folder.files.splice(i, 1)
        }
        if (!folder.files.length) {
          delete folders[folderId]
        }
      }
      this.plugin.setPluginState({selectedFolders: folders})
    }
  
    /**
     * Toggles file/folder checkbox to on/off state while updating files list.
     *
     * Note that some extra complexity comes from supporting shift+click to
     * toggle multiple checkboxes at once, which is done by getting all files
     * in between last checked file and current one, and applying an on/off state
     * for all of them, depending on current file state.
     */
    toggleCheckbox (e, file) {
      e.stopPropagation()
      e.preventDefault()
      let { folders, files, filterInput } = this.plugin.getPluginState()
      let items = folders.concat(files)
      if (filterInput !== '') {
        items = this.filterItems(items)
      }
      let itemsToToggle = [file]
      if (this.lastCheckbox && e.shiftKey) {
        let prevIndex = items.indexOf(this.lastCheckbox)
        let currentIndex = items.indexOf(file)
        if (prevIndex < currentIndex) {
          itemsToToggle = items.slice(prevIndex, currentIndex + 1)
        } else {
          itemsToToggle = items.slice(currentIndex, prevIndex + 1)
        }
      }
      this.lastCheckbox = file
      if (this.isChecked(file)) {
        for (let item of itemsToToggle) {
          const itemId = this.providerFileToId(item)
          if (this.plugin.isFolder(item)) {
            this.removeFolder(itemId)
          } else {
            if (itemId in this.plugin.uppy.getState().files) {
              this.plugin.uppy.removeFile(itemId)
            }
          }
        }
      } else {
        for (let item of itemsToToggle) {
          if (this.plugin.isFolder(item)) {
            this.addFolder(item)
          } else {
            this.addFile(item, true)
          }
        }
      }
    }
  
    providerFileToId (file) {
      return Utils.generateFileID({
        data: this.plugin.getItemData(file),
        name: this.plugin.getItemName(file) || this.plugin.getItemId(file),
        type: this.plugin.getMimeType(file)
      })
    }
  
    handleDemoAuth () {
      const state = this.plugin.getPluginState()
      this.plugin.setPluginState({}, state, {
        authenticated: true
      })
    }
  
    handleAuth () {
      const urlId = Math.floor(Math.random() * 999999) + 1
      const redirect = `${location.href}${location.search ? '&' : '?'}id=${urlId}`
  
      const authState = btoa(JSON.stringify({ redirect }))
      const link = `${this.Provider.authUrl()}?state=${authState}`
  
      const authWindow = window.open(link, '_blank')
      const checkAuth = () => {
        let authWindowUrl
  
        try {
          authWindowUrl = authWindow.location.href
        } catch (e) {
          if (e instanceof DOMException || e instanceof TypeError) {
            return setTimeout(checkAuth, 100)
          } else throw e
        }
  
        // split url because chrome adds '#' to redirects
        if (authWindowUrl && authWindowUrl.split('#')[0] === redirect) {
          authWindow.close()
          this._loaderWrapper(this.Provider.checkAuth(), this.plugin.onAuth, this.handleError)
        } else {
          setTimeout(checkAuth, 100)
        }
      }
  
      checkAuth()
    }
  
    handleError (error) {
      const uppy = this.plugin.uppy
      const message = uppy.i18n('uppyServerError')
      uppy.log(error.toString())
      uppy.info({message: message, details: error.toString()}, 'error', 5000)
    }
  
    handleScroll (e) {
      const scrollPos = e.target.scrollHeight - (e.target.scrollTop + e.target.offsetHeight)
      const path = this.plugin.getNextPagePath ? this.plugin.getNextPagePath() : null
  
      if (scrollPos < 50 && path && !this._isHandlingScroll) {
        this.Provider.list(path)
          .then((res) => {
            const { files, folders } = this.plugin.getPluginState()
            this._updateFilesAndFolders(res, files, folders)
          }).catch(this.handleError)
          .then(() => { this._isHandlingScroll = false }) // always called
  
        this._isHandlingScroll = true
      }
    }
  
    donePicking () {
      const dashboard = this.plugin.uppy.getPlugin('Dashboard')
      if (dashboard) dashboard.hideAllPanels()
    }
  
    // displays loader view while asynchronous request is being made.
    _loaderWrapper (promise, then, catch_) {
      promise
        .then(then).catch(catch_)
        .then(() => this.plugin.setPluginState({ loading: false })) // always called.
      this.plugin.setPluginState({ loading: true })
    }
  
    render (state) {
      const { authenticated, checkAuthInProgress, loading } = this.plugin.getPluginState()
  
      if (loading) {
        return LoaderView()
      }
  
      if (!authenticated) {
        return h(AuthView, {
          pluginName: this.plugin.title,
          pluginIcon: this.plugin.icon,
          demo: this.plugin.opts.demo,
          checkAuth: this.checkAuth,
          handleAuth: this.handleAuth,
          handleDemoAuth: this.handleDemoAuth,
          checkAuthInProgress: checkAuthInProgress
        })
      }
  
      const browserProps = Object.assign({}, this.plugin.getPluginState(), {
        getNextFolder: this.getNextFolder,
        getFolder: this.getFolder,
        addFile: this.addFile,
        filterItems: this.filterItems,
        filterQuery: this.filterQuery,
        toggleSearch: this.toggleSearch,
        sortByTitle: this.sortByTitle,
        sortByDate: this.sortByDate,
        logout: this.logout,
        demo: this.plugin.opts.demo,
        isActiveRow: this.isActiveRow,
        isChecked: this.isChecked,
        toggleCheckbox: this.toggleCheckbox,
        getItemName: this.plugin.getItemName,
        getItemIcon: this.plugin.getItemIcon,
        handleScroll: this.handleScroll,
        done: this.donePicking,
        title: this.plugin.title,
        viewType: this.opts.viewType
      })
  
      return Browser(browserProps)
    }
  }
  