<commit_before>/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2015, 2016, 2018, 2019, 2021 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 { EventStatus } from 'matrix-js-sdk/src/models/event';

import { MatrixClientPeg } from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import Resend from '../../../Resend';
import SettingsStore from '../../../settings/SettingsStore';
import { isUrlPermitted } from '../../../HtmlUtils';
import { isContentActionable } from '../../../utils/EventUtils';
import { MenuItem } from "../../structures/ContextMenu";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
import ForwardDialog from "../dialogs/ForwardDialog";
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import { Action } from "../../../dispatcher/actions";

export function canCancel(eventStatus) {
    return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
}

@replaceableComponent("views.context_menus.MessageContextMenu")
export default class MessageContextMenu extends React.Component {
    static propTypes = {
        /* the MatrixEvent associated with the context menu */
        mxEvent: PropTypes.object.isRequired,

        /* an optional EventTileOps implementation that can be used to unhide preview widgets */
        eventTileOps: PropTypes.object,

        /* an optional function to be called when the user clicks collapse thread, if not provided hide button */
        collapseReplyThread: PropTypes.func,

        /* callback called when the menu is dismissed */
        onFinished: PropTypes.func,

        /* if the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding) */
        onCloseDialog: PropTypes.func,
    };

    state = {
        canRedact: false,
        canPin: false,
    };

    componentDidMount() {
        MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions);
        this._checkPermissions();
    }

    componentWillUnmount() {
        const cli = MatrixClientPeg.get();
        if (cli) {
            cli.removeListener('RoomMember.powerLevel', this._checkPermissions);
        }
    }

    _checkPermissions = () => {
        const cli = MatrixClientPeg.get();
        const room = cli.getRoom(this.props.mxEvent.getRoomId());

        // We explicitly decline to show the redact option on ACL events as it has a potential
        // to obliterate the room - https://github.com/matrix-org/synapse/issues/4042
        // Similarly for encryption events, since redacting them "breaks everything"
        const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId)
            && this.props.mxEvent.getType() !== EventType.RoomServerAcl
            && this.props.mxEvent.getType() !== EventType.RoomEncryption;
        let canPin = room.currentState.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli);

        // HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
        if (!SettingsStore.getValue("feature_pinning")) canPin = false;

        this.setState({canRedact, canPin});
    };

    _isPinned() {
        const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
        const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, '');
        if (!pinnedEvent) return false;
        const content = pinnedEvent.getContent();
        return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
    }

    onResendReactionsClick = () => {
        for (const reaction of this._getUnsentReactions()) {
            Resend.resend(reaction);
        }
        this.closeMenu();
    };

    onReportEventClick = () => {
        const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog");
        Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
            mxEvent: this.props.mxEvent,
        }, 'mx_Dialog_reportEvent');
        this.closeMenu();
    };

    onViewSourceClick = () => {
        const ViewSource = sdk.getComponent('structures.ViewSource');
        Modal.createTrackedDialog('View Event Source', '', ViewSource, {
            mxEvent: this.props.mxEvent,
        }, 'mx_Dialog_viewsource');
        this.closeMenu();
    };

    onRedactClick = () => {
        const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
        Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
            onFinished: async (proceed, reason) => {
                if (!proceed) return;

                const cli = MatrixClientPeg.get();
                try {
                    if (this.props.onCloseDialog) this.props.onCloseDialog();
                    await cli.redactEvent(
                        this.props.mxEvent.getRoomId(),
                        this.props.mxEvent.getId(),
                        undefined,
                        reason ? { reason } : {},
                    );
                } catch (e) {
                    const code = e.errcode || e.statusCode;
                    // only show the dialog if failing for something other than a network error
                    // (e.g. no errcode or statusCode) as in that case the redactions end up in the
                    // detached queue and we show the room status bar to allow retry
                    if (typeof code !== "undefined") {
                        const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
                        // display error message stating you couldn't delete this.
                        Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, {
                            title: _t('Error'),
                            description: _t('You cannot delete this message. (%(code)s)', {code}),
                        });
                    }
                }
            },
        }, 'mx_Dialog_confirmredact');
        this.closeMenu();
    };

    onForwardClick = () => {
        Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
            matrixClient: MatrixClientPeg.get(),
            event: this.props.mxEvent,
            permalinkCreator: this.props.permalinkCreator,
        });
        this.closeMenu();
    };

    onPinClick = () => {
        const cli = MatrixClientPeg.get();
        const room = cli.getRoom(this.props.mxEvent.getRoomId());
        const eventId = this.props.mxEvent.getId();

        const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.pinned || [];
        if (pinnedIds.includes(eventId)) {
            pinnedIds.splice(pinnedIds.indexOf(eventId), 1);
        } else {
            pinnedIds.push(eventId);
            cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
                event_ids: [
                    ...room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids,
                    eventId,
                ],
            });
        }
        cli.sendStateEvent(this.props.mxEvent.getRoomId(), EventType.RoomPinnedEvents, { pinned: pinnedIds }, "");
        this.closeMenu();
    };

    closeMenu = () => {
        if (this.props.onFinished) this.props.onFinished();
    };

    onUnhidePreviewClick = () => {
        if (this.props.eventTileOps) {
            this.props.eventTileOps.unhideWidget();
        }
        this.closeMenu();
    };

    onQuoteClick = () => {
        dis.dispatch<ComposerInsertPayload>({
            action: Action.ComposerInsert,
            event: this.props.mxEvent,
        });
        this.closeMenu();
    };

    onPermalinkClick = (e: Event) => {
        e.preventDefault();
        const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
        Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
            target: this.props.mxEvent,
            permalinkCreator: this.props.permalinkCreator,
        });
        this.closeMenu();
    };

    onCollapseReplyThreadClick = () => {
        this.props.collapseReplyThread();
        this.closeMenu();
    };

    _getReactions(filter) {
        const cli = MatrixClientPeg.get();
        const room = cli.getRoom(this.props.mxEvent.getRoomId());
        const eventId = this.props.mxEvent.getId();
        return room.getPendingEvents().filter(e => {
            const relation = e.getRelation();
            return relation &&
                relation.rel_type === "m.annotation" &&
                relation.event_id === eventId &&
                filter(e);
        });
    }

    _getPendingReactions() {
        return this._getReactions(e => canCancel(e.status));
    }

    _getUnsentReactions() {
        return this._getReactions(e => e.status === EventStatus.NOT_SENT);
    }

    render() {
        const cli = MatrixClientPeg.get();
        const me = cli.getUserId();
        const mxEvent = this.props.mxEvent;
        const eventStatus = mxEvent.status;
        const unsentReactionsCount = this._getUnsentReactions().length;
        let resendReactionsButton;
        let redactButton;
        let forwardButton;
        let pinButton;
        let unhidePreviewButton;
        let externalURLButton;
        let quoteButton;
        let collapseReplyThread;

        // status is SENT before remote-echo, null after
        const isSent = !eventStatus || eventStatus === EventStatus.SENT;
        if (!mxEvent.isRedacted()) {
            if (unsentReactionsCount !== 0) {
                resendReactionsButton = (
                    <MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendReactionsClick}>
                        { _t('Resend %(unsentCount)s reaction(s)', {unsentCount: unsentReactionsCount}) }
                    </MenuItem>
                );
            }
        }

        if (isSent && this.state.canRedact) {
            redactButton = (
                <MenuItem className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
                    { _t('Remove') }
                </MenuItem>
            );
        }

        if (isContentActionable(mxEvent)) {
            forwardButton = (
                <MenuItem className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
                    { _t('Forward Message') }
                </MenuItem>
            );

            if (this.state.canPin) {
                pinButton = (
                    <MenuItem className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
                        { this._isPinned() ? _t('Unpin Message') : _t('Pin Message') }
                    </MenuItem>
                );
            }
        }

        const viewSourceButton = (
            <MenuItem className="mx_MessageContextMenu_field" onClick={this.onViewSourceClick}>
                { _t('View Source') }
            </MenuItem>
        );

        if (this.props.eventTileOps) {
            if (this.props.eventTileOps.isWidgetHidden()) {
                unhidePreviewButton = (
                    <MenuItem className="mx_MessageContextMenu_field" onClick={this.onUnhidePreviewClick}>
                        { _t('Unhide Preview') }
                    </MenuItem>
                );
            }
        }

        let permalink;
        if (this.props.permalinkCreator) {
            permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
        }
        // XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
        const permalinkButton = (
            <MenuItem
                element="a"
                className="mx_MessageContextMenu_field"
                onClick={this.onPermalinkClick}
                href={permalink}
                target="_blank"
                rel="noreferrer noopener"
            >
                { mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
                    ? _t('Share Permalink') : _t('Share Message') }
            </MenuItem>
        );

        if (this.props.eventTileOps) { // this event is rendered using TextualBody
            quoteButton = (
                <MenuItem className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}>
                    { _t('Quote') }
                </MenuItem>
            );
        }

        // Bridges can provide a 'external_url' to link back to the source.
        if (
            typeof(mxEvent.event.content.external_url) === "string" &&
            isUrlPermitted(mxEvent.event.content.external_url)
        ) {
            externalURLButton = (
                <MenuItem
                    element="a"
                    className="mx_MessageContextMenu_field"
                    target="_blank"
                    rel="noreferrer noopener"
                    onClick={this.closeMenu}
                    href={mxEvent.event.content.external_url}
                >
                    { _t('Source URL') }
                </MenuItem>
            );
        }

        if (this.props.collapseReplyThread) {
            collapseReplyThread = (
                <MenuItem className="mx_MessageContextMenu_field" onClick={this.onCollapseReplyThreadClick}>
                    { _t('Collapse Reply Thread') }
                </MenuItem>
            );
        }

        let reportEventButton;
        if (mxEvent.getSender() !== me) {
            reportEventButton = (
                <MenuItem className="mx_MessageContextMenu_field" onClick={this.onReportEventClick}>
                    { _t('Report Content') }
                </MenuItem>
            );
        }

        return (
            <div className="mx_MessageContextMenu">
                { resendReactionsButton }
                { redactButton }
                { forwardButton }
                { pinButton }
                { viewSourceButton }
                { unhidePreviewButton }
                { permalinkButton }
                { quoteButton }
                { externalURLButton }
                { collapseReplyThread }
                { reportEventButton }
            </div>
        );
    }
}
<commit_msg>Remove ComposerInsertPayload as this is a JS file<commit_after>/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2015, 2016, 2018, 2019, 2021 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 { EventStatus } from 'matrix-js-sdk/src/models/event';

