Logo Search packages:      
Sourcecode: kmess version File versions

chathistorydialog.cpp

 /***************************************************************************
                          chathistorydialog.cpp  -  chat logs browser
                             -------------------
    begin                : Sun Feb 22 2009
    copyright            : (C) 2009 by Dario Freddi
    email                : drf54321@gmail.com
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

#include "chathistorydialog.h"

#include "../chat/chatmessagestyle.h"
#include "../chat/chatmessageview.h"
#include "../utils/kmessconfig.h"
#include "../currentaccount.h"
#include "../kmessdebug.h"

#include <QDir>
#include <QTimer>
#include <QStandardItemModel>
#include <QSortFilterProxyModel>
#include <QtXml/QDomDocument>
#include <QtXml/QDomNode>

#include <KMessageBox>
#include <KLocalizedString>
#include <KDateTime>


#ifdef KMESSDEBUG_CHATHISTORYDIALOG
//   #define KMESSDEBUG_CHATHISTORYDIALOG_VERBOSE
#endif


// Constructor
ChatHistoryDialog::ChatHistoryDialog( QWidget *parent )
: KDialog( parent )
, logParser_( 0 )
, model_( new QStandardItemModel( this ) )
, proxyModel_( new QSortFilterProxyModel( this ) )
, reloadingLogs_( false )
, reloadLogsTimer_(0)
{
  // Set up the dialog
  QWidget *mainWidget = new QWidget( this );
  setupUi( mainWidget );
  setMainWidget( mainWidget );
  setButtons( Close );
  setWindowTitle( i18nc( "Dialog window title", "Chat History" ) );
  loadingLabel_->hide();

  // Let the dialog destroy itself when it's done
  setAttribute( Qt::WA_DeleteOnClose );

  // Create the chat view to show the chat logs
  chatView_ = new ChatMessageView( mainWidget );
  chatView_->updateChatStyle();
  rightSplitter_->addWidget( chatView_->widget() );

  // Make the splitter assign more space to the view than to the controls,
  // and set its minimum size
  QSizePolicy sizePolicy( QSizePolicy::Preferred, QSizePolicy::Expanding );
  sizePolicy.setVerticalStretch( 100 );
  chatView_->widget()->setMinimumSize( 300, 200 );
  chatView_->widget()->setSizePolicy( sizePolicy );

  loadContactsList();

  // Initialize the model for the list of contacts
  proxyModel_->setSourceModel( model_ );
  proxyModel_->setDynamicSortFilter( true );
  proxyModel_->setFilterCaseSensitivity( Qt::CaseInsensitive );

  listView_->setModel( proxyModel_ );

  QItemSelectionModel *selectionModel = listView_->selectionModel();

  // Attach the signals
  connect( searchEdit_,           SIGNAL(       textChanged(const QString&)                     ),
           proxyModel_,           SLOT  (         setFilterRegExp(const QString&)               ) );
  connect( selectionModel,        SIGNAL( currentChanged(const QModelIndex&,const QModelIndex&) ),
           this,                  SLOT  (         cacheContactXml(const QModelIndex&)           ) );
  connect( dateRadio_,            SIGNAL(       toggled(bool)                                   ),
           this,                  SLOT  (         reloadLogs()                                  ) );
  connect( fromBox_,              SIGNAL(       toggled(bool)                                   ),
           this,                  SLOT  (         reloadLogs()                                  ) );
  connect( toBox_,                SIGNAL(       toggled(bool)                                   ),
           this,                  SLOT  (         reloadLogs()                                  ) );
  connect( fromDate_,             SIGNAL(       changed(const QDate&)                           ),
           this,                  SLOT  (         checkDates()                                  ) );
  connect( toDate_,               SIGNAL(       changed(const QDate&)                           ),
           this,                  SLOT  (         checkDates()                                  ) );
  connect( conversationRadio_,    SIGNAL(       toggled(bool)                                   ),
           this,                  SLOT  (         reloadLogs()                                  ) );
  connect( conversationComboBox_, SIGNAL(       currentIndexChanged(int)                        ),
           this,                  SLOT  (         reloadLogs()                                  ) );

  // Register QDomElement as a meta type to allow the thread to send signals here
  qRegisterMetaType<QDomElement>( "QDomElement" );

  // Save the dialog and splitters sizes
  KConfigGroup group = KMessConfig::instance()->getGlobalConfig( "ChatHistoryDialog" );
  restoreDialogSize( group );
  mainSplitter_ ->setSizes( group.readEntry( "mainSplitterSizes",  QList<int>() << 1 << 100 ) );
  rightSplitter_->setSizes( group.readEntry( "rightSplitterSizes", QList<int>() << 1 << 100 ) );

  // Normally the focus would be set on the Search box, thus making the
  // preset explanation text to disappear.
  listView_->setFocus();
}



// Destructor
ChatHistoryDialog::~ChatHistoryDialog()
{
  // Save the dialog and splitters size
  KConfigGroup group = KMessConfig::instance()->getGlobalConfig( "ChatHistoryDialog" );
  saveDialogSize( group );
  group.writeEntry( "mainSplitterSizes",  mainSplitter_ ->sizes() );
  group.writeEntry( "rightSplitterSizes", rightSplitter_->sizes() );
}



// A conversation has been parsed by the thread
void ChatHistoryDialog::addConversation( int timestamp, const QDomElement &newConversation )
{
#ifdef KMESSDEBUG_CHATHISTORYDIALOG_VERBOSE
  kDebug() << "Adding new conversation on" << timestamp;
#endif

  QMutexLocker lock( &addConversationLock_ );
  const QDateTime &date = QDateTime::fromTime_t( timestamp );

  // Add this conversation to the internal list
  conversations_[ timestamp ] = newConversation;

  // Add the chat to the chat selection combo box, keeping it sorted by date (most recent first)
  // The first item (index 0) is skipped as it is a temporary item ("Loading...") which
  // should remain the first
  int index;
  for( index = 1; index < conversationComboBox_->count(); ++index )
  {
    int currentTimestamp = conversationComboBox_->itemData( index, Qt::UserRole ).toUInt();

    if( currentTimestamp < 1 )
    {
#ifdef KMESSDEBUG_CHATHISTORYDIALOG
      kWarning() << "Invalid timestamp!";
#endif
      continue;
    }

    if( currentTimestamp <= timestamp )
    {
      break;
    }
  }

  // Prevent the insertion from generating a signal which regenerates the logs
  conversationComboBox_->blockSignals( true );
  conversationComboBox_->insertItem( index,
                                     KGlobal::locale()->formatDateTime( date, KLocale::ShortDate ),
                                     timestamp );
  conversationComboBox_->blockSignals( false );

#ifdef KMESSDEBUG_CHATHISTORYDIALOG_VERBOSE
  kDebug() << "Added as item" << index;
#endif
}



// Caches the XML for the selected contact. Makes things a lot faster
void ChatHistoryDialog::cacheContactXml( const QModelIndex &index )
{
  const QModelIndex contactIndex( proxyModel_->mapToSource( index ) );
  if( ! contactIndex.isValid() )
  {
    return;
  }

  // Initially the list view emits a currentChanged() signal, but the list
  // doesn't bother to highlight the selected item
  listView_->setCurrentIndex( index );

  // Reset the xml loading thread
  if( logParser_ != 0 )
  {
    logParser_->end();
    logParser_ = 0;
  }

  // Reset the UI
  xml_.clear();
  conversations_.clear();
  setLoading( true );
  // Avoid reentering while we're still loading a contact's chats
  listView_->setEnabled( false );

  // Reset the cache
  xml_.setContent( QByteArray( "<conversations></conversations>" ) );
  QDomNode cachedConversations( xml_.firstChildElement( "conversations" ) );

  const QString handle( model_->itemFromIndex( contactIndex )->text() );
  QDir logsDir( KMessConfig::instance()->getAccountDirectory( CurrentAccount::instance()->getHandle() )
                + "/chatlogs" );

#ifdef KMESSDEBUG_CHATHISTORYDIALOG
  kDebug() << "Loading conversations for contact:" << handle;
#endif

  // Filter only to read from this contact's files, if any
  logsDir.setFilter( QDir::Files );
  logsDir.setNameFilters( QStringList() << ( handle + "*.xml" ) );

  bool showDate    = chatView_->getStyle()->getShowMessageDate();
  bool showTime    = chatView_->getStyle()->getShowMessageTime();
  bool showSeconds = chatView_->getStyle()->getShowMessageSeconds();

  const QFileInfoList &list = logsDir.entryInfoList();
  foreach( const QFileInfo &entry, list )
  {
    QString currentHandle( entry.fileName() );

#ifdef KMESSDEBUG_CHATHISTORYDIALOG_VERBOSE
    kDebug() << "Reading conversations from file:" << entry.fileName();
#endif

    QFile file( entry.absoluteFilePath() );

    if( ! file.open( QFile::ReadOnly | QFile::Text ) )
    {
      // Very unlikely, but...
      KMessageBox::error( this, i18nc( "Dialog box text",
                                       "There has been an error while opening your logs. This "
                                       "is commonly a permission problem, check if you have "
                                       "read/write access to directory <i>&quot;%1&quot;</i>. "
                                       "Otherwise, your logs may be corrupted.",
                                       logsDir.absolutePath() ),
                                i18nc( "Dialog box title", "Could not open chat history" ) );

      setLoading( false );
      return;
    }

    if( logParser_ == 0 )
    {
#ifdef KMESSDEBUG_CHATHISTORYDIALOG_VERBOSE
      kDebug() << "Starting log parser thread...";
#endif
      logParser_ = new XmlLogParser( showDate, showTime, showSeconds );
      connect( logParser_, SIGNAL( gotConversation(int,const QDomElement&) ),
               this,       SLOT  ( addConversation(int,const QDomElement&) ) );
      connect( logParser_, SIGNAL(        finished()                       ),
               this,       SLOT  (      setLoading()                       ) );
    }

#ifdef KMESSDEBUG_CHATHISTORYDIALOG_VERBOSE
    kDebug() << "Adding conversation to log parser thread...";
#endif
    logParser_->addDocument( entry.fileName(), file.readAll() );

    file.close();
  }

  if( list.isEmpty() )
  {
    setLoading( false );

#ifdef KMESSDEBUG_CHATHISTORYDIALOG_VERBOSE
    kDebug() << "No conversations available.";
#endif
  }
#ifdef KMESSDEBUG_CHATHISTORYDIALOG_VERBOSE
  else
  {
    kDebug() << "Conversations are available.";
  }
#endif

  // Restore the UI: the view will be reenabled by reloadLogs()
  listView_->setEnabled( true );
}



// Verify whether the chat date interval is valid or not
bool ChatHistoryDialog::checkDates( bool checkAndReloadLogs )
{
  // When the user first clicks on the "filter by date" radio button, neither the "from"
  // nor the "to" checkboxes are enabled, so show no chats.
  // It's better than showing *everything*, which may mean YEARS of chats.
  if( ! fromBox_->isChecked() && ! toBox_->isChecked() )
  {
#ifdef KMESSDEBUG_CHATHISTORYDIALOG_VERBOSE
    kDebug() << "No date filter chosen.";
#endif
    return false;
  }

  QDate fromDate( fromDate_->date() );
  QDate toDate  ( toDate_  ->date() );

  // Disallow negative date intervals
  if( fromDate > toDate )
  {
    toDate_->setDate( fromDate );

    // Setting the date calls this method for another check
    return false;
  }

  // Allow a maximum interval of one year of chats (and this may already
  // be an ENORMOUS amount of data, depending on the contact...)
  static const int maxDays = 365;
  int daysDifference = fromDate.daysTo( toDate ) - maxDays;
  if( daysDifference > 0 )
  {
#ifdef KMESSDEBUG_CHATHISTORYDIALOG_VERBOSE
    kDebug() << "Date difference is too long (" << maxDays << "days +" << daysDifference << "), fixing dates.";
#endif

    toDate = toDate.addDays( - daysDifference );
    toDate_->setDate( toDate );

    // Setting the date calls this method for another check
    return false;
  }

  if( checkAndReloadLogs )
  {
    reloadLogs();
  }

  return true;
}



// Load the list of contacts for which logs are available
void ChatHistoryDialog::loadContactsList()
{
  // Find all XML chat log files in the logs directory
  QDir logsDir( KMessConfig::instance()->getAccountDirectory( CurrentAccount::instance()->getHandle() )
                + "/chatlogs" );

  logsDir.setFilter( QDir::Files );
  logsDir.setNameFilters( QStringList() << "*.xml" );

  // Add them to the list
  const QFileInfoList &list = logsDir.entryInfoList();
  foreach( const QFileInfo &entry, list )
  {
    QString handle( entry.fileName() );
    handle.remove( handle.length() - 4, 4 );

    // Files ending with .1 should not be added
    if( handle.at( handle.length() - 1 ).isNumber() )
    {
      continue;
    }

    model_->appendRow( new QStandardItem( handle ) );
  }
}



// Reload the current chat log history
void ChatHistoryDialog::reloadLogs()
{
  // Never load logs immediately: wait an instant to allow the user to do more changes.
  // This makes the interface zippy and the user happy
  if( reloadLogsTimer_ == 0 )
  {
    reloadLogsTimer_ = new QTimer( this );
    reloadLogsTimer_->setSingleShot( true );
    connect( reloadLogsTimer_, SIGNAL( timeout() ),
             this,             SLOT  ( reloadLogs() ) );
  }
  if( reloadLogsTimer_ != sender() )
  {
#ifdef KMESSDEBUG_CHATHISTORYDIALOG
    kDebug() << "Scheduling reload...";
#endif
    reloadLogsTimer_->start( 250 );
    return;
  }

  // Reloading logs may be time consuming too, protect the operation from reentrancy
//   QMutexLocker lock( &addConversationLock_ );
  if( reloadingLogs_ )
  {
#ifdef KMESSDEBUG_CHATHISTORYDIALOG
    kDebug() << "Already running, exit.";
#endif
    return;
  }
  reloadingLogs_ = true;

  chatView_->widget()->setEnabled( false );

  if( conversations_.isEmpty() )
  {
#ifdef KMESSDEBUG_CHATHISTORYDIALOG
    kWarning() << "No cached conversations!";
#endif
    reloadingLogs_ = false;
    return;
  }

#ifdef KMESSDEBUG_CHATHISTORYDIALOG
  kDebug() << "Reloading...";
#endif

  // The text stream is used to dump the DOM nodes to an XML byte array.
  QString xmlData;
  QTextStream log( &xmlData );

  // Show a specific chat
  if( conversationRadio_->isChecked() )
  {
    // Get the timestamp of the requested chat from the combobox.
    int timestamp = conversationComboBox_->itemData( conversationComboBox_->currentIndex(),
                                                     Qt::UserRole ).toUInt();

#ifdef KMESSDEBUG_CHATHISTORYDIALOG_VERBOSE
    kDebug() << "Conversation timestamp:" << timestamp;
#endif

    if( ! conversations_.contains( timestamp ) )
    {
      kWarning() << "Chat not found!";
      reloadingLogs_ = false;
      return;
    }

    // Add the chat to the xml output
    log << conversations_[ timestamp ];
  }
  // There are two options only, but you *never* know.
  else if( dateRadio_->isChecked() )
  {
    // Assume the interval of dates makes sense: checkDates() fixes the dates for us.
    if( ! checkDates( false /*checkAndReloadLogs */ ) )
    {
      reloadingLogs_ = false;
      return;
    }

    // All chats in the specified time interval will be merged into one.

    // Add to the xml string the starting tag
    log << "<messageRoot>" ;

    QMapIterator<int,QDomNode> it( conversations_ );
    while( it.hasNext() )
    {
      it.next();
      const int       timestamp    = it.key();
      const QDomNode &conversation = it.value();
      const QDate &date = QDateTime::fromTime_t( timestamp ).date();

#ifdef KMESSDEBUG_CHATHISTORYDIALOG_VERBOSE
      kDebug() << "Conversation date:" << date;
#endif

      if( fromBox_->isChecked() && date < fromDate_->date() )
      {
#ifdef KMESSDEBUG_CHATHISTORYDIALOG_VERBOSE
        kDebug() << "Filtering out conversation on:" << date << "<" << fromDate_->date();
#endif
        continue;
      }
      else if( toBox_->isChecked() && date > toDate_->date() )
      {
#ifdef KMESSDEBUG_CHATHISTORYDIALOG_VERBOSE
        kDebug() << "Filtering out conversation on:" << date << ">" << toDate_->date()
                << "and all later ones";
#endif
        // We're at the last possible chat, skip the rest
        break;
      }

      // Add all messages of this conversation to the output
      const QDomNodeList &messages = conversation.childNodes();
      for( int index = 0; index < messages.size(); ++index )
      {
        log << messages.at( index );
      }

      // Without this, the data won't be fully sent to the byte array by the time setXml is called.
      log.flush();
    }

    // Add the closing tag
    log << "</messageRoot>";
  }
  else
  {
    kWarning() << "UI error, no chat selection choice was active!";
    reloadingLogs_ = false;
    return;
  }

  chatView_->setXml( xmlData );
  chatView_->widget()->setEnabled( true );

