<commit_before>// @flow

import invariant from 'invariant';
import _sum from 'lodash/fp/sum';
import * as React from 'react';
import {
  View,
  FlatList,
  Platform,
  TextInput,
  TouchableWithoutFeedback,
} 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,
  useFlattenedChatListData,
} 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,
  threadIsTopLevel,
} 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 SearchStatus = 'inactive' | 'activating' | 'active';
type State = {|
  +searchStatus: SearchStatus,
  +searchText: string,
  +threadsSearchResults: Set<string>,
  +usersSearchResults: $ReadOnlyArray<GlobalAccountUserInfo>,
  +openedSwipeableId: string,
|};
type PropsAndState = {| ...Props, ...State |};
class ChatThreadList extends React.PureComponent<Props, State> {
  state: State = {
    searchStatus: 'inactive',
    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);
  }

  componentDidUpdate(prevProps: Props, prevState: State) {
    const { flatList } = this;
    if (!flatList) {
      return;
    }
    const { searchStatus } = this.state;
    const prevSearchStatus = prevState.searchStatus;
    if (searchStatus === 'activating' && prevSearchStatus === 'inactive') {
      flatList.scrollToOffset({ offset: 0, animated: true });
    }
  }

  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 });
    }
  };

  onSearchFocus = () => {
    if (this.state.searchStatus !== 'inactive') {
      return;
    }
    if (this.scrollPos === 0) {
      this.setState({ searchStatus: 'active' });
    } else {
      this.setState({ searchStatus: 'activating' });
    }
  };

  onSearchBlur = () => {
    if (this.state.searchStatus !== 'active') {
      return;
    }
    const { flatList } = this;
    flatList && flatList.scrollToOffset({ offset: 0, animated: false });
    this.setState({ searchStatus: 'inactive' });
  };

  renderSearch(additionalProps?: $Shape<React.ElementConfig<typeof Search>>) {
    return (
      <View style={this.props.styles.searchContainer}>
        <Search
          searchText={this.state.searchText}
          onChangeText={this.onChangeSearchText}
          containerStyle={this.props.styles.search}
          onBlur={this.onSearchBlur}
          placeholder="Search threads"
          ref={this.searchInputRef}
          {...additionalProps}
        />
      </View>
    );
  }

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

  renderItem = (row: { item: Item }) => {
    const item = row.item;
    if (item.type === 'search') {
      return (
        <TouchableWithoutFeedback onPress={this.onSearchFocus}>
          {this.renderSearch({ active: false })}
        </TouchableWithoutFeedback>
      );
    }
    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}
      />
    );
  };

  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.searchStatus,
    (propsAndState: PropsAndState) => propsAndState.searchText,
    (propsAndState: PropsAndState) => propsAndState.threadsSearchResults,
    (propsAndState: PropsAndState) => propsAndState.emptyItem,
    (propsAndState: PropsAndState) => propsAndState.usersSearchResults,
    (
      reduxChatListData: $ReadOnlyArray<ChatThreadItem>,
      searchStatus: SearchStatus,
      searchText: string,
      threadsSearchResults: Set<string>,
      emptyItem?: React.ComponentType<{||}>,
      usersSearchResults: $ReadOnlyArray<GlobalAccountUserInfo>,
    ): Item[] => {
      const chatItems = [];

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

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

      if (searchStatus === 'inactive' || searchStatus === 'activating') {
        chatItems.unshift({ type: 'search', searchText });
      }

      return chatItems;
    },
  );

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

  render() {
    let floatingAction;
    if (Platform.OS === 'android') {
      floatingAction = (
        <FloatingAction
          actions={floatingActions}
          overrideWithAction
          onPressItem={this.composeThread}
          color="#66AA66"
        />
      );
    }
    let fixedSearch;
    const { searchStatus } = this.state;
    if (searchStatus === 'active') {
      fixedSearch = this.renderSearch({ autoFocus: true });
    }
    const scrollEnabled =
      searchStatus === 'inactive' || searchStatus === 'active';
    // this.props.viewerID is in extraData since it's used by MessagePreview
    // within ChatThreadListItem
    return (
      <View style={this.props.styles.container}>
        {fixedSearch}
        <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}
          scrollEnabled={scrollEnabled}
          removeClippedSubviews={true}
          ref={this.flatListRef}
        />
        {floatingAction}
      </View>
    );
  }

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

  onScroll = (event: { +nativeEvent: { +contentOffset: { +y: number } } }) => {
    const oldScrollPos = this.scrollPos;
    this.scrollPos = event.nativeEvent.contentOffset.y;
    if (this.scrollPos !== 0 || oldScrollPos === 0) {
      return;
    }
    if (this.state.searchStatus === 'activating') {
      this.setState({ searchStatus: 'active' });
    }
  };

  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({
            viewerID: this.props.viewerID,
            threadType: threadTypes.CHAT_SECRET,
          }),
          searching: true,
        },
      });
    }
  };
}

const unboundStyles = {
  icon: {
    fontSize: 28,
  },
  container: {
    flex: 1,
  },
  searchContainer: {
    backgroundColor: 'listBackground',
  },
  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 = useFlattenedChatListData();
  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] Don't render all threads in ChatThreadList<commit_after>// @flow

import invariant from 'invariant';
import _sum from 'lodash/fp/sum';
import * as React from 'react';
import {
  View,
  FlatList,
  Platform,
  TextInput,
  TouchableWithoutFeedback,
} 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,
  useFlattenedChatListData,
} 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,
  threadIsTopLevel,
} 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 SearchStatus = 'inactive' | 'activating' | 'active';
type State = {|
  +searchStatus: SearchStatus,
  +searchText: string,
  +threadsSearchResults: Set<string>,
  +usersSearchResults: $ReadOnlyArray<GlobalAccountUserInfo>,
  +openedSwipeableId: string,
  +numItemsToDisplay: number,
|};
type PropsAndState = {| ...Props, ...State |};
class ChatThreadList extends React.PureComponent<Props, State> {
  state: State = {
    searchStatus: 'inactive',
    searchText: '',
    threadsSearchResults: new Set(),
    usersSearchResults: [],
    openedSwipeableId: '',
    numItemsToDisplay: 25,
  };
  searchInput: ?React.ElementRef<typeof TextInput>;
  flatList: ?FlatList<Item>;
  scrollPos = 0;
  clearNavigationBlurListener: ?() => mixed;

  componentDidMount() {
    this.clearNavigationBlurListener = this.props.navigation.addListener(
      'blur',
      () => {
        this.setState({ numItemsToDisplay: 25 });
      },
    );

    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() {
    this.clearNavigationBlurListener && this.clearNavigationBlurListener();

    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);
  }

