<commit_before>import { shallowMount, mount } from '@vue/test-utils';
import CdrModal from 'componentdir/modal/CdrModal';
import Vue from 'vue';

describe('CdrModal.vue', () => {
  it('renders correctly', () => {
    const wrapper = shallowMount(CdrModal, {
      propsData: {
        opened: true,
        label: "Label is the modal title"
      },
      slots: {
        default: "Modal text content"
      },
    });
    expect(wrapper.element).toMatchSnapshot();
  });

  it('sets up noScroll, handlers when initialized open', () => {
    const spyAddNoScroll = jest.fn();
    const spyAddHandlers = jest.fn();
    const wrapper = shallowMount(CdrModal, {
      propsData: {
        opened: true,
        label: "My Modal Label",
      },
      methods: {
        addNoScroll: spyAddNoScroll,
        addHandlers: spyAddHandlers,
      },
    });

    expect(spyAddNoScroll).toHaveBeenCalled();
    expect(spyAddHandlers).toHaveBeenCalled();
  });

  it('calls correct function on opened prop update', () => {
    const wrapper = shallowMount(CdrModal, {
      propsData: {
        opened: false,
        label: "My Modal Label",
      },
    });

    spyOn(wrapper.vm, 'handleOpened');
    spyOn(wrapper.vm, 'handleClosed');

    wrapper.setProps({ opened: true });
    expect(wrapper.vm.handleOpened).toHaveBeenCalled();

    wrapper.setProps({ opened: false });
    expect(wrapper.vm.handleClosed).toHaveBeenCalled();
  });

  it('handleKeyDown', () => {
    const wrapper = shallowMount(CdrModal, {
      propsData: {
        opened: true,
        label: "My Modal Label",
      },
      attachToDocument: true,
    });
    spyOn(wrapper.vm, 'onClick');

    wrapper.trigger('keydown', {
      key: 'a',
    });
    expect(wrapper.vm.onClick).not.toHaveBeenCalled()

    wrapper.trigger('keydown', {
      key: 'Escape',
    });
    expect(wrapper.vm.onClick).toHaveBeenCalled();
  });

  it('handleOpened', () => {
    const wrapper = shallowMount(CdrModal, {
      propsData: {
        opened: false,
        label: "My Modal Label",
      },
    });

    spyOn(wrapper.vm, 'addNoScroll');
    spyOn(wrapper.vm, 'addHandlers');

    wrapper.setProps({ opened: true });

    expect(wrapper.vm.addNoScroll).toHaveBeenCalled();
    expect(wrapper.vm.reallyClosed).toBeFalsy();
    expect(wrapper.vm.lastActive).not.toEqual(null);

    wrapper.vm.$nextTick(() => {
      expect(wrapper.vm.addHandlers).toHaveBeenCalled();
    });
  });

  it('handleClosed', (done) => {
    spyOn(document, 'removeEventListener');
    global.scrollTo = jest.fn();

    const wrapper = shallowMount(CdrModal, {
      propsData: {
        opened: true,
        label: "My Modal Label",
      },
    });

    wrapper.setProps({ opened: false });

    expect(document.removeEventListener).toHaveBeenCalledWith('keydown', expect.anything());

    Vue.nextTick(() => {
      setTimeout(() => {
        expect(wrapper.vm.reallyClosed).toBe(true);
        expect(wrapper.vm.unsubscribe).toBe(null);
        expect(document.removeEventListener).nthCalledWith(2, 'focusin', expect.anything(), true);
        done();
      }, 1000);
    });
  });

  it('addNoScroll', () => {
    const wrapper = shallowMount(CdrModal, {
      propsData: {
        opened: true,
        label: "My Modal Label",
      },
    });
    const { documentElement, body } = document;

    expect(documentElement.classList.contains('cdr-modal__noscroll')).toBeTruthy();
    expect(body.classList.contains('cdr-modal__noscroll')).toBeTruthy()
  });

  it('removeNoScroll', () => {
    const wrapper = shallowMount(CdrModal, {
      propsData: {
        opened: true,
        label: "My Modal Label",
      },
    });
    const { documentElement, body } = document;
    wrapper.vm.removeNoScroll();

    expect(documentElement.classList.contains('noscroll')).toBeFalsy();
    expect(body.classList.contains('noscroll')).toBeFalsy()
  });

  it('addHandlers', () => {
    spyOn(document, 'addEventListener');
    const wrapper = shallowMount(CdrModal, {
      propsData: {
        opened: true,
        label: "My Modal Label",
      },
    });

    expect(document.addEventListener).toHaveBeenCalledWith('focusin', expect.anything(), true);
    expect(document.addEventListener).nthCalledWith(2, 'keydown', expect.anything());
  });

  it('beforeDestory', () => {
    spyOn(document, 'removeEventListener');
    const spyRemoveNoScroll = jest.fn();
    const spyUnsubscribe = jest.fn();

    const wrapper = shallowMount(CdrModal, {
      propsData: {
        opened: true,
        label: "My Modal Label",
      },
      methods: {
        removeNoScroll: spyRemoveNoScroll,
      },
    });

    wrapper.setData({ unsubscribe: spyUnsubscribe });
    wrapper.destroy();

    expect(spyUnsubscribe).toHaveBeenCalled();
    expect(spyRemoveNoScroll).toHaveBeenCalled();
    expect(document.removeEventListener).toHaveBeenCalledWith('focusin', expect.anything(), true);
    expect(document.removeEventListener).toHaveBeenCalledWith('keydown', expect.anything());
  });

  it('onClick', () => {
    const wrapper = shallowMount(CdrModal, {
      propsData: {
        opened: true,
        label: "My Modal Label",
        showLabel: true,
      },
    });

    wrapper.find('#close-modal-button').trigger('click');
    expect(wrapper.emitted('closed'));
  });
});<commit_msg>test(modal): update tests<commit_after>import { shallowMount, mount } from '@vue/test-utils';
import CdrModal from 'componentdir/modal/CdrModal';
import Vue from 'vue';
// import packageJson from '../../../../package.json';
import CdrButton from 'componentdir/button/CdrButton';

