<commit_before>/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.

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 createReactClass from 'create-react-class';
import classNames from 'classnames';
import dis from '../../../dispatcher';
import MatrixClientPeg from '../../../MatrixClientPeg';
import DMRoomMap from '../../../utils/DMRoomMap';
import sdk from '../../../index';
import {ContextMenu, ContextMenuButton, toRightOf} from '../../structures/ContextMenu';
import * as RoomNotifs from '../../../RoomNotifs';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import ActiveRoomObserver from '../../../ActiveRoomObserver';
import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore from "../../../settings/SettingsStore";
import {_t} from "../../../languageHandler";

module.exports = createReactClass({
    displayName: 'RoomTile',

    propTypes: {
        onClick: PropTypes.func,

        room: PropTypes.object.isRequired,
        collapsed: PropTypes.bool.isRequired,
        unread: PropTypes.bool.isRequired,
        highlight: PropTypes.bool.isRequired,
        // If true, apply mx_RoomTile_transparent class
        transparent: PropTypes.bool,
        isInvite: PropTypes.bool.isRequired,
        incomingCall: PropTypes.object,
    },

    getDefaultProps: function() {
        return {
            isDragging: false,
        };
    },

    getInitialState: function() {
         // eslint-disable-next-line camelcase
        const joinRules = this.props.room.currentState.getStateEvents("m.room.join_rules", "");
        const joinRule = joinRules && joinRules.getContent() && joinRules.getContent().join_rule;

        return ({
            joinRule,
            hover: false,
            badgeHover: false,
            contextMenuPosition: null, // DOM bounding box, null if non-shown
            roomName: this.props.room.name,
            notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
            notificationCount: this.props.room.getUnreadNotificationCount(),
            selected: this.props.room.roomId === RoomViewStore.getRoomId(),
            statusMessage: this._getStatusMessage(),
        });
    },

    _shouldShowStatusMessage() {
        if (!SettingsStore.isFeatureEnabled("feature_custom_status")) {
            return false;
        }
        const isInvite = this.props.room.getMyMembership() === "invite";
        const isJoined = this.props.room.getMyMembership() === "join";
        const looksLikeDm = this.props.room.getInvitedAndJoinedMemberCount() === 2;
        return !isInvite && isJoined && looksLikeDm;
    },

    _getStatusMessageUser() {
        if (!MatrixClientPeg.get()) return null; // We've probably been logged out

        const selfId = MatrixClientPeg.get().getUserId();
        const otherMember = this.props.room.currentState.getMembersExcept([selfId])[0];
        if (!otherMember) {
            return null;
        }
        return otherMember.user;
    },

    _getStatusMessage() {
        const statusUser = this._getStatusMessageUser();
        if (!statusUser) {
            return "";
        }
        return statusUser._unstable_statusMessage;
    },

    onRoomName: function(room) {
        if (room !== this.props.room) return;
        this.setState({
            roomName: this.props.room.name,
        });
    },

    onJoinRule: function(ev) {
        if (!ev || !ev.event ) return;
        /* eslint-disable camelcase */
        const { event: { type, room_id } } = ev;
        if (type !== "m.room.join_rules") return;
        if (room_id !== this.props.room.roomId) return;
        if ( !event.content ) return;
        const { event: { content: { join_rule } } } = ev;
        this.setState({ joinRule: join_rule });
        /* eslint-enable camelcase */
    },

    onAccountData: function(accountDataEvent) {
        if (accountDataEvent.getType() === 'm.push_rules') {
            this.setState({
                notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
            });
        }
    },

    onAction: function(payload) {
        switch (payload.action) {
            // XXX: slight hack in order to zero the notification count when a room
            // is read. Ideally this state would be given to this via props (as we
            // do with `unread`). This is still better than forceUpdating the entire
            // RoomList when a room is read.
            case 'on_room_read':
                if (payload.roomId !== this.props.room.roomId) break;
                this.setState({
                    notificationCount: this.props.room.getUnreadNotificationCount(),
                });
                break;
            // RoomTiles are one of the few components that may show custom status and
            // also remain on screen while in Settings toggling the feature.  This ensures
            // you can clearly see the status hide and show when toggling the feature.
            case 'feature_custom_status_changed':
                this.forceUpdate();
                break;
        }
    },

    _onActiveRoomChange: function() {
        this.setState({
            selected: this.props.room.roomId === RoomViewStore.getRoomId(),
        });
    },

    componentDidMount: function() {
        const cli = MatrixClientPeg.get();
        cli.on("accountData", this.onAccountData);
        cli.on("Room.name", this.onRoomName);
        cli.on("RoomState.events", this.onJoinRule);
        ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange);
        this.dispatcherRef = dis.register(this.onAction);

        if (this._shouldShowStatusMessage()) {
            const statusUser = this._getStatusMessageUser();
            if (statusUser) {
                statusUser.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
            }
        }
    },

    componentWillUnmount: function() {
        const cli = MatrixClientPeg.get();
        if (cli) {
            MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
            MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
            cli.removeListener("RoomState.events", this.onJoinRule);
        }
        ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange);
        dis.unregister(this.dispatcherRef);

        if (this._shouldShowStatusMessage()) {
            const statusUser = this._getStatusMessageUser();
            if (statusUser) {
                statusUser.removeListener(
                    "User._unstable_statusMessage",
                    this._onStatusMessageCommitted,
                );
            }
        }
    },

    componentWillReceiveProps: function(props) {
        // XXX: This could be a lot better - this makes the assumption that
        // the notification count may have changed when the properties of
        // the room tile change.
        this.setState({
            notificationCount: this.props.room.getUnreadNotificationCount(),
        });
    },

    // Do a simple shallow comparison of props and state to avoid unnecessary
    // renders. The assumption made here is that only state and props are used
    // in rendering this component and children.
    //
    // RoomList is frequently made to forceUpdate, so this decreases number of
    // RoomTile renderings.
    shouldComponentUpdate: function(newProps, newState) {
        if (Object.keys(newProps).some((k) => newProps[k] !== this.props[k])) {
            return true;
        }
        if (Object.keys(newState).some((k) => newState[k] !== this.state[k])) {
            return true;
        }
        return false;
    },

    _onStatusMessageCommitted() {
        // The status message `User` object has observed a message change.
        this.setState({
            statusMessage: this._getStatusMessage(),
        });
    },

    onClick: function(ev) {
        if (this.props.onClick) {
            this.props.onClick(this.props.room.roomId, ev);
        }
    },

    onMouseEnter: function() {
        this.setState( { hover: true });
        this.badgeOnMouseEnter();
    },

    onMouseLeave: function() {
        this.setState( { hover: false });
        this.badgeOnMouseLeave();
    },

    badgeOnMouseEnter: function() {
        // Only allow non-guests to access the context menu
        // and only change it if it needs to change
        if (!MatrixClientPeg.get().isGuest() && !this.state.badgeHover) {
            this.setState( { badgeHover: true } );
        }
    },

    badgeOnMouseLeave: function() {
        this.setState( { badgeHover: false } );
    },

    _showContextMenu: function(boundingClientRect) {
        // Only allow non-guests to access the context menu
        if (MatrixClientPeg.get().isGuest()) return;

        const state = {
            contextMenuPosition: boundingClientRect,
        };

        // If the badge is clicked, then no longer show tooltip
        if (this.props.collapsed) {
            state.hover = false;
        }

        this.setState(state);
    },

    onContextMenuButtonClick: function(e) {
        // Prevent the RoomTile onClick event firing as well
        e.stopPropagation();
        e.preventDefault();

        this._showContextMenu(e.target.getBoundingClientRect());
    },

    onContextMenu: function(e) {
        // Prevent the native context menu
        e.preventDefault();

        this._showContextMenu({
            right: e.clientX,
            top: e.clientY,
            height: 0,
        });
    },

    closeMenu: function() {
        this.setState({
            contextMenuPosition: null,
        });
        this.props.refreshSubList();
    },

    render: function() {
        const isInvite = this.props.room.getMyMembership() === "invite";
        const notificationCount = this.props.notificationCount;
        // var highlightCount = this.props.room.getUnreadNotificationCount("highlight");

        const notifBadges = notificationCount > 0 && RoomNotifs.shouldShowNotifBadge(this.state.notifState);
        const mentionBadges = this.props.highlight && RoomNotifs.shouldShowMentionBadge(this.state.notifState);
        const badges = notifBadges || mentionBadges;

        let subtext = null;
        if (this._shouldShowStatusMessage()) {
            subtext = this.state.statusMessage;
        }

        const isMenuDisplayed = Boolean(this.state.contextMenuPosition);

        const dmUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);

        const classes = classNames({
            'mx_RoomTile': true,
            'mx_RoomTile_selected': this.state.selected,
            'mx_RoomTile_unread': this.props.unread,
            'mx_RoomTile_unreadNotify': notifBadges,
            'mx_RoomTile_highlight': mentionBadges,
            'mx_RoomTile_invited': isInvite,
            'mx_RoomTile_menuDisplayed': isMenuDisplayed,
            'mx_RoomTile_noBadges': !badges,
            'mx_RoomTile_transparent': this.props.transparent,
            'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed,
            'mx_RoomTile_isPrivate': this.state.joinRule == "invite" && !dmUserId,
        });

        const avatarClasses = classNames({
            'mx_RoomTile_avatar': true,
        });

        const badgeClasses = classNames({
            'mx_RoomTile_badge': true,
            'mx_RoomTile_badgeButton': this.state.badgeHover || isMenuDisplayed,
        });

        let name = this.state.roomName;
        if (name == undefined || name == null) name = '';
        name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon

        let badge;
        if (badges) {
            const limitedCount = FormattingUtils.formatCount(notificationCount);
            const badgeContent = notificationCount ? limitedCount : '!';
            badge =
{ badgeContent }
;
        }

        let label;
        let subtextLabel;
        let tooltip;
        if (!this.props.collapsed) {
            const nameClasses = classNames({
                'mx_RoomTile_name': true,
                'mx_RoomTile_invite': this.props.isInvite,
                'mx_RoomTile_badgeShown': badges || this.state.badgeHover || isMenuDisplayed,
            });

            subtextLabel = subtext ? { subtext } : null;
            label =
{ name }
;
        } else if (this.state.hover) {
            const Tooltip = sdk.getComponent("elements.Tooltip");
            tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />;
        }

        //var incomingCallBox;
        //if (this.props.incomingCall) {
        //    var IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
        //    incomingCallBox = <IncomingCallBox incomingCall={ this.props.incomingCall }/>;
        //}

        const AccessibleButton = sdk.getComponent('elements.AccessibleButton');

        let contextMenuButton;
        if (!MatrixClientPeg.get().isGuest()) {
            contextMenuButton = (
                <ContextMenuButton
                    className="mx_RoomTile_menuButton"
                    label={_t("Options")}
                    isExpanded={isMenuDisplayed}
                    onClick={this.onContextMenuButtonClick} />
            );
        }

        const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');

        let ariaLabel = name;

        let dmIndicator;
        let dmOnline;
        if (dmUserId) {
            dmIndicator = ;

            const { room } = this.props;
            const member = room.getMember(dmUserId);
            if (member && member.membership === "join" && room.getJoinedMemberCount() === 2) {
                const UserOnlineDot = sdk.getComponent('rooms.UserOnlineDot');
                dmOnline = <UserOnlineDot userId={dmUserId} />;
            }
        }

        // The following labels are written in such a fashion to increase screen reader efficiency (speed).
        if (notifBadges && mentionBadges && !isInvite) {
            ariaLabel += " " + _t("%(count)s unread messages including mentions.", {
                count: notificationCount,
            });
        } else if (notifBadges) {
            ariaLabel += " " + _t("%(count)s unread messages.", { count: notificationCount });
        } else if (mentionBadges && !isInvite) {
            ariaLabel += " " + _t("Unread mentions.");
        } else if (this.props.unread) {
            ariaLabel += " " + _t("Unread messages.");
        }

        let contextMenu;
        if (isMenuDisplayed) {
            const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu');
            contextMenu = (
                <ContextMenu {...toRightOf(this.state.contextMenuPosition)} onFinished={this.closeMenu}>
                    <RoomTileContextMenu room={this.props.room} onFinished={this.closeMenu} />
                </ContextMenu>
            );
        }

        return <React.Fragment>
            <AccessibleButton
                tabIndex="0"
                className={classes}
                onClick={this.onClick}
                onMouseEnter={this.onMouseEnter}
                onMouseLeave={this.onMouseLeave}
                onContextMenu={this.onContextMenu}
                aria-label={ariaLabel}
                aria-selected={this.state.selected}
                role="treeitem"
            >

                        <RoomAvatar room={this.props.room} width={24} height={24} />
                        { dmIndicator }

                        { label }
                        { subtextLabel }

                    { dmOnline }
                    { contextMenuButton }
                    { badge }

                { /* { incomingCallBox } */ }
                { tooltip }
            </AccessibleButton>

            { contextMenu }
        </React.Fragment>;
    },
});
<commit_msg>use getContent() instead<commit_after>/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.

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 createReactClass from 'create-react-class';
import classNames from 'classnames';
import dis from '../../../dispatcher';
import MatrixClientPeg from '../../../MatrixClientPeg';
import DMRoomMap from '../../../utils/DMRoomMap';
import sdk from '../../../index';
import {ContextMenu, ContextMenuButton, toRightOf} from '../../structures/ContextMenu';
import * as RoomNotifs from '../../../RoomNotifs';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import ActiveRoomObserver from '../../../ActiveRoomObserver';
import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore from "../../../settings/SettingsStore";
import {_t} from "../../../languageHandler";

