/*************************************************************************** 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 "chatmessageview.h" #include "chatwindow.h" #include "contactswidget.h" #include #include #include #include #include #include #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 ) , 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 kDebug() << "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::sorry( 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 kDebug() << "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 kDebug() << "Called with forceExpiration=" << forceExpiration << ", handle=" << handle << ", friendlyName=" << friendlyName; kDebug() << "Current list:" << typingContactsNames_.keys(); #endif // Do not add anything to the list if the friendly name is empty if( ! handle.isEmpty() ) { QTime startTime = QTime::currentTime(); // 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 kDebug() << "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 kDebug() << "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 { kDebug() << "Removal from the list was forced."; } #endif } // Update the list to remove expired typing events QMutableHashIterator 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 kDebug() << "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 kDebug() << "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 kDebug() << "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 kDebug() << "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( 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 KIcon Chat::getParticipantsTabIcon() { const QStringList &participants( getParticipants() ); // If there are more participants use group icon if( participants.count() != 1 ) { return KIcon( "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 KIcon( "view-media-artist" ); } return KIcon( 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_ ) { kDebug() << "already initialized."; return false; } if( ! ChatView::initialize() ) { kDebug() << "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 kDebug() << "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 kDebug() << "Returning" << check; #endif return check; } // The chat is closing void Chat::queryClose() { #ifdef KMESSDEBUG_CHAT_GENERAL kDebug() << "Saving chat log"; #endif // Notify ChatMaster emit closing( this ); // Save the chat saveChatAutomatically(); /** * 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: * // check if there are active applications if ( mimeApplications_.count() + p2pApplications_.count() - abortingApplications_.count() == 0) return true; else return false; * */ /* // 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() { #ifdef KMESSDEBUG_CHAT_GENERAL kDebug() << "Saving chat log"; #endif // Just save the chat before going down saveChatAutomatically(); } // A message was received from a contact. void Chat::receivedMessage(const ChatMessage &message) { #ifdef KMESSDEBUG_CHAT_GENERAL kDebug() << "Displaying message, internal type=" << message.getType(); kDebug() << "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_.isNull() || lastSentAutoMessage_.elapsed() > 120000 ) ) { // Send an auto away message every two minutes (if the contact keeps writing) #ifdef KMESSDEBUG_CHAT_GENERAL kDebug() << "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; } } /** * Save the chat if needed. * * The boolean argument should not be used, as it's used to recurse once when * the chat should also be saved to an external file. */ void Chat::saveChatAutomatically( bool saveExternalFile ) { // Check if "save chats" is enabled if( ! currentAccount_->getSaveChats() ) { #ifdef KMESSDEBUG_CHAT_GENERAL kDebug() << "User has save chats disabled, not saving chat."; #endif return; } // Check if the message area is empty, we won't save empty chats if( isEmpty() ) { #ifdef KMESSDEBUG_CHAT_GENERAL kDebug() << "Message area is empty, not saving chat."; #endif return; } #ifdef KMESSDEBUG_CHAT_GENERAL if( ! saveExternalFile ) { kDebug() << "Automatically saving the XML log file."; } else { kDebug() << "Automatically saving the external log file."; } #endif QDir dir; QString fileName, 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(); } if( ! saveExternalFile ) { extension = "xml"; format = Account::EXPORT_XML; dir.setPath( KMessConfig::instance()->getAccountDirectory( currentAccount_->getHandle() ) + "/chatlogs" ); } else { dir.setPath( currentAccount_->getSaveChatPath() ); format = currentAccount_->getSaveChatsFormat(); switch( format ) { case Account::EXPORT_TEXT: extension = "txt"; break; case Account::EXPORT_HTML: extension = "html"; break; default: kWarning() << "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 /yyyy/contactemail.ext case Account::BYYEAR: if( ! dir.exists( year ) ) dir.mkdir( year ); dir.cd( year ); break; // Save as /yyyy/mm/contactemail.ext case Account::BYMONTH: if( ! dir.exists( year ) ) dir.mkdir( year ); dir.cd( year ); if( ! dir.exists( month ) ) dir.mkdir( month ); dir.cd( month ); break; // Save as /yyyy/mm/dd/contactemail.ext case Account::BYDAY: if( ! dir.exists( year ) ) dir.mkdir( year ); dir.cd( year ); if( ! dir.exists( month ) ) dir.mkdir( month ); dir.cd( month ); if( ! dir.exists( day ) ) dir.mkdir( day ); dir.cd( day ); break; // Save as /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 { KMessApplication *kmessApp = static_cast( kapp ); if( ! kmessApp->quitSelected() ) { KMessageBox::sorry( getChatWindow(), i18n( "KMess could not save the log for this chat:
" "The chat logs directory, "%1", does not exist.", dir.absolutePath() ) ); } // TODO: show a KDE save file dialog // Always warn via debug log, too kWarning() << "Unable to save the chat log. The base directory" << dir.absolutePath() << "doesn't exist."; return; } int fileNumber = 0; QString fileCount; QString savePath( dir.absolutePath() + "/" ); bool nextFileExists = KMessShared::selectNextFile( savePath, fileName, fileCount, extension, fileNumber ); QFile file( savePath + fileName + fileCount + "." + extension ); // There are log files already for this contact: verify if we can // append to the last one: logs should not be too big if( nextFileExists && file.size() > 2000000L ) { #ifdef KMESSDEBUG_CHAT_GENERAL kDebug() << "Last file" << file.fileName() << "is too big, choosing next one"; #endif // Find the first file after the one we can't append to KMessShared::selectNextFile( savePath, fileName, fileCount, extension, fileNumber ); file.setFileName( savePath + fileName + "." + QString::number( fileNumber ) + "." + extension ); } #ifdef KMESSDEBUG_CHAT_GENERAL kDebug() << "Chat file:" << file.fileName(); #endif // Save the chat in the format requested by the user if( ! saveChatToFile( file.fileName(), format, false, /* Don't overwrite */ false /* No user interaction */ ) ) { #ifdef KMESSDEBUG_CHAT_GENERAL kDebug() << "Save failed. Trying next file name"; #endif // If saving fails (due to a corrupted file for example) just save the chat // with the next available file name KMessShared::selectNextFile( savePath, fileName, fileCount, extension, fileNumber ); file.setFileName( savePath + fileName + "." + QString::number( fileNumber ) + "." + extension ); // This time warn the user if saving fails again saveChatToFile( file.fileName(), format, true, true ); } if( saveExternalFile == false && currentAccount_->getSaveChatsToFile() ) { #ifdef KMESSDEBUG_CHAT_GENERAL kDebug() << "Done, saving external file too..."; #endif saveChatAutomatically( true ); } } // 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( const QByteArray &inkData ) { #ifdef KMESSTEST KMESS_ASSERT( msnSwitchboardConnection_ ); #endif msnSwitchboardConnection_->sendInk( inkData ); } // 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 "%1" 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 ); } // 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 ) { kDebug() << "Switchboard was not set up."; } else { kDebug() << "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 kDebug() << "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 kDebug() << "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_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 kDebug() << "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 " "account settings.", 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 "%2" wink from %1, but displaying " "winks has been disabled. " "You can re-enable it in the " "account settings.", 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 ) ) { kWarning() << "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: "%2"!", 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 "cabextract" 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 "%2" wink from %1, but it " "could not be displayed. Make sure you have " "the "cabextract" 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 "cabextract" 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 "%2" wink from %1, but it " "could not be displayed. Extracting the wink " "package with "cabextract" 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 "%2" 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( sender() ); if( KMESS_NULL( contact ) ) return; // Make sure this contact is in chat if( ! isContactInChat( contact->getHandle(), false ) ) { #ifdef KMESSDEBUG_CHAT_GENERAL kDebug() << "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 "%2".", 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 kDebug() << "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 kDebug() << "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?! kWarning() << "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( Qt::escape( message.getBody() ) ); // 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:
%1", body, friendlyName ); } else { #ifdef KMESSDEBUG_CHAT_GENERAL kDebug() << "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 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 kDebug() << "Unable to find a contact to send files to. Aborting."; #endif return; } #ifdef KMESSDEBUG_CHAT_FILETRANSFER kDebug() << "File list contents:" << fileList; kDebug() << "Chosen handle:" << handle; #endif if( fileList.isEmpty() ) { #ifdef KMESSDEBUG_CHAT_FILETRANSFER kDebug() << "Asking for a file to send"; #endif // If no file names were given, ask for a filename KUrl fileName( KFileDialog::getOpenUrl( KUrl( "kfiledialog:///:fileupload" ) ) ); // Stop if no file was selected if( fileName.isEmpty() ) { return; } fileList.append( fileName ); } foreach( const QUrl &fileUrl, fileList ) { KUrl localFileUrl( KIO::NetAccess::mostLocalUrl( fileUrl, this ) ); // If the url can't be translated to a local file, copy it to disk if( ! localFileUrl.isLocalFile() ) { #ifdef KMESSDEBUG_CHAT_FILETRANSFER kDebug() << "Not a local file, copying it locally."; #endif // Download the remote file to a temporary folder QString localFilePath; if( ! KIO::NetAccess::download( fileUrl, localFilePath, this ) ) { #ifdef KMESSDEBUG_CHAT_FILETRANSFER kDebug() << "Copy failed."; #endif // Show a message to the user showMessage( ChatMessage( ChatMessage::TYPE_SYSTEM, ChatMessage::CONTENT_APP_FAILED, false, i18n( "The file "%1" could not be found on " "your computer, and the download failed.", localFileUrl.prettyUrl() ), handle ) ); continue; } // TODO Remove the temporary file after the transfer localFileUrl.setUrl( localFilePath ); } #ifdef KMESSDEBUG_CHAT_FILETRANSFER kDebug() << "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::null ); 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 kWarning() << "Tried to open a chat without a switchboard!"; #endif return; } #ifdef KMESSDEBUG_CHAT_GENERAL kDebug() << "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 kDebug() << "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 this is an offline chat, inform the user. if( getParticipants().size() == 1 && msnSwitchboardConnection_->isInactive() ) { ContactBase *contact = currentAccount_->getContactByHandle( getParticipants().first() ); if( contact && contact->isOffline() ) { #ifdef KMESSDEBUG_CHAT_GENERAL kDebug() << "Initial chat participant " << contact->getHandle() << " 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 ) ), contact->getHandle() ) ); } } } //Start voice conversation void Chat::startConversation() { if( KMESS_NULL(msnSwitchboardConnection_) ) { #ifdef KMESSDEBUG_CHAT_GENERAL kWarning() << "Tried to open a conversation without a switchboard!"; #endif return; } kWarning() << "not implemented!"; // msnSwitchboardConnection_->startApp( MsnSwitchboardConnection::VOICECONVERSATION ); } // Start GnomeMeeting with a contact. void Chat::startMeeting() { // Choose the contact. const QString& handle( chooseContact() ); if( KMESS_NULL(msnSwitchboardConnection_) ) { #ifdef KMESSDEBUG_CHAT_GENERAL kWarning() << "Tried to open a meeting with \'" << handle << "\' without a switchboard!"; #endif return; } emit requestNetMeeting(handle); } // Send webcam data to a contact. void Chat::startWebcamTransfer() { // Choose the contact. const QString& handle( chooseContact() ); if( KMESS_NULL(msnSwitchboardConnection_) ) { #ifdef KMESSDEBUG_CHAT_GENERAL kWarning() << "Tried to open a meeting with \'" << handle << "\' without a switchboard!"; #endif return; } emit requestWebcamTransfer(handle); } #include "chat.moc"