  componentDidUpdate(prevProps: Props, prevState: State) {
    const { flatList } = this;
    if (!flatList) {
      return;
    }

    if (this.state.searchText !== prevState.searchText) {
      flatList.scrollToOffset({ offset: 0, animated: false });
      return;
    }

    const { searchStatus } = this.state;
    const prevSearchStatus = prevState.searchStatus;
    if (searchStatus === 'activating' && prevSearchStatus === 'inactive') {
      flatList.scrollToOffset({ offset: 0, animated: true });
    }
  }

  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 });
    }
  };

  onSearchFocus = () => {
    if (this.state.searchStatus !== 'inactive') {
      return;
    }
    if (this.scrollPos === 0) {
      this.setState({ searchStatus: 'active' });
    } else {
      this.setState({ searchStatus: 'activating' });
    }
  };

  onSearchBlur = () => {
    if (this.state.searchStatus !== 'active') {
      return;
    }
    const { flatList } = this;
    flatList && flatList.scrollToOffset({ offset: 0, animated: false });
    this.setState({ searchStatus: 'inactive' });
  };

  renderSearch(additionalProps?: $Shape<React.ElementConfig<typeof Search>>) {
    return (
      <View style={this.props.styles.searchContainer}>
        <Search
          searchText={this.state.searchText}
          onChangeText={this.onChangeSearchText}
          containerStyle={this.props.styles.search}
          onBlur={this.onSearchBlur}
          placeholder="Search threads"
          ref={this.searchInputRef}
          {...additionalProps}
        />
      </View>
    );
  }

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

  renderItem = (row: { item: Item }) => {
    const item = row.item;
    if (item.type === 'search') {
      return (
        <TouchableWithoutFeedback onPress={this.onSearchFocus}>
          {this.renderSearch({ active: false })}
        </TouchableWithoutFeedback>
      );
    }
    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}
      />
    );
  };

  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.searchStatus,
    (propsAndState: PropsAndState) => propsAndState.searchText,
    (propsAndState: PropsAndState) => propsAndState.threadsSearchResults,
    (propsAndState: PropsAndState) => propsAndState.emptyItem,
    (propsAndState: PropsAndState) => propsAndState.usersSearchResults,
    (
      reduxChatListData: $ReadOnlyArray<ChatThreadItem>,
      searchStatus: SearchStatus,
      searchText: string,
      threadsSearchResults: Set<string>,
      emptyItem?: React.ComponentType<{||}>,
      usersSearchResults: $ReadOnlyArray<GlobalAccountUserInfo>,
    ): $ReadOnlyArray<Item> => {
      const chatItems = [];

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

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

      if (searchStatus === 'inactive' || searchStatus === 'activating') {
        chatItems.unshift({ type: 'search', searchText });
      }

      return chatItems;
    },
  );

  partialListDataSelector = createSelector(
    this.listDataSelector,
    (propsAndState: PropsAndState) => propsAndState.numItemsToDisplay,
    (items: $ReadOnlyArray<Item>, numItemsToDisplay: number) =>
      items.slice(0, numItemsToDisplay),
  );

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

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

  onEndReached = () => {
    if (this.listData.length === this.fullListData.length) {
      return;
    }
    this.setState((prevState) => ({
      numItemsToDisplay: prevState.numItemsToDisplay + 25,
    }));
  };

  render() {
    let floatingAction;
    if (Platform.OS === 'android') {
      floatingAction = (
        <FloatingAction
          actions={floatingActions}
          overrideWithAction
          onPressItem={this.composeThread}
          color="#66AA66"
        />
      );
    }
    let fixedSearch;
    const { searchStatus } = this.state;
    if (searchStatus === 'active') {
      fixedSearch = this.renderSearch({ autoFocus: true });
    }
    const scrollEnabled =
      searchStatus === 'inactive' || searchStatus === 'active';
    // this.props.viewerID is in extraData since it's used by MessagePreview
    // within ChatThreadListItem
    return (
      <View style={this.props.styles.container}>
        {fixedSearch}
        <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}
          scrollEnabled={scrollEnabled}
          removeClippedSubviews={true}
          onEndReached={this.onEndReached}
          onEndReachedThreshold={1}
          ref={this.flatListRef}
        />
        {floatingAction}
      </View>
    );
  }

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

  onScroll = (event: { +nativeEvent: { +contentOffset: { +y: number } } }) => {
    const oldScrollPos = this.scrollPos;
    this.scrollPos = event.nativeEvent.contentOffset.y;
    if (this.scrollPos !== 0 || oldScrollPos === 0) {
      return;
    }
    if (this.state.searchStatus === 'activating') {
      this.setState({ searchStatus: 'active' });
    }
  };

  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),
      numItemsToDisplay: 25,
    });
    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({
            viewerID: this.props.viewerID,
            threadType: threadTypes.CHAT_SECRET,
          }),
          searching: true,
        },
      });
    }
  };
}