module.exports = createReactClass({
    displayName: 'RoomTile',

    propTypes: {
        onClick: PropTypes.func,

        room: PropTypes.object.isRequired,
        collapsed: PropTypes.bool.isRequired,
        unread: PropTypes.bool.isRequired,
        highlight: PropTypes.bool.isRequired,
        // If true, apply mx_RoomTile_transparent class
        transparent: PropTypes.bool,
        isInvite: PropTypes.bool.isRequired,
        incomingCall: PropTypes.object,
    },

    getDefaultProps: function() {
        return {
            isDragging: false,
        };
    },

    getInitialState: function() {
         // eslint-disable-next-line camelcase
        const joinRules = this.props.room.currentState.getStateEvents("m.room.join_rules", "");
        const joinRule = joinRules && joinRules.getContent() && joinRules.getContent().join_rule;

        return ({
            joinRule,
            hover: false,
            badgeHover: false,
            contextMenuPosition: null, // DOM bounding box, null if non-shown
            roomName: this.props.room.name,
            notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
            notificationCount: this.props.room.getUnreadNotificationCount(),
            selected: this.props.room.roomId === RoomViewStore.getRoomId(),
            statusMessage: this._getStatusMessage(),
        });
    },

    _shouldShowStatusMessage() {
        if (!SettingsStore.isFeatureEnabled("feature_custom_status")) {
            return false;
        }
        const isInvite = this.props.room.getMyMembership() === "invite";
        const isJoined = this.props.room.getMyMembership() === "join";
        const looksLikeDm = this.props.room.getInvitedAndJoinedMemberCount() === 2;
        return !isInvite && isJoined && looksLikeDm;
    },

    _getStatusMessageUser() {
        if (!MatrixClientPeg.get()) return null; // We've probably been logged out

        const selfId = MatrixClientPeg.get().getUserId();
        const otherMember = this.props.room.currentState.getMembersExcept([selfId])[0];
        if (!otherMember) {
            return null;
        }
        return otherMember.user;
    },

    _getStatusMessage() {
        const statusUser = this._getStatusMessageUser();
        if (!statusUser) {
            return "";
        }
        return statusUser._unstable_statusMessage;
    },

    onRoomName: function(room) {
        if (room !== this.props.room) return;
        this.setState({
            roomName: this.props.room.name,
        });
    },

    onJoinRule: function(ev) {
        if (!ev || !ev.event ) return;
        /* eslint-disable camelcase */
        const { event: { type, room_id } } = ev;
        if (type !== "m.room.join_rules") return;
        if (room_id !== this.props.room.roomId) return;
        /* eslint-enable camelcase */
        const joinRule = event.getContent().join_rule;
        this.setState({ joinRule });
    },

    onAccountData: function(accountDataEvent) {
        if (accountDataEvent.getType() === 'm.push_rules') {
            this.setState({
                notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
            });
        }
    },

    onAction: function(payload) {
        switch (payload.action) {
            // XXX: slight hack in order to zero the notification count when a room
            // is read. Ideally this state would be given to this via props (as we
            // do with `unread`). This is still better than forceUpdating the entire
            // RoomList when a room is read.
            case 'on_room_read':
                if (payload.roomId !== this.props.room.roomId) break;
                this.setState({
                    notificationCount: this.props.room.getUnreadNotificationCount(),
                });
                break;
            // RoomTiles are one of the few components that may show custom status and
            // also remain on screen while in Settings toggling the feature.  This ensures
            // you can clearly see the status hide and show when toggling the feature.
            case 'feature_custom_status_changed':
                this.forceUpdate();
                break;
        }
    },

    _onActiveRoomChange: function() {
        this.setState({
            selected: this.props.room.roomId === RoomViewStore.getRoomId(),
        });
    },

    componentDidMount: function() {
        const cli = MatrixClientPeg.get();
        cli.on("accountData", this.onAccountData);
        cli.on("Room.name", this.onRoomName);
        cli.on("RoomState.events", this.onJoinRule);
        ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange);
        this.dispatcherRef = dis.register(this.onAction);

        if (this._shouldShowStatusMessage()) {
            const statusUser = this._getStatusMessageUser();
            if (statusUser) {
                statusUser.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
            }
        }
    },

    componentWillUnmount: function() {
        const cli = MatrixClientPeg.get();
        if (cli) {
            MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
            MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
            cli.removeListener("RoomState.events", this.onJoinRule);
        }
        ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange);
        dis.unregister(this.dispatcherRef);

        if (this._shouldShowStatusMessage()) {
            const statusUser = this._getStatusMessageUser();
            if (statusUser) {
                statusUser.removeListener(
                    "User._unstable_statusMessage",
                    this._onStatusMessageCommitted,
                );
            }
        }
    },

    componentWillReceiveProps: function(props) {
        // XXX: This could be a lot better - this makes the assumption that
        // the notification count may have changed when the properties of
        // the room tile change.
        this.setState({
            notificationCount: this.props.room.getUnreadNotificationCount(),
        });
    },

    // Do a simple shallow comparison of props and state to avoid unnecessary
    // renders. The assumption made here is that only state and props are used
    // in rendering this component and children.
    //
    // RoomList is frequently made to forceUpdate, so this decreases number of
    // RoomTile renderings.
    shouldComponentUpdate: function(newProps, newState) {
        if (Object.keys(newProps).some((k) => newProps[k] !== this.props[k])) {
            return true;
        }
        if (Object.keys(newState).some((k) => newState[k] !== this.state[k])) {
            return true;
        }
        return false;
    },

    _onStatusMessageCommitted() {
        // The status message `User` object has observed a message change.
        this.setState({
            statusMessage: this._getStatusMessage(),
        });
    },

    onClick: function(ev) {
        if (this.props.onClick) {
            this.props.onClick(this.props.room.roomId, ev);
        }
    },

    onMouseEnter: function() {
        this.setState( { hover: true });
        this.badgeOnMouseEnter();
    },

    onMouseLeave: function() {
        this.setState( { hover: false });
        this.badgeOnMouseLeave();
    },

    badgeOnMouseEnter: function() {
        // Only allow non-guests to access the context menu
        // and only change it if it needs to change
        if (!MatrixClientPeg.get().isGuest() && !this.state.badgeHover) {
            this.setState( { badgeHover: true } );
        }
    },

    badgeOnMouseLeave: function() {
        this.setState( { badgeHover: false } );
    },

    _showContextMenu: function(boundingClientRect) {
        // Only allow non-guests to access the context menu
        if (MatrixClientPeg.get().isGuest()) return;

        const state = {
            contextMenuPosition: boundingClientRect,
        };

        // If the badge is clicked, then no longer show tooltip
        if (this.props.collapsed) {
            state.hover = false;
        }

        this.setState(state);
    },

    onContextMenuButtonClick: function(e) {
        // Prevent the RoomTile onClick event firing as well
        e.stopPropagation();
        e.preventDefault();

        this._showContextMenu(e.target.getBoundingClientRect());
    },

    onContextMenu: function(e) {
        // Prevent the native context menu
        e.preventDefault();

        this._showContextMenu({
            right: e.clientX,
            top: e.clientY,
            height: 0,
        });
    },

    closeMenu: function() {
        this.setState({
            contextMenuPosition: null,
        });
        this.props.refreshSubList();
    },

    render: function() {
        const isInvite = this.props.room.getMyMembership() === "invite";
        const notificationCount = this.props.notificationCount;
        // var highlightCount = this.props.room.getUnreadNotificationCount("highlight");

        const notifBadges = notificationCount > 0 && RoomNotifs.shouldShowNotifBadge(this.state.notifState);
        const mentionBadges = this.props.highlight && RoomNotifs.shouldShowMentionBadge(this.state.notifState);
        const badges = notifBadges || mentionBadges;

        let subtext = null;
        if (this._shouldShowStatusMessage()) {
            subtext = this.state.statusMessage;
        }

        const isMenuDisplayed = Boolean(this.state.contextMenuPosition);

        const dmUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);

        const classes = classNames({
            'mx_RoomTile': true,
            'mx_RoomTile_selected': this.state.selected,
            'mx_RoomTile_unread': this.props.unread,
            'mx_RoomTile_unreadNotify': notifBadges,
            'mx_RoomTile_highlight': mentionBadges,
            'mx_RoomTile_invited': isInvite,
            'mx_RoomTile_menuDisplayed': isMenuDisplayed,
            'mx_RoomTile_noBadges': !badges,
            'mx_RoomTile_transparent': this.props.transparent,
            'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed,
            'mx_RoomTile_isPrivate': this.state.joinRule == "invite" && !dmUserId,
        });

        const avatarClasses = classNames({
            'mx_RoomTile_avatar': true,
        });

        const badgeClasses = classNames({
            'mx_RoomTile_badge': true,
            'mx_RoomTile_badgeButton': this.state.badgeHover || isMenuDisplayed,
        });

        let name = this.state.roomName;
        if (name == undefined || name == null) name = '';
        name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon

        let badge;
        if (badges) {
            const limitedCount = FormattingUtils.formatCount(notificationCount);
            const badgeContent = notificationCount ? limitedCount : '!';
            badge =
{ badgeContent }
;
        }

        let label;
        let subtextLabel;
        let tooltip;
        if (!this.props.collapsed) {
            const nameClasses = classNames({
                'mx_RoomTile_name': true,
                'mx_RoomTile_invite': this.props.isInvite,
                'mx_RoomTile_badgeShown': badges || this.state.badgeHover || isMenuDisplayed,
            });

            subtextLabel = subtext ? { subtext } : null;
            label =
{ name }
;
        } else if (this.state.hover) {
            const Tooltip = sdk.getComponent("elements.Tooltip");
            tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />;
        }

        //var incomingCallBox;
        //if (this.props.incomingCall) {
        //    var IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
        //    incomingCallBox = <IncomingCallBox incomingCall={ this.props.incomingCall }/>;
        //}

        const AccessibleButton = sdk.getComponent('elements.AccessibleButton');

        let contextMenuButton;
        if (!MatrixClientPeg.get().isGuest()) {
            contextMenuButton = (
                <ContextMenuButton
                    className="mx_RoomTile_menuButton"
                    label={_t("Options")}
                    isExpanded={isMenuDisplayed}
                    onClick={this.onContextMenuButtonClick} />
            );
        }

        const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');

        let ariaLabel = name;

        let dmIndicator;
        let dmOnline;
        if (dmUserId) {
            dmIndicator = ;

            const { room } = this.props;
            const member = room.getMember(dmUserId);
            if (member && member.membership === "join" && room.getJoinedMemberCount() === 2) {
                const UserOnlineDot = sdk.getComponent('rooms.UserOnlineDot');
                dmOnline = <UserOnlineDot userId={dmUserId} />;
            }
        }

        // The following labels are written in such a fashion to increase screen reader efficiency (speed).
        if (notifBadges && mentionBadges && !isInvite) {
            ariaLabel += " " + _t("%(count)s unread messages including mentions.", {
                count: notificationCount,
            });
        } else if (notifBadges) {
            ariaLabel += " " + _t("%(count)s unread messages.", { count: notificationCount });
        } else if (mentionBadges && !isInvite) {
            ariaLabel += " " + _t("Unread mentions.");
        } else if (this.props.unread) {
            ariaLabel += " " + _t("Unread messages.");
        }

        let contextMenu;
        if (isMenuDisplayed) {
            const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu');
            contextMenu = (
                <ContextMenu {...toRightOf(this.state.contextMenuPosition)} onFinished={this.closeMenu}>
                    <RoomTileContextMenu room={this.props.room} onFinished={this.closeMenu} />
                </ContextMenu>
            );
        }

        return <React.Fragment>
            <AccessibleButton
                tabIndex="0"
                className={classes}
                onClick={this.onClick}
                onMouseEnter={this.onMouseEnter}
                onMouseLeave={this.onMouseLeave}
                onContextMenu={this.onContextMenu}
                aria-label={ariaLabel}
                aria-selected={this.state.selected}
                role="treeitem"
            >

                        <RoomAvatar room={this.props.room} width={24} height={24} />
                        { dmIndicator }

                        { label }
                        { subtextLabel }

                    { dmOnline }
                    { contextMenuButton }
                    { badge }

                { /* { incomingCallBox } */ }
                { tooltip }
            </AccessibleButton>

            { contextMenu }
        </React.Fragment>;
    },
});
<|endoftext|>