describe('CdrModal.vue', () => {
  it('renders correctly', () => {
    const wrapper = shallowMount(CdrModal, {
      propsData: {
        opened: true,
        label: "Label is the modal title"
      },
      slots: {
        default: "Modal text content"
      },
    });
    expect(wrapper.element).toMatchSnapshot();
  });

  it('sets up noScroll, handlers when initialized open', () => {
    const spyAddNoScroll = jest.fn();
    const spyAddHandlers = jest.fn();
    const wrapper = shallowMount(CdrModal, {
      propsData: {
        opened: true,
        label: "My Modal Label",
      },
      methods: {
        addNoScroll: spyAddNoScroll,
        addHandlers: spyAddHandlers,
      },
    });

    expect(spyAddNoScroll).toHaveBeenCalled();
    expect(spyAddHandlers).toHaveBeenCalled();
  });

  it('calls correct function on opened prop update', () => {
    const wrapper = shallowMount(CdrModal, {
      propsData: {
        opened: false,
        label: "My Modal Label",
      },
    });

    spyOn(wrapper.vm, 'handleOpened');
    spyOn(wrapper.vm, 'handleClosed');

    wrapper.setProps({ opened: true });
    expect(wrapper.vm.handleOpened).toHaveBeenCalled();

    wrapper.setProps({ opened: false });
    expect(wrapper.vm.handleClosed).toHaveBeenCalled();
  });

  it('handleKeyDown', () => {
    const wrapper = shallowMount(CdrModal, {
      propsData: {
        opened: true,
        label: "My Modal Label",
      },
      attachToDocument: true,
    });
    spyOn(wrapper.vm, 'onClick');

    wrapper.trigger('keydown', {
      key: 'a',
    });
    expect(wrapper.vm.onClick).not.toHaveBeenCalled()

    wrapper.trigger('keydown', {
      key: 'Escape',
    });
    expect(wrapper.vm.onClick).toHaveBeenCalled();

    wrapper.destroy();
  });

  it('handleOpened', () => {
    const wrapper = shallowMount(CdrModal, {
      propsData: {
        opened: false,
        label: "My Modal Label",
      },
    });

    spyOn(wrapper.vm, 'addNoScroll');
    spyOn(wrapper.vm, 'addHandlers');

    wrapper.setProps({ opened: true });

    expect(wrapper.vm.addNoScroll).toHaveBeenCalled();
    expect(wrapper.vm.reallyClosed).toBeFalsy();
    expect(wrapper.vm.lastActive).not.toEqual(null);

    wrapper.vm.$nextTick(() => {
      expect(wrapper.vm.addHandlers).toHaveBeenCalled();
    });
  });

  it('handleClosed', (done) => {
    spyOn(document, 'removeEventListener');
    global.scrollTo = jest.fn();

    const wrapper = shallowMount(CdrModal, {
      propsData: {
        opened: true,
        label: "My Modal Label",
      },
    });

    wrapper.setProps({ opened: false });

    expect(document.removeEventListener).toHaveBeenCalledWith('keydown', expect.anything());

    Vue.nextTick(() => {
      setTimeout(() => {
        expect(wrapper.vm.reallyClosed).toBe(true);
        expect(wrapper.vm.unsubscribe).toBe(null);
        expect(document.removeEventListener).nthCalledWith(2, 'focusin', expect.anything(), true);
        done();
      }, 1000);
    });
  });

  it('addNoScroll', () => {
    const wrapper = shallowMount(CdrModal, {
      propsData: {
        opened: true,
        label: "My Modal Label",
      },
      attachToDocument: true,
    });
    const { documentElement, body } = document;

    expect(documentElement.classList.contains('cdr-modal__noscroll')).toBeTruthy();
    expect(body.classList.contains('cdr-modal__noscroll')).toBeTruthy()
    wrapper.destroy();
  });

  it('removeNoScroll', () => {
    const wrapper = shallowMount(CdrModal, {
      propsData: {
        opened: true,
        label: "My Modal Label",
      },
    });
    const { documentElement, body } = document;
    wrapper.vm.removeNoScroll();

    expect(documentElement.classList.contains('noscroll')).toBeFalsy();
    expect(body.classList.contains('noscroll')).toBeFalsy()
  });

  it('addHandlers', () => {
    spyOn(document, 'addEventListener');
    const wrapper = shallowMount(CdrModal, {
      propsData: {
        opened: true,
        label: "My Modal Label",
      },
    });

    expect(document.addEventListener).toHaveBeenCalledWith('focusin', expect.anything(), true);
    expect(document.addEventListener).nthCalledWith(2, 'keydown', expect.anything());
  });

  it('beforeDestory', () => {
    spyOn(document, 'removeEventListener');
    const spyRemoveNoScroll = jest.fn();
    const spyUnsubscribe = jest.fn();

    const wrapper = shallowMount(CdrModal, {
      propsData: {
        opened: true,
        label: "My Modal Label",
      },
      methods: {
        removeNoScroll: spyRemoveNoScroll,
      },
    });

    wrapper.setData({ unsubscribe: spyUnsubscribe });
    wrapper.destroy();

    expect(spyUnsubscribe).toHaveBeenCalled();
    expect(spyRemoveNoScroll).toHaveBeenCalled();
    expect(document.removeEventListener).toHaveBeenCalledWith('focusin', expect.anything(), true);
    expect(document.removeEventListener).toHaveBeenCalledWith('keydown', expect.anything());
  });

  it('onClick', () => {
    const wrapper = shallowMount(CdrModal, {
      propsData: {
        opened: true,
        label: "My Modal Label",
        showLabel: true,
      },
    });

    wrapper.find('#close-modal-button').trigger('click');
    expect(wrapper.emitted('closed'));
  });

  it('handleFocus', () => {
    const wrapper = mount(CdrModal, {
      propsData: {
        opened: true,
        label: "My Modal Label",
        showLabel: true,
      },
      attachToDocument: true,
    });

    const button = wrapper.find('button').element;
    button.focus();
    expect(button).toBe(document.activeElement);

    wrapper.vm.handleFocus({ target: document.createElement('a') });
    expect(document.scrollTop).toBe(undefined);
    expect(document.scrollLeft).toBe(undefined);
    wrapper.destroy();
  });
});<|endoftext|>


