diff --git a/KTp/Declarative/conversation.cpp b/KTp/Declarative/conversation.cpp index e218b48..e9d3198 100644 --- a/KTp/Declarative/conversation.cpp +++ b/KTp/Declarative/conversation.cpp @@ -1,307 +1,318 @@ /* Copyright (C) 2011 Lasath Fernando Copyright (C) 2016 Martin Klapetek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "conversation.h" #include "messages-model.h" #include #include #include #include #include "debug.h" #include "channel-delegator.h" class Conversation::ConversationPrivate { public: ConversationPrivate() { messages = 0; delegated = false; valid = false; isGroupChat = false; } MessagesModel *messages; //stores if the conversation has been delegated to another client and we are only observing the channel //and not handling it. bool delegated; bool valid; Tp::AccountPtr account; QTimer *pausedStateTimer; KPeople::PersonData *personData; // May be null for group chats. KTp::ContactPtr targetContact; bool isGroupChat; }; Conversation::Conversation(const Tp::TextChannelPtr &channel, const Tp::AccountPtr &account, QObject *parent) : QObject(parent), d (new ConversationPrivate) { qCDebug(KTP_DECLARATIVE); d->valid = false; d->isGroupChat = false; d->account = account; connect(d->account.data(), SIGNAL(connectionChanged(Tp::ConnectionPtr)), SLOT(onAccountConnectionChanged(Tp::ConnectionPtr))); d->messages = new MessagesModel(account, this); connect(d->messages, &MessagesModel::unreadCountChanged, this, &Conversation::unreadMessagesChanged); connect(d->messages, &MessagesModel::lastMessageChanged, this, &Conversation::lastMessageChanged); setTextChannel(channel); d->delegated = false; d->pausedStateTimer = new QTimer(this); d->pausedStateTimer->setSingleShot(true); connect(d->pausedStateTimer, SIGNAL(timeout()), this, SLOT(onChatPausedTimerExpired())); } Conversation::Conversation(const QString &contactId, const Tp::AccountPtr &account, QObject *parent) : QObject(parent), d(new ConversationPrivate) { d->valid = true; d->isGroupChat = false; d->account = account; d->personData = new KPeople::PersonData(QStringLiteral("ktp://") + d->account->objectPath().mid(35) + QStringLiteral("?") + contactId); d->messages = new MessagesModel(account, this); connect(d->messages, &MessagesModel::unreadCountChanged, this, &Conversation::unreadMessagesChanged); connect(d->messages, &MessagesModel::lastMessageChanged, this, &Conversation::lastMessageChanged); d->messages->setContactData(contactId, d->personData->name()); Q_EMIT avatarChanged(); Q_EMIT titleChanged(); Q_EMIT presenceIconChanged(); Q_EMIT validityChanged(d->valid); } void Conversation::setTextChannel(const Tp::TextChannelPtr &channel) { if (d->messages->account().isNull()) { d->messages->setAccount(d->account); } if (d->messages->textChannel() != channel) { d->messages->setTextChannel(channel); d->valid = channel->isValid(); connect(channel.data(), SIGNAL(invalidated(Tp::DBusProxy*,QString,QString)), SLOT(onChannelInvalidated(Tp::DBusProxy*,QString,QString))); + connect(channel.data(), &Tp::TextChannel::chatStateChanged, this, &Conversation::contactTypingChanged); + if (channel->targetContact().isNull()) { d->isGroupChat = true; } else { d->isGroupChat = false; d->targetContact = KTp::ContactPtr::qObjectCast(channel->targetContact()); d->personData = new KPeople::PersonData(QStringLiteral("ktp://") + d->account->objectPath().mid(35) + QStringLiteral("?") + d->targetContact->id()); connect(d->targetContact.constData(), SIGNAL(aliasChanged(QString)), SIGNAL(titleChanged())); connect(d->targetContact.constData(), SIGNAL(presenceChanged(Tp::Presence)), SIGNAL(presenceIconChanged())); connect(d->targetContact.constData(), SIGNAL(avatarDataChanged(Tp::AvatarData)), SIGNAL(avatarChanged())); } Q_EMIT avatarChanged(); Q_EMIT titleChanged(); Q_EMIT presenceIconChanged(); Q_EMIT validityChanged(d->valid); } } Tp::TextChannelPtr Conversation::textChannel() const { return d->messages->textChannel(); } MessagesModel* Conversation::messages() const { return d->messages; } QString Conversation::title() const { if (d->isGroupChat) { QString roomName = textChannel()->targetId(); return roomName.left(roomName.indexOf(QLatin1Char('@'))); } else { return d->personData->name(); } } QIcon Conversation::presenceIcon() const { if (d->isGroupChat) { return KTp::Presence(Tp::Presence::available()).icon(); } else if (!d->targetContact.isNull()) { return KTp::Presence(d->targetContact->presence()).icon(); } return QIcon(); } QIcon Conversation::avatar() const { if (d->isGroupChat) { return QIcon(); } else { const QString path = d->targetContact->avatarData().fileName; QIcon icon; if (!path.isEmpty()) { icon = QIcon(path); } if (icon.availableSizes().isEmpty()) { icon = QIcon::fromTheme(QStringLiteral("im-user")); } return icon; } } KTp::ContactPtr Conversation::targetContact() const { if (d->isGroupChat) { return KTp::ContactPtr(); } else { return d->targetContact; } } Tp::AccountPtr Conversation::account() const { return d->account; } Tp::Account* Conversation::accountObject() const { if (!d->account.isNull()) { return d->account.data(); } return 0; } bool Conversation::isValid() const { return d->valid; } void Conversation::onChannelInvalidated(Tp::DBusProxy *proxy, const QString &errorName, const QString &errorMessage) { qCDebug(KTP_DECLARATIVE) << proxy << errorName << ":" << errorMessage; d->valid = false; Q_EMIT validityChanged(d->valid); } void Conversation::onAccountConnectionChanged(const Tp::ConnectionPtr& connection) { //if we have reconnected and we were handling the channel if (connection && ! d->delegated) { //general convention is to never use ensureAndHandle when we already have a client registrar //ensureAndHandle will implicity create a new temporary client registrar which is a waste //it's also more code to get the new channel //However, we cannot use use ensureChannel as normal because without being able to pass a preferredHandler //we need a preferredHandler so that this handler is the one that ends up with the channel if multi handlers are active //we do not know the name that this handler is currently registered with Tp::PendingChannel *pendingChannel = d->account->ensureAndHandleTextChat(textChannel()->targetId()); connect(pendingChannel, SIGNAL(finished(Tp::PendingOperation*)), SLOT(onCreateChannelFinished(Tp::PendingOperation*))); } } void Conversation::onCreateChannelFinished(Tp::PendingOperation* op) { Tp::PendingChannel *pendingChannelOp = qobject_cast(op); Tp::TextChannelPtr textChannel = Tp::TextChannelPtr::dynamicCast(pendingChannelOp->channel()); if (textChannel) { setTextChannel(textChannel); } } void Conversation::delegateToProperClient() { ChannelDelegator::delegateChannel(d->account, d->messages->textChannel()); d->delegated = true; Q_EMIT conversationCloseRequested(); } void Conversation::requestClose() { qCDebug(KTP_DECLARATIVE); if (!d->messages->textChannel().isNull()) { d->messages->textChannel()->requestClose(); } } void Conversation::updateTextChanged(const QString &message) { if (!message.isEmpty()) { //if the timer is active, it means the user is continuously typing if (d->pausedStateTimer->isActive()) { //just restart the timer and don't spam with chat state changes d->pausedStateTimer->start(5000); } else { //if the user has just typed some text, set state to Composing and start the timer d->messages->textChannel()->requestChatState(Tp::ChannelChatStateComposing); d->pausedStateTimer->start(5000); } } else { //if the user typed no text/cleared the input field, set Active and stop the timer d->messages->textChannel()->requestChatState(Tp::ChannelChatStateActive); d->pausedStateTimer->stop(); } } void Conversation::onChatPausedTimerExpired() { d->messages->textChannel()->requestChatState(Tp::ChannelChatStatePaused); } Conversation::~Conversation() { qCDebug(KTP_DECLARATIVE); //if we are not handling the channel do nothing. if (!d->delegated) { d->messages->textChannel()->requestClose(); } delete d; } bool Conversation::hasUnreadMessages() const { if (d->messages) { return d->messages->unreadCount() > 0; } return false; } KPeople::PersonData* Conversation::personData() const { return d->personData; } + +bool Conversation::isContactTyping() const +{ + if (d->messages->textChannel()) { + return d->messages->textChannel()->chatState(d->targetContact) == Tp::ChannelChatStateComposing; + } + + return false; +} diff --git a/KTp/Declarative/conversation.h b/KTp/Declarative/conversation.h index 09e0f7f..70e1225 100644 --- a/KTp/Declarative/conversation.h +++ b/KTp/Declarative/conversation.h @@ -1,106 +1,110 @@ /* Copyright (C) 2011 Lasath Fernando Copyright (C) 2016 Martin Klapetek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #ifndef CONVERSATION_H #define CONVERSATION_H #include #include #include #include #include #include "messages-model.h" class MessagesModel; class Conversation : public QObject { Q_OBJECT Q_PROPERTY(MessagesModel *messages READ messages CONSTANT) Q_PROPERTY(bool valid READ isValid NOTIFY validityChanged) Q_PROPERTY(QString title READ title NOTIFY titleChanged) Q_PROPERTY(QIcon presenceIcon READ presenceIcon NOTIFY presenceIconChanged) Q_PROPERTY(QIcon avatar READ avatar NOTIFY avatarChanged) Q_PROPERTY(Tp::Account *account READ accountObject CONSTANT) Q_PROPERTY(KTp::ContactPtr targetContact READ targetContact CONSTANT) Q_PROPERTY(KPeople::PersonData *personData READ personData CONSTANT) Q_PROPERTY(bool hasUnreadMessages READ hasUnreadMessages NOTIFY unreadMessagesChanged) + Q_PROPERTY(bool isContactTyping READ isContactTyping NOTIFY contactTypingChanged) public: Conversation(const Tp::TextChannelPtr &channel, const Tp::AccountPtr &account, QObject *parent = 0); Conversation(const QString &contactId, const Tp::AccountPtr &account, QObject *parent = 0); ~Conversation() override; void setTextChannel(const Tp::TextChannelPtr &channel); Tp::TextChannelPtr textChannel() const; /** * Useful for offline history retrieving (as there's no text channel when offline) */ void setContactData(const QString &contactId, const QString &contactAlias); MessagesModel* messages() const; QString title() const; QIcon presenceIcon() const; QIcon avatar() const; /** * Target contact of this conversation. May be null if conversation is a group chat. */ KTp::ContactPtr targetContact() const; Tp::AccountPtr account() const; Tp::Account* accountObject() const; KPeople::PersonData* personData() const; bool isValid() const; bool hasUnreadMessages() const; + bool isContactTyping() const; + Q_SIGNALS: void validityChanged(bool isValid); void avatarChanged(); void titleChanged(); void presenceIconChanged(); void conversationCloseRequested(); void unreadMessagesChanged(); void lastMessageChanged(); + void contactTypingChanged(); public Q_SLOTS: void delegateToProperClient(); void requestClose(); void updateTextChanged(const QString &message); private Q_SLOTS: void onChannelInvalidated(Tp::DBusProxy *proxy, const QString &errorName, const QString &errorMessage); void onAccountConnectionChanged(const Tp::ConnectionPtr &connection); void onCreateChannelFinished(Tp::PendingOperation *op); void onChatPausedTimerExpired(); private: class ConversationPrivate; ConversationPrivate *d; }; Q_DECLARE_METATYPE(Conversation*) #endif // CONVERSATION_H