<commit_before>export default `
30-05-2017 - Resto Druid: Added Dreamwalker calculations. Thanks @greatman
30-05-2017 - Resto Druid: Added SotF + Soul of the Archdruid analyzer.
29-05-2017 - Disc Priest: Show Rapture PW:S casts seperate from regular PW:S casts in Cast Efficiency. (by Zerotorescue)
29-05-2017 - Fixed a crash when trying to parse a corrupt combatlog. (by versaya)
29-05-2017 - Resto Shaman: Added overhealing in Cast Efficiency tab for some resto shaman spells. Fixed Uncertain Reminder in case of pre-lust. Added GotQ target efficiency.	Don't allow CBT healing to feed into CBT.
29-05-2017 - Mistweaver Monk: Added Sheilun's Gift statistic and an overhealing suggestion. With Effusive Mists talent, Effuse casts at 12 stacks are considered wasted. Added simple proc counter for Dancing Mists. (by anom0ly)
28-05-2017 - Disc Priest: Added unused Power Word: Shield absorb statistic.
28-05-2017 - Added Restoration Shaman support by @Versaya! Thanks a lot for your contribution!
28-05-2017 - Added overhealing percentages to the Cast Efficiency tab.
27-05-2017 - Mistweaver Monk: Added Thunder Focus Tea spell buff tracking. Added Lifecycles mana saving tracking, added Spirit of the Crane mana return tracking. (by anom0ly)
27-05-2017 - Holy Paladin: Added Tier 20 4 set bonus statistic. Reworked how spells and their beacon transfer are matched to be much more reliable. (by Zerotorescue)
26-05-2017 - Mistweaver Monk: Added the remaining MW spells / abilities known as of now. Removed UT Usage issue, as this is going away in 7.2.5. Updated CPM Module to give a better understanding of MW Monk Spells. Incorporated TFT -> Viv usage in UT Proc calculations. All of this was done by @anom0ly. Thanks a lot for your contribution!
26-05-2017 - Resto Druid: Added Non healing time and dead GCD.
26-05-2017 - Resto Druid: Added Chameleon Song legendary analyzer. Also added additional information on tree of life tool tip.
25-05-2017 - Resto Druid: Added support for T20.
25-05-2017 - Disc Priest: Added Early Atonement refreshes statistic. Fixed Skjoldr sometimes not working properly. Both contributions were created by @Shadowdrizz. Thanks a lot for your contribution!
25-05-2017 - Added Patreon links to the specs I (Zerotorescue) maintain. Please let me know if you think this is inappropriate or makes you hesitate to contribute. Added Discord link.
25-05-2017 - Resto Druid: Fixed a minor error in DTA calculations with partial overheals.
24-05-2017 - Added Mistweaver Monk support by @Anomoly! Thanks a lot for your contribution!
An informative message is now shown when trying to parse a report without combatants (usually due to not having advanced combat logging enabled).
Holy Paladin: Show overhealing during cooldowns too. Innervates properly shows mana costs.
Disc Priest: Fix Shadowfiend showing with the Mindbender talent.
21-05-2017 - Fixed a rare crash when auto attacking something.
21-05-2017 - Added Resto Druid support by @blazyb (see: https://github.com/buimichael/RestoDruidAnalyzer).
20-05-2017 - Added Cooldowns tab to show casts and healing when affected by a cooldown. Added Amalgam's Seventh Spine mana gained statistic. Promises no longer includes mana reduction during Innervate.
18-05-2017 - Disc Priest: Added Shadow Word: Pain/Purge the Wicked global uptime statistic.
17-05-2017 - Disc Priest: Added Twist of Fate healing statistic (damage gain is in the tooltip).
17-05-2017 - Added Sephuz's Secret uptime indicator.
16-05-2017 - Disabled Retribution Paladin spec since it never really came out of the experimental phase.
16-05-2017 - Holy Paladin: Added Darkmoon Deck: Promises statistic. Disc Priest: Added Darkmoon Deck: Promises statistic. Added Light's Wrath to cast efficiency. Changed Pain Suppression cooldown to take into account the Pain is in your Mind trait.
16-05-2017 - Disc Priest: Added N'ero, Band of Promises statistic.
15-05-2017 - Disc Priest: Added Xalan the Feared's Clench statistic.
15-05-2017 - Disc Priest: Skjoldr, Sanctuary of Ivagont statistic now includes the healing gained via Share in the Light.
14-05-2017 - Disc Priest: Added Skjoldr, Sanctuary of Ivagont statistic. This does not yet include the healing gained via Share in the Light.
14-05-2017 - Disc Priest: Added Cord of Maiev, Priestess of the Moon statistic.
14-05-2017 - Added Disc Priest 2 set bonus statistic.
14-05-2017 - Disc Priest: Renamed Missed penance hits to Wasted Penance bolts. Wasted Penance bolts now accounts for (combat log) latency. Fixed Glyph of the Sha's Shadowfiend not being counted towards Shadowfiend casts. Fixed healing increases (most notably the 15% from Velen's) not working with Disc priest spells.
14-05-2017 - Added Discipline Priest spec. Currently includes basic statistics for Dead GCD time (should be fully operational), shared legendaries, missed Penance hits, cast efficiencies and the other build in tools.
13-05-2017 - Added full multispec support! The right spec specific parser is now selected based on the spec of the selected person. Only players with supported specs will be displayed in the player selection.
11-05-2017 - Fixed a bunch of bugs. The Always Be Casting/Healing module now supports debuffs which allows me to implement boss Haste buffs to make it more accurate. Elisande's Haste buffs are now implemented.
11-05-2017 - A lot more changes under the hood in order to make the analyzer multi-spec compatible. Almost everything was changed, so anything might have gotten broken. Please let me know.
08-05-2017 - Oops one of the lines got mixed up. Fixed Holy Light instead of Flash of Light being suggested to improve Infusion of Light proc usage. Casting Holy Light during IoL is worse, m'kay.
07-05-2017 - Untangled many lines - you shouldn't notice a difference.
05-05-2017 - Added Aura of Mercy and Sacrifice healing done statistic.
05-05-2017 - Changed the report selection interface.
04-05-2017 - Minor importance suggestions are now hidden behind a toggle by default.
01-05-2017 - Added overhealing suggestions and relaxed the non healing time / dead GCD time suggestions (they should be marked as minor issues more often).
27-04-2017 - Prevent caching of fights list so it updates with live logs. You can now directly link specific tabs on the results page.
23-04-2017 - Changed the LotM suggestion to be based on CPM and be much more forgiving. Added death indicators to the mana graph.
21-04-2017 - Added HPS numbers to item healing done. Damaging HS are now included in cast counts. Recommended cast efficiency of CS with CM reduced to 30%. The recommended unused IoL is now adjusted based on if you have CM, DP and/or 4PT19.
21-04-2017 - Added suggestion/issue importance indicators and show a positive message when there are no major issues.
20-04-2017 - Since Easter is over bosses will no longer resurrect in the mana graph<dfn data-tip="This may cause issues with bosses that do actually resurrect, but I will fix that once I encounter such a boss.">*</dfn>. All bosses in a fight get shown instead of just the first<dfn data-tip="If boss adds are classified as bosses they will be shown as well. So far this doesn't appear to happen, but I will fix it if I encounter this.">*</dfn>. This should make the boss health for Mythic Botanist work properly.
19-04-2017 - Added a Mana tab for analyzing your mana usage. This also shows boss health since it is often used as a target for mana levels.
18-04-2017 - Added Velen's Future Sight to cast efficiency. Fixed the parser crashing instead of showing a descriptive error when selected player doesn't appear in a log. Fixed a crash when buff with stacks expired without ever applying.
17-04-2017 - Added LotM suggestion when casting it too often, added Divine Protection and BoS to cast efficiency display. Added Divine Purpose proc indicator.
15-04-2017 - Added Tyr's Deliverance healing contribution.
15-04-2017 - Added Sacred Dawn healing contribution. Note that (when SD wasn't up before) Sacred Dawn increases the Light of Dawn healing on yourself on the initial cast, but other players get healed for the normal amount without the SD healing increase. The calculation is aware of this.
15-04-2017 - Cast efficiency now takes the extra amount of fight time you had to cast an ability into consideration (e.g. a single cast of 3 minute cooldown during a 3:30 fight will have a higher cast efficiency than during a 5:30 fight).
15-04-2017 - Added a Talents tab.
14-04-2017 - Added Suggestions which will give you (way too many) suggestions to improve. This may need to be tuned a bit; any feedback in that regard is welcome. Moved Cast efficiency and Mastery effectiveness player breakdown to separate tabs.
11-04-2017 - Added Rule of Law to cast efficiency and grouped spells by category.
10-04-2017 - Show an error when parsing crashes (usually caused by not having advanced combat logging on). Renamed Casts Per Minute to Cast Efficiency. Show absolute amount of casts in Cast Efficiency. Added Arcane Torrent to Cast Efficiency (only shown if you cast it at least once).
09-04-2017 - Added Casts Per Minute table with very basic recommendations.
08-04-2017 - Added Heals on beacon statistic.
08-04-2017 - Added Wowhead tooltips and show T19 4 set bonus gain.
08-04-2017 - Added Beacon of the Lightbringer mastery radius support!
08-04-2017 - Improve beacon healing tracking accuracy and it now works properly with Beacon of Virtue.
08-04-2017 - Total healing done count now includes absorbed healing.
07-04-2017 - New layout with many usability improvements!
04-04-2017 - Add an Always Be Casting (ABC) module that checks your Non healing time and dead GCD time (this is shown in the tooltip).
29-03-2017 - Fixed a bug where Maraad's healing statistic would show 0% healing after getting only 1 Maraad's charge.
29-03-2017 - Update healing bonuses to the 7.2 values (DoS & Ilterendi nerfs).
27-03-2017 - Added Maraad's Dying Breath healing statistic.
27-03-2017 - Added Prydaz and Obsidian Stone Spaulders healing statistic.
26-03-2017 - Added Chain of Thrayn healing statistic.
26-03-2017 - Completely refactor the core, rename to Holy Paladin Analyzer.
25-03-2017 - Added cast ratios statistic.
`;
<commit_msg>Update changelog<commit_after>export default `
30-05-2017 - Mistweaver Monk: Adjusted some of the Dancing Mists logic per review. Added in missed Whispers of Shaohao healing. Tracking Celestial Breath and Mists of Sheilun buffs / procs and the healing associated. Refreshing Jade Wind Healing Implemented. Corrected some negative calculations with Uplifting Trance. Correcting formatting error with SG Stacks. (by anom0ly)
30-05-2017 - Resto Druid: Added Dreamwalker calculations. Thanks @greatman
30-05-2017 - Resto Druid: Added SotF + Soul of the Archdruid analyzer.
29-05-2017 - Disc Priest: Show Rapture PW:S casts seperate from regular PW:S casts in Cast Efficiency. (by Zerotorescue)
29-05-2017 - Fixed a crash when trying to parse a corrupt combatlog. (by versaya)
29-05-2017 - Resto Shaman: Added overhealing in Cast Efficiency tab for some resto shaman spells. Fixed Uncertain Reminder in case of pre-lust. Added GotQ target efficiency.	Don't allow CBT healing to feed into CBT.
29-05-2017 - Mistweaver Monk: Added Sheilun's Gift statistic and an overhealing suggestion. With Effusive Mists talent, Effuse casts at 12 stacks are considered wasted. Added simple proc counter for Dancing Mists. (by anom0ly)
28-05-2017 - Disc Priest: Added unused Power Word: Shield absorb statistic.
28-05-2017 - Added Restoration Shaman support by @Versaya! Thanks a lot for your contribution!
28-05-2017 - Added overhealing percentages to the Cast Efficiency tab.
27-05-2017 - Mistweaver Monk: Added Thunder Focus Tea spell buff tracking. Added Lifecycles mana saving tracking, added Spirit of the Crane mana return tracking. (by anom0ly)
27-05-2017 - Holy Paladin: Added Tier 20 4 set bonus statistic. Reworked how spells and their beacon transfer are matched to be much more reliable. (by Zerotorescue)
26-05-2017 - Mistweaver Monk: Added the remaining MW spells / abilities known as of now. Removed UT Usage issue, as this is going away in 7.2.5. Updated CPM Module to give a better understanding of MW Monk Spells. Incorporated TFT -> Viv usage in UT Proc calculations. All of this was done by @anom0ly. Thanks a lot for your contribution!
26-05-2017 - Resto Druid: Added Non healing time and dead GCD.
26-05-2017 - Resto Druid: Added Chameleon Song legendary analyzer. Also added additional information on tree of life tool tip.
25-05-2017 - Resto Druid: Added support for T20.
25-05-2017 - Disc Priest: Added Early Atonement refreshes statistic. Fixed Skjoldr sometimes not working properly. Both contributions were created by @Shadowdrizz. Thanks a lot for your contribution!
25-05-2017 - Added Patreon links to the specs I (Zerotorescue) maintain. Please let me know if you think this is inappropriate or makes you hesitate to contribute. Added Discord link.
25-05-2017 - Resto Druid: Fixed a minor error in DTA calculations with partial overheals.
24-05-2017 - Added Mistweaver Monk support by @Anomoly! Thanks a lot for your contribution!
An informative message is now shown when trying to parse a report without combatants (usually due to not having advanced combat logging enabled).
Holy Paladin: Show overhealing during cooldowns too. Innervates properly shows mana costs.
Disc Priest: Fix Shadowfiend showing with the Mindbender talent.
21-05-2017 - Fixed a rare crash when auto attacking something.
21-05-2017 - Added Resto Druid support by @blazyb (see: https://github.com/buimichael/RestoDruidAnalyzer).
20-05-2017 - Added Cooldowns tab to show casts and healing when affected by a cooldown. Added Amalgam's Seventh Spine mana gained statistic. Promises no longer includes mana reduction during Innervate.
18-05-2017 - Disc Priest: Added Shadow Word: Pain/Purge the Wicked global uptime statistic.
17-05-2017 - Disc Priest: Added Twist of Fate healing statistic (damage gain is in the tooltip).
17-05-2017 - Added Sephuz's Secret uptime indicator.
16-05-2017 - Disabled Retribution Paladin spec since it never really came out of the experimental phase.
16-05-2017 - Holy Paladin: Added Darkmoon Deck: Promises statistic. Disc Priest: Added Darkmoon Deck: Promises statistic. Added Light's Wrath to cast efficiency. Changed Pain Suppression cooldown to take into account the Pain is in your Mind trait.
16-05-2017 - Disc Priest: Added N'ero, Band of Promises statistic.
15-05-2017 - Disc Priest: Added Xalan the Feared's Clench statistic.
15-05-2017 - Disc Priest: Skjoldr, Sanctuary of Ivagont statistic now includes the healing gained via Share in the Light.
14-05-2017 - Disc Priest: Added Skjoldr, Sanctuary of Ivagont statistic. This does not yet include the healing gained via Share in the Light.
14-05-2017 - Disc Priest: Added Cord of Maiev, Priestess of the Moon statistic.
14-05-2017 - Added Disc Priest 2 set bonus statistic.
14-05-2017 - Disc Priest: Renamed Missed penance hits to Wasted Penance bolts. Wasted Penance bolts now accounts for (combat log) latency. Fixed Glyph of the Sha's Shadowfiend not being counted towards Shadowfiend casts. Fixed healing increases (most notably the 15% from Velen's) not working with Disc priest spells.
14-05-2017 - Added Discipline Priest spec. Currently includes basic statistics for Dead GCD time (should be fully operational), shared legendaries, missed Penance hits, cast efficiencies and the other build in tools.
13-05-2017 - Added full multispec support! The right spec specific parser is now selected based on the spec of the selected person. Only players with supported specs will be displayed in the player selection.
11-05-2017 - Fixed a bunch of bugs. The Always Be Casting/Healing module now supports debuffs which allows me to implement boss Haste buffs to make it more accurate. Elisande's Haste buffs are now implemented.
11-05-2017 - A lot more changes under the hood in order to make the analyzer multi-spec compatible. Almost everything was changed, so anything might have gotten broken. Please let me know.
08-05-2017 - Oops one of the lines got mixed up. Fixed Holy Light instead of Flash of Light being suggested to improve Infusion of Light proc usage. Casting Holy Light during IoL is worse, m'kay.
07-05-2017 - Untangled many lines - you shouldn't notice a difference.
05-05-2017 - Added Aura of Mercy and Sacrifice healing done statistic.
05-05-2017 - Changed the report selection interface.
04-05-2017 - Minor importance suggestions are now hidden behind a toggle by default.
01-05-2017 - Added overhealing suggestions and relaxed the non healing time / dead GCD time suggestions (they should be marked as minor issues more often).
27-04-2017 - Prevent caching of fights list so it updates with live logs. You can now directly link specific tabs on the results page.
23-04-2017 - Changed the LotM suggestion to be based on CPM and be much more forgiving. Added death indicators to the mana graph.
21-04-2017 - Added HPS numbers to item healing done. Damaging HS are now included in cast counts. Recommended cast efficiency of CS with CM reduced to 30%. The recommended unused IoL is now adjusted based on if you have CM, DP and/or 4PT19.
21-04-2017 - Added suggestion/issue importance indicators and show a positive message when there are no major issues.
20-04-2017 - Since Easter is over bosses will no longer resurrect in the mana graph<dfn data-tip="This may cause issues with bosses that do actually resurrect, but I will fix that once I encounter such a boss.">*</dfn>. All bosses in a fight get shown instead of just the first<dfn data-tip="If boss adds are classified as bosses they will be shown as well. So far this doesn't appear to happen, but I will fix it if I encounter this.">*</dfn>. This should make the boss health for Mythic Botanist work properly.
19-04-2017 - Added a Mana tab for analyzing your mana usage. This also shows boss health since it is often used as a target for mana levels.
18-04-2017 - Added Velen's Future Sight to cast efficiency. Fixed the parser crashing instead of showing a descriptive error when selected player doesn't appear in a log. Fixed a crash when buff with stacks expired without ever applying.
17-04-2017 - Added LotM suggestion when casting it too often, added Divine Protection and BoS to cast efficiency display. Added Divine Purpose proc indicator.
15-04-2017 - Added Tyr's Deliverance healing contribution.
15-04-2017 - Added Sacred Dawn healing contribution. Note that (when SD wasn't up before) Sacred Dawn increases the Light of Dawn healing on yourself on the initial cast, but other players get healed for the normal amount without the SD healing increase. The calculation is aware of this.
15-04-2017 - Cast efficiency now takes the extra amount of fight time you had to cast an ability into consideration (e.g. a single cast of 3 minute cooldown during a 3:30 fight will have a higher cast efficiency than during a 5:30 fight).
15-04-2017 - Added a Talents tab.
14-04-2017 - Added Suggestions which will give you (way too many) suggestions to improve. This may need to be tuned a bit; any feedback in that regard is welcome. Moved Cast efficiency and Mastery effectiveness player breakdown to separate tabs.
11-04-2017 - Added Rule of Law to cast efficiency and grouped spells by category.
10-04-2017 - Show an error when parsing crashes (usually caused by not having advanced combat logging on). Renamed Casts Per Minute to Cast Efficiency. Show absolute amount of casts in Cast Efficiency. Added Arcane Torrent to Cast Efficiency (only shown if you cast it at least once).
09-04-2017 - Added Casts Per Minute table with very basic recommendations.
08-04-2017 - Added Heals on beacon statistic.
08-04-2017 - Added Wowhead tooltips and show T19 4 set bonus gain.
08-04-2017 - Added Beacon of the Lightbringer mastery radius support!
08-04-2017 - Improve beacon healing tracking accuracy and it now works properly with Beacon of Virtue.
08-04-2017 - Total healing done count now includes absorbed healing.
07-04-2017 - New layout with many usability improvements!
04-04-2017 - Add an Always Be Casting (ABC) module that checks your Non healing time and dead GCD time (this is shown in the tooltip).
29-03-2017 - Fixed a bug where Maraad's healing statistic would show 0% healing after getting only 1 Maraad's charge.
29-03-2017 - Update healing bonuses to the 7.2 values (DoS & Ilterendi nerfs).
27-03-2017 - Added Maraad's Dying Breath healing statistic.
27-03-2017 - Added Prydaz and Obsidian Stone Spaulders healing statistic.
26-03-2017 - Added Chain of Thrayn healing statistic.
26-03-2017 - Completely refactor the core, rename to Holy Paladin Analyzer.
25-03-2017 - Added cast ratios statistic.
`;
<|endoftext|>