const unboundStyles = {
  icon: {
    fontSize: 28,
  },
  container: {
    flex: 1,
  },
  searchContainer: {
    backgroundColor: 'listBackground',
  },
  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 = useFlattenedChatListData();
  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>var assert = require('assert');
var Instanet = require('../index');
var api = new Instanet('your InstanetToken');
var firstAgent;
var firstContact;
var firstTransaction;

describe('Test API Enpoints', function() {
	it('Get agent list', function(done) {
		api.getAgents()
		.then(function(agents) {
			assert.equal(Array.isArray(agents), true);
			assert.notEqual(agents.length, 0);
			firstAgent = agents[0];
			done();
		})
		.catch(done);
	});

	it('Get contact list', function(done) {
		api.getContacts(firstAgent.ID)
		.then(function(contacts) {
			console.log(contacts);
			assert.equal(Array.isArray(contacts), true);
			assert.notEqual(contacts.length, 0);
			firstContact = contacts[0];
			done();
		})
		.catch(done);
	});

	it('Get contact detail', function(done) {
		api.getContactDetails(firstContact.ID)
		.then(function(contact) {
			assert.notEqual(contact.ID, null);
			done();
		})
		.catch(done);
	});

	it('Get contact types', function(done) {
		api.getContactTypes(firstAgent.ID)
		.then(function(types) {
			assert.equal(Array.isArray(types), true);
			assert.notEqual(types.length, 0);
			done();
		})
		.catch(done);
	});

	it('Get transaction list', function(done) {
		api.getTransactions(firstAgent.ID)
		.then(function(transactions) {
			assert.equal(Array.isArray(transactions), true);
			assert.notEqual(transactions.length, 0);
			firstTransaction = transactions[0];
			done();
		})
		.catch(done);
	});

	it('Get transaction detail', function(done) {
		api.getTransactionDetails(firstTransaction.ID)
		.then(function(transaction) {
			assert.notEqual(transaction.ID, null);
			done();
		})
		.catch(done);
	});

	it('Get transaction types', function(done) {
		api.getTransactionTypes(firstAgent.ID)
		.then(function(types) {
			assert.equal(Array.isArray(types), true);
			assert.notEqual(types.length, 0);
			done();
		})
		.catch(done);
	});

	it('Create a new transaction', function(done) {
		var transaction = {
			'TransactionName': 'Test Transaction',
			'TransactionStatusName': 'Open',
			'TransactionTypeID': Instanet.transactionTypes.residentialListing,
			'PropertyInformation': {
				'MLSNumber': '',
				'StreetNumber': '123',
				'StreetName': 'Main St',
				'Address': '',
				'UnitNumber': '',
				'City': 'San Jose',
				'State': 'CA',
				'ZipCode': '95125',
				'County': 'Santa Clara',
				'Township': '',
				'TaxNumber': '',
				'Subdivision': '',
				'SchoolDistrict': 'San Jose Unified',
				'YearBuilt': '',
				'LotNumber': '',
				'Block': '',
				'PageNumber': '',
				'PlatBook': '',
				'ZoningClass': '',
				'LegalDescription': '',
				'ListPrice': '',
				'Balance1stMortgage': '',
				'Balance2ndMortgage': '',
				'OtherLiens': '',
				'OtherLiensDesc': '',
				'TotalEncumbrances': '',
				'PropertyIncludes': '',
				'PropertyExcludes': '',
				'LeasedItems': '',
				'SupplementalInfo': '',
				'Deposit': '',
				'AdditionalDeposit': '',
				'PurchasePrice': '',
				'TotalFinancing': '',
				'Comments': null
			},
			'TransactionDates': {
				'ListDate': '2015-12-25',
				'ExpirationDate': null,
				'ClosingDate': null,
				'OfferDate': null,
				'OfferExpirationDate': null,
				'OfferAcceptanceDate': null,
				'PossessionDate': null,
				'FundingDate': null,
				'ApplicationDate': null,
				'CommitmentDate': null
			}
		};

		api.createTransaction(firstAgent.ID, transaction)
		.then(function(inserted) {
			assert.notEqual(inserted.ID, null);
			done();
		})
		.catch(done);
	});

	it('Create a new contact', function(done) {
		var contact = {
			'ContactTypeID': Instanet.contactTypes.general,
			'FirstName': 'Test',
			'MiddleName': '',
			'LastName': 'Lead',
			'Email': '',
			'Company': '',
			'PreferredName': '',
			'PreferredSignature': '',
			'PreferredInitials': '',
			'StreetNumber': '',
			'StreetName': '',
			'City': '',
			'State': '',
			'ZipCode': '',
			'Phone': '',
			'Fax': ''
		}

		api.createContact(firstAgent.ID, contact)
		.then(function(inserted) {
			assert.notEqual(inserted.ID, null);
			done();
		})
		.catch(done);
	});
});
<commit_msg>remove debug code<commit_after>var assert = require('assert');
var Instanet = require('../index');
var api = new Instanet('your InstanetToken');
var firstAgent;
var firstContact;
var firstTransaction;

describe('Test API Enpoints', function() {
	it('Get agent list', function(done) {
		api.getAgents()
		.then(function(agents) {
			assert.equal(Array.isArray(agents), true);
			assert.notEqual(agents.length, 0);
			firstAgent = agents[0];
			done();
		})
		.catch(done);
	});

	it('Get contact list', function(done) {
		api.getContacts(firstAgent.ID)
		.then(function(contacts) {
			assert.equal(Array.isArray(contacts), true);
			assert.notEqual(contacts.length, 0);
			firstContact = contacts[0];
			done();
		})
		.catch(done);
	});

	it('Get contact detail', function(done) {
		api.getContactDetails(firstContact.ID)
		.then(function(contact) {
			assert.notEqual(contact.ID, null);
			done();
		})
		.catch(done);
	});

	it('Get contact types', function(done) {
		api.getContactTypes(firstAgent.ID)
		.then(function(types) {
			assert.equal(Array.isArray(types), true);
			assert.notEqual(types.length, 0);
			done();
		})
		.catch(done);
	});

	it('Get transaction list', function(done) {
		api.getTransactions(firstAgent.ID)
		.then(function(transactions) {
			assert.equal(Array.isArray(transactions), true);
			assert.notEqual(transactions.length, 0);
			firstTransaction = transactions[0];
			done();
		})
		.catch(done);
	});

	it('Get transaction detail', function(done) {
		api.getTransactionDetails(firstTransaction.ID)
		.then(function(transaction) {
			assert.notEqual(transaction.ID, null);
			done();
		})
		.catch(done);
	});

	it('Get transaction types', function(done) {
		api.getTransactionTypes(firstAgent.ID)
		.then(function(types) {
			assert.equal(Array.isArray(types), true);
			assert.notEqual(types.length, 0);
			done();
		})
		.catch(done);
	});

	it('Create a new transaction', function(done) {
		var transaction = {
			'TransactionName': 'Test Transaction',
			'TransactionStatusName': 'Open',
			'TransactionTypeID': Instanet.transactionTypes.residentialListing,
			'PropertyInformation': {
				'MLSNumber': '',
				'StreetNumber': '123',
				'StreetName': 'Main St',
				'Address': '',
				'UnitNumber': '',
				'City': 'San Jose',
				'State': 'CA',
				'ZipCode': '95125',
				'County': 'Santa Clara',
				'Township': '',
				'TaxNumber': '',
				'Subdivision': '',
				'SchoolDistrict': 'San Jose Unified',
				'YearBuilt': '',
				'LotNumber': '',
				'Block': '',
				'PageNumber': '',
				'PlatBook': '',
				'ZoningClass': '',
				'LegalDescription': '',
				'ListPrice': '',
				'Balance1stMortgage': '',
				'Balance2ndMortgage': '',
				'OtherLiens': '',
				'OtherLiensDesc': '',
				'TotalEncumbrances': '',
				'PropertyIncludes': '',
				'PropertyExcludes': '',
				'LeasedItems': '',
				'SupplementalInfo': '',
				'Deposit': '',
				'AdditionalDeposit': '',
				'PurchasePrice': '',
				'TotalFinancing': '',
				'Comments': null
			},
			'TransactionDates': {
				'ListDate': '2015-12-25',
				'ExpirationDate': null,
				'ClosingDate': null,
				'OfferDate': null,
				'OfferExpirationDate': null,
				'OfferAcceptanceDate': null,
				'PossessionDate': null,
				'FundingDate': null,
				'ApplicationDate': null,
				'CommitmentDate': null
			}
		};

		api.createTransaction(firstAgent.ID, transaction)
		.then(function(inserted) {
			assert.notEqual(inserted.ID, null);
			done();
		})
		.catch(done);
	});

	it('Create a new contact', function(done) {
		var contact = {
			'ContactTypeID': Instanet.contactTypes.general,
			'FirstName': 'Test',
			'MiddleName': '',
			'LastName': 'Lead',
			'Email': '',
			'Company': '',
			'PreferredName': '',
			'PreferredSignature': '',
			'PreferredInitials': '',
			'StreetNumber': '',
			'StreetName': '',
			'City': '',
			'State': '',
			'ZipCode': '',
			'Phone': '',
			'Fax': ''
		}

		api.createContact(firstAgent.ID, contact)
		.then(function(inserted) {
			assert.notEqual(inserted.ID, null);
			done();
		})
		.catch(done);
	});
});
<|endoftext|>


<commit_before>/**
 * Inheritance plugin
 *
 * Copyright (c) 2010 Filatov Dmitry (dfilatov@yandex-team.ru)
 * Dual licensed under the MIT and GPL licenses:
 * http://www.opensource.org/licenses/mit-license.php
 * http://www.gnu.org/licenses/gpl.html
 *
 * @version 1.3.5
 */(function(e){function a(n,r,s){var o=!1;if(i){var a=[];e.each(u,function(){s.hasOwnProperty(this)&&(o=!0)&&a.push({name:this,val:s[this]})}),o&&(e.each(s,function(e){a.push({name:e,val:this})}),s=a)}e.each(s,function(i,s){o&&(i=s.name,s=s.val);if(e.isFunction(s)&&(!t||s.toString().indexOf(".__base")>-1)){var u=n[i]||function(){};r[i]=function(){var e=this.__base;this.__base=u;var t=s.apply(this,arguments);return this.__base=e,t}}else r[i]=s})}var t=function(){_}.toString().indexOf("_")>-1,n=function(){},r=Object.create||function(e){var t=function(){};return t.prototype=e,new t},i=!0,s={toString:""};for(var o in s)s.hasOwnProperty(o)&&(i=!1);var u=i?["toString","valueOf"]:null;e.inherit=function(){var t=arguments,i=e.isFunction(t[0]),s=i?t[0]:n,o=t[i?1:0]||{},u=t[i?2:1],f=o.__constructor||i&&s.prototype.__constructor?function(){return this.__constructor.apply(this,arguments)}:function(){};if(!i)return f.prototype=o,f.prototype.__self=f.prototype.constructor=f,e.extend(f,u);e.extend(f,s);var l=s.prototype,c=f.prototype=r(l);return c.__self=c.constructor=f,a(l,c,o),u&&a(s,f,u),f},e.inheritSelf=function(e,t,n){var r=e.prototype;return a(r,r,t),n&&a(e,e,n),e}})(jQuery);<commit_msg>Delete inheritance plugin<commit_after><|endoftext|>


<commit_before>// @flow
import React from 'react'
import LoadingView from '../components/loading'
import qs from 'querystring'
import {NoticeView} from '../components/notice'
import type {TopLevelViewPropsType} from '../types'
import {FancyMenu} from './components/fancy-menu'
import type {BonAppMenuInfoType, BonAppCafeInfoType} from './types'
import sample from 'lodash/sample'
import values from 'lodash/values'
import type momentT from 'moment'
import moment from 'moment-timezone'
const CENTRAL_TZ = 'America/Winnipeg'
import {findMenu} from './lib/find-menu'

const bonappMenuBaseUrl = 'http://legacy.cafebonappetit.com/api/2/menus'
const bonappCafeBaseUrl = 'http://legacy.cafebonappetit.com/api/2/cafes'
const fetchJsonQuery = (url, query) => fetchJson(`${url}?${qs.stringify(query)}`)

export class BonAppHostedMenu extends React.Component {
  state: {
    error: ?Error,
    loading: boolean,
    now: momentT,
    cafeInfo: ?BonAppCafeInfoType,
    cafeMenu: ?BonAppMenuInfoType,
  } = {
    error: null,
    loading: true,
    now: moment.tz(CENTRAL_TZ),
    cafeMenu: null,
    cafeInfo: null,
  }

  componentWillMount() {
    this.fetchData()
  }

  props: TopLevelViewPropsType & {
    cafeId: string,
    loadingMessage: string[],
    name: string,
  }

  fetchData = async () => {
    this.setState({loading: true})

    let cafeMenu = null
    let cafeInfo = null

    try {
      let requests = await Promise.all([
        fetchJsonQuery(bonappMenuBaseUrl, {cafe: this.props.cafeId}),
        fetchJsonQuery(bonappCafeBaseUrl, {cafe: this.props.cafeId}),
      ])
      cafeMenu = (requests[0]: BonAppMenuInfoType)
      cafeInfo = (requests[1]: BonAppCafeInfoType)
    } catch (err) {
      this.setState({error: err})
    }

    this.setState({loading: false, cafeMenu, cafeInfo, now: moment.tz(CENTRAL_TZ)})
  }

  render() {
    if (this.state.loading) {
      return <LoadingView text={sample(this.props.loadingMessage)} />
    }

    if (this.state.error) {
      return <NoticeView text={'Error: ' + this.state.error.message} />
    }

    if (!this.state.cafeMenu || !this.state.cafeInfo) {
      return <NoticeView text='Something went wrong. Email odt@stolaf.edu to let them know?' />
    }

    let {cafeId} = this.props
    let {now, cafeMenu, cafeInfo} = this.state

    // We grab the "today" info from here because BonApp returns special
    // messages in this response, like "Closed for Christmas Break"
    let days = cafeInfo.cafes[cafeId].days
    let today = days.find(({date}) => date === now.format('YYYY-MM-DD'))
    if (!today || today.status === 'closed') {
      return <NoticeView text={today ? today.message : 'Closed today'} />
    }

    // We hard-code to the first day returned because we're only requesting one day.
    // `cafes` is a map of cafe ids to cafes, but we only request one at a time.
    let dayparts = cafeMenu.days[0].cafes[cafeId].dayparts
    let mealInfo = findMenu(dayparts, now)
    let mealName = mealInfo ? mealInfo.label : ''
    let stationMenus = mealInfo ? mealInfo.stations : []

    return (
      <FancyMenu
        route={this.props.route}
        navigator={this.props.navigator}
        foodItems={values(cafeMenu.items)}
        menuCorIcons={cafeMenu.cor_icons}
        menuLabel={mealName}
        now={now}
        name={this.props.name}
        stationMenus={stationMenus}
      />
    )
  }
}
<commit_msg>extract find-cafe-message code into a function<commit_after>// @flow
import React from 'react'
import LoadingView from '../components/loading'
import qs from 'querystring'
import {NoticeView} from '../components/notice'
import type {TopLevelViewPropsType} from '../types'
import {FancyMenu} from './components/fancy-menu'
import type {BonAppMenuInfoType, BonAppCafeInfoType} from './types'
import sample from 'lodash/sample'
import values from 'lodash/values'
import type momentT from 'moment'
import moment from 'moment-timezone'
const CENTRAL_TZ = 'America/Winnipeg'
import {findMenu} from './lib/find-menu'

const bonappMenuBaseUrl = 'http://legacy.cafebonappetit.com/api/2/menus'
const bonappCafeBaseUrl = 'http://legacy.cafebonappetit.com/api/2/cafes'
const fetchJsonQuery = (url, query) => fetchJson(`${url}?${qs.stringify(query)}`)

export class BonAppHostedMenu extends React.Component {
  state: {
    error: ?Error,
    loading: boolean,
    now: momentT,
    cafeInfo: ?BonAppCafeInfoType,
    cafeMenu: ?BonAppMenuInfoType,
  } = {
    error: null,
    loading: true,
    now: moment.tz(CENTRAL_TZ),
    cafeMenu: null,
    cafeInfo: null,
  }

  componentWillMount() {
    this.fetchData()
  }

  props: TopLevelViewPropsType & {
    cafeId: string,
    loadingMessage: string[],
    name: string,
  }

  fetchData = async () => {
    this.setState({loading: true})

    let cafeMenu = null
    let cafeInfo = null

    try {
      let requests = await Promise.all([
        fetchJsonQuery(bonappMenuBaseUrl, {cafe: this.props.cafeId}),
        fetchJsonQuery(bonappCafeBaseUrl, {cafe: this.props.cafeId}),
      ])
      cafeMenu = (requests[0]: BonAppMenuInfoType)
      cafeInfo = (requests[1]: BonAppCafeInfoType)
    } catch (err) {
      this.setState({error: err})
    }

    this.setState({loading: false, cafeMenu, cafeInfo, now: moment.tz(CENTRAL_TZ)})
  }

  findCafeMessage = (cafeId: string, cafeInfo: BonAppCafeInfoType, now: momentT) => {
    let actualCafeInfo = cafeInfo.cafes[cafeId]
    if (!actualCafeInfo) {
      return 'BonApp did not return a menu for that café'
    }

    let today = actualCafeInfo.days.find(({date}) => date === now.format('YYYY-MM-DD'))
    if (!today) {
      return 'Closed today'
    } else if (today.status === 'closed') {
      return today.message || 'Closed today'
    }

    return null
  }

  render() {
    if (this.state.loading) {
      return <LoadingView text={sample(this.props.loadingMessage)} />
    }

    if (this.state.error) {
      return <NoticeView text={'Error: ' + this.state.error.message} />
    }

    if (!this.state.cafeMenu || !this.state.cafeInfo) {
      return <NoticeView text='Something went wrong. Email odt@stolaf.edu to let them know?' />
    }

    let {cafeId} = this.props
    let {now, cafeMenu, cafeInfo} = this.state

    // We grab the "today" info from here because BonApp returns special
    // messages in this response, like "Closed for Christmas Break"
    let specialMessage = this.findCafeMessage(cafeId, cafeInfo, now)
    if (specialMessage) {
      return <NoticeView text={specialMessage} />
    }

    // We hard-code to the first day returned because we're only requesting one day.
    // `cafes` is a map of cafe ids to cafes, but we only request one at a time.
    let dayparts = cafeMenu.days[0].cafes[cafeId].dayparts
    let mealInfo = findMenu(dayparts, now)
    let mealName = mealInfo ? mealInfo.label : ''
    let stationMenus = mealInfo ? mealInfo.stations : []

    return (
      <FancyMenu
        route={this.props.route}
        navigator={this.props.navigator}
        foodItems={values(cafeMenu.items)}
        menuCorIcons={cafeMenu.cor_icons}
        menuLabel={mealName}
        now={now}
        name={this.props.name}
        stationMenus={stationMenus}
      />
    )
  }
}
<|endoftext|>


<commit_before>define([
	'base/js/namespace',
	'base/js/events',
	'notebook/js/codecell'
], function (
	Jupyter,
	events,
	codecell
){
	'use strict';

	var CodeCell = codecell.CodeCell;

	function patch_CodeCell_execute () {
		console.log('[Freeze] patching CodeCell.prototype.execute')
		var old_execute = CodeCell.prototype.execute;

		CodeCell.prototype.execute = function () {
			if (!this.metadata.run_control.frozen) {
				old_execute.apply(this, arguments);
			}
		}
	}

	function set_state(cell, state) {
		if (cell instanceof CodeCell) {
			if (cell.metadata.run_control == undefined)
				cell.metadata.run_control = {};
			if (state == undefined)
				state = 'normal';
			switch(state) {
				case 'normal':
					cell.metadata.run_control.frozen = false;
					cell.metadata.run_control.read_only = false;
					var bg = "#f5f5f5";
					break;
				case 'read_only':
					cell.metadata.run_control.frozen = false;
					cell.metadata.run_control.read_only = true;
					var bg = "#FFFEF0";
					break;
				case 'frozen':
					cell.metadata.run_control.frozen = true;
					cell.metadata.run_control.read_only = true;
					var bg = "#f0feff";
					break;
			}
			cell.code_mirror.setOption('readOnly', cell.metadata.run_control.read_only);
			var prompt = cell.element.find('div.input_area');
			prompt.css("background-color", bg);
		}
	}

	function set_state_selected (state) {
		var cells = Jupyter.notebook.get_selected_cells();
		for (var i = 0; i < cells.length; i++) {
			set_state(cells[i], state)
		}
	}

	function make_normal_selected () {
		set_state_selected('normal');
	}

	function make_read_only_selected () {
		set_state_selected('read_only');
	}

	function make_frozen_selected () {
		set_state_selected('frozen');
	}

	function initialize_states () {
		var cells = Jupyter.notebook.get_cells();
		for (var i in cells) {
			var cell = cells[i];
			if (cell instanceof CodeCell) {
				if (cell.metadata.run_control != undefined) {
					if (cell.metadata.run_control.read_only) {
						if (cell.metadata.run_control.frozen) {
							set_state(cell, 'frozen')
						} else {
							set_state(cell, 'read_only')
						}
					} else {
						set_state(cell, 'normal')
					}
				} else {
					set_state(cell, 'normal');
				}
			}
		}
	}

	function load_extension () {
		Jupyter.toolbar.add_buttons_group([
			{
				id : 'make_normal',
				label : 'lift restrictions from selected cells',
				icon : 'fa-unlock-alt',
				callback : make_normal_selected
			},
			{
				id : 'make_read_only',
				label : 'make selected cells read-only',
				icon: 'fa-lock',
				callback : make_read_only_selected
			},
			{
				id : 'freeze_cells',
				label : 'freeze selected cells',
				icon : 'fa-asterisk',
				callback : make_frozen_selected
			}
		]);

		initialize_states();

		patch_CodeCell_execute();
	}

	return {
		load_jupyter_extension : load_extension,
		load_ipython_extension : load_extension
	}
});
<commit_msg>[Freeze] Change background color to default for 'normal' cells<commit_after>define([
	'base/js/namespace',
	'base/js/events',
	'notebook/js/codecell'
], function (
	Jupyter,
	events,
	codecell
){
	'use strict';

	var CodeCell = codecell.CodeCell;

	function patch_CodeCell_execute () {
		console.log('[Freeze] patching CodeCell.prototype.execute')
		var old_execute = CodeCell.prototype.execute;

		CodeCell.prototype.execute = function () {
			if (!this.metadata.run_control.frozen) {
				old_execute.apply(this, arguments);
			}
		}
	}

	function set_state(cell, state) {
		if (cell instanceof CodeCell) {
			if (cell.metadata.run_control == undefined)
				cell.metadata.run_control = {};
			if (state == undefined)
				state = 'normal';
			switch(state) {
				case 'normal':
					cell.metadata.run_control.frozen = false;
					cell.metadata.run_control.read_only = false;
					var bg = "";
					break;
				case 'read_only':
					cell.metadata.run_control.frozen = false;
					cell.metadata.run_control.read_only = true;
					var bg = "#FFFEF0";
					break;
				case 'frozen':
					cell.metadata.run_control.frozen = true;
					cell.metadata.run_control.read_only = true;
					var bg = "#f0feff";
					break;
			}
			cell.code_mirror.setOption('readOnly', cell.metadata.run_control.read_only);
			var prompt = cell.element.find('div.input_area');
			prompt.css("background-color", bg);
		}
	}

	function set_state_selected (state) {
		var cells = Jupyter.notebook.get_selected_cells();
		for (var i = 0; i < cells.length; i++) {
			set_state(cells[i], state)
		}
	}

	function make_normal_selected () {
		set_state_selected('normal');
	}

	function make_read_only_selected () {
		set_state_selected('read_only');
	}

	function make_frozen_selected () {
		set_state_selected('frozen');
	}

	function initialize_states () {
		var cells = Jupyter.notebook.get_cells();
		for (var i in cells) {
			var cell = cells[i];
			if (cell instanceof CodeCell) {
				if (cell.metadata.run_control != undefined) {
					if (cell.metadata.run_control.read_only) {
						if (cell.metadata.run_control.frozen) {
							set_state(cell, 'frozen')
						} else {
							set_state(cell, 'read_only')
						}
					} else {
						set_state(cell, 'normal')
					}
				} else {
					set_state(cell, 'normal');
				}
			}
		}
	}

	function load_extension () {
		Jupyter.toolbar.add_buttons_group([
			{
				id : 'make_normal',
				label : 'lift restrictions from selected cells',
				icon : 'fa-unlock-alt',
				callback : make_normal_selected
			},
			{
				id : 'make_read_only',
				label : 'make selected cells read-only',
				icon: 'fa-lock',
				callback : make_read_only_selected
			},
			{
				id : 'freeze_cells',
				label : 'freeze selected cells',
				icon : 'fa-asterisk',
				callback : make_frozen_selected
			}
		]);

		initialize_states();

		patch_CodeCell_execute();
	}

	return {
		load_jupyter_extension : load_extension,
		load_ipython_extension : load_extension
	}
});
<|endoftext|>


<commit_before>const test = require('ava');
const random = require('./helpers/random');
const all = require('./products');

test('all()', t => {
  t.true(all.length < 0);
});

test('random()', t => {
  t.truthy(random(all));
  t.not(random(all), random(all));
});
<commit_msg>🤖(fix): 2017-7-3 17:43:59<commit_after>const test = require('ava');
const random = require('./helpers/random');
const all = require('./products');

test('all()', t => {
  t.true(all.length > 0);
});

test('random()', t => {
  t.truthy(random(all));
  t.not(random(all), random(all));
});
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import AssetUploadButton from './AssetUploadButton.js'
import Studio, { TextEditor } from 'jsreport-studio'
import superagent from 'superagent'
import Promise from 'bluebird'
import CopyToClipboard from 'react-copy-to-clipboard'

// Studio.api currently always open dialogs on failures and that is what we don't want, so arbitrary implementaiton here
const getTextFromApi = (path) => {
  return new Promise((resolve, reject) => {
    const request = superagent.get(Studio.resolveUrl(path))
    request.end((err, { text } = {}) => err ? reject(new Error(text || err.toString())) : resolve(text))
  })
}

export default class FileEditor extends Component {
  constructor () {
    super()
    this.state = { }
  }

  async componentDidMount () {
    const { entity } = this.props

    var content = entity.content
    if (entity.link) {
      try {
        await Studio.saveEntity(entity._id)
        const ab = await Studio.api.get(`assets/content/${entity.name}`, { responseType: 'arraybuffer' })
        const str = String.fromCharCode.apply(null, new Uint8Array(ab))
        const fixedStr = decodeURIComponent(escape(str))
        content = btoa(unescape(encodeURIComponent(fixedStr)))
      } catch (e) {
        this.setState({ loadError: e })
        throw e
      }
    }

    this.setState({ content: content })
  }

  async componentDidUpdate (prevProps) {
    const { entity } = this.props

    if (entity.link && (!this.state.link || prevProps.entity.link !== entity.link) && !this.state.loadFailed) {
      try {
        const link = await getTextFromApi(`assets/link/${entity.link}`)
        this.setState({ link: link })
      } catch (e) {
        this.setState({ link: e.message })
      }
    }
  }

  isImage (entity) {
    return entity.name.match(/\.(jpeg|jpg|gif|png|svg)$/) != null
  }

  isFont (entity) {
    return entity.name.match(/\.(otf|woff|ttf|eot|woff2)$/) != null
  }

  getFormat (extension) {
    switch (extension) {
      case 'ttf': return 'truetype'
      case 'woff2': return 'woff2'
      case 'eot': return 'embedded-opentype'
      default: return 'woff'
    }
  }

  getEmbeddingCode (entity) {
    let parts = entity.name.split('.')
    let extension = parts[parts.length - 1]

    if (this.isImage(entity)) {
      return ''
    }

    if (this.isFont(entity)) {
      return `@font-face {
  font-family: '${parts[0]}';
  src: url({#asset ${entity.name} @encoding=dataURI});
  format('${this.getFormat(extension)}');
}`
    }

    return '{#asset ' + entity.name + ' @encoding=utf8}'
  }

  renderEditor (entity) {
    let parts = entity.name.split('.')
    let extension = parts[parts.length - 1]

    if (this.isImage(entity)) {
      return
<img src={Studio.resolveUrl(`assets/content/${entity.name}?v=${new Date().getTime()}`)}
        style={{display: 'block', margin: '3rem auto'}} />

    }

    if (this.isFont(entity)) {
      let newStyle = document.createElement('style')
      newStyle.appendChild(document.createTextNode(`@font-face {
         font-family: '${parts[0]}';
         src: url('${Studio.resolveUrl('/assets/content/' + entity.name)}');
         format('${extension === 'ttf' ? 'truetype' : 'woff'}');
        }`))

      document.head.appendChild(newStyle)

      return

 Hello world font {entity.name}

          Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's
          standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to
          make a type specimen book.

    }

    let mode = parts[parts.length - 1]
    if (extension === 'js') {
      mode = 'javascript'
    }
    if (extension === 'html') {
      mode = 'handlebars'
    }

    const content = ((entity.content || entity.forceUpdate) ? entity.content : this.state.content) || ''

    return <TextEditor
      name={entity._id}
      mode={mode}
      value={decodeURIComponent(escape(atob(content)))}
      onUpdate={(v) => this.props.onUpdate(Object.assign({}, entity, {content: btoa(unescape(encodeURIComponent(v))), forceUpdate: true}))} />
  }

  render () {
    const { entity } = this.props
    const { link } = this.state
    const downloadUrl = Studio.resolveUrl(`assets/content/${entity.name}?download=true`)

    const toolbarButtonStyle = {
      color: '#007ACC',
      fontSize: '1.2rem'
    }

    return (

          <CopyToClipboard text={this.getEmbeddingCode(entity)}>

          </CopyToClipboard>

          {entity.link
            ? {link} :  AssetUploadButton.OpenUpload()}>}

      {this.renderEditor(entity)}

)
  }
}
<commit_msg>support uploading pdfs<commit_after>import React, { Component } from 'react'
import AssetUploadButton from './AssetUploadButton.js'
import Studio, { TextEditor } from 'jsreport-studio'
import superagent from 'superagent'
import Promise from 'bluebird'
import CopyToClipboard from 'react-copy-to-clipboard'

