<commit_before>/* global Cache */

const path = require('path')
const fs = require('mz/fs')
const request = require('request-promise')
const VirtualGroups = require('./VirtualGroups.js')
const DiscordMember = require('./DiscordMember')
const Util = require('./Util')

// The default settings for DiscordServer.
const DefaultSettings = {
  verifiedRole: null,
  verifiedRemovedRole: null,
  nicknameUsers: true,
  nicknameGroup: null,
  joinDM: true,
  verifyChannel: null,
  announceChannel: null,
  nicknameFormat: '%USERNAME%',
  welcomeMessage: 'Welcome to %SERVER%, %USERNAME%!',
  groupRankBindings: []
}

/**
 * A DiscordServer class, it represents a guild that the bot is in.
 * @class DiscordServer
 */
class DiscordServer {
  /**
   * Creates an instance of DiscordServer.
   * @param {DiscordBot} discordBot The discord bot that the server belongs to.
   * @param {Snowflake} id The guild id
   */
  constructor (discordBot, id) {
    this.id = id
    this.discordBot = discordBot
    this.bot = this.discordBot.bot

    this.server = this.bot.guilds.get(id)

    this.verifyCooldowns = new Map()
    this.nicknames = new Map()

    this.ongoingBulkUpdate = false
    this.bulkUpdateCount = 0

    setInterval(() => {
      this.verifyCooldowns = new Map()
      this.nicknames = new Map()
    }, 7200000)

    // Load this server's settings.
    this.settings = {}
    this.areSettingsLoaded = false
    this.ongoingSettingsUpdate = false
    this.settingsPath = path.join(__dirname, 'data', `${id}.json`)

    // We will load the settings in DiscordBot.getServer in order to know when
    // the server is ready from the promise it returns.
    // this.loadSettings();
  }

  isAuthorized () {
    return !this.discordBot.isPremium() || this.discordBot.authorizedOwners.includes(this.server.ownerID)
  }

  /**
   * This method loads the settings specific for this server.
   * It also creates a settings file if there isn't one.
   * @returns {undefined}
   * @memberof DiscordServer
   */
  async loadSettings () {
    // If there's no settings file for this server, create one.
    if (!await fs.exists(this.settingsPath)) {
      await fs.writeFile(this.settingsPath, JSON.stringify(DefaultSettings))
    }

    if (!await fs.exists(this.settingsPath)) {
      throw new Error(`Couldn't write settings file: ${this.settingsPath}`)
    }

    // Load the settings file.
    const fileData = await fs.readFile(this.settingsPath)

    if (this.areSettingsLoaded) return

    try {
      this.settings = JSON.parse(fileData)
      this.areSettingsLoaded = true
    } catch (e) {
      console.log(`${this.settingsPath} appears to be corrupted.`)
    }

    if (this.settings.commando != null && this.settings.commando.prefix != null) {
      this.server._commandPrefix = this.settings.commando.prefix
    }

    this.cleanupRankBindings()
  }

  /**
   * Clears the member cache for a specific Discord user.
   * @static
   * @param {Snowflake} id The Discord user id
   * @memberof DiscordServer
   */
  static async clearMemberCache (id) {
    await Cache.set('users', id, null)
  }

  /**
   * Returns a setting value. Tries the saved settings, then tries
   * the default settings.
   * @param {string} key The setting key to get
   * @returns {any} The setting value
   * @memberof DiscordServer
   */
  getSetting (key) {
    if (!this.areSettingsLoaded) {
      throw new Error('Attempt to get a setting from a server whose settings are not loaded')
    }

    if (typeof this.settings[key] !== 'undefined') {
      return this.settings[key]
    } else if (typeof DefaultSettings[key] !== 'undefined') {
      return DefaultSettings[key]
    } else {
      return null
    }
  }

  /**
   * Set a server setting and then save it to disk.
   * @param {string} key The setting key to set
   * @param {any} value The setting value to set
   * @memberof DiscordServer
   */
  async setSetting (key, value) {
    if (!this.areSettingsLoaded) {
      throw new Error('Attempt to change a setting from a server whose settings are not loaded')
    }
    this.ongoingSettingsUpdate = true

    this.settings[key] = value

    const tmpSettingsPath = this.settingsPath + '.tmp'
    await fs.writeFile(tmpSettingsPath, JSON.stringify(this.settings))

    try {
      JSON.parse(await fs.readFile(tmpSettingsPath, 'utf8')) // Throws if file got corrupted
    } catch (e) {
      this.ongoingSettingsUpdate = false
      throw new Error('Atomic save failed: file corrupted. Try again.')
    }

    await fs.rename(tmpSettingsPath, this.settingsPath)
    this.ongoingSettingsUpdate = false
  }

  /**
   * Converts a group binding from the old format to the new format.
   * @static
   * @param {object} binding The binding to convert in the old format
   * @returns {object} The binding in the new format
   * @memberof DiscordServer
   */
  static convertOldBinding (binding) {
    const newBinding = { role: binding.role }

    const ranks = []
    if (binding.operator === 'gt') {
      for (let i = binding.rank; i <= 255; i++) {
        ranks.push(i)
      }
    } else if (binding.operator === 'lt') {
      for (let i = 1; i <= binding.rank; i++) {
        ranks.push(i)
      }
    } else {
      ranks.push(binding.rank)
    }

    newBinding.groups = [
      {
        id: binding.group,
        ranks
      }
    ]

    return newBinding
  }

  /**
   * Fetches and caches a Roblox user's group information.
   * @static
   * @param {int} userid The roblox user id
   * @returns {object} The group information
   * @memberof DiscordServer
   */
  static async getRobloxMemberGroups (userid) {
    let groups = await Cache.get(`bindings.${userid}`, '__groups')
    if (!groups) {
      groups = await request({
        uri: `http://api.roblox.com/users/${userid}/groups`,
        json: true
      })

      if (!groups) {
        throw new Error('Group rank HTTP request is malformed or in unknown format')
      }

      Cache.set(`bindings.${userid}`, '__groups', groups)
    }

    return groups
  }

  /**
   * Checks if a group rank binding passes or fails for
   * a specific Roblox user.
   * @static
   * @param {object} binding The group binding
   * @param {int} userid The roblox user id
   * @param {string} username The roblox username
   * @returns {boolean} The group binding resolution
   * @memberof DiscordServer
   */
  static async resolveGroupRankBinding (binding, userid, username) {
    if (binding.group != null) {
      binding = this.convertOldBinding(binding)
    }

    const bindingHash = Util.md5(JSON.stringify(binding))

    // Check if the return value of this method has already been
    // cached in memory. If so, return that.
    const cachedBinding = await Cache.get(`bindings.${userid}`, bindingHash)
    if (cachedBinding !== null) {
      return cachedBinding
    }

    let returnValue = false

    for (const group of binding.groups) {
      if (VirtualGroups[group.id]) {
        returnValue = await VirtualGroups[group.id]({ id: userid, username }, group.ranks[0], DiscordServer)

        if (returnValue) break
      } else {
        // Check the rank of the user in the Roblox group.
        const groups = await DiscordServer.getRobloxMemberGroups(userid)

        let rank = 0
        for (const groupObj of groups) {
          if (groupObj.Id.toString() === group.id) {
            rank = groupObj.Rank
            break
          }
        }

        returnValue = group.ranks.includes(rank)

        if (returnValue) break
      }
    }

    // Cache the return value in memory.
    Cache.set(`bindings.${userid}`, bindingHash, returnValue)

    return returnValue
  }

  /**
   * Deletes a group rank binding associated with a role id.
   * @param {Snowflake} roleid The role id to clear all bindings that are associated with it.
   * @memberof DiscordServer
   */
  deleteGroupRankBinding (roleid) {
    if (roleid === 'all') {
      return this.setSetting('groupRankBindings', [])
    }

    const rankBindings = this.getSetting('groupRankBindings')

    for (let i = 0; i < rankBindings.length; i++) {
      const binding = rankBindings[i]

      if (binding.role === roleid || roleid === 'all') {
        rankBindings.splice(i, 1)
      }
    }

    this.setSetting('groupRankBindings', rankBindings)
  }

  /**
   * Deletes group bindings that are associated with Discord roles
   * that have been deleted.
   * @param {Channel} [noticeChannel] The channel to post the deletion notice to in a last resort if the owner is unreachable
   * @memberof DiscordServer
   */
  async cleanupRankBindings (lastResortChannel) {
    let verifiedRole = this.getSetting('verifiedRole')
    let unverifiedRole = this.getSetting('verifiedRemovedRole')

    if (verifiedRole && !await this.server.roles.fetch(verifiedRole)) {
      this.setSetting('verifiedRole', null)
      this.announce('Verified Role Deleted', 'Heads up! Looks like you (or someone) has deleted the verified role, so users will no longer get that when they verify.', { important: true, lastResortChannel })
    }

    if (unverifiedRole && !await this.server.roles.fetch(unverifiedRole)) {
      this.setSetting('verifiedRemovedRole', null)
      this.announce('Unverified Role Deleted', 'Heads up! Looks like you (or someone) has deleted the unverified role, so unverified users will no longer receive that role.', { important: true, lastResortChannel })
    }

    for (let binding of this.getSetting('groupRankBindings')) {
      let id = binding.role
      if (!await this.server.roles.fetch(id)) {
        this.deleteGroupRankBinding(id)

        this.announce('Bound Role Deleted', 'Heads up! Looks like you (or someone) has deleted a Discord role that was bound to ' + Util.getBindingText(binding, true), { important: true, lastResortChannel })
      }
    }
  }

  /**
   * Posts an announcement in the server's configured announcement channel.
   * If it's important, the server owner will be notified.
   * @param {string} title The title of the embed message
   * @param {string} text The body of the embed message
   * @param {object} [options={}] Options for the announcement
   * @param {boolean} options.important If this is an important announcement, the server owner will be mentioned or DM'd if no announce channel is set.
   * @memberof DiscordServer
   */
  async announce (title, text, options = {}) {
    const embed = {
      color: options.important ? 0xe74c3c : 0x0064ba,
      title,
      description: text
    }

    if (this.getSetting('announceChannel')) {
      const channel = await this.server.channels.get(this.getSetting('announceChannel'))

      if (channel) {
        try {
          await channel.send(options.important ? `${this.server.owner}` : '', { embed })
          return
        } catch (e) {}
      }
    }

    if (options.important) {
      try {
        await this.server.owner.send(`An important notice was triggered in your server "${this.server.name}" and there is no announcement channel configured, so it has been sent to you here:`, { embed })
        return
      } catch (e) {}

      if (this.server.systemChannel) {
        try {
          await this.server.systemChannel.send(`${this.server.owner} An important notice was triggered and there is no announcement channel configured and the server owner will not accept DMs from RoVer, so it has been posted here:`, { embed })
          return
        } catch (e) {}
      }

      if (options.lastResortChannel) {
        try {
          await options.lastResortChannel.send(`${this.server.owner} An important notice was triggered and there is no announcement channel configured and the server owner will not accept DMs from RoVer, so it has been posted here as a last resort:`, { embed })
        } catch (e) {}
      }
    }
  }

  /**
   * Gets this server's specific welcome message, or the default
   * one if none is configured.
   * @param {object} data String replacement data
   * @returns {string} The processed string
   * @memberof DiscordServer
   */
  getWelcomeMessage (data, member) {
    return Util.formatDataString(this.getSetting('welcomeMessage'), data, member)
  }

  /**
   * Checks to see if this server has configured a custom welcome message.
   * @returns {boolean} True if this server has a custom welcome message
   * @memberof DiscordServer
   */
  hasCustomWelcomeMessage () {
    return DefaultSettings.welcomeMessage !== this.getSetting('welcomeMessage')
  }

  /**
   * Returns a DiscordMember belonging to this DiscordServer.
   * @param {Snowflake} id The id of the user
   * @returns {Promise<DiscordMember>} The new discord member
   * @memberof DiscordServer
   */
  async getMember (id) {
    return DiscordMember.new(this, id)
  }