<commit_before>(() => {
    'use strict';
    /**
     * Controller responsável pela view do calendário.
     *
     */
    angular.module("calendarioModulo", []).controller("CalendarioController", ['$scope', '$compile', '$filter', '$state', 'uiCalendarConfig', 'APP_STATES',
        'Reserva', 'DataManipuladorService', 'LocaisService', 'ModalService', 'TIPOS_RESERVA', 'AgendamentoService', '$q',
        function ($scope, $compile, $filter, $state, uiCalendarConfig, APP_STATES, Reserva, DataManipuladorService, LocaisService, ModalService, TIPOS_RESERVA, AgendamentoService, $q) {

        const self = this;

        this.local;
        this.isLocalSelecionado = false;
        this.reservasSource = [];

        this.tiposReserva = TIPOS_RESERVA;

        /**
         * Callback executado quando o usuário clica em uma determinada reserva.
         * Aqui exibimos o Modal com as informações sobre a reserva.
         *
         * @param reserva Reserva clicado.
         * @return {Promise} Promise do modal de visualização da reserva
         */
        this.clickReserva = function(reserva) {
            const reservaObj = new Reserva(reserva);
            return ModalService.verReserva(reservaObj);
        };

        /**
         * Callback executado quando o usuário clica em um determinado dia.
         * Temos que decidir qual será o comportamento executado, pois podemos manter o usuário
         * na mesma tela e apenas alterar a visualização do calendário para o dia clicado, ou
         * podemos fazer o que estava sendo feito antes, redirecioná-lo para outra tela, visuali-
         * zando apenas o dia clicado.
         *
         * TODO: Verificar se precisaremos lidar com esse problema do fuso horário
         * OBS: Devido ao uso do Locale pt-br (Fuso horário de -3 GMT), o horário do dia retornado
         * está sendo atrasado em 3 horas. Devido a esse problema, a data retornada está sendo sem-
         * pre do dia anterior, pois por exemplo, ao clicarmos no dia 05/07/2017, ele deveria re-
         * tornar a data: 2017-07-05T00:00:00.000Z, porém por retardar 3 horas, a data retornada é
         * 2017-07-04T21:00:00.000Z. Por ora, como não encontrei solução para o problema, estou al-
         * terando manualmente a hora da data para 3 horas na frente.
         *
         * @param data Data clicado.
         */
        this.clickDia = function(data) {
            /**
             * O prório calendário liga com essa configuração do Locale, então ele sabe que está
             * 3 horas atrasado, por isso a alteração do horário para 3 horas a mais só deve ser
             * feita caso utilizemos o objeto Dia para alguma ação interna.
             *
             * const fatorFusoHorario = 3;
             * dia._d.setHours(dia._d.getHours() + fatorFusoHorario);
             */

            const calendario = uiCalendarConfig.calendars.calendario;

            if (calendario.fullCalendar('getView').type === 'agendaDay' ||
                calendario.fullCalendar('getView').type === 'basicDay') {
                self.criarReserva(data);
            } else {
                calendario.fullCalendar('changeView', 'agendaDay', data);
            }
        };

        /**
         * Objeto enviado à diretiva do calendário com todas as configurações desejadas.
         * Consultar: https://fullcalendar.io/docs/
         */
        this.calendarioConfig = {
            calendario: {
                height: "100%",
                editable: true,
                timezone: false,
                lang: 'pt-br',
                header:{
                    left: 'month agendaWeek agendaDay',
                    center: 'title',
                    right: 'today prev,next'
                },
                displayEventEnd: true,
                views: {
                    month: {
                        eventLimit: 4
                    }
                },
                eventClick: self.clickReserva,
                dayClick: self.clickDia,
                eventAfterAllRender: transformaBotoesCalendario,
                buttonText: {
                    agendaWeek: 'Semana',
                    agendaDay: 'Dia'
                }
            }
        };

        /**
         * Abre o modal para criação de reserva no dia especificado.
         *
         * @return {Promise} Promise do modal.
         */
        this.criarReserva = (data) => {
            const reserva = new Reserva({
                dia: DataManipuladorService.parseData(data),
                localId: self.local._id
            });

            return ModalService.verReserva(reserva);
        };

        this.onChangeLocal = function (local) {
            this.local = local;
            return AgendamentoService.carregarReservasDoLocal(local._id).then(data => {
                self.reservasSource.splice(0, self.reservasSource.length);
                const reservas = data.data;
                self.reservasSource.push(reservas);
                this.isLocalSelecionado = true;
                return data;
            });
        };

        this.voltaParaListagem = () => {
            this.isLocalSelecionado = false;
        };

        this.visualizarAuditorio = function () {
            $state.go(APP_STATES.LOCAL_ID_INFO.nome, {idLocal: self.local._id});
        };

        /**
         * Retorna um array de reservas contendo apenas as que tem horário de início maior
         * que o horário atual.
         *
         * OBS: Por ora esse método não está sendo utilizado, mas como Eric já implementou
         * e eu acho que pode ser útil, vou deixá-lo aqui por ora. {Vélmer}
         *
         * @param reservas Reservas a serem filtradas.
         * @param data Data em que as reservas especificadas ocorrem.
         * @returns {*} Reservas que iniciam após a hora atual.
         */
        function filtraReservasAtuaisFuturas(reservas, data) {
            const dateAgora = new Date();
            const horaAgora = dateAgora.getHours() + ':' + dateAgora.getMinutes();
            if (DataManipuladorService.isDataFutura(data)) {
                return reservas;
            }
            if (DataManipuladorService.isHoje(data)) {
                return reservas.filter(reserva => {
                    return reserva.fim >= horaAgora;
                });
            }
            return [];
        }

        /**
         * Modifica os botões do calendário para terem os estilos seguindo padrão
         * material, adicionando a classe md-button.
         */
        function transformaBotoesCalendario() {
            const listaBotoes = $('button[class*="fc-button"]');
            const mdButton = "md-button";
            listaBotoes.each(function (i) {
                if (!$(this).hasClass(mdButton))
                    $(this).addClass(mdButton);
            });
        }
    }]);
})();<commit_msg>Adicionando jsdoc<commit_after>(() => {
    'use strict';
    /**
     * Controller responsável pela view do calendário.
     *
     */
    angular.module("calendarioModulo", []).controller("CalendarioController", ['$scope', '$compile', '$filter', '$state', 'uiCalendarConfig', 'APP_STATES',
        'Reserva', 'DataManipuladorService', 'LocaisService', 'ModalService', 'TIPOS_RESERVA', 'AgendamentoService', '$q',
        function ($scope, $compile, $filter, $state, uiCalendarConfig, APP_STATES, Reserva, DataManipuladorService, LocaisService, ModalService, TIPOS_RESERVA, AgendamentoService, $q) {

        const self = this;

        this.local;
        this.isLocalSelecionado = false;
        this.reservasSource = [];

        this.tiposReserva = TIPOS_RESERVA;

        /**
         * Callback executado quando o usuário clica em uma determinada reserva.
         * Aqui exibimos o Modal com as informações sobre a reserva.
         *
         * @param reserva Reserva clicado.
         * @return {Promise} Promise do modal de visualização da reserva
         */
        this.clickReserva = function(reserva) {
            const reservaObj = new Reserva(reserva);
            return ModalService.verReserva(reservaObj);
        };

        /**
         * Callback executado quando o usuário clica em um determinado dia.
         * Temos que decidir qual será o comportamento executado, pois podemos manter o usuário
         * na mesma tela e apenas alterar a visualização do calendário para o dia clicado, ou
         * podemos fazer o que estava sendo feito antes, redirecioná-lo para outra tela, visuali-
         * zando apenas o dia clicado.
         *
         * TODO: Verificar se precisaremos lidar com esse problema do fuso horário
         * OBS: Devido ao uso do Locale pt-br (Fuso horário de -3 GMT), o horário do dia retornado
         * está sendo atrasado em 3 horas. Devido a esse problema, a data retornada está sendo sem-
         * pre do dia anterior, pois por exemplo, ao clicarmos no dia 05/07/2017, ele deveria re-
         * tornar a data: 2017-07-05T00:00:00.000Z, porém por retardar 3 horas, a data retornada é
         * 2017-07-04T21:00:00.000Z. Por ora, como não encontrei solução para o problema, estou al-
         * terando manualmente a hora da data para 3 horas na frente.
         *
         * @param data Data clicado.
         */
        this.clickDia = function(data) {
            /**
             * O prório calendário liga com essa configuração do Locale, então ele sabe que está
             * 3 horas atrasado, por isso a alteração do horário para 3 horas a mais só deve ser
             * feita caso utilizemos o objeto Dia para alguma ação interna.
             *
             * const fatorFusoHorario = 3;
             * dia._d.setHours(dia._d.getHours() + fatorFusoHorario);
             */

            const calendario = uiCalendarConfig.calendars.calendario;

            if (calendario.fullCalendar('getView').type === 'agendaDay' ||
                calendario.fullCalendar('getView').type === 'basicDay') {
                self.criarReserva(data);
            } else {
                calendario.fullCalendar('changeView', 'agendaDay', data);
            }
        };

        /**
         * Objeto enviado à diretiva do calendário com todas as configurações desejadas.
         * Consultar: https://fullcalendar.io/docs/
         */
        this.calendarioConfig = {
            calendario: {
                height: "100%",
                editable: true,
                timezone: false,
                lang: 'pt-br',
                header:{
                    left: 'month agendaWeek agendaDay',
                    center: 'title',
                    right: 'today prev,next'
                },
                displayEventEnd: true,
                views: {
                    month: {
                        eventLimit: 4
                    }
                },
                eventClick: self.clickReserva,
                dayClick: self.clickDia,
                eventAfterAllRender: transformaBotoesCalendario,
                buttonText: {
                    agendaWeek: 'Semana',
                    agendaDay: 'Dia'
                }
            }
        };

        /**
         * Abre o modal para criação de reserva no dia especificado.
         *
         * @return {Promise} Promise do modal.
         */
        this.criarReserva = (data) => {
            const reserva = new Reserva({
                dia: DataManipuladorService.parseData(data),
                localId: self.local._id
            });

            return ModalService.verReserva(reserva);
        };

        /**
         * Atualiza as reservas exibidas quando um local é selecionado, exibindo apenas as
         * reservas pertencentes ao mesmo.
         *
         * @param local Local a ter suas reservas carregadas e exibidas no calendário.
         * @return {Promise} Promessa contendo as reservas do local selecionado.
         */
        this.onChangeLocal = function (local) {
            this.local = local;
            return AgendamentoService.carregarReservasDoLocal(local._id).then(data => {
                self.reservasSource.splice(0, self.reservasSource.length);
                const reservas = data.data;
                self.reservasSource.push(reservas);
                this.isLocalSelecionado = true;
                return data;
            });
        };

        /**
         * Retorna a visualização para listagem de calendários.
         */
        this.voltaParaListagem = () => {
            this.isLocalSelecionado = false;
        };

        /**
         * Redireciona para tela de informações do auditório atual.
         */
        this.visualizarAuditorio = function () {
            $state.go(APP_STATES.LOCAL_ID_INFO.nome, {idLocal: self.local._id});
        };

        /**
         * Retorna um array de reservas contendo apenas as que tem horário de início maior
         * que o horário atual.
         *
         * OBS: Por ora esse método não está sendo utilizado, mas como Eric já implementou
         * e eu acho que pode ser útil, vou deixá-lo aqui por ora. {Vélmer}
         *
         * @param reservas Reservas a serem filtradas.
         * @param data Data em que as reservas especificadas ocorrem.
         * @returns {*} Reservas que iniciam após a hora atual.
         */
        function filtraReservasAtuaisFuturas(reservas, data) {
            const dateAgora = new Date();
            const horaAgora = dateAgora.getHours() + ':' + dateAgora.getMinutes();
            if (DataManipuladorService.isDataFutura(data)) {
                return reservas;
            }
            if (DataManipuladorService.isHoje(data)) {
                return reservas.filter(reserva => {
                    return reserva.fim >= horaAgora;
                });
            }
            return [];
        }

        /**
         * Modifica os botões do calendário para terem os estilos seguindo padrão
         * material, adicionando a classe md-button.
         */
        function transformaBotoesCalendario() {
            const listaBotoes = $('button[class*="fc-button"]');
            const mdButton = "md-button";
            listaBotoes.each(function (i) {
                if (!$(this).hasClass(mdButton))
                    $(this).addClass(mdButton);
            });
        }
    }]);
})();<|endoftext|>


