keep keep keep replace replace replace replace replace replace replace keep keep keep

// Components
import VSimpleCheckbox from '../VCheckbox/VSimpleCheckbox'
import VDivider from '../VDivider'
import VSubheader from '../VSubheader'
import {
  VList,
  VListItem,
  VListItemAction,
  VListItemContent,
  VListItemTitle,
} from '../VList'

// Directives
import ripple from '../../directives/ripple'

// Mixins
import Colorable from '../../mixins/colorable'
import Themeable from '../../mixins/themeable'

// Helpers
import {
  escapeHTML,
  getPropertyFromItem,
} from '../../util/helpers'

// Types
import mixins from '../../util/mixins'
import { VNode, PropType, VNodeChildren } from 'vue'
import { PropValidator } from 'vue/types/options'
import { SelectItemKey } from 'vuetify/types'

type ListTile = { item: any, disabled?: null | boolean, value?: boolean, index: number };

/* @vue/component */
export default mixins(Colorable, Themeable).extend({
  name: 'v-select-list',

  // https://github.com/vuejs/vue/issues/6872
  directives: {
    ripple,
  },

  props: {
    action: Boolean,
    dense: Boolean,
    hideSelected: Boolean,
    items: {
      type: Array,
      default: () => [],
    } as PropValidator<any[]>,
    itemDisabled: {
      type: [String, Array, Function] as PropType<SelectItemKey>,
      default: 'disabled',
    },
    itemText: {
      type: [String, Array, Function] as PropType<SelectItemKey>,
      default: 'text',
    },
    itemValue: {
      type: [String, Array, Function] as PropType<SelectItemKey>,
      default: 'value',
    },
    noDataText: String,
    noFilter: Boolean,
    searchInput: null as unknown as PropType<any>,
    selectedItems: {
      type: Array,
      default: () => [],
    } as PropValidator<any[]>,
  },

  computed: {
    parsedItems (): any[] {
      return this.selectedItems.map(item => this.getValue(item))
    },
    tileActiveClass (): string {
      return Object.keys(this.setTextColor(this.color).class || {}).join(' ')
    },
    staticNoDataTile (): VNode {
      const tile = {
        attrs: {
          role: undefined,
        },
        on: {
          mousedown: (e: Event) => e.preventDefault(), // Prevent onBlur from being called
        },
      }

      return this.$createElement(VListItem, tile, [
        this.genTileContent(this.noDataText),
      ])
    },
  },

  methods: {
    genAction (item: object, inputValue: any): VNode {
      return this.$createElement(VListItemAction, [
        this.$createElement(VSimpleCheckbox, {
          props: {
            color: this.color,
            value: inputValue,
            ripple: false,
          },
          on: {
            input: () => this.$emit('select', item),
          },
        }),
      ])
    },
    genDivider (props: { [key: string]: any }) {
      return this.$createElement(VDivider, { props })
    },
    genFilteredText (text: string) {
      text = text || ''

      if (!this.searchInput || this.noFilter) return escapeHTML(text)

      const { start, middle, end } = this.getMaskedCharacters(text)

      return `${escapeHTML(start)}${this.genHighlight(middle)}${escapeHTML(end)}`
    },
    genHeader (props: { [key: string]: any }): VNode {
      return this.$createElement(VSubheader, { props }, props.header)
    },
    genHighlight (text: string): string {
      return `<span class="v-list-item__mask">${escapeHTML(text)}</span>`
    },
    getMaskedCharacters (text: string): {
      start: string
      middle: string
      end: string
    } {
      const searchInput = (this.searchInput || '').toString().toLocaleLowerCase()
      const index = text.toLocaleLowerCase().indexOf(searchInput)

      if (index < 0) return { start: text, middle: '', end: '' }

      const start = text.slice(0, index)
      const middle = text.slice(index, index + searchInput.length)
      const end = text.slice(index + searchInput.length)
      return { start, middle, end }
    },
    genTile ({
      item,
      index,
      disabled = null,
      value = false,
    }: ListTile): VNode | VNode[] | undefined {
      if (!value) value = this.hasItem(item)

      if (item === Object(item)) {
        disabled = disabled !== null
          ? disabled
          : this.getDisabled(item)
      }

      const tile = {
        attrs: {
          // Default behavior in list does not
          // contain aria-selected by default
          'aria-selected': String(value),
          id: `list-item-${this._uid}-${index}`,
          role: 'option',
        },
        on: {
          mousedown: (e: Event) => {
            // Prevent onBlur from being called
            e.preventDefault()
          },
          click: () => disabled || this.$emit('select', item),
        },
        props: {
          activeClass: this.tileActiveClass,
          disabled,
          ripple: true,
          inputValue: value,
        },
      }

      if (!this.$scopedSlots.item) {
        return this.$createElement(VListItem, tile, [
          this.action && !this.hideSelected && this.items.length > 0
            ? this.genAction(item, value)
            : null,
          this.genTileContent(item, index),
        ])
      }

      const parent = this
      const scopedSlot = this.$scopedSlots.item({
        parent,
        item,
        attrs: {
          ...tile.attrs,
          ...tile.props,
        },
        on: tile.on,
      })

      return this.needsTile(scopedSlot)
        ? this.$createElement(VListItem, tile, scopedSlot)
        : scopedSlot
    },
    genTileContent (item: any, index = 0): VNode {
      const innerHTML = this.genFilteredText(this.getText(item))

      return this.$createElement(VListItemContent,
        [this.$createElement(VListItemTitle, {
          domProps: { innerHTML },
        })]
      )
    },
    hasItem (item: object) {
      return this.parsedItems.indexOf(this.getValue(item)) > -1
    },
    needsTile (slot: VNode[] | undefined) {
      return slot!.length !== 1 ||
        slot![0].componentOptions == null ||
        slot![0].componentOptions.Ctor.options.name !== 'v-list-item'
    },
    getDisabled (item: object) {
      return Boolean(getPropertyFromItem(item, this.itemDisabled, false))
    },
    getText (item: object) {
      return String(getPropertyFromItem(item, this.itemText, item))
    },
    getValue (item: object) {
      return getPropertyFromItem(item, this.itemValue, this.getText(item))
    },
  },

  render (): VNode {
    const children: VNodeChildren = []
    const itemsLength = this.items.length
    for (let index = 0; index < itemsLength; index++) {
      const item = this.items[index]

      if (this.hideSelected &&
        this.hasItem(item)
      ) continue

      if (item == null) children.push(this.genTile({ item, index }))
      else if (item.header) children.push(this.genHeader(item))
      else if (item.divider) children.push(this.genDivider(item))
      else children.push(this.genTile({ item, index }))
    }

    children.length || children.push(this.$slots['no-data'] || this.staticNoDataTile)

    this.$slots['prepend-item'] && children.unshift(this.$slots['prepend-item'])

    this.$slots['append-item'] && children.push(this.$slots['append-item'])

    return this.$createElement(VList, {
      staticClass: 'v-select-list',
      class: this.themeClasses,
      attrs: {
        role: 'listbox',
        tabindex: -1,
      },
      props: { dense: this.dense },
    }, children)
  },
})


<template>
  <div>
    <v-card
      class="d-flex align-content-center flex-wrap"
      color="grey lighten-2"
      flat
      tile
      min-height="200"
    >
      <v-card
        v-for="n in 20"
        :key="n"
        class="pa-2"
        outlined
        tile
      >
        Flex item
      </v-card>
    </v-card>
  </div>
</template>


<template>
  <v-card
    class="mx-auto"
    max-width="400"
  >
    <v-card-title class="white--text orange darken-4">
      User Directory

      <v-spacer></v-spacer>

      <v-btn
        color="white"
        class="text--primary"
        fab
        small
      >
        <v-icon>mdi-plus</v-icon>
      </v-btn>
    </v-card-title>

    <v-card-text class="pt-4">
      Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quasi nobis a at voluptates culpa optio amet! Inventore deserunt voluptatem maxime a veniam placeat, eos impedit nulla quos? Officiis, aperiam ducimus.
    </v-card-text>

    <v-divider></v-divider>

    <v-virtual-scroll
      :items="items"
      :item-height="50"
      height="300"
    >
      <template v-slot:default="{ item }">
        <v-list-item>
          <v-list-item-avatar>
            <v-avatar
              :color="item.color"
              size="56"
              class="white--text"
            >
              {{ item.initials }}
            </v-avatar>
          </v-list-item-avatar>

          <v-list-item-content>
            <v-list-item-title>{{ item.fullName }}</v-list-item-title>
          </v-list-item-content>

          <v-list-item-action>
            <v-btn
              depressed
              small
            >
              View User

              <v-icon
                color="orange darken-4"
                right
              >
                mdi-open-in-new
              </v-icon>
            </v-btn>
          </v-list-item-action>
        </v-list-item>
      </template>
    </v-virtual-scroll>
  </v-card>
</template>

<script>
  export default {
    data: () => ({
      colors: ['#2196F3', '#90CAF9', '#64B5F6', '#42A5F5', '#1E88E5', '#1976D2', '#1565C0', '#0D47A1', '#82B1FF', '#448AFF', '#2979FF', '#2962FF'],
      names: ['Oliver', 'Jake', 'Noah', 'James', 'Jack', 'Connor', 'Liam', 'John', 'Harry', 'Callum', 'Mason', 'Robert', 'Jacob', 'Jacob', 'Jacob', 'Michael', 'Charlie', 'Kyle', 'William', 'William', 'Thomas', 'Joe', 'Ethan', 'David', 'George', 'Reece', 'Michael', 'Richard', 'Oscar', 'Rhys', 'Alexander', 'Joseph', 'James', 'Charlie', 'James', 'Charles', 'William', 'Damian', 'Daniel', 'Thomas', 'Amelia', 'Margaret', 'Emma', 'Mary', 'Olivia', 'Samantha', 'Olivia', 'Patricia', 'Isla', 'Bethany'],
      surnames: ['Smith', 'Anderson', 'Clark', 'Wright', 'Mitchell', 'Johnson', 'Thomas', 'Rodriguez', 'Lopez', 'Perez', 'Williams', 'Jackson', 'Lewis', 'Hill', 'Roberts', 'Jones', 'White', 'Lee', 'Scott', 'Turner', 'Brown', 'Harris', 'Walker', 'Green', 'Phillips', 'Davis', 'Martin', 'Hall', 'Adams', 'Campbell', 'Miller', 'Thompson', 'Allen', 'Baker', 'Parker', 'Wilson', 'Garcia', 'Young', 'Gonzalez', 'Evans', 'Moore', 'Martinez', 'Hernandez', 'Nelson', 'Edwards', 'Taylor', 'Robinson', 'King', 'Carter', 'Collins'],
    }),

    computed: {
      items () {
        const namesLength = this.names.length
        const surnamesLength = this.surnames.length
        const colorsLength = this.colors.length

        return Array.from({ length: 10000 }, (k, v) => {
          const name = this.names[this.genRandomIndex(namesLength)]
          const surname = this.surnames[this.genRandomIndex(surnamesLength)]

          return {
            color: this.colors[this.genRandomIndex(colorsLength)],
            fullName: `${name} ${surname}`,
            initials: `${name[0]} ${surname[0]}`,
          }
        })
      },
    },

    methods: {
      genRandomIndex (length) {
        return Math.ceil(Math.random() * (length - 1))
      },
    },
  }
</script>


---
meta:
  title: Contributing
  description: Contributing to open source helps developers access amazing tools for free. Learn how you can help develop the Vuetify framework.
  keywords: contribute, contributing, feature request
related:
  - /getting-started/unit-testing/
  - /about/code-of-conduct/
  - /getting-started/frequently-asked-questions/
---

# Contributing

Vuetify is made possible by an amazing community that submits issues, creates pull requests, and provides invaluable feedback. It is our job to enable you to create amazing applications. A lot of the time, you come across something that can be made better. Maybe you find a bug, or you have an idea for additional functionality. That's great! It's as easy as cloning the Vuetify repository to get started working in a development environment.

<promoted-ad slug="vuetify-discord" />

## Reporting Issues

The issue list of this repo is exclusively for bug reports and feature requests. Non-conforming issues will be closed immediately. Before reporting an issue:

- Search for similar [issues](https://github.com/vuetifyjs/vuetify/issues), it may have been answered already.
- Try to reproduce with the [latest](https://github.com/vuetifyjs/vuetify/releases/latest) version in a [codepen](https://template.vuetifyjs.com/) or repository that can be cloned to produce the expected behavior.
- Make sure that the reproduction is **MINIMAL** and concise

These steps ensure that we have all the information necessary to quickly triage and resolve your issue. Once your reproduction is complete, submit a new issue using the [Vuetify Issue Creator](https://issues.vuetifyjs.com/).

When writing an issue please provide as much detail as possible. Note that "reproduction steps" should be a series of actions another developer should take after clicking your reproduction link, not a recollection of how you discovered the bug.

Issues that are convoluted and lacking a proper reproduction may be closed by a member of the [Core Team]. For additional questions regarding reporting issues and creating reproductions, join the official Vuetify Discord [community].

<alert type="success">

  **TIP**

  When you create a reproduction, exclude all **elements, properties, and data variables** that are not needed for the reproduction. This helps drastically reduce the time it takes to triage the issue and ultimately resolve it.

</alert>

In the next section you will learn step-by-step how to set up your local environment and how to configure Vuetify for development.

## Local development

The Vuetify repository is a [lerna](https://github.com/lerna/lerna) monorepo that connects the vuetify library, docs, api generator, and reduces the friction of working with multiple projects at once. The following guide is designed to get you up and running in no time.

### Setting up your environment

Required software:

- [Git](https://git-scm.com/) >v2.20
- [Node.js](https://nodejs.org/) LTS
- [Yarn](https://classic.yarnpkg.com/)

Some of our dependencies use [node-gyp](https://github.com/nodejs/node-gyp#installation) to build themselves. You don't need to install node-gyp itself but may require additional tools, especially on windows. See the node-gyp documentation for more details.

Once you have everything installed, clone the repository:

```bash
# Using HTTPS
git clone https://github.com/vuetifyjs/vuetify.git

# Using SSH
git clone git@github.com:vuetifyjs/vuetify.git
```

<alert type="info">

[Which remote URL should I use?](https://docs.github.com/en/free-pro-team@latest/github/using-git/which-remote-url-should-i-use)

</alert>

Then install dependencies and perform an initial build to link all the packages together:

```bash
# Navigate to the vuetify folder
cd vuetify

# Install all project dependencies
yarn

# Build the packages
yarn build
```

The build process compiles all the Vuetify packages for development and may take a while (grab some ☕). Once the packages are built, you can start developing.

### Vuetify

The Vuetify library is located in `packages/vuetify`. In `packages/vuetify/dev` you will find a `Playground.vue` file; running `yarn dev` from the project root will start a dev server on [localhost:8080](http://localhost:8080/) with this file loaded. You can test your changes in the playground then copy its contents into your pull request when you're ready.

You can also test vuetify in your own project using [`yarn link`](https://classic.yarnpkg.com/en/docs/cli/link/):

- Navigate to `packages/vuetify`
- Run `yarn link`
- Navigate to your project's directory
- Run `yarn link vuetify`

If your project is using vuetify-loader you will have to run `yarn build:lib` in the vuetify package to see changes, otherwise you can use `yarn watch` for incremental builds.

#### Playground.vue

The **Playground** file is a cleanroom used for Vuetify development and is the recommended way to iterate on changes within the framework.

```html
<template>
  <v-container>
    <!--  -->
  </v-container>
</template>

<script>
  export default {
    data: () => ({
      //
    }),
  }
</script>
```

The **App.vue** file used for Vuetify development is located in `packages/vuetify/dev`. It contains a [v-app](/api/v-app/) and [v-main](/api/v-main/) component and the local Playground.vue file.

### Documentation

The documentation is located in `packages/docs` but also uses some files from `packages/api-generator`. A dev server for the documentation can be started by running `yarn dev docs` from the project root and will be available on [localhost:8080](http://localhost:8080/) by default.

If you want to see changes from Vuetify in the documentation you need to run `yarn build:lib` in the vuetify package before starting the documentation server.

### Submitting Changes / Pull Requests

First you should create a fork of the vuetify repository to push your changes to. Information on forking repositories can be found in the [GitHub documentation](https://help.github.com/en/github/getting-started-with-github/fork-a-repo).

Then add your fork as a remote in git:

```bash
# Using HTTPS
git remote add fork https://github.com/YOUR_USERNAME/vuetify.git

# Using SSH
git remote add fork git@github.com:YOUR_USERNAME/vuetify.git
```

#### Choosing a base branch

Before starting development you should know which branch to base your changes on. If in doubt use master as changes to master can usually be merged into a different branch without rebasing.

| Type of change | Branch |
| --- | --- |
| Documentation | `master` |
| Bug fixes | `master` |
| New features | `dev` |
| Features with breaking changes | `next` |

```bash
# Switch to the desired branch
git switch master

# Pull down any upstream changes
git pull

# Create a new branch to work on
git switch --create fix/1234-some-issue
```

<alert type="warning">Never commit directly to the base branches, always create a feature branch to work on</alert>

Commit your changes following [our guidelines](#commit-guidelines), then push the branch to your fork with `git push -u fork` and open a pull request on the Vuetify repository following the provided template.

<alert type="warning">

  Pull requests that include unrelated commits or your local merges may be **closed** without notice.

</alert>

## Working with GitHub

Vuetify's repository lives on [GitHub](https://github.com/vuetifyjs/vuetify) and is the primary location for all development related information. In addition, we have a public GitHub project with a detailed overview of what we are currently working on in [Vuetify 3](https://titan.vuetifyjs.com).

Some of the more notable links within these services include:

**GitHub**

- [Issues]
- [Discussions](https://github.com/vuetifyjs/vuetify/discussions)
- [Projects](https://github.com/vuetifyjs/vuetify/projects)
- [Coding Guidelines](https://github.com/vuetifyjs/vuetify/wiki/coding-guidelines)

---

The following sections are designed to familiarize you with our standard operating procedures for Vuetify development.

<promoted-ad slug="vue-jobs" />

### Issue triage

With the size and popularity of Vuetify has come a constant influx of new issues, questions, and feature requests. To organize these requests the [Core Team] developed tools to aid not only the triaging of issues, but creating them as well.

The [Issues] board makes heavy use of GitHub's label system with some light automation, such as adding the `triage` label to new issues.

#### For Docs - Language

We **do not** accept PRs for any documentation changes pertaining to languages other than `en`. All changes for languages other than `en` are to be submitted through our [Crowdin project](https://crowdin.com/project/vuetify). You can help translate in one of 2 ways:

- Using in-context translation service directly through the documentation site. To get started simply select `Help Translate` in the language drop down in the docs.
- Directly through the [Crowdin project](https://crowdin.com/project/vuetify).

**Note**: Languages will not be added to the language drop down on the docs site until they have at least 50% of their translations completed.

### Requesting new features

Vuetify uses the **RFC** (request for comments) process for new feature suggestions. It is intended to provide a consistent and controlled path for new features to enter the framework.

Many changes, including bug fixes and documentation improvements can be implemented and reviewed via the normal GitHub pull request workflow.

Some changes though are _substantial_, and we ask that these be put through a bit of a design process and produce a consensus among the Vuetify [Core Team](/about/meet-the-team/) and the [community](https://discord.gg/eXubxyJ).

#### Getting started

In order to get a major feature added to Vuetify you must get your RFC merged into this repository as a `.md` file. The following is a guide on how to get started:

- Fork the Vuetify RFC repo <https://github.com/vuetifyjs/rfcs>

- Copy `0000-template.md` to `active-rfcs/0000-my-feature.md` (where **my-feature** is descriptive. **do not** assign an RFC number yet).

- Fill in the RFC. Be detailed and put care into the details.

  <alert type="error">

    RFCs that do not present convincing motivation, demonstrate understanding of the impact of the design, or are disingenuous about the drawbacks or alternatives tend to be poorly-received

  </alert>

- Submit a pull request. As a pull request the RFC will receive design feedback from the larger community, and the author should be prepared to revise it in response. New RFC pull requests start in the **Pending** stage.

- Build consensus and integrate feedback. RFCs that have broad support are much more likely to make progress than those that don't receive any comments.

- Eventually, the [Core Team] will decide whether the RFC is a candidate for inclusion in Vuetify.

- An RFC can be modified based upon feedback from the [Core Team] and [community]. Significant modifications may trigger a new _final comment_ period.

- An RFC may be rejected after public discussion has settled and comments have been made summarizing the rationale for rejection. A [Core Team] member will close the RFCs associated pull request, at which point the RFC will enter the **Rejected** stage.

- An RFC may be accepted at the close of its _final comment_ period. A [Core Team] member will merge the RFCs associated pull request, at which point the RFC will enter the **Active** stage.

Once an RFC is merged and the corresponding functionality implemented within the Vuetify repository, it will be part of the next _major_ or _minor_ release. Once released, the RFC will enter the **Released** stage and be locked.

For more information regarding RFCs, see the official repository: <https://github.com/vuetifyjs/rfcs>

### Commit guidelines

All commit messages are required to follow the [conventional-changelog](https://github.com/conventional-changelog/conventional-changelog) standard using the _angular_ preset. This standard format consists of 2 types of commits:

- With scope: `<type>(scope): <subject>`

  ```bash
  fix(VSelect): don't close when a detachable child is clicked

  fixes #12354
  ```

- Without scope: `<type>: <subject>`

  ```bash
  docs: restructure nav components

  Moved duplicated functionality in drawer to reduce
  scope of responsibility
  ```

#### General Rules

- Commit messages must have a subject line and may have body copy. These must be separated by a blank line.
- The subject line must not exceed 60 characters
- The subject line must be written in imperative mood (fix, not fixed / fixes etc.)
- The body copy must include a reference all issues resolved:

  ```bash
  docs(sass-variables): fix broken link to api

  resolves #3219
  resolves #3254
  ```

- The body copy must be wrapped at 72 characters
- The body copy must only contain explanations as to what and why, never how. The latter belongs in documentation and implementation.

#### Commit types

The following is a list of **commit types** used in the _angular_ preset:

- **feat:** Commits that result in new features or functionalities. Backwards compatible features will release with the next **MINOR** whereas breaking changes will be in the next **MAJOR**. The body of a commit with breaking changes must begin with `BREAKING CHANGE`, followed by a description of how the API has changed.
- **fix:** Commits that provide fixes for bugs within vuetify's codebase.
- **docs:** Commits that provide updates to the docs.
- **style:** Commits that do not affect how the code runs, these are simply changes to formatting.
- **refactor:** Commits that neither fixes a bug nor adds a feature.
- **perf:** Commits that improve performance.
- **test:** Commits that add missing or correct existing tests.
- **chore:** Other commits that don't modify src or test files.
- **revert:** Commits that revert previous commits.

<promoted-ad slug="vuetify-reddit" />

#### Commitizen

The [Vuetify team](/about/meet-the-team/) uses [commitizen] for all repository commits. This allows for easy to read and organized commits with minimal change to normal commit functions. Commitizen provides a fluid interface for handling semantic versioning and makes it easier to write [release notes](https://github.com/vuetifyjs/vuetify/releases).

To get started, [globally install the commitizen package](https://github.com/commitizen/cz-cli#conventional-commit-messages-as-a-global-utility) using [yarn](https://yarnpkg.com/) by running the following commands in your terminal:

```bash
# Install commitizen and the conventional changelog adapter
yarn global add commitizen cz-conventional-changelog

# Then create a .czrc file that tells commitizen
# which adapter to use globally.
echo '{ "path": "cz-conventional-changelog" }' > ~/.czrc
```

<alert type="warning">

  Sometimes creating a `.czrc` file does not work using the **command prompt**. If you get unexpected results, create the file in your user folder located in the home directory. This is typically located on your primary harddrive in the `Users` folder.

</alert>

Once complete, instead of using git commit you will run the command git cz in your terminal. From here, you are presented with a series of prompts used to build the commit message. For additional information, please review our guidlines on [commits](#commit-guidelines).

<backmatter />

[commitizen]: https://github.com/commitizen/cz-cli
[community]: https://community.vuetifyjs.com/
[Core Team]: /about/meet-the-team/
[pull request]: https://github.com/vuetifyjs/vuetify/pulls
[Issues]: https://github.com/vuetifyjs/vuetify/issues


keep keep keep keep keep replace replace replace replace replace replace replace replace replace replace keep keep keep

import Vue from 'vue'
import { VNode, VNodeDirective } from 'vue/types'
import { VuetifyIcon } from 'vuetify/types/services/icons'
import { DataTableCompareFunction, SelectItemKey, ItemGroup } from 'vuetify/types'

export function createSimpleFunctional (
  c: string,
  el = 'div',
  name?: string
) {
  return Vue.extend({
    name: name || c.replace(/__/g, '-'),

    functional: true,

    props: {
      tag: {
        type: String,
        default: el,
      },
    },

    render (h, { data, props, children }): VNode {
      data.staticClass = (`${c} ${data.staticClass || ''}`).trim()

      return h(props.tag, data, children)
    },
  })
}

export type BindingConfig = Pick<VNodeDirective, 'arg' | 'modifiers' | 'value'>
export function directiveConfig (binding: BindingConfig, defaults = {}): VNodeDirective {
  return {
    ...defaults,
    ...binding.modifiers,
    value: binding.arg,
    ...(binding.value || {}),
  }
}

export function addOnceEventListener (
  el: EventTarget,
  eventName: string,
  cb: (event: Event) => void,
  options: boolean | AddEventListenerOptions = false
): void {
  const once = (event: Event) => {
    cb(event)
    el.removeEventListener(eventName, once, options)
  }

  el.addEventListener(eventName, once, options)
}

let passiveSupported = false
try {
  if (typeof window !== 'undefined') {
    const testListenerOpts = Object.defineProperty({}, 'passive', {
      get: () => {
        passiveSupported = true
      },
    }) as EventListener & EventListenerOptions

    window.addEventListener('testListener', testListenerOpts, testListenerOpts)
    window.removeEventListener('testListener', testListenerOpts, testListenerOpts)
  }
} catch (e) { console.warn(e) } /* eslint-disable-line no-console */
export { passiveSupported }

export function addPassiveEventListener (
  el: EventTarget,
  event: string,
  cb: (event: any) => void,
  options: {}
): void {
  el.addEventListener(event, cb, passiveSupported ? options : false)
}

export function getNestedValue (obj: any, path: (string | number)[], fallback?: any): any {
  const last = path.length - 1

  if (last < 0) return obj === undefined ? fallback : obj

  for (let i = 0; i < last; i++) {
    if (obj == null) {
      return fallback
    }
    obj = obj[path[i]]
  }

  if (obj == null) return fallback

  return obj[path[last]] === undefined ? fallback : obj[path[last]]
}

export function deepEqual (a: any, b: any): boolean {
  if (a === b) return true

  if (
    a instanceof Date &&
    b instanceof Date &&
    a.getTime() !== b.getTime()
  ) {
    // If the values are Date, compare them as timestamps
    return false
  }

  if (a !== Object(a) || b !== Object(b)) {
    // If the values aren't objects, they were already checked for equality
    return false
  }

  const props = Object.keys(a)

  if (props.length !== Object.keys(b).length) {
    // Different number of props, don't bother to check
    return false
  }

  return props.every(p => deepEqual(a[p], b[p]))
}

export function getObjectValueByPath (obj: any, path: string, fallback?: any): any {
  // credit: http://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key#comment55278413_6491621
  if (obj == null || !path || typeof path !== 'string') return fallback
  if (obj[path] !== undefined) return obj[path]
  path = path.replace(/\[(\w+)\]/g, '.$1') // convert indexes to properties
  path = path.replace(/^\./, '') // strip a leading dot
  return getNestedValue(obj, path.split('.'), fallback)
}

export function getPropertyFromItem (
  item: object,
  property: SelectItemKey,
  fallback?: any
): any {
  if (property == null) return item === undefined ? fallback : item

  if (item !== Object(item)) return fallback === undefined ? item : fallback

  if (typeof property === 'string') return getObjectValueByPath(item, property, fallback)

  if (Array.isArray(property)) return getNestedValue(item, property, fallback)

  if (typeof property !== 'function') return fallback

  const value = property(item, fallback)

  return typeof value === 'undefined' ? fallback : value
}

export function createRange (length: number): number[] {
  return Array.from({ length }, (v, k) => k)
}

export function getZIndex (el?: Element | null): number {
  if (!el || el.nodeType !== Node.ELEMENT_NODE) return 0

  const index = +window.getComputedStyle(el).getPropertyValue('z-index')

  if (!index) return getZIndex(el.parentNode as Element)
  return index
}

const tagsToReplace = {
  '&': '&',
  '<': '<',
  '>': '>',
} as any

export function escapeHTML (str: string): string {
  return str.replace(/[&<>]/g, tag => tagsToReplace[tag] || tag)
}

export function filterObjectOnKeys<T, K extends keyof T> (obj: T, keys: K[]): { [N in K]: T[N] } {
  const filtered = {} as { [N in K]: T[N] }

  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    if (typeof obj[key] !== 'undefined') {
      filtered[key] = obj[key]
    }
  }

  return filtered
}

export function convertToUnit (str: string | number | null | undefined, unit = 'px'): string | undefined {
  if (str == null || str === '') {
    return undefined
  } else if (isNaN(+str!)) {
    return String(str)
  } else {
    return `${Number(str)}${unit}`
  }
}

export function kebabCase (str: string): string {
  return (str || '').replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
}

export function isObject (obj: any): obj is object {
  return obj !== null && typeof obj === 'object'
}

// KeyboardEvent.keyCode aliases
export const keyCodes = Object.freeze({
  enter: 13,
  tab: 9,
  delete: 46,
  esc: 27,
  space: 32,
  up: 38,
  down: 40,
  left: 37,
  right: 39,
  end: 35,
  home: 36,
  del: 46,
  backspace: 8,
  insert: 45,
  pageup: 33,
  pagedown: 34,
  shift: 16,
})

/**
 * This remaps internal names like '$cancel' or '$vuetify.icons.cancel'
 * to the current name or component for that icon.
 */
export function remapInternalIcon (vm: Vue, iconName: string): VuetifyIcon {
  // Look for custom component in the configuration
  const component = vm.$vuetify.icons.component

  // Look for overrides
  if (iconName.startsWith('$')) {
    // Get the target icon name
    const iconPath = `$vuetify.icons.values.${iconName.split('$').pop()!.split('.').pop()}`

    // Now look up icon indirection name,
    // e.g. '$vuetify.icons.values.cancel'
    const override = getObjectValueByPath(vm, iconPath, iconName)

    if (typeof override === 'string') iconName = override
    else return override
  }

  if (component == null) {
    return iconName
  }

  return {
    component,
    props: {
      icon: iconName,
    },
  }
}

export function keys<O> (o: O) {
  return Object.keys(o) as (keyof O)[]
}

/**
 * Camelize a hyphen-delimited string.
 */
const camelizeRE = /-(\w)/g
export const camelize = (str: string): string => {
  return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
}

/**
 * Returns the set difference of B and A, i.e. the set of elements in B but not in A
 */
export function arrayDiff (a: any[], b: any[]): any[] {
  const diff: any[] = []
  for (let i = 0; i < b.length; i++) {
    if (a.indexOf(b[i]) < 0) diff.push(b[i])
  }
  return diff
}

/**
 * Makes the first character of a string uppercase
 */
export function upperFirst (str: string): string {
  return str.charAt(0).toUpperCase() + str.slice(1)
}

export function groupItems<T extends any = any> (
  items: T[],
  groupBy: string[],
  groupDesc: boolean[]
): ItemGroup<T>[] {
  const key = groupBy[0]
  const groups: ItemGroup<T>[] = []
  let current
  for (let i = 0; i < items.length; i++) {
    const item = items[i]
    const val = getObjectValueByPath(item, key, null)
    if (current !== val) {
      current = val
      groups.push({
        name: val ?? '',
        items: [],
      })
    }
    groups[groups.length - 1].items.push(item)
  }
  return groups
}

export function wrapInArray<T> (v: T | T[] | null | undefined): T[] { return v != null ? Array.isArray(v) ? v : [v] : [] }

export function sortItems<T extends any = any> (
  items: T[],
  sortBy: string[],
  sortDesc: boolean[],
  locale: string,
  customSorters?: Record<string, DataTableCompareFunction<T>>
): T[] {
  if (sortBy === null || !sortBy.length) return items
  const stringCollator = new Intl.Collator(locale, { sensitivity: 'accent', usage: 'sort' })

  return items.sort((a, b) => {
    for (let i = 0; i < sortBy.length; i++) {
      const sortKey = sortBy[i]

      let sortA = getObjectValueByPath(a, sortKey)
      let sortB = getObjectValueByPath(b, sortKey)

      if (sortDesc[i]) {
        [sortA, sortB] = [sortB, sortA]
      }

      if (customSorters && customSorters[sortKey]) {
        const customResult = customSorters[sortKey](sortA, sortB)

        if (!customResult) continue

        return customResult
      }

      // Check if both cannot be evaluated
      if (sortA === null && sortB === null) {
        continue
      }

      // Dates should be compared numerically
      if (sortA instanceof Date && sortB instanceof Date) {
        return sortA.getTime() - sortB.getTime()
      }

      [sortA, sortB] = [sortA, sortB].map(s => (s || '').toString().toLocaleLowerCase())

      if (sortA !== sortB) {
        if (!isNaN(sortA) && !isNaN(sortB)) return Number(sortA) - Number(sortB)
        return stringCollator.compare(sortA, sortB)
      }
    }

    return 0
  })
}

export function defaultFilter (value: any, search: string | null, item: any) {
  return value != null &&
    search != null &&
    typeof value !== 'boolean' &&
    value.toString().toLocaleLowerCase().indexOf(search.toLocaleLowerCase()) !== -1
}

export function searchItems<T extends any = any> (items: T[], search: string): T[] {
  if (!search) return items
  search = search.toString().toLowerCase()
  if (search.trim() === '') return items

  return items.filter((item: any) => Object.keys(item).some(key => defaultFilter(getObjectValueByPath(item, key), search, item)))
}

/**
 * Returns:
 *  - 'normal' for old style slots - `<template slot="default">`
 *  - 'scoped' for old style scoped slots (`<template slot="default" slot-scope="data">`) or bound v-slot (`#default="data"`)
 *  - 'v-slot' for unbound v-slot (`#default`) - only if the third param is true, otherwise counts as scoped
 */
export function getSlotType<T extends boolean = false> (vm: Vue, name: string, split?: T): (T extends true ? 'v-slot' : never) | 'normal' | 'scoped' | void {
  if (vm.$slots.hasOwnProperty(name) && vm.$scopedSlots.hasOwnProperty(name) && (vm.$scopedSlots[name] as any).name) {
    return split ? 'v-slot' as any : 'scoped'
  }
  if (vm.$slots.hasOwnProperty(name)) return 'normal'
  if (vm.$scopedSlots.hasOwnProperty(name)) return 'scoped'
}

export function debounce (fn: Function, delay: number) {
  let timeoutId = 0 as any
  return (...args: any[]) => {
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => fn(...args), delay)
  }
}

export function throttle<T extends (...args: any[]) => any> (fn: T, limit: number) {
  let throttling = false
  return (...args: Parameters<T>): void | ReturnType<T> => {
    if (!throttling) {
      throttling = true
      setTimeout(() => throttling = false, limit)
      return fn(...args)
    }
  }
}

export function getPrefixedScopedSlots (prefix: string, scopedSlots: any) {
  return Object.keys(scopedSlots).filter(k => k.startsWith(prefix)).reduce((obj: any, k: string) => {
    obj[k.replace(prefix, '')] = scopedSlots[k]
    return obj
  }, {})
}

export function getSlot (vm: Vue, name = 'default', data?: object | (() => object), optional = false) {
  if (vm.$scopedSlots.hasOwnProperty(name)) {
    return vm.$scopedSlots[name]!(data instanceof Function ? data() : data)
  } else if (vm.$slots.hasOwnProperty(name) && (!data || optional)) {
    return vm.$slots[name]
  }
  return undefined
}

export function clamp (value: number, min = 0, max = 1) {
  return Math.max(min, Math.min(max, value))
}

export function padEnd (str: string, length: number, char = '0') {
  return str + char.repeat(Math.max(0, length - str.length))
}

export function chunk (str: string, size = 1) {
  const chunked: string[] = []
  let index = 0
  while (index < str.length) {
    chunked.push(str.substr(index, size))
    index += size
  }
  return chunked
}

export function humanReadableFileSize (bytes: number, binary = false): string {
  const base = binary ? 1024 : 1000
  if (bytes < base) {
    return `${bytes} B`
  }

  const prefix = binary ? ['Ki', 'Mi', 'Gi'] : ['k', 'M', 'G']
  let unit = -1
  while (Math.abs(bytes) >= base && unit < prefix.length - 1) {
    bytes /= base
    ++unit
  }
  return `${bytes.toFixed(1)} ${prefix[unit]}B`
}

export function camelizeObjectKeys (obj: Record<string, any> | null | undefined) {
  if (!obj) return {}

  return Object.keys(obj).reduce((o: any, key: string) => {
    o[camelize(key)] = obj[key]
    return o
  }, {})
}

export function mergeDeep (
  source: Dictionary<any> = {},
  target: Dictionary<any> = {}
) {
  for (const key in target) {
    const sourceProperty = source[key]
    const targetProperty = target[key]

    // Only continue deep merging if
    // both properties are objects
    if (
      isObject(sourceProperty) &&
      isObject(targetProperty)
    ) {
      source[key] = mergeDeep(sourceProperty, targetProperty)

      continue
    }

    source[key] = targetProperty
  }

  return source
}

export function fillArray<T> (length: number, obj: T) {
  return Array(length).fill(obj)
}

/**  Polyfill for Event.prototype.composedPath */
export function composedPath (e: Event): EventTarget[] {
  if (e.composedPath) return e.composedPath()

  const path = []
  let el = e.target as Element

  while (el) {
    path.push(el)

    if (el.tagName === 'HTML') {
      path.push(document)
      path.push(window)

      return path
    }

    el = el.parentElement!
  }
  return path
}


// Imports
@import './_variables.scss'

// Theme
+theme(v-data-table) using ($material)
  .v-data-table-header
    th.sortable
      .v-data-table-header__icon
        color: map-deep-get($material, 'text', 'disabled')

      &:hover,
      &.active
        color: map-deep-get($material, 'text', 'primary')

      &.active
        .v-data-table-header__icon
          color: map-deep-get($material, 'text', 'primary')

    &__sort-badge
      background-color: map-get($material, 'dividers')
      color: map-deep-get($material, 'text', 'primary')

// Block
.v-data-table-header
  th
    &.sortable
      pointer-events: auto
      cursor: pointer
      outline: 0
      .v-data-table-header__icon
        line-height: .9

    &.active, &:hover
      .v-data-table-header__icon
        transform: none
        opacity: 1

    &.desc
      .v-data-table-header__icon
        transform: rotate(-180deg)

.v-data-table-header__icon
  display: inline-block
  opacity: 0
  transition: .3s map-get($transition, 'swing')

.v-data-table-header__sort-badge
  display: inline-flex
  justify-content: center
  align-items: center
  border: 0px
  border-radius: 50%
  min-width: $data-table-header-sort-badge-min-height
  min-height: $data-table-header-sort-badge-min-width
  height: $data-table-header-sort-badge-height
  width: $data-table-header-sort-badge-width

// Mobile
.v-data-table-header-mobile
  th
    height: initial

.v-data-table-header-mobile__wrapper
  display: flex

  .v-select
    margin-bottom: $data-table-header-mobile-select-margin-bottom

    .v-chip
      height: $data-table-header-mobile-select-chip-height

    .v-chip__close.desc
      .v-icon
        transform: rotate(-180deg)

.v-data-table-header-mobile__select
  min-width: $data-table-header-mobile-select-max-width
  display: flex
  align-items: center
  justify-content: center


import CalendarBase from '../calendar-base'
import { parseTimestamp } from '../../util/timestamp'
import {
  mount,
  Wrapper,
  MountOptions,
} from '@vue/test-utils'
import { ExtractVue } from '../../../../util/mixins'

const Mock = CalendarBase.extend({
  render: h => h('div'),
})

describe('calendar-base.ts', () => {
  type Instance = ExtractVue<typeof Mock>
  let mountFunction: (options?: MountOptions<Instance>) => Wrapper<Instance>
  beforeEach(() => {
    mountFunction = (options?: MountOptions<Instance>) => {
      return mount(Mock, {
        ...options,
        mocks: {
          $vuetify: {
            lang: {
              current: 'en-US',
            },
          },
        },
      })
    }
  })

  it('should parse start & end', async () => {
    const wrapper = mountFunction({
      propsData: {
        start: '2019-01-29',
        end: '2019-02-08',
      },
    })

    expect(wrapper.vm.parsedStart).toBeDefined()
    expect(wrapper.vm.parsedStart).toMatchSnapshot()
    expect(wrapper.vm.parsedEnd).toBeDefined()
    expect(wrapper.vm.parsedEnd).toMatchSnapshot()
  })

  it('should create a day list', async () => {
    const wrapper = mountFunction({
      propsData: {
        start: '2019-01-29',
        end: '2019-02-08',
      },
    })

    expect(wrapper.vm.days).toBeDefined()
    expect(wrapper.vm.days).toHaveLength(11)
    expect(wrapper.vm.days).toMatchSnapshot()

    expect(wrapper.vm.days[0].date).toBe('2019-01-29')
    expect(wrapper.vm.days[10].date).toBe('2019-02-08')
  })

  it('should calculate weekday skips', async () => {
    const wrapper = mountFunction({
      propsData: {
        start: '2019-01-29',
        end: '2019-02-08',
      },
    })

    expect(wrapper.vm.weekdaySkips).toBeDefined()
    expect(wrapper.vm.weekdaySkips).toHaveLength(7)
  })

  it('should generate classes', async () => {
    const wrapper = mountFunction({
      propsData: {
        start: '2019-01-29',
        end: '2019-02-08',
      },
    })

    expect(wrapper.vm.getRelativeClasses(parseTimestamp('2019-01-28'))).toBeDefined()
    expect(wrapper.vm.getRelativeClasses(parseTimestamp('2019-01-28'))).toMatchSnapshot()
  })

  it('should generate classes with outside', async () => {
    const wrapper = mountFunction({
      propsData: {
        start: '2019-01-29',
        end: '2019-02-08',
      },
    })

    expect(wrapper.vm.getRelativeClasses(parseTimestamp('2019-01-28'), true)).toBeDefined()
    expect(wrapper.vm.getRelativeClasses(parseTimestamp('2019-01-28'), true)).toMatchSnapshot()
  })

  it('should return weekdayFormatter equal to weekdayFormat prop', async () => {
    const weekdayFormat = x => x
    const wrapper = mountFunction({
      propsData: {
        weekdayFormat,
      },
    })

    expect(wrapper.vm.weekdayFormatter).toEqual(weekdayFormat)
  })

  it('should long-format weekday', async () => {
    const wrapper = mountFunction({
      propsData: {
        start: '2019-01-29',
        end: '2019-02-08',
      },
    })

    expect(wrapper.vm.weekdayFormatter).toBeDefined()
    expect(typeof wrapper.vm.weekdayFormatter).toEqual('function')

    expect(wrapper.vm.weekdayFormatter(parseTimestamp('2019-01-28'), false)).toEqual('Monday')
    expect(wrapper.vm.weekdayFormatter(parseTimestamp('2019-01-27'), false)).toEqual('Sunday')
    expect(wrapper.vm.weekdayFormatter(parseTimestamp('2019-01-29'), false)).toEqual('Tuesday')
  })

  it('should short-format weekday', async () => {
    const wrapper = mountFunction()

    expect(wrapper.vm.weekdayFormatter).toBeDefined()
    expect(typeof wrapper.vm.weekdayFormatter).toEqual('function')

    expect(wrapper.vm.weekdayFormatter(parseTimestamp('2019-01-28'), true)).toEqual('Mon')
    expect(wrapper.vm.weekdayFormatter(parseTimestamp('2019-01-27'), true)).toEqual('Sun')
    expect(wrapper.vm.weekdayFormatter(parseTimestamp('2019-01-29'), true)).toEqual('Tue')
  })

  it('should get start of week', async () => {
    const wrapper = mountFunction()

    expect(wrapper.vm.getStartOfWeek(parseTimestamp('2019-01-28')).weekday).toEqual(0)
    expect(wrapper.vm.getStartOfWeek(parseTimestamp('2019-01-03')).weekday).toEqual(0)
  })

  it('should get end of week', async () => {
    const wrapper = mountFunction()

    expect(wrapper.vm.getEndOfWeek(parseTimestamp('2019-03-28')).weekday).toEqual(6)
    expect(wrapper.vm.getEndOfWeek(parseTimestamp('2019-12-31')).weekday).toEqual(6)
  })

  it('should return dayFormatter equal to dayFormat prop', async () => {
    const dayFormat = x => x
    const wrapper = mountFunction({
      propsData: {
        dayFormat,
      },
    })

    expect(wrapper.vm.dayFormatter).toEqual(dayFormat)
  })

  it('should format day', async () => {
    const wrapper = mountFunction({
      propsData: {
        start: '2019-01-29',
        end: '2019-02-08',
      },
    })

    expect(wrapper.vm.weekdayFormatter).toBeDefined()
    expect(typeof wrapper.vm.weekdayFormatter).toEqual('function')

    expect(wrapper.vm.dayFormatter(parseTimestamp('2019-01-28'), false)).toEqual('28')
    expect(wrapper.vm.dayFormatter(parseTimestamp('2019-01-27'), false)).toEqual('27')
    expect(wrapper.vm.dayFormatter(parseTimestamp('2019-01-29'), false)).toEqual('29')
  })
})


// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`VOverflowBtn.js should show label with persistentPlaceholder property set to true 1`] = `

          Some label

          <input id="input-15"
                 readonly="readonly"
                 type="text"
          >

              $dropdown

        <input type="hidden">

`;

exports[`VOverflowBtn.js should show label with persistentPlaceholder property set to true 2`] = `

          Some label

          <input id="input-15"
                 readonly="readonly"
                 type="text"
          >

              $dropdown

        <input type="hidden">

`;

exports[`VOverflowBtn.js should use default autocomplete selections 1`] = `

            foo

          <input id="input-1"
                 readonly="readonly"
                 type="text"
          >

              $dropdown

        <input type="hidden"
               value="foo"
        >

`;

exports[`VOverflowBtn.js should use default autocomplete selections 2`] = `

          <button type="button"
                  class="v-btn v-btn--text theme--light v-size--default"
          >

              foo

          </button>
          <input id="input-1"
                 readonly="readonly"
                 type="text"
          >

              $dropdown

        <input type="hidden"
               value="foo"
        >

`;


RocketChat.API.v1.addRoute('livechat/room.forward', { authRequired: true }, {

RocketChat.API.v1.success(Meteor.runAsUser(this.userId, () => Meteor.call('livechat:transfer', this.bodyParams)));

import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
import { RocketChat } from 'meteor/rocketchat:lib';
import LivechatVisitors from '../models/LivechatVisitors';

Meteor.methods({
	'livechat:transfer'(transferData) {
		if (!Meteor.userId() || !RocketChat.authz.hasPermission(Meteor.userId(), 'view-l-room')) {
			throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:transfer' });
		}

		check(transferData, {
			roomId: String,
			userId: Match.Optional(String),
			departmentId: Match.Optional(String),
		});

		const room = RocketChat.models.Rooms.findOneById(transferData.roomId);

		const guest = LivechatVisitors.findOneById(room.v._id);

		const subscription = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(room._id, Meteor.userId(), { fields: { _id: 1 } });
		if (!subscription && !RocketChat.authz.hasRole(Meteor.userId(), 'livechat-manager')) {
			throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'livechat:transfer' });
		}

		return RocketChat.Livechat.transfer(room, guest, transferData);
	},
});


import { Accounts } from 'meteor/accounts-base';
import { e2e } from 'meteor/rocketchat:e2e';

Accounts.onLogout(() => {
	e2e.stopClient();
});


import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import { Template } from 'meteor/templating';
import { t } from 'meteor/rocketchat:utils';
import { modal, SideNav } from 'meteor/rocketchat:ui-utils';
import toastr from 'toastr';
import moment from 'moment';

import './personalAccessTokens.html';

const PersonalAccessTokens = new Mongo.Collection('personal_access_tokens');

Template.accountTokens.helpers({
	isAllowed() {
		return RocketChat.authz.hasAllPermission(['create-personal-access-tokens']);
	},
	tokens() {
		return (PersonalAccessTokens.find({}).fetch()[0] && PersonalAccessTokens.find({}).fetch()[0].tokens) || [];
	},
	dateFormated(date) {
		return moment(date).format('L LT');
	},
});

const showSuccessModal = (token) => {
	modal.open({
		title: t('API_Personal_Access_Token_Generated'),
		text: t('API_Personal_Access_Token_Generated_Text_Token_s_UserId_s', { token, userId: Meteor.userId() }),
		type: 'success',
		confirmButtonColor: '#DD6B55',
		confirmButtonText: 'Ok',
		closeOnConfirm: true,
		html: true,
	}, () => {
	});
};
Template.accountTokens.events({
	'submit #form-tokens'(e, instance) {
		e.preventDefault();
		const tokenName = e.currentTarget.elements.tokenName.value.trim();
		if (tokenName === '') {
			return toastr.error(t('Please_fill_a_token_name'));
		}
		Meteor.call('personalAccessTokens:generateToken', { tokenName }, (error, token) => {
			if (error) {
				return toastr.error(t(error.error));
			}
			showSuccessModal(token);
			instance.find('#tokenName').value = '';
		});
	},
	'click .remove-personal-access-token'() {
		modal.open({
			title: t('Are_you_sure'),
			text: t('API_Personal_Access_Tokens_Remove_Modal'),
			type: 'warning',
			showCancelButton: true,
			confirmButtonColor: '#DD6B55',
			confirmButtonText: t('Yes'),
			cancelButtonText: t('Cancel'),
			closeOnConfirm: true,
			html: false,
		}, () => {
			Meteor.call('personalAccessTokens:removeToken', {
				tokenName: this.name,
			}, (error) => {
				if (error) {
					return toastr.error(t(error.error));
				}
				toastr.success(t('Removed'));
			});
		});
	},
	'click .regenerate-personal-access-token'() {
		modal.open({
			title: t('Are_you_sure'),
			text: t('API_Personal_Access_Tokens_Regenerate_Modal'),
			type: 'warning',
			showCancelButton: true,
			confirmButtonColor: '#DD6B55',
			confirmButtonText: t('API_Personal_Access_Tokens_Regenerate_It'),
			cancelButtonText: t('Cancel'),
			closeOnConfirm: true,
			html: false,
		}, () => {
			Meteor.call('personalAccessTokens:regenerateToken', {
				tokenName: this.name,
			}, (error, token) => {
				if (error) {
					return toastr.error(t(error.error));
				}
				showSuccessModal(token);
			});
		});
	},
});

Template.accountTokens.onCreated(function() {
	this.ready = new ReactiveVar(true);
	const subscription = this.subscribe('personalAccessTokens');
	this.autorun(() => {
		this.ready.set(subscription.ready());
	});
});

Template.accountTokens.onRendered(function() {
	Tracker.afterFlush(function() {
		SideNav.setFlex('accountFlex');
		SideNav.openFlex();
	});
});


import { Meteor } from 'meteor/meteor';
import { RocketChat } from 'meteor/rocketchat:lib';

Meteor.methods({
	deleteOldOTRMessages(roomId) {
		if (!Meteor.userId()) {
			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'deleteOldOTRMessages' });
		}

		const now = new Date();
		const subscription = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(roomId, Meteor.userId());
		if (subscription && subscription.t === 'd') {
			RocketChat.models.Messages.deleteOldOTRMessages(roomId, now);
		} else {
			throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'deleteOldOTRMessages' });
		}
	},
});


const room = RocketChat.models.Rooms.findOneById(transferData.roomId);

const guest = LivechatVisitors.findOneById(room.v._id);

const subscription = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(room._id, Meteor.userId(), { fields: { _id: 1 } });

if (!subscription && !RocketChat.authz.hasRole(Meteor.userId(), 'livechat-manager')) {

throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'livechat:transfer' });

keep keep keep keep keep replace replace keep keep keep keep keep

throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'livechat:transfer' });

import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
import { Random } from 'meteor/random';
import { TAPi18n } from 'meteor/tap:i18n';
import { RocketChat } from 'meteor/rocketchat:lib';
import { findGuest, findRoom, getRoom, settings } from '../lib/livechat';

RocketChat.API.v1.addRoute('livechat/room', {
	get() {
		try {
			check(this.queryParams, {
				token: String,
				rid: Match.Maybe(String),
			});

			const { token } = this.queryParams;
			const guest = findGuest(token);
			if (!guest) {
				throw new Meteor.Error('invalid-token');
			}

			const rid = this.queryParams.rid || Random.id();
			const room = getRoom(guest, rid);

			return RocketChat.API.v1.success(room);
		} catch (e) {
			return RocketChat.API.v1.failure(e);
		}
	},
});

RocketChat.API.v1.addRoute('livechat/room.close', {
	post() {
		try {
			check(this.bodyParams, {
				rid: String,
				token: String,
			});

			const { rid, token } = this.bodyParams;

			const visitor = findGuest(token);
			if (!visitor) {
				throw new Meteor.Error('invalid-token');
			}

			const room = findRoom(token, rid);
			if (!room) {
				throw new Meteor.Error('invalid-room');
			}

			if (!room.open) {
				throw new Meteor.Error('room-closed');
			}

			const language = RocketChat.settings.get('language') || 'en';
			const comment = TAPi18n.__('Closed_by_visitor', { lng: language });

			if (!RocketChat.Livechat.closeRoom({ visitor, room, comment })) {
				return RocketChat.API.v1.failure();
			}

			return RocketChat.API.v1.success({ rid, comment });
		} catch (e) {
			return RocketChat.API.v1.failure(e);
		}
	},
});

RocketChat.API.v1.addRoute('livechat/room.transfer', {
	post() {
		try {
			check(this.bodyParams, {
				rid: String,
				token: String,
				department: String,
			});

			const { rid, token, department } = this.bodyParams;

			const guest = findGuest(token);
			if (!guest) {
				throw new Meteor.Error('invalid-token');
			}

			let room = findRoom(token, rid);
			if (!room) {
				throw new Meteor.Error('invalid-room');
			}

			// update visited page history to not expire
			RocketChat.models.Messages.keepHistoryForToken(token);

			if (!RocketChat.Livechat.transfer(room, guest, { roomId: rid, departmentId: department })) {
				return RocketChat.API.v1.failure();
			}

			room = findRoom(token, rid);
			return RocketChat.API.v1.success({ room });
		} catch (e) {
			return RocketChat.API.v1.failure(e);
		}
	},
});

RocketChat.API.v1.addRoute('livechat/room.survey', {
	post() {
		try {
			check(this.bodyParams, {
				rid: String,
				token: String,
				data: [Match.ObjectIncluding({
					name: String,
					value: String,
				})],
			});

			const { rid, token, data } = this.bodyParams;

			const visitor = findGuest(token);
			if (!visitor) {
				throw new Meteor.Error('invalid-token');
			}

			const room = findRoom(token, rid);
			if (!room) {
				throw new Meteor.Error('invalid-room');
			}

			const config = settings();
			if (!config.survey || !config.survey.items || !config.survey.values) {
				throw new Meteor.Error('invalid-livechat-config');
			}

			const updateData = {};
			for (const item of data) {
				if ((config.survey.items.includes(item.name) && config.survey.values.includes(item.value)) || item.name === 'additionalFeedback') {
					updateData[item.name] = item.value;
				}
			}

			if (Object.keys(updateData).length === 0) {
				throw new Meteor.Error('invalid-data');
			}

			if (!RocketChat.models.Rooms.updateSurveyFeedbackById(room._id, updateData)) {
				return RocketChat.API.v1.failure();
			}

			return RocketChat.API.v1.success({ rid, data: updateData });
		} catch (e) {
			return RocketChat.API.v1.failure(e);
		}
	},
});


import { Meteor } from 'meteor/meteor';
import { WebApp } from 'meteor/webapp';
import { RocketChat } from 'meteor/rocketchat:lib';
import { RocketChatFile } from 'meteor/rocketchat:file';
import _ from 'underscore';

export let RocketChatFileEmojiCustomInstance;

Meteor.startup(function() {
	let storeType = 'GridFS';

	if (RocketChat.settings.get('EmojiUpload_Storage_Type')) {
		storeType = RocketChat.settings.get('EmojiUpload_Storage_Type');
	}

	const RocketChatStore = RocketChatFile[storeType];

	if (RocketChatStore == null) {
		throw new Error(`Invalid RocketChatStore type [${ storeType }]`);
	}

	console.log(`Using ${ storeType } for custom emoji storage`.green);

	let path = '~/uploads';
	if (RocketChat.settings.get('EmojiUpload_FileSystemPath') != null) {
		if (RocketChat.settings.get('EmojiUpload_FileSystemPath').trim() !== '') {
			path = RocketChat.settings.get('EmojiUpload_FileSystemPath');
		}
	}

	RocketChatFileEmojiCustomInstance = new RocketChatStore({
		name: 'custom_emoji',
		absolutePath: path,
	});

	return WebApp.connectHandlers.use('/emoji-custom/', Meteor.bindEnvironment(function(req, res/* , next*/) {
		const params =
			{ emoji: decodeURIComponent(req.url.replace(/^\//, '').replace(/\?.*$/, '')) };

		if (_.isEmpty(params.emoji)) {
			res.writeHead(403);
			res.write('Forbidden');
			res.end();
			return;
		}

		const file = RocketChatFileEmojiCustomInstance.getFileWithReadStream(encodeURIComponent(params.emoji));

		res.setHeader('Content-Disposition', 'inline');

		if (file == null) {
			// use code from username initials renderer until file upload is complete
			res.setHeader('Content-Type', 'image/svg+xml');
			res.setHeader('Cache-Control', 'public, max-age=0');
			res.setHeader('Expires', '-1');
			res.setHeader('Last-Modified', 'Thu, 01 Jan 2015 00:00:00 GMT');

			const reqModifiedHeader = req.headers['if-modified-since'];
			if (reqModifiedHeader != null) {
				if (reqModifiedHeader === 'Thu, 01 Jan 2015 00:00:00 GMT') {
					res.writeHead(304);
					res.end();
					return;
				}
			}

			const color = '#000';
			const initials = '?';

			const svg = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" pointer-events="none" width="50" height="50" style="width: 50px; height: 50px; background-color: ${ color };">
	<text text-anchor="middle" y="50%" x="50%" dy="0.36em" pointer-events="auto" fill="#ffffff" font-family="Helvetica, Arial, Lucida Grande, sans-serif" style="font-weight: 400; font-size: 28px;">
		${ initials }
	</text>
</svg>`;

			res.write(svg);
			res.end();
			return;
		}

		let fileUploadDate = undefined;
		if (file.uploadDate != null) {
			fileUploadDate = file.uploadDate.toUTCString();
		}

		const reqModifiedHeader = req.headers['if-modified-since'];
		if (reqModifiedHeader != null) {
			if (reqModifiedHeader === fileUploadDate) {
				res.setHeader('Last-Modified', reqModifiedHeader);
				res.writeHead(304);
				res.end();
				return;
			}
		}

		res.setHeader('Cache-Control', 'public, max-age=0');
		res.setHeader('Expires', '-1');
		if (fileUploadDate != null) {
			res.setHeader('Last-Modified', fileUploadDate);
		} else {
			res.setHeader('Last-Modified', new Date().toUTCString());
		}
		if (/^svg$/i.test(params.emoji.split('.').pop())) {
			res.setHeader('Content-Type', 'image/svg+xml');
		} else {
			res.setHeader('Content-Type', 'image/jpeg');
		}
		res.setHeader('Content-Length', file.length);

		file.readStream.pipe(res);
	}));
});


import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/tap:i18n';
import { RocketChat } from 'meteor/rocketchat:lib';
import { ChatIntegrations } from '../collections';
import moment from 'moment';

Template.integrations.helpers({
	hasPermission() {
		return RocketChat.authz.hasAtLeastOnePermission(['manage-integrations', 'manage-own-integrations']);
	},
	integrations() {
		return ChatIntegrations.find();
	},
	dateFormated(date) {
		return moment(date).format('L LT');
	},
	eventTypeI18n(event) {
		return TAPi18n.__(RocketChat.integrations.outgoingEvents[event].label);
	},
});


import { Meteor } from 'meteor/meteor';

const waitToLoad = function(orch) {
	return new Promise((resolve) => {
		let id = setInterval(() => {
			if (orch.isEnabled() && orch.isLoaded()) {
				clearInterval(id);
				id = -1;
				resolve();
			}
		}, 100);
	});
};

const waitToUnload = function(orch) {
	return new Promise((resolve) => {
		let id = setInterval(() => {
			if (!orch.isEnabled() && !orch.isLoaded()) {
				clearInterval(id);
				id = -1;
				resolve();
			}
		}, 100);
	});
};

export class AppMethods {
	constructor(orch) {
		this._orch = orch;

		this._addMethods();
	}

	isEnabled() {
		return typeof this._orch !== 'undefined' && this._orch.isEnabled();
	}

	isLoaded() {
		return typeof this._orch !== 'undefined' && this._orch.isEnabled() && this._orch.isLoaded();
	}

	_addMethods() {
		const instance = this;

		Meteor.methods({
			'apps/is-enabled'() {
				return instance.isEnabled();
			},

			'apps/is-loaded'() {
				return instance.isLoaded();
			},

			'apps/go-enable'() {
				if (!Meteor.userId()) {
					throw new Meteor.Error('error-invalid-user', 'Invalid user', {
						method: 'apps/go-enable',
					});
				}

				if (!RocketChat.authz.hasPermission(Meteor.userId(), 'manage-apps')) {
					throw new Meteor.Error('error-action-not-allowed', 'Not allowed', {
						method: 'apps/go-enable',
					});
				}

				RocketChat.settings.set('Apps_Framework_enabled', true);

				Promise.await(waitToLoad(instance._orch));
			},

			'apps/go-disable'() {
				if (!Meteor.userId()) {
					throw new Meteor.Error('error-invalid-user', 'Invalid user', {
						method: 'apps/go-enable',
					});
				}

				if (!RocketChat.authz.hasPermission(Meteor.userId(), 'manage-apps')) {
					throw new Meteor.Error('error-action-not-allowed', 'Not allowed', {
						method: 'apps/go-enable',
					});
				}

				RocketChat.settings.set('Apps_Framework_enabled', false);

				Promise.await(waitToUnload(instance._orch));
			},
		});
	}
}


return RocketChat.Livechat.transfer(room, guest, transferData);

const guest = LivechatVisitors.findOneById(room.v && room.v._id);


import { Meteor } from 'meteor/meteor';
import { RocketChat } from 'meteor/rocketchat:lib';

export default function handleOnCreateUser(newUser) {
	if (!newUser) {
		return this.log('Invalid handleOnCreateUser call');
	}
	if (!newUser.username) {
		return this.log('Invalid handleOnCreateUser call (Missing username)');
	}
	if (this.loggedInUsers.indexOf(newUser._id) !== -1) {
		return this.log('Duplicate handleOnCreateUser call');
	}

	this.loggedInUsers.push(newUser._id);

	Meteor.users.update({ _id: newUser._id }, {
		$set: {
			'profile.irc.fromIRC': false,
			'profile.irc.username': `${ newUser.username }-rkt`,
			'profile.irc.nick': `${ newUser.username }-rkt`,
			'profile.irc.hostname': 'rocket.chat',
		},
	});

	const user = RocketChat.models.Users.findOne({
		_id: newUser._id,
	});

	this.sendCommand('registerUser', user);

	const rooms = RocketChat.models.Rooms.findBySubscriptionUserId(user._id).fetch();

	rooms.forEach((room) => this.sendCommand('joinedChannel', { room, user }));
}


RocketChat.Migrations.add({
	version: 134,
	up() {
		const updateIfDefault = (customized, idEmail, idSubject) => {
			const setting = RocketChat.models.Settings.findOne({ _id: customized });
			const newSettingEmail = RocketChat.models.Settings.findOne({ _id: idEmail });
			const newSettingSubject = RocketChat.models.Settings.findOne({ _id: idSubject });

			delete newSettingSubject._id;
			delete newSettingSubject.enableQuery;
			delete newSettingSubject.i18nDefaultQuery;

			delete newSettingEmail._id;
			delete newSettingEmail.enableQuery;
			delete newSettingEmail.i18nDefaultQuery;
			if (setting && (setting.value === false || newSettingEmail.value === '')) {
				newSettingEmail.value = newSettingEmail.packageValue;
			}
			if (setting && (setting.value === false || newSettingSubject.value === '')) {
				newSettingSubject.value = newSettingSubject.packageValue;
			}

			RocketChat.models.Settings.upsert({ _id: idEmail }, newSettingEmail);
			RocketChat.models.Settings.upsert({ _id: idSubject }, newSettingSubject);
			RocketChat.models.Settings.remove({ _id: customized });
		};
		const rename = (oldId, newId) => {
			const oldSetting = RocketChat.models.Settings.findOne({ _id: oldId });
			const newSetting = RocketChat.models.Settings.findOne({ _id: newId });

			delete oldSetting._id;
			delete oldSetting.enableQuery;
			delete oldSetting.i18nDefaultQuery;

			oldSetting.packageValue = newSetting.packageValue;
			oldSetting.value = newSetting.value || newSetting.packageValue;

			RocketChat.models.Settings.upsert({ _id: newId }, oldSetting);
			RocketChat.models.Settings.removeById(oldId);
		};

		updateIfDefault('Accounts_Enrollment_Customized', 'Accounts_Enrollment_Email', 'Accounts_Enrollment_Email_Subject');
		updateIfDefault('Accounts_UserAddedEmail_Customized', 'Accounts_UserAddedEmail', 'Accounts_UserAddedEmailSubject');
		updateIfDefault('Forgot_Password_Customized', 'Forgot_Password_Email', 'Forgot_Password_Email_Subject');
		updateIfDefault('Verification_Customized', 'Verification_Email', 'Verification_Email_Subject');
		updateIfDefault('Invitation_Customized', 'Invitation_HTML', 'Invitation_Subject');

		rename('Accounts_UserAddedEmail', 'Accounts_UserAddedEmail_Email');
		rename('Accounts_UserAddedEmailSubject', 'Accounts_UserAddedEmail_Subject');
		rename('Invitation_HTML', 'Invitation_Email');

		Object.entries({
			Email_Header: '

[Site_Name]

',
			Email_Footer: '

Powered by Rocket.Chat

',
			Email_Footer_Direct_Reply: '

You can directly reply to this email.
Do not modify previous emails in the thread.
Powered by Rocket.Chat

',
		}).forEach(([_id, oldValue]) => {
			const setting = RocketChat.models.Settings.findOne({ _id });
			if (setting.value === oldValue) {
				RocketChat.models.Settings.updateValueById(_id, setting.packageValue);
			}

		});

	},
});


import { Meteor } from 'meteor/meteor';
import { RocketChat, handleError } from 'meteor/rocketchat:lib';
import { ChatRoom } from 'meteor/rocketchat:ui';
/*
 * Join is a named function that will replace /topic commands
 * @param {Object} message - The message object
 */

function Topic(command, params, item) {
	if (command === 'topic') {
		if ((Meteor.isClient && RocketChat.authz.hasPermission('edit-room', item.rid)) || (Meteor.isServer && RocketChat.authz.hasPermission(Meteor.userId(), 'edit-room', item.rid))) {
			Meteor.call('saveRoomSettings', item.rid, 'roomTopic', params, (err) => {
				if (err) {
					if (Meteor.isClient) {
						return handleError(err);
					} else {
						throw err;
					}
				}

				if (Meteor.isClient) {
					RocketChat.callbacks.run('roomTopicChanged', ChatRoom.findOne(item.rid));
				}
			});
		}
	}
}

RocketChat.slashCommands.add('topic', Topic, {
	description: 'Slash_Topic_Description',
	params: 'Slash_Topic_Params',
});


let CustomTranslations = RocketChat.settings.get('Custom_Translations');

console.error('Invalid setting Custom_Translations', e);

keep keep keep replace replace replace replace replace keep

if (typeof CustomTranslations === 'string' && CustomTranslations.trim() !== '') {

Template.loginForm.helpers
	userName: ->
		return Meteor.user()?.username

	namePlaceholder: ->
		return if RocketChat.settings.get 'Accounts_RequireNameForSignUp' then t('Name') else t('Name_optional')

	showFormLogin: ->
		return RocketChat.settings.get 'Accounts_ShowFormLogin'

	state: (state..., data) ->
		return state.indexOf(Template.instance().state.get()) > -1

	btnLoginSave: ->
		switch Template.instance().state.get()
			when 'register'
				return t('Submit')
			when 'login'
				return t('Login')
			when 'email-verification'
				return t('Send_confirmation_email')
			when 'forgot-password'
				return t('Reset_password')

	loginTerms: ->
		return RocketChat.settings.get 'Layout_Login_Terms'

	registrationAllowed: ->
		return RocketChat.settings.get('Accounts_RegistrationForm') is 'Public' or Template.instance().validSecretURL?.get()

	linkReplacementText: ->
		return RocketChat.settings.get('Accounts_RegistrationForm_LinkReplacementText')

	passwordResetAllowed: ->
		return RocketChat.settings.get 'Accounts_PasswordReset'

	requirePasswordConfirmation: ->
		return RocketChat.settings.get 'Accounts_RequirePasswordConfirmation'

	emailOrUsernamePlaceholder: ->
		return RocketChat.settings.get('Accounts_EmailOrUsernamePlaceholder') or t("Email_or_username")

	passwordPlaceholder: ->
		return RocketChat.settings.get('Accounts_PasswordPlaceholder') or t("Password")

	hasOnePassword: ->
		return OnePassword?.findLoginForUrl? && device?.platform?.toLocaleLowerCase() is 'ios'

	customFields: ->
		if not Template.instance().customFields
			return []

		customFieldsArray = []
		for key, value of Template.instance().customFields
			customFieldsArray.push
				fieldName: key,
				field: value

		return customFieldsArray

Template.loginForm.events
	'submit #login-card': (event, instance) ->
		event.preventDefault()

		button = $(event.target).find('button.login')
		RocketChat.Button.loading(button)

		formData = instance.validate()
		if formData
			if instance.state.get() is 'email-verification'
				Meteor.call 'sendConfirmationEmail', s.trim(formData.email), (err, result) ->
					RocketChat.Button.reset(button)
					RocketChat.callbacks.run('userConfirmationEmailRequested');
					toastr.success t('We_have_sent_registration_email')
					instance.state.set 'login'
				return

			if instance.state.get() is 'forgot-password'
				Meteor.call 'sendForgotPasswordEmail', s.trim(formData.email), (err, result) ->
					RocketChat.Button.reset(button)
					RocketChat.callbacks.run('userForgotPasswordEmailRequested');
					toastr.success t('We_have_sent_password_email')
					instance.state.set 'login'
				return

			if instance.state.get() is 'register'
				formData.secretURL = FlowRouter.getParam 'hash'
				Meteor.call 'registerUser', formData, (error, result) ->
					RocketChat.Button.reset(button)

					if error?
						if error.reason is 'Email already exists.'
							toastr.error t 'Email_already_exists'
						else
							handleError(error)
						return

					RocketChat.callbacks.run('userRegistered');

					Meteor.loginWithPassword s.trim(formData.email), formData.pass, (error) ->
						if error?.error is 'error-invalid-email'
							toastr.success t('We_have_sent_registration_email')
							instance.state.set 'login'
						else if error?.error is 'error-user-is-not-activated'
							instance.state.set 'wait-activation'

			else
				loginMethod = 'loginWithPassword'
				if RocketChat.settings.get('LDAP_Enable')
					loginMethod = 'loginWithLDAP'
				if RocketChat.settings.get('CROWD_Enable')
					loginMethod = 'loginWithCrowd'
				Meteor[loginMethod] s.trim(formData.emailOrUsername), formData.pass, (error) ->
					RocketChat.Button.reset(button)
					if error?
						if error.error is 'no-valid-email'
							instance.state.set 'email-verification'
						else
							toastr.error t 'User_not_found_or_incorrect_password'
						return
					localStorage.setItem('userLanguage', Meteor.user()?.language)
					setLanguage(Meteor.user()?.language)

	'click .register': ->
		Template.instance().state.set 'register'
		RocketChat.callbacks.run('loginPageStateChange', Template.instance().state.get());

	'click .back-to-login': ->
		Template.instance().state.set 'login'
		RocketChat.callbacks.run('loginPageStateChange', Template.instance().state.get());

	'click .forgot-password': ->
		Template.instance().state.set 'forgot-password'
		RocketChat.callbacks.run('loginPageStateChange', Template.instance().state.get());

	'click .one-passsword': ->
		if not OnePassword?.findLoginForUrl?
			return

		succesCallback = (credentials) ->
			$('input[name=emailOrUsername]').val(credentials.username)
			$('input[name=pass]').val(credentials.password)

		errorCallback = ->
			console.log 'OnePassword errorCallback', arguments

		OnePassword.findLoginForUrl(succesCallback, errorCallback, Meteor.absoluteUrl())

Template.loginForm.onCreated ->
	instance = @

	if RocketChat.settings.get('Accounts_CustomFields') isnt ''
		try
			@customFields = JSON.parse RocketChat.settings.get('Accounts_CustomFields')
		catch e
			console.error('Invalid JSON for Accounts_CustomFields')

	if Meteor.settings.public.sandstorm
		@state = new ReactiveVar('sandstorm')
	else if Session.get 'loginDefaultState'
		@state = new ReactiveVar(Session.get 'loginDefaultState')
	else
		@state = new ReactiveVar('login')

	@validSecretURL = new ReactiveVar(false)

	validateCustomFields = (formObj, validationObj) ->
		if not instance.customFields
			return

		for field, value of formObj when instance.customFields[field]?
			customField = instance.customFields[field]

			if customField.required is true and not value
				return validationObj[field] = t('Field_required')

			if customField.maxLength? and value.length > customField.maxLength
				return validationObj[field] = t('Max_length_is', customField.maxLength)

			if customField.minLength? and value.length < customField.minLength
				return validationObj[field] = t('Min_length_is', customField.minLength)

	@validate = ->
		formData = $("#login-card").serializeArray()
		formObj = {}
		validationObj = {}

		for field in formData
			formObj[field.name] = field.value

		if instance.state.get() isnt 'login'
			unless formObj['email'] and /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]+\b/i.test(formObj['email'])
				validationObj['email'] = t('Invalid_email')

		if instance.state.get() isnt 'forgot-password'
			unless formObj['pass']
				validationObj['pass'] = t('Invalid_pass')

		if instance.state.get() is 'register'
			if RocketChat.settings.get('Accounts_RequireNameForSignUp') and not formObj['name']
				validationObj['name'] = t('Invalid_name')

			if RocketChat.settings.get('Accounts_RequirePasswordConfirmation') and formObj['confirm-pass'] isnt formObj['pass']
				validationObj['confirm-pass'] = t('Invalid_confirm_pass')

			validateCustomFields(formObj, validationObj)

		$("#login-card h2").removeClass "error"
		$("#login-card input.error, #login-card select.error").removeClass "error"
		$("#login-card .input-error").text ''

		unless _.isEmpty validationObj
			button = $('#login-card').find('button.login')
			RocketChat.Button.reset(button)
			$("#login-card h2").addClass "error"
			for key, value of validationObj
				$("#login-card input[name=#{key}], #login-card select[name=#{key}]").addClass "error"
				$("#login-card input[name=#{key}]~.input-error, #login-card select[name=#{key}]~.input-error").text value
			return false

		return formObj

	if FlowRouter.getParam('hash')
		Meteor.call 'checkRegistrationSecretURL', FlowRouter.getParam('hash'), (err, success) =>
			@validSecretURL.set true

Template.loginForm.onRendered ->
	Session.set 'loginDefaultState'
	Tracker.autorun =>
		RocketChat.callbacks.run('loginPageStateChange', this.state.get())
		switch this.state.get()
			when 'login', 'forgot-password', 'email-verification'
				Meteor.defer ->
					$('input[name=email]').select().focus()

			when 'register'
				Meteor.defer ->
					$('input[name=name]').select().focus()


<template name="account">

			{{> burger}}

				{{_ "User_Settings"}}

</template>


Importer.AddImporter 'hipchat', Importer.HipChat,
	name: 'HipChat'
	description: TAPi18n.__('Importer_From_Description', { from: 'HipChat' })
	fileTypeRegex: new RegExp 'application\/.*?zip'


@filteredUsers = new Mongo.Collection 'filtered-users'
@filteredUsersMemory = new Mongo.Collection null
@channelAutocomplete = new Mongo.Collection 'channel-autocomplete'

Meteor.startup ->
	Tracker.autorun ->
		if not Meteor.user()? or not Session.get('openedRoom')?
			return

		filteredUsersMemory.remove({})
		messageUsers = RocketChat.models.Messages.find({rid: Session.get('openedRoom'), 'u.username': {$ne: Meteor.user().username}}, {fields: {'u.username': 1, ts: 1}, sort: {ts: -1}}).fetch()
		uniqueMessageUsersControl = {}
		messageUsers.forEach (messageUser) ->
			if not uniqueMessageUsersControl[messageUser.u.username]?
				uniqueMessageUsersControl[messageUser.u.username] = true
				filteredUsersMemory.upsert messageUser.u.username,
					_id: messageUser.u.username
					username: messageUser.u.username
					status: Session.get('user_' + messageUser.u.username + '_status') or 'offline'
					ts: messageUser.ts

Template.messagePopupConfig.helpers
	popupUserConfig: ->
		self = this
		template = Template.instance()
		config =
			title: t('People')
			collection: filteredUsersMemory
			template: 'messagePopupUser'
			getInput: self.getInput
			textFilterDelay: 200
			getFilter: (collection, filter) ->
				exp = new RegExp("#{RegExp.escape filter}", 'i')

				# Get users from messages
				items = filteredUsersMemory.find({ts: {$exists: true}, username: exp}, {limit: 5, sort: {ts: -1}}).fetch()

				# Get online users
				if items.length < 5 and filter?.trim() isnt ''
					messageUsers = _.pluck(items, 'username')
					Meteor.users.find({$and: [{username: exp}, {username: {$nin: [Meteor.user()?.username].concat(messageUsers)}}]}, {limit: 5 - messageUsers.length}).fetch().forEach (item) ->
						items.push
							_id: item.username
							username: item.username
							status: item.status

				# Get users of room
				if items.length < 5 and filter?.trim() isnt ''
					messageUsers = _.pluck(items, 'username')
					Tracker.nonreactive ->
						roomUsernames = RocketChat.models.Rooms.findOne(Session.get('openedRoom')).usernames
						for roomUsername in roomUsernames
							if messageUsers.indexOf(roomUsername) is -1 and exp.test(roomUsername)
								items.push
									_id: roomUsername
									username: roomUsername
									status: Session.get('user_' + roomUsername + '_status') or 'offline'

								if items.length >= 5
									break

				# Get users from db
				if items.length < 5 and filter?.trim() isnt ''
					messageUsers = _.pluck(items, 'username')
					template.userFilter.set
						name: filter
						except: messageUsers

					if template.subscriptionsReady()
						filteredUsers.find({username: exp}, {limit: 5 - messageUsers.length}).fetch().forEach (item) ->
							items.push
								_id: item.username
								username: item.username
								status: 'offline'

				all =
					_id: '@all'
					username: 'all'
					system: true
					name: t 'Notify_all_in_this_room'
					compatibility: 'channel group'

				exp = new RegExp("(^|\\s)#{RegExp.escape filter}", 'i')
				if exp.test(all.username) or exp.test(all.compatibility)
					items.unshift all

				template.resultsLength.set items.length
				return items

			getValue: (_id, collection, firstPartValue) ->
				if _id is '@all'
					if firstPartValue.indexOf(' ') > -1
						return 'all'

					return 'all:'

				if firstPartValue.indexOf(' ') > -1
					return _id

				return _id + ':'

		return config

	popupChannelConfig: ->
		self = this
		template = Template.instance()
		config =
			title: t('Channels')
			collection: channelAutocomplete
			trigger: '#'
			template: 'messagePopupChannel'
			getInput: self.getInput
			textFilterDelay: 200
			getFilter: (collection, filter) ->
				exp = new RegExp(filter, 'i')
				template.channelFilter.set filter
				if template.channelSubscription.ready()
					results = collection.find( { name: exp }, { limit: 5 }).fetch()
				else
					results = []

				template.resultsLength.set results.length
				return results

			getValue: (_id, collection) ->
				return collection.findOne(_id)?.name

		return config

	popupSlashCommandsConfig: ->
		self = this
		template = Template.instance()

		config =
			title: t('Commands')
			collection: RocketChat.slashCommands.commands
			trigger: '/'
			triggerAnywhere: false
			template: 'messagePopupSlashCommand'
			getInput: self.getInput
			getFilter: (collection, filter) ->
				commands = []
				for command, item of collection
					if command.indexOf(filter) > -1
						commands.push
							_id: command
							params: item.params
							description: TAPi18n.__ item.description

				commands = commands.sort (a, b) ->
					return a._id > b._id

				commands = commands[0..10]

				template.resultsLength.set commands.length
				return commands

		return config

	emojiEnabled: ->
		return RocketChat.emoji?

	popupEmojiConfig: ->
		if RocketChat.emoji?
			self = this
			template = Template.instance()
			config =
				title: t('Emoji')
				collection: RocketChat.emoji.list
				template: 'messagePopupEmoji'
				trigger: ':'
				prefix: ''
				getInput: self.getInput
				getFilter: (collection, filter) ->
					results = []
					key = ':' + filter

					if RocketChat.emoji.asciiList[key] or filter.length < 2
						return []

					# use ascii
					for shortname, value of RocketChat.emoji.asciiList
						if results.length > 10
							break

						if shortname.startsWith(key)
							results.push
								_id: shortname
								data: [value]

					# use shortnames
					for shortname, data of collection
						if results.length > 10
							break

						if shortname.startsWith(key)
							results.push
								_id: shortname
								data: data

					#if filter.length >= 3
					results.sort (a, b) ->
						a._id.length - b._id.length

					template.resultsLength.set results.length
					return results

		return config

	subscriptionNotReady: ->
		template = Template.instance()
		return 'notready' if template.resultsLength.get() is 0 and not template.subscriptionsReady()

Template.messagePopupConfig.onCreated ->
	@userFilter = new ReactiveVar {}
	@channelFilter = new ReactiveVar ''
	@resultsLength = new ReactiveVar 0

	template = @
	@autorun ->
		template.userSubscription = template.subscribe 'filteredUsers', template.userFilter.get()

	@autorun ->
		template.channelSubscription = template.subscribe 'channelAutocomplete', template.channelFilter.get()


for (const lang in CustomTranslations) {

const translations = CustomTranslations[lang];

keep replace replace replace replace keep keep keep keep

console.error('Invalid setting Custom_Translations', e);


Meteor.publish 'privateHistory', ->
	unless this.userId
		return this.ready()

	RocketChat.models.Rooms.findByContainigUsername RocketChat.models.Users.findOneById(this.userId).username,
		fields:
			t: 1
			name: 1
			msgs: 1
			ts: 1
			lm: 1
			cl: 1


Package.describe({
	name: 'rocketchat:slashcommands-unarchive',
	version: '0.0.1',
	summary: 'Command handler for the /unarchive command',
	git: ''
});

Package.onUse(function(api) {

	api.versionsFrom('1.0');

	api.use([
		'ecmascript',
		'check',
		'rocketchat:lib'
	]);

	api.use('templating', 'client');

	api.addFiles('client.js', 'client');
	api.addFiles('messages.js', 'server');
	api.addFiles('server.js', 'server');
});


Package.describe({
	name: 'rocketchat:sms',
	version: '0.0.1',
	summary: '',
	git: '',
	documentation: 'README.md'
});

Package.onUse(function(api) {
	api.versionsFrom('1.2.1');
	api.use('ecmascript');
	api.use('rocketchat:lib');

	api.addFiles('settings.js', 'server');
	api.addFiles('SMS.js', 'server');
	api.addFiles('services/twilio.js', 'server');
});

Npm.depends({
	'twilio': '2.9.1'
});


return OnePassword?.findLoginForUrl? && device?.platform?.toLocaleLowerCase() is 'ios'

keep keep keep keep keep replace keep keep keep


/*
* Unflip is a named function that will replace /unflip commands
* @param {Object} message - The message object
*/

function Unflip(command, params, item) {
	if (command === 'unflip') {
		var msg;

		msg = item;
		msg.msg = params + ' ┬─┬ ノ( ゜-゜ノ)';
		Meteor.call('sendMessage', msg);
	}
}

RocketChat.slashCommands.add('unflip', Unflip, {
	description: 'Slash_TableUnflip_Description',
	params: 'your message (optional)'
});


RocketChat.Notifications = new class
	constructor: ->
		self = @

		@debug = false

		@streamAll = new Meteor.Streamer 'notify-all'
		@streamRoom = new Meteor.Streamer 'notify-room'
		@streamUser = new Meteor.Streamer 'notify-user'

		@streamAll.allowWrite('none')
		@streamRoom.allowWrite('none')
		@streamUser.allowWrite('logged')

		@streamAll.allowRead('logged')

		@streamRoom.allowRead (eventName) ->
			if not @userId? then return false

			roomId = eventName.split('/')[0]

			user = Meteor.users.findOne @userId, {fields: {username: 1}}
			return RocketChat.models.Rooms.findOneByIdContainigUsername(roomId, user.username, {fields: {_id: 1}})?

		@streamUser.allowRead (eventName) ->
			userId = eventName.split('/')[0]
			return @userId? and @userId is userId

	notifyAll: (eventName, args...) ->
		console.log 'notifyAll', arguments if @debug is true

		args.unshift eventName
		@streamAll.emit.apply @streamAll, args

	notifyRoom: (room, eventName, args...) ->
		console.log 'notifyRoom', arguments if @debug is true

		args.unshift "#{room}/#{eventName}"
		@streamRoom.emit.apply @streamRoom, args

	notifyUser: (userId, eventName, args...) ->
		console.log 'notifyUser', arguments if @debug is true

		args.unshift "#{userId}/#{eventName}"
		@streamUser.emit.apply @streamUser, args

	notifyAllInThisInstance: (eventName, args...) ->
		console.log 'notifyAll', arguments if @debug is true

		args.unshift eventName
		@streamAll.emitWithoutBroadcast.apply @streamAll, args

	notifyRoomInThisInstance: (room, eventName, args...) ->
		console.log 'notifyRoomAndBroadcast', arguments if @debug is true

		args.unshift "#{room}/#{eventName}"
		@streamRoom.emitWithoutBroadcast.apply @streamRoom, args

	notifyUserInThisInstance: (userId, eventName, args...) ->
		console.log 'notifyUserAndBroadcast', arguments if @debug is true

		args.unshift "#{userId}/#{eventName}"
		@streamUser.emitWithoutBroadcast.apply @streamUser, args

## Permissions for client

# Enable emit for event typing for rooms and add username to event data
func = (eventName, username, typing) ->
	[room, e] = eventName.split('/')

	if e is 'webrtc'
		return true

	if e is 'typing'
		user = Meteor.users.findOne(@userId, {fields: {username: 1}})
		if user?.username is username
			return true

	return false

RocketChat.Notifications.streamRoom.allowWrite func


@MsgTyping = do ->
	timeout = 15000
	timeouts = {}
	renew = true
	renewTimeout = 10000
	selfTyping = new ReactiveVar false
	usersTyping = {}
	dep = new Tracker.Dependency

	addStream = (room) ->
		if _.isEmpty usersTyping[room]?.users
			usersTyping[room] = { users: {} }
			Notifications.onRoom room, 'typing', (username, typing) ->
				unless username is Meteor.user()?.username
					if typing is true
						users = usersTyping[room].users
						users[username] = Meteor.setTimeout ->
							delete users[username]
							usersTyping[room].users = users
							dep.changed()
						, timeout
						usersTyping[room].users = users
						dep.changed()
					else
						users = usersTyping[room].users
						delete users[username]
						usersTyping[room].users = users
						dep.changed()

	Tracker.autorun ->
		if visitor.getRoom()
			addStream visitor.getRoom()

	start = (room) ->
		return unless renew

		setTimeout ->
			renew = true
		, renewTimeout

		renew = false
		selfTyping.set true
		Notifications.notifyRoom room, 'typing', Meteor.user()?.username, true
		clearTimeout timeouts[room]
		timeouts[room] = Meteor.setTimeout ->
			stop(room)
		, timeout

	stop = (room) ->
		renew = true
		selfTyping.set false
		if timeouts?[room]?
			clearTimeout(timeouts[room])
			timeouts[room] = null
		Notifications.notifyRoom room, 'typing', Meteor.user()?.username, false

	get = (room) ->
		dep.depend()
		unless usersTyping[room]
			usersTyping[room] = { users: {} }
		users = usersTyping[room].users
		return _.keys(users) or []

	return {
		start: start
		stop: stop
		get: get
		selfTyping: selfTyping
	}


keep keep keep replace keep keep keep keep

for key, value of Template.instance().customFields.get()


Package.describe({
	name: 'rocketchat:api',
	version: '0.0.1',
	summary: 'Rest API',
	git: ''
});

Package.onUse(function(api) {
	api.versionsFrom('1.0');

	api.use([
		'coffeescript',
		'underscore',
		'rocketchat:lib',
		'nimble:restivus'
	]);

	api.addFiles('server/api.coffee', 'server');
	api.addFiles('server/routes.coffee', 'server');
});


Package.describe({
	name: 'rocketchat:slashcommands-leave',
	version: '0.0.1',
	summary: 'Message pre-processor that will translate /leave commands',
	git: ''
});

Package.onUse(function(api) {
	api.versionsFrom('1.0');

	api.use([
		'coffeescript',
		'rocketchat:lib'
	]);
	api.addFiles('leave.coffee');
});


Template.loginForm.onCreated ->
instance = @


Meteor.startup(function() {
	Tracker.autorun(function() {
		if (RocketChat.settings.get('OTR_Enable') && window.crypto) {
			RocketChat.OTR.crypto = window.crypto.subtle || window.crypto.webkitSubtle;
			RocketChat.OTR.enabled.set(true);
			RocketChat.TabBar.addButton({
				groups: ['directmessage'],
				id: 'otr',
				i18nTitle: 'OTR',
				icon: 'icon-key',
				template: 'otrFlexTab',
				order: 11
			});
		} else {
			RocketChat.OTR.enabled.set(false);
			RocketChat.TabBar.removeButton('otr');
		}
	});
});


/* globals Slingshot, FileUpload, AWS, SystemLogger */
var crypto = Npm.require('crypto');

var S3accessKey, S3secretKey, S3expiryTimeSpan;

var generateURL = function(file) {
	if (!file || !file.s3) {
		return;
	}
	let resourceURL = '/' + file.s3.bucket + '/' + file.s3.path + file._id;
	let expires = parseInt(new Date().getTime() / 1000) + Math.max(5, S3expiryTimeSpan);
	let StringToSign = 'GET\n\n\n' + expires +'\n'+resourceURL;
	let signature = crypto.createHmac('sha1', S3secretKey).update(new Buffer(StringToSign, 'utf-8')).digest('base64');
	return file.url + '?AWSAccessKeyId='+encodeURIComponent(S3accessKey)+'&Expires='+expires+'&Signature='+encodeURIComponent(signature);
};

FileUpload.addHandler('s3', {
	get(file, req, res) {
		let fileUrl = generateURL(file);

		if (fileUrl) {
			res.setHeader('Location', fileUrl);
			res.writeHead(302);
		}
		res.end();
	},
	delete(file) {
		let s3 = new AWS.S3();
		let request = s3.deleteObject({
			Bucket: file.s3.bucket,
			Key: file.s3.path + file._id
		});
		request.send();
	}
});

var createS3Directive = _.debounce(() => {
	var directiveName = 'rocketchat-uploads';

	var type = RocketChat.settings.get('FileUpload_Storage_Type');
	var bucket = RocketChat.settings.get('FileUpload_S3_Bucket');
	var acl = RocketChat.settings.get('FileUpload_S3_Acl');
	var accessKey = RocketChat.settings.get('FileUpload_S3_AWSAccessKeyId');
	var secretKey = RocketChat.settings.get('FileUpload_S3_AWSSecretAccessKey');
	var cdn = RocketChat.settings.get('FileUpload_S3_CDN');
	var region = RocketChat.settings.get('FileUpload_S3_Region');
	var bucketUrl = RocketChat.settings.get('FileUpload_S3_BucketURL');

	AWS.config.update({
		accessKeyId: RocketChat.settings.get('FileUpload_S3_AWSAccessKeyId'),
		secretAccessKey: RocketChat.settings.get('FileUpload_S3_AWSSecretAccessKey')
	});

	if (type === 'AmazonS3' && !_.isEmpty(bucket) && !_.isEmpty(accessKey) && !_.isEmpty(secretKey)) {
		if (Slingshot._directives[directiveName]) {
			delete Slingshot._directives[directiveName];
		}
		var config = {
			bucket: bucket,
			AWSAccessKeyId: accessKey,
			AWSSecretAccessKey: secretKey,
			key: function(file, metaContext) {
				var path = RocketChat.hostname + '/' + metaContext.rid + '/' + this.userId + '/';

				let upload = {
					s3: {
						bucket: bucket,
						region: region,
						path: path
					}
				};
				let fileId = RocketChat.models.Uploads.insertFileInit(metaContext.rid, this.userId, 's3', file, upload);

				return path + fileId;
			}
		};

		if (!_.isEmpty(acl)) {
			config.acl = acl;
		}

		if (!_.isEmpty(cdn)) {
			config.cdn = cdn;
		}

		if (!_.isEmpty(region)) {
			config.region = region;
		}

		if (!_.isEmpty(bucketUrl)) {
			config.bucketUrl = bucketUrl;
		}

		try {
			Slingshot.createDirective(directiveName, Slingshot.S3Storage, config);
		} catch (e) {
			SystemLogger.error('Error configuring S3 ->', e.message);
		}
	} else if (Slingshot._directives[directiveName]) {
		delete Slingshot._directives[directiveName];
	}
}, 500);

RocketChat.settings.get('FileUpload_Storage_Type', createS3Directive);

RocketChat.settings.get('FileUpload_S3_Bucket', createS3Directive);

RocketChat.settings.get('FileUpload_S3_Acl', createS3Directive);

RocketChat.settings.get('FileUpload_S3_AWSAccessKeyId', function(key, value) {
	S3accessKey = value;
	createS3Directive();
});

RocketChat.settings.get('FileUpload_S3_AWSSecretAccessKey', function(key, value) {
	S3secretKey = value;
	createS3Directive();
});

RocketChat.settings.get('FileUpload_S3_URLExpiryTimeSpan', function(key, value) {
	S3expiryTimeSpan = value;
	createS3Directive();
});

RocketChat.settings.get('FileUpload_S3_CDN', createS3Directive);

RocketChat.settings.get('FileUpload_S3_Region', createS3Directive);

RocketChat.settings.get('FileUpload_S3_BucketURL', createS3Directive);


/* eslint-disable */
// Use rawConnectHandlers so we get a response as quickly as possible
// https://github.com/meteor/meteor/blob/devel/packages/webapp/webapp_server.js

var syncUrl = "/_timesync";
if (__meteor_runtime_config__.ROOT_URL_PATH_PREFIX) {
	syncUrl = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX + syncUrl;
}

WebApp.rawConnectHandlers.use(syncUrl,
  function(req, res, next) {
    // Never ever cache this, otherwise weird times are shown on reload
    // http://stackoverflow.com/q/18811286/586086
    res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
    res.setHeader("Pragma", "no-cache");
    res.setHeader("Expires", 0);

    // Avoid MIME type warnings in browsers
    res.setHeader("Content-Type", "text/plain");

    // Cordova lives in meteor.local, so it does CORS
    if (req.headers && req.headers.origin === 'http://meteor.local') {
      res.setHeader('Access-Control-Allow-Origin', 'http://meteor.local');
    }

    res.end(Date.now().toString());
  }
);


if RocketChat.settings.get('Accounts_CustomFields') isnt ''

@customFields = JSON.parse RocketChat.settings.get('Accounts_CustomFields')

console.error('Invalid JSON for Accounts_CustomFields')

keep replace replace replace replace replace keep keep keep

if typeof Accounts_CustomFields is 'string' and Accounts_CustomFields.trim() isnt ''

Meteor.startup(function() {
	Meteor.autorun(function() {
		let CustomTranslations = RocketChat.settings.get('Custom_Translations');
		try {
			CustomTranslations = JSON.parse(CustomTranslations);
		} catch (e) {
			console.error('Invalid setting Custom_Translations', e);
		}

		for (const lang in CustomTranslations) {
			if (CustomTranslations.hasOwnProperty(lang)) {
				const translations = CustomTranslations[lang];
				TAPi18next.addResourceBundle(lang, 'project', translations);
			}
		}
	});
});


/* globals FileUploadBase, UploadFS, FileUpload:true, FileSystemStore:true */

FileSystemStore = new UploadFS.store.Local({
	collection: RocketChat.models.Uploads.model,
	name: 'fileSystem',
	filter: new UploadFS.Filter({
		onCheck: FileUpload.validateFileUpload
	})
});

FileUpload.FileSystem = class FileUploadFileSystem extends FileUploadBase {
	constructor(meta, file, data) {
		super(meta, file, data);
		this.handler = new UploadFS.Uploader({
			store: FileSystemStore,
			data: data,
			file: meta,
			onError: (err) => {
				var uploading = Session.get('uploading');
				if (uploading != null) {
					let item = _.findWhere(uploading, {
						id: this.id
					});
					if (item != null) {
						item.error = err.reason;
						item.percentage = 0;
					}
					return Session.set('uploading', uploading);
				}
			},
			onComplete: (fileData) => {
				var file = _.pick(fileData, '_id', 'type', 'size', 'name', 'identify');

				file.url = fileData.url.replace(Meteor.absoluteUrl(), '/');

				Meteor.call('sendFileMessage', this.meta.rid, null, file, () => {
					Meteor.setTimeout(() => {
						var uploading = Session.get('uploading');
						if (uploading != null) {
							let item = _.findWhere(uploading, {
								id: this.id
							});
							return Session.set('uploading', _.without(uploading, item));
						}
					}, 2000);
				});
			}
		});
	}
	start() {
		return this.handler.start();
	}

	getProgress() {
		return this.handler.getProgress();
	}

	stop() {
		return this.handler.stop();
	}
};