diff --git a/CMakeLists.txt b/CMakeLists.txt
index 77dc4ea3..a678b4eb 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -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
diff --git a/qml.qrc b/qml.qrc
index b2782a70..15cbfbd1 100644
--- a/qml.qrc
+++ b/qml.qrc
@@ -102,7 +102,6 @@
src/mainview/components/MessageWebView.qml
src/mainview/components/MessageWebViewHeader.qml
src/mainview/components/AccountComboBox.qml
- src/mainview/components/ConversationSmartListView.qml
src/mainview/components/CallStackView.qml
src/mainview/components/IncomingCallPage.qml
src/mainview/components/OutgoingCallPage.qml
@@ -114,7 +113,6 @@
src/mainview/components/ParticipantOverlay.qml
src/mainview/components/ProjectCreditsScrollView.qml
src/mainview/components/AccountComboBoxPopup.qml
- src/mainview/components/ConversationSmartListViewItemDelegate.qml
src/mainview/components/SidePanelTabBar.qml
src/mainview/components/WelcomePageQrDialog.qml
src/mainview/components/ConversationSmartListContextMenu.qml
@@ -138,5 +136,8 @@
src/mainview/js/pluginhandlerpickercreation.js
src/mainview/components/FilterTabButton.qml
src/mainview/components/AccountItemDelegate.qml
+ src/mainview/components/ConversationListView.qml
+ src/mainview/components/SmartListItemDelegate.qml
+ src/mainview/components/BadgeNotifier.qml
diff --git a/src/accountadapter.cpp b/src/accountadapter.cpp
index eefb8c6b..e29cc255 100644
--- a/src/accountadapter.cpp
+++ b/src/accountadapter.cpp
@@ -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;
}
diff --git a/src/accountadapter.h b/src/accountadapter.h
index 5e11d050..985781ab 100644
--- a/src/accountadapter.h
+++ b/src/accountadapter.h
@@ -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_;
diff --git a/src/calladapter.cpp b/src/calladapter.cpp
index 58598b72..1be1bec5 100644
--- a/src/calladapter.cpp
+++ b/src/calladapter.cpp
@@ -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(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);
}
}
diff --git a/src/calladapter.h b/src/calladapter.h
index a93fafa1..efb9ddcc 100644
--- a/src/calladapter.h
+++ b/src/calladapter.h
@@ -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
diff --git a/src/commoncomponents/AvatarImage.qml b/src/commoncomponents/AvatarImage.qml
index 06a0edd2..96acf5eb 100644
--- a/src/commoncomponents/AvatarImage.qml
+++ b/src/commoncomponents/AvatarImage.qml
@@ -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
diff --git a/src/commoncomponents/BaseContextMenu.qml b/src/commoncomponents/BaseContextMenu.qml
index da0b9639..636fc74e 100644
--- a/src/commoncomponents/BaseContextMenu.qml
+++ b/src/commoncomponents/BaseContextMenu.qml
@@ -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
+ }
}
}
diff --git a/src/commoncomponents/ModalPopup.qml b/src/commoncomponents/ModalPopup.qml
index 95a3ae48..83e7d820 100644
--- a/src/commoncomponents/ModalPopup.qml
+++ b/src/commoncomponents/ModalPopup.qml
@@ -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
diff --git a/src/commoncomponents/PresenceIndicator.qml b/src/commoncomponents/PresenceIndicator.qml
index d7baf769..5ee5825a 100644
--- a/src/commoncomponents/PresenceIndicator.qml
+++ b/src/commoncomponents/PresenceIndicator.qml
@@ -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
diff --git a/src/commoncomponents/Scaffold.qml b/src/commoncomponents/Scaffold.qml
index 9acb5a09..4e76288c 100644
--- a/src/commoncomponents/Scaffold.qml
+++ b/src/commoncomponents/Scaffold.qml
@@ -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()
+ }
}
diff --git a/src/constant/JamiStrings.qml b/src/constant/JamiStrings.qml
index f1b7c5d8..fdf9b441 100644
--- a/src/constant/JamiStrings.qml
+++ b/src/constant/JamiStrings.qml
@@ -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")
diff --git a/src/constant/JamiTheme.qml b/src/constant/JamiTheme.qml
index d7b5db5f..43df8965 100644
--- a/src/constant/JamiTheme.qml
+++ b/src/constant/JamiTheme.qml
@@ -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
diff --git a/src/contactadapter.cpp b/src/contactadapter.cpp
index d35cc64f..b4b8a439 100644
--- a/src/contactadapter.cpp
+++ b/src/contactadapter.cpp
@@ -1,4 +1,4 @@
-/*!
+/*
* Copyright (C) 2020 by Savoir-faire Linux
* Author: Edric Ladent Milaret
* Author: Andreas Traczyk
@@ -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();
+ const auto sectionName = contactIndex.data(Role::SectionName).value();
if (!sectionName.isEmpty()) {
smartListModel_->toggleSection(sectionName);
return;
}
- const auto convUid = contactIndex.data(SmartListModel::Role::UID).value();
- const auto accId = contactIndex.data(SmartListModel::Role::AccountId).value();
+ const auto convUid = contactIndex.data(Role::UID).value();
+ const auto accId = contactIndex.data(Role::AccountId).value();
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();
+ const auto contactUri = contactIndex.data(Role::URI).value();
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();
+ const auto contactUri = contactIndex.data(Role::URI).value();
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();
+ const auto contactUri = contactIndex.data(Role::URI).value();
if (contactUri.isEmpty()) {
return;
}
diff --git a/src/contactadapter.h b/src/contactadapter.h
index f5f30ea6..bb958c00 100644
--- a/src/contactadapter.h
+++ b/src/contactadapter.h
@@ -1,4 +1,4 @@
-/*!
+/*
* Copyright (C) 2020 by Savoir-faire Linux
* Author: Mingrui Zhang
*
@@ -20,6 +20,7 @@
#include "qmladapterbase.h"
#include "smartlistmodel.h"
+#include "conversationlistmodel.h"
#include
#include
@@ -38,30 +39,42 @@ class LRCInstance;
*/
class SelectableProxyModel final : public QSortFilterProxyModel
{
+ Q_OBJECT
+
public:
using FilterPredicate = std::function;
- 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 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_;
- std::unique_ptr selectableProxyModel_;
+ QScopedPointer smartListModel_;
+ QScopedPointer selectableProxyModel_;
QStringList defaultModerators_;
diff --git a/src/conversationlistmodel.cpp b/src/conversationlistmodel.cpp
new file mode 100644
index 00000000..cd3fd035
--- /dev/null
+++ b/src/conversationlistmodel.cpp
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2021 by Savoir-faire Linux
+ * Author: Andreas Traczyk
+ * Author: Mingrui Zhang
+ *
+ * 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 .
+ */
+
+#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 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(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();
+};
diff --git a/src/conversationlistmodel.h b/src/conversationlistmodel.h
new file mode 100644
index 00000000..3e143d74
--- /dev/null
+++ b/src/conversationlistmodel.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2021 by Savoir-faire Linux
+ * Author: Andreas Traczyk
+ * Author: Mingrui Zhang
+ *
+ * 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 .
+ */
+
+#pragma once
+
+#include "conversationlistmodelbase.h"
+#include "selectablelistproxymodel.h"
+
+#include "api/profile.h"
+
+#include
+
+// 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_;
+};
diff --git a/src/conversationlistmodelbase.cpp b/src/conversationlistmodelbase.cpp
new file mode 100644
index 00000000..f87f5c9d
--- /dev/null
+++ b/src/conversationlistmodelbase.cpp
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2020-2021 by Savoir-faire Linux
+ * Author: Andreas Traczyk
+ * Author: Mingrui Zhang
+ *
+ * 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 .
+ */
+
+#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
+ConversationListModelBase::roleNames() const
+{
+ using namespace ConversationList;
+ QHash 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(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(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(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));
+ }
+ }
+}
diff --git a/src/conversationlistmodelbase.h b/src/conversationlistmodelbase.h
new file mode 100644
index 00000000..fe99fdd0
--- /dev/null
+++ b/src/conversationlistmodelbase.h
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2020-2021 by Savoir-faire Linux
+ * Author: Andreas Traczyk
+ * Author: Mingrui Zhang
+ *
+ * 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 .
+ */
+
+#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 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 contactAvatarUidMap_;
+};
diff --git a/src/conversationsadapter.cpp b/src/conversationsadapter.cpp
index 6b7b76ba..ade05c59 100644
--- a/src/conversationsadapter.cpp
+++ b/src/conversationsadapter.cpp
@@ -1,11 +1,7 @@
-/*!
+/*
* Copyright (C) 2020 by Savoir-faire Linux
- * Author: Edric Ladent Milaret
- * Author: Anthony Léonard
- * Author: Olivier Soldano
- * Author: Andreas Traczyk
- * Author: Isa Nanic
* Author: Mingrui Zhang
+ * Author: Andreas Traczyk
*
* 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
+#include
+
+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(contact.profileInfo.type)},
+ {"isAudioOnly", isAudioOnly},
+ {"callState", static_cast(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;
diff --git a/src/conversationsadapter.h b/src/conversationsadapter.h
index 32de1980..11501ab6 100644
--- a/src/conversationsadapter.h
+++ b/src/conversationsadapter.h
@@ -1,6 +1,7 @@
-/*!
+/*
* Copyright (C) 2020 by Savoir-faire Linux
* Author: Mingrui Zhang
+ * Author: Andreas Traczyk
*
* 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
#include
@@ -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 convSrcModel_;
+ QScopedPointer convModel_;
+ QScopedPointer searchSrcModel_;
+ QScopedPointer searchModel_;
};
diff --git a/src/lrcinstance.cpp b/src/lrcinstance.cpp
index 738701f3..facb7be4 100644
--- a/src/lrcinstance.cpp
+++ b/src/lrcinstance.cpp
@@ -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
diff --git a/src/lrcinstance.h b/src/lrcinstance.h
index 71119750..e4cbe6e7 100644
--- a/src/lrcinstance.h
+++ b/src/lrcinstance.h
@@ -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_;
std::unique_ptr renderer_;
std::unique_ptr updateManager_;
- QString selectedAccountId_ {""};
+ QString selectedAccountId_ {};
MapStringString contentDrafts_;
MapStringString lastConferences_;
diff --git a/src/mainapplication.h b/src/mainapplication.h
index 881155d5..58c8a67f 100644
--- a/src/mainapplication.h
+++ b/src/mainapplication.h
@@ -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_;
};
diff --git a/src/mainview/MainView.qml b/src/mainview/MainView.qml
index b1669e49..1c565dbb 100644
--- a/src/mainview/MainView.qml
+++ b/src/mainview/MainView.qml
@@ -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()
}
}
diff --git a/src/mainview/components/AccountComboBox.qml b/src/mainview/components/AccountComboBox.qml
index 80df7b93..cbcd7f1d 100644
--- a/src/mainview/components/AccountComboBox.qml
+++ b/src/mainview/components/AccountComboBox.qml
@@ -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()
diff --git a/src/mainview/components/AccountComboBoxPopup.qml b/src/mainview/components/AccountComboBoxPopup.qml
index 4db441bc..3b0128d4 100644
--- a/src/mainview/components/AccountComboBoxPopup.qml
+++ b/src/mainview/components/AccountComboBoxPopup.qml
@@ -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
}
}
}
diff --git a/src/mainview/components/BadgeNotifier.qml b/src/mainview/components/BadgeNotifier.qml
new file mode 100644
index 00000000..90aa1432
--- /dev/null
+++ b/src/mainview/components/BadgeNotifier.qml
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2021 by Savoir-faire Linux
+ * Author: Mingrui Zhang
+ * Author: Andreas Traczyk
+ *
+ * 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 .
+ */
+
+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
+ }
+ }
+}
diff --git a/src/mainview/components/CallOverlay.qml b/src/mainview/components/CallOverlay.qml
index b0c611e9..1e03f290 100644
--- a/src/mainview/components/CallOverlay.qml
+++ b/src/mainview/components/CallOverlay.qml
@@ -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()
}
diff --git a/src/mainview/components/ContactPicker.qml b/src/mainview/components/ContactPicker.qml
index c2165b59..24110c82 100644
--- a/src/mainview/components/ContactPicker.qml
+++ b/src/mainview/components/ContactPicker.qml
@@ -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")
diff --git a/src/mainview/components/ContactPickerItemDelegate.qml b/src/mainview/components/ContactPickerItemDelegate.qml
index bd3aa237..d71bd2bc 100644
--- a/src/mainview/components/ContactPickerItemDelegate.qml
+++ b/src/mainview/components/ContactPickerItemDelegate.qml
@@ -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
diff --git a/src/mainview/components/ContactSearchBar.qml b/src/mainview/components/ContactSearchBar.qml
index 6cdc25a6..1f9dbc75 100644
--- a/src/mainview/components/ContactSearchBar.qml
+++ b/src/mainview/components/ContactSearchBar.qml
@@ -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 {
diff --git a/src/mainview/components/ConversationListView.qml b/src/mainview/components/ConversationListView.qml
new file mode 100644
index 00000000..48981cd5
--- /dev/null
+++ b/src/mainview/components/ConversationListView.qml
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2021 by Savoir-faire Linux
+ * Author: Mingrui Zhang
+ * Author: Andreas Traczyk
+ *
+ * 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 .
+ */
+
+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)
+ }
+ }
+}
diff --git a/src/mainview/components/ConversationSmartListContextMenu.qml b/src/mainview/components/ConversationSmartListContextMenu.qml
index ab4200d0..c4a3bce5 100644
--- a/src/mainview/components/ConversationSmartListContextMenu.qml
+++ b/src/mainview/components/ConversationSmartListContextMenu.qml
@@ -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()
diff --git a/src/mainview/components/ConversationSmartListView.qml b/src/mainview/components/ConversationSmartListView.qml
deleted file mode 100644
index a4f4dda5..00000000
--- a/src/mainview/components/ConversationSmartListView.qml
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright (C) 2020 by Savoir-faire Linux
- * Author: Mingrui Zhang
- * Author: Andreas Traczyk
- *
- * 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 .
- */
-
-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
- }
- }
-}
diff --git a/src/mainview/components/ConversationSmartListViewItemDelegate.qml b/src/mainview/components/ConversationSmartListViewItemDelegate.qml
deleted file mode 100644
index cc4ca6d4..00000000
--- a/src/mainview/components/ConversationSmartListViewItemDelegate.qml
+++ /dev/null
@@ -1,269 +0,0 @@
-/*
- * Copyright (C) 2020 by Savoir-faire Linux
- * Author: Mingrui Zhang
- *
- * 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 .
- */
-
-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
- })
- }
- }
- }
- }
-}
diff --git a/src/mainview/components/FilterTabButton.qml b/src/mainview/components/FilterTabButton.qml
index 5f70ac66..19021fd3 100644
--- a/src/mainview/components/FilterTabButton.qml
+++ b/src/mainview/components/FilterTabButton.qml
@@ -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 {
diff --git a/src/mainview/components/MessageWebView.qml b/src/mainview/components/MessageWebView.qml
index f2bad1d5..442d6abd 100644
--- a/src/mainview/components/MessageWebView.qml
+++ b/src/mainview/components/MessageWebView.qml
@@ -108,6 +108,14 @@ Rectangle {
}
}
+ Connections {
+ target: AccountAdapter
+
+ function onSelectedContactAdded(convId) {
+ MessagesAdapter.updateConversationForAddedContact()
+ }
+ }
+
JamiFileDialog {
id: jamiFileDialog
diff --git a/src/mainview/components/SidePanel.qml b/src/mainview/components/SidePanel.qml
index fde7cc28..d86ba5cb 100644
--- a/src/mainview/components/SidePanel.qml
+++ b/src/mainview/components/SidePanel.qml
@@ -1,6 +1,7 @@
/*
- * Copyright (C) 2020 by Savoir-faire Linux
+ * Copyright (C) 2020-2021 by Savoir-faire Linux
* Author: Mingrui Zhang
+ * Author: Andreas Traczyk
*
* 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
}
}
}
diff --git a/src/mainview/components/SidePanelTabBar.qml b/src/mainview/components/SidePanelTabBar.qml
index cd5f51ab..04cd82dd 100644
--- a/src/mainview/components/SidePanelTabBar.qml
+++ b/src/mainview/components/SidePanelTabBar.qml
@@ -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"
}
}
diff --git a/src/mainview/components/SmartListItemDelegate.qml b/src/mainview/components/SmartListItemDelegate.qml
new file mode 100644
index 00000000..fcabcf49
--- /dev/null
+++ b/src/mainview/components/SmartListItemDelegate.qml
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2020-2021 by Savoir-faire Linux
+ * Author: Mingrui Zhang
+ * Author: Andreas Traczyk
+ *
+ * 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 .
+ */
+
+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)
+ }
+}
diff --git a/src/messagesadapter.cpp b/src/messagesadapter.cpp
index 829b89a6..ecc4a90b 100644
--- a/src/messagesadapter.cpp
+++ b/src/messagesadapter.cpp
@@ -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 {
diff --git a/src/messagesadapter.h b/src/messagesadapter.h
index 8fdcbf85..350ad1dd 100644
--- a/src/messagesadapter.h
+++ b/src/messagesadapter.h
@@ -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();
diff --git a/src/qmlregister.cpp b/src/qmlregister.cpp
index b5bb3390..862ae16b 100644
--- a/src/qmlregister.cpp
+++ b/src/qmlregister.cpp
@@ -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);
diff --git a/src/qmlregister.h b/src/qmlregister.h
index 896a2dc2..b2a86842 100644
--- a/src/qmlregister.h
+++ b/src/qmlregister.h
@@ -42,13 +42,12 @@ Q_CLASSINFO("RegisterEnumClassesUnscoped", "false")
QQmlEngine::setObjectOwnership(I, QQmlEngine::CppOwnership); \
{ using T = std::remove_reference::type; \
qmlRegisterSingletonType(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(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
diff --git a/src/searchresultslistmodel.cpp b/src/searchresultslistmodel.cpp
new file mode 100644
index 00000000..8329524a
--- /dev/null
+++ b/src/searchresultslistmodel.cpp
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 by Savoir-faire Linux
+ * Author: Andreas Traczyk
+ *
+ * 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 .
+ */
+
+#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();
+}
diff --git a/src/searchresultslistmodel.h b/src/searchresultslistmodel.h
new file mode 100644
index 00000000..372302b3
--- /dev/null
+++ b/src/searchresultslistmodel.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2021 by Savoir-faire Linux
+ * Author: Andreas Traczyk
+ *
+ * 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 .
+ */
+
+#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) {};
+};
diff --git a/src/selectablelistproxymodel.cpp b/src/selectablelistproxymodel.cpp
new file mode 100644
index 00000000..b0bfab1d
--- /dev/null
+++ b/src/selectablelistproxymodel.cpp
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2021 by Savoir-faire Linux
+ * Author: Andreas Traczyk
+ *
+ * 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 .
+ */
+
+#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(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 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();
+ }
+ }
+ });
+ }
+}
diff --git a/src/selectablelistproxymodel.h b/src/selectablelistproxymodel.h
new file mode 100644
index 00000000..af5b529f
--- /dev/null
+++ b/src/selectablelistproxymodel.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2021 by Savoir-faire Linux
+ * Author: Andreas Traczyk
+ *
+ * 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 .
+ */
+
+#pragma once
+
+#include "conversationlistmodelbase.h"
+
+#include
+
+// 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 models,
+ QObject* parent = nullptr);
+ QList models_;
+};
diff --git a/src/settingsview/components/AdvancedCallSettings.qml b/src/settingsview/components/AdvancedCallSettings.qml
index fb567b14..239421ff 100644
--- a/src/settingsview/components/AdvancedCallSettings.qml
+++ b/src/settingsview/components/AdvancedCallSettings.qml
@@ -232,7 +232,7 @@ ColumnLayout {
onClicked: {
ContactPickerCreation.createContactPickerObjects(
- ContactPicker.ContactPickerType.CONVERSATION,
+ ContactList.CONVERSATION,
mainView)
ContactPickerCreation.openContactPicker()
}
diff --git a/src/settingsview/components/CurrentAccountSettings.qml b/src/settingsview/components/CurrentAccountSettings.qml
index 457814ad..144efab6 100644
--- a/src/settingsview/components/CurrentAccountSettings.qml
+++ b/src/settingsview/components/CurrentAccountSettings.qml
@@ -113,7 +113,7 @@ Rectangle {
id: deleteAccountDialog
onAccepted: {
- AccountAdapter.setSelectedConvId()
+ LRCInstance.deselectConversation()
if(UtilsAdapter.getAccountListSize() > 0) {
navigateToMainView()
diff --git a/src/smartlistmodel.cpp b/src/smartlistmodel.cpp
index 6269f43d..805218ff 100644
--- a/src/smartlistmodel.cpp
+++ b/src/smartlistmodel.cpp
@@ -1,4 +1,4 @@
-/*!
+/*
* Copyright (C) 2017-2020 by Savoir-faire Linux
* Author: Anthony Léonard
* Author: Andreas Traczyk
@@ -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
-SmartListModel::roleNames() const
-{
- QHash 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(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(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(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
{
diff --git a/src/smartlistmodel.h b/src/smartlistmodel.h
index 708c7455..f35a9e8d 100644
--- a/src/smartlistmodel.h
+++ b/src/smartlistmodel.h
@@ -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 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 sectionState_;
QMap conferenceables_;
- QMap contactAvatarUidMap_;
ConversationModel::ConversationQueueProxy conversations_;
};
diff --git a/src/utils.cpp b/src/utils.cpp
index 7c4c7d4e..69434abf 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -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;
}
diff --git a/src/utilsadapter.cpp b/src/utilsadapter.cpp
index 84d900c5..75b78cc7 100644
--- a/src/utilsadapter.cpp
+++ b/src/utilsadapter.cpp
@@ -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)
{
diff --git a/src/utilsadapter.h b/src/utilsadapter.h
index 50909b90..6fd23d37 100644
--- a/src/utilsadapter.h
+++ b/src/utilsadapter.h
@@ -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);