// Studio.api currently always open dialogs on failures and that is what we don't want, so arbitrary implementaiton here
const getTextFromApi = (path) => {
  return new Promise((resolve, reject) => {
    const request = superagent.get(Studio.resolveUrl(path))
    request.end((err, { text } = {}) => err ? reject(new Error(text || err.toString())) : resolve(text))
  })
}

export default class FileEditor extends Component {
  constructor () {
    super()
    this.state = { }
  }

  async componentDidMount () {
    const { entity } = this.props

    var content = entity.content
    if (entity.link) {
      try {
        await Studio.saveEntity(entity._id)
        const ab = await Studio.api.get(`assets/content/${entity.name}`, { responseType: 'arraybuffer' })
        const str = String.fromCharCode.apply(null, new Uint8Array(ab))
        const fixedStr = decodeURIComponent(escape(str))
        content = btoa(unescape(encodeURIComponent(fixedStr)))
      } catch (e) {
        this.setState({ loadError: e })
        throw e
      }
    }

    this.setState({ content: content })
  }

  async componentDidUpdate (prevProps) {
    const { entity } = this.props

    if (entity.link && (!this.state.link || prevProps.entity.link !== entity.link) && !this.state.loadFailed) {
      try {
        const link = await getTextFromApi(`assets/link/${entity.link}`)
        this.setState({ link: link })
      } catch (e) {
        this.setState({ link: e.message })
      }
    }
  }

