1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-09-10 12:03:18 +02:00

sidepanel: improve smartlist interface with underlying models

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

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

View file

@ -67,7 +67,11 @@ set(COMMON_SOURCES
${SRC_DIR}/screensaver.cpp ${SRC_DIR}/screensaver.cpp
${SRC_DIR}/systemtray.cpp ${SRC_DIR}/systemtray.cpp
${SRC_DIR}/appsettingsmanager.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 set(COMMON_HEADERS
${SRC_DIR}/avatarimageprovider.h ${SRC_DIR}/avatarimageprovider.h
@ -118,7 +122,11 @@ set(COMMON_HEADERS
${SRC_DIR}/screensaver.h ${SRC_DIR}/screensaver.h
${SRC_DIR}/systemtray.h ${SRC_DIR}/systemtray.h
${SRC_DIR}/appsettingsmanager.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 set(QML_LIBS
Qt5::Quick Qt5::Quick

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -38,6 +38,7 @@ Item {
property alias fillMode: rootImage.fillMode property alias fillMode: rootImage.fillMode
property alias sourceSize: rootImage.sourceSize property alias sourceSize: rootImage.sourceSize
property int transitionDuration: 150
property bool saveToConfig: false property bool saveToConfig: false
property int mode: AvatarImage.Mode.FromAccount property int mode: AvatarImage.Mode.FromAccount
property string imageProviderIdPrefix: { property string imageProviderIdPrefix: {
@ -178,7 +179,7 @@ Item {
NumberAnimation { NumberAnimation {
properties: "opacity" properties: "opacity"
easing.type: Easing.InOutQuad easing.type: Easing.InOutQuad
duration: 400 duration: transitionDuration
} }
} }
} }
@ -188,38 +189,15 @@ Item {
id: presenceIndicator id: presenceIndicator
anchors.right: root.right anchors.right: root.right
anchors.rightMargin: -1
anchors.bottom: root.bottom anchors.bottom: root.bottom
anchors.bottomMargin: -1
size: root.width * 0.3 size: root.width * 0.26
visible: showPresenceIndicator 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 { Connections {
target: ScreenInfo target: ScreenInfo

View file

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

View file

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

View file

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

View file

@ -23,6 +23,12 @@ import QtQuick.Controls 2.12
Rectangle { Rectangle {
property alias name: label.text property alias name: label.text
property bool stretchParent: false 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 border.width: 1
color: { color: {
@ -33,6 +39,19 @@ Rectangle {
} }
anchors.fill: parent anchors.fill: parent
focus: false 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: { Component.onCompleted: {
// fallback to some description of the object // fallback to some description of the object
if (label.text === "") 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 { Label {
id: label id: label
anchors.centerIn: parent anchors.centerIn: parent
} }
MouseArea {
anchors.fill: parent
onPressed: parent.forceActiveFocus()
}
} }

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
/*! /*
* Copyright (C) 2020 by Savoir-faire Linux * Copyright (C) 2020 by Savoir-faire Linux
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
* *
@ -20,6 +20,7 @@
#include "qmladapterbase.h" #include "qmladapterbase.h"
#include "smartlistmodel.h" #include "smartlistmodel.h"
#include "conversationlistmodel.h"
#include <QObject> #include <QObject>
#include <QSortFilterProxyModel> #include <QSortFilterProxyModel>
@ -38,30 +39,42 @@ class LRCInstance;
*/ */
class SelectableProxyModel final : public QSortFilterProxyModel class SelectableProxyModel final : public QSortFilterProxyModel
{ {
Q_OBJECT
public: public:
using FilterPredicate = std::function<bool(const QModelIndex&, const QRegExp&)>; using FilterPredicate = std::function<bool(const QModelIndex&, const QRegExp&)>;
explicit SelectableProxyModel(QAbstractItemModel* parent) explicit SelectableProxyModel(QAbstractListModel* parent = nullptr)
: QSortFilterProxyModel(parent) : QSortFilterProxyModel(parent)
{ {
setSourceModel(parent); setSourceModel(parent);
setSortRole(ConversationList::Role::LastInteractionTimeStamp);
sort(0, Qt::DescendingOrder);
setFilterCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive);
} }
~SelectableProxyModel() {}
void setPredicate(FilterPredicate filterPredicate) void setPredicate(FilterPredicate filterPredicate)
{ {
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. // 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; 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: private:
std::function<bool(const QModelIndex&, const QRegExp&)> filterPredicate_; FilterPredicate filterPredicate_;
}; };
class ContactAdapter final : public QmlAdapterBase class ContactAdapter final : public QmlAdapterBase
@ -73,6 +86,8 @@ public:
~ContactAdapter() = default; ~ContactAdapter() = default;
protected: protected:
using Role = ConversationList::Role;
void safeInit() override {}; void safeInit() override {};
Q_INVOKABLE QVariant getContactSelectableModel(int type); Q_INVOKABLE QVariant getContactSelectableModel(int type);
@ -81,10 +96,8 @@ protected:
private: private:
SmartListModel::Type listModeltype_; SmartListModel::Type listModeltype_;
QScopedPointer<SmartListModel> smartListModel_;
// SmartListModel is the source model of SelectableProxyModel. QScopedPointer<SelectableProxyModel> selectableProxyModel_;
std::unique_ptr<SmartListModel> smartListModel_;
std::unique_ptr<SelectableProxyModel> selectableProxyModel_;
QStringList defaultModerators_; QStringList defaultModerators_;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -219,6 +219,12 @@ LRCInstance::getCurrentCallModel()
return getCurrentAccountInfo().callModel.get(); return getCurrentAccountInfo().callModel.get();
} }
ContactModel*
LRCInstance::getCurrentContactModel()
{
return getCurrentAccountInfo().contactModel.get();
}
const QString& const QString&
LRCInstance::getCurrAccId() LRCInstance::getCurrAccId()
{ {
@ -301,6 +307,18 @@ LRCInstance::getCurrAccConfig()
return getCurrentAccountInfo().confProperties; 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 void
LRCInstance::subscribeToDebugReceived() LRCInstance::subscribeToDebugReceived()
{ {
@ -353,6 +371,8 @@ LRCInstance::setContentDraft(const QString& convUid,
{ {
auto draftKey = accountId + "_" + convUid; auto draftKey = accountId + "_" + convUid;
contentDrafts_[draftKey] = content; contentDrafts_[draftKey] = content;
// this signal is only needed to update the current smartlist
Q_EMIT draftSaved(convUid);
} }
void void
@ -374,41 +394,24 @@ LRCInstance::poplastConference(const QString& confId)
} }
void void
LRCInstance::selectConversation(const QString& accountId, const QString& convUid) LRCInstance::selectConversation(const QString& convId, const QString& accountId)
{ {
const auto& convInfo = getConversationFromConvUid(convUid, accountId); // if the account is not currently selected, do that first, then
// proceed to select the conversation
if (get_selectedConvUid() != convInfo.uid || convInfo.participants.size() > 0) { if (!accountId.isEmpty() && accountId != getCurrAccId()) {
// If the account is not currently selected, do that first, then Utils::oneShotConnect(this, &LRCInstance::currentAccountChanged, [this, convId] {
// proceed to select the conversation. set_selectedConvUid(convId);
auto selectConversation = [this, accountId, convUid = convInfo.uid] { });
const auto& convInfo = getConversationFromConvUid(convUid, accountId); setSelectedAccountId(accountId);
if (convInfo.uid.isEmpty()) { return;
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();
}
} }
Q_EMIT conversationSelected(); set_selectedConvUid(convId);
}
void
LRCInstance::deselectConversation()
{
set_selectedConvUid();
} }
void void

View file

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

View file

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

View file

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

View file

@ -34,7 +34,20 @@ Label {
signal settingBtnClicked signal settingBtnClicked
property alias popup: comboBoxPopup 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) { function resetAccountListModel(accountId) {
accountListModel.updateAvatarUid(accountId) accountListModel.updateAvatarUid(accountId)
accountListModel.reset() accountListModel.reset()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -28,31 +28,18 @@ import net.jami.Constants 1.0
import "../../commoncomponents" 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 { TabBar {
id: tabBar id: tabBar
property int currentTypeFilter: ConversationsAdapter.currentTypeFilter
currentIndex: 0
enum TabIndex { enum TabIndex {
Conversations, Conversations,
Requests 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) { function selectTab(tabIndex) {
ConversationsAdapter.currentTypeFilter = ConversationsAdapter.currentTypeFilter =
(tabIndex === SidePanelTabBar.Conversations) ? (tabIndex === SidePanelTabBar.Conversations) ?
@ -60,28 +47,25 @@ TabBar {
Profile.Type.PENDING Profile.Type.PENDING
} }
visible: tabBarVisible
currentIndex: 0
FilterTabButton { FilterTabButton {
id: pageOne id: conversationsTabButton
down: currentTypeFilter !== Profile.Type.PENDING
tabBar: parent tabBar: parent
down: true
labelText: JamiStrings.conversations labelText: JamiStrings.conversations
onSelected: selectTab(SidePanelTabBar.Conversations) onSelected: selectTab(SidePanelTabBar.Conversations)
badgeCount: totalUnreadMessagesCount badgeCount: ConversationsAdapter.totalUnreadMessageCount
acceleratorSequence: "Ctrl+L" acceleratorSequence: "Ctrl+L"
} }
FilterTabButton { FilterTabButton {
id: pageTwo id: requestsTabButton
down: !conversationsTabButton.down
tabBar: parent tabBar: parent
labelText: JamiStrings.invitations labelText: JamiStrings.invitations
onSelected: selectTab(SidePanelTabBar.Requests) onSelected: selectTab(SidePanelTabBar.Requests)
badgeCount: pendingRequestCount badgeCount: ConversationsAdapter.pendingRequestCount
acceleratorSequence: "Ctrl+R" acceleratorSequence: "Ctrl+R"
} }
} }

View file

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

View file

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

View file

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

View file

@ -27,6 +27,7 @@
#include "moderatorlistmodel.h" #include "moderatorlistmodel.h"
#include "deviceitemlistmodel.h" #include "deviceitemlistmodel.h"
#include "smartlistmodel.h" #include "smartlistmodel.h"
#include "conversationlistmodelbase.h"
#include "appsettingsmanager.h" #include "appsettingsmanager.h"
#include "distantrenderer.h" #include "distantrenderer.h"
@ -106,6 +107,10 @@ registerTypes()
QML_REGISTERTYPE(NS_MODELS, PluginListPreferenceModel); QML_REGISTERTYPE(NS_MODELS, PluginListPreferenceModel);
QML_REGISTERTYPE(NS_MODELS, SmartListModel); 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 // QQuickItems
QML_REGISTERTYPE(NS_MODELS, PreviewRenderer); QML_REGISTERTYPE(NS_MODELS, PreviewRenderer);
QML_REGISTERTYPE(NS_MODELS, VideoCallPreviewRenderer); QML_REGISTERTYPE(NS_MODELS, VideoCallPreviewRenderer);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -20,60 +20,32 @@
#pragma once #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; using namespace lrc::api;
class LRCInstance; class LRCInstance;
class SmartListModel : public AbstractListModelBase class SmartListModel : public ConversationListModelBase
{ {
Q_OBJECT Q_OBJECT
public: public:
using AccountInfo = lrc::api::account::Info; using Type = ContactList::Type;
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)
explicit SmartListModel(QObject* parent = nullptr, explicit SmartListModel(QObject* parent = nullptr,
SmartListModel::Type listModelType = Type::CONVERSATION, Type listModelType = Type::CONVERSATION,
LRCInstance* instance = nullptr); LRCInstance* instance = nullptr);
~SmartListModel();
/*
* QAbstractListModel.
*/
int rowCount(const QModelIndex& parent = QModelIndex()) const override; 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; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
QModelIndex index(int row, QModelIndex index(int row,
int column = 0, int column = 0,
const QModelIndex& parent = QModelIndex()) const override; const QModelIndex& parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex& child) const override;
Qt::ItemFlags flags(const QModelIndex& index) const override; Qt::ItemFlags flags(const QModelIndex& index) const override;
Q_INVOKABLE void setConferenceableFilter(const QString& filter = {}); Q_INVOKABLE void setConferenceableFilter(const QString& filter = {});
@ -81,28 +53,9 @@ public:
Q_INVOKABLE int currentUidSmartListModelIndex(); Q_INVOKABLE int currentUidSmartListModelIndex();
Q_INVOKABLE void fillConversationsList(); 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: 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_; Type listModelType_;
QMap<QString, bool> sectionState_; QMap<QString, bool> sectionState_;
QMap<ConferenceableItem, ConferenceableValue> conferenceables_; QMap<ConferenceableItem, ConferenceableValue> conferenceables_;
QMap<QString, QString> contactAvatarUidMap_;
ConversationModel::ConversationQueueProxy conversations_; ConversationModel::ConversationQueueProxy conversations_;
}; };

View file

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

View file

@ -144,29 +144,6 @@ UtilsAdapter::getBestId(const QString& accountId, const QString& uid)
return QString(); 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 void
UtilsAdapter::setConversationFilter(const QString& filter) UtilsAdapter::setConversationFilter(const QString& filter)
{ {

View file

@ -49,8 +49,6 @@ public:
Q_INVOKABLE QString GetRingtonePath(); Q_INVOKABLE QString GetRingtonePath();
Q_INVOKABLE bool checkStartupLink(); Q_INVOKABLE bool checkStartupLink();
Q_INVOKABLE void setConversationFilter(const QString& filter); 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 getBestName(const QString& accountId, const QString& uid);
Q_INVOKABLE const QString getPeerUri(const QString& accountId, const QString& uid); Q_INVOKABLE const QString getPeerUri(const QString& accountId, const QString& uid);
Q_INVOKABLE QString getBestId(const QString& accountId); Q_INVOKABLE QString getBestId(const QString& accountId);