<commit_before>// @flow

import type { LoadingStatus } from 'lib/types/loading-types';
import { loadingStatusPropType } from 'lib/types/loading-types';
import type { CalendarQuery } from 'lib/types/entry-types';
import {
  type ThreadInfo,
  threadInfoPropType,
  threadPermissions,
  type ClientThreadJoinRequest,
  type ThreadJoinPayload,
} from 'lib/types/thread-types';
import { messageTypes } from 'lib/types/message-types';
import {
  inputStatePropType,
  type InputState,
  type PendingMultimediaUpload,
} from '../input/input-state';

import * as React from 'react';
import invariant from 'invariant';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronRight } from '@fortawesome/free-solid-svg-icons';
import { faFileImage } from '@fortawesome/free-regular-svg-icons';
import PropTypes from 'prop-types';
import _difference from 'lodash/fp/difference';

import { joinThreadActionTypes, joinThread } from 'lib/actions/thread-actions';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors';
import { threadHasPermission, viewerIsMember } from 'lib/shared/thread-utils';
import { trimMessage } from 'lib/shared/message-utils';
import {
  type DispatchActionPromise,
  useServerCall,
  useDispatchActionPromise,
} from 'lib/utils/action-utils';

import css from './chat-message-list.css';
import LoadingIndicator from '../loading-indicator.react';
import { nonThreadCalendarQuery } from '../selectors/nav-selectors';
import { allowedMimeTypeString } from '../media/file-utils';
import Multimedia from '../media/multimedia.react';
import { useSelector } from '../redux/redux-utils';

type BaseProps = {|
  +threadInfo: ThreadInfo,
  +inputState: InputState,
|};
type Props = {|
  ...BaseProps,
  // Redux state
  +viewerID: ?string,
  +joinThreadLoadingStatus: LoadingStatus,
  +calendarQuery: () => CalendarQuery,
  +nextLocalID: number,
  +isThreadActive: boolean,
  // Redux dispatch functions
  +dispatchActionPromise: DispatchActionPromise,
  // async functions that hit server APIs
  +joinThread: (request: ClientThreadJoinRequest) => Promise<ThreadJoinPayload>,
|};
class ChatInputBar extends React.PureComponent<Props> {
  static propTypes = {
    threadInfo: threadInfoPropType.isRequired,
    inputState: inputStatePropType.isRequired,
    viewerID: PropTypes.string,
    joinThreadLoadingStatus: loadingStatusPropType.isRequired,
    calendarQuery: PropTypes.func.isRequired,
    nextLocalID: PropTypes.number.isRequired,
    isThreadActive: PropTypes.bool.isRequired,
    dispatchActionPromise: PropTypes.func.isRequired,
    joinThread: PropTypes.func.isRequired,
  };
  textarea: ?HTMLTextAreaElement;
  multimediaInput: ?HTMLInputElement;

  componentDidMount() {
    this.updateHeight();
    if (this.props.isThreadActive) {
      this.addReplyListener();
    }
  }

  componentWillUnmount() {
    if (this.props.isThreadActive) {
      this.removeReplyListener();
    }
  }

  componentDidUpdate(prevProps: Props) {
    if (this.props.isThreadActive && !prevProps.isThreadActive) {
      this.addReplyListener();
    } else if (!this.props.isThreadActive && prevProps.isThreadActive) {
      this.removeReplyListener();
    }

    const { inputState } = this.props;
    const prevInputState = prevProps.inputState;
    if (inputState.draft !== prevInputState.draft) {
      this.updateHeight();
    }
    const curUploadIDs = ChatInputBar.unassignedUploadIDs(
      inputState.pendingUploads,
    );
    const prevUploadIDs = ChatInputBar.unassignedUploadIDs(
      prevInputState.pendingUploads,
    );
    if (
      this.multimediaInput &&
      _difference(prevUploadIDs)(curUploadIDs).length > 0
    ) {
      // Whenever a pending upload is removed, we reset the file
      // HTMLInputElement's value field, so that if the same upload occurs again
      // the onChange call doesn't get filtered
      this.multimediaInput.value = '';
    } else if (
      this.textarea &&
      _difference(curUploadIDs)(prevUploadIDs).length > 0
    ) {
      // Whenever a pending upload is added, we focus the textarea
      this.textarea.focus();
      return;
    }

    if (this.props.threadInfo.id !== prevProps.threadInfo.id && this.textarea) {
      this.textarea.focus();
    }
  }

  static unassignedUploadIDs(
    pendingUploads: $ReadOnlyArray<PendingMultimediaUpload>,
  ) {
    return pendingUploads
      .filter(
        (pendingUpload: PendingMultimediaUpload) => !pendingUpload.messageID,
      )
      .map((pendingUpload: PendingMultimediaUpload) => pendingUpload.localID);
  }

  updateHeight() {
    const textarea = this.textarea;
    if (textarea) {
      textarea.style.height = 'auto';
      const newHeight = Math.min(textarea.scrollHeight, 150);
      textarea.style.height = `${newHeight}px`;
    }
  }

  addReplyListener() {
    invariant(
      this.props.inputState,
      'inputState should be set in addReplyListener',
    );
    this.props.inputState.addReplyListener(this.focusAndUpdateText);
  }

  removeReplyListener() {
    invariant(
      this.props.inputState,
      'inputState should be set in removeReplyListener',
    );
    this.props.inputState.removeReplyListener(this.focusAndUpdateText);
  }

  render() {
    const isMember = viewerIsMember(this.props.threadInfo);
    let joinButton = null;
    if (
      !isMember &&
      threadHasPermission(this.props.threadInfo, threadPermissions.JOIN_THREAD)
    ) {
      let buttonContent;
      if (this.props.joinThreadLoadingStatus === 'loading') {
        buttonContent = (
          <LoadingIndicator
            status={this.props.joinThreadLoadingStatus}
            size="medium"
            color="white"
          />
        );
      } else {
        buttonContent = Join Thread;
      }
      joinButton = (

          {buttonContent}

      );
    }

    const { pendingUploads, cancelPendingUpload } = this.props.inputState;
    const multimediaPreviews = pendingUploads.map((pendingUpload) => (
      <Multimedia
        uri={pendingUpload.uri}
        pendingUpload={pendingUpload}
        remove={cancelPendingUpload}
        multimediaCSSClass={css.multimedia}
        multimediaImageCSSClass={css.multimediaImage}
        key={pendingUpload.localID}
      />
    ));
    const previews =
      multimediaPreviews.length > 0 ? (

{multimediaPreviews}

      ) : null;

    let content;
    if (threadHasPermission(this.props.threadInfo, threadPermissions.VOICED)) {
      content = (

          <textarea
            rows="1"
            placeholder="Send a message..."
            value={this.props.inputState.draft}
            onChange={this.onChangeMessageText}
            onKeyDown={this.onKeyDown}
            ref={this.textareaRef}
            autoFocus
          />

            <input
              type="file"
              onChange={this.onMultimediaFileChange}
              ref={this.multimediaInputRef}
              accept={allowedMimeTypeString}
              multiple
            />
            <FontAwesomeIcon icon={faFileImage} />

            <FontAwesomeIcon icon={faChevronRight} className={css.sendButton} />
            Send

      );
    } else if (isMember) {
      content = (

          You don't have permission to send messages.

      );
    } else {
      const defaultRoleID = Object.keys(this.props.threadInfo.roles).find(
        (roleID) => this.props.threadInfo.roles[roleID].isDefault,
      );
      invariant(
        defaultRoleID !== undefined,
        'all threads should have a default role',
      );
      const defaultRole = this.props.threadInfo.roles[defaultRoleID];
      const membersAreVoiced = !!defaultRole.permissions[
        threadPermissions.VOICED
      ];
      if (membersAreVoiced) {
        content = (

            Join this thread to send messages.

        );
      } else {
        content = (

            You don't have permission to send messages.

        );
      }
    }

    return (

        {joinButton}
        {previews}
        {content}

    );
  }

  textareaRef = (textarea: ?HTMLTextAreaElement) => {
    this.textarea = textarea;
    if (textarea) {
      textarea.focus();
    }
  };

  onChangeMessageText = (event: SyntheticEvent<HTMLTextAreaElement>) => {
    this.props.inputState.setDraft(event.currentTarget.value);
  };

  focusAndUpdateText = (text: string) => {
    // We need to call focus() first on Safari, otherwise the cursor
    // ends up at the start instead of the end for some reason
    const { textarea } = this;
    invariant(textarea, 'textarea should be set');
    textarea.focus();

    // We reset the textarea to an empty string at the start so that the cursor
    // always ends up at the end, even if the text doesn't actually change
    textarea.value = '';
    const currentText = this.props.inputState.draft;
    if (!currentText.startsWith(text)) {
      const prependedText = text.concat(currentText);
      this.props.inputState.setDraft(prependedText);
      textarea.value = prependedText;
    } else {
      textarea.value = currentText;
    }

    // The above strategies make sure the cursor is at the end,
    // but we also need to make sure that we're scrolled to the bottom
    textarea.scrollTop = textarea.scrollHeight;
  };

  onKeyDown = (event: SyntheticKeyboardEvent<HTMLTextAreaElement>) => {
    if (event.keyCode === 13 && !event.shiftKey) {
      event.preventDefault();
      this.send();
    }
  };

  onSend = (event: SyntheticEvent<HTMLAnchorElement>) => {
    event.preventDefault();
    this.send();
  };

  send() {
    let { nextLocalID } = this.props;

    const text = trimMessage(this.props.inputState.draft);
    if (text) {
      // TODO we should make the send button appear dynamically
      // iff trimmed text is nonempty, just like native
      this.dispatchTextMessageAction(text, nextLocalID);
      nextLocalID++;
    }

    this.props.inputState.createMultimediaMessage(nextLocalID);
  }

  dispatchTextMessageAction(text: string, nextLocalID: number) {
    this.props.inputState.setDraft('');

    const localID = `local${nextLocalID}`;
    const creatorID = this.props.viewerID;
    invariant(creatorID, 'should have viewer ID in order to send a message');
    this.props.inputState.sendTextMessage({
      type: messageTypes.TEXT,
      localID,
      threadID: this.props.threadInfo.id,
      text,
      creatorID,
      time: Date.now(),
    });
  }

  multimediaInputRef = (multimediaInput: ?HTMLInputElement) => {
    this.multimediaInput = multimediaInput;
  };

  onMultimediaClick = () => {
    if (this.multimediaInput) {
      this.multimediaInput.click();
    }
  };

  onMultimediaFileChange = async (
    event: SyntheticInputEvent<HTMLInputElement>,
  ) => {
    const result = await this.props.inputState.appendFiles([
      ...event.target.files,
    ]);
    if (!result && this.multimediaInput) {
      this.multimediaInput.value = '';
    }
  };

  onClickJoin = (event: SyntheticEvent<HTMLAnchorElement>) => {
    event.preventDefault();
    this.props.dispatchActionPromise(joinThreadActionTypes, this.joinAction());
  };

  async joinAction() {
    const query = this.props.calendarQuery();
    return await this.props.joinThread({
      threadID: this.props.threadInfo.id,
      calendarQuery: {
        startDate: query.startDate,
        endDate: query.endDate,
        filters: [
          ...query.filters,
          { type: 'threads', threadIDs: [this.props.threadInfo.id] },
        ],
      },
    });
  }
}

