<commit_before>// @flow

import invariant from 'invariant';
import _sum from 'lodash/fp/sum';
import * as React from 'react';
import { View, FlatList, Platform, TextInput } from 'react-native';
import { FloatingAction } from 'react-native-floating-action';
import IonIcon from 'react-native-vector-icons/Ionicons';
import { createSelector } from 'reselect';

import { searchUsers } from 'lib/actions/user-actions';
import {
  type ChatThreadItem,
  chatListData,
} from 'lib/selectors/chat-selectors';
import { threadSearchIndex as threadSearchIndexSelector } from 'lib/selectors/nav-selectors';
import { usersWithPersonalThreadSelector } from 'lib/selectors/user-selectors';
import SearchIndex from 'lib/shared/search-index';
import {
  createPendingThread,
  createPendingThreadItem,
} from 'lib/shared/thread-utils';
import type { UserSearchResult } from 'lib/types/search-types';
import type { ThreadInfo } from 'lib/types/thread-types';
import { threadTypes } from 'lib/types/thread-types';
import type { GlobalAccountUserInfo, UserInfo } from 'lib/types/user-types';
import { useServerCall } from 'lib/utils/action-utils';

import Search from '../components/search.react';
import type { TabNavigationProp } from '../navigation/app-navigator.react';
import {
  MessageListRouteName,
  SidebarListModalRouteName,
  HomeChatThreadListRouteName,
  BackgroundChatThreadListRouteName,
  type NavigationRoute,
} from '../navigation/route-names';
import { useSelector } from '../redux/redux-utils';
import {
  type IndicatorStyle,
  indicatorStyleSelector,
  useStyles,
} from '../themes/colors';
import ChatThreadListItem from './chat-thread-list-item.react';
import type {
  ChatTopTabsNavigationProp,
  ChatNavigationProp,
} from './chat.react';

const floatingActions = [
  {
    text: 'Compose',
    icon: <IonIcon name="md-create" size={24} color="#FFFFFF" />,
    name: 'compose',
    position: 1,
  },
];

type Item =
  | ChatThreadItem
  | {| +type: 'search', +searchText: string |}
  | {| +type: 'empty', +emptyItem: React.ComponentType<{||}> |};

type BaseProps = {|
  +navigation:
    | ChatTopTabsNavigationProp<'HomeChatThreadList'>
    | ChatTopTabsNavigationProp<'BackgroundChatThreadList'>,
  +route:
    | NavigationRoute<'HomeChatThreadList'>
    | NavigationRoute<'BackgroundChatThreadList'>,
  +filterThreads: (threadItem: ThreadInfo) => boolean,
  +emptyItem?: React.ComponentType<{||}>,
|};
type Props = {|
  ...BaseProps,
  // Redux state
  +chatListData: $ReadOnlyArray<ChatThreadItem>,
  +viewerID: ?string,
  +threadSearchIndex: SearchIndex,
  +styles: typeof unboundStyles,
  +indicatorStyle: IndicatorStyle,
  +usersWithPersonalThread: $ReadOnlySet<string>,
  // async functions that hit server APIs
  +searchUsers: (usernamePrefix: string) => Promise<UserSearchResult>,
|};
type State = {|
  +searchText: string,
  +threadsSearchResults: Set<string>,
  +usersSearchResults: $ReadOnlyArray<GlobalAccountUserInfo>,
  +openedSwipeableId: string,
|};
type PropsAndState = {| ...Props, ...State |};
class ChatThreadList extends React.PureComponent<Props, State> {
  state: State = {
    searchText: '',
    threadsSearchResults: new Set(),
    usersSearchResults: [],
    openedSwipeableId: '',
  };
  searchInput: ?React.ElementRef<typeof TextInput>;
  flatList: ?FlatList<Item>;
  scrollPos = 0;

  componentDidMount() {
    const chatNavigation: ?ChatNavigationProp<
      'ChatThreadList',
    > = this.props.navigation.dangerouslyGetParent();
    invariant(chatNavigation, 'ChatNavigator should be within TabNavigator');
    const tabNavigation: ?TabNavigationProp<
      'Chat',
    > = chatNavigation.dangerouslyGetParent();
    invariant(tabNavigation, 'ChatNavigator should be within TabNavigator');
    tabNavigation.addListener('tabPress', this.onTabPress);
  }

  componentWillUnmount() {
    const chatNavigation: ?ChatNavigationProp<
      'ChatThreadList',
    > = this.props.navigation.dangerouslyGetParent();
    invariant(chatNavigation, 'ChatNavigator should be within TabNavigator');
    const tabNavigation: ?TabNavigationProp<
      'Chat',
    > = chatNavigation.dangerouslyGetParent();
    invariant(tabNavigation, 'ChatNavigator should be within TabNavigator');
    tabNavigation.removeListener('tabPress', this.onTabPress);
  }

  onTabPress = () => {
    if (!this.props.navigation.isFocused()) {
      return;
    }
    if (this.scrollPos > 0 && this.flatList) {
      this.flatList.scrollToOffset({ offset: 0, animated: true });
    } else if (this.props.route.name === BackgroundChatThreadListRouteName) {
      this.props.navigation.navigate({ name: HomeChatThreadListRouteName });
    }
  };

  renderItem = (row: { item: Item }) => {
    const item = row.item;
    if (item.type === 'search') {
      return (
        <Search
          searchText={this.state.searchText}
          onChangeText={this.onChangeSearchText}
          containerStyle={this.props.styles.search}
          placeholder="Search threads"
          ref={this.searchInputRef}
        />
      );
    }
    if (item.type === 'empty') {
      const EmptyItem = item.emptyItem;
      return <EmptyItem />;
    }
    return (
      <ChatThreadListItem
        data={item}
        onPressItem={this.onPressItem}
        onPressSeeMoreSidebars={this.onPressSeeMoreSidebars}
        onSwipeableWillOpen={this.onSwipeableWillOpen}
        currentlyOpenedSwipeableId={this.state.openedSwipeableId}
      />
    );
  };

  searchInputRef = (searchInput: ?React.ElementRef<typeof TextInput>) => {
    this.searchInput = searchInput;
  };

  static keyExtractor(item: Item) {
    if (item.type === 'chatThreadItem') {
      return item.threadInfo.id;
    } else if (item.type === 'empty') {
      return 'empty';
    } else {
      return 'search';
    }
  }

  static getItemLayout(data: ?$ReadOnlyArray<Item>, index: number) {
    if (!data) {
      return { length: 0, offset: 0, index };
    }
    const offset = ChatThreadList.heightOfItems(
      data.filter((_, i) => i < index),
    );
    const item = data[index];
    const length = item ? ChatThreadList.itemHeight(item) : 0;
    return { length, offset, index };
  }

  static itemHeight(item: Item): number {
    if (item.type === 'search') {
      return Platform.OS === 'ios' ? 54.5 : 55;
    }

    // itemHeight for emptyItem might be wrong because of line wrapping
    // but we don't care because we'll only ever be rendering this item by itself
    // and it should always be on-screen
    if (item.type === 'empty') {
      return 123;
    }

    return 60 + item.sidebars.length * 30;
  }

  static heightOfItems(data: $ReadOnlyArray<Item>): number {
    return _sum(data.map(ChatThreadList.itemHeight));
  }

  listDataSelector = createSelector(
    (propsAndState: PropsAndState) => propsAndState.chatListData,
    (propsAndState: PropsAndState) => propsAndState.searchText,
    (propsAndState: PropsAndState) => propsAndState.threadsSearchResults,
    (propsAndState: PropsAndState) => propsAndState.emptyItem,
    (propsAndState: PropsAndState) => propsAndState.usersSearchResults,
    (
      reduxChatListData: $ReadOnlyArray<ChatThreadItem>,
      searchText: string,
      threadsSearchResults: Set<string>,
      emptyItem?: React.ComponentType<{||}>,
      usersSearchResults: $ReadOnlyArray<GlobalAccountUserInfo>,
    ): Item[] => {
      const chatItems = [];

      if (!searchText) {
        chatItems.push(
          ...reduxChatListData.filter((item) =>
            this.props.filterThreads(item.threadInfo),
          ),
        );
      } else {
        const personalThreads = [];
        const nonPersonalThreads = [];
        for (const item of reduxChatListData) {
          if (!threadsSearchResults.has(item.threadInfo.id)) {
            continue;
          }
          if (item.threadInfo.type === threadTypes.PERSONAL) {
            personalThreads.push(item);
          } else {
            nonPersonalThreads.push(item);
          }
        }
        chatItems.push(...personalThreads, ...nonPersonalThreads);
        const { viewerID } = this.props;
        if (viewerID) {
          chatItems.push(
            ...usersSearchResults.map((user) =>
              createPendingThreadItem(viewerID, user),
            ),
          );
        }
      }

      if (emptyItem && chatItems.length === 0) {
        chatItems.push({ type: 'empty', emptyItem });
      }

      return [{ type: 'search', searchText }, ...chatItems];
    },
  );

  get listData() {
    return this.listDataSelector({ ...this.props, ...this.state });
  }

  render() {
    let floatingAction = null;
    if (Platform.OS === 'android') {
      floatingAction = (
        <FloatingAction
          actions={floatingActions}
          overrideWithAction
          onPressItem={this.composeThread}
          color="#66AA66"
        />
      );
    }
    // this.props.viewerID is in extraData since it's used by MessagePreview
    // within ChatThreadListItem
    return (
      <View style={this.props.styles.container}>
        <FlatList
          data={this.listData}
          renderItem={this.renderItem}
          keyExtractor={ChatThreadList.keyExtractor}
          getItemLayout={ChatThreadList.getItemLayout}
          extraData={`${this.props.viewerID || ''} ${
            this.state.openedSwipeableId
          }`}
          initialNumToRender={11}
          keyboardShouldPersistTaps="handled"
          onScroll={this.onScroll}
          style={this.props.styles.flatList}
          indicatorStyle={this.props.indicatorStyle}
          ref={this.flatListRef}
        />
        {floatingAction}
      </View>
    );
  }

  flatListRef = (flatList: ?FlatList<Item>) => {
    this.flatList = flatList;
  };

  onScroll = (event: { +nativeEvent: { +contentOffset: { +y: number } } }) => {
    this.scrollPos = event.nativeEvent.contentOffset.y;
  };

  async searchUsers(usernamePrefix: string) {
    if (usernamePrefix.length === 0) {
      return [];
    }

    const { userInfos } = await this.props.searchUsers(usernamePrefix);
    return userInfos.filter(
      (info) =>
        !this.props.usersWithPersonalThread.has(info.id) &&
        info.id !== this.props.viewerID,
    );
  }

  onChangeSearchText = async (searchText: string) => {
    const results = this.props.threadSearchIndex.getSearchResults(searchText);
    this.setState({ searchText, threadsSearchResults: new Set(results) });
    const usersSearchResults = await this.searchUsers(searchText);
    this.setState({ usersSearchResults });
  };

  onPressItem = (
    threadInfo: ThreadInfo,
    pendingPersonalThreadUserInfo?: UserInfo,
  ) => {
    this.onChangeSearchText('');
    if (this.searchInput) {
      this.searchInput.blur();
    }
    this.props.navigation.navigate({
      name: MessageListRouteName,
      params: { threadInfo, pendingPersonalThreadUserInfo },
      key: `${MessageListRouteName}${threadInfo.id}`,
    });
  };

  onPressSeeMoreSidebars = (threadInfo: ThreadInfo) => {
    this.onChangeSearchText('');
    if (this.searchInput) {
      this.searchInput.blur();
    }
    this.props.navigation.navigate({
      name: SidebarListModalRouteName,
      params: { threadInfo },
    });
  };

  onSwipeableWillOpen = (threadInfo: ThreadInfo) => {
    this.setState((state) => ({ ...state, openedSwipeableId: threadInfo.id }));
  };

  composeThread = () => {
    if (this.props.viewerID) {
      this.props.navigation.navigate({
        name: MessageListRouteName,
        params: {
          threadInfo: createPendingThread(
            this.props.viewerID,
            threadTypes.CHAT_SECRET,
            [],
          ),
          searching: true,
        },
      });
    }
  };
}