  /**
   * Returns whether or not a role is in use by the verified, not verified, or binding roles
   *
   * @param {Snowflake} id The role id
   * @returns {boolean} True if the role is in use
   * @memberof DiscordServer
   */
  isRoleInUse (id) {
    if (this.getSetting('verifiedRole') === id) return true
    if (this.getSetting('verifiedRemovedRole') === id) return true

    for (const binding of this.getSetting('groupRankBindings')) {
      if (binding.role === id) return true
    }

    return false
  }
}

module.exports = DiscordServer
<commit_msg>Satisfy Standard<commit_after>/* global Cache */

const path = require('path')
const fs = require('mz/fs')
const request = require('request-promise')
const VirtualGroups = require('./VirtualGroups.js')
const DiscordMember = require('./DiscordMember')
const Util = require('./Util')

// The default settings for DiscordServer.
const DefaultSettings = {
  verifiedRole: null,
  verifiedRemovedRole: null,
  nicknameUsers: true,
  nicknameGroup: null,
  joinDM: true,
  verifyChannel: null,
  announceChannel: null,
  nicknameFormat: '%USERNAME%',
  welcomeMessage: 'Welcome to %SERVER%, %USERNAME%!',
  groupRankBindings: []
}

/**
 * A DiscordServer class, it represents a guild that the bot is in.
 * @class DiscordServer
 */
class DiscordServer {
  /**
   * Creates an instance of DiscordServer.
   * @param {DiscordBot} discordBot The discord bot that the server belongs to.
   * @param {Snowflake} id The guild id
   */
  constructor (discordBot, id) {
    this.id = id
    this.discordBot = discordBot
    this.bot = this.discordBot.bot

    this.server = this.bot.guilds.get(id)

    this.verifyCooldowns = new Map()
    this.nicknames = new Map()

    this.ongoingBulkUpdate = false
    this.bulkUpdateCount = 0

    setInterval(() => {
      this.verifyCooldowns = new Map()
      this.nicknames = new Map()
    }, 7200000)

    // Load this server's settings.
    this.settings = {}
    this.areSettingsLoaded = false
    this.ongoingSettingsUpdate = false
    this.settingsPath = path.join(__dirname, 'data', `${id}.json`)

    // We will load the settings in DiscordBot.getServer in order to know when
    // the server is ready from the promise it returns.
    // this.loadSettings();
  }

  isAuthorized () {
    return !this.discordBot.isPremium() || this.discordBot.authorizedOwners.includes(this.server.ownerID)
  }

  /**
   * This method loads the settings specific for this server.
   * It also creates a settings file if there isn't one.
   * @returns {undefined}
   * @memberof DiscordServer
   */
  async loadSettings () {
    // If there's no settings file for this server, create one.
    if (!await fs.exists(this.settingsPath)) {
      await fs.writeFile(this.settingsPath, JSON.stringify(DefaultSettings))
    }

    if (!await fs.exists(this.settingsPath)) {
      throw new Error(`Couldn't write settings file: ${this.settingsPath}`)
    }

    // Load the settings file.
    const fileData = await fs.readFile(this.settingsPath)

    if (this.areSettingsLoaded) return

    try {
      this.settings = JSON.parse(fileData)
      this.areSettingsLoaded = true
    } catch (e) {
      console.log(`${this.settingsPath} appears to be corrupted.`)
    }

    if (this.settings.commando != null && this.settings.commando.prefix != null) {
      this.server._commandPrefix = this.settings.commando.prefix
    }

    this.cleanupRankBindings()
  }

  /**
   * Clears the member cache for a specific Discord user.
   * @static
   * @param {Snowflake} id The Discord user id
   * @memberof DiscordServer
   */
  static async clearMemberCache (id) {
    await Cache.set('users', id, null)
  }

  /**
   * Returns a setting value. Tries the saved settings, then tries
   * the default settings.
   * @param {string} key The setting key to get
   * @returns {any} The setting value
   * @memberof DiscordServer
   */
  getSetting (key) {
    if (!this.areSettingsLoaded) {
      throw new Error('Attempt to get a setting from a server whose settings are not loaded')
    }

    if (typeof this.settings[key] !== 'undefined') {
      return this.settings[key]
    } else if (typeof DefaultSettings[key] !== 'undefined') {
      return DefaultSettings[key]
    } else {
      return null
    }
  }

  /**
   * Set a server setting and then save it to disk.
   * @param {string} key The setting key to set
   * @param {any} value The setting value to set
   * @memberof DiscordServer
   */
  async setSetting (key, value) {
    if (!this.areSettingsLoaded) {
      throw new Error('Attempt to change a setting from a server whose settings are not loaded')
    }
    this.ongoingSettingsUpdate = true

    this.settings[key] = value

    const tmpSettingsPath = this.settingsPath + '.tmp'
    await fs.writeFile(tmpSettingsPath, JSON.stringify(this.settings))

    try {
      JSON.parse(await fs.readFile(tmpSettingsPath, 'utf8')) // Throws if file got corrupted
    } catch (e) {
      this.ongoingSettingsUpdate = false
      throw new Error('Atomic save failed: file corrupted. Try again.')
    }

    await fs.rename(tmpSettingsPath, this.settingsPath)
    this.ongoingSettingsUpdate = false
  }

  /**
   * Converts a group binding from the old format to the new format.
   * @static
   * @param {object} binding The binding to convert in the old format
   * @returns {object} The binding in the new format
   * @memberof DiscordServer
   */
  static convertOldBinding (binding) {
    const newBinding = { role: binding.role }

    const ranks = []
    if (binding.operator === 'gt') {
      for (let i = binding.rank; i <= 255; i++) {
        ranks.push(i)
      }
    } else if (binding.operator === 'lt') {
      for (let i = 1; i <= binding.rank; i++) {
        ranks.push(i)
      }
    } else {
      ranks.push(binding.rank)
    }

    newBinding.groups = [
      {
        id: binding.group,
        ranks
      }
    ]

    return newBinding
  }

  /**
   * Fetches and caches a Roblox user's group information.
   * @static
   * @param {int} userid The roblox user id
   * @returns {object} The group information
   * @memberof DiscordServer
   */
  static async getRobloxMemberGroups (userid) {
    let groups = await Cache.get(`bindings.${userid}`, '__groups')
    if (!groups) {
      groups = await request({
        uri: `http://api.roblox.com/users/${userid}/groups`,
        json: true
      })

      if (!groups) {
        throw new Error('Group rank HTTP request is malformed or in unknown format')
      }

      Cache.set(`bindings.${userid}`, '__groups', groups)
    }

    return groups
  }

  /**
   * Checks if a group rank binding passes or fails for
   * a specific Roblox user.
   * @static
   * @param {object} binding The group binding
   * @param {int} userid The roblox user id
   * @param {string} username The roblox username
   * @returns {boolean} The group binding resolution
   * @memberof DiscordServer
   */
  static async resolveGroupRankBinding (binding, userid, username) {
    if (binding.group != null) {
      binding = this.convertOldBinding(binding)
    }

    const bindingHash = Util.md5(JSON.stringify(binding))

    // Check if the return value of this method has already been
    // cached in memory. If so, return that.
    const cachedBinding = await Cache.get(`bindings.${userid}`, bindingHash)
    if (cachedBinding !== null) {
      return cachedBinding
    }

    let returnValue = false

    for (const group of binding.groups) {
      if (VirtualGroups[group.id]) {
        returnValue = await VirtualGroups[group.id]({ id: userid, username }, group.ranks[0], DiscordServer)

        if (returnValue) break
      } else {
        // Check the rank of the user in the Roblox group.
        const groups = await DiscordServer.getRobloxMemberGroups(userid)

        let rank = 0
        for (const groupObj of groups) {
          if (groupObj.Id.toString() === group.id) {
            rank = groupObj.Rank
            break
          }
        }

        returnValue = group.ranks.includes(rank)

        if (returnValue) break
      }
    }

    // Cache the return value in memory.
    Cache.set(`bindings.${userid}`, bindingHash, returnValue)

    return returnValue
  }

  /**
   * Deletes a group rank binding associated with a role id.
   * @param {Snowflake} roleid The role id to clear all bindings that are associated with it.
   * @memberof DiscordServer
   */
  deleteGroupRankBinding (roleid) {
    if (roleid === 'all') {
      return this.setSetting('groupRankBindings', [])
    }

    const rankBindings = this.getSetting('groupRankBindings')

    for (let i = 0; i < rankBindings.length; i++) {
      const binding = rankBindings[i]

      if (binding.role === roleid || roleid === 'all') {
        rankBindings.splice(i, 1)
      }
    }

    this.setSetting('groupRankBindings', rankBindings)
  }

  /**
   * Deletes group bindings that are associated with Discord roles
   * that have been deleted.
   * @param {Channel} [noticeChannel] The channel to post the deletion notice to in a last resort if the owner is unreachable
   * @memberof DiscordServer
   */
  async cleanupRankBindings (lastResortChannel) {
    const verifiedRole = this.getSetting('verifiedRole')
    const unverifiedRole = this.getSetting('verifiedRemovedRole')

    if (verifiedRole && !await this.server.roles.fetch(verifiedRole)) {
      this.setSetting('verifiedRole', null)
      this.announce('Verified Role Deleted', 'Heads up! Looks like you (or someone) has deleted the verified role, so users will no longer get that when they verify.', { important: true, lastResortChannel })
    }

    if (unverifiedRole && !await this.server.roles.fetch(unverifiedRole)) {
      this.setSetting('verifiedRemovedRole', null)
      this.announce('Unverified Role Deleted', 'Heads up! Looks like you (or someone) has deleted the unverified role, so unverified users will no longer receive that role.', { important: true, lastResortChannel })
    }

    for (const binding of this.getSetting('groupRankBindings')) {
      const id = binding.role
      if (!await this.server.roles.fetch(id)) {
        this.deleteGroupRankBinding(id)

        this.announce('Bound Role Deleted', 'Heads up! Looks like you (or someone) has deleted a Discord role that was bound to ' + Util.getBindingText(binding, true), { important: true, lastResortChannel })
      }
    }
  }

  /**
   * Posts an announcement in the server's configured announcement channel.
   * If it's important, the server owner will be notified.
   * @param {string} title The title of the embed message
   * @param {string} text The body of the embed message
   * @param {object} [options={}] Options for the announcement
   * @param {boolean} options.important If this is an important announcement, the server owner will be mentioned or DM'd if no announce channel is set.
   * @memberof DiscordServer
   */
  async announce (title, text, options = {}) {
    const embed = {
      color: options.important ? 0xe74c3c : 0x0064ba,
      title,
      description: text
    }

    if (this.getSetting('announceChannel')) {
      const channel = await this.server.channels.get(this.getSetting('announceChannel'))

      if (channel) {
        try {
          await channel.send(options.important ? `${this.server.owner}` : '', { embed })
          return
        } catch (e) {}
      }
    }

    if (options.important) {
      try {
        await this.server.owner.send(`An important notice was triggered in your server "${this.server.name}" and there is no announcement channel configured, so it has been sent to you here:`, { embed })
        return
      } catch (e) {}

      if (this.server.systemChannel) {
        try {
          await this.server.systemChannel.send(`${this.server.owner} An important notice was triggered and there is no announcement channel configured and the server owner will not accept DMs from RoVer, so it has been posted here:`, { embed })
          return
        } catch (e) {}
      }

      if (options.lastResortChannel) {
        try {
          await options.lastResortChannel.send(`${this.server.owner} An important notice was triggered and there is no announcement channel configured and the server owner will not accept DMs from RoVer, so it has been posted here as a last resort:`, { embed })
        } catch (e) {}
      }
    }
  }

  /**
   * Gets this server's specific welcome message, or the default
   * one if none is configured.
   * @param {object} data String replacement data
   * @returns {string} The processed string
   * @memberof DiscordServer
   */
  getWelcomeMessage (data, member) {
    return Util.formatDataString(this.getSetting('welcomeMessage'), data, member)
  }