const joinThreadLoadingStatusSelector = createLoadingStatusSelector(
  joinThreadActionTypes,
);

export default React.memo<BaseProps>(function ConnectedChatInputBar(
  props: BaseProps,
) {
  const viewerID = useSelector(
    (state) => state.currentUserInfo && state.currentUserInfo.id,
  );
  const nextLocalID = useSelector((state) => state.nextLocalID);
  const isThreadActive = useSelector(
    (state) => props.threadInfo.id === state.navInfo.activeChatThreadID,
  );
  const joinThreadLoadingStatus = useSelector(joinThreadLoadingStatusSelector);
  const calendarQuery = useSelector(nonThreadCalendarQuery);
  const dispatchActionPromise = useDispatchActionPromise();
  const callJoinThread = useServerCall(joinThread);
  return (
    <ChatInputBar
      {...props}
      viewerID={viewerID}
      joinThreadLoadingStatus={joinThreadLoadingStatus}
      calendarQuery={calendarQuery}
      nextLocalID={nextLocalID}
      isThreadActive={isThreadActive}
      dispatchActionPromise={dispatchActionPromise}
      joinThread={callJoinThread}
    />
  );
});
<commit_msg>[web] Update notice in a thread when user blocked target<commit_after>// @flow

import type { LoadingStatus } from 'lib/types/loading-types';
import { loadingStatusPropType } from 'lib/types/loading-types';
import type { CalendarQuery } from 'lib/types/entry-types';
import {
  type ThreadInfo,
  threadInfoPropType,
  threadPermissions,
  type ClientThreadJoinRequest,
  type ThreadJoinPayload,
} from 'lib/types/thread-types';
import { messageTypes } from 'lib/types/message-types';
import { type UserInfo, userInfoPropType } from 'lib/types/user-types';
import {
  inputStatePropType,
  type InputState,
  type PendingMultimediaUpload,
} from '../input/input-state';

import * as React from 'react';
import invariant from 'invariant';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronRight } from '@fortawesome/free-solid-svg-icons';
import { faFileImage } from '@fortawesome/free-regular-svg-icons';
import PropTypes from 'prop-types';
import _difference from 'lodash/fp/difference';

import { joinThreadActionTypes, joinThread } from 'lib/actions/thread-actions';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors';
import {
  threadHasPermission,
  viewerIsMember,
  threadFrozenDueToViewerBlock,
  threadActualMembers,
} from 'lib/shared/thread-utils';
import { trimMessage } from 'lib/shared/message-utils';
import {
  type DispatchActionPromise,
  useServerCall,
  useDispatchActionPromise,
} from 'lib/utils/action-utils';

import css from './chat-message-list.css';
import LoadingIndicator from '../loading-indicator.react';
import { nonThreadCalendarQuery } from '../selectors/nav-selectors';
import { allowedMimeTypeString } from '../media/file-utils';
import Multimedia from '../media/multimedia.react';
import { useSelector } from '../redux/redux-utils';

type BaseProps = {|
  +threadInfo: ThreadInfo,
  +inputState: InputState,
|};
type Props = {|
  ...BaseProps,
  // Redux state
  +viewerID: ?string,
  +joinThreadLoadingStatus: LoadingStatus,
  +calendarQuery: () => CalendarQuery,
  +nextLocalID: number,
  +isThreadActive: boolean,
  +userInfos: { [id: string]: UserInfo },
  // Redux dispatch functions
  +dispatchActionPromise: DispatchActionPromise,
  // async functions that hit server APIs
  +joinThread: (request: ClientThreadJoinRequest) => Promise<ThreadJoinPayload>,
|};
class ChatInputBar extends React.PureComponent<Props> {
  static propTypes = {
    threadInfo: threadInfoPropType.isRequired,
    inputState: inputStatePropType.isRequired,
    viewerID: PropTypes.string,
    joinThreadLoadingStatus: loadingStatusPropType.isRequired,
    calendarQuery: PropTypes.func.isRequired,
    nextLocalID: PropTypes.number.isRequired,
    isThreadActive: PropTypes.bool.isRequired,
    userInfos: PropTypes.objectOf(userInfoPropType).isRequired,
    dispatchActionPromise: PropTypes.func.isRequired,
    joinThread: PropTypes.func.isRequired,
  };
  textarea: ?HTMLTextAreaElement;
  multimediaInput: ?HTMLInputElement;

  componentDidMount() {
    this.updateHeight();
    if (this.props.isThreadActive) {
      this.addReplyListener();
    }
  }

  componentWillUnmount() {
    if (this.props.isThreadActive) {
      this.removeReplyListener();
    }
  }

  componentDidUpdate(prevProps: Props) {
    if (this.props.isThreadActive && !prevProps.isThreadActive) {
      this.addReplyListener();
    } else if (!this.props.isThreadActive && prevProps.isThreadActive) {
      this.removeReplyListener();
    }

    const { inputState } = this.props;
    const prevInputState = prevProps.inputState;
    if (inputState.draft !== prevInputState.draft) {
      this.updateHeight();
    }
    const curUploadIDs = ChatInputBar.unassignedUploadIDs(
      inputState.pendingUploads,
    );
    const prevUploadIDs = ChatInputBar.unassignedUploadIDs(
      prevInputState.pendingUploads,
    );
    if (
      this.multimediaInput &&
      _difference(prevUploadIDs)(curUploadIDs).length > 0
    ) {
      // Whenever a pending upload is removed, we reset the file
      // HTMLInputElement's value field, so that if the same upload occurs again
      // the onChange call doesn't get filtered
      this.multimediaInput.value = '';
    } else if (
      this.textarea &&
      _difference(curUploadIDs)(prevUploadIDs).length > 0
    ) {
      // Whenever a pending upload is added, we focus the textarea
      this.textarea.focus();
      return;
    }

    if (this.props.threadInfo.id !== prevProps.threadInfo.id && this.textarea) {
      this.textarea.focus();
    }
  }

  static unassignedUploadIDs(
    pendingUploads: $ReadOnlyArray<PendingMultimediaUpload>,
  ) {
    return pendingUploads
      .filter(
        (pendingUpload: PendingMultimediaUpload) => !pendingUpload.messageID,
      )
      .map((pendingUpload: PendingMultimediaUpload) => pendingUpload.localID);
  }

  updateHeight() {
    const textarea = this.textarea;
    if (textarea) {
      textarea.style.height = 'auto';
      const newHeight = Math.min(textarea.scrollHeight, 150);
      textarea.style.height = `${newHeight}px`;
    }
  }

  addReplyListener() {
    invariant(
      this.props.inputState,
      'inputState should be set in addReplyListener',
    );
    this.props.inputState.addReplyListener(this.focusAndUpdateText);
  }

  removeReplyListener() {
    invariant(
      this.props.inputState,
      'inputState should be set in removeReplyListener',
    );
    this.props.inputState.removeReplyListener(this.focusAndUpdateText);
  }

  render() {
    const isMember = viewerIsMember(this.props.threadInfo);
    let joinButton = null;
    if (
      !isMember &&
      threadHasPermission(this.props.threadInfo, threadPermissions.JOIN_THREAD)
    ) {
      let buttonContent;
      if (this.props.joinThreadLoadingStatus === 'loading') {
        buttonContent = (
          <LoadingIndicator
            status={this.props.joinThreadLoadingStatus}
            size="medium"
            color="white"
          />
        );
      } else {
        buttonContent = Join Thread;
      }
      joinButton = (

          {buttonContent}

      );
    }

    const { pendingUploads, cancelPendingUpload } = this.props.inputState;
    const multimediaPreviews = pendingUploads.map((pendingUpload) => (
      <Multimedia
        uri={pendingUpload.uri}
        pendingUpload={pendingUpload}
        remove={cancelPendingUpload}
        multimediaCSSClass={css.multimedia}
        multimediaImageCSSClass={css.multimediaImage}
        key={pendingUpload.localID}
      />
    ));
    const previews =
      multimediaPreviews.length > 0 ? (

{multimediaPreviews}

      ) : null;

    let content;
    if (threadHasPermission(this.props.threadInfo, threadPermissions.VOICED)) {
      content = (

          <textarea
            rows="1"
            placeholder="Send a message..."
            value={this.props.inputState.draft}
            onChange={this.onChangeMessageText}
            onKeyDown={this.onKeyDown}
            ref={this.textareaRef}
            autoFocus
          />

            <input
              type="file"
              onChange={this.onMultimediaFileChange}
              ref={this.multimediaInputRef}
              accept={allowedMimeTypeString}
              multiple
            />
            <FontAwesomeIcon icon={faFileImage} />

            <FontAwesomeIcon icon={faChevronRight} className={css.sendButton} />
            Send

      );
    } else if (
      threadFrozenDueToViewerBlock(
        this.props.threadInfo,
        this.props.viewerID,
        this.props.userInfos,
      ) &&
      threadActualMembers(this.props.threadInfo.members).length === 2
    ) {
      content = (

          You can't send messages to a user that you've blocked.

      );
    } else if (isMember) {
      content = (

          You don't have permission to send messages.

      );
    } else {
      const defaultRoleID = Object.keys(this.props.threadInfo.roles).find(
        (roleID) => this.props.threadInfo.roles[roleID].isDefault,
      );
      invariant(
        defaultRoleID !== undefined,
        'all threads should have a default role',
      );
      const defaultRole = this.props.threadInfo.roles[defaultRoleID];
      const membersAreVoiced = !!defaultRole.permissions[
        threadPermissions.VOICED
      ];
      if (membersAreVoiced) {
        content = (

            Join this thread to send messages.

        );
      } else {
        content = (

            You don't have permission to send messages.

        );
      }
    }

    return (

        {joinButton}
        {previews}
        {content}

    );
  }

  textareaRef = (textarea: ?HTMLTextAreaElement) => {
    this.textarea = textarea;
    if (textarea) {
      textarea.focus();
    }
  };

  onChangeMessageText = (event: SyntheticEvent<HTMLTextAreaElement>) => {
    this.props.inputState.setDraft(event.currentTarget.value);
  };

  focusAndUpdateText = (text: string) => {
    // We need to call focus() first on Safari, otherwise the cursor
    // ends up at the start instead of the end for some reason
    const { textarea } = this;
    invariant(textarea, 'textarea should be set');
    textarea.focus();

    // We reset the textarea to an empty string at the start so that the cursor
    // always ends up at the end, even if the text doesn't actually change
    textarea.value = '';
    const currentText = this.props.inputState.draft;
    if (!currentText.startsWith(text)) {
      const prependedText = text.concat(currentText);
      this.props.inputState.setDraft(prependedText);
      textarea.value = prependedText;
    } else {
      textarea.value = currentText;
    }

    // The above strategies make sure the cursor is at the end,
    // but we also need to make sure that we're scrolled to the bottom
    textarea.scrollTop = textarea.scrollHeight;
  };

  onKeyDown = (event: SyntheticKeyboardEvent<HTMLTextAreaElement>) => {
    if (event.keyCode === 13 && !event.shiftKey) {
      event.preventDefault();
      this.send();
    }
  };

  onSend = (event: SyntheticEvent<HTMLAnchorElement>) => {
    event.preventDefault();
    this.send();
  };