const unboundStyles = {
  icon: {
    fontSize: 28,
  },
  container: {
    flex: 1,
  },
  search: {
    marginBottom: 8,
    marginHorizontal: 12,
    marginTop: Platform.OS === 'android' ? 10 : 8,
  },
  flatList: {
    flex: 1,
    backgroundColor: 'listBackground',
  },
};

export default React.memo<BaseProps>(function ConnectedChatThreadList(
  props: BaseProps,
) {
  const boundChatListData = useSelector(chatListData);
  const viewerID = useSelector(
    (state) => state.currentUserInfo && state.currentUserInfo.id,
  );
  const threadSearchIndex = useSelector(threadSearchIndexSelector);
  const styles = useStyles(unboundStyles);
  const indicatorStyle = useSelector(indicatorStyleSelector);
  const callSearchUsers = useServerCall(searchUsers);
  const usersWithPersonalThread = useSelector(usersWithPersonalThreadSelector);

  return (
    <ChatThreadList
      {...props}
      chatListData={boundChatListData}
      viewerID={viewerID}
      threadSearchIndex={threadSearchIndex}
      styles={styles}
      indicatorStyle={indicatorStyle}
      searchUsers={callSearchUsers}
      usersWithPersonalThread={usersWithPersonalThread}
    />
  );
});
<commit_msg>[native] Do not show sidebars below matching top-level threads in the search results<commit_after>// @flow

import invariant from 'invariant';
import _sum from 'lodash/fp/sum';
import * as React from 'react';
import { View, FlatList, Platform, TextInput } from 'react-native';
import { FloatingAction } from 'react-native-floating-action';
import IonIcon from 'react-native-vector-icons/Ionicons';
import { createSelector } from 'reselect';

import { searchUsers } from 'lib/actions/user-actions';
import {
  type ChatThreadItem,
  chatListData,
} from 'lib/selectors/chat-selectors';
import { threadSearchIndex as threadSearchIndexSelector } from 'lib/selectors/nav-selectors';
import { usersWithPersonalThreadSelector } from 'lib/selectors/user-selectors';
import SearchIndex from 'lib/shared/search-index';
import {
  createPendingThread,
  createPendingThreadItem,
} from 'lib/shared/thread-utils';
import type { UserSearchResult } from 'lib/types/search-types';
import type { ThreadInfo } from 'lib/types/thread-types';
import { threadTypes } from 'lib/types/thread-types';
import type { GlobalAccountUserInfo, UserInfo } from 'lib/types/user-types';
import { useServerCall } from 'lib/utils/action-utils';

import Search from '../components/search.react';
import type { TabNavigationProp } from '../navigation/app-navigator.react';
import {
  MessageListRouteName,
  SidebarListModalRouteName,
  HomeChatThreadListRouteName,
  BackgroundChatThreadListRouteName,
  type NavigationRoute,
} from '../navigation/route-names';
import { useSelector } from '../redux/redux-utils';
import {
  type IndicatorStyle,
  indicatorStyleSelector,
  useStyles,
} from '../themes/colors';
import ChatThreadListItem from './chat-thread-list-item.react';
import type {
  ChatTopTabsNavigationProp,
  ChatNavigationProp,
} from './chat.react';

const floatingActions = [
  {
    text: 'Compose',
    icon: <IonIcon name="md-create" size={24} color="#FFFFFF" />,
    name: 'compose',
    position: 1,
  },
];

type Item =
  | ChatThreadItem
  | {| +type: 'search', +searchText: string |}
  | {| +type: 'empty', +emptyItem: React.ComponentType<{||}> |};

type BaseProps = {|
  +navigation:
    | ChatTopTabsNavigationProp<'HomeChatThreadList'>
    | ChatTopTabsNavigationProp<'BackgroundChatThreadList'>,
  +route:
    | NavigationRoute<'HomeChatThreadList'>
    | NavigationRoute<'BackgroundChatThreadList'>,
  +filterThreads: (threadItem: ThreadInfo) => boolean,
  +emptyItem?: React.ComponentType<{||}>,
|};
type Props = {|
  ...BaseProps,
  // Redux state
  +chatListData: $ReadOnlyArray<ChatThreadItem>,
  +viewerID: ?string,
  +threadSearchIndex: SearchIndex,
  +styles: typeof unboundStyles,
  +indicatorStyle: IndicatorStyle,
  +usersWithPersonalThread: $ReadOnlySet<string>,
  // async functions that hit server APIs
  +searchUsers: (usernamePrefix: string) => Promise<UserSearchResult>,
|};
type State = {|
  +searchText: string,
  +threadsSearchResults: Set<string>,
  +usersSearchResults: $ReadOnlyArray<GlobalAccountUserInfo>,
  +openedSwipeableId: string,
|};
type PropsAndState = {| ...Props, ...State |};
class ChatThreadList extends React.PureComponent<Props, State> {
  state: State = {
    searchText: '',
    threadsSearchResults: new Set(),
    usersSearchResults: [],
    openedSwipeableId: '',
  };
  searchInput: ?React.ElementRef<typeof TextInput>;
  flatList: ?FlatList<Item>;
  scrollPos = 0;

  componentDidMount() {
    const chatNavigation: ?ChatNavigationProp<
      'ChatThreadList',
    > = this.props.navigation.dangerouslyGetParent();
    invariant(chatNavigation, 'ChatNavigator should be within TabNavigator');
    const tabNavigation: ?TabNavigationProp<
      'Chat',
    > = chatNavigation.dangerouslyGetParent();
    invariant(tabNavigation, 'ChatNavigator should be within TabNavigator');
    tabNavigation.addListener('tabPress', this.onTabPress);
  }

  componentWillUnmount() {
    const chatNavigation: ?ChatNavigationProp<
      'ChatThreadList',
    > = this.props.navigation.dangerouslyGetParent();
    invariant(chatNavigation, 'ChatNavigator should be within TabNavigator');
    const tabNavigation: ?TabNavigationProp<
      'Chat',
    > = chatNavigation.dangerouslyGetParent();
    invariant(tabNavigation, 'ChatNavigator should be within TabNavigator');
    tabNavigation.removeListener('tabPress', this.onTabPress);
  }

  onTabPress = () => {
    if (!this.props.navigation.isFocused()) {
      return;
    }
    if (this.scrollPos > 0 && this.flatList) {
      this.flatList.scrollToOffset({ offset: 0, animated: true });
    } else if (this.props.route.name === BackgroundChatThreadListRouteName) {
      this.props.navigation.navigate({ name: HomeChatThreadListRouteName });
    }
  };

  renderItem = (row: { item: Item }) => {
    const item = row.item;
    if (item.type === 'search') {
      return (
        <Search
          searchText={this.state.searchText}
          onChangeText={this.onChangeSearchText}
          containerStyle={this.props.styles.search}
          placeholder="Search threads"
          ref={this.searchInputRef}
        />
      );
    }
    if (item.type === 'empty') {
      const EmptyItem = item.emptyItem;
      return <EmptyItem />;
    }
    return (
      <ChatThreadListItem
        data={item}
        onPressItem={this.onPressItem}
        onPressSeeMoreSidebars={this.onPressSeeMoreSidebars}
        onSwipeableWillOpen={this.onSwipeableWillOpen}
        currentlyOpenedSwipeableId={this.state.openedSwipeableId}
      />
    );
  };

  searchInputRef = (searchInput: ?React.ElementRef<typeof TextInput>) => {
    this.searchInput = searchInput;
  };

  static keyExtractor(item: Item) {
    if (item.type === 'chatThreadItem') {
      return item.threadInfo.id;
    } else if (item.type === 'empty') {
      return 'empty';
    } else {
      return 'search';
    }
  }

  static getItemLayout(data: ?$ReadOnlyArray<Item>, index: number) {
    if (!data) {
      return { length: 0, offset: 0, index };
    }
    const offset = ChatThreadList.heightOfItems(
      data.filter((_, i) => i < index),
    );
    const item = data[index];
    const length = item ? ChatThreadList.itemHeight(item) : 0;
    return { length, offset, index };
  }

  static itemHeight(item: Item): number {
    if (item.type === 'search') {
      return Platform.OS === 'ios' ? 54.5 : 55;
    }

    // itemHeight for emptyItem might be wrong because of line wrapping
    // but we don't care because we'll only ever be rendering this item by itself
    // and it should always be on-screen
    if (item.type === 'empty') {
      return 123;
    }

    return 60 + item.sidebars.length * 30;
  }

  static heightOfItems(data: $ReadOnlyArray<Item>): number {
    return _sum(data.map(ChatThreadList.itemHeight));
  }

  listDataSelector = createSelector(
    (propsAndState: PropsAndState) => propsAndState.chatListData,
    (propsAndState: PropsAndState) => propsAndState.searchText,
    (propsAndState: PropsAndState) => propsAndState.threadsSearchResults,
    (propsAndState: PropsAndState) => propsAndState.emptyItem,
    (propsAndState: PropsAndState) => propsAndState.usersSearchResults,
    (
      reduxChatListData: $ReadOnlyArray<ChatThreadItem>,
      searchText: string,
      threadsSearchResults: Set<string>,
      emptyItem?: React.ComponentType<{||}>,
      usersSearchResults: $ReadOnlyArray<GlobalAccountUserInfo>,
    ): Item[] => {
      const chatItems = [];

      if (!searchText) {
        chatItems.push(
          ...reduxChatListData.filter((item) =>
            this.props.filterThreads(item.threadInfo),
          ),
        );
      } else {
        const personalThreads = [];
        const nonPersonalThreads = [];
        for (const item of reduxChatListData) {
          if (!threadsSearchResults.has(item.threadInfo.id)) {
            continue;
          }
          if (item.threadInfo.type === threadTypes.PERSONAL) {
            personalThreads.push({ ...item, sidebars: [] });
          } else {
            nonPersonalThreads.push({ ...item, sidebars: [] });
          }
        }
        chatItems.push(...personalThreads, ...nonPersonalThreads);
        const { viewerID } = this.props;
        if (viewerID) {
          chatItems.push(
            ...usersSearchResults.map((user) =>
              createPendingThreadItem(viewerID, user),
            ),
          );
        }
      }

      if (emptyItem && chatItems.length === 0) {
        chatItems.push({ type: 'empty', emptyItem });
      }

      return [{ type: 'search', searchText }, ...chatItems];
    },
  );

  get listData() {
    return this.listDataSelector({ ...this.props, ...this.state });
  }

  render() {
    let floatingAction = null;
    if (Platform.OS === 'android') {
      floatingAction = (
        <FloatingAction
          actions={floatingActions}
          overrideWithAction
          onPressItem={this.composeThread}
          color="#66AA66"
        />
      );
    }
    // this.props.viewerID is in extraData since it's used by MessagePreview
    // within ChatThreadListItem
    return (
      <View style={this.props.styles.container}>
        <FlatList
          data={this.listData}
          renderItem={this.renderItem}
          keyExtractor={ChatThreadList.keyExtractor}
          getItemLayout={ChatThreadList.getItemLayout}
          extraData={`${this.props.viewerID || ''} ${
            this.state.openedSwipeableId
          }`}
          initialNumToRender={11}
          keyboardShouldPersistTaps="handled"
          onScroll={this.onScroll}
          style={this.props.styles.flatList}
          indicatorStyle={this.props.indicatorStyle}
          ref={this.flatListRef}
        />
        {floatingAction}
      </View>
    );
  }

  flatListRef = (flatList: ?FlatList<Item>) => {
    this.flatList = flatList;
  };

  onScroll = (event: { +nativeEvent: { +contentOffset: { +y: number } } }) => {
    this.scrollPos = event.nativeEvent.contentOffset.y;
  };

  async searchUsers(usernamePrefix: string) {
    if (usernamePrefix.length === 0) {
      return [];
    }

    const { userInfos } = await this.props.searchUsers(usernamePrefix);
    return userInfos.filter(
      (info) =>
        !this.props.usersWithPersonalThread.has(info.id) &&
        info.id !== this.props.viewerID,
    );
  }

