From d0f33969ebe1908ed6bb78e88f5f8f0cf1e41bf5 Mon Sep 17 00:00:00 2001 From: John Gibbon Date: Sun, 27 Dec 2020 00:01:59 +0100 Subject: [PATCH] support basic bot messages (reply markup) only inlineKeyboardButtonTypeCallback and inlineKeyboardButtonTypeUrl are implemented. --- harbour-fernschreiber.pro | 1 + images/icon-s-link.svg | 28 ++++++++ qml/components/AudioPreview.qml | 20 ++++-- qml/components/MessageListViewItem.qml | 14 +++- qml/components/ReplyMarkupButtons.qml | 99 ++++++++++++++++++++++++++ qml/js/functions.js | 11 ++- qml/pages/ChatPage.qml | 2 +- src/chatmodel.cpp | 23 ++++++ src/chatmodel.h | 1 + src/tdlibreceiver.cpp | 9 +++ src/tdlibreceiver.h | 2 + src/tdlibwrapper.cpp | 12 ++++ src/tdlibwrapper.h | 2 + 13 files changed, 217 insertions(+), 7 deletions(-) create mode 100644 images/icon-s-link.svg create mode 100644 qml/components/ReplyMarkupButtons.qml diff --git a/harbour-fernschreiber.pro b/harbour-fernschreiber.pro index 417c776..dc12009 100644 --- a/harbour-fernschreiber.pro +++ b/harbour-fernschreiber.pro @@ -55,6 +55,7 @@ DISTFILES += qml/harbour-fernschreiber.qml \ qml/components/PinnedMessageItem.qml \ qml/components/PollPreview.qml \ qml/components/PressEffect.qml \ + qml/components/ReplyMarkupButtons.qml \ qml/components/StickerPicker.qml \ qml/components/PhotoTextsListItem.qml \ qml/components/WebPagePreview.qml \ diff --git a/images/icon-s-link.svg b/images/icon-s-link.svg new file mode 100644 index 0000000..c585d3f --- /dev/null +++ b/images/icon-s-link.svg @@ -0,0 +1,28 @@ + +image/svg+xml + + \ No newline at end of file diff --git a/qml/components/AudioPreview.qml b/qml/components/AudioPreview.qml index ef8881e..3901f6b 100644 --- a/qml/components/AudioPreview.qml +++ b/qml/components/AudioPreview.qml @@ -123,13 +123,14 @@ Item { fillMode: Image.PreserveAspectCrop visible: status === Image.Ready ? true : false layer.enabled: audioMessageComponent.highlighted - layer.effect: PressEffect { source: singleImage } + layer.effect: PressEffect { source: placeholderImage } } BackgroundImage { + id: backgroundImage visible: placeholderImage.status !== Image.Ready layer.enabled: audioMessageComponent.highlighted - layer.effect: PressEffect { source: singleImage } + layer.effect: PressEffect { source: backgroundImage } } Rectangle { @@ -140,6 +141,17 @@ Item { width: parent.width visible: playButton.visible } + Label { + visible: !!(audioData.performer || audioData.title) + color: placeholderBackground.visible ? "white" : Theme.secondaryHighlightColor + wrapMode: Text.Wrap + anchors { + fill: placeholderBackground + margins: Theme.paddingSmall + } + text: audioData.performer + (audioData.performer && audioData.title ? " - " : "") + audioData.title + font.pixelSize: Theme.fontSizeTiny + } Column { width: parent.width @@ -366,7 +378,7 @@ Item { anchors.centerIn: parent width: Theme.iconSizeLarge height: Theme.iconSizeLarge - highlighted: videoMessageComponent.highlighted || down + highlighted: audioMessageComponent.highlighted || down icon { asynchronous: true source: "image://theme/icon-l-play?white" @@ -390,7 +402,7 @@ Item { value: messageAudio.position enabled: messageAudio.seekable visible: (messageAudio.duration > 0) - highlighted: videoMessageComponent.highlighted || down + highlighted: audioMessageComponent.highlighted || down onReleased: { messageAudio.seek(Math.floor(value)); messageAudio.play(); diff --git a/qml/components/MessageListViewItem.qml b/qml/components/MessageListViewItem.qml index da91f29..a3aafdb 100644 --- a/qml/components/MessageListViewItem.qml +++ b/qml/components/MessageListViewItem.qml @@ -398,7 +398,10 @@ ListItem { wrapMode: Text.Wrap textFormat: Text.StyledText onLinkActivated: { - Functions.handleLink(link); + var chatCommand = Functions.handleLink(link); + if(chatCommand) { + tdLibWrapper.sendTextMessage(chatInformation.id, chatCommand); + } } horizontalAlignment: messageListItem.textAlign linkColor: Theme.highlightColor @@ -435,6 +438,15 @@ ListItem { value: messageListItem.highlighted } + Loader { + id: replyMarkupLoader + width: parent.width + height: active ? (myMessage.reply_markup.rows.length * (Theme.itemSizeSmall + Theme.paddingSmall) - Theme.paddingSmall) : 0 + asynchronous: true + active: !!myMessage.reply_markup && myMessage.reply_markup.rows + source: Qt.resolvedUrl("ReplyMarkupButtons.qml") + } + Timer { id: messageDateUpdater interval: 60000 diff --git a/qml/components/ReplyMarkupButtons.qml b/qml/components/ReplyMarkupButtons.qml new file mode 100644 index 0000000..02d9c7f --- /dev/null +++ b/qml/components/ReplyMarkupButtons.qml @@ -0,0 +1,99 @@ +/* + Copyright (C) 2020 Sebastian J. Wolf and other contributors + + This file is part of Fernschreiber. + + Fernschreiber 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 3 of the License, or + (at your option) any later version. + + Fernschreiber 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Fernschreiber. If not, see . +*/ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import "../js/twemoji.js" as Emoji +import "../js/functions.js" as Functions +import "../js/debug.js" as Debug + +Column { + width: parent.width + height: childrenRect.height + spacing: Theme.paddingSmall + + Repeater { + model: myMessage.reply_markup.rows + delegate: Row { + width: parent.width + height: Theme.itemSizeSmall + spacing: Theme.paddingSmall + Repeater { + id: buttonsRepeater + model: modelData + property int itemWidth:precalculatedValues.textColumnWidth / count + delegate: MouseArea { + /* + Unimplemented callback types: + inlineKeyboardButtonTypeBuy + inlineKeyboardButtonTypeCallbackGame + inlineKeyboardButtonTypeCallbackWithPassword + inlineKeyboardButtonTypeLoginUrl + inlineKeyboardButtonTypeSwitchInline + */ + property var callbacks: ({ + inlineKeyboardButtonTypeCallback: function(){ + tdLibWrapper.getCallbackQueryAnswer(messageListItem.chatId, messageListItem.messageId, {data: modelData.type.data, "@type": "callbackQueryPayloadData"}) + }, + inlineKeyboardButtonTypeUrl: function() { + Functions.handleLink(modelData.type.url); + } + }) + enabled: !!callbacks[modelData.type["@type"]] + height: Theme.itemSizeSmall + width: (precalculatedValues.textColumnWidth + Theme.paddingSmall) / buttonsRepeater.count - (Theme.paddingSmall) + onClicked: { + callbacks[modelData.type["@type"]](); + } + Rectangle { + anchors.fill: parent + radius: Theme.paddingSmall + color: parent.pressed ? Theme.rgba(Theme.highlightBackgroundColor, Theme.highlightBackgroundOpacity) + : Theme.rgba(Theme.primaryColor, Theme.opacityFaint) + opacity: parent.enabled ? 1.0 : Theme.opacityLow + + Label { + width: Math.min(parent.width - Theme.paddingSmall*2, contentWidth) + truncationMode: TruncationMode.Fade + text: Emoji.emojify(modelData.text, Theme.fontSizeSmall) + color: parent.pressed ? Theme.highlightColor : Theme.primaryColor + anchors.centerIn: parent + font.pixelSize: Theme.fontSizeSmall + } + Icon { + property var sources: ({ + inlineKeyboardButtonTypeUrl: "../../images/icon-s-link.svg", + inlineKeyboardButtonTypeSwitchInline: "image://theme/icon-s-repost", + inlineKeyboardButtonTypeCallbackWithPassword: "image://theme/icon-s-asterisk" + }) + visible: !!sources[modelData.type["@type"]] + source: sources[modelData.type["@type"]] || "" + sourceSize: Qt.size(Theme.iconSizeSmall, Theme.iconSizeSmall) + highlighted: parent.pressed + anchors { + right: parent.right + top: parent.top + } + } + } + + } + } + } + } +} diff --git a/qml/js/functions.js b/qml/js/functions.js index cc08cf0..3b4d180 100644 --- a/qml/js/functions.js +++ b/qml/js/functions.js @@ -305,6 +305,13 @@ function enhanceMessageText(formattedText, ignoreEntities) { { offset: (entity.offset + entity.length), insertionString: "", removeLength: 0 } ); break; + case "textEntityTypeBotCommand": + var command = messageText.substring(entity.offset, entity.offset + entity.length); + messageInsertions.push( + { offset: entity.offset, insertionString: "", removeLength: 0 }, + { offset: (entity.offset + entity.length), insertionString: "", removeLength: 0 } + ); + break; } } @@ -347,7 +354,9 @@ function handleLink(link) { } else if (link.indexOf("tg://resolve?domain=") === 0) { tdLibWrapper.searchPublicChat(link.substring(20)); } - } else { + } else if (link.indexOf("botCommand://") === 0) { // this gets returned to send on ChatPage + return link.substring(13); + } else { if (link.indexOf(tMePrefix) === 0) { if (link.indexOf("joinchat") !== -1) { Debug.log("Joining Chat: ", link); diff --git a/qml/pages/ChatPage.qml b/qml/pages/ChatPage.qml index ef793a3..5f63add 100644 --- a/qml/pages/ChatPage.qml +++ b/qml/pages/ChatPage.qml @@ -922,7 +922,7 @@ Page { chatId: chatModel.chatId myMessage: model.display messageId: model.message_id - extraContentComponentName: chatView.contentComponentNames[model.content_type] + extraContentComponentName: chatView.contentComponentNames[model.content_type] || "" canReplyToMessage: chatPage.canSendMessages onReplyToMessage: { newMessageInReplyToRow.inReplyToMessage = myMessage diff --git a/src/chatmodel.cpp b/src/chatmodel.cpp index f7e85b3..4bdb0fb 100644 --- a/src/chatmodel.cpp +++ b/src/chatmodel.cpp @@ -38,6 +38,7 @@ namespace { const QString SENDER("sender"); const QString USER_ID("user_id"); const QString PINNED_MESSAGE_ID("pinned_message_id"); + const QString REPLY_MARKUP("reply_markup"); const QString _TYPE("@type"); } @@ -54,6 +55,7 @@ public: static bool lessThan(const MessageData *message1, const MessageData *message2); void setContent(const QVariantMap &content); + void setReplyMarkup(const QVariantMap &replyMarkup); int senderUserId() const; qlonglong senderChatId() const; bool senderIsChat() const; @@ -90,6 +92,10 @@ void ChatModel::MessageData::setContent(const QVariantMap &content) { messageData.insert(CONTENT, content); } +void ChatModel::MessageData::setReplyMarkup(const QVariantMap &replyMarkup) +{ + messageData.insert(REPLY_MARKUP, replyMarkup); +} bool ChatModel::MessageData::lessThan(const MessageData *message1, const MessageData *message2) { @@ -112,6 +118,7 @@ ChatModel::ChatModel(TDLibWrapper *tdLibWrapper) : connect(this->tdLibWrapper, SIGNAL(chatPhotoUpdated(qlonglong, QVariantMap)), this, SLOT(handleChatPhotoUpdated(qlonglong, QVariantMap))); connect(this->tdLibWrapper, SIGNAL(chatPinnedMessageUpdated(qlonglong, qlonglong)), this, SLOT(handleChatPinnedMessageUpdated(qlonglong, qlonglong))); connect(this->tdLibWrapper, SIGNAL(messageContentUpdated(qlonglong, qlonglong, QVariantMap)), this, SLOT(handleMessageContentUpdated(qlonglong, qlonglong, QVariantMap))); + connect(this->tdLibWrapper, SIGNAL(messageEditedUpdated(qlonglong, qlonglong, QVariantMap)), this, SLOT(handleMessageEditedUpdated(qlonglong, qlonglong, QVariantMap))); connect(this->tdLibWrapper, SIGNAL(messagesDeleted(qlonglong, QList)), this, SLOT(handleMessagesDeleted(qlonglong, QList))); } @@ -420,6 +427,22 @@ void ChatModel::handleMessageContentUpdated(qlonglong chatId, qlonglong messageI } } +void ChatModel::handleMessageEditedUpdated(qlonglong chatId, qlonglong messageId, const QVariantMap &replyMarkup) +{ + LOG("Message edited updated" << chatId << messageId); + if (chatId == this->chatId && messageIndexMap.contains(messageId)) { + LOG("We know the message that was updated" << messageId); + const int pos = messageIndexMap.value(messageId, -1); + if (pos >= 0) { + messages.at(pos)->setReplyMarkup(replyMarkup); + LOG("Message was edited at index" << pos); + const QModelIndex messageIndex(index(pos)); + emit dataChanged(messageIndex, messageIndex); + emit messageUpdated(pos); + } + } +} + void ChatModel::handleMessagesDeleted(qlonglong chatId, const QList &messageIds) { LOG("Messages were deleted in a chat" << chatId); diff --git a/src/chatmodel.h b/src/chatmodel.h index 9353037..bd45b55 100644 --- a/src/chatmodel.h +++ b/src/chatmodel.h @@ -71,6 +71,7 @@ private slots: void handleChatPhotoUpdated(qlonglong chatId, const QVariantMap &photo); void handleChatPinnedMessageUpdated(qlonglong chatId, qlonglong pinnedMessageId); void handleMessageContentUpdated(qlonglong chatId, qlonglong messageId, const QVariantMap &newContent); + void handleMessageEditedUpdated(qlonglong chatId, qlonglong messageId, const QVariantMap &replyMarkup); void handleMessagesDeleted(qlonglong chatId, const QList &messageIds); private: diff --git a/src/tdlibreceiver.cpp b/src/tdlibreceiver.cpp index 301e779..8508a32 100644 --- a/src/tdlibreceiver.cpp +++ b/src/tdlibreceiver.cpp @@ -132,6 +132,7 @@ TDLibReceiver::TDLibReceiver(void *tdLibClient, QObject *parent) : QThread(paren handlers.insert("secretChat", &TDLibReceiver::processSecretChat); handlers.insert("updateSecretChat", &TDLibReceiver::processUpdateSecretChat); handlers.insert("importedContacts", &TDLibReceiver::processImportedContacts); + handlers.insert("updateMessageEdited", &TDLibReceiver::processUpdateMessageEdited); } void TDLibReceiver::setActive(bool active) @@ -555,6 +556,14 @@ void TDLibReceiver::processUpdateSecretChat(const QVariantMap &receivedInformati emit secretChatUpdated(updatedSecretChat.value(ID).toLongLong(), updatedSecretChat); } +void TDLibReceiver::processUpdateMessageEdited(const QVariantMap &receivedInformation) +{ + const qlonglong chatId = receivedInformation.value(CHAT_ID).toLongLong(); + const qlonglong messageId = receivedInformation.value(MESSAGE_ID).toLongLong(); + LOG("Message was edited" << chatId << messageId); + emit messageEditedUpdated(chatId, messageId, receivedInformation.value("reply_markup").toMap()); +} + void TDLibReceiver::processImportedContacts(const QVariantMap &receivedInformation) { LOG("Contacts were imported"); diff --git a/src/tdlibreceiver.h b/src/tdlibreceiver.h index 727b7ad..aae67bc 100644 --- a/src/tdlibreceiver.h +++ b/src/tdlibreceiver.h @@ -63,6 +63,7 @@ signals: void notificationUpdated(const QVariantMap updatedNotification); void chatNotificationSettingsUpdated(const QString &chatId, const QVariantMap updatedChatNotificationSettings); void messageContentUpdated(qlonglong chatId, qlonglong messageId, const QVariantMap &newContent); + void messageEditedUpdated(qlonglong chatId, qlonglong messageId, const QVariantMap &replyMarkup); void messagesDeleted(qlonglong chatId, const QList &messageIds); void chats(const QVariantMap &chats); void chat(const QVariantMap &chats); @@ -152,6 +153,7 @@ private: void nop(const QVariantMap &receivedInformation); void processSecretChat(const QVariantMap &receivedInformation); void processUpdateSecretChat(const QVariantMap &receivedInformation); + void processUpdateMessageEdited(const QVariantMap &receivedInformation); void processImportedContacts(const QVariantMap &receivedInformation); }; diff --git a/src/tdlibwrapper.cpp b/src/tdlibwrapper.cpp index 42223a1..7f5dbe2 100644 --- a/src/tdlibwrapper.cpp +++ b/src/tdlibwrapper.cpp @@ -123,6 +123,7 @@ TDLibWrapper::TDLibWrapper(AppSettings *appSettings, MceInterface *mceInterface, connect(this->tdLibReceiver, SIGNAL(usersReceived(QString, QVariantList, int)), this, SIGNAL(usersReceived(QString, QVariantList, int))); connect(this->tdLibReceiver, SIGNAL(errorReceived(int, QString, QString)), this, SLOT(handleErrorReceived(int, QString, QString))); connect(this->tdLibReceiver, SIGNAL(contactsImported(QVariantList, QVariantList)), this, SIGNAL(contactsImported(QVariantList, QVariantList))); + connect(this->tdLibReceiver, SIGNAL(messageEditedUpdated(qlonglong, qlonglong, QVariantMap)), this, SIGNAL(messageEditedUpdated(qlonglong, qlonglong, QVariantMap))); connect(&emojiSearchWorker, SIGNAL(searchCompleted(QString, QVariantList)), this, SLOT(handleEmojiSearchCompleted(QString, QVariantList))); @@ -545,6 +546,17 @@ void TDLibWrapper::getMessage(const QString &chatId, const QString &messageId) this->sendRequest(requestObject); } +void TDLibWrapper::getCallbackQueryAnswer(const QString &chatId, const QString &messageId, const QVariantMap &payload) +{ + LOG("Getting Callback Query Answer" << chatId << messageId); + QVariantMap requestObject; + requestObject.insert(_TYPE, "getCallbackQueryAnswer"); + requestObject.insert("chat_id", chatId); + requestObject.insert("message_id", messageId); + requestObject.insert("payload", payload); + this->sendRequest(requestObject); +} + void TDLibWrapper::getChatPinnedMessage(const qlonglong &chatId) { LOG("Retrieving pinned message" << chatId); diff --git a/src/tdlibwrapper.h b/src/tdlibwrapper.h index 50eb2fb..9f25780 100644 --- a/src/tdlibwrapper.h +++ b/src/tdlibwrapper.h @@ -143,6 +143,7 @@ public: Q_INVOKABLE void sendPollMessage(const QString &chatId, const QString &question, const QVariantList &options, const bool &anonymous, const int &correctOption, const bool &multiple, const QString &replyToMessageId = "0"); Q_INVOKABLE void forwardMessages(const QString &chatId, const QString &fromChatId, const QVariantList &messageIds, const bool sendCopy, const bool removeCaption); Q_INVOKABLE void getMessage(const QString &chatId, const QString &messageId); + Q_INVOKABLE void getCallbackQueryAnswer(const QString &chatId, const QString &messageId, const QVariantMap &payload); Q_INVOKABLE void getChatPinnedMessage(const qlonglong &chatId); Q_INVOKABLE void setOptionInteger(const QString &optionName, int optionValue); Q_INVOKABLE void setOptionBoolean(const QString &optionName, bool optionValue); @@ -219,6 +220,7 @@ signals: void notificationUpdated(const QVariantMap updatedNotification); void chatNotificationSettingsUpdated(const QString &chatId, const QVariantMap chatNotificationSettings); void messageContentUpdated(qlonglong chatId, qlonglong messageId, const QVariantMap &newContent); + void messageEditedUpdated(qlonglong chatId, qlonglong messageId, const QVariantMap &replyMarkup); void messagesDeleted(qlonglong chatId, const QList &messageIds); void chatsReceived(const QVariantMap &chats); void chatReceived(const QVariantMap &chat);