Files
kmess/src/chat/chat.cpp
T
2009-05-13 18:20:42 +00:00

1760 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 "chatmessageview.h"
#include "chatwindow.h"
#include "contactswidget.h"
#include <QDir>
#include <QFile>
#include <QRegExp>
#include <KFileDialog>
#include <KIO/NetAccess>
#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 )
, 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<QString,QTime> 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<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
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:
* <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()
{
#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 <logs-path>/yyyy/contactemail.ext
case Account::BYYEAR:
if( ! dir.exists( year ) ) dir.mkdir( year );
dir.cd( year );
break;
// Save as <logs-path>/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 <logs-path>/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 <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
{
KMessApplication *kmessApp = static_cast<KMessApplication*>( kapp );
if( ! kmessApp->quitSelected() )
{
KMessageBox::sorry( getChatWindow(),
i18n( "<html>KMess could not save the log for this chat:<br />"
"The chat logs directory, &quot;%1&quot;, does not exist.</html>",
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 &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 );
}
// 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 <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 ) )
{
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: &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
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 &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
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:<br/>%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<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
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 &quot;%1&quot; 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"