  onChangeSearchText = async (searchText: string) => {
    const results = this.props.threadSearchIndex.getSearchResults(searchText);
    this.setState({ searchText, threadsSearchResults: new Set(results) });
    const usersSearchResults = await this.searchUsers(searchText);
    this.setState({ usersSearchResults });
  };

  onPressItem = (
    threadInfo: ThreadInfo,
    pendingPersonalThreadUserInfo?: UserInfo,
  ) => {
    this.onChangeSearchText('');
    if (this.searchInput) {
      this.searchInput.blur();
    }
    this.props.navigation.navigate({
      name: MessageListRouteName,
      params: { threadInfo, pendingPersonalThreadUserInfo },
      key: `${MessageListRouteName}${threadInfo.id}`,
    });
  };

  onPressSeeMoreSidebars = (threadInfo: ThreadInfo) => {
    this.onChangeSearchText('');
    if (this.searchInput) {
      this.searchInput.blur();
    }
    this.props.navigation.navigate({
      name: SidebarListModalRouteName,
      params: { threadInfo },
    });
  };

  onSwipeableWillOpen = (threadInfo: ThreadInfo) => {
    this.setState((state) => ({ ...state, openedSwipeableId: threadInfo.id }));
  };

  composeThread = () => {
    if (this.props.viewerID) {
      this.props.navigation.navigate({
        name: MessageListRouteName,
        params: {
          threadInfo: createPendingThread(
            this.props.viewerID,
            threadTypes.CHAT_SECRET,
            [],
          ),
          searching: true,
        },
      });
    }
  };
}

const unboundStyles = {
  icon: {
    fontSize: 28,
  },
  container: {
    flex: 1,
  },
  search: {
    marginBottom: 8,
    marginHorizontal: 12,
    marginTop: Platform.OS === 'android' ? 10 : 8,
  },
  flatList: {
    flex: 1,
    backgroundColor: 'listBackground',
  },
};

export default React.memo<BaseProps>(function ConnectedChatThreadList(
  props: BaseProps,
) {
  const boundChatListData = useSelector(chatListData);
  const viewerID = useSelector(
    (state) => state.currentUserInfo && state.currentUserInfo.id,
  );
  const threadSearchIndex = useSelector(threadSearchIndexSelector);
  const styles = useStyles(unboundStyles);
  const indicatorStyle = useSelector(indicatorStyleSelector);
  const callSearchUsers = useServerCall(searchUsers);
  const usersWithPersonalThread = useSelector(usersWithPersonalThreadSelector);

  return (
    <ChatThreadList
      {...props}
      chatListData={boundChatListData}
      viewerID={viewerID}
      threadSearchIndex={threadSearchIndex}
      styles={styles}
      indicatorStyle={indicatorStyle}
      searchUsers={callSearchUsers}
      usersWithPersonalThread={usersWithPersonalThread}
    />
  );
});
<|endoftext|>