  /**
   * Checks to see if this server has configured a custom welcome message.
   * @returns {boolean} True if this server has a custom welcome message
   * @memberof DiscordServer
   */
  hasCustomWelcomeMessage () {
    return DefaultSettings.welcomeMessage !== this.getSetting('welcomeMessage')
  }

  /**
   * Returns a DiscordMember belonging to this DiscordServer.
   * @param {Snowflake} id The id of the user
   * @returns {Promise<DiscordMember>} The new discord member
   * @memberof DiscordServer
   */
  async getMember (id) {
    return DiscordMember.new(this, id)
  }

  /**
   * Returns whether or not a role is in use by the verified, not verified, or binding roles
   *
   * @param {Snowflake} id The role id
   * @returns {boolean} True if the role is in use
   * @memberof DiscordServer
   */
  isRoleInUse (id) {
    if (this.getSetting('verifiedRole') === id) return true
    if (this.getSetting('verifiedRemovedRole') === id) return true

    for (const binding of this.getSetting('groupRankBindings')) {
      if (binding.role === id) return true
    }

    return false
  }
}

module.exports = DiscordServer
<|endoftext|>


<commit_before>import React, {Component} from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import EventListener from 'react-event-listener';
import keycode from 'keycode';
import autoPrefix from '../utils/autoPrefix';
import transitions from '../styles/transitions';
import Overlay from '../internal/Overlay';
import Paper from '../Paper';
import propTypes from '../utils/propTypes';

let openNavEventHandler = null;

class Drawer extends Component {
  static propTypes = {
    /**
     * The contents of the `Drawer`
     */
    children: PropTypes.node,
    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,
    /**
     * The CSS class name of the container element.
     */
    containerClassName: PropTypes.string,
    /**
     * Override the inline-styles of the container element.
     */
    containerStyle: PropTypes.object,
    /**
     * If true, swiping sideways when the `Drawer` is closed will not open it.
     */
    disableSwipeToOpen: PropTypes.bool,
    /**
     * If true, the `Drawer` will be docked. In this state, the overlay won't show and
     * clicking on a menu item will not close the `Drawer`.
     */
    docked: PropTypes.bool,
    /**
     * Property which is used to pass 'menu' string to ARIA role tag
     */
    drawerroleMenu: propTypes.string,
    /**
     * Property which is used to pass 'menuitem' string to ARIA role tag
     */
    drawerroleMenuItem: propTypes.string,
    /**
     * Callback function fired when the `open` state of the `Drawer` is requested to be changed.
     *
     * @param {boolean} open If true, the `Drawer` was requested to be opened.
     * @param {string} reason The reason for the open or close request. Possible values are
     * 'swipe' for open requests; 'clickaway' (on overlay clicks),
     * 'escape' (on escape key press), and 'swipe' for close requests.
     */
    onRequestChange: PropTypes.func,
    /**
     * If true, the `Drawer` is opened.  Providing a value will turn the `Drawer`
     * into a controlled component.
     */
    open: PropTypes.bool,
    /**
     * If true, the `Drawer` is positioned to open from the opposite side.
     */
    openSecondary: PropTypes.bool,
    /**
     * The CSS class name to add to the `Overlay` component that is rendered behind the `Drawer`.
     */
    overlayClassName: PropTypes.string,
    /**
     * Override the inline-styles of the `Overlay` component that is rendered behind the `Drawer`.
     */
    overlayStyle: PropTypes.object,
    /**
     * Override the inline-styles of the root element.
     */
    style: PropTypes.object,
    /**
     * The width of the left most (or right most) area in pixels where the `Drawer` can be
     * swiped open from. Setting this to `null` spans that area to the entire page
     * (**CAUTION!** Setting this property to `null` might cause issues with sliders and
     * swipeable `Tabs`: use at your own risk).
     */
    swipeAreaWidth: PropTypes.number,
    /**
     * The width of the `Drawer` in pixels or percentage in string format ex. `50%` to fill
     * half of the window or `100%` and so on. Defaults to using the values from theme.
     */
    width: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.number,
    ]),
    /**
     * The zDepth of the `Drawer`.
     */
    zDepth: propTypes.zDepth,

  };

  static defaultProps = {
    disableSwipeToOpen: false,
    docked: true,
    drawerroleMenu: 'menu',
    drawerroleMenuItem: 'menuitem',
    open: null,
    openSecondary: false,
    swipeAreaWidth: 30,
    width: null,
    zDepth: 2,
  };

  static contextTypes = {
    muiTheme: PropTypes.object.isRequired,
  };

  componentWillMount() {
    this.maybeSwiping = false;
    this.touchStartX = null;
    this.touchStartY = null;
    this.swipeStartX = null;

    this.setState({
      open: (this.props.open !== null ) ? this.props.open : this.props.docked,
      swiping: null,
    });
  }

  componentDidMount() {
    this.enableSwipeHandling();
  }

  componentWillReceiveProps(nextProps) {
    // If controlled then the open prop takes precedence.
    if (nextProps.open !== null) {
      this.setState({
        open: nextProps.open,
      });
      // Otherwise, if docked is changed, change the open state for when uncontrolled.
    } else if (this.props.docked !== nextProps.docked) {
      this.setState({
        open: nextProps.docked,
      });
    }
  }

  componentDidUpdate() {
    this.enableSwipeHandling();
  }

  componentWillUnmount() {
    this.disableSwipeHandling();
    this.removeBodyTouchListeners();
  }

  getStyles() {
    const muiTheme = this.context.muiTheme;
    const theme = muiTheme.drawer;

    const x = this.getTranslateMultiplier() * (this.state.open ? 0 : this.getMaxTranslateX());

    const styles = {
      root: {
        height: '100%',
        width: this.getTranslatedWidth() || theme.width,
        position: 'fixed',
        zIndex: muiTheme.zIndex.drawer,
        left: 0,
        top: 0,
        transform: `translate(${x}px, 0)`,
        transition: !this.state.swiping && transitions.easeOut(null, 'transform', null),
        backgroundColor: theme.color,
        overflow: 'auto',
        WebkitOverflowScrolling: 'touch', // iOS momentum scrolling
      },
      overlay: {
        zIndex: muiTheme.zIndex.drawerOverlay,
        pointerEvents: this.state.open ? 'auto' : 'none', // Bypass mouse events when left nav is closing.
      },
      rootWhenOpenRight: {
        left: 'auto',
        right: 0,
      },
    };

    return styles;
  }

  shouldShow() {
    return this.state.open || !!this.state.swiping;  // component is swiping
  }

  close(reason) {
    if (this.props.open === null) this.setState({open: false});
    if (this.props.onRequestChange) this.props.onRequestChange(false, reason);
    return this;
  }

  open(reason) {
    if (this.props.open === null) this.setState({open: true});
    if (this.props.onRequestChange) this.props.onRequestChange(true, reason);
    return this;
  }

  handleTouchTapOverlay = (event) => {
    event.preventDefault();
    this.close('clickaway');
  };

  handleKeyUp = (event) => {
    if (this.state.open && !this.props.docked && keycode(event) === 'esc') {
      this.close('escape');
    }
  };

  getTranslatedWidth() {
    if (typeof this.props.width === 'string') {
      if (!/^\d+(\.\d+)?%$/.test(this.props.width)) {
        throw new Error('Not a valid percentage format.');
      }
      const width = parseFloat(this.props.width) / 100.0;
      // We are doing our best on the Server to render a consistent UI, hence the
      // default value of 10000
      return typeof window !== 'undefined' ? width * window.innerWidth : 10000;
    } else {
      return this.props.width;
    }
  }

  getMaxTranslateX() {
    const width = this.getTranslatedWidth() || this.context.muiTheme.drawer.width;
    return width + 10;
  }

  getTranslateMultiplier() {
    return this.props.openSecondary ? 1 : -1;
  }

  enableSwipeHandling() {
    if (!this.props.docked) {
      document.body.addEventListener('touchstart', this.onBodyTouchStart);
      if (!openNavEventHandler) {
        openNavEventHandler = this.onBodyTouchStart;
      }
    } else {
      this.disableSwipeHandling();
    }
  }

  disableSwipeHandling() {
    document.body.removeEventListener('touchstart', this.onBodyTouchStart);
    if (openNavEventHandler === this.onBodyTouchStart) {
      openNavEventHandler = null;
    }
  }

  onBodyTouchStart = (event) => {
    const swipeAreaWidth = this.props.swipeAreaWidth;

    const touchStartX = this.context.muiTheme.isRtl ?
      (document.body.offsetWidth - event.touches[0].pageX) :
      event.touches[0].pageX;
    const touchStartY = event.touches[0].pageY;

    // Open only if swiping from far left (or right) while closed
    if (swipeAreaWidth !== null && !this.state.open) {
      if (this.props.openSecondary) {
        // If openSecondary is true calculate from the far right
        if (touchStartX < document.body.offsetWidth - swipeAreaWidth) return;
      } else {
        // If openSecondary is false calculate from the far left
        if (touchStartX > swipeAreaWidth) return;
      }
    }

    if (!this.state.open &&
         (openNavEventHandler !== this.onBodyTouchStart ||
          this.props.disableSwipeToOpen)
       ) {
      return;
    }

    this.maybeSwiping = true;
    this.touchStartX = touchStartX;
    this.touchStartY = touchStartY;

    document.body.addEventListener('touchmove', this.onBodyTouchMove);
    document.body.addEventListener('touchend', this.onBodyTouchEnd);
    document.body.addEventListener('touchcancel', this.onBodyTouchEnd);
  };

  removeBodyTouchListeners() {
    document.body.removeEventListener('touchmove', this.onBodyTouchMove);
    document.body.removeEventListener('touchend', this.onBodyTouchEnd);
    document.body.removeEventListener('touchcancel', this.onBodyTouchEnd);
  }

  setPosition(translateX) {
    const rtlTranslateMultiplier = this.context.muiTheme.isRtl ? -1 : 1;
    const drawer = ReactDOM.findDOMNode(this.refs.clickAwayableElement);
    const transformCSS = `translate(${(this.getTranslateMultiplier() * rtlTranslateMultiplier * translateX)}px, 0)`;
    this.refs.overlay.setOpacity(1 - translateX / this.getMaxTranslateX());
    autoPrefix.set(drawer.style, 'transform', transformCSS);
  }

  getTranslateX(currentX) {
    return Math.min(
             Math.max(
               this.state.swiping === 'closing' ?
                 this.getTranslateMultiplier() * (currentX - this.swipeStartX) :
                 this.getMaxTranslateX() - this.getTranslateMultiplier() * (this.swipeStartX - currentX),
               0
             ),
             this.getMaxTranslateX()
           );
  }

  onBodyTouchMove = (event) => {
    const currentX = this.context.muiTheme.isRtl ?
      (document.body.offsetWidth - event.touches[0].pageX) :
      event.touches[0].pageX;
    const currentY = event.touches[0].pageY;

    if (this.state.swiping) {
      event.preventDefault();
      this.setPosition(this.getTranslateX(currentX));
    } else if (this.maybeSwiping) {
      const dXAbs = Math.abs(currentX - this.touchStartX);
      const dYAbs = Math.abs(currentY - this.touchStartY);
      // If the user has moved his thumb ten pixels in either direction,
      // we can safely make an assumption about whether he was intending
      // to swipe or scroll.
      const threshold = 10;

      if (dXAbs > threshold && dYAbs <= threshold) {
        this.swipeStartX = currentX;
        this.setState({
          swiping: this.state.open ? 'closing' : 'opening',
        });
        this.setPosition(this.getTranslateX(currentX));
      } else if (dXAbs <= threshold && dYAbs > threshold) {
        this.onBodyTouchEnd();
      }
    }
  };

  onBodyTouchEnd = (event) => {
    if (this.state.swiping) {
      const currentX = this.context.muiTheme.isRtl ?
        (document.body.offsetWidth - event.changedTouches[0].pageX) :
        event.changedTouches[0].pageX;
      const translateRatio = this.getTranslateX(currentX) / this.getMaxTranslateX();

      this.maybeSwiping = false;
      const swiping = this.state.swiping;
      this.setState({
        swiping: null,
      });

      // We have to open or close after setting swiping to null,
      // because only then CSS transition is enabled.
      if (translateRatio > 0.5) {
        if (swiping === 'opening') {
          this.setPosition(this.getMaxTranslateX());
        } else {
          this.close('swipe');
        }
      } else {
        if (swiping === 'opening') {
          this.open('swipe');
        } else {
          this.setPosition(0);
        }
      }
    } else {
      this.maybeSwiping = false;
    }

    this.removeBodyTouchListeners();
  };

  render() {
    const {
      children,
      className,
      containerClassName,
      containerStyle,
      docked,
      drawerroleMenu,
      drawerroleMenuItem,
      openSecondary,
      overlayClassName,
      overlayStyle,
      style,
      zDepth,
    } = this.props;

    const styles = this.getStyles();

    let overlay;
    if (!docked) {
      overlay = (
        <Overlay
          ref="overlay"
          show={this.shouldShow()}
          className={overlayClassName}
          style={Object.assign(styles.overlay, overlayStyle)}
          transitionEnabled={!this.state.swiping}
          onTouchTap={this.handleTouchTapOverlay}
        />
      );
    }

    return (
      <div
        className={className}
        role={drawerroleMenu}
        style={style}
      >
        <EventListener target="window" onKeyUp={this.handleKeyUp} />
        {overlay}
        <Paper
          ref="clickAwayableElement"
          zDepth={zDepth}
          rounded={false}
          role={drawerroleMenuItem}
          transitionEnabled={!this.state.swiping}
          className={containerClassName}
          style={Object.assign(styles.root, openSecondary && styles.rootWhenOpenRight, containerStyle)}
        >
          {children}
        </Paper>
      </div>
    );
  }
}

