Compare commits

...

39 Commits

Author SHA1 Message Date
Carl Schwan
5222f3b221 Disable tooltip in BasicListItem
See https://invent.kde.org/frameworks/kirigami/-/merge_requests/1035
2023-04-22 10:11:43 +02:00
l10n daemon script
a7ee1fac3c GIT_SILENT Sync po/docbooks with svn 2023-04-22 02:08:02 +00:00
l10n daemon script
068bfc948b GIT_SILENT Sync po/docbooks with svn 2023-04-21 01:58:19 +00:00
l10n daemon script
47470f5a6d GIT_SILENT Sync po/docbooks with svn 2023-04-20 02:05:22 +00:00
James Graham
144dc1f8f4 Use kirigami-addons maximise components
Use new kirigami-addons MaximizeImage.qml to replace local FullScreenImage.qml

depends upon libraries/kirigami-addons!88

TODO
- [x] Remove the old `fullscreenimage.qml` before merging as no longer needed

Implements #571
2023-04-19 21:10:30 +00:00
Tobias Fella
4002bb804c Don't require passing tests on windows Qt6 2023-04-19 21:53:28 +02:00
Carl Schwan
760ed24b37 Add loading title to LoadingPage
Otherwise the page is a bit empty
2023-04-19 07:39:49 +00:00
Carl Schwan
489979af43 Fix Ctrl-PageUp/Down navigation 2023-04-19 07:39:23 +00:00
l10n daemon script
e482e12826 GIT_SILENT Sync po/docbooks with svn 2023-04-19 02:03:35 +00:00
Tobias Fella
fa7b9d54e2 Fix logout from account menu 2023-04-18 13:04:25 +00:00
Tobias Fella
f60114c7f6 Refactor account labels
Move them out of NeoChatUser, where they don't make any sense
2023-04-18 08:29:24 +00:00
l10n daemon script
de55253e54 GIT_SILENT Sync po/docbooks with svn 2023-04-18 01:58:53 +00:00
Shooting Star
8479e51051 Check if room is null before enter room
There will be some case, like the notify is still exist, but user have
logout their account, user leave the room, these cause the room become a
nullptr, and when click the notify message, it make neochat coredump
2023-04-17 19:25:53 +00:00
Tobias Fella
bb2fd7c9c4 Fix dialog button role 2023-04-17 19:34:43 +02:00
Nicolas Fella
a0b0a5d47f Set associated window for tray icon 2023-04-17 09:46:04 +00:00
l10n daemon script
10bdc1d3d1 GIT_SILENT Sync po/docbooks with svn 2023-04-17 02:41:30 +00:00
James Graham
6ec7d8d6b4 Use DelegateTypeRole searchmodel
Change EventTypeRole to DelegateTypeRole to match updated message event model.
2023-04-16 08:32:51 +00:00
l10n daemon script
0dca9588ff GIT_SILENT Sync po/docbooks with svn 2023-04-16 01:56:40 +00:00
Tobias Fella
ca03c530b2 Fix various warnings 2023-04-15 18:07:26 +00:00
James Graham
973ec24674 Document searchmodel 2023-04-15 17:57:21 +00:00
James Graham
1da767ff0a Document roomlistmodel 2023-04-15 17:18:33 +00:00
James Graham
89127876f9 Document serverlistmodel 2023-04-15 16:51:32 +00:00
Tobias Fella
47a738a703 Revert "Revert "Implement sending location messages""
This reverts commit 52dafbb6c8.
2023-04-15 16:01:04 +00:00
James Graham
516b1cff88 Document publicroomlistmodel 2023-04-15 09:00:49 +00:00
James Graham
6438977964 Document messagefiltermodel 2023-04-15 08:46:45 +00:00
James Graham
d750263d39 Document keywordnotificationrulemodel 2023-04-15 08:32:35 +00:00
James Graham
3ed952db9e Document emojimodel 2023-04-15 08:15:23 +00:00
James Graham
f8040a1bf6 Guard getTagType and isCloseTag
Add guard clauses for getTagType and isCloseTag to avoid crashing if the string is empty.

CCBUG: 468448
2023-04-15 07:59:05 +00:00
l10n daemon script
d83b31fd86 GIT_SILENT Sync po/docbooks with svn 2023-04-15 01:59:35 +00:00
James Graham
e0dbb657f6 Document devicesmodel 2023-04-14 13:14:39 +00:00
James Graham
a807cc6143 Document customemojimodel 2023-04-14 12:57:05 +00:00
l10n daemon script
fe064c0ef8 GIT_SILENT Sync po/docbooks with svn 2023-04-14 01:50:25 +00:00
James Graham
6cc773426f Document completionmodel
Document completionmodel and remove unnecessary includes
2023-04-13 17:35:44 +00:00
James Graham
db94408ba6 Document completionproxymodel 2023-04-13 17:15:38 +00:00
James Graham
333bd3cdb9 Document collapsestateproxymodel
Document collapsestateproxymodel and make som public functions private.
2023-04-13 16:41:41 +00:00
l10n daemon script
9a0d82eb31 GIT_SILENT Sync po/docbooks with svn 2023-04-13 01:58:59 +00:00
James Graham
0990c0507c Document actionsmodel and cleanup includes 2023-04-12 21:12:14 +00:00
Heiko Becker
9f76ce22c1 GIT_SILENT Update Appstream for new release
(cherry picked from commit 2c3ce1e88b)
2023-04-12 19:09:48 +02:00
l10n daemon script
6acd6075ff GIT_SILENT Sync po/docbooks with svn 2023-04-12 01:55:28 +00:00
89 changed files with 10423 additions and 9452 deletions

View File

@@ -59,4 +59,4 @@ Dependencies:
'frameworks/kdbusaddons': '@latest-kf6'
Options:
require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD', 'Windows' ]
require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD', 'Windows/Qt5' ]

View File

@@ -231,6 +231,7 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="23.04.0" date="2023-04-20"/>
<release version="23.01" date="2023-01-30">
<url>https://plasma-mobile.org/2023/01/30/january-blog-post/</url>
<description>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -593,6 +593,7 @@ void Controller::setActiveConnection(Connection *connection)
}
if (m_connection != nullptr) {
disconnect(m_connection, &Connection::syncError, this, nullptr);
disconnect(m_connection, &Connection::accountDataChanged, this, nullptr);
}
m_connection = connection;
if (connection != nullptr) {
@@ -616,12 +617,18 @@ void Controller::setActiveConnection(Connection *connection)
RoomManager::instance().warning(i18n("File too large to download."), i18n("Contact your matrix server administrator for support."));
}
});
connect(connection, &Connection::accountDataChanged, this, [this](const QString &type) {
if (type == QLatin1String("org.kde.neochat.account_label")) {
Q_EMIT activeAccountLabelChanged();
}
});
} else {
NeoChatConfig::self()->setActiveConnection(QString());
}
NeoChatConfig::self()->save();
Q_EMIT activeConnectionChanged();
Q_EMIT activeConnectionIndexChanged();
Q_EMIT activeAccountLabelChanged();
}
void Controller::saveWindowGeometry()
@@ -809,3 +816,22 @@ bool Controller::isFlatpak() const
return false;
#endif
}
void Controller::setActiveAccountLabel(const QString &label)
{
if (!m_connection) {
return;
}
QJsonObject json{
{"account_label", label},
};
m_connection->setAccountData("org.kde.neochat.account_label", json);
}
QString Controller::activeAccountLabel() const
{
if (!m_connection) {
return {};
}
return m_connection->accountDataJson("org.kde.neochat.account_label")["account_label"].toString();
}

View File

@@ -42,6 +42,7 @@ class Controller : public QObject
Q_PROPERTY(int activeConnectionIndex READ activeConnectionIndex NOTIFY activeConnectionIndexChanged)
Q_PROPERTY(int quotientMinorVersion READ quotientMinorVersion CONSTANT)
Q_PROPERTY(bool isFlatpak READ isFlatpak CONSTANT)
Q_PROPERTY(QString activeAccountLabel READ activeAccountLabel WRITE setActiveAccountLabel NOTIFY activeAccountLabelChanged)
public:
static Controller &instance();
@@ -103,6 +104,23 @@ public:
int quotientMinorVersion() const;
bool isFlatpak() const;
/**
* @brief The label for this account.
*
* Account labels are a concept Specific to NeoChat, allowing accounts to be labelled, e.g. for "Work", "Private", etc.
* @return The label, if it exists, otherwise an empty string
*/
[[nodiscard]] QString activeAccountLabel() const;
/**
* @brief Set the label for this account.
*
* Set to an empty string to remove the label
* @sa Controller::activeAccountLabel
* @param label The label to use, or an empty string
*/
void setActiveAccountLabel(const QString &label);
private:
explicit Controller(QObject *parent = nullptr);
@@ -152,6 +170,7 @@ Q_SIGNALS:
void keyVerificationKey(const QString &sas);
void activeConnectionIndexChanged();
void roomAdded(NeoChatRoom *room);
void activeAccountLabelChanged();
public Q_SLOTS:
void logout(Quotient::Connection *conn, bool serverSideLogout);