<commit_before>_OKCP.initSuggestQuestionsFeature = function () {
	var prevCat = '';
	var fullQuestionsList = _OKCP.fullQuestionsList;
	var firstClick = true;
	_OKCP.questionsToSuggest = {};

	$('

To submit your questions, carefully copy the following text and email it to  okcp.suggestions@gmail.com

').appendTo('body');

	$('.question').filter(function(){
			var qid = this.id.split('question_')[1];
			for (var i = 0, len = fullQuestionsList.length; i < len; i++) {
				if (fullQuestionsList[i].qid == qid) return false;
			}
			return true;
		}).prepend('
Suggest Question
');

	$('.okcp-suggest-question').click(function() {
		var question = $(this).parent();
		var qid = question.attr('id').split('question_')[1];
		var qtext = $('#qtext_' + qid).text();
		var answers = [];
		var obj, category;
		if (firstClick) {
			firstClick = false;
			alert('OkC Plugin:\nThis feature allows you to suggest questions for the developer to include in the plugin!');
		}
		$('#answer_'+qid+' .their_answer').each(function() {
			answers.push( $(this).parent().text() );
		});
		category = prompt("What category does this question fall under?",prevCat);
		if (category === '') {
			alert('Category name cannot be empty');
			return false;
		} else if (category === undefined) {
			return false;
		}
		category = category.toLowerCase();
		prevCat = category;
		category = category.split(' ').join('_');

		scoreWeightPlaceholder = [];
		for (var i = 0; i < answers.length; i++) {
			scoreWeightPlaceholder.push(1);
		}

		obj = {
			"qid": qid,
			"text": qtext,
			"answerText": answers,
			"score": scoreWeightPlaceholder,
			"weight": scoreWeightPlaceholder
		}

		_OKCP.questionsToSuggest[category] = _OKCP.questionsToSuggest[category] || [];
		_OKCP.questionsToSuggest[category].push(obj);

		$('.copy-this').show();
		$('.copy-this-text').text(JSON.stringify(_OKCP.questionsToSuggest));
		alert('Your question(s) have NOT been suggested YET. When you are ready to sumbit, scroll to the very bottom of the page and follow the instructions.');
		$(this).remove();
	});

	if (location.href.split('rqid=')[1] !== undefined && _OKCP.devmode) {
		//duplicated code, but works for devs in dev mode
		var question = $('.question:first');
		var qid = question.attr('id').split('question_')[1];
		var qtext = $('#qtext_' + qid).text();
		var answers = [];
		var obj, category;
		$('#answer_'+qid+' .their_answer').each(function() {
			answers.push( $(this).parent().text() );
		});

		scoreWeightPlaceholder = [];
		for (var i = 0; i < answers.length; i++) {
			scoreWeightPlaceholder.push(1);
		}

		obj = {
			"qid": qid,
			"text": qtext,
			"answerText": answers,
			"score": scoreWeightPlaceholder,
			"weight": scoreWeightPlaceholder
		};

		// log out pretty version
		var objStr = JSON.stringify(obj);
		objStr = '{\n\t' + objStr.split('{')[1];
		objStr = objStr.split('}')[0]+'\n}';
		objStr = objStr.split('"text"').join('\n\t"text"').split('"answerText"').join('\n\t"answerText"').split('"score"').join('\n\t"score"').split('"weight"').join('\n\t"weight"');
		console.log(objStr);
	}

};<commit_msg>suggestQuestion tweak<commit_after>_OKCP.initSuggestQuestionsFeature = function () {
	var prevCat = '';
	var fullQuestionsList = _OKCP.fullQuestionsList;
	var firstClick = true;
	_OKCP.questionsToSuggest = {};

	$('

To submit your questions, carefully copy the following text and email it to  okcp.suggestions@gmail.com

').appendTo('body');

	$('.question').filter(function(){
			var qid = this.id.split('question_')[1];
			for (var i = 0, len = fullQuestionsList.length; i < len; i++) {
				if (fullQuestionsList[i].qid == qid) return false;
			}
			return true;
		}).prepend('
Suggest Question
');

	$('.okcp-suggest-question').click(function() {
		var question = $(this).parent();
		var qid = question.attr('id').split('question_')[1];
		var qtext = $('#qtext_' + qid).text();
		var answers = [];
		var obj, category;
		if (firstClick) {
			firstClick = false;
			alert('OkC Plugin:\nThis feature allows you to suggest questions for the developer to include in the plugin!');
		}
		$('#answer_'+qid+' .their_answer').each(function() {
			answers.push( $(this).parent().text() );
		});
		category = prompt("What category does this question fall under?",prevCat);
		if (category === '') {
			alert('Category name cannot be empty');
			return false;
		} else if (category === undefined) {
			return false;
		}
		category = category.toLowerCase();
		prevCat = category;
		category = category.split(' ').join('_');

		scoreWeightPlaceholder = [];
		for (var i = 0; i < answers.length; i++) {
			scoreWeightPlaceholder.push(1);
		}

		obj = {
			"qid": qid,
			"text": qtext,
			"answerText": answers,
			"score": scoreWeightPlaceholder,
			"weight": scoreWeightPlaceholder
		}

		_OKCP.questionsToSuggest[category] = _OKCP.questionsToSuggest[category] || [];
		_OKCP.questionsToSuggest[category].push(obj);

		$('.copy-this').show();
		$('.copy-this-text').text(JSON.stringify(_OKCP.questionsToSuggest));
		alert('Your question(s) have NOT been suggested YET. When you are ready to sumbit, scroll to the very bottom of the page and follow the instructions.');
		$(this).remove();
		logCurrQuestionData(question);
	});

	function logCurrQuestionData(elem) {
		//duplicated code, but works for now
		var question = elem || $('.question:first');
		var qid = question.attr('id').split('question_')[1];
		var qtext = $('#qtext_' + qid).text();
		var answers = [];
		var obj, category;
		$('#answer_'+qid+' .their_answer').each(function() {
			answers.push( $(this).parent().text() );
		});

		scoreWeightPlaceholder = [];
		for (var i = 0; i < answers.length; i++) {
			scoreWeightPlaceholder.push(1);
		}

		obj = {
			"qid": qid,
			"text": qtext,
			"answerText": answers,
			"score": scoreWeightPlaceholder,
			"weight": scoreWeightPlaceholder
		};

		// log out pretty version
		var objStr = JSON.stringify(obj);
		objStr = '{\n\t' + objStr.split('{')[1];
		objStr = objStr.split('}')[0]+'\n}';
		objStr = objStr.split('"text"').join('\n\t"text"').split('"answerText"').join('\n\t"answerText"').split('"score"').join('\n\t"score"').split('"weight"').join('\n\t"weight"');
		console.log(objStr);
	}
	if (location.href.split('rqid=')[1] !== undefined && _OKCP.devmode) {
		logCurrQuestionData();
	}

};<|endoftext|>


<commit_before>const path = require('path');

const webpack = require('webpack');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

function resolveFromRoot(dir) {
  return path.resolve(__dirname, dir);
}

const DEVELOPMENT = 'development';
const PRODUCTION = 'production';

const env = process.env.NODE_ENV || DEVELOPMENT;

const isProd = env === PRODUCTION;

const rules = [
  {
    test: /\.js$/,
    exclude: /node_modules/,
    use: {
      loader: 'babel-loader',
      options: {
        cacheDirectory: true,
      },
    },
  },
];

const stats = {
  modules: false,
  chunks: false,
  colors: true,
};

const plugins = [
  new webpack.EnvironmentPlugin({
    NODE_ENV: DEVELOPMENT,
  }),
];

if (isProd) {
  plugins.push(...[
    new webpack.optimize.ModuleConcatenationPlugin(),
    new webpack.optimize.OccurrenceOrderPlugin(),
    new UglifyJsPlugin({
      uglifyOptions: {
        compress: {
          keep_fnames: true,
          warnings: false,
        },
        output: {
          comments: false,
        },
        mangle: {
          keep_fnames: true,
        },
      },
      sourceMap: true,
    }),
  ]);
}

module.exports = {
  entry: resolveFromRoot('./src'),
  output: {
    library: 'TVDML',
    libraryTarget: 'umd',
    filename: 'tvdml.js',
    path: resolveFromRoot('./dist'),
  },
  devtool: isProd ? 'source-map' : 'eval-source-map',
  module: { rules },
  plugins,
  stats,
};
<commit_msg>react marked as external dependency<commit_after>const path = require('path');

const webpack = require('webpack');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

function resolveFromRoot(dir) {
  return path.resolve(__dirname, dir);
}

const DEVELOPMENT = 'development';
const PRODUCTION = 'production';

const env = process.env.NODE_ENV || DEVELOPMENT;

const isProd = env === PRODUCTION;

const rules = [
  {
    test: /\.js$/,
    exclude: /node_modules/,
    use: {
      loader: 'babel-loader',
      options: {
        cacheDirectory: true,
      },
    },
  },
];

const stats = {
  modules: false,
  chunks: false,
  colors: true,
};

const plugins = [
  new webpack.EnvironmentPlugin({
    NODE_ENV: DEVELOPMENT,
  }),
];

if (isProd) {
  plugins.push(...[
    new webpack.optimize.ModuleConcatenationPlugin(),
    new webpack.optimize.OccurrenceOrderPlugin(),
    new UglifyJsPlugin({
      uglifyOptions: {
        compress: {
          keep_fnames: true,
          warnings: false,
        },
        output: {
          comments: false,
        },
        mangle: {
          keep_fnames: true,
        },
      },
      sourceMap: true,
    }),
  ]);
}

module.exports = {
  entry: resolveFromRoot('./src'),
  output: {
    library: 'TVDML',
    libraryTarget: 'umd',
    filename: 'tvdml.js',
    path: resolveFromRoot('./dist'),
  },
  devtool: isProd ? 'source-map' : 'eval-source-map',
  externals: ['react'], // peerDependencies
  module: { rules },
  plugins,
  stats,
};
<|endoftext|>


<commit_before>/**
 * A plugin to identify and validate heading tags (
,
, etc.)
 */

let $ = require("jquery");
let Plugin = require("../base");
let annotate = require("../shared/annotate")("headings");

let outlineItemTemplate = require("./outline-item.handlebars");
require("./style.less");

/* eslint new-cap:0 */
const ERRORS = {
    FIRST_NOT_H1(level) {
        return {
            title: "First heading is not an <h1>",
            description: `

                    To give your document a proper structure for assistive
                    technologies, it is important to lay out your headings
                    beginning with an <h1>. We found an
                    <h${level}> instead.

            `
        };
    },

    // This error is currently unused.
    //
    // The HTML5 outlining algorithm[1] enables the use of "sectioning roots"
    // to support multiple
 tags when embedded inside of containers like
    //
 or
. There are currently "no known implementations
    // of the outline algorithm in graphical browsers or assistive technology
    // user agents" [2], so we instead simply "use heading rank (h1-h6) to
    // convey document structure." [2].
    //
    // [1]: http://www.w3.org/html/wg/drafts/html/master/semantics.html#outline
    // [2]: https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Sections_and_Outlines_of_an_HTML5_document#The_HTML5_Outline_Algorithm
    MULTIPLE_H1: {
        title: "<h1> used when one is already present"
    },

    // This error accepts two arguments to display a relevant error message
    NONCONSECUTIVE_HEADER(prevLevel, currLevel) {
        let _tag = (level) => `<code><h${level}></code>`;
        let description = `

                This document contains an ${_tag(currLevel)} tag directly
                following an ${_tag(prevLevel)}. In order to maintain a consistent
                outline of the page for assistive technologies, reduce the gap in
                the heading level by upgrading this tag to an
                ${_tag(prevLevel+1)}`;

        // Suggest upgrading the tag to the same level as `prevLevel` iff
        // `prevLevel` is not 1
        if (prevLevel !== 1) {
            description += ` or ${_tag(prevLevel)}`;
        }

        description += `.</div>`;

        return {
            title: `
                Nonconsecutive heading level used (h${prevLevel} →
                h${currLevel})
            `,
            description: description
        };
    }
};

class HeadingsPlugin extends Plugin {
    getTitle() {
        return "Headings";
    }

    getDescription() {
        return "Highlights headings (
,
, etc) and order violations";
    }

    // Computes an outline of the page and reports any violations.
    outline($headings) {
        let $outline = $("
").addClass("tota11y-heading-outline");

        let prevLevel;
        $headings.each((i, el) => {
            let $el = $(el);
            let level = +$el.prop("tagName").slice(1);
            let error;

            // Check for any violations
            // NOTE: These violations do not overlap, but as we add more, we
            // may want to separate the conditionals here to report multiple
            // errors on the same tag.
            if (i === 0 && level !== 1) {
                error = ERRORS.FIRST_NOT_H1(level);
            } else if (prevLevel && level - prevLevel > 1) {
                error = ERRORS.NONCONSECUTIVE_HEADER(prevLevel, level);
            }

            prevLevel = level;

            // Render the entry in the outline for the "Summary" tab
            let $item = $(outlineItemTemplate({
                level: level,
                text: $el.text()
            }));

            $outline.append($item);

            // Highlight the heading element on hover
            annotate.toggleHighlight($el, $item);

            if (error) {
                // Register an error to the info panel
                let infoPanelError = this.error(
                    error.title, $(error.description), $el);

                // Place an error label on the heading tag
                annotate.errorLabel(
                    $el,
                    $el.prop("tagName").toLowerCase(),
                    error.title,
                    infoPanelError);

                // Mark the summary item as red
                // Pretty hacky, since we're borrowing label styles for this
                // summary tab
                $item
                    .find(".tota11y-heading-outline-level")
                    .addClass("tota11y-label-error");
            } else {
                // Label the heading tag
                annotate.label($el).addClass("tota11y-label-success");

                // Mark the summary item as green
                $item
                    .find(".tota11y-heading-outline-level")
                    .addClass("tota11y-label-success");
            }
        });

        return $outline;
    }

    run() {
        let $headings = $("h1, h2, h3, h4, h5, h6");
        // `this.outline` has the side-effect of also reporting violations
        let $outline = this.outline($headings);

        this.summary($outline);
    }

    cleanup() {
        annotate.removeAll();
    }
}

module.exports = HeadingsPlugin;
<commit_msg>Removed empty summary panel for the headings plugin - fixes #13<commit_after>/**
 * A plugin to identify and validate heading tags (
,
, etc.)
 */

let $ = require("jquery");
let Plugin = require("../base");
let annotate = require("../shared/annotate")("headings");

let outlineItemTemplate = require("./outline-item.handlebars");
require("./style.less");

/* eslint new-cap:0 */
const ERRORS = {
    FIRST_NOT_H1(level) {
        return {
            title: "First heading is not an <h1>",
            description: `

                    To give your document a proper structure for assistive
                    technologies, it is important to lay out your headings
                    beginning with an <h1>. We found an
                    <h${level}> instead.

            `
        };
    },

    // This error is currently unused.
    //
    // The HTML5 outlining algorithm[1] enables the use of "sectioning roots"
    // to support multiple
 tags when embedded inside of containers like
    //
 or
. There are currently "no known implementations
    // of the outline algorithm in graphical browsers or assistive technology
    // user agents" [2], so we instead simply "use heading rank (h1-h6) to
    // convey document structure." [2].
    //
    // [1]: http://www.w3.org/html/wg/drafts/html/master/semantics.html#outline
    // [2]: https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Sections_and_Outlines_of_an_HTML5_document#The_HTML5_Outline_Algorithm
    MULTIPLE_H1: {
        title: "<h1> used when one is already present"
    },

    // This error accepts two arguments to display a relevant error message
    NONCONSECUTIVE_HEADER(prevLevel, currLevel) {
        let _tag = (level) => `<code><h${level}></code>`;
        let description = `

                This document contains an ${_tag(currLevel)} tag directly
                following an ${_tag(prevLevel)}. In order to maintain a consistent
                outline of the page for assistive technologies, reduce the gap in
                the heading level by upgrading this tag to an
                ${_tag(prevLevel+1)}`;

        // Suggest upgrading the tag to the same level as `prevLevel` iff
        // `prevLevel` is not 1
        if (prevLevel !== 1) {
            description += ` or ${_tag(prevLevel)}`;
        }

        description += `.</div>`;

        return {
            title: `
                Nonconsecutive heading level used (h${prevLevel} →
                h${currLevel})
            `,
            description: description
        };
    }
};

class HeadingsPlugin extends Plugin {
    getTitle() {
        return "Headings";
    }

    getDescription() {
        return "Highlights headings (
,
, etc) and order violations";
    }

    /**
     * Computes an outline of the page and reports any violations.
     */
    outline($headings) {
        let $items = [];

        let prevLevel;
        $headings.each((i, el) => {
            let $el = $(el);
            let level = +$el.prop("tagName").slice(1);
            let error;

            // Check for any violations
            // NOTE: These violations do not overlap, but as we add more, we
            // may want to separate the conditionals here to report multiple
            // errors on the same tag.
            if (i === 0 && level !== 1) {
                error = ERRORS.FIRST_NOT_H1(level);
            } else if (prevLevel && level - prevLevel > 1) {
                error = ERRORS.NONCONSECUTIVE_HEADER(prevLevel, level);
            }

            prevLevel = level;

            // Render the entry in the outline for the "Summary" tab
            let $item = $(outlineItemTemplate({
                level: level,
                text: $el.text()
            }));

            $items.push($item);

            // Highlight the heading element on hover
            annotate.toggleHighlight($el, $item);

            if (error) {
                // Register an error to the info panel
                let infoPanelError = this.error(
                    error.title, $(error.description), $el);

                // Place an error label on the heading tag
                annotate.errorLabel(
                    $el,
                    $el.prop("tagName").toLowerCase(),
                    error.title,
                    infoPanelError);

                // Mark the summary item as red
                // Pretty hacky, since we're borrowing label styles for this
                // summary tab
                $item
                    .find(".tota11y-heading-outline-level")
                    .addClass("tota11y-label-error");
            } else {
                // Label the heading tag
                annotate.label($el).addClass("tota11y-label-success");

                // Mark the summary item as green
                $item
                    .find(".tota11y-heading-outline-level")
                    .addClass("tota11y-label-success");
            }
        });

        return $items;
    }

    run() {
        let $headings = $("h1, h2, h3, h4, h5, h6");
        // `this.outline` has the side-effect of also reporting violations
        let $items = this.outline($headings);

        if ($items.length) {
            let $outline = (

                    {$items}

            );

            this.summary($outline);
        }
    }

    cleanup() {
        annotate.removeAll();
    }
}

module.exports = HeadingsPlugin;
<|endoftext|>


<commit_before>const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlWebpackTemplate = require('html-webpack-template');
const webpack = require('webpack');
const merge = require('webpack-merge');
const glob = require('glob');

const parts = require('./webpack.parts');

const PATHS = {
  app: path.join(__dirname, 'app'),
  build: path.join(__dirname, 'build'),
};

const common = merge([
  {
    // Entry accepts a path or an object of entries.
    // We'll be using the latter form given it's
    // convenient with more complex configurations.
    //
    // Entries have to resolve to files! It relies on Node.js
    // convention by default so if a directory contains *index.js*,
    // it will resolve to that.
    entry: {
      app: PATHS.app,
    },
    output: {
      path: PATHS.build,
      filename: '[name].js',
    },
    plugins: [
      new HtmlWebpackPlugin({
        template: HtmlWebpackTemplate,
        title: 'Webpack demo',
        appMountId: 'app', // Generate #app where to mount
        mobile: true, // Scale page on mobile
        inject: false, // html-webpack-template requires this to work
      }),
    ],
  },
  parts.lintCSS({ include: PATHS.app }),
  parts.loadImages({
    options: {
      limit: 15000,
    },
  }),
  parts.loadFonts(),
  parts.loadJavaScript({ include: PATHS.app }),
]);

function production() {
  return merge([
    common,
    {
      performance: {
        hints: 'warning', // 'error' or false are valid too
        maxEntrypointSize: 200000, // in bytes
        maxAssetSize: 200000, // in bytes
      },
      output: {
        chunkFilename: 'scripts/[chunkhash].js',
        filename: '[name].[chunkhash].js',
      },
      plugins: [
        new webpack.HashedModuleIdsPlugin(),
      ],
      recordsPath: 'records.json',
    },
    parts.setFreeVariable(
      'process.env.NODE_ENV',
      'production'
    ),
    parts.clean(PATHS.build),
    parts.minifyJavaScript({ useSourceMap: true }),
    parts.extractBundles({
      bundles: [
        {
          name: 'vendor',
          entries: ['react', 'react-dom'],
        },
        {
          name: 'manifest',
        },
      ],
    }),
    parts.generateSourceMaps({ type: 'source-map' }),
    parts.lintJavaScript({ include: PATHS.app }),
    parts.extractCSS({
      use: ['css-loader', parts.autoprefix()],
    }),
    parts.purifyCSS({
      paths: glob.sync(path.join(PATHS.app, '**', '*')),
    }),
  ]);
}

function development() {
  return merge([
    common,
    {
      entry: {
        // react-hot-loader has to run before app!
        app: ['react-hot-loader/patch', PATHS.app],
      },
      output: {
        devtoolModuleFilenameTemplate: 'webpack:///[absolute-resource-path]',
      },
      plugins: [
        new webpack.NamedModulesPlugin(),
      ],
    },
    parts.generateSourceMaps({ type: 'cheap-module-eval-source-map' }),
    parts.loadCSS(),
    parts.devServer({
      // Customize host/port here if needed
      host: process.env.HOST,
      port: process.env.PORT,
    }),
    parts.lintJavaScript({
      include: PATHS.app,
      options: {
        // Emit warnings over errors to avoid crashing
        // HMR on error.
        emitWarning: true,
      },
    }),
  ]);
}

module.exports = function(env) {
  process.env.BABEL_ENV = env;

  if (env === 'production') {
    return production();
  }

  return development();
};
<commit_msg>Make font/image config clearer<commit_after>const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlWebpackTemplate = require('html-webpack-template');
const webpack = require('webpack');
const merge = require('webpack-merge');
const glob = require('glob');

const parts = require('./webpack.parts');

const PATHS = {
  app: path.join(__dirname, 'app'),
  build: path.join(__dirname, 'build'),
};

const common = merge([
  {
    // Entry accepts a path or an object of entries.
    // We'll be using the latter form given it's
    // convenient with more complex configurations.
    //
    // Entries have to resolve to files! It relies on Node.js
    // convention by default so if a directory contains *index.js*,
    // it will resolve to that.
    entry: {
      app: PATHS.app,
    },
    output: {
      path: PATHS.build,
      filename: '[name].js',
    },
    plugins: [
      new HtmlWebpackPlugin({
        template: HtmlWebpackTemplate,
        title: 'Webpack demo',
        appMountId: 'app', // Generate #app where to mount
        mobile: true, // Scale page on mobile
        inject: false, // html-webpack-template requires this to work
      }),
    ],
  },
  parts.lintCSS({ include: PATHS.app }),
  parts.loadImages({
    options: {
      limit: 15000,
      name: '[name].[hash].[ext]',
    },
  }),
  parts.loadFonts({
    options: {
      name: '[name].[hash].[ext]',
    },
  }),
  parts.loadJavaScript({ include: PATHS.app }),
]);

function production() {
  return merge([
    common,
    {
      performance: {
        hints: 'warning', // 'error' or false are valid too
        maxEntrypointSize: 200000, // in bytes
        maxAssetSize: 200000, // in bytes
      },
      output: {
        chunkFilename: 'scripts/[chunkhash].js',
        filename: '[name].[chunkhash].js',
      },
      plugins: [
        new webpack.HashedModuleIdsPlugin(),
      ],
      recordsPath: 'records.json',
    },
    parts.setFreeVariable(
      'process.env.NODE_ENV',
      'production'
    ),
    parts.clean(PATHS.build),
    parts.minifyJavaScript({ useSourceMap: true }),
    parts.extractBundles({
      bundles: [
        {
          name: 'vendor',
          entries: ['react', 'react-dom'],
        },
        {
          name: 'manifest',
        },
      ],
    }),
    parts.generateSourceMaps({ type: 'source-map' }),
    parts.lintJavaScript({ include: PATHS.app }),
    parts.extractCSS({
      use: ['css-loader', parts.autoprefix()],
    }),
    parts.purifyCSS({
      paths: glob.sync(path.join(PATHS.app, '**', '*')),
    }),
  ]);
}

function development() {
  return merge([
    common,
    {
      entry: {
        // react-hot-loader has to run before app!
        app: ['react-hot-loader/patch', PATHS.app],
      },
      output: {
        devtoolModuleFilenameTemplate: 'webpack:///[absolute-resource-path]',
      },
      plugins: [
        new webpack.NamedModulesPlugin(),
      ],
    },
    parts.generateSourceMaps({ type: 'cheap-module-eval-source-map' }),
    parts.loadCSS(),
    parts.devServer({
      // Customize host/port here if needed
      host: process.env.HOST,
      port: process.env.PORT,
    }),
    parts.lintJavaScript({
      include: PATHS.app,
      options: {
        // Emit warnings over errors to avoid crashing
        // HMR on error.
        emitWarning: true,
      },
    }),
  ]);
}

module.exports = function(env) {
  process.env.BABEL_ENV = env;

  if (env === 'production') {
    return production();
  }

  return development();
};
<|endoftext|>


<commit_before>module.exports = {
  extends: [
    'airbnb-base',
    './common.js'
  ],

  parser: 'babel-eslint',

  plugins: [
    'babel',
    'import',
  ],

  rules: {
    'func-names': 0,
    curly: ['error', 'all']
  },
};
<commit_msg>Fix import file not exists in project.<commit_after>module.exports = {
  extends: [
    'airbnb-base'
  ],

  parser: 'babel-eslint',

  plugins: [
    'babel',
    'import',
  ],

  rules: {
    'func-names': 0,
    curly: ['error', 'all']
  },
};
<|endoftext|>


<commit_before>/**
 * jspsych-palmer
 * Josh de Leeuw (October 2013)
 *
 * a jspsych plugin for presenting and querying about stimuli modeled after
 *
 * Palmer, S. (1977). Hierarchical Structure in Perceptual Representation. Cognitive Psychology, 9, 441.
 *
 * and
 *
 * Goldstone, R. L., Rogosky, B. J., Pevtzow, R., & Blair, M. (2005). Perceptual and semantic reorganization during category learning.
 * In H. Cohen & C. Lefebvre (Eds.) Handbook of Categorization in Cognitive Science. (pp. 651-678). Amsterdam: Elsevier.
 *
 * documentation: docs.jspsych.org
 *
 */

jsPsych.plugins.palmer = (function() {

  var plugin = {};

  plugin.info = {
    name: 'palmer',
    description: '',
    parameters: {
      configuration: {
        type: jsPsych.plugins.parameterType.INT,
        default: undefined,
        array: true,
        no_function: false,
        description: ''
      },
      show_feedback: {
        type: jsPsych.plugins.parameterType.BOOL,
        default: false,
        no_function: false,
        description: ''
      },
      grid_spacing: {
        type: jsPsych.plugins.parameterType.INT,
        default: 75,
        no_function: false,
        description: ''
      },
      circle_radius: {
        type: jsPsych.plugins.parameterType.INT,
        default: 20,
        no_function: false,
        description: ''
      },
      square_size: {
        type: jsPsych.plugins.parameterType.INT,
        default: 3,
        no_function: false,
        description: ''
      },
      timing_feedback: {
        type: jsPsych.plugins.parameterType.INT,
        default: 1000,
        no_function: false,
        description: ''
      },
      prompt: {
        type: jsPsych.plugins.parameterType.STRING,
        default: '',
        no_function: false,
        description: ''
      },
      button_label: {
        type: jsPsych.plugins.parameterType.STRING,
        default: 'Done',
        no_function: false,
        description: ''
      }
    }
  }

  plugin.trial = function(display_element, trial) {

    // default parameter settings
    trial.show_feedback = (typeof trial.show_feedback === 'undefined') ? false : trial.show_feedback;
    trial.grid_spacing = trial.grid_spacing || 75;
    trial.square_size = trial.square_size || 3;
    trial.circle_radius = trial.circle_radius || 20;
    trial.timing_item = trial.timing_item || 1000;
    trial.timing_feedback = trial.timing_feedback || 1000;
    trial.prompt = (typeof trial.prompt === 'undefined') ? "" : trial.prompt;
    trial.button_label = typeof trial.button_label === 'undefined' ? 'Next' : trial.button_label;

    // variables to keep track of user interaction
    var start_circle = -1;
    var end_circle = -1;
    var line_started = false;

    var size = trial.grid_spacing * (trial.square_size + 1);

    display_element.innerHTML = "<svg id='jspsych-palmer-snapCanvas' width='" + size + "' height='" + size + "'></svg>";

    var paper = Snap("#jspsych-palmer-snapCanvas");

    // create the circles at the vertices.
    var circles = [];
    var node_idx = 0;
    for (var i = 1; i <= trial.square_size; i++) {
      for (var j = 1; j <= trial.square_size; j++) {
        var circle = paper.circle(trial.grid_spacing * j, trial.grid_spacing * i, trial.circle_radius);
        circle.attr("fill", "#000").attr("stroke-width", "0").attr("stroke", "#000").data("node", node_idx);

        circle.hover(

          function() {
            this.attr("stroke-width", "2");
            //this.attr("stroke", "#000");
          },

          function() {
            this.attr("stroke-width", "0");
            //this.attr("stroke", "#fff")
          }).click(

          function() {
            if (!line_started) {
              line_started = true;
              start_circle = this.data("node");
              this.attr("fill", "#777").attr("stroke", "#777");
            } else {
              end_circle = this.data("node");
              draw_connection(start_circle, end_circle);
            }
          });

        node_idx++;
        circles.push(circle);
      }
    }

    function draw_connection(start_circle, end_circle) {
      var the_line = getLineIndex(start_circle, end_circle);
      if (the_line > -1) {
        toggle_line(the_line);
      }
      // reset highlighting on circles
      for (var i = 0; i < circles.length; i++) {
        circles[i].attr("fill", "#000").attr("stroke", "#000");
      }
      // cleanup the variables
      line_started = false;
      start_circle = -1;
      end_circle = -1;
    }

    // create all possible lines that connect circles
    var horizontal_lines = [];
    var vertical_lines = [];
    var backslash_lines = [];
    var forwardslash_lines = [];

    for (var i = 0; i < trial.square_size; i++) {
      for (var j = 0; j < trial.square_size; j++) {
        var current_item = (i * trial.square_size) + j;
        // add horizontal connections
        if (j < (trial.square_size - 1)) {
          horizontal_lines.push([current_item, current_item + 1]);
        }
        // add vertical connections
        if (i < (trial.square_size - 1)) {
          vertical_lines.push([current_item, current_item + trial.square_size]);
        }
        // add diagonal backslash connections
        if (i < (trial.square_size - 1) && j < (trial.square_size - 1)) {
          backslash_lines.push([current_item, current_item + trial.square_size + 1]);
        }
        // add diagonal forwardslash connections
        if (i < (trial.square_size - 1) && j > 0) {
          forwardslash_lines.push([current_item, current_item + trial.square_size - 1]);
        }
      }
    }

    var lines = horizontal_lines.concat(vertical_lines).concat(backslash_lines).concat(forwardslash_lines);

    // actually draw the lines
    var lineIsVisible = [];
    var lineElements = [];

    for (var i = 0; i < lines.length; i++) {
      var line = paper.path("M" + circles[lines[i][0]].attr("cx") + " " + circles[lines[i][0]].attr("cy") + "L" + circles[lines[i][1]].attr("cx") + " " + circles[lines[i][1]].attr("cy")).attr("stroke-width", "8").attr("stroke", "#000");
      line.attr({
        visibility: 'hidden'
      });
      lineElements.push(line);
      lineIsVisible.push(0);
    }

    // define some helper functions to toggle lines on and off

    // this function gets the index of a line based on the two circles it connects

    function getLineIndex(start_circle, end_circle) {
      var the_line = -1;
      for (var i = 0; i < lines.length; i++) {
        if ((start_circle == lines[i][0] && end_circle == lines[i][1]) || (start_circle == lines[i][1] && end_circle == lines[i][0])) {
          the_line = i;
          break;
        }
      }
      return the_line;
    }

    // this function turns a line on/off based on the index (the_line)

    function toggle_line(the_line) {
      if (the_line > -1) {
        if (lineIsVisible[the_line] === 0) {
          lineElements[the_line].attr({
            visibility: 'visible'
          });
          lineElements[the_line].prependTo(paper);
          lineIsVisible[the_line] = 1;
        } else {
          lineElements[the_line].attr({
            visibility: 'hidden'
          });
          lineElements[the_line].prependTo(paper);
          lineIsVisible[the_line] = 0;
        }
      }
    }

    // this function takes an array of length = num lines, and displays the line whereever there
    // is a 1 in the array.

    function showConfiguration(configuration) {
      for (var i = 0; i < configuration.length; i++) {
        if (configuration[i] != lineIsVisible[i]) {
          toggle_line(i);
        }
      }
    }

    // highlight a line

    function highlightLine(line) {
      lineElements[line].attr("stroke", "#f00");
    }

    // start recording the time
    var startTime = (new Date()).getTime();

    display_element.innerHTML += '
<button id="jspsych-palmer-submitButton" class="jspsych-btn" type="button">'+trial.button_label+'</button>
';
    display_element.querySelector('#jspsych-palmer-submitButton').addEventListener('click', function() {
      save_data();
    });

    if (trial.prompt !== "") {
      display_element.innerHTML += '
'+trial.prompt+'
';
    }

    function arrayDifferences(arr1, arr2) {
      var n_diff = 0;
      for (var i = 0; i < arr1.length; i++) {
        if (arr1[i] != arr2[i]) {
          n_diff++;
        }
      }
      return n_diff;
    }

    // save data
    var trial_data = {};

    function save_data() {

      // measure RT
      var endTime = (new Date()).getTime();
      var response_time = endTime - startTime;

      // check if configuration is correct
      // this is meaningless for trials where the user can't edit
      var n_diff = arrayDifferences(trial.configuration, lineIsVisible);
      var correct = (n_diff === 0);

      trial_data = {
        "configuration": JSON.stringify(lineIsVisible),
        "target_configuration": JSON.stringify(trial.configuration),
        "rt": response_time,
        "correct": correct,
        "num_wrong": n_diff,
      };

      if (trial.show_feedback) {
        // hide the button
        display_element.querySelector('#jspsych-palmer-submitButton').style.display = 'none';
        display_element.querySelector('#jspsych-palmer-prompt').style.display = 'none';

        showConfiguration(trial.configuration);
        var feedback = "";
        if (correct) {
          feedback = "Correct!";
        } else {
          if (n_diff > 1) {
            feedback = "You missed " + n_diff + " lines. The correct symbol is shown above.";
          } else {
            feedback = "You missed 1 line. The correct symbol is shown above.";
          }
        }
        display_element.innerHTML += "
" + feedback + "
";

        jsPsych.pluginAPI.setTimeout(function() {
          next_trial();
        }, trial.timing_feedback);

      } else {
        next_trial();
      }
    }

    function next_trial() {

      display_element.innerHTML = '';

      // next trial
      jsPsych.finishTrial(trial_data);

    }

  };

  // method for drawing palmer stimuli.
  // returns the string description of svg element containing the stimulus

  plugin.generate_stimulus = function(square_size, grid_spacing, circle_radius, configuration) {

    var size = grid_spacing * (square_size + 1);

    // create a div to hold the generated svg object
    var stim_div = document.querySelector('html').innerHTML += '
<svg id="jspsych-palmer-temp-stim" width="' + size + '" height="' + size + '"></svg>
';

    // create the snap object
    var paper = Snap("#jspsych-palmer-temp-stim");

    // create the circles at the vertices.
    var circles = [];
    var node_idx = 0;
    for (var i = 1; i <= square_size; i++) {
      for (var j = 1; j <= square_size; j++) {
        var circle = paper.circle(grid_spacing * j, grid_spacing * i, circle_radius);
        circle.attr("fill", "#000").attr("stroke-width", "0").attr("stroke", "#000").data("node", node_idx);
        node_idx++;
        circles.push(circle);
      }
    }

    // create all possible lines that connect circles
    var horizontal_lines = [];
    var vertical_lines = [];
    var backslash_lines = [];
    var forwardslash_lines = [];

    for (var i = 0; i < square_size; i++) {
      for (var j = 0; j < square_size; j++) {
        var current_item = (i * square_size) + j;
        // add horizontal connections
        if (j < (square_size - 1)) {
          horizontal_lines.push([current_item, current_item + 1]);
        }
        // add vertical connections
        if (i < (square_size - 1)) {
          vertical_lines.push([current_item, current_item + square_size]);
        }
        // add diagonal backslash connections
        if (i < (square_size - 1) && j < (square_size - 1)) {
          backslash_lines.push([current_item, current_item + square_size + 1]);
        }
        // add diagonal forwardslash connections
        if (i < (square_size - 1) && j > 0) {
          forwardslash_lines.push([current_item, current_item + square_size - 1]);
        }
      }
    }

    var lines = horizontal_lines.concat(vertical_lines).concat(backslash_lines).concat(forwardslash_lines);

    // actually draw the lines
    var lineIsVisible = [];
    var lineElements = [];

    for (var i = 0; i < lines.length; i++) {
      var line = paper.path("M" + circles[lines[i][0]].attr("cx") + " " + circles[lines[i][0]].attr("cy") + "L" + circles[lines[i][1]].attr("cx") + " " + circles[lines[i][1]].attr("cy")).attr("stroke-width", "8").attr("stroke", "#000");
      line.attr({
        visibility: 'hidden'
      });
      lineElements.push(line);
      lineIsVisible.push(0);
    }

    // define some helper functions to toggle lines on and off

    // this function turns a line on/off based on the index (the_line)

    function toggle_line(the_line) {
      if (the_line > -1) {
        if (lineIsVisible[the_line] === 0) {
          lineElements[the_line].attr({
            visibility: 'visible'
          });
          lineElements[the_line].prependTo(paper);
          lineIsVisible[the_line] = 1;
        } else {
          lineElements[the_line].attr({
            visibility: 'hidden'
          });
          lineElements[the_line].prependTo(paper);
          lineIsVisible[the_line] = 0;
        }
      }
    }

    // displays the line wherever there
    // is a 1 in the array.
    // showConfiguration(configuration)
    for (var i = 0; i < configuration.length; i++) {
      if (configuration[i] == 1) {
        toggle_line(i);
      }
    }

    var svg = document.getElementById("jspsych-palmer-container").innerHTML;

    document.getElementById('jspsych-palmer-container').outerHTML = '';

    return svg;
  };

  return plugin;
})();
<commit_msg>remove palmer plugin<commit_after><|endoftext|>


<commit_before>module.exports = {
    extends: [
        'prettier'
    ],
    env: {
        es6: true,
        browser: true,
        node: true
    },
    parser: 'babel-eslint',
    parserOptions: {
        ecmaVersion: 2017,
        sourceType: 'module',
        ecmaFeatures: {
            impliedStrict: true
        }
    },
    plugins: [
        'prettier'
    ],
    rules: {
        'prettier/prettier': ['error', {
            useTabs: true,
            tabWidth: 4,
            singleQuote: true,
            semi: false
        }]
    }
}<commit_msg>Added 'eslint:recommended' config<commit_after>module.exports = {
    extends: [
        'eslint:recommended',
        'prettier'
    ],
    env: {
        es6: true,
        browser: true,
        node: true
    },
    parser: 'babel-eslint',
    parserOptions: {
        ecmaVersion: 2017,
        sourceType: 'module',
        ecmaFeatures: {
            impliedStrict: true
        }
    },
    plugins: [
        'prettier'
    ],
    rules: {
        'prettier/prettier': ['error', {
            useTabs: true,
            tabWidth: 4,
            singleQuote: true,
            semi: false
        }]
    }
}<|endoftext|>


<commit_before>var allUsers = [];

/**
 * Gets all of the dates for a user and puts them into our data object
 * @param  {Object} user
 * @param  {Object} data
 */
function mixinDataForUser(user, data) {
    if (user.events && user.events.length) {
        for (var i = 0; i < user.events.length; i++) {
            var start = new moment(user.events[i].start);
            var end = new moment(user.events[i].end);

            // Add all of our start days
            if (!data[start.unix()]) {
                data[start.unix()] = 1;
            } else {
                data[start.unix()] += 1;
            }

            // Add all the subsequent days
            if (user.events[i].start !== user.events[i].end) {
                var diffInDays = end.diff(start, 'days');
                for (var j = 0; j < diffInDays; j++) {
                    var recurringDay = moment(start).add(j + 1, 'day');

                    if (!data[recurringDay.unix()]) {
                        data[recurringDay.unix()] = 1;
                    } else {
                        data[recurringDay.unix()] += 1;
                    }
                }
            }
        }
    }
}

/**
 * Displays a list of users who are gone on a specific day
 * @param  {String} date
 */
function displaySelectedDate(date) {
    var usersGone = {};

    // Loop through every user
    for (var i = 0; i < allUsers.length; i++) {
        var user = allUsers[i];
        if (user.events && user.events.length) {
            // Loop through every event
            for (var j = 0; j < user.events.length; j++) {
                var event = user.events[j];
                if (moment(date).isBetween(moment(event.start), moment(event.end), 'day', '[]')) {
                    usersGone[user.id] = user;
                    if (!usersGone[user.id].matchedEvents) {
                        usersGone[user.id].matchedEvents = [];
                    }
                    usersGone[user.id].matchedEvents.push(event);
                }
            }
        }
    }

    var $results = $('#cal-day-results');

    $.each(usersGone, function(userId, user) {
        var $resultCollection = $('
');
        var $item = $('
');
        $item.append('');
        $item.append(''+user.slackUser.real_name+'');
        $.each(user.matchedEvents, function(i, event) {
            var $event = $('
');
            var start = moment(event.start);
            var end = moment(event.end);

            if (end.diff(start, 'days') > 0) {
                $event.append(' All day');
            } else {
                $event.append(' ' + start.format('h:mma') + ' - ' + end.format('h:mma'));
            }
            if (event.reasonPrefix && event.reason) {
                $event.append(' ' + event.reasonPrefix + ' ' + event.reason);
                $event.append('
');
            }
            $item.append($event);
        });
        $resultCollection.append($item);
    });

    $results.html($resultCollection);
}

$(function() {
    var cal = new CalHeatMap();
    var data = {};

    // Get the data for our users
    if (window.USER) {
        allUsers.push(window.USER);
    } else if (window.USERS) {
        allUsers = window.USERS;
    }

    for (var i = 0; i < allUsers.length; i++) {
        mixinDataForUser(allUsers[i], data);
        mixinDataForUser(allUsers[i], data);
    }

    // Initialize our calendar
    cal.init({
        domain: 'month',
        data: data,
        onClick: displaySelectedDate,
    });
});
<commit_msg>Fixing JS error<commit_after>var allUsers = [];

/**
 * Gets all of the dates for a user and puts them into our data object
 * @param  {Object} user
 * @param  {Object} data
 */
function mixinDataForUser(user, data) {
    if (user.events && user.events.length) {
        for (var i = 0; i < user.events.length; i++) {
            var start = new moment(user.events[i].start);
            var end = new moment(user.events[i].end);

            // Add all of our start days
            if (!data[start.unix()]) {
                data[start.unix()] = 1;
            } else {
                data[start.unix()] += 1;
            }

            // Add all the subsequent days
            if (user.events[i].start !== user.events[i].end) {
                var diffInDays = end.diff(start, 'days');
                for (var j = 0; j < diffInDays; j++) {
                    var recurringDay = moment(start).add(j + 1, 'day');

                    if (!data[recurringDay.unix()]) {
                        data[recurringDay.unix()] = 1;
                    } else {
                        data[recurringDay.unix()] += 1;
                    }
                }
            }
        }
    }
}

/**
 * Displays a list of users who are gone on a specific day
 * @param  {String} date
 */
function displaySelectedDate(date) {
    var usersGone = {};

    // Loop through every user
    for (var i = 0; i < allUsers.length; i++) {
        var user = allUsers[i];
        if (user.events && user.events.length) {
            // Loop through every event
            for (var j = 0; j < user.events.length; j++) {
                var event = user.events[j];
                if (moment(date).isBetween(moment(event.start), moment(event.end), 'day', '[]')) {
                    usersGone[user.id] = user;
                    if (!usersGone[user.id].matchedEvents) {
                        usersGone[user.id].matchedEvents = [];
                    }
                    usersGone[user.id].matchedEvents.push(event);
                }
            }
        }
    }

    var $results = $('#cal-day-results').empty();
    var $resultCollection = $('
');

    $.each(usersGone, function(userId, user) {
        var $item = $('
');
        $item.append('');
        $item.append(''+user.slackUser.real_name+'');
        $.each(user.matchedEvents, function(i, event) {
            var $event = $('
');
            var start = moment(event.start);
            var end = moment(event.end);

            if (end.diff(start, 'days') > 0) {
                $event.append(' All day');
            } else {
                $event.append(' ' + start.format('h:mma') + ' - ' + end.format('h:mma'));
            }
            if (event.reasonPrefix && event.reason) {
                $event.append(' ' + event.reasonPrefix + ' ' + event.reason);
            }
            $item.append($event);
        });
        $resultCollection.append($item);
    });

    $results.html($resultCollection);
}

$(function() {
    var cal = new CalHeatMap();
    var data = {};

    // Get the data for our users
    if (window.USER) {
        allUsers.push(window.USER);
    } else if (window.USERS) {
        allUsers = window.USERS;
    }

    for (var i = 0; i < allUsers.length; i++) {
        mixinDataForUser(allUsers[i], data);
        mixinDataForUser(allUsers[i], data);
    }

    // Initialize our calendar
    cal.init({
        domain: 'month',
        data: data,
        onClick: displaySelectedDate,
    });
});
<|endoftext|>


<commit_before>const path = require('path');
const webpack = require('webpack');

module.exports = {
  cache: true,
  entry: [
    // 'webpack-hot-middleware',
    './client/index.jsx'],
  output: {
    path: path.join(__dirname, 'client/dist/'),
    publicPath: '/client/index.js',
    filename: 'bundle.js'
  },

  externals: {
    Materialize: 'Materialize'
  },
  plugins: [
    new webpack.NoEmitOnErrorsPlugin(),
    new webpack.optimize.OccurrenceOrderPlugin(),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.optimize.ModuleConcatenationPlugin(),
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      }
    }),
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify('production')
      }
    }),
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery',
      'window.jQuery': 'jquery',
      Hammer: 'hammerjs/hammer'
    })
  ],
  module: {
    rules: [
      {
        test: /.jsx?$/,
        enforce: 'pre',
        use: [
          {
            loader: 'babel-loader',
            query: {
              cacheDirectory: true,
              presets: ['es2015', 'react']
            }
          }
        ],
        include: path.join(__dirname, './client'),
        exclude: /node_modules/,
      },
      { test: /(\.s?css)$/,
        loader: ['style-loader', 'css-loader', 'sass-loader']
      },
      { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'file-loader'

      },
      { test: /\.woff2(\?\S*)?$/,
        loader: 'url-loader?limit=100000'
      },
      { test: /\.woff(\?\S*)?$/,
        loader: 'url-loader?limit=100000'
      },
      { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,

        loader: 'url-loader?limit=100000&mimetype=application/octet-stream'
      },
      {
        test: /\.(jpe?g|png|gif|svg)$/i,
        loaders: ['file-loader?name=/assets/img/[name].[ext]', {
          loader: 'image-webpack-loader',
          query: {
            mozjpeg: {
              progressive: true,
            },
            gifsicle: {
              interlaced: false,
            },
            optipng: {
              optimizationLevel: 4,
            },
            pngquant: {
              quality: '75-90',
              speed: 3,
            },
          },
        }],
        exclude: /node_modules/,
        include: __dirname,
      },
      {
        test: /materialize-css\/bin\//,
        loader: 'imports-loader?jQuery=jquery,$=jquery,hammerjs'
      },
      { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,

        loader: 'url-loader?limit=10000&mimetype=image/svg+xml'
      },
    ]
  },
  resolve: { extensions: ['.js', '.jsx', '.css'] },
  node: {
    dns: 'empty',
    net: 'empty',
    fs: 'empty'
  }
};
<commit_msg>Fix infinite redirection on heroku<commit_after>const path = require('path');
const webpack = require('webpack');

module.exports = {
  cache: true,
  entry: [
    // 'webpack-hot-middleware',
    './client/index.jsx'],
  output: {
    path: path.join(__dirname, 'client/dist/'),
    publicPath: '/client/index.js',
    filename: 'bundle.js'
  },

  externals: {
    Materialize: 'Materialize'
  },
  plugins: [
    new webpack.NoEmitOnErrorsPlugin(),
    new webpack.optimize.OccurrenceOrderPlugin(),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.optimize.ModuleConcatenationPlugin(),
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify('production')
      }
    }),
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery',
      'window.jQuery': 'jquery',
      Hammer: 'hammerjs/hammer'
    })
  ],
  module: {
    rules: [
      {
        test: /.jsx?$/,
        enforce: 'pre',
        use: [
          {
            loader: 'babel-loader',
            query: {
              cacheDirectory: true,
              presets: ['es2015', 'react']
            }
          }
        ],
        include: path.join(__dirname, './client'),
        exclude: /node_modules/,
      },
      { test: /(\.s?css)$/,
        loader: ['style-loader', 'css-loader', 'sass-loader']
      },
      { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'file-loader'

      },
      { test: /\.woff2(\?\S*)?$/,
        loader: 'url-loader?limit=100000'
      },
      { test: /\.woff(\?\S*)?$/,
        loader: 'url-loader?limit=100000'
      },
      { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,

        loader: 'url-loader?limit=100000&mimetype=application/octet-stream'
      },
      {
        test: /\.(jpe?g|png|gif|svg)$/i,
        loaders: ['file-loader?name=/assets/img/[name].[ext]', {
          loader: 'image-webpack-loader',
          query: {
            mozjpeg: {
              progressive: true,
            },
            gifsicle: {
              interlaced: false,
            },
            optipng: {
              optimizationLevel: 4,
            },
            pngquant: {
              quality: '75-90',
              speed: 3,
            },
          },
        }],
        exclude: /node_modules/,
        include: __dirname,
      },
      {
        test: /materialize-css\/bin\//,
        loader: 'imports-loader?jQuery=jquery,$=jquery,hammerjs'
      },
      { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,

        loader: 'url-loader?limit=10000&mimetype=image/svg+xml'
      },
    ]
  },
  resolve: { extensions: ['.js', '.jsx', '.css'] },
  node: {
    dns: 'empty',
    net: 'empty',
    fs: 'empty'
  }
};
<|endoftext|>