export default Drawer;
<commit_msg>#115 Letter casing warning message fixed.<commit_after>import React, {Component} from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import EventListener from 'react-event-listener';
import keycode from 'keycode';
import autoPrefix from '../utils/autoPrefix';
import transitions from '../styles/transitions';
import Overlay from '../internal/Overlay';
import Paper from '../Paper';
import propTypes from '../utils/propTypes';

let openNavEventHandler = null;

class Drawer extends Component {
  static propTypes = {
    /**
     * The contents of the `Drawer`
     */
    children: PropTypes.node,
    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,
    /**
     * The CSS class name of the container element.
     */
    containerClassName: PropTypes.string,
    /**
     * Override the inline-styles of the container element.
     */
    containerStyle: PropTypes.object,
    /**
     * If true, swiping sideways when the `Drawer` is closed will not open it.
     */
    disableSwipeToOpen: PropTypes.bool,
    /**
     * If true, the `Drawer` will be docked. In this state, the overlay won't show and
     * clicking on a menu item will not close the `Drawer`.
     */
    docked: PropTypes.bool,
    /**
     * Property which is used to pass 'menu' string to ARIA role tag
     */
    drawerroleMenu: PropTypes.string,
    /**
     * Property which is used to pass 'menuitem' string to ARIA role tag
     */
    drawerroleMenuItem: PropTypes.string,
    /**
     * Callback function fired when the `open` state of the `Drawer` is requested to be changed.
     *
     * @param {boolean} open If true, the `Drawer` was requested to be opened.
     * @param {string} reason The reason for the open or close request. Possible values are
     * 'swipe' for open requests; 'clickaway' (on overlay clicks),
     * 'escape' (on escape key press), and 'swipe' for close requests.
     */
    onRequestChange: PropTypes.func,
    /**
     * If true, the `Drawer` is opened.  Providing a value will turn the `Drawer`
     * into a controlled component.
     */
    open: PropTypes.bool,
    /**
     * If true, the `Drawer` is positioned to open from the opposite side.
     */
    openSecondary: PropTypes.bool,
    /**
     * The CSS class name to add to the `Overlay` component that is rendered behind the `Drawer`.
     */
    overlayClassName: PropTypes.string,
    /**
     * Override the inline-styles of the `Overlay` component that is rendered behind the `Drawer`.
     */
    overlayStyle: PropTypes.object,
    /**
     * Override the inline-styles of the root element.
     */
    style: PropTypes.object,
    /**
     * The width of the left most (or right most) area in pixels where the `Drawer` can be
     * swiped open from. Setting this to `null` spans that area to the entire page
     * (**CAUTION!** Setting this property to `null` might cause issues with sliders and
     * swipeable `Tabs`: use at your own risk).
     */
    swipeAreaWidth: PropTypes.number,
    /**
     * The width of the `Drawer` in pixels or percentage in string format ex. `50%` to fill
     * half of the window or `100%` and so on. Defaults to using the values from theme.
     */
    width: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.number,
    ]),
    /**
     * The zDepth of the `Drawer`.
     */
    zDepth: propTypes.zDepth,

  };

  static defaultProps = {
    disableSwipeToOpen: false,
    docked: true,
    drawerroleMenu: 'menu',
    drawerroleMenuItem: 'menuitem',
    open: null,
    openSecondary: false,
    swipeAreaWidth: 30,
    width: null,
    zDepth: 2,
  };

  static contextTypes = {
    muiTheme: PropTypes.object.isRequired,
  };

  componentWillMount() {
    this.maybeSwiping = false;
    this.touchStartX = null;
    this.touchStartY = null;
    this.swipeStartX = null;

    this.setState({
      open: (this.props.open !== null ) ? this.props.open : this.props.docked,
      swiping: null,
    });
  }

  componentDidMount() {
    this.enableSwipeHandling();
  }

  componentWillReceiveProps(nextProps) {
    // If controlled then the open prop takes precedence.
    if (nextProps.open !== null) {
      this.setState({
        open: nextProps.open,
      });
      // Otherwise, if docked is changed, change the open state for when uncontrolled.
    } else if (this.props.docked !== nextProps.docked) {
      this.setState({
        open: nextProps.docked,
      });
    }
  }

  componentDidUpdate() {
    this.enableSwipeHandling();
  }

  componentWillUnmount() {
    this.disableSwipeHandling();
    this.removeBodyTouchListeners();
  }

  getStyles() {
    const muiTheme = this.context.muiTheme;
    const theme = muiTheme.drawer;

    const x = this.getTranslateMultiplier() * (this.state.open ? 0 : this.getMaxTranslateX());

    const styles = {
      root: {
        height: '100%',
        width: this.getTranslatedWidth() || theme.width,
        position: 'fixed',
        zIndex: muiTheme.zIndex.drawer,
        left: 0,
        top: 0,
        transform: `translate(${x}px, 0)`,
        transition: !this.state.swiping && transitions.easeOut(null, 'transform', null),
        backgroundColor: theme.color,
        overflow: 'auto',
        WebkitOverflowScrolling: 'touch', // iOS momentum scrolling
      },
      overlay: {
        zIndex: muiTheme.zIndex.drawerOverlay,
        pointerEvents: this.state.open ? 'auto' : 'none', // Bypass mouse events when left nav is closing.
      },
      rootWhenOpenRight: {
        left: 'auto',
        right: 0,
      },
    };

    return styles;
  }

  shouldShow() {
    return this.state.open || !!this.state.swiping;  // component is swiping
  }

  close(reason) {
    if (this.props.open === null) this.setState({open: false});
    if (this.props.onRequestChange) this.props.onRequestChange(false, reason);
    return this;
  }

  open(reason) {
    if (this.props.open === null) this.setState({open: true});
    if (this.props.onRequestChange) this.props.onRequestChange(true, reason);
    return this;
  }

  handleTouchTapOverlay = (event) => {
    event.preventDefault();
    this.close('clickaway');
  };

  handleKeyUp = (event) => {
    if (this.state.open && !this.props.docked && keycode(event) === 'esc') {
      this.close('escape');
    }
  };

  getTranslatedWidth() {
    if (typeof this.props.width === 'string') {
      if (!/^\d+(\.\d+)?%$/.test(this.props.width)) {
        throw new Error('Not a valid percentage format.');
      }
      const width = parseFloat(this.props.width) / 100.0;
      // We are doing our best on the Server to render a consistent UI, hence the
      // default value of 10000
      return typeof window !== 'undefined' ? width * window.innerWidth : 10000;
    } else {
      return this.props.width;
    }
  }

  getMaxTranslateX() {
    const width = this.getTranslatedWidth() || this.context.muiTheme.drawer.width;
    return width + 10;
  }

  getTranslateMultiplier() {
    return this.props.openSecondary ? 1 : -1;
  }

  enableSwipeHandling() {
    if (!this.props.docked) {
      document.body.addEventListener('touchstart', this.onBodyTouchStart);
      if (!openNavEventHandler) {
        openNavEventHandler = this.onBodyTouchStart;
      }
    } else {
      this.disableSwipeHandling();
    }
  }

  disableSwipeHandling() {
    document.body.removeEventListener('touchstart', this.onBodyTouchStart);
    if (openNavEventHandler === this.onBodyTouchStart) {
      openNavEventHandler = null;
    }
  }

  onBodyTouchStart = (event) => {
    const swipeAreaWidth = this.props.swipeAreaWidth;

    const touchStartX = this.context.muiTheme.isRtl ?
      (document.body.offsetWidth - event.touches[0].pageX) :
      event.touches[0].pageX;
    const touchStartY = event.touches[0].pageY;

    // Open only if swiping from far left (or right) while closed
    if (swipeAreaWidth !== null && !this.state.open) {
      if (this.props.openSecondary) {
        // If openSecondary is true calculate from the far right
        if (touchStartX < document.body.offsetWidth - swipeAreaWidth) return;
      } else {
        // If openSecondary is false calculate from the far left
        if (touchStartX > swipeAreaWidth) return;
      }
    }

    if (!this.state.open &&
         (openNavEventHandler !== this.onBodyTouchStart ||
          this.props.disableSwipeToOpen)
       ) {
      return;
    }

    this.maybeSwiping = true;
    this.touchStartX = touchStartX;
    this.touchStartY = touchStartY;

    document.body.addEventListener('touchmove', this.onBodyTouchMove);
    document.body.addEventListener('touchend', this.onBodyTouchEnd);
    document.body.addEventListener('touchcancel', this.onBodyTouchEnd);
  };

  removeBodyTouchListeners() {
    document.body.removeEventListener('touchmove', this.onBodyTouchMove);
    document.body.removeEventListener('touchend', this.onBodyTouchEnd);
    document.body.removeEventListener('touchcancel', this.onBodyTouchEnd);
  }

  setPosition(translateX) {
    const rtlTranslateMultiplier = this.context.muiTheme.isRtl ? -1 : 1;
    const drawer = ReactDOM.findDOMNode(this.refs.clickAwayableElement);
    const transformCSS = `translate(${(this.getTranslateMultiplier() * rtlTranslateMultiplier * translateX)}px, 0)`;
    this.refs.overlay.setOpacity(1 - translateX / this.getMaxTranslateX());
    autoPrefix.set(drawer.style, 'transform', transformCSS);
  }

  getTranslateX(currentX) {
    return Math.min(
             Math.max(
               this.state.swiping === 'closing' ?
                 this.getTranslateMultiplier() * (currentX - this.swipeStartX) :
                 this.getMaxTranslateX() - this.getTranslateMultiplier() * (this.swipeStartX - currentX),
               0
             ),
             this.getMaxTranslateX()
           );
  }

  onBodyTouchMove = (event) => {
    const currentX = this.context.muiTheme.isRtl ?
      (document.body.offsetWidth - event.touches[0].pageX) :
      event.touches[0].pageX;
    const currentY = event.touches[0].pageY;

    if (this.state.swiping) {
      event.preventDefault();
      this.setPosition(this.getTranslateX(currentX));
    } else if (this.maybeSwiping) {
      const dXAbs = Math.abs(currentX - this.touchStartX);
      const dYAbs = Math.abs(currentY - this.touchStartY);
      // If the user has moved his thumb ten pixels in either direction,
      // we can safely make an assumption about whether he was intending
      // to swipe or scroll.
      const threshold = 10;

      if (dXAbs > threshold && dYAbs <= threshold) {
        this.swipeStartX = currentX;
        this.setState({
          swiping: this.state.open ? 'closing' : 'opening',
        });
        this.setPosition(this.getTranslateX(currentX));
      } else if (dXAbs <= threshold && dYAbs > threshold) {
        this.onBodyTouchEnd();
      }
    }
  };

  onBodyTouchEnd = (event) => {
    if (this.state.swiping) {
      const currentX = this.context.muiTheme.isRtl ?
        (document.body.offsetWidth - event.changedTouches[0].pageX) :
        event.changedTouches[0].pageX;
      const translateRatio = this.getTranslateX(currentX) / this.getMaxTranslateX();

      this.maybeSwiping = false;
      const swiping = this.state.swiping;
      this.setState({
        swiping: null,
      });

      // We have to open or close after setting swiping to null,
      // because only then CSS transition is enabled.
      if (translateRatio > 0.5) {
        if (swiping === 'opening') {
          this.setPosition(this.getMaxTranslateX());
        } else {
          this.close('swipe');
        }
      } else {
        if (swiping === 'opening') {
          this.open('swipe');
        } else {
          this.setPosition(0);
        }
      }
    } else {
      this.maybeSwiping = false;
    }

    this.removeBodyTouchListeners();
  };

  render() {
    const {
      children,
      className,
      containerClassName,
      containerStyle,
      docked,
      drawerroleMenu,
      drawerroleMenuItem,
      openSecondary,
      overlayClassName,
      overlayStyle,
      style,
      zDepth,
    } = this.props;

    const styles = this.getStyles();

    let overlay;
    if (!docked) {
      overlay = (
        <Overlay
          ref="overlay"
          show={this.shouldShow()}
          className={overlayClassName}
          style={Object.assign(styles.overlay, overlayStyle)}
          transitionEnabled={!this.state.swiping}
          onTouchTap={this.handleTouchTapOverlay}
        />
      );
    }

    return (
      <div
        className={className}
        role={drawerroleMenu}
        style={style}
      >
        <EventListener target="window" onKeyUp={this.handleKeyUp} />
        {overlay}
        <Paper
          ref="clickAwayableElement"
          zDepth={zDepth}
          rounded={false}
          role={drawerroleMenuItem}
          transitionEnabled={!this.state.swiping}
          className={containerClassName}
          style={Object.assign(styles.root, openSecondary && styles.rootWhenOpenRight, containerStyle)}
        >
          {children}
        </Paper>
      </div>
    );
  }
}