#ifdef KMESSDEBUG_CHATHISTORYDIALOG
  kDebug() << "Reload done.";
#endif

  reloadingLogs_ = false;
}



/**
 * Set the contact for whom the logs will be initially shown
 *
 * @param handle the handle to get contact history for
 * @returns whether there was history for the given contact contact
 */
bool ChatHistoryDialog::setContact( const QString &handle )
{
  if( handle.isEmpty() )
  {
#ifdef KMESSDEBUG_CHATHISTORYDIALOG
    kDebug() << "Handle is empty, pretending to have logs for the contact.";
#endif
    return true;
  }

  QList<QStandardItem*> items = model_->findItems( handle, Qt::MatchContains );

  if( items.isEmpty() )
  {
#ifdef KMESSDEBUG_CHATHISTORYDIALOG
    kDebug() << handle << "has no chat history";
#endif
    return false;
  }

  listView_->setCurrentIndex( proxyModel_->mapFromSource( items.first()->index() ) );

  return true;
}



// Set whether the dialog is loading chats or not
void ChatHistoryDialog::setLoading( bool isLoading )
{
  if( isLoading )
  {
    // Show the loading animation
    loadingLabel_->show();
    loadingLabel_->start();

    // Clear the chooser
    conversationComboBox_->clear();
    conversationComboBox_->insertItem( 0, i18nc( "Combo box default item", "Loading..." ), -1 );

    // Clear the view
    chatView_->clearView( true );

#ifdef KMESSDEBUG_CHATHISTORYDIALOG
    kDebug() << "Now loading...";
#endif
  }
  else
  {
    // Hide the loading animation
    loadingLabel_->stop();
    loadingLabel_->hide();

    if( conversationComboBox_->count() < 1
    ||  conversations_.isEmpty() )
    {
#ifdef KMESSDEBUG_CHATHISTORYDIALOG
      kDebug() << "No more loading - list empty.";
#endif
      conversationComboBox_->clear();
      conversationComboBox_->insertItem( 0, i18nc( "Combo box default item", "No logged chats" ), -1 );
    }
    else if( conversationComboBox_->itemData( 0, Qt::UserRole ) == -1 )
    {
      conversationComboBox_->removeItem( 0 );
#ifdef KMESSDEBUG_CHATHISTORYDIALOG
      kDebug() << "No more loading - removed temp item.";
#endif
    }
#ifdef KMESSDEBUG_CHATHISTORYDIALOG
    else
    {
      kDebug() << "Wtf?";
    }
#endif
  }

  chatView_->widget()->setEnabled( ! isLoading );
}