  send() {
    let { nextLocalID } = this.props;

    const text = trimMessage(this.props.inputState.draft);
    if (text) {
      // TODO we should make the send button appear dynamically
      // iff trimmed text is nonempty, just like native
      this.dispatchTextMessageAction(text, nextLocalID);
      nextLocalID++;
    }

    this.props.inputState.createMultimediaMessage(nextLocalID);
  }

  dispatchTextMessageAction(text: string, nextLocalID: number) {
    this.props.inputState.setDraft('');

    const localID = `local${nextLocalID}`;
    const creatorID = this.props.viewerID;
    invariant(creatorID, 'should have viewer ID in order to send a message');
    this.props.inputState.sendTextMessage({
      type: messageTypes.TEXT,
      localID,
      threadID: this.props.threadInfo.id,
      text,
      creatorID,
      time: Date.now(),
    });
  }

  multimediaInputRef = (multimediaInput: ?HTMLInputElement) => {
    this.multimediaInput = multimediaInput;
  };

  onMultimediaClick = () => {
    if (this.multimediaInput) {
      this.multimediaInput.click();
    }
  };

  onMultimediaFileChange = async (
    event: SyntheticInputEvent<HTMLInputElement>,
  ) => {
    const result = await this.props.inputState.appendFiles([
      ...event.target.files,
    ]);
    if (!result && this.multimediaInput) {
      this.multimediaInput.value = '';
    }
  };

  onClickJoin = (event: SyntheticEvent<HTMLAnchorElement>) => {
    event.preventDefault();
    this.props.dispatchActionPromise(joinThreadActionTypes, this.joinAction());
  };

  async joinAction() {
    const query = this.props.calendarQuery();
    return await this.props.joinThread({
      threadID: this.props.threadInfo.id,
      calendarQuery: {
        startDate: query.startDate,
        endDate: query.endDate,
        filters: [
          ...query.filters,
          { type: 'threads', threadIDs: [this.props.threadInfo.id] },
        ],
      },
    });
  }
}

const joinThreadLoadingStatusSelector = createLoadingStatusSelector(
  joinThreadActionTypes,
);

export default React.memo<BaseProps>(function ConnectedChatInputBar(
  props: BaseProps,
) {
  const viewerID = useSelector(
    (state) => state.currentUserInfo && state.currentUserInfo.id,
  );
  const nextLocalID = useSelector((state) => state.nextLocalID);
  const isThreadActive = useSelector(
    (state) => props.threadInfo.id === state.navInfo.activeChatThreadID,
  );
  const userInfos = useSelector((state) => state.userStore.userInfos);
  const joinThreadLoadingStatus = useSelector(joinThreadLoadingStatusSelector);
  const calendarQuery = useSelector(nonThreadCalendarQuery);
  const dispatchActionPromise = useDispatchActionPromise();
  const callJoinThread = useServerCall(joinThread);
  return (
    <ChatInputBar
      {...props}
      viewerID={viewerID}
      joinThreadLoadingStatus={joinThreadLoadingStatus}
      calendarQuery={calendarQuery}
      nextLocalID={nextLocalID}
      isThreadActive={isThreadActive}
      userInfos={userInfos}
      dispatchActionPromise={dispatchActionPromise}
      joinThread={callJoinThread}
    />
  );
});
<|endoftext|>


<commit_before>import { seq, create } from "./seq";

import { error } from "./error";

import { element, attribute } from "./access";

import "rxjs/add/operator/skipWhile";
import "rxjs/add/operator/takeWhile";

//const _vnodeFromCx = (cx,node) => cx && "vnode" in cx ? cx.vnode : node.cx.vnode;

export function children($node,axis) {
	var contextNode, depth = NaN;
	// move to first contextNode if isNaN, or the same depth
	var f = axis && axis.f ?
		axis.f.__is_NodeTypeTest ?
			axis.f.__Accessor ?
				node => axis.f(node) && node.name == axis.f.__Accessor :
				axis.f :
			axis.f :
		n => n;
	let start = false, end = false;
	const test = node => {
		if(isNaN(depth)) depth = node.depth;
		let switchContext = false;
		if(node.depth == depth) {
			switchContext = true;
			contextNode = node;
		}
		start = start || (node.type != 17 && node.parent === contextNode && f(node));
		const flagEnd = end;
		end = end || (node.type == 17 && node.parent === contextNode && f(node.node));
		return switchContext || (start && !flagEnd);
	};
	return $node.takeWhile(node => test(node)).filter(node => node.depth > depth);
}

function Axis(g, f, type) {
	return {
		__is_Axis: true,
		__type: type || 1,
		f: f,
		g: g
	};
}
function child($f) {
	const cx = this;
	return seq($f).map(f => {
		var axis = Axis(void(0), f, 1);
		const stepper = $node => children.bind(cx)($node,axis);
		axis.g = stepper;
		return axis;
	});
}

function _axify($path) {
	return seq($path).concatMap(path => {
		if (!path.__is_Axis) {
			// process strings (can this be combined?)
			if (typeof path == "string") {
				var at = /^@/.test(path);
				if (at) path = path.substring(1);
				return at ? attribute(path) : child(element(path));
			} else if (typeof path == "function") {
				if (path.__is_NodeTypeTest) return child(path);
				return self(path);
			} else {
				return error("XXX", "Unknown axis provided");
			}
		}
		return seq(path);
	});
}

export function selectStreaming($node, ...paths) {
	var cx = this;
	$node = seq($node).skipWhile(node => node.type != 1);
	// make sure the root is a valid context
	//
	return seq(paths)
		.concatMap(path => seq(_axify(path)))
		// execute the axis stepping function,
		// it should now take care of the testing / transformation function
		.map(path => $node => path.g.bind(cx)($node))
		// update the context state
		.reduce(($node, changeFn) => changeFn($node),$node)
		.concatAll();
}
<commit_msg>moved rx ops to seq, added self, fixed children test for all paths<commit_after>import { seq, forEach } from "./seq";

import { error } from "./error";

import { element, attribute } from "./access";

export function children($node, axis) {
	var parentNode, childNode, descendingNode, depth = NaN;
	// move to first childNode if isNaN, or the same depth
	var f = axis && axis.f ?
		axis.f.__is_NodeTypeTest ?
			axis.f.__Accessor ?
				node => axis.f(node) && node.name == axis.f.__Accessor :
				axis.f :
			axis.f :
		n => n;
	const _hasAncestor = (node,maybeAncestor) => {
		while(node.parent) {
			if(node.parent == maybeAncestor) return true;
			node = node.parent;
		}
	};
	const test = (node) => {
		if (isNaN(depth)) depth = node.depth;
		// if depth is at node, switch context
		// TODO filter while depth > cxDepth, move cx down while nodetest true
		const isClose = node.type == 17;
		if(!isClose) {
			if (node.depth == depth) {
				parentNode = node;
				//console.log("init",node.type,node.name);
			} else if(node.parent == parentNode) {
				if(f(node)) {
					childNode = node;
					//console.log("cx",node.type,node.name);
				}
			} else if(node.parent == childNode) {
				descendingNode = node;
				//console.log("desc",node.type,node.name);
			}
		}
		// 1. never emit init
		// 2. emit only cx nodes that pass filter
		// 3. emit only nodes that have cx as ancestor
		return (isClose ? node.node : node) == parentNode ? false :
			(isClose ? node.node : node) == childNode ? true :
				(isClose ? node.node : node) == descendingNode ? true :
					_hasAncestor(node,childNode);
	};
	return $node.filter(test);
}

export function Axis(g, f, type) {
	return {
		__is_Axis: true,
		__type: type || 1,
		f: f,
		g: g
	};
}

export function child($f) {
	var cx = this;
	return seq($f).map(f => {
		var axis = Axis(void 0, f, 1);
		var stepper = $node => children.bind(cx || this)($node, axis);
		axis.g = stepper;
		return axis;
	});
}

export function self($f) {
	return seq($f).map(f => {
		var axis = Axis(void 0, f, 1);
		var stepper = $node => $node;
		axis.g = stepper;
		return axis;
	});
}

function _axify($path) {
	return seq($path).concatMap(path => {
		if (!path.__is_Axis) {
			// process strings (can this be combined?)
			if (typeof path == "string") {
				var at = /^@/.test(path);
				if (at) path = path.substring(1);
				return at ? attribute(path) : child(element(path));
			} else if (typeof path == "function") {
				if (path.__is_NodeTypeTest) return child(path);
				return self(path);
			} else {
				return error("XXX", "Unknown axis provided");
			}
		}
		return seq(path);
	});
}

export function selectStreaming($node, ...paths) {
	var cx = this;
	$node = seq($node).skipWhile(node => node.type != 1);
	// make sure the root is a valid context

	var axes = forEach(seq(paths),_axify);

	// execute the axis stepping function,
	// it should now take care of the testing / transformation function
	return axes
		.map(path => $node => path.g.bind(cx)($node))
		// update the context state
		.reduce(($node, changeFn) => changeFn($node), $node)
		.concatAll();
}
<|endoftext|>