<commit_before>module.exports = {
    "extends": "airbnb",
    "plugins": [
        "react"
    ]
};<commit_msg>Add envs to eslint. Fix indentation<commit_after>module.exports = {
  "extends": "airbnb",
  "plugins": [
    "react"
  ],
  "env": {
    "node": true,
    "mocha": true,
  },
};
<|endoftext|>


<commit_before>'use strict';

var app = angular.module('app', ['ngRoute', 'ngResource']);

app.config(['$routeProvider', function($routeProvider) {
  $routeProvider
    .when('/', {
      templateUrl: 'home.html',
      controller: 'HomeCtrl'
    })
    .when('/employees', {
      templateUrl: 'employees.html',
      controller: 'EmployeesCtrl'
    })
    .when('/employees/:employeeId', {
      templateUrl: 'employee.html',
      controller: 'EmployeeCtrl'
    })
    .when('/teams', {
      templateUrl: 'teams.html',
      controller: 'TeamsCtrl'
    })
    .when('/teams/:teamId', {
      templateUrl: 'team.html',
      controller: 'TeamCtrl'
    })
    .otherwise({
      redirectTo: '/'
    });
}]);

app.factory('EmployeeService', ['$http', '$resource', function($http, $resource) {
  return $resource('/employees/:employeeId');
}]);