import { MatrixClientPeg } from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import Resend from '../../../Resend';
import SettingsStore from '../../../settings/SettingsStore';
import { isUrlPermitted } from '../../../HtmlUtils';
import { isContentActionable } from '../../../utils/EventUtils';
import { MenuItem } from "../../structures/ContextMenu";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
import ForwardDialog from "../dialogs/ForwardDialog";
import { Action } from "../../../dispatcher/actions";

export function canCancel(eventStatus) {
    return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
}

@replaceableComponent("views.context_menus.MessageContextMenu")
export default class MessageContextMenu extends React.Component {
    static propTypes = {
        /* the MatrixEvent associated with the context menu */
        mxEvent: PropTypes.object.isRequired,

        /* an optional EventTileOps implementation that can be used to unhide preview widgets */
        eventTileOps: PropTypes.object,

        /* an optional function to be called when the user clicks collapse thread, if not provided hide button */
        collapseReplyThread: PropTypes.func,

        /* callback called when the menu is dismissed */
        onFinished: PropTypes.func,

        /* if the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding) */
        onCloseDialog: PropTypes.func,
    };

    state = {
        canRedact: false,
        canPin: false,
    };

    componentDidMount() {
        MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions);
        this._checkPermissions();
    }

    componentWillUnmount() {
        const cli = MatrixClientPeg.get();
        if (cli) {
            cli.removeListener('RoomMember.powerLevel', this._checkPermissions);
        }
    }

    _checkPermissions = () => {
        const cli = MatrixClientPeg.get();
        const room = cli.getRoom(this.props.mxEvent.getRoomId());

        // We explicitly decline to show the redact option on ACL events as it has a potential
        // to obliterate the room - https://github.com/matrix-org/synapse/issues/4042
        // Similarly for encryption events, since redacting them "breaks everything"
        const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId)
            && this.props.mxEvent.getType() !== EventType.RoomServerAcl
            && this.props.mxEvent.getType() !== EventType.RoomEncryption;
        let canPin = room.currentState.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli);

        // HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
        if (!SettingsStore.getValue("feature_pinning")) canPin = false;

        this.setState({canRedact, canPin});
    };

    _isPinned() {
        const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
        const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, '');
        if (!pinnedEvent) return false;
        const content = pinnedEvent.getContent();
        return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
    }

    onResendReactionsClick = () => {
        for (const reaction of this._getUnsentReactions()) {
            Resend.resend(reaction);
        }
        this.closeMenu();
    };

    onReportEventClick = () => {
        const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog");
        Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
            mxEvent: this.props.mxEvent,
        }, 'mx_Dialog_reportEvent');
        this.closeMenu();
    };

    onViewSourceClick = () => {
        const ViewSource = sdk.getComponent('structures.ViewSource');
        Modal.createTrackedDialog('View Event Source', '', ViewSource, {
            mxEvent: this.props.mxEvent,
        }, 'mx_Dialog_viewsource');
        this.closeMenu();
    };

    onRedactClick = () => {
        const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
        Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
            onFinished: async (proceed, reason) => {
                if (!proceed) return;

                const cli = MatrixClientPeg.get();
                try {
                    if (this.props.onCloseDialog) this.props.onCloseDialog();
                    await cli.redactEvent(
                        this.props.mxEvent.getRoomId(),
                        this.props.mxEvent.getId(),
                        undefined,
                        reason ? { reason } : {},
                    );
                } catch (e) {
                    const code = e.errcode || e.statusCode;
                    // only show the dialog if failing for something other than a network error
                    // (e.g. no errcode or statusCode) as in that case the redactions end up in the
                    // detached queue and we show the room status bar to allow retry
                    if (typeof code !== "undefined") {
                        const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
                        // display error message stating you couldn't delete this.
                        Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, {
                            title: _t('Error'),
                            description: _t('You cannot delete this message. (%(code)s)', {code}),
                        });
                    }
                }
            },
        }, 'mx_Dialog_confirmredact');
        this.closeMenu();
    };

    onForwardClick = () => {
        Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
            matrixClient: MatrixClientPeg.get(),
            event: this.props.mxEvent,
            permalinkCreator: this.props.permalinkCreator,
        });
        this.closeMenu();
    };

    onPinClick = () => {
        const cli = MatrixClientPeg.get();
        const room = cli.getRoom(this.props.mxEvent.getRoomId());
        const eventId = this.props.mxEvent.getId();

        const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.pinned || [];
        if (pinnedIds.includes(eventId)) {
            pinnedIds.splice(pinnedIds.indexOf(eventId), 1);
        } else {
            pinnedIds.push(eventId);
            cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
                event_ids: [
                    ...room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids,
                    eventId,
                ],
            });
        }
        cli.sendStateEvent(this.props.mxEvent.getRoomId(), EventType.RoomPinnedEvents, { pinned: pinnedIds }, "");
        this.closeMenu();
    };

    closeMenu = () => {
        if (this.props.onFinished) this.props.onFinished();
    };

    onUnhidePreviewClick = () => {
        if (this.props.eventTileOps) {
            this.props.eventTileOps.unhideWidget();
        }
        this.closeMenu();
    };

    onQuoteClick = () => {
        dis.dispatch({
            action: Action.ComposerInsert,
            event: this.props.mxEvent,
        });
        this.closeMenu();
    };

    onPermalinkClick = (e: Event) => {
        e.preventDefault();
        const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
        Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
            target: this.props.mxEvent,
            permalinkCreator: this.props.permalinkCreator,
        });
        this.closeMenu();
    };

    onCollapseReplyThreadClick = () => {
        this.props.collapseReplyThread();
        this.closeMenu();
    };

    _getReactions(filter) {
        const cli = MatrixClientPeg.get();
        const room = cli.getRoom(this.props.mxEvent.getRoomId());
        const eventId = this.props.mxEvent.getId();
        return room.getPendingEvents().filter(e => {
            const relation = e.getRelation();
            return relation &&
                relation.rel_type === "m.annotation" &&
                relation.event_id === eventId &&
                filter(e);
        });
    }

    _getPendingReactions() {
        return this._getReactions(e => canCancel(e.status));
    }

    _getUnsentReactions() {
        return this._getReactions(e => e.status === EventStatus.NOT_SENT);
    }

    render() {
        const cli = MatrixClientPeg.get();
        const me = cli.getUserId();
        const mxEvent = this.props.mxEvent;
        const eventStatus = mxEvent.status;
        const unsentReactionsCount = this._getUnsentReactions().length;
        let resendReactionsButton;
        let redactButton;
        let forwardButton;
        let pinButton;
        let unhidePreviewButton;
        let externalURLButton;
        let quoteButton;
        let collapseReplyThread;

        // status is SENT before remote-echo, null after
        const isSent = !eventStatus || eventStatus === EventStatus.SENT;
        if (!mxEvent.isRedacted()) {
            if (unsentReactionsCount !== 0) {
                resendReactionsButton = (
                    <MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendReactionsClick}>
                        { _t('Resend %(unsentCount)s reaction(s)', {unsentCount: unsentReactionsCount}) }
                    </MenuItem>
                );
            }
        }

        if (isSent && this.state.canRedact) {
            redactButton = (
                <MenuItem className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
                    { _t('Remove') }
                </MenuItem>
            );
        }

        if (isContentActionable(mxEvent)) {
            forwardButton = (
                <MenuItem className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
                    { _t('Forward Message') }
                </MenuItem>
            );

            if (this.state.canPin) {
                pinButton = (
                    <MenuItem className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
                        { this._isPinned() ? _t('Unpin Message') : _t('Pin Message') }
                    </MenuItem>
                );
            }
        }

        const viewSourceButton = (
            <MenuItem className="mx_MessageContextMenu_field" onClick={this.onViewSourceClick}>
                { _t('View Source') }
            </MenuItem>
        );

        if (this.props.eventTileOps) {
            if (this.props.eventTileOps.isWidgetHidden()) {
                unhidePreviewButton = (
                    <MenuItem className="mx_MessageContextMenu_field" onClick={this.onUnhidePreviewClick}>
                        { _t('Unhide Preview') }
                    </MenuItem>
                );
            }
        }

        let permalink;
        if (this.props.permalinkCreator) {
            permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
        }
        // XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
        const permalinkButton = (
            <MenuItem
                element="a"
                className="mx_MessageContextMenu_field"
                onClick={this.onPermalinkClick}
                href={permalink}
                target="_blank"
                rel="noreferrer noopener"
            >
                { mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
                    ? _t('Share Permalink') : _t('Share Message') }
            </MenuItem>
        );

        if (this.props.eventTileOps) { // this event is rendered using TextualBody
            quoteButton = (
                <MenuItem className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}>
                    { _t('Quote') }
                </MenuItem>
            );
        }

        // Bridges can provide a 'external_url' to link back to the source.
        if (
            typeof(mxEvent.event.content.external_url) === "string" &&
            isUrlPermitted(mxEvent.event.content.external_url)
        ) {
            externalURLButton = (
                <MenuItem
                    element="a"
                    className="mx_MessageContextMenu_field"
                    target="_blank"
                    rel="noreferrer noopener"
                    onClick={this.closeMenu}
                    href={mxEvent.event.content.external_url}
                >
                    { _t('Source URL') }
                </MenuItem>
            );
        }

        if (this.props.collapseReplyThread) {
            collapseReplyThread = (
                <MenuItem className="mx_MessageContextMenu_field" onClick={this.onCollapseReplyThreadClick}>
                    { _t('Collapse Reply Thread') }
                </MenuItem>
            );
        }

        let reportEventButton;
        if (mxEvent.getSender() !== me) {
            reportEventButton = (
                <MenuItem className="mx_MessageContextMenu_field" onClick={this.onReportEventClick}>
                    { _t('Report Content') }
                </MenuItem>
            );
        }

        return (
            <div className="mx_MessageContextMenu">
                { resendReactionsButton }
                { redactButton }
                { forwardButton }
                { pinButton }
                { viewSourceButton }
                { unhidePreviewButton }
                { permalinkButton }
                { quoteButton }
                { externalURLButton }
                { collapseReplyThread }
                { reportEventButton }
            </div>
        );
    }
}
<|endoftext|>


<commit_before>// Copyright 2020 Google LLC
//
// 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.

'use strict';