<commit_before>/*
Copyright 2019 Tulir Asokan <tulir@maunium.net>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';
import PropTypes from 'prop-types';

import sdk from '../../../index';
import { _t } from '../../../languageHandler';

import * as recent from './recent';
import {DATA_BY_CATEGORY, getEmojiFromUnicode} from "../../../emoji";

export const CATEGORY_HEADER_HEIGHT = 22;
export const EMOJI_HEIGHT = 37;
export const EMOJIS_PER_ROW = 8;

class EmojiPicker extends React.Component {
    static propTypes = {
        onChoose: PropTypes.func.isRequired,
        selectedEmojis: PropTypes.instanceOf(Set),
        showQuickReactions: PropTypes.bool,
    };

    constructor(props) {
        super(props);

        this.state = {
            filter: "",
            previewEmoji: null,
            scrollTop: 0,
            // initial estimation of height, dialog is hardcoded to 450px height.
            // should be enough to never have blank rows of emojis as
            // 3 rows of overflow are also rendered. The actual value is updated on scroll.
            viewportHeight: 280,
        };

        // Convert recent emoji characters to emoji data, removing unknowns.
        this.recentlyUsed = recent.get()
            .map(unicode => getEmojiFromUnicode(unicode))
            .filter(data => !!data);
        this.memoizedDataByCategory = {
            recent: this.recentlyUsed,
            ...DATA_BY_CATEGORY,
        };

        this.categories = [{
            id: "recent",
            name: _t("Frequently Used"),
            enabled: this.recentlyUsed.length > 0,
            visible: this.recentlyUsed.length > 0,
            ref: React.createRef(),
        }, {
            id: "people",
            name: _t("Smileys & People"),
            enabled: true,
            visible: true,
            ref: React.createRef(),
        }, {
            id: "nature",
            name: _t("Animals & Nature"),
            enabled: true,
            visible: false,
            ref: React.createRef(),
        }, {
            id: "foods",
            name: _t("Food & Drink"),
            enabled: true,
            visible: false,
            ref: React.createRef(),
        }, {
            id: "activity",
            name: _t("Activities"),
            enabled: true,
            visible: false,
            ref: React.createRef(),
        }, {
            id: "places",
            name: _t("Travel & Places"),
            enabled: true,
            visible: false,
            ref: React.createRef(),
        }, {
            id: "objects",
            name: _t("Objects"),
            enabled: true,
            visible: false,
            ref: React.createRef(),
        }, {
            id: "symbols",
            name: _t("Symbols"),
            enabled: true,
            visible: false,
            ref: React.createRef(),
        }, {
            id: "flags",
            name: _t("Flags"),
            enabled: true,
            visible: false,
            ref: React.createRef(),
        }];

        this.bodyRef = React.createRef();

        this.onChangeFilter = this.onChangeFilter.bind(this);
        this.onHoverEmoji = this.onHoverEmoji.bind(this);
        this.onHoverEmojiEnd = this.onHoverEmojiEnd.bind(this);
        this.onClickEmoji = this.onClickEmoji.bind(this);
        this.scrollToCategory = this.scrollToCategory.bind(this);
        this.updateVisibility = this.updateVisibility.bind(this);
    }

    onScroll = () => {
        const body = this.bodyRef.current;
        this.setState({
            scrollTop: body.scrollTop,
            viewportHeight: body.clientHeight,
        });
        this.updateVisibility();
    };

    updateVisibility() {
        const body = this.bodyRef.current;
        const rect = body.getBoundingClientRect();
        for (const cat of this.categories) {
            const elem = body.querySelector(`[data-category-id="${cat.id}"]`);
            if (!elem) {
                cat.visible = false;
                cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible");
                continue;
            }
            const elemRect = elem.getBoundingClientRect();
            const y = elemRect.y - rect.y;
            const yEnd = elemRect.y + elemRect.height - rect.y;
            cat.visible = y < rect.height && yEnd > 0;
            // We update this here instead of through React to avoid re-render on scroll.
            if (cat.visible) {
                cat.ref.current.classList.add("mx_EmojiPicker_anchor_visible");
            } else {
                cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible");
            }
        }
    }

    scrollToCategory(category) {
        this.bodyRef.current.querySelector(`[data-category-id="${category}"]`).scrollIntoView();
    }

    onChangeFilter(filter) {
        filter = filter.toLowerCase(); // filter is case insensitive stored lower-case
        for (const cat of this.categories) {
            let emojis;
            // If the new filter string includes the old filter string, we don't have to re-filter the whole dataset.
            if (filter.includes(this.state.filter)) {
                emojis = this.memoizedDataByCategory[cat.id];
            } else {
                emojis = cat.id === "recent" ? this.recentlyUsed : DATA_BY_CATEGORY[cat.id];
            }
            emojis = emojis.filter(emoji => emoji.filterString.includes(filter));
            this.memoizedDataByCategory[cat.id] = emojis;
            cat.enabled = emojis.length > 0;
            // The setState below doesn't re-render the header and we already have the refs for updateVisibility, so...
            cat.ref.current.disabled = !cat.enabled;
        }
        this.setState({ filter });
        // Header underlines need to be updated, but updating requires knowing
        // where the categories are, so we wait for a tick.
        setTimeout(this.updateVisibility, 0);
    }

    onHoverEmoji(emoji) {
        this.setState({
            previewEmoji: emoji,
        });
    }

    onHoverEmojiEnd(emoji) {
        this.setState({
            previewEmoji: null,
        });
    }

    onClickEmoji(emoji) {
        if (this.props.onChoose(emoji.unicode) !== false) {
            recent.add(emoji.unicode);
        }
    }

    _categoryHeightForEmojiCount(count) {
        if (count === 0) {
            return 0;
        }
        return CATEGORY_HEADER_HEIGHT + (Math.ceil(count / EMOJIS_PER_ROW) * EMOJI_HEIGHT);
    }

    render() {
        const Header = sdk.getComponent("emojipicker.Header");
        const Search = sdk.getComponent("emojipicker.Search");
        const Category = sdk.getComponent("emojipicker.Category");
        const Preview = sdk.getComponent("emojipicker.Preview");
        const QuickReactions = sdk.getComponent("emojipicker.QuickReactions");
        let heightBefore = 0;
        return (

                <Search query={this.state.filter} onChange={this.onChangeFilter} />

                    {this.categories.map(category => {
                        const emojis = this.memoizedDataByCategory[category.id];
                        const categoryElement = (<Category key={category.id} id={category.id} name={category.name}
                            heightBefore={heightBefore} viewportHeight={this.state.viewportHeight}
                            scrollTop={this.state.scrollTop} emojis={emojis} onClick={this.onClickEmoji}
                            onMouseEnter={this.onHoverEmoji} onMouseLeave={this.onHoverEmojiEnd}
                            selectedEmojis={this.props.selectedEmojis} />);
                        const height = this._categoryHeightForEmojiCount(emojis.length);
                        heightBefore += height;
                        return categoryElement;
                    })}

                {this.state.previewEmoji || !this.props.showQuickReactions
                    ? <Preview emoji={this.state.previewEmoji} />
                    : <QuickReactions onClick={this.onClickEmoji} selectedEmojis={this.props.selectedEmojis} /> }

        );
    }
}

export default EmojiPicker;
<commit_msg>Deduplicate recent emoji<commit_after>/*
Copyright 2019 Tulir Asokan <tulir@maunium.net>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';
import PropTypes from 'prop-types';

import sdk from '../../../index';
import { _t } from '../../../languageHandler';

import * as recent from './recent';
import {DATA_BY_CATEGORY, getEmojiFromUnicode} from "../../../emoji";

export const CATEGORY_HEADER_HEIGHT = 22;
export const EMOJI_HEIGHT = 37;
export const EMOJIS_PER_ROW = 8;

class EmojiPicker extends React.Component {
    static propTypes = {
        onChoose: PropTypes.func.isRequired,
        selectedEmojis: PropTypes.instanceOf(Set),
        showQuickReactions: PropTypes.bool,
    };

    constructor(props) {
        super(props);

        this.state = {
            filter: "",
            previewEmoji: null,
            scrollTop: 0,
            // initial estimation of height, dialog is hardcoded to 450px height.
            // should be enough to never have blank rows of emojis as
            // 3 rows of overflow are also rendered. The actual value is updated on scroll.
            viewportHeight: 280,
        };

        // Convert recent emoji characters to emoji data, removing unknowns and duplicates
        this.recentlyUsed = Array.from(new Set(recent.get()
            .map(unicode => getEmojiFromUnicode(unicode))
            .filter(data => !!data)));
        this.memoizedDataByCategory = {
            recent: this.recentlyUsed,
            ...DATA_BY_CATEGORY,
        };

        this.categories = [{
            id: "recent",
            name: _t("Frequently Used"),
            enabled: this.recentlyUsed.length > 0,
            visible: this.recentlyUsed.length > 0,
            ref: React.createRef(),
        }, {
            id: "people",
            name: _t("Smileys & People"),
            enabled: true,
            visible: true,
            ref: React.createRef(),
        }, {
            id: "nature",
            name: _t("Animals & Nature"),
            enabled: true,
            visible: false,
            ref: React.createRef(),
        }, {
            id: "foods",
            name: _t("Food & Drink"),
            enabled: true,
            visible: false,
            ref: React.createRef(),
        }, {
            id: "activity",
            name: _t("Activities"),
            enabled: true,
            visible: false,
            ref: React.createRef(),
        }, {
            id: "places",
            name: _t("Travel & Places"),
            enabled: true,
            visible: false,
            ref: React.createRef(),
        }, {
            id: "objects",
            name: _t("Objects"),
            enabled: true,
            visible: false,
            ref: React.createRef(),
        }, {
            id: "symbols",
            name: _t("Symbols"),
            enabled: true,
            visible: false,
            ref: React.createRef(),
        }, {
            id: "flags",
            name: _t("Flags"),
            enabled: true,
            visible: false,
            ref: React.createRef(),
        }];

        this.bodyRef = React.createRef();

        this.onChangeFilter = this.onChangeFilter.bind(this);
        this.onHoverEmoji = this.onHoverEmoji.bind(this);
        this.onHoverEmojiEnd = this.onHoverEmojiEnd.bind(this);
        this.onClickEmoji = this.onClickEmoji.bind(this);
        this.scrollToCategory = this.scrollToCategory.bind(this);
        this.updateVisibility = this.updateVisibility.bind(this);
    }

    onScroll = () => {
        const body = this.bodyRef.current;
        this.setState({
            scrollTop: body.scrollTop,
            viewportHeight: body.clientHeight,
        });
        this.updateVisibility();
    };

    updateVisibility() {
        const body = this.bodyRef.current;
        const rect = body.getBoundingClientRect();
        for (const cat of this.categories) {
            const elem = body.querySelector(`[data-category-id="${cat.id}"]`);
            if (!elem) {
                cat.visible = false;
                cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible");
                continue;
            }
            const elemRect = elem.getBoundingClientRect();
            const y = elemRect.y - rect.y;
            const yEnd = elemRect.y + elemRect.height - rect.y;
            cat.visible = y < rect.height && yEnd > 0;
            // We update this here instead of through React to avoid re-render on scroll.
            if (cat.visible) {
                cat.ref.current.classList.add("mx_EmojiPicker_anchor_visible");
            } else {
                cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible");
            }
        }
    }

    scrollToCategory(category) {
        this.bodyRef.current.querySelector(`[data-category-id="${category}"]`).scrollIntoView();
    }

    onChangeFilter(filter) {
        filter = filter.toLowerCase(); // filter is case insensitive stored lower-case
        for (const cat of this.categories) {
            let emojis;
            // If the new filter string includes the old filter string, we don't have to re-filter the whole dataset.
            if (filter.includes(this.state.filter)) {
                emojis = this.memoizedDataByCategory[cat.id];
            } else {
                emojis = cat.id === "recent" ? this.recentlyUsed : DATA_BY_CATEGORY[cat.id];
            }
            emojis = emojis.filter(emoji => emoji.filterString.includes(filter));
            this.memoizedDataByCategory[cat.id] = emojis;
            cat.enabled = emojis.length > 0;
            // The setState below doesn't re-render the header and we already have the refs for updateVisibility, so...
            cat.ref.current.disabled = !cat.enabled;
        }
        this.setState({ filter });
        // Header underlines need to be updated, but updating requires knowing
        // where the categories are, so we wait for a tick.
        setTimeout(this.updateVisibility, 0);
    }

    onHoverEmoji(emoji) {
        this.setState({
            previewEmoji: emoji,
        });
    }

    onHoverEmojiEnd(emoji) {
        this.setState({
            previewEmoji: null,
        });
    }

    onClickEmoji(emoji) {
        if (this.props.onChoose(emoji.unicode) !== false) {
            recent.add(emoji.unicode);
        }
    }

    _categoryHeightForEmojiCount(count) {
        if (count === 0) {
            return 0;
        }
        return CATEGORY_HEADER_HEIGHT + (Math.ceil(count / EMOJIS_PER_ROW) * EMOJI_HEIGHT);
    }

    render() {
        const Header = sdk.getComponent("emojipicker.Header");
        const Search = sdk.getComponent("emojipicker.Search");
        const Category = sdk.getComponent("emojipicker.Category");
        const Preview = sdk.getComponent("emojipicker.Preview");
        const QuickReactions = sdk.getComponent("emojipicker.QuickReactions");
        let heightBefore = 0;
        return (

                <Search query={this.state.filter} onChange={this.onChangeFilter} />

                    {this.categories.map(category => {
                        const emojis = this.memoizedDataByCategory[category.id];
                        const categoryElement = (<Category key={category.id} id={category.id} name={category.name}
                            heightBefore={heightBefore} viewportHeight={this.state.viewportHeight}
                            scrollTop={this.state.scrollTop} emojis={emojis} onClick={this.onClickEmoji}
                            onMouseEnter={this.onHoverEmoji} onMouseLeave={this.onHoverEmojiEnd}
                            selectedEmojis={this.props.selectedEmojis} />);
                        const height = this._categoryHeightForEmojiCount(emojis.length);
                        heightBefore += height;
                        return categoryElement;
                    })}

                {this.state.previewEmoji || !this.props.showQuickReactions
                    ? <Preview emoji={this.state.previewEmoji} />
                    : <QuickReactions onClick={this.onClickEmoji} selectedEmojis={this.props.selectedEmojis} /> }

        );
    }
}

export default EmojiPicker;
<|endoftext|>


<commit_before>// A template handling library that brings in all Handlebars templates and partials and adds functions for more easily using them.
//
// Requires Handlebars.js and Pagedown (for markdown rendering)

(function ($) {
    "use strict";
    var templates = fluid.registerNamespace("ctr.components.templates");
    templates.compiled = {};

    templates.mdHelper = function(options) {
        if (Markdown && Markdown.getSanitizingConverter) {
            var converter = Markdown.getSanitizingConverter();
            // Double all single carriage returns so that they result in new paragraphs, at least for now
            converter.hooks.chain("preConversion", function (text) { return text.replace(/[\r\n]+/g, "\n\n"); });
            return converter.makeHtml(options.fn(this));
        }
        else {
            console.log("Pagedown or one of its dependencies is not available, so markdown will be passed on without any changes.");
        }

        // If we can't evolve the output, we just pass it through.
        return options.fn(this);
    };

    Handlebars.registerHelper('md', templates.mdHelper);

    templates.render = function(key,context) {
        // If a template exists, load that.  Otherwise, try to load the partial.
        var element = $("#partial-" + key).length ? $("#partial-" + key) : $("#template-" + key);

        // Cache each compiled template the first time we use it...
        if (templates.compiled[key]) {
            return templates.compiled[key](context);
        }
        else {
            if (!element || !element.html()) {
                console.log("Template '" + key + "' does not have any content. Skipping");
                return;
            }

            var template = Handlebars.compile(element.html());
            templates.compiled[key] = template;
            return template(context);
        }
    };

    templates.replaceWith = function(element,key,context) {
        element.html(templates.render(key,context));
    };

    templates.appendTo = function(element,key,context) {
        element.append(templates.render(key,context));
    };

    templates.prependTo = function (element,key,context) {
        element.prepend(templates.render(key,context));
    };

    templates.appendToBody = function (data, textStatus, jqXHR) {
        // TODO:  Replace this with a {that} reference
        $("body").append(data);

        // load all partials so that we can use them in context
        $("[id^=partial-]").each(function(index, element) {
            var id = element.id;
            var key = id.substring(id.indexOf("-")+1);
            Handlebars.registerPartial(key,$("#" + id).html());
        });
    };

    templates.loadTemplates = function(callback){
        var settings = {
            url: "/hbs",
            success: templates.appendToBody
        };
        if (callback) {
            $.ajax(settings).then(callback);
        }
        else {
            $.ajax(settings);
        }
    };
})(jQuery);

<commit_msg>Exposed before() and after() methods to give additional options when inserting template renders into an existing DOM.<commit_after>// A template handling library that brings in all Handlebars templates and partials and adds functions for more easily using them.
//
// Requires Handlebars.js and Pagedown (for markdown rendering)

(function ($) {
    "use strict";
    var templates = fluid.registerNamespace("ctr.components.templates");
    templates.compiled = {};

    templates.mdHelper = function(options) {
        if (Markdown && Markdown.getSanitizingConverter) {
            var converter = Markdown.getSanitizingConverter();
            // Double all single carriage returns so that they result in new paragraphs, at least for now
            converter.hooks.chain("preConversion", function (text) { return text.replace(/[\r\n]+/g, "\n\n"); });
            return converter.makeHtml(options.fn(this));
        }
        else {
            console.log("Pagedown or one of its dependencies is not available, so markdown will be passed on without any changes.");
        }

        // If we can't evolve the output, we just pass it through.
        return options.fn(this);
    };

    Handlebars.registerHelper('md', templates.mdHelper);

    templates.render = function(key,context) {
        // If a template exists, load that.  Otherwise, try to load the partial.
        var element = $("#partial-" + key).length ? $("#partial-" + key) : $("#template-" + key);

        // Cache each compiled template the first time we use it...
        if (templates.compiled[key]) {
            return templates.compiled[key](context);
        }
        else {
            if (!element || !element.html()) {
                console.log("Template '" + key + "' does not have any content. Skipping");
                return;
            }

            var template = Handlebars.compile(element.html());
            templates.compiled[key] = template;
            return template(context);
        }
    };

    templates.replaceWith = function(element,key,context) {
        element.html(templates.render(key,context));
    };

    templates.appendTo = function(element,key,context) {
        element.append(templates.render(key,context));
    };

    templates.prependTo = function (element,key,context) {
        element.prepend(templates.render(key,context));
    };

    templates.before = function (element,key,context) {
        element.before(templates.render(key,context));
    };

    templates.after = function (element,key,context) {
        element.after(templates.render(key,context));
    };

    templates.appendToBody = function (data, textStatus, jqXHR) {
        // TODO:  Replace this with a {that} reference
        $("body").append(data);

        // load all partials so that we can use them in context
        $("[id^=partial-]").each(function(index, element) {
            var id = element.id;
            var key = id.substring(id.indexOf("-")+1);
            Handlebars.registerPartial(key,$("#" + id).html());
        });
    };

    templates.loadTemplates = function(callback){
        var settings = {
            url: "/hbs",
            success: templates.appendToBody
        };
        if (callback) {
            $.ajax(settings).then(callback);
        }
        else {
            $.ajax(settings);
        }
    };
})(jQuery);

<|endoftext|>


<commit_before>/*
Copyright 2018 New Vector Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';

import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';

export default class KeyBackupPanel extends React.PureComponent {
    constructor(props) {
        super(props);

        this._startNewBackup = this._startNewBackup.bind(this);
        this._deleteBackup = this._deleteBackup.bind(this);
        this._onKeyBackupSessionsRemaining =
            this._onKeyBackupSessionsRemaining.bind(this);
        this._onKeyBackupStatus = this._onKeyBackupStatus.bind(this);
        this._restoreBackup = this._restoreBackup.bind(this);

        this._unmounted = false;
        this.state = {
            loading: true,
            error: null,
            backupInfo: null,
            sessionsRemaining: 0,
        };
    }

    componentWillMount() {
        this._checkKeyBackupStatus();

        MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatus);
        MatrixClientPeg.get().on(
            'crypto.keyBackupSessionsRemaining',
            this._onKeyBackupSessionsRemaining,
        );
    }

    componentWillUnmount() {
        this._unmounted = true;

        if (MatrixClientPeg.get()) {
            MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatus);
            MatrixClientPeg.get().removeListener(
                'crypto.keyBackupSessionsRemaining',
                this._onKeyBackupSessionsRemaining,
            );
        }
    }

    _onKeyBackupSessionsRemaining(sessionsRemaining) {
        this.setState({
            sessionsRemaining,
        });
    }

    _onKeyBackupStatus() {
        // This just loads the current backup status rather than forcing
        // a re-check otherwise we risk causing infinite loops
        this._loadBackupStatus();
    }

    async _checkKeyBackupStatus() {
        try {
            const {backupInfo, trustInfo} = await MatrixClientPeg.get().checkKeyBackup();
            this.setState({
                backupInfo,
                backupSigStatus: trustInfo,
                error: null,
                loading: false,
            });
        } catch (e) {
            console.log("Unable to fetch check backup status", e);
            if (this._unmounted) return;
            this.setState({
                error: e,
                backupInfo: null,
                backupSigStatus: null,
                loading: false,
            });
        }
    }

    async _loadBackupStatus() {
        this.setState({loading: true});
        try {
            const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
            const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
            if (this._unmounted) return;
            this.setState({
                error: null,
                backupInfo,
                backupSigStatus,
                loading: false,
            });
        } catch (e) {
            console.log("Unable to fetch key backup status", e);
            if (this._unmounted) return;
            this.setState({
                error: e,
                backupInfo: null,
                backupSigStatus: null,
                loading: false,
            });
        }
    }

    _startNewBackup() {
        Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
            import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
            {
                onFinished: () => {
                    this._loadBackupStatus();
                },
            },
        );
    }

    _deleteBackup() {
        const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
        Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, {
            title: _t('Delete Backup'),
            description: _t(
                "Are you sure? You will lose your encrypted messages if your " +
                "keys are not backed up properly.",
            ),
            button: _t('Delete Backup'),
            danger: true,
            onFinished: (proceed) => {
                if (!proceed) return;
                this.setState({loading: true});
                MatrixClientPeg.get().deleteKeyBackupVersion(this.state.backupInfo.version).then(() => {
                    this._loadBackupStatus();
                });
            },
        });
    }

    _restoreBackup() {
        const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
        Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {
        });
    }

    render() {
        const Spinner = sdk.getComponent("elements.Spinner");
        const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
        const encryptedMessageAreEncrypted = _t(
            "Encrypted messages are secured with end-to-end encryption. " +
            "Only you and the recipient(s) have the keys to read these messages.",
        );

        if (this.state.error) {
            return (

                    {_t("Unable to load key backup status")}

            );
        } else if (this.state.loading) {
            return <Spinner />;
        } else if (this.state.backupInfo) {
            const EmojiText = sdk.getComponent('elements.EmojiText');
            let clientBackupStatus;
            let restoreButtonCaption = _t("Restore from Backup");

            if (MatrixClientPeg.get().getKeyBackupEnabled()) {
                clientBackupStatus =

{encryptedMessageAreEncrypted}

{_t("This device is backing up your keys. ")}<EmojiText>✅</EmojiText>

;
            } else {
                clientBackupStatus =

{encryptedMessageAreEncrypted}

{_t(
                        "This device is not backing up your keys.", {},
                        {b: sub => {sub}},
                    )}

{_t("Back up your keys before signing out to avoid losing them.")}

;
                restoreButtonCaption = _t("Use key backup");
            }

            let uploadStatus;
            const { sessionsRemaining } = this.state;
            if (!MatrixClientPeg.get().getKeyBackupEnabled()) {
                // No upload status to show when backup disabled.
                uploadStatus = "";
            } else if (sessionsRemaining > 0) {
                uploadStatus =

                    {_t("Backing up %(sessionsRemaining)s keys...", { sessionsRemaining })}

;
            } else {
                uploadStatus =

                    {_t("All keys backed up")}

;
            }

            let backupSigStatuses = this.state.backupSigStatus.sigs.map((sig, i) => {
                const deviceName = sig.device ? (sig.device.getDisplayName() || sig.device.deviceId) : null;
                const validity = sub =>

                        {sub}
                    ;
                const verify = sub =>

                        {sub}
                    ;
                const device = sub => {deviceName};
                let sigStatus;
                if (!sig.device) {
                    sigStatus = _t(
                        "Backup has a signature from <verify>unknown</verify> device with ID %(deviceId)s.",
                        { deviceId: sig.deviceId }, { verify },
                    );
                } else if (sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()) {
                    sigStatus = _t(
                        "Backup has a <validity>valid</validity> signature from this device",
                        {}, { validity },
                    );
                } else if (sig.valid && sig.device.isVerified()) {
                    sigStatus = _t(
                        "Backup has a <validity>valid</validity> signature from " +
                        "<verify>verified</verify> device <device></device>",
                        {}, { validity, verify, device },
                    );
                } else if (sig.valid && !sig.device.isVerified()) {
                    sigStatus = _t(
                        "Backup has a <validity>valid</validity> signature from " +
                        "<verify>unverified</verify> device <device></device>",
                        {}, { validity, verify, device },
                    );
                } else if (!sig.valid && sig.device.isVerified()) {
                    sigStatus = _t(
                        "Backup has an <validity>invalid</validity> signature from " +
                        "<verify>verified</verify> device <device></device>",
                        {}, { validity, verify, device },
                    );
                } else if (!sig.valid && !sig.device.isVerified()) {
                    sigStatus = _t(
                        "Backup has an <validity>invalid</validity> signature from " +
                        "<verify>unverified</verify> device <device></device>",
                        {}, { validity, verify, device },
                    );
                }

                return

                    {sigStatus}

;
            });
            if (this.state.backupSigStatus.sigs.length === 0) {
                backupSigStatuses = _t("Backup is not signed by any of your devices");
            }

            let trustedLocally;
            if (this.state.backupSigStatus.trusted_locally) {
                trustedLocally = _t("This backup is trusted because it has been restored on this device");
            }

            return

{clientBackupStatus}

                    {_t("Advanced")}

{_t("Backup version: ")}{this.state.backupInfo.version}

{_t("Algorithm: ")}{this.state.backupInfo.algorithm}

                    {uploadStatus}

{backupSigStatuses}

{trustedLocally}

                    <AccessibleButton kind="primary" onClick={this._restoreBackup}>
                        {restoreButtonCaption}
                    </AccessibleButton>
                    <AccessibleButton kind="danger" onClick={this._deleteBackup}>
                        { _t("Delete Backup") }
                    </AccessibleButton>

;
        } else {
            return

{_t(
                        "Your keys are not being backed up from this device.", {},
                        {b: sub => {sub}},
                    )}

{encryptedMessageAreEncrypted}

{_t("Back up your keys before signing out to avoid losing them.")}

                <AccessibleButton kind="primary" onClick={this._startNewBackup}>
                    { _t("Start using Key Backup") }
                </AccessibleButton>

;
        }
    }
}
<commit_msg>Key backup: Handle case where your onw sig is invalid<commit_after>/*
Copyright 2018 New Vector Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';

import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';

export default class KeyBackupPanel extends React.PureComponent {
    constructor(props) {
        super(props);

        this._startNewBackup = this._startNewBackup.bind(this);
        this._deleteBackup = this._deleteBackup.bind(this);
        this._onKeyBackupSessionsRemaining =
            this._onKeyBackupSessionsRemaining.bind(this);
        this._onKeyBackupStatus = this._onKeyBackupStatus.bind(this);
        this._restoreBackup = this._restoreBackup.bind(this);

        this._unmounted = false;
        this.state = {
            loading: true,
            error: null,
            backupInfo: null,
            sessionsRemaining: 0,
        };
    }

    componentWillMount() {
        this._checkKeyBackupStatus();

        MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatus);
        MatrixClientPeg.get().on(
            'crypto.keyBackupSessionsRemaining',
            this._onKeyBackupSessionsRemaining,
        );
    }

    componentWillUnmount() {
        this._unmounted = true;

        if (MatrixClientPeg.get()) {
            MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatus);
            MatrixClientPeg.get().removeListener(
                'crypto.keyBackupSessionsRemaining',
                this._onKeyBackupSessionsRemaining,
            );
        }
    }

    _onKeyBackupSessionsRemaining(sessionsRemaining) {
        this.setState({
            sessionsRemaining,
        });
    }

    _onKeyBackupStatus() {
        // This just loads the current backup status rather than forcing
        // a re-check otherwise we risk causing infinite loops
        this._loadBackupStatus();
    }

    async _checkKeyBackupStatus() {
        try {
            const {backupInfo, trustInfo} = await MatrixClientPeg.get().checkKeyBackup();
            this.setState({
                backupInfo,
                backupSigStatus: trustInfo,
                error: null,
                loading: false,
            });
        } catch (e) {
            console.log("Unable to fetch check backup status", e);
            if (this._unmounted) return;
            this.setState({
                error: e,
                backupInfo: null,
                backupSigStatus: null,
                loading: false,
            });
        }
    }

    async _loadBackupStatus() {
        this.setState({loading: true});
        try {
            const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
            const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
            if (this._unmounted) return;
            this.setState({
                error: null,
                backupInfo,
                backupSigStatus,
                loading: false,
            });
        } catch (e) {
            console.log("Unable to fetch key backup status", e);
            if (this._unmounted) return;
            this.setState({
                error: e,
                backupInfo: null,
                backupSigStatus: null,
                loading: false,
            });
        }
    }

    _startNewBackup() {
        Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
            import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
            {
                onFinished: () => {
                    this._loadBackupStatus();
                },
            },
        );
    }

    _deleteBackup() {
        const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
        Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, {
            title: _t('Delete Backup'),
            description: _t(
                "Are you sure? You will lose your encrypted messages if your " +
                "keys are not backed up properly.",
            ),
            button: _t('Delete Backup'),
            danger: true,
            onFinished: (proceed) => {
                if (!proceed) return;
                this.setState({loading: true});
                MatrixClientPeg.get().deleteKeyBackupVersion(this.state.backupInfo.version).then(() => {
                    this._loadBackupStatus();
                });
            },
        });
    }

    _restoreBackup() {
        const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
        Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {
        });
    }

    render() {
        const Spinner = sdk.getComponent("elements.Spinner");
        const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
        const encryptedMessageAreEncrypted = _t(
            "Encrypted messages are secured with end-to-end encryption. " +
            "Only you and the recipient(s) have the keys to read these messages.",
        );

        if (this.state.error) {
            return (

                    {_t("Unable to load key backup status")}

            );
        } else if (this.state.loading) {
            return <Spinner />;
        } else if (this.state.backupInfo) {
            const EmojiText = sdk.getComponent('elements.EmojiText');
            let clientBackupStatus;
            let restoreButtonCaption = _t("Restore from Backup");

            if (MatrixClientPeg.get().getKeyBackupEnabled()) {
                clientBackupStatus =

{encryptedMessageAreEncrypted}

{_t("This device is backing up your keys. ")}<EmojiText>✅</EmojiText>

;
            } else {
                clientBackupStatus =

{encryptedMessageAreEncrypted}

{_t(
                        "This device is not backing up your keys.", {},
                        {b: sub => {sub}},
                    )}

{_t("Back up your keys before signing out to avoid losing them.")}

;
                restoreButtonCaption = _t("Use key backup");
            }

            let uploadStatus;
            const { sessionsRemaining } = this.state;
            if (!MatrixClientPeg.get().getKeyBackupEnabled()) {
                // No upload status to show when backup disabled.
                uploadStatus = "";
            } else if (sessionsRemaining > 0) {
                uploadStatus =

                    {_t("Backing up %(sessionsRemaining)s keys...", { sessionsRemaining })}

;
            } else {
                uploadStatus =

                    {_t("All keys backed up")}

;
            }

            let backupSigStatuses = this.state.backupSigStatus.sigs.map((sig, i) => {
                const deviceName = sig.device ? (sig.device.getDisplayName() || sig.device.deviceId) : null;
                const validity = sub =>

                        {sub}
                    ;
                const verify = sub =>

                        {sub}
                    ;
                const device = sub => {deviceName};
                let sigStatus;
                if (!sig.device) {
                    sigStatus = _t(
                        "Backup has a signature from <verify>unknown</verify> device with ID %(deviceId)s.",
                        { deviceId: sig.deviceId }, { verify },
                    );
                } else if (sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key() && sig.valid) {
                    sigStatus = _t(
                        "Backup has a <validity>valid</validity> signature from this device",
                        {}, { validity },
                    );
                } else if (sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key() && !sig.valid) {
                    // it can happen...
                    sigStatus = _t(
                        "Backup has an <validity>invalid</validity> signature from this device",
                        {}, { validity },
                    );
                } else if (sig.valid && sig.device.isVerified()) {
                    sigStatus = _t(
                        "Backup has a <validity>valid</validity> signature from " +
                        "<verify>verified</verify> device <device></device>",
                        {}, { validity, verify, device },
                    );
                } else if (sig.valid && !sig.device.isVerified()) {
                    sigStatus = _t(
                        "Backup has a <validity>valid</validity> signature from " +
                        "<verify>unverified</verify> device <device></device>",
                        {}, { validity, verify, device },
                    );
                } else if (!sig.valid && sig.device.isVerified()) {
                    sigStatus = _t(
                        "Backup has an <validity>invalid</validity> signature from " +
                        "<verify>verified</verify> device <device></device>",
                        {}, { validity, verify, device },
                    );
                } else if (!sig.valid && !sig.device.isVerified()) {
                    sigStatus = _t(
                        "Backup has an <validity>invalid</validity> signature from " +
                        "<verify>unverified</verify> device <device></device>",
                        {}, { validity, verify, device },
                    );
                }

                return

                    {sigStatus}

;
            });
            if (this.state.backupSigStatus.sigs.length === 0) {
                backupSigStatuses = _t("Backup is not signed by any of your devices");
            }

            let trustedLocally;
            if (this.state.backupSigStatus.trusted_locally) {
                trustedLocally = _t("This backup is trusted because it has been restored on this device");
            }

            return

{clientBackupStatus}

                    {_t("Advanced")}

{_t("Backup version: ")}{this.state.backupInfo.version}

{_t("Algorithm: ")}{this.state.backupInfo.algorithm}

                    {uploadStatus}

{backupSigStatuses}

{trustedLocally}

                    <AccessibleButton kind="primary" onClick={this._restoreBackup}>
                        {restoreButtonCaption}
                    </AccessibleButton>
                    <AccessibleButton kind="danger" onClick={this._deleteBackup}>
                        { _t("Delete Backup") }
                    </AccessibleButton>

;
        } else {
            return

{_t(
                        "Your keys are not being backed up from this device.", {},
                        {b: sub => {sub}},
                    )}

{encryptedMessageAreEncrypted}

{_t("Back up your keys before signing out to avoid losing them.")}

                <AccessibleButton kind="primary" onClick={this._startNewBackup}>
                    { _t("Start using Key Backup") }
                </AccessibleButton>

;
        }
    }
}
<|endoftext|>


<commit_before><commit_msg>Update project clipboard.js to v1.4.0<commit_after><|endoftext|>


<commit_before>let Cc = Components.classes;
let Ci = Components.interfaces;

let prompts = Cc['@mozilla.org/embedcomp/prompt-service;1'].getService(Ci.nsIPromptService);
let adban = Cc['@ad-ban.appspot.com/adban;1'].getService().wrappedJSObject;

let $ = function(id) {
  return document.getElementById(id);
};

let _ = function(id, params) {
  const adban_strings = $('adban-strings');
  if (params) {
    return adban_strings.getFormattedString(id, params);
  }
  return adban_strings.getString(id);
};

let alert_states = {};

let conditionalAlert = function(alert_name, msg) {
  let alert_state = alert_states[alert_name];
  if (!alert_state) {
    alert_state = alert_states[alert_name] = { value: false };
  }
  if (!alert_state.value) {
    prompts.alertCheck(window, 'AdBan', msg, _('dont-show-this-message-again'), alert_state);
  }
};

let openTab = function(url) {
  // use this hack, otherwise firefox 3.6 can skip the tab
  // if another tab is immediately opened after this tab.
  const open_tab_callback = function() {
    gBrowser.selectedTab = gBrowser.addTab(url);
  }
  setTimeout(open_tab_callback, 10);
};

let firstRun = function() {
  // add adban button to the main toolbar.
  const nav_bar = $('nav-bar');
  const current_set = nav_bar.currentSet;
  if (current_set.indexOf('adban-button') == -1) {
    // nav_bar.insertItem() doesn't work as expected.
    // the following code snippet was borrowed from
    // https://developer.mozilla.org/en/Code_snippets/Toolbar#Adding_button_by_default
    current_set += ',adban-button';
    nav_bar.currentSet = current_set;
    nav_bar.setAttribute('currentset', current_set);
    document.persist(nav_bar.id, 'currentset');
  }

  // open the help page for the extension.
  openTab(adban.FIRST_RUN_URL);
};

let stateToggle = function(from, to) {
  to.setAttribute('disabled', 'true');
  from.removeAttribute('disabled');
};

let onStateChange = function(is_active) {
  const cmd_adban_stop = $('cmd-adban-stop');
  const cmd_adban_start = $('cmd-adban-start');
  const adban_button = $('adban-button');
  if (is_active) {
    stateToggle(cmd_adban_stop, cmd_adban_start);
    adban_button.label = 'AdBan: ' + _('on');
  }
  else {
    stateToggle(cmd_adban_start, cmd_adban_stop);
    adban_button.label = 'AdBan: ' + _('off');
  }
};

let cmdStop = function() {
  adban.stop();
  conditionalAlert('adban-stopped', _('adban-stopped'));
};

let cmdStart = function() {
  adban.start();
};

let cmdComplaint = function() {
  const complaint_callback = function(site_url, comment) {
    const success_callback = function() {
      conditionalAlert('complaint-sent', _('complaint-sent', [site_url]));
    };
    adban.sendUrlComplaint(site_url, comment, success_callback);
  };
  const initial_site_url = gBrowser.currentURI.spec;
  // const initial_site_url = $('urlbar').value;
  const complaint_window = window.openDialog('chrome://adban/content/complaint-report.xul',
      'adban-complaint-window', '', complaint_callback, initial_site_url);
  complaint_window.focus();
};

let cmdHelp = function() {
  openTab(adban.HELP_URL);
};

let state_listener_id;

let init = function() {
  const EXTENSION_ID = 'adban@ad-ban.appspot.com';
  let extension;
  if (Application.extensions) {
    // Firefox 3.6
    extension = Application.extensions.get(EXTENSION_ID);
  }
  else {
    // Firefox 4+
    const extensions_getter = function(extensions) {
      extension = extensions.get(EXTENSION_ID);
    };
    Application.getExtensions(extensions_getter);
  }
  if (extension.firstRun) {
    dump('the AdBan first run\n');
    firstRun();
  }

  $('cmd-adban-stop').addEventListener('command', cmdStop, false);
  $('cmd-adban-start').addEventListener('command', cmdStart, false);
  $('cmd-adban-complaint').addEventListener('command', cmdComplaint, false);
  $('cmd-adban-help').addEventListener('command', cmdHelp, false);

  // DOMFrameContentLoaded doesn't work as expected,
  // while DOMContentLoaded catches iframes and frames.
  // see https://developer.mozilla.org/en/Gecko-Specific_DOM_Events .
  gBrowser.addEventListener('DOMContentLoaded', adban, true);
  state_listener_id = adban.subscribeToStateChange(onStateChange);

  window.addEventListener('unload', shutdown, false);
};

let shutdown = function() {
  adban.unsubscribeFromStateChange(state_listener_id);
  gBrowser.removeEventListener('DOMContentLoaded', adban, true);

  $('cmd-adban-stop').removeEventListener('command', cmdStop, false);
  $('cmd-adban-start').removeEventListener('command', cmdStart, false);
  $('cmd-adban-complaint').removeEventListener('command', cmdComplaint, false);
  $('cmd-adban-help').removeEventListener('command', cmdHelp, false);

  window.removeEventListener('load', init, false);
  window.removeEventListener('unload', shutdown, false);
};

window.addEventListener('load', init, false);

<commit_msg>updated first run check in order to be compatible with Firefox4<commit_after>let ADBAN_EXTENSION_ID = 'adban@ad-ban.appspot.com';

let Cc = Components.classes;
let Ci = Components.interfaces;

let prompts = Cc['@mozilla.org/embedcomp/prompt-service;1'].getService(Ci.nsIPromptService);
let adban = Cc['@ad-ban.appspot.com/adban;1'].getService().wrappedJSObject;

let $ = function(id) {
  return document.getElementById(id);
};

let _ = function(id, params) {
  const adban_strings = $('adban-strings');
  if (params) {
    return adban_strings.getFormattedString(id, params);
  }
  return adban_strings.getString(id);
};

let alert_states = {};

let conditionalAlert = function(alert_name, msg) {
  let alert_state = alert_states[alert_name];
  if (!alert_state) {
    alert_state = alert_states[alert_name] = { value: false };
  }
  if (!alert_state.value) {
    prompts.alertCheck(window, 'AdBan', msg, _('dont-show-this-message-again'), alert_state);
  }
};

let openTab = function(url) {
  // use this hack, otherwise firefox 3.6 can skip the tab
  // if another tab is immediately opened after this tab.
  const open_tab_callback = function() {
    gBrowser.selectedTab = gBrowser.addTab(url);
  }
  setTimeout(open_tab_callback, 10);
};

let firstRun = function() {
  // add adban button to the main toolbar.
  const nav_bar = $('nav-bar');
  const current_set = nav_bar.currentSet;
  if (current_set.indexOf('adban-button') == -1) {
    // nav_bar.insertItem() doesn't work as expected.
    // the following code snippet was borrowed from
    // https://developer.mozilla.org/en/Code_snippets/Toolbar#Adding_button_by_default
    current_set += ',adban-button';
    nav_bar.currentSet = current_set;
    nav_bar.setAttribute('currentset', current_set);
    document.persist(nav_bar.id, 'currentset');
  }

  // open the help page for the extension.
  openTab(adban.FIRST_RUN_URL);
};

let verifyFirstRun = function() {
  const extensions_getter_callback = function(extensions) {
    const extension = extensions.get(ADBAN_EXTENSION_ID);
    if (extension.firstRun) {
      dump('the AdBan first run\n');
      firstRun();
    }
  };
  if (Application.extensions) {
    // Firefox 3.6
    extensions_getter_callback(Application.extensions);
  }
  else {
    // Firefox 4+
    Application.getExtensions(extensions_getter_callback);
  }
};

let stateToggle = function(from, to) {
  to.setAttribute('disabled', 'true');
  from.removeAttribute('disabled');
};

let onStateChange = function(is_active) {
  const cmd_adban_stop = $('cmd-adban-stop');
  const cmd_adban_start = $('cmd-adban-start');
  const adban_button = $('adban-button');
  if (is_active) {
    stateToggle(cmd_adban_stop, cmd_adban_start);
    adban_button.label = 'AdBan: ' + _('on');
  }
  else {
    stateToggle(cmd_adban_start, cmd_adban_stop);
    adban_button.label = 'AdBan: ' + _('off');
  }
};

let cmdStop = function() {
  adban.stop();
  conditionalAlert('adban-stopped', _('adban-stopped'));
};

let cmdStart = function() {
  adban.start();
};

let cmdComplaint = function() {
  const complaint_callback = function(site_url, comment) {
    const success_callback = function() {
      conditionalAlert('complaint-sent', _('complaint-sent', [site_url]));
    };
    adban.sendUrlComplaint(site_url, comment, success_callback);
  };
  const initial_site_url = gBrowser.currentURI.spec;
  // const initial_site_url = $('urlbar').value;
  const complaint_window = window.openDialog('chrome://adban/content/complaint-report.xul',
      'adban-complaint-window', '', complaint_callback, initial_site_url);
  complaint_window.focus();
};

let cmdHelp = function() {
  openTab(adban.HELP_URL);
};

let state_listener_id;

let init = function() {
  verifyFirstRun();

  $('cmd-adban-stop').addEventListener('command', cmdStop, false);
  $('cmd-adban-start').addEventListener('command', cmdStart, false);
  $('cmd-adban-complaint').addEventListener('command', cmdComplaint, false);
  $('cmd-adban-help').addEventListener('command', cmdHelp, false);

  // DOMFrameContentLoaded doesn't work as expected,
  // while DOMContentLoaded catches iframes and frames.
  // see https://developer.mozilla.org/en/Gecko-Specific_DOM_Events .
  gBrowser.addEventListener('DOMContentLoaded', adban, true);
  state_listener_id = adban.subscribeToStateChange(onStateChange);

  window.addEventListener('unload', shutdown, false);
};

let shutdown = function() {
  adban.unsubscribeFromStateChange(state_listener_id);
  gBrowser.removeEventListener('DOMContentLoaded', adban, true);

  $('cmd-adban-stop').removeEventListener('command', cmdStop, false);
  $('cmd-adban-start').removeEventListener('command', cmdStart, false);
  $('cmd-adban-complaint').removeEventListener('command', cmdComplaint, false);
  $('cmd-adban-help').removeEventListener('command', cmdHelp, false);

  window.removeEventListener('load', init, false);
  window.removeEventListener('unload', shutdown, false);
};

window.addEventListener('load', init, false);

<|endoftext|>


<commit_before>(function (window, $, undefined) {

    function DeferredSQLTransaction(tx) {
        this.transaction = tx;
    };

    DeferredSQLTransaction.prototype.executeSql = function executeSql(sqlStatement, sqlArguments) {
        var self = this,
            d = $.Deferred(),
            callback = function (tx, resultSet) {
                d.resolve(self, resultSet);
            },
            errorCallback = function (tx, error) {
                d.reject(self, error);
                return true;
            };

        self.transaction.executeSql(sqlStatement, sqlArguments, callback, errorCallback);

        return d.promise();
    };

    function DeferredSQLDatabase(db) {
        this.database = db;
        this.version = db.version;
    }

    var openDatabaseDeferred = function openDatabase(name, version, displayName, estimatedSize) {
        var self = this,
            d = $.Deferred(),
            db = window.openDatabase(name, version, displayName, estimatedSize);

        return new DeferredSQLDatabase(db);
    };

    DeferredSQLDatabase.prototype.transaction = function transaction(callback) {
        var self = this,
            d = $.Deferred(),
            errorCallback = function (error) {
                d.reject(error);
            },
            successCallback = function () {
                d.resolve();
            },
            wrappedCallback = function (tx) {
                callback(new DeferredSQLTransaction(tx));
            };

        self.database.transaction(wrappedCallback, errorCallback, successCallback);

        return d.promise();
    };

    DeferredSQLDatabase.prototype.readTransaction = function readTransaction(callback) {
        var self = this,
            d = $.Deferred(),
            errorCallback = function (error) {
                d.reject(error);
            },
            successCallback = function (tx) {
                d.resolve(new DeferredSQLTransaction(tx));
            };

        self.database.readTransaction(callback, errorCallback, successCallback);

        return d.promise();
    };

    DeferredSQLDatabase.prototype.changeVersion = function changeVersion(oldVersion, newVersion, callback) {
        var self = this,
            d = $.Deferred(),
            errorCallback = function (error) {
                d.reject(error);
            },
            successCallback = function (tx) {
                self.version = self.database.version;

                d.resolve(new DeferredSQLTransaction(tx));
            };

        self.database.changeVersion(oldVersion, newVersion, callback, errorCallback, successCallback);

        return d.promise();
    };

    window.openDatabaseDeferred = openDatabaseDeferred;

})(window, jQuery);
<commit_msg>Added a handler for the creation callback<commit_after>(function (window, $, undefined) {

    function DeferredSQLTransaction(tx) {
        this.transaction = tx;
    };

    DeferredSQLTransaction.prototype.executeSql = function executeSql(sqlStatement, sqlArguments) {
        var self = this,
            d = $.Deferred(),
            successCallback = function (tx, resultSet) {
                d.resolve(self, resultSet);
            },
            errorCallback = function (tx, error) {
                d.reject(self, error);
                return true;
            };

        self.transaction.executeSql(sqlStatement, sqlArguments, successCallback, errorCallback);

        return d.promise();
    };

    function DeferredSQLDatabase(db) {
        this.database = db;
        this.version = db.version;
    }

    var openDatabaseDeferred = function openDatabase(name, version, displayName, estimatedSize, creationCallback) {
        var self = this,
            d = $.Deferred(),
            wrappedCreationCallback = function () {
                creationCallback(deferredDb);
            },
            db = window.openDatabase(name, version, displayName, estimatedSize, wrappedCreationCallback),
            deferredDb = new DeferredSQLDatabase(db);

        return deferredDb;
    };

    DeferredSQLDatabase.prototype.transaction = function transaction(callback) {
        var self = this,
            d = $.Deferred(),
            errorCallback = function (error) {
                d.reject(error);
            },
            successCallback = function () {
                d.resolve();
            },
            wrappedCallback = function (tx) {
                callback(new DeferredSQLTransaction(tx));
            };

        self.database.transaction(wrappedCallback, errorCallback, successCallback);

        return d.promise();
    };

    DeferredSQLDatabase.prototype.readTransaction = function readTransaction(callback) {
        var self = this,
            d = $.Deferred(),
            errorCallback = function (error) {
                d.reject(error);
            },
            successCallback = function () {
                d.resolve();
            },
            wrappedCallback = function (tx) {
                callback(new DeferredSQLTransaction(tx));
            };

        self.database.readTransaction(wrappedCallback, errorCallback, successCallback);

        return d.promise();
    };

    DeferredSQLDatabase.prototype.changeVersion = function changeVersion(oldVersion, newVersion, callback) {
        var self = this,
            d = $.Deferred(),
            errorCallback = function (error) {
                d.reject(error);
            },
            successCallback = function () {
                self.version = self.database.version;
                d.resolve();
            },
            wrappedCallback = function (tx) {
                callback(new DeferredSQLTransaction(tx));
            };

        self.database.changeVersion(oldVersion, newVersion, callback, errorCallback, successCallback);

        return d.promise();
    };

    window.openDatabaseDeferred = openDatabaseDeferred;

})(window, jQuery);
<|endoftext|>


<commit_before>import {
  updateInventory,
  addInventoryItem,
  removeInventoryItem,
  selectItem,
} from '../../../ui';
import { clamp, REALLY_BIG_NUMBER, clone } from '../../../utils';

import PineCone from '../../object/PineCone';

export default class Inventory {
  constructor(game) {
    this.game = game;

    this.money = {
      value: 0,
      max: REALLY_BIG_NUMBER,
    };

    this.items = {
      'wood-axe': {
        name: 'Wood Axe',
        value: true,
        sellable: false,
        selected: true,
      },
      bucket: {
        name: 'Bucket',
        value: false,
        sellable: false,
        selected: false,
      },
      water: {
        name: 'Water',
        value: 0,
        max: 15,
        sellable: false,
        selected: null,
      },
      log: {
        value: 0,
        max: 10,
        sellable: true,
        selected: null,
      },
      'pine-cone': {
        value: 0,
        max: 100,
        sellable: true,
        selected: false,
        place: PineCone,
      },
    };

    setupObj = setupObj.bind(this);

    setupObj(this.items);
    setupObj(this.money, 'money');

    // add items we started with to ui
    for (let itemName in this.items) {
      let value = this.items[itemName].value;

      if (value === false || value <= 0) continue;

      value = typeof value === 'boolean' ? undefined : value;

      const { sellable, selected } = this.items[itemName];

      addInventoryItem.call(this, itemName, value, sellable, selected);
    }

    this.seek = this.seek.bind(this);
  }

  get selected() {
    for (let name in this.items) {
      if (this.items[name].selected) return name;
    }
  }

  select(id) {
    if (this.items[id].selected || this.items[id].selected === null) return; // already selected or not selectable

    for (let name in this.items) {
      let item = this.items[name];

      if (!item.selected) continue;

      item.selected = false;
    }

    this.items[id].selected = true;
  }

  seek(direction) {
    const names = Object.keys(this.items).filter(function(itemName) {
      let item = this.items[itemName];

      return item.selected !== null && (item.value > 0 || item.value === true);
    }.bind(this));

    let i = names.indexOf(this.selected) + (direction === 'next' ? 1 : -1);
    i = wrap(i, names.length);

    const newName = names[i];

    this.select(newName);
  }
}

function wrap(value, length) {
  if (value < 0) return length - 1;
  if (value >= length) return 0;
  return value;
}

// set 'values' prop to read/write (clamped to max if present) and all other props to read only
function setupObj(obj, objName) {
  processObject = processObject.bind(this);

  if (objName === undefined) {
    const itemNames = Object.keys(obj);

    for (let i = 0; i < itemNames.length; i++) {
      processObject(obj[itemNames[i]], itemNames[i]);
    }
  } else {
    processObject(obj, objName);
  }

  function processObject(item, itemName) {
    for (let keyName in item) {
      if (keyName === 'value' || keyName === 'selected') { // TODO make work with selected too
        readWriteKey.call(this, item, itemName, keyName);
      } else {
        readKey(item, keyName);
      }
    }

    Object.defineProperty(item, 'isMax', { get: isMax.bind(item) });
  }
}

function isMax() {
  let maxed = false;

  const { value, max } = this;

  const type = typeof value;

  if (type === 'number') {
    maxed = value >= max;
  } else if (type === 'boolean') {
    maxed = value;
  }

  return maxed;
}

// sets up a getter-setter for 'value' prop, clamping it to obj.max
function readWriteKey(obj, itemName, keyName) {
  const privName = '_' + keyName;

  obj[privName] = obj[keyName];
  delete obj[keyName];

  Object.defineProperty(obj, keyName, {
    get: function() { return this[privName]; }.bind(obj),
    set: function(name, self, value) {
      if (typeof this[privName] !== 'number') {
        this[privName] = value;
      } else {
        this[privName] = clamp(value, 0, this.max || REALLY_BIG_NUMBER);
      }

      switch (keyName) {
        case 'value': {
          if (this[privName] <= 0 || this[privName] === false) {
            removeInventoryItem(name);

            self.select('wood-axe'); // default back to wood-axe
          } else {
            updateInventory.call(self, name, this[privName], this.sellable);
          }
          break;
        }
        case 'selected': {
          selectItem(name, this[privName]);
          break;
        }
      }
    }.bind(obj, itemName, this),
  });
}

// sets up a getter only for 'key' prop, making it read-only
function readKey(obj, keyName) {
  const oldKeyValue = obj[keyName];

  delete obj[keyName];

  Object.defineProperty(obj, keyName, {
    get: function() { return oldKeyValue; },
  });
}
<commit_msg>Remove unused item name props<commit_after>import {
  updateInventory,
  addInventoryItem,
  removeInventoryItem,
  selectItem,
} from '../../../ui';
import { clamp, REALLY_BIG_NUMBER, clone } from '../../../utils';

import PineCone from '../../object/PineCone';

export default class Inventory {
  constructor(game) {
    this.game = game;

    this.money = {
      value: 0,
      max: REALLY_BIG_NUMBER,
    };

    this.items = {
      'wood-axe': {
        value: true,
        sellable: false,
        selected: true,
      },
      bucket: {
        value: false,
        sellable: false,
        selected: false,
      },
      water: {
        value: 0,
        max: 15,
        sellable: false,
        selected: null,
      },
      log: {
        value: 0,
        max: 10,
        sellable: true,
        selected: null,
      },
      'pine-cone': {
        value: 0,
        max: 100,
        sellable: true,
        selected: false,
        place: PineCone,
      },
    };

    setupObj = setupObj.bind(this);

    setupObj(this.items);
    setupObj(this.money, 'money');

    // add items we started with to ui
    for (let itemName in this.items) {
      let value = this.items[itemName].value;

      if (value === false || value <= 0) continue;

      value = typeof value === 'boolean' ? undefined : value;

      const { sellable, selected } = this.items[itemName];

      addInventoryItem.call(this, itemName, value, sellable, selected);
    }

    this.seek = this.seek.bind(this);
  }

  get selected() {
    for (let name in this.items) {
      if (this.items[name].selected) return name;
    }
  }

  select(id) {
    if (this.items[id].selected || this.items[id].selected === null) return; // already selected or not selectable

    for (let name in this.items) {
      let item = this.items[name];

      if (!item.selected) continue;

      item.selected = false;
    }

    this.items[id].selected = true;
  }

  seek(direction) {
    const names = Object.keys(this.items).filter(function(itemName) {
      let item = this.items[itemName];

      return item.selected !== null && (item.value > 0 || item.value === true);
    }.bind(this));

    let i = names.indexOf(this.selected) + (direction === 'next' ? 1 : -1);
    i = wrap(i, names.length);

    const newName = names[i];

    this.select(newName);
  }
}

function wrap(value, length) {
  if (value < 0) return length - 1;
  if (value >= length) return 0;
  return value;
}

// set 'values' prop to read/write (clamped to max if present) and all other props to read only
function setupObj(obj, objName) {
  processObject = processObject.bind(this);

  if (objName === undefined) {
    const itemNames = Object.keys(obj);

    for (let i = 0; i < itemNames.length; i++) {
      processObject(obj[itemNames[i]], itemNames[i]);
    }
  } else {
    processObject(obj, objName);
  }

  function processObject(item, itemName) {
    for (let keyName in item) {
      if (keyName === 'value' || keyName === 'selected') { // TODO make work with selected too
        readWriteKey.call(this, item, itemName, keyName);
      } else {
        readKey(item, keyName);
      }
    }

    Object.defineProperty(item, 'isMax', { get: isMax.bind(item) });
  }
}

function isMax() {
  let maxed = false;

  const { value, max } = this;

  const type = typeof value;

  if (type === 'number') {
    maxed = value >= max;
  } else if (type === 'boolean') {
    maxed = value;
  }

  return maxed;
}

// sets up a getter-setter for 'value' prop, clamping it to obj.max
function readWriteKey(obj, itemName, keyName) {
  const privName = '_' + keyName;

  obj[privName] = obj[keyName];
  delete obj[keyName];

  Object.defineProperty(obj, keyName, {
    get: function() { return this[privName]; }.bind(obj),
    set: function(name, self, value) {
      if (typeof this[privName] !== 'number') {
        this[privName] = value;
      } else {
        this[privName] = clamp(value, 0, this.max || REALLY_BIG_NUMBER);
      }

      switch (keyName) {
        case 'value': {
          if (this[privName] <= 0 || this[privName] === false) {
            removeInventoryItem(name);

            self.select('wood-axe'); // default back to wood-axe
          } else {
            updateInventory.call(self, name, this[privName], this.sellable);
          }
          break;
        }
        case 'selected': {
          selectItem(name, this[privName]);
          break;
        }
      }
    }.bind(obj, itemName, this),
  });
}

// sets up a getter only for 'key' prop, making it read-only
function readKey(obj, keyName) {
  const oldKeyValue = obj[keyName];

  delete obj[keyName];

  Object.defineProperty(obj, keyName, {
    get: function() { return oldKeyValue; },
  });
}
<|endoftext|>