<commit_before>if (Meteor.isServer) {
    Meteor.startup(function() {

        var cheerio = Meteor.npmRequire('cheerio');
        SyncedCron.add({
            name: 'Get new Mario Maker levels',
            schedule: function(parser) {
                // parser is a later.parse object
                return parser.text('every 2 mins');
            },
            job: function() {
                $ = cheerio.load(Meteor.http.get("https://miiverse.nintendo.net/titles/6437256808751874777/6437256808751874782/in_game").content)
                var posts = $('div[class="post post-subtype-default trigger with-image"]');
                var postId;
                var user = {};
                var level = {};
                var contentText = "";
                var codeOffset = 0;
                var tagString = "";
                _.each(posts, function(post) {
                    contentText = $('.post-content-text', post).text();
                    codeOffset = contentText.search(/\(/);
                    level.code = contentText.substring(codeOffset);
                    level.title = contentText.substring(0, codeOffset);
                    tagString = contentText.match(/\[.*\]/g) ? contentText.match(/\[.*\]/g)[0] : "";
                    if (tagString && !Levels.findOne({
                        code: level.code
                    })) {
                        tagString = tagString.replace(/\[|]|\s/g, "").toLowerCase();
                        level.tags = tagString.split(';');
                        level.date = new Date();
                        level.postId = post.id;
                        user.icon = $('.icon', post).attr('src');
                        user.name = $('p a[href*=users]', post).text();
                        user.page = $('p a[href*=users]', post).attr('href');
                        level.icon = $('img[src*=cloud]', post).attr('src');
                        level.user = user;
                        Levels.insert(level);

                        _.each(level.tags, function(tag) {
                            var tagFound = Tags.findOne({
                                tag: tag
                            });
                            if (tagFound) {
                                Tags.update({
                                    _id: tagFound._id
                                }, {
                                    $inc: {
                                        count: 1
                                    }
                                });
                            } else {
                                Tags.insert({
                                    tag: tag,
                                    count: 1
                                });
                            }
                        });
                    }

                });
            }
        });

        SyncedCron.add({
            name: 'Curate removed levels',
            schedule: function(parser) {
                // parser is a later.parse object
                return parser.text('every 2 mins');
            },
            job: function() {
                Levels.find().fetch().forEach((level) => {
                    try {

                        Meteor.http.get(level.icon);

                    } catch (e) {
                        Levels.remove({
                            _id: level._id
                        });
                    }

                });
            }
        });
        SyncedCron.start();
    });
}<commit_msg>Changing curate job to every 24 hours.<commit_after>if (Meteor.isServer) {
    Meteor.startup(function() {

        var cheerio = Meteor.npmRequire('cheerio');
        SyncedCron.add({
            name: 'Get new Mario Maker levels',
            schedule: function(parser) {
                // parser is a later.parse object
                return parser.text('every 2 mins');
            },
            job: function() {
                $ = cheerio.load(Meteor.http.get("https://miiverse.nintendo.net/titles/6437256808751874777/6437256808751874782/in_game").content)
                var posts = $('div[class="post post-subtype-default trigger with-image"]');
                var postId;
                var user = {};
                var level = {};
                var contentText = "";
                var codeOffset = 0;
                var tagString = "";
                _.each(posts, function(post) {
                    contentText = $('.post-content-text', post).text();
                    codeOffset = contentText.search(/\(/);
                    level.code = contentText.substring(codeOffset);
                    level.title = contentText.substring(0, codeOffset);
                    tagString = contentText.match(/\[.*\]/g) ? contentText.match(/\[.*\]/g)[0] : "";
                    if (tagString && !Levels.findOne({
                        code: level.code
                    })) {
                        tagString = tagString.replace(/\[|]|\s/g, "").toLowerCase();
                        level.tags = tagString.split(';');
                        level.date = new Date();
                        level.postId = post.id;
                        user.icon = $('.icon', post).attr('src');
                        user.name = $('p a[href*=users]', post).text();
                        user.page = $('p a[href*=users]', post).attr('href');
                        level.icon = $('img[src*=cloud]', post).attr('src');
                        level.user = user;
                        Levels.insert(level);

                        _.each(level.tags, function(tag) {
                            var tagFound = Tags.findOne({
                                tag: tag
                            });
                            if (tagFound) {
                                Tags.update({
                                    _id: tagFound._id
                                }, {
                                    $inc: {
                                        count: 1
                                    }
                                });
                            } else {
                                Tags.insert({
                                    tag: tag,
                                    count: 1
                                });
                            }
                        });
                    }

                });
            }
        });

        SyncedCron.add({
            name: 'Curate removed levels',
            schedule: function(parser) {
                // parser is a later.parse object
                return parser.text('every 24 hours');
            },
            job: function() {
                Levels.find().fetch().forEach((level) => {
                    try {

                        Meteor.http.get(level.icon);

                    } catch (e) {
                        Levels.remove({
                            _id: level._id
                        });
                    }

                });
            }
        });
        SyncedCron.start();
    });
}<|endoftext|>


<commit_before>module.exports = function(grunt) {

  grunt.initConfig({

    requirejs: {
      almond: false,
      replaceRequireScript: [{
        files: ['index.html'],
        module: 'main'
      }],
      modules: [{name: 'main'}],
      dir: 'js',
      keepBuildDir: true,
      baseUrl: 'js',
      inlineText: true,
      wrap: true,
      mainConfigFile: 'js/main.js',
      paths: {
        jquery: 'empty:',
        remotestorage: 'empty:'
      },
      stubModules: [
        'utils/log',
        'text'
      ]
    },

    clean: {
      build: {
        dirs: [
          'js/collections',
          'js/models',
          'js/templates',
          'js/utils',
          'js/views'
        ],
        files: [
          'js/build.txt',
          'js/litewrite.js',
          './grunt.js',
          'grunt'
        ]
      }
    },

    watch: {
        files: [],
        tasks: ''
    }

  });

  grunt.loadNpmTasks('grunt-requirejs');
  grunt.loadNpmTasks('grunt-cleanx');

  grunt.registerTask('build', 'requirejs clean');

  grunt.registerTask('default', 'server watch');

};<commit_msg>forgot something in e2b32ce28d96b367adf46cca8ba2faaf4600ea83<commit_after>module.exports = function(grunt) {

  grunt.initConfig({

    requirejs: {
      almond: false,
      replaceRequireScript: [{
        files: ['index.html'],
        module: 'main'
      }],
      modules: [{name: 'main'}],
      dir: 'js',
      keepBuildDir: true,
      baseUrl: 'js',
      inlineText: true,
      wrap: true,
      mainConfigFile: 'js/main.js',
      paths: {
        jquery: 'empty:',
        remotestorage: 'empty:'
      },
      stubModules: [
        'text'
      ]
    },

    clean: {
      build: {
        dirs: [
          'js/collections',
          'js/models',
          'js/templates',
          'js/utils',
          'js/views'
        ],
        files: [
          'js/build.txt',
          'js/litewrite.js',
          './grunt.js',
          'grunt'
        ]
      }
    },

    watch: {
        files: [],
        tasks: ''
    }

  });

  grunt.loadNpmTasks('grunt-requirejs');
  grunt.loadNpmTasks('grunt-cleanx');

  grunt.registerTask('build', 'requirejs clean');

  grunt.registerTask('default', 'server watch');

};<|endoftext|>


<commit_before>app.controller("AppointmentsCtrl", function($scope, $state, $ionicPopup, $rootScope, barberInfo) {

  //Create index for DB once
  localAptDB.createIndex({
    index: {
      fields: ['date','barber']
    }
  });

  $scope.data = {};
  $scope.data.date = new moment();
  $scope.data.alarm = true;

  $scope.barber = barberInfo.getBarber();
  $scope.appointments = [];
  $scope.events = [];

  $scope.eqTime = function(atime) {
    return atime === $scope.data.date;
  };

  //Functions
  $scope.back = function() {
    $state.go('account');
  };

  $scope.schedule = function(apm) {
    // An elaborate, custom popup
    // console.log(apm.start_time);
    var myPopup = $ionicPopup.show({
      templateUrl: 'templates/schedule.html',
      title: apm.start_time,
      scope: $scope,
      buttons: [{
        text: 'Cancel',
        onTap: function(e) {
          return 'canceled';
        }
      }, {
        text: 'Confirm',
        type: 'button-assertive',
        onTap: function(e) {
          //  alert($scope.newDate.date);
          if (!$scope.data.name || !$scope.data.phone) {
            //don't allow the user to close unless he enters wifi password
            e.preventDefault();
          } else {
            return apm;
          }
        }
      }, ]
    });

    myPopup.then(function(res) {
      //  alert($scope.newDate.date);
      // console.log(res);
      if (res !== 'canceled') {

        var i = $scope.data.date.format('YYYY-MM-DDT');
        var j = apm.start_time;

        var k = i + j;
        k = moment(k, "YYYY-MM-DDTh:mm a");

        console.log('putting data');
        console.log(res.start_time);
        localAptDB.put({
          _id: k.format() + '-' + apm.slot_num + '-' + barberInfo.getBarber(),
          slot_num: apm.slot_num,
          client_name: $scope.data.name,
          client_phone: $scope.data.phone,
          barber: $scope.barber,
          date: k.format('YYYY-MM-DD'),
          time: k.format('h:mm a'),
          alarm: $scope.data.alarm,
          sms_0: false,
          sms_1: false,
          done: false
        }).then(function(response) {
          // console.log('Complete!');
          // Show some pops up fancy stuffs here, also go back to login.
          // console.log(response);
          // TODO: RELOAD Appointment

          $state.go('appointments');
        }).catch(function(err) {
          console.log(err);
        });
      }
    });
  };

  $scope.delete = function(id) {
    var confirmPopup = $ionicPopup.confirm({
      title: 'Are you sure you want to cancel?',
      // template: 'Are you sure you want to cancel this appointment?',
      buttons: [{
        text: 'Cancel',
        type: 'button-stable'
      }, {
        text: 'Yes',
        type: 'button-assertive',
        onTap: function(e) {
          return true;
        }
      }]
    });
    confirmPopup.then(function(res) {
      console.log(res);
      if (res) {
        console.log('about to delete');
        remoteAptDB.get(id).then(function(doc) {
          return remoteAptDB.remove(doc);
        }).then(function(result) {
          // handle result
          console.log('Deleted Document!');
        }).catch(function(err) {
          console.log(err);
        });
      } else {
        //canceled
        console.log('canceled!');
      }
    });
  };

  localAptDB.changes({
    live: true
  }).on('change', function(change) {
    // var nao = moment().format().substring(0, 13);
    // localAptDB.allDocs({
    //   include_docs: true,
    //   startkey: nao, //YMD
    //   endkey: barberInfo.getBarber()
    // }, function(err, response) {
    //   if (err) {
    //     return console.log(err);
    //   }
    //   // appointments = response.rows;
    //   $rootScope.$apply(); // <--- better call this!
    // });
  }).on('create', function(change) {
    // console.log("Appointments:");
    // console.log(change);
  }).on('delete', function(change) {

  });
  // Flex Calendar-------------------------------------------------------

  var populate = function Populate(date) {
    console.log('called populate');
    // body...
    // console.log(date);
    $scope.data.date = moment(date);
    console.log( $scope.data.date);
    var today = $scope.data.date.hours(9);
    for (var i = 0; i < 14; i++) {
      $scope.appointments[i] = {
        slot_num: i,
        date: today.format('YYYY-MM-DD'),
        start_time: today.add((45 * i), 'minutes').format('h:mm a'),
        end_time: today.add(45, 'minutes').format('h:mm a')
      };
      today.subtract((45 * i) + 45, 'minutes');
    }
  };

  // Process results and return completed appointments array***
  var process = function Process(date) {
    // console.log('call process');
    populate(date);
    // console.log(moment(date).format('YYYY-MM-DD'));
    // console.log(barberInfo.getBarber());
    var searchDate = moment(date).format("YYYY-MM-DD");
    // console.log('searchDate: ' + searchDate);
    localAptDB.find({
      selector: {
        date: searchDate.toString(),
        //TODO: Change to dynamic barber name
        barber: 'Gabino'
      }
    }).then(function(res) {
      // yo, a result
      console.log(res);
      for (var i = 0; i < res.docs.length; i++) {
        $scope.appointments[res.docs[i].slot_num] = res.docs[i];
      }
      $scope.$apply();
    }).catch(function(err) {
      // ouch, an error
      console.log('Find Error:');
      console.log(err);
    });
  };

  // 1st Population Loop for 1st time user.
  process(moment().format('YYYY-MM-DD'));

  $scope.options = {
    // defaultDate: new Date(yyyy, mm, dd),
    // minDate: new Date(2015, 06, 12),
    // maxDate: new Date(2015, 12, 31),
    dayNamesLength: 3, // 1 for "M", 2 for "Mo", 3 for "Mon"; 9 will show full day names. Default is 1.
    mondayIsFirstDay: true, //set monday as first day of week. Default is false
    eventClick: function(date_obj) {
      // Process data
      process(date_obj);
      // Re-populate with Appointment
    },
    dateClick: function(date_obj) {
      // apts = process(new moment(date_obj));
      // Populate with Blank field
      // populate(date_obj);
      process(date_obj);
    },
    changeMonth: function(month, year) {
      console.log(month, year);
    },
  };

  // Change this into DB.getall Appointment
  // $scope.events = [{
  //   date: moment()
  // }, {
  //   date: new Date(2015, 7, 16)
  // }];
  // $scope.events = $scope.appointments;
});
<commit_msg>minor refactoring<commit_after>app.controller("AppointmentsCtrl", function($scope, $state, $ionicPopup, $rootScope, barberInfo) {

  //Create index for DB once
  localAptDB.createIndex({
    index: {
      fields: ['date','barber']
    }
  });

  $scope.data = {};
  $scope.data.date = new moment();
  $scope.data.alarm = true;

  $scope.barber = barberInfo.getBarber();
  $scope.appointments = [];
  $scope.events = [];

  $scope.eqTime = function(atime) {
    return atime === $scope.data.date;
  };

  //Functions
  $scope.back = function() {
    $state.go('account');
  };

  $scope.schedule = function(apm) {
    // An elaborate, custom popup
    // console.log(apm.start_time);
    var myPopup = $ionicPopup.show({
      templateUrl: 'templates/schedule.html',
      title: apm.start_time,
      scope: $scope,
      buttons: [{
        text: 'Cancel',
        onTap: function(e) {
          return 'canceled';
        }
      }, {
        text: 'Confirm',
        type: 'button-assertive',
        onTap: function(e) {
          //  alert($scope.newDate.date);
          if (!$scope.data.name || !$scope.data.phone) {
            //don't allow the user to close unless he enters wifi password
            e.preventDefault();
          } else {
            return apm;
          }
        }
      }, ]
    });

    myPopup.then(function(res) {
      //  alert($scope.newDate.date);
      // console.log(res);
      if (res !== 'canceled') {

        var i = $scope.data.date.format('YYYY-MM-DDT');
        var j = apm.start_time;

        var k = i + j;
        k = moment(k, "YYYY-MM-DDTh:mm a");

        console.log('putting data');
        console.log(res.start_time);
        localAptDB.put({
          _id: k.format() + '-' + apm.slot_num + '-' + barberInfo.getBarber(),
          slot_num: apm.slot_num,
          client_name: $scope.data.name,
          client_phone: $scope.data.phone,
          barber: $scope.barber,
          date: k.format('YYYY-MM-DD'),
          time: k.format('h:mm a'),
          alarm: $scope.data.alarm,
          sms_0: false,
          sms_1: false,
          done: false
        }).then(function(response) {
          // console.log('Complete!');
          // Show some pops up fancy stuffs here, also go back to login.
          // console.log(response);
          // TODO: RELOAD Appointment

          $state.go('appointments');
        }).catch(function(err) {
          console.log(err);
        });
      }
    });
  };

  $scope.delete = function(id) {
    var confirmPopup = $ionicPopup.confirm({
      title: 'Are you sure you want to cancel?',
      // template: 'Are you sure you want to cancel this appointment?',
      buttons: [{
        text: 'Cancel',
        type: 'button-stable'
      }, {
        text: 'Yes',
        type: 'button-assertive',
        onTap: function(e) {
          return true;
        }
      }]
    });
    confirmPopup.then(function(res) {
      console.log(res);
      if (res) {
        console.log('about to delete');
        remoteAptDB.get(id).then(function(doc) {
          return remoteAptDB.remove(doc);
        }).then(function(result) {
          // handle result
          console.log('Deleted Document!');
        }).catch(function(err) {
          console.log(err);
        });
      } else {
        //canceled
        console.log('canceled!');
      }
    });
  };

  // Flex Calendar-------------------------------------------------------

  var populate = function Populate(date) {
    console.log('called populate');
    // body...
    // console.log(date);
    $scope.data.date = moment(date);
    console.log( $scope.data.date);
    var today = $scope.data.date.hours(9);
    for (var i = 0; i < 14; i++) {
      $scope.appointments[i] = {
        slot_num: i,
        date: today.format('YYYY-MM-DD'),
        start_time: today.add((45 * i), 'minutes').format('h:mm a'),
        end_time: today.add(45, 'minutes').format('h:mm a')
      };
      today.subtract((45 * i) + 45, 'minutes');
    }
  };

  // Process results and return completed appointments array***
  var process = function Process(date) {
    // console.log('call process');
    populate(date);
    // console.log(moment(date).format('YYYY-MM-DD'));
    // console.log(barberInfo.getBarber());
    var searchDate = moment(date).format("YYYY-MM-DD");
    // console.log('searchDate: ' + searchDate);
    localAptDB.find({
      selector: {
        date: searchDate.toString(),
        //TODO: Change to dynamic barber name
        barber: 'Gabino'
      }
    }).then(function(res) {
      // yo, a result
      console.log(res);
      for (var i = 0; i < res.docs.length; i++) {
        $scope.appointments[res.docs[i].slot_num] = res.docs[i];
      }
      $scope.$apply();
    }).catch(function(err) {
      // ouch, an error
      console.log('Find Error:');
      console.log(err);
    });
  };

  // 1st Population Loop for 1st time user.
  process(moment().format('YYYY-MM-DD'));

  $scope.options = {
    // defaultDate: new Date(yyyy, mm, dd),
    // minDate: new Date(2015, 06, 12),
    // maxDate: new Date(2015, 12, 31),
    dayNamesLength: 3, // 1 for "M", 2 for "Mo", 3 for "Mon"; 9 will show full day names. Default is 1.
    mondayIsFirstDay: true, //set monday as first day of week. Default is false
    eventClick: function(date_obj) {
      // Process data
      process(date_obj);
      // Re-populate with Appointment
    },
    dateClick: function(date_obj) {
      // apts = process(new moment(date_obj));
      // Populate with Blank field
      // populate(date_obj);
      process(date_obj);
    },
    changeMonth: function(month, year) {
      console.log(month, year);
    },
  };

  // Change this into DB.getall Appointment
  // $scope.events = [{
  //   date: moment()
  // }, {
  //   date: new Date(2015, 7, 16)
  // }];
  // $scope.events = $scope.appointments;
});
<|endoftext|>