// [START dialogflow_quickstart]

const dialogflow = require('@google-cloud/dialogflow');
const uuid = require('uuid');

/**
 * Send a query to the dialogflow agent, and return the query result.
 * @param {string} projectId The project to be used
 */
async function runSample(projectId = 'your-project-id') {
  // A unique identifier for the given session
  const sessionId = uuid.v4();

  // Create a new session
  const sessionClient = new dialogflow.SessionsClient();
  const sessionPath = sessionClient.projectAgentSessionPath(
    projectId,
    sessionId
  );

  // The text query request.
  const request = {
    session: sessionPath,
    queryInput: {
      text: {
        // The query to send to the dialogflow agent
        text: 'hello',
        // The language used by the client (en-US)
        languageCode: 'en-US',
      },
    },
  };

  // Send request and log result
  const responses = await sessionClient.detectIntent(request);
  console.log('Detected intent');
  const result = responses[0].queryResult;
  console.log(`  Query: ${result.queryText}`);
  console.log(`  Response: ${result.fulfillmentText}`);
  if (result.intent) {
    console.log(`  Intent: ${result.intent.displayName}`);
  } else {
    console.log('  No intent matched.');
  }
}
// [END dialogflow_quickstart]

const args = process.argv.slice(2);
if (args.length !== 1) {
  console.error(`
    USAGE:
       node quickstart.js <projectId>

     EXAMPLE:
       node quickstart.js my-project-id

    You can find your project ID in your Dialogflow agent settings:  https://dialogflow.com/docs/agents#settings.
  `);
  process.exit(1);
}

runSample(...args).catch(console.error);
<commit_msg>Update quickstart.js<commit_after>// Copyright 2020 Google LLC
//
// 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.

'use strict';

/**
 * List of all intents in the specified project.
 * @param {string} projectId The project to be used
 */
function main(projectId = 'YOUR_PROJECT_ID') {
  // [START dialogflow_list_intents]

  /**
   * TODO(developer): Uncomment the following lines before running the sample.
   */
  // const projectId = 'The Project ID to use, e.g. 'YOUR_GCP_ID';

  // Imports the Dialogflow library
  const dialogflow = require('@google-cloud/dialogflow');

  // Instantiates clients
  const intentsClient = new dialogflow.IntentsClient();

  async function listIntents() {
    // Construct request

    // The path to identify the agent that owns the intents.
    const projectAgentPath = intentsClient.projectAgentPath(projectId);

    console.log(projectAgentPath);

    const request = {
      parent: projectAgentPath,
    };

    // Send the request for listing intents.
    const [response] = await intentsClient.listIntents(request);
    response.forEach(intent => {
      console.log('====================');
      console.log(`Intent name: ${intent.name}`);
      console.log(`Intent display name: ${intent.displayName}`);
      console.log(`Action: ${intent.action}`);
      console.log(`Root folowup intent: ${intent.rootFollowupIntentName}`);
      console.log(`Parent followup intent: ${intent.parentFollowupIntentName}`);

      console.log('Input contexts:');
      intent.inputContextNames.forEach(inputContextName => {
        console.log(`\tName: ${inputContextName}`);
      });

      console.log('Output contexts:');
      intent.outputContexts.forEach(outputContext => {
        console.log(`\tName: ${outputContext.name}`);
      });
    });
  }

  listIntents();

  // [END dialogflow_list_intents]
}

main(...process.argv.slice(2));
<|endoftext|>