  isImage (entity) {
    return entity.name.match(/\.(jpeg|jpg|gif|png|svg)$/) != null
  }

  isFont (entity) {
    return entity.name.match(/\.(otf|woff|ttf|eot|woff2)$/) != null
  }

  isPdf (entity) {
    return entity.name.match(/\.(pdf)$/) != null
  }

  getFormat (extension) {
    switch (extension) {
      case 'ttf': return 'truetype'
      case 'woff2': return 'woff2'
      case 'eot': return 'embedded-opentype'
      default: return 'woff'
    }
  }

  getEmbeddingCode (entity) {
    let parts = entity.name.split('.')
    let extension = parts[parts.length - 1]

    if (this.isImage(entity)) {
      return ''
    }

    if (this.isFont(entity)) {
      return `@font-face {
  font-family: '${parts[0]}';
  src: url({#asset ${entity.name} @encoding=dataURI});
  format('${this.getFormat(extension)}');
}`
    }

    return '{#asset ' + entity.name + ' @encoding=utf8}'
  }

  renderEditor (entity) {
    let parts = entity.name.split('.')
    let extension = parts[parts.length - 1]

    if (this.isImage(entity)) {
      return
<img src={Studio.resolveUrl(`assets/content/${entity.name}?v=${new Date().getTime()}`)}
        style={{display: 'block', margin: '3rem auto'}} />

    }

    if (this.isFont(entity)) {
      let newStyle = document.createElement('style')
      newStyle.appendChild(document.createTextNode(`@font-face {
         font-family: '${parts[0]}';
         src: url('${Studio.resolveUrl('/assets/content/' + entity.name)}');
         format('${extension === 'ttf' ? 'truetype' : 'woff'}');
        }`))

      document.head.appendChild(newStyle)

      return

 Hello world font {entity.name}

          Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's
          standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to
          make a type specimen book.

    }

    if (this.isPdf(entity)) {
      return
<object style={{height: '100%'}} data={Studio.resolveUrl(`assets/content/${entity.name}?v=${new Date().getTime()}`)} type='application/pdf'>
        <embed src={Studio.resolveUrl(`assets/content/${entity.name}?v=${new Date().getTime()}`)} type='application/pdf' />
      </object>

    }

    let mode = parts[parts.length - 1]
    if (extension === 'js') {
      mode = 'javascript'
    }
    if (extension === 'html') {
      mode = 'handlebars'
    }

    const content = ((entity.content || entity.forceUpdate) ? entity.content : this.state.content) || ''

    return <TextEditor
      name={entity._id}
      mode={mode}
      value={decodeURIComponent(escape(atob(content)))}
      onUpdate={(v) => this.props.onUpdate(Object.assign({}, entity, {content: btoa(unescape(encodeURIComponent(v))), forceUpdate: true}))} />
  }