export default Drawer;
<|endoftext|>


<commit_before>var sakai = sakai || {};

sakai._inbox = {};

sakai.inbox = function(){

  	var page = 1;
	var totalpages = false;
	var list = false;
  	var tosearchfor = false;
	var empty = true;
	var isbusy = false;
	var message = false;

	sdata.Ajax.request({
		httpMethod: "GET",
		url: "/sdata/me?sid=" + Math.random(),
		onSuccess: function(data){
			var me = eval('(' + data + ')');
			$("#user_id").text(me.items.firstname + " " + me.items.lastname);
		},
		onFail: function(status){

		}
	});

	var loadList = function(page){

		$("#list_rendered").html("
");

		sdata.Ajax.request({
			httpMethod: "GET",
			url: "/sdata/message?list=true&page=" + page + "&items=5&sid=" + Math.random(),
			onSuccess: function(data){
				list = eval('(' + data + ')');
				printList();
			},
			onFail: function(status){
				$("#list_rendered").html("An error has occurred. Please try again later");
			}
		});
	}

	sakai._inbox.startShowingList = function(){

		$("#messagedetail").hide();
		$("#new_message_main").hide();
		$("#listingcontent").show();
		loadList(page);

	}

	var printList = function(){

		$("#paging").show();
		if (page == 1) {
			doPaging();
		}
		if (totalpages == 1){
			$("#un_prev").show();
			$("#prev").hide();
			$("#un_next").show();
			$("#next").hide();
		} else if (page == 1){
			$("#un_prev").show();
			$("#prev").hide();
			$("#un_next").hide();
			$("#next").show();
		} else if (page == totalpages){
			$("#un_prev").hide();
			$("#prev").show();
			$("#un_next").show();
			$("#next").hide();
		} else {
			$("#un_prev").hide();
			$("#prev").show();
			$("#un_next").hide();
			$("#next").show();
		}
		doList();
	}

	var doPaging = function(){

		isbusy = true;

		$("#numberofresults").text(list.total);

		totalpages = Math.ceil(list.total / 5);

		var select = document.getElementById("pages");
		select.options.length = 0;

		for (var i = 0; i < totalpages; i++) {
			var ii = new Option("Page " + (i + 1) + " of " + totalpages, (i + 1));
			select.options[select.options.length] = ii;
		}

		if (totalpages == 0){
			$("#paging").hide();
		} else {
			$("#paging").show();
		}

		isbusy = false;

	}

	$("#pages").bind("change", function(ev){
		if (!isbusy) {
			var select = document.getElementById("pages");
			var index = select.selectedIndex;
			var val = select.options[index].value;
			var npage = parseInt(val);
			page = npage;
			loadList(page);
		}
	});

	$("#next").bind("click", function(ev){
		page++;

		var select = document.getElementById("pages");

		select.selectedIndex = page - 1;

		loadList(page);
	});

	$("#prev").bind("click", function(ev){
		page = page - 1;
		var select = document.getElementById("pages");

		select.selectedIndex = page - 1;
		loadList(page);
	});

	var doList = function(){

		for (var i = 0; i < list.items.length; i++){
			var el = list.items[i];
			if (!el.profileinfo){
				el.profileinfo = {};
				el.profileinfo.userid = el.sender;
			}
			if (el.profileinfo.picture){
				var pict = eval('(' + el.profileinfo.picture + ')');
				el.picture = "/sdata/f/public/" + el.sender + "/" + pict.name;
			}
			if (el.profileinfo.firstName && el.profileinfo.lastName){
				el.name = el.profileinfo.firstName + " " + el.profileinfo.lastName;
			} else {
				el.name = el.profileinfo.userid;
			}
			if (el.title.length > 100){
				el.title = el.title.substring(0, 100) + " ...";
			}
			//el.message = el.message.split("\n")[0];
			if (el.message.length > 100){
				el.message = el.message.substring(0, 100) + " ...";
			}
		}

		$("#list_rendered").html(sdata.html.Template.render("list_rendered_template", list));

		$(".message_list").bind("click", function(ev){
			var id = ev.currentTarget.id.split("_")[ev.currentTarget.id.split.length];
			showMessage(id);
		});

		$(".message_remove").bind("click", function(ev){
			var id = ev.currentTarget.id.split("_");
			var data = {"delete" : true , "id" : id[id.length - 1]};
			sdata.Ajax.request({
				url: "/sdata/message/?sid=" + Math.random(),
				httpMethod: "POST",
				onSuccess: function(data){
					sakai._inbox.startShowingList();
				},
				onFail : function(data){
					//alert("An error has occured");
				},
				postData: data,
				contentType: "application/x-www-form-urlencoded"
			});
		});
	}

	var currentmessage = false;

	sakai._inbox.showMessage = function(id){
		currentmessage = id;
		sdata.Ajax.request({
				httpMethod: "GET",
				url: "/sdata/message?id=" + id + "&sid=" + Math.random(),
				onSuccess: function(data){
					var json = eval('(' + data + ')');
					message = json;
					json.item.message = json.item.message.replace(/\n/g,"
");

					if (json.item.profileinfo.firstName && json.item.profileinfo.lastName){
						json.item.name = json.item.profileinfo.firstName + " " + json.item.profileinfo.lastName;
					} else {
						json.item.name = json.item.profileinfo.displayName;
					}

					$("#listingcontent").hide();
					$("#new_message_main").hide();
					$("#messagedetail").show();
					$("#messagedetail").html(sdata.html.Template.render("messagedetail_template", json));

					$("#back_to_inbox").bind("click", function(ev){
						History.addBEvent("list");
					});
					$("#message_accept_invitation").bind("click", function(ev){
						var inviter = message.item.sender;
						var data = {"accept" : true, "inviter" : inviter};

						sdata.Ajax.request({
							url: "/sdata/connection/?sid=" + Math.random(),
							httpMethod: "POST",
							onSuccess: function(data){
								sakai._inbox.showMessage(id);
							},
							onFail : function(data){
								//alert("An error has occured");
							},
							postData: data,
							contentType: "application/x-www-form-urlencoded"
						});

					});

					try {
						sakai.MyInbox.doInit();
					} catch (err){}

					$("#message_start_reply").bind("click", function(ev){
						History.addBEvent("reply|" + currentmessage);
					});

				},
				onFail: function(status){
					$("#listingcontent").hide();
					$("#messagedetail").show();
					$("#messagedetail").html("This message cannot be found");
				}
			});
	}

	$("#cancel_send_message").bind("click", function(ev){
		History.addBEvent("list");
	});

	$("#send_new_message").bind("click", function(ev){
		History.addBEvent("new");
	});

	sakai._inbox.startNewMessage = function(user){

		$("#new_title_txt").attr("value","");
		$("#new_message_txt").attr("value","");

		$("#new_to").css("color","#000000");
		$("#new_title").css("color","#000000");
		$("#new_message").css("color","#000000");

		$("#messagedetail").hide();
		$("#listingcontent").hide();
		$("#new_message_main").show();

		sdata.Ajax.request({
			url: "/sdata/connection/?show=accepted&sid=" + Math.random(),
			httpMethod: "GET",
			onSuccess: function(data){
				var json = eval('(' + data + ')');

				var select = document.getElementById("message_to");

				// Remove all options

				select.options.length = 0;

				// Add new basic option

				var op = new Option("-- Select a connection --","");
				select.options[select.options.length] = op;

				// Add all your friends

				for (var i in json.items){
					var name = json.items[i].userid;
					if (json.items[i].firstName && json.items[i].lastName){
						name = json.items[i].firstName + " " + json.items[i].lastName;
					}
					var op = new Option(name, json.items[i].userid);
					select.options[select.options.length] = op;
				}

				if (user){
					for (var i = 0; i < select.options.length; i++){
						if (select.options[i].value == user){
							select.selectedIndex = i;
						}
					}
				}

			},
			onFail : function(data){
				//alert("An error has occured");
			}
		});

	}

	$("#do_new_message").bind("click", function(ev){
		var res = sdata.FormBinder.serialize($("#new_form"));

		$("#new_to").css("color","#000000");
		$("#new_title").css("color","#000000");
		$("#new_message").css("color","#000000");

		var fine = true;
		if (!res["message_to"][0]){
			$("#new_to").css("color","#FF0000");
			fine = false;
		}

		if (!res["new_title_txt"]){
			$("#new_title").css("color","#FF0000");
			fine = false;
		}

		if (!res["new_message_txt"]){
			$("#new_message").css("color","#FF0000");
			fine = false;
		}

		if (fine){

			data = { "isinvite" : false , "receiver" : res["message_to"][0] , "title" : res["new_title_txt"], "message" : res["new_message_txt"] };

			sdata.Ajax.request({
				url: "/sdata/message/?sid=" + Math.random(),
				httpMethod: "POST",
				onSuccess: function(data){
					History.addBEvent("list");
				},
				onFail : function(data){
					alert("An error has occured");
				},
				postData: data,
				contentType: "application/x-www-form-urlencoded"
			});

		}

	});

	var showMessage = function(id){
		History.addBEvent("message|" + id);
	}

	sakai._inbox.startReply = function(id){

		sakai._inbox.startNewMessage();

		sdata.Ajax.request({
			httpMethod: "GET",
			url: "/sdata/message?id=" + id + "&sid=" + Math.random(),
			onSuccess: function(data){
				var json = eval('(' + data + ')');
				$("#new_title_txt").attr("value","Re: " + json.item.title);
				$("#new_message_txt").attr("value", "\n\n> " + json.item.message.replace(/\n/g,"\n> "));

				var select = document.getElementById("message_to");
				for (var i = 0; i < select.options.length; i++){
					if (select.options[i].value == json.item.sender){
						select.selectedIndex = i;
					}
				}

			},
			onFail : function(status){
				//alert("An error has occured");
			}
		});

	};

	History.history_change();

};

sdata.registerForLoad("sakai.inbox");
<commit_msg>SAK-15667<commit_after>var sakai = sakai || {};

sakai._inbox = {};

sakai.inbox = function(){

  	var page = 1;
	var totalpages = false;
	var list = false;
  	var tosearchfor = false;
	var empty = true;
	var isbusy = false;
	var message = false;

	sdata.Ajax.request({
		httpMethod: "GET",
		url: "/rest/me?sid=" + Math.random(),
		onSuccess: function(data){
			var me = eval('(' + data + ')');
			if (!me.preferences.uuid){
				var redirect = document.location;
				document.location = "/dev/?url=" + sdata.util.URL.encode(redirect.pathname + redirect.search + redirect.hash);
			}
			$("#user_id").text(me.profile.firstName + " " + me.profile.lastName);
		},
		onFail: function(status){

		}
	});

	var loadList = function(page){

		$("#list_rendered").html("
");

		sdata.Ajax.request({
			httpMethod: "GET",
			url: "/sdata/message?list=true&page=" + page + "&items=5&sid=" + Math.random(),
			onSuccess: function(data){
				list = eval('(' + data + ')');
				printList();
			},
			onFail: function(status){
				$("#list_rendered").html("An error has occurred. Please try again later");
			}
		});
	}

	sakai._inbox.startShowingList = function(){

		$("#messagedetail").hide();
		$("#new_message_main").hide();
		$("#listingcontent").show();
		loadList(page);

	}

	var printList = function(){

		$("#paging").show();
		if (page == 1) {
			doPaging();
		}
		if (totalpages == 1){
			$("#un_prev").show();
			$("#prev").hide();
			$("#un_next").show();
			$("#next").hide();
		} else if (page == 1){
			$("#un_prev").show();
			$("#prev").hide();
			$("#un_next").hide();
			$("#next").show();
		} else if (page == totalpages){
			$("#un_prev").hide();
			$("#prev").show();
			$("#un_next").show();
			$("#next").hide();
		} else {
			$("#un_prev").hide();
			$("#prev").show();
			$("#un_next").hide();
			$("#next").show();
		}
		doList();
	}

	var doPaging = function(){

		isbusy = true;

		$("#numberofresults").text(list.total);

		totalpages = Math.ceil(list.total / 5);

		var select = document.getElementById("pages");
		select.options.length = 0;

		for (var i = 0; i < totalpages; i++) {
			var ii = new Option("Page " + (i + 1) + " of " + totalpages, (i + 1));
			select.options[select.options.length] = ii;
		}

		if (totalpages == 0){
			$("#paging").hide();
		} else {
			$("#paging").show();
		}

		isbusy = false;

	}

	$("#pages").bind("change", function(ev){
		if (!isbusy) {
			var select = document.getElementById("pages");
			var index = select.selectedIndex;
			var val = select.options[index].value;
			var npage = parseInt(val);
			page = npage;
			loadList(page);
		}
	});

	$("#next").bind("click", function(ev){
		page++;

		var select = document.getElementById("pages");

		select.selectedIndex = page - 1;

		loadList(page);
	});

	$("#prev").bind("click", function(ev){
		page = page - 1;
		var select = document.getElementById("pages");

		select.selectedIndex = page - 1;
		loadList(page);
	});

	var doList = function(){

		for (var i = 0; i < list.items.length; i++){
			var el = list.items[i];
			if (!el.profileinfo){
				el.profileinfo = {};
				el.profileinfo.userid = el.sender;
			}
			if (el.profileinfo.picture){
				var pict = eval('(' + el.profileinfo.picture + ')');
				el.picture = "/sdata/f/public/" + el.sender + "/" + pict.name;
			}
			if (el.profileinfo.firstName && el.profileinfo.lastName){
				el.name = el.profileinfo.firstName + " " + el.profileinfo.lastName;
			} else {
				el.name = el.profileinfo.userid;
			}
			if (el.title.length > 100){
				el.title = el.title.substring(0, 100) + " ...";
			}
			//el.message = el.message.split("\n")[0];
			if (el.message.length > 100){
				el.message = el.message.substring(0, 100) + " ...";
			}
		}

		$("#list_rendered").html(sdata.html.Template.render("list_rendered_template", list));

		$(".message_list").bind("click", function(ev){
			var id = ev.currentTarget.id.split("_")[ev.currentTarget.id.split.length];
			showMessage(id);
		});

		$(".message_remove").bind("click", function(ev){
			var id = ev.currentTarget.id.split("_");
			var data = {"delete" : true , "id" : id[id.length - 1]};
			sdata.Ajax.request({
				url: "/sdata/message/?sid=" + Math.random(),
				httpMethod: "POST",
				onSuccess: function(data){
					sakai._inbox.startShowingList();
				},
				onFail : function(data){
					//alert("An error has occured");
				},
				postData: data,
				contentType: "application/x-www-form-urlencoded"
			});
		});
	}

	var currentmessage = false;

	sakai._inbox.showMessage = function(id){
		currentmessage = id;
		sdata.Ajax.request({
				httpMethod: "GET",
				url: "/sdata/message?id=" + id + "&sid=" + Math.random(),
				onSuccess: function(data){
					var json = eval('(' + data + ')');
					message = json;
					json.item.message = json.item.message.replace(/\n/g,"
");

					if (json.item.profileinfo.firstName && json.item.profileinfo.lastName){
						json.item.name = json.item.profileinfo.firstName + " " + json.item.profileinfo.lastName;
					} else {
						json.item.name = json.item.profileinfo.displayName;
					}

					$("#listingcontent").hide();
					$("#new_message_main").hide();
					$("#messagedetail").show();
					$("#messagedetail").html(sdata.html.Template.render("messagedetail_template", json));

					$("#back_to_inbox").bind("click", function(ev){
						History.addBEvent("list");
					});
					$("#message_accept_invitation").bind("click", function(ev){
						var inviter = message.item.sender;
						var data = {"accept" : true, "inviter" : inviter};

						sdata.Ajax.request({
							url: "/sdata/connection/?sid=" + Math.random(),
							httpMethod: "POST",
							onSuccess: function(data){
								sakai._inbox.showMessage(id);
							},
							onFail : function(data){
								//alert("An error has occured");
							},
							postData: data,
							contentType: "application/x-www-form-urlencoded"
						});

					});

					try {
						sakai.MyInbox.doInit();
					} catch (err){}

					$("#message_start_reply").bind("click", function(ev){
						History.addBEvent("reply|" + currentmessage);
					});

				},
				onFail: function(status){
					$("#listingcontent").hide();
					$("#messagedetail").show();
					$("#messagedetail").html("This message cannot be found");
				}
			});
	}

	$("#cancel_send_message").bind("click", function(ev){
		History.addBEvent("list");
	});

	$("#send_new_message").bind("click", function(ev){
		History.addBEvent("new");
	});

	sakai._inbox.startNewMessage = function(user){

		$("#new_title_txt").attr("value","");
		$("#new_message_txt").attr("value","");

		$("#new_to").css("color","#000000");
		$("#new_title").css("color","#000000");
		$("#new_message").css("color","#000000");

		$("#messagedetail").hide();
		$("#listingcontent").hide();
		$("#new_message_main").show();

		sdata.Ajax.request({
			url: "/rest/friend/status?p=0&n=100&friendStatus=ACCEPTED&s=firstName&s=lastName&o=asc&o=asc&sid=" + Math.random(),
			httpMethod: "GET",
			onSuccess: function(data){
				var json = eval('(' + data + ')');

				var select = $("#message_to").get(0);

				// Remove all options

				select.options.length = 0;

				// Add new basic option

				var op = new Option("-- Select a connection --","");
				select.options[select.options.length] = op;

				// Add all your friends

				if (json.status.friends) {
					for (var i = 0; i < json.status.friends.length; i++) {
						var name = json.status.friends[i].friendUuid;
						if (json.status.friends[i].profile.firstName && json.status.friends[i].profile.lastName) {
							name =json.status.friends[i].profile.firstName + " " + json.status.friends[i].profile.lastName;
						}
						var op = new Option(name, json.status.friends[i].friendUuid);
						select.options[select.options.length] = op;
					}
				}

				if (user){
					for (var i = 0; i < select.options.length; i++){
						if (select.options[i].value == user){
							select.selectedIndex = i;
						}
					}
				}

			},
			onFail : function(data){
				//alert("An error has occured");
			}
		});

	}

	$("#do_new_message").bind("click", function(ev){
		var res = sdata.FormBinder.serialize($("#new_form"));

		$("#new_to").css("color","#000000");
		$("#new_title").css("color","#000000");
		$("#new_message").css("color","#000000");

		var fine = true;
		if (!res["message_to"][0]){
			$("#new_to").css("color","#FF0000");
			fine = false;
		}

		if (!res["new_title_txt"]){
			$("#new_title").css("color","#FF0000");
			fine = false;
		}

		if (!res["new_message_txt"]){
			$("#new_message").css("color","#FF0000");
			fine = false;
		}

		if (fine){

			data = { "isinvite" : false , "receiver" : res["message_to"][0] , "title" : res["new_title_txt"], "message" : res["new_message_txt"] };

			sdata.Ajax.request({
				url: "/sdata/message/?sid=" + Math.random(),
				httpMethod: "POST",
				onSuccess: function(data){
					History.addBEvent("list");
				},
				onFail : function(data){
					alert("An error has occured");
				},
				postData: data,
				contentType: "application/x-www-form-urlencoded"
			});

		}

	});

	var showMessage = function(id){
		History.addBEvent("message|" + id);
	}

	sakai._inbox.startReply = function(id){

		sakai._inbox.startNewMessage();

		sdata.Ajax.request({
			httpMethod: "GET",
			url: "/sdata/message?id=" + id + "&sid=" + Math.random(),
			onSuccess: function(data){
				var json = eval('(' + data + ')');
				$("#new_title_txt").attr("value","Re: " + json.item.title);
				$("#new_message_txt").attr("value", "\n\n> " + json.item.message.replace(/\n/g,"\n> "));

				var select = document.getElementById("message_to");
				for (var i = 0; i < select.options.length; i++){
					if (select.options[i].value == json.item.sender){
						select.selectedIndex = i;
					}
				}

			},
			onFail : function(status){
				//alert("An error has occured");
			}
		});

	};

	History.history_change();

};

sdata.registerForLoad("sakai.inbox");
<|endoftext|>


<commit_before>const TODOS = [ // 不支持的 API 列表
  'hideTabBar',
  'hideTabBarRedDot',
  'removeTabBarBadge',
  'setTabBarBadge',
  'setTabBarItem',
  'setTabBarStyle',
  'showTabBar',
  'showTabBarRedDot',
  'startPullDownRefresh',
  'saveImageToPhotosAlbum',
  'getRecorderManager',
  'getBackgroundAudioManager',
  'createInnerAudioContext',
  'chooseVideo',
  'saveVideoToPhotosAlbum',
  'createVideoContext',
  'openDocument',
  'startAccelerometer',
  'startCompass',
  'addPhoneContact',
  'createIntersectionObserver'
]

function _handleNetworkInfo (result) {
  switch (result.networkType) {
    case 'NOTREACHABLE':
      result.networkType = 'none'
      break
    case 'WWAN':
      // TODO ?
      result.networkType = '3g'
      break
    default:
      result.networkType = result.networkType.toLowerCase()
      break
  }
  return {}
}

const protocols = { // 需要做转换的 API 列表
  returnValue (methodName, res) { // 通用 returnValue 解析
    if (res.error || res.errorMessage) {
      res.errMsg = `${methodName}:fail ${res.errorMessage || res.error}`
      delete res.error
      delete res.errorMessage
    } else {
      res.errMsg = `${methodName}:ok`
    }
    return res
  },
  request: {
    name: 'httpRequest',
    args (fromArgs) {
      if (!fromArgs.header) { // 默认增加 header 参数，方便格式化 content-type
        fromArgs.header = {}
      }
      return {
        header (header = {}, toArgs) {
          const headers = {
            'content-type': 'application/json'
          }
          Object.keys(header).forEach(key => {
            headers[key.toLocaleLowerCase()] = header[key]
          })
          return {
            name: 'headers',
            value: headers
          }
        },
        method: 'method', // TODO 支付宝小程序仅支持 get,post
        responseType: false
      }
    },
    returnValue: {
      status: 'statusCode',
      headers: 'header'
    }
  },
  setNavigationBarColor: {
    name: 'setNavigationBar',
    args: {
      frontColor: false,
      animation: false
    }
  },
  setNavigationBarTitle: {
    name: 'setNavigationBar'
  },
  showModal ({
    showCancel = true
  } = {}) {
    if (showCancel) {
      return {
        name: 'confirm',
        args: {
          cancelColor: false,
          confirmColor: false,
          cancelText: 'cancelButtonText',
          confirmText: 'confirmButtonText'
        },
        returnValue (fromRes, toRes) {
          toRes.confirm = fromRes.confirm
          toRes.cancel = !fromRes.confirm
        }
      }
    }
    return {
      name: 'alert',
      args: {
        confirmColor: false,
        confirmText: 'buttonText'
      },
      returnValue (fromRes, toRes) {
        toRes.confirm = true
        toRes.cancel = false
      }
    }
  },
  showToast ({
    icon = 'success'
  } = {}) {
    const args = {
      title: 'content',
      icon: 'type',
      duration: false,
      image: false,
      mask: false
    }
    if (icon === 'loading') {
      return {
        name: 'showLoading',
        args
      }
    }
    return {
      name: 'showToast',
      args
    }
  },
  showActionSheet: {
    name: 'showActionSheet',
    args: {
      itemList: 'items',
      itemColor: false
    },
    returnValue: {
      index: 'tapIndex'
    }
  },
  showLoading: {
    args: {
      title: 'content',
      mask: false
    }
  },
  uploadFile: {
    args: {
      name: 'fileName'
    }
    // 从测试结果看，是有返回对象的，文档上没有说明。
  },
  downloadFile: {
    returnValue: {
      apFilePath: 'tempFilePath'
    }
  },
  connectSocket: {
    args: {
      method: false,
      protocols: false
    }
    // TODO 有没有返回值还需要测试下
  },
  chooseImage: {
    returnValue: {
      apFilePaths: 'tempFilePaths'
    }
  },
  previewImage: {
    args (fromArgs) {
      // 支付宝小程序的 current 是索引值，而非图片地址。
      if (fromArgs.current && Array.isArray(fromArgs.urls)) {
        const index = fromArgs.urls.indexOf(fromArgs.current)
        fromArgs.current = ~index ? index : 0
      }
      return {
        indicator: false,
        loop: false
      }
    }
  },
  saveFile: {
    args: {
      tempFilePath: 'apFilePath'
    },
    returnValue: {
      apFilePath: 'savedFilePath'
    }
  },
  getSavedFileInfo: {
    args: {
      filePath: 'apFilePath'
    },
    returnValue (result) {
      if (result.fileList && result.fileList.length) {
        result.fileList.forEach(file => {
          file.filePath = file.apFilePath
          delete file.apFilePath
        })
      }
      return {}
    }
  },
  removeSavedFile: {
    args: {
      filePath: 'apFilePath'
    }
  },
  getLocation: {
    args: {
      type: false,
      altitude: false
    }
    //     returnValue: {
    //       speed: false,
    //       altitude: false,
    //       verticalAccuracy: false
    //     }
  },
  openLocation: {
    args: {
      // TODO address 参数在阿里上是必传的
    }
  },
  getSystemInfo: {
    //     returnValue: {
    //       brand: false,
    //       statusBarHeight: false,
    //       SDKVersion: false
    //     }
  },
  getSystemInfoSync: {
    //     returnValue: {
    //       brand: false,
    //       statusBarHeight: false,
    //       SDKVersion: false
    //     }
  },
  getNetworkType: {
    returnValue: _handleNetworkInfo
  },
  onNetworkStatusChange: {
    returnValue: _handleNetworkInfo
  },
  stopAccelerometer: {
    name: 'offAccelerometerChange'
  },
  stopCompass: {
    name: 'offCompassChange'
  },
  scanCode: {
    name: 'scan',
    args: {
      onlyFromCamera: 'hideAlbum',
      scanType: false
    }
  },
  setClipboardData: {
    name: 'setClipboard',
    args: {
      data: 'text'
    }
  },
  getClipboardData: {
    name: 'getClipboard',
    returnValue: {
      text: 'data'
    }
  },
  pageScrollTo: {
    args: {
      duration: false
    }
  }
}

TODOS.forEach(todoApi => {
  protocols[todoApi] = false
})

export default protocols
<commit_msg>fix: mp-alipay scanCode<commit_after>const TODOS = [ // 不支持的 API 列表
  'hideTabBar',
  'hideTabBarRedDot',
  'removeTabBarBadge',
  'setTabBarBadge',
  'setTabBarItem',
  'setTabBarStyle',
  'showTabBar',
  'showTabBarRedDot',
  'startPullDownRefresh',
  'saveImageToPhotosAlbum',
  'getRecorderManager',
  'getBackgroundAudioManager',
  'createInnerAudioContext',
  'chooseVideo',
  'saveVideoToPhotosAlbum',
  'createVideoContext',
  'openDocument',
  'startAccelerometer',
  'startCompass',
  'addPhoneContact',
  'createIntersectionObserver'
]

function _handleNetworkInfo (result) {
  switch (result.networkType) {
    case 'NOTREACHABLE':
      result.networkType = 'none'
      break
    case 'WWAN':
      // TODO ?
      result.networkType = '3g'
      break
    default:
      result.networkType = result.networkType.toLowerCase()
      break
  }
  return {}
}

const protocols = { // 需要做转换的 API 列表
  returnValue (methodName, res) { // 通用 returnValue 解析
    if (res.error || res.errorMessage) {
      res.errMsg = `${methodName}:fail ${res.errorMessage || res.error}`
      delete res.error
      delete res.errorMessage
    } else {
      res.errMsg = `${methodName}:ok`
    }
    return res
  },
  request: {
    name: 'httpRequest',
    args (fromArgs) {
      if (!fromArgs.header) { // 默认增加 header 参数，方便格式化 content-type
        fromArgs.header = {}
      }
      return {
        header (header = {}, toArgs) {
          const headers = {
            'content-type': 'application/json'
          }
          Object.keys(header).forEach(key => {
            headers[key.toLocaleLowerCase()] = header[key]
          })
          return {
            name: 'headers',
            value: headers
          }
        },
        method: 'method', // TODO 支付宝小程序仅支持 get,post
        responseType: false
      }
    },
    returnValue: {
      status: 'statusCode',
      headers: 'header'
    }
  },
  setNavigationBarColor: {
    name: 'setNavigationBar',
    args: {
      frontColor: false,
      animation: false
    }
  },
  setNavigationBarTitle: {
    name: 'setNavigationBar'
  },
  showModal ({
    showCancel = true
  } = {}) {
    if (showCancel) {
      return {
        name: 'confirm',
        args: {
          cancelColor: false,
          confirmColor: false,
          cancelText: 'cancelButtonText',
          confirmText: 'confirmButtonText'
        },
        returnValue (fromRes, toRes) {
          toRes.confirm = fromRes.confirm
          toRes.cancel = !fromRes.confirm
        }
      }
    }
    return {
      name: 'alert',
      args: {
        confirmColor: false,
        confirmText: 'buttonText'
      },
      returnValue (fromRes, toRes) {
        toRes.confirm = true
        toRes.cancel = false
      }
    }
  },
  showToast ({
    icon = 'success'
  } = {}) {
    const args = {
      title: 'content',
      icon: 'type',
      duration: false,
      image: false,
      mask: false
    }
    if (icon === 'loading') {
      return {
        name: 'showLoading',
        args
      }
    }
    return {
      name: 'showToast',
      args
    }
  },
  showActionSheet: {
    name: 'showActionSheet',
    args: {
      itemList: 'items',
      itemColor: false
    },
    returnValue: {
      index: 'tapIndex'
    }
  },
  showLoading: {
    args: {
      title: 'content',
      mask: false
    }
  },
  uploadFile: {
    args: {
      name: 'fileName'
    }
    // 从测试结果看，是有返回对象的，文档上没有说明。
  },
  downloadFile: {
    returnValue: {
      apFilePath: 'tempFilePath'
    }
  },
  connectSocket: {
    args: {
      method: false,
      protocols: false
    }
    // TODO 有没有返回值还需要测试下
  },
  chooseImage: {
    returnValue: {
      apFilePaths: 'tempFilePaths'
    }
  },
  previewImage: {
    args (fromArgs) {
      // 支付宝小程序的 current 是索引值，而非图片地址。
      if (fromArgs.current && Array.isArray(fromArgs.urls)) {
        const index = fromArgs.urls.indexOf(fromArgs.current)
        fromArgs.current = ~index ? index : 0
      }
      return {
        indicator: false,
        loop: false
      }
    }
  },
  saveFile: {
    args: {
      tempFilePath: 'apFilePath'
    },
    returnValue: {
      apFilePath: 'savedFilePath'
    }
  },
  getSavedFileInfo: {
    args: {
      filePath: 'apFilePath'
    },
    returnValue (result) {
      if (result.fileList && result.fileList.length) {
        result.fileList.forEach(file => {
          file.filePath = file.apFilePath
          delete file.apFilePath
        })
      }
      return {}
    }
  },
  removeSavedFile: {
    args: {
      filePath: 'apFilePath'
    }
  },
  getLocation: {
    args: {
      type: false,
      altitude: false
    }
    //     returnValue: {
    //       speed: false,
    //       altitude: false,
    //       verticalAccuracy: false
    //     }
  },
  openLocation: {
    args: {
      // TODO address 参数在阿里上是必传的
    }
  },
  getSystemInfo: {
    //     returnValue: {
    //       brand: false,
    //       statusBarHeight: false,
    //       SDKVersion: false
    //     }
  },
  getSystemInfoSync: {
    //     returnValue: {
    //       brand: false,
    //       statusBarHeight: false,
    //       SDKVersion: false
    //     }
  },
  getNetworkType: {
    returnValue: _handleNetworkInfo
  },
  onNetworkStatusChange: {
    returnValue: _handleNetworkInfo
  },
  stopAccelerometer: {
    name: 'offAccelerometerChange'
  },
  stopCompass: {
    name: 'offCompassChange'
  },
  scanCode: {
    name: 'scan',
    args: {
      onlyFromCamera: 'hideAlbum',
      scanType: false
    },
    returnValue: {
      code: 'result'
    }
  },
  setClipboardData: {
    name: 'setClipboard',
    args: {
      data: 'text'
    }
  },
  getClipboardData: {
    name: 'getClipboard',
    returnValue: {
      text: 'data'
    }
  },
  pageScrollTo: {
    args: {
      duration: false
    }
  }
}

TODOS.forEach(todoApi => {
  protocols[todoApi] = false
})

export default protocols
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Route, withRouter } from 'react-router-dom';

const isModifiedEvent = (event) =>
  !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);

export class LinkContainer extends Component {
  static propTypes = {
    history: PropTypes.shape({
      push: PropTypes.func.isRequired,
      replace: PropTypes.func.isRequired,
      createHref: PropTypes.func.isRequired,
    }),
    children: PropTypes.element.isRequired,
    onClick: PropTypes.func,
    replace: PropTypes.bool,
    to: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.object,
    ]).isRequired,
    exact: PropTypes.bool,
    strict: PropTypes.bool,
    className: PropTypes.string,
    activeClassName: PropTypes.string,
    style: PropTypes.object,
    activeStyle: PropTypes.object,
    isActive: PropTypes.func,
  };

  static defaultProps = {
    replace: false,
    exact: false,
    strict: false,
    activeClassName: 'active',
  };

  handleClick = (event) => {
    const { children, onClick } = this.props;

    if (children.props.onClick) {
      children.props.onClick(event);
    }

    if (onClick) {
      onClick(event);
    }

    if (
      !event.defaultPrevented && // onClick prevented default
      event.button === 0 && // ignore right clicks
      !isModifiedEvent(event) // ignore clicks with modifier keys
    ) {
      event.preventDefault();

      const { replace, to, history } = this.props;

      if (replace) {
        history.replace(to);
      } else {
        history.push(to);
      }
    }
  };

  render() {
    const {
      history,
      children,
      replace, // eslint-disable-line no-unused-vars
      to,
      exact,
      strict,
      activeClassName,
      className,
      activeStyle,
      style,
      isActive: getIsActive,
      ...props,
    } = this.props;

    const href = history.createHref(
      typeof to === 'string' ? { pathname: to } : to
    );

    const child = React.Children.only(children);

    return (
      <Route
        path={typeof to === 'object' ? to.pathname : to}
        exact={exact}
        strict={strict}
        children={({ location, match }) => {
          const isActive = !!(getIsActive ? getIsActive(match, location) : match);

          return React.cloneElement(
            child,
            {
              ...props,
              className: [className, child.props.className, isActive ? activeClassName : null]
                .join(' ').trim(),
              style: isActive ? { ...style, ...activeStyle } : style,
              href,
              onClick: this.handleClick,
            }
          );
        }}
      />
    );
  }
}