<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// @license magnet:?xt=urn:btih:3877d6d54b3accd4bc32f8a48bf32ebc0901502a&dn=mpl-2.0.txt MPL 2.0
(function () {
    'use strict';
    /* global i18n: true */

    /**
     * 'Share' widget for GNU social
     * http://code.chromic.org/project/view/2/
     *
     * We make a few assumptions about the target instance:
     *   1) The API root is in the default location
     *   2) Fancy URLs are enabled
     *   3) CORS is allowed
     *   4) The Bookmark plugin is enabled
     *
     * If 1), 3) or 4) are wrong, we fall back to a regular
     * notice (instead of a bookmark notice)
     *
     * If 2) is wrong the user will be directed to a 404 :(
     */

    // TODO: input sanitation [1], [2]
    // TODO: server-side fallback if JS is disabled

    var createForm,
        bindClicks,
        frm,
        shareAsNotice,
        shareAsBookmark,
        extractURLParams,
        shareURL,
        shareTitle,
        closest,
        i18n;

    if (window.i18n === undefined) {
        i18n = {
            invalidId: 'The account id provided is invalid',
            yourAcctId: 'Your account ID:',
            idPlaceholder: 'user@example.org',
            shareAsBookmark: 'Share as a bookmark'
        };
    } else {
        i18n = window.i18n;
    }

    shareAsNotice = function (title, url, domain) {
        window.open('http://' + domain + '/notice/new?status_textarea=' + title + ' ' + url); // [1]
    };

    shareAsBookmark = function (title, url, domain) {
        window.open('http://' + domain + '/main/bookmark/new?url=' + url + '&title=' + title); // [2]
    };

    extractURLParams = function (queryStr) {
        var parts = queryStr.substr(1).split('&'),
            i, len, keyVal, params = {};

        for (i = 0, len = parts.length; i < len; i += 1) {
            keyVal = parts[i].split('=');
            params[keyVal[0]] = keyVal[1];
        }

        return params;
    };

    // Create the form that we'll re-use throughout the page
    createForm = function () {
        var err = document.createElement('div');
        err.setAttribute('class', 'gs-share-err');
        err.setAttribute('tabindex', '-1');
        err.setAttribute('aria-hidden', 'true');
        err.textContent = i18n.invalidId;

        frm = document.createElement('form');

        frm.setAttribute('class', 'gs-share-form');
        frm.setAttribute('tabindex', '-1');
        frm.setAttribute('aria-hidden', 'true');

        frm.innerHTML = '' + i18n.yourAcctId + '' +
            '<input type="text" id="gs-account" placeholder="' + i18n.idPlaceholder + '" />' +
            '<input type="checkbox" checked id="gs-bookmark" /> ' + i18n.shareAsBookmark + '' +
            '<input type="submit" />';
        frm.insertBefore(err, frm.firstChild);

        // Submit handler
        frm.addEventListener('submit', function (e) {
            e.preventDefault();

            var accountParts = document.getElementById('gs-account').value.split('@'),
                username, domain, xhr, bookmarkURL;

            if (accountParts.length === 2) {
                err.setAttribute('aria-hidden', 'true');

                username = accountParts[0];
                domain = accountParts[1];
                bookmarkURL = 'http://' + domain + '/api/bookmarks/' + username + '.json';

                // Try bookmark
                if (document.getElementById('gs-bookmark').checked) {
                    xhr = new XMLHttpRequest();

                    xhr.onreadystatechange = function () {
                        if (xhr.readyState === 4) {
                            if (xhr.status === 200) { // Success
                                shareAsBookmark(shareTitle, shareURL, domain);
                            } else { // Failure, fallback to regular notice
                                shareAsNotice(shareTitle, shareURL, domain);
                            }
                        }
                    };

                    xhr.open('GET', bookmarkURL, true);
                    xhr.send();
                } else { // Regular notice
                    shareAsNotice(shareTitle, shareURL, domain);
                }
            } else { // Invalid account id
                err.setAttribute('aria-hidden', 'false');
                err.focus();
            }
        });

        // Keydown handler
        frm.addEventListener('keydown', function (e) {
            if (e.keyCode === 27) { // Escape key closes the dialog
                frm.parentElement.getElementsByClassName('js-gs-share')[0].focus();
                frm.setAttribute('aria-hidden', 'true');
            }
        });

        document.body.appendChild(frm);
    };

    /**
     * Something similar to jQuery.closest
     *
     * Given `elm`, return the closest parent with class `cls`
     * or false if there is no matching ancestor.
     */
    closest = function (elm, cls) {
        while (elm !== document) {
            if (elm.classList.contains(cls)) {
                return elm;
            }

            elm = elm.parentNode;
        }

        return false;
    };

    bindClicks = function () {
        document.addEventListener('click', function (e) {
            var target = e.target,
                urlParams,
                lnk = closest(target, 'js-gs-share');

            // Don't do anything on right/middle click or if ctrl or shift was pressed while left-clicking
            if (!e.button && !e.ctrlKey && !e.shiftKey && lnk) {
                e.preventDefault();

                // Check for submission information in href first
                if (lnk.search !== undefined) {
                    urlParams = extractURLParams(lnk.search);
                    shareURL   = urlParams.url;
                    shareTitle = urlParams.title;
                } else { // If it's not there, try data-* attributes. If not, use current document url and title
                    shareURL   = lnk.getAttribute('data-url') || window.location.href;
                    shareTitle = lnk.getAttribute('data-title') || document.title;
                }

                // Move form after the clicked link
                lnk.parentNode.appendChild(frm);

                // Show form
                frm.setAttribute('aria-hidden', 'false');

                // Focus on form
                frm.focus();
            } else if (!frm.contains(target)) {
                frm.setAttribute('aria-hidden', 'true');
            }
        });
    };

    // Flag that js is enabled
    document.body.classList.add('js-gs-share-enabled');

    createForm();
    bindClicks();
}());
// @license-end
<commit_msg>Removes unecessary jshint global<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// @license magnet:?xt=urn:btih:3877d6d54b3accd4bc32f8a48bf32ebc0901502a&dn=mpl-2.0.txt MPL 2.0
(function () {
    'use strict';
    /**
     * 'Share' widget for GNU social
     * http://code.chromic.org/project/view/2/
     *
     * We make a few assumptions about the target instance:
     *   1) The API root is in the default location
     *   2) Fancy URLs are enabled
     *   3) CORS is allowed
     *   4) The Bookmark plugin is enabled
     *
     * If 1), 3) or 4) are wrong, we fall back to a regular
     * notice (instead of a bookmark notice)
     *
     * If 2) is wrong the user will be directed to a 404 :(
     */

    // TODO: input sanitation [1], [2]
    // TODO: server-side fallback if JS is disabled

    var createForm,
        bindClicks,
        frm,
        shareAsNotice,
        shareAsBookmark,
        extractURLParams,
        shareURL,
        shareTitle,
        closest,
        i18n;

    if (window.i18n === undefined) {
        i18n = {
            invalidId: 'The account id provided is invalid',
            yourAcctId: 'Your account ID:',
            idPlaceholder: 'user@example.org',
            shareAsBookmark: 'Share as a bookmark'
        };
    } else {
        i18n = window.i18n;
    }

    shareAsNotice = function (title, url, domain) {
        window.open('http://' + domain + '/notice/new?status_textarea=' + title + ' ' + url); // [1]
    };

    shareAsBookmark = function (title, url, domain) {
        window.open('http://' + domain + '/main/bookmark/new?url=' + url + '&title=' + title); // [2]
    };

    extractURLParams = function (queryStr) {
        var parts = queryStr.substr(1).split('&'),
            i, len, keyVal, params = {};

        for (i = 0, len = parts.length; i < len; i += 1) {
            keyVal = parts[i].split('=');
            params[keyVal[0]] = keyVal[1];
        }

        return params;
    };

    // Create the form that we'll re-use throughout the page
    createForm = function () {
        var err = document.createElement('div');
        err.setAttribute('class', 'gs-share-err');
        err.setAttribute('tabindex', '-1');
        err.setAttribute('aria-hidden', 'true');
        err.textContent = i18n.invalidId;

        frm = document.createElement('form');

        frm.setAttribute('class', 'gs-share-form');
        frm.setAttribute('tabindex', '-1');
        frm.setAttribute('aria-hidden', 'true');

        frm.innerHTML = '' + i18n.yourAcctId + '' +
            '<input type="text" id="gs-account" placeholder="' + i18n.idPlaceholder + '" />' +
            '<input type="checkbox" checked id="gs-bookmark" /> ' + i18n.shareAsBookmark + '' +
            '<input type="submit" />';
        frm.insertBefore(err, frm.firstChild);

        // Submit handler
        frm.addEventListener('submit', function (e) {
            e.preventDefault();

            var accountParts = document.getElementById('gs-account').value.split('@'),
                username, domain, xhr, bookmarkURL;

            if (accountParts.length === 2) {
                err.setAttribute('aria-hidden', 'true');

                username = accountParts[0];
                domain = accountParts[1];
                bookmarkURL = 'http://' + domain + '/api/bookmarks/' + username + '.json';

                // Try bookmark
                if (document.getElementById('gs-bookmark').checked) {
                    xhr = new XMLHttpRequest();

                    xhr.onreadystatechange = function () {
                        if (xhr.readyState === 4) {
                            if (xhr.status === 200) { // Success
                                shareAsBookmark(shareTitle, shareURL, domain);
                            } else { // Failure, fallback to regular notice
                                shareAsNotice(shareTitle, shareURL, domain);
                            }
                        }
                    };

                    xhr.open('GET', bookmarkURL, true);
                    xhr.send();
                } else { // Regular notice
                    shareAsNotice(shareTitle, shareURL, domain);
                }
            } else { // Invalid account id
                err.setAttribute('aria-hidden', 'false');
                err.focus();
            }
        });

        // Keydown handler
        frm.addEventListener('keydown', function (e) {
            if (e.keyCode === 27) { // Escape key closes the dialog
                frm.parentElement.getElementsByClassName('js-gs-share')[0].focus();
                frm.setAttribute('aria-hidden', 'true');
            }
        });

        document.body.appendChild(frm);
    };

    /**
     * Something similar to jQuery.closest
     *
     * Given `elm`, return the closest parent with class `cls`
     * or false if there is no matching ancestor.
     */
    closest = function (elm, cls) {
        while (elm !== document) {
            if (elm.classList.contains(cls)) {
                return elm;
            }

            elm = elm.parentNode;
        }

        return false;
    };

    bindClicks = function () {
        document.addEventListener('click', function (e) {
            var target = e.target,
                urlParams,
                lnk = closest(target, 'js-gs-share');

            // Don't do anything on right/middle click or if ctrl or shift was pressed while left-clicking
            if (!e.button && !e.ctrlKey && !e.shiftKey && lnk) {
                e.preventDefault();

                // Check for submission information in href first
                if (lnk.search !== undefined) {
                    urlParams = extractURLParams(lnk.search);
                    shareURL   = urlParams.url;
                    shareTitle = urlParams.title;
                } else { // If it's not there, try data-* attributes. If not, use current document url and title
                    shareURL   = lnk.getAttribute('data-url') || window.location.href;
                    shareTitle = lnk.getAttribute('data-title') || document.title;
                }

                // Move form after the clicked link
                lnk.parentNode.appendChild(frm);

                // Show form
                frm.setAttribute('aria-hidden', 'false');

                // Focus on form
                frm.focus();
            } else if (!frm.contains(target)) {
                frm.setAttribute('aria-hidden', 'true');
            }
        });
    };

    // Flag that js is enabled
    document.body.classList.add('js-gs-share-enabled');

    createForm();
    bindClicks();
}());
// @license-end
<|endoftext|>