View File

@@ -25,6 +25,7 @@ void FileTransferPseudoJob::fileTransferProgress(QString id, qint64 progress, qi
void FileTransferPseudoJob::fileTransferCompleted(QString id, QUrl localFile)
{
Q_UNUSED(localFile);
if (id != m_eventId) {
return;
}

View File

@@ -5,7 +5,6 @@
#include "controller.h"
#include "neochatroom.h"
#include "neochatuser.h"
#include "roommanager.h"
#include <events/roommemberevent.h>
#include <events/roompowerlevelsevent.h>

View File

@@ -9,20 +9,42 @@
class NeoChatRoom;
/**
* @class ActionsModel
*
* This class defines a model for chat actions.
*
* @note A chat action is a message starting with /, resulting in something other
* than a normal message being sent (e.g. /me, /join).
*/
class ActionsModel : public QAbstractListModel
{
public:
/**
* @brief Definition of an action.
*/
struct Action {
// The prefix, without '/' and space after the word
QString prefix;
QString prefix; /**< The prefix, without '/' and space after the word. */
/**
* @brief The function to execute when the action is triggered.
*/
std::function<QString(const QString &, NeoChatRoom *)> handle;
// If this is true, this action transforms a message to a different message and it will be sent.
// If this is false, this message does some action on the client and should not be sent as a message.
/**
* @brief Whether the action is a message type action.
*
* If this is true, a message action will be sent. If this is false, this
* message does some action on the client and should not be sent as a message.
*/
bool messageAction;
// If this action changes the message type, this is the new message type. Otherwise it's nullopt
/**
* @brief The new message type of a message being sent.
*
* For a non-message action or a message action that outputs the same type
* as its input, it's nullopt.
*/
std::optional<Quotient::RoomMessageEvent::MsgType> messageType = std::nullopt;
KLazyLocalizedString parameters;
KLazyLocalizedString description;
KLazyLocalizedString parameters; /**< The input parameters expected by the action. */
KLazyLocalizedString description; /**< The description of the action. */
};
static ActionsModel &instance()
{
@@ -30,18 +52,41 @@ public:
return _instance;
}
/**
* @brief Defines the model roles.
*/
enum Roles {
Prefix = Qt::DisplayRole,
Description,
CompletionType,
Parameters,
Prefix = Qt::DisplayRole, /**< The prefix, without '/' and space after the word. */
Description, /**< The description of the action. */
CompletionType, /**< The completion type (always "action" for this model). */
Parameters, /**< The input parameters expected by the action. */
};
Q_ENUM(Roles);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa EventRoles, QAbstractItemModel::roleNames()
*/
QHash<int, QByteArray> roleNames() const override;
/**
* @brief Return a vector with all supported actions.
*/
QVector<Action> &allActions() const;
private:

View File

@@ -6,26 +6,58 @@
#include "messageeventmodel.h"
#include <QSortFilterProxyModel>
/**
* @class CollapseStateProxyModel
*
* This model aggregates multiple sequential state events into a single entry.
*
* Events are only aggregated if they happened on the same day.
*
* @sa MessageEventModel
*/
class CollapseStateProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
AggregateDisplayRole = MessageEventModel::LastRole + 1,
StateEventsRole,
AuthorListRole,
AggregateDisplayRole = MessageEventModel::LastRole + 1, /**< Single line aggregation of all the state events. */
StateEventsRole, /**< List of state events in the aggregated state. */
AuthorListRole, /**< List of unique authors of the aggregated state event. */
};
/**
* @brief Whether a row should be shown out or not.
*
* @sa QSortFilterProxyModel::filterAcceptsRow
*/
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @brief Get the given role value at the given index.
*
* @sa QSortFilterProxyModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief QString aggregating the text of consecutive state events starting at row.
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractProxyModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
private:
/**
* @brief Aggregation of the text of consecutive state events starting at row.
*
* If state events happen on different days they will be split into two aggregate
* events.
*/
[[nodiscard]] QString aggregateEventToString(int row) const;
/**
* @brief Return a list of consecutive state events starting at row.
*
@@ -33,6 +65,7 @@ public:
* events.
*/
[[nodiscard]] QVariantList stateEventsList(int row) const;
/**
* @brief List of unique authors for the aggregate state events starting at row.
*/

View File

@@ -5,7 +5,6 @@
#include <QDebug>
#include "actionsmodel.h"
#include "chatdocumenthandler.h"
#include "completionproxymodel.h"
#include "customemojimodel.h"
#include "emojimodel.h"

View File

@@ -13,41 +13,89 @@ class UserListModel;
class NeoChatRoom;
class RoomListModel;
/**
* @class CompletionModel
*
* This class defines the model for suggesting completions in chat text.
*
* This model is able to select the appropriate completion type for the input text
* and present a list of options that can be presented to the user.
*/
class CompletionModel : public QAbstractListModel
{
Q_OBJECT
/**
* @brief The current text to search for completions.
*/
Q_PROPERTY(QString text READ text NOTIFY textChanged)
/**
* @brief The current room that the model is getting completions for.
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
/**
* @brief The current type of completion being done on the entered text.
*
* @sa AutoCompletionType
*/
Q_PROPERTY(AutoCompletionType autoCompletionType READ autoCompletionType NOTIFY autoCompletionTypeChanged);
/**
* @brief The RoomListModel to be used for room completions.
*/
Q_PROPERTY(RoomListModel *roomListModel READ roomListModel WRITE setRoomListModel NOTIFY roomListModelChanged);
public:
/**
* @brief Defines the different types of completion available.
*/
enum AutoCompletionType {
User,
Room,
Emoji,
Command,
None,
User, /**< A user in the current room. */
Room, /**< A matrix room. */
Emoji, /**< An emoji. */
Command, /**< A / command. */
None, /**< No available completion for the current text. */
};
Q_ENUM(AutoCompletionType)
/**
* @brief Defines the model roles.
*/
enum Roles {
Text = Qt::DisplayRole,
Subtitle,
Icon,
ReplacedText,
Text = Qt::DisplayRole, /**< The main text to show. */
Subtitle, /**< The subtitle text to show. */
Icon, /**< The icon to show. */
ReplacedText, /**< The text to replace the input text with for the completion. */
};
Q_ENUM(Roles);
CompletionModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa EventRoles, QAbstractItemModel::roleNames()
*/
QHash<int, QByteArray> roleNames() const override;
QString text() const;
void setText(const QString &text, const QString &fullText);
void updateCompletion();
NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
@@ -56,6 +104,7 @@ public:
void setRoomListModel(RoomListModel *roomListModel);
AutoCompletionType autoCompletionType() const;
void setAutoCompletionType(AutoCompletionType autoCompletionType);
Q_SIGNALS:
void textChanged();
@@ -70,7 +119,7 @@ private:
NeoChatRoom *m_room = nullptr;
AutoCompletionType m_autoCompletionType = None;
void setAutoCompletionType(AutoCompletionType autoCompletionType);
void updateCompletion();
UserListModel *m_userListModel;
RoomListModel *m_roomListModel;

View File

@@ -2,9 +2,8 @@
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "completionproxymodel.h"
#include <QDebug>
#include "neochatroom.h"
#include <QDebug>
bool CompletionProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
@@ -44,7 +43,6 @@ int CompletionProxyModel::secondaryFilterRole() const
void CompletionProxyModel::setSecondaryFilterRole(int role)
{
m_secondaryFilterRole = role;
Q_EMIT secondaryFilterRoleChanged();
}
QString CompletionProxyModel::filterText() const
@@ -55,7 +53,6 @@ QString CompletionProxyModel::filterText() const
void CompletionProxyModel::setFilterText(const QString &filterText)
{
m_filterText = filterText;
Q_EMIT filterTextChanged();
}
void CompletionProxyModel::setFullText(const QString &fullText)

View File

@@ -5,28 +5,71 @@
#include <QSortFilterProxyModel>
/**
* @class CompletionProxyModel
*
* A filter model to sort and filter completion results.
*
* This model is designed to work with multiple source models depending upon the
* completion type.
*
* A model value will be shown if its primary or secondary role values start with
* the filter text. The exception is if the full text perfectly matches
* the primary filter role value in which case the completion ends (i.e. the filter
* will return no results).
*
* @note The filter is primarily design to work with strings, therefore make sure
* that the source model roles that are to be filtered are strings.
*/
class CompletionProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
Q_PROPERTY(int secondaryFilterRole READ secondaryFilterRole WRITE setSecondaryFilterRole NOTIFY secondaryFilterRoleChanged)
Q_PROPERTY(QString filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged)
public:
/**
* @brief Wether a row should be shown or not.
*
* @sa QSortFilterProxyModel::filterAcceptsRow
*/
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
/**
* @brief Returns true if the value of source_left is less than source_right.
*
* @sa QSortFilterProxyModel::lessThan
*/
bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
/**
* @brief Get the current secondary filter role.
*/
int secondaryFilterRole() const;
/**
* @brief Set the secondary filter role.
*
* Refer to the source model for what value corresponds to what role.
*/
void setSecondaryFilterRole(int role);
/**
* @brief Get the current text being used to filter the source model.
*/
QString filterText() const;
/**
* @brief Set the text to be used to filter the source model.
*/
void setFilterText(const QString &filterText);
/**
* @brief Set the full text in the chatbar after the completion start.
*
* This is used to automatically end the completion if the user replicated the
* primary filter role value perfectly.
*/
void setFullText(const QString &fullText);
Q_SIGNALS:
void secondaryFilterRoleChanged();
void filterTextChanged();
private:
int m_secondaryFilterRole = -1;
QString m_filterText;

View File

@@ -58,7 +58,7 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
auto job = Controller::instance().activeConnection()->uploadFile(location.toLocalFile());
if (running(job)) {
connect(job, &BaseJob::success, this, [this, name, job] {
connect(job, &BaseJob::success, this, [name, job] {
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes");
auto json = data != nullptr ? data->contentJson() : QJsonObject();
auto emojiData = json["images"].toObject();
@@ -141,6 +141,8 @@ QVariant CustomEmojiModel::data(const QModelIndex &idx, int role) const
return QUrl(QStringLiteral("image://mxc/") + data.url.mid(6));
case Roles::MxcUrl:
return data.url.mid(6);
default:
return {};
}
return QVariant();

View File

@@ -17,19 +17,29 @@ struct CustomEmoji {
Q_PROPERTY(QString name MEMBER name)
};
/**
* @class CustomEmojiModel
*
* This class defines the model for custom user emojis.
*
* This is based upon the im.ponies.user_emotes spec (MSC2545).
*/
class CustomEmojiModel : public QAbstractListModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
Name = Qt::DisplayRole,
ImageURL,
ModelData, // for emulating the regular emoji model's usage, otherwise the UI code would get too complicated
MxcUrl = 50,
DisplayRole = 51,
ReplacedTextRole = 52,
DescriptionRole = 53, // also invalid, reserved
Name = Qt::DisplayRole, /**< The name of the emoji. */
ImageURL, /**< The URL for the custom emoji. */
ModelData, /**< for emulating the regular emoji model's usage, otherwise the UI code would get too complicated. */
MxcUrl = 50, /**< The mxc source URL for the custom emoji. */
DisplayRole = 51, /**< The name of the emoji. For compatibility with EmojiModel. */
ReplacedTextRole = 52, /**< The name of the emoji. For compatibility with EmojiModel. */
DescriptionRole = 53, /**< Invalid, reserved. For compatibility with EmojiModel. */
};
Q_ENUM(Roles);
@@ -39,14 +49,45 @@ public:
return _instance;
}
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
QHash<int, QByteArray> roleNames() const override;
/**
* @brief Substitute any custom emojis for an image in the input text.
*/
Q_INVOKABLE QString preprocessText(const QString &it);
/**
* @brief Return a list of custom emojis where the name contains the filter text.
*/
Q_INVOKABLE QVariantList filterModel(const QString &filter);
/**
* @brief Add a new emoji to the model.
*/
Q_INVOKABLE void addEmoji(const QString &name, const QUrl &location);
/**
* @brief Remove an emoji from the model.
*/
Q_INVOKABLE void removeEmoji(const QString &name);
private:

View File

@@ -12,28 +12,67 @@ namespace Quotient
class Connection;
}
/**
* @class DevicesModel
*
* This class defines the model for managing the devices of the local user.
*
* A device is any session where the local user is logged into a client. This means
* the same physical device can have multiple sessions for example if the user uses
* multiple clients on the same machine.
*/
class DevicesModel : public QAbstractListModel
{
Q_OBJECT
/**
* @brief The current connection that the model is getting its devices from.
*/
Q_PROPERTY(Quotient::Connection *connection READ connection NOTIFY connectionChanged)
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
Id,
DisplayName,
LastIp,
LastTimestamp,
Id, /**< The device ID. */
DisplayName, /**< Display name set by the user for this device. */
LastIp, /**< The IP address where this device was last seen. */
LastTimestamp, /**< The timestamp when this devices was last seen. */
};
Q_ENUM(Roles);
DevicesModel(QObject *parent = nullptr);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
QVariant data(const QModelIndex &index, int role) const override;
QHash<int, QByteArray> roleNames() const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
int rowCount(const QModelIndex &parent) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
QHash<int, QByteArray> roleNames() const override;
/**
* @brief Logout the device at the given index.
*/
Q_INVOKABLE void logout(int index, const QString &password);
/**
* @brief Set the display name of the device at the given index.
*/
Q_INVOKABLE void setName(int index, const QString &name);
Quotient::Connection *connection() const;

View File

@@ -51,12 +51,30 @@ struct Emoji {
Q_DECLARE_METATYPE(Emoji)
/**
* @class EmojiModel
*
* This class defines the model for visualising a list of emojis.
*/
class EmojiModel : public QAbstractListModel
{
Q_OBJECT
/**
* @brief Return a list of recently used emojis.
*/
Q_PROPERTY(QVariantList history READ history NOTIFY historyChanged)
/**
* @brief Return a list of emoji categories.
*
* @note No custom emoji categories will be included.
*/
Q_PROPERTY(QVariantList categories READ categories CONSTANT)
/**
* @brief Return a list of emoji categories with custom emojis.
*/
Q_PROPERTY(QVariantList categoriesWithCustom READ categoriesWithCustom CONSTANT)
public:
@@ -66,47 +84,92 @@ public:
return _instance;
}
/**
* @brief Defines the model roles.
*/
enum RoleNames {
ShortNameRole = Qt::DisplayRole,
UnicodeRole,
InvalidRole = 50,
DisplayRole = 51,
ReplacedTextRole = 52,
DescriptionRole = 53,
ShortNameRole = Qt::DisplayRole, /**< The name of the emoji. */
UnicodeRole, /**< The unicode character of the emoji. */
InvalidRole = 50, /**< Invalid, reserved. */
DisplayRole = 51, /**< The display text for an emoji. */
ReplacedTextRole = 52, /**< The text to replace the short name with (i.e. the unicode character). */
DescriptionRole = 53, /**< The long description of an emoji. */
};
Q_ENUM(RoleNames);
/**
* @brief Defines the potential categories an emoji can be placed in.
*/
enum Category {
Custom,
Search,
SearchNoCustom,
History,
HistoryNoCustom,
Smileys,
People,
Nature,
Food,
Activities,
Travel,
Objects,
Symbols,
Flags,
Component,
Custom, /**< A custom user emoji. */
Search, /**< The results of a filter. */
SearchNoCustom, /**< The results of a filter with no custom emojis. */
History, /**< Recently used emojis. */
HistoryNoCustom, /**< Recently used emojis with no custom emojis. */
Smileys, /**< Smileys & emotion emojis. */
People, /**< People & Body emojis. */
Nature, /**< Animals & Nature emojis. */
Food, /**< Food & Drink emojis. */
Activities, /**< Activities emojis. */
Travel, /**< Travel & Places emojis. */
Objects, /**< Objects emojis. */
Symbols, /**< Symbols emojis. */
Flags, /**< Flags emojis. */
Component, /**< ??? */
};
Q_ENUM(Category)
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa RoleNames, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE QVariantList history() const;
/**
* @brief Return a filtered list of emojis.
*
* @note This includes custom emojis, use filterModelNoCustom to return a result
* without custom emojis.
*
* @sa filterModelNoCustom
*/
Q_INVOKABLE static QVariantList filterModel(const QString &filter, bool limit = true);
/**
* @brief Return a filtered list of emojis without custom emojis.
*
* @note Use filterModel to return a result with custom emojis.
*
* @sa filterModel
*/
Q_INVOKABLE static QVariantList filterModelNoCustom(const QString &filter, bool limit = true);
/**
* @brief Return a list of emojis for the given category.
*/
Q_INVOKABLE QVariantList emojis(Category category) const;
/**
* @brief Return a list of emoji tones for the given base emoji.
*/
Q_INVOKABLE QVariantList tones(const QString &baseEmoji) const;
Q_INVOKABLE QVariantList history() const;
QVariantList categories() const;
QVariantList categoriesWithCustom() const;

View File

@@ -7,23 +7,54 @@
#include <QAbstractListModel>
/**
* @class KeywordNotificationRuleModel
*
* This class defines the model for managing notification push rule keywords.
*/
class KeywordNotificationRuleModel : public QAbstractListModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*/
enum EventRoles {
NameRole = Qt::DisplayRole,
NameRole = Qt::DisplayRole, /**< The push rule keyword. */
};
KeywordNotificationRuleModel(QObject *parent = nullptr);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa EventRoles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @brief Add a new keyword to the model.
*/
Q_INVOKABLE void addKeyword(const QString &keyword);
/**
* @brief Remove a keyword from the model.
*/
Q_INVOKABLE void removeKeywordAtIndex(int index);
private Q_SLOTS:

View File

@@ -498,7 +498,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == SourceRole) {
return evt.originalJson();
return QJsonDocument(evt.fullJson()).toJson();
}
if (role == DelegateTypeRole) {
@@ -983,7 +983,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == IsPendingRole) {
return row < m_currentRoom->pendingEvents().size();
return row < static_cast<int>(m_currentRoom->pendingEvents().size());
}
return {};

View File

@@ -5,10 +5,21 @@
#include <QSortFilterProxyModel>
/**
* @class MessageFilterModel
*
* This model filters out any messages that should be hidden.
*
* Deleted messages are only hidden if the user hasn't set them to be shown.
*/
class MessageFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
MessageFilterModel(QObject *parent = nullptr);
/**
* @brief Custom filter function to remove hidden messages.
*/
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
};

View File

@@ -12,6 +12,11 @@ PublicRoomListModel::PublicRoomListModel(QObject *parent)
{
}
Quotient::Connection *PublicRoomListModel::connection() const
{
return m_connection;
}
void PublicRoomListModel::setConnection(Connection *conn)
{
if (m_connection == conn) {
@@ -47,6 +52,11 @@ void PublicRoomListModel::setConnection(Connection *conn)
Q_EMIT hasMoreChanged();
}
QString PublicRoomListModel::server() const
{
return m_server;
}
void PublicRoomListModel::setServer(const QString &value)
{
if (m_server == value) {
@@ -76,6 +86,11 @@ void PublicRoomListModel::setServer(const QString &value)
Q_EMIT hasMoreChanged();
}
QString PublicRoomListModel::keyword() const
{
return m_keyword;
}
void PublicRoomListModel::setKeyword(const QString &value)
{
if (m_keyword == value) {

View File

@@ -13,54 +13,96 @@ namespace Quotient
class Connection;
}
/**
* @class PublicRoomListModel
*
* This class defines the model for visualising a list of public rooms.
*
* The model finds the public rooms visible to the given server (which doesn't have
* to be the user's home server) and can also apply a filter if desired.
*
* Due to the fact that the public room list could be huge the model is lazily loaded
* and requires that the next batch of rooms be manually called.
*/
class PublicRoomListModel : public QAbstractListModel
{
Q_OBJECT
/**
* @brief The current connection that the model is getting its rooms from.
*/
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
/**
* @brief The server to get the public room list from.
*/
Q_PROPERTY(QString server READ server WRITE setServer NOTIFY serverChanged)
/**
* @brief The filter keyword for the list of public rooms.
*/
Q_PROPERTY(QString keyword READ keyword WRITE setKeyword NOTIFY keywordChanged)
/**
* @brief Whether the model has more items to load.
*/
Q_PROPERTY(bool hasMore READ hasMore NOTIFY hasMoreChanged)
public:
/**
* @brief Defines the model roles.
*/
enum EventRoles {
NameRole = Qt::DisplayRole + 1,
AvatarRole,
TopicRole,
RoomIDRole,
AliasRole,
MemberCountRole,
AllowGuestsRole,
WorldReadableRole,
IsJoinedRole,
NameRole = Qt::DisplayRole + 1, /**< The name of the room. */
AvatarRole, /**< The source URL for the room's avatar. */
TopicRole, /**< The room topic. */
RoomIDRole, /**< The room matrix ID. */
AliasRole, /**< The room canonical alias. */
MemberCountRole, /**< The number of members in the room. */
AllowGuestsRole, /**< Whether the room allows guest users. */
WorldReadableRole, /**< Whether the room events can be seen by non-members. */
IsJoinedRole, /**< Whether the local user has joined the room. */
};
PublicRoomListModel(QObject *parent = nullptr);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role = NameRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa EventRoles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] Quotient::Connection *connection() const
{
return m_connection;
}
[[nodiscard]] Quotient::Connection *connection() const;
void setConnection(Quotient::Connection *conn);
[[nodiscard]] QString server() const
{
return m_server;
}
[[nodiscard]] QString server() const;
void setServer(const QString &value);
[[nodiscard]] QString keyword() const
{
return m_keyword;
}
[[nodiscard]] QString keyword() const;
void setKeyword(const QString &value);
[[nodiscard]] bool hasMore() const;
/**
* @brief Load the next set of rooms.
*
* @param count the maximum number of rooms to load.
*/
Q_INVOKABLE void next(int count = 50);
private:

View File

@@ -69,6 +69,11 @@ RoomListModel::RoomListModel(QObject *parent)
RoomListModel::~RoomListModel() = default;
Quotient::Connection *RoomListModel::connection() const
{
return m_connection;
}
void RoomListModel::setConnection(Connection *connection)
{
if (connection == m_connection) {
@@ -237,6 +242,11 @@ void RoomListModel::handleNotifications()
}
#endif
int RoomListModel::notificationCount() const
{
return m_notificationCount;
}
void RoomListModel::refreshNotificationCount()
{
int count = 0;
@@ -440,40 +450,40 @@ QHash<int, QByteArray> RoomListModel::roleNames() const
return roles;
}
QString RoomListModel::categoryName(int section)
QString RoomListModel::categoryName(int category)
{
switch (section) {
case 1:
switch (category) {
case NeoChatRoomType::Invited:
return i18n("Invited");
case 2:
case NeoChatRoomType::Favorite:
return i18n("Favorite");
case 3:
case NeoChatRoomType::Direct:
return i18n("Direct Messages");
case 4:
case NeoChatRoomType::Normal:
return i18n("Normal");
case 5:
case NeoChatRoomType::Deprioritized:
return i18n("Low priority");
case 6:
case NeoChatRoomType::Space:
return i18n("Spaces");
default:
return "Deadbeef";
}
}
QString RoomListModel::categoryIconName(int section)
QString RoomListModel::categoryIconName(int category)
{
switch (section) {
case 1:
switch (category) {
case NeoChatRoomType::Invited:
return QStringLiteral("user-invisible");
case 2:
case NeoChatRoomType::Favorite:
return QStringLiteral("favorite");
case 3:
case NeoChatRoomType::Direct:
return QStringLiteral("dialog-messages");
case 4:
case NeoChatRoomType::Normal:
return QStringLiteral("group");
case 5:
case NeoChatRoomType::Deprioritized:
return QStringLiteral("object-order-lower");
case 6:
case NeoChatRoomType::Space:
return QStringLiteral("group");
default:
return QStringLiteral("tools-report-bug");
@@ -511,7 +521,7 @@ NeoChatRoom *RoomListModel::roomByAliasOrId(const QString &aliasOrId)
return nullptr;
}
int RoomListModel::indexForRoom(NeoChatRoom *room) const
int RoomListModel::rowForRoom(NeoChatRoom *room) const
{
return m_rooms.indexOf(room);
}

View File

@@ -20,77 +20,137 @@ class NeoChatRoomType : public QObject
Q_OBJECT
public:
/**
* @brief Defines the room list categories a room can be assigned.
*/
enum Types {
Invited = 1,
Favorite,
Direct,
Normal,
Deprioritized,
Space,
Invited = 1, /**< The user has been invited to the room. */
Favorite, /**< The room is set as a favourite. */
Direct, /**< The room is a direct chat. */
Normal, /**< The default category for a joined room. */
Deprioritized, /**< The room is set as low priority. */
Space, /**< The room is a space. */
};
Q_ENUM(Types)
};
/**
* @class RoomListModel
*
* This class defines the model for visualising the user's list of joined rooms.
*/
class RoomListModel : public QAbstractListModel
{
Q_OBJECT
/**
* @brief The current connection that the model is getting its rooms from.
*/
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
/**
* @brief The total number of notifications for all the rooms.
*/
Q_PROPERTY(int notificationCount READ notificationCount NOTIFY notificationCountChanged)
public:
/**
* @brief Defines the model roles.
*/
enum EventRoles {
NameRole = Qt::UserRole + 1,
DisplayNameRole,
AvatarRole,
CanonicalAliasRole,
TopicRole,
CategoryRole,
UnreadCountRole,
NotificationCountRole,
HighlightCountRole,
LastEventRole,
LastActiveTimeRole,
JoinStateRole,
CurrentRoomRole,
CategoryVisibleRole,
SubtitleTextRole,
AvatarImageRole,
IdRole,
IsSpaceRole,
NameRole = Qt::UserRole + 1, /**< The name of the room. */
DisplayNameRole, /**< The display name of the room. */
AvatarRole, /**< The source URL for the room's avatar. */
CanonicalAliasRole, /**< The room canonical alias. */
TopicRole, /**< The room topic. */
CategoryRole, /**< The room category, e.g favourite. */
UnreadCountRole, /**< The number of unread messages in the room. */
NotificationCountRole, /**< The number of notifications in the room. */
HighlightCountRole, /**< The number of highlighted messages in the room. */
LastEventRole, /**< Text for the last event in the room. */
LastActiveTimeRole, /**< The timestamp of the last event sent in the room. */
JoinStateRole, /**< The local user's join state in the room. */
CurrentRoomRole, /**< The room object for the room. */
CategoryVisibleRole, /**< If the room's category is visible. */
SubtitleTextRole, /**< The text to show as the room subtitle. */
AvatarImageRole, /**< The room avatar as an image. */
IdRole, /**< The room matrix ID. */
IsSpaceRole, /**< Whether the room is a space. */
};
Q_ENUM(EventRoles)
RoomListModel(QObject *parent = nullptr);
~RoomListModel() override;
[[nodiscard]] Quotient::Connection *connection() const
{
return m_connection;
}
[[nodiscard]] Quotient::Connection *connection() const;
void setConnection(Quotient::Connection *connection);
void doResetModel();
Q_INVOKABLE [[nodiscard]] NeoChatRoom *roomAt(int row) const;
[[nodiscard]] int notificationCount() const;
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
Q_INVOKABLE [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa EventRoles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE [[nodiscard]] static QString categoryName(int section);
Q_INVOKABLE [[nodiscard]] static QString categoryIconName(int section);
/**
* @brief Return the room at the given row.
*/
Q_INVOKABLE [[nodiscard]] NeoChatRoom *roomAt(int row) const;
/**
* @brief Return a string to represent the given room category.
*/
Q_INVOKABLE [[nodiscard]] static QString categoryName(int category);
/**
* @brief Return a string with the name of the given room category icon.
*/
Q_INVOKABLE [[nodiscard]] static QString categoryIconName(int category);
/**
* @brief Set whether a given category should be visible or not.
*
* @param category the NeoChatRoomType::Types value for the category (it's an
* int due to the pain of Q_INVOKABLES and cpp enums).
* @param visible true if the category should be visible, false if not.
*/
Q_INVOKABLE void setCategoryVisible(int category, bool visible);
/**
* @brief Return whether a room category is set to be visible.
*/
Q_INVOKABLE [[nodiscard]] bool categoryVisible(int category) const;
Q_INVOKABLE [[nodiscard]] int indexForRoom(NeoChatRoom *room) const;
[[nodiscard]] int notificationCount() const
{
return m_notificationCount;
}
/**
* @brief Return the model row for the given room.
*/
Q_INVOKABLE [[nodiscard]] int rowForRoom(NeoChatRoom *room) const;
/**
* @brief Return a room for the given room alias or room matrix ID.
*
* The room must be in the model.
*/
Q_INVOKABLE NeoChatRoom *roomByAliasOrId(const QString &aliasOrId);
private Q_SLOTS:
void doResetModel();
void doAddRoom(Quotient::Room *room);
void updateRoom(Quotient::Room *room, Quotient::Room *prev);
void deleteRoom(Quotient::Room *room);

View File

@@ -43,20 +43,27 @@ void SearchModel::search()
}
SearchJob::RoomEventsCriteria criteria{
m_searchText,
{},
RoomEventFilter{
.rooms = {m_room->id()},
},
"recent",
SearchJob::IncludeEventContext{3, 3, true},
false,
none,
.searchTerm = m_searchText,
.keys = {},
.filter =
RoomEventFilter{
.unreadThreadNotifications = none,
.lazyLoadMembers = true,
.includeRedundantMembers = false,
.notRooms = {},
.rooms = {m_room->id()},
.containsUrl = false,
},
.orderBy = "recent",
.eventContext = SearchJob::IncludeEventContext{3, 3, true},
.includeState = false,
.groupings = none,
};
auto job = m_connection->callApi<SearchJob>(SearchJob::Categories{criteria});
m_job = job;
connect(job, &BaseJob::finished, this, [=] {
connect(job, &BaseJob::finished, this, [this, job] {
beginResetModel();
m_result = job->searchCategories().roomEvents;
endResetModel();
@@ -116,6 +123,7 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
int SearchModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
#ifdef QUOTIENT_07
if (m_result.has_value()) {
return m_result->results.size();
@@ -127,7 +135,7 @@ int SearchModel::rowCount(const QModelIndex &parent) const
QHash<int, QByteArray> SearchModel::roleNames() const
{
return {
{EventTypeRole, "eventType"},
{DelegateTypeRole, "delegateType"},
{DisplayRole, "display"},
{AuthorRole, "author"},
{ShowSectionRole, "showSection"},

View File

@@ -17,24 +17,50 @@ class Connection;
class NeoChatRoom;
/**
* @class SearchModel
*
* This class defines the model for visualising the results of a room message search.
*/
class SearchModel : public QAbstractListModel
{
Q_OBJECT
/**
* @brief The text to search messages for.
*/
Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged)
/**
* @brief The current connection that the model is using to search for messages.
*/
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
/**
* @brief The current room that the search is being done from.
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
/**
* @brief Whether the model is currently searching for messages.
*/
Q_PROPERTY(bool searching READ searching NOTIFY searchingChanged)
public:
/**
* @brief Defines the model roles.
*
* Some of the roles exist only for compatibility with the MessageEventModel,
* since the same delegates are used.
*/
enum Roles {
DisplayRole = Qt::DisplayRole,
EventTypeRole,
ShowAuthorRole,
AuthorRole,
ShowSectionRole,
SectionRole,
TimeRole,
DisplayRole = Qt::DisplayRole, /**< The message string. */
DelegateTypeRole, /**< The type of the event. */
ShowAuthorRole, /**< Whether the author should be shown (always true). */
AuthorRole, /**< The author of the event. */
ShowSectionRole, /**< Whether the section header should be shown. */
SectionRole, /**< The date of the event as a string. */
TimeRole, /**< The timestamp for when the event was sent. */
};
Q_ENUM(Roles);
SearchModel(QObject *parent = nullptr);
@@ -48,13 +74,33 @@ public:
NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
Q_INVOKABLE void search();
bool searching() const;
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
QHash<int, QByteArray> roleNames() const override;
bool searching() const;
/**
* @brief Start searching for messages.
*/
Q_INVOKABLE void search();
Q_SIGNALS:
void searchTextChanged();

View File

@@ -9,33 +9,89 @@
#include <QPointer>
#include <QUrl>
/**
* @class ServerListModel
*
* This class defines the model for visualising a list of matrix servers.
*
* The list of servers is retrieved from the local cache. Any additions are also
* stored locally so that they are retrieved on subsequent instantiations.
*
* The model also automatically adds the local user's home server and matrix.org to
* the model. Finally the model also adds an entry to create a space in the model
* for an "add new server" delegate.
*/
class ServerListModel : public QAbstractListModel
{
Q_OBJECT
public:
/**
* @brief Define the data required to represent a server.
*/
struct Server {
QString url;
bool isHomeServer;
bool isAddServerDelegate;
bool isDeletable;
QString url; /**< Server URL. */
bool isHomeServer; /**< Whether the server is the local user's home server. */
bool isAddServerDelegate; /**< Wether the item is the "add new server" delegate. */
bool isDeletable; /**< Whether the item can be deleted from the model. */
};
/**
* @brief Defines the model roles.
*/
enum EventRoles {
UrlRole = Qt::UserRole + 1,
IsHomeServerRole,
IsAddServerDelegateRole,
IsDeletableRole,
UrlRole = Qt::UserRole + 1, /**< Server URL. */
IsHomeServerRole, /**< Whether the server is the local user's home server. */
IsAddServerDelegateRole, /**< Whether the item is the add new server delegate. */
IsDeletableRole, /**< Whether the item can be deleted from the model. */
};
ServerListModel(QObject *parent = nullptr);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa EventRoles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @brief Start a check to see if the given URL is a valid matrix server.
*
* This function starts the check but due to the requests being asynchronous
* the caller will need to watch the serverCheckComplete signal for confirmation.
* The server URL should be treated as invalid until the signal is emitted true.
*
* @sa serverCheckComplete()
*/
Q_INVOKABLE void checkServer(const QString &url);
/**
* @brief Add a new server to the model.
*
* The server will also be stored in local cache.
*/
Q_INVOKABLE void addServer(const QString &url);
/**
* @brief Remove the server at the given index.
*
* The server will also be removed from local cache.
*/
Q_INVOKABLE void removeServerAtIndex(int index);
Q_SIGNALS:

View File

@@ -49,7 +49,7 @@ void StateModel::setRoom(NeoChatRoom *room)
Q_EMIT roomChanged();
beginResetModel();
endResetModel();
connect(room, &NeoChatRoom::changed, this, [=] {
connect(room, &NeoChatRoom::changed, this, [this] {
beginResetModel();
endResetModel();
});

View File

@@ -254,17 +254,16 @@ const RoomEvent *NeoChatRoom::lastEvent() const
continue;
}
if (event->isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
if (event->isStateEvent() && !NeoChatConfig::showStateEvent()) {
continue;
}
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(event)) {
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::showLeaveJoinEvent()) {
continue;
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::self()->showRename()) {
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::showRename()) {
continue;
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave()
&& !NeoChatConfig::self()->showAvatarUpdate()) {
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::showAvatarUpdate()) {
continue;
}
}
@@ -321,7 +320,7 @@ QString NeoChatRoom::lastEventToString(Qt::TextFormat format, bool stripNewlines
return roomMembername(event->senderId()) + (event->isStateEvent() ? QLatin1String(" ") : QLatin1String(": "))
+ eventToString(*event, format, stripNewlines);
}
return QLatin1String("");
return {};
}
bool NeoChatRoom::isEventHighlighted(const RoomEvent *e) const
@@ -765,6 +764,7 @@ QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
},
#ifdef QUOTIENT_07
[](const PollStartEvent &e) {
Q_UNUSED(e);
return i18n("started a poll");
},
#endif
@@ -1884,3 +1884,27 @@ NeoChatUser *NeoChatRoom::directChatRemoteUser() const
{
return dynamic_cast<NeoChatUser *>(connection()->directChatUsers(this)[0]);
}
void NeoChatRoom::sendLocation(float lat, float lon, const QString &description)
{
QJsonObject locationContent{
{"uri", "geo:%1,%2"_ls.arg(QString::number(lat), QString::number(lon))},
};
if (!description.isEmpty()) {
locationContent["description"] = description;
}
QJsonObject content{
{"body", i18nc("'Lat' and 'Lon' as in Latitude and Longitude", "Lat: %1, Lon: %2", lat, lon)},
{"msgtype", "m.location"},
{"geo_uri", "geo:%1,%2"_ls.arg(QString::number(lat), QString::number(lon))},
{"org.matrix.msc3488.location", locationContent},
{"org.matrix.msc3488.asset",
QJsonObject{
{"type", "m.pin"},
}},
{"org.matrix.msc1767.text", i18nc("'Lat' and 'Lon' as in Latitude and Longitude", "Lat: %1, Lon: %2", lat, lon)},
};
postJson("m.room.message", content);
}

View File

@@ -901,4 +901,13 @@ public Q_SLOTS:
* This will delete all messages by that user in this room that are currently loaded.
*/
void deleteMessagesByUser(const QString &user, const QString &reason);
/**
* @brief Sends a location to a room
* The event is sent in the migration format as specified in MSC3488
* @param lat latitude
* @param lon longitude
* @param description description for the location
*/
void sendLocation(float lat, float lon, const QString &description);
};

View File

@@ -16,13 +16,6 @@ NeoChatUser::NeoChatUser(QString userId, Connection *connection)
{
connect(static_cast<QGuiApplication *>(QGuiApplication::instance()), &QGuiApplication::paletteChanged, this, &NeoChatUser::polishColor);
polishColor();
if (connection->userId() == id()) {
connect(connection, &Connection::accountDataChanged, this, [this](QString type) {
if (type == QLatin1String("org.kde.neochat.account_label")) {
Q_EMIT accountLabelChanged();
}
});
}
}
QColor NeoChatUser::color()
@@ -46,17 +39,3 @@ void NeoChatUser::polishColor()
// https://github.com/quotient-im/libQuotient/wiki/User-color-coding-standard-draft-proposal
setColor(QColor::fromHslF(hueF(), 1, -0.7 * lightness + 0.9, 1));
}
void NeoChatUser::setAccountLabel(const QString &accountLabel)
{
Q_ASSERT(connection()->user()->id() == id());
QJsonObject json;
json["account_label"] = accountLabel;
connection()->setAccountData("org.kde.neochat.account_label", json);
}
QString NeoChatUser::accountLabel() const
{
Q_ASSERT(connection()->user()->id() == id());
return connection()->accountDataJson("org.kde.neochat.account_label")["account_label"].toString();
}

View File

@@ -11,8 +11,6 @@ class NeoChatUser : public Quotient::User
{
Q_OBJECT
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
// Only valid for the local user
Q_PROPERTY(QString accountLabel READ accountLabel WRITE setAccountLabel NOTIFY accountLabelChanged)
public:
NeoChatUser(QString userId, Quotient::Connection *connection);
@@ -20,13 +18,8 @@ public Q_SLOTS:
QColor color();
void setColor(const QColor &color);
// Only valid for the local user
QString accountLabel() const;
void setAccountLabel(const QString &accountLabel);
Q_SIGNALS:
void colorChanged(QColor _t1);
void accountLabelChanged();
private:
QColor m_color;

View File

@@ -71,6 +71,9 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
notification->setDefaultAction(i18n("Open NeoChat in this room"));
connect(notification, &KNotification::defaultActivated, this, [=]() {
WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
if (!room) {
return;
}
if (room->localUser()->id() != Controller::instance().activeConnection()->userId()) {
#ifdef QUOTIENT_07
Controller::instance().setActiveConnection(Accounts.get(room->localUser()->id()));
@@ -116,14 +119,23 @@ void NotificationsManager::postInviteNotification(NeoChatRoom *room, const QStri
});
notification->setActions({i18n("Accept Invitation"), i18n("Reject Invitation")});
connect(notification, &KNotification::action1Activated, this, [room, notification]() {
if (!room) {
return;
}
room->acceptInvitation();
notification->close();
});
connect(notification, &KNotification::action2Activated, this, [room, notification]() {
if (!room) {
return;
}
RoomManager::instance().leaveRoom(room);
notification->close();
});
connect(notification, &KNotification::closed, this, [this, room]() {
if (!room) {
return;
}
m_invitations.remove(room->id());
});

View File

@@ -65,8 +65,17 @@ QQC2.Control {
emojiDialog.open()
}
}
},
Kirigami.Action {
id: mapButton
icon.name: "globe"
property bool isBusy: false
text: i18n("Send a Location")
displayHint: QQC2.AbstractButton.IconOnly
tooltip: text
onTriggered: {
locationChooserComponent.createObject(QQC2.ApplicationWindow.overlay, {room: currentRoom}).open()
}
},
Kirigami.Action {
id: sendAction
@@ -473,4 +482,9 @@ QQC2.Control {
textField.cursorPosition = index + format.start.length + format.end.length;
}
}
Component {
id: locationChooserComponent
LocationChooser {}
}
}

View File

@@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtLocation 5.15
import QtPositioning 5.15
import org.kde.kirigamiaddons.labs.components 1.0 as Components
import org.kde.kirigami 2.15 as Kirigami
Components.AbstractMaximizeComponent {
id: root
required property var room
property var location
title: i18n("Choose a Location")
actions: [
Kirigami.Action {
icon.name: "document-send"
text: i18n("Send this location")
onTriggered: {
root.room.sendLocation(root.location.latitude, root.location.longitude, "");
root.close();
}
enabled: !!root.location
}
]
content: Map {
id: map
plugin: Plugin {
name: "osm"
PluginParameter {
name: "osm.useragent"
value: Application.name + "/" + Application.version + " (kde-devel@kde.org)"
}
PluginParameter {
name: "osm.mapping.providersrepository.address"
value: "https://autoconfig.kde.org/qtlocation/"
}
}
MouseArea {
anchors.fill: parent
onClicked: {
root.location = map.toCoordinate(Qt.point(mouseX, mouseY), false)
}
}
MapQuickItem {
id: point
visible: root.location
anchorPoint.x: sourceItem.width / 2
anchorPoint.y: sourceItem.height * 0.85
coordinate: root.location
autoFadeIn: false
sourceItem: Kirigami.Icon {
width: height
height: Kirigami.Units.iconSizes.huge
source: "gps"
isMask: true
color: Kirigami.Theme.highlightColor
Kirigami.Icon {
anchors.centerIn: parent
anchors.verticalCenterOffset: -parent.height / 8
width: height
height: parent.height / 3 + 1
source: "pin"
isMask: true
color: Kirigami.Theme.highlightColor
}
}
}
}
}

View File

@@ -1,312 +0,0 @@
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import Qt.labs.platform 1.1
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
QQC2.Popup {
id: root
property alias source: image.source
property string filename
property string blurhash: ""
property int imageWidth: -1
property int imageHeight: -1
property var modelData
parent: QQC2.Overlay.overlay
closePolicy: QQC2.Popup.CloseOnEscape
width: parent.width
height: parent.height
modal: true
padding: 0
background: null
ColumnLayout {
anchors.fill: parent
spacing: Kirigami.Units.largeSpacing
QQC2.Control {
Layout.fillWidth: true
contentItem: RowLayout {
spacing: Kirigami.Units.largeSpacing
Kirigami.Avatar {
id: avatar
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
name: modelData.author.name ?? modelData.author.displayName
source: modelData.author.avatarMediaId ? ("image://mxc/" + modelData.author.avatarMediaId) : ""
color: modelData.author.color
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
QQC2.Label {
id: nameLabel
text: modelData.author.displayName
textFormat: Text.PlainText
font.weight: Font.Bold
color: author.color
}
QQC2.Label {
id: timeLabel
text: time.toLocaleString(Qt.locale(), Locale.ShortFormat)
}
}
QQC2.Label {
id: imageLabel
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
text: modelData.display
font.weight: Font.Bold
elide: Text.ElideRight
}
QQC2.ToolButton {
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
text: i18n("Zoom in")
Accessible.name: text
icon.name: "zoom-in"
display: QQC2.AbstractButton.IconOnly
onClicked: {
image.scaleFactor = image.scaleFactor + 0.25
if (image.scaleFactor > 3) {
image.scaleFactor = 3
}
}
QQC2.ToolTip.text: text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
}
QQC2.ToolButton {
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
text: i18n("Zoom out")
Accessible.name: text
icon.name: "zoom-out"
display: QQC2.AbstractButton.IconOnly
onClicked: {
image.scaleFactor = image.scaleFactor - 0.25
if (image.scaleFactor < 0.25) {
image.scaleFactor = 0.25
}
}
QQC2.ToolTip.text: text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
}
QQC2.ToolButton {
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
text: i18n("Rotate left")
Accessible.name: text
icon.name: "image-rotate-left-symbolic"
display: QQC2.AbstractButton.IconOnly
onClicked: image.rotationAngle = image.rotationAngle - 90
QQC2.ToolTip.text: text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
}
QQC2.ToolButton {
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
text: i18n("Rotate right")
Accessible.name: text
icon.name: "image-rotate-right-symbolic"
display: QQC2.AbstractButton.IconOnly
onClicked: image.rotationAngle = image.rotationAngle + 90
QQC2.ToolTip.text: text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
}
QQC2.ToolButton {
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
text: i18n("Save as")
Accessible.name: text
icon.name: "document-save"
display: QQC2.AbstractButton.IconOnly
onClicked: {
var dialog = saveAsDialog.createObject(QQC2.ApplicationWindow.overlay)
dialog.open()
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId)
}
QQC2.ToolTip.text: text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
}
QQC2.ToolButton {
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
text: i18n("Close")
Accessible.name: text
icon.name: "dialog-close"
display: QQC2.AbstractButton.IconOnly
onClicked: {
root.close()
}
QQC2.ToolTip.text: text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
}
}
background: Rectangle {
color: Kirigami.Theme.alternateBackgroundColor
}
Kirigami.Separator {
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
height: 1
}
}
QQC2.BusyIndicator {
Layout.fillWidth: true
visible: image.status !== Image.Ready && root.blurhash === ""
running: visible
}
// Provides container to fill the space that isn't taken up by the top controls and clips the image when zooming makes it larger than the available area.
Item {
id: imageContainer
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.bottomMargin: Kirigami.Units.largeSpacing
clip: true
AnimatedImage {
id: image
property var scaleFactor: 1
property int rotationAngle: 0
property var rotationInsensitiveWidth: Math.min(root.imageWidth > 0 ? root.imageWidth : sourceSize.width, imageContainer.width - Kirigami.Units.largeSpacing * 2)
property var rotationInsensitiveHeight: Math.min(root.imageHeight > 0 ? root.imageHeight : sourceSize.height, imageContainer.height - Kirigami.Units.largeSpacing * 2)
anchors.centerIn: parent
width: rotationAngle % 180 === 0 ? rotationInsensitiveWidth : rotationInsensitiveHeight
height: rotationAngle % 180 === 0 ? rotationInsensitiveHeight : rotationInsensitiveWidth
fillMode: Image.PreserveAspectFit
clip: true
Behavior on width {
NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
}
Behavior on height {
NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
}
Image {
anchors.centerIn: parent
width: image.width
height: image.height
source: root.blurhash !== "" ? ("image://blurhash/" + root.blurhash) : ""
visible: root.blurhash !== "" && parent.status !== Image.Ready
}
transform: [
Rotation {
origin.x: image.width / 2
origin.y: image.height / 2
angle: image.rotationAngle
Behavior on angle {
RotationAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
}
},
Scale {
origin.x: image.width / 2
origin.y: image.height / 2
xScale: image.scaleFactor
yScale: image.scaleFactor
Behavior on xScale {
NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
}
Behavior on yScale {
NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
}
}
]
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
const contextMenu = fileDelegateContextMenu.createObject(parent, {
author: modelData.author,
message: modelData.message,
eventId: modelData.eventId,
source: modelData.source,
file: root.parent,
mimeType: modelData.mimeType,
progressInfo: modelData.progressInfo,
plainMessage: modelData.message,
});
contextMenu.closeFullscreen.connect(root.close)
contextMenu.open();
}
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onClicked: {
root.close()
}
}
}
}
Component {
id: saveAsDialog
FileDialog {
fileMode: FileDialog.SaveFile
folder: Config.lastSaveDirectory.length > 0 ? Config.lastSaveDirectory : StandardPaths.writableLocation(StandardPaths.DownloadLocation)
onAccepted: {
Config.lastSaveDirectory = folder
Config.save()
if (!currentFile) {
return;
}
currentRoom.downloadFile(eventId, currentFile)
}
}
}
onClosed: {
image.scaleFactor = 1
image.rotationAngle = 0
}
}

View File

@@ -0,0 +1,93 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import Qt.labs.platform 1.1
import org.kde.kirigami 2.13 as Kirigami
import org.kde.kirigamiaddons.labs.components 1.0 as Components
import org.kde.neochat 1.0
Components.AlbumMaximizeComponent {
id: root
property var modelData
property list<Components.AlbumModelItem> items: [
Components.AlbumModelItem {
type: root.modelData.delegateType === MessageEventModel.Image ? Components.AlbumModelItem.Image : Components.AlbumModelItem.Video
source: root.modelData.delegateType === MessageEventModel.Video ? modelData.progressInfo.localPath : modelData.mediaUrl
tempSource: modelData.content.info["xyz.amorgan.blurhash"] ? ("image://blurhash/" + modelData.content.info["xyz.amorgan.blurhash"]) : ""
caption: modelData.display
}
]
model: items
initialIndex: 0
leading: RowLayout {
Kirigami.Avatar {
id: userAvatar
implicitWidth: Kirigami.Units.iconSizes.medium
implicitHeight: Kirigami.Units.iconSizes.medium
name: modelData.author.name ?? modelData.author.displayName
source: modelData.author.avatarMediaId ? ("image://mxc/" + modelData.author.avatarMediaId) : ""
color: modelData.author.color
}
ColumnLayout {
spacing: 0
QQC2.Label {
id: userLabel
text: modelData.author.name ?? modelData.author.displayName
color: modelData.author.color
font.weight: Font.Bold
elide: Text.ElideRight
}
QQC2.Label {
id: dateTimeLabel
text: modelData.time.toLocaleString(Qt.locale(), Locale.ShortFormat)
color: Kirigami.Theme.disabledTextColor
elide: Text.ElideRight
}
}
}
onItemRightClicked: {
const contextMenu = fileDelegateContextMenu.createObject(parent, {
author: modelData.author,
message: modelData.message,
eventId: modelData.eventId,
source: modelData.source,
file: parent,
mimeType: modelData.mimeType,
progressInfo: modelData.progressInfo,
plainMessage: modelData.message,
});
contextMenu.closeFullscreen.connect(root.close)
contextMenu.open();
}
onSaveItem: {
var dialog = saveAsDialog.createObject(QQC2.ApplicationWindow.overlay)
dialog.open()
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(modelData.eventId)
}
Component {
id: saveAsDialog
FileDialog {
fileMode: FileDialog.SaveFile
folder: root.saveFolder
onAccepted: {
Config.lastSaveDirectory = folder
Config.save()
if (!currentFile) {
return;
}
currentRoom.downloadFile(eventId, currentFile)
}
}
}
}

View File

@@ -4,9 +4,10 @@
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import Qt.labs.platform 1.1
import QtQml.Models 2.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kirigamiaddons.labs.components 1.0 as Components
import org.kde.neochat 1.0
@@ -115,37 +116,27 @@ TimelineContainer {
}
}
Component {
id: fileDialog
FileDialog {
fileMode: FileDialog.SaveFile
folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
onAccepted: {
currentRoom.downloadFile(eventId, file)
}
}
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: {
img.QQC2.ToolTip.hide()
img.paused = true
fullScreenImage.open()
imageDelegate.ListView.view.interactive = false
var popup = maximizeImageComponent.createObject(QQC2.ApplicationWindow.overlay, {
modelData: model,
})
popup.closed.connect(() => {
imageDelegate.ListView.view.interactive = true
img.paused = false
popup.destroy()
})
popup.open()
}
}
FullScreenImage {
id: fullScreenImage
filename: eventId
source: mediaUrl
blurhash: model.content.info["xyz.amorgan.blurhash"]
imageWidth: content.info.w
imageHeight: content.info.h
modelData: model
onClosed: img.paused = false
Component {
id: maximizeImageComponent
NeochatMaximizeComponent {}
}
function downloadAndOpen() {

View File

@@ -8,6 +8,7 @@ import QtMultimedia 5.15
import Qt.labs.platform 1.1 as Platform
import org.kde.kirigami 2.13 as Kirigami
import org.kde.kirigamiaddons.labs.components 1.0 as Components
import org.kde.neochat 1.0
@@ -310,6 +311,31 @@ TimelineContainer {
}
}
}
QQC2.ToolButton {
id: maximizeButton
display: QQC2.AbstractButton.IconOnly
action: Kirigami.Action {
text: i18n("Maximize")
icon.name: "view-fullscreen"
onTriggered: {
videoDelegate.ListView.view.interactive = false
vid.pause()
var popup = maximizeVideoComponent.createObject(QQC2.ApplicationWindow.overlay, {
modelData: model,
})
popup.closed.connect(() => {
videoDelegate.ListView.view.interactive = true
popup.destroy()
})
popup.open()
}
}
}
Component {
id: maximizeVideoComponent
NeochatMaximizeComponent {}
}
}
background: Kirigami.ShadowedRectangle {
radius: 4

View File

@@ -79,7 +79,7 @@ Kirigami.Page {
text: i18n("Decline")
icon.name: "dialog-cancel"
onClicked: dialog.session.cancelVerification("m.user", "Declined")
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.CancelRole
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.RejectRole
}
}
}

View File

@@ -6,6 +6,7 @@ import QtQuick.Controls 2.12 as QQC2
import org.kde.kirigami 2.19 as Kirigami
Kirigami.Page {
title: i18n("Loading…")
Kirigami.LoadingPlaceholder {
id: loadingIndicator
anchors.centerIn: parent

View File

@@ -36,10 +36,11 @@ QQC2.Menu {
QQC2.MenuItem {
text: i18n("Logout")
icon.name: "list-remove-user"
onTriggered: confirmLogoutDialog.open()
onTriggered: confirmLogoutDialogComponent.createObject(QQC2.ApplicationWindow.overlay).open()
}
Dialog.ConfirmLogout {
id: confirmLogoutDialog
Component {
id: confirmLogoutDialogComponent
Dialog.ConfirmLogout {}
}
}

View File

@@ -58,7 +58,7 @@ Kirigami.ScrollablePage {
Connections {
target: RoomManager
function onCurrentRoomChanged() {
itemSelection.setCurrentIndex(roomListModel.index(roomListModel.indexForRoom(RoomManager.currentRoom), 0), ItemSelectionModel.SelectCurrent)
itemSelection.setCurrentIndex(roomListModel.index(roomListModel.rowForRoom(RoomManager.currentRoom), 0), ItemSelectionModel.SelectCurrent)
}
}
@@ -67,7 +67,7 @@ Kirigami.ScrollablePage {
while (index++ !== listView.count - 1) {
if (condition(listView.itemAtIndex(index))) {
listView.currentIndex = index;
listView.currentItem.action.trigger();
listView.currentItem.clicked();
return;
}
}
@@ -78,7 +78,7 @@ Kirigami.ScrollablePage {
while (index-- !== 0) {
if (condition(listView.itemAtIndex(index))) {
listView.currentIndex = index;
listView.currentItem.action.trigger();
listView.currentItem.clicked();
return;
}
}

View File

@@ -34,6 +34,7 @@ Kirigami.BasicListItem {
bottomPadding: Kirigami.Units.largeSpacing
visible: root.categoryVisible || root.filterText.length > 0 || Config.mergeRoomList
tooltipVisible: false
highlighted: ListView.view.currentIndex === index
focus: true
icon: undefined

View File

@@ -168,7 +168,7 @@ QQC2.ToolBar {
Layout.fillWidth: true
}
QQC2.Label {
text: (Controller.activeConnection.localUser.accountLabel.length > 0 ? (Controller.activeConnection.localUser.accountLabel + " ") : "") + Controller.activeConnection.localUser.id
text: (Controller.activeAccountLabel.length > 0 ? (Controller.activeAccountLabel + " ") : "") + Controller.activeConnection.localUser.id
font.pointSize: displayNameLabel.font.pointSize * 0.8
opacity: 0.7
textFormat: Text.PlainText

View File

@@ -433,12 +433,6 @@ Kirigami.ScrollablePage {
FileDelegateContextMenu {}
}
Component {
id: fullScreenImage
FullScreenImage {}
}
Component {
id: userDetailDialog

View File

@@ -92,7 +92,7 @@ Kirigami.ScrollablePage {
MobileForm.FormTextFieldDelegate {
id: accountLabel
label: i18n("Label:")
text: root.connection ? root.connection.localUser.accountLabel : ""
text: root.connection ? Controller.activeAccountLabel : ""
}
MobileForm.AbstractFormDelegate {
Layout.fillWidth: true
@@ -112,8 +112,8 @@ Kirigami.ScrollablePage {
if (root.connection.localUser.displayName !== name.text) {
root.connection.localUser.rename(name.text);
}
if (root.connection.localUser.accountLabel !== accountLabel.text) {
root.connection.localUser.setAccountLabel(accountLabel.text);
if (Controller.activeAccountLabel !== accountLabel.text) {
Controller.activeAccountLabel = accountLabel.text;
}
}
}

View File

@@ -29,7 +29,7 @@
<file alias="PushNotification.qml">qml/RoomSettings/PushNotification.qml</file>
<file alias="Categories.qml">qml/RoomSettings/Categories.qml</file>
<file alias="Permissions.qml">qml/RoomSettings/Permissions.qml</file>
<file alias="FullScreenImage.qml">qml/Component/FullScreenImage.qml</file>
<file alias="NeochatMaximizeComponent.qml">qml/Component/NeochatMaximizeComponent.qml</file>
<file alias="FancyEffectsContainer.qml">qml/Component/FancyEffectsContainer.qml</file>
<file alias="TypingPane.qml">qml/Component/TypingPane.qml</file>
<file alias="ShimmerGradient.qml">qml/Component/ShimmerGradient.qml</file>
@@ -115,5 +115,6 @@
<file alias="EmojiGrid.qml">qml/Component/Emoji/EmojiGrid.qml</file>
<file alias="SearchPage.qml">qml/Page/SearchPage.qml</file>
<file alias="LocationDelegate.qml">qml/Component/Timeline/LocationDelegate.qml</file>
<file alias="LocationChooser.qml">qml/Component/ChatBox/LocationChooser.qml</file>
</qresource>
</RCC>

View File

@@ -280,6 +280,9 @@ void TextHandler::nextTokenType()
QString TextHandler::getTagType() const
{
if (m_nextToken.isEmpty()) {
return QString();
}
const int tagTypeStart = m_nextToken[1] == u'/' ? 2 : 1;
const int tagTypeEnd = m_nextToken.indexOf(TextRegex::endTagType, tagTypeStart);
return m_nextToken.mid(tagTypeStart, tagTypeEnd - tagTypeStart);
@@ -287,6 +290,9 @@ QString TextHandler::getTagType() const
bool TextHandler::isCloseTag() const
{
if (m_nextToken.isEmpty()) {
return false;
}
return m_nextToken[1] == u'/';
}

View File

@@ -4,6 +4,8 @@
#include "trayicon_sni.h"
#include <KWindowSystem>
#include "windowcontroller.h"
TrayIcon::TrayIcon(QObject *parent)
: KStatusNotifierItem(parent)
{
@@ -13,6 +15,12 @@ TrayIcon::TrayIcon(QObject *parent)
KWindowSystem::setCurrentXdgActivationToken(providedToken());
Q_EMIT showWindow();
});
connect(&WindowController::instance(), &WindowController::windowChanged, this, [this] {
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
setAssociatedWindow(WindowController::instance().window());
#endif
});
}
void TrayIcon::show()

View File

@@ -24,6 +24,13 @@ WindowController &WindowController::instance()
void WindowController::setWindow(QWindow *window)
{
m_window = window;
Q_EMIT windowChanged();
}
QWindow *WindowController::window() const
{
return m_window;
}
void WindowController::restoreGeometry()

View File

@@ -14,11 +14,15 @@ public:
static WindowController &instance();
void setWindow(QWindow *window);
QWindow *window() const;
void restoreGeometry();
void saveGeometry();
void showAndRaiseWindow(const QString &startupId);
Q_SIGNALS:
void windowChanged();
private:
WindowController() = default;