export default withRouter(LinkContainer);
<commit_msg>Prop fixes for LinkContainer  * Made `history` propType required  * Added propTypes: `location`, `match`, `staticContext`, since `withRouter` will provide them  * Updated `render()` to strip props (`location`, `match`, `staticContext`) from children<commit_after>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Route, withRouter } from 'react-router-dom';

const isModifiedEvent = (event) =>
  !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);

export class LinkContainer extends Component {
  static propTypes = {
    history: PropTypes.shape({
      push: PropTypes.func.isRequired,
      replace: PropTypes.func.isRequired,
      createHref: PropTypes.func.isRequired,
    }).isRequired,
    location: PropTypes.object,
    match: PropTypes.object,
    staticContext: PropTypes.object,
    children: PropTypes.element.isRequired,
    onClick: PropTypes.func,
    replace: PropTypes.bool,
    to: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.object,
    ]).isRequired,
    exact: PropTypes.bool,
    strict: PropTypes.bool,
    className: PropTypes.string,
    activeClassName: PropTypes.string,
    style: PropTypes.object,
    activeStyle: PropTypes.object,
    isActive: PropTypes.func,
  };

  static defaultProps = {
    replace: false,
    exact: false,
    strict: false,
    activeClassName: 'active',
  };

  handleClick = (event) => {
    const { children, onClick } = this.props;

    if (children.props.onClick) {
      children.props.onClick(event);
    }

    if (onClick) {
      onClick(event);
    }

    if (
      !event.defaultPrevented && // onClick prevented default
      event.button === 0 && // ignore right clicks
      !isModifiedEvent(event) // ignore clicks with modifier keys
    ) {
      event.preventDefault();

      const { replace, to, history } = this.props;

      if (replace) {
        history.replace(to);
      } else {
        history.push(to);
      }
    }
  };

  render() {
    const {
      history,
      location: _location,            // eslint-disable-line no-unused-vars
      match: _match,                  // eslint-disable-line no-unused-vars
      staticContext: _staticContext,  // eslint-disable-line no-unused-vars
      children,
      replace,                          // eslint-disable-line no-unused-vars
      to,
      exact,
      strict,
      activeClassName,
      className,
      activeStyle,
      style,
      isActive: getIsActive,
      ...props,
    } = this.props;

    const href = history.createHref(
      typeof to === 'string' ? { pathname: to } : to
    );

    const child = React.Children.only(children);

    return (
      <Route
        path={typeof to === 'object' ? to.pathname : to}
        exact={exact}
        strict={strict}
        children={({ location, match }) => {
          const isActive = !!(getIsActive ? getIsActive(match, location) : match);

          return React.cloneElement(
            child,
            {
              ...props,
              className: [className, child.props.className, isActive ? activeClassName : null]
                .join(' ').trim(),
              style: isActive ? { ...style, ...activeStyle } : style,
              href,
              onClick: this.handleClick,
            }
          );
        }}
      />
    );
  }
}