app.factory('TeamService', ['$http', '$resource', function($http, $resource) {
  return $resource('/teams/:teamId');
}]);

app.controller('HomeCtrl', ['$scope', function($scope) {
  $scope.message = 'OK!';
}]);

app.controller('EmployeesCtrl', ['$scope', 'EmployeeService', function($scope, service) {
  service.query(function(data, headers) {
    $scope.employees = data;
  }, function(response) {
    // TODO: do something here... Probably just redirect to an error page
    console.log('%c ' + response, 'color:red');
  });
}]);

app.controller('EmployeeCtrl', ['$scope', '$routeParams', 'EmployeeService', function($scope, $routeParams, service) {
  service.get({
    employeeId: $routeParams.employeeId
  }, function(data, headers) {
    $scope.employee = data.employee;
  }, function(response) {
    // TODO: do something here... Probably just redirect to an error page
    console.log('%c ' + response, 'color:red');
  });
}]);

app.controller('TeamsCtrl', ['$scope', 'TeamService', function($scope, service) {
  service.query(function(data, headers) {
    $scope.teams = data;
  }, function(response) {
    // TODO: do something here... Probably just redirect to an error page
    console.log('%c ' + response, 'color:red');
  });
}]);

app.controller('TeamCtrl', ['$scope', '$routeParams', 'TeamService', function($scope, $routeParams, service) {
  service.get({
    teamId: $routeParams.teamId
  }, function(data, headers) {
    $scope.team = data.team;
  }, function(response) {
    // TODO: do something here... Probably just redirect to an error page
    console.log('%c ' + response, 'color:red');
  });
}]);