<commit_before>import 'core-js/fn/object/assign';
import injectTapEventPlugin from 'react-tap-event-plugin';

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { Router, Route, Redirect, IndexRoute, browserHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';

import Storage from 'utils/Storage';
import configureStore from 'store';

import Main from 'containers/common/Main';

import LoginView from 'views/common/LoginView';

import DashboardView from 'views/common/DashboardView';
import DashboardIndexView from 'views/common/DashboardIndexView';

import ParticipantsView from 'views/participant/ParticipantsView';
import ParticipantEditView from 'views/participant/ParticipantEditView';
import ParticipantNewView from 'views/participant/ParticipantNewView';
import ParticipantPreview from 'containers/participant/ParticipantPreview';

import ExperimentsView from 'views/experiment/ExperimentsView';
import ExperimentNewView from 'views/experiment/ExperimentNewView';
import ExperimentEditView from 'views/experiment/ExperimentEditView';
import ExperimentPreviewView from 'views/experiment/ExperimentPreviewView';

import PhotosView from 'views/photo/PhotosView';
import PhotoCreate from 'containers/photo/PhotoCreate';

import QrCodePrint from 'containers/common/QrCodePrint';
import Results from 'containers/common/Results';

const store = configureStore(browserHistory);
const history = syncHistoryWithStore(browserHistory, store);

const checkIfAuthenticated = (nextState, replace, callback) => {
  const {token} = Storage.getItem('authenticated') || {};

  if (!token) {
    replace('/login');
  }

  callback();
};

const setPageTitle = (pathname) => {
  const defaultTitle = 'Admin Panel';
  const regex = /\/dashboard\/([a-z]+)/g;
  const matched = regex.exec(pathname);

  if (matched) {
    document.title = `${defaultTitle} | ${matched[1][0].toUpperCase() + matched[1].slice(1)}`;
  } else {
    document.title = defaultTitle;
  }
};

injectTapEventPlugin();
history.listen(location => setPageTitle(location.pathname))

ReactDOM.render(
  <Provider store={store}>
    <Router history={history}>
      <Route component={Main}>
        <Redirect from="/" to="login" />
        <Route path="login" component={LoginView}/>
        <Route path="dashboard" component={DashboardView} onEnter={checkIfAuthenticated}>
          <IndexRoute component={DashboardIndexView}/>

          <Route path="experiments">
            <IndexRoute component={ExperimentsView} />
            <Route path="new" component={ExperimentNewView} />
            <Route path=":id" component={ExperimentPreviewView} />
            <Route path="edit/:id" component={ExperimentEditView} />
          </Route>

          <Route path="participants">
            <IndexRoute component={ParticipantsView} />
            <Route path="new" component={ParticipantNewView} />
            <Route path=":id" component={ParticipantPreview} />
            <Route path="edit/:id" component={ParticipantEditView} />
            <Route path=":id/qr-code" component={QrCodePrint} />
          </Route>

          <Route path="photos">
            <IndexRoute component={PhotosView} />
            <Route path="new" component={PhotoCreate} />
          </Route>

          <Route path="experiments/:experimentId/participant/:participantId/results" component={Results} />
        </Route>
      </Route>
    </Router>
  </Provider>
, document.getElementById('app'));
<commit_msg>Participant results fix.<commit_after>import 'core-js/fn/object/assign';
import injectTapEventPlugin from 'react-tap-event-plugin';

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { Router, Route, Redirect, IndexRoute, browserHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';

import Storage from 'utils/Storage';
import configureStore from 'store';

import Main from 'containers/common/Main';

import LoginView from 'views/common/LoginView';

import DashboardView from 'views/common/DashboardView';
import DashboardIndexView from 'views/common/DashboardIndexView';

import ParticipantsView from 'views/participant/ParticipantsView';
import ParticipantEditView from 'views/participant/ParticipantEditView';
import ParticipantNewView from 'views/participant/ParticipantNewView';
import ParticipantPreview from 'containers/participant/ParticipantPreview';

import ExperimentsView from 'views/experiment/ExperimentsView';
import ExperimentNewView from 'views/experiment/ExperimentNewView';
import ExperimentEditView from 'views/experiment/ExperimentEditView';
import ExperimentPreviewView from 'views/experiment/ExperimentPreviewView';

import PhotosView from 'views/photo/PhotosView';
import PhotoCreate from 'containers/photo/PhotoCreate';

import QrCodePrint from 'containers/common/QrCodePrint';
import Results from 'containers/common/Results';

const store = configureStore(browserHistory);
const history = syncHistoryWithStore(browserHistory, store);

const checkIfAuthenticated = (nextState, replace, callback) => {
  const {token} = Storage.getItem('authenticated') || {};

  if (!token) {
    replace('/login');
  }

  callback();
};

const setPageTitle = (pathname) => {
  const defaultTitle = 'Admin Panel';
  const regex = /\/dashboard\/([a-z]+)/g;
  const matched = regex.exec(pathname);

  if (matched) {
    document.title = `${defaultTitle} | ${matched[1][0].toUpperCase() + matched[1].slice(1)}`;
  } else {
    document.title = defaultTitle;
  }
};

injectTapEventPlugin();
history.listen(location => setPageTitle(location.pathname))

ReactDOM.render(
  <Provider store={store}>
    <Router history={history}>
      <Route component={Main}>
        <Redirect from="/" to="login" />
        <Route path="login" component={LoginView}/>
        <Route path="dashboard" component={DashboardView} onEnter={checkIfAuthenticated}>
          <IndexRoute component={DashboardIndexView}/>

          <Route path="experiments">
            <IndexRoute component={ExperimentsView} />
            <Route path="new" component={ExperimentNewView} />
            <Route path=":id" component={ExperimentPreviewView} />
            <Route path="edit/:id" component={ExperimentEditView} />
          </Route>

          <Route path="participants">
            <IndexRoute component={ParticipantsView} />
            <Route path="new" component={ParticipantNewView} />
            <Route path=":id" component={ParticipantPreview} />
            <Route path="edit/:id" component={ParticipantEditView} />
            <Route path=":id/qr-code" component={QrCodePrint} />
          </Route>

          <Route path="photos">
            <IndexRoute component={PhotosView} />
            <Route path="new" component={PhotoCreate} />
          </Route>

          <Route path="experiments/:experimentId/participants/:participantId/results" component={Results} />
        </Route>
      </Route>
    </Router>
  </Provider>
, document.getElementById('app'));
<|endoftext|>


<commit_before>import React, { useContext } from 'react'
import clsx from 'clsx'
import { makeStyles, useTheme } from '@material-ui/core/styles'
import Drawer from '@material-ui/core/Drawer'
import CssBaseline from '@material-ui/core/CssBaseline'
import AppBar from '@material-ui/core/AppBar'
import Toolbar from '@material-ui/core/Toolbar'
import List from '@material-ui/core/List'
import Typography from '@material-ui/core/Typography'
import Divider from '@material-ui/core/Divider'
import IconButton from '@material-ui/core/IconButton'
import MenuIcon from '@material-ui/icons/Menu'
import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'
import ChevronRightIcon from '@material-ui/icons/ChevronRight'
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText'
import InboxIcon from '@material-ui/icons/MoveToInbox'
import MailIcon from '@material-ui/icons/Mail'
import { useIntl } from 'react-intl'
import OnlineContext from 'base-shell/lib/providers/Online/Context'
import MenuContext from '../../providers/Menu/Context'
import useMediaQuery from '@material-ui/core/useMediaQuery'

const drawerWidth = 240
const offlineIndicatorHeight = 12

const useStyles = makeStyles((theme) => ({
  root: {
    width: '100%',
    display: 'flex',
    flexDirection: 'column',
  },
  appBar: {
    transition: theme.transitions.create(['margin', 'width'], {
      easing: theme.transitions.easing.sharp,
      duration: theme.transitions.duration.leavingScreen,
    }),
  },
  appBarShift: {
    width: `calc(100% - ${drawerWidth}px)`,
    marginLeft: drawerWidth,
    transition: theme.transitions.create(['margin', 'width'], {
      easing: theme.transitions.easing.easeOut,
      duration: theme.transitions.duration.enteringScreen,
    }),
  },
  menuButton: {
    marginRight: theme.spacing(2),
  },
  hide: {
    display: 'none',
  },
  drawer: {
    width: drawerWidth,
    flexShrink: 0,
  },
  drawerPaper: {
    width: drawerWidth,
  },
  drawerHeader: {
    display: 'flex',
    alignItems: 'center',
    padding: theme.spacing(0, 1),
    // necessary for content to be below app bar
    ...theme.mixins.toolbar,
    justifyContent: 'flex-end',
  },
  offlineIndicator: {
    position: 'absolute',
    display: 'flex',
    alignItems: 'center',
    backgroundColor: theme.palette.secondary.main,
    justifyContent: 'center',
    right: 0,
    left: 0,
    height: offlineIndicatorHeight,
  },
  content: {
    flexGrow: 1,
    //padding: theme.spacing(3),
    transition: theme.transitions.create('margin', {
      easing: theme.transitions.easing.sharp,
      duration: theme.transitions.duration.leavingScreen,
    }),
    marginLeft: -drawerWidth,
  },
  contentShift: {
    transition: theme.transitions.create('margin', {
      easing: theme.transitions.easing.easeOut,
      duration: theme.transitions.duration.enteringScreen,
    }),
    marginLeft: 0,
  },
}))

const Page = ({ children, pageTitle }) => {
  const isOnline = useContext(OnlineContext)
  const isDesktop = useMediaQuery('(min-width:600px)')

  const { isDesktopOpen, setDesktopOpen, setMobileOpen } = useContext(
    MenuContext
  )
  const intl = useIntl()
  let headerTitle = ''

  if (typeof pageTitle === 'string' || pageTitle instanceof String) {
    headerTitle = pageTitle
  }

  const classes = useStyles()
  const theme = useTheme()

  const handleDrawerOpen = () => {
    if (isDesktop) {
      setDesktopOpen(true)
    } else {
      setMobileOpen(true)
    }
  }

  return (

      <AppBar
        position="fixed"
        className={clsx(classes.appBar, {
          [classes.appBarShift]: isDesktop && isDesktopOpen,
        })}
      >
        <Toolbar>
          <IconButton
            color="inherit"
            aria-label="open drawer"
            onClick={handleDrawerOpen}
            edge="start"
            className={clsx(
              classes.menuButton,
              isDesktop && isDesktopOpen && classes.hide
            )}
          >
            <MenuIcon />
          </IconButton>
          <Typography variant="h6" noWrap>
            {headerTitle}
          </Typography>
        </Toolbar>
      </AppBar>

        {!isOnline && (
          <React.Fragment>

              <Typography variant="caption" noWrap>
                {intl.formatMessage({
                  id: 'offline',
                  defaultMessage: 'Offline',
                })}
              </Typography>

          </React.Fragment>
        )}
        {children}

  )
}

export default Page
<commit_msg>removing unused dependencies<commit_after>import React, { useContext } from 'react'
import clsx from 'clsx'
import { makeStyles } from '@material-ui/core/styles'
import AppBar from '@material-ui/core/AppBar'
import Toolbar from '@material-ui/core/Toolbar'
import Typography from '@material-ui/core/Typography'
import IconButton from '@material-ui/core/IconButton'
import MenuIcon from '@material-ui/icons/Menu'
import { useIntl } from 'react-intl'
import OnlineContext from 'base-shell/lib/providers/Online/Context'
import MenuContext from '../../providers/Menu/Context'
import useMediaQuery from '@material-ui/core/useMediaQuery'

const drawerWidth = 240
const offlineIndicatorHeight = 12

const useStyles = makeStyles((theme) => ({
  root: {
    width: '100%',
    display: 'flex',
    flexDirection: 'column',
  },
  appBar: {
    transition: theme.transitions.create(['margin', 'width'], {
      easing: theme.transitions.easing.sharp,
      duration: theme.transitions.duration.leavingScreen,
    }),
  },
  appBarShift: {
    width: `calc(100% - ${drawerWidth}px)`,
    marginLeft: drawerWidth,
    transition: theme.transitions.create(['margin', 'width'], {
      easing: theme.transitions.easing.easeOut,
      duration: theme.transitions.duration.enteringScreen,
    }),
  },
  menuButton: {
    marginRight: theme.spacing(2),
  },
  hide: {
    display: 'none',
  },
  drawer: {
    width: drawerWidth,
    flexShrink: 0,
  },
  drawerPaper: {
    width: drawerWidth,
  },
  drawerHeader: {
    display: 'flex',
    alignItems: 'center',
    padding: theme.spacing(0, 1),
    // necessary for content to be below app bar
    ...theme.mixins.toolbar,
    justifyContent: 'flex-end',
  },
  offlineIndicator: {
    position: 'absolute',
    display: 'flex',
    alignItems: 'center',
    backgroundColor: theme.palette.secondary.main,
    justifyContent: 'center',
    right: 0,
    left: 0,
    height: offlineIndicatorHeight,
  },
  content: {
    flexGrow: 1,
    //padding: theme.spacing(3),
    transition: theme.transitions.create('margin', {
      easing: theme.transitions.easing.sharp,
      duration: theme.transitions.duration.leavingScreen,
    }),
    marginLeft: -drawerWidth,
  },
  contentShift: {
    transition: theme.transitions.create('margin', {
      easing: theme.transitions.easing.easeOut,
      duration: theme.transitions.duration.enteringScreen,
    }),
    marginLeft: 0,
  },
}))

const Page = ({ children, pageTitle }) => {
  const isOnline = useContext(OnlineContext)
  const isDesktop = useMediaQuery('(min-width:600px)')

  const { isDesktopOpen, setDesktopOpen, setMobileOpen } = useContext(
    MenuContext
  )
  const intl = useIntl()
  let headerTitle = ''

  if (typeof pageTitle === 'string' || pageTitle instanceof String) {
    headerTitle = pageTitle
  }

  const classes = useStyles()

  const handleDrawerOpen = () => {
    if (isDesktop) {
      setDesktopOpen(true)
    } else {
      setMobileOpen(true)
    }
  }

  return (

      <AppBar
        position="fixed"
        className={clsx(classes.appBar, {
          [classes.appBarShift]: isDesktop && isDesktopOpen,
        })}
      >
        <Toolbar>
          <IconButton
            color="inherit"
            aria-label="open drawer"
            onClick={handleDrawerOpen}
            edge="start"
            className={clsx(
              classes.menuButton,
              isDesktop && isDesktopOpen && classes.hide
            )}
          >
            <MenuIcon />
          </IconButton>
          <Typography variant="h6" noWrap>
            {headerTitle}
          </Typography>
        </Toolbar>
      </AppBar>

        {!isOnline && (
          <React.Fragment>

              <Typography variant="caption" noWrap>
                {intl.formatMessage({
                  id: 'offline',
                  defaultMessage: 'Offline',
                })}
              </Typography>

          </React.Fragment>
        )}
        {children}

  )
}

export default Page
<|endoftext|>


<commit_before>import React, {PropTypes, Component, Children} from 'react';
import createInstantSearchManager from './createInstantSearchManager';

function validateNextProps(props, nextProps) {
  if (!props.searchState && nextProps.searchState) {
    throw new Error(
      'You can\'t switch <InstantSearch> from being uncontrolled to controlled'
    );
  } else if (props.searchState && !nextProps.searchState) {
    throw new Error(
      'You can\'t switch <InstantSearch> from being controlled to uncontrolled'
    );
  }
}

/* eslint valid-jsdoc: 0 */
/**
 * @description
 * `<InstantSearch>` is the root component of all react-instantsearch implementation.
 * It provides to all the connected components (aka widgets) a mean to interact
 * with the searchState.
 * @kind widget
 * @propType {string} appId - The Algolia application id.
 * @propType {string} apiKey - Your Algolia Search-Only API key.
 * @propType {string} indexName - The index in which to search.
 * @propType {object} [searchParameters] - Object containing query parameters to be sent to Algolia.
 * It will be overriden by the search parameters resolved via the widgets.
 *
 * Typical use case: setting the distinct setting is done by providing an object like: `{distinct: 1}`.
 * For more information about the kind of object that can be provided on the
 * [official API documentation](https://www.algolia.com/doc/rest-api/search#full-text-search-parameters).
 *
 * Read the [search parameters guide](guide/Search%20parameters.html).
 * @propType {func} onSearchStateChange - See [URL Routing](guide/advanced-topics.html#url-routing).
 * @propType {object} searchState - See [URL Routing](guide/advanced-topics.html#url-routing).
 * @propType {func} createURL - See [URL Routing](guide/advanced-topics.html#url-routing).
 * @example
 * import {InstantSearch, SearchBox, Hits} from 'react-instantsearch/dom';
 *
 * export default function Search() {
 *   return (
 *     <InstantSearch
 *       appId="appId"
 *       apiKey="apiKey"
 *       indexName="indexName"
 *     >
 *

 *         <SearchBox />
 *         <Hits />
 *

 *     </InstantSearch>
 *   );
 * }
 */
class InstantSearch extends Component {
  constructor(props) {
    super(props);

    this.isControlled = Boolean(props.searchState);

    const initialState = this.isControlled ? props.searchState : {};

    this.aisManager = createInstantSearchManager({
      indexName: props.indexName,
      searchParameters: props.searchParameters,
      algoliaClient: props.algoliaClient,
      initialState,
    });
  }

  componentWillReceiveProps(nextProps) {
    validateNextProps(this.props, nextProps);
    if (this.isControlled) {
      this.aisManager.onExternalStateUpdate(nextProps.searchState);
    }
  }

  getChildContext() {
    // If not already cached, cache the bound methods so that we can forward them as part
    // of the context.
    if (!this._aisContextCache) {
      this._aisContextCache = {
        ais: {
          onInternalStateUpdate: this.onWidgetsInternalStateUpdate.bind(this),
          createHrefForState: this.createHrefForState.bind(this),
        },
      };
    }

    return {
      ais: {
        ...this._aisContextCache.ais,
        store: this.aisManager.store,
        widgetsManager: this.aisManager.widgetsManager,
      },
    };
  }

  createHrefForState(searchState) {
    searchState = this.aisManager.transitionState(searchState);
    return this.isControlled && this.props.createURL ? this.props.createURL(searchState, this.getKnownKeys()) : '#';
  }

  onWidgetsInternalStateUpdate(searchState) {
    searchState = this.aisManager.transitionState(searchState);

    if (this.props.onSearchStateChange) {
      this.props.onSearchStateChange(searchState);
    }

    if (!this.isControlled) {
      this.aisManager.onExternalStateUpdate(searchState);
    }
  }

  getKnownKeys() {
    return this.aisManager.getWidgetsIds();
  }

  render() {
    const childrenCount = Children.count(this.props.children);
    const {Root, props} = this.props.root;
    if (childrenCount === 0)
      return null;
    else
      return <Root {...props}>{this.props.children}</Root>;
  }
}

InstantSearch.propTypes = {
  // @TODO: These props are currently constant.
  indexName: PropTypes.string.isRequired,

  algoliaClient: PropTypes.object.isRequired,

  searchParameters: PropTypes.object,

  createURL: PropTypes.func,

  searchState: PropTypes.object,
  onSearchStateChange: PropTypes.func,

  children: PropTypes.node,

  root: PropTypes.shape({
    Root: PropTypes.string.isRequired,
    props: PropTypes.object,
  }).isRequired,
};

InstantSearch.childContextTypes = {
  // @TODO: more precise widgets manager propType
  ais: PropTypes.object.isRequired,
};

export default InstantSearch;
<commit_msg>docs(review): grammar updates<commit_after>import React, {PropTypes, Component, Children} from 'react';
import createInstantSearchManager from './createInstantSearchManager';

function validateNextProps(props, nextProps) {
  if (!props.searchState && nextProps.searchState) {
    throw new Error(
      'You can\'t switch <InstantSearch> from being uncontrolled to controlled'
    );
  } else if (props.searchState && !nextProps.searchState) {
    throw new Error(
      'You can\'t switch <InstantSearch> from being controlled to uncontrolled'
    );
  }
}

/* eslint valid-jsdoc: 0 */
/**
 * @description
 * `<InstantSearch>` is the root component of all react-instantsearch implementations.
 * It provides all the connected components (aka widgets) a means to interact
 * with the searchState.
 * @kind widget
 * @propType {string} appId - The Algolia application id.
 * @propType {string} apiKey - Your Algolia Search-Only API key.
 * @propType {string} indexName - The index in which to search.
 * @propType {object} [searchParameters] - Object containing query parameters to be sent to Algolia.
 * It will be overriden by the search parameters resolved via the widgets.
 *
 * Typical use case: setting the distinct setting is done by providing an object like: `{distinct: 1}`.
 * For more information about the kind of object that can be provided on the
 * [official API documentation](https://www.algolia.com/doc/rest-api/search#full-text-search-parameters).
 *
 * Read the [search parameters guide](guide/Search%20parameters.html).
 * @propType {func} onSearchStateChange - See [URL Routing](guide/advanced-topics.html#url-routing).
 * @propType {object} searchState - See [URL Routing](guide/advanced-topics.html#url-routing).
 * @propType {func} createURL - See [URL Routing](guide/advanced-topics.html#url-routing).
 * @example
 * import {InstantSearch, SearchBox, Hits} from 'react-instantsearch/dom';
 *
 * export default function Search() {
 *   return (
 *     <InstantSearch
 *       appId="appId"
 *       apiKey="apiKey"
 *       indexName="indexName"
 *     >
 *

 *         <SearchBox />
 *         <Hits />
 *

 *     </InstantSearch>
 *   );
 * }
 */
class InstantSearch extends Component {
  constructor(props) {
    super(props);

    this.isControlled = Boolean(props.searchState);

    const initialState = this.isControlled ? props.searchState : {};

    this.aisManager = createInstantSearchManager({
      indexName: props.indexName,
      searchParameters: props.searchParameters,
      algoliaClient: props.algoliaClient,
      initialState,
    });
  }

  componentWillReceiveProps(nextProps) {
    validateNextProps(this.props, nextProps);
    if (this.isControlled) {
      this.aisManager.onExternalStateUpdate(nextProps.searchState);
    }
  }

  getChildContext() {
    // If not already cached, cache the bound methods so that we can forward them as part
    // of the context.
    if (!this._aisContextCache) {
      this._aisContextCache = {
        ais: {
          onInternalStateUpdate: this.onWidgetsInternalStateUpdate.bind(this),
          createHrefForState: this.createHrefForState.bind(this),
        },
      };
    }

    return {
      ais: {
        ...this._aisContextCache.ais,
        store: this.aisManager.store,
        widgetsManager: this.aisManager.widgetsManager,
      },
    };
  }

  createHrefForState(searchState) {
    searchState = this.aisManager.transitionState(searchState);
    return this.isControlled && this.props.createURL ? this.props.createURL(searchState, this.getKnownKeys()) : '#';
  }

  onWidgetsInternalStateUpdate(searchState) {
    searchState = this.aisManager.transitionState(searchState);

    if (this.props.onSearchStateChange) {
      this.props.onSearchStateChange(searchState);
    }

    if (!this.isControlled) {
      this.aisManager.onExternalStateUpdate(searchState);
    }
  }

  getKnownKeys() {
    return this.aisManager.getWidgetsIds();
  }

  render() {
    const childrenCount = Children.count(this.props.children);
    const {Root, props} = this.props.root;
    if (childrenCount === 0)
      return null;
    else
      return <Root {...props}>{this.props.children}</Root>;
  }
}

InstantSearch.propTypes = {
  // @TODO: These props are currently constant.
  indexName: PropTypes.string.isRequired,

  algoliaClient: PropTypes.object.isRequired,

  searchParameters: PropTypes.object,

  createURL: PropTypes.func,

  searchState: PropTypes.object,
  onSearchStateChange: PropTypes.func,

  children: PropTypes.node,

  root: PropTypes.shape({
    Root: PropTypes.string.isRequired,
    props: PropTypes.object,
  }).isRequired,
};

InstantSearch.childContextTypes = {
  // @TODO: more precise widgets manager propType
  ais: PropTypes.object.isRequired,
};

export default InstantSearch;
<|endoftext|>


<commit_before>/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 * @emails oncall+relay
 * @format
 */

// flowlint ambiguous-object-type:error

'use strict';

const RelayDefaultHandlerProvider = require('../handlers/RelayDefaultHandlerProvider');
const RelayFeatureFlags = require('../util/RelayFeatureFlags');
const RelayModernQueryExecutor = require('./RelayModernQueryExecutor');
const RelayObservable = require('../network/RelayObservable');
const RelayOperationTracker = require('../store/RelayOperationTracker');
const RelayPublishQueue = require('./RelayPublishQueue');
const RelayRecordSource = require('./RelayRecordSource');

const defaultGetDataID = require('./defaultGetDataID');
const defaultRequiredFieldLogger = require('./defaultRequiredFieldLogger');
const generateID = require('../util/generateID');
const invariant = require('invariant');

import type {HandlerProvider} from '../handlers/RelayDefaultHandlerProvider';
import type {
  GraphQLResponse,
  INetwork,
  LogRequestInfoFunction,
  PayloadData,
  UploadableMap,
} from '../network/RelayNetworkTypes';
import type {Observer} from '../network/RelayObservable';
import type {RequestParameters} from '../util/RelayConcreteNode';
import type {
  CacheConfig,
  Disposable,
  RenderPolicy,
  Variables,
} from '../util/RelayRuntimeTypes';
import type {ActiveState} from './RelayModernQueryExecutor';
import type {TaskScheduler} from './RelayModernQueryExecutor';
import type {GetDataID} from './RelayResponseNormalizer';
import type {
  IEnvironment,
  LogFunction,
  MissingFieldHandler,
  RequiredFieldLogger,
  OperationAvailability,
  OperationDescriptor,
  OperationLoader,
  OperationTracker,
  OptimisticResponseConfig,
  OptimisticUpdateFunction,
  PublishQueue,
  ReactFlightPayloadDeserializer,
  SelectorStoreUpdater,
  SingularReaderSelector,
  Snapshot,
  Store,
  StoreUpdater,
} from './RelayStoreTypes';

export type EnvironmentConfig = {|
  +configName?: string,
  +handlerProvider?: ?HandlerProvider,
  +treatMissingFieldsAsNull?: boolean,
  +log?: ?LogFunction,
  +operationLoader?: ?OperationLoader,
  +reactFlightPayloadDeserializer?: ?ReactFlightPayloadDeserializer,
  +network: INetwork,
  +scheduler?: ?TaskScheduler,
  +store: Store,
  +missingFieldHandlers?: ?$ReadOnlyArray<MissingFieldHandler>,
  +operationTracker?: ?OperationTracker,
  /**
   * This method is likely to change in future versions, use at your own risk.
   * It can potentially break existing calls like store.get(<id>),
   * because the internal ID might not be the `id` field on the node anymore
   */
  +UNSTABLE_DO_NOT_USE_getDataID?: ?GetDataID,
  +UNSTABLE_defaultRenderPolicy?: ?RenderPolicy,
  +options?: mixed,
  +isServer?: boolean,
  +requiredFieldLogger?: RequiredFieldLogger,
|};

class RelayModernEnvironment implements IEnvironment {
  __log: LogFunction;
  +_defaultRenderPolicy: RenderPolicy;
  _operationLoader: ?OperationLoader;
  _reactFlightPayloadDeserializer: ?ReactFlightPayloadDeserializer;
  _network: INetwork;
  _publishQueue: PublishQueue;
  _scheduler: ?TaskScheduler;
  _store: Store;
  configName: ?string;
  _missingFieldHandlers: ?$ReadOnlyArray<MissingFieldHandler>;
  _operationTracker: OperationTracker;
  _getDataID: GetDataID;
  _treatMissingFieldsAsNull: boolean;
  _operationExecutions: Map<string, ActiveState>;
  +options: mixed;
  +_isServer: boolean;
  requiredFieldLogger: RequiredFieldLogger;

  constructor(config: EnvironmentConfig) {
    this.configName = config.configName;
    const handlerProvider = config.handlerProvider
      ? config.handlerProvider
      : RelayDefaultHandlerProvider;
    this._treatMissingFieldsAsNull = config.treatMissingFieldsAsNull === true;
    const operationLoader = config.operationLoader;
    const reactFlightPayloadDeserializer =
      config.reactFlightPayloadDeserializer;
    if (__DEV__) {
      if (operationLoader != null) {
        invariant(
          typeof operationLoader === 'object' &&
            typeof operationLoader.get === 'function' &&
            typeof operationLoader.load === 'function',
          'RelayModernEnvironment: Expected `operationLoader` to be an object ' +
            'with get() and load() functions, got `%s`.',
          operationLoader,
        );
      }
      if (reactFlightPayloadDeserializer != null) {
        invariant(
          typeof reactFlightPayloadDeserializer === 'function',
          'RelayModernEnvironment: Expected `reactFlightPayloadDeserializer` ' +
            ' to be a function, got `%s`.',
          reactFlightPayloadDeserializer,
        );
      }
    }
    this.__log = config.log ?? emptyFunction;
    this.requiredFieldLogger =
      config.requiredFieldLogger ?? defaultRequiredFieldLogger;
    this._defaultRenderPolicy =
      config.UNSTABLE_defaultRenderPolicy ??
      RelayFeatureFlags.ENABLE_PARTIAL_RENDERING_DEFAULT === true
        ? 'partial'
        : 'full';
    this._operationLoader = operationLoader;
    this._operationExecutions = new Map();
    this._network = config.network;
    this._getDataID = config.UNSTABLE_DO_NOT_USE_getDataID ?? defaultGetDataID;
    this._publishQueue = new RelayPublishQueue(
      config.store,
      handlerProvider,
      this._getDataID,
    );
    this._scheduler = config.scheduler ?? null;
    this._store = config.store;
    this.options = config.options;
    this._isServer = config.isServer ?? false;

    (this: any).__setNet = newNet => (this._network = newNet);

    if (__DEV__) {
      const {inspect} = require('./StoreInspector');
      (this: any).DEBUG_inspect = (dataID: ?string) => inspect(this, dataID);
    }

    // Register this Relay Environment with Relay DevTools if it exists.
    // Note: this must always be the last step in the constructor.
    const _global =
      typeof global !== 'undefined'
        ? global
        : typeof window !== 'undefined'
        ? window
        : undefined;
    const devToolsHook = _global && _global.__RELAY_DEVTOOLS_HOOK__;
    if (devToolsHook) {
      devToolsHook.registerEnvironment(this);
    }
    this._missingFieldHandlers = config.missingFieldHandlers;
    this._operationTracker =
      config.operationTracker ?? new RelayOperationTracker();
    this._reactFlightPayloadDeserializer = reactFlightPayloadDeserializer;
  }

  getStore(): Store {
    return this._store;
  }

  getNetwork(): INetwork {
    return this._network;
  }

  getOperationTracker(): RelayOperationTracker {
    return this._operationTracker;
  }

  isRequestActive(requestIdentifier: string): boolean {
    const activeState = this._operationExecutions.get(requestIdentifier);
    return activeState === 'active';
  }

  UNSTABLE_getDefaultRenderPolicy(): RenderPolicy {
    return this._defaultRenderPolicy;
  }

  applyUpdate(optimisticUpdate: OptimisticUpdateFunction): Disposable {
    const dispose = () => {
      this._scheduleUpdates(() => {
        this._publishQueue.revertUpdate(optimisticUpdate);
        this._publishQueue.run();
      });
    };
    this._scheduleUpdates(() => {
      this._publishQueue.applyUpdate(optimisticUpdate);
      this._publishQueue.run();
    });
    return {dispose};
  }

  revertUpdate(update: OptimisticUpdateFunction): void {
    this._scheduleUpdates(() => {
      this._publishQueue.revertUpdate(update);
      this._publishQueue.run();
    });
  }

  replaceUpdate(
    update: OptimisticUpdateFunction,
    newUpdate: OptimisticUpdateFunction,
  ): void {
    this._scheduleUpdates(() => {
      this._publishQueue.revertUpdate(update);
      this._publishQueue.applyUpdate(newUpdate);
      this._publishQueue.run();
    });
  }

  applyMutation(optimisticConfig: OptimisticResponseConfig): Disposable {
    const subscription = RelayObservable.create(sink => {
      const source = RelayObservable.create(_sink => {});
      const executor = RelayModernQueryExecutor.execute({
        operation: optimisticConfig.operation,
        operationExecutions: this._operationExecutions,
        operationLoader: this._operationLoader,
        optimisticConfig,
        publishQueue: this._publishQueue,
        reactFlightPayloadDeserializer: this._reactFlightPayloadDeserializer,
        scheduler: this._scheduler,
        sink,
        source,
        store: this._store,
        updater: null,
        operationTracker: this._operationTracker,
        getDataID: this._getDataID,
        treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
      });
      return () => executor.cancel();
    }).subscribe({});
    return {
      dispose: () => subscription.unsubscribe(),
    };
  }

  check(operation: OperationDescriptor): OperationAvailability {
    if (
      this._missingFieldHandlers == null ||
      this._missingFieldHandlers.length === 0
    ) {
      return this._store.check(operation);
    }
    return this._checkSelectorAndHandleMissingFields(
      operation,
      this._missingFieldHandlers,
    );
  }

  commitPayload(operation: OperationDescriptor, payload: PayloadData): void {
    RelayObservable.create(sink => {
      const executor = RelayModernQueryExecutor.execute({
        operation: operation,
        operationExecutions: this._operationExecutions,
        operationLoader: this._operationLoader,
        optimisticConfig: null,
        publishQueue: this._publishQueue,
        reactFlightPayloadDeserializer: this._reactFlightPayloadDeserializer,
        scheduler: this._scheduler,
        sink,
        source: RelayObservable.from({
          data: payload,
        }),
        store: this._store,
        updater: null,
        operationTracker: this._operationTracker,
        getDataID: this._getDataID,
        isClientPayload: true,
        treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
      });
      return () => executor.cancel();
    }).subscribe({});
  }

  commitUpdate(updater: StoreUpdater): void {
    this._scheduleUpdates(() => {
      this._publishQueue.commitUpdate(updater);
      this._publishQueue.run();
    });
  }

  lookup(readSelector: SingularReaderSelector): Snapshot {
    return this._store.lookup(readSelector);
  }

  subscribe(
    snapshot: Snapshot,
    callback: (snapshot: Snapshot) => void,
  ): Disposable {
    return this._store.subscribe(snapshot, callback);
  }

  retain(operation: OperationDescriptor): Disposable {
    return this._store.retain(operation);
  }

  isServer(): boolean {
    return this._isServer;
  }

  _checkSelectorAndHandleMissingFields(
    operation: OperationDescriptor,
    handlers: $ReadOnlyArray<MissingFieldHandler>,
  ): OperationAvailability {
    const target = RelayRecordSource.create();
    const result = this._store.check(operation, {target, handlers});
    if (target.size() > 0) {
      this._scheduleUpdates(() => {
        this._publishQueue.commitSource(target);
        this._publishQueue.run();
      });
    }
    return result;
  }

  _scheduleUpdates(task: () => void) {
    const scheduler = this._scheduler;
    if (scheduler != null) {
      scheduler.schedule(task);
    } else {
      task();
    }
  }

  /**
   * Returns an Observable of GraphQLResponse resulting from executing the
   * provided Query or Subscription operation, each result of which is then
   * normalized and committed to the publish queue.
   *
   * Note: Observables are lazy, so calling this method will do nothing until
   * the result is subscribed to: environment.execute({...}).subscribe({...}).
   */
  execute({
    operation,
    cacheConfig,
    updater,
  }: {|
    operation: OperationDescriptor,
    cacheConfig?: ?CacheConfig,
    updater?: ?SelectorStoreUpdater,
  |}): RelayObservable<GraphQLResponse> {
    const [logObserver, logRequestInfo] = this.__createLogObserver(
      operation.request.node.params,
      operation.request.variables,
    );
    return RelayObservable.create(sink => {
      const source = this._network
        .execute(
          operation.request.node.params,
          operation.request.variables,
          cacheConfig || {},
          null,
          logRequestInfo,
        )
        .do(logObserver);
      const executor = RelayModernQueryExecutor.execute({
        operation,
        operationExecutions: this._operationExecutions,
        operationLoader: this._operationLoader,
        optimisticConfig: null,
        publishQueue: this._publishQueue,
        reactFlightPayloadDeserializer: this._reactFlightPayloadDeserializer,
        scheduler: this._scheduler,
        sink,
        source,
        store: this._store,
        updater,
        operationTracker: this._operationTracker,
        getDataID: this._getDataID,
        treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
      });
      return () => executor.cancel();
    });
  }

  /**
   * Returns an Observable of GraphQLResponse resulting from executing the
   * provided Mutation operation, the result of which is then normalized and
   * committed to the publish queue along with an optional optimistic response
   * or updater.
   *
   * Note: Observables are lazy, so calling this method will do nothing until
   * the result is subscribed to:
   * environment.executeMutation({...}).subscribe({...}).
   */
  executeMutation({
    cacheConfig,
    operation,
    optimisticResponse,
    optimisticUpdater,
    updater,
    uploadables,
  }: {|
    cacheConfig?: ?CacheConfig,
    operation: OperationDescriptor,
    optimisticUpdater?: ?SelectorStoreUpdater,
    optimisticResponse?: ?Object,
    updater?: ?SelectorStoreUpdater,
    uploadables?: ?UploadableMap,
  |}): RelayObservable<GraphQLResponse> {
    const [logObserver, logRequestInfo] = this.__createLogObserver(
      operation.request.node.params,
      operation.request.variables,
    );
    return RelayObservable.create(sink => {
      let optimisticConfig;
      if (optimisticResponse || optimisticUpdater) {
        optimisticConfig = {
          operation: operation,
          response: optimisticResponse,
          updater: optimisticUpdater,
        };
      }
      const source = this._network
        .execute(
          operation.request.node.params,
          operation.request.variables,
          {
            ...cacheConfig,
            force: true,
          },
          uploadables,
          logRequestInfo,
        )
        .do(logObserver);
      const executor = RelayModernQueryExecutor.execute({
        operation,
        operationExecutions: this._operationExecutions,
        operationLoader: this._operationLoader,
        optimisticConfig,
        publishQueue: this._publishQueue,
        reactFlightPayloadDeserializer: this._reactFlightPayloadDeserializer,
        scheduler: this._scheduler,
        sink,
        source,
        store: this._store,
        updater,
        operationTracker: this._operationTracker,
        getDataID: this._getDataID,
        treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
      });
      return () => executor.cancel();
    });
  }

  /**
   * Returns an Observable of GraphQLResponse resulting from executing the
   * provided Query or Subscription operation responses, the result of which is
   * then normalized and comitted to the publish queue.
   *
   * Note: Observables are lazy, so calling this method will do nothing until
   * the result is subscribed to:
   * environment.executeWithSource({...}).subscribe({...}).
   */
  executeWithSource({
    operation,
    source,
  }: {|
    operation: OperationDescriptor,
    source: RelayObservable<GraphQLResponse>,
  |}): RelayObservable<GraphQLResponse> {
    return RelayObservable.create(sink => {
      const executor = RelayModernQueryExecutor.execute({
        operation,
        operationExecutions: this._operationExecutions,
        operationLoader: this._operationLoader,
        operationTracker: this._operationTracker,
        optimisticConfig: null,
        publishQueue: this._publishQueue,
        reactFlightPayloadDeserializer: this._reactFlightPayloadDeserializer,
        scheduler: this._scheduler,
        sink,
        source,
        store: this._store,
        getDataID: this._getDataID,
        treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
      });
      return () => executor.cancel();
    });
  }

  toJSON(): mixed {
    return `RelayModernEnvironment(${this.configName ?? ''})`;
  }

  __createLogObserver(
    params: RequestParameters,
    variables: Variables,
  ): [Observer<GraphQLResponse>, LogRequestInfoFunction] {
    const transactionID = generateID();
    const log = this.__log;
    const logObserver = {
      start: subscription => {
        log({
          name: 'execute.start',
          transactionID,
          params,
          variables,
        });
      },
      next: response => {
        log({
          name: 'execute.next',
          transactionID,
          response,
        });
      },
      error: error => {
        log({
          name: 'execute.error',
          transactionID,
          error,
        });
      },
      complete: () => {
        log({
          name: 'execute.complete',
          transactionID,
        });
      },
      unsubscribe: () => {
        log({
          name: 'execute.unsubscribe',
          transactionID,
        });
      },
    };
    const logRequestInfo = info => {
      log({
        name: 'execute.info',
        transactionID,
        info,
      });
    };
    return [logObserver, logRequestInfo];
  }
}

// Add a sigil for detection by `isRelayModernEnvironment()` to avoid a
// realm-specific instanceof check, and to aid in module tree-shaking to
// avoid requiring all of RelayRuntime just to detect its environment.
(RelayModernEnvironment: any).prototype['@@RelayModernEnvironment'] = true;

function emptyFunction() {}

module.exports = RelayModernEnvironment;
<commit_msg>Add a default requiredField logger to `createEnvironment`.<commit_after>/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 * @emails oncall+relay
 * @format
 */

// flowlint ambiguous-object-type:error

'use strict';

const RelayDefaultHandlerProvider = require('../handlers/RelayDefaultHandlerProvider');
const RelayFeatureFlags = require('../util/RelayFeatureFlags');
const RelayModernQueryExecutor = require('./RelayModernQueryExecutor');
const RelayObservable = require('../network/RelayObservable');
const RelayOperationTracker = require('../store/RelayOperationTracker');
const RelayPublishQueue = require('./RelayPublishQueue');
const RelayRecordSource = require('./RelayRecordSource');

const defaultGetDataID = require('./defaultGetDataID');
const defaultRequiredFieldLogger = require('./defaultRequiredFieldLogger');
const generateID = require('../util/generateID');
const invariant = require('invariant');

import type {HandlerProvider} from '../handlers/RelayDefaultHandlerProvider';
import type {
  GraphQLResponse,
  INetwork,
  LogRequestInfoFunction,
  PayloadData,
  UploadableMap,
} from '../network/RelayNetworkTypes';
import type {Observer} from '../network/RelayObservable';
import type {RequestParameters} from '../util/RelayConcreteNode';
import type {
  CacheConfig,
  Disposable,
  RenderPolicy,
  Variables,
} from '../util/RelayRuntimeTypes';
import type {ActiveState} from './RelayModernQueryExecutor';
import type {TaskScheduler} from './RelayModernQueryExecutor';
import type {GetDataID} from './RelayResponseNormalizer';
import type {
  IEnvironment,
  LogFunction,
  MissingFieldHandler,
  RequiredFieldLogger,
  OperationAvailability,
  OperationDescriptor,
  OperationLoader,
  OperationTracker,
  OptimisticResponseConfig,
  OptimisticUpdateFunction,
  PublishQueue,
  ReactFlightPayloadDeserializer,
  SelectorStoreUpdater,
  SingularReaderSelector,
  Snapshot,
  Store,
  StoreUpdater,
} from './RelayStoreTypes';

export type EnvironmentConfig = {|
  +configName?: string,
  +handlerProvider?: ?HandlerProvider,
  +treatMissingFieldsAsNull?: boolean,
  +log?: ?LogFunction,
  +operationLoader?: ?OperationLoader,
  +reactFlightPayloadDeserializer?: ?ReactFlightPayloadDeserializer,
  +network: INetwork,
  +scheduler?: ?TaskScheduler,
  +store: Store,
  +missingFieldHandlers?: ?$ReadOnlyArray<MissingFieldHandler>,
  +operationTracker?: ?OperationTracker,
  /**
   * This method is likely to change in future versions, use at your own risk.
   * It can potentially break existing calls like store.get(<id>),
   * because the internal ID might not be the `id` field on the node anymore
   */
  +UNSTABLE_DO_NOT_USE_getDataID?: ?GetDataID,
  +UNSTABLE_defaultRenderPolicy?: ?RenderPolicy,
  +options?: mixed,
  +isServer?: boolean,
  +requiredFieldLogger?: ?RequiredFieldLogger,
|};

class RelayModernEnvironment implements IEnvironment {
  __log: LogFunction;
  +_defaultRenderPolicy: RenderPolicy;
  _operationLoader: ?OperationLoader;
  _reactFlightPayloadDeserializer: ?ReactFlightPayloadDeserializer;
  _network: INetwork;
  _publishQueue: PublishQueue;
  _scheduler: ?TaskScheduler;
  _store: Store;
  configName: ?string;
  _missingFieldHandlers: ?$ReadOnlyArray<MissingFieldHandler>;
  _operationTracker: OperationTracker;
  _getDataID: GetDataID;
  _treatMissingFieldsAsNull: boolean;
  _operationExecutions: Map<string, ActiveState>;
  +options: mixed;
  +_isServer: boolean;
  requiredFieldLogger: RequiredFieldLogger;

  constructor(config: EnvironmentConfig) {
    this.configName = config.configName;
    const handlerProvider = config.handlerProvider
      ? config.handlerProvider
      : RelayDefaultHandlerProvider;
    this._treatMissingFieldsAsNull = config.treatMissingFieldsAsNull === true;
    const operationLoader = config.operationLoader;
    const reactFlightPayloadDeserializer =
      config.reactFlightPayloadDeserializer;
    if (__DEV__) {
      if (operationLoader != null) {
        invariant(
          typeof operationLoader === 'object' &&
            typeof operationLoader.get === 'function' &&
            typeof operationLoader.load === 'function',
          'RelayModernEnvironment: Expected `operationLoader` to be an object ' +
            'with get() and load() functions, got `%s`.',
          operationLoader,
        );
      }
      if (reactFlightPayloadDeserializer != null) {
        invariant(
          typeof reactFlightPayloadDeserializer === 'function',
          'RelayModernEnvironment: Expected `reactFlightPayloadDeserializer` ' +
            ' to be a function, got `%s`.',
          reactFlightPayloadDeserializer,
        );
      }
    }
    this.__log = config.log ?? emptyFunction;
    this.requiredFieldLogger =
      config.requiredFieldLogger ?? defaultRequiredFieldLogger;
    this._defaultRenderPolicy =
      config.UNSTABLE_defaultRenderPolicy ??
      RelayFeatureFlags.ENABLE_PARTIAL_RENDERING_DEFAULT === true
        ? 'partial'
        : 'full';
    this._operationLoader = operationLoader;
    this._operationExecutions = new Map();
    this._network = config.network;
    this._getDataID = config.UNSTABLE_DO_NOT_USE_getDataID ?? defaultGetDataID;
    this._publishQueue = new RelayPublishQueue(
      config.store,
      handlerProvider,
      this._getDataID,
    );
    this._scheduler = config.scheduler ?? null;
    this._store = config.store;
    this.options = config.options;
    this._isServer = config.isServer ?? false;

    (this: any).__setNet = newNet => (this._network = newNet);

    if (__DEV__) {
      const {inspect} = require('./StoreInspector');
      (this: any).DEBUG_inspect = (dataID: ?string) => inspect(this, dataID);
    }

    // Register this Relay Environment with Relay DevTools if it exists.
    // Note: this must always be the last step in the constructor.
    const _global =
      typeof global !== 'undefined'
        ? global
        : typeof window !== 'undefined'
        ? window
        : undefined;
    const devToolsHook = _global && _global.__RELAY_DEVTOOLS_HOOK__;
    if (devToolsHook) {
      devToolsHook.registerEnvironment(this);
    }
    this._missingFieldHandlers = config.missingFieldHandlers;
    this._operationTracker =
      config.operationTracker ?? new RelayOperationTracker();
    this._reactFlightPayloadDeserializer = reactFlightPayloadDeserializer;
  }

  getStore(): Store {
    return this._store;
  }

  getNetwork(): INetwork {
    return this._network;
  }

  getOperationTracker(): RelayOperationTracker {
    return this._operationTracker;
  }

  isRequestActive(requestIdentifier: string): boolean {
    const activeState = this._operationExecutions.get(requestIdentifier);
    return activeState === 'active';
  }

  UNSTABLE_getDefaultRenderPolicy(): RenderPolicy {
    return this._defaultRenderPolicy;
  }

  applyUpdate(optimisticUpdate: OptimisticUpdateFunction): Disposable {
    const dispose = () => {
      this._scheduleUpdates(() => {
        this._publishQueue.revertUpdate(optimisticUpdate);
        this._publishQueue.run();
      });
    };
    this._scheduleUpdates(() => {
      this._publishQueue.applyUpdate(optimisticUpdate);
      this._publishQueue.run();
    });
    return {dispose};
  }

  revertUpdate(update: OptimisticUpdateFunction): void {
    this._scheduleUpdates(() => {
      this._publishQueue.revertUpdate(update);
      this._publishQueue.run();
    });
  }

  replaceUpdate(
    update: OptimisticUpdateFunction,
    newUpdate: OptimisticUpdateFunction,
  ): void {
    this._scheduleUpdates(() => {
      this._publishQueue.revertUpdate(update);
      this._publishQueue.applyUpdate(newUpdate);
      this._publishQueue.run();
    });
  }

  applyMutation(optimisticConfig: OptimisticResponseConfig): Disposable {
    const subscription = RelayObservable.create(sink => {
      const source = RelayObservable.create(_sink => {});
      const executor = RelayModernQueryExecutor.execute({
        operation: optimisticConfig.operation,
        operationExecutions: this._operationExecutions,
        operationLoader: this._operationLoader,
        optimisticConfig,
        publishQueue: this._publishQueue,
        reactFlightPayloadDeserializer: this._reactFlightPayloadDeserializer,
        scheduler: this._scheduler,
        sink,
        source,
        store: this._store,
        updater: null,
        operationTracker: this._operationTracker,
        getDataID: this._getDataID,
        treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
      });
      return () => executor.cancel();
    }).subscribe({});
    return {
      dispose: () => subscription.unsubscribe(),
    };
  }

  check(operation: OperationDescriptor): OperationAvailability {
    if (
      this._missingFieldHandlers == null ||
      this._missingFieldHandlers.length === 0
    ) {
      return this._store.check(operation);
    }
    return this._checkSelectorAndHandleMissingFields(
      operation,
      this._missingFieldHandlers,
    );
  }

  commitPayload(operation: OperationDescriptor, payload: PayloadData): void {
    RelayObservable.create(sink => {
      const executor = RelayModernQueryExecutor.execute({
        operation: operation,
        operationExecutions: this._operationExecutions,
        operationLoader: this._operationLoader,
        optimisticConfig: null,
        publishQueue: this._publishQueue,
        reactFlightPayloadDeserializer: this._reactFlightPayloadDeserializer,
        scheduler: this._scheduler,
        sink,
        source: RelayObservable.from({
          data: payload,
        }),
        store: this._store,
        updater: null,
        operationTracker: this._operationTracker,
        getDataID: this._getDataID,
        isClientPayload: true,
        treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
      });
      return () => executor.cancel();
    }).subscribe({});
  }

  commitUpdate(updater: StoreUpdater): void {
    this._scheduleUpdates(() => {
      this._publishQueue.commitUpdate(updater);
      this._publishQueue.run();
    });
  }

  lookup(readSelector: SingularReaderSelector): Snapshot {
    return this._store.lookup(readSelector);
  }

  subscribe(
    snapshot: Snapshot,
    callback: (snapshot: Snapshot) => void,
  ): Disposable {
    return this._store.subscribe(snapshot, callback);
  }

  retain(operation: OperationDescriptor): Disposable {
    return this._store.retain(operation);
  }

  isServer(): boolean {
    return this._isServer;
  }

  _checkSelectorAndHandleMissingFields(
    operation: OperationDescriptor,
    handlers: $ReadOnlyArray<MissingFieldHandler>,
  ): OperationAvailability {
    const target = RelayRecordSource.create();
    const result = this._store.check(operation, {target, handlers});
    if (target.size() > 0) {
      this._scheduleUpdates(() => {
        this._publishQueue.commitSource(target);
        this._publishQueue.run();
      });
    }
    return result;
  }

  _scheduleUpdates(task: () => void) {
    const scheduler = this._scheduler;
    if (scheduler != null) {
      scheduler.schedule(task);
    } else {
      task();
    }
  }

  /**
   * Returns an Observable of GraphQLResponse resulting from executing the
   * provided Query or Subscription operation, each result of which is then
   * normalized and committed to the publish queue.
   *
   * Note: Observables are lazy, so calling this method will do nothing until
   * the result is subscribed to: environment.execute({...}).subscribe({...}).
   */
  execute({
    operation,
    cacheConfig,
    updater,
  }: {|
    operation: OperationDescriptor,
    cacheConfig?: ?CacheConfig,
    updater?: ?SelectorStoreUpdater,
  |}): RelayObservable<GraphQLResponse> {
    const [logObserver, logRequestInfo] = this.__createLogObserver(
      operation.request.node.params,
      operation.request.variables,
    );
    return RelayObservable.create(sink => {
      const source = this._network
        .execute(
          operation.request.node.params,
          operation.request.variables,
          cacheConfig || {},
          null,
          logRequestInfo,
        )
        .do(logObserver);
      const executor = RelayModernQueryExecutor.execute({
        operation,
        operationExecutions: this._operationExecutions,
        operationLoader: this._operationLoader,
        optimisticConfig: null,
        publishQueue: this._publishQueue,
        reactFlightPayloadDeserializer: this._reactFlightPayloadDeserializer,
        scheduler: this._scheduler,
        sink,
        source,
        store: this._store,
        updater,
        operationTracker: this._operationTracker,
        getDataID: this._getDataID,
        treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
      });
      return () => executor.cancel();
    });
  }

  /**
   * Returns an Observable of GraphQLResponse resulting from executing the
   * provided Mutation operation, the result of which is then normalized and
   * committed to the publish queue along with an optional optimistic response
   * or updater.
   *
   * Note: Observables are lazy, so calling this method will do nothing until
   * the result is subscribed to:
   * environment.executeMutation({...}).subscribe({...}).
   */
  executeMutation({
    cacheConfig,
    operation,
    optimisticResponse,
    optimisticUpdater,
    updater,
    uploadables,
  }: {|
    cacheConfig?: ?CacheConfig,
    operation: OperationDescriptor,
    optimisticUpdater?: ?SelectorStoreUpdater,
    optimisticResponse?: ?Object,
    updater?: ?SelectorStoreUpdater,
    uploadables?: ?UploadableMap,
  |}): RelayObservable<GraphQLResponse> {
    const [logObserver, logRequestInfo] = this.__createLogObserver(
      operation.request.node.params,
      operation.request.variables,
    );
    return RelayObservable.create(sink => {
      let optimisticConfig;
      if (optimisticResponse || optimisticUpdater) {
        optimisticConfig = {
          operation: operation,
          response: optimisticResponse,
          updater: optimisticUpdater,
        };
      }
      const source = this._network
        .execute(
          operation.request.node.params,
          operation.request.variables,
          {
            ...cacheConfig,
            force: true,
          },
          uploadables,
          logRequestInfo,
        )
        .do(logObserver);
      const executor = RelayModernQueryExecutor.execute({
        operation,
        operationExecutions: this._operationExecutions,
        operationLoader: this._operationLoader,
        optimisticConfig,
        publishQueue: this._publishQueue,
        reactFlightPayloadDeserializer: this._reactFlightPayloadDeserializer,
        scheduler: this._scheduler,
        sink,
        source,
        store: this._store,
        updater,
        operationTracker: this._operationTracker,
        getDataID: this._getDataID,
        treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
      });
      return () => executor.cancel();
    });
  }

  /**
   * Returns an Observable of GraphQLResponse resulting from executing the
   * provided Query or Subscription operation responses, the result of which is
   * then normalized and comitted to the publish queue.
   *
   * Note: Observables are lazy, so calling this method will do nothing until
   * the result is subscribed to:
   * environment.executeWithSource({...}).subscribe({...}).
   */
  executeWithSource({
    operation,
    source,
  }: {|
    operation: OperationDescriptor,
    source: RelayObservable<GraphQLResponse>,
  |}): RelayObservable<GraphQLResponse> {
    return RelayObservable.create(sink => {
      const executor = RelayModernQueryExecutor.execute({
        operation,
        operationExecutions: this._operationExecutions,
        operationLoader: this._operationLoader,
        operationTracker: this._operationTracker,
        optimisticConfig: null,
        publishQueue: this._publishQueue,
        reactFlightPayloadDeserializer: this._reactFlightPayloadDeserializer,
        scheduler: this._scheduler,
        sink,
        source,
        store: this._store,
        getDataID: this._getDataID,
        treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
      });
      return () => executor.cancel();
    });
  }

  toJSON(): mixed {
    return `RelayModernEnvironment(${this.configName ?? ''})`;
  }

  __createLogObserver(
    params: RequestParameters,
    variables: Variables,
  ): [Observer<GraphQLResponse>, LogRequestInfoFunction] {
    const transactionID = generateID();
    const log = this.__log;
    const logObserver = {
      start: subscription => {
        log({
          name: 'execute.start',
          transactionID,
          params,
          variables,
        });
      },
      next: response => {
        log({
          name: 'execute.next',
          transactionID,
          response,
        });
      },
      error: error => {
        log({
          name: 'execute.error',
          transactionID,
          error,
        });
      },
      complete: () => {
        log({
          name: 'execute.complete',
          transactionID,
        });
      },
      unsubscribe: () => {
        log({
          name: 'execute.unsubscribe',
          transactionID,
        });
      },
    };
    const logRequestInfo = info => {
      log({
        name: 'execute.info',
        transactionID,
        info,
      });
    };
    return [logObserver, logRequestInfo];
  }
}

// Add a sigil for detection by `isRelayModernEnvironment()` to avoid a
// realm-specific instanceof check, and to aid in module tree-shaking to
// avoid requiring all of RelayRuntime just to detect its environment.
(RelayModernEnvironment: any).prototype['@@RelayModernEnvironment'] = true;

function emptyFunction() {}

module.exports = RelayModernEnvironment;
<|endoftext|>