export default withRouter(LinkContainer);
<|endoftext|>


<commit_before>/**
 * @file ListItem
 * @author qiusiqi(qiusiqi@baidu.com)
 */

import san from 'san';
import {TouchRipple, CenterRipple} from '../Ripple';
import Icon from '../Icon';
import padStyles from '../filters/padStyles';

export default san.defineComponent({

    template: `

<slot name="left"></slot>

{{ primaryText }}

{{ secondaryText | raw }}

            <san-touch-ripple san-if="!disableRipple && !disabled" />

<slot name="right"></slot>

                <san-icon>expand_{{ open | listOpenIcon }}</san-icon>
                <san-center-ripple />

                <slot name="nested"></slot>

    `,

    components: {
        'san-touch-ripple': TouchRipple,
        'san-center-ripple': CenterRipple,
        'san-icon': Icon
    },

    messages: {
        'UI:nested-counter'(arg) {
            let target = arg.value;

            target.set('nestedLevel', target.get('nestedLevel') + 1);
            this.dispatch('UI:nested-counter', target);
        }
    },

    initData() {
        return {
            nestedLevel: 1,
            secondaryTextLines: 1
        };
    },

    filters: {
        padStyles,

        listOpenIcon(open) {
            return open ? 'less' : 'more';
        },
        listOpen(open) {
            return open ? '' : 'hide';
        }
    },

    computed: {
        listItemClass() {
            return (this.data.get('disabled') ? 'disabled' : '')
                + (this.data.get('inset') ? ' inset' : '')
                + (this.data.get('toggleNested') ? ' nested' : '')
                + (this.data.get('secondaryTextLines') > 1 ? ' three-lines' : '')
                + (
                    this.data.get('selectValue')
                    && this.data.get('selectValue') === this.data.get('value') ? ' selected' : ''
                );
        },
        itemContentStyle() {
            return {
                marginLeft: (this.data.get('nestedLevel') - 1) * 16 + 'px',
                paddingLeft: this.data.get('hasLeft') ? '72px' : '16px',
                paddingRight: this.data.get('hasRight') ? '56px' : '16px'
            };
        },
        secondaryTextStyle() {
            return {
                '-webkit-line-clamp': this.data.get('secondaryTextLines')
            };
        }
    },

    inited() {
        this.transBoolean('inset');
        this.transBoolean('disabled');
        this.transBoolean('toggleNested');
        this.transBoolean('disableRipple');
        this.transBoolean('primaryTogglesNestedList');
        this.transBoolean('initiallyOpen');
        this.data.set('open', this.data.get('initiallyOpen'));

        this.dispatch('UI:nested-counter', this.data);
        this.dispatch('UI:list-item-attached');
    },

    attached() {
        let slotChilds = this.slotChilds;
        let hasLeft = true;
        let hasRight = true;

        for (let slot of slotChilds) {
            if (slot.name === 'left') {
                hasLeft = false;
            }
            if (slot.name === 'right') {
                hasRight = false;
            }
        }

        this.data.set('hasLeft', hasLeft);
        this.data.set('hasRight', hasRight);
    },

    toggleList(evt, driver) {
        evt.stopPropagation();

        if (this.data.get('disabled')) {
            return;
        }

        if (driver !== 'EXPAND') {
            this.dispatch('UI:list-item-selected');

            if (!this.data.get('primaryTogglesNestedList')) {
                return;
            }
        }

        let open = this.data.get('open');
        this.data.set('open', !open);

        this.fire('nestedListToggle', open);
    },

    transBoolean(key) {
        let value = this.data.get(key);
        this.data.set(key, value === 'false' ? false : !!value);
    }
});
<commit_msg>del dulp code<commit_after>/**
 * @file ListItem
 * @author qiusiqi(qiusiqi@baidu.com)
 */

