1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-07-24 17:35:43 +02:00

sidepanel: improve smartlist interface with underlying models

Minor cosmetic changes to the account combo box, search bar, filter
tabs, and smartlist.

Change-Id: Ie8173504859b325374e42f0dbb4e0ae75f3ed740
Gitlab: #373
Gitlab: #374
Gitlab: #388
This commit is contained in:
Andreas Traczyk 2021-04-15 23:17:58 -04:00
parent 0d6d94d124
commit e64a9e7ee7
56 changed files with 1998 additions and 1346 deletions

View file

@ -67,7 +67,11 @@ set(COMMON_SOURCES
${SRC_DIR}/screensaver.cpp
${SRC_DIR}/systemtray.cpp
${SRC_DIR}/appsettingsmanager.cpp
${SRC_DIR}/lrcinstance.cpp)
${SRC_DIR}/lrcinstance.cpp
${SRC_DIR}/selectablelistproxymodel.cpp
${SRC_DIR}/conversationlistmodelbase.cpp
${SRC_DIR}/conversationlistmodel.cpp
${SRC_DIR}/searchresultslistmodel.cpp)
set(COMMON_HEADERS
${SRC_DIR}/avatarimageprovider.h
@ -118,7 +122,11 @@ set(COMMON_HEADERS
${SRC_DIR}/screensaver.h
${SRC_DIR}/systemtray.h
${SRC_DIR}/appsettingsmanager.h
${SRC_DIR}/lrcinstance.h)
${SRC_DIR}/lrcinstance.h
${SRC_DIR}/selectablelistproxymodel.h
${SRC_DIR}/conversationlistmodelbase.h
${SRC_DIR}/conversationlistmodel.h
${SRC_DIR}/searchresultslistmodel.h)
set(QML_LIBS
Qt5::Quick

View file

@ -102,7 +102,6 @@
<file>src/mainview/components/MessageWebView.qml</file>
<file>src/mainview/components/MessageWebViewHeader.qml</file>
<file>src/mainview/components/AccountComboBox.qml</file>
<file>src/mainview/components/ConversationSmartListView.qml</file>
<file>src/mainview/components/CallStackView.qml</file>
<file>src/mainview/components/IncomingCallPage.qml</file>
<file>src/mainview/components/OutgoingCallPage.qml</file>
@ -114,7 +113,6 @@
<file>src/mainview/components/ParticipantOverlay.qml</file>
<file>src/mainview/components/ProjectCreditsScrollView.qml</file>
<file>src/mainview/components/AccountComboBoxPopup.qml</file>
<file>src/mainview/components/ConversationSmartListViewItemDelegate.qml</file>
<file>src/mainview/components/SidePanelTabBar.qml</file>
<file>src/mainview/components/WelcomePageQrDialog.qml</file>
<file>src/mainview/components/ConversationSmartListContextMenu.qml</file>
@ -138,5 +136,8 @@
<file>src/mainview/js/pluginhandlerpickercreation.js</file>
<file>src/mainview/components/FilterTabButton.qml</file>
<file>src/mainview/components/AccountItemDelegate.qml</file>
<file>src/mainview/components/ConversationListView.qml</file>
<file>src/mainview/components/SmartListItemDelegate.qml</file>
<file>src/mainview/components/BadgeNotifier.qml</file>
</qresource>
</RCC>

View file

@ -43,8 +43,6 @@ AccountAdapter::safeInit()
this,
&AccountAdapter::onCurrentAccountChanged);
deselectConversation();
auto accountId = lrcInstance_->getCurrAccId();
setProperties(accountId);
connectAccount(accountId);
@ -65,7 +63,6 @@ AccountAdapter::getDeviceModel()
void
AccountAdapter::changeAccount(int row)
{
deselectConversation(); // Hack UI
auto accountList = lrcInstance_->accountModel().getAccountList();
if (accountList.size() > row) {
lrcInstance_->setSelectedAccountId(accountList.at(row));
@ -269,12 +266,6 @@ AccountAdapter::setCurrAccDisplayName(const QString& text)
lrcInstance_->setCurrAccDisplayName(text);
}
void
AccountAdapter::setSelectedConvId(const QString& convId)
{
lrcInstance_->set_selectedConvUid(convId);
}
lrc::api::profile::Type
AccountAdapter::getCurrentAccountType()
{
@ -347,23 +338,6 @@ AccountAdapter::passwordSetStatusMessageBox(bool success, QString title, QString
}
}
void
AccountAdapter::deselectConversation()
{
if (lrcInstance_->get_selectedConvUid().isEmpty()) {
return;
}
// TODO: remove this unhealthy section
auto currentConversationModel = lrcInstance_->getCurrentConversationModel();
if (currentConversationModel == nullptr) {
return;
}
lrcInstance_->set_selectedConvUid();
}
void
AccountAdapter::connectAccount(const QString& accountId)
{
@ -374,7 +348,7 @@ AccountAdapter::connectAccount(const QString& accountId)
QObject::disconnect(accountProfileUpdatedConnection_);
QObject::disconnect(contactAddedConnection_);
QObject::disconnect(addedToConferenceConnection_);
QObject::disconnect(contactUnbannedConnection_);
QObject::disconnect(bannedStatusChangedConnection_);
accountStatusChangedConnection_
= QObject::connect(accInfo.accountModel,
@ -390,27 +364,22 @@ AccountAdapter::connectAccount(const QString& accountId)
Q_EMIT accountStatusChanged(accountId);
});
contactAddedConnection_
= QObject::connect(accInfo.contactModel.get(),
&lrc::api::ContactModel::contactAdded,
[this, accountId](const QString& contactUri) {
const auto& convInfo = lrcInstance_->getConversationFromConvUid(
lrcInstance_->get_selectedConvUid());
if (convInfo.uid.isEmpty()) {
return;
}
auto& accInfo = lrcInstance_->accountModel().getAccountInfo(
accountId);
if (contactUri
== accInfo.contactModel
->getContact(convInfo.participants.at(0))
.profileInfo.uri) {
/*
* Update conversation.
*/
Q_EMIT updateConversationForAddedContact();
}
});
contactAddedConnection_ = QObject::connect(
accInfo.contactModel.get(),
&lrc::api::ContactModel::contactAdded,
[this, accountId](const QString& contactUri) {
const auto& convInfo = lrcInstance_->getConversationFromConvUid(
lrcInstance_->get_selectedConvUid());
if (convInfo.uid.isEmpty()) {
return;
}
auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId);
auto selectedContactUri
= accInfo.contactModel->getContact(convInfo.participants.at(0)).profileInfo.uri;
if (contactUri == selectedContactUri) {
Q_EMIT selectedContactAdded(convInfo.uid);
}
});
addedToConferenceConnection_
= QObject::connect(accInfo.callModel.get(),
@ -420,12 +389,15 @@ AccountAdapter::connectAccount(const QString& accountId)
lrcInstance_->renderer()->addDistantRenderer(confId);
});
contactUnbannedConnection_ = QObject::connect(accInfo.contactModel.get(),
&lrc::api::ContactModel::bannedStatusChanged,
[this](const QString&, bool banned) {
if (!banned)
Q_EMIT contactUnbanned();
});
bannedStatusChangedConnection_
= QObject::connect(accInfo.contactModel.get(),
&lrc::api::ContactModel::bannedStatusChanged,
[this](const QString& uri, bool banned) {
if (!banned)
Q_EMIT contactUnbanned();
else
Q_EMIT lrcInstance_->contactBanned(uri);
});
} catch (...) {
qWarning() << "Couldn't get account: " << accountId;
}

View file

@ -95,16 +95,14 @@ public:
Q_INVOKABLE bool hasVideoCall();
Q_INVOKABLE bool isPreviewing();
Q_INVOKABLE void setCurrAccDisplayName(const QString& text);
Q_INVOKABLE void setSelectedConvId(const QString& convId = {});
Q_INVOKABLE lrc::api::profile::Type getCurrentAccountType();
Q_INVOKABLE void setCurrAccAvatar(bool fromFile, const QString& source);
Q_SIGNALS:
// Trigger other components to reconnect account related signals.
void accountStatusChanged(QString accountId = {});
void updateConversationForAddedContact();
void accountStatusChanged(QString accountId);
void selectedContactAdded(QString convId);
// Send report failure to QML to make it show the right UI state .
void reportFailure();
@ -119,8 +117,6 @@ private:
lrc::api::profile::Type currentAccountType_ {};
int accountListSize_ {};
void deselectConversation();
// Make account signal connections.
void connectAccount(const QString& accountId);
@ -134,7 +130,7 @@ private:
QMetaObject::Connection accountProfileUpdatedConnection_;
QMetaObject::Connection contactAddedConnection_;
QMetaObject::Connection addedToConferenceConnection_;
QMetaObject::Connection contactUnbannedConnection_;
QMetaObject::Connection bannedStatusChangedConnection_;
QMetaObject::Connection registeredNameSavedConnection_;
AppSettingsManager* settingsManager_;

View file

@ -59,9 +59,9 @@ CallAdapter::CallAdapter(SystemTray* systemTray, LRCInstance* instance, QObject*
[this](const QString& accountId, const QString& convUid) {
acceptACall(accountId, convUid);
Q_EMIT lrcInstance_->notificationClicked();
lrcInstance_->selectConversation(accountId, convUid);
lrcInstance_->selectConversation(convUid, accountId);
updateCall(convUid, accountId);
Q_EMIT callSetupMainViewRequired(accountId, convUid);
Q_EMIT lrcInstance_->conversationUpdated(convUid, accountId);
});
connect(systemTray_,
&SystemTray::declineCallActivated,
@ -200,6 +200,7 @@ CallAdapter::onShowIncomingCallView(const QString& accountId, const QString& con
auto selectedAccountId = lrcInstance_->getCurrAccId();
auto* callModel = lrcInstance_->getCurrentCallModel();
// new call
if (!callModel->hasCall(convInfo.callId)) {
if (QApplication::focusObject() == nullptr || accountId != selectedAccountId) {
showNotification(accountId, convInfo.uid);
@ -219,17 +220,21 @@ CallAdapter::onShowIncomingCallView(const QString& accountId, const QString& con
return;
}
}
Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid);
Q_EMIT lrcInstance_->updateSmartList();
// select
lrcInstance_->selectConversation(convInfo.uid, accountId);
return;
}
// this slot has been triggered as a result of either selecting a conversation
// with an active call, placing a call, or an incoming call for the current
// or any other conversation
auto call = callModel->getCall(convInfo.callId);
auto isCallSelected = lrcInstance_->get_selectedConvUid() == convInfo.uid;
if (call.isOutgoing) {
if (isCallSelected) {
Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid);
// don't reselect
Q_EMIT lrcInstance_->conversationUpdated(convInfo.uid, accountId);
}
} else {
auto accountProperties = lrcInstance_->accountModel().getAccountConfig(selectedAccountId);
@ -254,10 +259,12 @@ CallAdapter::onShowIncomingCallView(const QString& accountId, const QString& con
showNotification(accountId, convInfo.uid);
return;
} else {
Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid);
// only update
Q_EMIT lrcInstance_->conversationUpdated(convInfo.uid, accountId);
}
} else {
Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid);
// only update
Q_EMIT lrcInstance_->conversationUpdated(convInfo.uid, accountId);
}
} else { // Not current conversation
if (currentConvHasCall) {
@ -269,19 +276,19 @@ CallAdapter::onShowIncomingCallView(const QString& accountId, const QString& con
return;
}
}
Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid);
// reselect
lrcInstance_->selectConversation(convInfo.uid, accountId);
}
}
}
Q_EMIT callStatusChanged(static_cast<int>(call.status), accountId, convInfo.uid);
Q_EMIT lrcInstance_->updateSmartList();
}
void
CallAdapter::onShowCallView(const QString& accountId, const QString& convUid)
{
updateCall(convUid, accountId);
Q_EMIT callSetupMainViewRequired(accountId, convUid);
Q_EMIT lrcInstance_->conversationUpdated(convUid, accountId);
}
void
@ -399,8 +406,6 @@ CallAdapter::showNotification(const QString& accountId, const QString& convUid)
from = accInfo.contactModel->bestNameForContact(convInfo.participants[0]);
}
Q_EMIT lrcInstance_->updateSmartList();
#ifdef Q_OS_LINUX
auto contactPhoto = Utils::contactPhoto(lrcInstance_,
convInfo.participants[0],
@ -414,12 +419,11 @@ CallAdapter::showNotification(const QString& accountId, const QString& convUid)
Utils::QImageToByteArray(contactPhoto));
#else
auto onClicked = [this, accountId, convUid = convInfo.uid]() {
const auto& convInfo = lrcInstance_->getConversationFromConvUid(convUid, accountId);
if (convInfo.uid.isEmpty()) {
return;
}
Q_EMIT lrcInstance_->notificationClicked();
Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid);
const auto& convInfo = lrcInstance_->getConversationFromConvUid(convUid, accountId);
if (convInfo.uid.isEmpty())
return;
lrcInstance_->selectConversation(convInfo.uid, accountId);
};
systemTray_->showNotification(tr("is calling you"), from, onClicked);
#endif
@ -484,7 +488,7 @@ CallAdapter::connectCallModel(const QString& accountId)
case lrc::api::call::Status::TIMEOUT:
case lrc::api::call::Status::TERMINATING: {
lrcInstance_->renderer()->removeDistantRenderer(callId);
Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid);
Q_EMIT lrcInstance_->conversationUpdated(convInfo.uid, accountId);
if (convInfo.uid.isEmpty()) {
break;
}
@ -517,7 +521,7 @@ CallAdapter::connectCallModel(const QString& accountId)
/*
* Reset the call view corresponding accountId, uid.
*/
lrcInstance_->set_selectedConvUid(otherConv.uid);
lrcInstance_->selectConversation(otherConv.uid);
updateCall(otherConv.uid, otherConv.accountId, forceCallOnly);
}
}

View file

@ -80,11 +80,9 @@ public:
Q_SIGNALS:
void callStatusChanged(int index, const QString& accountId, const QString& convUid);
void updateConversationSmartList();
void updateParticipantsInfos(const QVariantList& infos,
const QString& accountId,
const QString& callId);
void callSetupMainViewRequired(const QString& accountId, const QString& convUid);
void previewVisibilityNeedToChange(bool visible);
// For Call Overlay

View file

