Members of the KDE Community are recommended to subscribe to the kde-community mailing list at https://mail.kde.org/mailman/listinfo/kde-community to allow them to participate in important discussions and receive other important announcements

Commit 87447c89 authored by Xavier's avatar Xavier Committed by Linus Jahn

Implement XEP-0382: Spoiler messages

Spoiler messages are used to hide messages containing NSFW content / very long
messages / secret content by default. Kaidan can now send & receive those messages.
A context action on the chat page has been added for this purpose.

Details about the XEP:
https://xmpp.org/extensions/xep-0382.html
parent 142541b5
Pipeline #3313 passed with stages
in 9 minutes and 7 seconds
......@@ -41,7 +41,7 @@
#include <QSqlQuery>
#include <QSqlRecord>
static const int DATABASE_LATEST_VERSION = 9;
static const int DATABASE_LATEST_VERSION = 10;
static const char *DATABASE_TABLE_INFO = "dbinfo";
static const char *DATABASE_TABLE_MESSAGES = "Messages";
static const char *DATABASE_TABLE_ROSTER = "Roster";
......@@ -145,6 +145,8 @@ void Database::convertDatabase()
convertDatabaseToV8(); version = 8; break;
case 8:
convertDatabaseToV9(); version = 9; break;
case 9:
convertDatabaseToV10(); version = 10; break;
default:
break;
}
......@@ -208,6 +210,8 @@ void Database::createNewDatabase()
"'mediaThumb' BLOB,"
"'mediaHashes' TEXT,"
"'edited' BOOL," // whether the message has been edited
"'spoilerHint' TEXT," //spoiler hint if isSpoiler
"'isSpoiler' BOOL," // message is spoiler
"FOREIGN KEY('author') REFERENCES Roster ('jid'),"
"FOREIGN KEY('recipient') REFERENCES Roster ('jid')"
")"
......@@ -339,6 +343,16 @@ void Database::convertDatabaseToV9()
execQuery(query);
}
void Database::convertDatabaseToV10()
{
QSqlQuery query(database);
query.prepare("ALTER TABLE 'Messages' ADD 'isSpoiler' BOOL");
execQuery(query);
query.prepare("ALTER TABLE 'Messages' ADD 'spoilerHint' TEXT");
execQuery(query);
}
void Database::execQuery(QSqlQuery &query)
{
if (!query.exec()) {
......
......@@ -61,6 +61,7 @@ private:
void convertDatabaseToV7();
void convertDatabaseToV8();
void convertDatabaseToV9();
void convertDatabaseToV10();
void execQuery(QSqlQuery &query);
QSqlDatabase database;
......
......@@ -44,6 +44,9 @@
#define APPLICATION_DESCRIPTION "A simple, user-friendly Jabber/XMPP client"
#define VERSION_STRING "0.4.0-dev"
// XML namespaces
#define NS_SPOILERS "urn:xmpp:spoiler:0"
/**
* Map of JIDs to contact names
*/
......
......@@ -366,7 +366,7 @@ signals:
* you should only send messages to JIDs from your roster, otherwise you
* won't be able to see the message history.
*/
void sendMessage(QString jid, QString message);
void sendMessage(QString jid, QString message, bool isSpoiler, QString spoilerHint);
/**
* Correct the last message
......
......@@ -102,6 +102,14 @@ void MessageHandler::handleMessage(const QXmppMessage &msg)
entry.id = msg.id();
entry.sentByMe = (entry.author == client->configuration().jidBare());
entry.message = msg.body();
for (const QXmppElement &extension : msg.extensions()) {
if (extension.tagName() == "spoiler" &&
extension.attribute("xmlns") == NS_SPOILERS) {
entry.isSpoiler = true;
entry.spoilerHint = extension.value();
break;
}
}
entry.type = MessageType::MessageText; // default to text message without media
// check if message contains a link and also check out of band url
......@@ -166,9 +174,16 @@ void MessageHandler::handleMessage(const QXmppMessage &msg)
if (!entry.sentByMe && !isCarbonMessage)
Notifications::sendMessageNotification(contactName.toStdString(),
msg.body().toStdString());
// TODO: Move back following call to RosterManager::handleMessage when spoiler
// messages are implemented in QXmpp
emit kaidan->getRosterModel()->setLastMessageRequested(contactJid,
entry.isSpoiler ? entry.spoilerHint.isEmpty() ? tr("Spoiler") : entry.spoilerHint
: msg.body()
);
}
void MessageHandler::sendMessage(QString toJid, QString body)
void MessageHandler::sendMessage(QString toJid, QString body, bool isSpoiler, QString spoilerHint)
{
// TODO: Add offline message cache and send when connnected again
if (client->state() != QXmppClient::ConnectedState) {
......@@ -181,19 +196,30 @@ void MessageHandler::sendMessage(QString toJid, QString body)
}
MessageModel::Message msg;
msg.isSpoiler = isSpoiler;
msg.spoilerHint = spoilerHint;
msg.author = client->configuration().jidBare();
msg.recipient = toJid;
msg.id = QXmppUtils::generateStanzaHash(48);
msg.sentByMe = true;
msg.message = body;
msg.type = MessageType::MessageText; // text message without media
msg.timestamp = QDateTime::currentDateTime().toUTC().toString(Qt::ISODate);
msg.timestamp = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
emit model->addMessageRequested(msg);
QXmppMessage m(msg.author, msg.recipient, body);
m.setId(msg.id);
m.setReceiptRequested(true);
if (isSpoiler) {
QXmppElementList extensions = m.extensions();
QXmppElement spoiler = QXmppElement();
spoiler.setTagName("spoiler");
spoiler.setValue(msg.spoilerHint);
spoiler.setAttribute("xmlns", NS_SPOILERS);
extensions.append(spoiler);
m.setExtensions(extensions);
}
if (client->sendPacket(m))
emit model->setMessageAsSentRequested(msg.id);
......
......@@ -72,7 +72,7 @@ public slots:
/**
* Sends a new message to the server and inserts it into the database
*/
void sendMessage(QString toJid, QString body);
void sendMessage(QString toJid, QString body, bool isSpoiler, QString spoilerHint);
/**
* Sends the corrected version of a message
......
......@@ -160,6 +160,7 @@ void MessageModel::updateMessage(const QString id, Message msg)
rec.setValue("edited", msg.edited);
rec.setValue("isSent", msg.isSent);
rec.setValue("isDelivered", msg.isDelivered);
rec.setValue("isSpoiler", msg.isSpoiler);
if (!msg.timestamp.isEmpty())
rec.setValue("timestamp", msg.timestamp);
if (!msg.message.isEmpty())
......@@ -206,6 +207,8 @@ void MessageModel::addMessage(Message msg)
record.setValue("type", (quint8) msg.type);
record.setValue("edited", msg.edited);
record.setValue("mediaUrl", msg.mediaUrl);
record.setValue("isSpoiler", msg.isSpoiler);
record.setValue("spoilerHint", msg.spoilerHint);
if (msg.mediaSize)
record.setValue("mediaSize", msg.mediaSize);
record.setValue("mediaContentType", msg.mediaContentType);
......
......@@ -66,6 +66,8 @@ public:
bool edited = false;
bool isSent = false;
bool isDelivered = false;
bool isSpoiler = false;
QString spoilerHint;
QString mediaUrl;
quint64 mediaSize;
QString mediaContentType;
......
......@@ -134,11 +134,15 @@ void RosterManager::removeContact(const QString jid)
}
}
void RosterManager::handleSendMessage(const QString jid, const QString message)
void RosterManager::handleSendMessage(const QString &jid, const QString &message,
bool isSpoiler, const QString spoilerHint)
{
if (client->state() == QXmppClient::ConnectedState) {
// update last message of the contact
emit model->setLastMessageRequested(jid, message);
emit model->setLastMessageRequested(jid,
isSpoiler ? spoilerHint.isEmpty() ? tr("Spoiler") : spoilerHint
: message
);
// update last exchanged datetime (sorting order in contact list)
QString dateTime = QDateTime::currentDateTime().toUTC().toString(Qt::ISODate);
......@@ -158,11 +162,8 @@ void RosterManager::handleMessage(const QXmppMessage &msg)
QString contactJid = sentByMe ? QXmppUtils::jidToBareJid(msg.to())
: fromJid;
// update last message of the contact
emit model->setLastMessageRequested(contactJid, msg.body());
// update last exchanged datetime (sorting order in contact list)
QString dateTime = QDateTime::currentDateTime().toUTC().toString(Qt::ISODate);
QString dateTime = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
emit model->setLastExchangedRequested(contactJid, dateTime);
// when we sent a message we can ignore all unread message notifications
......
......@@ -56,7 +56,8 @@ public:
public slots:
void addContact(const QString jid, const QString name, const QString msg);
void removeContact(const QString jid);
void handleSendMessage(const QString jid, const QString message);
void handleSendMessage(const QString &jid, const QString &message,
bool isSpoiler = false, const QString isSpoilerMessage = "");
private slots:
void populateRoster();
......
......@@ -40,9 +40,18 @@ import "elements"
Kirigami.ScrollablePage {
property string chatName
property string recipientJid
property bool isWritingSpoiler
title: chatName
keyboardNavigationEnabled: true
actions.contextualActions: [
Kirigami.Action {
visible: !isWritingSpoiler
iconSource: "password-show-off"
text: qsTr("Send a spoiler message")
onTriggered: isWritingSpoiler = true
}
]
SendMediaSheet {
id: sendMediaSheet
......@@ -56,7 +65,6 @@ Kirigami.ScrollablePage {
sendMediaSheet.fileUrl = fileUrl
sendMediaSheet.open()
}
}
function openFileDialog(filterName, filter) {
......@@ -139,6 +147,9 @@ Kirigami.ScrollablePage {
isLastMessage: model.id === kaidan.messageModel.lastMessageId(recipientJid)
textEdit: messageField
edited: model.edited
isSpoiler: model.isSpoiler
isShowingSpoiler: false
spoilerHint: model.spoilerHint
}
}
......@@ -153,7 +164,6 @@ Kirigami.ScrollablePage {
spread: 0.3
cached: true // element is static
}
Layout.fillWidth: true
padding: 0
wheelEnabled: true
background: Rectangle {
......@@ -162,6 +172,7 @@ Kirigami.ScrollablePage {
RowLayout {
anchors.fill: parent
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Controls.ToolButton {
id: attachButton
......@@ -185,32 +196,67 @@ Kirigami.ScrollablePage {
}
}
Controls.TextArea {
id: messageField
ColumnLayout {
Layout.minimumHeight: messageField.height + Kirigami.Units.smallSpacing * 2
Layout.fillWidth: true
placeholderText: qsTr("Compose message")
wrapMode: Controls.TextArea.Wrap
topPadding: Kirigami.Units.gridUnit * 0.8
bottomPadding: topPadding
selectByMouse: true
background: Item {}
state: "compose"
states: [
State {
name: "compose"
},
State {
name: "edit"
spacing: 0
RowLayout {
visible: isWritingSpoiler
Controls.TextArea {
id: spoilerHintField
Layout.fillWidth: true
placeholderText: qsTr("Spoiler hint")
wrapMode: Controls.TextArea.Wrap
selectByMouse: true
background: Item {}
}
Controls.ToolButton {
Layout.preferredWidth: Kirigami.Units.gridUnit * 1.5
Layout.preferredHeight: Kirigami.Units.gridUnit * 1.5
padding: 0
Kirigami.Icon {
source: "tab-close"
smooth: true
anchors.centerIn: parent
width: Kirigami.Units.gridUnit * 1.5
height: width
}
onClicked: {
isWritingSpoiler = false
spoilerHintField.text = ""
}
}
]
Keys.onReturnPressed: {
if (event.key === Qt.Key_Return) {
if (event.modifiers & Qt.ControlModifier) {
messageField.append("")
} else {
sendButton.onClicked()
event.accepted = true
}
Kirigami.Separator {
visible: isWritingSpoiler
Layout.fillWidth: true
}
Controls.TextArea {
id: messageField
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
placeholderText: qsTr("Compose message")
wrapMode: Controls.TextArea.Wrap
selectByMouse: true
background: Item {}
state: "compose"
states: [
State {
name: "compose"
},
State {
name: "edit"
}
]
Keys.onReturnPressed: {
if (event.key === Qt.Key_Return) {
if (event.modifiers & Qt.ControlModifier) {
messageField.append("")
} else {
sendButton.onClicked()
event.accepted = true
}
}
}
}
......@@ -280,15 +326,18 @@ Kirigami.ScrollablePage {
// send the message
if (messageField.state == "compose") {
kaidan.sendMessage(recipientJid, messageField.text)
kaidan.sendMessage(recipientJid, messageField.text,
isWritingSpoiler, spoilerHintField.text)
} else if (messageField.state == "edit") {
kaidan.correctMessage(recipientJid, kaidan.messageModel.lastMessageId(recipientJid),
messageField.text)
}
// clean up the text field
// clean up the text fields
messageField.text = ""
messageField.state = "compose"
spoilerHintField.text = ""
isWritingSpoiler = false
// reenable the button
sendButton.enabled = true
......
......@@ -54,10 +54,13 @@ RowLayout {
property string name
property var upload: {
if (mediaType !== Enums.MessageText &&
kaidan.transferCache.hasUpload(msgId)) {
kaidan.transferCache.hasUpload(msgId)) {
kaidan.transferCache.jobByMessageId(model.id)
}
}
property bool isSpoiler
property bool isShowingSpoiler
property string spoilerHint
// own messages are on the right, others on the left
layoutDirection: sentByMe ? Qt.RightToLeft : Qt.LeftToRight
......@@ -100,10 +103,17 @@ RowLayout {
Rectangle {
id: box
anchors.fill: parent
color: sentByMe ? Kirigami.Theme.complementaryTextColor
: Kirigami.Theme.highlightColor
: Kirigami.Theme.highlightColor
radius: Kirigami.Units.smallSpacing * 2
layer.enabled: box.visible
layer.effect: DropShadow {
verticalOffset: Kirigami.Units.gridUnit * 0.08
horizontalOffset: Kirigami.Units.gridUnit * 0.08
color: Kirigami.Theme.disabledTextColor
samples: 10
spread: 0.1
}
MouseArea {
anchors.fill: parent
......@@ -122,7 +132,7 @@ RowLayout {
id: contextMenu
Controls.MenuItem {
text: qsTr("Copy Message")
onTriggered: kaidan.copyToClipboard(messageBody)
onTriggered: isShowingSpoiler ? kaidan.copyToClipboard(messageBody) : kaidan.copyToClipboard(spoilerHint)
}
Controls.MenuItem {
......@@ -134,15 +144,6 @@ RowLayout {
}
}
}
layer.enabled: box.visible
layer.effect: DropShadow {
verticalOffset: Kirigami.Units.gridUnit * 0.08
horizontalOffset: Kirigami.Units.gridUnit * 0.08
color: Kirigami.Theme.disabledTextColor
samples: 10
spread: 0.1
}
}
ColumnLayout {
......@@ -150,48 +151,104 @@ RowLayout {
spacing: 0
anchors.centerIn: parent
anchors.margins: 4
RowLayout {
id: spoilerHintRow
visible: isSpoiler
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
if (mouse.button === Qt.LeftButton){
isShowingSpoiler = !isShowingSpoiler
}
}
}
Controls.Label {
id: dateLabeltest
text: spoilerHint == "" ? qsTr("Spoiler") : spoilerHint
color: sentByMe ? Kirigami.Theme.buttonTextColor
: Kirigami.Theme.complementaryTextColor
font.pixelSize: Kirigami.Units.gridUnit * 0.8
}
Controls.ToolButton {
visible: {
mediaType !== Enums.MessageText && !isLoading && mediaLocation === ""
Item {
Layout.fillWidth: true
height: 1
}
text: qsTr("Download")
onClicked: {
print("Donwload")
kaidan.downloadMedia(msgId, mediaGetUrl)
Kirigami.Icon {
height: 28
width: 28
source: isShowingSpoiler ? "password-show-off" : "password-show-on"
color: sentByMe ? Kirigami.Theme.buttonTextColor : Kirigami.Theme.complementaryTextColor
}
}
// media loader
Loader {
id: media
source: {
if (mediaType == Enums.MessageImage &&
mediaLocation !== "")
"ChatMessageImage.qml"
else
""
}
property string sourceUrl: "file://" + mediaLocation
Layout.maximumWidth: root.width - Kirigami.Units.gridUnit * 6
Layout.preferredHeight: loaded ? item.paintedHeight : 0
Kirigami.Separator {
visible: isSpoiler
Layout.fillWidth: true
color: {
var bgColor = sentByMe ? Kirigami.Theme.backgroundColor : Kirigami.Theme.highlightColor
var textColor = sentByMe ? Kirigami.Theme.textColor : Kirigami.Theme.buttonTextColor
return Qt.tint(textColor, Qt.rgba(bgColor.r, bgColor.g, bgColor.b, 0.7))
}
}
// message body
Controls.Label {
visible: messageBody !== ""
text: kaidan.formatMessage(messageBody)
textFormat: Text.StyledText
wrapMode: Text.Wrap
color: sentByMe ? Kirigami.Theme.buttonTextColor
: Kirigami.Theme.complementaryTextColor
onLinkActivated: Qt.openUrlExternally(link)
Layout.maximumWidth: mediaType === Enums.MessageImage && media.width !== 0
? media.width
: root.width - Kirigami.Units.gridUnit * 6
}
ColumnLayout {
visible: isSpoiler && isShowingSpoiler || !isSpoiler
Controls.ToolButton {
visible: {
(mediaType !== Enums.MessageText && !isLoading && mediaLocation === "")
}
text: qsTr("Download")
onClicked: {
print("Donwload")
kaidan.downloadMedia(msgId, mediaGetUrl)
}
}
// media loader
Loader {
id: media
source: {
if (mediaType == Enums.MessageImage &&
mediaLocation !== "")
"ChatMessageImage.qml"
else
""
}
property string sourceUrl: "file://" + mediaLocation
Layout.maximumWidth: root.width - Kirigami.Units.gridUnit * 6
Layout.preferredHeight: loaded ? item.paintedHeight : 0
}
// message body
Controls.Label {
id: messageLabel
visible: messageBody !== ""
text: kaidan.formatMessage(messageBody)
textFormat: Text.StyledText
wrapMode: Text.Wrap
color: sentByMe ? Kirigami.Theme.buttonTextColor
: Kirigami.Theme.complementaryTextColor
onLinkActivated: Qt.openUrlExternally(link)
Layout.maximumWidth: mediaType === Enums.MessageImage && media.width !== 0
? media.width
: root.width - Kirigami.Units.gridUnit * 6
}
Kirigami.Separator {
visible: isSpoiler && isShowingSpoiler
Layout.fillWidth: true
color: {
var bgColor = sentByMe ? Kirigami.Theme.backgroundColor : Kirigami.Theme.highlightColor
var textColor = sentByMe ? Kirigami.Theme.textColor : Kirigami.Theme.buttonTextColor
return Qt.tint(textColor, Qt.rgba(bgColor.r, bgColor.g, bgColor.b, 0.7))
}
}
}
// message meta: date, isRead
RowLayout {
// progress bar for upload/download status
......
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