import san from 'san';
import {TouchRipple, CenterRipple} from '../Ripple';
import Icon from '../Icon';
import padStyles from '../filters/padStyles';

export default san.defineComponent({

    template: `

<slot name="left"></slot>

{{ primaryText }}

{{ secondaryText | raw }}

            <san-touch-ripple san-if="!disableRipple && !disabled" />

<slot name="right"></slot>

                <san-icon>expand_{{ open | listOpenIcon }}</san-icon>
                <san-center-ripple />

                <slot name="nested"></slot>

    `,

    components: {
        'san-touch-ripple': TouchRipple,
        'san-center-ripple': CenterRipple,
        'san-icon': Icon
    },

    messages: {
        'UI:nested-counter'(arg) {
            let target = arg.value;

            target.set('nestedLevel', target.get('nestedLevel') + 1);
            this.dispatch('UI:nested-counter', target);
        }
    },

    initData() {
        return {
            nestedLevel: 1,
            secondaryTextLines: 1
        };
    },

    filters: {
        padStyles,

        listOpenIcon(open) {
            return open ? 'less' : 'more';
        },
        listOpen(open) {
            return open ? '' : 'hide';
        }
    },

    computed: {
        listItemClass() {
            return (this.data.get('disabled') ? 'disabled' : '')
                + (this.data.get('inset') ? ' inset' : '')
                + (this.data.get('secondaryTextLines') > 1 ? ' three-lines' : '')
                + (
                    this.data.get('selectValue')
                    && this.data.get('selectValue') === this.data.get('value') ? ' selected' : ''
                );
        },
        itemContentStyle() {
            return {
                marginLeft: (this.data.get('nestedLevel') - 1) * 16 + 'px',
                paddingLeft: this.data.get('hasLeft') ? '72px' : '16px',
                paddingRight: this.data.get('hasRight') ? '56px' : '16px'
            };
        },
        secondaryTextStyle() {
            return {
                '-webkit-line-clamp': this.data.get('secondaryTextLines')
            };
        }
    },

    inited() {
        this.transBoolean('inset');
        this.transBoolean('disabled');
        this.transBoolean('toggleNested');
        this.transBoolean('disableRipple');
        this.transBoolean('primaryTogglesNestedList');
        this.transBoolean('initiallyOpen');
        this.data.set('open', this.data.get('initiallyOpen'));

        this.dispatch('UI:nested-counter', this.data);
        this.dispatch('UI:list-item-attached');
    },

    attached() {
        let slotChilds = this.slotChilds;
        let hasLeft = true;
        let hasRight = true;

        for (let slot of slotChilds) {
            if (slot.name === 'left') {
                hasLeft = false;
            }
            if (slot.name === 'right') {
                hasRight = false;
            }
        }

        this.data.set('hasLeft', hasLeft);
        this.data.set('hasRight', hasRight);
    },

    toggleList(evt, driver) {
        evt.stopPropagation();

        if (this.data.get('disabled')) {
            return;
        }

        if (driver !== 'EXPAND') {
            this.dispatch('UI:list-item-selected');

            if (!this.data.get('primaryTogglesNestedList')) {
                return;
            }
        }

        let open = this.data.get('open');
        this.data.set('open', !open);

        this.fire('nestedListToggle', open);
    },

    transBoolean(key) {
        let value = this.data.get(key);
        this.data.set(key, value === 'false' ? false : !!value);
    }
});
<|endoftext|>