Basic sticker support

This commit is contained in:
Nicolas Werner 2021-07-15 20:37:52 +02:00
parent 1dc20f9164
commit 8a1666bc88
No known key found for this signature in database
GPG Key ID: C8D75E610773F2D9
17 changed files with 419 additions and 37 deletions

View File

@ -355,6 +355,7 @@ set(SRC_FILES
src/Olm.cpp
src/RegisterPage.cpp
src/SSOHandler.cpp
src/ImagePackModel.cpp
src/TrayIcon.cpp
src/UserSettingsPage.cpp
src/UsersModel.cpp
@ -559,6 +560,7 @@ qt5_wrap_cpp(MOC_HEADERS
src/MxcImageProvider.h
src/RegisterPage.h
src/SSOHandler.h
src/ImagePackModel.h
src/TrayIcon.h
src/UserSettingsPage.h
src/UsersModel.h

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="sticky-note" class="svg-inline--fa fa-sticky-note fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M312 320h136V56c0-13.3-10.7-24-24-24H24C10.7 32 0 42.7 0 56v400c0 13.3 10.7 24 24 24h264V344c0-13.2 10.8-24 24-24zm129 55l-98 98c-4.5 4.5-10.6 7-17 7h-6V352h128v6.1c0 6.3-2.5 12.4-7 16.9z"></path></svg>

After

Width:  |  Height:  |  Size: 429 B

View File

@ -8,6 +8,7 @@ import im.nheko 1.0
TextEdit {
id: r
textFormat: TextEdit.RichText
readOnly: true
focus: false
@ -19,14 +20,13 @@ TextEdit {
onLinkActivated: Nheko.openLink(link)
ToolTip.visible: hoveredLink
ToolTip.text: hoveredLink
Component.onCompleted: {
TimelineManager.fixImageRendering(r.textDocument, r);
}
CursorShape {
anchors.fill: parent
cursorShape: hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
Component.onCompleted: {
TimelineManager.fixImageRendering(r.textDocument, r)
}
}

View File

@ -2,6 +2,7 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "./emoji"
import "./voip"
import QtQuick 2.12
import QtQuick.Controls 2.3
@ -87,7 +88,7 @@ Rectangle {
Layout.alignment: Qt.AlignBottom // | Qt.AlignHCenter
Layout.maximumHeight: Window.height / 4
Layout.minimumHeight: Settings.fontSize
implicitWidth: inputBar.width - 4 * (22 + 16) - 24
implicitWidth: inputBar.width - 5 * (22 + 16) - 24
TextArea {
id: messageInput
@ -319,6 +320,30 @@ Rectangle {
}
ImageButton {
id: stickerButton
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
Layout.margins: 8
hoverEnabled: true
width: 22
height: 22
image: ":/icons/icons/ui/sticky-note-solid.svg"
ToolTip.visible: hovered
ToolTip.text: qsTr("Stickers")
onClicked: stickerPopup.visible ? stickerPopup.close() : stickerPopup.show(stickerButton, TimelineManager.completerFor("stickers", room.roomId()), function(row) {
room.input.sticker(stickerPopup.model.sourceModel, row);
TimelineManager.focusMessageInput();
})
StickerPicker {
id: stickerPopup
colors: Nheko.colors
}
}
ImageButton {
id: emojiButton
@ -330,7 +355,7 @@ Rectangle {
image: ":/icons/icons/ui/smile.png"
ToolTip.visible: hovered
ToolTip.text: qsTr("Emoji")
onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, function(emoji) {
onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(function(emoji) {
messageInput.insert(messageInput.cursorPosition, emoji);
TimelineManager.focusMessageInput();
})

View File

@ -92,16 +92,20 @@ ScrollView {
}
}
EmojiButton {
ImageButton {
id: reactButton
visible: chat.model ? chat.model.permissions.canSend(MtxEvent.Reaction) : false
width: 16
hoverEnabled: true
image: ":/icons/icons/ui/smile.png"
ToolTip.visible: hovered
ToolTip.text: qsTr("React")
emojiPicker: emojiPopup
event_id: row.model ? row.model.eventId : ""
onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, function(emoji) {
var event_id = row.model ? row.model.eventId : "";
room.input.reaction(event_id, emoji);
TimelineManager.focusMessageInput();
})
}
ImageButton {

View File

@ -1,23 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "../"
import QtQuick 2.10
import QtQuick.Controls 2.1
import im.nheko 1.0
import im.nheko.EmojiModel 1.0
ImageButton {
id: emojiButton
property var colors: currentActivePalette
property var emojiPicker
property string event_id
image: ":/icons/icons/ui/smile.png"
onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, function(emoji) {
room.input.reaction(event_id, emoji);
TimelineManager.focusMessageInput();
})
}

View File

@ -0,0 +1,174 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "../"
import QtGraphicalEffects 1.0
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import im.nheko 1.0
import im.nheko.EmojiModel 1.0
Menu {
id: stickerPopup
property var callback
property var colors
property alias model: gridView.model
property var textArea
property real highlightHue: Nheko.colors.highlight.hslHue
property real highlightSat: Nheko.colors.highlight.hslSaturation
property real highlightLight: Nheko.colors.highlight.hslLightness
readonly property int stickerDim: 128
readonly property int stickerDimPad: 128 + Nheko.paddingSmall
readonly property int stickersPerRow: 3
function show(showAt, model_, callback) {
console.debug("Showing sticker picker");
model = model_;
stickerPopup.callback = callback;
popup(showAt ? showAt : null);
}
margins: 0
bottomPadding: 1
leftPadding: 1
rightPadding: 1
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
//height: columnView.implicitHeight + 4
//width: columnView.implicitWidth
width: stickersPerRow * stickerDimPad + 20
Rectangle {
color: Nheko.colors.window
height: columnView.implicitHeight + 4
width: stickersPerRow * stickerDimPad + 20
ColumnLayout {
id: columnView
spacing: 0
anchors.leftMargin: 3
anchors.rightMargin: 3
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 2
// Search field
TextField {
id: emojiSearch
Layout.topMargin: 3
Layout.preferredWidth: stickersPerRow * stickerDimPad + 20 - 6
palette: Nheko.colors
background: null
placeholderTextColor: Nheko.colors.buttonText
color: Nheko.colors.text
placeholderText: qsTr("Search")
selectByMouse: true
rightPadding: clearSearch.width
onTextChanged: searchTimer.restart()
onVisibleChanged: {
if (visible)
forceActiveFocus();
}
Timer {
id: searchTimer
interval: 350 // tweak as needed?
onTriggered: stickerPopup.model.searchString = emojiSearch.text
}
ToolButton {
id: clearSearch
visible: emojiSearch.text !== ''
icon.source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? Nheko.colors.highlight : Nheko.colors.buttonText)
focusPolicy: Qt.NoFocus
onClicked: emojiSearch.clear()
hoverEnabled: true
background: null
anchors {
verticalCenter: parent.verticalCenter
right: parent.right
}
// clear the default hover effects.
Image {
height: parent.height - 2 * Nheko.paddingSmall
width: height
source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? Nheko.colors.highlight : Nheko.colors.buttonText)
anchors {
verticalCenter: parent.verticalCenter
right: parent.right
margins: Nheko.paddingSmall
}
}
}
}
// emoji grid
GridView {
id: gridView
Layout.preferredHeight: cellHeight * 3.5
Layout.preferredWidth: stickersPerRow * stickerDimPad + 20
Layout.leftMargin: 4
cellWidth: stickerDimPad
cellHeight: stickerDimPad
boundsBehavior: Flickable.StopAtBounds
clip: true
currentIndex: -1 // prevent sorting from stealing focus
cacheBuffer: 500
// Individual emoji
delegate: AbstractButton {
width: stickerDim
height: stickerDim
hoverEnabled: true
ToolTip.text: ":" + model.shortcode + ": - " + model.body
ToolTip.visible: hovered
// TODO: maybe add favorites at some point?
onClicked: {
console.debug("Picked " + model.shortcode);
stickerPopup.close();
callback(model.originalRow);
}
contentItem: Image {
height: stickerDim
width: stickerDim
source: model.url.replace("mxc://", "image://MxcImage/")
fillMode: Image.PreserveAspectFit
}
background: Rectangle {
anchors.fill: parent
color: hovered ? Nheko.colors.highlight : 'transparent'
radius: 5
}
}
ScrollBar.vertical: ScrollBar {
id: emojiScroll
}
}
}
}
}

View File

@ -26,6 +26,7 @@
<file>icons/ui/search@2x.png</file>
<file>icons/ui/settings.png</file>
<file>icons/ui/settings@2x.png</file>
<file>icons/ui/sticky-note-solid.svg</file>
<file>icons/ui/smile.png</file>
<file>icons/ui/smile@2x.png</file>
<file>icons/ui/speech-bubbles-comment-option.png</file>
@ -150,8 +151,8 @@
<file>qml/ForwardCompleter.qml</file>
<file>qml/TypingIndicator.qml</file>
<file>qml/RoomSettings.qml</file>
<file>qml/emoji/EmojiButton.qml</file>
<file>qml/emoji/EmojiPicker.qml</file>
<file>qml/emoji/StickerPicker.qml</file>
<file>qml/UserProfile.qml</file>
<file>qml/delegates/MessageDelegate.qml</file>
<file>qml/delegates/TextMessage.qml</file>

View File

@ -3382,6 +3382,13 @@ Cache::getChildRoomIds(const std::string &room_id)
return roomids;
}
std::optional<mtx::events::collections::RoomAccountDataEvents>
Cache::getAccountData(mtx::events::EventType type, const std::string &room_id)
{
auto txn = ro_txn(env_);
return getAccountData(txn, type, room_id);
}
std::optional<mtx::events::collections::RoomAccountDataEvents>
Cache::getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id)
{

View File

@ -88,6 +88,12 @@ public:
//! Retrieve if the room is a space
bool getRoomIsSpace(lmdb::txn &txn, lmdb::dbi &statesdb);
//! retrieve a specific event from account data
//! pass empty room_id for global account data
std::optional<mtx::events::collections::RoomAccountDataEvents> getAccountData(
mtx::events::EventType type,
const std::string &room_id = "");
//! Get a specific state event
template<typename T>
std::optional<mtx::events::StateEvent<T>> getStateEvent(const std::string &room_id,

91
src/ImagePackModel.cpp Normal file
View File

@ -0,0 +1,91 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ImagePackModel.h"
#include "Cache_p.h"
#include "CompletionModelRoles.h"
ImagePackModel::ImagePackModel(const std::string &roomId, bool stickers, QObject *parent)
: QAbstractListModel(parent)
, room_id(roomId)
{
auto accountpackV =
cache::client()->getAccountData(mtx::events::EventType::ImagePackInAccountData);
auto enabledRoomPacksV =
cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms);
std::optional<mtx::events::msc2545::ImagePack> accountPack;
if (accountpackV) {
auto tmp =
std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePack>>(
&*accountpackV);
if (tmp)
accountPack = tmp->content;
}
// mtx::events::msc2545::ImagePackRooms *enabledRoomPacks = nullptr;
// if (enabledRoomPacksV)
// enabledRoomPacks =
// std::get_if<mtx::events::msc2545::ImagePackRooms>(&*enabledRoomPacksV);
if (accountPack && (!accountPack->pack || (stickers ? accountPack->pack->is_sticker()
: accountPack->pack->is_emoji()))) {
QString packname;
if (accountPack->pack)
packname = QString::fromStdString(accountPack->pack->display_name);
for (const auto &img : accountPack->images) {
if (img.second.overrides_usage() &&
(stickers ? !img.second.is_sticker() : !img.second.is_emoji()))
continue;
ImageDesc i{};
i.shortcode = QString::fromStdString(img.first);
i.packname = packname;
i.image = img.second;
images.push_back(std::move(i));
}
}
}
QHash<int, QByteArray>
ImagePackModel::roleNames() const
{
return {
{CompletionModel::CompletionRole, "completionRole"},
{CompletionModel::SearchRole, "searchRole"},
{CompletionModel::SearchRole2, "searchRole2"},
{Roles::Url, "url"},
{Roles::ShortCode, "shortcode"},
{Roles::Body, "body"},
{Roles::PackName, "packname"},
{Roles::OriginalRow, "originalRow"},
};
}
QVariant
ImagePackModel::data(const QModelIndex &index, int role) const
{
if (hasIndex(index.row(), index.column(), index.parent())) {
switch (role) {
case CompletionModel::CompletionRole:
return QString::fromStdString(images[index.row()].image.url);
case Roles::Url:
return QString::fromStdString(images[index.row()].image.url);
case CompletionModel::SearchRole:
case Roles::ShortCode:
return images[index.row()].shortcode;
case CompletionModel::SearchRole2:
case Roles::Body:
return QString::fromStdString(images[index.row()].image.body);
case Roles::PackName:
return images[index.row()].packname;
case Roles::OriginalRow:
return index.row();
default:
return {};
}
}
return {};
}

52
src/ImagePackModel.h Normal file
View File

@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QAbstractListModel>
#include <mtx/events/mscs/image_packs.hpp>
class ImagePackModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles
{
Url = Qt::UserRole,
ShortCode,
Body,
PackName,
OriginalRow,
};
ImagePackModel(const std::string &roomId, bool stickers, QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override
{
(void)parent;
return (int)images.size();
}
QVariant data(const QModelIndex &index, int role) const override;
mtx::events::msc2545::PackImage imageAt(int row)
{
if (row < 0 || static_cast<size_t>(row) >= images.size())
return {};
return images.at(static_cast<size_t>(row)).image;
}
private:
std::string room_id;
struct ImageDesc
{
QString shortcode;
QString packname;
mtx::events::msc2545::PackImage image;
};
std::vector<ImageDesc> images;
};

View File

@ -21,6 +21,7 @@
#include "ChatPage.h"
#include "CompletionProxyModel.h"
#include "Config.h"
#include "ImagePackModel.h"
#include "Logging.h"
#include "MainWindow.h"
#include "MatrixClient.h"
@ -501,6 +502,22 @@ InputBar::video(const QString &filename,
room->sendMessageEvent(video, mtx::events::EventType::RoomMessage);
}
void
InputBar::sticker(ImagePackModel *model, int row)
{
if (!model || row < 0)
return;
auto img = model->imageAt(row);
mtx::events::msg::StickerImage sticker{};
sticker.info = img.info.value_or(mtx::common::ImageInfo{});
sticker.url = img.url;
sticker.body = img.body;
room->sendMessageEvent(sticker, mtx::events::EventType::Sticker);
}
void
InputBar::command(QString command, QString args)
{

View File

@ -12,6 +12,7 @@
#include <mtx/responses/messages.hpp>
class TimelineModel;
class ImagePackModel;
class QMimeData;
class QDropEvent;
class QStringList;
@ -57,6 +58,7 @@ public slots:
MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED,
bool rainbowify = false);
void reaction(const QString &reactedEvent, const QString &reactionKey);
void sticker(ImagePackModel *model, int row);
private slots:
void startTyping();

View File

@ -1300,6 +1300,14 @@ struct SendMessageVisitor
sendRoomEvent<mtx::events::msg::KeyVerificationCancel,
mtx::events::EventType::KeyVerificationCancel>(msg);
}
void operator()(mtx::events::Sticker msg)
{
msg.type = mtx::events::EventType::Sticker;
if (cache::isRoomEncrypted(model_->room_id_.toStdString())) {
model_->sendEncryptedMessage(msg, mtx::events::EventType::Sticker);
} else
emit model_->addPendingMessageToStore(msg);
}
TimelineModel *model_;
};
@ -1309,6 +1317,7 @@ TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event)
{
std::visit(
[](auto &msg) {
// gets overwritten for reactions and stickers in SendMessageVisitor
msg.type = mtx::events::EventType::RoomMessage;
msg.event_id = "m" + http::client()->generate_txn_id();
msg.sender = http::client()->user_id().to_string();

View File

@ -410,10 +410,17 @@ template<class T>
void
TimelineModel::sendMessageEvent(const T &content, mtx::events::EventType eventType)
{
mtx::events::RoomEvent<T> msgCopy = {};
msgCopy.content = content;
msgCopy.type = eventType;
emit newMessageToSend(msgCopy);
if constexpr (std::is_same_v<T, mtx::events::msg::StickerImage>) {
mtx::events::Sticker msgCopy = {};
msgCopy.content = content;
msgCopy.type = eventType;
emit newMessageToSend(msgCopy);
} else {
mtx::events::RoomEvent<T> msgCopy = {};
msgCopy.content = content;
msgCopy.type = eventType;
emit newMessageToSend(msgCopy);
}
resetReply();
resetEdit();
}

View File

@ -19,6 +19,7 @@
#include "DelegateChooser.h"
#include "DeviceVerificationFlow.h"
#include "EventAccessors.h"
#include "ImagePackModel.h"
#include "Logging.h"
#include "MainWindow.h"
#include "MatrixClient.h"
@ -144,6 +145,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
qRegisterMetaType<mtx::events::msg::KeyVerificationReady>();
qRegisterMetaType<mtx::events::msg::KeyVerificationRequest>();
qRegisterMetaType<mtx::events::msg::KeyVerificationStart>();
qRegisterMetaType<ImagePackModel *>();
qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
"im.nheko",
@ -593,6 +595,11 @@ TimelineViewManager::completerFor(QString completerName, QString roomId)
auto proxy = new CompletionProxyModel(roomModel);
roomModel->setParent(proxy);
return proxy;
} else if (completerName == "stickers") {
auto stickerModel = new ImagePackModel(roomId.toStdString(), true);
auto proxy = new CompletionProxyModel(stickerModel);
stickerModel->setParent(proxy);
return proxy;
}
return nullptr;
}