app.directive('imageFallback', function() {
  return {
    link: function(scope, elem, attrs) {
      elem.bind('error', function() {
        angular.element(this).attr('src', attrs.imageFallback);
      });
    }
  };
});
<commit_msg>remove extra $http variables from factories<commit_after>'use strict';

var app = angular.module('app', ['ngRoute', 'ngResource']);

app.config(['$routeProvider', function($routeProvider) {
  $routeProvider
    .when('/', {
      templateUrl: 'home.html',
      controller: 'HomeCtrl'
    })
    .when('/employees', {
      templateUrl: 'employees.html',
      controller: 'EmployeesCtrl'
    })
    .when('/employees/:employeeId', {
      templateUrl: 'employee.html',
      controller: 'EmployeeCtrl'
    })
    .when('/teams', {
      templateUrl: 'teams.html',
      controller: 'TeamsCtrl'
    })
    .when('/teams/:teamId', {
      templateUrl: 'team.html',
      controller: 'TeamCtrl'
    })
    .otherwise({
      redirectTo: '/'
    });
}]);

app.factory('EmployeeService', ['$resource', function($resource) {
  return $resource('/employees/:employeeId');
}]);

app.factory('TeamService', ['$resource', function($resource) {
  return $resource('/teams/:teamId');
}]);

app.controller('HomeCtrl', ['$scope', function($scope) {
  $scope.message = 'OK!';
}]);

app.controller('EmployeesCtrl', ['$scope', 'EmployeeService', function($scope, service) {
  service.query(function(data, headers) {
    $scope.employees = data;
  }, function(response) {
    // TODO: do something here... Probably just redirect to an error page
    console.log('%c ' + response, 'color:red');
  });
}]);

app.controller('EmployeeCtrl', ['$scope', '$routeParams', 'EmployeeService', function($scope, $routeParams, service) {
  service.get({
    employeeId: $routeParams.employeeId
  }, function(data, headers) {
    $scope.employee = data.employee;
  }, function(response) {
    // TODO: do something here... Probably just redirect to an error page
    console.log('%c ' + response, 'color:red');
  });
}]);

app.controller('TeamsCtrl', ['$scope', 'TeamService', function($scope, service) {
  service.query(function(data, headers) {
    $scope.teams = data;
  }, function(response) {
    // TODO: do something here... Probably just redirect to an error page
    console.log('%c ' + response, 'color:red');
  });
}]);

app.controller('TeamCtrl', ['$scope', '$routeParams', 'TeamService', function($scope, $routeParams, service) {
  service.get({
    teamId: $routeParams.teamId
  }, function(data, headers) {
    $scope.team = data.team;
  }, function(response) {
    // TODO: do something here... Probably just redirect to an error page
    console.log('%c ' + response, 'color:red');
  });
}]);

