Commit 0829dd92 authored by Wescoeur's avatar Wescoeur

feat(Chat): supports messages composing

parent 36b349ed
...@@ -414,6 +414,10 @@ ...@@ -414,6 +414,10 @@
<translation>Unable to send file. <translation>Unable to send file.
Server url not configured.</translation> Server url not configured.</translation>
</message> </message>
<message>
<source>isComposing</source>
<translation>%1 is typing...</translation>
</message>
</context> </context>
<context> <context>
<name>Cli</name> <name>Cli</name>
......
...@@ -414,6 +414,10 @@ ...@@ -414,6 +414,10 @@
<translation>Impossible d&apos;envoyer un fichier. <translation>Impossible d&apos;envoyer un fichier.
Url du serveur non configurée.</translation> Url du serveur non configurée.</translation>
</message> </message>
<message>
<source>isComposing</source>
<translation>%1 est en train d&apos;écrire...</translation>
</message>
</context> </context>
<context> <context>
<name>Cli</name> <name>Cli</name>
......
...@@ -199,6 +199,20 @@ ChatModel::ChatModel (QObject *parent) : QAbstractListModel(parent) { ...@@ -199,6 +199,20 @@ ChatModel::ChatModel (QObject *parent) : QAbstractListModel(parent) {
QObject::connect(mCoreHandlers.get(), &CoreHandlers::messageReceived, this, &ChatModel::handleMessageReceived); QObject::connect(mCoreHandlers.get(), &CoreHandlers::messageReceived, this, &ChatModel::handleMessageReceived);
QObject::connect(mCoreHandlers.get(), &CoreHandlers::callStateChanged, this, &ChatModel::handleCallStateChanged); QObject::connect(mCoreHandlers.get(), &CoreHandlers::callStateChanged, this, &ChatModel::handleCallStateChanged);
// Deal with remote composing.
QTimer *timer = new QTimer(this);
timer->setInterval(500);
QObject::connect(timer, &QTimer::timeout, this, [this] {
bool isRemoteComposing = mChatRoom->isRemoteComposing();
if (isRemoteComposing != mIsRemoteComposing) {
mIsRemoteComposing = isRemoteComposing;
emit isRemoteComposingChanged(mIsRemoteComposing);
}
});
timer->start();
} }
ChatModel::~ChatModel () { ChatModel::~ChatModel () {
...@@ -259,7 +273,7 @@ bool ChatModel::removeRows (int row, int count, const QModelIndex &parent) { ...@@ -259,7 +273,7 @@ bool ChatModel::removeRows (int row, int count, const QModelIndex &parent) {
QString ChatModel::getSipAddress () const { QString ChatModel::getSipAddress () const {
if (!mChatRoom) if (!mChatRoom)
return ""; return QString("");
return ::Utils::coreStringToAppString( return ::Utils::coreStringToAppString(
mChatRoom->getPeerAddress()->asStringUriOnly() mChatRoom->getPeerAddress()->asStringUriOnly()
...@@ -305,6 +319,10 @@ void ChatModel::setSipAddress (const QString &sipAddress) { ...@@ -305,6 +319,10 @@ void ChatModel::setSipAddress (const QString &sipAddress) {
emit sipAddressChanged(sipAddress); emit sipAddressChanged(sipAddress);
} }
bool ChatModel::getIsRemoteComposing () const {
return mIsRemoteComposing;
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
void ChatModel::removeEntry (int id) { void ChatModel::removeEntry (int id) {
...@@ -473,6 +491,10 @@ bool ChatModel::fileWasDownloaded (int id) { ...@@ -473,6 +491,10 @@ bool ChatModel::fileWasDownloaded (int id) {
return entry.second && ::fileWasDownloaded(static_pointer_cast<linphone::ChatMessage>(entry.second)); return entry.second && ::fileWasDownloaded(static_pointer_cast<linphone::ChatMessage>(entry.second));
} }
void ChatModel::compose () {
return mChatRoom->compose();
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
const ChatModel::ChatEntryData ChatModel::getFileMessageEntry (int id) { const ChatModel::ChatEntryData ChatModel::getFileMessageEntry (int id) {
......
...@@ -38,6 +38,7 @@ class ChatModel : public QAbstractListModel { ...@@ -38,6 +38,7 @@ class ChatModel : public QAbstractListModel {
Q_OBJECT; Q_OBJECT;
Q_PROPERTY(QString sipAddress READ getSipAddress WRITE setSipAddress NOTIFY sipAddressChanged); Q_PROPERTY(QString sipAddress READ getSipAddress WRITE setSipAddress NOTIFY sipAddressChanged);
Q_PROPERTY(bool isRemoteComposing READ getIsRemoteComposing NOTIFY isRemoteComposingChanged);
public: public:
enum Roles { enum Roles {
...@@ -88,6 +89,8 @@ public: ...@@ -88,6 +89,8 @@ public:
QString getSipAddress () const; QString getSipAddress () const;
void setSipAddress (const QString &sipAddress); void setSipAddress (const QString &sipAddress);
bool getIsRemoteComposing () const;
void removeEntry (int id); void removeEntry (int id);
void removeAllEntries (); void removeAllEntries ();
...@@ -105,8 +108,12 @@ public: ...@@ -105,8 +108,12 @@ public:
bool fileWasDownloaded (int id); bool fileWasDownloaded (int id);
void compose ();
signals: signals:
void sipAddressChanged (const QString &sipAddress); void sipAddressChanged (const QString &sipAddress);
bool isRemoteComposingChanged (bool status);
void allEntriesRemoved (); void allEntriesRemoved ();
void messageSent (const std::shared_ptr<linphone::ChatMessage> &message); void messageSent (const std::shared_ptr<linphone::ChatMessage> &message);
...@@ -133,6 +140,8 @@ private: ...@@ -133,6 +140,8 @@ private:
void handleCallStateChanged (const std::shared_ptr<linphone::Call> &call, linphone::CallState state); void handleCallStateChanged (const std::shared_ptr<linphone::Call> &call, linphone::CallState state);
void handleMessageReceived (const std::shared_ptr<linphone::ChatMessage> &message); void handleMessageReceived (const std::shared_ptr<linphone::ChatMessage> &message);
bool mIsRemoteComposing = false;
QList<ChatEntryData> mEntries; QList<ChatEntryData> mEntries;
std::shared_ptr<linphone::ChatRoom> mChatRoom; std::shared_ptr<linphone::ChatRoom> mChatRoom;
......
...@@ -69,6 +69,14 @@ ChatProxyModel::ChatProxyModel (QObject *parent) : QSortFilterProxyModel(parent) ...@@ -69,6 +69,14 @@ ChatProxyModel::ChatProxyModel (QObject *parent) : QSortFilterProxyModel(parent)
ChatModel *chat = static_cast<ChatModel *>(mChatModelFilter->sourceModel()); ChatModel *chat = static_cast<ChatModel *>(mChatModelFilter->sourceModel());
QObject::connect(chat, &ChatModel::sipAddressChanged, this, [this](const QString &sipAddress) {
emit sipAddressChanged(sipAddress);
});
QObject::connect(chat, &ChatModel::isRemoteComposingChanged, this, [this](bool status) {
emit isRemoteComposingChanged(status);
});
QObject::connect(chat, &ChatModel::messageReceived, this, [this](const shared_ptr<linphone::ChatMessage> &) { QObject::connect(chat, &ChatModel::messageReceived, this, [this](const shared_ptr<linphone::ChatMessage> &) {
mMaxDisplayedEntries++; mMaxDisplayedEntries++;
}); });
...@@ -88,13 +96,21 @@ ChatProxyModel::ChatProxyModel (QObject *parent) : QSortFilterProxyModel(parent) ...@@ -88,13 +96,21 @@ ChatProxyModel::ChatProxyModel (QObject *parent) : QSortFilterProxyModel(parent)
); \ ); \
} }
#define CREATE_PARENT_MODEL_FUNCTION_PARAM(METHOD, ARG_TYPE) \ #define CREATE_PARENT_MODEL_FUNCTION(METHOD) \
void ChatProxyModel::METHOD() { \
static_cast<ChatModel *>(mChatModelFilter->sourceModel())->METHOD(); \
}
#define CREATE_PARENT_MODEL_FUNCTION_WITH_PARAM(METHOD, ARG_TYPE) \
void ChatProxyModel::METHOD(ARG_TYPE value) { \ void ChatProxyModel::METHOD(ARG_TYPE value) { \
static_cast<ChatModel *>(mChatModelFilter->sourceModel())->METHOD(value); \ static_cast<ChatModel *>(mChatModelFilter->sourceModel())->METHOD(value); \
} }
CREATE_PARENT_MODEL_FUNCTION_PARAM(sendFileMessage, const QString &); CREATE_PARENT_MODEL_FUNCTION(compose);
CREATE_PARENT_MODEL_FUNCTION_PARAM(sendMessage, const QString &); CREATE_PARENT_MODEL_FUNCTION(removeAllEntries);
CREATE_PARENT_MODEL_FUNCTION_WITH_PARAM(sendFileMessage, const QString &);
CREATE_PARENT_MODEL_FUNCTION_WITH_PARAM(sendMessage, const QString &);
CREATE_PARENT_MODEL_FUNCTION_WITH_ID(downloadFile); CREATE_PARENT_MODEL_FUNCTION_WITH_ID(downloadFile);
CREATE_PARENT_MODEL_FUNCTION_WITH_ID(openFile); CREATE_PARENT_MODEL_FUNCTION_WITH_ID(openFile);
...@@ -103,14 +119,11 @@ CREATE_PARENT_MODEL_FUNCTION_WITH_ID(removeEntry); ...@@ -103,14 +119,11 @@ CREATE_PARENT_MODEL_FUNCTION_WITH_ID(removeEntry);
CREATE_PARENT_MODEL_FUNCTION_WITH_ID(resendMessage); CREATE_PARENT_MODEL_FUNCTION_WITH_ID(resendMessage);
#undef CREATE_PARENT_MODEL_FUNCTION #undef CREATE_PARENT_MODEL_FUNCTION
#undef CREATE_PARENT_MODEL_FUNCTION_WITH_PARAM
#undef CREATE_PARENT_MODEL_FUNCTION_WITH_ID #undef CREATE_PARENT_MODEL_FUNCTION_WITH_ID
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
void ChatProxyModel::removeAllEntries () {
static_cast<ChatModel *>(mChatModelFilter->sourceModel())->removeAllEntries();
}
QString ChatProxyModel::getSipAddress () const { QString ChatProxyModel::getSipAddress () const {
return static_cast<ChatModel *>(mChatModelFilter->sourceModel())->getSipAddress(); return static_cast<ChatModel *>(mChatModelFilter->sourceModel())->getSipAddress();
} }
...@@ -121,6 +134,10 @@ void ChatProxyModel::setSipAddress (const QString &sipAddress) { ...@@ -121,6 +134,10 @@ void ChatProxyModel::setSipAddress (const QString &sipAddress) {
); );
} }
bool ChatProxyModel::getIsRemoteComposing () const {
return static_cast<ChatModel *>(mChatModelFilter->sourceModel())->getIsRemoteComposing();
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
void ChatProxyModel::loadMoreEntries () { void ChatProxyModel::loadMoreEntries () {
......
...@@ -35,6 +35,7 @@ class ChatProxyModel : public QSortFilterProxyModel { ...@@ -35,6 +35,7 @@ class ChatProxyModel : public QSortFilterProxyModel {
Q_OBJECT; Q_OBJECT;
Q_PROPERTY(QString sipAddress READ getSipAddress WRITE setSipAddress NOTIFY sipAddressChanged); Q_PROPERTY(QString sipAddress READ getSipAddress WRITE setSipAddress NOTIFY sipAddressChanged);
Q_PROPERTY(bool isRemoteComposing READ getIsRemoteComposing NOTIFY isRemoteComposingChanged);
public: public:
ChatProxyModel (QObject *parent = Q_NULLPTR); ChatProxyModel (QObject *parent = Q_NULLPTR);
...@@ -54,8 +55,12 @@ public: ...@@ -54,8 +55,12 @@ public:
Q_INVOKABLE void openFile (int id); Q_INVOKABLE void openFile (int id);
Q_INVOKABLE void openFileDirectory (int id); Q_INVOKABLE void openFileDirectory (int id);
Q_INVOKABLE void compose ();
signals: signals:
void sipAddressChanged (const QString &sipAddress); void sipAddressChanged (const QString &sipAddress);
bool isRemoteComposingChanged (bool status);
void moreEntriesLoaded (int n); void moreEntriesLoaded (int n);
void entryTypeFilterChanged (ChatModel::EntryType type); void entryTypeFilterChanged (ChatModel::EntryType type);
...@@ -67,6 +72,8 @@ private: ...@@ -67,6 +72,8 @@ private:
QString getSipAddress () const; QString getSipAddress () const;
void setSipAddress (const QString &sipAddress); void setSipAddress (const QString &sipAddress);
bool getIsRemoteComposing () const;
ChatModelFilter *mChatModelFilter; ChatModelFilter *mChatModelFilter;
int mMaxDisplayedEntries = ENTRIES_CHUNK_SIZE; int mMaxDisplayedEntries = ENTRIES_CHUNK_SIZE;
......
...@@ -2,6 +2,12 @@ ...@@ -2,6 +2,12 @@
// `Chat.qml` Logic. // `Chat.qml` Logic.
// ============================================================================= // =============================================================================
.import Linphone 1.0 as Linphone
.import 'qrc:/ui/scripts/LinphoneUtils/linphone-utils.js' as LinphoneUtils
// =============================================================================
function initView () { function initView () {
chat.tryToLoadMoreEntries = false chat.tryToLoadMoreEntries = false
chat.bindToEnd = true chat.bindToEnd = true
...@@ -11,7 +17,7 @@ function loadMoreEntries () { ...@@ -11,7 +17,7 @@ function loadMoreEntries () {
if (chat.atYBeginning && !chat.tryToLoadMoreEntries) { if (chat.atYBeginning && !chat.tryToLoadMoreEntries) {
chat.tryToLoadMoreEntries = true chat.tryToLoadMoreEntries = true
chat.positionViewAtBeginning() chat.positionViewAtBeginning()
proxyModel.loadMoreEntries() container.proxyModel.loadMoreEntries()
} }
} }
...@@ -20,16 +26,28 @@ function getComponentFromEntry (chatEntry) { ...@@ -20,16 +26,28 @@ function getComponentFromEntry (chatEntry) {
return 'FileMessage.qml' return 'FileMessage.qml'
} }
if (chatEntry.type === ChatModel.CallEntry) { if (chatEntry.type === Linphone.ChatModel.CallEntry) {
return 'Event.qml' return 'Event.qml'
} }
return chatEntry.isOutgoing ? 'OutgoingMessage.qml' : 'IncomingMessage.qml' return chatEntry.isOutgoing ? 'OutgoingMessage.qml' : 'IncomingMessage.qml'
} }
function getIsComposingMessage () {
if (!container.proxyModel.isRemoteComposing) {
return ''
}
var sipAddressObserver = chat.sipAddressObserver
return qsTr('isComposing').replace(
'%1',
LinphoneUtils.getContactUsername(sipAddressObserver.contact || sipAddressObserver.sipAddress)
)
}
function handleFilesDropped (files) { function handleFilesDropped (files) {
chat.bindToEnd = true chat.bindToEnd = true
files.forEach(proxyModel.sendFileMessage) files.forEach(container.proxyModel.sendFileMessage)
} }
function handleMoreEntriesLoaded (n) { function handleMoreEntriesLoaded (n) {
...@@ -47,8 +65,12 @@ function handleMovementStarted () { ...@@ -47,8 +65,12 @@ function handleMovementStarted () {
chat.bindToEnd = false chat.bindToEnd = false
} }
function handleTextChanged () {
container.proxyModel.compose()
}
function sendMessage (text) { function sendMessage (text) {
textArea.text = '' textArea.text = ''
chat.bindToEnd = true chat.bindToEnd = true
proxyModel.sendMessage(text) container.proxyModel.sendMessage(text)
} }
...@@ -11,6 +11,8 @@ import 'Chat.js' as Logic ...@@ -11,6 +11,8 @@ import 'Chat.js' as Logic
// ============================================================================= // =============================================================================
Rectangle { Rectangle {
id: container
property alias proxyModel: chat.model property alias proxyModel: chat.model
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
...@@ -196,6 +198,8 @@ Rectangle { ...@@ -196,6 +198,8 @@ Rectangle {
Layout.preferredHeight: ChatStyle.sendArea.height + ChatStyle.sendArea.border.width Layout.preferredHeight: ChatStyle.sendArea.height + ChatStyle.sendArea.border.width
borderColor: ChatStyle.sendArea.border.color borderColor: ChatStyle.sendArea.border.color
bottomWidth: ChatStyle.sendArea.border.width
topWidth: ChatStyle.sendArea.border.width topWidth: ChatStyle.sendArea.border.width
DroppableTextArea { DroppableTextArea {
...@@ -208,9 +212,20 @@ Rectangle { ...@@ -208,9 +212,20 @@ Rectangle {
placeholderText: qsTr('newMessagePlaceholder') placeholderText: qsTr('newMessagePlaceholder')
onDropped: Logic.handleFilesDropped(files) onDropped: Logic.handleFilesDropped(files)
onTextChanged: Logic.handleTextChanged(text)
onValidText: Logic.sendMessage(text) onValidText: Logic.sendMessage(text)
} }
} }
Text {
Layout.fillWidth: true
color: ChatStyle.composingText.color
font.pointSize: ChatStyle.composingText.pointSize
leftPadding: ChatStyle.composingText.leftPadding
text: Logic.getIsComposingMessage()
}
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
......
...@@ -33,6 +33,12 @@ QtObject { ...@@ -33,6 +33,12 @@ QtObject {
} }
} }
property QtObject composingText: QtObject {
property color color: Colors.b
property int pointSize: Units.dp * 9
property int leftPadding: 6
}
property QtObject entry: QtObject { property QtObject entry: QtObject {
property int bottomMargin: 10 property int bottomMargin: 10
property int deleteIconSize: 17 property int deleteIconSize: 17
...@@ -101,7 +107,6 @@ QtObject { ...@@ -101,7 +107,6 @@ QtObject {
property QtObject images: QtObject { property QtObject images: QtObject {
property int height: 48 property int height: 48
// `width` can be used.
} }
property QtObject incoming: QtObject { property QtObject incoming: QtObject {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment