support basic bot messages (reply markup)

only inlineKeyboardButtonTypeCallback and inlineKeyboardButtonTypeUrl are implemented.
This commit is contained in:
John Gibbon 2020-12-27 00:01:59 +01:00
parent 13a91fa0e7
commit d0f33969eb
13 changed files with 217 additions and 7 deletions

View file

@ -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 \

28
images/icon-s-link.svg Normal file
View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xml:space="preserve"
style="enable-background:new 0 0 32 32;"
viewBox="0 0 32 32"
height="32"
width="32"
y="0px"
x="0px"
id="Layer_1"
version="1.1"><metadata
id="metadata19"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs17" />
<path
id="rect853-3-7"
d="m 5.2218178,19.08774 c -2.1185651,2.118565 -2.1185651,5.573984 -4e-7,7.692548 2.1185648,2.118565 5.5739836,2.118565 7.6925486,0 l 4.866883,-4.866883 c 2.118565,-2.118565 2.118565,-5.573984 0,-7.692549 -2.118564,-2.118564 -5.573983,-2.118564 -7.692548,0 z m 1.4142135,1.414213 4.8668837,-4.866883 c 1.359552,-1.359552 3.504569,-1.359552 4.864121,0 1.359553,1.359553 1.359552,3.504569 0,4.864121 l -4.866884,4.866883 c -1.359552,1.359553 -3.5045682,1.359554 -4.864121,10e-7 -1.3595521,-1.359552 -1.3595521,-3.504569 3e-7,-4.864122 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.6;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /><path
id="rect853-6"
d="m 17.290288,17.295275 c -0.604056,-0.141842 -1.179293,-0.451814 -1.657396,-0.929916 -1.359554,-1.359552 -1.358171,-3.503188 0.0014,-4.86274 l -1.7e-5,-4e-6 4.866884,-4.8668837 c 1.359553,-1.3595528 3.503187,-1.3609338 4.86274,-0.00138 1.359553,1.3595528 1.359553,3.5045687 1e-6,4.8641217 l -4.866884,4.866883 c -0.354012,0.354012 -0.76128,0.615843 -1.19407,0.785494 0.09971,0.686342 0.114666,1.379464 -0.04531,2.08658 0.973641,-0.21685 1.898514,-0.702806 2.653575,-1.457867 l 4.866884,-4.866884 c 2.118565,-2.118565 2.118564,-5.5739835 0,-7.6925482 -2.118565,-2.1185648 -5.572602,-2.1171831 -7.691168,0.00138 l -4.866865,4.8668902 c -2.118564,2.118564 -2.119946,5.572602 -0.0014,7.691167 0.825683,0.825683 1.854427,1.329567 2.927951,1.511652 0.245536,-0.61844 0.301651,-1.33063 0.143675,-1.995945 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /></svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -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();

View file

@ -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

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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
}
}
}
}
}
}
}
}

View file

@ -305,6 +305,13 @@ function enhanceMessageText(formattedText, ignoreEntities) {
{ offset: (entity.offset + entity.length), insertionString: "</u>", removeLength: 0 }
);
break;
case "textEntityTypeBotCommand":
var command = messageText.substring(entity.offset, entity.offset + entity.length);
messageInsertions.push(
{ offset: entity.offset, insertionString: "<a href=\"botCommand://" + command + "\">", removeLength: 0 },
{ offset: (entity.offset + entity.length), insertionString: "</a>", 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);

View file

@ -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

View file

@ -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<qlonglong>)), this, SLOT(handleMessagesDeleted(qlonglong, QList<qlonglong>)));
}
@ -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<qlonglong> &messageIds)
{
LOG("Messages were deleted in a chat" << chatId);

View file

@ -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<qlonglong> &messageIds);
private:

View file

@ -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");

View file

@ -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<qlonglong> &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);
};

View file

@ -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);

View file

@ -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<qlonglong> &messageIds);
void chatsReceived(const QVariantMap &chats);
void chatReceived(const QVariantMap &chat);