Files
kmess/src/chat/chat.cpp
T
2026-07-06 09:37:59 +02:00

1754 lines
55 KiB
C++

/***************************************************************************
chat.cpp - description
-------------------
begin : Wed Jan 15 22:41:32 CST 2003
copyright : (C) 2003 by Mike K. Bennett
email : mkb137b@hotmail.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 "chat.h"
#include "../contact/contactextension.h"
#include "../contact/invitedcontact.h"
#include "../contact/msnobject.h"
#include "../model/contactlist.h"
#include "../utils/kmessconfig.h"
#include "../utils/kmessshared.h"
#include "../currentaccount.h"
#include "../kmessapplication.h"
#include "../kmessdebug.h"
#include "../kmess.h"
#include "chathistorymanager.h"
#include "chathistorywriter.h"
#include "chatmessageview.h"
#include "chatwindow.h"
#include "contactswidget.h"
#include <QDir>
#include <QElapsedTimer>
#include <QFile>
#include <QRegExp>
#include <QUrl>
#include <QFileDialog>
#include <KLocalizedString>
#include <KMessageBox>
#ifdef KMESSDEBUG_CHAT
#define KMESSDEBUG_CHAT_GENERAL
#define KMESSDEBUG_CHAT_CONTACTS
#define KMESSDEBUG_CHAT_FILETRANSFER
// #define KMESSDEBUG_CHAT_TYPING_MESSAGES
#endif
// The constructor
Chat::Chat( QWidget *parent )
: ChatView( parent )
, firstMessage_( true )
, initialized_( false )
, logWriter_( 0 )
, msnSwitchboardConnection_( 0 )
{
hide();
// Setup the contact typing information timer
typingTimer_.setSingleShot( true );
connect( &typingTimer_, SIGNAL( timeout() ),
this, SLOT ( contactTyping() ) );
}
// The destructor
Chat::~Chat()
{
#ifdef KMESSDEBUG_CHAT_GENERAL
kmDebug() << "DESTROYED.";
#endif
}
// Choose the contact to start an invitation with.
const QString Chat::chooseContact()
{
if( KMESS_NULL(msnSwitchboardConnection_) ) return QString();
// Choose the contact.
const QStringList &contactsInChat = msnSwitchboardConnection_->getContactsInChat();
switch( contactsInChat.count() )
{
case 0:
// No contacts in the chat
if( msnSwitchboardConnection_->getLastContact().isEmpty() )
{
// Send invitation to the first contact that should appear in the chat.
return msnSwitchboardConnection_->getFirstContact();
}
else
{
// Resume chat with last contact
return msnSwitchboardConnection_->getLastContact();
}
case 1:
// Choose the only contact available in the chat
return contactsInChat.first();
default:
// Multiple contacts in the chat.
// TODO: The official client opens a contact-choose dialog here, and starts a new chat with the selected contact.
KMessageBox::error( this, i18nc( "Error dialog box text",
"You cannot send invitations when there are multiple contacts in a chat. "
"Please start a separate chat with the contact you wanted to send the invitation to." ) );
return QString();
}
}
// A contact joined the chat
void Chat::contactJoined( ContactBase* contact, bool offlineContact )
{
// Don't replace the bool offlineContact with contact->isOffline(), because there may be
// real chats with invisible contacts
const QString& handle = contact->getHandle();
const QString& friendlyName = contact->getFriendlyName( STRING_CLEANED_ESCAPED );
const QStringList participants ( getParticipants() );
// Don't show the message when the contact went/is offline (because in such cases, the message
// is unreliable, and if we started the chat we're just pretending the contact joined)
if( ! offlineContact && ( currentAccount_->getShowSessionInfo() || participants.count() > 1 ) )
{
showMessage( ChatMessage( ChatMessage::TYPE_PRESENCE,
ChatMessage::CONTENT_PRESENCE_JOIN,
false,
i18n("%1 has joined the chat.", friendlyName ),
handle,
friendlyName ) );
}
// Change the chat title
emit chatInfoChanged();
// Allow future changes in the nickname and status to update the chat informations
if( contact )
{
// disconnect any old signals, just to be sure
disconnect( contact, 0, this, 0 );
connect( contact, SIGNAL( changedStatus() ),
this, SLOT ( slotContactChangedStatus() ) );
connect( contact, SIGNAL( changedStatus() ),
this, SIGNAL( chatInfoChanged() ) );
connect( contact, SIGNAL( changedFriendlyName() ),
this, SIGNAL( chatInfoChanged() ) );
}
}
// A contact left the chat
void Chat::contactLeft( ContactBase *contact, bool isChatIdle )
{
const QString& handle = contact->getHandle();
const QStringList participants ( getParticipants() );
#ifdef KMESSDEBUG_CHAT_GENERAL
kmDebug() << "contact '" << handle << "' has left" << (isChatIdle ? " due to inactivity" : "") << ".";
#endif
// A contact has left the chat.
// Group chats: it gets disconnected completely from it
// 1-on-1 chats: the chat UI will still show the contact, so don't disconnect its update signals
if( participants.count() > 1 )
{
disconnect( contact, 0, this, 0 );
}
// Always show a message when this is a group chat,
// but never if it would be the first message.
if( ! firstMessage_ && ( currentAccount_->getShowSessionInfo() || participants.count() > 1 ) )
{
const QString& contactName = contact->getFriendlyName( STRING_CLEANED_ESCAPED );
QString message;
if( isChatIdle )
{
message = i18nc( "Message shown in chat, %1 is the contact's friendly name",
"The chat went idle, %1 has left it.",
contactName );
}
else
{
message = i18nc( "Message shown in chat, %1 is the contact's friendly name",
"%1 has left the chat.",
contactName );
}
showMessage( ChatMessage( ChatMessage::TYPE_PRESENCE,
ChatMessage::CONTENT_PRESENCE_LEAVE,
false,
message,
handle,
contactName ) );
}
// Change the caption to remove the contact who has left
emit chatInfoChanged();
}
// A contact is typing
// If called without arguments, it will only update the list of typing contacts.
void Chat::contactTyping( ContactBase *contact, bool forceExpiration )
{
int secsTo;
int nextUpdate = CHAT_TYPING_EXPIRATION_TIME; // Allow a maximum number of seconds between each update
QString handle;
QString friendlyName;
if( contact != 0 )
{
handle = contact->getHandle();
friendlyName = contact->getFriendlyName( STRING_CLEANED );
if( friendlyName.isEmpty() ) // If the contact has no friendlyName, use its handle as name
{
friendlyName = handle;
}
}
#ifdef KMESSDEBUG_CHAT_TYPING_MESSAGES
kmDebug() << "Called with forceExpiration=" << forceExpiration << ", handle=" << handle << ", friendlyName=" << friendlyName;
kmDebug() << "Current list:" << typingContactsNames_.keys();
#endif
// Do not add anything to the list if the friendly name is empty
if( ! handle.isEmpty() )
{
QElapsedTimer startTime;
// If a contact is still typing, re-insert it in the list, so that the typing start time is updated
if( typingContactsNames_.contains( handle ) )
{
#ifdef KMESSDEBUG_CHAT_TYPING_MESSAGES
kmDebug() << "Removing" << handle << "from the list.";
#endif
typingContactsNames_.remove( handle );
typingContactsTimes_.remove( handle );
}
// When we receive a message from this contact, we only remove it from the typing contacts
if( ! forceExpiration )
{
#ifdef KMESSDEBUG_CHAT_TYPING_MESSAGES
kmDebug() << "Adding" << handle << "to the list.";
#endif
// Add this contact to the list of contacts which are typing, and record when it has started writing
startTime.start();
typingContactsNames_.insert( handle, friendlyName );
typingContactsTimes_.insert( handle, startTime );
}
#ifdef KMESSDEBUG_CHAT_TYPING_MESSAGES
else
{
kmDebug() << "Removal from the list was forced.";
}
#endif
}
// Update the list to remove expired typing events
QMutableHashIterator<QString,QElapsedTimer> it( typingContactsTimes_ );
while( it.hasNext() )
{
it.next();
secsTo = (int) ( it.value().elapsed() * .001 ); // Only keep the seconds
// If the typing event has expired, stop displaying it
if( secsTo >= CHAT_TYPING_EXPIRATION_TIME )
{
#ifdef KMESSDEBUG_CHAT_TYPING_MESSAGES
kmDebug() << "Removing" << handle << "from the list due to expiration.";
#endif
typingContactsNames_.remove( it.key() );
typingContactsTimes_.remove( it.key() );
}
// Find the typing event which will expire first, so we can next update when it expires
if( secsTo && secsTo < nextUpdate )
{
nextUpdate = secsTo;
}
}
#ifdef KMESSDEBUG_CHAT_TYPING_MESSAGES
kmDebug() << "List after cleanup and update:" << typingContactsNames_.keys();
#endif
// The more time passes from the last update, the more we get close to the next update
if( nextUpdate < CHAT_TYPING_EXPIRATION_TIME )
{
nextUpdate = CHAT_TYPING_EXPIRATION_TIME - nextUpdate;
}
#ifdef KMESSDEBUG_CHAT_TYPING_MESSAGES
kmDebug() << "Next update in" << nextUpdate << "seconds.";
#endif
// Update the user interface
if( typingContactsNames_.contains( handle ) )
{
contactsWidget_->contactTyping( contact );
}
else
{
contactsWidget_->messageReceived( handle );
}
#ifdef KMESSDEBUG_CHAT_TYPING_MESSAGES
kmDebug() << "emitting signal from:" << this;
#endif
// Let the other window components update, too
emit gotTypingMessage( this );
// Activate the timer for the next update if needed
if( ! typingContactsTimes_.isEmpty() )
{
typingTimer_.start( nextUpdate * 1000 );
}
}
// Return a list of the contacts in the chat to be used as window caption
const QString Chat::getCaption()
{
// Grep the current participants or the last participant
const QStringList participants( getParticipants() );
QString caption;
// How many people are there in here?
switch( participants.count() )
{
// The chat is completely empty - it is not even a chat!
case 0:
break;
// One contact is connected; or the only contact of the session has left
case 1:
caption = currentAccount_->getContactFriendlyNameByHandle( participants[0], STRING_CLEANED );
break;
// Two contacts are connected
case 2:
caption = i18nc( "Name of a chat tab", "%1 and %2",
currentAccount_->getContactFriendlyNameByHandle( participants[0], STRING_CLEANED ),
currentAccount_->getContactFriendlyNameByHandle( participants[1], STRING_CLEANED )
);
break;
// Three or more contacts are connected
default:
caption = i18nc( "Name of a chat tab", "%1 et al.",
currentAccount_->getContactFriendlyNameByHandle( participants[0], STRING_CLEANED )
);
}
// Replace the 'return chars' with 'space chars'
return caption.replace( "\n", " " );
}
// Return the chat window which currently contains this Chat
ChatWindow *Chat::getChatWindow()
{
return qobject_cast<ChatWindow*>( window() );
}
// Return the list of contacts in the chat
const QStringList Chat::getParticipants() const
{
if( msnSwitchboardConnection_ == 0 )
{
return lastKnownContacts_;
}
return msnSwitchboardConnection_->getContactsInChat();
}
// Return the icon to use in the tab icon chat
QIcon Chat::getParticipantsTabIcon()
{
const QStringList participants( getParticipants() );
// If there are more participants use group icon
if( participants.count() != 1 )
{
return QIcon::fromTheme( "system-users" );
}
// Else use the status icon of the current contact
const QString& handle = participants.first();
// Find the contact, if it isn't in the contact list, use the "unknown" tab icon
const ContactBase *contact = currentAccount_->getContactByHandle( handle );
if( contact == 0 )
{
return QIcon::fromTheme( "view-media-artist" );
}
return QIcon( MsnStatus::getIcon( contact->getStatus() ) );
}
// Return the list of previously sent messages
QStringList& Chat::getQuickRetypeList()
{
return quickRetypeList_;
}
// Return the list of contacts which are typing
const QStringList Chat::getTypingContacts() const
{
return typingContactsNames_.values();
}
// Return the date and time the chat has started
QDateTime Chat::getStartTime() const
{
return QDateTime( startDate_, startTime_ );
}
// Return the switchboard connection used by the chat window.
MsnSwitchboardConnection * Chat::getSwitchboardConnection() const
{
return msnSwitchboardConnection_;
}
// Initialize the object
bool Chat::initialize( MsnSwitchboardConnection *switchboardConnection )
{
if ( initialized_ )
{
kmDebug() << "already initialized.";
return false;
}
if( ! ChatView::initialize() )
{
kmDebug() << "Couldn't setup the chat view widget.";
return false;
}
// Connect the contacts widget's signals
const ContactList *contactList = currentAccount_->getContactList();
connect( contactsWidget_, SIGNAL( contactAllowed(QString) ),
this, SIGNAL( contactAllowed(QString) ) );
connect( contactsWidget_, SIGNAL( contactAdded(QString,bool) ),
this, SIGNAL( contactAdded(QString,bool) ) );
connect( contactsWidget_, SIGNAL( contactBlocked(QString,bool) ),
this, SIGNAL( contactBlocked(QString,bool) ) );
connect( contactsWidget_, SIGNAL( startPrivateChat( const QString ) ),
this, SIGNAL( startPrivateChat( const QString ) ) );
connect( contactList, SIGNAL( contactAdded(Contact*) ),
contactsWidget_, SLOT ( contactAdded(Contact*) ) );
connect( contactList, SIGNAL( contactRemoved(Contact*) ),
contactsWidget_, SLOT ( contactRemoved(Contact*) ) );
// Save the switchboard connection
setSwitchboardConnection( switchboardConnection );
// Save the date/time the chat started.
// Use it to save a chatlog later.
startDate_ = QDate::currentDate();
startTime_ = QTime::currentTime();
// Update the chat window caption with the contact name
emit chatInfoChanged();
initialized_ = true;
return true;
}
// Invite a contact to the chat
void Chat::inviteContacts( const QStringList &contacts )
{
if( msnSwitchboardConnection_ == 0 )
{
return;
}
// Invite each selected contact, if it's not in chat already
foreach( const QString &handle, contacts )
{
if( ! isContactInChat( handle, false ) )
{
msnSwitchboardConnection_->inviteContact( handle );
}
}
}
// Return whether or not the chat is at its first incoming message
bool Chat::isChatFirstMessage()
{
return firstMessage_;
}
// Return whether or not the contact is in this chat.
bool Chat::isContactInChat( const QString &handle, bool isExclusiveChatWithContact )
{
const QStringList participants( getParticipants() );
#ifdef KMESSDEBUG_CHAT_CONTACTS
kmDebug() << "Is" << handle << "in chat" << this << "?"
<< "1-on-1 required?" << isExclusiveChatWithContact
<< "Participants:" << participants
<< "Has switchboard?" << ( msnSwitchboardConnection_ != 0 )
<< "Last known contacts:" << lastKnownContacts_;
#endif
if( participants.isEmpty() )
{
return false;
}
// Check if the contact is one of the participants to this chat.
bool check = participants.contains( handle );
// Check if is exclusive chat with the contact passed as the argument
if( check && isExclusiveChatWithContact )
{
check = ( participants.count() == 1 );
}
#ifdef KMESSDEBUG_CHAT_CONTACTS
kmDebug() << "Returning" << check;
#endif
return check;
}
// The chat is closing
void Chat::queryClose()
{
#ifdef KMESSDEBUG_CHAT_GENERAL
kmDebug() << "Saving chat log";
#endif
// Hide it now, to increase responsiveness
hide();
// Notify ChatMaster
emit closing( this );
// Save the chat logs
endChatLogging();
/**
* TODO: Uncomment this part: it needs a _reliable_ method to check whether there are running file
* transfers. The commented part uses "ApplicationList::isEmpty()" which I've removed, the new
* method to check will probably be elsewhere. The main code of that method was this:
* <code>
// check if there are active applications
if ( mimeApplications_.count() +
p2pApplications_.count() -
abortingApplications_.count() == 0)
return true; else return false;
* </code>
*/
/*
// If there are active applications show a warning to the user
if( participants_.count() == 1 ) // Transfers are not possibile in multiple chats
{
// Check if there are active applications
ContactBase *contact = currentAccount_->getContactByHandle( participants_.first() );
if( contact != 0 && contact->getApplicationList() != 0
&& ! contact->getApplicationList()->isEmpty() )
{
int choice = KMessageBox::warningContinueCancel( this, i18n("Continue closing the chat window?\nActive transfers will be aborted!"));
if( choice == KMessageBox::Cancel )
{
return false;
}
}
}
*/
}
// The application is exiting
void Chat::queryExit()
{
// Hide it now, to increase responsiveness
hide();
#ifdef KMESSDEBUG_CHAT_GENERAL
kmDebug() << "Saving chat log";
#endif
// Just save the chat before going down
endChatLogging();
}
// A message was received from a contact.
void Chat::receivedMessage(const ChatMessage &message)
{
#ifdef KMESSDEBUG_CHAT_GENERAL
kmDebug() << "Displaying message, internal type=" << message.getType();
kmDebug() << "First message = " << firstMessage_ << ".";
#endif
showMessage( message );
// Do not notify about presence messages
if( message.getType() == ChatMessage::TYPE_PRESENCE )
{
return;
}
// Update the date of the last received message. Only allowed for contacts on the contact list.
Contact *sourceContact = currentAccount_->getContactList()->getContactByHandle( message.getContactHandle() );
if( sourceContact != 0 && sourceContact->getExtension() )
{
sourceContact->getExtension()->setLastMessageDate();
}
// Check if we should send an automatic reply to the chat message
if( currentAccount_ != 0
&& msnSwitchboardConnection_ != 0
&& currentAccount_->getAutoreply()
&& ( ! lastSentAutoMessage_.isValid() || lastSentAutoMessage_.elapsed() > 120000 ) )
{
// Send an auto away message every two minutes (if the contact keeps writing)
#ifdef KMESSDEBUG_CHAT_GENERAL
kmDebug() << "Sending auto reply to chat message.";
#endif
QString awayMessage( i18nc( "Automatic reply message",
"%1 (This message was sent automatically)",
currentAccount_->getAutoreplyMessage() ) );
// Send the autoreply message to the contact(s)
msnSwitchboardConnection_->sendChatMessage( awayMessage );
// Show the autoreply message
ChatMessage autoMessage = ChatMessage( ChatMessage::TYPE_OUTGOING,
ChatMessage::CONTENT_MESSAGE,
false,
awayMessage,
currentAccount_->getHandle(),
currentAccount_->getFriendlyName( STRING_ORIGINAL ),
currentAccount_->getPicturePath(),
currentAccount_->getFont(),
currentAccount_->getFontColor() );
showMessage( autoMessage );
// Save the time the message was sent, to display the next after some time
lastSentAutoMessage_.start();
}
// Remove this contact from the list of typing messages and let the parent window to update
// this chat's typing information
contactTyping( sourceContact, true );
// Emit the new message signal so that the user will receive a notification
emit gotChatMessage( message, this );
// If this was the first received message, clear the flag
if( firstMessage_ )
{
firstMessage_ = false;
}
}
/**
* End the chat log.
*/
void Chat::endChatLogging()
{
kmDebug() << "Ending chat logging";
// End the log writing session, if any
delete logWriter_;
logWriter_ = 0;
if( ! currentAccount_->getSaveChatsToFile() )
{
#ifdef KMESSDEBUG_CHAT_GENERAL
kmDebug() << "User has disabled exporting chats to file. Canceling.";
#endif
return;
}
// Check if the message area is empty, we won't save empty chats
if( isEmpty() )
{
#ifdef KMESSDEBUG_CHAT_GENERAL
kmDebug() << "Message area is empty, not exporting chat.";
#endif
return;
}
KMessApplication *kmessApp = static_cast<KMessApplication *>(QApplication::instance());
bool canDoUserInteraction = ! kmessApp->quitSelected(); // Not if we're exiting
QDir dir;
QString fileName;
QString extension;
Account::ChatExportFormat format;
// Set the base file name, same for all logs
const QStringList participants( getParticipants() );
if( participants.isEmpty() )
{
// Should never, never, ever happen.
fileName = "KMess";
}
else
{
fileName = participants.first();
}
dir.setPath( currentAccount_->getSaveChatPath() );
format = currentAccount_->getSaveChatsFormat();
switch( format )
{
case Account::EXPORT_TEXT:
extension = "txt";
break;
case Account::EXPORT_HTML:
extension = "html";
break;
default:
kmWarning() << "Extension is unknown for save format" << format << "! Using \"html\".";
extension = "html";
format = Account::EXPORT_HTML;
break;
}
const QString year ( startDate_.toString( "yyyy" ) );
const QString month( startDate_.toString( "MM" ) );
const QString day ( startDate_.toString( "dd" ) );
// Determine the path where to save the log file
switch( currentAccount_->getSavedChatDirectoryStructure() )
{
// Save as <logs-path>/yyyy/contactemail.ext
case Account::BYYEAR:
dir.setPath( dir.absolutePath() + "/" + year );
break;
// Save as <logs-path>/yyyy/mm/contactemail.ext
case Account::BYMONTH:
dir.setPath( dir.absolutePath() + "/" + year + "/" + month );
break;
// Save as <logs-path>/yyyy/mm/dd/contactemail.ext
case Account::BYDAY:
dir.setPath( dir.absolutePath() + "/" + year + "/" + month + "/" + day );
break;
// Save as <logs-path>/contactemail.ext
case Account::SINGLEDIRECTORY:
default:
break;
}
// Try creating the directory if it doesn't exist
if( ! dir.exists() )
{
dir.mkpath( dir.absolutePath() );
}
if( ! dir.exists() ) // It couldn't be created
{
// Always warn via debug log, and also via a messagebox if we're not exiting
kmWarning() << "Unable to save the chat log. The base directory" << dir.absolutePath() << "doesn't exist and could not be created.";
if( canDoUserInteraction )
{
KMessageBox::error( getChatWindow(),
i18n( "<html>KMess could not save the log for the chat with &quot;%1&quot;:<br />"
"The chat logs directory, &quot;%2&quot;, does not exist.</html>",
currentAccount_->getContactFriendlyNameByHandle( participants.first() ),
dir.absolutePath() ) );
}
return;
}
// Obtain the file path to be used to export the chat
QFile logFile;
QFileInfo fileInfo;
QString lastFile;
QString nextFile( KMessShared::nextSequentialFile( dir.absolutePath(), fileName, extension, lastFile ) );
fileInfo.setFile( lastFile );
// Where there already is a log file for this contact, verify if we can
// append to it. Log files should not be too big
if( lastFile.isEmpty() || ! fileInfo.isWritable() || fileInfo.size() > MAX_LOG_FILE_SIZE )
{
#ifdef KMESSDEBUG_CHAT_GENERAL
if( lastFile.isEmpty() )
{
kmDebug() << "No file to append to, creating a new file:" << nextFile;
}
else
{
kmDebug() << "Last file" << lastFile << "cannot be used, creating a new file:" << nextFile;
}
#endif
fileInfo.setFile( nextFile );
}
else
{
#ifdef KMESSDEBUG_CHAT_GENERAL
kmDebug() << "Appending to last chat log file:" << lastFile;
#endif
}
// Save the chat in the format requested by the user
if( ! saveChatToFile( fileInfo.absoluteFilePath(),
format,
false, /* Don't overwrite */
canDoUserInteraction /* No user interaction */ ) )
{
kmWarning() << "Exporting chat log to file" << fileInfo.absoluteFilePath() << "failed!";
// Try again with a new file if we were trying to append to the last one
if( fileInfo.absoluteFilePath() == lastFile )
{
fileInfo.setFile( nextFile );
if( ! saveChatToFile( fileInfo.absoluteFilePath(),
format,
false, /* Don't overwrite */
canDoUserInteraction /* No user interaction */ ) )
{
kmWarning() << "Second attempt at exporting chat log to file" << fileInfo.absoluteFilePath() << "failed!";
}
}
}
}
// Send a message to the current chat
void Chat::sendChatMessage( const QString &message )
{
#ifdef KMESSTEST
KMESS_ASSERT( msnSwitchboardConnection_ );
#endif
// If we send the first message of the chat, clear the first message status
if( firstMessage_ )
{
firstMessage_ = false;
}
msnSwitchboardConnection_->sendChatMessage( message );
}
// Send an ink drawing to the current chat
void Chat::sendInkMessage( InkFormat format, const QByteArray &inkData )
{
#ifdef KMESSTEST
KMESS_ASSERT( msnSwitchboardConnection_ );
#endif
msnSwitchboardConnection_->sendInk( format, inkData );
}
// Reimplemented from ChatView to support logging
void Chat::showMessage( const ChatMessage& message )
{
// Log the message
if( currentAccount_->getSaveChats() && logWriter_ )
{
logWriter_->logMessage( message );
}
ChatView::showMessage( message );
}
// Send an "user is typing" message to the contacts
void Chat::sendTypingMessage()
{
if( KMESS_NULL(msnSwitchboardConnection_) ) return;
msnSwitchboardConnection_->sendTypingMessage();
}
// Send an wink to the current chat
void Chat::sendWink( const MsnObject &msnObject )
{
QString winkHtml;
msnSwitchboardConnection_->sendWink( msnObject );
// Only show the sent wink if they're enabled
if( currentAccount_->getShowWinks() )
{
WinksWidget::getHtmlFromWink( msnObject.getLocation(), winkHtml );
}
// If the stored wink didn't have a name, don't show it:
QString notificationText;
if( msnObject.getFriendly().isEmpty() )
{
notificationText = i18nc( "Message shown in the chat window (when the wink name is unknown)",
"You have sent a wink!" );
}
else
{
notificationText = i18nc( "Message shown in the chat window, %1 is the wink name",
"You have sent the &quot;%1&quot; wink!",
msnObject.getFriendly() );
}
ChatMessage message( ChatMessage::TYPE_NOTIFICATION,
ChatMessage::CONTENT_NOTIFICATION_WINK,
false,
notificationText + winkHtml, // Add the actual wink after the message
QString() );
showMessage( message );
// Log the message
if( currentAccount_->getSaveChats() && logWriter_ )
{
logWriter_->logMessage( message );
}
}
// The user wants to block or unblock a contact.
void Chat::setContactBlocked( QString handle, bool isBlocked )
{
emit contactBlocked( handle, isBlocked );
}
// Enable or disable the parts of the window which allow user interaction
void Chat::setEnabled( bool isEnabled )
{
// Also remove the switchboard connection when disabling
if( ! isEnabled && msnSwitchboardConnection_ != 0 )
{
setSwitchboardConnection( 0 );
}
if( ! isEnabled )
{
getChatWindow()->showStatusMessage( ChatStatusBar::Disconnected, i18n("The chat has been disabled because you are no longer connected to the Live Messenger server." ));
}
else
{
getChatWindow()->showStatusMessage( ChatStatusBar::DefaultType, " " );
}
// Do not reactivate if there is no switchboard connection (ChatMaster will recreate it for us)
if( isEnabled && msnSwitchboardConnection_ == 0 )
{
return;
}
ChatView::setEnabled( isEnabled );
if ( isEnabled )
{
getChatWindow()->setUIState( ChatWindow::Connected );
}
}
// Change the switchboard connection used by the chat window.
void Chat::setSwitchboardConnection( MsnSwitchboardConnection *newConnection )
{
// Everything is connected already.
#ifdef KMESSDEBUG_CHAT_GENERAL
if( msnSwitchboardConnection_ == 0 )
{
kmDebug() << "Switchboard was not set up.";
}
else
{
kmDebug() << "Switchboard was "
<< ( msnSwitchboardConnection_ == newConnection ? "already set" : "different" ) << "." << endl;
}
#endif
if( msnSwitchboardConnection_ != 0 )
{
// Notify that all contacts have left the chat
if( ! msnSwitchboardConnection_->isEmpty() )
{
const QStringList participants( getParticipants() );
#ifdef KMESSDEBUG_CHAT_GENERAL
kmDebug() << "Removing old switchboard's participants from the chat:" << participants;
#endif
foreach( const QString &handle, participants )
{
ContactBase* contact = currentAccount_->getContactByHandle( handle );
if( contact == 0 )
{
contact = currentAccount_->addInvitedContact( handle );
}
// NOTE: This slot doesn't use the second parameter
contactsWidget_->contactLeft( contact, false );
}
}
// Delete all signals between us and the old switchboard
disconnect( this, 0, msnSwitchboardConnection_, 0 );
disconnect( msnSwitchboardConnection_, 0, contactsWidget_, 0 );
disconnect( msnSwitchboardConnection_, 0, this, 0 );
// We are switching switchboards, clean up
if( msnSwitchboardConnection_ != newConnection )
{
// Some info of the previous switchboard must be recorded to allow
// retrieving details about the chat when no switchboard is present
lastKnownContacts_ = msnSwitchboardConnection_->getContactsInChat();
// Also delete the old connection
msnSwitchboardConnection_->deleteLater();
}
}
msnSwitchboardConnection_ = newConnection;
// The old connection has been removed, we're done
if( newConnection == 0 )
{
return;
}
// Make the new signals connections
connect( msnSwitchboardConnection_, SIGNAL( contactJoinedChat(ContactBase*) ),
contactsWidget_, SLOT ( contactJoined(ContactBase*) ) );
connect( msnSwitchboardConnection_, SIGNAL( contactLeftChat(ContactBase*, bool) ),
contactsWidget_, SLOT ( contactLeft(ContactBase*, bool) ) );
connect( msnSwitchboardConnection_, SIGNAL( contactTyping(ContactBase*) ),
contactsWidget_, SLOT ( contactTyping(ContactBase*) ) );
connect( msnSwitchboardConnection_, SIGNAL( contactJoinedChat(ContactBase*) ),
this, SLOT ( contactJoined(ContactBase*) ) );
connect( msnSwitchboardConnection_, SIGNAL( contactLeftChat(ContactBase*, bool) ),
this, SLOT ( contactLeft(ContactBase*, bool) ) );
connect( msnSwitchboardConnection_, SIGNAL( contactTyping(ContactBase*) ),
this, SLOT ( contactTyping(ContactBase*) ) );
connect( msnSwitchboardConnection_, SIGNAL( chatMessage(const ChatMessage&) ),
this, SLOT ( receivedMessage(const ChatMessage&) ) );
connect( msnSwitchboardConnection_, SIGNAL( receivedNudge(ContactBase*) ),
this, SLOT ( slotReceivedNudge(ContactBase*) ) );
connect( msnSwitchboardConnection_, SIGNAL( sendingFailed(const QString&,const MimeMessage&)),
this, SLOT ( slotSendingFailed(const QString&,const MimeMessage&)) );
connect( msnSwitchboardConnection_, SIGNAL( showWarning(MsnSwitchboardConnection::WarningType,ContactBase*)),
this, SLOT ( showWarning(MsnSwitchboardConnection::WarningType,ContactBase*)) );
connect( msnSwitchboardConnection_, SIGNAL( deleteMe(MsnSwitchboardConnection*) ),
this, SLOT ( setSwitchboardConnection() ) );
// Add to the contacts widget all the contacts that are present in the new switchboard
const QStringList participants( getParticipants() );
#ifdef KMESSDEBUG_CHAT_GENERAL
kmDebug() << "Adding new switchboard's participants to the chat:" << participants;
#endif
foreach( const QString &handle, participants )
{
ContactBase *contact = currentAccount_->getContactByHandle( handle );
if( contact == 0 )
{
contact = currentAccount_->addInvitedContact( handle );
}
contactJoined( contact, true /* don't notify about the join */ );
contactsWidget_->contactJoined( contact );
}
}
// A warning from the switchboard has been received, display it
void Chat::showWarning( MsnSwitchboardConnection::WarningType type, ContactBase *contact )
{
QString handle, friendlyName;
if( contact == 0 )
{
handle = currentAccount_->getHandle();
friendlyName = currentAccount_->getFriendlyName( STRING_CLEANED_ESCAPED );
}
else
{
handle = contact->getHandle();
friendlyName = contact->getFriendlyName( STRING_CLEANED_ESCAPED );
}
switch( type )
{
case MsnSwitchboardConnection::WARNING_CONNECTION_DROP:
showMessage( ChatMessage( ChatMessage::TYPE_SYSTEM,
ChatMessage::CONTENT_SYSTEM_ERROR,
true,
i18nc( "Warning message shown in chat",
"There has been a connection problem." ),
handle,
friendlyName ) );
break;
case MsnSwitchboardConnection::WARNING_TOO_MANY_EMOTICONS:
showMessage( ChatMessage( ChatMessage::TYPE_SYSTEM,
ChatMessage::CONTENT_SYSTEM_ERROR,
true,
i18nc( "Warning message shown in chat",
"There were too many different custom emoticons in your last message. Only the first 7 will be sent." ),
handle,
friendlyName ) );
break;
case MsnSwitchboardConnection::WARNING_UNSUPPORTED_VOICECLIP:
#ifdef KMESSTEST
KMESS_ASSERT( contact != 0 );
#endif
showMessage( ChatMessage( ChatMessage::TYPE_SYSTEM,
ChatMessage::CONTENT_SYSTEM_ERROR,
true,
i18nc( "Warning message shown in chat, %1 is the contact's friendly name",
"%1 has sent you a voice clip, but KMess does not support voice clips yet.",
friendlyName ),
handle,
friendlyName ) );
break;
case MsnSwitchboardConnection::WARNING_UNSUPPORTED_ACTIONMESSAGE:
#ifdef KMESSTEST
KMESS_ASSERT( contact != 0 );
#endif
showMessage( ChatMessage( ChatMessage::TYPE_SYSTEM,
ChatMessage::CONTENT_SYSTEM_ERROR,
true,
i18nc( "Warning message shown in chat, %1 is the contact's friendly name",
"%1 has sent you an action message, but KMess does not support action messages yet.",
friendlyName ),
handle,
friendlyName ) );
break;
case MsnSwitchboardConnection::WARNING_INK_UNSUPPORTED_BY_CONTACT:
showMessage( ChatMessage( ChatMessage::TYPE_SYSTEM,
ChatMessage::CONTENT_SYSTEM_ERROR,
true,
i18nc( "Warning message shown in chat",
"One or more contacts do not support the handwriting message." ),
handle,
friendlyName ) );
break;
case MsnSwitchboardConnection::WARNING_UNSUPPORTED_UNKNOWN:
#ifdef KMESSTEST
KMESS_ASSERT( contact != 0 );
#endif
default:
showMessage( ChatMessage( ChatMessage::TYPE_SYSTEM,
ChatMessage::CONTENT_SYSTEM_ERROR,
true,
i18nc( "Warning message shown in chat, %1 is the contact's friendly name",
"%1 has sent you a Live Messenger feature that KMess does not support yet.",
friendlyName ),
handle,
friendlyName ) );
break;
}
}
// Show a wink received by a contact
void Chat::showWink( const QString &handle, const QString &filename, const QString &animationName )
{
#ifdef KMESSDEBUG_CHAT_GENERAL
kmDebug() << "Processing received wink data.";
#endif
QString message;
const QString friendlyName( currentAccount_->getContactFriendlyNameByHandle( handle, STRING_CLEANED_ESCAPED ) );
// If we aren't showing winks, notify the user that we got one but don't actually display the animation.
if( ! currentAccount_->getShowWinks() )
{
if( animationName.isEmpty() )
{
message = i18nc( "Message shown in the chat window, %1 is the contact's friendly name",
"You received a wink from %1, but displaying "
"winks has been disabled. "
"You can re-enable it in the <a href='kmess://accountconfig'>"
"account settings</a>.",
friendlyName );
}
else
{
message = i18nc( "Message shown in the chat window, %1 is the contact's "
"friendly name, %2 is the wink name",
"You received the &quot;%2&quot; wink from %1, but displaying "
"winks has been disabled. "
"You can re-enable it in the <a href='kmess://accountconfig'>"
"account settings</a>.",
friendlyName,
animationName );
}
showMessage( ChatMessage( ChatMessage::TYPE_NOTIFICATION,
ChatMessage::CONTENT_NOTIFICATION_WINK,
false,
message,
handle ) );
return;
}
// Test whether the file exists, before cabextract fails.
if( ! QFile::exists( filename ) )
{
kmWarning() << "file not found: " << filename << "!";
return;
}
QString html;
WinksWidget::CABEXTRACTOR value = WinksWidget::getHtmlFromWink( filename, html );
if( value == WinksWidget::SUCCESS )
{
if( animationName.isEmpty() )
{
message = i18nc( "Message shown in the chat window, %1 is the contact's friendly name",
"%1 has sent you a wink!",
friendlyName );
}
else
{
message = i18nc( "Message shown in the chat window, %1 is the contact's "
"friendly name, %2 is the wink name",
"%1 has sent you a wink: &quot;%2&quot;!",
friendlyName,
animationName );
}
// Show a notification
receivedMessage( ChatMessage( ChatMessage::TYPE_NOTIFICATION,
ChatMessage::CONTENT_NOTIFICATION_WINK,
true,
message + html,
handle,
friendlyName ) );
return;
}
// Extraction failed
switch( value )
{
case WinksWidget::NOTINSTALLED:
if( animationName.isEmpty() )
{
message = i18nc( "Message shown in the chat window, %1 is the contact's friendly name",
"You received a wink from %1, but it "
"could not be displayed. Make sure you have "
"the &quot;cabextract&quot; program installed.",
friendlyName );
}
else
{
message = i18nc( "Message shown in the chat window, %1 is the contact's "
"friendly name, %2 is the wink name",
"You received the &quot;%2&quot; wink from %1, but it "
"could not be displayed. Make sure you have "
"the &quot;cabextract&quot; program installed.",
friendlyName,
animationName );
}
break;
case WinksWidget::FAILED:
if( animationName.isEmpty() )
{
message = i18nc( "Message shown in the chat window, %1 is the contact's friendly name",
"You received a wink from %1, but it "
"could not be displayed. Extracting the wink "
"package with &quot;cabextract&quot; has failed.",
friendlyName );
}
else
{
message = i18nc( "Message shown in the chat window, %1 is the contact's "
"friendly name, %2 is the wink name",
"You received the &quot;%2&quot; wink from %1, but it "
"could not be displayed. Extracting the wink "
"package with &quot;cabextract&quot; has failed.",
friendlyName,
animationName );
}
break;
default:
if( animationName.isEmpty() )
{
message = i18nc( "Message shown in the chat window, %1 is the contact's friendly name",
"You received a wink from %1, but it "
"could not be displayed. The data could not be "
"read.",
friendlyName );
}
else
{
message = i18nc( "Message shown in the chat window, %1 is the contact's "
"friendly name, %2 is the wink name",
"You received the &quot;%2&quot; wink from %1, but it "
"could not be displayed. The data could not be "
"read.",
friendlyName,
animationName );
}
break;
}
showMessage( ChatMessage( ChatMessage::TYPE_SYSTEM,
ChatMessage::CONTENT_SYSTEM_ERROR,
false,
message,
handle ) );
}
// Display a message to notify of a contact status change
void Chat::slotContactChangedStatus()
{
ContactBase *contact = static_cast<ContactBase*>( sender() );
if( KMESS_NULL( contact ) ) return;
// Make sure this contact is in chat
if( ! isContactInChat( contact->getHandle(), false ) )
{
#ifdef KMESSDEBUG_CHAT_GENERAL
kmDebug() << "Received a status change signal from a contact not in chat!";
#endif
return;
}
QString message;
const QString &friendlyName = contact->getFriendlyName( STRING_CLEANED_ESCAPED );
// Display a message when the user goes offline.
if( contact->isOffline() )
{
// Show a different one depending if this is an exclusive chat (which means we send offline IMs)
if( getParticipants().size() == 1 )
{
message = i18n( "%1 has gone offline. Any messages you send will be delivered the next time he or she logs in.",
friendlyName );
}
else if( currentAccount_->getShowSessionInfo() )
{
message = i18n( "%1 has gone offline.",
friendlyName );
}
else
{
return;
}
}
else if( currentAccount_->getShowSessionInfo() )
{
message = i18n( "%1 has changed his or her status to &quot;%2&quot;.",
friendlyName,
MsnStatus::getName( contact->getStatus() ) );
}
else
{
return;
}
receivedMessage( ChatMessage( ChatMessage::TYPE_PRESENCE,
ChatMessage::CONTENT_PRESENCE_STATUS,
true,
message,
contact->getHandle() ) );
}
// Notify the user of a received nudge
void Chat::slotReceivedNudge( ContactBase *contact )
{
#ifdef KMESSDEBUG_CHAT_GENERAL
kmDebug() << "Received a nudge.";
#endif
// Get friendly name
const QString& friendlyName = contact->getFriendlyName( STRING_CLEANED_ESCAPED );
ChatMessage message( ChatMessage::TYPE_NOTIFICATION,
ChatMessage::CONTENT_NOTIFICATION_NUDGE,
true,
i18n( "%1 has sent you a nudge!", friendlyName ),
contact->getHandle(),
friendlyName );
receivedMessage( message );
// Emit the new message signal so that the user will receive a notification
emit gotChatMessage( message, this );
// Start the buzz effect.
emit gotNudge();
}
// Signal that a message could not be sent to the switchboard
void Chat::slotSendingFailed( const QString &handle, const MimeMessage &message )
{
const QString contentType( message.getValue( "Content-Type" ) );
QString errorMessage;
QString friendlyName;
#ifdef KMESSDEBUG_CHAT_GENERAL
kmDebug() << "Showing a sending failure notice.";
#endif
// It's possible that a message could not be delivered to any of the contacts in chat:
// in this case, the handle is *
if( handle == "*" )
{
friendlyName = i18nc( "Phrase to be inserted in place of a contact name, when a message can't be delivered "
"to any of the recipients",
"all contacts" );
}
else
{
friendlyName = currentAccount_->getContactFriendlyNameByHandle( handle, STRING_CLEANED_ESCAPED );
}
if( contentType.startsWith( "text/x-msnmsgr-datacast" ) )
{
switch( message.getValue( "ID" ).toInt() )
{
case 1: // Nudge
errorMessage = i18nc( "Error message shown in chat, %1 is the contact's friendly name",
"Failed to send the nudge to %1.",
friendlyName );
break;
case 2: // Wink
errorMessage = i18nc( "Error message shown in chat, %1 is the contact's friendly name",
"Failed to send the wink to %1.",
friendlyName );
break;
default: // Unsupported datacast?!
kmWarning() << "Failed sending datacast message: no sending failure chat message available!";
return;
}
}
else if( contentType.startsWith( "image/gif" ) || contentType.startsWith( "application/x-ms-ink" ) )
{
errorMessage = i18nc( "Error message shown in chat, %1 is the contact's friendly name", "Failed to send the handwritten message to %1.",
friendlyName );
}
else if( contentType.startsWith( "text/plain" ) && ! message.getBody().isEmpty() )
{
QString body( message.getBody().toHtmlEscaped() );
// Cut down the message a bit if needed
if( body.length() > 16 )
{
body.truncate( 16 );
body += "...";
}
errorMessage = i18nc( "Error message shown in chat, %1 is the sent message, %2 is the contact's friendly name",
"Failed to send the message to %2:<br/>%1",
body,
friendlyName );
}
else
{
#ifdef KMESSDEBUG_CHAT_GENERAL
kmDebug() << "Not displaying a sending failure notice for messages of type" << contentType;
#endif
return;
}
showMessage( ChatMessage( ChatMessage::TYPE_SYSTEM,
ChatMessage::CONTENT_SYSTEM_ERROR,
false,
errorMessage,
handle,
friendlyName ) );
}
// Send a file to a contact.
void Chat::startFileTransfer( QList<QUrl> fileList )
{
if( KMESS_NULL(msnSwitchboardConnection_) ) return;
// Ask for a contact
const QString handle( chooseContact() );
// If no contact can be chosen, stop
if( handle.isEmpty() )
{
#ifdef KMESSDEBUG_CHAT_FILETRANSFER
kmDebug() << "Unable to find a contact to send files to. Aborting.";
#endif
return;
}
#ifdef KMESSDEBUG_CHAT_FILETRANSFER
kmDebug() << "File list contents:" << fileList;
kmDebug() << "Chosen handle:" << handle;
#endif
if( fileList.isEmpty() )
{
#ifdef KMESSDEBUG_CHAT_FILETRANSFER
kmDebug() << "Asking for a file to send";
#endif
// If no file names were given, ask for a filename
QUrl fileName( QFileDialog::getOpenFileUrl( getChatWindow() ) );
// Stop if no file was selected
if( fileName.isEmpty() )
{
return;
}
fileList.append( fileName );
}
foreach( const QUrl &fileUrl, fileList )
{
QUrl localFileUrl( fileUrl );
// If the url can't be translated to a local file, copy it to disk
if( ! localFileUrl.isLocalFile() )
{
#ifdef KMESSDEBUG_CHAT_FILETRANSFER
kmDebug() << "Not a local file, copying it locally.";
#endif
// Download the remote file to a temporary folder
QString localFilePath;
if( ! KMessShared::downloadToLocalFile( fileUrl, localFilePath ) )
{
#ifdef KMESSDEBUG_CHAT_FILETRANSFER
kmDebug() << "Copy failed.";
#endif
// Show a message to the user
showMessage( ChatMessage( ChatMessage::TYPE_SYSTEM,
ChatMessage::CONTENT_APP_FAILED,
false,
i18n( "The file &quot;%1&quot; could not be found on "
"your computer, and the download failed.",
localFileUrl.toDisplayString() ),
handle ) );
continue;
}
// TODO Remove the temporary file after the transfer
localFileUrl = QUrl( QUrl::fromLocalFile( localFilePath ) );
}
#ifdef KMESSDEBUG_CHAT_FILETRANSFER
kmDebug() << "Original file URL:" << fileUrl << "-- Local file URL:" << localFileUrl;
#endif
// Ask the ChatMaster to initiate the file transfer. If there are multiple contacts
// in this chat, it needs to create a new chat session for that contact.
emit requestFileTransfer( handle, localFileUrl.toLocalFile() );
}
}
// Send a nudge to a contact
void Chat::slotSendNudge()
{
if( msnSwitchboardConnection_ == 0 )
{
return;
}
// Show a more detailed message if we're in an active chat
QString messageText;
const QStringList &contactsInChat = msnSwitchboardConnection_->getContactsInChat();
if( contactsInChat.count() == 1 && msnSwitchboardConnection_->isConnected() )
{
messageText = i18nc( "Message shown in chat window, %1 is the contact's friendly name",
"You have sent a nudge to %1!",
currentAccount_->getContactFriendlyNameByHandle( contactsInChat.first(), STRING_CLEANED_ESCAPED ) );
}
else
{
// Chat is either empty (should resume), or a multi-chat
messageText = i18n( "You have sent a nudge!" );
}
// Display message
ChatMessage message( ChatMessage::TYPE_NOTIFICATION,
ChatMessage::CONTENT_NOTIFICATION_NUDGE,
false,
messageText,
QString() );
showMessage( message );
// Send nudge
msnSwitchboardConnection_->sendNudge();
// Shake the window
emit gotNudge();
}
// Start a chat
void Chat::startChat()
{
if( KMESS_NULL(msnSwitchboardConnection_) )
{
#ifdef KMESSDEBUG_CHAT_GENERAL
kmWarning() << "Tried to open a chat without a switchboard!";
#endif
return;
}
#ifdef KMESSDEBUG_CHAT_GENERAL
kmDebug() << "Opening a chat of type:" << ( msnSwitchboardConnection_->isInactive() ? "Offline" : "Active" );
#endif
// Add the participant contacts to the chat, even if they're offline
const QStringList participants( msnSwitchboardConnection_->getContactsInChat() );
#ifdef KMESSDEBUG_CHAT_GENERAL
kmDebug() << "Adding initial chat participants to the chat:" << participants;
#endif
foreach( const QString &handle, participants )
{
ContactBase *contact = currentAccount_->getContactByHandle( handle );
if( contact == 0 )
{
contact = currentAccount_->addInvitedContact( handle );
}
contactsWidget_->contactJoined( contact );
}
if( participants.isEmpty() )
{
kmWarning() << "The list of participants is empty!";
return;
}
const QString handle( participants.first() );
// Show the last recorded conversation, if any is present
if( participants.count() == 1 )
{
QString lastChat( ChatHistoryManager::lastChatLog( handle ) );
if( ! lastChat.isEmpty() )
{
#ifdef KMESSDEBUG_CHAT_GENERAL
kmDebug() << "Adding last chat log into the chat window";
#endif
setContents( lastChat );
}
}
// Start up the log writer
if( currentAccount_->getSaveChats() )
{
logWriter_ = ChatHistoryManager::getWriter( handle );
}
// if this is an offline chat, inform the user.
if( getParticipants().size() == 1 && msnSwitchboardConnection_->isInactive() )
{
ContactBase *contact = currentAccount_->getContactByHandle( handle );
if( contact && contact->isOffline() )
{
#ifdef KMESSDEBUG_CHAT_GENERAL
kmDebug() << "Initial chat participant" << handle << "is offline; pretending the contact joined";
#endif
contactJoined( contact, true );
receivedMessage( ChatMessage( ChatMessage::TYPE_PRESENCE,
ChatMessage::CONTENT_SYSTEM_NOTICE,
false,
i18n( "%1 is currently offline. Any messages you send will be delivered the next time he or she logs in.",
contact->getFriendlyName( STRING_CLEANED_ESCAPED ) ),
handle ) );
}
}
scrollToBottom( true /* forced scrolling */ );
}