// Worker thread's constructor
XmlLogParser::XmlLogParser( bool showDate, bool showTime, bool showSeconds )
: end_( false )
, showDate_( showDate )
, showTime_( showTime )
, showSeconds_( showSeconds )
{
}



// Add another document to the worker thread's queue
void XmlLogParser::addDocument( const QString &fileName, const QByteArray &newDocument )
{
  documents_[ fileName ] = newDocument;

  if( ! isRunning() )
  {
    start( QThread::HighPriority );
  }
}



// Stop the worker thread
void XmlLogParser::end()
{
  disconnect( this );
  end_ = true;

  quit();
  deleteLater();
}



// Worker thread's method to parse XML files into conversations
void XmlLogParser::run()
{
#ifdef KMESSDEBUG_CHATHISTORYDIALOG_VERBOSE
  kDebug() << "Initially present:" << documents_.count() << "documents.";
#endif

  while( ! documents_.isEmpty() )
  {
    QHash<const QString,QByteArray>::iterator it = documents_.begin();
    const QString &fileName      = it.key();
    QByteArray    &documentBytes = it.value();

    QDomDocument document;
    document.setContent( documentBytes );

#ifdef KMESSDEBUG_CHATHISTORYDIALOG_VERBOSE
    kDebug() << "Parsing document from XML file:" << fileName;
#endif

    // There is no message root in this XML file
    QDomNode messageRoot( document.firstChildElement( "messageRoot" ) );
    if( messageRoot.isNull() )
    {
      kWarning() << "Invalid XML file!";
      continue;
    }

    // Since in the chat logs only the message timestamps are saved, add to each message
    // the date and time in the current format
    const QDomNodeList &messages = document.elementsByTagName( "message" );
#ifdef KMESSDEBUG_CHATHISTORYDIALOG_VERBOSE
    kDebug() << messages.size() << "messages to parse.";
#endif
    for( int indexMsg = 0; indexMsg < messages.size(); ++indexMsg )
    {
      QDomElement message( messages.at( indexMsg ).toElement() );
      int timestamp = message.attribute( "timestamp" ).toUInt();
      const QDateTime &datetime = QDateTime::fromTime_t( timestamp );

      if( ! message.hasAttribute( "timestamp" ) || timestamp == 0 )
      {
        continue;
      }

      if( end_ )
      {
        kWarning() << "Caught end, closing.";
        return;
      }

      if ( showDate_ )
      {
        message.setAttribute( "time", KGlobal::locale()->formatDateTime( datetime, KLocale::ShortDate, showSeconds_ ) );
      }
      else
      {
        message.setAttribute( "time", KGlobal::locale()->formatTime( datetime.time(), showSeconds_ ) );
      }
    }
#ifdef KMESSDEBUG_CHATHISTORYDIALOG_VERBOSE
    kDebug() << messages.size() << "messages parsed.";
#endif

    // Add all conversations from this file to the cached XML and the UI
    const QDomNodeList &fileConversations = messageRoot.childNodes();
    for( int index = 0; index < fileConversations.size(); ++index )
    {
      QDomElement conversation( fileConversations.at( index ).toElement() );

      if( conversation.nodeName() != "conversation" )
      {
        kWarning() << "Unexpected element" << conversation.nodeName() << "found in the XML log:"
                    << fileName;
        continue;
      }


      // Check if the <conversation> has a valid timestamp
      const QDomNode &timestampNode = conversation.attributes().namedItem( "timestamp" );
      const int timestamp = timestampNode.nodeValue().toInt();

      if( timestampNode.isNull() )
      {
#ifdef KMESSDEBUG_CHATHISTORYDIALOG
        kWarning() << "Invalid XML: invalid timestamp!";
#endif
        continue;
      }

      if( end_ )
      {
        kWarning() << "Caught end, closing.";
        return;
      }

      // Change it to a <messageRoot> so that it'll be readily usable when selected
      conversation.setTagName( "messageRoot" );
//       conversation.removeAttribute( "timestamp" );  // Ignored by ChatMessageView?

      emit gotConversation( timestamp, conversation );
    }

    // Document done, remove it from the queue
    documents_.remove( fileName );

    if( end_ )
    {
      kWarning() << "Caught end, closing.";
      return;
    }
  }

#ifdef KMESSDEBUG_CHATHISTORYDIALOG_VERBOSE
  kDebug() << "All documents have been parsed.";
#endif
}



#include "chathistorydialog.moc"

Generated by  Doxygen 1.6.0   Back to index