<commit_before>/**
 *    Copyright (c) ppy Pty Ltd <<EMAIL>>.
 *
 *    This file is part of osu!web. osu!web is distributed with the hope of
 *    attracting more community contributions to the core ecosystem of osu!.
 *
 *    osu!web is free software: you can redistribute it and/or modify
 *    it under the terms of the Affero GNU General Public License version 3
 *    as published by the Free Software Foundation.
 *
 *    osu!web is distributed WITHOUT ANY WARRANTY; without even the implied
 *    warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *    See the GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with osu!web.  If not, see <http://www.gnu.org/licenses/>.
 */

import { route } from 'laroute';
import * as _ from 'lodash';
import { inject, observer } from 'mobx-react';
import Message from 'models/chat/message';
import * as moment from 'moment';
import * as React from 'react';
import { Spinner } from 'spinner';
import RootDataStore from 'stores/root-data-store';
import { StringWithComponent } from 'string-with-component';
import { UserAvatar } from 'user-avatar';
import { ChatChannelSwitchAction } from '../actions/chat-actions';
import DispatcherAction from '../actions/dispatcher-action';
import DispatchListener from '../dispatch-listener';
import Dispatcher from '../dispatcher';
import { MessageDivider } from './message-divider';
import MessageGroup from './message-group';

interface Props {
  dataStore?: RootDataStore;
  dispatcher?: Dispatcher;
}

@inject('dataStore')
@inject('dispatcher')
@observer
export default class ConversationView extends React.Component<Props> implements DispatchListener {
  private assumeHasBacklog: boolean = false;
  private chatViewRef = React.createRef<HTMLDivElement>();
  private readonly dataStore: RootDataStore;
  private didSwitchChannel: boolean = true;
  private unreadMarkerRef = React.createRef<HTMLDivElement>();

  constructor(props: Props) {
    super(props);

    this.dataStore = props.dataStore!;
    props.dispatcher!.register(this);
  }

  componentDidMount() {
    this.componentDidUpdate();
    $(window).on('throttled-scroll', _.throttle(this.onScroll, 1000));
  }

  componentDidUpdate = () => {
    const chatView = this.chatViewRef.current;
    if (!chatView) {
      return;
    }

    const dataStore = this.dataStore;
    const channel = dataStore.channelStore.channels.get(dataStore.uiState.chat.selected);
    if (!channel?.loaded) {
      return;
    }

    if (this.didSwitchChannel) {
      if (this.unreadMarkerRef.current) {
        this.scrollToUnread();
      } else {
        this.scrollToBottom();
      }
      this.didSwitchChannel = false;
    } else {
      if (this.dataStore.uiState.chat.autoScroll) {
        this.scrollToBottom();
      }
    }
  }

  handleDispatchAction(action: DispatcherAction) {
    if (action instanceof ChatChannelSwitchAction) {
      this.didSwitchChannel = true;
    }
  }

  noCanSendMessage(): React.ReactNode {
    const dataStore: RootDataStore = this.dataStore;
    const presence = dataStore.channelStore.channels.get(dataStore.uiState.chat.selected);

    if (!presence) {
      // this shouldn't happen...
      return;
    }

    if (presence.type === 'PM' || presence.type === 'NEW') {
      return (
        <div>
          <div className='chat-conversation__cannot-message'>{osu.trans('chat.cannot_send.user')}</div>
          <ul className='chat-conversation__cannot-message-reasons'>
            <li>{osu.trans('chat.cannot_send.reasons.friends_only')}</li>
            <li>{osu.trans('chat.cannot_send.reasons.target_restricted')}</li>
            <li>{osu.trans('chat.cannot_send.reasons.restricted')}</li>
            <li>{osu.trans('chat.cannot_send.reasons.blocked')}</li>
          </ul>
        </div>
      );
    } else if (presence.type === 'GROUP') {
      return (
        <div>
          <div className='chat-conversation__cannot-message'>{osu.trans('chat.cannot_send.channel')}</div>
          <ul className='chat-conversation__cannot-message-reasons'>
            <li>{osu.trans('chat.cannot_send.reasons.channel_moderated')}</li>
            <li>{osu.trans('chat.cannot_send.reasons.restricted')}</li>
          </ul>
        </div>
      );
    }
  }