  render () {
    const { entity } = this.props
    const { link } = this.state
    const downloadUrl = Studio.resolveUrl(`assets/content/${entity.name}?download=true`)

    const toolbarButtonStyle = {
      color: '#007ACC',
      fontSize: '1.2rem'
    }

    return (

          <CopyToClipboard text={this.getEmbeddingCode(entity)}>

          </CopyToClipboard>

          {entity.link
            ? {link} :  AssetUploadButton.OpenUpload()}>}

      {this.renderEditor(entity)}

)
  }
}
<|endoftext|>


<commit_before>// Copyright 2015, Google, Inc.
// Licensed under the Apache License, Version 2.0 (the "License")
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

'use strict';

var CV_URL = 'https://vision.googleapis.com/v1/images:annotate?key=' + window.apiKey;

$(document).ready(function () {
  $('#fileform').on('submit', uploadFiles);
});

/**
 * 'submit' event handler - reads the image bytes and sends it to the Cloud
 * Vision API.
 */
function uploadFiles (event) {
  event.preventDefault(); // Prevent the default form post

  // Grab the file and asynchronously convert to base64.
  var file = $('#fileform [name=fileField]')[0].files[0];
  var reader = new FileReader();
  reader.onloadend = processFile;
  reader.readAsDataURL(file);
}

/**
 * Event handler for a file's data url - extract the image data and pass it off.
 */
function processFile (event) {
  var content = event.target.result;
  sendFileToCloudVision(content.replace('data:image/jpeg;base64,', ''));
}

/**
 * Sends the given file contents to the Cloud Vision API and outputs the
 * results.
 */
function sendFileToCloudVision (content) {
  var type = $('#fileform [name=type]').val();

  // Strip out the file prefix when you convert to json.
  var request = {
    requests: [{
      image: {
        content: content
      },
      features: [{
        type: type,
        maxResults: 200
      }]
    }]
  };

  $('#results').text('Loading...');
  $.post({
    url: CV_URL,
    data: JSON.stringify(request),
    contentType: 'application/json'
  }).fail(function (jqXHR, textStatus, errorThrown) {
    $('#results').text('ERRORS: ' + textStatus + ' ' + errorThrown);
  }).done(displayJSON);
}

/**
 * Displays the results.
 */
function displayJSON (data) {
  var contents = JSON.stringify(data, null, 4);
  $('#results').text(contents);
  var evt = new Event('results-displayed');
  evt.results = contents;
  document.dispatchEvent(evt);
}
<commit_msg>removes unnecessary document.ready wrapper<commit_after>// Copyright 2015, Google, Inc.
// Licensed under the Apache License, Version 2.0 (the "License")
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

'use strict';

var CV_URL = 'https://vision.googleapis.com/v1/images:annotate?key=' + window.apiKey;

$(function () {
  $('#fileform').on('submit', uploadFiles);
});

/**
 * 'submit' event handler - reads the image bytes and sends it to the Cloud
 * Vision API.
 */
function uploadFiles (event) {
  event.preventDefault(); // Prevent the default form post

  // Grab the file and asynchronously convert to base64.
  var file = $('#fileform [name=fileField]')[0].files[0];
  var reader = new FileReader();
  reader.onloadend = processFile;
  reader.readAsDataURL(file);
}

/**
 * Event handler for a file's data url - extract the image data and pass it off.
 */
function processFile (event) {
  var content = event.target.result;
  sendFileToCloudVision(content.replace('data:image/jpeg;base64,', ''));
}

/**
 * Sends the given file contents to the Cloud Vision API and outputs the
 * results.
 */
function sendFileToCloudVision (content) {
  var type = $('#fileform [name=type]').val();

  // Strip out the file prefix when you convert to json.
  var request = {
    requests: [{
      image: {
        content: content
      },
      features: [{
        type: type,
        maxResults: 200
      }]
    }]
  };

  $('#results').text('Loading...');
  $.post({
    url: CV_URL,
    data: JSON.stringify(request),
    contentType: 'application/json'
  }).fail(function (jqXHR, textStatus, errorThrown) {
    $('#results').text('ERRORS: ' + textStatus + ' ' + errorThrown);
  }).done(displayJSON);
}

/**
 * Displays the results.
 */
function displayJSON (data) {
  var contents = JSON.stringify(data, null, 4);
  $('#results').text(contents);
  var evt = new Event('results-displayed');
  evt.results = contents;
  document.dispatchEvent(evt);
}
<|endoftext|>