<commit_before>class Adapter {
  constructor(deps) {
    deps.forEach(dep => window[dep]())
    this._defaultBranch = {}
  }

  /**
   * Loads the code tree of a repository.
   * @param {Object} opts: {
   *                  path: the starting path to load the tree,
   *                  repo: the current repository,
   *                  node (optional): the selected node (null to load entire tree),
   *                  token (optional): the personal access token
   *                 }
   * @param {Function} transform(item)
   * @param {Function} cb(err: error, tree: Array[Array|item])
   */
  _loadCodeTree(opts, transform, cb) {
    const folders = { '': [] }
    const $dummyDiv = $('
')
    const {path, repo, node} = opts

    opts.encodedBranch = opts.encodedBranch || encodeURIComponent(decodeURIComponent(repo.branch))

    this._getTree(path, opts, (err, tree) => {
      if (err) return cb(err)

      this._getSubmodules(tree, opts, (err, submodules) => {
        if (err) return cb(err)

        submodules = submodules || {}

        const nextChunk = (iteration = 0) => {
          const CHUNK_SIZE = 300

          for (let i = 0; i < CHUNK_SIZE; i++) {
            const item = tree[iteration * CHUNK_SIZE + i]

            // we're done
            if (item === undefined) {
              return cb(null, folders[''])
            }

            // runs transform requested by subclass
            if (transform) {
              transform(item)
            }

            // if lazy load and has parent, prefix with parent path
            if (node && node.path) {
              item.path = node.path + '/' + item.path
            }

            const path = item.path
            const type = item.type
            const index = path.lastIndexOf('/')
            const name = $dummyDiv.text(path.substring(index + 1)).html() // sanitizes, closes #9

            item.id = NODE_PREFIX + path
            item.text = name
            item.icon = type // uses `type` as class name for tree node

            if (node) {
              folders[''].push(item)
            }
            else {
              folders[path.substring(0, index)].push(item)
            }

            if (type === 'tree' || type === 'blob') {
              if (type === 'tree') {
                if (node) item.children = true
                else folders[item.path] = item.children = []
              }

              // encodes but retains the slashes, see #274
              const encodedPath = path.split('/').map(encodeURIComponent).join('/')
              item.a_attr = {
                href: `/${repo.username}/${repo.reponame}/${type}/${repo.branch}/${encodedPath}`
              }
            }
            else if (type === 'commit') {
              let moduleUrl = submodules[item.path]

              if (moduleUrl) { // fixes #105
                // special handling for submodules hosted in GitHub
                if (~moduleUrl.indexOf('github.com')) {
                  moduleUrl = moduleUrl.replace(/^git(:\/\/|@)/, window.location.protocol + '//')
                                       .replace('github.com:', 'github.com/')
                                       .replace(/.git$/, '')
                  item.text = `${name}
                               @
                               ${item.sha.substr(0, 7)}`
                }
                item.a_attr = { href: moduleUrl }
              }
            }
          }

          setTimeout(() => nextChunk(iteration + 1))
        }

        nextChunk()
      })
    })
  }

  _handleError(jqXHR, cb) {
    let error, message, needAuth

    switch (jqXHR.status) {
      case 0:
        error = 'Connection error'
        message =
          `Cannot connect to website.
           If your network connection to this website is fine, maybe there is an outage of the API.
           Please try again later.`
        needAuth = false
        break
      case 206:
        error = 'Repo too large'
        message =
          `This repository is too large to be retrieved at once.
           If you frequently work with this repository, go to Settings and uncheck the "Load entire tree at once" option.`
        break
      case 401:
        error = 'Invalid token'
        message =
          `The token is invalid.
           Follow this link
           to create a new token and paste it below.`
        needAuth = true
        break
      case 409:
        error = 'Empty repository'
        message = 'This repository is empty.'
        break
      case 404:
        error = 'Private repository'
        message =
          `Accessing private repositories requires a access token.
           Follow this link
           to create one and paste it below.`
        needAuth = true
        break
      case 403:
        if (~jqXHR.getAllResponseHeaders().indexOf('X-RateLimit-Remaining: 0')) {
          // It's kinda specific for GitHub
          error = 'API limit exceeded'
          message =
            `You have exceeded the GitHub API hourly limit and need GitHub access token
             to make extra requests. Follow this link
             to create one and paste it below.`
          needAuth = true
          break
        }
        else {
          error = 'Forbidden'
          message =
            `You are not allowed to access the API.
             You might need to provide an access token.
             Follow this link
             to create one and paste it below.`
          needAuth = true
          break
        }
      default:
        error = message = jqXHR.statusText
        needAuth = false
        break
    }
    cb({
      error: `Error: ${error}`,
      message: message,
      needAuth: needAuth
    })
  }

  /**
   * Inits behaviors after the sidebar is added to the DOM.
   * @api public
   */
  init($sidebar) {
    $sidebar
      .resizable({ handles: 'e', minWidth: this.getMinWidth() })
      .addClass(this.getCssClass())
  }

  /**
   * Returns the CSS class to be added to the Octotree sidebar.
   * @api protected
   */
  getCssClass() {
    throw new Error('Not implemented')
  }

  /**
   * Returns the minimum width acceptable for the sidebar.
   * @api protected
   */
  getMinWidth() {
    return 200
  }

  /**
   * Returns whether the adapter is capable of loading the entire tree in
   * a single request. This is usually determined by the underlying the API.
   * @api public
   */
  canLoadEntireTree() {
    return false
  }

  /**
   * Loads the code tree.
   * @api public
   */
  loadCodeTree(opts, cb) {
    throw new Error('Not implemented')
  }

  /**
   * Returns the URL to create a personal access token.
   * @api public
   */
  getCreateTokenUrl() {
    throw new Error('Not implemented')
  }

  /**
   * Updates the layout based on sidebar visibility and width.
   * @api public
   */
  updateLayout(togglerVisible, sidebarVisible, sidebarWidth) {
    throw new Error('Not implemented')
  }

  /**
   * Returns repo info at the current path.
   * @api public
   */
  getRepoFromPath(showInNonCodePage, currentRepo, token, cb) {
    throw new Error('Not implemented')
  }

  /**
   * Selects the file at a specific path.
   * @api public
   */
  selectFile(path) {
    window.location.href = path
  }

  /**
   * Selects a submodule.
   * @api public
   */
  selectSubmodule(path) {
    window.location.href = path
  }

  /**
   * Opens file or submodule in a new tab.
   * @api public
   */
  openInNewTab(path) {
    window.open(path, '_blank').focus()
  }

  /**
   * Downloads a file.
   * @api public
   */
  downloadFile(path, fileName) {
    const link = document.createElement('a')
    link.setAttribute('href', path.replace(/\/blob\//, '/raw/'))
    link.setAttribute('download', fileName)
    link.click()
  }

  /**
   * Gets tree at path.
   * @param {Object} opts - {token, repo}
   * @api protected
   */
  _getTree(path, opts, cb) {
    throw new Error('Not implemented')
  }

  /**
   * Gets submodules in the tree.
   * @param {Object} opts - {token, repo, encodedBranch}
   * @api protected
   */
  _getSubmodules(tree, opts, cb) {
    throw new Error('Not implemented')
  }
}
<commit_msg>Fixed typo. (#391)<commit_after>class Adapter {
  constructor(deps) {
    deps.forEach(dep => window[dep]())
    this._defaultBranch = {}
  }

  /**
   * Loads the code tree of a repository.
   * @param {Object} opts: {
   *                  path: the starting path to load the tree,
   *                  repo: the current repository,
   *                  node (optional): the selected node (null to load entire tree),
   *                  token (optional): the personal access token
   *                 }
   * @param {Function} transform(item)
   * @param {Function} cb(err: error, tree: Array[Array|item])
   */
  _loadCodeTree(opts, transform, cb) {
    const folders = { '': [] }
    const $dummyDiv = $('
')
    const {path, repo, node} = opts

    opts.encodedBranch = opts.encodedBranch || encodeURIComponent(decodeURIComponent(repo.branch))

    this._getTree(path, opts, (err, tree) => {
      if (err) return cb(err)

      this._getSubmodules(tree, opts, (err, submodules) => {
        if (err) return cb(err)

        submodules = submodules || {}

        const nextChunk = (iteration = 0) => {
          const CHUNK_SIZE = 300

          for (let i = 0; i < CHUNK_SIZE; i++) {
            const item = tree[iteration * CHUNK_SIZE + i]

            // we're done
            if (item === undefined) {
              return cb(null, folders[''])
            }

            // runs transform requested by subclass
            if (transform) {
              transform(item)
            }

            // if lazy load and has parent, prefix with parent path
            if (node && node.path) {
              item.path = node.path + '/' + item.path
            }

            const path = item.path
            const type = item.type
            const index = path.lastIndexOf('/')
            const name = $dummyDiv.text(path.substring(index + 1)).html() // sanitizes, closes #9

            item.id = NODE_PREFIX + path
            item.text = name
            item.icon = type // uses `type` as class name for tree node

            if (node) {
              folders[''].push(item)
            }
            else {
              folders[path.substring(0, index)].push(item)
            }

            if (type === 'tree' || type === 'blob') {
              if (type === 'tree') {
                if (node) item.children = true
                else folders[item.path] = item.children = []
              }

              // encodes but retains the slashes, see #274
              const encodedPath = path.split('/').map(encodeURIComponent).join('/')
              item.a_attr = {
                href: `/${repo.username}/${repo.reponame}/${type}/${repo.branch}/${encodedPath}`
              }
            }
            else if (type === 'commit') {
              let moduleUrl = submodules[item.path]

              if (moduleUrl) { // fixes #105
                // special handling for submodules hosted in GitHub
                if (~moduleUrl.indexOf('github.com')) {
                  moduleUrl = moduleUrl.replace(/^git(:\/\/|@)/, window.location.protocol + '//')
                                       .replace('github.com:', 'github.com/')
                                       .replace(/.git$/, '')
                  item.text = `${name}
                               @
                               ${item.sha.substr(0, 7)}`
                }
                item.a_attr = { href: moduleUrl }
              }
            }
          }

          setTimeout(() => nextChunk(iteration + 1))
        }

        nextChunk()
      })
    })
  }

  _handleError(jqXHR, cb) {
    let error, message, needAuth

    switch (jqXHR.status) {
      case 0:
        error = 'Connection error'
        message =
          `Cannot connect to website.
           If your network connection to this website is fine, maybe there is an outage of the API.
           Please try again later.`
        needAuth = false
        break
      case 206:
        error = 'Repo too large'
        message =
          `This repository is too large to be retrieved at once.
           If you frequently work with this repository, go to Settings and uncheck the "Load entire tree at once" option.`
        break
      case 401:
        error = 'Invalid token'
        message =
          `The token is invalid.
           Follow this link
           to create a new token and paste it below.`
        needAuth = true
        break
      case 409:
        error = 'Empty repository'
        message = 'This repository is empty.'
        break
      case 404:
        error = 'Private repository'
        message =
          `Accessing private repositories requires an access token.
           Follow this link
           to create one and paste it below.`
        needAuth = true
        break
      case 403:
        if (~jqXHR.getAllResponseHeaders().indexOf('X-RateLimit-Remaining: 0')) {
          // It's kinda specific for GitHub
          error = 'API limit exceeded'
          message =
            `You have exceeded the GitHub API hourly limit and need GitHub access token
             to make extra requests. Follow this link
             to create one and paste it below.`
          needAuth = true
          break
        }
        else {
          error = 'Forbidden'
          message =
            `You are not allowed to access the API.
             You might need to provide an access token.
             Follow this link
             to create one and paste it below.`
          needAuth = true
          break
        }
      default:
        error = message = jqXHR.statusText
        needAuth = false
        break
    }
    cb({
      error: `Error: ${error}`,
      message: message,
      needAuth: needAuth
    })
  }

  /**
   * Inits behaviors after the sidebar is added to the DOM.
   * @api public
   */
  init($sidebar) {
    $sidebar
      .resizable({ handles: 'e', minWidth: this.getMinWidth() })
      .addClass(this.getCssClass())
  }

  /**
   * Returns the CSS class to be added to the Octotree sidebar.
   * @api protected
   */
  getCssClass() {
    throw new Error('Not implemented')
  }

  /**
   * Returns the minimum width acceptable for the sidebar.
   * @api protected
   */
  getMinWidth() {
    return 200
  }

  /**
   * Returns whether the adapter is capable of loading the entire tree in
   * a single request. This is usually determined by the underlying the API.
   * @api public
   */
  canLoadEntireTree() {
    return false
  }

  /**
   * Loads the code tree.
   * @api public
   */
  loadCodeTree(opts, cb) {
    throw new Error('Not implemented')
  }

  /**
   * Returns the URL to create a personal access token.
   * @api public
   */
  getCreateTokenUrl() {
    throw new Error('Not implemented')
  }

  /**
   * Updates the layout based on sidebar visibility and width.
   * @api public
   */
  updateLayout(togglerVisible, sidebarVisible, sidebarWidth) {
    throw new Error('Not implemented')
  }

  /**
   * Returns repo info at the current path.
   * @api public
   */
  getRepoFromPath(showInNonCodePage, currentRepo, token, cb) {
    throw new Error('Not implemented')
  }

  /**
   * Selects the file at a specific path.
   * @api public
   */
  selectFile(path) {
    window.location.href = path
  }

  /**
   * Selects a submodule.
   * @api public
   */
  selectSubmodule(path) {
    window.location.href = path
  }

  /**
   * Opens file or submodule in a new tab.
   * @api public
   */
  openInNewTab(path) {
    window.open(path, '_blank').focus()
  }

  /**
   * Downloads a file.
   * @api public
   */
  downloadFile(path, fileName) {
    const link = document.createElement('a')
    link.setAttribute('href', path.replace(/\/blob\//, '/raw/'))
    link.setAttribute('download', fileName)
    link.click()
  }

  /**
   * Gets tree at path.
   * @param {Object} opts - {token, repo}
   * @api protected
   */
  _getTree(path, opts, cb) {
    throw new Error('Not implemented')
  }

  /**
   * Gets submodules in the tree.
   * @param {Object} opts - {token, repo, encodedBranch}
   * @api protected
   */
  _getSubmodules(tree, opts, cb) {
    throw new Error('Not implemented')
  }
}
<|endoftext|>


<commit_before>jQuery.validator.addMethod("nieES",function(value, element) {

	"use strict";

	var n = 0,
		suma = 0,
		pos = '',
		NIE = value.toUpperCase(),
		cadenadni = "TRWAGMYFPDXBNJZSQVHLCKE";

	// Basic format test
	if (!NIE.match('((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)')) {
		return false;
	}

	//comprobacion de NIEs
	//T
	if (/^[T]{1}/.test(NIE)){
		if (NIE[8] === /^[T]{1}[A-Z0-9]{8}$/.test(NIE)){
			return true;
		}else{
			return false;
		}
	}

	//XYZ
	if (/^[XYZ]{1}/.test(NIE)){
		pos = NIE.replace('X','0');
		pos = pos.replace('Y','1');
		pos = pos.replace('Z','2');
		pos = pos.substring(0, 8) % 23;
		if (NIE[8] === cadenadni.substring(pos, pos + 1)){
			return true;
		}
		else{
			return false;
		}
	}

	return false;

}, "Please specify a valid NIE number.");

<commit_msg>NIF, NIE and CIF numbers validation:<commit_after>jQuery.validator.addMethod("nieES",function(value, element) {

	"use strict";

	var n = 0,
		suma = 0,
		pos = '',
		NIE = value.toUpperCase(),
		cadenadni = "TRWAGMYFPDXBNJZSQVHLCKE";

	// Basic format test
	if (!NIE.match('((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)')) {
		return false;
	}

	//comprobacion de NIEs
	//T
	if (/^[T]{1}/.test(NIE)){
		return (NIE[8] === /^[T]{1}[A-Z0-9]{8}$/.test(NIE));
	}

	//XYZ
	if (/^[XYZ]{1}/.test(NIE)){
		pos = NIE.replace('X','0');
		pos = pos.replace('Y','1');
		pos = pos.replace('Z','2');
		pos = pos.substring(0, 8) % 23;
		return (NIE[8] === cadenadni.substring(pos, pos + 1));
	}

	return false;

}, "Please specify a valid NIE number.");

<|endoftext|>


<commit_before><commit_msg>updated setTheFile to show remove button<commit_after><|endoftext|>


<commit_before><commit_msg>gui: Fix GUI boot (#1464)<commit_after><|endoftext|>


<commit_before>import database from '../database';
import User from '../database/models/user';
import BlogPost from '../database/models/blog-post';
import log from '../log';
import getNewPosts from './get-users-new-posts';
import settle from 'promise-settle';

function collateUpdateInformation(getNewPostResults) {
  const newAndModifiedBlogMetadata = [];
  getNewPostResults.forEach(result => {
    if (result.isFulfilled()) {
      const postMetadata = result.value();
      if (postMetadata) {
        newAndModifiedBlogMetadata.push(result.value());
      }
    } else {
      log.warn({ error: `${result.reason()}` }, 'getUsersNewPost promise was rejected');
    }
  });
  return newAndModifiedBlogMetadata;
}

export default function aggregate() {
  return new Promise((resolve, reject) => {
    database.sync()
      .then(() => {
        const users = User.findAll({
          attributes: ['id', 'authenticationProvider', 'firstName', 'lastName', 'blogFeedURI']
        });
        log.info({ users }, 'Users loaded from database');
        return users;
      })
      .then(users => {
        const getUsersNewPostsPromises = users.map(user => getNewPosts(user));
        return settle(getUsersNewPostsPromises);
      })
      .then(collateUpdateInformation)
      .then(updates =>
        database.transaction(transaction =>
          BlogPost.bulkCreate(updates.map(update => update.newPosts).reduce((a, b) => a.concat(b)), { transaction })
        ).then(() => {
          log.info('Transaction complete');
        })
      )
      .then(resolve)
      .catch(reject);
  });
}
<commit_msg>Added User.feedLastModified updating as part of transaction<commit_after>import database from '../database';
import User from '../database/models/user';
import BlogPost from '../database/models/blog-post';
import log from '../log';
import getNewPosts from './get-users-new-posts';
import settle from 'promise-settle';

function collateUpdateInformation(getNewPostResults) {
  const newAndModifiedBlogMetadata = [];
  getNewPostResults.forEach(result => {
    if (result.isFulfilled()) {
      const postMetadata = result.value();
      if (postMetadata) {
        newAndModifiedBlogMetadata.push(result.value());
      }
    } else {
      log.warn({ error: `${result.reason()}` }, 'getUsersNewPost promise was rejected');
    }
  });
  return newAndModifiedBlogMetadata;
}

export default function aggregate() {
  return new Promise((resolve, reject) => {
    database.sync()
      .then(() => {
        const users = User.findAll({
          attributes: ['id', 'authenticationProvider', 'firstName', 'lastName', 'blogFeedURI']
        });
        log.info({ users }, 'Users loaded from database');
        return users;
      })
      .then(users => {
        const getUsersNewPostsPromises = users.map(user => getNewPosts(user));
        return settle(getUsersNewPostsPromises);
      })
      .then(collateUpdateInformation)
      .then(updates =>
        database.transaction(transaction =>
          BlogPost.bulkCreate(updates.map(update => update.newPosts).reduce((a, b) => a.concat(b)), { transaction })
          .then(() => {
            const authorUpdatePromises = updates.map(update => User.update({ feedLastModified: update.lastModifiedDate },
                                                                          { where: { id: update.authorId }, transaction }));
            return Promise.all(authorUpdatePromises);
          })
        )
      )
      .then(resolve)
      .catch(reject);
  });
}
<|endoftext|>


<commit_before>/**
 * Package dependencies
 */
const base64url = require('base64url')
const crypto = require('crypto')
const {spawnSync} = require('child_process')
const {TextEncoder, TextDecoder} = require('text-encoding')
const keyto = require('@trust/keyto')
const elEdDSA = require('elliptic').eddsa;
const elliptic = require('elliptic')

/**
 * Local dependencies
 */
const Algorithm = require ('../algorithms/Algorithm')
const CryptoKey = require('../keys/CryptoKey')
const CryptoKeyPair = require('../keys/CryptoKeyPair')
const JsonWebKey = require('../keys/JsonWebKey')
const KeyAlgorithm = require('../dictionaries/KeyAlgorithm')
const EcKeyAlgorithm = require('../dictionaries/EcKeyAlgorithm')

/**
 * Errors
 */
const {
  DataError,
  OperationError,
  InvalidAccessError,
  KeyFormatNotSupportedError,
  CurrentlyNotSupportedError
} = require('../errors')

/**
 * EDDSA
 */
class EDDSA extends Algorithm {

  /**
   * dictionaries
   */
  static get dictionaries () {
    return [
      KeyAlgorithm,
      EcKeyAlgorithm
    ]
  }

  /**
   * members
   */
  static get members () {
    return {
      name: String
    }
  }

  /**
   * sign
   *
   * @description
   * Create a digital signature
   *
   * @param {CryptoKey} key
   * @param {BufferSource} data
   *
   * @returns {ArrayBuffer}
   */
  sign (key, data) {
    let result
    // Ensure the key is a private type only
    if (key.type !== 'private'){
      throw new InvalidAccessError('Signing requires a private key')
    }

    // Ensure data is hex string, array or Buffer
    if (!Array.isArray(data) && !Buffer.isBuffer(data) && typeof data !== 'string'){
      throw new DataError('Data must be an Array, Buffer or hex string')
    }

    // Ensure key.handle is hex string, array or Buffer
    if (!Array.isArray(key.handle) && !Buffer.isBuffer(key.handle) && typeof key.handle !== 'string' ){
      throw DataError('Key handle must be an Array, Buffer or hex string')
    }

    try {
      // Create curve via elliptic
      let ec = new elEdDSA('ed25519')

      // Generate keypair from private key
      let ecKey
      if (typeof key.handle === 'string'){
        ecKey = ec.keyFromSecret(key.handle, 'hex')
      } else {
        ecKey = ec.keyFromSecret(key.handle)
      }

      // Perform the signing
      result = ecKey.sign(data).toHex()
    } catch (error) {
      throw new OperationError(error.message)
    }
    // Return resulting buffer
    return Uint8Array.from(Buffer.from(result,'hex')).buffer
  }//sign

  /**
   * verify
   *
   * @description
   * Verifies a digital signature
   *
   * @param {CryptoKey} key
   * @param {BufferSource} signature
   * @param {BufferSource} data
   *
   * @returns {Boolean}
   */
  verify (key, signature, data) {
    let result
    // Ensure the key is a public or secret type
    if (key.type !== 'secret' && key.type !== 'public') {
      throw new InvalidAccessError('Verifying requires a public or secret key')
    }

    // Ensure data is hex string, array or Buffer
    if (!Array.isArray(data) && !Buffer.isBuffer(data) && typeof data !== 'string'){
      throw new DataError('Data must be an Array, Buffer or hex string')
    }

    // Ensure key.handle is hex string, array or Buffer
    if (!Array.isArray(key.handle) && !Buffer.isBuffer(key.handle) && typeof key.handle !== 'string' ){
      throw DataError('Key handle must be an Array, Buffer or hex string')
    }

    // Ensure signature is ArrayBuffer or hex string
    if (!signature instanceof ArrayBuffer && typeof signature !== 'string'){
      throw DataError('Signature must be an ArrayBuffer or hex string')
    }

    try {
      // Create curve via elliptic
      let ec = new elEdDSA('ed25519')

      // Generate keypair from key
      let ecKey
      if (key.type === 'private' && key.extractable){
        if (typeof key.handle === 'string'){
          ecKey = ec.keyFromSecret(key.handle, 'hex')
        } else {
          ecKey = ec.keyFromSecret(key.handle)
        }
      } else
      if (key.type === 'public'){
        if (typeof key.handle === 'string'){
          ecKey = ec.keyFromPublic(key.handle, 'hex')
        } else {
          ecKey = ec.keyFromPublic(key.handle.toString('hex'),'hex')
        }
      } else {
        throw new OperationError("Invalid key type")
      }

      // Convert ArrayBuffer back to hex
      if (signature instanceof ArrayBuffer){
        signature = Buffer.from(signature).toString('hex')
      }

      // Perform the verification
      result = ecKey.verify(data,signature)
    } catch (error) {
      throw new OperationError(error.message)
    }
    // Return boolean result
    return result
  }//verify

  /**
   * generateKey
   *
   * @description
   * Generate an EDDSA key pair
   *
   * @param {EcKeyGenParams} params
   * @returns {CryptoKey}
   */
  generateKey (params, extractable, usages) {
    // Validate usages
    usages.forEach(usage => {
        if (usage !== 'sign' && usage !== 'verify') {
          throw new SyntaxError('Key usages can only include "sign", or "verify"')
        }
    })
    // Generate random key for scret portion
    let secretBytes = crypto.randomBytes(32)

    // Derive public bytes
    let ec = new elEdDSA('ed25519')
    let ecKey = ec.keyFromSecret(secretBytes)
    let pubKey = Buffer.from(ecKey.pubBytes())

    // Set algorithm be a new EDDSA
    let algorithm = new EDDSA(params)

    // Create private key object
    let privateKey = new CryptoKey({
      type: 'private',
      algorithm,
      extractable,
      usages: ['sign'],
      handle: secretBytes
    })

    // Create public key object
    let publicKey = new CryptoKey({
      type: 'public',
      algorithm,
      extractable: true,
      usages: ['verify'],
      handle: pubKey
    })

    // Return the generated Key
    return new CryptoKeyPair({publicKey,privateKey})
  }//generateKey

  /**
   * importKey
   *
   * @description
   *
   * @param {string} format
   * @param {string|JsonWebKey} keyData
   * @param {KeyAlgorithm} algorithm
   * @param {Boolean} extractable
   * @param {Array} keyUsage
   *
   * @returns {CryptoKey}
   */
  importKey (format, keyData, algorithm, extractable, keyUsages) {
    let key, hash, normalizedHash, jwk, privateKeyInfo
    // 1-2. Check formatting
    // 2.1. "spki" format
    if (format === 'spki') {
      throw new CurrentlyNotSupportedError(format,'jwk')
    }

    // 2.2. "pkcs8" format
    else if (format === 'pkcs8') {
      throw new CurrentlyNotSupportedError(format,'jwk')
    }

    // 2.3. "jwk" format
    else if (format === 'jwk') {
      // 2.3.1 Ensure data is JsonWebKey dictionary
      if (typeof keyData === 'object' && !Array.isArray(keyData)){
        jwk = new JsonWebKey(keyData)
      } else {
        throw new DataError('Invalid jwk format')
      }

      // 2.3.2. Ensure 'd' field and keyUsages match up
      if (jwk.d !== undefined && keyUsages.some(usage => usage !== 'sign')) {
        throw new SyntaxError('Key usages must include "sign"')
      }

      if (jwk.d === undefined && !keyUsages.some(usage => usage === 'verify')) {
        throw new SyntaxError('Key usages must include "verify"')
      }

      // 2.3.3 Validate 'kty' field
      if (jwk.kty !== 'OKP'){
        throw new DataError('Key type must be "EC".')
      }

      // 2.3.4. Validate 'use' field
      if (keyUsages !== undefined && jwk.use !== undefined && jwk.use !== 'sig'){
        throw new DataError('Key use must be "sig".')
      }

      // 2.3.5. Validate 'key_ops' field
      if (jwk.key_ops !== undefined) {
        jwk.key_ops.forEach(op => {
          if (op !== 'sign'
            && op !== 'verify' ) {
            throw new DataError('Key operation can only include "sign", or "verify".')
          }
        })
      }

      // 2.3.6. Validate 'ext' field
      if (jwk.ext !== undefined && jwk.ext === false && extractable === true){
        throw new DataError('Cannot be extractable when "ext" is set to false')
      }
        // 2.3.9.1.1-3 Ommited due to redundancy
        // 2.3.9.1.4.1. Validate 'd' property
        if (jwk.d) {
          // 2.3.9.1.4.1.1. TODO jwk validation here...
          // 2.3.9.1.4.1.2-3 Generate new private CryptoKeyObject
          key = new CryptoKey({
              type: 'private',
              extractable,
              usages: ['sign'],
              handle: keyto.from(jwk, 'jwk').toString('pem', 'private_pkcs1')
          })
        }
        // 2.3.9.1.4.2. Otherwise...
        else {
          // 2.3.9.1.4.2.1. TODO jwk validation here...
          // 2.3.9.1.4.2.2-3 Generate new public CryptoKeyObject
          key = new CryptoKey({
            type: 'public',
            extractable: true,
            usages: ['verify'],
            handle: keyto.from(jwk, 'jwk').toString('pem', 'public_pkcs8')
          })
        }
      // 2.3.10. Ommitted due to redudancy
      // 2.3.11-14 Set new alg object
      key.algorithm = new EDDSA(algorithm)
    }
    // 2.4. "raw" format
    else if (format === 'raw') {
      throw new CurrentlyNotSupportedError(format,'jwk')
    }
    // 2.5. Otherwise bad format
    else {
      throw new KeyFormatNotSupportedError(format)
    }
    // 3. Return key
    return key
  }//importKey

  /**
   * exportKey
   *
   * @description
   *
   * @param {string} format
   * @param {CryptoKey} key
   *
   * @returns {*}
   */
  exportKey (format, key) {
    // 1. Setup resulting var
    let result

    // 2. Validate handle slot
    if (!key.handle) {
      throw new OperationError('Missing key material')
    }

    // 3.1. "spki" format
    if (format === 'spki') {
      throw new CurrentlyNotSupportedError(format,"jwk' or 'raw")
    }
    // 3.2. "pkcs8" format
    else if (format === 'pkcs8') {
      throw new CurrentlyNotSupportedError(format,"jwk' or 'raw")
    }
    // 2.3. "jwk" format
    else if (format === 'jwk') {
      // 2.3.1-3 Create new jwk
      let jwk = keyto.from(key.handle, 'pem').toJwk(key.type)

      // 2.3.4. Set "key_ops" field
      jwk.key_ops = key.usages

      // 2.3.5. Set "ext" field
      jwk.ext = key.extractable

      // 2.3.6. Set result to jwk object
      result = jwk
    }
    // 3.4. "raw" format
    else if (format === 'raw') {
      // 3.4.1. Validate that the internal use is public
      if (key.type !== 'public'){
        throw new InvalidAccessError('Can only access public key data.')
      }
      // 3.4.2. Omitted due to redundancy
      // 3.4.3. Let resulting data be Buffer containing data
      result = Buffer.from(key.handle)
    }
    // 3.5. Otherwise bad format
    else {
      throw new KeyFormatNotSupportedError(format)
    }

    // 4. Result result
    return result
  }

}//EDDSA

/**
 * Export
 */
module.exports = EDDSA

let ed = new EDDSA({name: 'ED25519'})
let secretKey = {
  type : 'private',
  handle : Buffer.from(`4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb`,'hex')
}
let pubKey = {
  type : 'public',
  handle : Buffer.from(`3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c`,'hex')
}

let msg = (Buffer.from('72','hex'))

let sig = `92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00`
let enc = ed.sign(secretKey,'72')
let dec = ed.verify(pubKey,enc,msg)
// console.log("enc",enc)
// console.log("dec",dec)

let gennedKey = ed.generateKey({name: "EDDSA"},true,['sign','verify'])
// console.log("gennedKey",gennedKey.publicKey.handle,gennedKey.privateKey.handle)

let ec = new elEdDSA('ed25519')
let kty = "OKP"
let crv = "Ed25519"
let d = "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A"
let x = "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"

let hexD = base64url.toBuffer(d)
let hexX = base64url.toBuffer(x)

// console.log('hexD',hexD)
// console.log('hexX',hexX)

let sk = ec.keyFromSecret(secretKey.handle)
// console.log(sk)
// console.log(Buffer.from(sk.pubBytes()))

let enc1 = ed.sign(gennedKey.privateKey,msg)
let dec1 = ed.verify(gennedKey.publicKey,enc1,msg)
console.log("enc1",enc1)
console.log("dec1",dec1)

// let testKey = ec.keyFromSecret(`9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60`,'hex')
// console.log( testKey )
<commit_msg>wip(eddsa): finished export and import Key<commit_after>/**
 * Package dependencies
 */
const base64url = require('base64url')
const crypto = require('crypto')
const {spawnSync} = require('child_process')
const {TextEncoder, TextDecoder} = require('text-encoding')
const keyto = require('@trust/keyto')
const elEdDSA = require('elliptic').eddsa;
const elliptic = require('elliptic')

/**
 * Local dependencies
 */
const Algorithm = require ('../algorithms/Algorithm')
const CryptoKey = require('../keys/CryptoKey')
const CryptoKeyPair = require('../keys/CryptoKeyPair')
const JsonWebKey = require('../keys/JsonWebKey')
const KeyAlgorithm = require('../dictionaries/KeyAlgorithm')
const EcKeyAlgorithm = require('../dictionaries/EcKeyAlgorithm')

/**
 * Errors
 */
const {
  DataError,
  OperationError,
  InvalidAccessError,
  KeyFormatNotSupportedError,
  CurrentlyNotSupportedError
} = require('../errors')

/**
 * EDDSA
 */
class EDDSA extends Algorithm {

  /**
   * dictionaries
   */
  static get dictionaries () {
    return [
      KeyAlgorithm,
      EcKeyAlgorithm
    ]
  }

  /**
   * members
   */
  static get members () {
    return {
      name: String
    }
  }

  /**
   * sign
   *
   * @description
   * Create a digital signature
   *
   * @param {CryptoKey} key
   * @param {BufferSource} data
   *
   * @returns {ArrayBuffer}
   */
  sign (key, data) {
    let result
    // Ensure the key is a private type only
    if (key.type !== 'private'){
      throw new InvalidAccessError('Signing requires a private key')
    }

    // Ensure data is hex string, array or Buffer
    if (!Array.isArray(data) && !Buffer.isBuffer(data) && typeof data !== 'string'){
      throw new DataError('Data must be an Array, Buffer or hex string')
    }

    // Ensure key.handle is hex string, array or Buffer
    if (!Array.isArray(key.handle) && !Buffer.isBuffer(key.handle) && typeof key.handle !== 'string' ){
      throw DataError('Key handle must be an Array, Buffer or hex string')
    }

    try {
      // Create curve via elliptic
      let ec = new elEdDSA('ed25519')

      // Generate keypair from private key
      let ecKey
      if (typeof key.handle === 'string'){
        ecKey = ec.keyFromSecret(key.handle, 'hex')
      } else {
        ecKey = ec.keyFromSecret(key.handle)
      }

      // Perform the signing
      result = ecKey.sign(data).toHex()
    } catch (error) {
      throw new OperationError(error.message)
    }
    // Return resulting buffer
    return Uint8Array.from(Buffer.from(result,'hex')).buffer
  }//sign

  /**
   * verify
   *
   * @description
   * Verifies a digital signature
   *
   * @param {CryptoKey} key
   * @param {BufferSource} signature
   * @param {BufferSource} data
   *
   * @returns {Boolean}
   */
  verify (key, signature, data) {
    let result
    // Ensure the key is a public or secret type
    if (key.type !== 'secret' && key.type !== 'public') {
      throw new InvalidAccessError('Verifying requires a public or secret key')
    }

    // Ensure data is hex string, array or Buffer
    if (!Array.isArray(data) && !Buffer.isBuffer(data) && typeof data !== 'string'){
      throw new DataError('Data must be an Array, Buffer or hex string')
    }

    // Ensure key.handle is hex string, array or Buffer
    if (!Array.isArray(key.handle) && !Buffer.isBuffer(key.handle) && typeof key.handle !== 'string' ){
      throw DataError('Key handle must be an Array, Buffer or hex string')
    }

    // Ensure signature is ArrayBuffer or hex string
    if (!signature instanceof ArrayBuffer && typeof signature !== 'string'){
      throw DataError('Signature must be an ArrayBuffer or hex string')
    }

    try {
      // Create curve via elliptic
      let ec = new elEdDSA('ed25519')

      // Generate keypair from key
      let ecKey
      if (key.type === 'private' && key.extractable){
        if (typeof key.handle === 'string'){
          ecKey = ec.keyFromSecret(key.handle, 'hex')
        } else {
          ecKey = ec.keyFromSecret(key.handle)
        }
      } else
      if (key.type === 'public'){
        if (typeof key.handle === 'string'){
          ecKey = ec.keyFromPublic(key.handle, 'hex')
        } else {
          ecKey = ec.keyFromPublic(key.handle.toString('hex'),'hex')
        }
      } else {
        throw new OperationError("Invalid key type")
      }

      // Convert ArrayBuffer back to hex
      if (signature instanceof ArrayBuffer){
        signature = Buffer.from(signature).toString('hex')
      }

      // Perform the verification
      result = ecKey.verify(data,signature)
    } catch (error) {
      throw new OperationError(error.message)
    }
    // Return boolean result
    return result
  }//verify

  /**
   * generateKey
   *
   * @description
   * Generate an EDDSA key pair
   *
   * @param {EcKeyGenParams} params
   * @returns {CryptoKey}
   */
  generateKey (params, extractable, usages) {
    // Validate usages
    usages.forEach(usage => {
        if (usage !== 'sign' && usage !== 'verify') {
          throw new SyntaxError('Key usages can only include "sign", or "verify"')
        }
    })
    // Generate random key for scret portion
    let secretBytes = crypto.randomBytes(32)

    // Derive public bytes
    let ec = new elEdDSA('ed25519')
    let ecKey = ec.keyFromSecret(secretBytes)
    let pubKey = Buffer.from(ecKey.pubBytes())

    // Set algorithm be a new EDDSA
    let algorithm = new EDDSA(params)

    // Create private key object
    let privateKey = new CryptoKey({
      type: 'private',
      algorithm,
      extractable,
      usages: ['sign'],
      handle: secretBytes
    })

    // Create public key object
    let publicKey = new CryptoKey({
      type: 'public',
      algorithm,
      extractable: true,
      usages: ['verify'],
      handle: pubKey
    })

    // Return the generated Key
    return new CryptoKeyPair({publicKey,privateKey})
  }//generateKey

  /**
   * importKey
   *
   * @description
   *
   * @param {string} format
   * @param {string|JsonWebKey} keyData
   * @param {KeyAlgorithm} algorithm
   * @param {Boolean} extractable
   * @param {Array} keyUsage
   *
   * @returns {CryptoKey}
   */
  importKey (format, keyData, algorithm, extractable, keyUsages) {
    let key, hash, normalizedHash, jwk, privateKeyInfo
    // Check formatting
    // "spki" format
    if (format === 'spki') {
      throw new CurrentlyNotSupportedError(format,'jwk')
    }

    // "pkcs8" format
    else if (format === 'pkcs8') {
      throw new CurrentlyNotSupportedError(format,'jwk')
    }

    // "jwk" format
    else if (format === 'jwk') {
      // Ensure data is JsonWebKey dictionary
      if (typeof keyData === 'object' && !Array.isArray(keyData)){
        jwk = new JsonWebKey(keyData)
      } else {
        throw new DataError('Invalid jwk format')
      }

      // Ensure 'd' field and keyUsages match up
      if (jwk.d !== undefined && keyUsages.some(usage => usage !== 'sign')) {
        throw new SyntaxError('Key usages must include "sign"')
      }

      if (jwk.d === undefined && !keyUsages.some(usage => usage === 'verify')) {
        throw new SyntaxError('Key usages must include "verify"')
      }

      // Validate 'kty' field
      if (jwk.kty !== 'OKP'){
        throw new DataError('Key type must be "OKP".')
      }

      // Validate 'crv' field
      if (jwk.crv !== 'Ed25519'){
        throw new DataError('Crv type must be "Ed25519".')
      }

      // Validate 'key_ops' field
      if (jwk.key_ops !== undefined){
        jwk.key_ops.forEach(op => {
          if (op !== 'sign'
            && op !== 'verify' ) {
            throw new DataError('Key operation can only include "sign", or "verify".')
          }
        })
      }

      // Validate 'd' property
      if (jwk.d && jwk.x){
        try {
          // Generate new private CryptoKeyObject
          key = new CryptoKey({
              type: 'private',
              extractable,
              usages: ['sign'],
              handle: base64url.toBuffer(jwk.d)
          })
        } catch (error) {
          throw new DataError('Invalid "d" field value.')
        }
      }
      // Validate 'x' property
      else if(jwk.x){
        // Generate new public CryptoKeyObject
        try {
          key = new CryptoKey({
            type: 'public',
            extractable: true,
            usages: ['verify'],
            handle: base64url.toBuffer(jwk.x)
          })
        } catch (error) {
          throw new DataError('Invalid "x" field value.')
        }
      }
      else {
        throw new DataError('Unknown jwk format, missing "x" and "d" fields.')
      }

      // Ensure the key length
      if (key.handle.length !== 32){
        throw new DataError('Key handle must be 32 bytes in length.')
      }

      // Set new alg object
      key.algorithm = new EDDSA(algorithm)
    }
    // "raw" format
    else if (format === 'raw') {
      throw new CurrentlyNotSupportedError(format,'jwk')
    }
    // Otherwise bad format
    else {
      throw new KeyFormatNotSupportedError(format)
    }

    // Return key
    return key
  }//importKey

  /**
   * exportKey
   *
   * @description
   *
   * @param {string} format
   * @param {CryptoKey} key
   *
   * @returns {*}
   */
  exportKey (format, key) {
    // Setup resulting var
    let result

    // Validate handle slot
    if (!key.handle) {
      throw new OperationError('Missing key material')
    }

    // Ensure data is accessible
    if (key.extractable !== true){
      throw new InvalidAccessError('Key handle is not extractable.')
    }

    // "spki" format
    if (format === 'spki'){
      throw new CurrentlyNotSupportedError(format,"jwk' or 'raw")
    }
    // "pkcs8" format
    else if (format === 'pkcs8'){
      throw new CurrentlyNotSupportedError(format,"jwk' or 'raw")
    }
    // "jwk" format
    else if (format === 'jwk'){
      // Create new jwk
      let jwk = new JsonWebKey({
        "kty": "OKP",
        "crv": "Ed25519"
      })

      // If the key is Private then derive 'd' and 'x'
      let ec = new elEdDSA('ed25519')
      if (key.type === 'private'){
        let ecKey = ec.keyFromSecret(key.handle)
        jwk.d = base64url(key.handle)
        jwk.x = base64url(ecKey.pubBytes())
      }
      // If the key is Public then derive 'x'
      else if (key.type === 'public'){
        let ecKey = ec.keyFromPublic(key.handle)
        jwk.x = base64url(ecKey.pubBytes())
      }
      // Otherwise throw error
      else {
        throw new DataError ("Unknown key type.")
      }

      // Set "key_ops" field
      jwk.key_ops = key.usages

      // Set "ext" field
      jwk.ext = key.extractable

      // Set result to jwk object
      result = jwk
    }
    // "raw" format
    else if (format === 'raw'){
      // Let resulting data be Buffer containing data
      result = Buffer.from(key.handle)
    }
    // "hex" format
    else if (format === 'hex'){
      // Let resulting data be Buffer containing data
      result = base64url(key.handle)
    }
    // Otherwise throw bad format
    else {
      throw new KeyFormatNotSupportedError(format)
    }

    // Result result
    return result
  }

}//EDDSA

/**
 * Export
 */
module.exports = EDDSA

/*
let ed = new EDDSA({name: 'ED25519'})
let secretKey = {
  type : 'private',
  handle : Buffer.from(`4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb`,'hex')
}
let pubKey = {
  type : 'public',
  handle : Buffer.from(`3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c`,'hex')
}

let msg = (Buffer.from('72','hex'))

let sig = `92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00`
let enc = ed.sign(secretKey,'72')
let dec = ed.verify(pubKey,enc,msg)
// console.log("enc",enc)
// console.log("dec",dec)

let gennedKey = ed.generateKey({name: "EDDSA"},true,['sign','verify'])
// console.log("gennedKey",gennedKey.publicKey.handle,gennedKey.privateKey.handle)

let ec = new elEdDSA('ed25519')
let kty = "OKP"
let crv = "Ed25519"
let d = "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A"
let x = "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"

let hexD = base64url.toBuffer(d)
let hexX = base64url.toBuffer(x)

// console.log('hexD',hexD)
// console.log('hexX',hexX)

let sk = ec.keyFromSecret(secretKey.handle)
// console.log(sk)
// console.log(Buffer.from(sk.pubBytes()))

let enc1 = ed.sign(gennedKey.privateKey,msg)
let dec1 = ed.verify(gennedKey.publicKey,enc1,msg)
// console.log("enc1",enc1)
// console.log("dec1",dec1)

// let testKey = ec.keyFromSecret(`9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60`,'hex')
// console.log( testKey )

let impPrivateKey = ed.importKey(
  "jwk",
  {
    "kty":"OKP",
    "crv":"Ed25519",
    "d":"nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A",
    "x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
  },
  {
    name: "EDDSA"
  },
  true,
  ['sign']
)
let impPublicKey = ed.importKey(
  "jwk",
  {
    "kty":"OKP",
    "crv":"Ed25519",
    "x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
  },
  {
    name: "EDDSA"
  },
  true,
  ['verify']
)

let enc2 = ed.sign(impPrivateKey,msg)
let dec2 = ed.verify(impPublicKey,enc2,msg)
// console.log("enc2",enc2)
// console.log("dec2",dec2)

let expPrvKey = ed.exportKey('jwk',impPrivateKey)
let expPubKey = ed.exportKey('jwk',impPublicKey)

console.log('expPrvKey',expPrvKey)
console.log('expPubKey',expPubKey)

let expPrvKeyR = ed.exportKey('raw',impPrivateKey)
let expPubKeyR = ed.exportKey('raw',impPublicKey)

console.log('expPrvKeyR',expPrvKeyR)
console.log('expPubKeyR',expPubKeyR)

let expPrvKeyH = ed.exportKey('hex',impPrivateKey)
let expPubKeyH = ed.exportKey('hex',impPublicKey)

console.log('expPrvKeyH',expPrvKeyH)
console.log('expPubKeyH',expPubKeyH)
*/<|endoftext|>