app.directive('imageFallback', function() {
  return {
    link: function(scope, elem, attrs) {
      elem.bind('error', function() {
        angular.element(this).attr('src', attrs.imageFallback);
      });
    }
  };
});
<|endoftext|>


<commit_before>describe('Redirecter::Http', function() {
  var RedirecterHttp = require('../../../lib/redirecter/http'),
      DummyServer = require('../../support/dummy_server'),
      EasyClient = require('../../support/easy_client'),
      Http = require('http'),
      server,
      subject;

  beforeAll(function(){
    server = new DummyServer();
    server.start();
    this.memorize('client', function() {
      return new EasyClient({
        hostname: 'localhost',
        port: this.port()
      });
    });
    this.memorize('port', 3300);
    this.memorize('serverPort', 3000);
    this.memorize('config', function() {
      return {
        port: this.port(),
        proxy: {
          host: 'localhost',
          port: this.serverPort()
        }
      };
    });
  });

  beforeEach(function() {
    this.memorize('subject', function() {
      return RedirecterHttp(this.config());
    });
    this.memorized('subject').listen();
  });

  describe('when performing a get request', function() {
    it('request to server', function() {
      this.memorized('client').call(function(data) {
        console.info(data);
      });
    });
  });
});
<commit_msg>Use nock for request mocking<commit_after>describe('Redirecter::Http', function() {

  var RedirecterHttp = require('../../../lib/redirecter/http'),
      DummyServer = require('../../support/dummy_server'),
      EasyClient = require('../../support/easy_client'),
      nock = require('nock'),
      Http = require('http'),
      server,
      subject;

  beforeAll(function(){
    this.memorize('client', function() {
      return new EasyClient({
        hostname: 'localhost',
        port: this.port()
      });
    });
    this.memorize('port', 3300);
    this.memorize('serverPort', 3000);
    this.memorize('config', function() {
      return {
        port: this.port(),
        proxy: {
          host: 'localhost',
          port: this.serverPort()
        }
      };
    });
    nock('http://localhost:' + this.memorized('serverPort')).
      get('/').reply(200, 'data mocked');
  });

  beforeEach(function() {
    this.memorize('subject', function() {
      return RedirecterHttp(this.config());
    });
    this.memorized('subject').listen();
  });

  describe('when performing a get request', function() {
    it('request to server', function() {
      this.memorized('client').call(function(data) {
        console.info(data);
      });
    });
  });
});
<|endoftext|>


<commit_before>/*!
 * Facebook React Starter Kit | https://github.com/kriasoft/react-starter-kit
 * Copyright (c) KriaSoft, LLC. All rights reserved. See LICENSE.txt
 */

'use strict';

var webpack = require('webpack');
var update = require('react/lib/update');
var argv = require('minimist')(process.argv.slice(2));

var DEBUG = !argv.release;
var AUTOPREFIXER_LOADER = 'autoprefixer-loader?{browsers:[' +
  '"Android 2.3", "Android >= 4", "Chrome >= 20", "Firefox >= 24", ' +
  '"Explorer >= 8", "iOS >= 6", "Opera >= 12", "Safari >= 6"]}';

// Common configuration chunk to be used for both
// client-side (app.js) and server-side (server.js) bundles
var config = {
  output: {
    path: './build/',
    publicPath: './build/',
    sourcePrefix: '  '
  },

  cache: DEBUG,
  debug: DEBUG,
  devtool: DEBUG ? '#inline-source-map' : false,

  stats: {
    colors: true,
    reasons: DEBUG
  },

  plugins: [
    new webpack.optimize.OccurenceOrderPlugin()
  ],

  resolve: {
    extensions: ['', '.webpack.js', '.web.js', '.js', '.jsx']
  },

  module: {
    preLoaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'jshint'
      }
    ],

    loaders: [
      {
        test: /\.css$/,
        loader: 'style-loader!css-loader!' + AUTOPREFIXER_LOADER
      },
      {
        test: /\.less$/,
        loader: 'style-loader!css-loader!' + AUTOPREFIXER_LOADER +'!less-loader'
      },
      {
        test: /\.gif/,
        loader: 'url-loader?limit=10000&mimetype=image/gif'
      },
      {
        test: /\.jpg/,
        loader: 'url-loader?limit=10000&mimetype=image/jpg'
      },
      {
        test: /\.png/,
        loader: 'url-loader?limit=10000&mimetype=image/png'
      },
      {
        test: /\.svg/,
        loader: 'url-loader?limit=10000&mimetype=image/svg+xml'
      },
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: 'babel-loader?experimental'
      }
    ]
  }
};

// Configuration for the client-side bundle
var appConfig = update(config, {
  entry: {$set: './src/app.js'},
  output: {
    filename: {$set: 'app.js'}
  },
  plugins: {
    $push: [
      new webpack.DefinePlugin({
        'process.env.NODE_ENV': DEBUG ? '"development"' : '"production"',
        '__DEV__': DEBUG,
        '__SERVER__': false
      })
    ].concat(DEBUG ? [] : [
      new webpack.optimize.DedupePlugin(),
      new webpack.optimize.UglifyJsPlugin(),
      new webpack.optimize.AggressiveMergingPlugin()
    ])
  }
});

// Configuration for the server-side bundle
var serverConfig = update(config, {
  entry: {$set: './src/server.js'},
  output: {
    filename: {$set: 'server.js'},
    libraryTarget: {$set: 'commonjs2'}
  },
  target: {$set: 'node'},
  externals: {$set: /^[a-z\-0-9]+$/},
  node: {
    $set: {
      console: false,
      global: false,
      process: false,
      Buffer: false,
      __filename: false,
      __dirname: false
    }
  },
  plugins: {
    $push: [
      new webpack.DefinePlugin({
        'process.env.NODE_ENV': DEBUG ? '"development"' : '"production"',
        '__DEV__': DEBUG,
        '__SERVER__': true
      })
    ]
  },
  module: {
    loaders: {
      $apply: function(loaders) {
        // Remove style-loader
        return loaders.map(function(loader) {
          return update(loader, {
            loader: {
              $apply: function(loader) {
                return loader.replace('style-loader!', '');
              }
            }
          });
        });
      }
    }
  }
});

module.exports = [appConfig, serverConfig];
<commit_msg>Replace react/lib/update with _.cloneDeep, _.merge in webpack.config.js<commit_after>/*!
 * Facebook React Starter Kit | https://github.com/kriasoft/react-starter-kit
 * Copyright (c) KriaSoft, LLC. All rights reserved. See LICENSE.txt
 */

'use strict';

var _ = require('lodash');
var webpack = require('webpack');
var argv = require('minimist')(process.argv.slice(2));

var DEBUG = !argv.release;

var AUTOPREFIXER_LOADER = 'autoprefixer-loader?{browsers:[' +
  '"Android 2.3", "Android >= 4", "Chrome >= 20", "Firefox >= 24", ' +
  '"Explorer >= 8", "iOS >= 6", "Opera >= 12", "Safari >= 6"]}';

var GLOBALS = {
  'process.env.NODE_ENV': DEBUG ? '"development"' : '"production"',
  '__DEV__': DEBUG
};

//
// Common configuration chunk to be used for both
// client-side (app.js) and server-side (server.js) bundles
// -----------------------------------------------------------------------------

var config = {
  output: {
    path: './build/',
    publicPath: './build/',
    sourcePrefix: '  '
  },

  cache: DEBUG,
  debug: DEBUG,
  devtool: DEBUG ? '#inline-source-map' : false,

  stats: {
    colors: true,
    reasons: DEBUG
  },

  plugins: [
    new webpack.optimize.OccurenceOrderPlugin()
  ],

  resolve: {
    extensions: ['', '.webpack.js', '.web.js', '.js', '.jsx']
  },

  module: {
    preLoaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'jshint'
      }
    ],

    loaders: [
      {
        test: /\.css$/,
        loader: 'style-loader!css-loader!' + AUTOPREFIXER_LOADER
      },
      {
        test: /\.less$/,
        loader: 'style-loader!css-loader!' + AUTOPREFIXER_LOADER + '!less-loader'
      },
      {
        test: /\.gif/,
        loader: 'url-loader?limit=10000&mimetype=image/gif'
      },
      {
        test: /\.jpg/,
        loader: 'url-loader?limit=10000&mimetype=image/jpg'
      },
      {
        test: /\.png/,
        loader: 'url-loader?limit=10000&mimetype=image/png'
      },
      {
        test: /\.svg/,
        loader: 'url-loader?limit=10000&mimetype=image/svg+xml'
      },
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: 'babel-loader?experimental'
      }
    ]
  }
};

//
// Configuration for the client-side bundle (app.js)
// -----------------------------------------------------------------------------

var appConfig = _.merge(_.cloneDeep(config), {
  entry: './src/app.js',
  output: {
    filename: 'app.js'
  },
  plugins: config.plugins.concat([
      new webpack.DefinePlugin(_.merge(GLOBALS, {'__SERVER__': false}))
    ].concat(DEBUG ? [] : [
      new webpack.optimize.DedupePlugin(),
      new webpack.optimize.UglifyJsPlugin(),
      new webpack.optimize.AggressiveMergingPlugin()
    ])
  )
});

//
// Configuration for the server-side bundle (server.js)
// -----------------------------------------------------------------------------

var serverConfig = _.merge(_.cloneDeep(config), {
  entry: './src/server.js',
  output: {
    filename: 'server.js',
    libraryTarget: 'commonjs2'
  },
  target: 'node',
  externals: /^[a-z\-0-9]+$/,
  node: {
    console: false,
    global: false,
    process: false,
    Buffer: false,
    __filename: false,
    __dirname: false
  },
  plugins: config.plugins.concat(
    new webpack.DefinePlugin(_.merge(GLOBALS, {'__SERVER__': true}))
  )
});

// Remove style-loader
serverConfig.module.loaders.forEach(function(loader) {
  loader.loader = loader.loader.replace('style-loader!', '');
});

module.exports = [appConfig, serverConfig];
<|endoftext|>


<commit_before>module.exports = {
  root: true,
  parserOptions: {
    ecmaVersion: 2018,
    sourceType: 'module'
  },
  plugins: [
    'ember'
  ],
  extends: [
    'eslint:recommended',
    'plugin:ember/recommended'
  ],
  env: {
    browser: true
  },
  rules: {
  },
  overrides: [
    // node files
    {
      files: [
        '.eslintrc.js',
        '.template-lintrc.js',
        'ember-cli-build.js',
        'index.js',
        'testem.js',
        'blueprints/*/index.js',
        'config/**/*.js',
        'tests/dummy/config/**/*.js'
      ],
      excludedFiles: [
        'addon/**',
        'addon-test-support/**',
        'app/**',
        'tests/dummy/app/**'
      ],
      parserOptions: {
        sourceType: 'script'
      },
      env: {
        browser: false,
        node: true
      },
      plugins: ['node'],
      rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, {
        // add your custom rules and overrides for node files here
      })
    }
  ]
};
<commit_msg>fix: switch "ember/no-observers" lint rule to a warning since we depend on it to tell the slider about state changes<commit_after>module.exports = {
  root: true,
  parserOptions: {
    ecmaVersion: 2018,
    sourceType: 'module'
  },
  plugins: [
    'ember'
  ],
  extends: [
    'eslint:recommended',
    'plugin:ember/recommended'
  ],
  env: {
    browser: true
  },
  rules: {
    'ember/no-observers': 'warn'
  },
  overrides: [
    // node files
    {
      files: [
        '.eslintrc.js',
        '.template-lintrc.js',
        'ember-cli-build.js',
        'index.js',
        'testem.js',
        'blueprints/*/index.js',
        'config/**/*.js',
        'tests/dummy/config/**/*.js'
      ],
      excludedFiles: [
        'addon/**',
        'addon-test-support/**',
        'app/**',
        'tests/dummy/app/**'
      ],
      parserOptions: {
        sourceType: 'script'
      },
      env: {
        browser: false,
        node: true
      },
      plugins: ['node'],
      rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, {
        // add your custom rules and overrides for node files here
      })
    }
  ]
};
<|endoftext|>