<commit_before>(function () {
  'use strict';

  var ColorPalette = {
    'Blue': '#4572A7',
    'Red': '#AA4643',
    'Green': '#89A54E',
    'Purple': '#80699B',
    'Cyan': '#3D96AE',
    'Orange': '#DB843D',
    'Light Blue': '#92A8CD',
    'Lilac': '#A47D7C',
    'Light Green': '#B5CA92',
    'Brown': '#A52A2A',
    'Black': '#000000',
    'Gray': '#808080',
    'Pink': '#FFC0CB',
    'Dark Blue': '#00008b'
  };
  var ColorPaletteArray = _.values(ColorPalette)

  var fillXValues = function(seriesList) {
    var xValues = _.uniq(_.flatten(_.pluck(seriesList, 'x')));
    xValues.sort();
    _.each(seriesList, function(series) {
      series.x.sort();
      _.each(xValues, function(value, index) {
        if (series.x[index] != value) {
          series.x.splice(index, 0, value);
          series.y.splice(index, 0, 0);
        }
      });
    });
  }

  var normalAreaStacking = function(seriesList) {
    fillXValues(seriesList);
    _.each(seriesList, function(series) {
      series.text = [];
      series.hoverinfo = 'text+name';
    });
    for (var i = 0; i < seriesList.length; i++) {
      for (var j = 0; j < seriesList[i].y.length; j++) {
        var sum = i > 0 ? seriesList[i-1].y[j] : 0;
        seriesList[i].text.push('Value: ' + seriesList[i].y[j] + '
Sum: ' + (sum + seriesList[i].y[j]));
        seriesList[i].y[j] += sum;
      }
    }
  }

  var percentAreaStacking = function(seriesList) {
    if (seriesList.length == 0)
      return;
    fillXValues(seriesList);
    _.each(seriesList, function(series) {
      series.text = [];
      series.hoverinfo = 'text+name';
    });
    for (var i = 0; i < seriesList[0].y.length; i++) {
      var sum = 0;
      for(var j = 0; j < seriesList.length; j++) {
        sum += seriesList[j].y[i];
      }
      for(var j = 0; j < seriesList.length; j++) {
        var value = seriesList[j].y[i] / sum * 100;
        seriesList[j].text.push('Value: ' + seriesList[j].y[i] + '
Relative: ' + value.toFixed(2) + '%');

        seriesList[j].y[i] = value;
        if (j > 0)
          seriesList[j].y[i] += seriesList[j-1].y[i];
      }
    }
  }

  var percentBarStacking = function(seriesList) {
    if (seriesList.length == 0)
      return;
    fillXValues(seriesList);
    _.each(seriesList, function(series) {
      series.text = [];
      series.hoverinfo = 'text+name';
    });
    for (var i = 0; i < seriesList[0].y.length; i++) {
      var sum = 0;
      for(var j = 0; j < seriesList.length; j++) {
        sum += seriesList[j].y[i];
      }
      for(var j = 0; j < seriesList.length; j++) {
        var value = seriesList[j].y[i] / sum * 100;
        seriesList[j].text.push('Value: ' + seriesList[j].y[i] + '
Relative: ' + value.toFixed(2) + '%');
        seriesList[j].y[i] = value;
      }
    }
  }

  var normalizeValue = function(value) {
    if (moment.isMoment(value)) {
      return value.format("YYYY-MM-DD HH:MM:SS.ssssss");
    }
    return value;
  }

  angular.module('plotly-chart', [])
    .constant('ColorPalette', ColorPalette)
    .directive('plotlyChart', function () {
      return {
        restrict: 'E',
        template: '<plotly data="data" layout="layout" options="plotlyOptions"></plotly>',
        scope: {
          options: "=",
          series: "=",
          minHeight: "="
        },
        link: function (scope, element, attrs) {
          var getScaleType = function(scale) {
            if (scale == 'datetime')
              return 'date';
            if (scale == 'logarithmic')
              return 'log';
            return scale;
          }

          var setType = function(series, type) {
            if (type == 'column') {
              series['type'] = 'bar';
            } else  if (type == 'line') {
              series['mode'] = 'lines';
            } else if (type == 'area') {
              series['fill'] = scope.options.series.stacking == null ? 'tozeroy' : 'tonexty';
              series['mode'] = 'lines';
            } else if (type == 'scatter') {
              series['type'] = 'scatter';
              series['mode'] = 'markers';
            }
          }

          var getColor = function(index) {
            return ColorPaletteArray[index % ColorPaletteArray.length];
          }

          var bottomMargin = 50,
              pixelsPerLegendRow = 21;
          var redraw = function() {
            scope.data.length = 0;
            scope.layout.showlegend = _.has(scope.options, 'legend') ? scope.options.legend.enabled : true;
            scope.layout.height = Math.max(scope.minHeight, pixelsPerLegendRow * scope.series.length);
            scope.layout.margin.b = scope.layout.height - (scope.minHeight - bottomMargin) ;
            delete scope.layout.barmode;
            delete scope.layout.xaxis;
            delete scope.layout.yaxis;
            delete scope.layout.yaxis2;

            if (scope.options.globalSeriesType == 'pie') {
              var hasX = _.contains(_.values(scope.options.columnMapping), 'x');
              var rows = scope.series.length > 2 ? 2 : 1;
              var cellsInRow = Math.ceil(scope.series.length / rows)
              var cellWidth = 1 / cellsInRow;
              var cellHeight = 1 / rows;
              var xPadding = 0.02;
              var yPadding = 0.05;
              _.each(scope.series, function(series, index) {
                var xPosition = (index % cellsInRow) * cellWidth;
                var yPosition = Math.floor(index / cellsInRow) * cellHeight;
                var plotlySeries = {values: [], labels: [], type: 'pie', hole: .4,
                                    text: series.name, textposition: 'inside', name: series.name,
                                    domain: {x: [xPosition, xPosition + cellWidth - xPadding],
                                             y: [yPosition, yPosition + cellHeight - yPadding]}};
                _.each(series.data, function(row, index) {
                  plotlySeries.values.push(row.y);
                  plotlySeries.labels.push(hasX ? row.x : 'Slice ' + index);
                });
                scope.data.push(plotlySeries);
              });
              return;
            }

            var hasY2 = false;
            _.each(scope.series, function(series, index) {
              var seriesOptions = scope.options.seriesOptions[series.name] || {};
              var plotlySeries = {x: [],
                                  y: [],
                                  name: seriesOptions.name || series.name,
                                  marker: {color: seriesOptions.color ? seriesOptions.color : getColor(index)}};
              if (seriesOptions.yAxis == 1 && scope.options.series.stacking == null) {
                hasY2 = true;
                plotlySeries.yaxis = 'y2';
              }
              setType(plotlySeries, seriesOptions.type);
              var data = series.data;
              if (scope.options.sortX) {
                data = _.sortBy(data, 'x');
              }
              _.each(data, function(row) {
                plotlySeries.x.push(normalizeValue(row.x));
                plotlySeries.y.push(normalizeValue(row.y));
              });
              scope.data.push(plotlySeries)
            });

            var getTitle = function(axis) {
              if (angular.isDefined(axis) && angular.isDefined(axis.title)) {
                return axis.title.text;
              }
              return null;
            }

            scope.layout.xaxis = {title: getTitle(scope.options.xAxis),
                                  type: getScaleType(scope.options.xAxis.type)};
            if (angular.isDefined(scope.options.xAxis.labels)) {
              scope.layout.xaxis.showticklabels = scope.options.xAxis.labels.enabled;
            }
            if (angular.isArray(scope.options.yAxis)) {
              scope.layout.yaxis = {title: getTitle(scope.options.yAxis[0]),
                                    type: getScaleType(scope.options.yAxis[0].type)};
            }
            if (hasY2 && angular.isDefined(scope.options.yAxis)) {
              scope.layout.yaxis2 = {title: getTitle(scope.options.yAxis[1]),
                                     type: getScaleType(scope.options.yAxis[1].type),
                                     overlaying: 'y',
                                     side: 'right'};
            } else {
              delete scope.layout.yaxis2;
            }
            if (scope.options.series.stacking == 'normal') {
              scope.layout.barmode = 'stack';
              if (scope.options.globalSeriesType == 'area') {
                normalAreaStacking(scope.data);
              }
            } else if (scope.options.series.stacking == 'percent') {
              scope.layout.barmode = 'stack';
              if (scope.options.globalSeriesType == 'area') {
                percentAreaStacking(scope.data);
              } else if (scope.options.globalSeriesType == 'column') {
                percentBarStacking(scope.data);
              }
            }
          }

          scope.$watch('series', redraw);
          scope.$watch('options', redraw, true);
          scope.layout = {margin: {l: 50, r: 50, b: 50, t: 20, pad: 4}, autosize: true, hovermode: 'closest'};
          scope.plotlyOptions = {showLink: false, displaylogo: false};
          scope.data = [];
        }
      }
    });
})();
<commit_msg>Chart: pie chart colors should use our custom palette and not the default plotly palette<commit_after>(function () {
  'use strict';

  var ColorPalette = {
    'Blue': '#4572A7',
    'Red': '#AA4643',
    'Green': '#89A54E',
    'Purple': '#80699B',
    'Cyan': '#3D96AE',
    'Orange': '#DB843D',
    'Light Blue': '#92A8CD',
    'Lilac': '#A47D7C',
    'Light Green': '#B5CA92',
    'Brown': '#A52A2A',
    'Black': '#000000',
    'Gray': '#808080',
    'Pink': '#FFC0CB',
    'Dark Blue': '#00008b'
  };
  var ColorPaletteArray = _.values(ColorPalette)

  var fillXValues = function(seriesList) {
    var xValues = _.uniq(_.flatten(_.pluck(seriesList, 'x')));
    xValues.sort();
    _.each(seriesList, function(series) {
      series.x.sort();
      _.each(xValues, function(value, index) {
        if (series.x[index] != value) {
          series.x.splice(index, 0, value);
          series.y.splice(index, 0, 0);
        }
      });
    });
  }

  var normalAreaStacking = function(seriesList) {
    fillXValues(seriesList);
    _.each(seriesList, function(series) {
      series.text = [];
      series.hoverinfo = 'text+name';
    });
    for (var i = 0; i < seriesList.length; i++) {
      for (var j = 0; j < seriesList[i].y.length; j++) {
        var sum = i > 0 ? seriesList[i-1].y[j] : 0;
        seriesList[i].text.push('Value: ' + seriesList[i].y[j] + '
Sum: ' + (sum + seriesList[i].y[j]));
        seriesList[i].y[j] += sum;
      }
    }
  }

  var percentAreaStacking = function(seriesList) {
    if (seriesList.length == 0)
      return;
    fillXValues(seriesList);
    _.each(seriesList, function(series) {
      series.text = [];
      series.hoverinfo = 'text+name';
    });
    for (var i = 0; i < seriesList[0].y.length; i++) {
      var sum = 0;
      for(var j = 0; j < seriesList.length; j++) {
        sum += seriesList[j].y[i];
      }
      for(var j = 0; j < seriesList.length; j++) {
        var value = seriesList[j].y[i] / sum * 100;
        seriesList[j].text.push('Value: ' + seriesList[j].y[i] + '
Relative: ' + value.toFixed(2) + '%');

        seriesList[j].y[i] = value;
        if (j > 0)
          seriesList[j].y[i] += seriesList[j-1].y[i];
      }
    }
  }

  var percentBarStacking = function(seriesList) {
    if (seriesList.length == 0)
      return;
    fillXValues(seriesList);
    _.each(seriesList, function(series) {
      series.text = [];
      series.hoverinfo = 'text+name';
    });
    for (var i = 0; i < seriesList[0].y.length; i++) {
      var sum = 0;
      for(var j = 0; j < seriesList.length; j++) {
        sum += seriesList[j].y[i];
      }
      for(var j = 0; j < seriesList.length; j++) {
        var value = seriesList[j].y[i] / sum * 100;
        seriesList[j].text.push('Value: ' + seriesList[j].y[i] + '
Relative: ' + value.toFixed(2) + '%');
        seriesList[j].y[i] = value;
      }
    }
  }

  var normalizeValue = function(value) {
    if (moment.isMoment(value)) {
      return value.format("YYYY-MM-DD HH:MM:SS.ssssss");
    }
    return value;
  }

  angular.module('plotly-chart', [])
    .constant('ColorPalette', ColorPalette)
    .directive('plotlyChart', function () {
      return {
        restrict: 'E',
        template: '<plotly data="data" layout="layout" options="plotlyOptions"></plotly>',
        scope: {
          options: "=",
          series: "=",
          minHeight: "="
        },
        link: function (scope, element, attrs) {
          var getScaleType = function(scale) {
            if (scale == 'datetime')
              return 'date';
            if (scale == 'logarithmic')
              return 'log';
            return scale;
          }

          var setType = function(series, type) {
            if (type == 'column') {
              series['type'] = 'bar';
            } else  if (type == 'line') {
              series['mode'] = 'lines';
            } else if (type == 'area') {
              series['fill'] = scope.options.series.stacking == null ? 'tozeroy' : 'tonexty';
              series['mode'] = 'lines';
            } else if (type == 'scatter') {
              series['type'] = 'scatter';
              series['mode'] = 'markers';
            }
          }

          var getColor = function(index) {
            return ColorPaletteArray[index % ColorPaletteArray.length];
          }

          var bottomMargin = 50,
              pixelsPerLegendRow = 21;
          var redraw = function() {
            scope.data.length = 0;
            scope.layout.showlegend = _.has(scope.options, 'legend') ? scope.options.legend.enabled : true;
            scope.layout.height = Math.max(scope.minHeight, pixelsPerLegendRow * scope.series.length);
            scope.layout.margin.b = scope.layout.height - (scope.minHeight - bottomMargin) ;
            delete scope.layout.barmode;
            delete scope.layout.xaxis;
            delete scope.layout.yaxis;
            delete scope.layout.yaxis2;

            if (scope.options.globalSeriesType == 'pie') {
              var hasX = _.contains(_.values(scope.options.columnMapping), 'x');
              var rows = scope.series.length > 2 ? 2 : 1;
              var cellsInRow = Math.ceil(scope.series.length / rows)
              var cellWidth = 1 / cellsInRow;
              var cellHeight = 1 / rows;
              var xPadding = 0.02;
              var yPadding = 0.05;
              _.each(scope.series, function(series, index) {
                var xPosition = (index % cellsInRow) * cellWidth;
                var yPosition = Math.floor(index / cellsInRow) * cellHeight;
                var plotlySeries = {values: [], labels: [], type: 'pie', hole: .4,
                                    marker: {colors: ColorPaletteArray},
                                    text: series.name, textposition: 'inside', name: series.name,
                                    domain: {x: [xPosition, xPosition + cellWidth - xPadding],
                                             y: [yPosition, yPosition + cellHeight - yPadding]}};
                _.each(series.data, function(row, index) {
                  plotlySeries.values.push(row.y);
                  plotlySeries.labels.push(hasX ? row.x : 'Slice ' + index);
                });
                scope.data.push(plotlySeries);
              });
              return;
            }

            var hasY2 = false;
            _.each(scope.series, function(series, index) {
              var seriesOptions = scope.options.seriesOptions[series.name] || {};
              var plotlySeries = {x: [],
                                  y: [],
                                  name: seriesOptions.name || series.name,
                                  marker: {color: seriesOptions.color ? seriesOptions.color : getColor(index)}};
              if (seriesOptions.yAxis == 1 && scope.options.series.stacking == null) {
                hasY2 = true;
                plotlySeries.yaxis = 'y2';
              }
              setType(plotlySeries, seriesOptions.type);
              var data = series.data;
              if (scope.options.sortX) {
                data = _.sortBy(data, 'x');
              }
              _.each(data, function(row) {
                plotlySeries.x.push(normalizeValue(row.x));
                plotlySeries.y.push(normalizeValue(row.y));
              });
              scope.data.push(plotlySeries)
            });

            var getTitle = function(axis) {
              if (angular.isDefined(axis) && angular.isDefined(axis.title)) {
                return axis.title.text;
              }
              return null;
            }

            scope.layout.xaxis = {title: getTitle(scope.options.xAxis),
                                  type: getScaleType(scope.options.xAxis.type)};
            if (angular.isDefined(scope.options.xAxis.labels)) {
              scope.layout.xaxis.showticklabels = scope.options.xAxis.labels.enabled;
            }
            if (angular.isArray(scope.options.yAxis)) {
              scope.layout.yaxis = {title: getTitle(scope.options.yAxis[0]),
                                    type: getScaleType(scope.options.yAxis[0].type)};
            }
            if (hasY2 && angular.isDefined(scope.options.yAxis)) {
              scope.layout.yaxis2 = {title: getTitle(scope.options.yAxis[1]),
                                     type: getScaleType(scope.options.yAxis[1].type),
                                     overlaying: 'y',
                                     side: 'right'};
            } else {
              delete scope.layout.yaxis2;
            }
            if (scope.options.series.stacking == 'normal') {
              scope.layout.barmode = 'stack';
              if (scope.options.globalSeriesType == 'area') {
                normalAreaStacking(scope.data);
              }
            } else if (scope.options.series.stacking == 'percent') {
              scope.layout.barmode = 'stack';
              if (scope.options.globalSeriesType == 'area') {
                percentAreaStacking(scope.data);
              } else if (scope.options.globalSeriesType == 'column') {
                percentBarStacking(scope.data);
              }
            }
          }

          scope.$watch('series', redraw);
          scope.$watch('options', redraw, true);
          scope.layout = {margin: {l: 50, r: 50, b: 50, t: 20, pad: 4}, autosize: true, hovermode: 'closest'};
          scope.plotlyOptions = {showLink: false, displaylogo: false};
          scope.data = [];
        }
      }
    });
})();
<|endoftext|>