  onScroll = () => {
    const chatView = this.chatViewRef.current;
    if (chatView) {
      this.dataStore.uiState.chat.autoScroll = chatView.scrollTop + chatView.clientHeight >= chatView.scrollHeight;
    }
  }

  render(): React.ReactNode {
    const dataStore: RootDataStore = this.dataStore;
    const channel = dataStore.channelStore.channels.get(dataStore.uiState.chat.selected);

    if (!channel) {
      return <div className='conversation' />;
    }

    const lazerLink = 'https://github.com/ppy/osu/releases';
    const oldPMLink = `https://osu.ppy.sh/forum/ucp.php?i=pm&mode=compose&u=${channel.pmTarget}`;
    const conversationStack: JSX.Element[] = [];
    let currentGroup: Message[] = [];
    let unreadMarkerShown = false;
    let currentDay: number;

    _.each(channel.messages, (message: Message, key: number) => {
      // check if the last read indicator needs to be shown
      if (!unreadMarkerShown && message.messageId > dataStore.uiState.chat.lastReadId && message.sender.id !== currentUser.id) {
        unreadMarkerShown = true;

        // If the unread marker is the first element in this conversation, it most likely means that the unread cursor
        // is even further in the past, making the displayed marker somewhat useless (until we can back-load those
        // past messages in)... thus we ignore it when auto-scrolling and just go to the bottom instead.
        //
        // TODO: Actually in hindsight, there's another scenario where the first element in the conversation is an
        // unread marker - when you receive new PMs and have yet to read any. Will look to handle this case later...
        if (!_.isEmpty(conversationStack)) {<commit_msg>Ensure assumeHasBacklog flag is cleared at start of render<commit_after>/**
 *    Copyright (c) ppy Pty Ltd <<EMAIL>>.
 *
 *    This file is part of osu!web. osu!web is distributed with the hope of
 *    attracting more community contributions to the core ecosystem of osu!.
 *
 *    osu!web is free software: you can redistribute it and/or modify
 *    it under the terms of the Affero GNU General Public License version 3
 *    as published by the Free Software Foundation.
 *
 *    osu!web is distributed WITHOUT ANY WARRANTY; without even the implied
 *    warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *    See the GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with osu!web.  If not, see <http://www.gnu.org/licenses/>.
 */

import { route } from 'laroute';
import * as _ from 'lodash';
import { inject, observer } from 'mobx-react';
import Message from 'models/chat/message';
import * as moment from 'moment';
import * as React from 'react';
import { Spinner } from 'spinner';
import RootDataStore from 'stores/root-data-store';
import { StringWithComponent } from 'string-with-component';
import { UserAvatar } from 'user-avatar';
import { ChatChannelSwitchAction } from '../actions/chat-actions';
import DispatcherAction from '../actions/dispatcher-action';
import DispatchListener from '../dispatch-listener';
import Dispatcher from '../dispatcher';
import { MessageDivider } from './message-divider';
import MessageGroup from './message-group';

interface Props {
  dataStore?: RootDataStore;
  dispatcher?: Dispatcher;
}

@inject('dataStore')
@inject('dispatcher')
@observer
export default class ConversationView extends React.Component<Props> implements DispatchListener {
  private assumeHasBacklog: boolean = false;
  private chatViewRef = React.createRef<HTMLDivElement>();
  private readonly dataStore: RootDataStore;
  private didSwitchChannel: boolean = true;
  private unreadMarkerRef = React.createRef<HTMLDivElement>();

  constructor(props: Props) {
    super(props);

    this.dataStore = props.dataStore!;
    props.dispatcher!.register(this);
  }

  componentDidMount() {
    this.componentDidUpdate();
    $(window).on('throttled-scroll', _.throttle(this.onScroll, 1000));
  }

  componentDidUpdate = () => {
    const chatView = this.chatViewRef.current;
    if (!chatView) {
      return;
    }

    const dataStore = this.dataStore;
    const channel = dataStore.channelStore.channels.get(dataStore.uiState.chat.selected);
    if (!channel?.loaded) {
      return;
    }

    if (this.didSwitchChannel) {
      if (this.unreadMarkerRef.current) {
        this.scrollToUnread();
      } else {
        this.scrollToBottom();
      }
      this.didSwitchChannel = false;
    } else {
      if (this.dataStore.uiState.chat.autoScroll) {
        this.scrollToBottom();
      }
    }
  }

  handleDispatchAction(action: DispatcherAction) {
    if (action instanceof ChatChannelSwitchAction) {
      this.didSwitchChannel = true;
    }
  }

  noCanSendMessage(): React.ReactNode {
    const dataStore: RootDataStore = this.dataStore;
    const presence = dataStore.channelStore.channels.get(dataStore.uiState.chat.selected);

    if (!presence) {
      // this shouldn't happen...
      return;
    }

    if (presence.type === 'PM' || presence.type === 'NEW') {
      return (
        <div>
          <div className='chat-conversation__cannot-message'>{osu.trans('chat.cannot_send.user')}</div>
          <ul className='chat-conversation__cannot-message-reasons'>
            <li>{osu.trans('chat.cannot_send.reasons.friends_only')}</li>
            <li>{osu.trans('chat.cannot_send.reasons.target_restricted')}</li>
            <li>{osu.trans('chat.cannot_send.reasons.restricted')}</li>
            <li>{osu.trans('chat.cannot_send.reasons.blocked')}</li>
          </ul>
        </div>
      );
    } else if (presence.type === 'GROUP') {
      return (
        <div>
          <div className='chat-conversation__cannot-message'>{osu.trans('chat.cannot_send.channel')}</div>
          <ul className='chat-conversation__cannot-message-reasons'>
            <li>{osu.trans('chat.cannot_send.reasons.channel_moderated')}</li>
            <li>{osu.trans('chat.cannot_send.reasons.restricted')}</li>
          </ul>
        </div>
      );
    }
  }

  onScroll = () => {
    const chatView = this.chatViewRef.current;
    if (chatView) {
      this.dataStore.uiState.chat.autoScroll = chatView.scrollTop + chatView.clientHeight >= chatView.scrollHeight;
    }
  }

  render(): React.ReactNode {
    const dataStore: RootDataStore = this.dataStore;
    const channel = dataStore.channelStore.channels.get(dataStore.uiState.chat.selected);
    this.assumeHasBacklog = false;

    if (!channel) {
      return <div className='conversation' />;
    }

    const lazerLink = 'https://github.com/ppy/osu/releases';
    const oldPMLink = `https://osu.ppy.sh/forum/ucp.php?i=pm&mode=compose&u=${channel.pmTarget}`;
    const conversationStack: JSX.Element[] = [];
    let currentGroup: Message[] = [];
    let unreadMarkerShown = false;
    let currentDay: number;

    _.each(channel.messages, (message: Message, key: number) => {
      // check if the last read indicator needs to be shown
      if (!unreadMarkerShown && message.messageId > dataStore.uiState.chat.lastReadId && message.sender.id !== currentUser.id) {
        unreadMarkerShown = true;

        // If the unread marker is the first element in this conversation, it most likely means that the unread cursor
        // is even further in the past, making the displayed marker somewhat useless (until we can back-load those
        // past messages in)... thus we ignore it when auto-scrolling and just go to the bottom instead.
        //
        // TODO: Actually in hindsight, there's another scenario where the first element in the conversation is an
        // unread marker - when you receive new PMs and have yet to read any. Will look to handle this case later...
        if (!_.isEmpty(conversationStack)) {
{'fraction_non_alphanumeric': 0.1216883330590752, 'fraction_numerical': 0.0008227743952608195, 'mean_word_length': 3.1986183074265977, 'pattern_counts': {'":': 0, '<': 65, '<?xml version=': 0, '>': 75, 'https://': 4, 'lorem ipsum': 0, 'www.': 2, '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': '6639123', 'n_tokens_mistral': 3601, 'n_tokens_neox': 3509, 'n_words': 930}