@ -38,6 +38,7 @@ Item {
property alias fillMode: rootImage.fillMode
property alias sourceSize: rootImage.sourceSize
property int transitionDuration: 150
property bool saveToConfig: false
property int mode: AvatarImage.Mode.FromAccount
property string imageProviderIdPrefix: {
@ -178,7 +179,7 @@ Item {
NumberAnimation {
properties: "opacity"
easing.type: Easing.InOutQuad
duration: 400
duration: transitionDuration
}
}
}
@ -188,38 +189,15 @@ Item {
id: presenceIndicator
anchors.right: root.right
anchors.rightMargin: -1
anchors.bottom: root.bottom
anchors.bottomMargin: -1
size: root.width * 0.3
size: root.width * 0.26
visible: showPresenceIndicator
}
Rectangle {
id: unreadMessageCountRect
anchors.right: root.right
anchors.top: root.top
width: root.width * 0.3
height: root.width * 0.3
visible: unreadMessagesCount > 0
Text {
id: unreadMessageCounttext
anchors.centerIn: unreadMessageCountRect
text: unreadMessagesCount > 9 ? "…" : unreadMessagesCount
color: "white"
font.pointSize: JamiTheme.indicatorFontSize
}
radius: 30
color: JamiTheme.notificationRed
}
Connections {
target: ScreenInfo

View file

@ -31,6 +31,12 @@ Menu {
property int commonBorderWidth: 1
font.pointSize: JamiTheme.menuFontSize
modal: true
Overlay.modal: Rectangle {
color: "transparent"
}
// TODO: investigate
function openMenu(){
visible = true
visible = false
@ -38,6 +44,8 @@ Menu {
}
background: Rectangle {
id: container
implicitWidth: menuItemsPreferredWidth
implicitHeight: menuItemsPreferredHeight
* (root.count - generalMenuSeparatorCount)
@ -45,5 +53,15 @@ Menu {
border.width: commonBorderWidth
border.color: JamiTheme.tabbarBorderColor
color: JamiTheme.backgroundColor
layer.enabled: true
layer.effect: DropShadow {
z: -1
horizontalOffset: 3.0
verticalOffset: 3.0
radius: 16.0
samples: 16
color: JamiTheme.shadowColor
}
}
}

View file

@ -67,7 +67,7 @@ Popup {
height: root.height
horizontalOffset: 3.0
verticalOffset: 3.0
radius: container.radius * 2
radius: container.radius * 4
samples: 16
color: JamiTheme.shadowColor
source: container

View file

@ -30,7 +30,7 @@ Rectangle {
// This is set to REGISTERED for contact presence
// as status is not currently tracked for contact items.
property int status: Account.Status.REGISTERED
property int size: 12
property int size: 15
width: size
height: size

View file

@ -23,6 +23,12 @@ import QtQuick.Controls 2.12
Rectangle {
property alias name: label.text
property bool stretchParent: false
property string tag: this.toString()
signal moveX(real dx)
signal moveY(real dy)
property real ox: 0
property real oy: 0
property real step: 0.5
border.width: 1
color: {
@ -33,6 +39,19 @@ Rectangle {
}
anchors.fill: parent
focus: false
Keys.onPressed: {
if (event.key === Qt.Key_Left)
moveX(-step)
else if (event.key === Qt.Key_Right)
moveX(step)
else if (event.key === Qt.Key_Down)
moveY(step)
else if (event.key === Qt.Key_Up)
moveY(-step)
console.log(tag, ox, oy)
event.accepted = true;
}
Component.onCompleted: {
// fallback to some description of the object
if (label.text === "")
@ -45,10 +64,24 @@ Rectangle {
}
}
onMoveX: {
parent.anchors.leftMargin += dx
parent.x += dx
ox += dx;
}
onMoveY: {
parent.anchors.topMargin += dy
parent.y += dy
oy += dy
}
Label {
id: label
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
onPressed: parent.forceActiveFocus()
}
}

View file

@ -330,9 +330,6 @@ Item {
"Use the \"Link Another Device\" feature to obtain a PIN.")
property string connectFromAnotherDevice: qsTr("Link device")
// KeyBoardShortcutTable
property string conversations: qsTr("Conversations")
// LinkDevicesDialog
property string pinTimerInfos: qsTr("The PIN and the account password should be entered in your device within 10 minutes.")
property string close: qsTr("Close")
@ -405,6 +402,8 @@ Item {
// SmartList
property string clearText: qsTr("Clear Text")
property string conversations: qsTr("Conversations")
property string searchResults: qsTr("Search Results")
// SmartList context menu
property string declineContactRequest: qsTr("Decline contact request")

View file

@ -55,10 +55,10 @@ Item {
property color notificationBlue: "#31b7ff"
property color unPresenceOrange: "orange"
property color placeHolderTextFontColor: "#767676"
property color draftRed: "#cf5300"
property color draftTextColor: "#cf5300"
property color selectedTabColor: primaryForegroundColor
property color filterBadgeColor: mediumGrey
property color filterBadgeTextColor: blackColor
property color filterBadgeColor: "#eed4d8"
property color filterBadgeTextColor: "#cc0022"
// General buttons
property color pressedButtonColor: darkTheme ? pressColor : "#a0a0a0"
@ -174,10 +174,14 @@ Item {
property real titleFontSize: 16
property real primaryRadius: 4
property real smartlistItemFontSize: 10.5
property real smartlistItemInfoFontSize: 9
property real filterItemFontSize: smartlistItemFontSize
property real filterBadgeFontSize: 8.25
property real accountListItemHeight: 64
property real accountListAvatarSize: 40
property real smartListItemHeight: 64
property real smartListAvatarSize: 52
property real smartListTransitionDuration: 120
property real maximumWidthSettingsView: 600
property real settingsHeaderpreferredHeight: 64

View file

@ -1,4 +1,4 @@
/*!
/*
* Copyright (C) 2020 by Savoir-faire Linux
* Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
@ -48,13 +48,13 @@ ContactAdapter::getContactSelectableModel(int type)
switch (listModeltype_) {
case SmartListModel::Type::CONVERSATION:
selectableProxyModel_->setPredicate([this](const QModelIndex& index, const QRegExp&) {
return !defaultModerators_.contains(index.data(SmartListModel::URI).toString());
return !defaultModerators_.contains(index.data(Role::URI).toString());
});
break;
case SmartListModel::Type::CONFERENCE:
selectableProxyModel_->setPredicate([](const QModelIndex& index, const QRegExp&) {
return index.data(SmartListModel::Presence).toBool();
return index.data(Role::Presence).toBool();
});
break;
case SmartListModel::Type::TRANSFER:
@ -68,13 +68,11 @@ ContactAdapter::getContactSelectableModel(int type)
.contactModel->bestIdForContact(conv.participants[0]);
QRegExp matchExcept = QRegExp(QString("\\b(?!" + calleeDisplayId + "\\b)\\w+"));
match = matchExcept.indexIn(index.data(SmartListModel::Role::DisplayID).toString())
!= -1;
match = matchExcept.indexIn(index.data(Role::BestId).toString()) != -1;
}
if (match) {
match = regexp.indexIn(index.data(SmartListModel::Role::DisplayID).toString())
!= -1;
match = regexp.indexIn(index.data(Role::BestId).toString()) != -1;
}
return match && !index.parent().isValid();
});
@ -95,8 +93,8 @@ ContactAdapter::setSearchFilter(const QString& filter)
} else if (listModeltype_ == SmartListModel::Type::CONVERSATION) {
selectableProxyModel_->setPredicate(
[this, filter](const QModelIndex& index, const QRegExp&) {
return (!defaultModerators_.contains(index.data(SmartListModel::URI).toString())
&& index.data(SmartListModel::DisplayName).toString().contains(filter));
return (!defaultModerators_.contains(index.data(Role::URI).toString())
&& index.data(Role::BestName).toString().contains(filter));
});
}
selectableProxyModel_->setFilterRegExp(
@ -114,15 +112,14 @@ ContactAdapter::contactSelected(int index)
switch (listModeltype_) {
case SmartListModel::Type::CONFERENCE: {
// Conference.
const auto sectionName = contactIndex.data(SmartListModel::Role::SectionName)
.value<QString>();
const auto sectionName = contactIndex.data(Role::SectionName).value<QString>();
if (!sectionName.isEmpty()) {
smartListModel_->toggleSection(sectionName);
return;
}
const auto convUid = contactIndex.data(SmartListModel::Role::UID).value<QString>();
const auto accId = contactIndex.data(SmartListModel::Role::AccountId).value<QString>();
const auto convUid = contactIndex.data(Role::UID).value<QString>();
const auto accId = contactIndex.data(Role::AccountId).value<QString>();
const auto callId = lrcInstance_->getCallIdForConversationUid(convUid, accId);
if (!callId.isEmpty()) {
@ -133,7 +130,7 @@ ContactAdapter::contactSelected(int index)
callModel->joinCalls(thisCallId, callId);
} else {
const auto contactUri = contactIndex.data(SmartListModel::Role::URI).value<QString>();
const auto contactUri = contactIndex.data(Role::URI).value<QString>();
auto call = lrcInstance_->getCallInfoForConversation(convInfo);
if (!call) {
return;
@ -143,7 +140,7 @@ ContactAdapter::contactSelected(int index)
} break;
case SmartListModel::Type::TRANSFER: {
// SIP Transfer.
const auto contactUri = contactIndex.data(SmartListModel::Role::URI).value<QString>();
const auto contactUri = contactIndex.data(Role::URI).value<QString>();
if (convInfo.uid.isEmpty()) {
return;
@ -170,7 +167,7 @@ ContactAdapter::contactSelected(int index)
}
} break;
case SmartListModel::Type::CONVERSATION: {
const auto contactUri = contactIndex.data(SmartListModel::Role::URI).value<QString>();
const auto contactUri = contactIndex.data(Role::URI).value<QString>();
if (contactUri.isEmpty()) {
return;
}

View file

@ -1,4 +1,4 @@
/*!
/*
* Copyright (C) 2020 by Savoir-faire Linux
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
*
@ -20,6 +20,7 @@
#include "qmladapterbase.h"
#include "smartlistmodel.h"
#include "conversationlistmodel.h"
#include <QObject>
#include <QSortFilterProxyModel>
@ -38,30 +39,42 @@ class LRCInstance;
*/
class SelectableProxyModel final : public QSortFilterProxyModel
{
Q_OBJECT
public:
using FilterPredicate = std::function<bool(const QModelIndex&, const QRegExp&)>;
explicit SelectableProxyModel(QAbstractItemModel* parent)
explicit SelectableProxyModel(QAbstractListModel* parent = nullptr)
: QSortFilterProxyModel(parent)
{
setSourceModel(parent);
setSortRole(ConversationList::Role::LastInteractionTimeStamp);
sort(0, Qt::DescendingOrder);
setFilterCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive);
}
~SelectableProxyModel() {}
void setPredicate(FilterPredicate filterPredicate)
{
filterPredicate_ = filterPredicate;
}
virtual bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
virtual bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override
{
// Accept all contacts in conversation list filtered with account type, except those in a call.
auto index = sourceModel()->index(source_row, 0, source_parent);
auto index = sourceModel()->index(sourceRow, 0, sourceParent);
return filterPredicate_ ? filterPredicate_(index, filterRegExp()) : false;
}
bool lessThan(const QModelIndex& left, const QModelIndex& right) const override
{
QVariant leftData = sourceModel()->data(left, sortRole());
QVariant rightData = sourceModel()->data(right, sortRole());
// we're assuming the sort role data type here is some integral time
return leftData.toUInt() < rightData.toUInt();
};
private:
std::function<bool(const QModelIndex&, const QRegExp&)> filterPredicate_;
FilterPredicate filterPredicate_;
};
class ContactAdapter final : public QmlAdapterBase
@ -73,6 +86,8 @@ public:
~ContactAdapter() = default;
protected:
using Role = ConversationList::Role;
void safeInit() override {};
Q_INVOKABLE QVariant getContactSelectableModel(int type);
@ -81,10 +96,8 @@ protected:
private:
SmartListModel::Type listModeltype_;
// SmartListModel is the source model of SelectableProxyModel.
std::unique_ptr<SmartListModel> smartListModel_;
std::unique_ptr<SelectableProxyModel> selectableProxyModel_;
QScopedPointer<SmartListModel> smartListModel_;
QScopedPointer<SelectableProxyModel> selectableProxyModel_;
QStringList defaultModerators_;

View file

@ -0,0 +1,131 @@
/*
* Copyright (C) 2021 by Savoir-faire Linux
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "conversationlistmodel.h"
#include "uri.h"
ConversationListModel::ConversationListModel(LRCInstance* instance, QObject* parent)
: ConversationListModelBase(instance, parent)
{
connect(
model_,
&ConversationModel::beginInsertRows,
this,
[this](int position, int rows) {
beginInsertRows(QModelIndex(), position, position + (rows - 1));
},
Qt::DirectConnection);
connect(model_,
&ConversationModel::endInsertRows,
this,
&ConversationListModel::endInsertRows,
Qt::DirectConnection);
connect(
model_,
&ConversationModel::beginRemoveRows,
this,
[this](int position, int rows) {
beginRemoveRows(QModelIndex(), position, position + (rows - 1));
},
Qt::DirectConnection);
connect(model_,
&ConversationModel::endRemoveRows,
this,
&ConversationListModel::endRemoveRows,
Qt::DirectConnection);
connect(model_, &ConversationModel::dataChanged, this, [this](int position) {
const auto index = createIndex(position, 0);
Q_EMIT ConversationListModel::dataChanged(index, index);
});
}
int
ConversationListModel::rowCount(const QModelIndex& parent) const
{
// For list models only the root node (an invalid parent) should return the list's size. For all
// other (valid) parents, rowCount() should return 0 so that it does not become a tree model.
if (!parent.isValid() && model_) {
return model_->getConversations().size();
}
return 0;
}
QVariant
ConversationListModel::data(const QModelIndex& index, int role) const
{
const auto& data = model_->getConversations();
if (!index.isValid() || data.empty())
return {};
return dataForItem(data.at(index.row()), role);
}
ConversationListProxyModel::ConversationListProxyModel(QAbstractListModel* model, QObject* parent)
: SelectableListProxyModel(model, parent)
{
setSortRole(ConversationList::Role::LastInteractionTimeStamp);
sort(0, Qt::DescendingOrder);
setFilterCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive);
}
bool
ConversationListProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
{
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
auto rx = filterRegExp();
auto uriStripper = URI(rx.pattern());
bool stripScheme = (uriStripper.schemeType() < URI::SchemeType::COUNT__);
FlagPack<URI::Section> flags = URI::Section::USER_INFO | URI::Section::HOSTNAME
| URI::Section::PORT;
if (!stripScheme) {
flags |= URI::Section::SCHEME;
}
rx.setPattern(uriStripper.format(flags));
auto uri = index.data(ConversationList::Role::URI).toString();
auto alias = index.data(ConversationList::Role::Alias).toString();
auto registeredName = index.data(ConversationList::Role::RegisteredName).toString();
auto itemProfileType = index.data(ConversationList::Role::ContactType).toInt();
auto typeFilter = static_cast<profile::Type>(itemProfileType) == currentTypeFilter_;
if (index.data(ConversationList::Role::IsBanned).toBool()) {
return typeFilter
&& (rx.exactMatch(uri) || rx.exactMatch(alias) || rx.exactMatch(registeredName));
}
return typeFilter
&& (rx.indexIn(uri) != -1 || rx.indexIn(alias) != -1 || rx.indexIn(registeredName) != -1);
}
bool
ConversationListProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const
{
QVariant leftData = sourceModel()->data(left, sortRole());
QVariant rightData = sourceModel()->data(right, sortRole());
// we're assuming the sort role data type here is some integral time
return leftData.toULongLong() < rightData.toULongLong();
}
void
ConversationListProxyModel::setTypeFilter(const profile::Type& typeFilter)
{
beginResetModel();
currentTypeFilter_ = typeFilter;
endResetModel();
updateSelection();
};

View file

@ -0,0 +1,55 @@
/*
* Copyright (C) 2021 by Savoir-faire Linux
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "conversationlistmodelbase.h"
#include "selectablelistproxymodel.h"
#include "api/profile.h"
#include <QSortFilterProxyModel>
// A wrapper view model around ConversationModel's underlying data
class ConversationListModel final : public ConversationListModelBase
{
Q_OBJECT
public:
explicit ConversationListModel(LRCInstance* instance, QObject* parent = nullptr);
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
};
// The top level filtered and sorted model to be consumed by QML ListViews
class ConversationListProxyModel final : public SelectableListProxyModel
{
Q_OBJECT
public:
explicit ConversationListProxyModel(QAbstractListModel* model, QObject* parent = nullptr);
bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override;
bool lessThan(const QModelIndex& left, const QModelIndex& right) const override;
Q_INVOKABLE void setTypeFilter(const profile::Type& typeFilter);
private:
profile::Type currentTypeFilter_;
};

View file

@ -0,0 +1,201 @@
/*
* Copyright (C) 2020-2021 by Savoir-faire Linux
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "conversationlistmodelbase.h"
ConversationListModelBase::ConversationListModelBase(LRCInstance* instance, QObject* parent)
: AbstractListModelBase(parent)
{
lrcInstance_ = instance;
model_ = lrcInstance_->getCurrentConversationModel();
}
int
ConversationListModelBase::columnCount(const QModelIndex& parent) const
{
Q_UNUSED(parent)
return 1;
}
QHash<int, QByteArray>
ConversationListModelBase::roleNames() const
{
using namespace ConversationList;
QHash<int, QByteArray> roles;
#define X(role) roles[role] = #role;
CONV_ROLES
#undef X
return roles;
}
QVariant
ConversationListModelBase::dataForItem(item_t item, int role) const
{
if (item.participants.isEmpty()) {
return QVariant();
}
// WARNING: not swarm ready
auto peerUri = item.participants[0];
ContactModel* contactModel {nullptr};
contact::Info contact {};
try {
const auto& accountInfo = lrcInstance_->getAccountInfo(item.accountId);
contactModel = accountInfo.contactModel.get();
contact = contactModel->getContact(peerUri);
} catch (...) {
return QVariant(false);
}
// Since we are using image provider right now, image url representation should be unique to
// be able to use the image cache, account avatar will only be updated once PictureUid changed
switch (role) {
case Role::BestName:
return QVariant(contactModel->bestNameForContact(peerUri));
case Role::BestId:
return QVariant(contactModel->bestIdForContact(peerUri));
case Role::Presence:
return QVariant(contact.isPresent);
case Role::PictureUid:
return QVariant(contactAvatarUidMap_[peerUri]);
case Role::Alias:
return QVariant(contact.profileInfo.alias);
case Role::RegisteredName:
return QVariant(contact.registeredName);
case Role::URI:
return QVariant(peerUri);
case Role::UnreadMessagesCount:
return QVariant(item.unreadMessages);
case Role::LastInteractionTimeStamp: {
if (!item.interactions.empty()) {
auto ts = static_cast<qint32>(item.interactions.at(item.lastMessageUid).timestamp);
return QVariant(ts);
}
break;
}
case Role::LastInteractionDate: {
if (!item.interactions.empty()) {
auto& date = item.interactions.at(item.lastMessageUid).timestamp;
return QVariant(Utils::formatTimeString(date));
}
break;
}
case Role::LastInteraction: {
if (!item.interactions.empty()) {
return QVariant(item.interactions.at(item.lastMessageUid).body);
}
break;
}
case Role::ContactType: {
return QVariant(static_cast<int>(contact.profileInfo.type));
}
case Role::IsBanned: {
return QVariant(contact.isBanned);
}
case Role::UID:
return QVariant(item.uid);
case Role::InCall: {
const auto& convInfo = lrcInstance_->getConversationFromConvUid(item.uid);
if (!convInfo.uid.isEmpty()) {
auto* callModel = lrcInstance_->getCurrentCallModel();
return QVariant(callModel->hasCall(convInfo.callId));
}
return QVariant(false);
}
case Role::IsAudioOnly: {
const auto& convInfo = lrcInstance_->getConversationFromConvUid(item.uid);
if (!convInfo.uid.isEmpty()) {
auto* call = lrcInstance_->getCallInfoForConversation(convInfo);
if (call) {
return QVariant(call->isAudioOnly);
}
}
return QVariant();
}
case Role::CallStackViewShouldShow: {
const auto& convInfo = lrcInstance_->getConversationFromConvUid(item.uid);
if (!convInfo.uid.isEmpty() && !convInfo.callId.isEmpty()) {
auto* callModel = lrcInstance_->getCurrentCallModel();
const auto& call = callModel->getCall(convInfo.callId);
return QVariant(callModel->hasCall(convInfo.callId)
&& ((!call.isOutgoing
&& (call.status == call::Status::IN_PROGRESS
|| call.status == call::Status::PAUSED
|| call.status == call::Status::INCOMING_RINGING))
|| (call.isOutgoing && call.status != call::Status::ENDED)));
}
return QVariant(false);
}
case Role::CallState: {
const auto& convInfo = lrcInstance_->getConversationFromConvUid(item.uid);
if (!convInfo.uid.isEmpty()) {
if (auto* call = lrcInstance_->getCallInfoForConversation(convInfo)) {
return QVariant(static_cast<int>(call->status));
}
}
return QVariant();
}
case Role::Draft: {
if (!item.uid.isEmpty()) {
const auto draft = lrcInstance_->getContentDraft(item.uid, item.accountId);
if (!draft.isEmpty()) {
// Pencil Emoji
uint cp = 0x270F;
auto emojiString = QString::fromUcs4(&cp, 1);
return emojiString + draft;
}
}
return QVariant("");
}
}
return QVariant();
}
void
ConversationListModelBase::updateContactAvatarUid(const QString& contactUri)
{
contactAvatarUidMap_[contactUri] = Utils::generateUid();
}
void
ConversationListModelBase::fillContactAvatarUidMap(
const lrc::api::ContactModel::ContactInfoMap& contacts)
{
if (contacts.size() == 0) {
contactAvatarUidMap_.clear();
return;
}
if (contactAvatarUidMap_.isEmpty() || contacts.size() != contactAvatarUidMap_.size()) {
bool useContacts = contacts.size() > contactAvatarUidMap_.size();
auto contactsKeyList = contacts.keys();
auto contactAvatarUidMapKeyList = contactAvatarUidMap_.keys();
for (int i = 0;
i < (useContacts ? contactsKeyList.size() : contactAvatarUidMapKeyList.size());
++i) {
// Insert or update
if (i < contactsKeyList.size() && !contactAvatarUidMap_.contains(contactsKeyList.at(i)))
contactAvatarUidMap_.insert(contactsKeyList.at(i), Utils::generateUid());
// Remove
if (i < contactAvatarUidMapKeyList.size()
&& !contacts.contains(contactAvatarUidMapKeyList.at(i)))
contactAvatarUidMap_.remove(contactAvatarUidMapKeyList.at(i));
}
}
}

View file

@ -0,0 +1,88 @@
/*
* Copyright (C) 2020-2021 by Savoir-faire Linux
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "abstractlistmodelbase.h"
// TODO: many of these roles should probably be factored out
#define CONV_ROLES \
X(BestName) \
X(BestId) \
X(Presence) \
X(Alias) \
X(RegisteredName) \
X(URI) \
X(UnreadMessagesCount) \
X(LastInteractionTimeStamp) \
X(LastInteractionDate) \
X(LastInteraction) \
X(ContactType) \
X(IsBanned) \
X(UID) \
X(InCall) \
X(IsAudioOnly) \
X(CallStackViewShouldShow) \
X(CallState) \
X(SectionName) \
X(AccountId) \
X(PictureUid) \
X(Draft)
namespace ConversationList {
Q_NAMESPACE
enum Role {
DummyRole = Qt::UserRole + 1,
#define X(role) role,
CONV_ROLES
#undef X
};
Q_ENUM_NS(Role)
} // namespace ConversationList
// A generic wrapper view model around ConversationModel's underlying data
class ConversationListModelBase : public AbstractListModelBase
{
Q_OBJECT
public:
using item_t = const conversation::Info&;
explicit ConversationListModelBase(LRCInstance* instance, QObject* parent = nullptr);
int columnCount(const QModelIndex& parent) const override;
QHash<int, QByteArray> roleNames() const override;
QVariant dataForItem(item_t item, int role = Qt::DisplayRole) const;
// Update the avatar uid map to prevent the image provider from pulling from the cache
void updateContactAvatarUid(const QString& contactUri);
protected:
using Role = ConversationList::Role;
// Assign a uid for each contact avatar; it will serve as the PictureUid role
void fillContactAvatarUidMap(const ContactModel::ContactInfoMap& contacts);
// Convenience pointer to be pulled from lrcinstance
ConversationModel* model_;
// AvatarImageProvider helper
QMap<QString, QString> contactAvatarUidMap_;
};

View file

@ -1,11 +1,7 @@
/*!
/*
* Copyright (C) 2020 by Savoir-faire Linux
* Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
* Author: Anthony Léonard <anthony.leonard@savoirfairelinux.com>
* Author: Olivier Soldano <olivier.soldano@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
* Author: Isa Nanic <isa.nanic@savoirfairelinux.com>
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -26,35 +22,92 @@
#include "utils.h"
#include "qtutils.h"
#include "systemtray.h"
#include "qmlregister.h"
#include <QApplication>
#include <QJsonObject>
using namespace lrc::api;
ConversationsAdapter::ConversationsAdapter(SystemTray* systemTray,
LRCInstance* instance,
QObject* parent)
: QmlAdapterBase(instance, parent)
, currentTypeFilter_(profile::Type::RING)
, systemTray_(systemTray)
, convSrcModel_(new ConversationListModel(lrcInstance_))
, convModel_(new ConversationListProxyModel(convSrcModel_.get()))
, searchSrcModel_(new SearchResultsListModel(lrcInstance_))
, searchModel_(new SelectableListProxyModel(searchSrcModel_.get()))
{
QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, convModel_.get(), "ConversationListModel");
QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, searchModel_.get(), "SearchResultsListModel");
new SelectableListProxyGroupModel({convModel_.data(), searchModel_.data()}, this);
setTypeFilter(currentTypeFilter_);
connect(this, &ConversationsAdapter::currentTypeFilterChanged, [this]() {
lrcInstance_->getCurrentConversationModel()->setFilter(currentTypeFilter_);
setTypeFilter(currentTypeFilter_);
});
connect(lrcInstance_, &LRCInstance::conversationSelected, [this]() {
auto convUid = lrcInstance_->get_selectedConvUid();
if (!convUid.isEmpty()) {
Q_EMIT showConversation(lrcInstance_->getCurrAccId(), convUid);
connect(lrcInstance_, &LRCInstance::selectedConvUidChanged, [this]() {
auto convId = lrcInstance_->get_selectedConvUid();
if (convId.isEmpty()) {
// deselected
convModel_->deselect();
searchModel_->deselect();
} else {
// selected
const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId);
if (convInfo.uid.isEmpty())
return;
auto& accInfo = lrcInstance_->getAccountInfo(convInfo.accountId);
accInfo.conversationModel->selectConversation(convInfo.uid);
accInfo.conversationModel->clearUnreadInteractions(convInfo.uid);
try {
// Set contact filter (for conversation tab selection)
// WARNING: not swarm ready
auto& contact = accInfo.contactModel->getContact(convInfo.participants.front());
if (contact.profileInfo.type != profile::Type::INVALID
&& contact.profileInfo.type != profile::Type::TEMPORARY)
set_currentTypeFilter(contact.profileInfo.type);
} catch (const std::out_of_range& e) {
qWarning() << e.what();
}
// reposition index in case of programmatic selection
// currently, this may only occur for the conversation list
// and not the search list
convModel_->selectSourceRow(lrcInstance_->indexOf(convId));
}
});
connect(lrcInstance_, &LRCInstance::draftSaved, [this](const QString& convId) {
auto row = lrcInstance_->indexOf(convId);
const auto index = convSrcModel_->index(row, 0);
Q_EMIT convSrcModel_->dataChanged(index, index);
});
connect(lrcInstance_, &LRCInstance::contactBanned, [this](const QString& uri) {
auto& convInfo = lrcInstance_->getConversationFromPeerUri(uri);
if (convInfo.uid.isEmpty())
return;
auto row = lrcInstance_->indexOf(convInfo.uid);
const auto index = convSrcModel_->index(row, 0);
Q_EMIT convSrcModel_->dataChanged(index, index);
});
updateConversationFilterData();
#ifdef Q_OS_LINUX
// notification responses
connect(systemTray_,
&SystemTray::openConversationActivated,
[this](const QString& accountId, const QString& convUid) {
Q_EMIT lrcInstance_->notificationClicked();
selectConversation(accountId, convUid);
Q_EMIT lrcInstance_->updateSmartList();
Q_EMIT modelSorted(convUid);
lrcInstance_->selectConversation(convUid, accountId);
});
connect(systemTray_,
&SystemTray::acceptPendingActivated,
@ -80,85 +133,71 @@ ConversationsAdapter::ConversationsAdapter(SystemTray* systemTray,
void
ConversationsAdapter::safeInit()
{
// TODO: remove these safeInits, they are possibly called
// multiple times during qml component inits
conversationSmartListModel_ = new SmartListModel(this,
SmartListModel::Type::CONVERSATION,
lrcInstance_);
Q_EMIT modelChanged(QVariant::fromValue(conversationSmartListModel_));
connect(&lrcInstance_->behaviorController(),
&BehaviorController::showChatView,
[this](const QString& accountId, const QString& convId) {
Q_EMIT showConversation(accountId, convId);
});
connect(&lrcInstance_->behaviorController(),
&BehaviorController::newUnreadInteraction,
this,
&ConversationsAdapter::onNewUnreadInteraction);
&ConversationsAdapter::onNewUnreadInteraction,
Qt::UniqueConnection);
connect(&lrcInstance_->behaviorController(),
&BehaviorController::newReadInteraction,
this,
&ConversationsAdapter::onNewReadInteraction);
&ConversationsAdapter::onNewReadInteraction,
Qt::UniqueConnection);
connect(&lrcInstance_->behaviorController(),
&BehaviorController::newTrustRequest,
this,
&ConversationsAdapter::onNewTrustRequest);
&ConversationsAdapter::onNewTrustRequest,
Qt::UniqueConnection);
connect(&lrcInstance_->behaviorController(),
&BehaviorController::trustRequestTreated,
this,
&ConversationsAdapter::onTrustRequestTreated);
&ConversationsAdapter::onTrustRequestTreated,
Qt::UniqueConnection);
connect(lrcInstance_,
&LRCInstance::currentAccountChanged,
this,
&ConversationsAdapter::onCurrentAccountIdChanged);
&ConversationsAdapter::onCurrentAccountIdChanged,
Qt::UniqueConnection);
connectConversationModel();
setProperty("currentTypeFilter",
QVariant::fromValue(lrcInstance_->getCurrentAccountInfo().profileInfo.type));
set_currentTypeFilter(lrcInstance_->getCurrentAccountInfo().profileInfo.type);
}
void
ConversationsAdapter::backToWelcomePage()
{
deselectConversation();
lrcInstance_->deselectConversation();
Q_EMIT navigateToWelcomePageRequested();
}
void
ConversationsAdapter::selectConversation(const QString& accountId, const QString& convUid)
{
lrcInstance_->selectConversation(accountId, convUid);
}
void
ConversationsAdapter::deselectConversation()
{
if (lrcInstance_->get_selectedConvUid().isEmpty()) {
return;
}
auto currentConversationModel = lrcInstance_->getCurrentConversationModel();
if (currentConversationModel == nullptr) {
return;
}
lrcInstance_->set_selectedConvUid();
}
void
ConversationsAdapter::onCurrentAccountIdChanged()
{
lrcInstance_->deselectConversation();
convSrcModel_.reset(new ConversationListModel(lrcInstance_));
convModel_->bindSourceModel(convSrcModel_.get());
searchSrcModel_.reset(new SearchResultsListModel(lrcInstance_));
searchModel_->bindSourceModel(searchSrcModel_.get());
connectConversationModel();
setProperty("currentTypeFilter",
QVariant::fromValue(lrcInstance_->getCurrentAccountInfo().profileInfo.type));
updateConversationFilterData();
set_currentTypeFilter(lrcInstance_->getCurrentAccountInfo().profileInfo.type);
}
void
@ -189,11 +228,9 @@ ConversationsAdapter::onNewUnreadInteraction(const QString& accountId,
auto onClicked = [this, accountId, convUid, uri = interaction.authorUri] {
Q_EMIT lrcInstance_->notificationClicked();
const auto& convInfo = lrcInstance_->getConversationFromConvUid(convUid, accountId);
if (!convInfo.uid.isEmpty()) {
selectConversation(accountId, convInfo.uid);
Q_EMIT lrcInstance_->updateSmartList();
Q_EMIT modelSorted(convInfo.uid);
}
if (convInfo.uid.isEmpty())
return;
lrcInstance_->selectConversation(convInfo.uid, accountId);
};
systemTray_->showNotification(interaction.body, from, onClicked);
#endif
@ -209,6 +246,10 @@ ConversationsAdapter::onNewReadInteraction(const QString& accountId,
// hide notification
auto notifId = QString("%1;%2;%3").arg(accountId).arg(convUid).arg(interactionId);
systemTray_->hideNotification(notifId);
#else
Q_UNUSED(accountId)
Q_UNUSED(convUid)
Q_UNUSED(interactionId)
#endif
}
@ -227,6 +268,9 @@ ConversationsAdapter::onNewTrustRequest(const QString& accountId, const QString&
NotificationType::REQUEST,
Utils::QImageToByteArray(contactPhoto));
}
#else
Q_UNUSED(accountId)
Q_UNUSED(peerUri)
#endif
}
@ -237,6 +281,9 @@ ConversationsAdapter::onTrustRequestTreated(const QString& accountId, const QStr
// hide notification
auto notifId = QString("%1;%2").arg(accountId).arg(peerUri);
systemTray_->hideNotification(notifId);
#else
Q_UNUSED(accountId)
Q_UNUSED(peerUri)
#endif
}
@ -244,46 +291,30 @@ void
ConversationsAdapter::onModelChanged()
{
conversationSmartListModel_->fillConversationsList();
updateConversationsFilterWidget();
auto* convModel = lrcInstance_->getCurrentConversationModel();
const auto& convInfo = lrcInstance_->getConversationFromConvUid(
lrcInstance_->get_selectedConvUid());
if (convInfo.uid.isEmpty() || convInfo.participants.isEmpty()) {
return;
}
const auto contactURI = convInfo.participants[0];
if (contactURI.isEmpty()
|| convModel->owner.contactModel->getContact(contactURI).profileInfo.type
== lrc::api::profile::Type::TEMPORARY) {
return;
}
Q_EMIT modelSorted(QVariant::fromValue(convInfo.uid));
updateConversationFilterData();
}
void
ConversationsAdapter::onProfileUpdated(const QString& contactUri)
{
// TODO: this will need a dataChanged call to keep the avatar
// updated. previously, 'reload-smartlist' was invoked here
conversationSmartListModel_->updateContactAvatarUid(contactUri);
Q_EMIT updateListViewRequested();
}
void
ConversationsAdapter::onConversationUpdated(const QString&)
{
updateConversationsFilterWidget();
Q_EMIT updateListViewRequested();
updateConversationFilterData();
}
void
ConversationsAdapter::onFilterChanged()
{
conversationSmartListModel_->fillConversationsList();
updateConversationsFilterWidget();
updateConversationFilterData();
if (!lrcInstance_->get_selectedConvUid().isEmpty())
Q_EMIT indexRepositionRequested();
Q_EMIT updateListViewRequested();
}
void
@ -304,10 +335,9 @@ ConversationsAdapter::onConversationCleared(const QString& convUid)
{
// If currently selected, switch to welcome screen (deselecting
// current smartlist item).
if (convUid != lrcInstance_->get_selectedConvUid()) {
return;
if (convUid == lrcInstance_->get_selectedConvUid()) {
lrcInstance_->deselectConversation();
}
backToWelcomePage();
}
void
@ -319,26 +349,94 @@ ConversationsAdapter::onSearchStatusChanged(const QString& status)
void
ConversationsAdapter::onSearchResultUpdated()
{
// currently for contact pickers
conversationSmartListModel_->fillConversationsList();
Q_EMIT updateListViewRequested();
// smartlist search results
searchSrcModel_->onSearchResultsUpdated();
}
void
ConversationsAdapter::updateConversationsFilterWidget()
ConversationsAdapter::updateConversationFilterData()
{
// Update status of "Conversations" and "Invitations".
auto invites = lrcInstance_->getCurrentAccountInfo().contactModel->pendingRequestCount();
if (invites == 0 && currentTypeFilter_ == lrc::api::profile::Type::PENDING) {
setProperty("currentTypeFilter", QVariant::fromValue(lrc::api::profile::Type::RING));
// TODO: this may be further spliced to respond separately to
// incoming messages and invites
// total unread message and pending invite counts, and tab selection
auto& accountInfo = lrcInstance_->getCurrentAccountInfo();
int totalUnreadMessages {0};
if (accountInfo.profileInfo.type != profile::Type::SIP) {
auto& convModel = accountInfo.conversationModel;
auto conversations = convModel->getFilteredConversations(profile::Type::RING, false);
conversations.for_each([&totalUnreadMessages](const conversation::Info& conversation) {
totalUnreadMessages += conversation.unreadMessages;
});
}
set_totalUnreadMessageCount(totalUnreadMessages);
set_pendingRequestCount(accountInfo.contactModel->pendingRequestCount());
if (pendingRequestCount_ == 0 && currentTypeFilter_ == profile::Type::PENDING) {
set_currentTypeFilter(profile::Type::RING);
}
showConversationTabs(invites);
}
void
ConversationsAdapter::refill()
ConversationsAdapter::setFilter(const QString& filterString)
{
if (conversationSmartListModel_)
conversationSmartListModel_->fillConversationsList();
convModel_->setFilter(filterString);
searchSrcModel_->setFilter(filterString);
}
void
ConversationsAdapter::setTypeFilter(const profile::Type& typeFilter)
{
convModel_->setTypeFilter(typeFilter);
}
QVariantMap
ConversationsAdapter::getConvInfoMap(const QString& convId)
{
const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId);
if (convInfo.participants.empty())
return {};
auto peerUri = convInfo.participants[0];
ContactModel* contactModel {nullptr};
contact::Info contact {};
try {
const auto& accountInfo = lrcInstance_->getAccountInfo(convInfo.accountId);
contactModel = accountInfo.contactModel.get();
contact = contactModel->getContact(peerUri);
} catch (...) {
return {};
}
bool isAudioOnly {false};
if (!convInfo.uid.isEmpty()) {
auto* call = lrcInstance_->getCallInfoForConversation(convInfo);
if (call) {
isAudioOnly = call->isAudioOnly;
}
}
bool callStackViewShouldShow {false};
call::Status callState {};
if (!convInfo.callId.isEmpty()) {
auto* callModel = lrcInstance_->getCurrentCallModel();
const auto& call = callModel->getCall(convInfo.callId);
callStackViewShouldShow = callModel->hasCall(convInfo.callId)
&& ((!call.isOutgoing
&& (call.status == call::Status::IN_PROGRESS
|| call.status == call::Status::PAUSED
|| call.status == call::Status::INCOMING_RINGING))
|| (call.isOutgoing && call.status != call::Status::ENDED));
callState = call.status;
}
// WARNING: not swarm ready
// titles should come from conversation, not contact model
return {{"convId", convId},
{"bestId", contactModel->bestIdForContact(peerUri)},
{"bestName", contactModel->bestNameForContact(peerUri)},
{"uri", peerUri},
{"contactType", static_cast<int>(contact.profileInfo.type)},
{"isAudioOnly", isAudioOnly},
{"callState", static_cast<int>(callState)},
{"callStackViewShouldShow", callStackViewShouldShow}};
}
bool
@ -402,7 +500,7 @@ ConversationsAdapter::connectConversationModel(bool updateFilter)
Qt::UniqueConnection);
if (updateFilter) {
currentTypeFilter_ = lrc::api::profile::Type::INVALID;
currentTypeFilter_ = profile::Type::INVALID;
}
return true;
}
@ -421,8 +519,8 @@ ConversationsAdapter::updateConversationForNewContact(const QString& convUid)
const auto contact = convModel->owner.contactModel->getContact(convInfo.participants[0]);
if (!contact.profileInfo.uri.isEmpty()
&& contact.profileInfo.uri == lrcInstance_->get_selectedConvUid()) {
lrcInstance_->set_selectedConvUid(convUid);
convModel->selectConversation(convUid);
lrcInstance_->selectConversation(convUid, convInfo.accountId);
convModel_->selectSourceRow(lrcInstance_->indexOf(convUid));
}
} catch (...) {
return;

View file

@ -1,6 +1,7 @@
/*!
/*
* Copyright (C) 2020 by Savoir-faire Linux
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -21,6 +22,8 @@
#include "lrcinstance.h"
#include "qmladapterbase.h"
#include "smartlistmodel.h"
#include "conversationlistmodel.h"
#include "searchresultslistmodel.h"
#include <QObject>
#include <QString>
@ -30,9 +33,10 @@ class SystemTray;
class ConversationsAdapter final : public QmlAdapterBase
{
Q_OBJECT
QML_PROPERTY(lrc::api::profile::Type, currentTypeFilter)
QML_PROPERTY(int, totalUnreadMessageCount)
QML_PROPERTY(int, pendingRequestCount)
Q_PROPERTY(lrc::api::profile::Type currentTypeFilter MEMBER currentTypeFilter_ NOTIFY
currentTypeFilterChanged)
public:
explicit ConversationsAdapter(SystemTray* systemTray,
LRCInstance* instance,
@ -44,25 +48,22 @@ protected:
public:
Q_INVOKABLE bool connectConversationModel(bool updateFilter = true);
Q_INVOKABLE void selectConversation(const QString& accountId, const QString& uid);
Q_INVOKABLE void deselectConversation();
Q_INVOKABLE void refill();
Q_INVOKABLE void updateConversationsFilterWidget();
Q_INVOKABLE void setFilter(const QString& filterString);
Q_INVOKABLE void setTypeFilter(const profile::Type& typeFilter);
Q_INVOKABLE QVariantMap getConvInfoMap(const QString& convId);
Q_SIGNALS:
void showConversation(const QString& accountId, const QString& convUid);
void showConversationTabs(bool visible);
void showSearchStatus(const QString& status);
void modelChanged(const QVariant& model);
void modelSorted(const QVariant& uid);
void updateListViewRequested();
void navigateToWelcomePageRequested();
void currentTypeFilterChanged();
void indexRepositionRequested();
private Q_SLOTS:
void onCurrentAccountIdChanged();
// cross-account slots
void onNewUnreadInteraction(const QString& accountId,
const QString& convUid,
uint64_t interactionId,
@ -73,6 +74,7 @@ private Q_SLOTS:
void onNewTrustRequest(const QString& accountId, const QString& peerUri);
void onTrustRequestTreated(const QString& accountId, const QString& peerUri);
// per-account slots
void onModelChanged();
void onProfileUpdated(const QString&);
void onConversationUpdated(const QString&);
@ -83,13 +85,18 @@ private Q_SLOTS:
void onSearchStatusChanged(const QString&);
void onSearchResultUpdated();
void updateConversationFilterData();
private:
void backToWelcomePage();
void updateConversationForNewContact(const QString& convUid);
SmartListModel* conversationSmartListModel_;
lrc::api::profile::Type currentTypeFilter_ {};
SystemTray* systemTray_;
QScopedPointer<ConversationListModel> convSrcModel_;
QScopedPointer<ConversationListProxyModel> convModel_;
QScopedPointer<SearchResultsListModel> searchSrcModel_;
QScopedPointer<SelectableListProxyModel> searchModel_;
};

View file

@ -219,6 +219,12 @@ LRCInstance::getCurrentCallModel()
return getCurrentAccountInfo().callModel.get();
}
ContactModel*
LRCInstance::getCurrentContactModel()
{
return getCurrentAccountInfo().contactModel.get();
}
const QString&
LRCInstance::getCurrAccId()
{
@ -301,6 +307,18 @@ LRCInstance::getCurrAccConfig()
return getCurrentAccountInfo().confProperties;
}
int
LRCInstance::indexOf(const QString& convId)
{
auto& convs = getCurrentConversationModel()->getConversations();
auto it = std::find_if(convs.begin(),
convs.end(),
[convId](const lrc::api::conversation::Info& conv) {
return conv.uid == convId;
});
return it != convs.end() ? std::distance(convs.begin(), it) : -1;
}
void
LRCInstance::subscribeToDebugReceived()
{
@ -353,6 +371,8 @@ LRCInstance::setContentDraft(const QString& convUid,
{
auto draftKey = accountId + "_" + convUid;
contentDrafts_[draftKey] = content;
// this signal is only needed to update the current smartlist
Q_EMIT draftSaved(convUid);
}
void
@ -374,41 +394,24 @@ LRCInstance::poplastConference(const QString& confId)
}
void
LRCInstance::selectConversation(const QString& accountId, const QString& convUid)
LRCInstance::selectConversation(const QString& convId, const QString& accountId)
{
const auto& convInfo = getConversationFromConvUid(convUid, accountId);
if (get_selectedConvUid() != convInfo.uid || convInfo.participants.size() > 0) {
// If the account is not currently selected, do that first, then
// proceed to select the conversation.
auto selectConversation = [this, accountId, convUid = convInfo.uid] {
const auto& convInfo = getConversationFromConvUid(convUid, accountId);
if (convInfo.uid.isEmpty()) {
return;
}
auto& accInfo = getAccountInfo(convInfo.accountId);
set_selectedConvUid(convInfo.uid);
accInfo.conversationModel->clearUnreadInteractions(convInfo.uid);
try {
// Set contact filter (for conversation tab selection)
auto& contact = accInfo.contactModel->getContact(convInfo.participants.front());
setProperty("currentTypeFilter", QVariant::fromValue(contact.profileInfo.type));
} catch (const std::out_of_range& e) {
qDebug() << e.what();
}
};
if (convInfo.accountId != getCurrAccId()) {
Utils::oneShotConnect(this, &LRCInstance::currentAccountChanged, [selectConversation] {
selectConversation();
});
set_selectedConvUid();
setSelectedAccountId(convInfo.accountId);
} else {
selectConversation();
}
// if the account is not currently selected, do that first, then
// proceed to select the conversation
if (!accountId.isEmpty() && accountId != getCurrAccId()) {
Utils::oneShotConnect(this, &LRCInstance::currentAccountChanged, [this, convId] {
set_selectedConvUid(convId);
});
setSelectedAccountId(accountId);
return;
}
Q_EMIT conversationSelected();
set_selectedConvUid(convId);
}
void
LRCInstance::deselectConversation()
{
set_selectedConvUid();
}
void

View file

@ -72,6 +72,7 @@ public:
NewAccountModel& accountModel();
ConversationModel* getCurrentConversationModel();
NewCallModel* getCurrentCallModel();
ContactModel* getCurrentContactModel();
AVModel& avModel();
PluginModel& pluginModel();
BehaviorController& behaviorController();
@ -95,15 +96,18 @@ public:
const conversation::Info& getConversationFromCallId(const QString& callId,
const QString& accountId = {});
Q_INVOKABLE void selectConversation(const QString& convId, const QString& accountId = {});
Q_INVOKABLE void deselectConversation();
const QString& getCurrAccId();
void setSelectedAccountId(const QString& accountId = {});
void selectConversation(const QString& accountId, const QString& convUid);
int getCurrentAccountIndex();
void setAvatarForAccount(const QPixmap& avatarPixmap, const QString& accountID);
void setCurrAccAvatar(const QPixmap& avatarPixmap);
void setCurrAccAvatar(const QString& avatar);
void setCurrAccDisplayName(const QString& displayName);
const account::ConfProperties_t& getCurrAccConfig();
int indexOf(const QString& convId);
void startAudioMeter(bool async);
void stopAudioMeter(bool async);
@ -121,15 +125,16 @@ Q_SIGNALS:
void currentAccountChanged();
void restoreAppRequested();
void notificationClicked();
void updateSmartList();
void quitEngineRequested();
void conversationSelected();
void conversationUpdated(const QString& convId, const QString& accountId);
void draftSaved(const QString& convId);
void contactBanned(const QString& uri);
private:
std::unique_ptr<Lrc> lrc_;
std::unique_ptr<RenderManager> renderer_;
std::unique_ptr<UpdateManager> updateManager_;
QString selectedAccountId_ {""};
QString selectedAccountId_ {};
MapStringString contentDrafts_;
MapStringString lastConferences_;

View file

@ -34,6 +34,7 @@
class ConnectivityMonitor;
class AppSettingsManager;
class SystemTray;
class CallAdapter;
// Provides information about the screen the app is displayed on
class ScreenInfo : public QObject
@ -98,4 +99,6 @@ private:
SystemTray* systemTray_;
ScreenInfo screenInfo_;
CallAdapter* callAdapter_;
};

View file

@ -59,8 +59,6 @@ Rectangle {
property string currentAccountId: AccountAdapter.currentAccountId
onCurrentAccountIdChanged: {
var index = UtilsAdapter.getCurrAccList().indexOf(currentAccountId)
mainViewSidePanel.refreshAccountComboBox(index)
if (inSettingsView) {
settingsView.accountListChanged()
settingsView.setSelected(settingsView.selectedMenu, true)
@ -80,7 +78,7 @@ Rectangle {
function showWelcomeView() {
currentConvUID = ""
callStackView.needToCloseInCallConversationAndPotentialWindow()
mainViewSidePanel.deselectConversationSmartList()
LRCInstance.deselectConversation()
if (isPageInStack("callStackViewObject", sidePanelViewStack) ||
isPageInStack("communicationPageMessageWebView", sidePanelViewStack) ||
isPageInStack("communicationPageMessageWebView", mainViewStack) ||
@ -140,8 +138,7 @@ Rectangle {
if (checkCurrentCall && currentAccountIsCalling()) {
var callConv = UtilsAdapter.getCallConvForAccount(
AccountAdapter.currentAccountId)
ConversationsAdapter.selectConversation(
AccountAdapter.currentAccountId, callConv)
LRCInstance.selectConversation(callConv)
CallAdapter.updateCall(callConv, currentAccountId)
} else {
showWelcomeView()
@ -171,56 +168,50 @@ Rectangle {
}
}
// ConversationSmartListViewItemDelegate provides UI information
function setMainView(currentUserDisplayName, currentUserAlias, currentUID,
callStackViewShouldShow, isAudioOnly, callState) {
function setMainView(convId) {
if (!(communicationPageMessageWebView.jsLoaded)) {
communicationPageMessageWebView.jsLoadedChanged.connect(
function(currentUserDisplayName, currentUserAlias, currentUID,
callStackViewShouldShow, isAudioOnly, callState) {
return function() {
setMainView(currentUserDisplayName, currentUserAlias, currentUID,
callStackViewShouldShow, isAudioOnly, callState)
}
}(currentUserDisplayName, currentUserAlias, currentUID,
callStackViewShouldShow, isAudioOnly, callState))
function(convId) {
return function() { setMainView(convId) }
}(convId))
return
}
if (callStackViewShouldShow) {
var item = ConversationsAdapter.getConvInfoMap(convId)
if (item.convId === undefined)
return
communicationPageMessageWebView.headerUserAliasLabelText = item.bestName
communicationPageMessageWebView.headerUserUserNameLabelText = item.bestId
if (item.callStackViewShouldShow) {
if (inSettingsView) {
toggleSettingsView()
}
MessagesAdapter.setupChatView(currentUID)
communicationPageMessageWebView.headerUserAliasLabelText = currentUserAlias
communicationPageMessageWebView.headerUserUserNameLabelText = currentUserDisplayName
MessagesAdapter.setupChatView(convId)
callStackView.setLinkedWebview(communicationPageMessageWebView)
callStackView.responsibleAccountId = AccountAdapter.currentAccountId
callStackView.responsibleConvUid = currentUID
currentConvUID = currentUID
callStackView.responsibleConvUid = convId
currentConvUID = convId
if (callState === Call.Status.IN_PROGRESS || callState === Call.Status.PAUSED) {
CallAdapter.updateCall(currentUID, AccountAdapter.currentAccountId)
if (isAudioOnly)
if (item.callState === Call.Status.IN_PROGRESS ||
item.callState === Call.Status.PAUSED) {
CallAdapter.updateCall(convId, AccountAdapter.currentAccountId)
if (item.isAudioOnly)
callStackView.showAudioCallPage()
else
callStackView.showVideoCallPage()
} else if (callState === Call.Status.INCOMING_RINGING) {
} else if (item.callState === Call.Status.INCOMING_RINGING) {
callStackView.showIncomingCallPage()
} else {
callStackView.showOutgoingCallPage(callState)
callStackView.showOutgoingCallPage(item.callState)
}
pushCallStackView()
} else if (!inSettingsView) {
if (currentConvUID !== currentUID) {
if (currentConvUID !== convId) {
callStackView.needToCloseInCallConversationAndPotentialWindow()
MessagesAdapter.setupChatView(currentUID)
communicationPageMessageWebView.headerUserAliasLabelText = currentUserAlias
communicationPageMessageWebView.headerUserUserNameLabelText = currentUserDisplayName
MessagesAdapter.setupChatView(convId)
pushCommunicationMessageWebView()
communicationPageMessageWebView.focusMessageWebView()
currentConvUID = currentUID
currentConvUID = convId
} else if (isPageInStack("callStackViewObject", sidePanelViewStack)
|| isPageInStack("callStackViewObject", mainViewStack)) {
callStackView.needToCloseInCallConversationAndPotentialWindow()
@ -233,11 +224,16 @@ Rectangle {
color: JamiTheme.backgroundColor
Connections {
target: CallAdapter
target: LRCInstance
// selectConversation causes UI update
function onCallSetupMainViewRequired(accountId, convUid) {
ConversationsAdapter.selectConversation(accountId, convUid)
function onSelectedConvUidChanged() {
mainView.setMainView(LRCInstance.selectedConvUid)
}
function onConversationUpdated(convUid, accountId) {
if (convUid === LRCInstance.selectedConvUid &&
accountId === currentAccountId)
mainView.setMainView(convUid)
}
}
@ -291,12 +287,6 @@ Rectangle {
Connections {
target: AccountAdapter
function onUpdateConversationForAddedContact() {
MessagesAdapter.updateConversationForAddedContact()
mainViewSidePanel.clearContactSearchBar()
mainViewSidePanel.forceReselectConversationSmartListCurrentIndex()
}
function onAccountStatusChanged(accountId) {
accountComboBox.resetAccountListModel(accountId)
}
@ -438,17 +428,12 @@ Rectangle {
Connections {
target: MessagesAdapter
function onNeedToUpdateSmartList() {
mainViewSidePanel.forceUpdateConversationSmartListView()
}
function onNavigateToWelcomePageRequested() {
backToMainView()
}
function onInvitationAccepted() {
mainViewSidePanel.selectTab(SidePanelTabBar.Conversations)
showWelcomeView()
}
}

View file

@ -34,7 +34,20 @@ Label {
signal settingBtnClicked
property alias popup: comboBoxPopup
// Reset accountListModel.
// TODO: remove these refresh hacks use QAbstractItemModels correctly
Connections {
target: AccountAdapter
function onCurrentAccountIdChanged() {
root.update()
resetAccountListModel(AccountAdapter.currentAccountId)
}
function onAccountStatusChanged(accountId) {
resetAccountListModel(accountId)
}
}
function resetAccountListModel(accountId) {
accountListModel.updateAvatarUid(accountId)
accountListModel.reset()

View file

@ -112,11 +112,11 @@ Popup {
layer {
enabled: true
effect: DropShadow {
color: JamiTheme.shadowColor
verticalOffset: 2
horizontalOffset: 2
horizontalOffset: 3.0
verticalOffset: 3.0
radius: 16.0
samples: 16
radius: 10
color: JamiTheme.shadowColor
}
}
}

View file

@ -0,0 +1,79 @@
/*
* Copyright (C) 2021 by Savoir-faire Linux
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick 2.14
import net.jami.Constants 1.0
Rectangle {
id: root
property real size
property int count: 0
property int lastCount: count
property bool populated: false
property bool animate: true
width: size
height: size
radius: JamiTheme.primaryRadius
color: JamiTheme.filterBadgeColor
visible: count > 0
Text {
id: countLabel
anchors.centerIn: root
text: count > 9 ? "…" : count
color: JamiTheme.filterBadgeTextColor
font.pointSize: JamiTheme.filterBadgeFontSize
font.weight: Font.ExtraBold
}
onCountChanged: {
if (count > lastCount && animate)
notifyAnim.start()
lastCount = count
if (!populated)
populated = true
}
ParallelAnimation {
id: notifyAnim
ColorAnimation {
target: root; properties: "color"
from: JamiTheme.filterBadgeTextColor
to: JamiTheme.filterBadgeColor
duration: 150; easing.type: Easing.InOutQuad
}
ColorAnimation {
target: countLabel; properties: "color"
from: JamiTheme.filterBadgeColor
to: JamiTheme.filterBadgeTextColor
duration: 150; easing.type: Easing.InOutQuad
}
NumberAnimation {
target: root; property: "y"
from: -3; to: 0
duration: 150; easing.type: Easing.InOutQuad
}
}
}

View file

@ -399,7 +399,7 @@ Rectangle {
onAddToConferenceButtonClicked: {
// Create contact picker - conference.
ContactPickerCreation.createContactPickerObjects(
ContactPicker.ContactPickerType.JAMICONFERENCE,
ContactList.CONFERENCE,
callOverlayRect)
ContactPickerCreation.openContactPicker()
}
@ -517,7 +517,7 @@ Rectangle {
onTransferCallButtonClicked: {
// Create contact picker - sip transfer.
ContactPickerCreation.createContactPickerObjects(
ContactPicker.ContactPickerType.SIPTRANSFER,
ContactList.TRANSFER,
callOverlayRect)
ContactPickerCreation.openContactPicker()
}

View file

@ -30,15 +30,7 @@ import "../../commoncomponents"
Popup {
id: contactPickerPopup
property int type: ContactPicker.ContactPickerType.JAMICONFERENCE
// Important to keep it one, since enum in c++ starts at one for conferences.
enum ContactPickerType {
CONVERSATION = 0,
JAMICONFERENCE,
SIPTRANSFER
}
property int type: ContactList.CONFERENCE
contentWidth: 250
contentHeight: contactPickerPopupRectColumnLayout.height + 50
@ -89,9 +81,9 @@ Popup {
text: {
switch(type) {
case ContactPicker.ContactPickerType.JAMICONFERENCE:
case ContactList.CONFERENCE:
return qsTr("Add to conference")
case ContactPicker.ContactPickerType.SIPTRANSFER:
case ContactList.TRANSFER:
return qsTr("Transfer this call")
default:
return qsTr("Add default moderator")

View file

@ -66,7 +66,7 @@ ItemDelegate {
font: contactPickerContactName.font
elide: Text.ElideMiddle
elideWidth: contactPickerContactInfoRect.width
text: DisplayName
text: BestName
}
color: JamiTheme.textColor
@ -88,7 +88,7 @@ ItemDelegate {
font: contactPickerContactId.font
elide: Text.ElideMiddle
elideWidth: contactPickerContactInfoRect.width
text: DisplayID == DisplayName ? "" : DisplayID
text: BestId == BestName ? "" : BestId
}
text: textMetricsContactPickerContactId.elidedText

View file

@ -32,6 +32,8 @@ Rectangle {
signal contactSearchBarTextChanged(string text)
signal returnPressedWhileSearching
property alias textContent: contactSearchBar.text
function clearText() {
contactSearchBar.clear()
fakeFocus.forceActiveFocus()
@ -108,10 +110,11 @@ Rectangle {
anchors.right: root.right
anchors.rightMargin: 10
preferredSize: 20
preferredSize: 21
radius: JamiTheme.primaryRadius
visible: contactSearchBar.text.length
opacity: visible ? 1 : 0
normalColor: root.color
imageColor: JamiTheme.primaryForegroundColor
@ -120,6 +123,10 @@ Rectangle {
toolTipText: JamiStrings.clearText
onClicked: contactSearchBar.clear()
Behavior on opacity {
NumberAnimation { duration: 500; easing.type: Easing.OutCubic }
}
}
Shortcut {

View file

@ -0,0 +1,222 @@
/*
* Copyright (C) 2021 by Savoir-faire Linux
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import net.jami.Models 1.0
import net.jami.Adapters 1.0
import net.jami.Constants 1.0
ListView {
id: root
// the following should be marked required (Qtver >= 5.15)
// along with `required model`
property string headerLabel
property bool headerVisible
delegate: SmartListItemDelegate {}
currentIndex: model.currentFilteredRow
clip: true
maximumFlickVelocity: 1024
ScrollIndicator.vertical: ScrollIndicator {}
// highlight selection
// down and hover states are done within the delegate
highlight: Rectangle {
width: ListView.view ? ListView.view.width : 0
color: JamiTheme.selectedColor
}
highlightMoveDuration: 60
headerPositioning: ListView.OverlayHeader
header: Rectangle {
z: 2
color: JamiTheme.backgroundColor
visible: root.headerVisible
width: root.width
height: root.headerVisible ? 20 : 0
Text {
anchors {
left: parent.left
leftMargin: 16
verticalCenter: parent.verticalCenter
}
text: headerLabel + " (" + root.count + ")"
font.pointSize: JamiTheme.smartlistItemFontSize
font.weight: Font.DemiBold
color: JamiTheme.textColor
}
}
Connections {
target: model
// actually select the conversation
function onValidSelectionChanged() {
var row = model.currentFilteredRow
var convId = model.dataForRow(row, ConversationList.UID)
LRCInstance.selectConversation(convId)
}
}
onCountChanged: positionViewAtBeginning()
Component.onCompleted: {
// TODO: remove this
ConversationsAdapter.setQmlObject(this)
}
add: Transition {
NumberAnimation {
property: "opacity"; from: 0; to: 1.0
duration: JamiTheme.smartListTransitionDuration
}
}
displaced: Transition {
NumberAnimation {
properties: "x,y"; easing.type: Easing.OutCubic
duration: JamiTheme.smartListTransitionDuration
}
NumberAnimation {
property: "opacity"; to: 1.0
duration: JamiTheme.smartListTransitionDuration * (1 - from)
}
}
Behavior on opacity {
NumberAnimation {
easing.type: Easing.OutCubic
duration: 2 * JamiTheme.smartListTransitionDuration
}
}
function openContextMenuAt(x, y, delegate) {
var mappedCoord = root.mapFromItem(delegate, x, y)
contextMenu.openMenuAt(mappedCoord.x, mappedCoord.y)
}
ConversationSmartListContextMenu {
id: contextMenu
function openMenuAt(x, y) {
contextMenu.x = x
contextMenu.y = y
// TODO:
// - accountId, convId only
// - userProfile dialog should use a loader/popup
var row = root.indexAt(x, y + root.contentY)
var item = {
"convId": model.dataForRow(row, ConversationList.UID),
"displayId": model.dataForRow(row, ConversationList.BestId),
"displayName": model.dataForRow(row, ConversationList.BestName),
"uri": model.dataForRow(row, ConversationList.URI),
"contactType": model.dataForRow(row, ConversationList.ContactType),
}
responsibleAccountId = AccountAdapter.currentAccountId
responsibleConvUid = item.convId
contactType = item.contactType
userProfile.responsibleConvUid = item.convId
userProfile.aliasText = item.displayName
userProfile.registeredNameText = item.displayId
userProfile.idText = item.uri
userProfile.contactImageUid = item.convId
openMenu()
}
}
Shortcut {
sequence: "Ctrl+Shift+X"
context: Qt.ApplicationShortcut
enabled: root.visible
onActivated: {
CallAdapter.placeCall()
communicationPageMessageWebView.setSendContactRequestButtonVisible(false)
}
}
Shortcut {
sequence: "Ctrl+Shift+C"
context: Qt.ApplicationShortcut
enabled: root.visible
onActivated: {
CallAdapter.placeAudioOnlyCall()
communicationPageMessageWebView.setSendContactRequestButtonVisible(false)
}
}
Shortcut {
sequence: "Ctrl+Shift+L"
context: Qt.ApplicationShortcut
enabled: root.visible
onActivated: MessagesAdapter.clearConversationHistory(
AccountAdapter.currentAccountId,
UtilsAdapter.getCurrConvId())
}
Shortcut {
sequence: "Ctrl+Shift+B"
context: Qt.ApplicationShortcut
enabled: root.visible
onActivated: {
MessagesAdapter.blockConversation(UtilsAdapter.getCurrConvId())
}
}
Shortcut {
sequence: "Ctrl+Shift+Delete"
context: Qt.ApplicationShortcut
enabled: root.visible
onActivated: MessagesAdapter.removeConversation(
AccountAdapter.currentAccountId,
UtilsAdapter.getCurrConvId(),
false)
}
Shortcut {
sequence: "Ctrl+Down"
context: Qt.ApplicationShortcut
enabled: root.visible
onActivated: {
if (currentIndex + 1 >= count)
return
model.select(currentIndex + 1)
}
}
Shortcut {
sequence: "Ctrl+Up"
context: Qt.ApplicationShortcut
enabled: root.visible
onActivated: {
if (currentIndex <= 0)
return
model.select(currentIndex - 1)
}
}
}

View file

@ -34,6 +34,8 @@ Item {
property string responsibleConvUid: ""
property int contactType: Profile.Type.INVALID
function isOpen() { return ContextMenuGenerator.getMenu().visible }
function openMenu() {
ContextMenuGenerator.initMenu()
var hasCall = UtilsAdapter.getCallId(responsibleAccountId, responsibleConvUid) !== ""
@ -41,18 +43,14 @@ Item {
ContextMenuGenerator.addMenuItem(qsTr("Start video call"),
"qrc:/images/icons/videocam-24px.svg",
function (){
ConversationsAdapter.selectConversation(
responsibleAccountId,
responsibleConvUid, false)
LRCInstance.selectConversation(responsibleConvUid, responsibleAccountId)
CallAdapter.placeCall()
communicationPageMessageWebView.setSendContactRequestButtonVisible(false)
})
ContextMenuGenerator.addMenuItem(qsTr("Start audio call"),
"qrc:/images/icons/place_audiocall-24px.svg",
function (){
ConversationsAdapter.selectConversation(
responsibleAccountId,
responsibleConvUid, false)
LRCInstance.selectConversation(responsibleConvUid, responsibleAccountId)
CallAdapter.placeAudioOnlyCall()
communicationPageMessageWebView.setSendContactRequestButtonVisible(false)
})
@ -116,7 +114,7 @@ Item {
})
}
ContextMenuGenerator.addMenuSeparator()
ContextMenuGenerator.addMenuItem(qsTr("Profile"),
ContextMenuGenerator.addMenuItem(qsTr("Contact details"),
"qrc:/images/icons/person-24px.svg",
function (){
userProfile.open()

View file

@ -1,171 +0,0 @@
/*
* Copyright (C) 2020 by Savoir-faire Linux
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import net.jami.Models 1.0
import net.jami.Adapters 1.0
ListView {
id: root
signal needToDeselectItems
signal forceUpdatePotentialInvalidItem
// Refresh all items within the model.
function updateListView() {
if (!root.model)
return
root.model.dataChanged(
root.model.index(0, 0),
root.model.index(
root.model.rowCount() - 1, 0))
root.forceUpdatePotentialInvalidItem()
}
function repositionIndex(uid = "") {
// Only update index if it has changed
var currentI = root.currentIndex
if (uid === "")
uid = mainView.currentConvUID
root.currentIndex = -1
updateListView()
for (var i = 0; i < count; i++) {
if (root.model.data(
root.model.index(i, 0), SmartListModel.UID) === uid) {
root.currentIndex = i
break
}
}
}
ConversationSmartListContextMenu {
id: smartListContextMenu
}
Connections {
target: ConversationsAdapter
function onModelChanged(model) {
root.model = model
}
// When the model has been sorted, we need to adjust the focus (currentIndex)
// to the previously focused conversation item.
function onModelSorted(uid) {
repositionIndex(uid)
}
function onUpdateListViewRequested() {
updateListView()
}
function onIndexRepositionRequested() {
repositionIndex()
}
}
Connections {
target: LRCInstance
function onUpdateSmartList() { updateListView() }
}
clip: true
maximumFlickVelocity: 1024
delegate: ConversationSmartListViewItemDelegate {
id: smartListItemDelegate
onUpdateContactAvatarUidRequested: root.model.updateContactAvatarUid(uid)
}
ScrollIndicator.vertical: ScrollIndicator {}
Shortcut {
sequence: "Ctrl+Shift+X"
context: Qt.ApplicationShortcut
enabled: root.visible
onActivated: {
CallAdapter.placeCall()
communicationPageMessageWebView.setSendContactRequestButtonVisible(false)
}
}
Shortcut {
sequence: "Ctrl+Shift+C"
context: Qt.ApplicationShortcut
enabled: root.visible
onActivated: {
CallAdapter.placeAudioOnlyCall()
communicationPageMessageWebView.setSendContactRequestButtonVisible(false)
}
}
Shortcut {
sequence: "Ctrl+Shift+L"
context: Qt.ApplicationShortcut
enabled: root.visible
onActivated: MessagesAdapter.clearConversationHistory(
AccountAdapter.currentAccountId,
UtilsAdapter.getCurrConvId())
}
Shortcut {
sequence: "Ctrl+Shift+B"
context: Qt.ApplicationShortcut
enabled: root.visible
onActivated: {
MessagesAdapter.blockConversation(UtilsAdapter.getCurrConvId())
}
}
Shortcut {
sequence: "Ctrl+Shift+Delete"
context: Qt.ApplicationShortcut
enabled: root.visible
onActivated: MessagesAdapter.removeConversation(
AccountAdapter.currentAccountId,
UtilsAdapter.getCurrConvId(),
false)
}
Shortcut {
sequence: "Ctrl+Down"
context: Qt.ApplicationShortcut
enabled: root.visible
onActivated: {
if (currentIndex + 1 >= count)
return
root.currentIndex += 1
}
}
Shortcut {
sequence: "Ctrl+Up"
context: Qt.ApplicationShortcut
enabled: root.visible
onActivated: {
if (currentIndex <= 0)
return
root.currentIndex -= 1
}
}
}

View file

@ -1,269 +0,0 @@
/*
* Copyright (C) 2020 by Savoir-faire Linux
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import net.jami.Models 1.0
import net.jami.Adapters 1.0
import net.jami.Constants 1.0
import "../../commoncomponents"
ItemDelegate {
id: smartListItemDelegate
height: 72
property int lastInteractionPreferredWidth: 80
signal updateContactAvatarUidRequested(string uid)
property bool openedMenu: false
function convUid() {
return UID
}
Connections {
target: conversationSmartListView
// Hack, make sure that smartListItemDelegate does not show extra item
// when searching new contacts.
function onForceUpdatePotentialInvalidItem() {
smartListItemDelegate.visible =
conversationSmartListView.model.rowCount() <= index ? false : true
}
// When currentIndex is -1, deselect items, if not, change select item
function onCurrentIndexChanged() {
if (conversationSmartListView.currentIndex === -1
|| conversationSmartListView.currentIndex !== index) {
itemSmartListBackground.color = Qt.binding(function () {
return InCall ? Qt.lighter(JamiTheme.selectionBlue,
1.8) : JamiTheme.backgroundColor
})
} else {
itemSmartListBackground.color = Qt.binding(function () {
return InCall ? Qt.lighter(JamiTheme.selectionBlue,
1.8) : JamiTheme.selectedColor
})
ConversationsAdapter.selectConversation(
AccountAdapter.currentAccountId, UID)
}
}
}
Connections {
target: ConversationsAdapter
function onShowConversation(accountId, convUid) {
if (convUid === UID) {
mainView.setMainView(DisplayID == DisplayName ? "" : DisplayID,
DisplayName, UID, CallStackViewShouldShow, IsAudioOnly, CallState)
}
}
}
AvatarImage {
id: conversationSmartListUserImage
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 16
width: 40
height: 40
mode: AvatarImage.Mode.FromContactUri
showPresenceIndicator: Presence === undefined ? false : Presence
unreadMessagesCount: UnreadMessagesCount
Component.onCompleted: {
var contactUid = URI
if (ContactType === Profile.Type.TEMPORARY)
updateContactAvatarUidRequested(contactUid)
updateImage(contactUid, PictureUid)
}
}
RowLayout {
id: rowUsernameAndLastInteractionDate
anchors.left: conversationSmartListUserImage.right
anchors.leftMargin: 16
anchors.top: parent.top
anchors.topMargin: conversationSmartListUserLastInteractionMessage.text !== "" ?
16 : parent.height/2-conversationSmartListUserName.height/2
anchors.right: parent.right
anchors.rightMargin: 10
Text {
id: conversationSmartListUserName
Layout.alignment: conversationSmartListUserLastInteractionMessage.text !== "" ?
Qt.AlignLeft : Qt.AlignLeft | Qt.AlignVCenter
TextMetrics {
id: textMetricsConversationSmartListUserName
font: conversationSmartListUserName.font
elide: Text.ElideRight
elideWidth: LastInteractionDate ? (smartListItemDelegate.width - lastInteractionPreferredWidth
- conversationSmartListUserImage.width-32)
: smartListItemDelegate.width - lastInteractionPreferredWidth
text: DisplayName === undefined ? "" : DisplayName
}
text: textMetricsConversationSmartListUserName.elidedText
font.pointSize: JamiTheme.smartlistItemFontSize
color: JamiTheme.textColor
}
Text {
id: conversationSmartListUserLastInteractionDate
Layout.alignment: Qt.AlignRight
TextMetrics {
id: textMetricsConversationSmartListUserLastInteractionDate
font: conversationSmartListUserLastInteractionDate.font
elide: Text.ElideRight
elideWidth: lastInteractionPreferredWidth
text: LastInteractionDate === undefined ? "" : LastInteractionDate
}
text: textMetricsConversationSmartListUserLastInteractionDate.elidedText
font.pointSize: JamiTheme.textFontSize
color: JamiTheme.faddedLastInteractionFontColor
}
}
Text {
id: conversationSmartListUserLastInteractionMessage
anchors.left: conversationSmartListUserImage.right
anchors.leftMargin: 16
anchors.bottom: rowUsernameAndLastInteractionDate.bottom
anchors.bottomMargin: -20
TextMetrics {
id: textMetricsConversationSmartListUserLastInteractionMessage
font: conversationSmartListUserLastInteractionMessage.font
elide: Text.ElideRight
elideWidth: LastInteractionDate ? (smartListItemDelegate.width - lastInteractionPreferredWidth
- conversationSmartListUserImage.width-32)
: smartListItemDelegate.width - lastInteractionPreferredWidth
text: InCall ? UtilsAdapter.getCallStatusStr(CallState) : (Draft ? Draft : LastInteraction)
}
font.family: Qt.platform.os === "windows" ? "Segoe UI Emoji" : Qt.application.font.family
font.hintingPreference: Font.PreferNoHinting
text: textMetricsConversationSmartListUserLastInteractionMessage.elidedText
maximumLineCount: 1
font.pointSize: JamiTheme.textFontSize
color: Draft ? JamiTheme.draftRed : JamiTheme.faddedLastInteractionFontColor
}
background: Rectangle {
id: itemSmartListBackground
color: InCall ? Qt.lighter(JamiTheme.selectionBlue, 1.8) : JamiTheme.backgroundColor
implicitWidth: conversationSmartListView.width
implicitHeight: parent.height
border.width: 0
}
MouseArea {
id: mouseAreaSmartListItemDelegate
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
function openContextMenu(mouse) {
openedMenu = true
smartListContextMenu.parent = mouseAreaSmartListItemDelegate
// Make menu pos at mouse.
var relativeMousePos = mapToItem(itemSmartListBackground,
mouse.x, mouse.y)
smartListContextMenu.x = relativeMousePos.x
smartListContextMenu.y = relativeMousePos.y
smartListContextMenu.responsibleAccountId = AccountAdapter.currentAccountId
smartListContextMenu.responsibleConvUid = UID
smartListContextMenu.contactType = ContactType
userProfile.responsibleConvUid = UID
userProfile.aliasText = DisplayName
userProfile.registeredNameText = DisplayID
userProfile.idText = URI
userProfile.contactImageUid = UID
smartListContextMenu.openMenu()
}
onPressed: {
if (!InCall) {
itemSmartListBackground.color = JamiTheme.pressColor
}
}
onDoubleClicked: {
if (!InCall) {
ConversationsAdapter.selectConversation(AccountAdapter.currentAccountId,
UID,
false)
if (AccountAdapter.currentAccountType === Profile.Type.SIP)
CallAdapter.placeAudioOnlyCall()
else
CallAdapter.placeCall()
communicationPageMessageWebView.setSendContactRequestButtonVisible(false)
}
}
onPressAndHold: {
openContextMenu(mouse)
}
onReleased: {
if (!InCall) {
itemSmartListBackground.color = JamiTheme.selectionBlue
}
if (mouse.button === Qt.RightButton) {
openContextMenu(mouse)
} else if (mouse.button === Qt.LeftButton && !openedMenu) {
conversationSmartListView.currentIndex = -1
conversationSmartListView.currentIndex = index
}
openedMenu = false
}
onEntered: {
if (!InCall) {
itemSmartListBackground.color = JamiTheme.hoverColor
}
}
onExited: {
if (!InCall) {
if (conversationSmartListView.currentIndex !== index
|| conversationSmartListView.currentIndex === -1) {
itemSmartListBackground.color = Qt.binding(function () {
return InCall ? Qt.lighter(JamiTheme.selectionBlue,
1.8) : JamiTheme.backgroundColor
})
} else {
itemSmartListBackground.color = Qt.binding(function () {
return InCall ? Qt.lighter(JamiTheme.selectionBlue,
1.8) : JamiTheme.selectedColor
})
}
}
}
}
}

View file

@ -20,10 +20,7 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import QtGraphicalEffects 1.14
import net.jami.Models 1.0
import net.jami.Adapters 1.0
import net.jami.Constants 1.0
import "../../commoncomponents"
@ -34,7 +31,7 @@ TabButton {
property var tabBar: undefined
property alias labelText: label.text
property alias acceleratorSequence: accelerator.sequence
property int badgeCount
property alias badgeCount: badge.count
signal selected
hoverEnabled: true
@ -57,32 +54,17 @@ TabButton {
id: label
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.bottomMargin: 1
font.pointSize: JamiTheme.filterItemFontSize
color: Qt.lighter(JamiTheme.textColor,
root.down == true ? 1.0 : 1.5)
color: JamiTheme.textColor
opacity: root.down ? 1.0 : 0.5
}
Rectangle {
id: badgeRect
readonly property real size: 20
BadgeNotifier {
id: badge
size: 20
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
width: size
height: size
radius: JamiTheme.primaryRadius
color: JamiTheme.filterBadgeColor
visible: badgeCount > 0
Text {
anchors.centerIn: badgeRect
text: badgeCount > 9 ? "…" : badgeCount
color: JamiTheme.filterBadgeTextColor
font.pointSize: JamiTheme.filterBadgeFontSize
}
}
}
}
@ -91,9 +73,7 @@ TabButton {
width: rect.width
anchors.bottom: rect.bottom
height: 2
color: root.down === true ?
JamiTheme.textColor :
"transparent"
color: root.down ? JamiTheme.textColor : "transparent"
}
Shortcut {

View file

@ -108,6 +108,14 @@ Rectangle {
}
}
Connections {
target: AccountAdapter
function onSelectedContactAdded(convId) {
MessagesAdapter.updateConversationForAddedContact()
}
}
JamiFileDialog {
id: jamiFileDialog

View file

@ -1,6 +1,7 @@
/*
* Copyright (C) 2020 by Savoir-faire Linux
* Copyright (C) 2020-2021 by Savoir-faire Linux
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -32,60 +33,29 @@ Rectangle {
color: JamiTheme.backgroundColor
property bool tabBarVisible: true
property int pendingRequestCount: 0
property int totalUnreadMessagesCount: 0
anchors.fill: parent
// Hack -> force redraw.
function forceReselectConversationSmartListCurrentIndex() {
var index = conversationSmartListView.currentIndex
conversationSmartListView.currentIndex = -1
conversationSmartListView.currentIndex = index
}
Connections {
target: AccountAdapter
function onCurrentAccountIdChanged() {
clearContactSearchBar()
}
// For contact request conv to be focused correctly.
function setCurrentUidSmartListModelIndex() {
conversationSmartListView.currentIndex
= conversationSmartListView.model.currentUidSmartListModelIndex()
}
function updatePendingRequestCount() {
pendingRequestCount = UtilsAdapter.getTotalPendingRequest()
}
function updateTotalUnreadMessagesCount() {
totalUnreadMessagesCount = UtilsAdapter.getTotalUnreadMessages()
function onSelectedContactAdded(convId) {
clearContactSearchBar()
LRCInstance.selectConversation(convId)
}
}
function clearContactSearchBar() {
contactSearchBar.clearText()
}
function refreshAccountComboBox(index) {
accountComboBox.update()
clearContactSearchBar()
accountComboBox.resetAccountListModel()
}
function deselectConversationSmartList() {
ConversationsAdapter.deselectConversation()
conversationSmartListView.currentIndex = -1
}
function forceUpdateConversationSmartListView() {
conversationSmartListView.updateListView()
}
function selectTab(tabIndex) {
sidePanelTabBar.selectTab(tabIndex)
}
// Intended -> since strange behavior will happen without this for stackview.
anchors.top: parent.top
anchors.fill: parent
// Search bar container to embed search label
ContactSearchBar {
id: contactSearchBar
@ -98,116 +68,114 @@ Rectangle {
anchors.rightMargin: 15
onContactSearchBarTextChanged: {
UtilsAdapter.setConversationFilter(text)
// not calling positionViewAtBeginning will cause
// sort animation visual bugs
conversationListView.positionViewAtBeginning()
ConversationsAdapter.setFilter(text)
}
onReturnPressedWhileSearching: {
var convUid = conversationSmartListView.itemAtIndex(0).convUid()
var currentAccountId = AccountAdapter.currentAccountId
ConversationsAdapter.selectConversation(currentAccountId, convUid)
conversationSmartListView.repositionIndex(convUid)
var listView = searchResultsListView.count ?
searchResultsListView :
conversationListView
if (listView.count)
listView.model.select(0)
}
}
SidePanelTabBar {
id: sidePanelTabBar
visible: ConversationsAdapter.pendingRequestCount &&
!contactSearchBar.textContent
anchors.top: contactSearchBar.bottom
anchors.topMargin: 10
anchors.topMargin: visible ? 10 : 0
width: sidePanelRect.width
height: tabBarVisible ? 42 : 0
height: visible ? 42 : 0
}
Rectangle {
id: searchStatusRect
visible: lblSearchStatus.text !== ""
visible: searchStatusText.text !== ""
anchors.top: tabBarVisible ? sidePanelTabBar.bottom : contactSearchBar.bottom
anchors.topMargin: tabBarVisible ? 0 : 10
anchors.top: sidePanelTabBar.bottom
anchors.topMargin: visible ? 10 : 0
width: parent.width
height: 72
height: visible ? 42 : 0
color: "transparent"
color: JamiTheme.backgroundColor
Image {
id: searchIcon
anchors.left: searchStatusRect.left
anchors.leftMargin: 24
anchors.verticalCenter: searchStatusRect.verticalCenter
width: 24
height: 24
Text {
id: searchStatusText
layer {
enabled: true
effect: ColorOverlay {
color: JamiTheme.textColor
}
}
fillMode: Image.PreserveAspectFit
mipmap: true
source: "qrc:/images/icons/ic_baseline-search-24px.svg"
}
Label {
id: lblSearchStatus
anchors.verticalCenter: searchStatusRect.verticalCenter
anchors.left: searchIcon.right
anchors.leftMargin: 24
width: searchStatusRect.width - searchIcon.width - 24*2 - 8
text: ""
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 32
anchors.right: parent.right
anchors.rightMargin: 32
color: JamiTheme.textColor
wrapMode: Text.WordWrap
font.pointSize: JamiTheme.menuFontSize
}
MouseArea {
id: mouseAreaSearchRect
anchors.fill: parent
hoverEnabled: true
onReleased: {
searchStatusRect.color = Qt.binding(function(){return JamiTheme.normalButtonColor})
}
onEntered: {
searchStatusRect.color = Qt.binding(function(){return JamiTheme.hoverColor})
}
onExited: {
searchStatusRect.color = Qt.binding(function(){return JamiTheme.backgroundColor})
}
font.pointSize: JamiTheme.filterItemFontSize
}
}
ConversationSmartListView {
id: conversationSmartListView
Connections {
target: ConversationsAdapter
function onShowSearchStatus(status) {
searchStatusText.text = status
}
}
ColumnLayout {
id: smartListLayout
anchors.top: searchStatusRect.visible ? searchStatusRect.bottom : (tabBarVisible ? sidePanelTabBar.bottom : contactSearchBar.bottom)
anchors.topMargin: (tabBarVisible || searchStatusRect.visible) ? 0 : 10
width: parent.width
height: tabBarVisible ? sidePanelRect.height - sidePanelTabBar.height - contactSearchBar.height - 20 :
sidePanelRect.height - contactSearchBar.height - 20
anchors.top: searchStatusRect.bottom
anchors.topMargin: (sidePanelTabBar.visible ||
searchStatusRect.visible) ? 0 : 12
anchors.bottom: parent.bottom
Connections {
target: ConversationsAdapter
spacing: 4
function onShowConversationTabs(visible) {
tabBarVisible = visible
updatePendingRequestCount()
updateTotalUnreadMessagesCount()
ConversationListView {
id: searchResultsListView
visible: count
opacity: visible ? 1 :0
Layout.topMargin: 10
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
Layout.preferredHeight: visible ? contentHeight : 0
Layout.maximumHeight: {
var otherContentHeight = conversationListView.contentHeight + 16
if (conversationListView.visible)
if (otherContentHeight < parent.height / 2)
return parent.height - otherContentHeight
else
return parent.height / 2
else
return parent.height
}
function onShowSearchStatus(status) {
lblSearchStatus.text = status
}
model: SearchResultsListModel
headerLabel: JamiStrings.searchResults
headerVisible: visible
}
Component.onCompleted: {
ConversationsAdapter.setQmlObject(this)
conversationSmartListView.currentIndex = -1
ConversationListView {
id: conversationListView
visible: count
Layout.preferredWidth: parent.width
Layout.fillHeight: true
model: ConversationListModel
headerLabel: JamiStrings.conversations
headerVisible: searchResultsListView.visible
}
}
}

View file

@ -28,31 +28,18 @@ import net.jami.Constants 1.0
import "../../commoncomponents"
// TODO:
// - totalUnreadMessagesCount and pendingRequestCount could be
// properties of ConversationsAdapter
// - onCurrentTypeFilterChanged shouldn't need to update the smartlist
// - tabBarVisible could be factored out
TabBar {
id: tabBar
property int currentTypeFilter: ConversationsAdapter.currentTypeFilter
currentIndex: 0
enum TabIndex {
Conversations,
Requests
}
Connections {
target: ConversationsAdapter
function onCurrentTypeFilterChanged() {
pageOne.down = ConversationsAdapter.currentTypeFilter !== Profile.Type.PENDING
pageTwo.down = ConversationsAdapter.currentTypeFilter === Profile.Type.PENDING
setCurrentUidSmartListModelIndex()
forceReselectConversationSmartListCurrentIndex()
}
}
function selectTab(tabIndex) {
ConversationsAdapter.currentTypeFilter =
(tabIndex === SidePanelTabBar.Conversations) ?
@ -60,28 +47,25 @@ TabBar {
Profile.Type.PENDING
}
visible: tabBarVisible
currentIndex: 0
FilterTabButton {
id: pageOne
id: conversationsTabButton
down: currentTypeFilter !== Profile.Type.PENDING
tabBar: parent
down: true
labelText: JamiStrings.conversations
onSelected: selectTab(SidePanelTabBar.Conversations)
badgeCount: totalUnreadMessagesCount
badgeCount: ConversationsAdapter.totalUnreadMessageCount
acceleratorSequence: "Ctrl+L"
}
FilterTabButton {
id: pageTwo
id: requestsTabButton
down: !conversationsTabButton.down
tabBar: parent
labelText: JamiStrings.invitations
onSelected: selectTab(SidePanelTabBar.Requests)
badgeCount: pendingRequestCount
badgeCount: ConversationsAdapter.pendingRequestCount
acceleratorSequence: "Ctrl+R"
}
}

View file

@ -0,0 +1,184 @@
/*
* Copyright (C) 2020-2021 by Savoir-faire Linux
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import net.jami.Models 1.0
import net.jami.Adapters 1.0
import net.jami.Constants 1.0
import "../../commoncomponents"
ItemDelegate {
id: root
width: ListView.view.width
height: JamiTheme.smartListItemHeight
function convUid() {
return UID
}
Component.onCompleted: {
if (ContactType === Profile.Type.TEMPORARY)
root.ListView.view.model.updateContactAvatarUid(URI)
avatar.updateImage(URI, PictureUid)
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 15
anchors.rightMargin: 15
spacing: 10
AvatarImage {
id: avatar
Connections {
target: root.ListView.view.model
function onDataChanged(index) {
var model = root.ListView.view.model
avatar.updateImage(URI === undefined ?
model.data(index, ConversationList.URI):
URI,
PictureUid === undefined ?
model.data(index, ConversationList.PictureUid):
PictureUid)
}
}
Layout.preferredWidth: JamiTheme.smartListAvatarSize
Layout.preferredHeight: JamiTheme.smartListAvatarSize
mode: AvatarImage.Mode.FromContactUri
showPresenceIndicator: Presence === undefined ? false : Presence
transitionDuration: 0
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 0
// best name
Text {
Layout.fillWidth: true
Layout.preferredHeight: 20
Layout.alignment: Qt.AlignVCenter
elide: Text.ElideRight
text: BestName === undefined ? "" : BestName
font.pointSize: JamiTheme.smartlistItemFontSize
font.weight: UnreadMessagesCount ? Font.Bold : Font.Normal
color: JamiTheme.textColor
}
RowLayout {
visible: ContactType !== Profile.Type.TEMPORARY
&& LastInteractionDate !== undefined
Layout.fillWidth: true
Layout.preferredHeight: 20
Layout.alignment: Qt.AlignTop
// last Interaction date
Text {
Layout.alignment: Qt.AlignVCenter
text: LastInteractionDate === undefined ? "" : LastInteractionDate
font.pointSize: JamiTheme.smartlistItemInfoFontSize
font.weight: UnreadMessagesCount ? Font.DemiBold : Font.Normal
color: JamiTheme.textColor
}
// last Interaction
Text {
elide: Text.ElideRight
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
text: Draft ?
Draft :
(LastInteraction === undefined ? "" : LastInteraction)
font.pointSize: JamiTheme.smartlistItemInfoFontSize
font.weight: UnreadMessagesCount ? Font.Normal : Font.Light
font.hintingPreference: Font.PreferNoHinting
maximumLineCount: 1
color: JamiTheme.textColor
// deal with poor rendering of the pencil emoji on Windows
font.family: Qt.platform.os === "windows" && Draft ?
"Segoe UI Emoji" :
Qt.application.font.family
lineHeight: font.family === "Segoe UI Emoji" ? 1.25 : 1
}
}
}
ColumnLayout {
visible: InCall || UnreadMessagesCount
Layout.preferredWidth: childrenRect.width
Layout.fillHeight: true
spacing: 2
// call status
Text {
Layout.preferredHeight: 20
Layout.alignment: Qt.AlignRight
text: InCall ? UtilsAdapter.getCallStatusStr(CallState) : ""
font.pointSize: JamiTheme.smartlistItemInfoFontSize
font.weight: Font.Medium
color: JamiTheme.textColor
}
// unread message count
Item {
Layout.preferredWidth: childrenRect.width
Layout.preferredHeight: childrenRect.height
Layout.alignment: Qt.AlignTop | Qt.AlignRight
BadgeNotifier {
size: 20
count: UnreadMessagesCount
animate: index === 0
}
}
}
}
background: Rectangle {
color: {
if (root.pressed)
return Qt.darker(JamiTheme.selectedColor, 1.1)
else if (root.hovered)
return Qt.darker(JamiTheme.selectedColor, 1.05)
else
return "transparent"
}
}
onClicked: ListView.view.model.select(index)
onDoubleClicked: {
ListView.view.model.select(index)
if (AccountAdapter.currentAccountType === Profile.Type.SIP)
CallAdapter.placeAudioOnlyCall()
else
CallAdapter.placeCall()
// TODO: factor this out (visible should be observing)
communicationPageMessageWebView.setSendContactRequestButtonVisible(false)
}
onPressAndHold: ListView.view.openContextMenuAt(pressX, pressY, root)
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: root.ListView.view.openContextMenuAt(mouse.x, mouse.y, root)
}
}

View file

@ -153,8 +153,6 @@ MessagesAdapter::connectConversationModel()
Q_UNUSED(convUid);
removeInteraction(interactionId);
});
currentConversationModel->setFilter("");
}
void
@ -169,7 +167,7 @@ MessagesAdapter::sendContactRequest()
void
MessagesAdapter::updateConversationForAddedContact()
{
auto* convModel = lrcInstance_->getCurrentConversationModel();
auto convModel = lrcInstance_->getCurrentConversationModel();
const auto& convInfo = lrcInstance_->getConversationFromConvUid(
lrcInstance_->get_selectedConvUid());
@ -193,7 +191,6 @@ MessagesAdapter::slotSendMessageContentSaved(const QString& content)
auto restoredContent = lrcInstance_->getContentDraft(lrcInstance_->get_selectedConvUid(),
lrcInstance_->getCurrAccId());
setSendMessageContent(restoredContent);
Q_EMIT needToUpdateSmartList();
}
void
@ -202,7 +199,6 @@ MessagesAdapter::slotUpdateDraft(const QString& content)
if (!LastConvUid_.isEmpty()) {
lrcInstance_->setContentDraft(LastConvUid_, lrcInstance_->getCurrAccId(), content);
}
Q_EMIT needToUpdateSmartList();
}
void
@ -460,11 +456,9 @@ MessagesAdapter::setConversationProfileData(const lrc::api::conversation::Info&
try {
auto& contact = accInfo->contactModel->getContact(contactUri);
auto bestName = accInfo->contactModel->bestNameForContact(contactUri);
setInvitation(contact.profileInfo.type == lrc::api::profile::Type::PENDING
|| contact.profileInfo.type == lrc::api::profile::Type::TEMPORARY,
setInvitation(contact.profileInfo.type == lrc::api::profile::Type::PENDING,
bestName,
contactUri);
if (!contact.profileInfo.avatar.isEmpty()) {
setSenderImage(contactUri, contact.profileInfo.avatar);
} else {

View file

@ -92,7 +92,6 @@ protected:
void contactIsComposing(const QString& convUid, const QString& contactUri, bool isComposing);
Q_SIGNALS:
void needToUpdateSmartList();
void contactBanned();
void navigateToWelcomePageRequested();
void invitationAccepted();

View file

@ -27,6 +27,7 @@
#include "moderatorlistmodel.h"
#include "deviceitemlistmodel.h"
#include "smartlistmodel.h"
#include "conversationlistmodelbase.h"
#include "appsettingsmanager.h"
#include "distantrenderer.h"
@ -106,6 +107,10 @@ registerTypes()
QML_REGISTERTYPE(NS_MODELS, PluginListPreferenceModel);
QML_REGISTERTYPE(NS_MODELS, SmartListModel);
// Roles & type enums for models
QML_REGISTERNAMESPACE(NS_MODELS, ConversationList::staticMetaObject, "ConversationList");
QML_REGISTERNAMESPACE(NS_MODELS, ContactList::staticMetaObject, "ContactList");
// QQuickItems
QML_REGISTERTYPE(NS_MODELS, PreviewRenderer);
QML_REGISTERTYPE(NS_MODELS, VideoCallPreviewRenderer);

View file

@ -42,13 +42,12 @@ Q_CLASSINFO("RegisterEnumClassesUnscoped", "false")
QQmlEngine::setObjectOwnership(I, QQmlEngine::CppOwnership); \
{ using T = std::remove_reference<decltype(*I)>::type; \
qmlRegisterSingletonType<T>(NS, VER_MAJ, VER_MIN, N, \
[I](QQmlEngine*, QJSEngine*) -> QObject* { \
return I; }); }
[i=I](QQmlEngine*, QJSEngine*) -> QObject* { \
return i; }); }
#define QML_REGISTERSINGLETONTYPE_CUSTOM(NS, T, P) \
qmlRegisterSingletonType<T>(NS, VER_MAJ, VER_MIN, #T, \
[p=P](QQmlEngine* e, QJSEngine* se) -> QObject* { \
Q_UNUSED(e); Q_UNUSED(se); \
[p=P](QQmlEngine*, QJSEngine*) -> QObject* { \
return p; \
});
// clang-format on

View file

@ -0,0 +1,57 @@
/*
* Copyright (C) 2021 by Savoir-faire Linux
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "searchresultslistmodel.h"
SearchResultsListModel::SearchResultsListModel(LRCInstance* instance, QObject* parent)
: ConversationListModelBase(instance, parent)
{}
int
SearchResultsListModel::rowCount(const QModelIndex& parent) const
{
// For list models only the root node (an invalid parent) should return the list's size. For all
// other (valid) parents, rowCount() should return 0 so that it does not become a tree model.
if (!parent.isValid() && model_) {
return model_->getAllSearchResults().size();
}
return 0;
}
QVariant
SearchResultsListModel::data(const QModelIndex& index, int role) const
{
const auto& data = model_->getAllSearchResults();
if (!index.isValid() || data.empty())
return {};
return dataForItem(data.at(index.row()), role);
}
void
SearchResultsListModel::setFilter(const QString& filterString)
{
model_->setFilter(filterString);
}
void
SearchResultsListModel::onSearchResultsUpdated()
{
beginResetModel();
fillContactAvatarUidMap(lrcInstance_->getCurrentAccountInfo().contactModel->getAllContacts());
endResetModel();
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (C) 2021 by Savoir-faire Linux
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "conversationlistmodelbase.h"
#include "selectablelistproxymodel.h"
// A wrapper view model around ConversationModel's search result data
class SearchResultsListModel : public ConversationListModelBase
{
Q_OBJECT
public:
explicit SearchResultsListModel(LRCInstance* instance, QObject* parent = nullptr);
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
Q_INVOKABLE void setFilter(const QString& filterString);
public Q_SLOTS:
void onSearchResultsUpdated();
};
// The top level pre sorted and filtered model to be consumed by QML ListViews
class SearchResultsListProxyModel final : public SelectableListProxyModel
{
Q_OBJECT
public:
explicit SearchResultsListProxyModel(QAbstractListModel* model, QObject* parent = nullptr)
: SelectableListProxyModel(model, parent) {};
};

View file

@ -0,0 +1,167 @@
/*
* Copyright (C) 2021 by Savoir-faire Linux
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "selectablelistproxymodel.h"
SelectableListProxyModel::SelectableListProxyModel(QAbstractListModel* model, QObject* parent)
: QSortFilterProxyModel(parent)
, currentFilteredRow_(-1)
, selectedSourceIndex_(QModelIndex())
{
bindSourceModel(model);
}
void
SelectableListProxyModel::bindSourceModel(QAbstractListModel* model)
{
setSourceModel(model);
connect(sourceModel(),
&QAbstractListModel::dataChanged,
this,
&SelectableListProxyModel::updateSelection,
Qt::UniqueConnection);
connect(model,
&QAbstractListModel::rowsInserted,
this,
&SelectableListProxyModel::updateSelection,
Qt::UniqueConnection);
connect(model,
&QAbstractListModel::rowsRemoved,
this,
&SelectableListProxyModel::updateSelection,
Qt::UniqueConnection);
connect(sourceModel(),
&QAbstractListModel::modelReset,
this,
&SelectableListProxyModel::deselect,
Qt::UniqueConnection);
}
void
SelectableListProxyModel::setFilter(const QString& filterString)
{
setFilterFixedString(filterString);
updateSelection();
}
void
SelectableListProxyModel::select(const QModelIndex& index)
{
selectedSourceIndex_ = mapToSource(index);
updateSelection();
}
void
SelectableListProxyModel::select(int row)
{
select(index(row, 0));
}
void
SelectableListProxyModel::deselect()
{
selectedSourceIndex_ = QModelIndex();
currentFilteredRow_ = -1;
Q_EMIT currentFilteredRowChanged();
}
QVariant
SelectableListProxyModel::dataForRow(int row, int role) const
{
return data(index(row, 0), role);
}
void
SelectableListProxyModel::selectSourceRow(int row)
{
// note: the convId <-> index binding loop present
// is broken here
if (row == -1 || selectedSourceIndex_.row() == row)
return;
selectedSourceIndex_ = sourceModel()->index(row, 0);
updateSelection();
}
void
SelectableListProxyModel::updateContactAvatarUid(const QString& contactUri)
{
auto base = qobject_cast<ConversationListModelBase*>(sourceModel());
if (base)
base->updateContactAvatarUid(contactUri);
}
void
SelectableListProxyModel::updateSelection()
{
// if there has been no valid selection made, there is
// nothing to update
if (!selectedSourceIndex_.isValid() && currentFilteredRow_ == -1)
return;
auto lastFilteredRow = currentFilteredRow_;
auto filteredIndex = mapFromSource(selectedSourceIndex_);
// if the source model is empty, invalidate the selection
if (sourceModel()->rowCount() == 0) {
set_currentFilteredRow(-1);
Q_EMIT validSelectionChanged();
return;
}
// if the source and filtered index is no longer valid
// this would indicate that a mutation has occured,
// thus any arbritrary ux decision is okay here
if (!selectedSourceIndex_.isValid()) {
auto row = qMax(--currentFilteredRow_, 0);
selectedSourceIndex_ = mapToSource(index(row, 0));
filteredIndex = mapFromSource(selectedSourceIndex_);
currentFilteredRow_ = filteredIndex.row();
Q_EMIT currentFilteredRowChanged();
Q_EMIT validSelectionChanged();
return;
}
// update the row for ListView observers
set_currentFilteredRow(filteredIndex.row());
// finally, if the filter index is invalid, then we have
// probably just filtered out the selected item and don't
// want to force reselection of other ui components, as the
// source index is still valid, in that case, or if the
// row hasn't changed, don't notify
if (filteredIndex.isValid() && lastFilteredRow != currentFilteredRow_) {
Q_EMIT validSelectionChanged();
}
}
SelectableListProxyGroupModel::SelectableListProxyGroupModel(QList<SelectableListProxyModel*> models,
QObject* parent)
: QObject(parent)
, models_(models)
{
Q_FOREACH (auto* m, models_) {
connect(m, &SelectableListProxyModel::validSelectionChanged, [this, m] {
// deselct all other lists in the group
Q_FOREACH (auto* otherM, models_) {
if (m != otherM) {
otherM->deselect();
}
}
});
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright (C) 2021 by Savoir-faire Linux
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "conversationlistmodelbase.h"
#include <QSortFilterProxyModel>
// The base class for a filtered and sorted model.
// The model may be part of a group and if so, will track a
// mutually exclusive selection.
class SelectableListProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
QML_PROPERTY(int, currentFilteredRow)
public:
explicit SelectableListProxyModel(QAbstractListModel* model, QObject* parent = nullptr);
void bindSourceModel(QAbstractListModel* model);
Q_INVOKABLE void setFilter(const QString& filterString);
Q_INVOKABLE void select(const QModelIndex& index);
Q_INVOKABLE void select(int row);
Q_INVOKABLE void deselect();
Q_INVOKABLE QVariant dataForRow(int row, int role) const;
void selectSourceRow(int row);
// this may not be the best place for this but it prevents a level of
// inheritance and prevents code duplication
Q_INVOKABLE void updateContactAvatarUid(const QString& contactUri);
public Q_SLOTS:
void updateSelection();
Q_SIGNALS:
void validSelectionChanged();
private:
QPersistentModelIndex selectedSourceIndex_;
};
class SelectableListProxyGroupModel : public QObject
{
Q_OBJECT
public:
explicit SelectableListProxyGroupModel(QList<SelectableListProxyModel*> models,
QObject* parent = nullptr);
QList<SelectableListProxyModel*> models_;
};

View file

@ -232,7 +232,7 @@ ColumnLayout {
onClicked: {
ContactPickerCreation.createContactPickerObjects(
ContactPicker.ContactPickerType.CONVERSATION,
ContactList.CONVERSATION,
mainView)
ContactPickerCreation.openContactPicker()
}

View file

@ -113,7 +113,7 @@ Rectangle {
id: deleteAccountDialog
onAccepted: {
AccountAdapter.setSelectedConvId()
LRCInstance.deselectConversation()
if(UtilsAdapter.getAccountListSize() > 0) {
navigateToMainView()

View file

@ -1,4 +1,4 @@
/*!
/*
* Copyright (C) 2017-2020 by Savoir-faire Linux
* Author: Anthony Léonard <anthony.leonard@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
@ -34,17 +34,14 @@
SmartListModel::SmartListModel(QObject* parent,
SmartListModel::Type listModelType,
LRCInstance* instance)
: AbstractListModelBase(parent)
: ConversationListModelBase(instance, parent)
, listModelType_(listModelType)
{
lrcInstance_ = instance;
if (listModelType_ == Type::CONFERENCE) {
setConferenceableFilter();
}
}
SmartListModel::~SmartListModel() {}
int
SmartListModel::rowCount(const QModelIndex& parent) const
{
@ -70,102 +67,75 @@ SmartListModel::rowCount(const QModelIndex& parent) const
return 0;
}
int
SmartListModel::columnCount(const QModelIndex& parent) const
{
Q_UNUSED(parent);
return 1;
}
QVariant
SmartListModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid()) {
return QVariant();
}
if (!index.isValid())
return {};
try {
auto& currentAccountInfo = lrcInstance_->accountModel().getAccountInfo(
lrcInstance_->getCurrAccId());
auto& convModel = currentAccountInfo.conversationModel;
if (listModelType_ == Type::TRANSFER) {
switch (listModelType_) {
case Type::TRANSFER: {
try {
auto& currentAccountInfo = lrcInstance_->accountModel().getAccountInfo(
lrcInstance_->getCurrAccId());
auto& convModel = currentAccountInfo.conversationModel;
auto filterType = currentAccountInfo.profileInfo.type;
auto& item = convModel->getFilteredConversations(filterType).at(index.row());
return getConversationItemData(item, currentAccountInfo, role);
} else if (listModelType_ == Type::CONFERENCE) {
auto calls = conferenceables_[ConferenceableItem::CALL];
auto contacts = conferenceables_[ConferenceableItem::CONTACT];
QString itemConvUid {}, itemAccountId {};
if (calls.size() == 0) {
itemConvUid = contacts.at(index.row()).at(0).convId;
itemAccountId = contacts.at(index.row()).at(0).accountId;
} else {
bool callsOpen = sectionState_[tr("Calls")];
bool contactsOpen = sectionState_[tr("Contacts")];
auto callSectionEnd = callsOpen ? calls.size() + 1 : 1;
auto contactSectionEnd = contactsOpen ? callSectionEnd + contacts.size() + 1
: callSectionEnd + 1;
if (index.row() < callSectionEnd) {
if (index.row() == 0) {
return QVariant(role == Role::SectionName
? (callsOpen ? "" : "") + QString(tr("Calls"))
: "");
} else {
auto idx = index.row() - 1;
itemConvUid = calls.at(idx).at(0).convId;
itemAccountId = calls.at(idx).at(0).accountId;
}
} else if (index.row() < contactSectionEnd) {
if (index.row() == callSectionEnd) {
return QVariant(role == Role::SectionName
? (contactsOpen ? "" : "") + QString(tr("Contacts"))
: "");
} else {
auto idx = index.row() - (callSectionEnd + 1);
itemConvUid = contacts.at(idx).at(0).convId;
itemAccountId = contacts.at(idx).at(0).accountId;
}
return dataForItem(item, role);
} catch (const std::exception& e) {
qWarning() << e.what();
}
} break;
case Type::CONFERENCE: {
auto calls = conferenceables_[ConferenceableItem::CALL];
auto contacts = conferenceables_[ConferenceableItem::CONTACT];
QString itemConvUid {}, itemAccountId {};
if (calls.size() == 0) {
itemConvUid = contacts.at(index.row()).at(0).convId;
itemAccountId = contacts.at(index.row()).at(0).accountId;
} else {
bool callsOpen = sectionState_[tr("Calls")];
bool contactsOpen = sectionState_[tr("Contacts")];
auto callSectionEnd = callsOpen ? calls.size() + 1 : 1;
auto contactSectionEnd = contactsOpen ? callSectionEnd + contacts.size() + 1
: callSectionEnd + 1;
if (index.row() < callSectionEnd) {
if (index.row() == 0) {
return QVariant(role == Role::SectionName
? (callsOpen ? "" : "") + QString(tr("Calls"))
: "");
} else {
auto idx = index.row() - 1;
itemConvUid = calls.at(idx).at(0).convId;
itemAccountId = calls.at(idx).at(0).accountId;
}
} else if (index.row() < contactSectionEnd) {
if (index.row() == callSectionEnd) {
return QVariant(role == Role::SectionName
? (contactsOpen ? "" : "") + QString(tr("Contacts"))
: "");
} else {
auto idx = index.row() - (callSectionEnd + 1);
itemConvUid = contacts.at(idx).at(0).convId;
itemAccountId = contacts.at(idx).at(0).accountId;
}
}
if (role == Role::AccountId) {
return QVariant(itemAccountId);
}
auto& itemAccountInfo = lrcInstance_->accountModel().getAccountInfo(itemAccountId);
auto& item = lrcInstance_->getConversationFromConvUid(itemConvUid, itemAccountId);
return getConversationItemData(item, itemAccountInfo, role);
} else if (listModelType_ == Type::CONVERSATION) {
auto& item = conversations_.at(index.row());
return getConversationItemData(item, currentAccountInfo, role);
}
} catch (const std::exception& e) {
qWarning() << e.what();
}
return QVariant();
}
if (role == Role::AccountId) {
return QVariant(itemAccountId);
}
QHash<int, QByteArray>
SmartListModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[DisplayName] = "DisplayName";
roles[DisplayID] = "DisplayID";
roles[Presence] = "Presence";
roles[URI] = "URI";
roles[UnreadMessagesCount] = "UnreadMessagesCount";
roles[LastInteractionDate] = "LastInteractionDate";
roles[LastInteraction] = "LastInteraction";
roles[ContactType] = "ContactType";
roles[UID] = "UID";
roles[InCall] = "InCall";
roles[IsAudioOnly] = "IsAudioOnly";
roles[CallStackViewShouldShow] = "CallStackViewShouldShow";
roles[CallState] = "CallState";
roles[SectionName] = "SectionName";
roles[AccountId] = "AccountId";
roles[Draft] = "Draft";
roles[PictureUid] = "PictureUid";
return roles;
auto& item = lrcInstance_->getConversationFromConvUid(itemConvUid, itemAccountId);
return dataForItem(item, role);
} break;
case Type::CONVERSATION: {
auto& item = conversations_.at(index.row());
return dataForItem(item, role);
} break;
default:
break;
}
return {};
}
void
@ -194,39 +164,6 @@ SmartListModel::fillConversationsList()
endResetModel();
}
void
SmartListModel::updateContactAvatarUid(const QString& contactUri)
{
contactAvatarUidMap_[contactUri] = Utils::generateUid();
}
void
SmartListModel::fillContactAvatarUidMap(const ContactModel::ContactInfoMap& contacts)
{
if (contacts.size() == 0) {
contactAvatarUidMap_.clear();
return;
}
if (contactAvatarUidMap_.isEmpty() || contacts.size() != contactAvatarUidMap_.size()) {
bool useContacts = contacts.size() > contactAvatarUidMap_.size();
auto contactsKeyList = contacts.keys();
auto contactAvatarUidMapKeyList = contactAvatarUidMap_.keys();
for (int i = 0;
i < (useContacts ? contactsKeyList.size() : contactAvatarUidMapKeyList.size());
++i) {
// Insert or update
if (i < contactsKeyList.size() && !contactAvatarUidMap_.contains(contactsKeyList.at(i)))
contactAvatarUidMap_.insert(contactsKeyList.at(i), Utils::generateUid());
// Remove
if (i < contactAvatarUidMapKeyList.size()
&& !contacts.contains(contactAvatarUidMapKeyList.at(i)))
contactAvatarUidMap_.remove(contactAvatarUidMapKeyList.at(i));
}
}
}
void
SmartListModel::toggleSection(const QString& section)
{
@ -251,140 +188,6 @@ SmartListModel::currentUidSmartListModelIndex()
return -1;
}
QVariant
SmartListModel::getConversationItemData(const conversation::Info& item,
const account::Info& accountInfo,
int role) const
{
if (item.participants.size() <= 0) {
return QVariant();
}
auto& contactModel = accountInfo.contactModel;
// Since we are using image provider right now, image url representation should be unique to
// be able to use the image cache, account avatar will only be updated once PictureUid changed
switch (role) {
case Role::DisplayName: {
if (!item.participants.isEmpty())
return QVariant(contactModel->bestNameForContact(item.participants[0]));
return QVariant("");
}
case Role::DisplayID: {
if (!item.participants.isEmpty())
return QVariant(contactModel->bestIdForContact(item.participants[0]));
return QVariant("");
}
case Role::Presence: {
if (!item.participants.isEmpty()) {
auto& contact = contactModel->getContact(item.participants[0]);
return QVariant(contact.isPresent);
}
return QVariant(false);
}
case Role::PictureUid: {
if (!item.participants.isEmpty()) {
return QVariant(contactAvatarUidMap_[item.participants[0]]);
}
return QVariant("");
}
case Role::URI: {
if (!item.participants.isEmpty()) {
return QVariant(item.participants[0]);
}
return QVariant("");
}
case Role::UnreadMessagesCount:
return QVariant(item.unreadMessages);
case Role::LastInteractionDate: {
if (!item.interactions.empty()) {
auto& date = item.interactions.at(item.lastMessageUid).timestamp;
return QVariant(Utils::formatTimeString(date));
}
return QVariant("");
}
case Role::LastInteraction: {
if (!item.interactions.empty()) {
return QVariant(item.interactions.at(item.lastMessageUid).body);
}
return QVariant("");
}
case Role::LastInteractionType: {
if (!item.interactions.empty()) {
return QVariant(static_cast<int>(item.interactions.at(item.lastMessageUid).type));
}
return QVariant(0);
}
case Role::ContactType: {
if (!item.participants.isEmpty()) {
auto& contact = contactModel->getContact(item.participants[0]);
return QVariant(static_cast<int>(contact.profileInfo.type));
}
return QVariant(0);
}
case Role::UID:
return QVariant(item.uid);
case Role::InCall: {
const auto& convInfo = lrcInstance_->getConversationFromConvUid(item.uid);
if (!convInfo.uid.isEmpty()) {
auto* callModel = lrcInstance_->getCurrentCallModel();
return QVariant(callModel->hasCall(convInfo.callId));
}
return QVariant(false);
}
case Role::IsAudioOnly: {
const auto& convInfo = lrcInstance_->getConversationFromConvUid(item.uid);
if (!convInfo.uid.isEmpty()) {
auto* call = lrcInstance_->getCallInfoForConversation(convInfo);
if (call) {
return QVariant(call->isAudioOnly);
}
}
return QVariant();
}
case Role::CallStackViewShouldShow: {
const auto& convInfo = lrcInstance_->getConversationFromConvUid(item.uid);
if (!convInfo.uid.isEmpty() && !convInfo.callId.isEmpty()) {
auto* callModel = lrcInstance_->getCurrentCallModel();
const auto& call = callModel->getCall(convInfo.callId);
return QVariant(
callModel->hasCall(convInfo.callId)
&& ((!call.isOutgoing
&& (call.status == lrc::api::call::Status::IN_PROGRESS
|| call.status == lrc::api::call::Status::PAUSED
|| call.status == lrc::api::call::Status::INCOMING_RINGING))
|| (call.isOutgoing && call.status != lrc::api::call::Status::ENDED)));
}
return QVariant(false);
}
case Role::CallState: {
const auto& convInfo = lrcInstance_->getConversationFromConvUid(item.uid);
if (!convInfo.uid.isEmpty()) {
if (auto* call = lrcInstance_->getCallInfoForConversation(convInfo)) {
return QVariant(static_cast<int>(call->status));
}
}
return QVariant();
}
case Role::SectionName:
return QVariant(QString());
case Role::Draft: {
if (!item.uid.isEmpty()) {
const auto draft = lrcInstance_->getContentDraft(item.uid, accountInfo.id);
if (!draft.isEmpty()) {
/*
* Pencil Emoji
*/
uint cp = 0x270F;
auto emojiString = QString::fromUcs4(&cp, 1);
return emojiString + lrcInstance_->getContentDraft(item.uid, accountInfo.id);
}
}
return QVariant("");
}
}
return QVariant();
}
QModelIndex
SmartListModel::index(int row, int column, const QModelIndex& parent) const
{
@ -399,13 +202,6 @@ SmartListModel::index(int row, int column, const QModelIndex& parent) const
return QModelIndex();
}
QModelIndex
SmartListModel::parent(const QModelIndex& child) const
{
Q_UNUSED(child);
return QModelIndex();
}
Qt::ItemFlags
SmartListModel::flags(const QModelIndex& index) const
{

View file

@ -20,60 +20,32 @@
#pragma once
#include "abstractlistmodelbase.h"
#include "conversationlistmodelbase.h"
namespace ContactList {
Q_NAMESPACE
enum Type { CONVERSATION, CONFERENCE, TRANSFER, COUNT__ };
Q_ENUM_NS(Type)
} // namespace ContactList
using namespace lrc::api;
class LRCInstance;
class SmartListModel : public AbstractListModelBase
class SmartListModel : public ConversationListModelBase
{
Q_OBJECT
public:
using AccountInfo = lrc::api::account::Info;
using ConversationInfo = lrc::api::conversation::Info;
using ContactInfo = lrc::api::contact::Info;
enum class Type { CONVERSATION, CONFERENCE, TRANSFER, COUNT__ };
enum Role {
DisplayName = Qt::UserRole + 1,
DisplayID,
Presence,
URI,
UnreadMessagesCount,
LastInteractionDate,
LastInteraction,
LastInteractionType,
ContactType,
UID,
ContextMenuOpen,
InCall,
IsAudioOnly,
CallStackViewShouldShow,
CallState,
SectionName,
AccountId,
PictureUid,
Draft
};
Q_ENUM(Role)
using Type = ContactList::Type;
explicit SmartListModel(QObject* parent = nullptr,
SmartListModel::Type listModelType = Type::CONVERSATION,
Type listModelType = Type::CONVERSATION,
LRCInstance* instance = nullptr);
~SmartListModel();
/*
* QAbstractListModel.
*/
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
int columnCount(const QModelIndex& parent) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
QModelIndex index(int row,
int column = 0,
const QModelIndex& parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex& child) const override;
Qt::ItemFlags flags(const QModelIndex& index) const override;
Q_INVOKABLE void setConferenceableFilter(const QString& filter = {});
@ -81,28 +53,9 @@ public:
Q_INVOKABLE int currentUidSmartListModelIndex();
Q_INVOKABLE void fillConversationsList();
/*
* This function is to update contact avatar uuid for current account when there's an contact
* avatar changed.
*/
Q_INVOKABLE void updateContactAvatarUid(const QString& contactUri);
private:
QVariant getConversationItemData(const ConversationInfo& item,
const AccountInfo& accountInfo,
int role) const;
/*
* Give a uuid for each contact avatar for current account and it will serve PictureUid role
*/
void fillContactAvatarUidMap(const ContactModel::ContactInfoMap& contacts);
/*
* List sectioning.
*/
Type listModelType_;
QMap<QString, bool> sectionState_;
QMap<ConferenceableItem, ConferenceableValue> conferenceables_;
QMap<QString, QString> contactAvatarUidMap_;
ConversationModel::ConversationQueueProxy conversations_;
};

View file

@ -501,9 +501,9 @@ Utils::formatTimeString(const std::time_t& timeStamp)
{
auto currentTimeStamp = QDateTime::fromSecsSinceEpoch(timeStamp);
auto now = QDateTime::currentDateTime();
auto timeStampDMY = currentTimeStamp.toString("dd/MM/yyyy");
if (timeStampDMY == now.toString("dd/MM/yyyy")) {
return currentTimeStamp.toString("hh:mm");
auto timeStampDMY = currentTimeStamp.toString("dd/MM/yy");
if (timeStampDMY == now.toString("dd/MM/yy")) {
return currentTimeStamp.toString("hhmm");
} else {
return timeStampDMY;
}

View file

@ -144,29 +144,6 @@ UtilsAdapter::getBestId(const QString& accountId, const QString& uid)
return QString();
}
int
UtilsAdapter::getTotalUnreadMessages()
{
int totalUnreadMessages {0};
if (lrcInstance_->getCurrentAccountInfo().profileInfo.type != lrc::api::profile::Type::SIP) {
auto* convModel = lrcInstance_->getCurrentConversationModel();
auto ringConversations = convModel->getFilteredConversations(lrc::api::profile::Type::RING,
false);
ringConversations.for_each(
[&totalUnreadMessages](const lrc::api::conversation::Info& conversation) {
totalUnreadMessages += conversation.unreadMessages;
});
}
return totalUnreadMessages;
}
int
UtilsAdapter::getTotalPendingRequest()
{
auto& accountInfo = lrcInstance_->getCurrentAccountInfo();
return accountInfo.contactModel->pendingRequestCount();
}
void
UtilsAdapter::setConversationFilter(const QString& filter)
{

View file

@ -49,8 +49,6 @@ public:
Q_INVOKABLE QString GetRingtonePath();
Q_INVOKABLE bool checkStartupLink();
Q_INVOKABLE void setConversationFilter(const QString& filter);
Q_INVOKABLE int getTotalUnreadMessages();
Q_INVOKABLE int getTotalPendingRequest();
Q_INVOKABLE const QString getBestName(const QString& accountId, const QString& uid);
Q_INVOKABLE const QString getPeerUri(const QString& accountId, const QString& uid);
Q_INVOKABLE QString getBestId(const QString& accountId);