<commit_before>import React, { memo, useMemo, useCallback, useState, useEffect, useRef } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import cn from 'classnames';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';

import {
  createHomeFeed,
  updateHomeFeed,
  deleteHomeFeed,
  updateHomeFeedReset,
  getAllSubscriptions,
} from '../../redux/action-creators';
import { doSequence } from '../../redux/async-helpers';
import { UserPicture } from '../user-picture';
import { ButtonLink, useKeyboardEvents } from '../button-link';
import { useBool } from '../hooks/bool';
import { Throbber } from '../throbber';
import { Icon } from '../fontawesome-icons';
import { OverlayPopup } from '../overlay-popup';
import styles from './list-editor.module.scss';

export const ListEditor = memo(function ListEditor({
  listId = null,
  // close function receives an argument that is:
  // 1. ID of edited _or created_ list
  // 2. null or undefined if editor was closed with "Cancel" button
  close = () => null,
}) {
  const dispatch = useDispatch();
  const allSubscriptions = useSelector((state) => state.allSubscriptions);
  const allHomeFeeds = useSelector((state) => state.homeFeeds);
  const allUsers = useSelector((state) => state.users);
  const allSubscriptionsStatus = useSelector((state) => state.allSubscriptionsStatus);
  const submitStatus = useSelector((state) => state.crudHomeFeedStatus);

  // Fetch subscriptions and homefeeds if there are not fetched yet
  useEffect(() => void (allSubscriptionsStatus.initial && dispatch(getAllSubscriptions())), [
    allSubscriptionsStatus.initial,
    dispatch,
  ]);

  // Edited list (or null for the new one)
  const list = useMemo(() => allHomeFeeds.find((feed) => feed.id === listId), [
    allHomeFeeds,
    listId,
  ]);

  // Map of user's selection states: { [userId]: boolean }
  const [selected, setSelected] = useState({});
  // Fill the selection map when allSubscriptions data is ready
  useEffect(() => {
    const listId = list?.id;
    if (allSubscriptionsStatus.success && listId) {
      const sel = {};
      for (const { id, homeFeeds } of allSubscriptions) {
        if (homeFeeds.includes(listId)) {
          sel[id] = true;
        }
      }
      setSelected(sel);
    }
  }, [allSubscriptionsStatus.success, allSubscriptions, list?.id]);

  const totalCount = allSubscriptions.length;
  const selectedCount = useMemo(() => Object.values(selected).filter(Boolean).length, [selected]);

  const onSelect = useCallback((id, sel) => setSelected({ ...selected, [id]: sel }), [selected]);

  // Filter users by selection
  const [showSelected, , showSelectionOnly, showAll] = useBool(false);

  // Filter users by name
  const [nameFilter, setNameFilter] = useState('');
  const onFilterChange = useCallback(({ target }) => setNameFilter(target.value), []);
  const clearFilter = useCallback(() => setNameFilter(''), []);

  // List title edit
  const [listTitle, setListTitle] = useState('');
  const onListTitleChange = useCallback(({ target }) => setListTitle(target.value), []);

  // Set list title when data is loaded
  useEffect(() => {
    if (allSubscriptionsStatus.success) {
      setListTitle(list?.title || '');
    }
  }, [allSubscriptionsStatus.success, list?.title]);

  // Prepare users data
  const users = useMemo(() => {
    const filter = nameFilter.trim();
    return allSubscriptions
      .map(({ id, homeFeeds }) => ({
        ...allUsers[id],
        selected: !!selected[id],
        homeless: homeFeeds.length === 0 || (homeFeeds.length === 1 && homeFeeds[0] === list?.id),
      }))
      .filter((u) => !showSelected || u.selected)
      .filter((u) => !filter || u.username.includes(filter) || u.screenName.includes(filter));
  }, [allSubscriptions, allUsers, selected, showSelected, nameFilter, list?.id]);

  const hasHomeless = useMemo(() => users.some((u) => u.homeless && !u.selected), [users]);
  const returnListId = useRef(list?.id);

  // Handle submit
  const canSubmit = useMemo(() => allSubscriptionsStatus.success && listTitle.trim() !== '', [
    allSubscriptionsStatus.success,
    listTitle,
  ]);

  const doSubmit = useCallback(() => {
    if (!canSubmit) {
      return;
    }
    const listId = list && list.id;
    const data = {
      subscribedTo: Object.keys(selected).filter((id) => !!selected[id]),
    };

    if (!list || !list.isInherent) {
      data.title = listTitle;
    }
    doSequence(dispatch)(
      (dispatch) => dispatch(listId ? updateHomeFeed(listId, data) : createHomeFeed(data)),
      (_, { payload }) => (returnListId.current = payload.timeline.id),
    );
  }, [canSubmit, selected, dispatch, list, listTitle]);

  // Handle cancel
  const doCancel = useCallback(() => close(), [close]);

  // Handle delete
  const doDelete = useCallback(() => {
    const selectedIds = Object.keys(selected).filter((id) => !!selected[id]);
    if (
      !confirm(
        `Are you sure you want to delete this list?${
          selectedIds.length > 0
            ? `\nAll remaining list subscriptions will be moved to the main Home list.`
            : ``
        }`,
      )
    ) {
      return;
    }
    const listId = list?.id;

    doSequence(dispatch)(
      (dispatch) => dispatch(updateHomeFeed(listId, { subscribedTo: selectedIds })),
      (dispatch) => dispatch(deleteHomeFeed(listId)),
    );
  }, [dispatch, list?.id, selected]);

  // Close this dialog on success and pass returnListId to the close function
  useEffect(
    () =>
      void (submitStatus.success && (close(returnListId.current), dispatch(updateHomeFeedReset()))),
    [submitStatus.success, close, dispatch],
  );

  return (
    <OverlayPopup close={doCancel}>
      <section className={styles.wrapper}>
        <header className={styles.header}>
          {list && list.isInherent ? (
            <h3>{list.title}</h3>
          ) : (
            <dl className={styles.topForm}>
              <dt>List title</dt>
              <dd>
                <input
                  type="text"
                  className="form-control input-lg"
                  placeholder={`Name your ${list ? '' : 'new '}list (required)`}
                  value={listTitle}
                  onChange={onListTitleChange}
                />
              </dd>
            </dl>
          )}
          <p>
            Show:{' '}
            <ButtonLinkX onClick={showAll} selected={!showSelected}>
              All ({totalCount})
            </ButtonLinkX>
            {' - '}
            <ButtonLinkX onClick={showSelectionOnly} selected={showSelected}>
              Selected ({selectedCount})
            </ButtonLinkX>
            {' - '}
            Filter by name:{' '}
            <input
              type="search"
              name="nameFilter"
              autoFocus
              value={nameFilter}<commit_msg>Auto focus the list title input on 'Create friend list' dialog<commit_after>import React, { memo, useMemo, useCallback, useState, useEffect, useRef } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import cn from 'classnames';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';

import {
  createHomeFeed,
  updateHomeFeed,
  deleteHomeFeed,
  updateHomeFeedReset,
  getAllSubscriptions,
} from '../../redux/action-creators';
import { doSequence } from '../../redux/async-helpers';
import { UserPicture } from '../user-picture';
import { ButtonLink, useKeyboardEvents } from '../button-link';
import { useBool } from '../hooks/bool';
import { Throbber } from '../throbber';
import { Icon } from '../fontawesome-icons';
import { OverlayPopup } from '../overlay-popup';
import styles from './list-editor.module.scss';

export const ListEditor = memo(function ListEditor({
  listId = null,
  // close function receives an argument that is:
  // 1. ID of edited _or created_ list
  // 2. null or undefined if editor was closed with "Cancel" button
  close = () => null,
}) {
  const dispatch = useDispatch();
  const allSubscriptions = useSelector((state) => state.allSubscriptions);
  const allHomeFeeds = useSelector((state) => state.homeFeeds);
  const allUsers = useSelector((state) => state.users);
  const allSubscriptionsStatus = useSelector((state) => state.allSubscriptionsStatus);
  const submitStatus = useSelector((state) => state.crudHomeFeedStatus);

  // Fetch subscriptions and homefeeds if there are not fetched yet
  useEffect(() => void (allSubscriptionsStatus.initial && dispatch(getAllSubscriptions())), [
    allSubscriptionsStatus.initial,
    dispatch,
  ]);

  // Edited list (or null for the new one)
  const list = useMemo(() => allHomeFeeds.find((feed) => feed.id === listId), [
    allHomeFeeds,
    listId,
  ]);

  // Map of user's selection states: { [userId]: boolean }
  const [selected, setSelected] = useState({});
  // Fill the selection map when allSubscriptions data is ready
  useEffect(() => {
    const listId = list?.id;
    if (allSubscriptionsStatus.success && listId) {
      const sel = {};
      for (const { id, homeFeeds } of allSubscriptions) {
        if (homeFeeds.includes(listId)) {
          sel[id] = true;
        }
      }
      setSelected(sel);
    }
  }, [allSubscriptionsStatus.success, allSubscriptions, list?.id]);

  const totalCount = allSubscriptions.length;
  const selectedCount = useMemo(() => Object.values(selected).filter(Boolean).length, [selected]);

  const onSelect = useCallback((id, sel) => setSelected({ ...selected, [id]: sel }), [selected]);

  // Filter users by selection
  const [showSelected, , showSelectionOnly, showAll] = useBool(false);

  // Filter users by name
  const [nameFilter, setNameFilter] = useState('');
  const onFilterChange = useCallback(({ target }) => setNameFilter(target.value), []);
  const clearFilter = useCallback(() => setNameFilter(''), []);

  // List title edit
  const [listTitle, setListTitle] = useState('');
  const onListTitleChange = useCallback(({ target }) => setListTitle(target.value), []);

  // Set list title when data is loaded
  useEffect(() => {
    if (allSubscriptionsStatus.success) {
      setListTitle(list?.title || '');
    }
  }, [allSubscriptionsStatus.success, list?.title]);

  // Prepare users data
  const users = useMemo(() => {
    const filter = nameFilter.trim();
    return allSubscriptions
      .map(({ id, homeFeeds }) => ({
        ...allUsers[id],
        selected: !!selected[id],
        homeless: homeFeeds.length === 0 || (homeFeeds.length === 1 && homeFeeds[0] === list?.id),
      }))
      .filter((u) => !showSelected || u.selected)
      .filter((u) => !filter || u.username.includes(filter) || u.screenName.includes(filter));
  }, [allSubscriptions, allUsers, selected, showSelected, nameFilter, list?.id]);

  const hasHomeless = useMemo(() => users.some((u) => u.homeless && !u.selected), [users]);
  const returnListId = useRef(list?.id);

  // Handle submit
  const canSubmit = useMemo(() => allSubscriptionsStatus.success && listTitle.trim() !== '', [
    allSubscriptionsStatus.success,
    listTitle,
  ]);

  const doSubmit = useCallback(() => {
    if (!canSubmit) {
      return;
    }
    const listId = list && list.id;
    const data = {
      subscribedTo: Object.keys(selected).filter((id) => !!selected[id]),
    };

    if (!list || !list.isInherent) {
      data.title = listTitle;
    }
    doSequence(dispatch)(
      (dispatch) => dispatch(listId ? updateHomeFeed(listId, data) : createHomeFeed(data)),
      (_, { payload }) => (returnListId.current = payload.timeline.id),
    );
  }, [canSubmit, selected, dispatch, list, listTitle]);

  // Handle cancel
  const doCancel = useCallback(() => close(), [close]);

  // Handle delete
  const doDelete = useCallback(() => {
    const selectedIds = Object.keys(selected).filter((id) => !!selected[id]);
    if (
      !confirm(
        `Are you sure you want to delete this list?${
          selectedIds.length > 0
            ? `\nAll remaining list subscriptions will be moved to the main Home list.`
            : ``
        }`,
      )
    ) {
      return;
    }
    const listId = list?.id;

    doSequence(dispatch)(
      (dispatch) => dispatch(updateHomeFeed(listId, { subscribedTo: selectedIds })),
      (dispatch) => dispatch(deleteHomeFeed(listId)),
    );
  }, [dispatch, list?.id, selected]);

  // Close this dialog on success and pass returnListId to the close function
  useEffect(
    () =>
      void (submitStatus.success && (close(returnListId.current), dispatch(updateHomeFeedReset()))),
    [submitStatus.success, close, dispatch],
  );

  return (
    <OverlayPopup close={doCancel}>
      <section className={styles.wrapper}>
        <header className={styles.header}>
          {list && list.isInherent ? (
            <h3>{list.title}</h3>
          ) : (
            <dl className={styles.topForm}>
              <dt>List title</dt>
              <dd>
                <input
                  type="text"
                  autoFocus={!list}
                  className="form-control input-lg"
                  placeholder={`Name your ${list ? '' : 'new '}list (required)`}
                  value={listTitle}
                  onChange={onListTitleChange}
                />
              </dd>
            </dl>
          )}
          <p>
            Show:{' '}
            <ButtonLinkX onClick={showAll} selected={!showSelected}>
              All ({totalCount})
            </ButtonLinkX>
            {' - '}
            <ButtonLinkX onClick={showSelectionOnly} selected={showSelected}>
              Selected ({selectedCount})
            </ButtonLinkX>
            {' - '}
            Filter by name:{' '}
            <input
              type="search"
              name="nameFilter"
              autoFocus={!!list}
              value={nameFilter}
{'fraction_non_alphanumeric': 0.1495806824754193, 'fraction_numerical': 0.001156737998843262, 'mean_word_length': 2.8107438016528925, 'pattern_counts': {'":': 0, '<': 39, '<?xml version=': 0, '>': 105, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7310166', 'n_tokens_mistral': 3974, 'n_tokens_neox': 3927, 'n_words': 970}